hw活动仪式

309 阅读8分钟

我们的活动是在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…