背景
基于 single-spa
+ systemjs
实现的 Mone 微前端架构存在加载缓慢、不利于子应用独立部署、不利于提取公共依赖以及不便于对资源进行压缩等问题。
qiankun 框架
qiankun 简介
qiankun
是阿里开源的一款基于single-spa
封装的微前端框架,目前已经过阿里多款产品实践,是目前较为成熟的微前端框架。qiankun
在single-spa
的基础上封装了许多功能,弥补了single-spa
的不足同时也带来了新的功能,如下图:
为什么选择 qiankun
考虑到 Mone 目前需要的优化以及后期的扩展性,qiankun
比较符合当前的场景需求,同时qiankun
经过实际大型项目的考验,可以放心使用,最后qiankun
官方文档较为友好,便于上手,因此我们选择使用qiankun
来重构 Mone。
Monorepo
Monorepo(monolithic repository)是管理项目代码的一种方式,区别于传统的一个项目一个库的模式,monorepo 提倡将多个项目集中到一个库下进行管理。目前已有许多知名项目采用了这种方式如:Babel、Vue3 等。
monorepo 实现的项目结构大致如下:
├── packages
| ├── pkg1
| | ├── package.json
| ├── pkg2
| | ├── package.json
├── package.json
其中 packages 下面是一个个不同项目/包,每个项目/包有自己的依赖文件。Monorepo 最主大的好处是工作流程的统一性,无需切换 repo,多个项目的修改在一个 repo 下完成,统一测试、统一发版,只需要一套配置就可以管理多个包的构建、测试 和发布。但是在一个 repo 下管理项目也存在弊端,随着 packages 下的包越来越多,整个 repo 会越来越庞大,甚至由于每个包都用独立的依赖,node_modules 将存在许多重复的包,造成不必要的资源浪费。
随着 packages 下的项目/包越来越多,repo 的体积会变大是肯定的,但是重复的依赖却是可以想办法提取的,因此采用 Monorepo 的管理方式还是比较符合目前 Mone 的整体结构和需求。
基于 Lerna + yarn 和 qiankun 实现 monorepo 管理方式的 Mone 项目落地
lerna 简介
lerna 是一种工具,针对使用 git 和 npm 管理多软件包代码仓库的工作流程进行优化,lerna 可以较好的进行包版本管理,使用配备的命令对包同时进行运行、打包及发布等,由于本人目前对 lerna 的了解和使用也不算多,更多的优势和缺点任然需要在实践中慢慢摸索体会,如果碰巧你的项目中需要用到这些能力,请尽情去尝试它。
yarn workspace 简介
Yarn workspace 能更好的统一管理有多个项目的仓库,既可在每个项目下使用独立的 package.json 管理依赖,又可便利的享受一条 yarn 命令安装或者升级所有依赖等。更重要的是,yarn 可以提取各个项目中的公共依赖,使多个项目共享同一个 node_modules
目录,提升开发效率和降低磁盘空间占用。
创建一个 lerna 项目
通过以下命令可以初始化一个 lerna 管理的项目
// 创建一个git仓库文件夹,并初始化成lerna仓库
> git init lerna-repo && cd lerna-repo
> lerna init
初始化后整个仓库的结构大致如下:
lerna-repo/
packages/
package.json
lerna.json
在 lerna.json 中加上以下配置:
"npmClient": "yarn", // 使用yarn作为npm工具
"useWorkspaces": true, // 使用yarn workspaces
package.json 中大致配置如下:
// package.json
{
"name": "root",
"private": true,
"workspaces": [
//指定workspace路径
"packages/*"
],
"devDependencies": {
"lerna": "^3.22.1" // 当前使用的lerna版本
}
}
一个基本的 lerna 仓库基本上就配置好了,当然还可以有更多配置,但目前我们只需要这些,如果你需要用到其他配置可以自行参考网上的资料。例如我们可以在 package.json 中配置基于 commitizen 的 commit 规范设置等。
基本的架子搭好之后我们可以进入 packages 目录下开始创建我们需要的项目,流程跟平时一样,唯一的不同是这些项目将会被 lerna 管理起来。
由于我们是基于原有项目进行的改造,我们只需要把原有的项目全都放到 packages 下即可。
基于 qiankun + vue 搭建 Mone 基座
通过上面的创建 lerna 仓库的步骤创建一个项目目录,在 packages 目录下使用 vue-cil 创建我们的基座项目(即 Mone 主项目),在主项目下安装 qiankun 框架,在入口文件中按照 qiankun 的指引,引入注册子应用的 API,处理哈希路由模式,如果有需要通过 props 配置传递需要的数据,一个微前端项目基本就搭建成功了。最后只需要在挂载子应用的地方调用 start 方法即可。
import { registerMicroApps } from "qiankun";
new Vue({
router,
store,
i18n,
render: (h) => h(App),
}).$mount("#app");
// 注册子应用 其中变量 MicroApps 为按照qiankun给的格式构建的子应用数组
// 第二个参数传入了一个配置对象,包含了子应用的三个生命周期
registerMicroApps(MicroApps, {
beforeLoad: (app) => console.log("before load11111", app.name),
beforeMount: [
(app) => {
console.log("before Mount1111", app.name);
},
],
beforeUnmount: (app) => {
console.log("before Unmount", app.name);
},
});
// MicroApps
import store from "./store";
const getActiveRule = (hash) => (location) => location.hash.startsWith(hash);
export default [
{
name: "osg",
entry: process.env.VUE_APP_MONE_OSG, // 全局配置的环境变量
container: "#subapp",
activeRule: getActiveRule("#/osg"), // https://qiankun.umijs.org/zh/cookbook#微应用的路由模式如何选择
props: {
userInfo: store.state.user.userInfo, //共享主应用的store的用户信息
},
},
];
// 判断是否是是否是微前端模式,使用乾坤启动后会在全局挂载这个变量,用于判断当前
// 的模式是独立运行还是微前端,更多启动配置参考qiankun官方文档
if (!window.qiankunStarted) {
window.qiankunStarted = true;
start({
prefetch: "all", // 启动时预加载所有子应用静态资源
});
}
// 子应用入口文件引入下面代码以动态修改运行时的资源路径
// 修改运行时publicPath 如果是微前端模式则修改,否则不做处理
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
console.log(`subapp-osg:${__webpack_public_path__}`);
}
创建更多子应用
基于上面的配置,在 packages 目录下创建更多子应用项目,每个子应用的入口文件都需要引入修改资源路径的代码及抛出生命周期钩子。
let instance = null; // 子应用实例
let osgRouter = router; // 子应用路由
// 渲染函数
function render(props = {}) {
const { container } = props; // 获取主应用传入的参数
instance = new Vue({
router: osgRouter,
store,
render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}
// 作为独立应用运行
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {}
export async function mount(props) {
store.state.userInfo = props.userInfo;
render(props);
}
// 卸载时销毁子应用实例
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = "";
instance = null;
}
通过 lerna 启动本地运行或打包构建
// 清除应用下所有依赖包
> lerna clean
// 引导安装依赖,如果配置了yarn workspace将会调用 yarn install --hoist进行依赖提升安装,将相同的依赖安装到根目录,并链接所有的交叉依赖
> lerna bootstrap
// 本地运行 将会对packages目录下所有子项目进行 npm run serve操作,子应用必须有自己的package.json文件并配置serve命令
> lerna run serve
// 同本地运行命令类似,运行所有子项目的构建命令
> lerna run build
// 通过 lerna 进行 publish,将packages目录下的包进行发布版
> lerna publish
更多命令参考lerna 官方文档
总结
目前微前端的实践已经算比较成熟,网络上也有比较多的资料参考,这次 Mone 的微前端落地参考了许多资料,其实 monorepo 的仓库管理方式更适合做框架项目或者组件库等 npm 包,之所以用来管理微前端项目最大的原因其实是为了更好的依赖管理和依赖共用,此次实践仅仅是使用到了 lerna 的一点点功能,更强大的包与包之间依赖的场景还未用到,初次尝试也还未更深入的研究 lerna,在此仅作为一个微前端的实现参考,如需进行组件库等的实现,可以进一步研究方案。