配置 entry 和 output
entry
entry 是 webpack 开始打包的入口文件,从这个入口文件开始,应用程序启动执行。如果传递一个数组,那么数组的每一项都会执行。
entry 可以传入一个字符串或者一个字符串数组:
// 字符串
...
entry: './src/index.js',
...
// 字符串数组
...
entry: ['./src/index.js', './src/index2.js'],
...
如果在 output 选项里面没有配置 filename 选项名字的话,chunk 会被命名为 main,即生成 main.js。
其实上面的写法,实际上就是下面的简写:
...
entry: {
main: '.src/index.js'
}
...
打包结果如下图:

entry 也可以传入一个对象,并且 output 选项里面没有配置 filename 选项名字的话,则每个键(key)会是 chunk 的名称,该值描述了 chunk 的入口起点。
这个其实就是一个多页面的应用程序,每一个入口都是一个独立的页面。
// 对象
...
entry: {
main: './src/index.js',
sub: './src/index2.js',
},
...
打包出来的结果如下图:

output
output 的配置必须是一个对象,它指示 webpack 如何去输出、以及在哪里输出你的「bundle、asset 和其他你所打包或使用 webpack 载入的任何内容」。
输出配置只有这一个,即使有多个入口也是只有这一个。
// 最基础的单页面 output 应用配置
...
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
}
...
filename 代表输出的文件名;
当项目是多页面应用的项目的时候,需要用 占位符来确保每个文件具有独立的名称。
...
entry: {
main: './src/index.js',
sub: './src/index2.js',
},
output: {
filename: '[name].js',
path: path.resolve('./dist'),
}
...
这个时候在 dist 目录也会生成 main.js 与 sub.js:

filename 还有其他的占位符,如下:

有的时候我们想要把打包出来的资源放在 cdn 上面,比如我想给打包出来的 main.js 和 sub.js 加上一个 cdn 网址前缀 http://cdn.com.cn,我们可以在 output 配置中添加 publicPath 选项即可实现:
...
output: {
publicPath: 'http://cdn.com.cn',
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
}
...
我们重新运行一下 npm run bundle,我们会发现在打包出来的 index.html 中,引用的 main.js 前加上了相应的前缀:

配置 Loader
webpack 开箱即用只支持 JS 和 JSON 两种文件类型,需要通过 Loaders 去支持其它文 件类型并且把它们转化成有效的模块,并且可以添加到依赖图中。
Loaders 本身是一个函数,接受源文件作为参数,返回转换的结果。
从打包图片说起
我们前面说过 webpack 可以打包除了 js 意外的其他文件,比如 图片文件,css文件等,那么我们就来试一下,我们在上节课的代码中 src 目录下面加上 一张图片 webpack.png,然后在 index.js 中引入:
import webpackSrc from './webpack.png';
然后运行 npm run bundle。发现打包出错了,如下图:

为什么会出现这个问题,是因为 webpack 默认是知道怎么打包 js 文件的,但是碰到其他类型的文件的时候,webpack 就不知道怎么进行打包了,因此我们需要在配置文件里面告诉 webpack 对于此类文件 模块 需要怎么进行打包。
于是我们在 webpack.config.js 中进行如下配置,添加一个 module 配置项:
const path = require('path');
module.exports = {
mode: 'development',
entry: './src/index.js',
module: {
rules: [{
test: /\.png$/,
use: {
loader: 'file-loader',
}
}]
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'bundle')
}
}
我们写了一个规则,就是说,当我们遇到 png 结尾的图片文件的时候,就使用 file-loader 来帮助我们进行打包这个文件模块。这里我们用到了 file-loader ,所以我们需要安装一下这个依赖:
npm install file-loader -D
安装完依赖之后,我们重新运行 npm run bundle 这个命令,我们打包出了两个文件,一个是 bundle.js 文件,一个是 哈希值为名字的图片文件:

在上面我们 将这个 图片 import 进来了,但是我们不知道这是一个什么东西,我们就把它打印出来看一波:
import webpackSrc from './webpack.png';
console.log(webpackSrc);
我们重新进行一次打包,并且在打包完成之后,将 index.html 在浏览器上打开,我们会发现webpackSrc 的值就是我们刚刚打包生成的图片的文件名:

