webpack5实用配置经验总结

场景一:使用ESM形式和Typescript编写webpack配置文件

参照官网的配置Typescript,有三种解决方案,目前采用的是使用tsconfig.json中重载ts-node的配置,存在一种问题:在package.json中声明了type: module,运行webpack命令会报如下错误:

TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts"

这是因为在决定文件应该如何编译和执行时,ts-node 会匹配 node 和 tsc 的行为。这意味着 TypeScript 文件会根据你的 tsconfig.json “module” 选项进行转换,并根据 node 的 package.json “type” 字段的规则进行执行。在某些情况下,需要为某些文件覆盖默认的编译和执行方式。ts-node在编译webpack.config.ts的时候要求以CommonJS的形式执行。如果你的 package.json 配置了 “type”: “module”,而 tsconfig.json 配置了 “module”: “esnext”,则默认情况下该配置文件是原生 ECMAScript。

你可以移除type: 'module'的声明解决这个问题, 但当你不想为了一个配置文件而去除这个声明,可以设置ts-node的moduleTypes选项。配置如下:

{










  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "module": "ESNext",
    "target": "ES2018",
    "moduleResolution": "node",
  },
  "ts-node": {
    "compilerOptions": {
      "module": "CommonJS",
    },
    "transpileOnly": true,
    "moduleTypes": {
      "webpack.config.ts": "cjs",
    }
  }
}

场景二:使用babel-loader、swc-loader编译react项目

babel-loader

{










  "presets": [
    [
      "@babel/preset-react",
      {
        "runtime": "automatic"
      }
    ],
    "@babel/preset-typescript"
  ]
}

swc-loader

{










  "jsc": {
    "parser": {
      "syntax": "typescript",
      "jsx": true
    },

    "transform": {
      "react": {
        "runtime": "[automatic](url)"
      }
    }
  }
}

以一个最简单的一行dom的react项目,在build模式下的耗时,swc用时2676ms,babel用时3408ms,相比较而言,swc比babel快了大概27%左右,如果用于真实项目,差异应该会更为明显,具体的平台兼容配置需要配合自身项目所需的polyfill来进行。
babel-loader vs swc-loader.png

场景三:如何正确配置hash

webpack主要分为三种hash: fullhash\chunkhash\contenthash

  1. fullhash一般不会去使用,因为每次打包的文件都将是新的,不会被缓存。
  2. chunkhash主要是针对chunk生成一个文件,webpack配置了多少个entry就会生成多少个js文件,每一个entry都是一个chunk,当你使用了懒加载的方式,例如React.lazy(() => {}),引入的组件也将是一个新的chunk(可以这么理解,懒加载的东西是你需要的时候才会去拿的,所以得单独形成一个文件)。
  3. contenthash是基于内容变化的哈希,最显而易见的例子是:React中经常将js和css单独分开写,js中引入css,但是打包会打成一起,我们通常会使用一些webpack插件来将css单独提取出来,css文件也会有对应的哈希值,以便能够充分运用浏览器的缓存,如果你只改了js部分的代码,提取出来的css哈希值也会随之改变,即使没用对css文件进行修改,这时候就需要使用contenthash了。

除了这些之外,在实践中可以发现,在一个entry中通过懒加载分离了部分chunk会有entry的哈希值失效的情况,例如在App中引入了ComponentA组件,会形成两个js文件:ComponentA.c46eaeaa5987b833.jsapp.a8246d7638ecc817.js,改变了ComponentA,会生成一个新的哈希值,而app引入的时候,由于引入的文件名发生了变化,导致app文件的哈希值也需要重新生成,这就造成了对entry的chunk缓存失效。

这种情况就需要设置runtime chunk了,会单独形成一个runtime-[entry]-[hash].js文件,用来记录entry运行时文件之间的关系,每次变化的时候,就只需要更新这个runtime文件,降低了缓存失效出现的情况。

还有也可以对资源进行hash化,设置assetModuleFilename为contenthash,可以让你的图片在移动或是重命名的时候,也不会使得哈希值失效。

附上webpack配置:

{










    output: {
        assetModuleFilename: 'images/[contenthash][ext][query]',
    },
    plugins: [
        new MiniCssExtractPlugin({
          filename: 'css/[name].[contenthash].css',
          chunkFilename: 'css/[id].[contenthash].css',
        })
    ],
    optimization: {
        runtimeChunk: true,
    }

}

场景四:应该如何选择合适的sourcemap风格

webpack可以通过设置devtool来实现源码和经过处理的代码的映射,总的来说,一般由evalcheap-[module]source-map组成,在webpack5中,配置devtool的风格将更为严格,需要符合以下正则表达式。

