引言
hello,当你看到这篇文章时可能你对微前端已经有个大概的了解了,我就分享下自己对于微前端qiankun技术架构在实际工程中从技术预研、项目应用到部署落地的整个过程,如果能够给你提供一些帮助那就再好不过了。
本文包含以下内容:
业务场景
说说我们团队是基于什么样的业务场景使用这个技术架构的吧:
- 子系统由不同团队开发维护的,使用技术栈及开发规范不统一,并且是分开部署的。
- 要将子系统集成在一个平台应用展示、仍支持子系统单独访问,或者子系统任意组合成应用。(例如A B C D 四个子系统可以单独访问,也支持ABC组合成一个应用)
- 后续追加集成子系统是渐进式、无感知的。不要影响当前已完成集成的这个平台应用。
什么是微前端
微前端是一种前端架构模式,是一种多个团队通过独立发布功能的方式(独立仓库、独立开发、测试、部署和维护)来共同构建现代化 web 应用的技术手段及方法策略。
微前端有什么优势特点
- 一个复杂庞大的项目拆成多个微应用,独立仓库、独立开发、测试、部署和维护,互不影响。
- 跟技术栈无关,允许使用不同的前端技术栈(如 React、Vue、Angular 等)进行开发,团队可以根据自身技术优势选择合适的工具。
- 多个应用结合在一起,可以一起运行,又可以单独运行,每个微应用之间状态隔离,运行时状态不共享。
- 渐进式重构与升级:适用于需要逐步迁移旧系统的场景,通过将老旧模块逐步替换为新的微前端模块,实现平滑过渡
理论上通过微前端实现的功能通过iframe嵌入也能实现,我们在这两种技术之间也做过权衡。
微前端相比于iframe的优势(为什么不是用iframe)
- 适用场景
- 微前端:适用于大型企业级应用、多团队协作、技术栈异构、渐进式重构等场景。
- iframe:适用于简单的内容嵌入、第三方服务集成等场景。
- 隔离性与安全性
- 微前端:通过 JavaScript 沙箱、Web Components 等技术实现隔离,同时提供更灵活的通信机制。
- iframe:提供天然的隔离机制,但可能存在安全风险,例如点击劫持和恶意脚本注入。
- 通信机制
- 微前端:提供丰富的通信机制,支持父子应用及子子应用之间的通信。
- iframe:通信方式较为有限,通常依赖 postMessage 或 URL 参数。
- 性能
- 微前端:支持按需加载、资源预加载和懒加载,优化了页面加载性能。
- iframe:每个 iframe 都需要独立加载完整的文档和资源,会增加页面的加载时间和网络请求次数,性能开销较大。
- 用户体验
- 微前端:可以实现更流畅的用户体验,子应用之间的切换更加自然。
- iframe:可能会导致页面布局混乱、滚动条问题,影响用户体验。
- 开发与维护
- 微前端:支持多框架共存,允许团队独立开发和部署子应用,维护成本较低。
- iframe:虽然集成简单,但需要处理跨域通信、SEO 优化等问题。
- SEO 友好性
- 微前端:对 SEO 更友好,搜索引擎更容易解析页面内容。
- iframe:搜索引擎对 iframe 中的内容索引和排名处理较为复杂,可能影响 SEO。
接下来我就讲讲实际项目中的应用,跟着我一步一步来,不要错过任何细节哦!
主应用配置
- 安装qiankun
yarn add qiankun
- 定义子应用挂载节点
//注意id跟下面注册配置的container保持一致哦
<div id="micro-app-container"></div>
- 注册子应用
//mains.ts
import { registerMicroApps, start } from "qiankun";
registerMicroApps([
//当匹配到activeRule的时候,请求entry的资源,渲染到container
{
name: "qiankun-vue3-app",//子应用1
entry: '//localhost:3021',//本地承载应用资源的服务路径
<!-- entry: '/qk-app/vue-app/',//生产则使用服务器路径(要通过nginx进行代理) -->
<!-- entry: process.env.MICRO_APP_VUE, //所以这里建议使用环境变量 -->
container: "#micro-app-container",//绑定挂载节点
activeRule: "/#/sub-app/micro-app-vue",//这里可以理解为路由,
<!--注意 注意 注意!!!>
<!--如果部署到服务器nginx站点资源非根路径下。activeRule需要调整后面部署时详细描述。-->
<!--该问题见社区讨论:https://github.com/umijs/qiankun/discussions/2858 >
props: {
//给子应用传递数据
nickName: "shier",
shared,
appId: "/sub-app/micro-app-vue",
},
},
{
name: "qiankun-react-app", //子应用2
entry: '//localhost:3022',
container: "#micro-app-container",
activeRule: "/#/sub-app/qiankun-react-app",
props: {
nickName: "shier",
appId: "/sub-app/micro-react-vue",
shared,
},
}
])
//样式隔离
start({
prefetch: true, // 预加载子资源
sandbox: {
experimentalStyleIsolation: true// 实验性沙箱(会导致弹窗异常)
}
})
子应用配置
- src目录下创建public-path.js文件
/src/public-path.ts
if ((window as any).__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
- 引入public-path.js文件(==注意,要在main.ts文件最顶部引入==)
//main.ts //注意,要在文件最顶部引入
import "./public-path";
- 配置生命周期
//main.ts
let instance: any = null;
function render(props: any = {}) {
(window as any).G = props
const { container } = props;
// 当传入的 shared 为空时,使用子应用自身的 shared
// 当传入的 shared 不为空时,主应用传入的 shared 将会重载子应用的 shared
// 建议:只在存在复杂状态且需要子应用自行维护状态池才使用shared
const { shared = SharedModule.getShared() } = props;
SharedModule.overloadShared(shared);
instance = createApp(App);
instance.use(store).use(router).use(registerApp).mount(container ? container.querySelector('#app') : '#app');
}
// 如果是单独启动的子文件,保证仍能正常运行
if (!(window as any).__POWERED_BY_QIANKUN__) {
render()
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props: any) {
render(props);
}
export async function unmount() {
instance.unmount()
instance._container.innerHTML = ''
instance = null
}
//vue2
<!-- export async function unmount() {
instance.$destroy()
instance.$el.innerHTML = ''
instance = null
} -->
- vue.config.js
const { name } = require('./package.json')
const dev = process.env.NODE_ENV === "development";
//qiankun环境部署
publicPath: dev ? `//localhost:3020` : "/qk-app/vue-app/"//服务器部署的文件夹名,
devServer: {
//配置端口,与主应用配置的entry保持一致
port: "3020",
//解决跨域
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
// 输出重构 打包编译后的js文件名称,添加时间戳.不配置识别不到生命周期
library: `${name}-[name]`,
libraryTarget: "umd", // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`,
}
}
- 配置路由前缀 ==注意:路由携带参数,最好使用router.push({name:"",query:{}})==
//router/index.ts
let prefix: any = "";
// 判断是 qiankun 环境则增加路由前缀,路由前缀要跟注册子应用的activeRule对应
if ((window as any).__POWERED_BY_QIANKUN__) {
// prefix = (window as any).G.appId
prefix = '/sub-app/micro-app-vue'
}
//所有静态路由加上prefix前缀
const routes = [
{
path: prefix + "/",
name: "index",
component: () =>
import(/* webpackChunkName: "user" */ "../layouts/BasicLayout.vue"),
children: []
}
...
]
//路由拦截适配qiankun
if ((window as any).__POWERED_BY_QIANKUN__) {
if (to.path === prefix + "/init") {
next();
} else {
if (to.path.includes(prefix)) {
next();
} else {
next({ path: prefix + to.path });
}
}
} else {
//项目中原来的路由拦截代码
}
部署
--html---------------根文件夹
--main-app --------主应用
--css
--img
--js
--index.html
--vue-app ---------子应用1
--react-app--------子应用2
- 附:nginx配置
<!----为什么这里不是 location / ,因为通常多个项目会进行目录划分,不会直接资源文件放在根路径下>
location /qk-app {
alias /mdata/web/main-app/;
index index.html index.htm;
}
location /qk-app/vue-app {
alias /mdata/web/vue-app/;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /qk-app/react-app{
alias /mdata/web/react-app/;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
注意:由于子部署在二级目录,注册时activeRule要做以下调整
{
name: "vue-app",
entry: "/qk-app/vue-app/",
container: "#micro-app-container",
<!---如果由于部署层级问题主应用代理路径为/qk-app,那么在activeRule也要加上前缀->
activeRule: ”/qk-app/#/sub-app/micro-app-vue
props: {
},
}
==这里再次强调:注册应用时的entry、vue.config.js中的publicpath、nginx配置中的location三者保持一致==
实现原理
Qiankun 继承了 single-spa 的特点,通过监听 hashChange 和 popState 事件来检测路由变化。在路由变化时,Qiankun 会匹配到需要渲染的子应用,并加载其 HTML。如果缓存中存在,则直接返回;否则通过 fetch 函数下载 HTML 入口(即HTML Entry),对于 js/css/img/video 等资源都是相对路径,通常做法是动态修改 webpack 打包的 publicPath,然后就可以自动注入前缀给这些资源。
加载子应用的核心流程
- 解析子应用的入口(Entry):
如果子应用使用 HTML Entry,Qiankun 会请求子应用的 HTML 文件,并解析其中的 script 和 link 标签,提取 JS 和 CSS 资源。如果子应用使用 JS Entry,Qiankun 会直接加载指定的 JS 文件。
- 加载子应用的资源:
根据解析出的资源 URL,动态加载子应用的 JS 和 CSS 文件。
- 执行子应用的 JS 代码:
在 JS 沙箱中执行子应用的 JS 代码,确保子应用的全局变量不会污染主应用。
- 挂载子应用:
调用子应用的 bootstrap 和 mount 生命周期钩子,将子应用的 DOM 挂载到主应用指定的容器中。
js沙箱机制
ProxySandbox:通过 JavaScript 的 Proxy 对象拦截对全局对象的修改,确保子应用的修改不会影响其他应用。 SnapshotSandbox:在子应用加载前后对全局对象进行快照,确保子应用卸载后可以恢复到原始状态。
css沙箱
动态样式隔离:实现原理是当子应用被加载时,其对应的样式会被注入到页面中;当子应用被卸载时,qiankun 会自动移除其样式,确保页面的样式环境保持干净
影子DOM沙箱(Shadow DOM):手动开启 ,qiankun 会为每个微应用的容器包裹上一个 shadow dom 节点,从而确保微应用的样式不会对全局造成影响
作用域沙箱(Scope CSS):手动开启 ,qiankun 会改写子应用所添加的样式,为所有样式规则增加一个特殊的选择器规则来限定其影响范围
最后:有什么问题欢迎指出交流。