我们来分析一下 webpack 打包的流程,最开始我们通过运行 npm run bundle,开始打包,因为本身 webpack 是知道怎么打包 js 的,所以它就一直打包打包,但当他遇到了 图片文件的时候,他就不知道怎们进行打包了,它就到配置文件的 module 选项中去找相应的规则。在配置文件中我们规定了当 webpack 遇到 图片文件的时候,就使用 file-loader 来帮我们进行打包文件。
其实 file-loader 的底层原理其实就是,当它发现有图片文件的时候,它就帮图片文件自动打包移动到 bundle 这个文件夹下,同时会给这个图片给一个名字,现在是一个一长串哈希值作为名字,后买呢我们会讲如何给他改名,然后它会讲这个图片名称作为一个返回值返回给我们引入模块的变量之中。
file-loader 不能仅仅能打包图片文件,还能打包其他类型的文件,比如字体文件、txt、Excel 文件等,只要你想讲某个文件返回到某一个目录,并且返回这个文件名的时候,file-loader 都可以做到。
loader 是什么?
通过上面这个例子,我们来思考一下,loader 究竟是什么东西?
其实loader 就是一个方案规则,他知道对于某一个特定的文件,webpack 该怎么去进行打包,因为本身,webpak 自己是不知道怎么去打包的,所以需要去使用 loader 来打包文件。
我们再举一个例子,可能有些朋友写过 vue,在 vue 中,文件是以 .vue 文件结尾的文件,webpack 是不认识 .vue 文件的,所以需要安装一个 打包 vue-loader 来帮助 webpack 打包 vue 文件。
如何配置loader
最开始,我们举了一个 file-loader 的例子来说明 loader 的作用,现在我们来看看如何配置 loader。
首先我们在 配置文件中加入 module 这个配置项,它是一个对象,在这个对象里面配置相应的处理模块的规则。
在 module 的选项里 有一个 rules 数组,rules 就是配置模块的读取和解析规则,通常就是用来配置 loader。数组里面的每一项都描述了如何处理对应的文件,配置一项 rules 时大致可以通过一项方式来完成:
- 条件匹配:通过
test、include、exclude三个配置来选中Loader要应用的规则的文件 - 应用规则:对选中的文件,通过 use 配置项来应用
loader,可以只应用一个loader或者按照从后往前的顺序来应用一组loader,同时我们可以分别向loader传入相应的参数。 - 重置
loader的执行顺序:因为一组loader的执行顺序默认是从右到左执行的,通过enforce选项可以将一期中一个loader的执行顺序放到最前或者最后。
举个🌰:
module: {
rules: [
{
// 命中 js 文件
test: /\.js$/,
// 使用 babel-loader 来解析 js 文件
use: ['babel-loader'],
// 只命中 src 目录下的 js 文件,加快 webpack 的加载速度
include: path.resolve(__dirname, 'src')
},
{
// 命中 less 文件
test: /\.less$/,
// 从右到左依次使用 less-loader、css-loader、style-loader
use: ['style-loader', 'css-loader', 'less-loader'],
// 排除 node_modules 下面的 less 文件
exclude: path.resolve(__dirname, 'node_modules')
},
{
// 命中字体、图片文件
test: /\.(gif|png|jpe?g|eot|woff|ttf|svg|pdf)$/,
// 采用 file-loader 加载,并给 file-loader 传入
// 相应的配置参数,通过 placeholders 指定 输出的名字
use: {
loader: 'file-loader',
options: {
name: '[name].[ext]',
}
},
}
]
}
常用的 loader 及其用法
打包静态资源(图片等):file-loader、url-loader
在最开始的一节我们有讲过这个 file-loader, 但是我们发现通过 file-loader 打包出来的图片文件名,是一个哈希值,如果我们想要打包出来的图片要和原来的图片名字一样,这个我们就需要,往 file-loader 传入一下参数了我们往 loader 里面传入一些参数,如下图所示:
rules: [{
test: /\.png$/,
use: {
loader: 'file-loader',
options: {
name: '[name].[ext]'
}
}
}]
我们在进行一次打包,我们会发现我们打包出来的文件 变成了 webpack.png 了。上面代码中 [name].[ext] 其实在 file-loader 里面被称为 placeholders 占位符,通过官网我们可以看到 file-loader 有如下这么 占位符:

我们可以在试一下,在 name 后加上一个 哈希值,我们可以这么写:
rules: [{
test: /\.png$/,
use: {
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]'
}
}
}]
这样我们就能够看到 打包出来的图片 后面 跟上了一串哈希值;
有的时候,我们还有一个需求,就是想把图片打包到具体的一个文件夹中,我们可以进行如下配置:
rules: [{
test: /\.png$/,
use: {
loader: 'file-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
}
}
}]
这样我们就可以看到打包出来了的图片文件 被放到了 /images 文件夹下。
其实 file-loader 的配置参数还有很多,我只是讲了其中的几个,具体其他的 参数值,大家可以在用到的时候自行去官网查询。
与 file-loader 比较相似的,有一个 url-loader,它也可以实现 所有的 file-loader 的所有功能,我们安装一下 url-loader:
npm install url-loader -D
然后修改一下 webpack 的配置文件:
module: {
rules: [{
test: /\.png$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
}
}
}]
},
我们 重新运行 npm run bundle,我们会发现,如片没有被打包到 bundle 的文件夹中,我们试着去访问一下index.html, 但是 index.html 会出现图片。
其实问题就出在 url-loader 已经把图片 转换成了 base64 格式并打包到了 bundle.js 中去了,我们可以看一下代码:

这样就会有问题出来,当我们的图片特别大的时候,打包出来的 js 就会特别大,加载这个js 文件的时间就会变长,页面会有一段时间的白屏,解决这个问题也很简单,我们可以再加一个 limit 配置参数:
module: {
rules: [{
test: /\.png$/,
use: {
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath: 'images/',
limit: 10240,
}
}
}]
},
这个配置参数的意思就是,当图片的大小 大于 limit 的时候, url-loader 就会跟 file-loader 一样,把图片打包到 images 文件夹下;当图片的大小 小于 limit 的时候,url-loader 会直接将图片转化成 base64 的格式打包到js 中,减少网络请求。
好,这两个loader 我们就先讲到这里,具体其他的参数大家可以参考官网:
打包样式文件(样式篇)
打包样式我们会用到一下这些 loaders:style-loader、css-loader、less-loader/sass-loader、postcss-loader。
接下来我们一个一个讲:
加入我们想要将图片样式进行一下调整,于是我们就可以写一个样式文件 index.css:
.avatar {
width: 500px;
margin-left: 500px;
}
接着我们给 img 添加 avatar 这个类名,并引入index.css:
// ES Moudule 模块引入方式
import webpackSrc from './webpack.png';
import './index.css';
var img = new Image();
img.src = webpackSrc;
img.classList.add('avatar'); // 添加类名
var dom = document.getElementById('root');
dom.append(img);
因为 webpack 不知道 css 类型的文件怎么进行打包,所以我们需要安装相应的 loader,一般解析 css 我们需要安装 style-loader 和 css-loader :
npm install style-loader css-loader -D
然后修改相应的 webpack 配置文件,往 rules 添加一条匹配规则 :
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
我们重新运行 npm run bundle,我们可以看到,打包出来的图片上加上了相应的 类名:

