微信小程序路由實戰

歡迎來到我博客閱讀: BlueSun - 微信小程序路由實戰

0. 目錄

1. 前言

在微信小程序由一個 App()實例,和衆多Page()組成。而在小程序中全部頁面的路由所有由框架進行管理,框架以棧的形式維護了全部頁面,而後提供瞭如下 API 來進行路由之間的跳轉:前端

  1. wx.navigateTo
  2. wx.redirectTo
  3. wx.navigateBack
  4. wx.switchTab
  5. wx.reLaunch

可是,對於一個企業應用,把這些問題留給了開發者:vue

  1. 原生 API 使用了 Callback 的函數實現形式,與咱們現代廣泛的 Promiseasync/await 存在 gap。
  2. 基於小程序路由的設計,暴露給外部的是真實路由(如掃碼,公衆號連接等方式),對後續項目重構留下歷史包袱。
  3. 小程序頁面棧最多十層, 在超過十層後 wx.navigateTo 失效,須要開發者判斷使用 wx.redirectTo 或其餘API
  4. 小程序頁面棧存在一種特殊的頁面:Tab 頁面,須要使用 wx.switchTab 才能跳轉。須要開發者主動判斷,不方便後期改動 Tab 頁面屬性。
  5. 額外的,對於小程序碼,要使用無數量限制 API wxacode.getUnlimited ,存在參數長度限制32位之內。須要開發者自行解決。

而本文,指望能對這若干問題,逐個提供解決方案。java

2. 智能路由跳轉 — Navigator 模塊

在這裏咱們一塊兒解決:git

  1. 原生 API 非 Promsie
  2. 頁面棧突破十層時特殊處理
  3. 特殊頁面 Tab 的跳轉處理

咱們的思路是,但願能設計一種邏輯,根據場景來自動判斷使用哪一個微信路由 API,而後對外只提供一個函數,例如:github

gotoPage('/pages/goods/index')

具體邏輯以下:vue-router

  1. 當跳轉的路由爲小程序 tab 頁面時,則使用 wx.switchTab
  2. 當頁面棧達到 10 層以後,若是要跳轉的頁面在頁面棧中,使用 wx.navigateBack({ delta: X }) 出棧到目標頁面。
  3. 當頁面棧達到 10 層以後,目標頁面不存在頁面棧中,使用 wx.redirectTo 替換棧頂頁面。
  4. 其餘狀況使用 wx.navigateTo

順帶的,咱們把這個函數以 Promise 形式實現,以及支持參數做爲 object傳入,例如:typescript

gotoPage('/pages/goods/index', { name: 'jc' }).then(...).catch(...);

大部分場景下,只要使用gotoPage就能知足。數據庫

那確定也會有特定的狀況,須要顯式的指定使用 navigateTo/switchTab/redirectTo/navigateBack的哪個。

那麼咱們也按照相似的實現,知足相同模式的 API

navigateTo('/pages/goods/index', { name: 'jc' }).then(...).catch(...);
switchTab('/pages/goods/index', { name: 'jc' }).then(...).catch(...);
redirectTo('/pages/goods/index', { name: 'jc' }).then(...).catch(...);
navigateBack('/pages/goods/index', { name: 'jc' }).then(...).catch(...);

這些函數均可之內聚到同一個模塊,咱們稱其爲:Navigator

const navigator = new Navigator();
navigator.gotoPage(...);
navigator.navigateTo(...);
navigator.switchTab(...);
navigator.redirectTo(...);
navigator.navigateBack(...);

模塊設計:

navigator-class

3. 虛擬路由策略 — Router 模塊

在這裏,咱們解決:

  1. 對外暴露了真實路由,致使歷史包袱沉重的問題。

在許多應用開發中,咱們常常須要把某種模式匹配到的全部路由,全都映射到同個頁面中去。
例如,咱們有一個 Goods 頁面,對於全部 ID 各不相同的商品,都要使用這個頁面來承載。

那麼在代碼層面上,指望能實現這樣的調用方式:

// 建立路由實例
const router = new Router();

// 註冊路由
router.register({
  path: '/goods/:id', // 虛擬路由
  route: '/pages/goods/index', // 真實路由
});

// 跳轉到 /pages/goods/index,參數: onLoad(options) 的 options = { id: '123' }
router.gotoPage('/goods/123');

// 跳轉到 /pages/goods/index,參數: onLoad(options) 的 options = { id: '456' }
router.gotoPage('/goods/456');

Class Router 的核心邏輯是完成:

  1. 路由的註冊,完成「虛擬路徑」和「真實路徑」關係的存儲。
  2. 知足「虛擬路徑」到「真實路徑」的轉換,而且識別「動態路徑參數」(dynamic segment)。
  3. 路由跳轉。

