Module Federation 最佳實踐
Module Federation[1] 官方稱爲模塊聯邦
,模塊聯邦是webpack5
支持的一個最新特性,多個獨立構建的應用,可以組成一個應用,這些獨立的應用不存在依賴關係,可以獨立部署,官方稱爲微前端
。
什麼模塊聯邦
,微前端
,瞬間高大上了,但是官方那解釋和示例似乎看起來還是似懂非懂。
正文開始...
在閱讀本文前,本文將會從以下幾點去探討MDF
-
爲什麼會有
MDF
-
MDF
給我解決了什麼樣的問題 -
MDF
在多個應用中如何使用 -
寫了一個例子感受
MDF
的強大
爲什麼會有 Module Federation
我們先看一下圖
在以前,我們每一個項目都會是一個獨立的倉庫,一個獨立項目,一個獨立的應用,多個項目應用之間都是互相獨立,獨立構建,獨立部署。
現在假設application-a
項目有一個組件是Example
, 假設application-b
中也有一個組件需要這個組件Example
我們之前的做法就是把a
項目的Example
拷貝到b
項目中,如果這個Example
組件有依賴第三方插件,那麼我們在b
項目也需要安裝對應的第三方插件,而且有一種場景,就是哪天這個Example
組件需要更新了,那麼兩個應用得重複修改兩次。
於是你想到另外一種方案,我是不是可以把這個獨立的組件可以抽象成一個獨立的組件倉庫,用npm
去管理這個組件庫,而且這樣有組件的版本控制,看起來是一種非常不錯的辦法。
但是...,請看下面,MDF 解決的問題
MDF 解決的問題
webpack5
升級了,module Federation
允許一個應用可以動態的加載另一個應用的代碼,而且共享依賴項
現在就變成了一個項目 A 中可以動態加載項目 B,項目 B 也可以動態加載項目 A,A 應用的任何應用可以通過MFD
共享給其他應用使用。
我們可以用下面一張圖理解下
甚至你可以把B
應用利用模塊聯邦導出,在A
應用中使用。
現在終於明白爲啥會有module federation
了吧,本質上就是多個獨立的應用之間,可以相互引用,可以減少重複的代碼,更好的維護多個應用。我在 A 項目寫的一個組件,我發現 B 項目也有用,那麼我可以把這個組件共享給 B 使用。而不是 cv 操作,或者把這個組件搞個獨立 npm 倉庫 (這也是一種比較可靠的方案)
舉個栗子
新建一個目錄module-federation
, 然後新建一個packages
目錄,對應的目錄結構如下
|---packages
|
|-----application-a
|---src
|---App.jsx
|---app.js
|---public
|---index.js
|---...
|---package.json
|-----application-b
|---...
|----package.json
wsrun
我們在application-a
與application-b
中新建一個package.json
, 我們使用一個工具wsrun
,可以批量啓動或者打包多個應用
{
"name": "module-federation",
"version": "1.0.0",
"description": "模塊聯邦demo測試",
"main": "index.js",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "wsrun --parallel start",
"build": "yarn workspaces run build",
"dev": "wsrun --parallel dev"
},
"keywords": [],
"author": "maicFir",
"license": "ISC",
"devDependencies": {
"wsrun": "^5.2.4"
}
}
在application-a
應用中,我們主要看下以下幾個文件
- package.json
{
"name": "application_a",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack server --port=8081 --open",
"build": "webpack"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/preset-react": "^7.17.12",
"babel-loader": "^8.2.5",
"html-webpack-plugin": "^5.5.0",
"webpack": "^5.73.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.9.3"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
- webpack.config.js
// application-a/webpack.config.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 引入moduleFed插件
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const { dependencies } = require("./package.json");
module.exports = {
mode: 'development',
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js'
},
resolve: {
extensions: ['.jsx', '.js', '.json'],
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
options: {
presets: ['@babel/env']
}
}
]
},
plugins: [
new ModuleFederationPlugin({
name: 'application_a',
library: { type: 'var', name: 'application_a' },
// 另外一個應用html中引入的模塊聯邦入口文件
filename: 'remoteEntry.js',
// 選擇暴露當前應用需要給外部使用的組件,供其他應用使用
exposes: {
'./Example': './src/compments/Example',
},
// 這裏是選擇關聯其他應用的組件
remotes: {
'application_b': 'application_b',
},
// react react-dom會獨立分包加載
shared: {
...dependencies,
react: {
singleton: true,
requiredVersion: dependencies["react"],
},
"react-dom": {
singleton: true,
requiredVersion: dependencies["react-dom"],
},
}
// shared: ['react', 'react-dom'], 這樣會error
}),
new HtmlWebpackPlugin({
template: './public/index.html'
}),
// 熱加載
new webpack.HotModuleReplacementPlugin()
],
devServer: {
hot: true
}
};
我們在看下入口entry
文件
// application-a/index.js
import('./src/app.js')
app.js
// application-a/src/app.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
const appDom = document.getElementById('app');
const root = createRoot(appDom);
root.render(<App />);
在App.jsx
// application-a/src/App.jsx
import React from 'react';
// 引入application_b應用的Example,Example2組件
// import Example1 from 'application_b/Example';
// import Example2 from 'application_b/Example2';
//or
const Example1 = React.lazy(() => import('application_b/Example'));
const Example2 = React.lazy(() => import('application_b/Example2'));
function App() {
return (
<div>
<p>this is applicatin a</p>
<Example1 />
<Example2 />
</div>
);
}
export default App;
Example.jsx
// application-a/src/compments/Example.jsx
import React from 'react';
export default function Example1() {
return <h1>我是A應用的一個組件-example1</h1>;
}
至此我們application-a
這個項目已經 ok 了
我們再看下application-b
// application-b/webpack.config.js
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 引入moduleFederation
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const { dependencies } = require("./package.json");
module.exports = {
mode: 'development',
entry: './index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js'
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
options: {
presets: ['@babel/env']
}
}
]
},
resolve: {
extensions: ['.jsx', '.js', '.json'],
},
plugins: [
new ModuleFederationPlugin({
name: 'application_b',
library: { type: 'var', name: 'application_b' },
filename: 'remoteEntry.js',
// 當前組件需要暴露出去的組件
exposes: {
'./Example': './src/compments/Example',
'./Example2': './src/compments/Example2',
},
// 關聯需要引入的其他應用
remotes: {
'application_a': 'application_a',
},
shared: {
...dependencies,
react: {
singleton: true,
requiredVersion: dependencies["react"],
},
"react-dom": {
singleton: true,
requiredVersion: dependencies["react-dom"],
},
},
// shared: ['react', 'react-dom'],
}),
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new webpack.HotModuleReplacementPlugin()
],
devServer: {
hot: true
}
};
我們在application-b/src/compments
新建了兩個組件
Example
import React from 'react';
export default function Example() {
return <h1>我是B應用-example1</h1>;
}
Example1
import React from 'react';
export default function Example2() {
return <h1>我是B應用-example2</h1>;
}
在webpack.config.js
中我們在exposes
中導出了,這樣能給其他應用使用
...
plugin: [
new ModuleFederationPlugin({
name: 'application_b',
library: { type: 'var', name: 'application_b' },
filename: 'remoteEntry.js',
exposes: {
'./Example': './src/compments/Example',
'./Example2': './src/compments/Example2',
},
...
}),
]
在 html 中引入remoteEntry.js
由於我需要在application-a
中使用application-b
暴露出來的組件
因此我需要在application-a
的模版頁面中引入
<!--application-a/public/index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta />
<title>application-a</title>
<script src="http://localhost:8082/remoteEntry.js"></script>
</head>
<body>
<div></div>
</body>
</html>
如果我需要在application-b
中需要application-a
中的組件,同樣需要引入
<!--application-b/public/index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta />
<title>application-b</title>
<script src="http://localhost:8081/remoteEntry.js"></script>
</head>
<body>
<div></div>
</body>
</html>
在根目錄下執行npm run start
, 注意子應用裏面的名字也必須是start
,相當於批量開啓應用
application-a
application-b
application-a
需要application-b
應用的兩個組件就已經無縫的應用到了自己應用中去
我們會發現,在application-a
應用共享出來的模塊,在application-b
中的要提前在html
中下載引入。
注意的一些問題
- exposes 使用錯誤
// error
exposes: {
'Example': './src/compments/Example',
},
這樣會導致在application-a
中的 Example` 無法使用
exposes: {
'./Example': './src/compments/Example',
/*
'./App': './src/App' // 這樣會報錯,另外一個應用引入會報錯
*/
},
另外exposes
只能暴露內部jsx
的組件,不能是js
文件,不能是整個App.jsx
應用。主要是App.jsx
有引用application-a
的引用
如果application-b
中,App.jsx
改成以下
import React from 'react';
function App() {
return (
<div>
<h3>hello application B</h3>
</div>
);
}
export default App;
那麼此時我可以把整個application-b
應用當成組件在application-a
中使用,但是得把當前應用暴露出去
// application-b/webpack.config.js
exposes: {
'./Example': './src/compments/Example',
'./Example2': './src/compments/Example2',
'./App': './src/App'
},
在application-a
的App.jsx
// application-a/src/App.jsx
import React from 'react';
// import Example1 from 'application_b/Example';
// import Example2 from 'application_b/Example2';
// or
const Example1 = React.lazy(() => import('application_b/Example'));
const Example2 = React.lazy(() => import('application_b/Example2'));
const AppFromB = React.lazy(() => import('application_b/App'));
function App() {
return (
<div>
<p>this is applicatin a</p>
<Example1 />
<Example2 />
<p>下面是從另外一個應用動態加載過來的</p>
<AppFromB></AppFromB>
</div>
);
}
export default App;
iframe
嵌套另外一個獨立的項目
- shared 提示版本問題
...
shared: ['react', 'react-dom'],
正確做法
const { dependencies } = require("./package.json");
...
shared: {
...dependencies,
react: {
singleton: true,
requiredVersion: dependencies["react"],
},
"react-dom": {
singleton: true,
requiredVersion: dependencies["react-dom"],
},
},
另外推薦幾篇關於MDF
的參考資料以及不錯的文章
-
ModuleFederationWebpack5[2]
-
how-to-use-webpack-module-federation[3]
-
module-federation-examples[4]
-
federated-libraries[5]
經本地不斷的測試,終於瞭解webpack5 MDF
的一些使用場景以及它在具體業務使用的可能性,更多關於 MDF 信息參考官方文檔 [6]
總結
-
瞭解
module federation
,官方解釋就是模塊聯邦
, 主要依賴內部 webpack 提供的一個插件ModuleFederationPlugin
, 可以將內部的組件共享給其他應用使用 -
MDF
解決了什麼樣的問題,允許一個應用 A 加載另外一個應用 B, 並且依賴共享,兩個獨立的應用之間互不影響 -
寫了一個例子,進一步理解
MDF
-
本文示例 code example[7]
參考資料
[1]module Federation: https://webpack.docschina.org/concepts/module-federation/
[2]ModuleFederationWebpack5: https://github.com/sokra/slides/blob/master/content/ModuleFederationWebpack5.md
[3]how-to-use-webpack-module-federation: https://betterprogramming.pub/how-to-use-webpack-module-federation-in-react-70455086b2b0
[4]module-federation-examples: https://github.com/module-federation/module-federation-examples
[5]federated-libraries: https://federated-libraries.vercel.app/get-started
[6] 官方文檔: https://webpack.docschina.org/plugins/module-federation-plugin/
[7]code example: https://github.com/maicFir/lessonNote/tree/master/webpack/webpack-14-module-federation
最後,看完覺得有收穫的,點個贊,在看,轉發,收藏等於學會,歡迎關注 Web 技術學苑,好好學習,天天向上!
Web 技術學苑 專注前端 web 技術、分享 web 技術
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/pT_tugg_EvE5pnMCaUqliw