vue router 与微信分享失败
最近在接入微信分享时发现一个奇怪的现象:
首次分享没问题,二次分享总是失败。
具体操作如下:
-
直接在微信扫码打开页面
-
分享 (分享正常,title、content都带上了)
-
点击分享链接打开页面
-
二次分享 (分享失败)
首先查看官方文档,显然是JS-SDK鉴权失败导致的。
在微信中分享时需要先去获取JS-SDK权限验证的签名。这个签名请求的参数中就需要带上当前页面的url。
签名生成规则如下:
参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分)。
对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式拼接成字符串string1。这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义。
而二次分享和首次分享的区别在于——
分享出去的页面后面加上了类似from=singlemessage
、 isappinstalled=1
的参数,在二分分享时验证JS-SDK使用的是加上了微信参数后的地址。
例如,我们最终分享出去的页面地址就加上了微信参数:
但是这不是问题的直接原因,因为我们在每次打开页面时都会动态地用当前页面的url去请求签名,这个url已经带上了微信参数。
那么验证失败只可能有一个原因——我们的url在获得签名后发生了改变。
在微信中打开页面后打印当前页面url验证一下:
发现页面地址真的变了。这里注意到 query string
按照 key 的字母顺序重排了。这个重排动作并不是浏览器做的,因为当我访问别的活动页地址时,并不会有这样的重排。
观察到重排现象后,我开始怀疑vue-router了。果然在issue列表中找到了issues-926(看到掉进坑里的不止我一个,莫名其妙的开心了起来)。
按照 vue-router 的 release log ,在 2.1.0 版本的stringifyQuery
函数中已经去掉了sort动作。
export function stringifyQuery (obj: Dictionary<string>): string { |
升级到最新版本后再次测试果然二次分享ok了。
然而这里的坑并没有结束,只是被隐藏的更深了。
vue-router
在 parseQuery
时将 query 字符串构造为一个字典对象;然后在 history.pushState 时再将这个字典对象stringifyQuery
转化为一个 queryString 字符串。
在序列化和反序列化的过程中,最重要的是保持前后的信息一致。
vue-router
2.1.0 之前的版本由于对Object.keys(obj)
进行sort
导致了顺序信息的丢失,引起序列化前后地址不一致。
那么去掉了sort
操作后的vue-router
能还原顺序信息了吗?
99%的情况下可以,除非 query 中带有 Array 类型的 value…
举例说明:
?foo=bar&baz=qux&foo=bla |
经过vue-router
的parseQuery
后,变成了
{ |
这里转化没问题,再通过 stringifyQuery
转为字符串呢,
?foo=bar&foo=bla&baz=qux |
地址又变了!
拿标准实现 URLSearchParams
测试一下就会发现:在URLSearchParams
上调用toString
不会改变原来的queryString顺序:
var paramsString = "foo=bar&baz=qux&foo=bala"; |
(2) ["foo", "bar"] |
而在vue-router内部,parseQuery
在遇到重复的key
时,会把字典中原有的value转化为数组,再将新的 value push到数组尾部。这里不可避免的丢失了key
数组原来出现在url中的顺序。
归根结底,还是数据结构选取的锅:parseQuery
生成的对象应该是一个 key-value 组成的pair list
而不是dictionary
。
找到思路了,vue-router源码也是现成的,那就改吧。
由于 query 对象是通过$router
对外暴露的,直接改parseQuery
的返回值类型代价太大了。我的思路是给 query 对象增加一个属性。
function parseQuery (query: string): Dictionary<string> { |
在parseQuery()
函数中额外添加_rawQuery
数组属性,为了简单,这里使用了二维数组来模拟pair。按照queryString
key-value 出现的顺序初始化数组。注意:为了不影响原来对Component暴露的query对象,特地将_rawQuery
的enumerable
置为false。
export function stringifyQuery (obj: Dictionary<string>): string { |
在stringifyQuery()
函数中先判断是否有_rawQuery
,有的话优先用_rawQuery
。其余操作一致。
改完后,跑一遍vue-router单元测试。
npm run test:unit |
测试通过,浏览器中的?foo=bar&baz=qux&foo=bla
顺序也保留下来了,完美。
总结:
- query string 本身只是键值对,是顺序无关的,w3c规范也没有强调说一定要符合特定的顺序。vue-router 2.1.0之前的版本的
sort
操作纯属画蛇添足。最好的做法是保留原有的顺序信息,把问题抛到framework外部去解决。 - 微信js-sdk的签名接口设计的不合理:只考虑了key排序的问题,而忽视了作为value的url的排序问题。由于url query参数的顺序不影响url的相等性,在计算签名时应该在接口内部对url的query进行排序操作。
Author: deskid
Link: https://deskid.github.io/2018/02/02/2018-02-02-1/
License: 知识共享署名-非商业性使用 4.0 国际许可协议