软件技术学习笔记

个人博客,记录软件技术与程序员的点点滴滴。

为微前端Hack Vue Router

这几天研究一下Vue,看看在Vue中如何实现微前端。期间发现Vue Router v3比较坑:

  1. 只支持动态加载从根路由(parent)开始的嵌套路由,这限制了有路由的微前端颗粒必需从根路由开始。没有路由的微前端,不受这影响。😟
  2. 在导航守卫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()方法即可完成微前端资源加载。

原理代码如下:

  1. 创建router实例时,替换其history.transitionTo方法。
  2. 微前端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的资源都关注,微前端没有通用的运行时框架。要上微前端的每个团队,都需要熟悉代码的提炼。