上一篇文章:從0實現一個前端微服務(上)中講到,single-spa
的原理就是,將子項目中的link/script
標籤和<div id="app"></div>
插入到主項目,而這個操做的核心就是動態加載js
和css
。javascript
動態加載js
咱們使用的是system.js
,藉助這個插件,咱們只須要將子項目的app.js
暴露給它便可。css
本文章基於GitHub上一個single-spa的demo修改,因此最好有研究過這個demo
,另外本文的基於最新的vue-cli4
開發。html
要實現的效果就是子項目獨立開發部署,順便還能被主項目集成。前端
vue-cli4
直接使用vue create nav
命令生成一個vue
項目。須要注意的是,導航項目路由必須用 history 模式 vue
index.html
文件<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>home-nav</title>
<!-- 配置文件注意寫成絕對路徑:/開頭,不然訪問子項目的時候重定向的index.html,相對目錄會出錯 -->
<script type="systemjs-importmap" src="/config/importmap.json"></script>
<!-- 預請求single-spa,vue,vue-router文件 -->
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js" as="script" crossorigin="anonymous" />
<link rel="preload" href="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js" as="script" crossorigin="anonymous" />
<link rel="preload" href="https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js" as="script" crossorigin="anonymous" />
<!-- 引入system.js相關文件 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-exports.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js"></script>
</head>
<body>
<script> (function() { System.import('single-spa').then(singleSpa => { singleSpa.registerApplication( 'appVueHistory', () => System.import('appVueHistory'), location => location.pathname.startsWith('/app-vue-history/') ) singleSpa.registerApplication( 'appVueHash', () => System.import('appVueHash'), location => location.pathname.startsWith('/app-vue-hash/') ) singleSpa.start(); }) })() </script>
<div class="wrap">
<div class="nav-wrap">
<div id="app"></div>
</div>
<div class="single-spa-container">
<div id="single-spa-application:appVueHash"></div>
<div id="single-spa-application:appVueHistory"></div>
</div>
</div>
<style> .wrap{ display: flex; } .nav-wrap{ flex: 0 0 200px; } .single-spa-container{ width: 200px; flex-grow: 1; } </style>
</body>
</html>
複製代碼
config/importmap.json
:{
"imports": {
"appVue": "http://localhost:7778/app.js",
"appVueHistory": "http://localhost:7779/app.js",
"single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js",
"vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js",
"vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js"
}
}
複製代碼
若是是新開發的項目,能夠先用vue-cli4
生成一個vue
項目,路由使用的是hash
模式。java
若是是老項目,須要分別安裝一下三個插件:node
npm install systemjs-webpack-interop -S
複製代碼
npm install single-spa-vue -S
複製代碼
npm install vue-cli-plugin-single-spa -D
複製代碼
若是是新項目,則可使用如下命令:react
vue add single-spa
複製代碼
注意:該命令會改寫你的 main.js,老項目不要用這個命令 webpack
該命令作了四事件:ios
(1) 安裝 single-spa-vue
插件
(2) 安裝 systemjs-webpack-interop
插件,並生成 set-public-path.js
(3) 修改main.js
(4) 修改webpack
配置(容許跨域,關閉熱更新,去掉splitChunks
等)
因爲single-spa
模式也有開發和生成環境,因此有4種環境:正常開發,single-spa
開發,正常打包,single-spa
打包。可是咱們只須要兩個環境變量文件便可區分開,分別在在根目錄下新建環境變量文件:
.env.devSingleSpa
文件(區分正常開發和single-spa
模式開發):
NODE_ENV = development
VUE_APP__ENV = singleSpa
複製代碼
.env.singleSpa
文件(區分正常打包和single-spa
模式打包):
NODE_ENV = production
VUE_APP__ENV = singleSpa
複製代碼
single-spa
和正常開發模式不同的地方僅僅在入口文件。其中入口文件中須要引入的插件(vuex
,vue-router
,axios
,element-ui
等)徹底同樣,不同的地方在於,正常開發是new Vue(options)
,single-spa
則是調用singleSpaVue(Vue,options)
函數,而且將三個生命週期export
。
因此我將兩種模式下公共的部分任然寫在main.js
,並導出兩種模式所需的配置對象:
import store from "./store";
import Vue from 'vue';
import App from './App.vue';
import router from './router';
const appOptions = {
render: (h) => h(App),
router,
store,
}
Vue.config.productionTip = false;
export default appOptions;
複製代碼
新增index.js
(正常模式入口文件) :
import appOptions from './main';
import './main';
import Vue from 'vue';
new Vue(appOptions).$mount('#app');
複製代碼
新增index.spa.js
(single-spa
模式入口文件) :
import './set-public-path'
import singleSpaVue from 'single-spa-vue';
import appOptions from './main';
import './main';
import Vue from 'vue';
const vueLifecycles = singleSpaVue({
Vue,
appOptions
});
const { bootstrap, mount, unmount } = vueLifecycles;
export { bootstrap, mount, unmount };
複製代碼
其中index.spa.js
裏面的set-public-path.js
:
import { setPublicPath } from 'systemjs-webpack-interop'
//模塊的名稱必須和system.js的配置文件(importmap.json)中的模塊名稱保持一致
setPublicPath('appVueHash')
複製代碼
vue.config.js
)single-spa
模式和正常模式只有入口文件不一樣,其餘的都同樣。也就是說打包以後,只有app.js
文件不一樣,那麼其餘的文件是否能夠複用,可否實現一次打包,便可部署兩種模式?
答案是能夠的:打包的時候我先執行sing-spa
的打包,而後執行正常模式打包,最後將single-spa
打包生成的app.js
文件拷貝到正常打包的文件根目錄下。這樣只須要拿着dist
目錄部署便可,single-spa
不須要作任何修改便可同步更新。
須要注意的是文件不能帶有hash值了,文件沒了hash值就須要服務器本身生成hash值來設置緩存了。
const CopyPlugin = require('copy-webpack-plugin');
const env = process.env.VUE_APP__ENV; // 是不是single-spa
const modeEnv = process.env.NODE_ENV; // 開發環境仍是生產環境
const config = {
productionSourceMap: false,//去掉sourceMap
filenameHashing: false,//去掉文件名的hash值
};
const enteyFile = env === 'singleSpa' ? './src/index.spa.js' : './src/index.js';
//正常打包的app.js在js目錄下,而single-spa模式則須要在根目錄下。
//打包時會從dist-spa/js目錄將app.js拷貝到正常打包的根目錄下,因此不用管,只須要判斷single-spa的開發模式便可
const filename = modeEnv === 'development' ? '[name].js' : 'js/[name].js';
chainWebpack = config => {
config.entry('app')
.add(enteyFile)
.end()
.output
.filename(filename);
if(env === 'singleSpa'){
//vue,vue-router不打包進app.js,使用外鏈
config.externals(['vue', 'vue-router'])
}
}
if(env === 'singleSpa'){
Object.assign(config, {
outputDir: 'dist-spa',
devServer: {
hot: false,//關閉熱更新
port: 7778
},
chainWebpack,
})
}else{
Object.assign(config, {
chainWebpack,
configureWebpack: modeEnv === 'production' ? {
plugins: [
//將single-spa模式下打包生成的app.js拷貝到正常模式打包的主目錄
new CopyPlugin([{
from: 'dist-spa/js/app.js',
to: ''
}])
],
} : {},
})
}
module.exports = config;
複製代碼
打包後的文件效果:
其中js/app.js
是正常模式生成的,而與index.html
同目錄的app.js
是dist-spa/js/app.js
拷貝過來的,是single-spa
模式的入口文件,其餘的文件複用。
package.json
)single-spa
模式下開發/打包都須要改動環境變量,將正常的build
命令修改爲:按順序打包兩次,就能夠實現和原來同樣打包部署流程。
"scripts": {
"spa-serve": "vue-cli-service serve --mode devSingleSpa",
"serve": "vue-cli-service serve",
"spa-build": "vue-cli-service build --mode singleSpa",
"usual-build": "vue-cli-service build",
"build": "npm run spa-build && npm run usual-build",
"lint": "vue-cli-service lint"
},
複製代碼
single-spa
開發使用npm run spa-serve
,正常開發不變。
打包任然使用npm run build
,而後將dist
目錄下的文件部署到子項目服務器便可。
因爲咱們給子項目路由強行加了不一樣前綴(/app-vue-history
),在hash
模式是沒問題的,由於hash
模式下路由跳轉只會修改url
的hash
值,不會修改path
值。history
模式則須要告訴vue-router
,/app-vue-history/
是項目路由前綴,跳轉只須要修改這後面的部分,不然路由跳轉會直接覆蓋所有路徑。那麼這個配置項就是base
屬性:
const router = new VueRouter({
mode: "history",
base: '/',//默認是base
routes,
});
複製代碼
辦法也很簡單,判斷下環境變量,single-spa
模式下base
屬性是/app-vue-history
,正常模式則不變。
可是因爲咱們打包後複用了除app.js
之外的文件,因此只有入口文件才能區分開環境,解決辦法是:
router/index.js
路由文件不導出實例化的路由對象,而導出一個函數:
const router = base => new VueRouter({
mode: "history",
base,
routes,
});
複製代碼
而且main.js
再也不引入路由文件,改爲在入口文件分別引入。
正常模式的入口文件index.js
:
import router from './router';
const baseUrl = '/';
appOptions.router = router(baseUrl);
複製代碼
single-spa
模式的入口文件index.spa.js
:
import router from './router';
const baseUrl = '/app-vue-history';
appOptions.router = router(baseUrl);
複製代碼
system.js
的做用就是動態按需加載模塊。假如咱們子項目都使用了vue
,vuex
,vue-router
,每一個項目都打包一次,就會很浪費。system.js
能夠配合webpack
的externals
屬性,將這些模塊配置成外鏈,而後實現按需加載:
固然了,你也能夠直接用script
標籤將這些公共的js
所有引入,可是這樣會形成浪費,好比說子項目A用到了vue-router
和axios
,可是沒用到vuex
,子項目A刷新,則仍是會請求vuex
,就很浪費,system.js
則會按需加載。
同時,子項目打包成umd
格式,system.js
能夠實現按需加載子項目。
上一篇文章中講到,直接引入子項目的js/css
能夠呈現出子系統,可是動態生成的HTML
中,img/video/audio
等文件的路徑是相對的,致使加載不出來。而解決辦法1是:修改vue-cli4
的 publicPath
設置爲完整的絕對路徑http://localhost:8080/
便可。
這個插件做用就是將子項目的publicPath
暴露出來給system.js
,system.js
根據項目名稱匹配到配置文件(importmap.json
),而後解析配置的url
,將前綴賦給publicPath
。
那麼publicPath
如何動態設置呢?webpack官網中給出的辦法是:webpack
暴露了一個名爲 __webpack_public_path__
的全局變量,直接修改這個值便可。
systemjs-webpack-interop
部分源碼截圖(public-path-system-resolve.js
):
因此這也是爲何single-spa
的入口文件app.js
要和index.html
目錄一致,由於他直接截取了app.js
的路徑做爲了publicPath
。
這個插件的主要做用是幫咱們寫了single-spa
所須要的三個週期事件:bootstrap
,mount
,unmount
。
在mount
週期作的事情就是生成咱們須要的<div id="app"></div>
,固然了,id的名稱它是根據項目名取得:
而後就是在這個div
裏面實例化vue
:
因此若是咱們想讓子項目內容在咱們自定義的區域(默認插入到body
),其中一個辦法是將div
寫好:
home-nav/public/index.html
:
另外一個辦法就是修改這部分代碼,讓他插入到咱們想要插入的地方,而不是body
。
unmount
週期它卸載了實例化的vue
而且清空了DOM
,想要實現keep-alive
效果咱們得修改這部分代碼(後面有介紹)
這個插件主要是用於命令vue add single-spa
執行時,覆蓋你的main.js
而且生成set-public-path.js
,同時修改你的webpack
配置。可是執行npm install vue-cli-plugin-single-spa -D
命令時,它只會覆蓋你的webpack
配置。
其修改webpack
配置的源碼:
module.exports = (api, options) => {
options.css.extract = false
api.chainWebpack(webpackConfig => {
webpackConfig
.devServer
.headers({
'Access-Control-Allow-Origin': '*',
})
.set('disableHostCheck', true)
webpackConfig.optimization.delete('splitChunks')
webpackConfig.output.libraryTarget('umd')
webpackConfig.set('devtool', 'sourcemap')
})
}
複製代碼
回到最初的起點,咱們實現single-spa
最重要的事:動態引入子項目的js/css
,可是你發現沒有,全程都只看到js
的引入,絲毫沒有說起css
,那麼css
文件咋辦?答案就是options.css.extract = false
。
vue-cli3
官網中介紹,這個值爲false
,就是不單獨生成css
文件,和js
文件打包到一塊兒,這讓咱們只須要關心js
文件的引入便可,可是也爲css
污染問題埋下了坑。
另外一個配置就是容許跨域,同時還有文章開頭說起的system.js
要求子項目打包成umd
形式,也是它配置的。
還有一個比較關鍵的配置:webpackConfig.optimization.delete('splitChunks')
,正常狀況下,咱們打包以後的文件除了入口文件app.js
,還有一個文件是chunk-vendors.js
,這個文件裏面包含了一些公共的第三方插件,這樣一來,子項目就有兩個入口文件(或者說得同時加載這兩個文件),因此只能去掉splitChunks
。
部署的時候除入口文件(app.js
)外,其餘的路由文件都複用了正常打包的文件,因此環境變量須要由入口文件注入到全局使用。
index.spa.js
文件:
appOptions.store.commit('setSingleSpa',true);
複製代碼
避免頻繁修改配置文件,設置一個固定的特殊端口,儘可能避免端口衝突。
開發模式仍正常開發,可是single-spa
聯調須要關閉熱更新,不然本地websocket
會一直報failed
。
single-spa
開發中我發現熱更新正常生效。
配置文件注意寫成絕對路徑,不然訪問子項目的時候路由重定向回主項目的index.html
,裏面的url相對目錄會出錯。
home-nav/public/index.html
:
<script type="systemjs-importmap" src="/config/importmap.json"></script>
複製代碼
查看single-spa-vue
源碼能夠發現,在unmount
生命週期,它將vue
實例destroy
(銷燬了)而且清空了DOM
。要想實現keep-alive
,咱們只須要去掉destroy
而且不清空DOM
,而後本身使用display:none
來隱藏和顯示子項目的DOM
便可。
function unmount(opts, mountedInstances) {
return Promise
.resolve()
.then(() => {
mountedInstances.instance.$destroy();
mountedInstances.instance.$el.innerHTML = '';
delete mountedInstances.instance;
if (mountedInstances.domEl) {
mountedInstances.domEl.innerHTML = ''
delete mountedInstances.domEl
}
})
}
複製代碼
咱們使用配置css.extract = true
以後,css
再也不單獨生成文件,而是打包到js
裏面,生成的樣式包裹在style
標籤裏面,子項目卸載以後,樣式文件並無刪除,樣式多了就可能形成樣式污染。
解決辦法:
辦法1:命名規範 + css-scope
+ 去掉全局樣式
辦法2:卸載應用的時候去掉樣式的style
標籤(待研究)
若是必定要寫全局變量,能夠用相似「換膚」的辦法解決:在子項目給body/html
加一個惟一的id(正常開發部署用),而後這個全局的樣式前面加上這個id,而single-spa
模式則須要修改single-spa-vue
,在mount
週期給body/html
加上這個惟一的id,在unmount
週期去掉,這樣就能夠保證這個全局css只對這個項目生效了。
首先得規範開發:在組件的destroy
生命週期去掉全局的屬性/事件,其次還有個辦法就是在子項目加載以前對window
對象作一個快照,而後在卸載的時候恢復以前的狀態。
能夠藉助localstorage
和自定義事件通訊。localstorage
通常用來共享用戶的登錄信息等,而自定義事件通常用於共享實時數據,例如消息數量等。
//一、子組件A 建立事件並攜帶數據
const myCustom = new CustomEvent("custom",{ detail: { data: 'test' } });
//二、子組件B 註冊事件監聽器
window.addEventListener("custom",function(e){
//接收到數據
})
//三、子組件A觸發事件
window.dispatchEvent(myCustom);
複製代碼
其中一個辦法就是沒權限的系統直接隱藏入口導航,而後就是直接輸入url
進入,仍是會加載子項目,可是子項目判斷無權限以後顯示一個403頁面便可。能夠看到子系統對應的入口文件是寫在一個json
文件裏面的,那麼總不能全部人都能讀取到這個json
吧,或者說想實現不一樣權限的用戶的json
配置不一樣。
咱們能夠動態生成script
標籤:
//在加載模塊以前先生成配置json
function insertNewImportMap(newMapJSON) {
const newScript = document.createElement('script')
newScript.type = 'systemjs-importmap';
newScript.innerText = JSON.stringify(newMapJSON);
const test = document.querySelector('#test')
test.insertAdjacentElement('beforebegin',newScript);
}
//內容從接口獲取
const devDependencies = {
imports: {
"navbar": "http://localhost:8083/app.js",
"app1": "http://localhost:8082/app.js",
"app2": "http://localhost/app.js",
"single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js",
"vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js",
"vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js"
}
}
insertNewImportMap(devDependencies);
複製代碼
若是不想本身搭建node靜態文件服務器,給你們推薦一個軟件:XAMPP
文章中的完整demo
文件地址:github.com/gongshun/si…
目前存在的問題
子項目之間路由跳轉無法去掉url
的hash
值,例如從'/app1/#/home'
跳轉到'/app2/'
時,hash值仍會被帶上:'/app2/#/'
,目前看無影響,可是有可能會影響到子項目的路由判斷。
子項目之間即便是同一技術棧也無法統一框架版本,雖然目前是有將公共框架抽離出來的操做,可是實際工做中可能比較難控制。
項目總體開發調試的時候,若是A項目是開發環境,而B項目是打包環境,路由來回切換則會報錯,兩個都是開發環境,或者兩個都是生產環境則不會。(緣由未知)
下一步計劃
qiankun
框架react
項目改造和angular
項目改造,雖然原理相似,可是細節仍是會不一樣最後,感謝你們閱讀,祝你們新年快樂!
有什麼問題歡迎指出,下一篇文章預計年後更新了,須要大量實踐總結。