最近这段时间微前端
这个概念愈来愈被说起,它采用了微服务
的相关理念,咱们能够把一个应用拆分红多个能够互不依赖能够独立开发并单独部署的模块,而后在运行时把它们组合成一个完整的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~