我们回过头来看看 style-loader 和 css-loader 的作用,css-loader 的作用就是帮我们分析出几个 css 文件之间的关系,并最终把这些 css 文件合并成一段 css,style-loader 的作用就是,在得到 css 生成的内容之后,把这段内容挂载到页面的 head 部分。所以在处理 css 文件的时候,我们一般需要 style-loader 与 css-loader 配合使用。
有时我们想要在项目中使用 less,来帮助我们更好的写 css,我们修改 index.css 后缀 index.less:
// index.less
body {
.avatar {
width: 500px;
margin-left: 500px;
}
}
这个时候我们就需要安装相应的 less-loader 来帮助我们处理相应的 less 文件,我们安装一下依赖:
npm install less-loader less -D
然后修改一下 配置文件:
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
}
接着我们运行 npm run bundle,发现页面正确的输出了图片,还带上了样式。
这里我们还要继续强调一下,loader 的执行顺序是 从下到上,从右到左 的顺序执行的。
有的时候我们还会遇到一些问题,就是我们会在 less 文件里面使用一些最新的 css3 的属性,一般我们需要在这个属性上加上相应的浏览器的厂商前缀,如-webkit、-ms、-moz 等,我们在代码里加上:
body {
.avatar {
width: 500px;
margin-left: 500px;
transform: scale(0.8);
}
}
就是我们想要把这张图片缩放到原来的 0.8 倍,我们重新打包一下,我们可以看到这个 transform 没有带上相应的前缀:

这个时候我们就需要postcss-loader,首先我们安装一下 postcss-loader 和 autoprefixer,同时在项目的根目录下创建 postcss.config.js,和 webpack.config.js 类似,用来 配置 postcss:
npm install postcss-loader autoprefixer -D
我们修改一下 postcss.config.js:
module.exports = {
plugins: [
require('autoprefixer')
]
}
同时我们修改 webpack 的配置文件:
{
test: /\.less$/,
use: [
'style-loader',
'css-loader',
'less-loader',
'postcss-loader',
]
}
打开浏览器,我们会发现 transform 被加上了 浏览器的厂商前缀。

在这里我们来分析一下整个过程,当 webpack 开始加载 index.less 文件的时候,它会遵循 从右到左,从下到上的原则,依次走 postcss-loader,less-loader,css-loader,style-loader,当使用 post-loader 的时候,他会去找相应的 posts.config.js 配置文件,并执行 autoprefixer 这个插件,然后在依次往上执行,知道加载解析完成。
loader 的执行顺序
有的时候,我们会在 js 里面加上 less 文件的时候,webpack 会依次去走一下 4 个loader,但是有的时候 less 文件里面 引入其他的 less 文件,这个时候就有可能不去走 下面的 less-loader 与 postcss-loader 了,所以我们就需要在 css-loader 里面传入一个参数 importLoaders 为 2:
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2
}
},
'less-loader',
'postcss-loader',
]
}
上面这个参数的意思就是,通过在 less 里面引入的文件 还需要去走下面的两个 loader,这就保证了,不管你是 js 引入的还是less 引入的,都会从下到上依次去走四个 loader
css 模块化
接下来我们讲一下 less 的模块化,我们 新建一个 createAvatar.js 用来创建一张图片:
import webpackSrc from './webpack.png';
function createAvatar () {
var img = new Image();
img.src = webpackSrc;
img.classList.add('avatar'); // 添加类名
var dom = document.getElementById('root');
dom.append(img);
}
export default createAvatar;
我们在 index.js 进行引入:
// ES Moudule 模块引入方式
import webpackSrc from './webpack.png';
import createAvatar from './createAvatar';
import './index.less';
createAvatar();
var img = new Image();
img.src = webpackSrc;
img.classList.add('avatar'); // 添加类名
var dom = document.getElementById('root');
dom.append(img);
我们重新打包一下项目,我们可以看到页面中生成了两张图片:

但是现在我们如果只想改一下 index.js 生成的图片的样式,我们会发现一个问题,就是 createAvatar 这个模块产生的 css 页会被我们修改。换句话说就是,这个样式其实是一个全局的样式,一经修改,项目中所有的这个calss 类名 都会被修改。
所以这个时候我们就引入了一个概念 CSS Module(css 模块化),意识就是说这个 css 只在这个模块里面有效,其他模块不影响。这就很好的解决了我们上面说的那个问题。
我们在修改一下 webpack.config.js:
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
modules: true,
}
},
'less-loader',
'postcss-loader',
]
}
同时我们修改一下各个模块的引入方式:
// index.js
import webpackSrc from './webpack.png';
import createAvatar from './createAvatar';
// import './index.less';
import style from './index.less'
createAvatar();
var img = new Image();
img.src = webpackSrc;
img.classList.add(style.avatar); // 添加类名
var dom = document.getElementById('root');
dom.append(img);
// createAvatar.js
import webpackSrc from './webpack.png';
import style from './index.less'
function createAvatar () {
var img = new Image();
img.src = webpackSrc;
img.classList.add(style.avatar); // 添加类名
var dom = document.getElementById('root');
dom.append(img);
}
export default createAvatar;
这样带来的好处就是,我们写的各个模块里的样式文件都只对自己的模块生效,非常独立,不会对其他模块产生影响。
px2rem-loader
这个是一个 移动端 css px 自动转换为 rem 的 loader。
有的时候我们需要做移动端的自适应,以前我们可以通过 css 媒体查询来实现响应式布局,但是这样的话就需要写多套代码。
这个时候我们就可以使用 rem 来进行 css 布局,rem 是 CSS3 新增的一个相对单位。相对于 html 根元素。更详细的大家可以参考 px、em、rem区别介绍。
我们可以搭配手淘的 lib-flexible,我们首先安装相应的插件:
npm install amfe-flexible px2rem-loader -D
接着我们在配置一下:
// ...
module.exports = {
// ...
module: {
rules: [
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
// modules: true,
}
},
'less-loader',
'postcss-loader',
{
loader: 'px2rem-loader',
options: {
remUnit: 75, // rem 相对 px 转换的单位,1rem = 75px
remPrecision: 8 // px 转化为 rem 小数点的位数
}
},
]
}]
},
// ...
}
接着在打包出来的 index.html 中引入我们的计算 font-size 的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>模块化问题例子</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
<script src="../node_modules/amfe-flexible/index.js"></script>
</head>
<body>
<div id='root'></div>
</body>
</html>
接着我们就可以看到 px 可以转化为 rem 了:

