添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
体贴的牛腩  ·  VMware vSphere ...·  1 年前    · 
怕老婆的可乐  ·  MSB4018 ...·  1 年前    · 
随手写了个plugin,就将小程序体积减少了120k

随手写了个plugin,就将小程序体积减少了120k

前言


这是一个关于 reduce-enum-webpack-plugin 由来的故事,我已经把它放到 github 和发到 npm 了,有需要的可以看下,觉得能帮到你的话也请给个 star~

我们的接口是通过 proto 转为 ts (不知道 proto 是什么的可以看下 这篇文章 ),目的是为了更好的类型提示,比如字段类型、后端字段注释,enum 等等,而不是后端定义一套数据结构,前端又得重复定义。

生成 api 后,我们就可以直接 import 对应的接口名,接口有对应的 RequestType 和 ResponseType,大大提升了开发体验。

例子如下:

但直到在小程序中使用后,发现打包出来的主包实在太大了,所以用 webpack-bundle-analyzer 分析下问题究竟出在哪里,想必大家都知道,问题肯定是出在这个 api 的包了。是的,没错,深入定位后发现具体问题是出现在生成的 enum

比如有些模块目录下,一个 enum Status_code 就大概 140 行,而更恐怖的是一个 enum Permission 就达到 1600 行!!

但 api 项目中可不只一个 enum 呀,每个模块目录下都有大大小小的 num。

这对于 web 项目来说可能影响不是特别的大,但对于体积限制寸土寸金的小程序来说,就成为了限制项目上线的阻碍了。(毕竟超过 2m 就没法上传了。。。)

而大家都知道,ts 转 js 的过程中,enum 的转化过程如下:

点击 链接 可看到

也就是说,enum 最终是转化为一个对象,对象结构类似如下:

