—— 一個基于single-spa的vue2升級至vue3的項目
Author 柯雨
Date 2021-01-21
一、 項目背景
隨著2020年9月vue3的正式發布,為了在之后的工作之中能夠逐步全面的使用vue3替代vue2,之前所開發的系統項目升級的需求也就提上了日程。考慮到第一,vue3的使用是新技術的嘗試,第二,vue3的相關生態還在進一步完善之中,因此,我們使用了一個部門內部項目來作為我們的實踐對象,在將其成功升級并等待Vue3相關生態完善穩定后再逐步推廣至其它業務項目之中。
二、前期調研
經過調研,vue2升級至vue3的主流升級方式為更改侵入式修改項目代碼,將vue3中不兼容的特性及代碼進行手動的替換。這種方式的優勢是升級技術難度較低,只需了解vue3與vue2的區別即可。但于此同時,它的缺點也是相當明顯的:首先,由于在升級過程中對代碼邏輯做了一定修改,導致項目可能會出現新的bug;其次,需要逐個頁面逐個組件檢查,在項目較為龐大時需要耗費大量人力;最后,這樣的升級只能解決vue2至vue3的升級問題,如果想更近一步的讓項目支持TS,或者項目使用了element想更換為其它支持vue3的組件庫(目前element沒有任何支持vue3的意思,不過可以嘗試使用element plus),我們還需要付出大量額外的努力,甚至可能不亞于重寫整個項目。
這時,一種完全不同的方式就進入了我們的考慮范圍之中,那就是使用微前端(Micro-Frontends)作為我們的升級方式。這種方式不僅可以用在vue2至vue3升級之中,實際上它更可以作為使用不同技術棧的團隊(React, Angular等)所開發前端項目的整合之中。
三、微前端
3.1什么是微前端
微前端這一說法最早是由THOUGHTWORKS技術雷達在2016年中收錄(Micro frontends),為了解決大型前端項目(多團隊,多技術,新老技術共存)的問題,通過借鑒后端的微服務概念所提出的。微前端是一種類似于微服務的架構,它將微服務的理念應用于瀏覽器端,即將 Web 應用由單一的單體應用轉變為多個小型前端應用聚合為一的應用。與此同時,各個前端應用還可以獨立運行、獨立開發、獨立部署。
3.2微前端的優勢(Micro Frontends By Cam Jackson)
漸進式升級
對于許多團隊來說,這是微前端之旅的起始。由于歷史的技術局限或者交付時間的壓力等原因,存在著很多龐大陳舊的前端項目。對于這些項目而言,重構是一個迫在眉睫的選項。相較于全部推翻重寫,我們更希望一點一點慢慢把老舊的部分翻新,與此同時,持續的為客戶提供新的功能。微前端架構能為我們實踐這種想法提供了可能性。我們只需要對舊項目做一些修改,就可以在添加新功能時選擇是否繼續修改老的項目,或者使用新技術來開發。簡單、解藕的代碼庫
每一個微前端項目相比之前的整體而言代碼量都是大大減少的。簡單獨立的代碼庫不僅使得我們更容易理解自己所需開發維護的項目,也減少了組件之間的不當耦合。于此同時,微前端架構也迫使我們明確了不同部分之間數據以及事件的流向。-
獨立開發部署
與微服務一樣,能夠獨立部署是微前端的關鍵所在。無論你的前端代碼在哪里,每一個微前端項目都需要能夠獨立運行、獨立開發、獨立部署。這樣,維護該微前端的團隊就可以獨自決定他們的開發方向。
獨立開發部署.png -
團隊自治
得益于分離了代碼庫和部署過程,我們向著團隊自治邁出了至關重要的一步,每個團隊對于想要開發的業務以及如何快速高效開發項目都擁有了獨立的決定權。當然,為了這一目標,我們的團隊應該按照所負責的業務縱向劃分,而非通過技術能力橫向劃分,下圖詳細展示了了這一劃分方式。
團隊結構.png
3.3微前端的實現方案(微前端-最容易看懂的微前端知識)
單純根據對概念的理解,很容易想到實現微前端的重要思想就是將應用進行拆解和整合,通常是一個父應用加上一些子應用,那么使用類似Nginx配置不同應用的轉發,或是采用iframe來將多個應用整合到一起等等這些其實都屬于微前端的實現方案,他們之間的對比如下:
方案 | 描述 | 優點 | 缺點 |
---|---|---|---|
Nginx路由轉發??????????? | 通過Nginx配置反向代理來實現不同路徑映射到不同應用,例如www.abc.com/app1對應app1,www.abc.com/app2對應app2,這種方案本身并不屬于前端層面的改造,更多的是運維的配置 | 簡單,快速,易配置 | 在切換應用時會觸發瀏覽器刷新,影響體驗 |
iframe嵌套 | 父應用單獨是一個頁面,每個子應用嵌套一個iframe,父子通信可采用postMessage或者contentWindow方式 | 實現簡單,子應用之間自帶沙箱,天然隔離,互不影響 | iframe的樣式顯示、兼容性等都具有局限性;子應用間通信困難 |
Web Components | 每個子應用需要采用純Web Components技術編寫組件,是一套全新的開發模式 | 每個子應用擁有獨立的script和css,也可單獨部署 | 對于歷史系統改造成本高,子應用通信較為復雜易踩坑 |
組合式應用路由分發 ??????????? | 每個子應用獨立構建和部署,運行時由父應用來進行路由管理,應用加載,啟動,卸載,以及通信機制 | 純前端改造,體驗良好,可無感知切換,子應用相互隔離 ????????????????????????? | 需要設計和開發,由于父子應用處于同一頁面運行,需要解決子應用的樣式沖突,變量對象污染,通信機制等技術點 |
根據上面的對比,我們最終采用了組合式應用路由分發這種方案。
3.4微前端框架
誠然我們可以自己處理路由的分發,不過目前業內已經有了多種框架來幫助我們更輕松快速的集成微前端架構:
Mooa:基于Angular的微前端服務框架
Single-Spa:最早的微前端框架,兼容多種前端技術棧。
Qiankun:基于Single-Spa,阿里系開源微前端框架。
Icestark:阿里飛冰微前端框架,兼容多種前端技術棧。
Ara Framework:由服務端渲染延伸出的微前端框架。
我們這里采用single-spa來實現該項目的微前端架構。Single-spa借鑒了組件生命周期的思想,它為微應用設置了針對路由的生命周期。當微應用匹配路由處于激活狀態時,微應用會把自身的內容掛載到頁面上,反之則卸載。single-spa 又約定應用應包含以下生命周期:bootstrap 引導函數(應用內容首次掛載到頁面前調用)、mount 掛載函數、unmount 卸載函數(須移除事件綁定等內容)以及Update更新函數(非必要)。
四、微前端實踐
4.1項目總覽
該系統為部門內部工具類項目,項目基于Vue2框架開發,目前已經完成了十余個頁面的開發工作。下圖是其首頁截圖,可以看出,我們的頁面主要分為兩個部分,所有頁面通用的側邊欄部分和主體頁面部分。因此,我們可以初步將項目分為三個微前端應用,繼續使用vue2開發的側邊欄部分,使用vue2開發的老頁面以及使用vue3開發的新頁面。
4.2項目整體結構
在實踐中,因為我們的主要目的是將項目從vue2升級至vue3,該項目代碼在當下以及可見的未來都將由我們團隊甚至說我個人單獨維護,因此我們將全部相關代碼放入了一個代碼庫中(當然,如果你的場景不同的話,也完全可以將其放入幾個互不相關的代碼庫中并對它們單獨部署)。下圖展示了我們所設計的微前端項目的整體結構,其中包含了一個基座項目root和兩個分別使用vue2 (JS, Element)和vue3 (TS, AntDesign)的微應用。
其中最重要的就是基座項目,通過引入并配置single-spa,實現了通過監聽URL變化的前綴(這里我們所有vue2應用以pre2/...為前綴,vue3應用以pre3/…為前綴),從而加載不同的微應用的目的。
4.3基座項目(root)
我們基座項目的主要作用是將頁面上的DOM在不同的URL前綴下分配給不同的微應用使用,這里先來看一下root/App.vue文件,這里我們除了將布局以及將側邊欄組件引入外,更重要的是我們創建了一個id為main的空div,這個div在項目中將根據URL被不同的微服務所使用,渲染出所需的主體頁面部分。
<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>
項目中路由分發以及微服務加載的實現主要是由single-spa框架幫助我們完成,我們在其配置文件single-spa-config.js中引入single-spa并通過registerApplication方法來注冊微應用vue2與vue3 。
singleSpa.registerApplication( //注冊微前端服務
'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方法在這里接收了三個參數(以vue2微服務為例,vue3同理)。第一個參數是注冊的微服務名稱;第二個參數是一個加載時方法,該方法會在相應微服務第一次加載時調用,這個方法需要返回加載后的微服務(這個對象中存儲了相應微服務的生命周期函數)。第三個參數是一個判斷何時加載該微服務的方法,這個方法接收了window.location作為參數,通過返回boolean值來確定是否加載此微服務。
下面詳細說明一下第二個參數,也就是加載時方法所做的事情。可以看到,我們區分了開發環境和正式環境,這是考慮到微服務模塊在dev模式下所有代碼都打包進了一個app.js文件中,而打包后的代碼可能會分為多個js文件。所以在正式環境中一方面我們需要在微前端項目中通過stats-wbpack-plugin生成一個資源清單文件,另一方面在我們在基座中獲取這個清單文件并引入相應的資源。此外,讀者可能對于這里所返回的加載后的微服務window.singleVue從哪里來的有所困惑,對于這點,我們將在之后講解。
最后,我們需要配置側邊欄菜單項以便用戶點擊不同菜單時展示不同頁面,這里我們不需要在基座中注冊vue-router,只需要根據用戶所點菜單更改URL即可,具體的router注冊放在相應的微應用中完成。不過需要注意的是,在更改URL時不要使用直接修改location.href等會導致前端頁面刷新的方法,而是應該使用history.pushState(HTML5新特性)等單純改變URL的方法。這是因為,頁面刷新會導致應用基座以及微服務全部重新加載。
4.4 vue2微應用項目(vue2)
這個vue2項目與傳統的vue2項目結構并無什么不同。只是在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,這個庫可以幫助我們直接實現微服務注冊中所必須的生命周期函數,也就是bootstap,mount,unmount這三個方法。這個文件中,需要關注的有以下幾點:
- 我們的vue掛載對象是在基座項目中準備好的id為main的div。
- 通過檢查window.singleSpaNavigate(引入single-spa后會將singleSpaNavigate添加在windows對象上)是否存在來判斷當前項目是獨立部署運行的還是與作為一個微應用向外提供服務的。如果是獨立運行的,那么與傳統的開發方式一至,直接掛載于頁面上即可。
- 使用single-spa-vue庫索提供的方法,傳入VUE及其相應的配置,生成vueLifecycles這個包含single-spa所需生命周期方法的對象并將相應方法導出供外部使用。
- 這里我們進行了vue-router的注冊,router的注冊使得頁面可以根據URL的改變掛載并渲染微服務下不同的組件。
接下來我們來看一下該微應用的webpack配置。這里需要注意兩個部分的改造。首先是在output中,我們通過library:singleVue以及libraryTarget: window將入口文件中所導出的生命周期函數對象以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供基座項目讀取并引入微應用相關資源使用。
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微應用項目(vue3)
這里的配置與vue2微應用項目改造思路如出一轍,只是在項目入口文件中因為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文件夾,可以看到基座項目以及兩個微應用分別打包在了不同的目錄之下(再次重申,這只是我們的做法,微前端架構完全支持你將這幾個微應用部署在不同的服務器上)。
在部署時,我們需要配置nginx的server配置,將/pre2以及/pre3前綴去除,所有的相關請求都指向基座項目,再由基座項目負責加載其余微應用所對應的資源,不然應用項目請求時會發生無法找到所需資源的狀況。
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/(.*) /;
}
六、總結
整個微前端項目的改造過程中,我遇到了很多的阻礙,主要的問題來自于對微前端概念不熟悉以及網上繁雜多樣的微前端實現方案。雖然這個項目最終得以完成并上線運行,但是其自身仍然存在一些問題有待進一步調研解決:
- 不同微應用間切換時,由于應用的初次加載導致的短暫白屏問題。(根據需要采用預加載或增加loading動畫提升用戶體驗)
- 不同微應用css樣式相互影響的問題。(加入項目前綴)
- 添加新頁面時需要同時修改基座項目及微應用項目本身。(引入路由配置文件,菜單根據配置文件生成)
- Keep-alive組件在微服務切換時失效。(修改single-spa-vue unmout生命周期行為,阻止vue destroy)
在這個項目中,我們只是嘗試性的使用路由轉發式微前端來解決vue2至vue3的升級問題,當然微前端的應用場景遠不止這一種,vue2升級vue3的方式也遠不止這一種。但是作為一個還算不錯的解決方案,不妨趁著團隊技術棧升級的過程將微前端應用于自己的項目之中,為之后遇到其它更復雜場景的使用打下堅實的基礎。畢竟微應用可獨立開發部署的特性使得你不必擔心自己努力開發的代碼由于微前端架構在未來的不適用而白白浪費,哪怕真的在未來某一天發現這個架構開始不適用于你的項目之中了,那么我們也只需稍微修改一下項目結構即可重新回到傳統的項目架構。
最后,寫這篇文章的目的一方面是記錄分享自己在微前端開發過程中所獲得的經驗,另一方面也是拋磚引玉,希望與大家多多交流。