此外
lib-flexible官方也说现在推荐我们使用viewport来代替它。vw的兼容方案可以参阅《如何在Vue项目中使用vw实现移动端适配》一文。或者我们还可以使用
hotcss,他也是移动端布局开发解决方案之一。
row-loader
有的时候我们需要将资源内联到 html 中去,我们可以通过 row-loader 来内联 html 和 js 代码。
html
我们新建一个 meta.html,有的时候我们要在多个页面引入,就可以使用内联:
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
接着我们在模版文件 index.html 使用 raw-loader,这里需要注意的是我们需要安装 0.5.* 版本的 row-loader,此外我们是通过 html-webpack-plugin 来生成 html 的,我们可以使用 ejs 的语法:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>模块化问题例子</title>
${require('raw-loader!./meta.html')}
<script src="../node_modules/amfe-flexible/index.js"></script>
</head>
<body>
<div id='root'></div>
</body>
</html>
js
上面我们讲到的使用 lib-flexible,我们可以使用内联代码来做,但有的时候 js 代码可能会有 es6,因此我们也需要跑一下 babel-loader:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>模块化问题例子</title>
${require('raw-loader!./meta.html')}
<script>${require('raw-loader!babel-loader!../node_modules/amfe-flexible')}</script>
</head>
<body>
<div id='root'></div>
</body>
</html>
我们进行打包一下,可以在打包后的 index.html 引入了相应的文件:

css
内联 css ,我们有下面两个方案。
style-loader
我们可以设置相应的参数完成内敛:
// ...
module.exports = {
// ...
module: {
rules: [
{
test: /\.less$/,
use: [
{
loader: 'style-loader',
options: {
insertAt: 'top', // 样式插入到 <head>
singleton: true, //将所有的style标签合并成一个
}
},
// ...
]
}
]
},
// ...
}
这是用于将外部样式表转换为嵌入式样式表的 webpack 插件,配置如下:
...
const HTMLInlineCSSWebpackPlugin = require("html-inline-css-webpack-plugin").default;
module.exports = {
// ...
plugins: [
// ...
new HTMLInlineCSSWebpackPlugin(),
],
// ...
}
打包图标字体
接下来我们将一下,如何打包图标字体,一般在网站中我们会使用各种各样的图标,那我们如何来使用 webpack 打包图标呢?
一般我们会从 iconfont 网站下载相应的图标,大家可以注册一个账号新建一个项目,

大家可以随便添加几个图标并且点击下载,如图:

下载完成之后,解压文件夹如下:

我们只需要下面的一些字体文件 eot、svg 、woff、woff2、ttf 以及应用这些文件的 iconfont.css,我们在项目中创建 fonts 文件夹,并将上面的字体文件拉入到 fonts 文件夹中。同时将 iconfont.css 中的文件复制到 index.less中,并修改一下引用路径:

同时我们修改一下 index.js,插入一个图标文件:
// ES Moudule 模块引入方式
// import webpackSrc from './webpack.png';
// import createAvatar from './createAvatar';
// import './index.less';
// import style from './index.less'
// createAvatar();
// var img = new Image();
// img.src = webpackSrc;
// img.classList.add(style.avatar); // 添加类名
// var dom = document.getElementById('root');
// dom.append(img);
var dom = document.getElementById('root');
dom.innerHTML = "<div class='iconfont icon-left'></div>"
最后我们修改一下 webpack.config.js,在 rules 中添加一条对字体文件的打包处理:
{
test: /\.(eot|ttf|svg|woff|woff2)$/,
use: {
loader: 'file-loader',
}
},
我们执行一下 npm run bundle,在页面中出现了相应的图标:

