Escenas
Cuando estaba optimizando el rendimiento recientemente, descubrí que una imagen que quería cargar con anticipación no llegaba a la memoria caché del navegador. El entorno de desarrollo verificó claramente que era buena, pero cuando me conecté, descubrí que las dos mismas imágenes se cargaron debido a la falla y el rendimiento fue negativo .
Entonces, ¿qué pasó para causar la señorita?
Hablemos primero de la situación actual:
Para un proyecto ordinario vue
, al desarrollar el entorno, escribí un estilo simple y establecí una imagen de fondo
.xx {
background-image: url(https://xx.cdn/xx.jpg?a=/nooo/nooo)
}
复制代码
Luego, para que esta imagen se cargue antes, cuando esté a punto de mostrarse, se puede mostrar rápidamente ( porque el navegador encontrará que hay un caché en este momento ) para optimizar la experiencia del usuario, por lo que en una posición previa , escribí un JS simple para avanzar en la carga
const img = new Image()
img.src = 'https://xx.cdn/xx.jpg?a=/nooo/nooo'
复制代码
Fue bueno durante el desarrollo, y lo miraré después del empaquetado. El estilo ha cambiado.
.xx{background-image:url(https://xx.cdn/xx.jpg?a=%2Fnooo%2Fnooo)}
复制代码
El núcleo es que la parte de la dirección de la imagen se cambiaquery
a=/nooo/nooo
encode
a=%2Fnooo%2Fnooo
, y el navegador considera directamente estas dos como imágenes diferentes, lo que da como resultado que no haya ningún golpe de caché.
Entonces, ¿quién lo hizo?
De hecho, al ver este estado, hay una conclusión directa en mi mente: se estima que cuando se empaquetó, qué loader
complemento o complemento hizo query
la value
parte en el enlaceencodeURIComponent
encodeURIComponent('/nooo/nooo') === '%2Fnooo%2Fnooo'
// encodeURI 不会处理 '/'
encodeURI('/nooo/nooo') === '/nooo/nooo'
复制代码
Siguiendo esta línea de pensamiento, lo encontré, ¡pero no lo encontré en absoluto! ! !
Parece que todavía estoy pensando de manera demasiado simple, y solo puedo pasar por un período de investigación que varía de persona a persona ( no estoy webpack
tan familiarizado con los relevantes, así que dediqué algún tiempoCSS
), y descubrí que en el último paso de optimización, el enlace fue bloqueado encode
. . Y @vue/cli-service
la v4
versión que uso aquí, la lógica de optimización en su código es la siguiente
// @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
编码逻辑基本一致,但是范围更广,且对空格会做特殊处理