微前端
# qiankun
qiankun 是一个基于 single-spa (opens new window) 的微前端 (opens new window)实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。解决single-spa样式隔离、js沙箱等问题。
微前端架构具备以下几个核心价值:
技术栈无关 主框架不限制接入应用的技术栈,微应用具备完全自主权
独立开发、独立部署 微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
独立运行时 每个微应用之间状态隔离,运行时状态不共享
# 主应用
主应用不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start
即可。
先安装 qiankun
:
$ yarn add qiankun # 或者 npm i qiankun -S
注册微应用并启动:
// 注册微应用
import { registerMicroApps,start } from 'qiankun';
const apps = [
{
name: 'vueApp',
entry: '//localhost:3000',
container: '#vue',
activeRule: '/vue',
props:{ vueName:'小明',age:18 }
},
{
name: 'vueplusApp',
entry: '//localhost:10000',
container: '#vueplus',
activeRule: '/vueplus',
props:{ vueName:'小红',age:18 }
},
]
registerMicroApps(apps)
start()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
主应用的路由配置:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
redirect: '/login',
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path: '/home',
name: 'Home',
component: () => import('../views/Home.vue'),
redirect: '/hello',
children: [
{
path: '/hello',
name: 'Hello',
component: () => import('../components/HelloWorld.vue')
},
{
path: "/vue",
name: "vue",
children:[
{
path:'/vue/*',
},
]
},
{
path: "/vueplus",
name: "vueplus",
children:[
{
path:'/vueplus/*',
},
]
},
],
},
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
搭建基地,并设置微应用容器显示的位置:
// home.vue
<template>
<div class="home">
<el-container>
<el-aside width="200px"
><el-menu
class="el-menu-vertical-demo"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
:router="true"
>
<el-menu-item index="/vue"><i class="el-icon-s-grid"></i>vue应用</el-menu-item>
<el-menu-item index="/vueplus"><i class="el-icon-s-grid"></i>vueplus应用</el-menu-item>
</el-menu></el-aside
>
<el-main
><router-view />
<div id="vue"></div>
<div id="vueplus"></div>
</el-main>
</el-container>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 微应用
微应用分为有 webpack
构建和无 webpack
构建项目,有 webpack
的微应用(主要是指 Vue、React、Angular)需要做的事情有:
- 新增
public-path.js
文件,用于修改运行时的publicPath
。什么是运行时的 publicPath ? (opens new window)。
注意:运行时的 publicPath 和构建时的 publicPath 是不同的,两者不能等价替代。
- 微应用建议使用
history
模式的路由,需要设置路由base
,值和它的activeRule
是一样的。 - 在入口文件最顶部引入
public-path.js
,修改并导出三个生命周期函数。 - 修改
webpack
打包,允许开发环境跨域和umd
打包。
主要的修改就是以上四个,可能会根据项目的不同情况而改变。例如,你的项目是 index.html
和其他的所有文件分开部署的,说明你们已经将构建时的 publicPath
设置为了完整路径,则不用修改运行时的 publicPath
(第一步操作可省)。
无 webpack
构建的微应用直接将 lifecycles
挂载到 window
上即可。
# Vue 微应用
以 vue-cli 3+
生成的 vue 2.x
项目为例,vue 3
版本等稳定后再补充。
在
src
目录新增public-path.js
:if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
1
2
3router/index.js
const router = new VueRouter({ mode: 'history', base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/", routes })
1
2
3
4
5入口文件
main.js
修改,为了避免根 id#app
与其他的 DOM 冲突,需要限制查找范围。import Vue from 'vue' import App from './App.vue' import router from './router' import "./public-path"; import actions from "@/shared/actions"; Vue.config.productionTip = false let instance = null function render(props = {}){ if (props) { // 注入 actions 实例 actions.setActions(props); } const { container } = props; instance = new Vue({ router, render: h => h(App) }).$mount(container ? container.querySelector('#app') : '#app') } // 独立运行时 if (!window.__POWERED_BY_QIANKUN__) { render(); } // 向主应用暴露三个生命周期钩子函数,必须是promise函数 /** * bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。 * 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。 */ export async function bootstrap(props) { console.log('[vue] vue app bootstraped'); Vue.prototype.$vueName = props.vueName Vue.prototype.$age = props.age } /** * 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法 */ export async function mount(props) { console.log('[vue] props from main framework', props); render(props); } /** * 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例 */ export async function unmount() { instance.$destroy(); instance.$el.innerHTML = ''; instance = null; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53打包配置修改(
vue.config.js
):
module.exports = {
devServer:{
port:3000,
// 关闭主机检查,使微应用可以被 fetch
disableHostCheck: true,
// 允许跨域
headers:{
'Access-Control-Allow-Origin':'*'
}
},
configureWebpack:{
output:{
library:'vueApp',
libraryTarget:'umd',
jsonpFunction: `webpackJsonp_vueApp`
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Actions通信
官方提供的应用间通信方式 - Actions
通信,这种通信方式比较适合业务划分清晰,应用间通信较少的微前端应用场景。
# 通信原理
qiankun
内部提供了 initGlobalState
方法用于注册 MicroAppStateActions
实例用于通信,该实例有三个方法,分别是:
setGlobalState
:设置globalState
- 设置新的值时,内部将执行浅检查
,如果检查到globalState
发生改变则触发通知,通知到所有的观察者
函数。onGlobalStateChange
:注册观察者
函数 - 响应globalState
变化,在globalState
发生改变时触发该观察者
函数。offGlobalStateChange
:取消观察者
函数 - 该实例不再响应globalState
变化。
主应用
首先,我们在主应用中注册一个 MicroAppStateActions
实例并导出,代码实现如下:
// qiankun-base/src/shared/actions.ts
import { initGlobalState } from "qiankun";
const initialState = {};
const actions = initGlobalState(initialState);
export default actions;
2
3
4
5
6
7
8
在注册 MicroAppStateActions
实例后,我们在需要通信的组件中使用该实例,并注册 观察者
函数,我们这里以登录功能为例,实现如下:
// qiankun-base/src/views/Login.vue
import actions from "@/shared/actions";
export default {
data() {
return {};
},
mounted() {
// 注册一个观察者函数
actions.onGlobalStateChange((state, prevState) => {
// state: 变更后的状态; prevState: 变更前的状态
console.log("主应用观察者:token 改变前的值为 ", prevState.token);
console.log(
"主应用观察者:登录状态发生改变,改变后的 token 的值为 ",
state.token
);
});
},
methods: {
login() {
// 登录成功后,设置 token
actions.setGlobalState({ token: "dasdasbfsdf" });
this.$router.push("/home");
},
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
微应用
我们已经完成了主应用的登录功能,将 token
信息记录在了 globalState
中。现在,我们进入子应用,使用 token
获取用户信息并展示在页面中。
我们首先来改造我们的 Vue
子应用,首先我们设置一个 Actions
实例,代码实现如下:
// qiankun-vue/src/shared/actions.vue
function emptyAction() {
// 警告:提示当前使用的是空 Action
console.warn("Current execute action is empty!");
}
class Actions {
// 默认值为空 Action
actions = {
onGlobalStateChange: emptyAction,
setGlobalState: emptyAction
};
/**
* 设置 actions
*/
setActions(actions) {
this.actions = actions;
}
/**
* 映射
*/
onGlobalStateChange(...args) {
return this.actions.onGlobalStateChange(...args);
}
/**
* 映射
*/
setGlobalState(...args) {
return this.actions.setGlobalState(...args);
}
}
const actions = new Actions();
export default actions;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
我们创建 actions
实例后,我们需要为其注入真实 Actions
。我们在入口文件 main.js
的 render
函数中注入,代码实现如下:
// qiankun-vue/src/main.js
//...
/**
* 渲染函数
* 主应用生命周期钩子中运行/子应用单独启动时运行
*/
function render(props) {
if (props) {
// 注入 actions 实例
actions.setActions(props);
}
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? "/vue" : "/",
mode: "history",
routes,
});
// 挂载应用
instance = new Vue({
router,
render: (h) => h(App),
}).$mount("#app");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
从上面的代码可以看出,挂载子应用时将会调用 render
方法,我们在 render
方法中将主应用的 actions
实例注入即可。
最后我们在子应用的 通讯页
获取 globalState
中的 token
,使用 token
来获取用户信息,最后在页面中显示用户信息。代码实现如下:
// 引入 actions 实例
import actions from "@/shared/actions";
export default {
name: "HelloWorld",
props: {
msg: String,
},
data() {
return {
token: "",
};
},
mounted() {
// 注册观察者函数
// onGlobalStateChange 第二个参数为 true,表示立即执行一次观察者函数
actions.onGlobalStateChange((state) => {
const { token } = state;
console.log(token);
this.token = token;
// 未登录 - 返回主页
if (!token) {
console.log("未检测到登录信息!");
window.location.href = "http://192.169.1.151:8080";
}
}, true);
},
methods: {
// 在主应用的某个组件中用setGlobalState更改state值
send() {
let token = "小明修改了token";
actions.setGlobalState({ token });
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
从上面的代码可以看到,我们在组件挂载时注册了一个 观察者
函数并立即执行,从 globalState/state
中获取 token
,然后使用 token
获取用户信息,最终渲染在页面中。