vue-router重复点击报错和新链接跳转失败

- 技术 programming

最近解决了项目的前端路由问题,具体而言有两个:

由于我们公司内外网隔离,所以无法具体截图现象。

路由重复点击,会在控制台输出报错

报错的大概内容为: vueAll.js?v=3.0:2 Uncaught (in promise) NavigationDuplicated: Avoided redundant navigation to current location: xxxx

阅读 vue-router 的文档,可以看到

router.push 或 router.replace 将返回一个 Promise

查看 vue-router 源码,从源码此处可以看出,我们对此方法进行异常处理,便可一劳永逸解决此问题。

vue-router-push.png

因此在初始化路由时,对此方法进行处理:

import VueRouter from 'vue-router';

function handleRouterErr (methodsList) {
    methodsList.forEach(item => {
        const ORIGIN_METHOD = VueRouter.prototype[item];

        VueRouter.prototype[item] = function (location) {
            let vm = this;
            return ORIGIN_METHOD.call(vm, location).catch(err => err);
        }
    });
}

// 调用
handleRouterErr(['push', 'replace'])

无法在新标签页打开链接

右键一个 vue-router 渲染的 a 标签,新标签页打开后页面显示 Not Found

base配置不正确

首先,我发现我们所有的页面都是 ip/index#xxxx 的形式,但是此时打开的链接确是 ui/#/#/xxxx 的形式。也就是说渲染出来的a标签的href是不正确的。因此很容易想到我们路由配置有问题。

路由是这样写的:

router = new VueRouter({
    routes: ROUTER_DATA,
    base: '/ui/#'
});

那显然这个 base 是不对的,期望最后渲染的href应当是 index#xxx 的形式,所以这里应该将base改成 /index/

为什么这里不需要 # 符号了?因为查看 vue-router 代码可以看到,构造 href 逻辑如下:

function createHref (base: string, fullPath: string, mode) {
  var path = mode === 'hash' ? '#' + fullPath : fullPath
  return base ? cleanPath(base + '/' + path) : path
}

mode 默认为 hash,所以会自动添加 # 号,这也解释了为何新标签页的那个链接存在两个 #。

我们的服务器上并没有 ui 这个目录,所以自然返回 404。

新标签页打开报错

当修改完base后,这次新标签页打开的链接似乎正常了:ip/index/#/xxxx,但是打开页面白屏,控制台报错了:uncaught syntaxerror unexpected token '<'

仔细一看,发现此时链接其实还是不正确的,相比于正确的链接,在index后面多了一个 /。为何多了这个会导致报错?

我选择一个报错的js文件请求,从请求中可以看到两个问题:

寻其根本,还是第一个请求不正确的问题导致的。

这些报错文件我看了一下,是在 webpack 打包后的 index.html 中引入的,在这个 html 文件中,都是以相对路径 ./static/xxx 引入的。于是我想这里应该改成绝对路径,于是我将webpack配置中的 relativePublicPath 设置为 true,重新生成打包文件后,问题就解决了。

更简单的解决方式

我上面的解决方式改了三个地方:

组长觉得应该不需要这样改,他搜索了 base 相关信息,发现大家都不配置 base,于是和我说删掉 base 这个配置试试。我一试,还真就解决新标签页跳转问题了。

那么为什么这种修改方式是可以的?

首先还是回到链接身上观察一下,在不修改 base 的情况下:

然后再来看 vue-router 构造 href 的规则:

function createHref (base: string, fullPath: string, mode) {
  var path = mode === 'hash' ? '#' + fullPath : fullPath
  return base ? cleanPath(base + '/' + path) : path
}

我们项目的 base 配置是 /ui/#,mode 默认为 hash ,fullPath 是 router-link 中设置的 to 参数,那么此时 path 就是 #/{fullPath},我们配置了 base,那么最终结果就是 /ui/#/#/{fullPath}。路由链接发生了改变,浏览器会重新请求资源,但是服务器上并没有 /ui/ 这个文件导致无法返回正确的页面。

此时我们把base删去,那么整个 createHref 的结果就是 #/{fullPath}。我们改变的仅仅只是 hash 部分,hash 部分并不会被包括在 http 请求中,它是用来指导浏览器动作的,对服务器端没影响,因此,改变 hash 不会重新加载页面,也就是此时此链接点击时,浏览器跳转的实际地址为 ip/index#{fullPath},和我们希望的链接形式是一致的。

删除一行代码就解决了。

那么这种改法是偶然还是通用?

之所以说是不是偶然,因为整个过程看起来似乎是刚好拼成了我们期望的链接形式。对于这个问题,我们需要看看 base 到底是什么。

文档上写的是:

应用的基路径。例如,如果整个单页应用服务在 /app/ 下,然后 base 就应该设为 “/app/”

举个例子:

假设单页应用的入口文件地址是:www.xxx.com/test/index.html,那么 base 应当是 /test/index.html。 如果后端服务在 /test/ 下,那么后端配置 Nginx 配置,将 /test/ 重定向到 /test/index.html,那么我们的 base 此时就是写 /test/。

实际项目:

在我们的项目中,我们访问项目时,整个过程是 ip -> ip/index,从请求中可以看到发送了一个 ip/index 的网络请求,这个请求指向一个 cgi 文件,最终返回了 index.html 文件,那么此时前端代码或者说得更小一点就是 vue-router 开始工作了,渲染出正确的组件。

那么回到最开始的问题:这种改法是偶然还是通用?

答案是通用的。因为我们配置了项目的根路径重定向到 /index,所以这里我们的 base 应该是配置 / ,但是vue-router默认 base 为 /,所以我们可以删去。这么一品,反而也有一些偶然的味道了。