var Status = {
    PAID: 0,
    UN_PAID: 1,
    0: 'PAID',
    1: 'UN_PAID',

先别急

疑点一:webpack 不是能 tree-shaking 吗?

是的,我一开始也是这么想,难道 shaking 失败了?

大家上面也看到 ts enum 转 js 是一个 IIFE 的过程,是有副作用的,这种情况下 webpack 是没法对其 tree-shaking 的,用大白话就是,这些枯枝烂叶看起来摇摇欲坠,但是无论你怎么摇晃,它们就是死活不掉。

疑点二:你难道不会用 const enum 吗?

好,那我就用用呗,大家能看到,用了 const enum 后,转 js 的过程突然变得如此的简洁!!

哇,看起来很有希望,赶紧全部都声明为 const enum!!

然后,我开始打包,结果发现:

const enum 不支持

因为上面的 const enum 只限于当前文件使用,也就是说,你在单文件里使用,是有如下效果的

if (code === Status.PAID) { /*...*/ }
if (code ===




    
 0 /* Status.PAID */) { /*...*/ }

但如果你是 export 给其他地方使用,babel 就没法处理了哇

疑点三:@babel/preset-typescript v7.15.0 不是有个 optimizeConstEnums 吗

大致作用就是将 babel 编译为一个对象,比如:

enum Status {
var Status = {
    PAID: 0
// 而不是
var Status = {
    PAID: 0,
    0: 'PAID'

但我配置后,发现还是不起作用(可能我哪里弄错了?)

// babel.config.js
module.exports = () => {
  return {
    presets: [
      'xxx',
        '@babel/preset-typescript',
          optimizeConstEnums: true,
ps,注意 preset 的顺序是从后到前

既然不起作用,那我们自己实现就好吧~(麻烦知道问题原因的大佬点拨下,万分感谢~)

分析下打包后的产物

发现结果都是类似 e[e["xxx"]=0]="xxx" ,那看起来不难,我们让其最终只生成 e["xxx"]=0 不就好?

2023-03-05 14.02.36.gif

动手,就是现在,就在这里!!

看到上面的产物,那我们可以写个 webpack plugin,在 asset 输出之前,对每个 js asset 做下处理。

简单了解下 plugin

首先我们要知道,plugin 可以监听 webpack 打包过程中的各个节点,从而做出相应的逻辑,优化并拆分 bundle。

而 webpack 的生命周期钩子有这么多

官网勾子链接

一个最简单的 plugin 例子如下:

class MyPlugin {
  // 每个plugin上面必须有apply方法,接收一个compiler实例
  apply(compiler) {
    // 通过compiler上的不同hook的生命周期节点,监听对应的逻辑
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {

在 emit 阶段处理

根据 官网 可知,emit 阶段是 输出 asset 到 output 目录之前执行 ,那我们在输出之前,对 enum 产物做下提前处理。

自然而然想到判断产物中如果有对应的 e[e["xxx"]=0]="xxx" 格式的话,用正则提取中间的 e["xxx"]=0

那如何写这个正则?

我们观察下,上面结构有两个重复的,即'e'、'xxx',那看起来我们可以用正则的 反向引用 来实现,这里先跟大家介绍下什么是反向引用。

分支任务,什么是反向引用

举个

比如我要匹配到上面的 e,而 e 是属于 \w ,那我可以这么写:

/(\w)\[\1/

上面的 \1 就表示引用匹配的 () 中的第一个分组

而其中的 xxx 也是同样道理,我们照猫画虎,得到的正则长这样:

/(\w)\[(\1\["(\w*?)"\]=\d+)\]="\3"/g
  • 其中(\w)表示第一个字母,如 e
  • \1 表示引用(\w)匹配到的字母,也就是必须严格如 e[e ,而不能 e[w
  • (\1["(\w*?)"]=\d+)是第二个分组,也就是第二个括号,也正是我们要提取的,所以结果会返回 $2 ,然后其中的 (\w*?) 是第三个括号
  • 所以后面的 \3 就代表引用第三个括号匹配的内容
  • \d+ 表示多个数字,因为可能是 10,300,而不都只是 0、1、2...
  • 结尾 g 表示匹配全部,而不是只替换一个,因为有很多个
这里大家可以先用回调函数的写法,看看回调函数长啥样,上面的正则不是一蹴而就写成了,是经过很多次调试才得到的结果(本来想问 chatgpt的,但回答结果总是错误。。,所以只能自己动手)

那最终得到的结果是:

newContent = content.replace(/(\w)\[(\1\["(\w*?)"\]=\d+)\]="\3"/g, '$2')

那我们写下 plugin:

class RuduceEnumWebpackPlugin {
  // 每个plugin实例必须有apply方法,接收compiler实例
  apply(compiler) {
    // compilation => 此次打包的上下文
    compiler.hooks.emit.tap(RuduceEnumWebpackPlugin.name, (compilation) => {
      // 遍历资源文息,其中assetName为每个资源的名称
      for (const assetName in compilation.assets) {
        if (assetName.endsWith('.js')) {
          // 每个资源的值
          const content = compilation.assets[assetName].source()
          const newContent = content.replace(/(\w)\[(\1\["(\w*?)"\]=\d+)\]="\3"/g, '$2')
          // 覆盖原始内容
          compilation.assets[assetName] = {
            source: () => newContent,
            size: () => newContent.length,

在项目中使用

小试牛刀

我们在 webpack 配置中:

plugins: [
  new RuduceEnumWebpackPlugin()

看看打包后的产物长啥样:

看起来很符合预期

问题往往没那么简单

1.e[e"xxx"=1e3]="xxx"

如果 enum 是:

enum Status {
  xxx = 1000,

那它转换后的结果不是 e[e"xxx"=1000]="xxx" ,而是 e[e"xxx"=1e3]="xxx" ,且 e 前面不只有一个数字,有可能是 1024e3

我们可看到 998,999 是没问题的,但到了 1000 就变成 1e3 了

还有个问题,数字可能是负数,所以要加上 -?

那这个容易,修改下正则就好了

replace(/(\w)\[(\1\[("\w*")?\]=(-?\d+|\d+e\d))\]=\3/g, '$2')
  • -?\d+:是匹配 1、10、-1、-10
  • \d+e\d:是匹配 1e3、 123e3 测试下:

如果是 e[e.xxx=0]="xxx"呢?

你还别说,真有这种可能,我用了 uglifyjs-webpack-plugin 后,产物长这样了:

区别是通过(\w).xxx=123,那我们继续修正:

// 之前最新得到的正则:
/(\w)\[(\1\[("\w*")?\]=(-?\d+|\d+e\d))\]=\3/g
// 修改e['xxx'] => e.xxx,得到
/(\w)\[(\1\.(\w+)=(-?\d+|\d+e\d))\]=\3/g
// 后面的\3要加上"",也就是"\3"
/(\w)\[(\1\.(\w+)=(-?\d+|\d+e\d))\]="\3"/g

测试下结果:

成功将 e[e.W=10]="W" 变为 e.W=10

那么也就是:

const newContent = content
                        .replace(/(\w)\[(\1\.(\w+)=(-?\d+|\d+e\d))\]="\3"/g, '$2')
                        .replace(/(\w)\[(\1\[("(\w*)")?\]=(-?\d+|\d+e\d))\]=\3/g, (_, __, $2) => {
                            return $2.replace(/\["(.*?)"\]/, (_, $1) => `.${$1}`)
这里可以把两个正则合成一个,但为了看起来清晰一点,就把它拆开了,大家可以尝试下怎么合起来

看下效果:

如果是 e[ e.xxx ]="xxx"呢?

可能有些人会问,如果是上面这种情况呢?我们看下

对应 链接

这种情况实际上不用处理

整理封装下 plugin

经过上面的一步步试错和推导,我们得到了最终的 plugin,如下:

class RuduceEnumWebpackPlugin {
  // 每个plugin实例必须有apply方法,接收compiler实例
  apply(compiler) {
    // compilation => 此次打包的上下文
    compiler.hooks.emit.tap('MyPlugin', (compilation) => {
      // 遍历资源文息,其中assetName为每个资源的名称,value为文件内容
      for (const assetName in compilation.assets) {
        if (assetName.endsWith('.js')) {
          // 每个资源的值
          const content = compilation.assets[assetName].source()
          const newContent = content
          .replace(/(\w)\[(\1\.(\w+)=(-?\d+|\d+e\d))\]="\3"/g, '$2')
          .replace(/(\w)\[(\1\[("\w*")?\]=(-?\d+|\d+e\d))\]=\3/g, '$2')
          // 覆盖原始对象
          compilation.assets[assetName] = {
            ource: () => newContent,
            size: () => newContent.length,

特别注意!! 使用该 plugin 的前提是只通过 enum.key 访问,而不要使用 enum.value 获取 key 值,也就是说,这种是不允许的:

enum Status {
 // ❌、
Status[0]

请遵循这种用法:

// ✅
Status.PAID