對於「路由的註冊」,咱們在其內部存儲一個 map 就能完成。

而對於「路徑的轉換」, vue-router 有相似的實現,經過其源碼發現,內部是使用 path-to-regexp 做爲路徑匹配引擎,咱們能夠拿來用之。

而後對於「路由的跳轉」,咱們能夠直接複用上面提到的 Navigator 模塊,經過輸入真實路徑,來完成路由的跳轉。

模塊設計:

route-class

其中:

  1. RouteMatcher:提供動態路由參數匹配功能,內部使用 path-to-regexp 做爲路徑匹配引擎。
  2. Route: 爲每一個路徑建立路由器,存儲每一個路由的虛擬路徑和真實路由的關係。
  3. Router:整合內部各模塊,對外提供統一且優雅的調用方式。

4. 落地中轉策略 — LandTransfer 模塊

在這裏,咱們解決:

  1. 小程序掃碼、公衆號連接等場景下的落地頁統一。
  2. 小程序碼,對於無限量API wxacode.getUnlimited ,突破參數32位長度限制。

4.1. 對於要解決的第一個問題:統一的落地頁

咱們把如:掃小程序碼、公衆號菜單、公衆號文章等方式打開小程序某個頁面的路徑稱爲「外部路由」。

根據小程序的設計,暴露給外部的鏈接是真實的頁面路徑,如:/pages/home/index,該設計在實踐中存在的弊端:各個落地頁分散,後期修改真實文件路徑難度大。

「中長生命週期」 產品中,隨着產品的迭代,咱們不免會遇到項目的重構。若是分發出去的都是沒通過處理的真實路徑的話,咱們重構時就會束手束腳,要作不少的兼容操做。由於你不知道,分發出去的小程序二維碼, 有多少被打印到實體物料中。

那麼,「虛擬路由」+「落地中轉」 的策略就顯得基本且重要了。

「虛擬路由」的功能,Router 模塊給咱們提供了支持了,咱們還須要對外提供一個統一的落地頁面,讓它來完成對內部路由的中轉。

基本邏輯:

  1. 分發出去的真實路由,指向到惟一的落地頁面,如:$LAND_PAGE: /pages/land-page/index
  2. 由這個落地頁面,進行內部路由的重定向轉發,經過接收 參數,如:path=/user&name=jc&age=18

普通模式

在代碼層面上,咱們但願能實現這樣的使用:

// /pages/land-page/index.ts

const landTransfer = new LandTransfer(landTransferOptions);

Page({
  onLoad(options) {
      landTransfer
        .run(options)
        .then(() => {...})
        .catch(() => {...});
  }
});

而後針對 TS,咱們還可使用裝飾器版本,更加簡便:

import { landTransferDecorator } from 'wxapp-router';

Page({
  @landTransferDecorator(landTransferOptions)
  onLoad(options) {
    // ...
  },
});

4.2. 對於第二個要解決的問題:短鏈參數

微信小程序主要提供了兩個接口去生成小程序碼:

  1. wxacode.get: 獲取小程序碼,適用於須要的碼數量較少的業務場景。經過該接口生成的小程序碼,永久有效,數量限制爲 100,000
  2. wxacode.getUnlimited: 獲取小程序碼,適用於須要的碼數量極多的業務場景。經過該接口生成的小程序碼,永久有效,數量暫無限制。

第一種方式,wxacode.get 數量限制爲 10w 個,雖然量很大了,絕大多數的小程序可能用不到這個量。

但若是咱們運營的是一箇中大型電商小程序的話,假如:1w 種商品 x 10 種商品規格,那就會超過這個數量。到時候再進行改造,就困難了。

因此,若是抱着是運營一個 「中長生命週期」 的產品的話,咱們會使用第二種方式:wxacode.getUnlimited

不盡人意的是,雖然它沒有數量限制,可是對參數會有 32 個字符的限制,顯然是不夠用的(一個 uuid 就 32 字符了)。

對於這種狀況,咱們可使用「短鏈參數」的形式解決,因爲wxacode.getUnlimited 會經過 scene字段做爲 query 參數傳遞給小程序的,那麼咱們能夠經過 scene參數來實現短鏈服務,這須要後端配合。

先後端交互以下:

Scene短鏈模式

  1. 當小程序須要生成小程序碼的時候,請求後端提供的接口,例如:/api/encodeShortParams
  2. 後端把內容轉換爲 32 字符內的字符串,存儲到數據庫中。
  3. 後端經過 wxacode.getUnlimited 接口,以短鏈字符串做爲 scene的值,以商定好的統一落地頁 $LAND_PAGE做爲 page值,生成小程序碼。
  4. 當經過小程序碼進入小程序,小程序獲取到 scene參數,請求後端提供的接口,例如:/api/decodeShrotParams
  5. 小程序理解內容,跳轉到目標頁面中去。

