最近应用 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:并不能说是一种模块标准,更准确而言是一组模块形式的集合,目标是使一个模块能运行在各种环境下,根据当前全局对象判断目前处于哪种模块环境。