环境: webpack v4.46.0,Nodejs v16.6.2
新建项目文件夹并安装 webpack 和 webpack-cli:
在项目根目录下新建 webpack.config.js
,作为 webpack 的默认配置文件。
用一张图来方便理解:
简单地说,module 是任何通过 import 或者 require 导入的代码,包括 js、css、图片资源等;多个 module 可以组成一个 chunk,每个 chunk 经过处理后会输出一个 bundle 文件
1)单入口单出口
以 src 目录下的 index.js
作为入口文件,打包输出文件 bundle.js
到 dist 目录下
这种配置一般用于单页应用,最终只会产生一个 chunk。
2)多入口单出口
对应的 entry
改用数组:
这种配置一般用于多页应用,但最终也只会产生一个 chunk。
3)多入口多出口
对应的 enrty
改用对象,key 表示打包后输出文件的名字,output.filename
中用占位符表示这个名字:
这种配置一般用于多页应用,且最终会产生多个 chunk。
指定 webpack 进行打包构建的环境是开发环境还是生产环境 —— 根据环境的不同,webpack 会默认开启不同的优化选项。
loader 相当于是一个转换器。webpack 默认只能解析 js / json 文件,对于其他类型的文件需要借助 loader 进行转换,之后才能解析。
webpack 执行打包构建的生命周期中会触发很多事件,plugin 监听某些事件并执行那些 loader 做不了的特定任务。
filename 很好理解,就是 entry 入口文件对应输出文件的名字,而 chunkFilename 指的是没有被列入 entry 中,但在某些情况下又不得不单独打包输出的文件的名字。
典型的例子就是动态加载模块,比如说入口文件 index.js 如下:
webpack 的配置如下:
打包后,entry 入口文件会对应产生一个叫做 index 的 chunk,同时输出一个叫做 index.js 的 bundle 文件。而 test.js 是动态加载的,它会被单独打包到另一个叫做 test (通过魔法注释指定)的 chunk 中,同时输出一个叫做 test.chunk.js 的文件。
path 很好理解,就是 entry 入口文件对应输出文件的路径,而 publicPath 指的是引用静态资源时的固定前缀。项目上线后通常可以配置 publicPath 为一个 cdn 地址,这样就可以引用部署到 cdn 上的静态资源了。
配置 bundle 文件名的时候,通常可以使用 xxx.[hash].js
这样的命名形式,这里的占位符可以使用 hash、chunkhash 以及 contenthash。
这三个 hash 通常和 CDN 缓存有关,代码改变触发文件 hash 改变,hash 改变导致资源引用的 URL 改变,从而触发 CDN 回源从服务器重新拉取最新的资源。
以多页面应用为例,假设有 A、B 两个页面。
PS:另外需要注意的是,chunkhash/contenthash 和 HMR 热更新不能一起使用。因为热更新针对的是开发环境,chunkhash 以及 contenthash 针对的是生产环境(涉及到 CDN 缓存)。
安装 babel 相关依赖(preset-env
对应的是 ES6 的 preset):
项目根目录下增加 .babelrc
文件:
增加 module.rules
项,配置 loader:
以加载和解析 less 文件为例。less-loader 将 less 文件解析为 css 文件,css-loader 解析 css 文件,style-loader 将 css 文件中的样式注入到 style 标签中。
安装:
配置:
解析图片或者字体需要用到 file-loader:
url-loader 是对 file-loader 的封装,可以提供 limit
参数:当图片体积小于 limit 参数的时候,使用 url-loader 进行处理,会用 base64 对图片进行编码,通过 dataUrl 引用图片;否则使用 file-loader 进行处理。注意这里一定要设置 esModule: false
,否则图片和字体默认会被视为 ES 模块,无法在页面中正常引用。
资源内联可以提高项目文件的可维护程度,并减少请求数量。以多页面应用为例,假如每个页面都有公用的 meta 信息,不可能每个 .html
文件都去写一遍,这时候就可以把 meta 信息集中到一个 meta.html
中,在需要用到的页面内联进去即可。
内联 HTML 和 JS 可以使用 raw-loader@0.5.1。
inline.html
文件:
inline.js
文件:
模板 HTML 文件:
构建后生成的 index.html
文件:
PS:这里之所以要使用 0.5.1 版本的 raw-loader,是因为这个版本的 raw-loader 默认采用 CJS 的模块导出方案,所以可以使用 require 直接导入;之后的版本则默认采用 ES Module 导出方案,如果想使用高版本,有两种方法:
<%= require('...').default %>
require 一个 ES6 模块两种方案:
style
中,这种方式下构建产物中不会直接出现样式代码;安装:
配置:
注意:
MiniCssExtractPlugin.loader
和 style-loader
功能上是冲突的,不能一起使用require(..).default
,因为 html-inline-css-webpack-plugin 导出方式是采用 ES Module。前面讲的内联,都是内联 src 下的文件到 html 中,那么有没有办法可以将 bundle 中的 css 和 js 文件内联到 html 中呢?这就要使用 html-webpack-inline-source-plugin 了。
它是 html-webpack-plugin 的一个插件,所以两者都要安装。之后进行配置:
PS:注意必须指定安装 @1.0.0-beta.2 版本的 plugin,npm 默认安装的是旧版本,有 bug。
图片和字体的内联,其实就是使用前面提到过的 url-loader,注意需要设置 esModule: false
。
文件监听也就是 watch mode。开启文件监听后,每次源代码发生更改,都会自动重新进行构建。
文件监听的原理是轮询文件的“最后一次编辑时间”是否改变。ignored
指定忽略监听的文件或者文件夹;poll
表示每秒轮询多少次;此外,并不是文件一更改就马上重新构建,必须是在 aggregateTimeout
指定的时间内没有再次更改之后,才会重新构建,有点类似于做了一层防抖处理,避免频繁构建。
热重载也就是 live reload,可以在每次源代码发生更改时自动重新进行构建 + 自动刷新浏览器。这里需要使用 webpack-dev-server 实现热重载。
安装:
在 package.json 中配置指令:
配置 webpack-dev-server:
热重载也有缺点,就是每次都会全局刷新浏览器,所有的状态都会重置。所以在热重载的基础上引入了热更新 —— 也就是 HMR(模块热替换),它既可以实现局部视图刷新,也可以保存数据状态。这里需要使用 webpack 内置的 HotModuleReplacementPlugin 实现热更新。
在生产环境下(mode: production
),打包后的文件都是经过压缩的,代码出错后不容易调试。而开启 sourcemap 之后,可以直接定位到出错的源代码,调试就很方便了。
本地开发的时候我们会起一个 http://localhost:8080 的服务器,这时候请求后端接口 http://mysite/api/getData会报跨域错误。此时可以通过 webpack-dev-server 配置一层与本地服务器同源的代理服务器,它会接受请求,再将请求转发给真正的后端服务器(同源仅作用于浏览器和服务器之间,所以这个转发是没问题的)。
举个例子,这是我们发起的请求:
这是 webpack-dev-server 的跨域配置:
我们发起请求的 url /api/getData/
会自动加上同源前缀,变成 http://localhost/api/getData,相当于现在是向同源的代理服务器发起请求;又由于 url 可以匹配 /api
,所以这个请求会被进一步转发给真正的后端服务器,相当于发起 http://mysite/api/getData 这个请求。
默认情况下,我们可能需要在 dist 目录下手动创建一个 html 文件去引用构建产物,这是比较麻烦的。通过 html-webpack-plugin,可以在 dist 目录下自动生成一个引用构建产物(资源)的 html 文件。
安装:
配置:
构建日志中可能包含很多我们并不关心的信息,可以借助 plugin 优化一下。
配置:
每次构建结束后会触发 compiler 对象的 done 钩子函数,可以在这个 hook 中捕获构建错误并进行相关处理:
开发应用的时候一般有两种环境,一个是方便开发调试的开发环境,一个是上线后给用户使用的生产环境。不同的环境,webpack 的配置也不同,比如生产环境需要配置代码压缩,开发环境需要配置热更新等。我们现在是共用一个 webpack.config.js 文件,所以需要解决的问题是:如何根据不同环境使用不同的 webpack 配置文件?
我们的基本策略是分离三个配置文件:
node 有一个 process 对象,我们在 process.env
上挂载一个 NODE_ENV
环境变量,用来标记当前是什么环境。因为不同操作系统设置环境变量的方式不同,为了方便统一设置,这里使用 cross-env 这个库。接着,我们在所有文件中都可以通过 node.env.NODE_ENV
获取当前环境类型。
package.json 文件:
webpack.base.js 文件:
webpack.prod.js 文件:
webpack.dev.js 文件:
基本策略是仍然使用单个配置文件,但是导出的不再是对象,而是函数。运行构建命令的时候传入一个 mode 参数,这个参数被函数接受,从而判断当前环境。
webpack.config.js 文件:
postcss 本身提供了一个强大的插件系统,可以对 css 进行后处理。在 webpack 中,需要通过 postcss-loader 去使用 postcss。
1)自动补齐前缀
为了向下兼容旧浏览器,某些比较新的 css 属性必须加上浏览器厂商的前缀,可以通过 postcss-loader 和 autoprefixer 实现前缀的自动补齐。
安装:
配置:
2)单位转换
移动端的适配分为两步:
这里进行单位转化的插件,就可以使用 px2rem-laoder、postcss-plugin-rem 等来完成。
1)单独使用
单独使用 eslint,首先需要安装 eslint 本体:
生成 eslint 配置文件 .eslintrc.json:
根据我们回答的问题,会预先配置好文件中的一些选项。如果想要使用其它公司团队的 eslint 规范,需要单独安装:
并修改配置文件中的 extends
选项:
运行下面命令对指定文件进行 lint 检测:
2)集成到 webpack 中使用
在 webpack 中集成 eslint 有两种方式,一种是 eslint-loader,但它存在一些问题,不久将被弃用;webpack 5 开始更推崇使用 eslint-webpack-plugin。这里以后者为例。
首先安装:
接着到项目根目录下新建 .eslintrc.json 文件,内容和上面差不多。如果想要使用某个公司团队的 eslint 规范,同样需要单独安装 npm 包。
然后配置 webpack.config.js:
基本使用就是这样了,每次运行构建 eslint 都会检测代码。默认情况下 eslint 的报错信息采用的是 stylish 的展示风格,可能不太直观,可以使用特定的插件修改报错信息的展示风格。安装:
配置:
在 webpack 中集成 stylelint 有三种方式:
这里以第三种方式为例。首先安装:
在项目根目录下新建 .stylelintrc.json 文件:
配置:
每次运行构建 stylelint 都会检测代码。
PS:以上两种 lintPlugin 的安装都不需要额外安装 eslint 或者 stylelint,因为 npm 从 v7 开始会自动安装 peerDependencies。
vue-cli 集成了 vue 和 webpack,但还是有必要掌握如何用 webpack 搭建一个基础的 Vue 开发环境。
创建目录并安装相关依赖:
新建 src 文件夹,src/App.vue
是 SFC 单文件组件:
src/index.js 作为打包入口:
src/index.html 作为构建产物使用的模板 html:
配置 webpack:
注意关于 vue 的几个依赖的作用:
.vue
中的各个语言块.vue
文件中的 <style></style>
语言块对于多页面应用来说,每个页面都对应“一个 entry + 一个 HtmlWebpackPlugin 实例”。如果每次添加或者删除页面都需要重新配置那就太麻烦了,因此理想的方案是根据页面情况实现自动配置。
假设项目结构如下:
page1 和 page2 目录下都有一个 index.js 表示页面入口,index.html 表示页面的模板 html。
首先安装 glob 方便读取文件路径:
通过一个 setMPA 函数处理多页面应用配置:
调用 setMPA 生成配置对象,注入到 webpack 的配置中:
以打包 + 发布一个 promise 为例。
新建 ./src/index.js ,编写核心代码。接着新建 webpack.config.js 进行打包配置:
新建 ./index.js 作为该库的入口文件:
打包:
发布:
使用:
首先正常安装
在需要的文件中导入使用:
webpack 本身可以配置 stats 展示打包构建的信息,但这些信息颗粒度比较大,不利于进行性能分析。所以这里还是要借助插件来分析。
性能分析其一,用 speed-measure-webpack-plugin 分析构建速度,它可以分析每个 loader 和 plugin 的耗时。安装后配置如下:
耗时长的 loader 或者 plugin 会显示为红色,这也是我们需要关注并优化的重心。
性能分析其二,用 webpack-bundle-analyzer 分析打包体积,在浏览器的 8888 端口下可以看到每个文件的体积信息以及各个 chunk 的包含关系,方便我们进行分析。
PS:安装这个 npm 包可能会报很恶心的 node gyp 错误,可以尝试切换回 npm 原来的镜像源(不要使用淘宝镜像源)
文件结构优化指的是要合理地拆分代码文件。为什么要拆分代码文件呢?一般是从两个角度考量:
通过 import()
或者 require.ensure()
对某些体积较大的模块实现按需加载、动态加载的时候,这些模块会打包到单独的文件中。如果用户用不到这个模块,那么他们就无需加载它,不再像之前那样一股脑地加载整个代码文件。
对于多页面应用,采用之前提到的多页面应用打包方案,使每个页面都有自己对应的文件,这样用户在进入某个页面的时候,只需要加载和这个页面相关的资源,而不是全部一次性加载。
形成 chunk 的方法有三种:
通过配置 optimization.splitChunks.cacheGroups
,可以将公用的第三方库代码抽离成一个单独的 chunk,下面通过一个例子来理解这个过程。
假设我们的应用有两个页面,对应两个 entry 入口文件:
webpack 配置如下:
chunks: "async"
chunks 的默认值就是 async,表示会将异步导入(动态导入)的模块抽离成单独的 chunk。
对于 page1.js:本身 entry 文件就会对应一个 chunk,而 jq 和 react 都是同步导入的,因此不会从这个 chunk 中分离,它们三个最终会打包到一起,并输出到 page1.bundle.js 文件。而 lodash 是动态导入的,会分离到一个单独的 chunk 中,并输出到 vendors~page1-lodash.js 文件
对于 page2.js:本身 entry 文件就会对应一个 chunk,而 jq 是同步导入的,因此不会从这个 chunk 中分离,它们两个最终会打包到一起,并输出到 page2.bundle.js 文件。而 lodash 是动态导入的,它会和 page1.js 中同样动态导入的 lodash 一起打包到同一个 chunk 中,最终输出到 vendors~page1-lodash.js 文件。react 也是动态导入的,它也会打包到一个单独的 chunk 中,最终输出到 vendors~page2-react.js 文件
综上,最终会有 4 个 chunk,输出到 4 个 bundle 文件中。从控制台打印信息可以看出确实是这样的:
借助 BundleAnalyzePlugun 可以更加直观地分析 bundle 的成分,如下图:
chunks: "initial"
initial 表示,不管模块是同步导入还是异步导入,都会被抽离成单独的 chunk。如果不同的 chunk 都通过同步导入的方式共用了同一个模块,则这两个模块可以被抽离到同一个 chunk 中。
综上,最终会有 6 个 chunk,输出到 6 个 bundle 文件中。
控制台打印信息如下:
BundleAnalyzePlugun 的分析情况如下:
PS:使用 chunks:"initial"
的时候需要注意,会有一个 minSize 字段表示被抽离成单独 chunk 的模块至少需要多大,如果模块体积本身小于这个值,则它也不会被单独抽离成 chunk,而是和 entry 对应的 chunk 打包在一起。
chunks: "all"
all 的特点在于,只要两个 chunk 共用了同一个模块,则不管模块在各自的 chunk 中是同步导入还是异步导入,最终都可以被抽离到同一个单独的 chunk 中。
控制台打印信息如下:
BundleAnalyzePlugun 的分析情况如下:
从这三种设置的结果可以看出,chunks:"all"
是可以最大程度复用代码的,因为在它的规则下,只要是模块被共用了,就可以被抽离到同一个 chunk 中。
webpack 打包构建的过程中会进行很多搜索过程,如果可以通过修改配置减小文件搜索的范围,那么就可以提高构建速度。
从配置 noParse 的角度来说:
默认情况下,我们导入 jq 或者 lodash 这样的库时,webpack 会去递归地解析这些库是否有其他第三方依赖。这个过程其实是不必要的,所以可以通过 noParse 配置不需要递归解析的模块:
从配置 resolve 的角度来说:
resolve.alias
可以配置路径的别名,减少类似 import xxx from '../../a/b' 这样繁琐的导入语句。不仅开发上更加方便,而且 webpack 解析到别名的时候,可以直接去对应的目录找到模块。
使用:
注意,在 HTML 或者 CSS 中使用别名路径的时候,必须加 ~
前缀。另外,必须安装 html-loader 和 css-loader,webpack 才能正确解析别名路径对于资源的引用。
resolve.extensions
提供一个后缀名数组,如果像 import
这样的导入语句省略了文件后缀名,则会为文件依次加上数组中的后缀名,看文件是否存在。这意味着我们经过配置后,在导入语句中可以省略文件的后缀名。
一般来说,应该将出现频率较高的后缀名写在前面,加快 webpack 解析时的匹配速度。另外不应该配置并省略过多的后缀名,否则会增加 webpack 解析时的查找时间。
resolve.modules
指定 webpack 去哪些路径下查找模块,默认会从项目根目录开始找,找不到就往外层找。一般我们自己写的模块或者第三方模块都在项目根目录下了,所以可以指定一下目录,减少不必要的向外查找。
resolve.mainFields
指定的是去查找第三方模块的 package.json
文件的哪些字段,从而找到模块的入口文件。一般来说入口文件都配置在 main 字段中,所以可以直接将其配置为 ['main']
。
从配置 loader 的角度来说
可以排除掉一些不需要解析的文件,或者精准指定需要解析的文件,从而减小解析时间,加快构建速度。
以 babel-loader 为例,默认情况下它会解析根目录中的所有 js 文件,但实际上,node_modules 中的很多第三方包本身就已经经过处理了,无需再进行解析,那么这部分就可以排除掉;同时,我们需要解析的通常是自己编写的代码,所以可以明确指定解析 src 目录下的文件:
webpack@4 之前使用 HappyPack,webpack@4 之后使用 thread-loader。安装后配置:
排在 thread-loader 之后的那些 loader 会被放在一个进程池中单独运行。这里需要注意,进程池中的 loader 不能产生新文件,因此类似 MiniCssExtractPlugin.loader 这样产生 css 文件的 loader 是不能使用 thread-loader 的。
PS:不管是 HappyPack 还是 thread-loader 都只适用于大型项目,小型项目中的优化很不明显,甚至可能反而降低打包构建的速度。
CSS 和 JS 的压缩可以开启多进程并行压缩(默认开启):
前面讲到的都是提升首次构建速度,我们可以将首次构建的结果缓存下来,然后利用缓存提升二次构建的速度。
开启 babel-loader 的缓存功能:
开启 terser-webpack-plugin 的缓存功能:
或者使用 cache-loader 缓存构建结果:
或者使用 hard-source-webpack-plugin 缓存构建结果:
优化打包体积即减小打包体积,基本策略是对文件体积进行压缩,或者设法减少文件的数量。
tree-shaking 就是所谓的摇树优化,可以实现 DCE,即去除没有用到的代码,包括:
在讲 tree-shaking 之前,首先要理解静态分析的概念。静态分析指的是不需要实际执行代码,仅从字面量就可以对代码进行分析,诸如 import()
和 CommonJS 的 require()
都是动态的,而 ESModule 则不一样,它支持静态分析 —— 在使用 ESModule 的时候,模块是否导入、是否导出、模块之间的依赖关系,这些都是可以提前确定好的,而这种静态分析的特性正是实现 tree-shaking 的关键。
tree-shaking 如何发挥作用呢?以下面的代码为例:
虽然只是导入并使用了 a,但实际上最终 a 和 b 都会被打包到 bundle 中,这会无形增加代码体积。但是如果使用了 tree-shaking,则最终只有 a 会被打包。
同样的,如果是下面这种情况:
使用了 tree-shaking 之后,由于没有用到 b,所以最终 b 也不会被打包。
对于有副作用的代码(会向外界产生可观察的变化),tree-shaking 无法将其修剪掉。如果确定自己的项目没有副作用,可以配置 webpack.config.js 的 optimization.sideEffects: true
(生产环境自动开启),同时配置 package.json 的 sideEffects: false
,告知 webpack 无需考虑副作用的问题,可以放心进行 tree-shaking;另外也可以指定一个路径数组,明确告知 webpack 哪些文件有副作用,从而让 webpack 为其它没有副作用的文件进行 tree-shaking 处理。
前面也说过,tree-shaking 依赖于 ESModule 的静态分析,那么对于 lodash 这样不使用 ESModule 模块规范的第三方库,怎么进行 tree-shaking 呢?这时候可以考虑使用这种库的 es 版本,比如 lodash 对应的就有一个 lodash-es 版本。
webpack 在生产环境下默认开启 tree-shaking,当然也可以手动开启:
scope-hoisting 可以将多个函数声明压缩为一个,这样做的好处一个是减少声明语句,从而减小代码体积;一个是减少函数作用域数量,从而降低内存开销。
webpack 在生产环境下默认开启 scope-hoisting,当然也可以手动开启:
由 html-webpack-plugin 生成的 html 文件,可以通过设置 minify: true
开启压缩功能(生产环境下默认开启)。
webpack 默认内置了 uglifyjs-webpack-plugin
或者 terser-webpack-plugin
,并且在生产环境下自动开启,因此 JS 代码默认就是经过压缩的。
当然也可以自定义配置(具体配置项需要参考 terser):
上面指的是对 JS 代码进行压缩,还有一种减少 JS 代码的策略是动态引入 polyfill。
babel 所做的事情只是转换语法,比如 const 转化为 var,箭头函数转化为普通函数等,对于诸如 map、Promise 这样比较新的 api 则无法进行处理,这时候就需要借助 polyfill 实现向下兼容。但是单纯使用 babel-polyfill 的问题在于,任何时候都是全量引入的,而有些用户的浏览器比较新,其实用不着使用 polyfill。
所以如果能实现动态引入 polyfill,也可以减少代码体积。这里借助的是 polyfill-service,我们引用它提供的 polyfill CDN,对于不同的浏览器,它会返回不同版本的 polyfill。
有三种方案,不管是哪一种,底层使用的压缩引擎都是 cssnano,而 css-loader 已经内置了 cssnano,因此无需额外安装。
方案一:postcss+ cssnano
需要安装 postcss-loader,接着进行配置:
方案二:optimize-css-assets-webpack-plugin + cssnano
适用于 webpack@5 之前的版本。需要安装 optimize-css-assets-webpack-plugin,接着进行配置:
方案三: css-minimizer-webpack-plugin
适用于 webpack@5 之后的版本。需要安装 css-minimizer-webpack-plugin,接着进行配置:
上面的 css 压缩都基于 cssnano,它很好用,但无法移除那些没有使用过的样式。这里可以使用 purgecss-webpack-plugin 实现 css 中的 tree-shaking。
注意这里的 path 并不是指要移除无用样式的 css 所在的路径,而是指引用这个 css 的文件所在的路径,可以直接配置为 src 下的所有文件。purgecss 会对这些文件进行分析,最终产出一个移除了无用样式的 css 文件。
从减小文件数量的角度来说:
1)可以使用前面提到的 url-loader,对体积小于 limit 的图片进行 base64 编码,转化为 dataUrl 内联进我们的应用
2)对于 svg 图片,可以使用类似的 svg-url-loader 对其进行 utf-8 编码,转化为 dataUrl 内联进应用。它相比 base64 编码的优势在于,字符串更短,浏览器解析更快
3)对于图标类图片,可以使用 postcss-loader + postcss-sprites(需要额外安装 phantomjs),它可以将多张图片合并成一张雪碧图,并且自动调整图片的背景位置。
大致配置如下:
postcss-sprites 作用的原理可以简单理解为,将 css 文件中引用到的图片资源合并成一张雪碧图,并自动处理背景图的展示位置。经由 file-loader 处理后,最后产出的 bundle 中只包含雪碧图这一张图片。
这里需要注意,spritePath
配置的是雪碧图的存放路径。一般雪碧图放在 src 中而不是 dist 中,因为 dist 中本来就会在 file-loader 的作用下产出图片,没有必要重复导出雪碧图到 dist 中 —— 即使导出了,也属于没有被使用的静态资源,会被 clean-webpack-plugin 清理掉。
此外,postcss-sprites 这个插件不支持识别 resolve.alias
配置的别名。
从减小文件大小的角度来说,对大体积的图片可以使用 image-webpack-loader 进行无损压缩。这个 loader 必须在 file-loader 之前处理图片,所以最好配置 enforce: 'pre'
。