高效微前端开发环境
一、背景
前面两篇文章主要介绍了当前国内微前端框架的出现的背景以及衍生出来的一些解决问题的手段,现阶段国内的一些比较成熟的微前端框架已经能够满足我们的业务诉求,但是对于我们前端开发者来说,开发体验也是同样重要的,比如 single-spa 就给开发者提供了一个快速创建项目的脚手架 create-single-spa,本地开发测试工具import-map-overrides 等,而像 qiankun、wujie 他们在这方面做得比较少,只介绍了如何去将他们的框架接入到现有的项目中,下面会具体介绍当前微前端本地开的一些痛点。
二、微前端本地开发的难点
相较于由单一一方完成完整的传统系统,微前端模式的特点明显,整个系统由多个开发团队开发的多个模块组合而成,这也注定微前端系统和传统系统在本地开发时存在较大差异,诞生了许多新困难和挑战
1、接入工程搭建
微前端开发中, host方常对于接入方技术架构有额外要求。
举例:host 方采用 MF 接入子应用,故子应用 webpack 要有对应的升级和配置。
如何搭建符合接入要求的项目,是微前端开发模式下的一个挑战,毕竟不能要求所有的接入方完整的学习 host 系统工程标准与要求。因此高效的方式是为接入方提供一套 “开箱即用”的开发环境。
2、联调与效果验证
接入方开发的内容只是系统的一部分,甚至是一小部分,开发过程中,验证效果可能需要依赖系统主体前端功能才能正常运行,比如说需要使用 host 方提供的前端运行时 API。如果只能本地开发并部署后才能验证效果的话,开发效率将非常低。
举例: 如在项目 A 中增加一个新视频处理智能工具 — 视频去水印。工具需要用到基座平台的消息通知, 以告知用户处理进度和预计耗时, 消息通知功能位于系统通用 header。如果本地开发无法查看 host 方的 header, 无法验证调用 host 方消息通知前端 API 效果的话, 无疑开发体验差、开发效率低。
三、不推荐的微前端开发模式
当前社区比较火的几个微前端框架(qiankun、wujie)并没有给出一套具体高效的开发方案,而是只介绍了如何去将微前端框架接入到现有的项目中,下面是通过对这些微前端框架的一些落地实践,总结出来的当前微前端的开发模式。
1、创建项目
各个团队按照自己团队的开发规范搭建微应用开发环境,微应用之间的构建脚本、webpack 配置不统一、缺少统一的开发规范,这就造成后面项目组合时需要做大量的环境兼容或升级处理,比如说沙箱隔离。
2、效果验证
模式一
将所有的子应用部署到服务器之后再去测试环境验证效果。
步骤
- 将所有子应用部署到服务器。
- 在主应用中组合要接入的所有的子应用并部署到测试环境。
- 在测试环境中进行效果验证,如果子项目存在问题则需要对应的开发团队进行修复再重新部署,最终到测试环境测试。
问题
- 子应用在部署之前只能单独开发测试、看不到其他的项目。
- 子应用只有部署后才能在测试环境中和其他应用进行效果验证,比如上面说的消息通知。
- 在测试环境中测试出来的 bug 修复后需要再次进行部署才能到测试环境中进行测试。
模式二
本地手动启动主应用和待开发的子应用,其他子应用都是用他们的已部署版本。
步骤
- 将所有子应用部署到服务器。
- 将本地开发的子应用 A 运行起来。
- 拉取主应用的最新代码并运行起来,并修改项目 A 对应的服务地址。
- 在主应用中对项目 A 进行效果验证,如果有问题可直接本地修改再部署。
问题
- 每个团队都需要下载并实时拉取主应用的最新代码,需要熟悉主应用的接入及其工作流程。
四、single-spa的开发模式
1、开发流程
-
使用create-single-spa命令行创建项目
single-spa 为了能够帮助我们快速高效的搭建一个微应用开发环境,为开发者们提供了一个 cli—— create-single-spa,它主要用于创建新项目,但也可用于迁移现有项目(尤其是迁移 CRA 或无框架项目)。
-
启动本地开发 执行
yarn start
或者npm run start
-
打开host项目,并配置好 import-map-overrides 插件
import-map-overrides 是一个适用于 node 环境和 browser 环境的库,主要用于覆盖 import-maps 中的映射地址,在本地开发时我们可以使用这个插件将测试环境(沙盒环境)中对应项目的 importmaps 映射地址替换成本地服务地址。
<!DOCTYPE html>
<html lang="en">
<head>
+ <script
+ type="text/javascript"
+ src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.3.0/dist/import-map-overrides.js"></script>
</head>
<body>
+ <import-map-overrides-full></import-map-overrides-full>
</body>
</html>
- 打开测试环境进行效果验证
这里需要利用开发者工具将对应项目的地址修改为本地的地址。
- 本地开发直至完成
2、流程概览
3、跨域问题
- 如果子应用调用 host 方的接口进行开发, 无跨域问题
- 如果子应用是调用本方接口, 建议安装chrome跨域插件解决
五、基于MF的微前端插件化开发模式初探
自从 webpack5 推出 MF 以来,MF 在微前端中出现的频率也越来越高,上面介绍的 imports-map-overrides 它只适用于 systemjs 的场景,那在 MF 的场景下我们要怎么才能实现这样的本地开发效果呢? 下面我们将通过使用 MF 开发一个微前端项目来简单学习一下 MF 的插件化能力,进而推出一套基于 MF 的本地开发模式。
1、需求简介
我们的微前端项目需求也很简单,我们需要一个基座
,用于承载所有的子应用,两个子应用
,每个子应用会将自己路由级别的组件
暴露出来统一放到基座中,从而实现应用组合。
使用过 MF 的同学应该很容易想到该怎么去实现这个需求,在基座中的 MF 插件中配置好各个子应用的 remote
信息,在各个子应用的 MF 插件中配置好需要暴露的组件——expose
,如下所示:
// 子应用
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./ModernComponent': './src/components/ModernReactComponent'
}
})
// 基座
new ModuleFederationPlugin({
name: 'host',
remotes: {
app1: 'xxx/remoteEntry.js',
}
})
但是我们仔细想想,这种方式够灵活吗,假如说我们是 ToB 产品,可能某个客户有些路由页面不需要,或者说有新的子应用的路由页面又要新加进来,这样基座和子应用就需要频繁的改动,丧失了 MF 的灵活性,所以我们能不能借助 MF 去实现插件化呢?使得每个子应用能够具备插件的灵活性,动态的去接入到基座中呢?
2、流程概览
如图所示,ShareStore 是基座与各个子应用之间的连接桥梁,子应用需要将自己的路由级别的组件注册到 ShareStore 中就能在基座中展示出来,这种方式的优点就是
灵活性高,能够在运行时动态
的去插入其他应用。下面会具体介绍是如何体现灵活、动态
的特性。
3、原理介绍
基座核心代码
- html文件
<!doctype html>
<html lang="zh-cn">
<body>
<script type="overridable-modulemap" id="plugins">
[
{
"name": "project1",
"url": "http://localhost:8999/remoteEntry.js"
},
{
"name": "project2",
"url": "http://localhost:5000/remoteEntry.js"
}
]
</script>
<div id="app"></div>
</body>
</html>
在 html 文件中我们配置了每个子应用的基本信息,name
表示子应用通过 MF 暴露出来的全局变量的名称,url
表示子应用 MF 入口文件的地址。
- 入口文件
import shareStore from 'share-store'
function loadScript(url: string): Promise<void> {
return new Promise((resolve, reject) => {
const newScript = document.createElement('script');
newScript.onerror = reject;
newScript.onload = () => resolve();
newScript.async = true;
document.head.appendChild(newScript);
newScript.src = url;
});
}
async function loadPlugin(url: string, scope: string): Promise<void> {
await loadScript(url);
// 动态加载邦联模块并初始化
await __webpack_init_sharing__('default');
const container = window._Plugin[scope];
await container.init(__webpack_share_scopes__.default);
// 执行远程模块
const init = await window._Plugin[scope].get('.');
init();
}
(async () => {
// 读取插件配置, 确定当前程序有哪些插件, 然后加载使用
const plugins = JSON.parse(document.getElementById('plugins')!.textContent || '[]');
// 加载、执行插件
const promises = plugins.map(p => loadPlugin(p.url, p.name))
await Promise.all(promises);
// 在所有插件注册完成后, 正式启动程序, 渲染页面
Promise.all(shareStore.promises).then(() => {
render(
<HashRouter>
<Routes>
{
shareStore.routes.map(r => <Route path={r.path} key={r.path} element={r.component} />)
}
</Routes>
</HashRouter>,
document.getElementById('app')
);
});
})();
在入口文件中做了两件事:
- 获取 html 文件中的子应用信息,并使用 MF 动态加载的方式加载子应用的入口文件,从而完成子应用的路由注册。
- 渲染子应用注册的路由组件,并调用 React 的 render 方法进行页面渲染。
子应用核心代码
import shareStore from 'share-store'
shareStore.addRoutes({
path: xxx,
component: <Page1/>
})
在子应用中只需要安装 shareStore 将路由信息注册到 shareStore 就好,同时为了能够拥有像 single-spa 那个的本地开发并进行效果验证的体验,作者也开发了一个适用于 MF 的开发者工具——module-map-override,借助这个工具开发者就能快速在基座运行时添加新的功能模块。 微前端项目地址。
六、总结
不管未来如何实现微前端,一套便捷高效的微前端开发模式和环境是必须的,一个好的微前端开发模式能够有效的节约开发人员的开发和测试成本,提高开发效率。在之后的微前端开发中我们建议 host 方需要提供以下能力:
- 项目初始化命令行工具
- 符合项目要求的构建脚本
- 系统沙盒环境
- 类似single-spa的运行时插件修改调整工具
转载自:https://juejin.cn/post/7228589271292002359