Better

Ethan的博客,欢迎访问交流

Webpack 小知识以及 CRA 处理方式

最近应用 StoryBook 时,碰到 Webpack 上的一些配置问题,于是简单了解了一下,顺带简单过了一遍 CRA 是如何处理的。

CRA 处理

javascript 处理

  • babel 转译
  • ForkTsCheckerWebpackPlugin 做类型检查

css 部分

  • 开发环境使用 style-loader,生产环境使用 MiniCssExtractPlugin.loader
    • 生产环境从 bundle 中提取出 CSS,以便于并行加载 CSS/JS 资源,更利于客户端缓存
    • 开发环境使用 style-loader,使用多个 style 标签将将 CSS 插入到 DOM 中
  • css-loader
    • 用于处理 CSS 中各种加载语法,比如导入(url/@import/image-set)以及模块化问题
    • importLoaders 参数决定在使用 css-loader 之前,要使用几个其他的 loader 处理
    • modules 配置 css modules:getLocalIdent 自定义生成函数,支持 :local:global 语法
  • postcss-loader
    • 准确来说是一个编译插件的容器,开发者可以指定使用哪些插件来实现特定的功能
    • 处理兼容性问题,将未来的 CSS 特性带到今天
    • 提供了 postcss-js parser 用于支持 CSS-in-JS
    • 相关插件:AutoPrefixer、postcss-cssnext
  • less-loader/sass-loader:编译为 css

其他资源:url-loader

  • 如果小于某个值,则转成 base64 减少请求
  • 否则类似于 file-loader,仅处理图片路径,返回 publicPath

关于是否生成 sourcemap

  • 生产环境取决去用户设置,开发环境直接生成,因为 debug 需要
  • 生产环境使用 source-map,开发环境使用 cheap-module-source-map

Webpack 特性

chunk 和 module

  • module:每个源码 js 文件其实都可以看成一个 module
  • chunk:每个打包落地的 js 文件其实都是一个 chunk,每个 chunk 包含很多 module
  • 默认的 chunk 数量实际上是由你的入口文件的 js 数量决定的,但是如果你配置动态加载或者提取公共包的话,也会生成新的 chunk

webpack 几个重要特性

  • code splitting
    • 代码分片与公共模块提取
      • 通过入口划分代码:手工配置和提取公共模块十分复杂,且会带来公共模块和业务模块处于不同依赖树等问题
      • SplitChunks or CommonsChunkPlugin
      • vendor 配合 SplitChunks 提高浏览器缓存的稳定性
  • tree shaking
  • webpack-dev-server:只会将打包结果放在内存中,不会写入实际的 bundle.js

可以考虑开启缓存避免重复打包,自定义 loader 中可以通过 this.cacheable 进行判定

CommonsChunkPlugin

  • 作用将多个 chunk 从公共的部分提取出来,减少重复打包,提升性能,更高效利用客户端缓存。
  • 提取多入口公共模块,或单入口的 vendor 模块
  • 支持设置提取范围,比如指定哪几个 entry 进行抽取
  • 支持设置提取规则,默认时只要一个模块被两个入口 chunk 所使用就会被提取出来,这有时候不是我们需要的
    • 数字:minChunk 设置为 n 时,只有该模块被 n 个入口同时引用才会提取。Infinity 的作用:只想提取特定的几个模块,并将这个模块通过数组型入口传入,或者是为了生成一个没有任何模块而仅仅包含 webpack 初始化环境的文件,通常称为 manifest
    • 函数:返回 true 时表示提取
  • 存在的问题
    • 一个 plugin 只能提取一个 vendor
    • manifest 会多加载一个资源
    • 异步 Chunk 下不能正常提取

optimization.SplitChunks(Webpack 4)

  • 支持异步加载的场景
  • 指定 chunks 为 all 则表示对所有 chunk 生效,默认只对异步 chunk 生效
  • mode 属性用于指定是开发环境还是生产环境,用于自动添加一些配置
  • 只需要设置一些提取条件,如提取模式、提取模块的体积等,达到条件后会自动被提取出来

关于 exclude 和 include 配置

  • exclude 用于排除被匹配到的模块
  • include 只对正则匹配到的模块生效,加入我们将 include 设置为 src 目录,那么 node_modules 自然就被排除了
  • exclude 和 include 同时存在时,exclude 优先级更高

Webpack 生产环境

