本文正在参加「金石计划」
前言
工欲善其事必先利其器;想要在项目中推行微前端,先要进行技术选型,需要进行细致的调研;但是如果没有人告诉你有这些微前端解决方案,你用百度搜索恐怕一个星期也难以有一个结果;所以我来了,来帮你们梳理一下市面上的微前端解决方案;
Systemjs
Systemjs是一个模块加载方案,利用它我们就可以实现一个简单的微前端,没想到吧,哈哈!
下面开始操作:我们用一个html文件作为主应用,然后vue项目作为子应用;
我们的主应用有以下任务:
- 公共依赖以CDN导入
- 整个应用的路由管理
- Vue组件的挂载
我们的子应用就是每一个路由组件,其他什么也不用做,是不是很熟悉?我们只是把这些路由组件拆分成了子应用独立出去了
要使用Systemjs加载模块的话需要配置WebpackSystemRegister插件:
const { VueLoaderPlugin } = require("vue-loader");
const WebpackSystemRegister = require("webpack-system-register");
module.exports = {
entry: {
buylist: "./src/BuyList.vue",
},
module: {
rules: [
{
test: /\.css$/i,
use: ["vue-style-loader", "css-loader"],
},
{
test: /\.vue$/i,
use: "vue-loader",
},
],
},
plugins: [new VueLoaderPlugin(), new WebpackSystemRegister({})],
};
我们的BuyList组件:
<template>
<div>BuyList</div>
</template>
<script>
export default {
name: 'BuyList',
}
</script>
这种方式实现下来很简单,但是我们需要梳理一下开发流程:我们先开发子应用BuyList,此时需要单独为子应用起一个服务web-dev-server(这里我没有写,可以自行实现),开发完成之后打包将dist包上传到CDN,然后主项目去加载,这个时候我们还需要发布主项目然后查看效果;它最大的问题就是不能做到跨技术栈;我们继续探索其他的解决方案;
UMD
这是个一个国外大佬的微前端demo:github.com/micro-front…
除了infra我们不用管,先把公共依赖项目content run起来,然后把其他子项目服务起起来,最后起container主项目,我们主要看一下它是怎么实现的微前端,先看一下App组件这是应用入口,我们看到还是通过路由去分发各个子应用
const App = () => (
<BrowserRouter>
<React.Fragment>
<AppHeader />
<Switch>
<Route exact path="/" component={Browse} />
<Route exact path="/restaurant/:id" component={Restaurant} />
<Route exact path="/random" render={Random} />
<Route exact path="/about" render={About} />
</Switch>
</React.Fragment>
</BrowserRouter>
);
然后我们看一看MicroFrontend
的实现,它的总体流程是:
- fetch获取对应路由组件下的打包文件列表
manifest
,然后动态将打包出的js文件(统一都是main.js
)添加到DOM
上 - 当脚本加载完毕后执行子应用的
render
方法 - 如果下一次发现子应用脚本已经加载完毕,那么直接调用子应用
render
方法
子应用不直接渲染这个App,而是将render方法挂载到window
对象上:
window.renderRestaurant = (containerId, history) => {
ReactDOM.render(
<App history={history} />,
document.getElementById(containerId),
);
unregister();
};
window.unmountRestaurant = containerId => {
ReactDOM.unmountComponentAtNode(document.getElementById(containerId));
};
部署时我们需要将asset-manifest.json
部署到CDN上以便主应用访问,其他子应用只需要单独发包上传到CDN就可以了;这种方式类似于打成UMD包,让子应用全局可访问;这种方式也不能支持跨技术栈;
有大佬说过:不跨技术栈的微前端不算是好的微前端;
因此上面这两种简单的方案并不能根本地解决我们目前的痛点;但是有一个神器可以不用吹灰之力就解决跨技术栈的问题,有请iframe
闪亮登场;
iframe
还记得前后端不分离的时候,后端写页面就会用到很多iframe去切页面,这样还可以公用菜单,很方便;但是随着用户要求变高,这种方案逐渐废弃了,因为它的加载速度实在太慢了,与当前这个快餐时代明显不符;
但是开发者却很青睐iframe
,因为它太好用了,一个src
属性,什么都搞定了,一个页面可以内嵌无数个页面,nice
!后来iframe
成为所有微前端方案的终极目标:
像iframe一样使用微前端(没有接入成本),体验上超过iframe;
再参考一下上面两种方案,他们其实都利用了单页的路由,那么这就是一个优化方向,能不能把多页的切换优化为单页切换,这就是single-spa
single-spa
single-spa
优化了应用切换时的体验,同时也完善了子应用加载时的生命周期管理,为后面的微前端发展奠定了基础
但是还是无法完成我们的终极使命:跨技术栈、跨版本;于是出现了乾坤,它是基于single-spa开发的又一个微前端框架;
同时,跨技术栈、跨版本还有另外一个思路,那就是web-components,因为它与技术栈无关,天然地跨技术栈,能不能把我们的组件都转化为web-components去渲染呢?这当然是个好主意;对应的也有鹅厂的无界
qiankun
qinakun
的运行分为三部曲:
- 注册子应用
{
name:'sub-vue',
entry:'//localhost:8080',
container:"#app1",
activeRule:'/vue',
},
{
name:'sub-vue2',
entry:'//localhost:8081',
container:"#app2",
activeRule:'/vue2'
}
])
- 设置默认进入的应用
setDefaultMountApp('/vue');
- 启动应用
start();
子应用只需要暴露一些生命周期函数就行了:
export async function bootstrap() {
console.log('react app bootstraped');
}
export async function mount(props) {
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root'),
);
}
下面我们尝试将上面这个React
微前端改造为qiankun
:
- 先改造子应用,暴露生命周期函数,改造
webpack
配置,这里我们需要将create-react-app
改为craco
- 先执行主应用的render,然后在组件中预留一个
id=container
的子应用容器 - 注册子应用,设置默认应用,启动应用
下面我们来看看改造成本:右边是源代码,左边是改造后代码
主应用index.js
的对比如下:
主应用App.js
由于不需要路由了,改造点很多:
子应用改动则比较大:首先是index.js把之前的代码全部废弃,暴露子应用生命周期函数,并且如果是开发环境需要render一下
然后脚手架需要修改为craco
,并配置craco.config.js
:
const packageName = require('./package.json').name;
module.exports = {
webpack: {
configure: webpackConfig => {
webpackConfig.output = Object.assign({}, webpackConfig.output, {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
});
return webpackConfig;
},
},
};
还需要配置一个public-path
:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
wujie
正如无界官网所说,他是一个极致的微前端框架,那么我们接下来用它改造我们之前那个项目:
我们主应用是React
,直接引入WujieReact
组件,然后render一下:
ReactDOM.render(
<>
<App />
<WujieReact
width="100%"
height="100%"
name="browser"
url={'http://localhost:3001/'}
/>
</>,
document.getElementById('root'),
);
删除掉主应用的路由,子应用无需改造,这样就直接完成了,主应用+browser子应用,成本很低,的确如官网所说,但是我们的子应用样式丢失了
icestark
飞冰也是一种微前端解决方案,我们也试试它;
主应用改造:使用飞冰提供的路由代替原来的路由,每个路由对应一个子应用;这里一个关键问题是需要把所有子应用打包得到的css、js文件放到url中
ReactDOM.render(
<>
<App />
<AppRouter>
<AppRoute
activePath="/browser"
title="browse"
url={[
'//localhost:3001/static/js/bundle.js',
'//localhost:3001/static/js/0.chunk.js',
'//localhost:3001/static/js/main.chunk.js'
]}
/>
</AppRouter>
</>,
document.getElementById('root'),
);
子应用改造:增加几个函数导出生命周期
import { isInIcestark, setLibraryName } from '@ice/stark-app';
export function mount(props) {
ReactDOM.render(<App/>, props.container);
}
export function unmount(props) {
ReactDOM.unmountComponentAtNode(props.container);
}
// 注意:`setLibraryName` 的入参需要与 webpack 工程配置的 output.library 保持一致
setLibraryName('microApp');
if (!isInIcestark()) {
ReactDOM.render(<App />, document.getElementById('container'));
}
子应用也需要修改webpack配置:
module.exports = {
webpack: {
configure: webpackConfig => {
webpackConfig.output = Object.assign({}, webpackConfig.output, {
// 设置模块导出规范为 umd
libraryTarget: 'umd',
// 可选,设置模块在 window 上暴露的名称
library: 'microApp',
});
console.log("webpackConfig",webpackConfig)
return webpackConfig;
},
},
};
综上,我们总结一下:
改造成本:iframe < wujie < qiankun = icestark
Star数量:qinkun14.1K > wujie2.2K > icestark1.9K
副作用: icestark = qiankun < wujie(样式没了)
当然本文主要聚焦于各个微前端框架的改造成本,可能对比不是那么全面,大家可以更深入地去对比;
后记
上面我们介绍了
Systemjs、UMD
、iframe
、qiankun
、wujie
、icestark
这六种应用级微前端解决方案,当然还有模块级微前端Module Federation
,单独使用它无法构建应用;还有EMP
微前端方案,它的文档不全,不好去写demo,大家可以自行尝试;希望能够给大家的技术选型带来帮助;但是微前端是不断发展的,目前没有任何一个微前端能够在性能上超越iframe而且0配置,所以“革命尚未成功,同志仍需努力”;