掘友的一个bug,害我重新阅了一遍vue-router源码
大家好,我是苏先生,一名热爱钻研、乐于分享的前端工程师,跟大家分享一句我很喜欢的话:人活着,其实就是一种心态,你若觉得快乐,幸福便无处不在
github与好文
背景
最近在刷jj圈子的时候,看到这么一个求助帖子
这勾起了我的好奇心,因为我似乎还没遇到过这种情况,于是我就联系了这位jy,拿到了他的实现代码
问题复现
于是,我找到我之前开发的页面,并同样使用query传递一个具有嵌套结构的对象
此时页面跳转,在目标路由页面下打印如下
然后我们刷新当前页面,问题复现
复现版本
vue-router@3.6.5
尝试修复
现在,我们根据该帖子下边热心jy给出的方案来尝试进行下修复
尝试一:
我们在传递参数之前,使用JSON.stringify转一下,对应的,在目标路由页面下使用JSON.parse,此时刷新后也能正常拿到,方案可行👍
所以,为什么传递嵌套对象在刷新时候会有问题呢?
尝试二:
现在我们使用params来替换query,这里我就不尝试了,因为params刷新会丢失参数,那么问题来了,params和query传参有什么区别呢?
源码分析
刷新后为什么对象参数会变成"[object object]"
刷新时的query取值
由于浏览器的刷新行为会导致页面重载,因此会重新进入vue-router
的初始化流程,而在初始化阶段,会进行一次路由导航
history.transitionTo(
history.getCurrentLocation(),
...
);
所以我们进入到history.getCurrentLocation
的方法定义
HashHistory.prototype.getCurrentLocation = function getCurrentLocation () {
return getHash()
};
接着,进入到getHash
的方法定义,如下,它将当前浏览器的url作为了默认导航的跳转路径
function getHash () {
var href = window.location.href;
var index = href.indexOf('#');
if (index < 0) { return '' }
href = href.slice(index + 1);
return href
}
然后通过match方法从生成的路由列表中匹配当前的路由,其中location
即上一步获取的href
route = this.router.match(location, this.current);
接着,我们进入match函数,它将首先对location进行normalize,可以看到,这里会提取出query并返回,而关键的代码就是parsePath函数
function normalizeLocation(...) {
...
var parsedPath = parsePath(next.path || '');
...
return {
...
query: query,
...
}
}
从parsePath的实现上来看,它就是先将href从#断开,然后取?后边的部分作为query参数
function parsePath (path) {
var hash = '';
var query = '';
var hashIndex = path.indexOf('#');
if (hashIndex >= 0) {
hash = path.slice(hashIndex);
path = path.slice(0, hashIndex);
}
var queryIndex = path.indexOf('?');
if (queryIndex >= 0) {
query = path.slice(queryIndex + 1);
path = path.slice(0, queryIndex);
}
return {
path: path,
query: query,
hash: hash
}
}
而从浏览器获取到的query本身就是一个字符串
事情好像是水落石出了,但是又感觉缺了点什么,通过编程式跳转路由时,页面地址也是一样的啊
于是我们合理又大胆的猜测:编程式导航不会从页面地址进行参数解析!!!
...
跳转时的query取值
不过,口说无凭,我们还是来进行下验证
首先,本次的导航行为并不被vue-router默认执行,即,它执行的是push
函数,我们找到该函数定义,可以看到,还是调用的transitionTo,但是此时的location不再是从页面地址读取了,而是使用我们传递的对象,即:{path:"路由地址",query:"路由参数"}
HashHistory.prototype.push = function push (location, ...) {
...
this.transitionTo(
location,
...
);
};
换句话说,如果对该location执行parsePath调用,则一定获取不到query
实际上,当它从path中解析不出来的时候,就会降级使用我们页面传递的query
var query = location.query
总结
综上所述,两次路由跳转执行的是同一套逻辑,只是入参不同,vue-router
内部会优先从path中进行解析,如果找不到则会使用我们在跳转路由时传递的query字段作为参数
params和query的区别
现在,我们来分析,为什么params字段刷新后会丢失参数?其实这从前文“刷新时的query取值”中就能看出一二:因为params传递的参数不会被拼接到页面的url地址上,所以刷新后,既不能从页面地址解析出,又无法从跳转参数中取到
那么是什么导致的一个拼接到页面一个又不会拼接呢?
首先,我们找到导致页面url地址改变的最底层代码
function pushState (url, replace) {
...
var history = window.history;
if (replace) {
...
history.replaceState(stateCopy, '', url);
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url);
}
}
可以看到,它执行的是window.history.pushState
,并且从该接口的文档说明中,我们没有找到单独设置网页地址参数相关的参数,那就意味着,query和params的处理是vue-router
内部进行区别对待去拼接到url的
所以我们把重点锁定在path的处理逻辑上:
在vue-router
内部在normalize完路由地址后,会根据地址去调用_createRoute生成路由对象
_createRoute(record$1, location, redirectedFrom)
它们两个最终都会执行getFullPath
获取最终的路由跳转地址,如下,可以看到之针对query对象进行了拼接,而对于params则直接忽视
function getFullPath (...) {
...
var query = ref.query; if ( query === void 0 ) query = {};
...
return (path || '/') + stringify(query) + hash
}
总结
本文对jy的一个bug进行了复现并从源码角度分析了产生的原因,并由此引出了vue-router
中query和params传参的差异并分析了其导致该差异的源码层面的实现
如果本文对您有用,希望能得到您的点赞和收藏
订阅专栏,每周更新2-3篇类型体操,每月1-3篇vue3源码解析,等你哟😎
转载自:https://juejin.cn/post/7246037930325491770