《Webpack实战》笔记

Ch.1 | Webpack简介
Webpack的作用是模块(模块:执行一个功能的代码)打包。
传统引入script文件到页面中的开发方式:
- 模块间耦合度高,依赖性强也就算了,依赖还不显式;
- 每个script分别请求,网络开销很大;
- 每个script的顶层作用域是全局作用域,在声明函数和变量时会污染全局作用域,而且会造成命名冲突。
Webpack打包模块:
- 模块间依赖关系清晰;
- 将模块合并到一个js文件后合并发送请求,减少网络开销;
- 每个模块的作用域仅限于这个模块,不污染全局。
一般webpack配置在本地,团队开发时共用一个版本。配置时可以用npm scripts来维护命令行脚本,当打包脚本参数过多的时候,用webpack.config.js以文件的形式维护配置参数。
webpack-dev-server提供自动更新功能,省下了每次更改代码后都要重新 npm run build
的麻烦。通过 npm install webpack-dev-server --save-dev
安装。webpack-dev-server是一个仅在本地开发才会用到的服务,生产环境中不需要,所以将其作为工程的devDependencies记录在package.json中。
Ch.2 | 模块打包
CommonJS和ES6是目前比较普遍的模块标准。它们的区别在于:
Ch.3 | 资源输入输出
webpack在打包的时候所有有依赖关系的模块被封装为一个chunk。在chunk内定义一个入口文件entry,一般情况下单页应用只要一个entry即可,但是一个chunk打包出的bundle在压缩前大于250k时webpack边认为这个bundle过于大了。此时可以考虑打包成多页应用,每一个entry对应一个bundle。
资源入口的配置项有:
- chunk name:如果工程只有一个entry,chunk name默认为"main"。
- context:是一个字符串,用于描述资源入口的路径前缀,使用绝对路径,使entry路径可以简洁些(尤其是在多入口的情况下)。其默认值是当前工程的根目录。
- entry:入口文件的形式可以是字符串、数组、对象、函数。
当第三方模块依赖比较多时,可以提取vendor将这些模块打包到一个单独的bundle中,以便更有效地利用客户端缓存,加快页面渲染速度。
资源出口的配置项有:
- filename:一个字符串,用于控制输出资源的文件名。在多入口的情况下可以使用模板。
- path:绝对路径字符串,指定资源输出的位置。webpack 4之后默认的位置就已经是dist目录。
- publicPath:可以是HTML相关或者Host相关的相对路径,也可以是CDN相关的绝对路径,指定间接资源的请求位置。
Ch.4 | 预处理器
Webpack的基本思想是‘一切皆模块’,整个项目通过模块依赖关系的描述连接在一起。通过webpack维护模块间的关系可以使工程结构更直观,代码的可维护性更强。
Webpack本身只认识JS,对于其他类型的资源需要各种各样的loader进行转译。loader(预处理器)本质上就是一个函数:output = loader(input)
在module中引入loader时的可配置项中比较重要的是:
- test:接受一个正则表达式或正则表达式数组,只有匹配上的模块才会使用loader的规则(input)
- use:接受一个包含所有loader的数组
⚠️ webpack在打包时是按照数组从后往前的顺序将资源交给loader处理的,因此写rules的use数组时要把最后生效的放在最前面。
每一个loader配置项中比较重要的一些:
- exclude:指定排除在外的模块
- include:指定生效的模块
⚠️ exclude优先级高于include,可以利用这一点对include中的子目录进行排除。
- resource:被加载的模块
- issue:加载者
- enforce:指定loader种类,只接受”pre”和”post”两个字符串中的一个,用来指定loader的执行顺序
常用的loader有:
- babel-loader:将ES6+编译成ES5
- ts-loader:连接webpack与Typescript的模块
- html-loader:将HTML文件转化为字符串并进行格式化
- handlebars-loader:处理handlebars模版
- file-loader:打包文件类型的资源,并返回其publicPath
- url-loader:与file-loader类似,唯一的不同是可以设置一个文件大小的阈值,大于它时返回publicPath,小于时返回文件的base64编码
- vue-loader:处理vue组件(vue-cli已经做了webpack的基本配置)
自定义一个loader需要:
- loader初始化
- 启用缓存
- 获取options
- 启用source-map以便开发时在浏览器控制台查看源码
Ch.5 | 样式处理
extract-text-webpack-plugin和mini-css-extract-plugin都是将style标签中的CSS提取出来输出单独CSS文件的webpack社区的插件。每一个chunk(即入口)生成1个CSS文件。extract-text-webpack-plugin和mini-css-extract-plugin的区别在于后者支持按需加载CSS。
Sass和Less都是CSS的语法预处理器,对CSS的语法做了些增强。webpack打包时需要对CSS预处理器做相应的配置。
PostCSS是一个编译插件的容器,它接受样式源代码并交由编译插件处理,最后输出CSS。由于它可以自由地跟插件结合,PostCSS可以在做浏览器兼容时自动添加前缀,为CSS做些stylelint统一代码风格,还可以跟CSSNext结合使用最新的CSS语法特性。
CSS Modules的核心理念是将CSS模块化使得每个CSS文件:
- 有单独的作用域,不会和外界发生命名冲突
- 方便对CSS进行依赖管理
- 方便通过composes复用
开启css-loader中的modules配置项(modules: true)即可使用。
Ch.6 | 代码分片 (Code Splitting)
代码分片可以有效降低首屏加载资源的大小,保证首屏的加载速度。 不论是单页Web 应用还是多页应用(即有多个入口),一些不常变动的工具和库可以提取出来放在一个单独的入口,利用客户端缓存提高加载速度。 optimization.Split.Chunks是一个webpack插件,它根据一些声明式的提取条件提取出Chunk中的模块进行打包,相比CommonsChunkPlugin更强大且易用。
异步加载/按需加载:当模块数量过多、资源体积过大时,可以lazy load模块,将一些用不到的模块延时加载。
在Webpack中用import() 函数进行lazy load。这个import函数加载的模块及其依赖会被异步地加载,并返回一个Promise对象。与ES6中要求import必须出现在代码的顶层作用域不同,Webpack的import函数可以在任何作用域调用。Webpack还允许使用magic comments对异步资源进行命名(默认的异步资源是从0.js开始的数字id)。
Ch.7 | 生产环境配置
Webpack可以: A. 用同一套配置文件(config.js)在不同环境下打包,用一个变量控制其环境 B. 为development和production各创建一个配置文件,再用webpack-merge进行配置合并和公共部分管理。 在Webpack4中直接修改mode项可以直接切换打包模式,自动添加许多适用于当前环境的配置项。
source map指的是将编译、打包、压缩后的代码映射回源代码的过程(打包后的代码几乎不存在可读性,因此维护还是通过source map映射回源代码)。source map跟踪源代码的每一步处理,最后生成.map文件。当dev tool被打开时,map文件会同时被加载,浏览器会使用它对打包后的bundle文件进行解析,分析出源代码的目录结构和内容。 只要不打开dev tool,浏览器是不会加载map文件的,所以map文件大一些不用过于担心。 source map有多种类型的配置,cheap-module-eval-source-map是一个不错的选择,属于打包速度和源码信息还原程度的一个良好折中。vue-cli3预选的也是此模式。
另外,CSS同样有source map,不过vue-cli3是默认关闭的。 source map的安全隐患是任何人都可以通过dev tool看到工程源码,导致代码被抄袭,资源被爬取,系统被攻击等问题。针对这个问题Webpack提供了hidden-source-map和nosources-source-map(好丑陋的命名)两种配置策略来提升source map的安全性。
hidden-source-map不会在bundle文件中添加对于map文件的引用,因此在浏览器dev tool中看不到map文件,因此也无法解析bundle。可以将map文件上传到**Sentry**,通过它进行报错和源码维护。
nosources-source-map相比之下安全性没那么强,但是使用方式相对简单。打包部署之后,在dev tool的Sources选项卡中看得到源码的目录结构,但是文件的具体内容会被隐藏起来。Console中依然可以查看源码的错误栈和准确行数。
还有一个办法是正常打包出source map,然后通过服务器的nginx设置(或其他类似工具)将.map文件只对固定的白名单(比如公司内网)开放。
资源压缩(uglify)是指将代码移除空格、换行、执行不到的代码等,缩短变量名,在执行结果不变的前提下将代码替换为更短的形式。一般代码在uglify之后体积会显著缩小,同时也变得基本不可读,在一定程度上提升了代码的安全性。在Webpack4中默认的JS压缩工具是terser的插件terser-webpack-plugin。CSS文件可以用optimize-css-assets-webpack-plugin进行压缩。这个插件本质上使用的是压缩器cssnano。
在资源过期前,浏览器会重复利用本地缓存。如果想要强迫用户端更新代码(比如修复了一个bug),可以更改资源的URL。具体方法是:
- **资源hash:**在每次打包时对资源内容计算一次hash,并作为版本号存放在文件名中,如bundle@2e0a691e769edb228e2.js,每当代码发生变化时相应hash也会变化。
- **输出动态HTML:**用html-webpack-plugin自动将更新过hash的资源名同步到HTML中的引用路径。
- 记得使用代码分片确保不常变动的代码部分仍旧使用缓存。(Webpack3中还存在chunk id不稳定的问题,在Webpack4中已修复)
webpack-bundle-analyzer可以帮助分析一个bundle的构成。bundlesize是一个工具包,通过npm执行bundlesize命令可以验证bubdle体积是否超限。可以将其作为自动化测试的一部分来监控和分析bundle体积。
Ch.8 | 打包优化
一条软件工程领域的经验:不要过早优化。在项目的初期不要看到任何优化点就拿来加到项目中,这样不但增加了复杂度,优化的效果也不会太理想。一般是当项目发展到一定规模后,性能问题随之而来,这时再去分析然后对症下药,才能达到理想的优化效果。
提升性能的方法无非两种:
A. 增加资源 (more source):比如使用更多CPU或内存
B. 缩小范围 (less request):去掉冗余的流程,减少重复性工作
增加资源: HappyPack是一个通过多线程来提升Webpack打包速度的工具,适合转译任务(比如用babel-loader转译ES6+语法,或者用ts-loader转译TypeScript等)比较重的项目,但对于本身不太耗时的Sass或Less的转译增益不多。
缩小范围:
- 使用exclude和include缩小模块的作用域(Ch.4)。
- 使用noParse将不需要Webpack解析(即不希望应用任何loader规则),内部也不存在对其他模块依赖的库忽略。注意这些库只是不被解析,但依然会被打包到bundle中。
- 使用IgnorePlugin完全排除一些模块,使其即便被引用了也不会被打包进资源文件中。
- 有些loader会有一个cache配置项用来在编译代码后保存一份缓存。Webpack5添加了一个实验阶段(目前无法自动检测缓存是否过期,需要手动修改cache.version)的配置项cache: {type:"filesystem"},未来这一块会优化。
DllPlugin有点儿像Code Splitting,但区别在于Code Splitting的思路是根据预设的规则提取模块,而DllPlugin则是将vendor完全拆出来,并且事先编译和打包,实际工程构建时直接取用即可。因此,理论上来说DllPlugin在打包速度上会比Code Splitting更快一些,但是相应地增加了配置和资源管理的复杂度。
由于ES6 Module依赖关系是在编译而非运行时构建的,因此打包时可以利用webpack的 🌳 tree shaking 🌳 功能检测死代码,对它们进行标记,并在资源压缩阶段用前面提到过的terser-webpack-plugin将它们从最终的bundle中去掉。 ⚠️ tree shaking只对ES6 Module生效。有一些npm包同时提供了ES6 Module和CommonJS两种导出形式,为了最大化tree shaking的效用,应该尽可能选择ES6 Module形式的模块。 此外,由babel-loader进行依赖解析的话,转化出模块是CommonJS形式的,无法进行tree shaking。因此如果工程中使用了babel-loader的话,要禁用它的模块依赖解析。
Ch.9 | 开发环境调优
H(ot) M(odule) R(eplacement) 模块热替换的工作原理:
Ch.10 | 更多JavaScript打包工具
性能与通用性有时是一对互相制衡的指标。若一个工具通用性特别强,那么它往往无法针对某一种场景做到极致,必然会有一些取舍,性能上可能就不如那些专注于某一个小领域的工具。webpack相较于其他JS打包工具便是具有良好通用性而打包性能比较一般的例子。
Rollup专注于JS打包,自身附加代码少,具备tree shaking,如果项目需求只是简单打包一些JS库的话可以比webpack更快速地完成打包。此外Rollup还支持amd、esm、iife、umd及system这些输出资源的模块形式。
Parcel改进了资源处理流程,若干个loader可以一次性一起输出字符串,打包速度快,此外配置相当简便(零配置)。
(完)