最近项目重构,leader想要前端应用微应用技术,作为前端基石,为今天新旧技术迭代做准备。so,今天开始尝试阿里 qiankun,并搭建一个demo。
在尝试新技术之前,我们需要分别准备一个主应用test-qiankun-main-vue3
,及一个子应用test-qiankun-son-app1
。
公司现在前端技术栈 vue3
+ typeScript
,所以使用 vue-cli 4
脚手架来创建我们的父子应用
$ vue create
现在,为了更好的展示效果,我们现将主应用使用element-plus简单改造一下。
// /userCenter/index.vue 页面
XXXX系统
退出
客户订单
版权所有:xxxxxxx 公司
主菜单已经有了一个自己的模块【客户订单列表】,现在,假设我们有一个新的(或旧的)项目,里面有一个账单模块,我们需要将这个“账单模块”整合到主应用的后台页面中来。(请忽视简陋的页面)
我们分别在主应用和子应用中都安装 qiankun
$ npm i qiankun -S
然后将主引用的 /userCenter/index 复制一份到/userCenter/childApp,略做修改,因为我们需要再该页面加载子应用,所以在该页面注册子应用
XXXX系统
退出
版权所有:xxxxxxx 公司
左侧菜单组件
{{item.name}}
{{son.name}}
主应用路由增加对/childApp/app1
路径的支持
// /route/index.ts
const routes = [
{
path: '/userCenter',
name: 'userCenter',
component: userCenter,
children:[
{
path: '/customerOrderList',
name: 'customerOrderList',
component: () => import('../views/orderManagement/customerOrderList.vue'),
}
]
},
{
path: '/:childApp+',
name: 'childApp',
component: childApp,
},
]
然后是修改子应用
// /router/index.ts
// const router = createRouter({
// history: createWebHistory(process.env.BASE_URL),
// routes
// })
// export default router
export default routes
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
// import router from './router'
import store from './store'
import ElementPlus from 'element-plus'
import '@/assets/style/init.scss'
import '@/assets/style/initElement.scss'
import { createRouter, createWebHistory} from 'vue-router'
import routes from './router'
const isQiankun = window.__POWERED_BY_QIANKUN__;
if (isQiankun) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
let router = null;
let instance:AnyObject|null = null;
function render(props:any = {}) {
const container:any = props.container;
const base:string = isQiankun ? '/childApp/app1/' : process.env.BASE_URL; //如果检测到主应用,则使用在主应用中注册时匹配的baseUrl
router = createRouter({
history: createWebHistory(base),
routes
});
instance = createApp(App);
instance.use(ElementPlus);
instance.use(store);
instance.use(router);
instance.mount(container ? container.querySelector('#app') : '#app');
}
if(!isQiankun) { // 如果不是在qiankun框架下,则单独运行,便于调试
render();
}
// 返回的给qiankun主应用的子应用生命周期钩子
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props:any) {
console.log('[vue] props from main framework', props);
render(props);
}
export async function unmount() {
// 卸载子应用实例的根组件
console.log('[vue] vue app unmount');
if(instance) instance.unmount();
if(instance) instance._container.innerHTML = '';
instance = null;
router = null;
}
vue.config.js
const { name } = require('./package');
module.exports = {
productionSourceMap: true,
devServer: {
port: 8090,
headers: {
'Access-Control-Allow-Origin': '*', //允许跨域
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
注意,主应用加载子应用,是通过请求方式获取的子应用程序相关资源(document、js、css、图片),所以会有跨域问题跨域问题,生产环境需要后台配置子应用允许跨域。
而在开发环境,因为使用的时webpack.devServer服务,配置'Access-Control-Allow-Origin': '*'
即可开启跨域,进行开发调试。
现在,我们打开主应用看看效果
现在我们已经成功在主应用上加载了一个子应用,那么,我们再加一个呢?
假设一下, 我们现在有一个旧的历史项目 test-qiankun-app1
,我们想讲这个项目也加载进主应用中,现在我们来改造一下。
首先,修改一下子应用test-qiankun-app1
项目
/router/index.ts
import Vue from 'vue'
import VueRouter, { RouteConfig} from 'vue-router'
import Home from '../views/Home.vue'
import {Route,NavigationGuardNext} from "vue-router";
Vue.use(VueRouter)
const routes: Array = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
// const router = new VueRouter({
// mode: 'history',
// base: process.env.BASE_URL,
// routes
// })
export default routes
main.ts
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import routes from './router'
import store from './store'
Vue.config.productionTip = false
const isQiankun = window.__POWERED_BY_QIANKUN__;
const appName = "子应用 app1";
if (isQiankun) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
let router = null;
let instance:AnyObject|null = null;
function render(props:any = {}) {
const container:any = props.container;
router = new VueRouter({
base: isQiankun ? '/childApp/app2/' : '/',
mode: 'history',
routes,
});
router.beforeEach((to, from, next) => {
console.log(`----- ${appName} beforeEach()`,`【${from.path}】 => 【${to.path}】`);
next();
});
router.afterEach((to, from) => {
console.log(`----- ${appName} afterEach()`,`【${from.path}】 => 【${to.path}】`);
});
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
console.log(instance)
}
if(!isQiankun) {
render();
}
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props:any) {
console.log('[vue] props from main framework', props);
render(props);
}
export async function unmount() {
if(instance) instance.$destroy();
if(instance) instance.$el.innerHTML = '';
instance = null;
router = null;
}
vue.config.js
const { name } = require('./package');
module.exports = {
devServer: {
port: 8089,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
然后,我们在主应用注册微应的配置项中加入test-qiankun-app1
,将它在主应用中命名为app2
主应用 childApp.vue
TestMenu.vue
展示效果
到这里,出现了一个问题,我们在主应用中打开app2后,点击app2内部的路由跳转后,再点击主应用的路由,主应用的路由无效,且控制台出现警告提示,而app1则没有该问题。
具体错误信息
[Vue Router warn]: Error with push/replace State DOMException: Failed to execute 'replaceState' on 'History': A history state object with URL 'http://192.168.1.111:8080undefined/' cannot be created in a document with origin 'http://192.168.1.111:8080' and URL 'http://192.168.1.111:8080/childApp/app2/'.
at History.eval [as replaceState] (webpack-internal:///./node_modules/single-spa/lib/esm/single-spa.min.js:33:10677)
at changeLocation (webpack-internal:///./node_modules/vue-router/dist/vue-router.esm-bundler.js:566:60)
at Object.push (webpack-internal:///./node_modules/vue-router/dist/vue-router.esm-bundler.js:601:9)
at finalizeNavigation (webpack-internal:///./node_modules/vue-router/dist/vue-router.esm-bundler.js:3202:31)
at eval (webpack-internal:///./node_modules/vue-router/dist/vue-router.esm-bundler.js:3078:27)
控制台警告输出中包含'http://192.168.1.111:8080undefined/'
,根据字面意思,简单推断应该是子应用内部路由变更时,主应用内部路由栈错误记录了一条undefined信息,那么预估错误可能与vue-router有关系。
且只有test-qiankun-app1
出现这种问题,test-qiankun-son-app1
没有该问题,那么,我们先从vue-router的版本入手。
粗略比对一下 主应用、微引用test-qiankun-app1
、微应用test-qiankun-son-app1
的各依赖版本
应用 | 项目名 | vue | vue-router | 是否存在undefined问题 |
---|---|---|---|---|
主应用 | 3.0.0 | 4.0.0-0 | ||
app1 | test-qiankun-son-app1 | 3.0.0 | 4.0.0-0 | 否 |
app2 | test-qiankun-app1 | 2.6.11 | 3.2.0 | 是 |
可以看出,出现问题的微应用app2的vue-router版本与主应用和微应用app1明显不同,且横跨了一个大版本,进一步加深了是vue-router版本导致问题的怀疑。
为什么验证我们的猜想,我们新建一个项目test-qiankun-son-app3
,在主应用中将它注册为微应用app3
,且app3的vue/vue-router版本与主应用一致。
registerMicroApps([
{
name: 'app1',
entry: 'http://192.168.1.111:8090',
container: '#childAppContainer',
activeRule: '/childApp/app1',
},
{
name: 'app2',
entry: 'http://192.168.1.111:8089',
container: '#childAppContainer',
activeRule: '/childApp/app2',
},
{
name: 'app3',
entry: 'http://192.168.1.111:8091',
container: '#childAppContainer',
activeRule: '/childApp/app3',
},
])
接下来,我们演示一下,没有出现路由undefined问题
经过简单的对比后,我们发现,在阿里qiankun微框架下,主应用vue-router版本4.0时,微应用使用vue-router3.x版本时会存在路由undefined问题。
当然,这种论证对比还不够严谨。
正常情况下,因为app2的vue版本与主应用也不一致,我们还需要在app2项目中,将vue-router版本升级到4.0,进行对比,验证不同vue版本下,相同vue-router版本是否会产生该问题。更细致一些,还需要将主应用vue、vue-router版本降级,再使用不同依赖版本的子应用对该问题进行验证。
当前任务紧迫,该事暂时搁置,后续抽时间进行。也欢迎有时间和兴趣验证的同仁与我分享一下结果。
后记
根据阿里qiankun文档对微前端核心价值的定义包括
技术栈无关:主框架不限制接入应用的技术栈,微应用具备完全自主权
而公司本次重构,选择微前端框架作为基石,也是希望几年后公司开发人员流失,亦或新技术迭代维护时,更长的维护老项目的生命。
虽然抱着使用框架一劳永逸的想法,但是在demo阶段,我发现了vue-router版本将导致主、子应用路由undefined问题,才明白即使框架考虑的再周全,在后续的使用中,也无法避免其他技术迭代导致可能的差异。
所以理想和现实还是会有偏差的,而比使用框架更重要的是,身为开发人员自身学习的心。