微前端改造
这里以Vue3为例子,主应用和子应用均使用vue3
路由的话,建议主应用和子应用使用相同模式,即均为history或者均为hash
以下先使用为history模式讲解,最后会写如何使用hash模式。
History模式
主应用改造
一般情况下,我们会将带导航的layout
的部分,直接放在主应用中。当然不是说不能拆,是能拆的,因为导航的layout
明显是个路由不敏感部分,完全可以拆解为单独的子应用。
1.添加qiankun
yarn add qiankun
2.vue.config.js
其实没什么要改的,但我这还是建议把这两个加上
module.exports = {
publicPath: '/main/',
// 修改打包名
outputDir: 'main',
};
3.router.js
const router = createRouter({
// 这里就是publicPath了
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;
4.加载微应用的改造,可以在src下建一个micro目录
|----index.js //注册
|----store.js //应用间通信
|----subapps.js //配置信息
4.1 index.js
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,建议搜搜看,很有意思,当然这是题外话。)
并将这个响应式用于页面展示。
// 应用间通信
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
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意思是给子应用获取的对象,意味着有些东西,可以从主应用往下传递给子应用。这里我传递了两个值
routeBasePath:子应用的路由地址前缀,在history模式下用于填写子应用的basepath,非常有用
mainRouter:主应用的router,在history模式下应用间跳转非常有用
这个东西我们还可以做点有意思的操作,比如:无星的微前端之旅(四)——qiankun线上服务代理到本地
5.提供挂载节点#sub-apps
我们在路由定义中,把所有子应用的components都匹配到一个View中。
这个没什么写代码的意义,截个图直接掠过。
这个view提供一个dom节点用于后续挂载和启动
如何在主应用的某个路由页面加载微应用
这里有一个需要注意的点。不是js代码,而是css。
主应用加载子应用的时候,会新增一个qiankun的div盒子,可能这个盒子会影响样式,导致撑不开。所以需要使用css选择器让qiankun注入的盒子加一些css进行改变以达到预期效果。
6.main.js修改
改造前:
// 默认生成的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');
改造后:
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添加核心配置
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
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
它的作用,点击标题查看文档
3.src文件夹下新建一个micro/store.js,用于应用间通信相关
// 全局数据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改造
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改造
改造前:
// 默认生成的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');
改造后:
// 注意哦,这一行引入不要忘记了,要加载最上面
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全部修改为
module.exports = {
publicPath: './',
}
2.主应用和子应用中router修改为
const setupRouter = () => createRouter({
history: createWebHashHistory(),
routes,
});
3.主应用subapps.js修改为
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;
修改完毕。
至此,主应用和子应用均修改完毕。