—— 一個基于single-spa的vue2升級至vue3的項目
Author 柯雨
Date 2021-01-21
一裆悄、 項目背景
隨著2020年9月vue3的正式發(fā)布,為了在之后的工作之中能夠逐步全面的使用vue3替代vue2隔显,之前所開發(fā)的系統(tǒng)項目升級的需求也就提上了日程。考慮到第一赏寇,vue3的使用是新技術(shù)的嘗試捻撑,第二磨隘,vue3的相關(guān)生態(tài)還在進一步完善之中,因此顾患,我們使用了一個部門內(nèi)部項目來作為我們的實踐對象番捂,在將其成功升級并等待Vue3相關(guān)生態(tài)完善穩(wěn)定后再逐步推廣至其它業(yè)務(wù)項目之中。
二江解、前期調(diào)研
經(jīng)過調(diào)研设预,vue2升級至vue3的主流升級方式為更改侵入式修改項目代碼,將vue3中不兼容的特性及代碼進行手動的替換犁河。這種方式的優(yōu)勢是升級技術(shù)難度較低鳖枕,只需了解vue3與vue2的區(qū)別即可魄梯。但于此同時,它的缺點也是相當明顯的:首先宾符,由于在升級過程中對代碼邏輯做了一定修改酿秸,導致項目可能會出現(xiàn)新的bug;其次魏烫,需要逐個頁面逐個組件檢查辣苏,在項目較為龐大時需要耗費大量人力;最后哄褒,這樣的升級只能解決vue2至vue3的升級問題考润,如果想更近一步的讓項目支持TS,或者項目使用了element想更換為其它支持vue3的組件庫(目前element沒有任何支持vue3的意思读处,不過可以嘗試使用element plus)糊治,我們還需要付出大量額外的努力,甚至可能不亞于重寫整個項目罚舱。
這時井辜,一種完全不同的方式就進入了我們的考慮范圍之中,那就是使用微前端(Micro-Frontends)作為我們的升級方式管闷。這種方式不僅可以用在vue2至vue3升級之中粥脚,實際上它更可以作為使用不同技術(shù)棧的團隊(React, Angular等)所開發(fā)前端項目的整合之中。
三包个、微前端
3.1什么是微前端
微前端這一說法最早是由THOUGHTWORKS技術(shù)雷達在2016年中收錄(Micro frontends)刷允,為了解決大型前端項目(多團隊,多技術(shù)碧囊,新老技術(shù)共存)的問題树灶,通過借鑒后端的微服務(wù)概念所提出的。微前端是一種類似于微服務(wù)的架構(gòu)糯而,它將微服務(wù)的理念應(yīng)用于瀏覽器端天通,即將 Web 應(yīng)用由單一的單體應(yīng)用轉(zhuǎn)變?yōu)槎鄠€小型前端應(yīng)用聚合為一的應(yīng)用。與此同時熄驼,各個前端應(yīng)用還可以獨立運行像寒、獨立開發(fā)、獨立部署瓜贾。
3.2微前端的優(yōu)勢(Micro Frontends By Cam Jackson)
漸進式升級
對于許多團隊來說诺祸,這是微前端之旅的起始。由于歷史的技術(shù)局限或者交付時間的壓力等原因祭芦,存在著很多龐大陳舊的前端項目筷笨。對于這些項目而言,重構(gòu)是一個迫在眉睫的選項。相較于全部推翻重寫奥秆,我們更希望一點一點慢慢把老舊的部分翻新逊彭,與此同時,持續(xù)的為客戶提供新的功能构订。微前端架構(gòu)能為我們實踐這種想法提供了可能性侮叮。我們只需要對舊項目做一些修改,就可以在添加新功能時選擇是否繼續(xù)修改老的項目悼瘾,或者使用新技術(shù)來開發(fā)囊榜。簡單、解藕的代碼庫
每一個微前端項目相比之前的整體而言代碼量都是大大減少的亥宿。簡單獨立的代碼庫不僅使得我們更容易理解自己所需開發(fā)維護的項目卸勺,也減少了組件之間的不當耦合。于此同時烫扼,微前端架構(gòu)也迫使我們明確了不同部分之間數(shù)據(jù)以及事件的流向曙求。-
獨立開發(fā)部署
與微服務(wù)一樣,能夠獨立部署是微前端的關(guān)鍵所在映企。無論你的前端代碼在哪里悟狱,每一個微前端項目都需要能夠獨立運行、獨立開發(fā)堰氓、獨立部署挤渐。這樣,維護該微前端的團隊就可以獨自決定他們的開發(fā)方向双絮。
-
團隊自治
得益于分離了代碼庫和部署過程浴麻,我們向著團隊自治邁出了至關(guān)重要的一步,每個團隊對于想要開發(fā)的業(yè)務(wù)以及如何快速高效開發(fā)項目都擁有了獨立的決定權(quán)囤攀。當然软免,為了這一目標,我們的團隊應(yīng)該按照所負責的業(yè)務(wù)縱向劃分抚岗,而非通過技術(shù)能力橫向劃分或杠,下圖詳細展示了了這一劃分方式。
3.3微前端的實現(xiàn)方案(微前端-最容易看懂的微前端知識)
單純根據(jù)對概念的理解宣蔚,很容易想到實現(xiàn)微前端的重要思想就是將應(yīng)用進行拆解和整合,通常是一個父應(yīng)用加上一些子應(yīng)用认境,那么使用類似Nginx配置不同應(yīng)用的轉(zhuǎn)發(fā)胚委,或是采用iframe來將多個應(yīng)用整合到一起等等這些其實都屬于微前端的實現(xiàn)方案,他們之間的對比如下:
方案 | 描述 | 優(yōu)點 | 缺點 |
---|---|---|---|
Nginx路由轉(zhuǎn)發(fā)??????????? | 通過Nginx配置反向代理來實現(xiàn)不同路徑映射到不同應(yīng)用叉信,例如www.abc.com/app1對應(yīng)app1亩冬,www.abc.com/app2對應(yīng)app2,這種方案本身并不屬于前端層面的改造,更多的是運維的配置 | 簡單硅急,快速覆享,易配置 | 在切換應(yīng)用時會觸發(fā)瀏覽器刷新,影響體驗 |
iframe嵌套 | 父應(yīng)用單獨是一個頁面营袜,每個子應(yīng)用嵌套一個iframe撒顿,父子通信可采用postMessage或者contentWindow方式 | 實現(xiàn)簡單,子應(yīng)用之間自帶沙箱荚板,天然隔離凤壁,互不影響 | iframe的樣式顯示、兼容性等都具有局限性跪另;子應(yīng)用間通信困難 |
Web Components | 每個子應(yīng)用需要采用純Web Components技術(shù)編寫組件拧抖,是一套全新的開發(fā)模式 | 每個子應(yīng)用擁有獨立的script和css,也可單獨部署 | 對于歷史系統(tǒng)改造成本高免绿,子應(yīng)用通信較為復(fù)雜易踩坑 |
組合式應(yīng)用路由分發(fā) ??????????? | 每個子應(yīng)用獨立構(gòu)建和部署唧席,運行時由父應(yīng)用來進行路由管理,應(yīng)用加載嘲驾,啟動袱吆,卸載,以及通信機制 | 純前端改造距淫,體驗良好绞绒,可無感知切換,子應(yīng)用相互隔離 ????????????????????????? | 需要設(shè)計和開發(fā)榕暇,由于父子應(yīng)用處于同一頁面運行蓬衡,需要解決子應(yīng)用的樣式?jīng)_突,變量對象污染彤枢,通信機制等技術(shù)點 |
根據(jù)上面的對比狰晚,我們最終采用了組合式應(yīng)用路由分發(fā)這種方案。
3.4微前端框架
誠然我們可以自己處理路由的分發(fā)缴啡,不過目前業(yè)內(nèi)已經(jīng)有了多種框架來幫助我們更輕松快速的集成微前端架構(gòu):
Mooa:基于Angular的微前端服務(wù)框架
Single-Spa:最早的微前端框架壁晒,兼容多種前端技術(shù)棧。
Qiankun:基于Single-Spa业栅,阿里系開源微前端框架秒咐。
Icestark:阿里飛冰微前端框架,兼容多種前端技術(shù)棧碘裕。
Ara Framework:由服務(wù)端渲染延伸出的微前端框架携取。
我們這里采用single-spa來實現(xiàn)該項目的微前端架構(gòu)。Single-spa借鑒了組件生命周期的思想帮孔,它為微應(yīng)用設(shè)置了針對路由的生命周期雷滋。當微應(yīng)用匹配路由處于激活狀態(tài)時,微應(yīng)用會把自身的內(nèi)容掛載到頁面上,反之則卸載晤斩。single-spa 又約定應(yīng)用應(yīng)包含以下生命周期:bootstrap 引導函數(shù)(應(yīng)用內(nèi)容首次掛載到頁面前調(diào)用)焕檬、mount 掛載函數(shù)、unmount 卸載函數(shù)(須移除事件綁定等內(nèi)容)以及Update更新函數(shù)(非必要)澳泵。
四实愚、微前端實踐
4.1項目總覽
該系統(tǒng)為部門內(nèi)部工具類項目,項目基于Vue2框架開發(fā)烹俗,目前已經(jīng)完成了十余個頁面的開發(fā)工作爆侣。下圖是其首頁截圖,可以看出幢妄,我們的頁面主要分為兩個部分兔仰,所有頁面通用的側(cè)邊欄部分和主體頁面部分。因此蕉鸳,我們可以初步將項目分為三個微前端應(yīng)用乎赴,繼續(xù)使用vue2開發(fā)的側(cè)邊欄部分,使用vue2開發(fā)的老頁面以及使用vue3開發(fā)的新頁面潮尝。
4.2項目整體結(jié)構(gòu)
在實踐中榕吼,因為我們的主要目的是將項目從vue2升級至vue3,該項目代碼在當下以及可見的未來都將由我們團隊甚至說我個人單獨維護勉失,因此我們將全部相關(guān)代碼放入了一個代碼庫中(當然羹蚣,如果你的場景不同的話,也完全可以將其放入幾個互不相關(guān)的代碼庫中并對它們單獨部署)乱凿。下圖展示了我們所設(shè)計的微前端項目的整體結(jié)構(gòu)顽素,其中包含了一個基座項目root和兩個分別使用vue2 (JS, Element)和vue3 (TS, AntDesign)的微應(yīng)用。
其中最重要的就是基座項目徒蟆,通過引入并配置single-spa胁出,實現(xiàn)了通過監(jiān)聽URL變化的前綴(這里我們所有vue2應(yīng)用以pre2/...為前綴,vue3應(yīng)用以pre3/…為前綴)段审,從而加載不同的微應(yīng)用的目的全蝶。
4.3基座項目(root)
我們基座項目的主要作用是將頁面上的DOM在不同的URL前綴下分配給不同的微應(yīng)用使用,這里先來看一下root/App.vue文件寺枉,這里我們除了將布局以及將側(cè)邊欄組件引入外抑淫,更重要的是我們創(chuàng)建了一個id為main的空div,這個div在項目中將根據(jù)URL被不同的微服務(wù)所使用型凳,渲染出所需的主體頁面部分丈冬。
<template>
<el-container
class="height100">
<el-aside
class="sideNav"
style="user-select:none;">
<header
class="title">
XXX平臺
</header>
<sider/>
</el-aside>
<el-container class="container height100">
<el-header
class="pl0 pr0"
style="user-select:none;">
<head-nav/>
</el-header>
<div id="main" class="height100"/>
</el-container>
</el-container>
</template>
項目中路由分發(fā)以及微服務(wù)加載的實現(xiàn)主要是由single-spa框架幫助我們完成,我們在其配置文件single-spa-config.js中引入single-spa并通過registerApplication方法來注冊微應(yīng)用vue2與vue3 甘畅。
singleSpa.registerApplication( //注冊微前端服務(wù)
're2',
async () => {
if (process.env.NODE_ENV === 'development') {
await runScript('http://127.0.0.1:4000/re2/app.js');
return window.singleVue
} else {
let singleVue = null
await getManifest('/re2/manifest.json', 'app').then(() => {
singleVue = window.singleVue
});
return singleVue;
}
},
location => location.pathname.startsWith('/pre2') // 配置微前端模塊前綴
)
registerApplication方法在這里接收了三個參數(shù)(以vue2微服務(wù)為例,vue3同理)。第一個參數(shù)是注冊的微服務(wù)名稱;第二個參數(shù)是一個加載時方法疏唾,該方法會在相應(yīng)微服務(wù)第一次加載時調(diào)用蓄氧,這個方法需要返回加載后的微服務(wù)(這個對象中存儲了相應(yīng)微服務(wù)的生命周期函數(shù))。第三個參數(shù)是一個判斷何時加載該微服務(wù)的方法槐脏,這個方法接收了window.location作為參數(shù)喉童,通過返回boolean值來確定是否加載此微服務(wù)。
下面詳細說明一下第二個參數(shù)顿天,也就是加載時方法所做的事情堂氯。可以看到牌废,我們區(qū)分了開發(fā)環(huán)境和正式環(huán)境咽白,這是考慮到微服務(wù)模塊在dev模式下所有代碼都打包進了一個app.js文件中,而打包后的代碼可能會分為多個js文件鸟缕。所以在正式環(huán)境中一方面我們需要在微前端項目中通過stats-wbpack-plugin生成一個資源清單文件晶框,另一方面在我們在基座中獲取這個清單文件并引入相應(yīng)的資源。此外懂从,讀者可能對于這里所返回的加載后的微服務(wù)window.singleVue從哪里來的有所困惑授段,對于這點,我們將在之后講解番甩。
最后侵贵,我們需要配置側(cè)邊欄菜單項以便用戶點擊不同菜單時展示不同頁面,這里我們不需要在基座中注冊vue-router,只需要根據(jù)用戶所點菜單更改URL即可缘薛,具體的router注冊放在相應(yīng)的微應(yīng)用中完成窍育。不過需要注意的是,在更改URL時不要使用直接修改location.href等會導致前端頁面刷新的方法掩宜,而是應(yīng)該使用history.pushState(HTML5新特性)等單純改變URL的方法蔫骂。這是因為,頁面刷新會導致應(yīng)用基座以及微服務(wù)全部重新加載牺汤。
4.4 vue2微應(yīng)用項目(vue2)
這個vue2項目與傳統(tǒng)的vue2項目結(jié)構(gòu)并無什么不同辽旋。只是在vue的入口文件main.js以及webpack的打包上略有不同。
import Vue from 'vue'
import routers from '@/router'
import store from './store'
import singleSpaVue from 'single-spa-vue'
const vueOptions = {
el: '#main',
router: routers,
store,
render: (h) => h(App),
}
if (!window.singleSpaNavigate){
new Vue(vueOptions)
}
/* eslint-disable no-new */
const vueLifecycles = singleSpaVue({
Vue,
appOptions: vueOptions
})
export const bootstrap = vueLifecycles.bootstrap
export const mount = vueLifecycles.mount
export const unmount = vueLifecycles.unmount
在main.js中檐迟,我們需要引入single-spa-vue,這個庫可以幫助我們直接實現(xiàn)微服務(wù)注冊中所必須的生命周期函數(shù)补胚,也就是bootstap,mount,unmount這三個方法。這個文件中追迟,需要關(guān)注的有以下幾點:
- 我們的vue掛載對象是在基座項目中準備好的id為main的div溶其。
- 通過檢查window.singleSpaNavigate(引入single-spa后會將singleSpaNavigate添加在windows對象上)是否存在來判斷當前項目是獨立部署運行的還是與作為一個微應(yīng)用向外提供服務(wù)的。如果是獨立運行的敦间,那么與傳統(tǒng)的開發(fā)方式一至瓶逃,直接掛載于頁面上即可束铭。
- 使用single-spa-vue庫索提供的方法,傳入VUE及其相應(yīng)的配置厢绝,生成vueLifecycles這個包含single-spa所需生命周期方法的對象并將相應(yīng)方法導出供外部使用契沫。
- 這里我們進行了vue-router的注冊,router的注冊使得頁面可以根據(jù)URL的改變掛載并渲染微服務(wù)下不同的組件昔汉。
接下來我們來看一下該微應(yīng)用的webpack配置懈万。這里需要注意兩個部分的改造。首先是在output中靶病,我們通過library:singleVue以及l(fā)ibraryTarget: window將入口文件中所導出的生命周期函數(shù)對象以singleVue的名稱掛載在window上会通。
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
output: {
library: "singleVue", // 導出名稱
libraryTarget: "window", //掛載目標
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
}
//......
其次,在plugins中我們使用stats-webpack-plugin導出項目的資源清單manifest.json供基座項目讀取并引入微應(yīng)用相關(guān)資源使用娄周。
const StatsPlugin = require('stats-webpack-plugin')
//......
plugins: [
new StatsPlugin('manifest.json', {
chunkModules: false,
entrypoints: true,
source: false,
chunks: false,
modules: false,
assets: false,
children: false,
exclude: [/node_modules/]
})
]
//......
4.5 vue3微應(yīng)用項目(vue3)
這里的配置與vue2微應(yīng)用項目改造思路如出一轍涕侈,只是在項目入口文件中因為vue3的新掛載方式而略有不同。這里就不再詳細闡述了昆咽。
import {createApp, h} from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import Antd from 'ant-design-vue'
import singleSpaVue from 'single-spa-vue'
if(!window.singleSpaNavigate){
createApp(App).use(store).use(router).use(Antd).mount('#app')
}
const vueLifecycles = singleSpaVue({
createApp,
appOptions: {
render(): any {
return h(App)
},
},
handleInstance: (app: any) => {
app.use(store).use(router).use(Antd).mount('#main')
}
})
export const bootstrap = vueLifecycles.bootstrap
export const mount = vueLifecycles.mount
export const unmount = vueLifecycles.unmount
五驾凶、項目部署
項目在整體打包后會生成一個dist文件夾,可以看到基座項目以及兩個微應(yīng)用分別打包在了不同的目錄之下(再次重申掷酗,這只是我們的做法调违,微前端架構(gòu)完全支持你將這幾個微應(yīng)用部署在不同的服務(wù)器上)。
在部署時,我們需要配置nginx的server配置,將/pre2以及/pre3前綴去除饼暑,所有的相關(guān)請求都指向基座項目睬塌,再由基座項目負責加載其余微應(yīng)用所對應(yīng)的資源当编,不然應(yīng)用項目請求時會發(fā)生無法找到所需資源的狀況。
server {
listen 9090;
server_name localhost.com;
location / {
root /Users/zhangkeyu/Desktop/project/Remonitor-UI/dist;
index root/index.html;
}
location /pre2/ {
rewrite ^/pre2/(.*) /;
}
location /pre3/ {
rewrite ^/pre3/(.*) /;
}
六、總結(jié)
整個微前端項目的改造過程中,我遇到了很多的阻礙然痊,主要的問題來自于對微前端概念不熟悉以及網(wǎng)上繁雜多樣的微前端實現(xiàn)方案。雖然這個項目最終得以完成并上線運行屉符,但是其自身仍然存在一些問題有待進一步調(diào)研解決:
- 不同微應(yīng)用間切換時剧浸,由于應(yīng)用的初次加載導致的短暫白屏問題。(根據(jù)需要采用預(yù)加載或增加loading動畫提升用戶體驗)
- 不同微應(yīng)用css樣式相互影響的問題矗钟。(加入項目前綴)
- 添加新頁面時需要同時修改基座項目及微應(yīng)用項目本身唆香。(引入路由配置文件,菜單根據(jù)配置文件生成)
- Keep-alive組件在微服務(wù)切換時失效吨艇。(修改single-spa-vue unmout生命周期行為躬它,阻止vue destroy)
在這個項目中,我們只是嘗試性的使用路由轉(zhuǎn)發(fā)式微前端來解決vue2至vue3的升級問題东涡,當然微前端的應(yīng)用場景遠不止這一種冯吓,vue2升級vue3的方式也遠不止這一種倘待。但是作為一個還算不錯的解決方案,不妨趁著團隊技術(shù)棧升級的過程將微前端應(yīng)用于自己的項目之中桑谍,為之后遇到其它更復(fù)雜場景的使用打下堅實的基礎(chǔ)延柠。畢竟微應(yīng)用可獨立開發(fā)部署的特性使得你不必擔心自己努力開發(fā)的代碼由于微前端架構(gòu)在未來的不適用而白白浪費祸挪,哪怕真的在未來某一天發(fā)現(xiàn)這個架構(gòu)開始不適用于你的項目之中了锣披,那么我們也只需稍微修改一下項目結(jié)構(gòu)即可重新回到傳統(tǒng)的項目架構(gòu)。
最后贿条,寫這篇文章的目的一方面是記錄分享自己在微前端開發(fā)過程中所獲得的經(jīng)驗雹仿,另一方面也是拋磚引玉,希望與大家多多交流整以。