我们的活动是在A项目中有一个小卡片,里面有链接,点击打开新的页面,内容是B项目的,B项目的内容是一个倒计时,倒计时结束之后,进入下一页展示内容。
一、前期思考
最近要搞一个hw活动仪式,在正式写之前,我们思考了一下如何更好的实现这个需求。我们的项目比较大,这个需求也搞得很大......如果将这个活动直接放在项目A中,会有三个影响:
- 会造成项目体积增大,我们项目A本身就很大了,每次打包就很慢。。。。
- 担心项目中有些代码运行时,会对活动的内容产生影响。
- 活动过于火热,大量用户直接访问,怕我们的服务器会崩。。。。
所以经过我们的思考,我们决定将活动单独提出来,作为一个新项目B,在原来的项目A中对项目B中的js、css、img等内容进行预加载,活动开始发开一个新的页面(新项目的),这样我们就可以稍微早一点上线,在活动开始之前,用户访问时,对资源进行预加载,就能大大的减少我们服务器的压力。
所以这里主要分为两个部分:新项目B的搭建,旧项目A对项目B资源的预加载。
二、新项目B的搭建
我们选择了直接使用webpack进行打包,所以项目B除了活动内容之外,就是关于webpack的配置。
下面是项目B的目录:
在webpack方面,我们要创建两个webpack的文件,一个是webpack.build.config.js,用于最后打包最终文件,一个webpack.config.js,用于在本地环境测试。entry、output文件我们就不说了。
1、.styl文件解析
css样式我们使用的是stylus作为预处理器的,因为stylus中有变量、函数、循环等方便的功能,让我们像写js代码一样,以最少的代码完成功能。
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const genericNames = require('generic-names');
const localIdentName = '[name]__[local]-[hash:base64:5]';
const localIdentName = '[name]__[local]-[hash:base64:5]';
const generateScope = genericNames(localIdentName, {
context: process.cwd(),
});
......
{
test: /\.styl/i,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
getLocalIdent: (
{ resourcePath },
localIdentName,
localName,
) => {
return generateScope(localName, resourcePath);
},
},
},
},
'stylus-loader',
],
},
这里需要三步才能处理完css:
- 使用stylus-loader将.styl文件的内容处理成css代码。
- 将处理的内容交给css-loader,处理成浏览器能够识别的代码。
- 最后交给MiniCssExtractPlugin,将css拆分成一个单独的css文件,再head中以link的方式引入即可。
这里如果不想将css打包成一个单独的文件,以link引入,可以使用style-loader将css以style标签的形式插入到heade中。
2、js文件解析
{
test: /\.(js|jsx)$/,
use: {
loader: 'babel-loader',
options: {
include: [path.resolve('./src/')],
presets: ['@babel/env', '@babel/preset-react'],
plugins: [
'@babel/plugin-syntax-dynamic-import',
[
'babel-plugin-react-css-modules',
{
filetypes: { '.styl': { syntax: 'sugarss' } },
generateScopedName: genericNames(
'[name]__[local]-[hash:base64:5]',
),
handleMissingStyleName: 'warn',
exclude: '.css$',
webpackHotModuleReloading: true,
},
],
],
},
},
},
-
babel-loader作为一个中间桥梁,调用presets和plugins中的插件,解析js文件。
-
@babel/preset-react对react代码进行解析。
-
@babel/env中包含将es6代码转换成es5代码的规则。
**3、**cssModule
由于担心代码class冲突,所以这里使用了cssModule。
cssModule即将class进行处理,生成一个独一无二的类名,不会产生命名冲突的问题。
生成的格式为:
文件_类名-hash。默认使用styleName来定义类名,可以在下面js的配置更改,我们在写代码时,只需要要
即可。cssModule配置一共分为两步:
- css配置:
也就是css-loader中配置的
const genericNames = require('generic-names');
const localIdentName = '[name]__[local]-[hash:base64:5]';
const generateScope = genericNames(localIdentName, {
context: process.cwd()
});
const localIdentName = '[name]__[local]-[hash:base64:5]';
const generateScope = genericNames(localIdentName, {
context: process.cwd()
});
.......
modules: {
getLocalIdent: ({ resourcePath },localIdentName,localName)=> {
return generateScope(localName, resourcePath);
},
}
......
-
js配置
......
plugins: [ '@babel/plugin-syntax-dynamic-import', [ 'babel-plugin-react-css-modules', { filetypes: { '.styl': { syntax: 'sugarss' } }, generateScopedName: genericNames( '[name]__[local]-[hash:base64:5]', ), handleMissingStyleName: 'warn', exclude: '.css$', webpackHotModuleReloading: true, }, ], ] ....
css的getLocalIdent和js的generateScopedName都使用generic-names处理,是因为在新版本的css-loader和babel-plugin-react-css-modules生成hash的算法不一样,导致js生成的hash和css的hash不一样,样式取不到的问题。
4、静态资源解析
{
test: /\.(jpg|png|jpeg|gif|mp3)$/,
exclude: [path.resolve('node_modules')],
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 12 * 1024, // 4kb
},
},
},
这个rules规则是解析图片或者音频资源的,type可以取多个值,具体可以看这里。
5、优化
在optimization中,我们可以根据自己的需求压缩优化bundle。
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
......
optimization: {
minimize: true,
minimizer: [
new CssMinimizerPlugin({
test: /\.css$/,
exclude: /node_modules/,
minify: CssMinimizerPlugin.cleanCssMinify,
}),
],
splitChunks: {
chunks: 'async',
minChunks: 1,
minSize: 200000,
cacheGroups: {
commons: {
test: /(react|react-dom)/,
name: 'vendors',
chunks: 'all',
},
},
},
},
-
minimize属性是告诉webpack使用minimizer中配置的插件,处理文件。为false时,将不会使用minimizer中的配置,而使用默认的配置。
-
minChunks是告诉webpack,当共享模块数大于或者等于1时,被拆分成单独的文件。
-
minSize是当打包文件大于200000个字节时,将会被拆分成一个新的文件。
-
commons中配置是将react、react-dom拆分打包成一个文件,文件名为vendors。
6、生成m****anifest.json文件
const WebpackAssetsManifest = require('webpack-assets-manifest');
......
new WebpackAssetsManifest({
publicPath: 'https://test.com/'
}),
该插件用于生成一个manifest.json,文件内容是各个文件名与文件路径的映射,比如:
{
"test.png": "https://test.com/test.png"
}
这样我们就可以在用到的地方读取这个json文件,取出所有的文件地址进行请求,实现资源的预加载。
这里一般将除index.html之外的内容都放在CDN上,提前预加载其他的资源,缓存下来,在打开html时,直接使用,这样服务器的压力将大大减少。
7、预加载配置
在要进行预加载的地方,请求上面说到的json文件,读取文件中的目录,插入到head中。
这里需要为文件指定prefetch或者preload。
prefetch会在浏览器进程空闲时取请求资源,通常用于预加载之后的资源。
preload会立即请求资源,并且在浏览器渲染之前请求,可以进行跨域请求。
2023年优化补充:
回顾2022年的活动,有几点重要的问题需要记录:
- map3在预加载时自动下载。我们网站的用户比较特殊,很多人有使用NeatDownloadManager,导致在预加载map3文件时,该软件进行拦截,用户不确定该map3文件是否安全,对此产生疑问。
- setTimeout在tab页后台时,睡眠导致倒计时不准确。 为了优化后台标签的加载损耗(以及降低耗电量),后台标签中的setTimeout会被节流。
- 倒计时的写法。 去年活动上的倒计时使用的是css,这种倒计时在页面上展示时会出现卡顿情况,所以该用js。
- 页与页之间的衔接卡点靠动画延迟时间还是利用setTimout。 去年活动的页与页之间的执行完全靠css动画的延迟来连接的,导致前面的时间有一丁点变化,后面的都得跟着变,及其麻烦。
- 预加载导致资源提前暴漏。 去年在项目A端要显示B端的大屏,由于比较着急,所以使用的是js+css,将B端的文件拉取之后放大在A端,这样产生的问题是B端所有的资源都提前暴漏了,活动还没开始,就有人已经拉完资源并拼凑出来了。
- js预加载立即执行的问题。 在活动开始之前预加载js文件时,加载完成,如果不做处理,js文件会立即执行,这并不是我们想要的结果,我们希望是只进行预加载,不立即执行,以及如果立即执行,会导致我们的代码逻辑可能出现问题,产生并不想要的结果。
- 音乐的自动播放限制。 浏览器建议对于音乐实现点击播放,而不是自动播放。具体情况看参考链接。
对于上面的问题我们的解决方案是:
-
map3在预加载时自动下载。 通过webpack将map文件打包到js文件中,在下载js文件的时候,NeatDownloadManager不再拦截。
-
setTimeout在tab页后台时,睡眠导致倒计时不准确。 目前使用的逻辑是:
errorTime = (系统时间戳+后端返回时间戳 - 矫正函数执行时的时间戳)% 1000
如果errorTime大于150,并小于850,则需要矫正。
-
页与页之间的衔接卡点靠动画延迟时间还是利用setTimout。 今年使用了setTimeout以及使用useState来切换页面,这样只要关注页和页的时间,每一页内的动画不受影响。但是这里注意,如果涉及到每一页背景的切换动画,就必须在全局使用css动画连接了。
-
预加载导致资源提前暴漏。 今年的实现方案是预加载只加载倒计时的代码,倒计时后的代码不进行预加载(因为比较少),这样就防止之后的代码被扒出来。但是在活动开始前一小时或者多久,我们还是会上线之后的代码,这时还是会被扒出来。具体实现方案是对后面的文件使用lazy加载,在上传cdn时(我们文件都上传到cdn了),不上传倒计时之后的代码,这也能解决用户手动更改倒计时,想进入下一页的情况。
-
js预加载立即执行的问题。 对js文件使用preload,即加载后先不执行。
今年产生的问题:
-
mac是定时同步时间,windows是打开浏览器同步时间(没有做长时间验证)。 我们活动开始时间是取浏览器系统时间+后端返回的时间差值。后端返回的时间差值是固定的,但是由于mac的定时同步时间问题,会导致系统时间漂移,从而影响我们的开始时间。
-
动画的职能单一。 一个动画只做一件事,并且只有class。
-
资源预加载之后,代码会被扒出来。 可以进行代码混淆。
参考链接:
1、音乐的自动播放限制:developer.chrome.com/blog/autopl…