小程式多業務線融合【完整分包業務接入】
- 同一個主體(公司、部門)下有多個小程式
- 這些小程式,由一個主小程式和後來新建的多條業務線構成(每條業務線擁有獨立的小程式)
- 各業務線的小程式需要掛載到主程式下面,因為需要主程式導流
- 同時各業務線自己的小程式也照常釋出更新
- == 一套程式碼,通過打包命令,來生成獨立包和分包 ==(分包生成完需要拷貝到主程式的subPages目錄下)
專案概述
我的這條業務線叫歡樂送(專案名為enjoy_given),是轉轉旗下一個免費的以物換物平臺
因為我們這條業務線小程式是用mpvue構建的(整個專案也是通過mpvue的cli生成的),所以後面相關配置都是以mpvue為例,如果是wepy專案基本也大同小異。
下面就是我們的目錄結構

src目錄下的幾個js檔案需要專門介紹下:
src/App.vue 是小程式的入口檔案,裡面定義的是小程式的生命週期
src/main.js 裡面初始化通用業務、定義小程式頁面路徑和全域性變數
src/vars.js 存放整個專案的全域性變數
src/baseInstall.js 基礎方法裝配邏輯(如:給vue物件掛載登入、統計邏輯、識別渠道號等)
分包配置概述
-
首先要配置source和appid
作為分包時,這兩個引數都要統一採用主包引數(建議通過webpack配置來實現)
source:是每條業務線登入、註冊、和介面訪問時用的標識,用來區分該使用者來自於哪條業務線
appid:微信分配的小程式appid
為什麼要配置這兩引數:因為不配置沒法登入
-
頁面路徑問題
作為分包時,所有頁面的跳轉路徑都要加主包的跳轉字首(建議通過包裝跳轉方法navigateTo、redirectTo、reLaunch、navigateBack實現,建議配合webpack統一處理)
當新業務線作為分包接入主程式時,頁面跳轉路徑前需要統一加一個字首
如:獨立小程式首頁路徑為 /pages/content/index/main
作為分包時,主程式分配的包為/subPages/enjoy_given
那麼分包業務線首頁路徑為: /subPages/enjoy_given/pages/content/index/main
-
wxss引用路徑問題
不要用使根目錄引入方式(建議採用webpack或者shell指令碼來完成)
因為在分包狀態下,用根目錄訪問方式會直接訪問主程式的根目錄,檔案是不存在的
-
圖片路徑問題
所有圖片路徑統一採用cdn資源訪問方式,不要引用本地圖片
-
對於分包的main.js和App.vue入口檔案不執行的問題
可以通過抽離基礎業務裝配方法,對於每一個從主包跳到分包頁面的入口分別引入,後面會細說
-
對於小程式內的h5頁面拉起小程式頁面
在開啟webview時候,要加入一個標誌位,或者prefix,告訴h5頁面,當前處於分包當中,開啟的小程式path要加字首
-
分享路徑問題,在路徑前面也要加入路徑字首
可以通過一個通用的分享方法,進行統一處理,後面會細說
-
小程式的所有頁面都需要在主包入口檔案(app.vue)註冊,每新增頁面都要註冊
這個是坑,尤其新增頁面時,會很容易忽略這個問題,這裡要特別強調下
分包接入需要注意的地方
-
storage命名問題,為了避免和主程式或者其他業務線小程式發生衝突(建議採用 zz_業務名_xxx, 我們業務名是enjoy_given,簡稱eg,如: zz_eg_address, zz指的就是轉轉)
-
登入問題,推薦和主程式使用同樣的cookie名稱,這樣可以通用一套使用者資訊,免得雙方各維護一套,還能避免重複授權。
-
支付問題,保證下單時和支付時,cookie中的引數保持一致
-
除錯,可以找主程式那邊要個主程式的測試包,把生成的程式碼(dist目錄下的內容)拷貝到主程式包的 subPages/業務名/ 下面
例如我們的目錄是 subPages/enjoy_given/(目錄結構同上)
一套程式碼,通過不同打包命令生成對應的程式包(獨立包和分包)
package.json中scripts
"scripts": { "dev": "node build/dev-server.js", "start": "node build/dev-server.js", "build": "rimraf dist && node build/build.js", "lint": "eslint --ext .js,.vue src", "build_subPkg": "node build/build-subpkg.js && sh ./scripts/path-replace.sh" } 複製程式碼
獨立小程式(除錯) npm run dev 獨立小程式(構建) npm run build 主程式分包(構建) npm run build_subPkg 複製程式碼
為什麼沒有主程式分包(測試)
因為我們無論是構建測試分包還是構建正式分包,都要把生成dist下的程式碼拷貝到主程式的subPages/enjoy_given/目錄下,成本基本是一樣的,所以,就沒有寫構件分包的命令
分包webpack配置
因為需要相容獨立小程式和分包業務,webpack我們建議分開配置
我們對測試環境和正式環境分別配置了webpack, 通過對webpack配置替換全域性變數,直接修改專案的全域性引數 。 通過npm命令動態執行替換。
為了分開配置,我們拷貝了一份build.js更名為build-subpkg.js
"scripts": { ..., "build_subPkg": "node build/build-subpkg.js && sh ./scripts/path-replace.sh" } 複製程式碼
build_subPkg命令就是讀取的build-subpkg.js檔案 build.js和build-subpkg.js中99%的內容都一樣,只有一行不一樣
var webpackConfig = require('./webpack.prod.conf') 變更為 var webpackConfig = require('./webpack.subpkg.prod.conf') 複製程式碼
所以下一步就是建立webpack.subpkg.prod.conf檔案 webpack.subpkg.prod.conf由webpack.prod.conf拷貝而來,裡面依舊99%的內容一致
// webpack.prod.conf
... var config = require('../config') var env = config.build.env ... var webpackConfig = merge(baseWebpackConfig, { ... plugins: [ new webpack.DefinePlugin({ 'process.env': env, 'app.source': env.APP_SOURCE, 'app.udeskDebug': env.UDESK_DEBUG, 'app.id': env.APP_ID, 'app.pathPrefix': env.APP_PATH_RREFIX, 'app.isUseCrazyFormId': env.IS_USE_CRAZY_FORMD_ID }), ... ] }) 複製程式碼
// webpack.subpkg.prod.conf
... var config = require('../config') var env = config.build.env ... var webpackConfig = merge(baseWebpackConfig, { ... plugins: [ new webpack.DefinePlugin({ 'process.env': env, 'app.source': env.APP_SUB_PKG_SOURCE, 'app.udeskDebug': env.UDESK_DEBUG, 'app.id': env.APP_SUB_PKG_ID, 'app.pathPrefix': env.APP_SUB_PKG_PATH_RREFIX, 'app.isUseCrazyFormId': env.IS_USE_CRAZY_FORMD_ID }), ... ] }) 複製程式碼
DefinePlugin外掛是用來進行全域性替換的 如:'process.env': '"hahaha"', 指的就是全域性process.env替換為"hahaha"
裡面通過定義多個全域性變數,實現打包時,通過不同的命令替換對應環境下的全域性變數 我們看一下../config/index.js中的檔案
var path = require('path') module.exports = { build: { env: require('./prod.env'), ... }, dev: { env: require('./dev.env'), ... } } 複製程式碼
引入了dev.env.js和prod.env.js
以prod.env.js為例
module.exports = { // 環境 NODE_ENV: '"production"', // 歡樂送獨立小程式source APP_SOURCE: '114', // 歡樂送分包小程式source APP_SUB_PKG_SOURCE: '103', // 歡樂送獨立程式appid APP_ID: '"wxaaaaaaaaaaaaaaa"', // 歡樂送分包程式appid APP_SUB_PKG_ID: '"wxbbbbbbbbbbbbbbbb"', // udesk測試標誌位 UDESK_DEBUG: false, // 歡樂送獨立小程式頁面路徑字首 APP_PATH_RREFIX: '""', // 歡樂送分包小程式頁面路徑字首 APP_SUB_PKG_PATH_RREFIX: '"/subPages/enjoy_given"', // 是否啟用crazyFormId IS_USE_CRAZY_FORMD_ID: true } 複製程式碼
然後我們再來看一下存放全域性變數的檔案src/vars.js(上面專案截圖中有)
// 小程式常量 export default { ... // 小程式版本號 version: '1.3.5', // 小程式appid appId: app.id, // 小程式source(由webpack根據不同環境統一替換) source: app.source, // 路徑字首 pathPrefix: app.pathPrefix, // 是否啟用CrazyFormId isUseCrazyFormId: app.isUseCrazyFormId } 複製程式碼
var webpackConfig = merge(baseWebpackConfig, { ... plugins: [ new webpack.DefinePlugin({ 'process.env': env, 'app.source': env.APP_SUB_PKG_SOURCE, 'app.udeskDebug': env.UDESK_DEBUG, 'app.id': env.APP_SUB_PKG_ID, 'app.pathPrefix': env.APP_SUB_PKG_PATH_RREFIX, 'app.isUseCrazyFormId': env.IS_USE_CRAZY_FORMD_ID }), ... ] }) 複製程式碼
在打包完成後,全域性變數檔案中的"app.xxx"會被webpack中的同名變數替換掉
如vars.js中 appId: app.id的app.id會被替換,獨立小程式時該值為"wxaaaaaaaaaaaaaaa",作為分包業務時,該值為"wxbbbbbbbbbbbbbb"
這樣整個替換全域性變數的流程就跑完了
==作為分包,接入主程式中,自己的main.js和App.vue都不會執行==
這個是大坑,因為很多通用業務的初始化如登入、cookie、統計都是在這裡完成的。
解決方案
把基礎功能的裝配業務(如在錄、統計、識別渠道號等邏輯)從main.js中抽離到另一個檔案,我這裡叫baseInstall.js。 裡面我還加入了對query的處理,比如渠道號channel和微信入口scene。
那這樣的話,src/main.js就會變得非常簡單,
import Vue from 'vue' import App from './App' import baseInstall from './baseInstall' App.mpType = 'app' baseInstall.init()// !!!最關鍵就是這行程式碼!!! const app = new Vue(App) app.$mount() export default { config: { pages: [ '^pages/content/index/main',// 首頁 ... ], window: { ... } } } 複製程式碼
裡面最關鍵的是baseInstall.init()這行程式碼
下面我們來看看baseInstall.js
// 通用業務裝配初始化 ... async function init (opts) { let options = opts ... // 獲取指定渠道號 const channel = options.channel || options.c || '' // 設定渠道號 if (channel) { VARS.channel = channel.indexOf('waeg_') === 0 ? channel : ('waeg_' + channel) } ... if (!VARS.baseInstallFlag) { // 為了避免重複裝備,通過標誌位進行區分 VARS.baseInstallFlag = true ... // 登入配置 ZZLogin.config({ source: VARS.source }) ZZLogin.install() Navigator.install() // 統計 LeStatic.config({ appid: VARS.source, pageTypePrefix (currentRoute) { return 'waeg_' } }).install() ... } // 寫入cookie cookie.set({ channelid: VARS.channel, fromShareUid: VARS.shareUid }) return options } export default { init } 複製程式碼
為什麼要用VARS.baseInstallFlag標誌位
因為,在分包時候是不執行main.js的,實際場景,會從主包的業務直接跳轉到分包的一些頁面。
由於沒有固定入口,所以在這些頁面中都要加入baseInstall.js的引入,為了避免重複裝配,才會設定這個標誌位。
為什麼要把這些業務抽離
baseInstall.init裡面涵蓋了所有啟動小程式時需要初始化的業務
前面也提到了在作為分包時,自己的App.vue和main.js是不會執行的。
那怎麼辦,這樣,就在所有的頁面中,在onLoad的生命週期中加入baseInstall.init方法。 ,所以我們抽離肯定是為了更方便的複用了。
以首頁為例(pages/content/index/index.vue)
import baseInstall from '@/baseInstall' export default { ... async onLoad (options) { options = await baseInstall.init(options) ... } } 複製程式碼
用async/await是因為baseInstall.init中部分邏輯用到了非同步請求
因為主程式不會讀取main.js,所以,所有的分包頁面路徑,都要統一在主程式中註冊
注:每新增一個頁面,都要在主程式中註冊。也就是新增一個頁面,就要通知主程式那邊,在他們的檔案裡統一註冊
頁面路徑
在分包中,所有頁面路徑訪問要加入字首
如:原來訪問/pages/content/index/main就可以了
但是分包的訪問路徑為: /subPages/enjoy_given /pages/content/index/main
解決方案:
以包裝的navigateTo為例
async navigateTo (route) { route.url = VARS.pathPrefix + (route.url.indexOf('/') === 0 ? '' : '/') + route.url // 這裡做字首處理 console.log('[Navigator] navigateTo:', route) ... wx.navigateTo(route) } 複製程式碼
這裡面需不需要加字首,都是由全域性變數VARS中的pathPrefix來決定
而pathPrefix是在打包過程中由webpack根據打包命令動態替換的
圖片訪問路徑問題
圖片訪問路徑統一採用cdn的資源訪問路徑,不要用本地訪問路徑,要不然在分包路徑中是有問題的,同時也會增加程式包的體積
wxss路徑問題
用mpvue生成的wxss檔案,裡面會把通用的vendor.wxss引入,但是引入路徑是根路徑,作為分包,直接引入根路徑,會去訪問主包的路徑,導致檔案無法找到。
@import "/static/css/vendor.wxss"; //在分包中用根路徑是無法找到檔案的 ._button,._input[type=button],._input[type=reset],._input[type=submit],._textarea{-webkit-appearance:none}._button:after{border:none}page{background-color:#fff}... 複製程式碼
解決方案
通過shell指令碼對檔案進行批量替換 scripts/path-replace.sh
#!/bin/sh sed -i "_bak" "s/\/static\/css\/vendor\.wxss/\/subPages\/enjoy_given\/static\/css\/vendor\.wxss/g" `grep "\/static\/css\/vendor\.wxss" -rl ./dist/static/css/pages/**/*.wxss ./dist/static/css/pages/*/*/*.wxss` 複製程式碼
這段shell指令碼的目的就是把./dist/static/css/pages/下所有的wxss檔案中的/static/css/vendor.wxss替換成/subPages/huanlesong/static/css\vendor.wxss
替換完成後,路徑變更ok 生成正式包的時候,用npm run build_subPkg就ok了
分享路徑問題
主程式和獨立小程式分享出來的路徑也是一樣的,處理方式和跳轉類似。
解決方案
建議通過通用方法統一處理,我們的做法是,在頁面的onShareAppMessage中加入通用方法Share.getFinalShareInfo
以首頁分享為例
import Share from '@/lib/share' export default { ... onShareAppMessage () { ... return Share.getFinalShareInfo({ title: 'xxx', path: `/pages/content/index/main`, imageUrl: 'xxxx' }) } } 複製程式碼
分享時統一呼叫Share.getFinalShareInfo方法
我們再來看下share.js
export default class Share { static getFinalShareInfo (shareInfo) { ... // 路徑字首處理 shareInfo.path = VARS.pathPrefix + (shareInfo.path.indexOf('/') === 0 ? '' : '/') + shareInfo.path ... return shareInfo } } 複製程式碼
這樣整個分包業務就配置完成了。是不是很麻煩~
當初和主程式融合時候確實踩了很多坑,這裡我把解決方案和大家分享下
如果有更好的解決方案,也希望一起交流:)