而前端對於統一落地頁的邏輯處理,咱們只須要在第一個問題的基礎上,增長一個轉換短鏈參數內容的邏輯就好了:

短鏈模式

代碼層面上,咱們咱們只須要多定義轉換短鏈參數的方式:convertScenePrams

// in /pages/land-page/index.js
import { landTransferDecorator } from 'wxapp-router';

const landTransferOptions = {
  // 此處接收 onLoad(options) 中的 options.scene
  convertSceneParams: (sceneParams) => {
    return API.convertScene({ sceneParams }).then((content) => {
      // 假如後端存的是 JSON 字符串,前端decode
      // 要求 content = { path: '/home', a: 1, b:2 }
      return JSON.parse(content);
    });
  },
};

Page({
  @landTransferDecorator(landTransferOptions)
  onLoad(options) {
    // ...
  },
});

而其中的 API.convertScene 就對接服務端提供 HTTP 接口服務來完成。

4.3. LandTransfer 模塊設計

land-transfer

5. 更好的開發體驗

5.1. Typescript + Router

對於小程序內部的路由跳轉,咱們除了指定一個字符串的路由,咱們是否也能夠經過鏈式調用,像調用函數那樣去跳轉頁面呢?相似這樣;

routes.pages.user.go({ name: 'jc' });

這樣作的好處是:

  1. 更天然的調用方式。
  2. 能結合 TS,來作到類型提示和聯想。

因爲事先 wxapp-router 並不知道開發者須要註冊的路由是什麼樣的,因此路由的 TS 聲明文件,須要開發者來定義。

例如,咱們在項目中維護一份路由文件:

// config/routes.ts

// 建立路由實例
const router = new Router();

const routesConfig = [{
  path: '/user',
  route: '/pages/user/index',
}, {
  path: '/goods',
  route: '/pages/goods/index',
}];

type RoutesType {
  paegs: {
    user: Route<{name: string}>,
    goods: Route,
  }
}

// 註冊路由
router.batchRegister(routesConfig);

// 獲取 routes
const routes: RoutesType = router.getRoutes();

export default routes;

而後在別的地方使用它:

import routes from './routes.ts';

routes.pages.user.go({ name: 'jc' });

5.2. 智能生成路由配置

若是路由變多的時候,咱們還須要對每一個路由手動去編寫 RoutesType 的話,就有點難受了。

在小程序中,咱們把正式路由都配置到 app.json ,那麼在遵循既定的項目結構狀況下,咱們能夠經過自動構建,完成大部分工做,例如:

  1. 智能註冊路由
  2. 智能識別頁面入參聲明

5.3. 自定義組件跳轉

以上都是腳本層面的使用,小程序中還有 wxml, 咱們但願能在有個組件快速使用:

<Router path="/pageA" query="{{pageAQuery}}"></Router>
<Router path="/pageB" query="{{pageBQuery}}" type="redirectTo"></Router>
<Router path="/pageC/katy"></Router>

那麼,實現一個自定義組件,而後把 Router模塊包裝一下,問題就不大了。

示例代碼:

// components/router.wxml

<view class="wxapp-router" bind:tap="gotoPage">
    <slot />
</view>
// components/router.ts

Component({
    properties: {
        path: String,
        type: {
            type: String,
            value: 'gotoPage'
        },
        route: String,
        query: Object,
        delta: Number,
        setData: Object,
    },

    methods: {
        gotoPage(event) {
            const router = getApp().router;
            const { path, route, type, query} = this.data;
            const toPath = route || path;

            if (['gotoPage', 'navigateTo', 'switchTab', 'redirectTo'].includes(type)) {
                (router as any)[type](toPath, query);
            }

            if (type === 'navigateBack') {
                const { delta, setData } = this.data;
                router.navigateBack({ delta }, { setData })
            }
        }
    }
})

6. 總體架構圖

最後,咱們來總體回顧一下各模塊的設計

架構設計

  1. Navigator:封裝微信原生路由 API,提供智能跳轉策略。
  2. LandTransfer:提供落地頁中轉策略。
  3. RouteMatcher:提供動態路由參數匹配功能。
  4. Route: 爲每一個路徑建立路由器。
  5. Router:整合內部各模塊,對外提供優雅的調用方式。
  6. Logger:內部日誌器。
  7. Path-to-regexp: 開源社區的路由匹配引擎。

7. 最後的最後

鑑於寫過不少的實戰類的文章,會有很多同窗想要到總體的示例代碼,此次我就索性寫了一個工具,Enjoy it!

wxapp-router: 🛵 The router for Wechat Miniprogram

相關文章
相關標籤/搜索