关于 webpakc 样式打包的相关 loader,我这变就介绍到这里,关于这几个 loader 的其他配置,大家可以参考官网对应 loader 介绍:
其他常用 loader
其他的常用 loader,还有诸如:
raw-loader:将文件以字符串的形式导入thread-loader:多进程打包js和css的loader,我们会在 Webpack 性能优化 中讲到babel-loader:转换ES6、ES7等JS新特性语法,我们会在接下去的小结专门讲一下ts-loader:将typescript代码转化为js,我们会在 Webpack 实战配置案例 中讲到
其他的还有很多 loader 是 Webpack 官方推荐的, 大家可以在需要用到的时候去查询相应的文档,笔者在这里就不细讲了。
配置 plugin
上一节我们讲了 loader,我们知道了在 webpack 中,loader 可以完成对不同类型文件的打包,这一节我们讲一下 plugins,让我们的打包变得更加便捷。
什么是 plugin
webpack 中的 plugin 大多都提供额外的能力,增强 webpack 。其用于 bundle 文件的优化,资源管理与环境变量的注入等。
它能使 webapck 运行到某一个时间段,做相应的事情,其实很类似 react 中的生命周期函数。它作用于整个构建过程,你其实还可以理解成任何 loader 无法做的事情都可以通过 plugins 来实现。
配置只需要把插件实例添加到 plugins 字段的数组中。不过由于需要提供不同的功能,不同的插件本身的配置比较多样化。
社区中有很多 webpack 插件可功能使用,包括 webpack 官方推荐 的 以及社区中 webpack plugins ,有些的插件都会提供详细的使用说明文档,能让使用者快速上手。
下面我们就来讲几个常用的 plugin 来了解一下插件的使用方法。
常用的 plugins
html-webpack-plugin
这个插件会帮助我们在 webpack 打包结束后,自动生成一个 html 文件,并把打包产生文件引入到这个 html 文件中去。
接下来我们来看看这个 plugin 的用法。
上一节我们将 loader 的时候,当我们运行完 npm run bundle,页面会生成 bundle 文件夹,并生成了一系列的打包文件,但是就是没有相应的 index.html 文件,所以我们需要手动的去添加 index.html,并在文件中引入打包出来的 bundle.js 文件,过程会比较麻烦。
所以我们可以借助 htmlWebpackPlugin 来帮我们生成 index.html,首先我们要安装一下:
npm install html-webpack-plugin -D
接着修改我们的 webpack.config.js:
// 引入 htmlWebpackPlugin
const htmlWebpackPlugin = require('html-webpack-plugin');
// 给 webpack 添加一项配置 plugins
...
plugins: [
new htmlWebpackPlugin(),
]
...
删除一下 bundle 文件夹,重新运行 npm run bundle,我们会发现新生成的 bundle 文件夹多出了 一个 index.html:

但是我们会发现,通过webpack 生成的的 html 文件,少了一个 id 为 root 的 div 元素,

我们之前在 demo 里写的代码都是插入到 root 的 div 中的,所以这里我们需要给 htmlWebpackPlugin 传入一些配置来解决上面这个问题,我们在 src 目录创建一个 index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>模块化问题例子</title>
</head>
<body>
<div id='root'></div>
</body>
</html>
然后修改一下 webpack.config.js,向 htmlWebpackPlugin 传入一些配置文件:
// 给 webpack 添加一项配置 plugins
...
plugins: [
new htmlWebpackPlugin({
template: 'src/index.html', // 模板文件
}),
]
...
删除一下 bundle 文件夹,重新运行 npm run bundle,我们会发现现在的index.html,有了 id 为 root 的 div 元素。

上面的 template 参数的意思就是说,htmlWebpackPlugin 是以 src 下的 index.html 作为模版去生成 html 文件的。
更多配置
inject
向 template 或者 templateContent 中注入所有静态资源,不同的配置值注入的位置不经相同。
true或者body:所有JavaScript资源插入到body元素的底部
head: 所有JavaScript资源插入到head元素中
false: 所有静态资源css和JavaScript都不会注入到模板文件中
minify
这个值可以用来压缩 html。
参数是一个对象或者 false,传递 html-minifier 选项给 minify 输出,false 就是不使用 html 压缩,minify 具体配置参数可以参考:html-minifier
chunks
允许插入到模板中的一些 chunk,不配置此项默认会将 entry 中所有的 chunk 注入到模板中。在配置多个页面时,每个页面注入的 chunk 应该是不相同的,需要通过该配置为不同页面注入不同的 chunk;
这个值在之后我们将多页面打包会用到,大家可以参考 多页应用打包 那一节。
更多的配置大家可以参考官网的:htmlWebpackPlugin
clean-webpack-plugin
这个插件能帮我们在打包之前先删除掉打包出来的文件夹,比如我们修改了 webpack.config.js 中的 output 选项:
...
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'bundle')
}
...
重新运行 npm run bundle,我们会发现 bundle 文件夹下面的 bundle.js 文件还存在,同时并生成了 dist.js 文件:

但其实 bundle.js 在这里已经没有什么用了,所以我们需要在打包之前删除这个 bundle 文件夹,这个时候我们就可以借助 clean-webpack-plugin 来解决这个问题,首先我们安装一下:
npm install clean-webpack-plugin -D
接着修改我们的 webpack.config.js:
// 引入 clean-webpack-plugin
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
...
plugins: [
new htmlWebpackPlugin({
template: 'src/index.html', // 模板文件
}),
new cleanWebpackPlugin(),
]
...
重新运行 npm run bundle,我们会发现,原来的 bundle.js 消失了。
在最新版的 webpack 中 new CleanWebpackPlugin() 中不需要写里面的目标路径,会自动清除生成的文件夹,比如是 bundle 文件夹,之前的版本中,我们需要传入要删除的相应文件夹名字:new cleanWebpackPlugin(['bundle']);同时 CleanWebpackPlugin 的引入方式也要变一下。由原来的:
const CleanWebpackPlugin = require('clean-webpack-plugin');
变成现在的:
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
关于 CleanWebpackPlugin 的更多用法,可以参考官网的 clean-webpack-plugin
其他常用的 plugins
mini-css-extract-plugin:Webpack4.0中将css从bundle文件中提取成一个独立的css文件;在3.0版本使用 extract-text-webpack-plugin。terser-webpack-plugin:压缩js的插件,支持压缩es6代码,webpack4.0默认使用的一个压缩插件,在3.0版本使用uglifyjs-webpack-plugin来压缩js代码。copy-webpack-plugin:将文件或者文件夹拷贝到构建的输出目录zip-webpack-plugin:将打包出的资源生成一个zip包- optimize-css-assets-webpack-plugin:压缩
css代码的插件 webpack.DefinePlugin:创建一个在 编译 时可以配置的全局常量,比如设置process.env.NODE_ENV,可以在js业务代码中使用。webpack.DllPlugin:抽取第三方js,使用dll打包,笔者会在之后Webpack性能优化将到。
更多的 plugins 大家可以参考 官网 plugins,后续我们也会讲到到很多相关的 plugins,大家可以慢慢将这些 plugins 放到自己的知识储备库中去。
配置 sourceMap
什么是 sourceMap
SourceMap 是一个映射关系。能够帮我们更好的定位源码的错误。
举个例子,现在我们发现打包出来的 dist 目录下的 main.js 的 97 行报错了,但因为他是打包后的文件,我们知道 main.js 第几行报错其实没有任何意义。这个时候 sourcemap 就出来帮我们解决了这个问题,因为他是打包文件和源码的一个映射关系,它知道 dist 目录下 main.js 文件的 97 行 实际上对应的 src 目录下的 index.js 文件的第一行,这样我们就能够快速定位问题,并进行修复了。
如何配置 sourceMap
首先我们在修改一下 webpack.config.js 文件,添加参数 devtool 并且设置为 none,因为 webpack 默认会帮我们 把 sourceMap 给开起来,为了验证它的作用,我们暂时先把它关闭。
// 添加参数
...
devtool: 'none',
...
并修改一下 index.js 文件:
// 故意拼错 console.log,使 js 报错
consle.log('hello, darrell');
我们运行 npm run bundle,我们会发现打包成功执行,我们打开 dist 目录下的 index.html,我们会发现控制台有一个报错:

点击右上角的 main.js,我们会发现以下错误:

但其实我们并不想知道错误具体在 main.js 的哪一行,我们想知道 这一行代码是在 src 目录下面的哪一个文件,并且在哪一行。
所以这里我们可以修改配置 devtool 的参数:
// 添加参数
...
devtool: 'source-map',
...
我们重新运行 npm run bundle,我们会发现在 dist 目录下面,除了 index.html 和 main.js ,还额外的生成了 一个 main.js.map 文件。

打开 index.html,控制台依然会报错,但是我们点击 main.js,便会发现跳到了 src 目录下的 index.js 下了。

这就是 source-map 的意义所在。
devtool 的 相关配置
下面是官网给我们的 devtool 的相关配置的比较:

从上图中我们可以看到 devtool 有非常多的配置,不同的配置构建的速度会有一些差异,中间的很多参数都是可以穿插使用的。
-
inine有这个的配置,直接会将.map文件直接打包到对应的js中去,从而加快相应的速度

使用这个我们会发现,打包出来的文件没有 .map 文件了,而是以 base64 的形式放入了打包的文件中了。
-
cheap有这个的配置,意思是 map 文件只会帮你定为到具体的 某一行,并不会把代码定位到 具体的 某一行 某一列,从而加快速度;cheap还有一个作用,就是这个选项只使针对业务代码,也就是说只能定位到业务代码里面的错误,并不能定位到我们引用的第三方文件(比如说loader,第三方模块)的错误。 -
module有这个的配置,意思是 它不仅会帮我们定位 自己的业务代码中的错误,还会同时帮我们定位第三方模块的错误。 -
eval有这个的配置,使用eval包裹模块代码,并且存在//@sourceURL,这个是打包速度最快,性能最好的的一种方式,但是有的时候,对于代码比较复杂的情况,它提示出来的错误可能不够全面。
从图中可以看到,打包完成的 js 中没有 base64 格式的 map 文件了,只有一段 被 eval 包裹的文件了。
项目开发的最佳实践
开发环境下:development
提示出来的错误比较全,打包速度比较快。
...
devtool: 'cheap-module-eval-source-map'
...
生产环境下:production
即在线上环境,一般会关掉 source-map(因为没什么必要),但是有的时候,如果线上代码出了问题,我们也希望通过 source-map 快速定位问题,我们可以使用如下配置。
...
devtool: 'cheap-module-source-map'
...
配置 webpack-dev-derver
我们在平时的开发过程当中,一般在我们的项目中肯定去会发一些 ajax 请求,而这个请求是基于 http 协议的,所以我们需要起一个服务器,在我们起的这个服务器中去完成我们的一系列功能开发。
这个时候就会用到 webpack-dev-derver,它有很多的参数可供我们配置,比如项目启动的时候自动帮我们开启浏览器、指定端口起服务器等、自动帮我们刷新浏览器
接下去我们就来讲一下:
安装与配置
- 安装
webpack-dev-server
npm install webpack-dev-server -D
- 修改配置文件
webpack.config.js
...
devServer: {
contentBase: './dist', // 指定目录 起 服务器
open: true, // 项目启动自动打开浏览器
port: 8080 // 在 8080 端口起服务
},
...
- 接着我们修改一下
package.json文件的scripts:
...
"scripts": {
"bundle": "webpack",
"start": "webpack-dev-server"
},
...
- 最后我们运行
npm start,我们可以看到webpack-dev-server帮我们起了一个http://localhost:8080/的服务,并自动打开了浏览器。

