在 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 入口开始分析
- createWebHistory()
该方法用于创建 HTML5 历史记录。单页应用程序最常见的历史记录,返回一个 routerHistory 对象
- createRouter()
该方法创建一个可以被 vue-app 使用的路由实例,返回一个 router 对象。
- app.use(router)
在该方法中参数 plugin 就是第二步中返回的 router 对象,该对象中有 install 方法,直接执行 plugin.install(app2,...options)
可以看到 install 方法做了一下事情:
- 全局注册 RouterLink 和 RouterView 组件
- 添加全局属性route(当前 location 对应的路由对象)
- 首次执行 install,则通过 push 方法跳转到 url 对应的路由
- 注入三个 provider
- 拦截 vue 实例的 unmount 方法,在 unmount 方法调用之前,先执行 Router 相关事件的移除和状态恢复工作
这里重点分析 push 方法跳转路由,按下 F11 进入 push 方法
继续进入 pushWithRedirect(to)方法
这里 failure 是 undefined,执行的是 navigate(toLocation,from),该方法也异步的,返回一个 Promise。根据浏览器事件循环机制(event-loop),javascript 遇到异步函数先将异步函数加入异步任务队列,再继续执行接下来的同步任务。所以这里又回到 install 方法继续往下执行,install 方法执行完后开始执行 app.mount('#app')
- 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,
]);
将 setupResult 赋值给子节点组件实例的 render 属性
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
);
//...
const subTree = (instance.subTree = renderComponentRoot(instance));
又调用 renderComponentRoot 生成子节点
此时 render2 是 RouterView 组件的 setup 的返回值
返回值 result 是空节点类型的 vnode,因为 router-view 组件中没有子节点
同步函数执行完后开始执行异步队列中的函数
failure2 = finalizeNavigation(toLocation, from, true, replace2, data);
注意该函数中的 toLocation
这里将传入的 toLocation 赋值给 currentRoute.value
组件挂载完后又执行const nextTree = renderComponentRoot(instance);
此时 render2 是 RouterView 组件的 setup 的返回值
结果发现 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 的值由初始值
改为
由上述计算属性计算结果得知 matchedRoute 为 undefined
结论
打包后的 electron 包用的是本地文件通过 file 协议访问,在 vue-router 用 history 模式下 location.pathname 并不是常规的 history 路由而是包含文件绝对路径
源码中 matchers 为当前定义的所有路由的路径解析器
而 path 却包含文件绝对路径导致无法匹配路由,最终导致无路由组件可渲染,页面空白。
// 获取当前路径匹配的路由
matcher = matchers.find((m) => m.re.test(path));
啊,终于分析完了:)