为微前端Hack Vue Router
这几天研究一下Vue,看看在Vue中如何实现微前端。期间发现Vue Router v3比较坑:
- 只支持动态加载从根路由(parent)开始的嵌套路由,这限制了有路由的微前端颗粒必需从根路由开始。没有路由的微前端,不受这影响。😟
- 在导航守卫router.beforeEach之前,Vue Router已经闭包缓存本次的匹配路由,造成router.beforeEach中动态加载新路由也无法立即匹配到。
坑1,不好解决,等待Vue Router v4出炉就好,暂时使用大颗粒的含路由微前端。坑2,我们需要在router.beforeEach之前加载微前端APP相关的JS与路由注册。本文就介绍解决坑2的简单方法。
1. Hack Vue Router v3
翻开Vue Router v3的源码,我们可以发现:页面初始化或点击<router-link>
时,均调用到router.history.transitionTo()
函数之后才开始处理路由匹配。我们只需包装一下transitionTo()方法即可完成微前端资源加载。
原理代码如下:
- 创建router实例时,替换其history.transitionTo方法。
- 微前端APP提供模块初始化函数。
// router/index.ts
export function createRouter() {
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes,
});
const { history } = router as any;
const originalTransitionTo = history.transitionTo as TransitionTo;
const transitionTo: TransitionTo = function transitionTo(location, onComplete?, onAbort?) {
const path = (typeof location === 'string') ? location : (location.path as string);
if (path.match(/^\/lazy(\/.*)?$/)) {
import(/* webpackChunkName: "lazy" */ '../views/lazy').then(({ initModule }) => {
const { app } = router;
initModule({
app, router, store: app.$store,
});
originalTransitionTo.call(history, location, onComplete, onAbort);
}, (err) => {
console.error('[ERROR] Lazy routes failed.', err);
if (onAbort) {
onAbort(err);
}
});
} else {
originalTransitionTo.call(history, location, onComplete, onAbort);
}
};
history.transitionTo = transitionTo;
return router;
}
// lazy/index.ts
import routes from './routes';
import LazyPage from './LazyPage.vue';
import { InitModule } from '../../router/types';
import lazyModule from './store';
const initModule: InitModule = ({ router, store }) => {
if (!store.hasModule('lazy')) {
store.registerModule('lazy', lazyModule, { preserveState: !!store.state.lazy });
router.addRoutes(routes);
}
};
export {
initModule,
};
export default LazyPage;
2. 迁移到微前端框架
真上微前端时,上面的代码需要提炼成可配置或注册的。
if (path.match(/^\/lazy(\/.*)?$/)) {
import(/* webpackChunkName: "lazy" */ '../views/lazy')
以上两行代码改成从微前端框架中查询:
// router/index.ts
const matchedMicro = microRegister.matchByRoute(path);
if (matchedMicro) {
matchedMicro.loadResource().then(({ initModule }) => ...
核心又变成matchedMicro.loadResource()
😄
关键点是非Webpack异步加载JS/CSS、注册initModule方法、再用APP ID查出initModule方法。
同时,微前端APP中也需要添加注册initModule方法的代码:
// my-app/index.ts
import { microRegister } from 'some-micro-frontend-framework';
...
microRegister.registerInitMethod('my-app-id', initModule);
前端对每KB的资源都关注,微前端没有通用的运行时框架。要上微前端的每个团队,都需要熟悉代码的提炼。