由于文章篇幅较长,为了更好的阅读体验,本文分为上、中、下三篇:
上篇介绍了什么是 webpack,为什么需要 webpack,webpack 的文件输入和输出
中篇介绍了 webpack 在输入和输出这段中间所做的事情,也就是 loader 和 plugins
下篇介绍了 webpack 的优化,以及在开发环境和生产环境的不同用法
在上一篇中,介绍了通过设置 entry(入口文件)和output(出口文件),来对源代码进行处理,但是在处理过程中,webpack 是如何针对不同的文件进行打包的呢?这就是 loader 和 plugins 的要做的事情了。
loader
个人认为 loader 是 webpack 中最厉害的一个功能了,它让我们可以在项目随意 import
各种类型的文件,css scss html img 等等都不在话下,如果有相关的 loader 支持,甚至可以 import
其它语言的代码。
简单的说 loader 就是一个处理器,在 webpack 中配置好相应的 loader 之后,就可以在代码中像加载 JavaScript 模块一样使用 import
把其它类型的代码当做 JavaScript 模块加载。
loader 的用法有三种
- 在
webpack.config.js
中配置,这种方式是最常用的,下面会着重介绍。 - 在代码中显示的指定 loader ,下面的代码表示从 styles.css 加载样式文件,用
style-loader
和css-loader
来处理 css 文件。
import styles from 'style-loader!css-loader?modules!./styles.css';
- 在命令行中为某些类型文件执行 loader 。下面的命令表示在打包过程中,对
.css
文件使用style-loader
和css-loader
来处理。
webpack --module-bind 'css=style-loader!css-loader'
loader 的配置
loader 的配置其实比较简单,只是提供了太多简写,让新手有点摸不着头脑,首先用 JavaScript、TypeScript、css、scss 来展示常用的几种配置方式:
// webpack.config.js
module.exports = {
entry,
output,
module: {
rules: [{
test: /\.js$/,
loader: 'babel-loader',
options: { presets: ['env'] },
include: __dirname + '/src'
},
{ test: /\.tsx?$/, use: 'ts-loader' },
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.scss$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: { modules: true }
},
{ loader: 'postcss-loader' },
{ loader: 'sass-loader' }
]
}]
}
}
从上面的代码中我们可以发现,use
这个选项的配置是最没节操了,它可以是字符串、数组、甚至被 loader
这个选项代替,其实这都是简写。rules.loader
和 loader.options
是 rules.use: [ {loader, options} ]
的简写。这些配置项的含义分别是:
test
: 正则表达式,用来匹配文件的扩展名。use
: 对匹配出的文件使用的 loader 配置,如上面所说,该选项配置灵活,可以简写loader
: loader 名称options
: loader 的额外配置选项include / exclude
: 包括 或 排除 的文件夹,两个选项只能同时出现一个,上面的例子中include: __dirname + '/src'
表示babel-loader
只编译/src
文件下的文件,其它的不做处理;相反的,exclude: __dirname + '/src'
表示不编译/src
下的文件。
下面就来详细的介绍一下常用的 loader 和其配置。
(题外话,本来是先将 babel-loader 放到第一个介绍的,但是由于篇幅较长,且有些难懂,所以将其放到了后面)
样式处理
npm i style-loader css-loader less less-loader node-sass sass-loader postcss-loader autoprefixer -D
对于样式文件的处理,我们(我)通常会用到以下这些 loader :
- style-loader
- css-loader
- postcss-loader
- less-loader
- sass-loader
那么这些 loader 的使用场景和区别是什么呢?
首先介绍 less-loader 和 sass-loader 。less 和 sass 都是 css 预处理器,可以让 css 编写起来更爽,但是不能直接在浏览器中运行,所以需要先将
.less
或.scss
文件先转换成 css 。这就是 less-loader 和 sass-loader 的作用。无论是直接编写的 css ,还是由 less 或 sass 转换而来的 css 都不是 JavaScript 模块,这时候就要用到 css-loader ,它的作用就是把 css 转成 JavaScript 模块插入到代码中。
样式文件已经转换好了,但并不会产生任何效果。因为这些样式还没有添加到页面中,这时候就该轮到 style-loader 出场了,它的作用就是把转换后的样式添加到页面中,就像下面这样。
- 最后还有 postcss-loader 它的作用也很强大,最常用的功能就是帮助我们自动为一些样式属性名添加私有前戳(-moz、-ms、-webkit)。写过 vue 的同学都知道,当我们给 style 标签添加 scope 属性的时候,打包后的类名会自动添加自定义属性(
例如 .panel[_v-72f4cef2]
),这个功能就是基于 postcss-loader 实现的。
postcss 需要一份配置文件,这份配置文件以写在单独的文件中 (postcss.config.js
),也可以写在 package.json
的 postcss
属性中:
{
"postcss": {
"plugins": {
"autoprefixer":{}
}
}
}
对 postcss 感兴趣的同学可以看看这篇文章: PostCSS真的太好用了! (https://segmentfault.com/a/1190000014782560)
这些 loader 的执行顺序是 :
sass-loader
or less-loader
→ postcss-loader
→ css-loader
→ style-loader
通过对这些 loader 的配置,我们就可以把样式文件当做 js 文件一样引入了。
// styles.css
.red { color: red; }
// index.js
import './styles.css';
这里需要在额外提一下 css module ,这也是一个很好的特性,写 react 的朋友对它应该很熟悉:
// index.js
import styles from './styles.css';
export default () => (
<h2 className={styles.red}>css module</h2>
);
从上面的代码中可以看出,我们将样式当做 对象 styles
导入 jsx 中,那么该样式下的所有类名就是 styles
的属性名了。
这样的写法也同样适用于 ES6 的模板字符串:
// index.js
import styles from './styles.css';
const html = `<h2 class="${styles.title}">css module</h2>`;
document.body.innerHTML = html;
只要在 css-loader 的 options 中设置 { modules: true }
既可以开启此功能。
上面的这些配置,可以帮我们将 css 封装成 js对象 ,打包在 .js 文件中,然后运行的时候,以 <style></style>
的方式动态插入到页面中,但我们更希望可以将这些样式从 js 文件中抽取出来放到 css 文件,一来这样显得更优雅一些,二来可以减少 js 为文件体积,避免动态创建 style 标签所带来的性能损耗。这个功能需要在 plugins 中进行设置,下面也会讲到。
file-loader、url-loader
npm i file-loader url-loader -D
如果我们在页面中通过相对路径来引入图片、音频、视频、字体等文件资源时,在 webpack 环境中可能出现路径错误404的问题。主要原因是 开发时的目录结构 和 打包后的目录结构 一般都是不一样的,因此导致路径失效,而 file-loader 就是为了解决这个问题的。
file-loader 可以解析页面中引入的资源的路径,然后根据配置,将这些资源拷贝到打包后的目录中。
url-loader 则是对 file-loader 进行了一次封装,如果解析的资源是图片,则可以将改图片转成 base64 从而减少 http 请求一提升性能,同时也可以设置 limit。 只对指定大小的图片进行转换。
同样的也可以在 js 中引入资源
// index.js
import logo from './images/logo.png';
const img = new Image();
img.addEventListener('load', () => document.body.appendChild(img));
img.src = logo;
下面是 url-loader 的简单配置参考:
// webpack.config.js
module.exports = {
entry,
output,
module: {
rules: [{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [{
loader: 'url-loader',
options: {
limit: 10000, // 10KB 转换为base64
name: 'images/[name].[ext]' // 拷贝到 images 目录下
}
}]
}]
}
}
html-loader
npm i html-loader -D
在 Web 开发中,通常会用到很多 html 模板,传统的方式是将模板存在服务端,前端通过 http 请求加载模板,或者在 JavaScript 中拼接字符串,或者在页面中将模板内容写在 <script type="text/template"></script>
内。
而在 webpack 环境下,我们也可以把 html模板 当做 JavaScript 的模块来加载,以 Vue 为例:
<!-- template.html -->
<h2>{{ title }}</h2>
// index.js
import tpl from './template.html'
new Vue({
el: '#app',
template: tpl,
data: {
title: 'Hello Webpack'
}
});
在上面代码中,我们将 template.html
的内容以字符串方式导出,这正是 html-loader 的功能,也可以在配置只启用压缩功能。
// webpack.config.js
module.exports = {
entry,
output,
module: {
rules: [{
test: /\.html$/,
use: [{
loader: 'html-loader',
options: {
minimize: true // 开启压缩
}
}]
}]
}
}
babel-loader(重点)
npm i @babel/core babel-loader @babel/preset-env @babel/runtime @babel/plugin-transform-runtime -D
Babel is a compiler for writing next generation JavaScript.
从官方的简短介绍中可以知道, babel 属于编译器,输入 JavaScript 源码,输出 JavaScript 源码(source to source),其作用就是将目前部分浏览器目前还不支持的 ES2015+ 语法转换为 ES5 语法。
babel-loader 则是让 babel 可以在 webpack 中使用的工具,同理如果你使用的是 gulp ,则需要用到 gulp-babel 这个包。
实际上,如果只是用 babel 的话,输入的代码和编译后输出的代码是相同的(被 webpack 混淆打包的代码与 babel 无关)。因为 babel 的转换工作全都是由 babel 的插件来完成的。
关于 babel 的介绍和使用,仅仅一个小节的篇幅是完全不够,所以这里贴一个链接,有兴趣的读者一点要点进去看一下 一口(很长的)气了解 babel。
babel 也是需要进行配置的,一般有两种方式:
- 在根目录创建
.babelrc
- 在
package.json
的babel
属性中进行配置
我更倾向于在 package.json
进行配置,因为根目录放置太多文件,强迫症实在无法接受。无论是在 .babelrc
还是 package.json
中配置,配置的内容都是一样的,下面以在 package.json
中配置为例:
{
"babel": {
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"targets": {
"browsers": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
}
]
],
"plugins": [
"@babel/plugin-transform-runtime",
"@babel/plugin-syntax-dynamic-import"
]
}
}
在该配置中,presets
和 plugins
对应的值都是数组,同时数组的每一项可以是 string
(只指定名字),也可以是 array
(指定名字,并进行更具体的配置)
plugins
表示用到的插件,比如我们在代码中使用到了 import()
动态加载模块这个语法,那么就要在 plugins
添加 @babel/plugin-syntax-dynamic-import
这个插件了;我们需要对 babel 编译后的代码进行去重,就需要用到 @babel/plugin-transform-runtime
。 当然,这两个插件也是需要单独安装的 npm i @babel/runtime @babel/plugin-transform-runtime @babel/plugin-syntax-dynamic-import -D
。
presets
一组 plugins
的集合。比如我们可以把 @babel/plugin-transform-runtime 和 @babel/plugin-syntax-dynamic-import 打包到一起,叫 preset-my ,这样我们只需要在 presets 中添加 preset-my 就可以了,省去了对 plugins 的配置 。上面的配置文件只配置一个 @babel/preset-env
,这是最常用的配置,@babel/preset-env
后面的对象是对 @babel/preset-env
具体配置。我们注意到,其中有一个 targets.browsers
属性,指定了浏览器版本,这个属性也可以放在 package.json
的 browserslist
中。
为什么配置了 presets 还需要配置 plugins 呢?很简单,如上面所说, presets 是一组 plugins 的集合,也就说 babel 对不同阶段的语法做了整合,方便我们使用。但是在上面的配置中,我们只使用了 @babel/preset-env
这个集合里的插件,而 import()
处于 stage-3
阶段(记不太清了,也可能是 stage-2
),不包含于 @babel/preset-env
,所以就需要在 plugins 单独添加 @babel/plugin-syntax-dynamic-import
插件来对 import()
语法进行转换了。
社区中也提供了一些 presets ,比如 react 的 @babel/preset-react , vue 的 @vue/babel-preset-app
babel 的执行顺序是:
读取plugins数组
→ 按正序执行plugins内插件
→ 读取presets数组
→ 按倒序执行presets内容
简单的介绍了 babel 后,开始配置 babel-loader :
// webpack.config.js
module.exports = {
entry,
output,
module: {
rules: [{
test: /\.js$/,
loader: 'babel-loader',
// options: { presets: ['env'] }, 该项的配置和上面babel的配置完全相同,已经在package.json配置过,这里不需要再配置
include: __dirname + '/src' // 只对 ./src 目录下的代码进行编译
}]
}
}
ts-loader
npm i typescript ts-loader -D
如果你的项目是用 typescript
开发的,这时候就要样到 ts-loader 了。
ts-loader 的配置比较简单,但是有许多需要注意的细节,详情可以参照这里:https://github.com/TypeStrong/ts-loader/blob/master/README.md#configuration
plugins
讲完了 entry、output 和 loader,下面开始讲讲 plugins 。细心的读者应该已经发现,还没有提到代码的压缩,而且按照上面的方式打包会把 .css
和 .js
文件打包在一起,并且打包后的文件体积很大,可能还会存在冗余的代码等等一些问题,plugins 就是为了解决这类问题而产生的。
这里不要把 loader
和 plugins
搞混了,laoder 只是把特定的文件类型转换成 JavaScript 模块,plugins 是在打包过程中对所有模块进行特定的操作 。plugins
的值是一个数组,所有的 webpack 都需要手动通过关键字 new
来实例化。 下面就介绍一些常见的插件。
html-webpack-plugin
npm i html-webpack-plugin -D
webpack 是对 JavaScript 进行打包的,打包出的只能是 .js
文件。 而 JavaScript 要想在浏览器中运行,那就必须在 html 中通过 script 的方式引入。在没有其他工具帮助的情况下,我们只能手动创建 html 文件,然后再把打包后的 .js
文件和 .css
文件写到这个文件中,这样做很麻烦。这时候可以用 html-webpack-plugin
这个插件来自动完成上面的工作。
html-webpack-plugin
提供了一些配置项,如果不行配置,它会自动帮我创建一个空的 html 文件,然后将打包后的资源插入到这个页面内:
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry,
output,
plugins: [
new HtmlWebpackPlugin() // 创建 /dist/index.html 文件,并将 index_bundle.js 插入到这个页面中。
]
}
同样,我们也可以为其指定一个模板页:
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry,
output,
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html', // 生成的文件名称,默认为 index.html
template: 'src/index.html', // 以 src/index.html 为模板文件
inject: 'body', // 将打包后的文件注入到 body 区域内
title: 'Hello webpack', // 生成文件的标题
minify: { // 对生成的文件进行压缩,可以设置为 true ,也可以是对向,进行更具体的配置
collapseWhitespace: true, // 删除空格
minifyCSS: true,
minifyJS: true,
removeAttributeQuotes: true,
removeComments: true,
removeTagWhitespace: true,
}
})
]
}
插件也可以通过多次实例化来重复使用:
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry,
output,
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'src/index.html',
chunks: ['index', 'vendor'] // 只注入 index.bundle.js 和 vendor.bundle.js
}),
new HtmlWebpackPlugin({
filename: 'about.html',
template: 'src/about.html',
excludeChunks: ['index'] // 将 index.bundle.js 排除,其余的都注入
})
]
}
分离css和js
- webpack v4
npm i mini-css-extract-plugin -D
前面在介绍用 loader 处理样式的时候说到,这些样式最终会被混入到打包后的 .js
文件中,在页面运行的时候,在以 <style></style>
的方式动态的插入到 DOM 节点中,这种做法有两个很明显的缺点:
- js 和 css 糅杂在一起,增加了单个文件的体积。
- 在页面运行时动态的去创建 style 标签,多多少少会有些性能影响
如果能把这些 css 从打包后的 js 中抽取出来,就可以解决上面的两个问题,这时候就要用到 mini-css-extract-plugin
这个插件了。
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry,
output,
module: {
rules: [{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[id].[contenthash].css',
})
]
}
从上面的配置中可以看出,mini-css-extract-plugin
并不是单独作为一个 plugin
来使用的,它还充当了 loader 的作用,代替了 style-loader 。前面在介绍 style-loader 的时候提到,它的作用是将转换后的样式插入到页面中,既然我们现在需要将 css 和 js 分离开,所以也就不需要再用到 style-loader 了。
当作为插件使用的时候, mini-css-extract-plugin 可以接受两个可选参数:
- filename :分离出的css文件名称,写法和 output 的 filename 选项相同,唯一区别是当你想使用缓存的时候,填写的是 contenthash 而不是 chunkhash
- chunkFilename :切割出的css文件块名称,写法和 filename 相同
最近发现 extract-text-webpack-plugin 也支持 webpack4 用法了 mini-css-extract-plugin 完全相同,而且相较于 mini-css-extract-plugin 还多了一些可选的配置
npm i extract-text-webpack-plugin -D
// webpack.config.js
const ExtractCssChunksPlugin = require('extract-css-chunks-webpack-plugin');
module.exports = {
entry,
output,
module: {
rules: [{
test: /\.css$/,
use: [ExtractCssChunksPlugin.loader, 'style-loader']
}]
},
plugins: [
new ExtractCssChunksPlugin({
filename: '[name].[contenthash].css',
chunkFilename: '[id].[contenthash].css',
hot: true, //HMR 下面会着重介绍
orderWarning: true, // Disable to remove warnings about conflicting order between imports
reloadAll: true, //当启用HMR时,强制重新加载所有css
cssModules: true //如果启用了 cssModules 此选项设置为 true
})
]
}
压缩css
npm i optimize-css-assets-webpack-plugin -D
在将 css 从 js 中分离出来不之前,我们是不需要考虑压缩 css 的,因为样式都被打包进了 js 文件中,当我们设置 mode 为 production 时,webpack 会自动压缩 js 文件。但是我们现在将 css 从 js 中分离出来了,webpack 目前还不能自动压缩 css 文件。干!真是麻烦!这时候又要用到插件来帮我压缩分离出来的 css 文件了。
// webpack.config.js
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
entry,
output,
plugins: [
new OptimizeCssAssetsPlugin()
]
}
这里讲一个坑,在 webpack4 之前,压缩都是通过 webpack.optimize.UglifyJsPlugin 这个插件来完成。webpack4 新增了 mode 和 optimization 两个选项,当 mode 设置为 production 时会自动压缩 js 文件(这个已经提过多次了),其实将 mode 设置为 production 时, optimization.minimize 便会默认设置为 true ,意思就是在打包的时候对 js 进行压缩。而如果你想用第三方压缩插件,你可以将插件写在 plugins 中,也可以写在 optimization.minimizer 中。但是如你将压缩插件写在 optimization.minimizer 中时,webpack 就会默认读取 ptimizatio.minimizer 这个选项了,这也就意味着,这时候如果你不手动的配置 js 压缩插件,js 文件是不会被压缩,这时候又需要寻找压缩 js 的插件,比如 uglifyjs-webpack-plugin ,然后再配置一下,说实话这样真的很烦,所以我直接将压缩的插件配置在了 plugins 中,这样就省去了对 js 压缩插件的配置。webpack 的文档中描述了相关说明 Minimizing For Production
复制静态资源
npm i copy-webpack-plugin -D
有时候我们的项目中会有一些静态资源,比如网站的favicon、你从不知道的地方找来的不知名的js插件等等,这些静态资源并不会在项目中通过 import
的方式显式的加载进来,而是在直接写在页面中
...
<link rel="shortcut icon" href="static/favicon.ico">
...
<script src="static/xxx.js"></script>
对于这些静态资源,webpack 在打包过程中不会对它们进行处理,所有需要我们 copy 到打包后的目录中,从而保证项目不会因为缺少这些静态文件而报错, copy-webpack-plugin 的作用便是 copy 这些静态资源到指定的目录中的。
// webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
entry,
output,
plugins: [
new CopyWebpackPlugin([
{ from:'static/**',to: 'dist/static' }
])
]
}
上面的配置表示将 static 文件夹下所有的文件都复制到 dist/static 下面,如果你熟悉 gulp 的话,你会发现这其实就是一个移除了 pipe 的 gulp。
其实对于copy文件这种脏活累活你也可以用你熟悉的方式来完成,比如 gulp、fs-extra 等。
clean-webpack-plugin
npm i clean-webpack-plugin -D
如果我们打包输出的文件使用了 chunkhash 、 hash 等来命名的话,随着文件的变更和打包次数的增加,dist 目录会淤积很多无用的打包文件,这时候便可以借助 clean-webpack-plugin 帮我们清除一些这些无用的文件
// webpack.config.js
const CleanWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
entry,
output,
plugins: [
new CleanWebpackPlugin('dist', {
root: __dirname,
verbose: true,
dry: false
})
]
}
和 copy 文件一样,删除文件这种话不一定非得让 webpack 来做,我们也可以借助其他的方式来完成,比如我要再提一遍的的 gulp ,又或者 rimraf 、 del 等。但是区别是你需要手动的控制一下任务的流程,总不能在打包完成才删除问吧,所以用 webpack 提供的插件是不需要考虑任务流程的问题。
上面介绍了5个 webpack 的 plugin ,主要目的是让大家体会 webpack plugin 的作用基本用法。 实际上 webpack 的 plugin 还有很多很多,几乎可以满足你在项目构建中的各种需求,webpack 官网了列举很多官方推荐的 plugin https://webpack.js.org/plugins/ ,有兴趣的同学可以前往查看。