背景
隨着項目的成長,單頁spa逐漸包含了許多業務線
- 商城系統
- 售後系統
- 會員系統
- ...
當項目頁面超過一定數量(>150個)之後,會產生一系列的問題
- 項目編譯的時間(啓動server,修改代碼)越來越長,而每次調試關注的可能只是其中1、2個頁面
- 所有的需求都定位到當前git,需求過多導致測試環境經常不夠用
基於以上問題有了對git進行拆分的技術需求。本文介紹我們的實現方式
技術框架
- vue: 2.4.2
- vue-router: 2.7.0
- vuex: 2.5.0
- webpack: 4.7.0
實現
假設我們要從main業務線拆分一個業務線 hello 出來
流程
- 用戶訪問路由 #/hello/index;
- main業務線router未匹配,走公共*處理;
- 公共router判定當前路由爲業務線hello路由,請求hello的bundle js;
- hello入口js執行過程中,將自身的router與store註冊到main業務線;
- 註冊完畢,標記當前業務線hello爲已註冊;
- 之後路由調用next。會自動繼續請求 #/hello/index對應的入口chunk(js,css)頁面跳轉成功;
- 此時hello已經與main業務線完成融合,hello可以自由使用全部的store,使用router可以自由跳轉任何頁面。done
業務線配置
請求業務線路由(步驟1)
第一次請求#/hello/index時,此時router中所有路由無法匹配,會走公共處理
const router = new VueRouter({
routes: [
...
// 不同路由默認跳轉鏈接不同
{
path: '*',
async beforeEnter(to, from, next) {
// 業務線攔截
let isService = await service.handle(to, from, next);
// 非業務線頁面,走默認處理
if(!isService) {
next('/error');
}
}
}
]
});
業務線初始化(步驟2、步驟3)
首先我們需要一個全局的業務線配置,存放各個業務線的入口js文件
consg config = {
"hello": {
"src": [
"http://local.aaa.com:7000/dist/dev/js/hellomanifest.js",
"http://local.aaa.com:7000/dist/dev/js/hellobundle.js"
]
},
"其他業務線": {...}
}
此時需要利用業務線配置,判斷當前路由是否屬於業務線,是的話就請求業務線,不是返回false
// 業務線接入處理
export const handle = async (to, from, next) => {
let path = to.path || "";
let paths = path.split('/');
let serviceName = paths[1];
let cfg = config[serviceName];
// 非業務線路由
if(!cfg) {
return false;
}
// 該業務線已經加載
if(cfg.loaded) {
next();
return true;
}
for(var i=0; i<cfg.src.length; i++) {
await loadScript(cfg.src[i]);
}
cfg.loaded = true;
next(to); // 繼續請求頁面
return true;
}
有幾點需要注意
- 一般業務線配置存放在後端,此處爲了說明直接列出
- 業務線只加載1次,loaded爲判定條件。加載過的話直接進行next
hello的入口entry.js做的工作(步驟4)
爲了節省資源,hello業務線不再重複打包vue,vuex等main業務線已經加載的框架。
那麼爲了hello能正常工作,需要main業務線將以上框架傳遞給hello,方法爲直接將相關變量掛在到window:
import Vue from 'vue';
import { default as globalRouter } from 'app/router.js'; 2個需要動態賦值
import { default as globalStore } from 'app/vuex/index.js';
import Vuex from 'vuex'
// 掛載業務線數據
function registerApp(appName, {
store,
router
}) {
if(router) {
globalRouter.addRoutes(router);
}
if(store) {
globalStore.registerModule(appName, Object.assign(store, {
namespaced: true
}));
}
}
window.bapp = Object.assign(window.bapp || {}, {
Vue,
Vuex,
router: globalRouter,
store: globalStore,
util: {
registerApp
}
});
注意registerApp這個方法,此方法爲hello與main業務線融合的掛載方法,由業務線調用。
上一步已經正常運行了hello的entry.js,那我們看看hello在entry中幹了什麼:
import App from 'app/pages/Hello.vue'; // 路由器根實例
import {APP_NAME} from 'app/utils/global';
import store from 'app/vuex/index';
let router = [{
path: `/${APP_NAME}`,
name: 'hello',
meta: {
title: '頁面測試',
needLogin: true
},
component: App,
children: [
{
path: 'index',
name: 'hello-index',
meta: {
title: '商品列表'
},
component: resolve => require.ensure([], () => resolve(require('app/pages/goods/Goods.vue').default), 'hello-goods')
},
{
path: 'newreq',
name: 'hello-newreq',
meta: {
title: '新品頁面'
},
component: resolve => require.ensure([], () => resolve(require('app/pages/newreq/List.vue').default), 'hello-newreq')
},
]
}]
window.bapp && bapp.util.registerApp(APP_NAME, {router, store});
注意幾點
- APP_NAME是業務線的唯一標識,也就是hello
- 業務線有自己的router和store
- 業務線主動調用registerApp,將自己的router和store與main業務線相應的router、store融合
- store融合的時候需要添加namespace,因爲此時整合hello業務線store成爲了globalStore的一個module
- addRoute和registerModule是router與store的動態註冊方法
業務線配置更新
業務線配置需要在hello每次編譯完成後更新,更新分爲本地調試更新和線上更新。
- 本地調試更新只需要更新本地一個配置文件,然後在loadConfig請求接口時由main業務線讀取該文件返回給js。
- 線上更新更爲簡單,每次發佈編譯後,將當前入口js+md5的完整路徑更新到後端
以上,使用webpack-plugin比較適合當前場景,如下
class ServiceUpdatePlugin {
constructor(options) {
this.options = options;
this.runCount = 0;
}
// 更新本地配置文件
updateLocalConfig({srcs}) {
....
}
// 更新線上配置文件
uploadOnlineConfig({files}) {
....
}
apply(compiler) {
// 調試環境:編譯完畢,修改本地文件
if(process.env.NODE_ENV === 'dev') {
// 本地調試沒有md5值,不需要每次刷新
compiler.hooks.done.tap('ServiceUpdatePlugin', (stats) => {
if(this.runCount > 0) {
return;
}
let assets = stats.compilation.assets;
let publicPath = stats.compilation.options.output.publicPath;
let js = Object.keys(assets).filter(item => {
// 過濾入口文件
return item.startsWith('js/');
}).map(path => `${publicPath}${path}`);
this.updateLocalConfig({srcs: js});
this.runCount++;
});
}
// 發佈環境:上傳完畢,請求後端修改
else {
if(!compiler.hooks.mcUploaded) {
console.error('依賴包 @mc/webpack-mc-static 未引入:mnpm install @mc/webpack-mc-static');
return;
}
compiler.hooks.uploaded.tap('ServiceUpdatePlugin', (upFiles) => {
let entries = upFiles.filter(file => {
return file &&
file.endsWith('js') &&
file.includes('js/');
});
this.uploadOnlineConfig({files: entries});
return;
})
}
}
}
注意,uploaded事件由我們項目組的靜態資源上傳plugin發出,會傳遞當前所有上傳文件完整路徑。需要等文件上傳完畢纔可更新業務線
調試發佈
基於上面的plugin。
調試過程如下:
- 啓動main業務線server(端口7777)
- 啓動hello業務線server(端口7000),此時啓動成功會同時更新本地業務線配置文件
- 訪問hello頁面,加載本地配置後,加載7000端口提供的靜態資源
發佈test過程如下:
- 本地 npm run test
- 上傳文件並更新test環境業務線配置
- 此時訪問test環境頁面已經更新
可以看到hello發佈是比main業務線更加輕量的,這時因爲業務線即時更新接口,但是main業務線還需要更新html的web服務
小結
至此已經完成了我們一開始的主體需求,可以看到主要利用了vue家族的動態註冊方法。下面是一些過程中的問題和解決思路
遇到的問題與解決
hello業務線的wepback打包
- 爲了能與main業務線區分,會給hello業務線的bundle與manifest重命名,增加了業務線名稱前綴
- 由於主要框架都由main業務線加載完畢,因此hello不再分vendor。爲了兼容js個數加速業務線初始化,入口文件越少越好
- 嘗試去掉manifest.js失敗,去掉後hello其他模塊無法正常引入,todo
{
...
entry: {
[app_name + 'bundle']: path.resolve(SRC, `entry.js`)
},
output: {
publicPath: `http://local.aaa.com:${PORT}${devDefine.publicPath}`
},
...
optimization: {
runtimeChunk: {
name: app_name+ 'manifest'
},
splitChunks: {
cacheGroups: false // 業務線不分包
}
}
},
...
}
同名同路徑文件sourcemap互相覆蓋
因爲hello是由main業務線拆分而來,那麼hello的需要目錄接口與main業務線一直,有些文件(如entry.js)的路徑和main業務線一模一樣。
這樣產生了1個問題,後加載的文件sourcemap會覆蓋前一個文件。
這樣不利於調試,爲了區分hello與main業務線,重命名hello下src目錄爲hello_src,實現另類的命名空間
router拆分問題
最開始使用/:name來做公共處理。
但是發現router的優先級按照數組的插入順序,那麼後插入的hello路由優先級將一直低於/:name路由。
之後使用做公共處理,一直處於兜底,沒有此問題。
store拆分
hello的store做爲globalStore的一個module註冊,需要標註 namespaced: true,否則拿不到數據
接口拆分
雖然前端工程拆分了,但是後端接口依然是走相同的域名,因此可以給hello暴露一個生成接口參數的公共方法,然後由hello自己組織。
公共利用
可以直接使用全局組件,mixins,directives,可以直接使用font。
局部的相關內容需要拷貝到hello或者暴露給hello纔可用。
圖片完全無法複用
本地server策略
main業務線由於需要對來往request有比較精細的操作,因此是自己實現的express來達到本地調試目的。
hello工程的唯一作用是提供本地當前的js與css,因此使用官方devServer就夠了。