最近這段時間微前端
這個概念愈來愈被說起,它採用了微服務
的相關理念,咱們能夠把一個應用拆分紅多個能夠互不依賴能夠獨立開發並單獨部署的模塊,而後在運行時把它們組合成一個完整的App。html
經過這樣的手段,咱們可使用不一樣的技術去開發應用的各個部分,好比這個模塊已經用React
開發好了咱們能夠繼續用React
,那個新模塊團隊更偏向於用Vue來實現咱們就能夠用Vue
去實現。咱們能夠有專門的團隊去維護各個獨立的模塊,維護起來也會更加方便。這樣咱們團隊協做的方式也就跟着改變了。前端
從Webpack5
開始,已經內置了對微前端開發的支持,它們提供了一個新的功能叫Module Federation
(我也不知道該怎麼翻譯這個術語會比較恰當),提供了足夠的能力來讓咱們實現微前端開發。react
話很少說,咱們仍是經過一個簡單的例子來感覺下總體的一個概念跟流程。咱們會實現一個簡單的App,而後把它經過webpack改形成微前端的形式。webpack
此次全部配置都由咱們來手動完成。首先咱們新建一個空白目錄,而後在項目裏面執行:git
npm init -y
複製代碼
而後爲了使用webpack,github
npm add webpack webpack-nano -D
複製代碼
接下來咱們就能夠經過在根目錄新建一個webpack.config.js
文件來配置整個打包過程啦!web
咱們在開發時跟運行時配置是有差異的,通常你們可能會編寫webpack.production.js
跟webpack.development.js
兩個文件,來配置不一樣的環境。但這樣可能會讓咱們的配置對象變得很大很臃腫不容易維護,咱們須要在一大堆配置中找到咱們想要的配置去修改,並且各個環境的配置也不是徹底不一樣,那咱們得封裝啊,咱們得抽象啊,咱們要想辦法複用啊!npm
咱們能不能把這個大的配置對象拆解成一個個具備特定功能的配置對象來單獨維護呢?json
好比咱們這個項目會經過mini-html-webpack-plugin
來生成最終的index.html
文件,那咱們就能夠寫一個單獨的函數來導出配置這個頁面的相關配置bootstrap
exports.page = ({title}) => ({
plugins: [new MiniHtmlWebpackPlugin({
context: {title}
})]
})
複製代碼
這樣後續咱們要改變頁面相關的配置時就咱們就會知道來修改這個page
函數,咱們甚至能夠替換成新的插件,而須要這個配置的地方只須要調用這個函數就能拿到配置,不須要關心細節,它們對咱們的變更是無感知的,天然也不會受到影響。咱們的配置也就能以函數的形式在各個環境中複用。
那麼問題來了,畢竟webpack最終仍是隻認它認識的那個配置形式,因此咱們還須要把這些函數返回的小配置對象合併成一個大的完整的配置對象。注意像Object.assign
這種處理方式對數組不太友好,會丟失數據,你們能夠本身實現相關邏輯,或者使用webpack-merge
這個包來處理。
爲了更好地管理webpack配置,不讓複雜的配置花了眼,咱們能夠再新建一個webpack.parts.js
文件,在這裏定義一個個小函數來返回配置特定功能的配置對象。
而後在webpack.config.js
裏面,咱們能夠導入這些函數,而且咱們能夠經過運行時傳過來的mode
來判斷須要給什麼環境打包,動態生成最後的配置:
const {mode} = require('webpack-nano/argv')
const parts = require('./webpack.parts')
const {merge} = require('webpack-merge')
const commonConfig = merge([
{mode},
{entry: ["./App"]},
parts.page({title: 'React Micro-Frontend'}),
parts.loadJavaScript()
])
const productionConfig = merge([parts.eliminateUnusedCss()])
const developmentConfig = merge([{entry: ['webpack-plugin-serve/client']}, parts.devServer()])
const getConfig = (mode) => {
process.env.NODE_ENV = mode
switch (mode) {
case 'production':
return merge([commonConfig, productionConfig])
case 'development':
return merge([commonConfig, developmentConfig])
default:
throw new Error(`Trying to use an unknown mode, ${mode}`);
}
}
module.exports = getConfig(mode)
複製代碼
這最大限度地避免了咱們配置文件的臃腫。
而後咱們還須要配置咱們的開發環境,咱們固然不想在開發時每次都手動去刷新頁面,這邊用到了一個插件webpack-plugin-serve
來作實時更新:
exports.devServer = () => ({
watch: true,
plugins: [
new WebpackPluginServe(
{
port: Process.env.PORT || 8000,
host: '127.0.0.1',
static: './dist',
liveReload: true,
waitForBuild: true
})
]
})
複製代碼
而後咱們這邊使用了React
做爲前端框架:
npm add react react-dom
複製代碼
爲了讓編譯器可以正確理解咱們的React
組件,咱們要使用babel
:
npm add babel-loader @babel/core @babel/preset-env @babel/preset-react -D
複製代碼
配置一下babel-loader
:
exports.loadJavaScript = () => ({
module: {
rules: [
{ test: /\.js$/, include: APP_SOURCE, use: "babel-loader" },
],
},
});
複製代碼
別忘了還要增長一個.babelrc
文件
{
"presets": [
[
"@babel/preset-env",
{
"modules": false
}
],
[
"@babel/preset-react"
]
]
}
複製代碼
如今咱們的React
組件能被正確處理了,咱們能夠開始寫咱們的組件了。
首先是咱們的Header
組件:
import React from "react";
const Header = () => {
return <header>
<h1>Micro-Frontend With React</h1>
</header>
}
export default Header;
複製代碼
而後是咱們的Main
組件:
import React from "react";
import Header from "./Header";
const Main = () => {
return (
<main>
<Header/>
<span>a Demo for Micro-Frontend using Webpack5</span>
</main>
);
}
export default Main
複製代碼
最後是入口文件:
import ReactDOM from "react-dom";
import React from "react";
import Main from "./Main";
const container = document.createElement("div");
document.body.appendChild(container);
ReactDOM.render(<Main/>, container);
複製代碼
打開package.json
文件配置以下腳本:
"scripts": {
"build": "wp --mode production",
"start": "wp --mode development"
}
複製代碼
如今咱們能夠經過在終端執行npm run start
來預覽咱們的App了。
接下來咱們來把它改形成微前端的形式,把Header
作成單獨的模塊,而後其它的作成另一個模塊,這時候就要用到ModuleFederationPlugin
了。
首先咱們要配置這個插件:
const {ModuleFederationPlugin} = require("webpack").container;
exports.federateModule = ({
name,
filename,
exposes,
remotes,
shared,
}) => ({
plugins: [
new ModuleFederationPlugin({
name,
filename,
exposes,
remotes,
shared,
}),
],
});
複製代碼
其中name
是惟一ID,用於標記當前服務,filename
是提供給其餘服務加載的文件,exposes
則是須要暴露的模塊,remotes
指定要使用的其它服務,shared則是配置公共模塊(好比lodash
這種)
exposes
選項的表示當前應用是一個 Remote
,exposes
內的模塊能夠被其餘的 Host
引用,引用方式爲import(${name}/${expose})
。remotes
選項的表示當前應用是一個 Host
,能夠引用 remote
中 expose
的模塊。咱們要在webpack.config.js
裏面配置這兩個模塊:
const componentConfig = {
App: merge(
{
entry: [path.join(__dirname, "src", "bootstrap.js")],
},
parts.page({title: 'React Micro-Frontend'}),
parts.federateModule({
name: "app",
remotes: {mf: "mf@/mf.js"},
shared: sharedDependencies,
})
),
Header: merge(
{
entry: [path.join(__dirname, "src", "Header.js")],
},
parts.federateModule({
name: "mf",
filename: "mf.js",
exposes: {"./Header": "./src/Header"},
shared: sharedDependencies,
})
),
};
複製代碼
由於咱們爲了簡化代碼把全部代碼都寫在一個項目裏了,更常見的狀況是每一個模塊均可以有屬於本身的代碼倉庫,並且可使用不一樣的技術來實現。,這種狀況咱們處理的方式基本不變,引用遠程依賴時記得按照相似[name]@[protocol]://[domain]:[port][filename]的形式去指定remotes
就好。
那爲了模擬多個項目獨立編譯,咱們也是用了組件名來設置不一樣的配置,這邊對於Header
咱們並不想直接在瀏覽器中運行,而對於App
咱們想要在瀏覽器中看到完整的頁面,因此咱們把對頁面相關的配置移到對App
的配置中,這webpack.config.js
在動態生成配置對象時也須要接受一個組件名做爲參數了。
const {mode, component} = require('webpack-nano/argv')
...
const getConfig = (mode, component) => {
switch (mode) {
case 'production':
return merge([commonConfig, productionConfig, componentConfig[component]])
case 'development':
return merge([commonConfig, developmentConfig, componentConfig[component]])
default:
throw new Error(`Trying to use an unknown mode, ${mode}`);
}
}
複製代碼
而後咱們要在Main
裏修改引入Header
的路徑
import Header from "mf/Header";
複製代碼
最後是要經過一個引導文件bootstrap.js
來加載這一切
import("./App");
複製代碼
這是由於remote
暴露的js文件須要優先加載,若是App.js
不是異步的,在import Header
的時候,會依賴mf.js
,直接運行可能致使mf.js
還沒有加載完畢,因此會有問題。
經過network面板也能夠看出,mf.js
是先於 App.js
加載的,因此咱們的 App.js
必須是個異步邏輯。
經過npm run build -- --component Header
咱們先完成對Header
的編譯,而後再經過npm run start -- --component App
完成項目的運行,打開瀏覽器,應該能夠看到跟以前同樣的界面。
總的來講,這爲團隊協做代碼共享提供了新的方式,同時有一些侵入性,並且咱們的項目就得都依賴於webpack
了。我我的以爲沒啥問題,畢竟如今大部分項目都會用到webpack
,比較介意這一點的同窗能夠關注下vite
,vite
利用瀏覽器原生的模塊化能力來提供代碼共享的解決方案。今天咱們僅僅用Module Federation
實現了一個小demo,關於微前端
跟webpack的管理
都不是一篇文章就可以說得清楚的,還有不少事情能夠聊,我們後面再分別單獨展開講講,Happy coding~