隨着項目的成長,單頁spa
逐漸包含了許多業務線css
當項目頁面超過必定數量(150+)以後,會產生一系列的問題html
項目編譯的時間(啓動server,修改代碼)愈來愈長,而每次調試關注的可能只是其中一、2個頁面
全部的需求都定位到當前git,需求過多致使測試環境常常排隊
基於以上問題有了對git進行拆分的技術需求。具體以下前端
spa
因爲改善的是開發環境,固然不但願拆分項目影響用戶體驗。若是徹底將業務線拆分紅2個獨立頁面,那麼用戶在業務線之間跳轉時將再也不流暢,由於全部框架以及靜態資源都會在頁面切換的時候重載。所以要求跳轉業務線的時候依然停留在spa內部,
不刷新頁面
,共用同一個頁面入口;
由於大部分業務線須要用到的框架(vue
,vuex
...), 公共組件(dialog
,toast
)都已經在spa入口加載過了,不但願業務線重複加載這些資源。業務線項目中應該只包含本身獨有的資源,並能使用公共資源;
業務線之間應該能用router互相跳轉,能訪問其餘業務線包括全局的store
需求如上,下面介紹的實現方式vue
假設要從主項目拆分一個業務線 hello
出來webpack
#/hello/index
;*
處理;bundle js
;chunk
(js,css)頁面跳轉成功;須要的功能就是這些,下面分步驟看看具體實現git
第一次請求#/hello/index
時,此時router中全部路由沒法匹配,會走公共*
處理web
/** 主項目 **/ const router = new VueRouter({ routes: [ ... // 不一樣路由默認跳轉連接不一樣 { path: '*', async beforeEnter(to, from, next) { // 業務線攔截 let isService = await service.handle(to, from, next); // 非業務線頁面,走默認處理 if(!isService) { next('/error'); } } } ] });
首先須要一個全局的業務線配置,存放各個業務線的入口js文件vue-router
const config = { "hello": { "src": [ "http://local.aaa.com:7000/dist/dev/js/hellobundle.js" ] }, "其餘業務線": {...} }
此時須要利用業務線配置,判斷當前路由是否屬於業務線,是的話就請求業務線,不是返回falsevuex
/** 主項目 **/ // 業務線接入處理 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; }
有幾點須要注意express
loaded
爲斷定條件。加載過的話直接進行next#/hello/index
的路由,此時next能夠正常跳轉。緣由見下一節爲了節省資源,hello業務線再也不重複打包vue
,vuex
等主項目已經加載的框架。
那麼爲了hello能正常工做,須要主項目將以上框架傳遞給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與主項目融合的掛載方法,由業務線調用。
上一步已經正常運行了hello的entry.js,那咱們看看hello在entry中幹了什麼:
/** hello **/ 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
是業務線的惟一標識,也就是hellorouter
和store
registerApp
,將本身的router和store與主項目融合namespace: true
,由於此時整個hello業務線store成爲了globalStore的一個moduleaddRoutes
和registerModule
是router與store的動態註冊方法name
須要和主項目保持惟一業務線配置須要在hello每次編譯完成後更新,更新分爲本地調試更新
和線上更新
。
本地調試更新
只須要更新一個本地配置文件service-line-config.json
,而後在請求業務線config時由主項目讀取該文件返回給js。線上更新
更爲簡單,每次發佈編譯後,將當前入口js+md5的完整url更新到後端以上,看到使用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 { 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發出,會傳遞當前全部上傳文件完整路徑。須要等文件上傳cdn完畢纔可更新業務線
以後在webpack中使用便可
/** hello **/ { ... plugins: [ // 業務線js md5更新 new McServiceUpdatePlugin({ app_name, configFile: path.resolve(process.cwd(), '../mainProject/app/service-line-config.json') }) ], ... }
注意本地調試時業務線config是主項目
纔會用到的,所以直接更新主項目目錄下的配置文件
基於上面的plugin,有如下效果
7777
);7000
),此時啓動成功會同時更新本地文件service-line-config.json
;7000
端口提供的靜態資源(如http://local.aaa.com:7000/dist/dev/js/hellobundle.js)npm run test
能夠看到hello發佈是比主項目更加輕量的,這是由於業務線只更新接口,可是主項目要發佈還須要更新html的web服務
至此已經完成了一開始的主體需求,訪問業務線頁面後,業務線頁面會和主項目頁面合併成爲1個新的spa,spa內部store和router徹底共享。
能夠看到主要利用了vue家族的動態註冊方法。下面是一些過程當中遇到的問題和解決思路
bundle
重命名,增長了業務線名稱前綴入口文件越少越好,所以刪除了一些打包配置
vendor
: 主要第三方庫由主項目加載dll
: dll資源由主項目加載manifest
)配置: 各業務線將各自處理依賴加載/** hello **/ { ... entry: { [app_name + 'bundle']: path.resolve(SRC, `entry.js`) }, output: { publicPath: `http://local.aaa.com:${PORT}${devDefine.publicPath}`, library: app_name // 業務線命名空間 }, ... optimization: { runtimeChunk: false, // 依賴處理與bundle合併 splitChunks: { cacheGroups: false // 業務線不分包 } }, ... }
注意library
的設置隔離了各個業務線
入口文件
依賴
最開始使用/:name來作公共處理。
可是發現router的優先級按照數組的插入順序,那麼後插入的hello路由優先級將一直低於/:name路由。
以後使用*
作公共處理,將一直處於兜底,問題解決。
hello的store作爲globalStore的一個module註冊,須要標註 namespaced: true
,不然拿不到數據;
store使用基本和主項目一致:
/** hello **/ let { Vuex } = bapp; // 全局store獲取 let { mapState: gmapState, mapActions: gmapActions, createNamespacedHelpers } = Vuex; // 本業務線store獲取 const { mapState, mapActions } = createNamespacedHelpers(`${APP_NAME}/feedback`) export default { ... computed: { ...gmapState('userInfo', { userName: state => state.userName }), ...gmapState('hello/feedback', { helloName2: state => state.helloName }), ...mapState({ helloName: state => state.helloName }) }, }
雖然前端工程拆分了,可是後端接口依然是走相同的域名,所以能夠給hello暴露一個生成接口參數的公共方法,而後由hello本身組織。
能夠直接使用全局組件
,mixins
,directives
,能夠直接使用font
。
局部的相關內容須要拷貝到hello或者暴露給hello纔可用。
圖片徹底沒法複用
主項目因爲須要對request有比較精細的操做,所以是咱們本身實現的express
來本地調試。
可是hello工程的惟一做用是提供本地當前的js與css,所以使用官方devServer
就夠了。
以上