深入理解React Router:从原理到实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.6.2 decodeURI解码问题

在push、replace方法或者浏览器“前进”和“后退”时的事件回调函数中,history源码都会调用createLocation创建location对象。

每次进行相关操作都会对pathname解码一次,但是并没有对search与hash进行相关的操作。

1.search没有解码带来的问题

以browserHistory为例,由于history.push/replace方法最终会调用浏览器的pushState/replaceState方法,因此如果pushState/replaceState方法入参中有Unicode字符,则浏览器会将非特殊的Unicode字符的UTF-8码中的每个字节加上“%”作为实际地址,window.location.search或者window.location.hash得到的路径中都将带有“%”。但是在history库中,如果调用history库的方法,如history.push/replace,则参数列表中传入的字符串也将同步到history.location.search或者history.location.hash中,将不会做编码处理:

从浏览器记录的角度来看,其记录URL字符串为编码后的字符串。此时,如果进行浏览器的“前进”和“后退”操作,则browserHistory将更新地址,browserHistory的search和hash此时将从window.location中获取,获取到的值将为编码后的带“%”的字符串,因此造成search与hash前后不一致的问题:

这在编码时需要注意。

2.pathname解码

对于pathname,在history源码中,设置history.location前对pathname做了一次decodeURI解码处理。因此解决了上述问题,无论是执行push、replace操作,还是浏览器在“前进”和“后退”时,如browserHistory.location.pathname,都将得到解码后的字符:

这解决了在导航过程中pathname前后不一致的问题,给路由匹配带来了两个好处。

1)编码便利

由于browserHistory.location.pathname前后提供的pathname一致,因此对于Route的path属性中的特殊字符,可不用进行编码(Route将在第6章进行介绍):

提供给该Route的进行命中匹配的路径将永远为解码后的字符串,即不会提供/%E4%B8%AD%E6%96%87给该Route进行匹配。

2)参数解析

对于命名路由(命名路由将在第6章进行介绍),如:

当调用如browserHistory.push('/foo/中文')时,由于得到的browserHistory.location.pathname都为解码后的字符串,因此获取name这个命名变量的值也会得到解码后的字符串,即:

永远不会出现{name:"/%E4%B8%AD%E6%96%87"}这样的情况。

由于在源码中进行了decodeURI解码,因此开发者在编码过程中如声明路由、进行导航等操作时不需要关心字符的解码问题。

3.pathname解码带来的问题

虽然前后一致的pathname带来了编码的方便,但是引入pathname解码同时也带来了一些问题。

前面对push等方法的调用,传入参数字符串中没有特殊字符“%”。对于没有“%”的字符串的导航路径,则不会有相关问题。但是如果导航路径中有“%”,则需要注意,由于浏览器使用百分号编码,如"/%E4%B8%AD%E6%96%87",在每个字节前都加上了“%”,因此此时的“%”被认为有特殊作用,后面跟一个字节的十六进制形式。如果希望导航路径中的“%”没有特殊作用,则需要对“%”进行一次编码,调用encodeURI('%')得到“%25”。下面以一个例子来说明。

如果不编码而直接调用,如:

对于history库来说,由于其在导航操作时会对pathname进行decodeURI解码操作,这就默认“%”为特殊符号。上述错误即产生于:

无法解码非法的“%”,如果不希望此错误产生,能导航到/abc%d路径,则可对路径部分进行一次encode操作:

由于history库内部会执行一次decodeURI操作,因此得到的browserHistory.location.pathname,即原路径/abc%d,不会出现报错。细心的读者会发现浏览器路径此时为/abc%d:

浏览器认为“%”有特殊意义,不会对其进行处理,上述操作等同于:

由于当前路径为/abc%d,因此虽然编码过一次没有报错,但是如果在浏览器中执行一次后退操作,再执行一次前进操作,又回到此路径时,则会出现解码错误:

由于window.location.pathname为/abc%d,在导航过程中,当路径/abc%d传入history库中后,同样会执行一次decodeURI操作,这时无法解析/abc%d中非法的“%”字符,因而报错。

这就是在history库中引入decodeURI所带来的问题。

要解决此问题,需要使浏览器地址中无特殊意义的“%”也得到编码,即要得到:

则调用pushState时需要传入对“%”进行编码后的字符串:

由于history库会执行一次decodeURI操作,因此要想获得上述pushState的效果,则需要在调用push/replace方法时进行两次编码:

第一次编码是为了对特殊字符进行编码,第二次编码是为了抵消history库中的decodeURI操作。当执行完两次编码操作后,调用push方法得到:

注意,由于此时的browserHistory.location.pathname为编码后的值/abc%25d,因此获取原始值/abc%d需要进行一次解码(decodeURI(browserHistory.location.pathname))才可实现。

在这样处理之后,在浏览器中执行一次前进、后退操作,由于window.location.pathname为/abc%25d,因此在传入history库中后,要进行一次解码:

这样最终解决了两处报错的问题,但是执行push/replace操作得到的location路径将与浏览器前进、后退时得到的location路径不一致:

由于有此种问题存在,在此场景中,使用browserHistory.location.pathname变量获取原始路径可能有编码和未编码两种情况,对此可引入一些帮助函数(如safeDecode)来处理:

同时,由于获取路径存在两种情况,因此如在Route声明时,也需声明两条路径:

这样的声明使得两条路径中的任意一条都能匹配成功。

正是由于存在上述问题,在history计划的5.x版本中,可能会将内部的decodeURI解码逻辑移除。但是如果移除了decodeURI解码,则编码的便利性也将消失,如对于“/中文”路径,Route可能会接收到“/%E4%B8%AD%E6%96%87”“/中文”两类字符串,这可能需要React Router库内部调用safaDecode等逻辑进行联动修改,才能兼容history的升级改动。