/^(inline-|hidden-|eval-)?(nosources-)?(cheap-(module-)?)?source-map$/

  1. eval关键字下速度比较快,模式编译速度非常快,通过eval函数直接包裹模块,但它只能映射到webpack-loader转换后的代码,而不能直接映射到源码。
  2. cheap关键字会抛弃列维度的映射,只能够到行,同样不能直接映射到源码,需要同时跟着设置module关键字,将各种loader(例如babel-loader)产生的sourceMap一同关联上,追溯到源码。
  3. source-map关键字会产生一个.map文件来记录映射的信息。
  4. inline关键字会将source-map转换为DataUrl后添加到bundle中,可能在发布单文件的时候会用到。
  5. nosources在生成source-map的时候,.map文件中会包含源代码,设置了这个,map文件中就不会有sourcesContent这个字段了。
  6. hidden将隐藏产物文件中的sourceMappingURL信息。

以最简单的react代码,定位console.log的错误信息:

function Home() {
  console.loge(111)
  return (
    <div>
      <h2>Home</h2>
    </div>
  )
}
export default Home

适合dev模式的应用

因为dev模式下不需要将map文件独立出来,所以建议用用eval来包裹模块来提升编译速度。

  • eval-cheap-module-source-map

cheap-module定位到源文件的行级别,个人认为就已经足够了,在编译的速度和sourceMap的定位的准确性来说,做了一个良好的权衡。
eval-cheap-module-source-map.gif

  • eval-source-map

如果对于定位精度要求高的,不在意速度的,想要具体到某一行某一列的错误,可以采用eval-source-map的形式。

eval-source-map.gif

适合production模式下的应用

如果在dist产物中存在,浏览器都会自动根据//# sourceMappingURL=去加载对应的source-map文件。

  • hidden-source-map

可以出去产物中的sourceMappingURL地址,让浏览器识别不到要加载的map文件在哪,需要用户手动去添加对应的source-map文件。一般都是文件名加上.map,所以防君子不防小人(?头)。
hidden-source-map.gif

  • nosources-source-map

可以让浏览器定位不到源代码的位置,但是sourcemap会提示具体报错的代码在什么文件以及第几行第几列,这样拥有源代码的人就可以知道在哪,其他人也无法知晓。具体可以配合类似Sentry这种工具,上传map文件到监控平台,去查看对应的报错信息。

nosources-source-map.gif

以上这些也可以结合SourceMapDevToolPlugin实现粒度更细的控制。

场景五:资源加载

webpack5通过内置的通过 loader 或内置的Asset Modules引入任何其他类型的文件,在升级的时候需要注意对应的平替关系。

资源模块类型 描述 替代的 loader
asset/resource 发送一个单独的文件并导出URL file-loader
asset/inline 导出一个资源的 data URI url-loader
asset/source 导出资源的源代码 raw-loader
asset 在导出一个 data URI 和发送一个单独的文件之间自动选择 url-loader,并配置资源体积限制
‘javascript/auto’ 将 asset 模块的类型设置为 ‘javascript/auto’ 可以解决和旧的loader一起使用的asset重复的问题 旧的 assets loader,如 file-loader/url-loader/raw-loader
{










    module: {
        rules: [
            {
              test: /.(png|jpe?g|gif|svg)$/i,
              type: 'asset/resource',
            },
            {
              test: /.(woff|woff2|eot|ttf|otf)$/i,
              type: 'asset/resource',
            }
        ]
    }

  }
}

在实践中发现项目存在以下两点问题:

同一个域名下同时请求的资源数非常多

浏览器请求最多6个。这是由于浏览器对同一域名下的并发请求数量做了限制,这个限制的原因是为了避免过多的请求导致网络拥塞和服务器过载。同时,这个限制也可以帮助浏览器更好地资源,提高页面加载速度和性能。
image.png
这种情况一般有以下解决方案:

  1. 将资源分散到不同的域名下,以增加并发请求数量的限制,例如不同的CDN
  2. 将资源合并为一个文件,以减少请求数量,俗称雪碧图
  3. 采用http2的多路复用
  4. 合理的将图片资源内联

将chrome的高速3G模式打开,让资源加载时间更明显一点,发现有两张大于8kb的图片不是从内存缓存中去的,也就是说它们额外发起了一次请求,如果你页面中还有几个业务相关的接口请求,就会受到浏览器请求个数的限制,这时候我们就可以通过配置资源体积限制来有条件将png格式的内容转化成内联的base64格式。

image.png