常用配置
前面例子涉及到的几个配置我就不再讲了,这里讲几个用到的比较多的配置,
proxy
如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求 ,那么代理某些 URL 会很有用。同时还能解决开发环境的跨域问题。
它的原理是使用 **http-proxy-middleware **去把请求代理到一个外部的服务器。
下面是几种常用的用法,取自文章 Webpack-dev-server的proxy用法:
- 用法一:请求到
/api/xxx现在会被代理到请求http://localhost:3000/api/xxx, 例如/api/user现在会被代理到请求http://localhost:3000/api/user
mmodule.exports = {
//...
devServer: {
proxy: {
'/api': 'http://localhost:3000'
}
}
};
- 用法二:如果你想要代码多个路径代理到同一个
target下, 你可以使用由一个或多个「具有context属性的对象」构成的数组:
module.exports = {
//...
devServer: {
proxy: [{
context: ['/auth', '/api'],
target: 'http://localhost:3000',
}]
}
};
- 用法三:如果你不想始终传递
/api,则需要重写路径:
module.exports = {
//...
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: {'^/api' : ''}
}
}
}
};
请求到 /api/xxx 现在会被代理到请求 http://localhost:3000/xxx, 例如 /api/user 现在会被代理到请求 http://localhost:3000/user
- 用法四:默认情况下,不接受运行在 HTTPS 上,且使用了无效证书的后端服务器。如果你想要接受,只要设置
secure: false就行。修改配置如下:
module.exports = {
//...
devServer: {
proxy: {
'/api': {
target: 'https://other-server.example.com',
secure: false
}
}
}
};
-
用法五:有时你不想代理所有的请求。可以基于一个函数的返回值绕过代理。在函数中你可以访问请求体、响应体和代理选项。必须返回 false 或路径,来跳过代理请求。
例如:对于浏览器请求,你想要提供一个 HTML 页面,但是对于 API 请求则保持代理。你可以这样做:
module.exports = {
//...
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
bypass: function(req, res, proxyOptions) {
if (req.headers.accept.indexOf('html') !== -1) {
console.log('Skipping proxy for browser request.');
return '/index.html';
}
}
}
}
}
};
- 解决跨域原理:上面的参数列表中有一个
changeOrigin参数, 是一个布尔值, 设置为true, 本地就会虚拟一个服务器接收你的请求并代你发送该请求,
module.exports = {
//...
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
}
}
}
};
- 我们最后在举一个例子,梳理一下整个流程,先看代码:
module.exports = {
//...
devServer: {
'/proxy': {
target: 'http://your_api_server.com',
changeOrigin: true,
pathRewrite: {
'^/proxy': ''
}
}
}
};
- 假设你主机名为
localhost:8080, 请求API的url是http://your_api_server.com/user/list '/proxy':如果点击某个按钮,触发请求API事件,这时请求url是http://localhost:8080/proxy/user/list。changeOrigin:如果true,那么http://localhost:8080/proxy/user/list变为http://your_api_server.com/proxy/user/list。pathRewrite:重写路径。匹配/proxy,然后变为'',那么url最终为http://your_api_server.com/user/list。
publicPath
假设服务器运行在 http://localhost:8080 并且 output.filename 被设置为 bundle.js。默认 publicPath 是 "/",所以你的包(bundle)可以通过 http://localhost:8080/bundle.js 访问。
可以修改 publicPath,将 bundle 放在一个目录:
publicPath: "/assets/"
你的包现在可以通过 http://localhost:8080/assets/bundle.js 访问。
historyApiFallback
当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html。通过传入以下启用:
historyApiFallback: true
通过传入一个对象,比如使用 rewrites 这个选项,此行为可进一步地控制:
historyApiFallback: {
rewrites: [
{ from: /^\/$/, to: '/views/landing.html' },
{ from: /^\/subpage/, to: '/views/subpage.html' },
{ from: /./, to: '/views/404.html' }
]
}
hot
启用 webpack 的模块热替换特性,下一节我们会讲到。
更多配置大家可以参考官网的介绍:
实现一个简单的 webpack-dev-server
因为 webpack 是基于 node 环境的,我们先在根目录下创建一个 server.js。
我们使用 express 框架来起一个服务器。同时使用 webpack-dev-middleware 来监听文件变化,当文件变化的时候,自动刷新一下服务器。
- 首先安装依赖
npm install express webpack-dev-middleware -D
- 编写
server.js文件
const express = require('express');
const webpack = require('webpack');
// 监听文件变化
const webpackDevMiddleware = require('webpack-dev-middleware');
// 导入配置文件
const config = require('./webpack.config.js');
// 返回 webpack 的编译器
// complier 的意思就是 通过 webpack 和 其配置文件,可以随时对文件进行编译
const complier = webpack(config);
// 创建服务器的实例
const app = express();
// 中间件可以接受两个参数,编译器 和 其他的配置参数
app.use(webpackDevMiddleware(complier, {}));
// 启动一个 express 服务
app.listen(3000, () => {
console.log('server is running');
});
- 接着我们修改一下
package.json文件的scripts:
...
"scripts": {
"bundle": "webpack",
"start": "webpack-dev-server",
"server": "node server.js"
},
...
- 最后我们运行
npm run server,我们可以发现在localhost:3000起了一个服务,页面上可以看到webpack-dev-server文字:

- 我们修改一下
header.js文件,修改完页面不会自动刷新,我们需要自己刷新一下页面。其实效果就跟webpack --watch一样。
function Header() {
var dom = document.getElementById('root');
var header = document.createElement('div');
header.innerText = 'webpack-dev-server111';
dom.append(header);
}
export default Header;
我们可以看到页面显示如图:

这次我们完成了一个简单的 webpack-dev-server,但是功能还是比较单一,要完善这个插件,其实很耗费精力。所以这里面我们只要了解一下就行。在工作当中我们还是使用 webpack-dev-server 就行了。
配置 HMR 热更新
什么是 HMR
模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一, 它允许在运行时更新各种模块,而无需进行完全刷新。
一般如果我们使用了 webpack-dev-server,当我们修改了项目中的文件的时候,一般会重新刷新一下页面,这会导致我们刚刚在页面中操作的东西都被还原。
举两个🌰
less 中:
首先我们在修改 index.js 文件,下面的 js 代码的意思就是页面上插入一个按钮,点这个按钮的时候,生成一个 <div>item</div> 元素;
import './index.less';
var btn = document.createElement('button');
btn.innerHTML = '新增';
document.body.appendChild(btn);
btn.onclick = function() {
var div = document.createElement('div');
div.innerHTML = 'item';
document.body.appendChild(div);
}
接着修改 index.less 文件:用于给偶数的 item 加一个背景色。
div:nth-of-type(odd) {
background: red;
}
然后我们运行 npm start,点击 item 可以在页面中看到:

我们在修改一下 index.less 文件,
div:nth-of-type(odd) {
background: yellow;
}
保存后我们会发现,页码被刷新了,重置了之前的红色条纹。当再点击的时候,才会出现 黄色条纹:

js 中
我们修改一下 index.js,并在 src 下新建 number.js 和 counter.js,当作我们项目的两个模块。
index.js如下:
// import './index.less';
// var btn = document.createElement('button');
// btn.innerHTML = '新增';
// document.body.appendChild(btn);
// btn.onclick = function() {
// var div = document.createElement('div');
// div.innerHTML = 'item';
// document.body.appendChild(div);
// }
import counter from './counter';
import number from './number';
counter();
number();
if(module.hot) {
module.hot.accept('./number', () => {
document.body.removeChild(document.getElementById('number'));
number();
})
}
number.js:新建一个div,并给这个div赋值1000
function number() {
var div = document.createElement('div');
div.setAttribute('id', 'number');
div.innerHTML = 3000;
document.body.appendChild(div);
}
export default number;
counter.js:新建一个div,并给这个div赋值1,并给这个div添加一个点击事件,每当点击的时候,自动加一。
function counter() {
var div = document.createElement('div');
div.setAttribute('id', 'counter');
div.innerHTML = 1;
div.onclick = function() {
div.innerHTML = parseInt(div.innerHTML, 10) + 1
}
document.body.appendChild(div);
}
export default counter;
我们重新运行 npm start,我们可以看到如下图:

接着我们点击 counter.js 导出的数字,让其变为 16,接着我们将 number.js 中的 1000 改为 3000,

修改之后:

我们会发现上面我们辛苦点的数字又被还原到了 1。
要解决上面两个问题,我们就需要使用 HMR 了。
配置
我们修改 webpack.congig.js 配置文件:
const webpack = require('webpack');
...
module.exports = {
...
devServer: {
contentBase: './dist',
open: true,
port: 8080,
hot: true,
hotOnly: true
},
...
plugins: [
...
new webpack.HotModuleReplacementPlugin()
],
...
}
修改完后,我们重启一下服务:npm start
再看 less:
我们先点几下新增,如下图所示:

接着我们将 less 中 yellow 改为 #4caf50,保存后回到页面:

之前我们新增的 item 还在,而且颜色变成了我们修改后的样子。
less 热更新成功
再看 js:
我们先点几下 counter.js 里面的数字,如下图所示:

接着我们将 number.js 中的数字 1000 改为 6000,保存后回到页面:

之前我们新增的 16 还在,但是数字没有变成 6000:
这是因为我们还需要再 index.js 代码中加上一行代码:
if(module.hot) {
module.hot.accept('./number', () => {
document.body.removeChild(document.getElementById('number'));
number();
})
}
上面的的代码意思就是 如果我们开启了热更新,并且我们发现 number.js 有变动的话,我们就重新的把原来的 number.js 创建的 <div> 删除,并重新运行一下 number.js
重新起一下服务,在按照上面的步骤操作一下,我们发现新增的 16 还在,数字也改成了 6000:

那么为什么我们在打包 less 的时候就不需要写着一行代码呢,其实是因为 css-loader 默认已经帮我们做了这一件事情了,其中我们经常使用 React、vue 框架他们的底层已经帮我们做好了这些事情,所以我们在代码上面基本上没有看到过类似上面的代码。
至此, js 热更新成功。
实现原理
来看一张图,如下:

先来讲几个概念:
File System
代表我们的文件系统,里面有我们的所有代码文件
Webpack Compile
Webpack 的编译器,将 JS 编译成 Bundle
HMR Server
将热更新的文件输出给 HMR Rumtime
Bundle server
提供文件在浏览器的访问
HMR Rumtime
客户端 HMR 的中枢,用来更新文件的变化,与 HMR server 通过 websocket 保持长链接,由此传输热更新的文件
bundle.js
代表构建出来的文件
大致流程
分为两个流程,一个是文件系统的文件通过 webpack 的编译器进行编译,接着被放到 Bundle Server 服务器上,也就是 1 -> 2 -> A -> B 的流程;
第二个流程是,当文件系统发生改变的时候,Webpack 会重新编译,将更新后的代码发送给了 HMR Server,接着便通知给了 HMR Runtime,一般来说热更新的文件或者说是 module 是以 json 的形式传输给 浏览器的 HMR Runtime 的,最终 HMR Runtime 就会更新我们前端的代码。1 -> 2 -> 3 -> 4 -> 5
要注意的几个点:
webpack-dev-server是将打包的代码放到内存之中,不是在output指定的目录之下,这样能使webpack速度更快。webpack-dev-server底层是基于webpack-dev-middleware这个库的,他能调用webpack相关的Api对代码变化进行监控,并且告诉webpack,将代码打包到内存中。Websocket不会将更新好的代码直接发给服务器端,而是发一个更新模块的哈希值,真正处理这个hash的还是webpack。

- 浏览器端
HMR.runtime会根据最新的hash值,向服务器端拿到所有要更新的模块的hash值,接着再通过一个jsonp请求来获取这些hash对应的最新模块代码。


- 浏览器端拿到最新的更新代码后,如我们在配置文件中配置的一样,是根据
HotModuleReplacementPlugin对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。 - 当模块的热替换过程中,如果替换模块失败,就会会推倒
live reload操作,也就是进行浏览器刷新来获取最新打包代码。