Webpack 4 定制前端开发环境
本文总结了 Webpack 4 的一些配置点,希望通过学习这些配置点,降低使用门槛。纵观前端的打包历史,似乎是欠了太多债,有很多历史包袱,所以工程化这一块一直是一大痛点。配置繁琐,容易出错,本文总结了最新的 Webpack 4 功能点,一起来学习一下,可以点击右侧的文章目录直达。
webpack 概念和基础使用
安装和使用
1 | # 安装 webpack |
webpack 的基本概念
webpack 本质上是一个打包工具,它会根据代码的内容解析模块依赖,帮助我们把多个模块的代码打包
webpack 会把我们项目中使用到的多个代码模块(可以是不同文件类型),打包构建成项目运行仅需要的几个静态文件
入口(entry)
入口可以使用
entry
字段来进行配置,webpack
支持配置多个入口来进行构建
1 | module.exports = { |
转换器(loader)
可以把
loader
理解为是一个转换器,负责把某种文件格式的内容转换成 webpack 可以支持打包的模块
- 当我们需要使用不同的
loader
来解析处理不同类型的文件时,我们可以在module.rules
字段下来配置相关的规则,例如使用Babel
来处理.js
文件
1 | module: { |
插件(plugin)
模块代码转换的工作由
loader
来处理,除此之外的其他任何工作都可以交由plugin
来完成。通过添加我们需要的plugin
,可以满足更多构建中特殊的需求。例如,要使用压缩JS
代码的uglifyjs-webpack-plugin
插件,只需在配置中通过plugins
字段添加新的plugin
即可…
1 | const UglifyPlugin = require('uglifyjs-webpack-plugin'); |
plugin
理论上可以干涉webpack
整个构建流程,可以在流程的每一个步骤中定制自己的构建需求
输出(output)
构建结果的文件名、路径等都是可以配置的,使用
output
字段
1 | module.exports = { |
我们一开始直接使用
webpack
构建时,默认创建的输出内容就是./dist/main.js
一个简单的 webpack 配置
我们把上述涉及的几部分配置内容合到一起,就可以创建一个简单的
webpack
配置了,webpack
运行时默认读取项目下的webpack.config.js
文件作为配置。所以我们在项目中创建一个webpack.config.js
文件
1 | const path = require('path'); |
搭建基础的前端开发环境
关联 HTML
webpack
默认从作为入口的.js
文件进行构建(更多是基于SPA
去考虑),但通常一个前端项目都是从一个页面(即 HTML)出发的,最简单的方法是,创建一个 HTML 文件,使用script
标签直接引用构建好的 JS 文件,如…
1 | <script src="./dist/bundle.js"></script> |
- 但是,如果我们的文件名或者路径会变化,例如使用
[hash]
来进行命名,那么最好是将HTML
引用路径和我们的构建结果关联起来,这个时候我们可以使用html-webpack-plugin
html-webpack-plugin
是一个独立的node package
,所以在使用之前我们需要先安装它,把它安装到项目的开发依赖中
1 | npm install html-webpack-plugin -D |
然后在
webpack
配置中,将html-webpack-plugin
添加到plugins
列表中
1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); |
这样配置好之后,构建时
html-webpack-plugin
会为我们创建一个HTML
文件,其中会引用构建出来的 JS 文件。实际项目中,默认创建的HTML
文件并没有什么用,我们需要自己来写HTML
文件,可以通过html-webpack-plugin
的配置,传递一个写好的 HTML 模板…
1 | module.exports = { |
这样,通过
html-webpack-plugin
就可以将我们的页面和构建JS
关联起来,回归日常,从页面开始开发。如果需要添加多个页面关联,那么实例化多个html-webpack-plugin
, 并将它们都放到plugins
字段数组中就可以了…
构建 CSS
我们编写
CSS
,并且希望使用webpack
来进行构建,为此,需要在配置中引入loader
来解析和处理CSS
文件
1 | module.exports = { |
css-loader
负责解析CSS
代码,主要是为了处理CSS
中的依赖,例如@import
和url()
等引用外部文件的声明;style-loader
会将css-loader
解析的结果转变成JS
代码,运行时动态插入style
标签来让CSS
代码生效…
经由上述两个
loader
的处理后,CSS 代码会转变为 JS,和index.js
一起打包了。如果需要单独把 CSS 文件分离出来,我们需要使用extract-text-webpack-plugin
插件
1 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); |
CSS 预处理器
在上述使用 CSS 的基础上,通常我们会使用
Less/Sass
等 CSS 预处理器,webpack 可以通过添加对应的loader
来支持,以使用Less
为例,我们可以在官方文档中找到对应的loader
1 | module.exports = { |
处理图片文件
在前端项目的样式中总会使用到图片,虽然我们已经提到
css-loader
会解析样式中用url()
引用的文件路径,但是图片对应的jpg/png/gif
等文件格式,webpack
处理不了。是的,我们只要添加一个处理图片的loader
配置就可以了,现有的file-loader
就是个不错的选择…
file-loader
可以用于处理很多类型的文件,它的主要作用是直接输出文件,把构建后的文件路径返回。配置很简单,在rules
中添加一个字段,增加图片类型文件的解析配置
1 | module.exports = { |
使用 Babel
Babel
是一个让我们能够使用ES
新特性的JS
编译工具,我们可以在webpack
中配置 Babel,以便使用ES6
、ES7
标准来编写JS
代码
1 | module.exports = { |
启动静态服务
至此,我们完成了处理多种文件类型的 webpack 配置。我们可以使用
webpack-dev-server
在本地开启一个简单的静态服务来进行开发
1 | "scripts": { |
尝试着运行
npm start
或者yarn start
,然后就可以访问http://localhost:8080/
来查看你的页面了。默认是访问index.html
,如果是其他页面要注意访问的 URL 是否正确
完整示例代码
1 | const path = require('path'); |
webpack 如何解析代码模块路径
webpack 中有一个很关键的模块
enhanced-resolve
就是处理依赖模块路径的解析的,这个模块可以说是 Node.js 那一套模块路径解析的增强版本,有很多可以自定义的解析配置
- 在 webpack 配置中,和模块路径解析相关的配置都在
resolve
字段下
1 | module.exports = { |
常用的一些配置
resolve.alias
假设我们有个
utils
模块极其常用,经常编写相对路径很麻烦,希望可以直接import 'utils'
来引用,那么我们可以配置某个模块的别名,如
1 | alias: { |
上述的配置是模糊匹配,意味着只要模块路径中携带了 utils 就可以被替换掉,如:
1 | import 'utils/query.js' // 等同于 import '[项目绝对路径]/src/utils/query.js' |
如果需要进行精确匹配可以使用:
1 | alias: { |
resolve.extensions
1 | extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx'], |
这个配置的作用是和文件后缀名有关的, 这个配置可以定义在进行模块路径解析时,webpack 会尝试帮你补全那些后缀名来进行查找
配置 loader
loader 匹配规则
当我们需要配置
loader
时,都是在module.rules
中添加新的配置项,在该字段中,每一项被视为一条匹配使用loader
的规则
1 | module.exports = { |
loader
的匹配规则中有两个最关键的因素:一个是匹配条件,一个是匹配规则后的应用
规则条件配置
大多数情况下,配置
loader
的匹配条件时,只要使用test
字段就好了,很多时候都只需要匹配文件后缀名来决定使用什么loader
,但也不排除在某些特殊场景下,我们需要配置比较复杂的匹配条件。webpack 的规则提供了多种配置形式…
{test: ...}
匹配特定条件{include: ...}
匹配特定路径{exclude: ...}
排除特定路径{and: [...] }
必须匹配数组中所有条件{or: [...] }
匹配数组中任意一个条件{not: [...] }
排除匹配数组中所有条件…
上述的所谓条件的值可以是:
- 字符串:必须以提供的字符串开始,所以是字符串的话,这里我们需要提供绝对路径
- 正则表达式:调用正则的
test
方法来判断匹配 - 函数:
(path) => boolean
,返回true
表示匹配 - 数组:至少包含一个条件的数组
- 对象:匹配所有属性值的条件…
1 | rules: [ |
使用 loader 配置
module.rules
的匹配规则最重要的还是用于配置loader
,我们可以使用use
字段
1 | rules: [ |
use
字段可以是一个数组,也可以是一个字符串或者表示loader
的对象。如果只需要一个loader
,也可以这样:use: {loader: 'babel-loader'
,options: { ...} }
loader 应用顺序
- 对于上面的
less
规则配置,一个style.less
文件会途径less-loader
、css-loader
、style-loader
处理,成为一个可以打包的模块。 loader
的应用顺序在配置多个loader
一起工作时很重要,通常会使用在 CSS 配置上,除了style-loader
和css-loader
,你可能还要配置less-loader
然后再加个postcss
的autoprefixer
等。- 上述从后到前的顺序是在同一个
rule
中进行的,那如果多个rule
匹配了同一个模块文件,loader
的应用顺序又是怎样的呢?看一份这样的配置…
1 | rules: [ |
这样无法法保证
eslint-loader
在babel-loader
应用前执行。webpack
在rules
中提供了一个enforce
的字段来配置当前rule
的loader
类型,没配置的话是普通类型,我们可以配置pre
或post
,分别对应前置类型或后置类型的loader
…
- 所有的
loader
按照 前置 -> 行内 -> 普通 -> 后置 的顺序执行。所以当我们要确保eslint-loader
在babel-loader
之前执行时,可以如下添加enforce
配置
1 | rules: [ |
当项目文件类型和应用的
loader
不是特别复杂的时候,通常建议把要应用的同一类型loader
都写在同一个匹配规则中,这样更好维护和控制
完整代码
1 | const path = require('path'); |
使用 plugin
更多的插件可以在这里查找:plugins in awesome-webpack
DefinePlugin
DefinePlugin
是webpack
内置的插件,可以使用webpack.DefinePlugin
直接获取
- 这个插件用于创建一些在编译时可以配置的全局常量,这些常量的值我们可以在
webpack
的配置中去指定,例如
1 | module.exports = { |
有了上面的配置,就可以在应用代码文件中,访问配置好的变量了,如:
1 | console.log('Running App version' + VERSION); |
上面配置的注释已经简单说明了这些配置的效果,这里再简述一下整个配置规则。
- 如果配置的值是字符串,那么整个字符串会被当成代码片段来执行,其结果作为最终变量的值,如上面的
"1+1"
,最后的结果是2
- 如果配置的值不是字符串,也不是一个对象字面量,那么该值会被转为一个字符串,如
true
,最后的结果是'true'
- 如果配置的是一个对象字面量,那么该对象的所有
key
会以同样的方式去定义 - 这样我们就可以理解为什么要使用
JSON.stringify()
了,因为JSON.stringify(true)
的结果是'true'
,JSON.stringify("5fa3b9")
的结果是"5fa3b9"
。
社区中关于
DefinePlugin
使用得最多的方式是定义环境变量,例如PRODUCTION = true
或者__DEV__ = true
等。部分类库在开发环境时依赖这样的环境变量来给予开发者更多的开发调试反馈,例如 react 等。
- 建议使用
process.env.NODE_ENV
: … 的方式来定义process.env.NODE_ENV
,而不是使用process: {env: { NODE_ENV: ...} }
的方式,因为这样会覆盖掉process
这个对象,可能会对其他代码造成影响…
copy-webpack-plugin
我们一般会把开发的所有源码和资源文件放在
src/
目录下,构建的时候产出一个build/
目录,通常会直接拿build
中的所有文件来发布。有些文件没经过webpack
处理,但是我们希望它们也能出现在build
目录下,这时就可以使用CopyWebpackPlugin
来处理了…
1 | const CopyWebpackPlugin = require('copy-webpack-plugin'); |
extract-text-webpack-plugin
我们用它来把依赖的
CSS
分离出来成为单独的文件。这里再看一下使用extract-text-webpack-plugin
的配置
1 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); |
在上述的配置中,我们使用了
index.css
作为单独分离出来的文件名,但有的时候构建入口不止一个,extract-text-webpack-plugin
会为每一个入口创建单独分离的文件,因此最好这样配置
1 | // 这样确保在使用多个构建入口时,生成不同名称的文件 |
更好使用 webpack-dev-server
webpack-dev-server
是webpack
官方提供的一个工具,可以基于当前的webpack
构建配置快速启动一个静态服务。当mode
为development
时,会具备hot reload
的功能,即当源码文件变化时,会即时更新当前页面,以便你看到最新的效果…
基础使用
webpack-dev-server
是一个npm package
,安装后在已经有webpack
配置文件的项目目录下直接启动就可以
webpack-dev-server
默认使用8080
端口
1 | npm install webpack-dev-server -g |
package
中的scripts
配置:
1 | { |
配置
在 webpack 的配置中,可以通过
devServer
字段来配置webpack-dev-server
,如端口设置、启动gzip
压缩等,这里简单讲解几个常用的配置
public
字段用于指定静态服务的域名,默认是http://localhost:8080/
,当你使用Nginx
来做反向代理时,应该就需要使用该配置来指定Nginx
配置使用的服务域名port
字段用于指定静态服务的端口,如上,默认是8080
,通常情况下都不需要改动publicPath
字段用于指定构建好的静态文件在浏览器中用什么路径去访问,默认是/
,例如,对于一个构建好的文件bundle.js
,完整的访问路径是http://localhost:8080/bundle.js
,如果你配置了publicPath: 'assets/'
,那么上述bundle.js
的完整访问路径就是http://localhost:8080/assets/bundle.js
。可以使用整个URL
来作为publicPath
的值,如publicPath: 'http://localhost:8080/assets/'
。如果你使用了HMR
,那么要设置publicPath
就必须使用完整的URL
建议将
devServer.publicPath
和output.publicPath
的值保持一致
proxy
用于配置webpack-dev-server
将特定URL
的请求代理到另外一台服务器上。当你有单独的后端开发服务器用于请求 API 时,这个配置相当有用。例如
1 | proxy: { |
before
和after
配置用于在webpack-dev-server
定义额外的中间件,如
1 | before(app){ |
before
在webpack-dev-server
静态资源中间件处理之前,可以用于拦截部分请求返回特定内容,或者实现简单的数据mock
。after
在webpack-dev-server
静态资源中间件处理之后,比较少用到,可以用于打印日志或者做一些额外处理…
开发和生产环境的构建配置差异
- 我们在日常的前端开发工作中,一般都会有两套构建环境:一套开发时使用,构建结果用于本地开发调试,不进行代码压缩,打印
debug
信息,包含sourcemap
文件 - 另外一套构建后的结果是直接应用于线上的,即代码都是压缩后,运行时不打印
debug
信息,静态文件不包括sourcemap
的。有的时候可能还需要多一套测试环境,在运行时直接进行请求mock
等工作 webpack 4.x
版本引入了mode
的概念,在运行webpack
时需要指定使用production
或development
两个mode
其中一个,这个功能也就是我们所需要的运行两套构建环境的能力。
在配置文件中区分 mode
之前我们的配置文件都是直接对外暴露一个
JS
对象,这种方式暂时没有办法获取到webpack
的mode
参数,我们需要更换一种方式来处理配置。根据官方的文档多种配置类型,配置文件可以对外暴露一个函数,因此我们可以这样做
1 | module.exports = (env, argv) => ({ |
这样获取
mode
之后,我们就能够区分不同的构建环境,然后根据不同环境再对特殊的loader
或plugin
做额外的配置就可以了
- 以上是
webpack 4.x
的做法,由于有了mode
参数,区分环境变得简单了。不过在当前业界,估计还是使用webpack 3.x
版本的居多,所以这里也简单介绍一下3.x
如何区分环境
webpack
的运行时环境是Node.js
,我们可以通过Node.js
提供的机制给要运行的webpack
程序传递环境变量,来控制不同环境下的构建行为。例如,我们在npm
中的scripts
字段添加一个用于生产环境的构建命令…
1 | { |
然后在
webpack.config.js
文件中可以通过process.env.NODE_ENV
来获取命令传入的环境变量
1 | const config = { |
运行时的环境变量
我们使用 webpack 时传递的 mode 参数,是可以在我们的应用代码运行时,通过
process.env.NODE_ENV
这个变量获取的。这样方便我们在运行时判断当前执行的构建环境,使用最多的场景莫过于控制是否打印debug
信息…
- 下面这个简单的例子,在应用开发的代码中实现一个简单的
console
打印封装
1 | export default function log(...args) { |
同样,以上是
webpack 4.x
的做法,下面简单介绍一下3.x
版本应该如何实现。这里需要用到DefinePlugin
插件,它可以帮助我们在构建时给运行时定义变量,那么我们只要在前面webpack 3.x
版本区分构建环境的例子的基础上,再使用DefinePlugin
添加环境变量即可影响到运行时的代码…
1 | module.exports = { |
常见的环境差异配置
常见的 webpack 构建差异配置
- 生产环境可能需要分离
CSS
成单独的文件,以便多个页面共享同一个CSS
文件 - 生产环境需要压缩
HTML/CSS/JS
代码 - 生产环境需要压缩图片
- 开发环境需要生成
sourcemap
文件 - 开发环境需要打印
debug
信息 - 开发环境需要
live reload
或者hot reload
的功能…
webpack 4.x
的mode
已经提供了上述差异配置的大部分功能,mode
为production
时默认使用JS
代码压缩,而mode
为development
时默认启用hot
reload
,等等。这样让我们的配置更为简洁,我们只需要针对特别使用的loader
和plugin
做区分配置就可以了…
webpack 3.x
版本还是只能自己动手修改配置来满足大部分环境差异需求,所以如果你要开始一个新的项目,建议直接使用webpack 4.x
版本
拆分配置
前面我们列出了几个环境差异配置,可能这些构建需求就已经有点多了,会让整个
webpack
的配置变得复杂,尤其是有着大量环境变量判断的配置。我们可以把webpack
的配置按照不同的环境拆分成多个文件,运行时直接根据环境变量加载对应的配置即可。基本的划分如下…
webpack.base.js
:基础部分,即多个文件中共享的配置webpack.development.js
:开发环境使用的配置webpack.production.js
:生产环境使用的配置webpack.test.js
:测试环境使用的配置…
如何处理这样的配置拆分
首先我们要明白,对于
webpack
的配置,其实是对外暴露一个JS
对象,所以对于这个对象,我们都可以用JS
代码来修改它,例如
1 | const config = { |
因此,只要有一个工具能比较智能地合并多个配置对象,我们就可以很轻松地拆分 webpack 配置,然后通过判断环境变量,使用工具将对应环境的多个配置对象整合后提供给 webpack 使用。这个工具就是 webpack-merge
- 我们的 webpack 配置基础部分,即
webpack.base.js
应该大致是这样的
1 | module.exports = { |
然后
webpack.development.js
需要添加loader
或plugin
,就可以使用webpack-merge
的API
,例如
1 | const {smart} = require('webpack-merge') |
可见
webpack-merge
提供的smart
方法,可以帮助我们更加轻松地处理loader
配置的合并。webpack-merge
还有其他API
可以用于自定义合并行为
完整代码
webpack.config.js
1 | module.exports = function(env, argv) { |
configs/webpack.base.js
1 | const path = require('path'); |
configs/webpack.development.js
1 | const webpack = require('webpack'); |
configs/webpack.production.js
1 | const merge = require('webpack-merge'); |
模块热替换提高开发效率
HMR
全称是Hot Module Replacement
,即模块热替换。在这个概念出来之前,我们使用过Hot Reloading
,当代码变更时通知浏览器刷新页面,以避免频繁手动刷新浏览器页面。HMR 可以理解为增强版的Hot Reloading
,但不用整个页面刷新,而是局部替换掉部分模块代码并且使其生效,可以看到代码变更后的效果。所以,HMR
既避免了频繁手动刷新页面,也减少了页面刷新时的等待,可以极大地提高前端页面开发效率…
配置使用 HMR
HMR
是webpack
提供的非常有用的一个功能,跟我们之前提到的一样,安装好webpack-dev-server
, 添加一些简单的配置,即在webpack
的配置文件中添加启用HMR
需要的两个插件
1 | const webpack = require('webpack'); |
module.hot 常见的 API
前面
HMR
实现部分已经讲解了实现 HMR 接口的重要性,下面来看看常见的module.hot
API
有哪些,以及如何使用
module.hot.accept
方法指定在应用特定代码模块更新时执行相应的callback
,第一个参数可以是字符串或者数组,如
1 | if (module.hot) { |
module.hot.decline
对于指定的代码模块,拒绝进行模块代码的更新,进入更新失败状态,如module.hot.decline('./bar.js')
。这个方法比较少用到module.hot.dispose
用于添加一个处理函数,在当前模块代码被替换时运行该函数,例如
1 | if (module.hot) { |
module.hot.accept
通常用于指定当前依赖的某个模块更新时需要做的处理,如果是当前模块更新时需要处理的动作,使用module.hot.dispose
会更加容易方便module.hot.removeDisposeHandler
用于移除dispose
方法添加的callback
图片加载优化
CSS Sprites
- 如果你使用的
webpack 3.x
版本,需要CSS Sprites
的话,可以使用webpack-spritesmith
或者sprite-webpack-plugin
。 - 我们以
webpack-spritesmith
为例,先安装依赖…
1 | module: { |
在你需要的样式代码中引入
sprite.styl
后调用需要的mixins
即可
1 | @import '~sprite.styl' |
如果你使用的是
webpack 4.x
,你需要配合使用postcss
和postcss-sprites
,才能实现CSS Sprites
的相关构建
图片压缩
- 在一般的项目中,图片资源会占前端资源的很大一部分,既然代码都进行压缩了,占大头的图片就更不用说了
- 我们之前提及使用
file-loader
来处理图片文件,在此基础上,我们再添加一个image-webpack-loader
来压缩图片文件。简单的配置如下…
1 | module.exports = { |
使用 DataURL
有的时候我们的项目中会有一些很小的图片,因为某些缘故并不想使用
CSS Sprites
的方式来处理(譬如小图片不多,因此引入 CSS Sprites 感觉麻烦),那么我们可以在 webpack 中使用url-loader
来处理这些很小的图片…
url-loader
和file-loader
的功能类似,但是在处理文件的时候,可以通过配置指定一个大小,当文件小于这个配置值时,url-loader
会将其转换为一个base64
编码的DataURL
,配置如下
1 | module.exports = { |
代码压缩
webpack 4.x
版本运行时,mode
为production
即会启动压缩JS
代码的插件,而对于webpack
3.x
,使用压缩JS
代码插件的方式也已经介绍过了。在生产环境中,压缩JS
代码基本是一个必不可少的步骤,这样可以大大减小JavaScript
的体积,相关内容这里不再赘述。- 除了 JS 代码之外,我们一般还需要 HTML 和 CSS 文件,这两种文件也都是可以压缩的,虽然不像 JS 的压缩那么彻底(替换掉长变量等),只能移除空格换行等无用字符,但也能在一定程度上减小文件大小。在 webpack 中的配置使用也不是特别麻烦,所以我们通常也会使用。
- 对于 HTML 文件,之前介绍的
html-webpack-plugin
插件可以帮助我们生成需要的 HTML 并对其进行压缩…
1 | module.exports = { |
- 如上,使用
minify
字段配置就可以使用HTML
压缩,这个插件是使用html-minifier
来实现HTML
代码压缩的,minify
下的配置项直接透传给html-minifier
,配置项参考html-minifier
文档即可。 - 对于 CSS 文件,我们之前介绍过用来处理 CSS 文件的
css-loader
,也提供了压缩 CSS 代码的功能:
1 | module.exports = { |
在
css-loader
的选项中配置minimize
字段为true
来使用CSS
压缩代码的功能。css-loader
是使用cssnano
来压缩代码的,minimize
字段也可以配置为一个对象,来将相关配置传递给cssnano
分离代码文件
- 关于分离 CSS 文件这个主题,之前在介绍如何搭建基本的前端开发环境时有提及,在
webpack
中使用extract-text-webpack-plugin
插件即可。 - 先简单解释一下为何要把 CSS 文件分离出来,而不是直接一起打包在 JS 中。最主要的原因是我们希望更好地利用缓存。
- 假设我们原本页面的静态资源都打包成一个 JS 文件,加载页面时虽然只需要加载一个 JS 文件,但是我们的代码一旦改变了,用户访问新的页面时就需要重新加载一个新的 JS 文件。有些情况下,我们只是单独修改了样式,这样也要重新加载整个应用的 JS 文件,相当不划算。
- 还有一种情况是我们有多个页面,它们都可以共用一部分样式(这是很常见的,CSS Reset、基础组件样式等基本都是跨页面通用),如果每个页面都单独打包一个 JS 文件,那么每次访问页面都会重复加载原本可以共享的那些 CSS 代码。如果分离开来,第二个页面就有了 CSS 文件的缓存,访问速度自然会加快。虽然对第一个页面来说多了一个请求,但是对随后的页面来说,缓存带来的速度提升相对更加可观
3.x
以前的版本是使用CommonsChunkPlugin
来做代码分离的,而webpack 4.x
则是把相关的功能包到了optimize.splitChunks
中,直接使用该配置就可以实现代码分离。
webpack 4.x 的 optimization
1 | module.exports = { |
我们需要在 HTML 中引用两个构建出来的 JS 文件,并且
commons.js
需要在入口代码之前。下面是个简单的例子
1 | <script src="commons.js" charset="utf-8"></script> |
如果你使用了
html-webpack-plugin
,那么对应需要的 JS 文件都会在 HTML 文件中正确引用,不用担心。如果没有使用,那么你需要从stats
的entrypoints
属性来获取入口应该引用哪些 JS 文件,可以参考 Node API 了解如何从 stats 中获取信息
显式配置共享类库可以这么操作
1 | module.exports = { |
上述第一种做法是显示指定哪些类库作为公共部分,第二种做法实现的功能差不多,只是利用了 test 来做模块路径的匹配,第三种做法是把所有在 node_modules 下的模块,即作为依赖安装的,都作为公共部分。你可以针对项目情况,选择最合适的做法
webpack 3.x 的 CommonsChunkPlugin
webpack 3.x
以下的版本需要用到 webpack 自身提供的CommonsChunkPlugin
插件。我们先来看一个最简单的例子
1 | module.exports = { |
chunk
在这里是构建的主干,可以简单理解为一个入口对应一个chunk
。- 以上插件配置在构建后会生成一个
commons.js
文件,该文件就是代码中的公共部分。上面的配置中minChunks
字段为 3,该字段的意思是当一个模块被 3 个以上的chunk
依赖时,这个模块就会被划分到commons chunk
中去。单从这个配置的角度上讲,这种方式并没有4.x
的chunks: "all"
那么方便。
CommonsChunkPlugin 也是支持显式配置共享类库的
1 | module.exports = { |
上述配置会生成一个名为
vendor.js
的共享代码文件,里面包含了React
和React-Redux
库的代码,可以提供给多个不同的入口代码使用。这里的minChunks
字段的配置,我们使用了Infinity
,可以理解为webpack
不自动抽离公共模块。如果这里和之前一样依旧设置为 3,那么被 3 个以上的chunk
依赖的模块会和React
、React-Redux
一同打包进vendor
,这样就失去显式指定的意义了。
minChunks
其实还可以是一个函数,如:
1 | minChunks: (module, count) => { |
该函数在分析每一个依赖的时候会被调用,传入当前依赖模块的信息
module
,以及已经被作为公共模块的数量count
,你可以在函数中针对每一个模块做更加精细化的控制。看一个简单的例子:
1 | minChunks: (module, count) => { |
- 更多使用
CommonsChunkPlugin
的配置参考官方文档commons-chunk-plugin
。
进一步控制 JS 大小
按需加载模块
在 webpack 的构建环境中,要按需加载代码模块很简单,遵循 ES 标准的动态加载语法
dynamic-import
来编写代码即可,webpack
会自动处理使用该语法编写的模块
1 | // import 作为一个方法使用,传入模块名即可,返回一个 promise 来获取模块暴露的对象 |
- 注意一下,如果你使用了
Babel
的话,还需要Syntax Dynamic Import
这个Babel
插件来处理import()
这种语法。 - 由于动态加载代码模块的语法依赖于
promise
,对于低版本的浏览器,需要添加promise
的polyfill
后才能使用。 - 如上的代码,webpack 构建时会自动把
lodash
模块分离出来,并且在代码内部实现动态加载lodash
的功能。动态加载代码时依赖于网络,其模块内容会异步返回,所以 import 方法是返回一个promise
来获取动态加载的模块内容。 import
后面的注释webpackChunkName: "lodash"
用于告知webpack
所要动态加载模块的名称。我们在 webpack 配置中添加一个output.chunkFilename
的配置…
1 | output: { |
这样就可以把分离出来的文件名称用 lodash 标识了,如下图:
如果没有添加注释
webpackChunkName: "lodash" 以及 output.chunkFilename
配置,那么分离出来的文件名称会以简单数字的方式标识,不便于识别
以上完整示例代码
1 | const path = require('path'); |