{
  module: {
    rules: [
      {
        test: /.(png|jpe?g|gif)$/i,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 16 * 1024, // 16kb
          },
        },
      }
    ],
  },
}

具体这个阈值需要大家根据自己的业务情况去定,可以根据页面bundle.js内联了图片资源之后的加载时间情况,和请求的资源排队相加耗时哪个会更多一些,不要过度优化,有条件的可以一步到位,上cdn或者是http2。

未使用的图片打包没有过滤

项目中实际只使用了一张图片,assets目录下的所有image都被打包了,经常会有人这么去加载图片资源:

const img = require(`../../assets/${item.image}`)

在require一个资源的时候,require是同步的,需要运行时才能决定这究竟需要哪张图片,所以打包的时候会把assets目录下的资源全部打包进去,即使某些资源是未使用的状态。

所以需要将资源具体化,先把运行时可能要用到的图片都import进来,再根据item.image的值使用对应的图片。
或者把require的路径写死,不允许有字符串模版变量等。

如果前端都不知道要加载哪张图片,就应该用http链接的形式了,请求静态资源服务器。

场景六:集成speed-measure-webpack-plugin

由于该插件维护频率较低,但使用面特别广,webpack5在集成速度测量插件的时候,会出现各种奇怪的问题。

smp.wrap包裹的ts类型错误

因为speed-measure-webpack-plugin所依赖的webpack版本是4,webpack4的Configuration类型和webpack5不兼容,所以使用pnpm覆写speed-measure类型定义文件的@types/webpack版本,通过用”>”来从依赖的选择器分离出package的选择器,可以通过依赖名称前缀一个$来直接引用项目里@types/webpack的版本。

{










  "pnpm": {
    "overrides": {
      "@types/speed-measure-webpack-plugin>@types/webpack": "$@types/webpack"
    }
  }
}

You forgot to add 'mini-css-extract-plugin' plugin

在配合mini-css插件的时候出现报错,解决办法是降级到1.3.6版本。但我们肯定是希望使用最新版的插件了,
因为在speed-measure-webpack-plugin插件列表中获取了两次,可以在使用wrap包装webpack配置之前提取插件定义,然后在包装后将其放回。

const cssPluginIndex = mergedConfig.plugins!.findIndex(
  e => e.constructor.name === 'MiniCssExtractPlugin',
)
const cssPlugin = mergedConfig.plugins![cssPluginIndex]
const configToExport = smp.wrap(mergedConfig)
configToExport.plugins![cssPluginIndex] = cssPlugin

你也可以使用time-analytics-webpack-plugin,这是该插件的替代品,但看issues中貌似时间测量不太准确,而且尚未修复。

场景七:如何做代码分割(code splitting)

webpack5自带的部分默认配置基本上就是最佳实践了,做好代码分割主要是看自身项目的情况,而且对于单页面和多页面,http1.1和http2.0也有不同的区别,通常都会有以下几个通用的配置,coding splitting的最终目的是为了减少重复代码而划分成一个个公共的chunk文件,充分利用浏览器的缓存策略,提高缓存的命中率。

