场景
近期在做性能优化的时候,发现本想提前加载的一张图片却没有命中浏览器缓存,开发环境时明明验证是好好的,上线了却发现没命中导致加载了两张相同的图片,性能负优化。
那到底发生了什么导致没有命中啊?
先说下现状吧:
一个普普通通的vue
项目,开发环境时,写了一个简单的样式,设置了一张背景图片
.xx {
background-image: url(https://xx.cdn/xx.jpg?a=/nooo/nooo)
}
然后为了让这张图片早点加载,等到要展示的时候,可以快速展现(因为此时浏览器会发现有缓存),优化用户体验,所以在某个前置位置,写了一个简单的JS来提前加载
const img = new Image()
img.src = 'https://xx.cdn/xx.jpg?a=/nooo/nooo'
开发时好好的,等打包之后再看,样式被改了
.xx{background-image:url(https://xx.cdn/xx.jpg?a=%2Fnooo%2Fnooo)}
核心在于图片地址中的query
部分a=/nooo/nooo
被encode
成a=%2Fnooo%2Fnooo
了,浏览器直接把这两个当做不同的图片了,导致没有命中缓存。
那么,谁干的?
其实看到这个状态,脑中直接有个结论就是:估计是打包的时候,哪个loader
或者插件对链接中query
的value
部分做了encodeURIComponent
encodeURIComponent('/nooo/nooo') === '%2Fnooo%2Fnooo'
// encodeURI 不会处理 '/'
encodeURI('/nooo/nooo') === '/nooo/nooo'
跟着这个思路找下去,发现,根本没找到!!!
看来自己还是想得太简单了,只能经过了一段因人而异(本人对于webpack
相关的并没有那么熟悉,所以花了一些时间)的排查时间,发现CSS
在最后一步优化的时候,链接被encode
了。而本人这边使用的是@vue/cli-service
的v4
版本,其代码中优化逻辑如下
// @vue/cli-service
const cssnanoOptions = {
preset: ['default', {
mergeLonghand: false,
cssDeclarationSorter: false
}]
}
if (rootOptions.productionSourceMap && sourceMap) {
cssnanoOptions.map = { inline: false }
}
if (isProd) {
webpackConfig
.plugin('optimize-css')
.use(require('@intervolga/optimize-cssnano-plugin'), [{
sourceMap: rootOptions.productionSourceMap && sourceMap,
cssnanoOptions
}])
}
也就是说调用了@intervolga/optimize-cssnano-plugin
来优化CSS
代码的,而这个包是调用cssnano
来进行优化
// @intervolga/optimize-cssnano-plugin
const cssnano = require('cssnano');
const promise = postcss([cssnano(cssnanoOptions)]).
...
再看cssnano
涉及到链接处理的包,最像的应该是postcss-normalize-url
,不过文档上并没有说进行这个优化会导致encode
,只能继续看其源码,找到一个最像的:其会调用normalize-url
来优化链接
// cssnano/packages/postcss-normalize-url
import normalize from 'normalize-url';
normalizedURL = normalize(url, options);
再来看normalize-url
里面做了啥。里面完全没有任何encode
的代码,而最可能导致被encode
的操作是:
// normalize-url
const urlObj = new URL(urlString);
// Sort query parameters
if (opts.sortQueryParameters) {
urlObj.searchParams.sort();
}
难道sort
会导致链接被encode
?试了一下,确实会:
var a = new URL('https://xxx.com/add?a=/9&b=77')
console.log(a.search)
// ?a=/9&b=77
a.searchParams.sort()
console.log(a.search)
// ?a=%2F9&b=77
其实,对于URLSearchParams
的增、删、改之类的操作,都会导致参数被encode
,猜测是因为这些操作会让其先decode
,处理之后再encode
回去导致的。
那,怎么解决?
原因知道了,解决方案也就很明确了,主要是两个方向:
1. 改自身代码
严格意义上来说,本身写https://xx.cdn/xx.jpg?a=/nooo/nooo
就不太规范,应该将query
里面的参数都进行encode
,也就是把代码中涉及的地方,都改成标准的,类似https://xx.cdn/xx.jpg?a=%2Fnooo%2Fnooo
2. 改打包配置
既然是因为打包时sort
引起的,那就不要sort
即可,另外,额外研究后,可以发现cssnano
在v5.0.11
时,将这个当做Bug来修复了,默认不开启sortQueryParameters
5.0.11 (2021-11-16)
Bug fixes
- c38f14c3ce3d0: postcss-normalize-url: avoid changing parameter encoding
另外,其实@vue/cli-service
最新的v5
版本,也已经使用了新版本的cssnano
,正常也不会有这样问题。
既然这样,那么不使用sort
似乎也可以理解,而在@vue/cli-service
的v4
版本时,可以通过修改配置来关闭
// vue.config.js
module.exports = {
chainWebpack: config => {
// 注意只有正式环境才需要
if (process.env.NODE_ENV === 'production') {
config.plugin('optimize-css').tap(([options]) => {
// 直接改掉配置,虽然不太喜欢这样hack的写法
options.cssnanoOptions.preset[1].normalizeUrl = {
sortQueryParameters: false,
}
return [options]
})
}
}
}
结束了?
我们似乎找到了原因,而且也找到了解决方案,从问题本身来说,已经结束了。
但是,URLSearchParams
的操作导致的encode
与encodeURIComponent
是什么关系?是否等同呢?
要回答这个问题,也就引出了Percent-encoding
Percent-encoding
Percent-encoding, also known as URL encoding
说是Percent-encoding
,直译过来就是百分号编码,其实就是URL编码,核心逻辑也很简单,基本上百分号加上两位十六进制的字符。其详细的编码过程,在whatwg
上有,在percent-encoded-bytes 可以查看
URLSearchParams
和encodeURIComponent
都是Percent-encoding
,所以它们的的基本逻辑是一致的,只不过它们的编码范围不一致
根据whatwg
,encodeURIComponent
的编码范围为component percent-encode set
The component percent-encode set is the userinfo percent-encode set and U+0024 ($) to U+0026 (&), inclusive, U+002B (+), and U+002C (,).
而URLSearchParams
涉及的编码范围是application/x-www-form-urlencoded percent-encode set
The
application/x-www-form-urlencoded
percent-encode set is the component percent-encode set and U+0021 (!), U+0027 (') to U+0029 RIGHT PARENTHESIS, inclusive, and U+007E (~).
其包括了encodeURIComponent
的编码范围,还额外包含了 !、' 等字符
还有一个非常重要的区别:URLSearchParams
会把空格编码成加号(+),而encodeURIComponent
则会编码成%20
!
URLSearchParams
objects will percent-encode anything in theapplication/x-www-form-urlencoded
percent-encode set, and will encode U+0020 SPACE as U+002B (+).
有了这个了解之后,再回头看之前的解决方案1(改自己代码),就要十分小心了,尤其是正好卡在URLSearchParams
和encodeURIComponent
之间有所区别的字符:
var a = new URL('https://xxx.com/add?a=%209&b=!77')
console.log(a.search)
// ?a=%209&b=!77
a.searchParams.sort()
console.log(a.search)
// ?a=+9&b=%2177
上述例子中,我们以为已经编码了(以为是和encodeURIComponent
等效),结果就是被打脸。
这么看起来,还是第二个解决方案比较靠谱一点
最后
简单总结一下就是:
@vue/cli-service
的v4
版本在CSS
优化压缩时,会默认使用URLSearchParams
来编码,v5
版本正常不会有该问题,如果想要修复,可以直接关闭cssnano
的sortQueryParameters
URLSearchParams
与encodeURIComponent
编码逻辑基本一致,但是范围更广,且对空格会做特殊处理