问题概述
某工作日,线上某用户向客服专员反馈没法正常访问“查看报价页面”,页面内容没有呈现。客服专员收到反馈后,将问题转交给SRE
处理。很奇怪的是,SRE
访问生产环境“查看报价页面”显示正常,为了进一步分析定位问题,SRE
向用户申请了远程操作,将将一些具有价值的信息记录下来,主要有以下两个方面:
分析与定位
通过上述信息,可以知道用户与SRE
访问页面的差异,SRE
访问“查看报价页面”可以正常获取所有资源,而用户无法获取部分字体和样式文件。根据浏览器加载渲染原理,部分字体和样式加载失败大概率不会导致页面DOM
无法呈现,无法下结论之时,不妨先假设字体和样式文件影响到了DOM
渲染。
当无法从表象分析出线上问题原因时,第一步需要在开发环境或者测试环境复现问题场景,然后排查从请求资源到页面渲染的执行过程。
问题的引入点:域名解析
在复现场景之前,需要先知道访问成功和失败之间的差异。通过收集到的信息来看,请求域名解析的IP
有明显不同:
- 正常访问资源,
DNS
域名解析
Request URL |
Remote Address |
---|---|
https://at.alicdn.com/t/font_1353866_klyxwbettba.css |
121.31.31.251:443 |
https://g.alicdn.com/de/prismplayer/2.9.21/skins/default/aliplayer-min.css |
119.96.90.252:443 |
https://at.alicdn.com/t/font_2296011_yhl1znqn0gp.woff2 |
121.31.31.251:443 |
https://at.alicdn.com/t/font_1353866_klyxwbettba.woff2?t=1639626666505 |
121.31.31.251:443 |
生产环境请求资源失败,
DNS
域名解析at.alicdn.com
:116.153.65.231
g.alicdn.com
:211.91.241.230
用户和SRE
所处地区不同,访问资源时域名解析命中的边缘节点服务也会不同,而at.alicdn.com
与g.alicdn.com
是公网免费的CDN
域名,某些边缘节点服务稳定性不够,拉取不到资源也是可能发生的。
问题根本原因:模块加载
开发环境与测试环境复现差异
修改本地hosts
,添加用户域名解析的地址映射,在测试环境和开发环境尝试复现。两个环境均不能获取到字体和样式文件,测试环境(https://ec-hwbeta.casstime.com
)页面内容没有呈现(复现成功),开发环境页面内容正常呈现(复现失败),分析开始陷入胡同。
开发环境:
测试环境:
这时候就要开始分析了,两个环境复现问题的差异点在哪里?
不难发现,两个环境最主要的区别在于yarn start
与yarn build
的区别,也就是构建配置的区别。
开发环境
1、create-react-app
关键构建配置
- 启用
style-loader
,默认通过style
标签将样式注入到html
中; - 不启用
MiniCssExtractPlugin.loader
分离样式和OptimizeCSSAssetsPlugin
压缩样式; - 启用
optimization.splitChunks
代码分割; - 启用
optimization.runtimeChunk
抽离webpack
运行时代码;
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader')
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
// css is located in `static/css`, use '../../' to locate index.html folder
// in production `paths.publicUrlOrPath` can be a relative path
options: paths.publicUrlOrPath.startsWith('.')
? { publicPath: '../../' }
: {},
},
].filter(Boolean);
return loaders;
}
module: {
rules: [
{
oneof: [
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
},
'sass-loader'
),
},
]
}
]
}
optimization: {
minimize: isEnvProduction,
minimizer: [
// 压缩css
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: shouldUseSourceMap
? {
// `inline: false` forces the sourcemap to be output into a
// separate file
inline: false,
// `annotation: true` appends the sourceMappingURL to the end of
// the css file, helping the browser find the sourcemap
annotation: true,
}
: false,
},
})
],
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
splitChunks: {
chunks: 'all',
name: false,
},
// Keep the runtime chunk separated to enable long term caching
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`,
},
}
css-loader
在解析样式表中@import
和url()
过程中,如果index.module.scss
中使用@import
引入第三方样式库aliplayer-min.css
,@import aliplayer-min.css
部分和index.module.scss
中其余部分将会被分离成两个module
,然后分别追加到样式数组中,数组中的每个”样式项“将被style-loader
处理使用style
标签注入到html
中
2、执行链路
开发环境的构建配置基本清楚,再来看看执行流程。执行yarn start
启用本地服务,localhost:3000
访问“查看报价页面”。首先会经过匹配路由,然后react-loadable
调用webpack runtime
中加载chunk
的函数__webpack_require__.e
,该函数会根据入参chunkId
使用基于promise
实现的script
请求对应chunk
,返回Promise
。如果Promise.all()
存在一个Promise
转变成Promise
,那么Promise.all
的执行结果就是Promise
。因为css chunk
是通过style
标签注入到html
中,所以__webpack_require__.e
只需要加载js chunk
,当所有的js chunk
都请求成功时,Promise.all
的执行结果就是Promise
,fulfilled
状态会被react-loadable
中的then
捕获,更新组件内部状态值,触发重新渲染,执行render
函数返回jsx element
对象。因此,内容区域正常显示。
生产环境
1、create-react-app
关键构建配置
- 不启用
style-loader
,默认动态创建link
标签注入样式; - 启用了
MiniCssExtractPlugin.loader
分离样式; - 启用
optimization.splitChunks
代码分割; - 为了更好的利用浏览器强缓存,设置
optimization.runtimeChunk
,分离webpack runtime
;
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader')
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
// css is located in `static/css`, use '../../' to locate index.html folder
// in production `paths.publicUrlOrPath` can be a relative path
options: paths.publicUrlOrPath.startsWith('.')
? { publicPath: '../../' }
: {},
},
].filter(Boolean);
return loaders;
}
module: {
rules: [
{
oneof: [
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
},
'sass-loader'
),
},
]
}
]
}
optimization: {
minimize: isEnvProduction,
minimizer: [],
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
splitChunks: {
chunks: 'all',
name: false,
},
// Keep the runtime chunk separated to enable long term caching
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`,
},
},
plugins: [
// Generates an `index.html` file with the