首先得了解下splitting的默认配置都是怎么样的

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async', // 提取chunks的方式,initial 则会从非 async 里面提取,all则所有的都会提
      minSize: 20000, // 生成 chunk 的最小体积
      minRemainingSize: 0, // 确保拆分后剩余的最小 chunk 体积超过限制来避免大小为零的模块,通常不需要指定
      minChunks: 1, // 拆分前必须共享模块的最小 chunks 数,举例:设置为2的时候,需要你有两个chunk都用到的模块,才会被拆分
      maxAsyncRequests: 30, // 按需加载时的最大并行请求数
      maxInitialRequests: 30, // 你的入口文件chunk最大并行的请求数
      enforceSizeThreshold: 50000, // 强制执行拆分的体积阈值,大于50000字节的被强制拆分成一个chunk
      cacheGroups: { // 相当于一个继承的关系,局部配置
        defaultVendors: {
          test: /[\/]node_modules[\/]/,
          priority: -10,
          reuseExistingChunk: true, // 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

设置异步和同步chunk之间共享

webpack默认只对异步模块划分chunk,对于initial的chunk来说,即使你包再大,也不会进行拆包。举个例子体会下:如果你设置了一个entry入口,包含了react,react-dom等依赖,webpack会将这些依赖和你的业务代码全部打进一个文件中。所以我们需要设置splitChunks的chunks: 'all',让同步chunk也享受webpack的拆包策略。因为webpack的默认生成chunks的最小体积(minSize)为20000字节且共享模块最小chunk数(minChunks)为1,配合上chunks: ‘all’以及defaultVendors的配置,这才拆分出来。你可以试下,如果将minSize指定成一个很大的数,676这个chunk就会被打进入口app文件的chunk中,注意下这里的字节数不是生成的chunk文件的大小乘以1024,文件系统中显示的已经是打包后的了。

如果要为node_modules下生成的包取个别名,可以为cacheGroups的defaultVendors设置name属性,例如vendor,就可以将下面的676的随机值替换成vendor了。
image.png

合理划分chunks数量和chunks大小

在划分chunks的时候,根据个人偏好和项目情况,都可能有着不同的配置。在这里只是给出一个参考:

  1. 如果是http1.1的情况,浏览器会限制同一时刻并发的请求数目,对于chrome来说同一时刻只能存在6个请求,我们可以设置maxAsyncRequests和maxInitialRequests来进行控制,在按需加载异步chunks的时候,最多允许再存在五个异步请求,maxAsyncRequests可以设置为5,针对一个entry来说,初始化chunk的数量maxInitialRequests为下面打包后的js文件数量。

image.png

{










  splitChunks: {
    maxAsyncRequests: 5,
    maxInitialRequests: 3
  }
}

  1. 如果是http2的情况,浏览器可以利用多路复用的机制,尽可能多的划分请求,所以这一块的配置用webpack5默认的就足够了,通常最多30个异步请求。当然你也可以把node_modules里面的依赖包分别形成一个chunk,这样可以充分的利用缓存,只要你某个依赖的版本不变,打包出来的chunk也不会变。这里会遍历node_modules下的每一个模块,为模块生成一个chunk。但要注意的是:你的minSize,也就是最小生成chunk的体积,需要设置合理,你设置为0的话,会将每个依赖包都单独打包,对于一些小的依赖包,可以混合在一个chunk中一起下发。我在这将react和react-dom单独打成了一个js文件,其他node_modules的依赖都混在vendor下。
{










    cacheGroups: {
        vendors: {
          test: /[\/]node_modules[\/]/,
          priority: -10,
          minSize: 0,
          name(module: Module, _chunks: Chunk[], cacheGroupKey: string) {
            const absolutePath = module.identifier().split('!').pop()!
            const parts = absolutePath.split(path.sep)
            const nodeModulesIndex = parts.lastIndexOf('node_modules')
            const packageName = parts[nodeModulesIndex + 1]
            const deps = ['react', 'react-dom']
            if (deps.includes(packageName))
              return `${cacheGroupKey}-${deps.join('~')}`

            return `${cacheGroupKey}`
          },
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true, // 如果该chunk包含的modules都已经另一个被分割的chunk中存在,那么直接引用已存在的chunk,不会再重新产生一个
        },
      },
    },
}
  1. 当然,如果有CDN的话,可以尝试将体积大的第三方包给external出来,通过在html模板中添加script标签引入React和ReactDom。因为CDN和你的站点不是同域的,浏览器的并发请求限制只是限制同域下的请求个数,所以也可以用来加快首屏的渲染速度。
{










  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM',
  },
}

场景八:使用持久化缓存

webpack5中最为重大的更新就是增加了文件系统的缓存,大幅度提升了开发和构建时的速度,我们可以通过以下配置进行开启:

{










  cache: {

    type: 'filesystem',

  },
}

需要注意的是:filesystem对 node_modules 来说,哈希值和时间戳的比较会被跳过,出于性能考虑,只使用包名和版本来进行缓存的更新,这也就意味着webpack认为第三方包只能通过对应的包管理器去修改,我们不应该去直接修改node_modules。如果你通过npm link\yarn link等调试本地包的时候,一定不要去设置resolve.symlinks: false,否则缓存就会出问题。

持久化缓存可以大幅提升构建的速度,又为什么不让它作为默认的选项呢?
因为需要让缓存失效的情况有太多了,webpack无法做到开箱即用。例如:

  1. 升级npm加载器和插件的时候
  2. 更改配置中正在读取的文件
  3. 当你有一个自定义构建脚本并更改它的时候

这些都不应该使用缓存。推荐设置buildDependencies的config为你的项目配置文件,如webpack.config.ts,__filename代表了当前的文件。用以配置需要让缓存失效来保障项目构建时的安全性,当你使用fs.readFile之类的文件读写API的时候,被读取的文件是不会让缓存失效的,如果有必要,需要手动添加到buildDependencies中。

{










  cache: {

    type: 'filesystem',

    buildDependencies: {
      config: [__filename],
    },

  },
}

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYuny9p6' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片