一、背景
由于公司业务不断扩张,原本的后台管理系统显得越来越臃肿,每次运行打包都要好几分钟,极大的耽误了开发调试的时间。起初的做法是按照业务将项目进行了拆包,所有的项目都放在服务器同一文件夹下,父项目将自身实例,路由,状态管理等挂载window下供子项目使用,父子项目共用实例。生产模式下父项目加载的时候会去读取其它子项目的html文件,将相应的js及css文件均加载进来,本地调试模式的时候各子项目会读取线上某个环境父项目的index.html作为自己的模板从而运行起来。这种做法的坏处就是子项目是无法独立运行、独立发布的,于是新项目搭建的时候公司采用了微前端。
二、微前端
微服务是面向服务架构(SOA)的一种变体,把应用程序设计成一系列松耦合的细粒度服务,并通过轻量级的通信协议组织起来,具体地,将应用构建成一组小型服务。这些服务都能够独立部署、独立扩展,每个服务都具有稳固的模块边界,甚至允许使用不同的编程语言来编写不同服务,也可以由不同的团队来管理。
当公司业务需求越来越多的时候,项目就会越来越庞大,后续如果想对项目进行重构亦或是某块大的业务需求想要独立出来都是很困难的事情,再或者有了新的更好的技术框架,也是很难再去改造之前的项目。这种时候微前端的优势就很明显了,将庞大的块拆成一小块一小块,分而治之,关键的优势在于:
- 技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权 - 独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新 - 增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略 - 独立运行时
每个微应用之间状态隔离,运行时状态不共享
本文微前端的实现采用了qiankun。
三、qiankun
qiankun 是一个生产可用的微前端框架,它基于 single-spa,具备 js 沙箱、样式隔离、HTML Loader、预加载 等微前端系统所需的能力。qiankun 可以用于任意 js 框架,微应用接入像嵌入一个 iframe 系统一样简单。
接下来简单搭建下基于qiankun的微前端。
四、实践
首先新建两个vue项目,一个作为主应用,一个作为微应用。
主应用
接着我们首先改造主应用:
1、安装qiankun
yarn add qiankun
2、在主应用中注册微应用
首先在src目录下新增micro目录,新增app.js用来存放需要注册的微应用。
src/micro/app.js
const getActiveRule = (hash) => (location) => location.hash.startsWith(hash);
const apps = [
{
name: 'sub-app1',
entry: '//localhost:8081',
container: '#sub-container',
activeRule: getActiveRule('#/sub1')
}
]
export default apps
- name -
string- 必选,微应用的名称,微应用之间需要保持唯一。 - entry -
string | { scripts?: string[]; styles?: string[]; html?: string }- 必选,微应用的入口。- 配置为字符串时,表示微应用的访问地址,上述示例只配置了本地访问的地址,实际项目中需要根据环境配置微应用的访问地址。
- 配置为对象时,
html的值是微应用的html内容字符串,而不是微应用的而访问地址。微应用的publicPath将会被设置为/。(这种方式没有尝试过,有兴趣的小伙伴可以试试~)
- container -
string | HTMLElement- 必选,微应用的容器节点的选择器或者Element实例。上述示例即代表id为'sub-container'的容器节点,也可以写成container: document.querySelector('#sub-container')。 - activeRule -
string | (location: Location) => boolean | Array<string | (location: Location) => boolean>- 必选,微应用的激活规则。-
支持直接配置字符串或字符串数组,如
activeRule: '/sub1'或activeRule: ['/sub1', '/sub2'], -
支持配置一个active function函数或者一组active function。函数会传入当前location作为参数,函数返回true时表明当前微应用会被激活。如
location => location.pathname.startsWith('/app1')本文采用的是hash模式,所以写成
activeRule: getActiveRule('#/sub1'),也可以直接写activeRule: '#/sub1',但是如果主应用是 history 模式或者主应用部署在非根目录,这样写不会生效。
-
- loader -
(loading: boolean) => void- 可选,loading状态发生变化时会调用的方法。 - props -
object- 可选,主应用需要传递给微应用的数据。可用来进行项目间的通信。
接下来,在micro下新增index.js用来注册我们的微应用
src/micro/index.js
import {
registerMicroApps,
addGlobalUncaughtErrorHandler,
start,
} from "qiankun";
import apps from './app'
registerMicroApps(apps, {
beforeLoad: (app) => {
console.log('before load', app.name)
return Promise.resolve()
},
afterMount: app => {
console.log("after mount", app.name);
return Promise.resolve()
}
})
addGlobalUncaughtErrorHandler((event) => {
console.error(event, 'event');
const { message: msg } = event
if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {
console.error("微应用加载失败,请检查应用是否可运行");
}
});
export default start;
registerMicroApps(apps, lifeCycles?)
- apps -
Array<RegistrableApp>- 必选,微应用的一些注册信息。 - lifeCycles -
LifeCycles- 可选,全局的微应用声明周期钩子。 该API是用来注册微应用的基础配置信息。当浏览器的url发生变化时,会自动检查每一个微应用注册的activeRule规则,符合规则的应用将会被自动激活。
addGlobalUncaughtErrorHandler(handler)
- handler -
(...args: any[]) => void- 必选 该API是用来添加全局的未捕获异常处理器。
start(opts?)
- opts -
Options- 可选 具体参数可参考官方文档 该API是用来启动qiankun的。
3、启动
首先需要改造下我们的App.vue,新增一个id为“sub-container“的节点
src/App.vue
<template>
<div>
<div id="nav">
<router-link to="/home">Home</router-link> |
<router-link to="/sub1">子项目</router-link>
</div>
<div id="sub-container"></div>
<router-view />
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
</style>
接着需要改造下main.js,执行启动函数。`
src/main.js
import { createApp } from "vue"
import App from "./App.vue"
import router from "./router"
import start from "./micro"
const instance = createApp(App).use(router).mount("#app")
instance.$nextTick(() => {
start()
})
上面有一点需要注意的是,start函数必须在DOM节点加载完成后调用,不然会报找不到容器DOM。具体可见常见问题。
至此,我们的主应用搭建完成,接着来搭建微应用。
微应用
微应用不需要额外安装任何其它依赖即可接入qiankun主应用。
1、在src目录新增public-path.js文件,用于修改运行时的publicPath。
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
2、入口文件main.js修改,这里我们需要引入刚刚新增的public-path.js文件,并导出bootstrap、mount、unmount三个生命周期钩子,以供主应用在适当的时机调用。
import "./public-path"
import { createApp } from "vue"
import App from "./App.vue"
import router from "./router"
// 用于保存vue实例
let instance = null
function render(props) {
const { container } = props
instance = createApp(App)
instance
.use(router)
.mount(container ? container.querySelector("#app") : "#app")
}
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log("VueMicroApp bootstraped")
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props: any) {
console.log("VueMicroApp mount", props)
render(props)
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
console.log("VueMicroApp unmount", instance)
instance.unmount()
}
// 独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
这里需要注意的是为了避免根id#app与其它的DOM冲突,需要限制查找范围。如果直接使用根id则会报以下错误:
Application died in status NOT_MOUNTED: Target container with #container not existed after xxx mounted!
3、打包配置修改
除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:
vue.config.js
const path = require("path");
module.exports = {
devServer: {
// 监听端口
port: 8081,
// 关闭主机检查,使微应用可以被 fetch
disableHostCheck: true,
// 配置跨域请求头,解决开发环境的跨域问题
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
output: {
// 微应用的包名,这里与主应用中注册的微应用名称一致
library: "sub-app1",
// 将你的 library 暴露为所有的模块定义下都可运行的方式
libraryTarget: "umd",
// 按需加载相关,设置为 webpackJsonp_MicroAppOrde 即可
jsonpFunction: `webpackJsonp_sub-app1`,
},
},
};
4、修改路由
给微应用的路由统一添加前缀,值和我们在主应用里配置的该微应用的activeRule一致。
import { createRouter, createWebHashHistory } from "vue-router"
const routes = [
{
path: "/",
name: "sub1",
component: () => import("../views/sub1.vue")
}
]
const microAppPrefix = "/sub1"
const setRoutesMicroAppPrefix = routes => {
if (routes.length) {
routes.forEach(item => {
if (item.path && item.path.indexOf(microAppPrefix) === -1) {
item.path = `${microAppPrefix}${item.path}`
}
if (item.children && item.children.length) {
setRoutesMicroAppPrefix(item.children)
}
})
}
}
if (window.__POWERED_BY_QIANKUN__) {
setRoutesMicroAppPrefix(routes)
}
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
微应用全部配置完成,让我们来看看效果~
五、参考文章
1、qiankun