模块联合、React 和 Typescript 微前端简介
微前端是目前互联网上最热门的话题之一。我们经常听到这个词,但什么是微前端呢?想象一下,一个网站包含许多组件,例如导航栏、页脚、主容器和侧边菜单。如果它们由不同的域名提供服务,会发生什么?是的,你猜对了,我们最终会得到一个微前端。现在,得益于微前端技术,我们可以分别处理这些应用程序。我们可以分别编写它们的单元测试、端到端测试,甚至可以使用不同的框架,例如 Angular、Vue 和 Svelte。
目前有两个主要参与者可以实现这些目标,其中一个是模块联合,另一个是单 SPA,我在这里介绍过:🔗使用单 SPA 将 CRA 迁移到微前端。
与单 SPA不同,模块联合的约束少得多。在模块联合中,您可以随心所欲地构建项目,而在单 SPA 中,您需要设置一个配置文件,并围绕该文件构建项目。
微前端只有一件事令人担忧,那就是配置。初始配置很容易让人望而却步,因为您需要整合很多组件,如果这是您的第一次尝试,在没有指导的情况下,很容易迷失方向。
工作示例
这是一个 POC(概念验证)项目,它可能看起来不太好,但这不是我们的重点。
模块联合
模块联合实际上是 Webpack 配置的一部分。此配置使我们能够将 CRA 的不同部分公开或接收给另一个 CRA 项目。
这些独立的项目之间不应存在依赖关系,因此它们可以单独开发和部署。
让我们首先创建我们的Container
项目,该项目导出其他两个应用程序APP-1
和APP-2
。
npx create-react-app container --template typescript
容器应用
项目结构
container
├─ package.json
├─ public
│ ├─ index.dev.html
│ └─ index.prod.html
├─ src
│ ├─ App.tsx
│ ├─ bootstrap.tsx
│ └─ index.ts
├─ tsconfig.json
├─ webpack.config.js
├─ webpack.prod.js
└─ yarn.lock
让我们添加依赖项
yarn add html-webpack-plugin serve ts-loader webpack webpack-cli webpack-dev-server
我们需要做一些修改。创建一个名为 的文件bootstrap.tsx
,并将其移入index.ts
其中bootstrap.tsx
。
bootstrap.tsx
import App from './App';
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
并将它们添加到index.ts
索引.ts
import('./bootstrap');
export {};
最后,将这些添加到app.tsx
以后使用。我们稍后再讨论。
应用程序.tsx
import React from 'react';
//@ts-ignore
import CounterAppTwo from 'app2/CounterAppTwo';
//@ts-ignore
import CounterAppOne from 'app1/CounterAppOne';
export default () => (
<div style={{ margin: '20px' }}>
<React.Suspense fallback="Loading header...">
<div
style={{
border: '1px dashed black',
height: '50vh',
display: 'flex',
justifyContent: 'space-around',
alignItems: 'center',
flexDirection: 'column',
}}
>
<h1>CONTAINER</h1>
<div
style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-around',
}}
>
<div
style={{
marginRight: '2rem',
padding: '2rem',
border: '1px dashed black',
}}
>
<h2>APP-1</h2>
<CounterAppOne />
</div>
<div style={{ border: '1px dashed black', padding: '2rem' }}>
<h2>APP-2</h2>
<CounterAppTwo />
</div>
</div>
</div>
</React.Suspense>
</div>
);
我们已经完成了组件部分,接下来是关键部分。我们需要设置我们的容器应用 Webpack 来接收app-1
和app-2
。
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
const deps = require('./package.json').dependencies;
module.exports = {
entry: './src/index.ts',
mode: 'development',
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: 3000,
},
output: {
publicPath: 'http://localhost:3000/',
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
},
module: {
rules: [
{
test: /\.(js|jsx|tsx|ts)$/,
loader: 'ts-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'container',
library: { type: 'var', name: 'container' },
remotes: {
app1: 'app1',
app2: 'app2',
},
shared: {
...deps,
react: { singleton: true, eager: true, requiredVersion: deps.react },
'react-dom': {
singleton: true,
eager: true,
requiredVersion: deps['react-dom'],
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.dev.html',
}),
],
};
按如下方式更新您的package.json
脚本:
"scripts": {
"start": "webpack serve --open",
"build": "webpack --config webpack.prod.js",
"serve": "serve dist -p 3002",
"clean": "rm -rf dist"
}
更新您的tsconfig
如下内容:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"jsx": "react-jsx"
},
"include": ["src"]
}
最重要的考虑因素是ModuleFederationPlugin
。我们指定name
模块,并remotes
从项目外部接收数据。并设置共享依赖项以便立即使用。
不要弄乱远程名称。如果名称设置不正确,项目将无法编译。
最后一步是编辑index.html
。
<html>
<head>
<script src="http://localhost:3001/remoteEntry.js"></script>
<script src="http://localhost:3002/remoteEntry.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
在这里,我们添加具有相应端口的遥控器。
现在我们的容器应用已经准备就绪,我们需要设置app-1
和app-2
,并公开<Counter />
组件。步骤基本相同,我们将设置bootstrap.tsx
和webpack.config.js
。Webpack
配置中只有细微的变化。
应用-1
项目结构
├─ package.json
├─ public
│ └─ index.html
├─ README.md
├─ src
│ ├─ App.tsx
│ ├─ bootstrap.tsx
│ ├─ components
│ │ └─ CounterAppOne.tsx
│ └─ index.ts
├─ tsconfig.json
├─ webpack.config.js
├─ webpack.prod.js
└─ yarn.lock
让我们添加依赖项
npx create-react-app app-1 --template typescript
yarn add html-webpack-plugin serve ts-loader webpack webpack-cli webpack-dev-server
就像我们在容器应用程序中所做的那样,我们将设置bootstrap.tsx
,index.ts
和app.tsx
。
bootstrap.tsx
import App from './App';
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
并将它们添加到index.ts
索引.ts
import('./bootstrap');
export {};
最后,将这些添加到app.tsx
以后使用。我们稍后再讨论。
应用程序.tsx
import React from 'react';
import CounterAppOne from './components/CounterAppOne';
const App = () => (
<div style={{ margin: '20px' }}>
<div>APP-1 - S4 </div>
<div>
<CounterAppOne />
</div>
</div>
);
export default App;
现在我们将创建<Counter />
组件,稍后我们将在 webpack 配置中将其公开给容器。
组件 > CounterAppOne.tsx
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>
Add by one each click <strong>APP-1</strong>
</p>
<p>Your click count: {count} </p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
};
export default Counter;
我们基本上已经完成了,只需要添加 webpack 配置。
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
const deps = require('./package.json').dependencies;
module.exports = {
entry: './src/index.ts',
mode: 'development',
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: 3001,
},
output: {
publicPath: 'http://localhost:3001/',
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
},
module: {
rules: [
{
test: /\.(js|jsx|tsx|ts)$/,
loader: 'ts-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'app1',
library: { type: 'var', name: 'app1' },
filename: 'remoteEntry.js',
exposes: {
// expose each component
'./CounterAppOne': './src/components/CounterAppOne',
},
shared: {
...deps,
react: { singleton: true, eager: true, requiredVersion: deps.react },
'react-dom': {
singleton: true,
eager: true,
requiredVersion: deps['react-dom'],
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
按如下方式更新您的package.json
脚本:
"scripts": {
"start": "webpack serve --open",
"build": "webpack --config webpack.prod.js",
"serve": "serve dist -p 3001",
"clean": "rm -rf dist"
}
更新您的tsconfig
如下内容:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"jsx": "react-jsx"
},
"include": ["src"]
}
编辑index.html
。
<html>
<head> </head>
<body>
<div id="root"></div>
</body>
</html>
此配置有一些不同。我们设置了不同的端口,公开了我们的应用而不是远程访问,并且我们有一个叫做filename
where 的配置,用于将我们的
模块公开给不同的模块。记住,我们<script src="http://local
host:3001/remoteEntry.js"></script>
在容器中添加了 index.html。这就是container
我们查找 的地方app-1
。
这里的重要事项:
- 名称:“app1”
- 文件名:'remoteEntry.js'
- 暴露
暴露错误的路径很可能导致编译失败。设置错误的名称也会导致问题,因为如果找不到container
,查找 就会失败。app-1
应用-2
项目结构
├─ package.json
├─ public
│ └─ index.html
├─ README.md
├─ src
│ ├─ App.tsx
│ ├─ bootstrap.tsx
│ ├─ components
│ │ └─ CounterAppTwo.tsx
│ └─ index.ts
├─ tsconfig.json
├─ webpack.config.js
├─ webpack.prod.js
└─ yarn.lock
App-2 基本相同。创建一个新的 React 项目,执行上述所有操作,然后添加<CounterAppTwo />
并webpack
配置即可。
组件 > CounterAppTwo
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(1);
return (
<div>
<p>
Multiply by two each click <strong>APP-2</strong>
</p>
<p>Your click count: {count}</p>
<button onClick={() => setCount((prevState) => prevState * 2)}>Click me</button>
</div>
);
};
export default Counter;
webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
const deps = require('./package.json').dependencies;
module.exports = {
entry: './src/index.ts',
mode: 'development',
devServer: {
contentBase: path.join(__dirname, 'dist'),
port: 3002,
},
output: {
publicPath: 'http://localhost:3002/',
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
},
module: {
rules: [
{
test: /\.(js|jsx|tsx|ts)$/,
loader: 'ts-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new ModuleFederationPlugin({
name: 'app2',
library: { type: 'var', name: 'app2' },
filename: 'remoteEntry.js',
exposes: {
// expose each component
'./CounterAppTwo': './src/components/CounterAppTwo',
},
shared: {
...deps,
react: { singleton: true, eager: true, requiredVersion: deps.react },
'react-dom': {
singleton: true,
eager: true,
requiredVersion: deps['react-dom'],
},
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
按如下方式更新您的package.json
脚本:
"scripts": {
"start": "webpack serve --open",
"build": "webpack --config webpack.prod.js",
"serve": "serve dist -p 3002",
"clean": "rm -rf dist"
}
更新您的tsconfig
如下内容:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"jsx": "react-jsx"
},
"include": ["src"]
}
编辑index.html
。
<html>
<head> </head>
<body>
<div id="root"></div>
</body>
</html>
现在进入每个项目并运行,yarn start
然后导航到 localhost:3000。如果你打开
开发者控制台的“源”选项卡,你会看到每个应用来自不同的端口。
围捕
优点
- 更易于维护
- 更容易测试
- 独立部署
- 提高团队的可扩展性
缺点
- 需要大量配置
- 如果其中一个项目崩溃也可能影响其他微前端
- 在开发过程中,后台运行多个项目
本质上,它非常简单,将一堆应用整合到同一个网站,并由不同的服务器提供服务。如果你正在处理庞大的代码库,那么这绝对是一项非常棒的技术
。将庞大的组件解耦成小型应用,感觉轻而易举。我希望我鼓励你尝试一下微前端。