我的GitHub
0%

无星的微前端之旅(三)——qiankun改造

微前端改造

这里以Vue3为例子,主应用和子应用均使用vue3

路由的话,建议主应用和子应用使用相同模式,即均为history或者均为hash

以下先使用为history模式讲解,最后会写如何使用hash模式。


History模式

主应用改造

一般情况下,我们会将带导航的layout的部分,直接放在主应用中。当然不是说不能拆,是能拆的,因为导航的layout明显是个路由不敏感部分,完全可以拆解为单独的子应用。

1.添加qiankun

1
yarn add qiankun

2.vue.config.js

其实没什么要改的,但我这还是建议把这两个加上

1
2
3
4
5
6
module.exports = {
publicPath: '/main/',
// 修改打包名
outputDir: 'main',
};

3.router.js

1
2
3
4
5
6
const router = createRouter({
// 这里就是publicPath了
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;

4.加载微应用的改造,可以在src下建一个micro目录

1
2
3
|----index.js     //注册
|----store.js //应用间通信
|----subapps.js //配置信息

4.1 index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { registerMicroApps } from 'qiankun';
import subapps from './subapps';
// 判断是否以某字符串开头
// 注册并加载
function register() {
// 注册微应用
registerMicroApps(subapps, {
beforeLoad: (app) => console.log('before load', app.name),
beforeMount: [
(app) => console.log('before mount', app.name),
],
afterMount: [
(app) => console.log('before mount', app.name),
],
beforeUnmount: [
(app) => console.log('before mount', app.name),
],
afterUnmount: [
(app) => console.log('before mount', app.name),
],
});
}

export default register;

4.2 store.js

应用间通信,qiankun提供了一个简单的apiinitGlobalState

但是这玩意不是“响应式”的,换句话说,它改变不会引起页面变化。

得益于vue3提供的reactive,我们可以很方便的构造一个响应式。
(同时也因为非常方便,所以也有很多大佬喊出了不再需要vuex,建议搜搜看,很有意思,当然这是题外话。)

并将这个响应式用于页面展示。

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
// 应用间通信
import { initGlobalState } from 'qiankun';
import { reactive } from 'vue';

// 初始化state,加reative使其变为响应式
const initialState = reactive({
token: '123',
});

const actions = initGlobalState(initialState);

// 监听全局变化
actions.onGlobalStateChange((newState, oldState) => {
console.log('主应用监听', '变化前', oldState, '变化后', newState);
Object.keys(newState).forEach((key) => {
initialState[key] = newState[key];
});
}, true);

// 获取globalState
function getGlobalState(key) {
return key ? initialState[key] : initialState;
}

// 更新通知所有微应用
function setGlobalState(globalState) {
actions.setGlobalState(globalState);
}
// 卸载全局变化
function offGlobalStateChange() {
actions.offGlobalStateChange();
}

export default {
actions,
getGlobalState,
setGlobalState,
offGlobalStateChange,
};

4.2 subapps.js

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
import router from '@/router/index';
// 此时在开发,测试环境,理论上已经挂载了subapps的入口
const baseUrl = process.env.BASE_URL;

const subapps = [
{
name: 'sub-login',
entry: process.env.VUE_APP_SUB_LOGIN,
container: '#sub-apps',
activeRule: `${baseUrl}sub-login`,
props: {
routeBasePath: '/sub-login',
mainRouter: router,
},
},
{
name: 'sub-user-manage',
entry: process.env.VUE_APP_SUB_USER_MANAGE,
container: '#sub-apps',
activeRule: `${baseUrl}sub-user-manage`,
props: {
routeBasePath: '/sub-user-manage',
mainRouter: router,
},
},
];

export default subapps;

name,entry,container,activeRule

在第上一篇已经介绍过了

这里多了一个props,props意思是给子应用获取的对象,意味着有些东西,可以从主应用往下传递给子应用。这里我传递了两个值

1
2
3
routeBasePath:子应用的路由地址前缀,在history模式下用于填写子应用的basepath,非常有用

mainRouter:主应用的router,在history模式下应用间跳转非常有用

这个东西我们还可以做点有意思的操作,比如:无星的微前端之旅(四)——qiankun线上服务代理到本地

5.提供挂载节点#sub-apps

我们在路由定义中,把所有子应用的components都匹配到一个View中。

这个没什么写代码的意义,截个图直接掠过。

1

这个view提供一个dom节点用于后续挂载和启动

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
<template>
<div id="sub-apps" />
</template>
<script>
import Actions from '@/micro/store';
import {
defineComponent, onMounted, onUnmounted,
} from 'vue';
import { start } from 'qiankun';

export default defineComponent({
name: 'SubApps',
setup() {

onMounted(async () => {
console.log('Subapps页面加载');
if (!window.qiankunStarted) {
window.qiankunStarted = true;
start();
}
});
onUnmounted(() => {
if (window.qiankunStarted) {
window.qiankunStarted = false;
Actions.offGlobalStateChange();
}
});
return {
};
},
});
</script>
<style lang="less">
#sub-apps {
height: 100%;
>div:first-child {
height: 100%;
}
}
</style>

如何在主应用的某个路由页面加载微应用

这里有一个需要注意的点。不是js代码,而是css。

主应用加载子应用的时候,会新增一个qiankun的div盒子,可能这个盒子会影响样式,导致撑不开。所以需要使用css选择器让qiankun注入的盒子加一些css进行改变以达到预期效果。

1

6.main.js修改

改造前:

1
2
3
4
5
6
7
8
// 默认生成的main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

createApp(App).use(store).use(router).mount('#app');

改造后:

1
2
3
4
5
6
7
8
9
10
11
12
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
// 注册
import register from './micro/index';
// 添加全局通信
import '@/micro/store';

createApp(App).use(store).use(router).mount('#app');
// 注册
register();

到此为止,主应用改造完毕。


子应用改造

1.vue.config.js添加核心配置

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
const packageName = require('./package.json').name;

module.exports = {
// 先写为/,后续会修改
publicPath: '/',
// 输出目录重命名为项目名称,方便后期部署
outputDir: packageName,
// 来自qiankun文档的修改
configureWebpack: {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
},
devServer: {
open: true,
// 建议添加端口,不同模块加载不同端口,方便开发制定加载
port: 3001,
// 必须添加,qiankun需要支持跨域
headers: {
'Access-Control-Allow-Origin': '*',
},
},
};

2.添加public-path

1
2
3
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

它的作用,点击标题查看文档

3.src文件夹下新建一个micro/store.js,用于应用间通信相关

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
// 全局数据store
import { reactive } from 'vue';

let actions = null;
const initialState = reactive({});
let routeBasePath = '';
function setActions(tmpActions) {
// 如果有兴趣,还可以把这里的初始化和vuex关联起来
actions = tmpActions;
actions.onGlobalStateChange((newState, oldState) => {
console.log('子应用监听', '变化前', oldState, '变化后', newState);
Object.keys(newState).forEach((key) => {
initialState[key] = newState[key];
});
}, true);
}

function setGlobalState(state) {
return actions.setGlobalState(state);
}
function getActions() {
return actions;
}

function getGlobalState(key) {
return key ? initialState[key] : initialState;
}
// 基础数据
function setRouteBasePath(path) {
routeBasePath = path;
}
// 基础数据
function getRouteBasePath() {
return routeBasePath;
}

export default {
setActions,
getActions,
setGlobalState,
getGlobalState,
setRouteBasePath,
getRouteBasePath,
};

4.router改造

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
import { createRouter, createWebHistory } from 'vue-router';
import Actions from '@/micro/store';

const routes = [
{
path: '/',
name: 'Login',
component: () => import('../views/Login/index.vue'),
},
{
path: '/register',
name: 'Register',
component: () => import('../views/Register/index.vue'),
},
{
path: '/config',
name: 'Config',
component: () => import('../views/Config/index.vue'),
},
];

const setupRouter = () => createRouter({
// 获取来自主应用的前缀
history: createWebHistory(Actions.getRouteBasePath()),
routes,
});

export default setupRouter;

5.main.js改造

改造前:

1
2
3
4
5
6
7
8
// 默认生成的main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';

createApp(App).use(store).use(router).mount('#app');

改造后:

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
// 注意哦,这一行引入不要忘记了,要加载最上面
import './public-path';
import { createApp } from 'vue';
import Actions from '@/micro/store';

import App from './App.vue';
import store from './store';
import setupRouter from './router';

let instance = null;
let router = null;
function render(props = {}) {
const { container, routeBasePath, mainRouter } = props;
// 这里需要注意,往下注入的必须和子打包配置一致
Actions.setRouteBasePath(routeBasePath || process.env.BASE_URL);
router = setupRouter();
instance = createApp(App);
instance.use(router);
instance.use(store);

router.isReady().then(() => {
instance.mount(container ? container.querySelector('#app') : '#app');
});
}

if (!window.__POWERED_BY_QIANKUN__) {
// 如果是非乾坤访问,意思是子应用单独访问
render();
}

export async function bootstrap() {
console.log('%c ', 'color: green;', 'vue3.0 app bootstraped');
}

export async function mount(props) {
// 获取props中的全局通信
Actions.setActions(props);
// 只有从qiankun访问,才会到这个生命周期
render(props);
}

export async function unmount() {
// 这里千万不要忘记置空!!!!
instance.unmount();
instance._container.innerHTML = '';
instance = null;
router = null;
}

好了,子应用到此就改造完毕了。


Hash

如果是hash模式,那么需要变动的地方就比较多了

1.主应用和子应用中vue.config.js全部修改为

1
2
3
module.exports = {
publicPath: './',
}

2.主应用和子应用中router修改为

1
2
3
4
5

const setupRouter = () => createRouter({
history: createWebHashHistory(),
routes,
});

3.主应用subapps.js修改为

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
import router from '@/router/index';
// 此时在开发,测试环境,理论上已经挂载了subapps的入口
const baseUrl = '#/';
const getActiveRule = (hash) => (location) => location.hash.startsWith(hash);
const subapps = [
{
name: 'sub-login',
entry: process.env.VUE_APP_SUB_LOGIN,
container: '#sub-apps',
activeRule: getActiveRule(`${baseUrl}sub-login`),
props: {
routeBasePath: '/sub-login',
mainRouter: router,
},
},
{
name: 'sub-user-manage',
entry: process.env.VUE_APP_SUB_USER_MANAGE,
container: '#sub-apps',
activeRule: getActiveRule(`${baseUrl}sub-login`),
props: {
routeBasePath: '/sub-user-manage',
mainRouter: router,
},
},
];

export default subapps;

修改完毕。

至此,主应用和子应用均修改完毕。

我是阿星,阿星的阿,阿星的星!