从源码分析为什么electron+vue项目中router不能用history模式

826 阅读4分钟

在 electron+vue 项目中,vue-router 采用 history 模式在开发环境下能够正常显示页面,但是打包后发现页面 app 节点中的内容为空,页面空白

<div id="app" data-v-app=""><!----></div>

查资料后得知,electron+vue 应用在生产环境下 vue-router 不支持 history 模式,换成 hash 模式后才解决问题,这篇文章主要分析记录下为什么 electron+vue 项目中 router 不能用 history 模式?如果不想看分析过程可直接看结尾的结论。

该文章中所使用的的 vue-router 版本是 4.1.2

调试 vue-router 源码

这里我们先修改下 vite.config.ts

build: {
  minify: false; // 设置为 false 可以禁用最小化混淆 方便在生产环境直接调试
}

从项目中的 vue-router 入口开始分析

  1. createWebHistory()

20220721204828 该方法用于创建 HTML5 历史记录。单页应用程序最常见的历史记录,返回一个 routerHistory 对象

  1. createRouter()

20220722091953 该方法创建一个可以被 vue-app 使用的路由实例,返回一个 router 对象。

  1. app.use(router)

20220722103338
在该方法中参数 plugin 就是第二步中返回的 router 对象,该对象中有 install 方法,直接执行 plugin.install(app2,...options)

20220722092507
可以看到 install 方法做了一下事情:

  • 全局注册 RouterLink 和 RouterView 组件
  • 添加全局属性router(Router实例对象本身),router(Router实例对象本身),route(当前 location 对应的路由对象)
  • 首次执行 install,则通过 push 方法跳转到 url 对应的路由
  • 注入三个 provider
  • 拦截 vue 实例的 unmount 方法,在 unmount 方法调用之前,先执行 Router 相关事件的移除和状态恢复工作

这里重点分析 push 方法跳转路由,按下 F11 进入 push 方法 20220722094219
继续进入 pushWithRedirect(to)方法 20220722094744
这里 failure 是 undefined,执行的是 navigate(toLocation,from),该方法也异步的,返回一个 Promise。根据浏览器事件循环机制(event-loop),javascript 遇到异步函数先将异步函数加入异步任务队列,再继续执行接下来的同步任务。所以这里又回到 install 方法继续往下执行,install 方法执行完后开始执行 app.mount('#app')

  1. app.mount('#app')

这里开始执行挂载操作

const proxy = mount(container, false, container instanceof SVGElement);

从根组件开始创建 vnode

const vnode = createVNode(rootComponent, rootProps);

这里 rootComponent 是 app.vue 的编译结果

渲染 vnode

render2(vnode, rootContainer, isSVG);

新旧节点对比

patch(container._vnode || null, vnode, container, null, null, null, isSVG);

处理组件

processComponent(
  n1,
  n2,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  slotScopeIds,
  optimized
);

挂载组件

mountComponent(
  n2,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
);

创建组件实例

const instance = (initialVNode.component = createComponentInstance(
  initialVNode,
  parentComponent,
  parentSuspense
));

做组件实例对象的初始化工作

setupComponent(instance);
// ...
setupRenderEffect(
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
);

调用 renderComponentRoot 生成子节点 vnode

const subTree = (instance.subTree = renderComponentRoot(instance));
// ...
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
// ...
processComponent(
  n1,
  n2,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  slotScopeIds,
  optimized
);
// ...
mountComponent(
  n2,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
);
// ...
setupComponent(instance);

执行子节点的 setup 方法,setupResult 就是 RouterView 组件的 setup 方法的返回值

const setupResult = callWithErrorHandling(setup, instance, 0, [
  instance.props,
  setupContext,
]);

20220722151315

将 setupResult 赋值给子节点组件实例的 render 属性 20220722151742

setupRenderEffect(
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  isSVG,
  optimized
);
//...
const subTree = (instance.subTree = renderComponentRoot(instance));

又调用 renderComponentRoot 生成子节点 20220722152243
此时 render2 是 RouterView 组件的 setup 的返回值 返回值 result 是空节点类型的 vnode,因为 router-view 组件中没有子节点 20220722154230

同步函数执行完后开始执行异步队列中的函数 20220722155003

failure2 = finalizeNavigation(toLocation, from, true, replace2, data);

注意该函数中的 toLocation 20220722155422
这里将传入的 toLocation 赋值给 currentRoute.value

组件挂载完后又执行const nextTree = renderComponentRoot(instance); 20220722155728
此时 render2 是 RouterView 组件的 setup 的返回值 20220722160008
结果发现 matchedRoute 为 undefined,导致 ViewComponent 也为 undefined,匹配到的路由组件为 undefined,在接下来的 patch 函数中将注释节点插入<div id="#app"></div>中,最终导致页面空白。

为何 matchRoute 为 undefined

app2.provide(routerViewLocationKey, currentRoute);
const injectedRoute = inject(routerViewLocationKey);
const routeToDisplay = computed(() => props.route || injectedRoute.value);
const matchedRouteRef = computed(
  () => routeToDisplay.value.matched[depth.value]
);

由于 currentRoute 的值由初始值
20220722162956
改为
20220722163038
由上述计算属性计算结果得知 matchedRoute 为 undefined

结论

打包后的 electron 包用的是本地文件通过 file 协议访问,在 vue-router 用 history 模式下 location.pathname 并不是常规的 history 路由而是包含文件绝对路径 20220722164615 源码中 matchers 为当前定义的所有路由的路径解析器
20220722165241
而 path 却包含文件绝对路径导致无法匹配路由,最终导致无路由组件可渲染,页面空白。

// 获取当前路径匹配的路由
matcher = matchers.find((m) => m.re.test(path));

啊,终于分析完了:)