新聞中心
背景
隨著SPA大規(guī)模的應(yīng)用,緊接著就帶來一個(gè)新問題:一個(gè)規(guī)模化應(yīng)用需要拆分。

網(wǎng)站建設(shè)哪家好,找成都創(chuàng)新互聯(lián)!專注于網(wǎng)頁設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、小程序設(shè)計(jì)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶創(chuàng)新互聯(lián)還提供了管城免費(fèi)建站歡迎大家使用!
一方面功能快速增加導(dǎo)致打包時(shí)間成比例上升,而緊急發(fā)布時(shí)要求是越短越好,這是矛盾的。另一方面當(dāng)一個(gè)代碼庫集成了所有功能時(shí),日常協(xié)作絕對是非常困難的。而且最近十多年,前端技術(shù)的發(fā)展是非常快的,每隔兩年就是一個(gè)時(shí)代,導(dǎo)致同志們必須升級項(xiàng)目甚至于換一個(gè)框架。但如果大家想在一個(gè)規(guī)?;瘧?yīng)用中一個(gè)版本做好這件事,基本上是不可能的。
最早的解決方案是采用iframe的方法,根據(jù)功能主要模塊拆分規(guī)?;瘧?yīng)用,子應(yīng)用之間使用跳轉(zhuǎn)。但這個(gè)方案最大問題是導(dǎo)致頁面重新加載和白屏。
那有什么好的解決方案呢?微前端這樣具有跨應(yīng)用的解決方案在此背景下應(yīng)運(yùn)而生了!
微前端的概念
微前端是什么:微前端是一種類似于微服務(wù)的架構(gòu),是一種由獨(dú)立交付的多個(gè)前端應(yīng)用組成整體的架構(gòu)風(fēng)格,將前端應(yīng)用分解成一些更小、更簡單的能夠獨(dú)立開發(fā)、測試、部署的應(yīng)用,而在用戶看來仍然是內(nèi)聚的單個(gè)產(chǎn)品。有一個(gè)基座應(yīng)用(主應(yīng)用),來管理各個(gè)子應(yīng)用的加載和卸載。
f135ab0912746bd6.png
所以微前端不是指具體的庫,不是指具體的框架,不是指具體的工具,而是一種理想與架構(gòu)模式。
微前端的核心三大原則就是:獨(dú)立運(yùn)行、獨(dú)立部署、獨(dú)立開發(fā)
微前端的優(yōu)勢
采用微前端架構(gòu)的好處就是,將這些小型應(yīng)用融合為一個(gè)完整的應(yīng)用,或者將原本運(yùn)行已久、沒有關(guān)聯(lián)的幾個(gè)應(yīng)用融合為一個(gè)應(yīng)用可以將多個(gè)項(xiàng)目融合為一,又可以減少項(xiàng)目之間的耦合,提升項(xiàng)目擴(kuò)展性。
實(shí)現(xiàn)微前端的幾種方式
- 從single-spa到qiankun
- 基于WebComponent的micro-app
- webpack5實(shí)現(xiàn)的Module Federation
微前端框架的分類
Single-spa
single-spa是一個(gè)很好的微前端基礎(chǔ)框架,而qiankun框架就是基于single-spa來實(shí)現(xiàn)的,在single-spa的基礎(chǔ)上做了一層封裝,也解決了single-spa的一些缺陷。
首先我們先來了解該如何使用single-spa來完成微前端的搭建。
single-spa.jpg
Single-spa實(shí)現(xiàn)原理
首先在基座應(yīng)用中注冊所有App的路由,single-spa保存各子應(yīng)用的路由映射關(guān)系,充當(dāng)微前端控制器Controler,。URL響應(yīng)時(shí),匹配子應(yīng)用路由并加載渲染子應(yīng)用。上圖便是對single-spa完整的描述。
有了理論基礎(chǔ),接下來,我們來看看代碼層面時(shí)如何使用的。
以下以Vue工程為例基座構(gòu)建single-spa,在Vue工程入口文件main.js完成基座的配置。
基座配置
//main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerApplication, start } from 'single-spa'
Vue.config.productionTip = false
const mountApp = (url) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = url
script.onload = resolve
script.onerror = reject
// 通過插入script標(biāo)簽的方式掛載子應(yīng)用
const firstScript = document.getElementsByTagName('script')[0]
// 掛載子應(yīng)用
firstScript.parentNode.insertBefore(script, firstScript)
})
}
const loadApp = (appRouter, appName) => {
// 遠(yuǎn)程加載子應(yīng)用
return async () => {
//手動掛載子應(yīng)用
await mountApp(appRouter + '/js/chunk-vendors.js')
await mountApp(appRouter + '/js/app.js')
// 獲取子應(yīng)用生命周期函數(shù)
return window[appName]
}
}
// 子應(yīng)用列表
const appList = [
{
// 子應(yīng)用名稱
name: 'app1',
// 掛載子應(yīng)用
app: loadApp('http://localhost:8083', 'app1'),
// 匹配該子路由的條件
activeWhen: location => location.pathname.startsWith('/app1'),
// 傳遞給子應(yīng)用的對象
customProps: {}
},
{
name: 'app2',
app: loadApp('http://localhost:8082', 'app2'),
activeWhen: location => location.pathname.startsWith('/app2'),
customProps: {}
}
]
// 注冊子應(yīng)用
appList.map(item => {
registerApplication(item)
})
// 注冊路由并啟動基座
new Vue({
router,
mounted() {
start()
},
render: h => h(App)
}).$mount('#app')
復(fù)制代碼
構(gòu)建基座的核心是:配置子應(yīng)用信息,通過registerApplication注冊子應(yīng)用,在基座工程掛載階段start啟動基座。
子應(yīng)用配置
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue'
Vue.config.productionTip = false
const appOptions = {
el: '#microApp',
router,
render: h => h(App)
}
// 支持應(yīng)用獨(dú)立運(yùn)行、部署,不依賴于基座應(yīng)用
// 如果不是微應(yīng)用環(huán)境,即啟動自身掛載的方式
if (!process.env.isMicro) {
delete appOptions.el
new Vue(appOptions).$mount('#app')
}
// 基于基座應(yīng)用,導(dǎo)出生命周期函數(shù)
const appLifecycle = singleSpaVue({
Vue,
appOptions
})
// 拋出子應(yīng)用生命周期
// 啟動生命周期函數(shù)
export const bootstrap = (props) => {
console.log('app2 bootstrap')
return appLifecycle.bootstrap(() => { })
}
// 掛載生命周期函數(shù)
export const mount = (props) => {
console.log('app2 mount')
return appLifecycle.mount(() => { })
}
// 卸載生命周期函數(shù)
export const unmount = (props) => {
console.log('app2 unmount')
return appLifecycle.unmount(() => { })
}
復(fù)制代碼
配置子應(yīng)用為umd打包方式
//vue.config.js
const package = require('./package.json')
module.exports = {
// 告訴子應(yīng)用在這個(gè)地址加載靜態(tài)資源,否則會去基座應(yīng)用的域名下加載
publicPath: '//localhost:8082',
// 開發(fā)服務(wù)器
devServer: {
port: 8082
},
configureWebpack: {
// 導(dǎo)出umd格式的包,在全局對象上掛載屬性package.name,基座應(yīng)用需要通過這個(gè)
// 全局對象獲取一些信息,比如子應(yīng)用導(dǎo)出的生命周期函數(shù)
output: {
// library的值在所有子應(yīng)用中需要唯一
library: package.name,
libraryTarget: 'umd'
}
}
復(fù)制代碼
配置子應(yīng)用環(huán)境變量
// .env.micro
NODE_ENV=development
VUE_APP_BASE_URL=/app2
isMicro=true
復(fù)制代碼
子應(yīng)用配置的核心是用singleSpaVue生成子路由配置后,必須要拋出其生命周期函數(shù)。
用以上方式便可輕松實(shí)現(xiàn)一個(gè)簡單的微前端應(yīng)用了。
那么我們有single-spa這種微前端解決方案,為什么還需要qiankun呢?
相比于single-spa,qiankun他解決了JS沙盒環(huán)境,不需要我們自己去進(jìn)行處理。在single-spa的開發(fā)過程中,我們需要自己手動的去寫調(diào)用子應(yīng)用JS的方法(如上面的 createScript方法),而qiankun不需要,乾坤只需要你傳入響應(yīng)的apps的配置即可,會幫助我們?nèi)ゼ虞d。
Qiankun
Qiankun的優(yōu)勢
- 基于 single-spa[1] 封裝,提供了更加開箱即用的 API。
- 技術(shù)棧無關(guān),任意技術(shù)棧的應(yīng)用均可 使用/接入,不論是 React/Vue/Angular/JQuery 還是其他等框架。
- HTML Entry 接入方式,讓你接入微應(yīng)用像使用 iframe 一樣簡單。
- 樣式隔離,確保微應(yīng)用之間樣式互相不干擾。
- JS 沙箱,確保微應(yīng)用之間 全局變量/事件 不沖突。
- 資源預(yù)加載,在瀏覽器空閑時(shí)間預(yù)加載未打開的微應(yīng)用資源,加速微應(yīng)用打開速度。
基座配置
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'reactApp',
entry: '//localhost:3000',
container: '#container',
activeRule: '/app-react',
},
{
name: 'vueApp',
entry: '//localhost:8080',
container: '#container',
activeRule: '/app-vue',
},
{
name: 'angularApp',
entry: '//localhost:4200',
container: '#container',
activeRule: '/app-angular',
},
]);
// 啟動 qiankun
start();
復(fù)制代碼子應(yīng)用配置
以 create react app 生成的 react 16 項(xiàng)目為例,搭配 react-router-dom 5.x。
1.在 src 目錄新增 public-path.js,解決子應(yīng)用掛載時(shí),訪問靜態(tài)資源沖突
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
復(fù)制代碼2.設(shè)置 history 模式路由的 base:
復(fù)制代碼
3.入口文件 index.js 修改,為了避免根 id #root 與其他的 DOM 沖突,需要限制查找范圍。
import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
function render(props) {
const { container } = props;
ReactDOM.render(, container ? container.querySelector('#root') :
document.querySelector('#root'));
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('[react16] react app bootstraped');
}
export async function mount(props) {
console.log('[react16] props from main framework', props);
render(props);
}
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') :
document.querySelector('#root'));
}
復(fù)制代碼
4.修改 webpack 配置
安裝插件 @rescripts/cli,當(dāng)然也可以選擇其他的插件,例如 react-app-rewired。
npm i -D @rescripts/cli
復(fù)制代碼
根目錄新增 .rescriptsrc.js:
const { name } = require('./package');
module.exports = {
webpack: (config) => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.globalObject = 'window';
return config;
},
devServer: (_) => {
const config = _;
config.headers = {
'Access-Control-Allow-Origin': '*',
};
config.historyApiFallback = true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;
return config;
},
};
復(fù)制代碼以上對Qiankun的使用可以看出,與single-spa使用過程很相似。不同的是,Qiankun的使用過程更簡便了。一些內(nèi)置的操作交由給Qiankun內(nèi)部實(shí)現(xiàn)。這是一種IOC思想的實(shí)現(xiàn),我們只管面向容器化開發(fā),其他操作交給Qiankun框架管理。
Micro-app
micro-app并沒有沿襲single-spa的思路,而是借鑒了WebComponent的思想,通過CustomElement結(jié)合自定義的ShadowDom,將微前端封裝成一個(gè)類WebComponent組件,從而實(shí)現(xiàn)微前端的組件化渲染。并且由于自定義ShadowDom的隔離特性,micro-app不需要像single-spa和qiankun一樣要求子應(yīng)用修改渲染邏輯并暴露出方法,也不需要修改webpack配置,是目前市面上接入微前端成本最低的方案。
WebComponent的概念
**WebComponent**[2]是HTML5提供的一套自定義元素的接口,**WebComponent**[3]是一套不同的技術(shù),允許您創(chuàng)建可重用的定制元素(它們的功能封裝在您的代碼之外)并且在您的 web 應(yīng)用中使用它們。以上是MDN社區(qū)對WebComponent的解釋。
- Custom elements(自定義元素): 一組 JavaScript API,允許您定義 custom elements 及其行為,然后可以在您的用戶界面中按照需要使用它們。
- Shadow DOM(影子 DOM) :一組 JavaScript API,用于將封裝的“影子”DOM 樹附加到元素(與主文檔 DOM 分開呈現(xiàn))并控制其關(guān)聯(lián)的功能。通過這種方式,您可以保持元素的功能私有,這樣它們就可以被腳本化和樣式化,而不用擔(dān)心與文檔的其他部分發(fā)生沖突。
- HTML templates(HTML 模板): 和
元素使您可以編寫不在呈現(xiàn)頁面中顯示的標(biāo)記模板。然后它們可以作為自定義元素結(jié)構(gòu)的基礎(chǔ)被多次重用。
接下來用一個(gè)小例子更快來理解WebComponent的概念。
一個(gè)存在組件內(nèi)交互的WebComponent
// 基于HTMLElement自定義組件元素
class CounterElement extends HTMLElement {
// 在構(gòu)造器中生成shadow節(jié)點(diǎn)
constructor() {
super();
this.counter = 0;
// 打開影子節(jié)點(diǎn)
// 影子節(jié)點(diǎn)是為了隔離外部元素的影響
const shadowRoot = this.attachShadow({ mode: 'open' });
// 定義組件內(nèi)嵌樣式
const styles = `
#counter-increment {
width: 60px;
height: 30px;
margin: 20px;
background: none;
border: 1px solid black;
}
`;
// 定義組件HTMl結(jié)構(gòu)
shadowRoot.innerHTML = `
Counter
Button
; 0 ;
`;
// 獲取+號按鈕及數(shù)值內(nèi)容
this.incrementButton = this.shadowRoot.querySelector('#counter-increment');
this.counterValue = this.shadowRoot.querySelector('#counter-value');
// 實(shí)現(xiàn)點(diǎn)擊組件內(nèi)事件驅(qū)動
this.incrementButton.addEventListener("click", this.decrement.bind(this));
}
increment() {
this.counter++
this.updateValue();
}
// 替換counter節(jié)點(diǎn)內(nèi)容,達(dá)到更新數(shù)值的效果
updateValue() {
this.counterValue.innerHTML = this.counter;
}
}
// 在真實(shí)dom上,生成自定義組件元素
customElements.define('counter-element', CounterElement);
復(fù)制代碼
有了對WebComponent的理解,接下來,我們更明白了Micro-app的優(yōu)勢。
micro-app的優(yōu)勢
d879637b4bb34253.png
- 使用簡單
我們將所有功能都封裝到一個(gè)類WebComponent組件中,從而實(shí)現(xiàn)在基座應(yīng)用中嵌入一行代碼即可渲染一個(gè)微前端應(yīng)用。
同時(shí)micro-app還提供了js沙箱、樣式隔離、元素隔離、預(yù)加載、數(shù)據(jù)通信、靜態(tài)資源補(bǔ)全等一系列完善的功能。
- 零依賴
micro-app沒有任何依賴,這賦予它小巧的體積和更高的擴(kuò)展性。
- 兼容所有框架
為了保證各個(gè)業(yè)務(wù)之間獨(dú)立開發(fā)、獨(dú)立部署的能力,micro-app做了諸多兼容,在任何技術(shù)框架中都可以正常運(yùn)行。
基座的簡易配置
基座存在預(yù)加載子應(yīng)用、父子應(yīng)用通信、公共文件共享等等:
// index.js
import React from "react"
import ReactDOM from "react-dom"
import App from './App'
import microApp from '@micro-zoe/micro-app'
const appName = 'my-app'
// 預(yù)加載
microApp.preFetch([
{ name: appName, url: 'xxx' }
])
// 基座向子應(yīng)用數(shù)據(jù)通信
microApp.setData(appName, { type: '新的數(shù)據(jù)' })
// 獲取指定子應(yīng)用數(shù)據(jù)
const childData = microApp.getData(appName)
microApp.start({
// 公共文件共享
globalAssets: {
js: ['js地址1', 'js地址2', ...], // js地址
css: ['css地址1', 'css地址2', ...], // css地址
}
})
復(fù)制代碼
分配一個(gè)路由給子應(yīng)用:
// router.js
import { BrowserRouter, Switch, Route } from 'react-router-dom'
export default function AppRoute () {
return (
)
}
復(fù)制代碼
子應(yīng)用的簡易配置:
// index.js
import React from "react"
import ReactDOM from "react-dom"
import App from './App'
import microApp from '@micro-zoe/micro-app'
const appName = 'my-app'
// 子應(yīng)用運(yùn)行時(shí),切換靜態(tài)資源訪問路徑
if (window.__MICRO_APP_ENVIRONMENT__) {
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}
// 基子應(yīng)用向基座發(fā)送數(shù)據(jù)
// dispatch只接受對象作為參數(shù)
window.microApp.dispatch({ type: '子應(yīng)用發(fā)送的數(shù)據(jù)' })
// 獲取基座數(shù)據(jù)
const data = window.microApp.getData() // 返回基座下發(fā)的data數(shù)據(jù)
//性能優(yōu)化,umd模式
// 如果子應(yīng)用渲染和卸載不頻繁,那么使用默認(rèn)模式即可,如果子應(yīng)用渲染和卸載非常頻繁建議使用umd模式
// 將渲染操作放入 mount 函數(shù) -- 必填
export function mount() {
ReactDOM.render(, document.getElementById("root"))
}
// 將卸載操作放入 unmount 函數(shù) -- 必填
export function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById("root"))
}
// 微前端環(huán)境下,注冊mount和unmount方法
if (window.__MICRO_APP_ENVIRONMENT__) {
window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
} else {
// 非微前端環(huán)境直接渲染
mount()
}
復(fù)制代碼
設(shè)置子應(yīng)用路由
import { BrowserRouter, Switch, Route } from 'react-router-dom'
export default function AppRoute () {
return (
// 設(shè)置基礎(chǔ)路由,子應(yīng)用可以通過window.__MICRO_APP_BASE_ROUTE__獲取基座下發(fā)的baseroute,
// 如果沒有設(shè)置baseroute屬性,則此值默認(rèn)為空字符串
...
)
}
復(fù)制代碼以上便是Micro-app的用法
Module Federation
Module Federation是Webpack5提出的概念,module federation用來解決多個(gè)應(yīng)用之間代碼共享的問題,讓我們更加優(yōu)雅的實(shí)現(xiàn)跨應(yīng)用的代碼共享。
MF想做的事和微前端想解決的問題是類似的,把一個(gè)應(yīng)用進(jìn)行拆分成多個(gè)應(yīng)用,每個(gè)應(yīng)用可獨(dú)立開發(fā),獨(dú)立部署,一個(gè)應(yīng)用可以動態(tài)加載并運(yùn)行另一個(gè)應(yīng)用的代碼,并實(shí)現(xiàn)應(yīng)用之間的依賴共享。
為了實(shí)現(xiàn)這樣的功能, MF在設(shè)計(jì)上提出了這幾個(gè)核心概念。
Container
一個(gè)被 ModuleFederationPlugin 打包出來的模塊被稱為 Container。通俗點(diǎn)講就是,如果我們的一個(gè)應(yīng)用使用了 ModuleFederationPlugin 構(gòu)建,那么它就成為一個(gè) Container,它可以加載其他的 Container,可以被其他的 Container 所加載。
Host&Remote
從消費(fèi)者和生產(chǎn)者的角度看 Container,Container 又可被稱作 Host 或 Remote。
Host:消費(fèi)方,它動態(tài)加載并運(yùn)行其他 Container 的代碼。
Remote:提供方,它暴露屬性(如組件、方法等)供 Host 使用
可以知道,這里的 Host 和 Remote 是相對的,因?yàn)?一個(gè) Container 既可以作為 Host,也可以作為 Remote。
Shared
一個(gè) Container 可以 Shared 它的依賴(如 react、react-dom)給其他 Container 使用,也就是共享依賴。
微信圖片_20220626184254.png
微信圖片_20220626184305.png
以上是webpack5與之前版本的模塊管理對比圖
微應(yīng)用配置
通過webpack5的配置達(dá)成微應(yīng)用的效果
// 配置webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
new ModuleFederationPlugin({
name: "appA",
//出口文件
filename: "remoteEntry.js",
//暴露可訪問的組件
exposes: {
"./input": "./src/input",
},
//或者其他模塊的組件
//如果把這一模塊當(dāng)作基座模塊的話,
//這里應(yīng)該配置其他子應(yīng)用模塊的入口文件
remotes: {
appB: "appB@http://localhost:3002/remoteEntry.js",
},
//共享依賴,其他模塊不需要再次下載,便可使用
shared: ['react', 'react-dom'],
})
復(fù)制代碼
以上便是我對微應(yīng)用架構(gòu)的理解,以及微應(yīng)用架構(gòu)技術(shù)的演變過程。不難看出,這些技術(shù)的演變都朝著易用性和可拓展性的方向演進(jìn)。其中技術(shù)也有其時(shí)代的局限性,不過思想和技術(shù)總是在不斷進(jìn)步的。這幾類技術(shù)選型都有其優(yōu)缺點(diǎn),各有千秋,我們可以根據(jù)不同的需要選擇不同的技術(shù)來構(gòu)建應(yīng)用。
網(wǎng)站欄目:微前端架構(gòu)的幾種技術(shù)選型
標(biāo)題網(wǎng)址:http://www.dlmjj.cn/article/coogjgd.html


咨詢
建站咨詢