生产环境常用

  • DefinePlugin 定义相关挂载在全局对象上的环境变量
  • 很多框架与库都采用 process.env.NODE_ENV 定义环境变量,当值为 production 时,框架和库在打包时如果发现了它就可以去掉一些开发环境的代码,如警告信息和日志等
  • sourcemap:提供给浏览器进行解析,便于调试,map 文件通常很大,但不用担心,只要不打开开发者工具,浏览器是不会加载的,对普通用户而言没有影响
    • hidden-source-map:会产出完整的 map 文件,但不会在 bundle 中添加引用,如果想要追溯源码,则要利用第三方服务,如 Sentry
    • nosources-source-map:对安全性的保护没那么强,使用相对简单,能看到源码结构,但内容会被隐藏起来,仍然可以看到源代码错误栈,或 console 日志的准确行数
    • 或我们正常打包出 source map,通过 nginx 设置,将 .map 文件只对固定的白名单开放
  • 代码压缩
    • UglifyJS 和 terser,后者由于支持 ES6+ 代码的压缩,更加面向未来,Webpack4 中默认使用了 terser-webpack-plugin
    • 压缩 CSS:使用 mini-css-extract-plugin 提取后,接着使用 optimize-css-assets-webpack-plugin 进行压,这个插件本质使用的是 cssnano
  • 缓存
    • 资源 hash
    • html-webpack-plugin 自动应用最新的 bundle 文件,而不必手动的更新资源 URL 了
    • 使 chunk id 更稳定:由于 webpack 为每个模块指定的 id 是按数字递增的,当有新的模块进来会导致其他 id 发生变化,进而影响 chunk 的内容,解决的办法更改模块 id 的生成方式,当然 Webpack4 已经更改了模块 id 生成机制,不存在这个问题了
  • 体积监控与分析
    • 使用 webpack-bundle-analyzer 进行 bundle 构成分析
    • 使用 bundlesize 进行 maxSize 超限分析

Webpack 打包优化

打包优化

  • 原则:不要过早优化
  • 宏观角度来看,提升性能的方法无非两种:增加资源或者缩小范围
  • 多线程打包与 HappyPack
  • 缩小打包作用域
    • exclude/include
    • noParse
    • IgnorePlugin
    • cache
  • 动态链接库思想与 DllPlugin
    • 动态链接库:当一段相同的子程序被多个程序调用时,为了减少内存消耗,将子程序存储为一个可执行文件,当被多个程序调用时只在内存中生成和使用同一个实例
    • DllPlugin 对第三方模块或不常变化的模块进行预先编译和打包,然后实际构建中直接取用即可
  • 死代码检测与 tree shaking
    • 基于 ES Module 静态依赖检查,于是有了 tree shaking 功能,在资源压缩中去除死代码

Webpack 开发环境

开发环境调优

  • webpack-dashboard:更好的展示 webpack 打包情况的看板
  • webpack-merge:更方便的配置合并工具
  • speed-measure-webpack-plugin:分析整个打包过程中,各个 loader 和 plugin 上耗费的事件,有助于找出性能瓶颈
  • size-plugin:监控资源体积的变化情况
  • HotModuleReplacementPlugin 模块热替换
    • 为每个模块绑定一个 module.hot 对象,这个对象包含了 HMR 的 API
    • 一种方式是手动添加 HMR API,或是借助现成的工具,如 react-hot-loader/vue-loader

Webpack5:多应用共享

多应用如何做到代码共享,Webpack5 的 Module federation:允许运行时动态决定代码的引入和加载。先看下简单配置

{
    plugins: [
        new ModuleFederationPlugin({
            name: 'app1',
            library: {
                type: 'var',
                name: 'app1',
            },
            remotes: {
                app2: 'app2',
            },
            shared: ['react', 'react-dom']
        })
    ]
}

你需要了解的是

  • remotes 的代码自己不打包,类似 external
  • shared 的代码自己是有打包的

基础:esm

commonjs

  • 与直接通过 script 标签插入不同的是,commonjs 会形成一个属于模块自身的作用域,所有的变量和函数只有自己能访问,对外是不可见的
  • 导入 commonjs 模块时,第一次被加载,会执行该模块,然后导出内容。如曾被加载过,则不会再次执行,而是直接导出上次的结果。module 有个 loaded 属性用来表明是否被加载过。

es module

  • 也是每个文件作为一个模块,每个模块拥有自身的作用域,不同的是导入、导出语句
  • 自动采用严格模式

commonjs 和 es module 的区别

  • 动态与静态
    • commonjs 模块依赖关系的建立发生在代码运行阶段
    • es module 模块依赖关系的建立发生在代码编译阶段:路径不能是表达式,导入导出必须位于顶层作用域,因此在编译阶段就可以分析出来。
  • 值拷贝与动态映射
    • commonjs 获取的是值的拷贝,es module 是值的动态映射,且这个映射是只读的
  • 循环依赖
    • commonjs 模块若遇到循环依赖,没有办法得到预想中的结果,会得到空对象
    • es module 遇到循环依赖,同样无法得到预想的结果,会得到 undefined,但由于 es module 是动态映射,模块执行完后,undefined 就变成了我们定义的值。因此可以更好的支持循环依赖,只是需要开发者保证当导入的值被使用时已经设置好正确的导出值

静态分析的优势

  • 死代码检测和排除:静态分析打包时可以去掉未曾使用过的模块
  • 模块变量类型检查:确保模块之间传递的值或接口类型是正确的
  • 编译器优化:commonjs 本质上都是导入对象,es module 支持导入变量,减少了引用层级

UMD:并不能说是一种模块标准,更准确而言是一组模块形式的集合,目标是使一个模块能运行在各种环境下,根据当前全局对象判断目前处于哪种模块环境。



留言