大家好,我是多喝热水。
最近我们公司编程导航网站需要去集成一些工具库,而这些工具又是一些独立存在的个体,我们在想如何能够以最低成本去接入这些工具的功能,因此我们调研了一些主流的微前端框架并进行了实践。
本文将深入探讨微前端应用的架构设计,分析社区中主流的微前端解决方案,并通过实际案例演示如何跑通一个微前端应用~
一、微前端应用的架构?
主应用
微前端需要一个主应用,它负责去调度不同的微应用,相当于是一个应用基座。
微应用
微应用可以有多个,可以是不同技术栈开发的项目,如 Vue.js、React.js、Solid.js 等。
二、社区微前端的解决方案
1、无界
腾讯在维护的,后起之秀,非常轻量,3k star,无痛接入 vite,对比下来个人比较喜欢的一种微前端方案。
无界的运行模式
此图为无界官方文档的运行模式图
跑通一个案例
1)第一步安装主应用基座,执行如下图命令:
2)第二步初始化package.json文件并基于 pnpm 配置 monorepo 架构(详细文档移步 从理解软/硬链接掌握PNPM),如下:
3)编写 pnpm-workspace.yaml 文件,如下:
arduino
packages:
- 'main' # main目录下的packge.json需要安装
- 'packages/**' #packages目录下的所有项目的package.json需要安装
4)创建多个微应用,如下:
4)安装无界并将所有微应用关联起来
无界对于Nodejs的版本有要求,需要Nodejs < v18,参考此处文档无界
在主应用安装wujie-vue3(同时会把我们主/微应用所需要的所有依赖的都安装完),这个是作者封装的组件,可以减少用户配置的成本,如下:
此时我们的目录是这样的,node_modules中只保留最核心的包,一些辅助用的包,如Babel会被提升到最外层的node_modules,如下:
5)在主应用中注册无界组件,如下:
6)启动主应用和所有微应用
这里我们可以给最外层的package.json配置一下scripts脚本,避免每次都需要进入packages文件夹启动微应用,配置完成依次启动,如下:
7)启动后在主应用中引入对应的微应用
8)完成启动
总结
从跑通这个案例来看,无界的接入非常简单轻量,且完全支持 vite(这是其他微前端框架没有的)
简单了解一下原理
1)CSS 隔离:使用 shadowDOM 隔离
2)JS 隔离:使用一个空的 iFrame 隔离
3)多应用通讯:使用 Proxy
2、qiankun
该框架是蚂蚁在维护的,15k star,目前官方使用的是 webpack 作为构建工具,没有明确表示支持 vite,社区有 vite-plugin-qiankun 插件支持,但是配置起来还是有一定的心智负担,且可能存在不确定的问题,故此处使用webpack进行演示。
跑通一个案例
1)创建主应用(这次我们使用create-react-app来创建基于react的主应用)
2)配置monorepo架构(与无界中一致,这里不再赘述)
3)创建微应用(这里我们统一都使用 webpack 作为构建工具)
首先我们安装一下 vue/cli,如下:
再创建一个基于vue/cli的微应用,如下:
4)在主应用中注册微应用
js
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'vue app',
entry: '//localhost:7100',
container: '#vueapp',
activeRule: '/vueapp',
},
]);
start();
在主应用中配置微应用的显示位置,如下:
5)配置微应用,根据文档所述,我们需要在微应用的入口导出对应的生命周期钩子给到qiankun在合适的时机调用,且需要根据不同的运行时设置不同的publicPath,参考此处文档 项目实践 - qiankun
第一步,在微应用入口文件的同级目录下创建一个public-path.js文件,并写入如下内容:
js
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
第二步,在微应用的package.json中加入如下声明,告诉 eslint 这个 webpack_public_path 全局变量时存在的,不要抛出错误,如下:
js
"eslintConfig": {
...
"globals": {
"__webpack_public_path__": true
},
...
},
第三步,在微应用入口文件抛出生命周期钩子函数给到qiankun调用,如下:
ts
import "./public-path";
import { createApp } from "vue";
import App from "./App.vue";
let instance = null;
function render(props = {}) {
const { container } = props;
createApp(App).mount(container ? container.querySelector("#app") : "#app");
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log("[vue] vue app bootstraped");
}
export async function mount(props) {
console.log("[vue] props from main framework", props);
render(props);
}
export async function unmount() {
instance?.unmount();
instance.$el.innerHTML = "";
instance = null;
}
第四步,在vue.config.js文件中配置微应用的打包方式,并配置跨域【重要】
js
const { defineConfig } = require("@vue/cli-service");
const { name } = require("./package");
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: "umd", // 把微应用打包成 umd 库格式
chunkLoadingGlobal: `webpackJsonp_${name}`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
},
},
});
6)启动微应用和主应用
7)访问/vue-app 路由成功渲染微应用
总结
1)配置比较繁琐,个人感觉需要用户去配置的东西太多了,单从启动一个微前端应用来说,确实没有无界方便
2)暂时无法支持 vite 作为开发服务器,只能使用 webpack,接入相对比较重
3、两者实现原理对比(拓展)
无界 | qiankun | |
---|---|---|
应用加载机制 | 无需注册,直接将子应用注入主应用同域的iframe中运行 | 首先基于single-spa 注册子应用,然后通过import-html-entry获取子应用的相关资源并对这些资源进行加工,随后构造和执行一些生命周期中需要执行的方法,并返回一个函数,该函数的返回值是一个包括了子应用生命周期方法的对象 |
沙箱隔离机制 | js通过iframe来隔离,css通过web component + shadowdom来隔离 | js隔离机制 : SnapshotSandbox、LegacySandbox、ProxySandboxcss隔离机制:strictStyleIsolation、experimentalStyleIsolation |
路由保持机制 | 浏览器的前进后退可以不作任何处理直接作用于子应用,通过监听iframe的路由变化可以将子应用的url同步到主应用 | 通过props实现全局路由数据的存储及共享 |
应用通信机制 | 提供多种通信方式:window.parent直接通信、props数据注入、去中心化EventBus通信机制 | 通过发布订阅模式来实现通信,状态和回调处理函数全局统一维护,全局状态发生变化时触发各个应用注册的回调函数执行,将新旧状态传递到所有应用 |
4、两者优缺点对比
优点 | 缺点 | |
---|---|---|
无界 | 1)具备qiankun的所有优点2)主应用使用成本及子应用适配成本低3)css沙箱和js沙箱都采用了原生隔离,无需担心污染问题4)支持路由保活和共享依赖5)具有强大插件系统,方便在运行时修改子应用代码 | 目前还比较新, 社区不够活跃 |
qiankun | 1)能监听路由自动加载和卸载当前路由对应的子应用2)具有完备沙箱方案来隔离js和css3)支持静态资源预加载4)应用间通信简单 | 1)基于路由匹配,无法同时激活多个子应用,也不支持子应用保活2)改造成本较大,从 webpack、代码、路由等都要做一系列的适配3)css 沙箱无法绝对的隔离,js 沙箱在某些场景下执行性能下降 严重4)无法支持 vite 等 ES Module 脚本运行 |
三、对微前端的一些看法
什么时候需要用到它?
1)不同项目使用的技术栈不同,想将它们组合到一起
2)为了解决项目代码组织问题,而不是性能问题
一个使用场景
假如我有一个系统是用 jQuery + PHP 写的,但是它已经在线上稳定运行了好几年的时间,而现在需要在这个系统上加一些新需求,但是碍于技术栈过于老旧而导致开发效率低下,想使用新的技术栈来开发(如Vue.js、React.js),重构又浪费时间和人力,那这个时候就可以考虑接入微前端,把旧系统作为一个微应用,新的业务逻辑重开一个项目写(也是一个微应用),这样我们使用最小的成本提升了开发效率。
优点
1)老旧技术栈项目也能平滑迁移至新的技术栈,提升项目的可扩展性
2)主应用不限制接入应用的技术栈,微应用可以自主选择技术栈
3)独立部署,降低一个前端应用每次部署涉及的范围,一定程度上减少了项目风险
缺点
1)应用拆分的越小,架构就会变得复杂、维护成本就会变高
2)技术栈多样化,同时也意味着技术栈混乱(但一般也不会超过2种以上?)
四、其他补充
micro-app
star 5k,也是一个微前端框架,京东推出,micro-app 的接入和wujie的步骤一致,仅仅只是将 wujie-vue3 换成了 @micro-zoe/micro-app
如下所示:
js
import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import microApp from "@micro-zoe/micro-app";
createApp(App).mount("#app");
microApp.start();
注意 :在主应用中如果子应用是采用vite接入,那么需要使用 iframe, 官方的解释
js
<template>
<micro-app url="http://localhost:5173" height="500px" name="vue-app" iframe></micro-app>
<micro-app url="http://localhost:5174" height="500px" name="react-app" iframe></micro-app>
</template>
否则你可能会看到如下错误: