background

Recently, the team is developing a small program mall project. During the development process, we found some inconveniences when using the route related interface of wechat small program, as follows:

  • Parameter passing is not friendly, only supportedstringType, which can only be combined by splicingurl
  • Page stack up to ten layers, usewx.navigateTo, the current page level exceeds10Level cannot jump
  • When a route is changed, you need to search for a new route globallyurl, but easy to forget to change, high maintenance costs
  • Determine whether the current route needs to be developedtabBarPage, and then selectnavigateTo, or useswitchTabmethods

Then encounter the above problems, we change how to encapsulate routing files and routing methods, improve the small program experience and development efficiency?

Using vue-router for reference, route configuration management and secondary encapsulation are implemented

Vue – the router routing

The above mentioned vue-Router routing idea and API use mode, to solve the small program routing jump and parameters exist problems?

We believe that in the development of VUE project, using vue-Router routing tool first need to configure the first route, create a route instance, and finally call the route jump method. Let’s see how vue-router is used first

First, configure the route and create the route instance

import { createRouter, createWebHistory } from 'vue-router'
import Home from '.. /views/Home.vue'

const routes = [
  {
    path: '/'.name: 'Home'.component: Home,
    meta: {
      title: 'home'.icon: 'icon-home'}}, {path: '/about'.name: 'About'.// route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () = > import(/* webpackChunkName: "about" */ '.. /views/About.vue')}]const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router
Copy the code

Then, call the route jump method, the page jump

import { useRoute } from 'vue-router';

const route = useRoute();

// route by route path
route.push('home');

// object
route.push({ path: 'home' });

// named route navigation with parameters
route.push({ name: 'user'.params: { userId: '123'}});// with query params, resulting in /profile? info=some-info
route.push({ path: 'profile'.query: { info: 'some-info'}});// promise async await
await route.push({ path: 'home' });
Copy the code

It follows that based onvue-routerThe routing model,

  • urlParameters are configured as objects
  • Route Forward mode Uses routespathOr routingnameTo jump
  • Route configuration has strong scalability. For example, what type of page is the route
  • Route hop method is supportedpromiseAsynchronous function call

Routing encapsulation

First of all, we have determined that the core of small program routing encapsulation is routing configuration management and routing encapsulation method, and then the encapsulation of small program routing.

Routing Interface design

Small program provides routing method has five: switchTab, navigateTo, navigateBack, redirectTo, reLaunch, draw lessons from the vue – routing hop router API methods to design our routing API:

Encapsulating routing API Native routing API describe
route.go(string | object) Different types of routes are forwarded providetypeRoute parameters: Supports different types of routes
route.push(string | object) wx.navigateTo(object) Keep the current page and go to a page in the application. But you cannot jump to the Tabbar page. usewx.navigateBackYou can return to the original page. The page stack in the applets is up to ten layers
route.tab(string | object) wx.switchTab(object) Jump to tabBarPage and close all other nontabBar page
route.replace(string | object) wx.redirectTo(object) Close the current page and switch to a page in the application. But not allowed to jump totabbarpage
route.relanuch(string | object) wx.reLaunch(object) Close all pages and open to a page within the app

Route Configuration Management

Next, we started to design the routing configuration. First, we defined the small program routing configuration items as follows:

/** * Applets routing configuration item */
export type RouteRecord = {
  / * * *@description Route path URL */url? : string,/ * * *@description The route uniquely identifies eg: Home, Logs */name? : string,/ * * *@description Route alias name */title? : string,/ * * *@description Whether to tabBar routing page */tabBar? : boolean,/ * * *@description Attach custom data */meta? : Record<string | number | symbol, unknown>, };Copy the code

Then, configure the applets routing:

/** * Applets routing configuration */
export const routes: RouteRecordRaw[] = [
  {
    name: 'Home'.title: 'home'.url: '/pages/home/index'.tabBar: true}, {name: 'ShoppingCart'.title: 'Shopping cart'.url: '/pages/shopping-cart/index'.tabBar: true}, {name: 'Mine'.title: 'I'.url: '/pages/mine/index'.tabBar: true}, {name: 'GoodsDetail'.title: 'Merchandise Details Page'.url: '/pages/goods-detail/index',},... ]Copy the code

This route configuration is exactly the same as vue-router ~🐶!

Encapsulated routing method

Above, we have defined the apPLETS routing API and routing configuration, next we start to package the routing tool, based on the router.js in vue-router-next source code to design routing instances, as follows:

/** * Creates a Router Matcher */
export function createRouterMatcher(routes: RouteRecordRaw[]) {
  const matcherMap = new Map<RouteRecordName, Partial<RouteRecord>>();
  return matcherMap;
}

/** * Creates a Router instance */
export function createRouter(options: RouterOptions) {
  const { routes } = options;
  const matcher = createRouterMatcher(routes);

  async function go(to: RouteNavigateAction | string, query? : RouteQuery) :Promise<RouteNavigatCallbackResult> {}

  async function push(navigate) {}

  async function tab(navigate) {}

  async function replace(navigate) {}

  async function relaunch(navigate) {}

  async function back(navigateBack) {}

  return {
    routes,
    matcher,
    go,
    push,
    tab,
    replace,
    relaunch,
    back,
  };
}
Copy the code

CreateRouter creates the route instance

The route instance is created by executing the createRouter(options) method, as in VueRouter. The configured route configuration items are imported into the route instance. In this way, we can jump to the route alias, determine the route type, and resolve parameters according to the route configuration

import routes from './config/router';

const route = createRouter({
  routes,
});

route.go({ name: 'home' });
Copy the code

CreateRouterMatcher Creates a route match

Create a route match createRouter(Routes). The parameter is route configuration array. Traverse routes

export function createRouterMatcher(routes: RouteRecordRaw[]) {
  const matcherMap = new Map<RouteRecordName, Partial<RouteRecord>>();

  routes.forEach((route) = > {
    route.name && matcherMap.set(route.name, route);
  });

  return matcherMap;
}
Copy the code

The following is the core method of route jump encapsulation

Route jump encapsulation

We can make a simple list of tasks based on the problems of applets routing mentioned in the background.

  • Meet route idnameParameter to redirect a route
  • supportqueryObject modeurlParameter passing
  • Distinguish betweentabBarPage, automatic selectionnavigateTo / switchTabmethods
  • Resolve page hierarchy overload10Level cannot jump
  • Support the incomingstringRouting path of type parameter
Supports route id jump

The createRouterMatcher route match has already been created, so when the route id value is passed in as a parameter, we can use the matcher.ge(key) method to look up the current route configuration class to get the route URL

/** * Get the current route URL according to route id */
function getNavigateUrl(navigate: BaseRouteNavigateOption) :string {
  if(navigate.name && ! navigate.url) {const url = matcher.get(navigate.name)?.url;
    if(!!!!! url)return url;
    throw new Error('page route is not found');
  }

  if(! navigate? .url) {throw new Error('page route is not found');
  }

  return navigate.url as string;
}
Copy the code
async function go(to: RouteNavigateOption) :Promise<RouteNavigatCallbackResult> {
  const url = getNavigateUrl(to); // Get the route URL
  to.url = url;
  return await wx.navigateTo(to);
}

// go({ name: 'Home' }) => // wx.navigateTo('/pages/home/index')
Copy the code
supportqueryThe object parameters

Query = ${url}? Query = ${url}? ${serializeQuery(query)

/** * Parse URL query parameters */
function serializeQuery(obj: RouteQuery = {}) {
  return Object.keys(obj)
    .map((key) = > `The ${encodeURIComponent(key)}=The ${encodeURIComponent(obj[key])}`)
    .join('&');
}
Copy the code
async function go(to: RouteNavigateOption) :Promise<RouteNavigatCallbackResult> {
  const url = getNavigateUrl(to); // Get the route URL
  constquery = serializeQuery(to? .query);// Parse the quey argument

  to.url = url.indexOf('? ') > =0 ? `${url}${query}` : `${url}?${query}`;

  return await wx.navigateTo(to);
}

// go({ url: '/pages/goods-detail/index', query: { id: 123 } }) => wx.navigateTo('/pages/goods-detail/index? id=123')
Copy the code
Distinguish betweentabBar, the choice ofnavigateTo / switchTabmethods

When we set the tabBar option in the routing configuration item, we can distinguish whether the tabBar page is tabBar. If the parameter is name, the route id is directly searched from the route matching table. If the parameter is the URL route path, you need to traverse the current URL to find the route. Finally, determine whether the current route is tabBar page

/** * Check whether tabBar routing page */
function hasTabBarNavigate(navigate: BaseRouteNavigateOption) :boolean {
  let isTabBar = false;

  if(navigate? .name) { isTabBar = !! matcher.get(navigate.name)?.tabBar; }if(navigate? .url) { matcher.forEach((item) = > {
      consturl = navigate? .url? .split('? ') [0];

      if(item.url === url) { isTabBar = !! item? .tabBar; }});return isTabBar;
  }

  return isTabBar;
}
Copy the code
async function go(to: RouteNavigateOption) :Promise<RouteNavigatCallbackResult> {
  const url = getNavigateUrl(to); // Get the route URL
  constquery = serializeQuery(to? .query);// Parse the quey argument

  to.url = url.indexOf('? ') > =0 ? `${url}${query}` : `${url}?${query}`;

  if (hasTabBarNavigate(to)) {
    const navigateTab = assign(navigate, {
      url: navigate.url.split('? ') [0].// wx.switchTab: URL does not support queryString
    });
    return await wx.switchTab(navigateTab);
  }

  return await wx.navigateTo(to);
}
Copy the code
Resolve page hierarchy overload10Level cannot jump

Since the applets page stack is up to ten layers, we can use different routing APIS based on the number of page levels

  • When the page level is less than10, directly jump.
  • When the page level is greater than or equal to10, if the page exists in the page stack, roll back to the corresponding page stack; If no, close the current page and switch to a new page

Wx. redirectTo (wx.redirectTo, wx.navigateTo, wx.redirectTo, wx. NavigateBack returns wx.navigateBack if yes.

Basic core code logic on so much, is not the feeling is very simple ~🐶

async function go(to: RouteNavigateOption | string) :Promise<RouteNavigatCallbackResult> {
  const navigate = asNavigateObject(to);
  const url = getNavigateUrl(to); // Get the route URL
  constquery = serializeQuery(to? .query);// Parse the quey argument

  const maxDeep = 10; // Maximum page stack depth
  const pageStack = getCurrentPages();

  navigate.url = url.indexOf('? ') > =0 ? `${url}${query}` : `${url}?${query}`;

  // The page stack has reached the upper limit
  if (pageStack.length >= maxDeep) {
    const curDelta = findPageInHistory(navigate.url); // Check whether the current route exists in the page stack

    // Current page: in page stack
    if (curDelta > -1)
      return back({
        delta: pageStack.length - curDelta,
        data: navigate? .query, });// Current page: not in the page stack
    return wx.redirectTo(navigate);
  }

  // tabBar or switchTab route
  if(hasTabBarNavigate(navigate) || navigate? .type === RouteType.SWITCH_TAB) {const navigateTab = assign(navigate, {
      url: navigate.url.split('? ') [0].// wx.switchTab: URL does not support queryString
    });
    return await wx.switchTab(navigateTab);
  }

  / / redirectTo routing
  if(navigate? .type === RouteType.REDIRECT_TO) {return await wx.redirectTo(navigate);
  }

  / / reLaunch routing
  if(navigate? .type === RouteType.RELAUNCH) {return await wx.reLaunch(navigate);
  }

  / / navigateTo routing
  return await wx.navigateTo(navigate);
}
Copy the code

The rest is a simple encapsulation of route.push, route.replace, ‘ ‘route. TAB, route.relaunch, route.back’ methods to complete the small program routing tool.

async function push(to: RouteNavigateOption | string) {
  return await go(assign(to, { type: RouteType.NAVIGATE_TO }));
}

async function tab(to: RouteNavigateOption | string) {
  return await go(assign(to, { type: RouteType.SWITCH_TAB }));
}

async function replace(to: RouteNavigateOption | string) {
  return await go(assign(to, { type: RouteType.REDIRECT_TO }));
}

async function relaunch(to: RouteNavigateOption | string) {
  return await go(assign(to, { type: RouteType.RELAUNCH }));
}

async function back(to: RouteNavigateBackOption | number) {
  const pageStack = getCurrentPages();
  const { delta = 1, data = {} } = typeof to === 'number' ? { delta: to } : to;

  // Set the data on the previous page
  if (Object.keys(data).length > 0) {
    const backPage = pageStack[pageStack.length - 1- delta]; backPage? .setData(data); }return await wx.navigateBack({ delta });
}
Copy the code

When used on a page, it is pleasant to use vue-router routing for routing, setting back, and so on

import route form './config/router.ts';

Page({
  data: {},

  // Jump to commodity details page
  onClickGoodsItem(e) {
    const id = e.detail.id
    route.go({ name: 'GoodsDeatil'.query: { id }})
  },

  // Jump to the search page
  onClickSearchItem() {
    route.push({ url: '/pages/search/index'})},// return to the previous page
  onBack() {
    route.back(1);// or route.back({ delta: 1 })}});Copy the code

tip

When there are too many routing pages in a small program, this will result in the need to write a lot of routing tables, and will also increase the size of the routing table file, which is very unfriendly for the pursuit of performance optimization. And there’s concurrent maintenanceapp.jsonPage configuration, relatively troublesome

Therefore, the route configuration table can be filled in only the route items on the tabBar page, which can reduce the size of the configuration table file. In addition, the automatic route selection can be automatically selected using navigateTo/switchTab methods. However, it should be noted that other non-Tabbar pages cannot be jumped with the identification of route name. Other effects are not too significant

If the current project is based ongulpAs a compilation build package, and the routing configuration table is not too much, in fact, can be combined with the routing configuration table, dynamically generatedapp.jsonThe routing configuration is currently used in our project

  1. In the project I putapp.jsonNamed after aapp.json.ts, in order to prevent already projectsapp.tsThe same name.
// app.json.ts
import router from './config/router';

/** * Rewrite the routing path */
const rewriteRouteUrl = (url = ' ', fileRoot = ' ') = > {
  if (fileRoot) {
    const reg = new RegExp(` /${fileRoot}/ `.'i');
    return url.replace(reg, ' ');
  }
  return url.replace(/\//i.' ');
};

/** * Get applets, subcontract routing configuration, tabBar navigation configuration *@param Routes Applets route configuration */
const getRoutes = (routes: RouteRecordRaw[]) = > {
  const appRoutes: AppRoutes = { pages: [].subpackages: [].tabBarList: []};return routes.reduce(({ pages, subpackages, tabBarList }, route) = > {
    if(route? .root &&Array.isArray(route? .pages)) { subpackages.push({root: route? .root,pages: route? .pages.map((item: RouteRecordRaw) = > rewriteRouteUrl(item.url, route.root)),
      });
    } else{ pages? .push(rewriteRouteUrl(route.url)); }if(route? .tabBar) {const{ iconPath, selectedIconPath } = route? .meta || {}; tabBarList.push({text: route? .title,pagePath: rewriteRouteUrl(route? .url), iconPath, selectedIconPath, }); }return {
      pages,
      subpackages,
      tabBarList,
    };
  }, appRoutes);
};

const { pages, subpackages, tabBarList } = getRoutes(router);

export default {
  pages,
  subpackages,
  window: {
    navigationStyle: 'default'.backgroundTextStyle: 'light'.navigationBarBackgroundColor: '#fff'.navigationBarTitleText: ' '.navigationBarTextStyle: 'black',},tabBar: {
    color: '# 333333'.selectedColor: '#ff7437'.borderStyle: 'white'.list: tabBarList,
  },
  sitemapLocation: 'sitemap.json'};Copy the code
  1. And then go throughgulptheapp.json.tsCompiler outputapp.jsonFile. Here is a simple idea: mainly throughwebpackCompile the packaged output code and use itevalModule performsjsCode, after the implementation of the code to restore the file stream output, and finally modify the file suffix name
const eval = require('eval');
const webpack = require('webpack-stream');
const named = require('vinyl-named');

/** * toJson task * compile ts conversion json */
const toJson = () = > src(globs.jsonts, { since: since(toJson) })
  .pipe(named(file= > file.relative.slice(0, -path.extname(file.path).length)))
  .pipe(webpack({
    mode: NODE_ENV,
    resolve: {
      alias: aliasOptions,
      extensions: ['.ts'.'.js'],},output: {
      libraryTarget: 'commonjs',},module: {
      rules: [{test: /\.(js|ts)? $/,
          loader: 'esbuild-loader'.options: {
            loader: 'ts'.target: 'es2015'.tsconfigRaw: require('.. /tsconfig.json'),},exclude: /(node_modules)/,
        },
      ],
    },
  }))
  .pipe(tap((file) = > {
    const res = eval(file.contents.toString());
    const str = JSON.stringify(res.default || res, null.2);
    file.contents = Buffer.from(str, 'utf8'); // String restores to a file stream
  }))
  .pipe(rename({ extname: ' ' }))
  .pipe(dest(distDir));
Copy the code

conclusion

In fact, there are many ways to route jump of small programs. Choosing an appropriate route jump mode will improve user experience. Encapsulation is mainly to improve development efficiency and reduce later maintenance costs