Webpack配置全解析(基础篇)

  Webpack凭借强大的功能,成为最流行和最活跃的打包工具,也是面试时高级程序员必须掌握的“软技能”;笔者结合在项目中的使用经验,介绍webpack的使用;本文是入门篇,主要介绍webpack的入口、输出和各种loader、plugins的使用。

本文所有的demo代码均在WebpackDemo

概念

  来看一下官网对webpack的定义:

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  首先webpack是一个静态模块打包器,所谓的静态模块,包括脚本、样式表和图片等等;webpack打包时首先遍历所有的静态资源,根据资源的引用,构建出一个依赖关系图,然后再将模块划分,打包出一个或多个bundle。再次白piao一下官网的图,生动的描述了这个过程:

logo.png

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  提到webpack,就不得不提webpack的四个核心概念

  • 入口(entry):指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始
  • 输出(output):在哪里输出它所创建的 bundles
  • loader:让 webpack 能够去处理那些非 JavaScript 文件
  • 插件(plugins):用于执行范围更广的任务

你的第一个打包器

  我们首先在全局安装webpack:

1
npm install webpack webpack-cli –g

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  webpack可以不使用配置文件,直接通过命令行构建,用法如下:

1
webpack <entry> [<entry>] -o <output>

  这里的entry和output就对应了上述概念中的入口和输入,我们来新建一个入口文件:

1
2
3
4
//demo1/index.js
var a = 1
console.log(a)
document.write('hello webpack')

  有了入口文件我们还需要通过命令行定义一下输入路径dist/bundle.js:

1
webpack index.js -o dist/bundle.js

  这样webpack就会在dist目录生成打包后的文件。

demo1-result.png

  我们也可以在项目目录新建一个html引入打包后的bundle.js文件查看效果。

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

配置文件

  命令行的打包方式仅限于简单的项目,如果我们的项目较为复杂,有多个入口,我们不可能每次打包都把入口记下来;因此一般项目中都使用配置文件来进行打包;配置文件的命令方式如下:

1
webpack [--config webpack.config.js]

  配置文件默认的名称就是webpack.config.js,一个项目中经常会有多套配置文件,我们可以针对不同环境配置不同的文件,通过--config来进行切换:

1
2
3
4
//生产环境配置
webpack --config webpack.prod.config.js
//开发环境配置
webpack --config webpack.dev.config.js

多种配置类型

  config配置文件通过module.exports导出一个配置对象:

1
2
3
4
5
6
7
8
9
10
11
12
//webpack.config.js
var path = require('path');
module.exports = {
mode: 'development',
//入口文件
entry: './index.js',
//输出目录
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
};

  除了导出为对象,还可以导出为一个函数,函数中会带入命令行中传入的环境变量等参数,这样可以更方便的对环境变量进行配置;比如我们在打包线上正式环境和线上开发环境可以通过env进行区分:

1
2
3
4
5
6
7
8
9
var path = require('path');
//env:环境对象
module.exports = function(env, argv){
return {
//其他配置
entry: './index.js',
output: {}
}
};

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  另外还可以导出为一个Promise,用于异步加载配置,比如可以动态加载入口文件:

1
2
3
4
5
6
7
8
9
10
module.exports = () => {
return new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve({
entry: './index.js',
output: {}
})
}, 5000)
})
}

入口

  正如在上面提到的,入口是整个依赖关系的起点入口;我们常用的单入口配置是一个页面的入口:

1
2
3
module.exports = {
entry: './index.js',
}

  它是下面的简写:

1
2
3
4
5
module.exports = {
entry: {
main: './index.js'
},
}

  但是我们一个页面可能不止一个模块,因此需要将多个依赖文件一起注入,这时就需要用到数组了,代码在demo2中:

1
2
3
4
5
6
7
8
9
10
module.exports = {
entry: [
//轮播图模块
'./src/banner.js',
//主模块
'./src/index.js',
//底部模块
'./src/foot.js'
],
}

  有时候我们一个项目可能有不止一个页面,需要将多个页面分开打包,entry支持传入对象的形式,代码在demo3中:

1
2
3
4
5
6
7
8
//demo3
module.exports = {
entry: {
home: './src/home.js',
list: './src/list.js',
detail: ['./src/detail.js', './src/common.js'],
},
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  这样webpack就会构建三个不同的依赖关系。

输出

  output选项用来控制webpack如何输入编译后的文件模块;虽然可以有多个entry,但是只能配置一个output

1
2
3
4
5
6
7
8
9
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
//CDN地址
publicPath: '/',
},
}

  这里我们配置了一个单入口,输出也就是bundle.js;但是如果存在多入口的模式就行不通了,webpack会提示Conflict: Multiple chunks emit assets to the same filename,即多个文件资源有相同的文件名称;webpack提供了占位符来确保每一个输出的文件都有唯一的名称:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
entry: {
home: './src/home.js',
list: './src/list.js',
detail: ['./src/detail.js', './src/common.js'],
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js',
},
}

  这样webpack打包出来的文件就会按照入口文件的名称来进行分别打包生成三个不同的bundle文件;还有以下不同的占位符字符串:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

占位符 描述
[hash] 模块标识符(module identifier)的 hash
[chunkhash] chunk 内容的 hash
[name] 模块名称
[id] 模块标识符
[query] 模块的 query,例如,文件名 ? 后面的字符串

  在这里引入Module、Chunk和Bundle的概念,上面代码中也经常会看到有这两个名词的出现,那么他们三者到底有什么区别呢?首先我们发现module是经常出现在我们的代码中,比如module.exports;而Chunk经常和entry一起出现,Bundle总是和output一起出现。

  • module:我们写的源码,无论是commonjs还是amdjs,都可以理解为一个个的module
  • chunk:当我们写的module源文件传到webpack进行打包时,webpack会根据文件引用关系生成chunk文件,webpack 会对这些chunk文件进行一些操作
  • bundle:webpack处理好chunk文件后,最后会输出bundle文件,这个bundle文件包含了经过加载和编译的最终源文件,所以它可以直接在浏览器中运行。

  我们通过下面一张图更深入的理解这三个概念:

chunk-bundle.png

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

总结:

  module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:我们直接写出来的是module,webpack处理时是chunk,最后生成浏览器可以直接运行的bundle。

hash、chunkhash、contenthash

  理解了chunk的概念,相信上面表中chunkhash和hash的区别也很容易理解了;

  • hash:是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值。
  • chunkhash:跟入口文件的构建有关,根据入口文件构建对应的chunk,生成每个chunk对应的hash;入口文件更改,对应chunk的hash值会更改。
  • contenthash:跟文件内容本身相关,根据文件内容创建出唯一hash,也就是说文件内容更改,hash就更改。

模式

  在webpack2和webpack3中我们需要手动加入插件来进行代码的压缩、环境变量的定义,还需要注意环境的判断,十分的繁琐;在webpack4中直接提供了模式这一配置,开箱即可用;如果忽略配置,webpack还会发出警告。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
mode: 'development',
};
//相当于
module.exports = {
devtool:'eval',
plugins: [
new webpack.NamedModulesPlugin(),
new webpack.NamedChunksPlugin(),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("development")
})
]
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  开发模式是告诉webpack,我现在是开发状态,也就是打包出来的内容要对开发友好,便于代码调试以及实现浏览器实时更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
mode: 'production',
};
//相当于
module.exports = {
plugins: [
new UglifyJsPlugin(/*...*/),
new webpack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production")
}),
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.NoEmitOnErrorsPlugin()
]
}

  生产模式不用对开发友好,只需要关注打包的性能和生成更小体积的bundle。看到这里用到了很多Plugin,不用慌,下面我们会一一解释他们的作用。

  相信很多童鞋都曾有过疑问,为什么这边DefinePlugin定义环境变量的时候要用JSON.stringify("production"),直接用"production"不是更简单吗?

  我们首先来看下JSON.stringify("production")生成了什么;运行结果是""production"",注意这里,并不是你眼睛花了或者屏幕上有小黑点,结果确实比"production"多嵌套了一层引号。

  我们可以简单的把DefinePlugin这个插件理解为将代码里的所有process.env.NODE_ENV替换为字符串中的内容。假如我们在代码中有如下判断环境的代码:

1
2
3
4
5
6
7
8
9
10
11
12
//webpack.config.js
module.exports = {
plugins: [
new webpack.DefinePlugin({
"process.env.NODE_ENV": "production"
}),
]
}
//src/index.js
if (process.env.NODE_ENV === 'production') {
console.log('production');
}

  这样生成出来的代码就会编译成这样:

1
2
3
4
5
//dist/bundle.js
//代码中并没有定义production变量
if (production === 'production') {
console.log('production');
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  但是我们代码中可能并没有定义production变量,因此会导致代码直接报错,所以我们需要通过JSON.stringify来包裹一层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//webpack.config.js
module.exports = {
plugins: [
new webpack.DefinePlugin({
//"process.env.NODE_ENV": JSON.stringify("production")
//相当于
"process.env.NODE_ENV": '"production"'
}),
]
}
//dist/bundle.js
if ("production" === 'production') {
console.log('production');
}

  这样编译出来的代码就没有问题了。

自动生成页面

  在上面的代码中我们发现都是手动来生成index.html,然后引入打包后的bundle文件,但是这样太过繁琐,而且如果生成的bundle文件引入了hash值,每次生成的文件名称不一样,因此我们需要一个自动生成html的插件;首先我们需要安装这个插件:

1
npm install --save-dev html-webpack-plugin

  在demo3中,我们生成了三个不同的bundle.js,我们希望在三个不同的页面能分别引入这三个文件,如下修改config文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module.exports = {
//省略其他代码
plugins: [
new HtmlWebpackPlugin({
//引用的模板文件
template: './index.html',
//生成的html名称
filename: 'home.html',
chunks: ['home']
}),
new HtmlWebpackPlugin({
template: './index.html',
filename: 'list.html',
chunks: ['list']
}),
new HtmlWebpackPlugin({
template: './index.html',
filename: 'detail.html',
chunks: ['detail']
}),
]
}

  我们以index.html作为模板文件,生成home、list、detail三个不同的页面,并且通过chunks分别引入不同的bundle;如果这里不写chunks,每个页面就会引入所有生成出来的bundle。

  html-webpack-plugin还支持以下字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
new HtmlWebpackPlugin({
template: './index.html',
filename: 'all.html',
//页面注入title
title: 'html webpack plugin title',
//默认引入所有的chunks链接
chunks: 'all',
//注入页面位置
inject: true,
//启用hash
hash: true,
favicon: '',
//插入meta标签
meta: {
'viewport': 'width=device-width, initial-scale=1.0'
},
minify: {
//清除script标签引号
removeAttributeQuotes: true,
//清除html中的注释
removeComments: true,
//清除html中的空格、换行符
//将html压缩成一行
collapseWhitespace: false,
//压缩html的行内样式成一行
minifyCSS: true,
//清除内容为空的元素(慎用)
removeEmptyElements: false,
//清除style和link标签的type属性
removeStyleLinkTypeAttributes: false
}
}),

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  上面设置title后需要在模板文件中设置模板字符串:

1
<title><%= htmlWebpackPlugin.options.title %></title>

loader

  loader用于对模块module的源码进行转换,默认webpack只能识别commonjs代码,但是我们在代码中会引入比如vue、ts、less等文件,webpack就处理不过来了;loader拓展了webpack处理多种文件类型的能力,将这些文件转换成浏览器能够渲染的js、css。

  module.rules允许我们配置多个loader,能够很清晰的看出当前文件类型应用了哪些loader,loader的代码均在demo4中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {}
}
},
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
},
]
}
}

  我们可以看到rules属性值是一个数组,每个数组对象表示了不同的匹配规则;test属性时一个正则表达式,匹配不同的文件后缀;use表示匹配了这个文件后调用什么loader来处理,当有多个loader的时候,use就需要用到数组。

  多个loader支持链式传递,能够对资源进行流水线处理,上一个loader处理的返回值传递给下一个loader;loader处理有一个优先级,从右到左,从下到上;在上面demo中对css的处理就遵从了这个优先级,css-loader先处理,处理好了再给style-loader;因此我们写loader的时候也要注意前后顺序。

css-loader和style-loader

  css-loader和style-loader从名称看起来功能很相似,然而两者的功能有着很大的区别,但是他们经常会成对使用;安装方法:

1
npm i -D css-loader style-loader

  css-loader用来解释@import和url();style-loader用来将css-loader生成的样式表通过<style>标签,插入到页面中去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* /src/head.css */
.head{
display: flex;
background: #666;
}
/* /src/foot.css */
.foot{
background: #ccc;
}
/* /src/index.css */
@import './head.css';
@import './foot.css';
.wrap {
background: #999;
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  然后在入口文件中将index.css引入,就能看到打包的效果,页面中插入了三个style标签,代码在demo4:

css-loader.png

sass-loader和less-loader

  这两个loader看名字大家也能猜到了,就是用来处理sass和less样式的。安装方法:

1
npm i -D sass-loader less-loader node-sass

  在config中进行配置,代码在demo4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
//其他配置
rules: {
test: /\.scss$/,
use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
},{
loader: 'sass-loader'
}]
},{
test: /\.less$/,
use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
},{
loader: 'less-loader'
}]
}
}

postcss-loader

  都0202年了,小伙伴肯定不想一个一个的手动添加-moz、-ms、-webkit等浏览器私有前缀;postcss提供了很多对样式的扩展功能;啥都不说,先安装起来:

1
npm i -D postcss-loader

  老规矩,还是在config中进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
rules: [{
test: /\.scss$/,
use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
}, {
loader: 'postcss-loader'
},{
loader: 'sass-loader'
}]
},{
test: /\.less$/,
use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
}, {
loader: 'postcss-loader'
},{
loader: 'less-loader'
}]
}]

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  正当我们兴冲冲的打包看效果时,发现样式还是老样子,并没有什么改变。

why

  这是因为postcss主要功能只有两个:第一就是把css解析成JS可以操作的抽象语法树AST,第二就是调用插件来处理AST并得到结果;所以postcss一般都是通过插件来处理css,并不会直接处理,所以我们需要先安装一些插件:

1
npm i -D autoprefixer postcss-plugins-px2rem cssnano

  在项目根目录新建一个.browserslistrc文件。

1
2
> 0.25%
last 2 versions

  我们将postcss的配置单独提取到项目根目录下的postcss.config.js

1
2
3
4
5
6
7
8
9
10
module.exports = {
plugins: [
//自动添加前缀
require('autoprefixer'),
//px转为rem,应用于移动端
require('postcss-plugins-px2rem')({ remUnit: 75 }),
//优化合并css
require('cssnano'),
]
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  有了autoprefixer插件,我们打包后的css就自动加上了前缀。

babel-loader

  兼容低版本浏览器的痛相信很多童鞋都经历过,写完代码发现自己的js代码不能运行在IE10或者IE11上,然后尝试着引入各种polyfill;babel的出现给我们提供了便利,将高版本的ES6甚至ES7转为ES5;我们首先安装babel所需要的依赖:

1
2
npm i -D babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime
npm i -S @babel/runtime

  然后在config添加loader对js进行处理:

1
2
3
4
5
6
7
8
9
{
//省略其他配置
rules: [{
test: /\.js/,
use: {
loader: 'babel-loader'
}
}]
}

  同样的,我们把babel的配置提取到根目录,新建一个.babelrc文件:

1
2
3
4
5
6
7
8
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"@babel/plugin-transform-runtime"
]
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  我们可以在index.js中尝试写一些es6的语法,看到代码会被转译成es5,代码在demo4中。由于babel-loader的转译速度很慢,在后面我们加入了时间插件后可以看到每个loader的耗时,babel-loader是最耗时间;因此我们要尽可能少的使用babel来转译文件,我们对config进行改进,

1
2
3
4
5
6
7
8
9
10
11
{
//省略其他配置
rules: [{
test: /\.js$/,
use: {
loader: 'babel-loader'
},
// exclude: /node_modules/,
include: [path.resolve(__dirname, 'src')]
}]
}

  正则上使用$来进行精确匹配,通过exclude将node_modules中的文件进行排除,include将只匹配src中的文件;可以看出来include的范围比exclude更缩小更精确,因此也是推荐使用include。

file-loader和url-loader

  file-loader和url-loader都是用来处理图片、字体图标等文件;url-loader工作时分两种情况:当文件大小小于limit参数,url-loader将文件转为base-64编码,用于减少http请求;当文件大小大于limit参数时,调用file-loader进行处理;因此我们优先使用url-loader,首先还是进行安装,安装url-loader之前还需要把file-loader先安装:

1
npm i file-loader url-loader -D

  接下来还是修改config:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
//省略其他配置
rules: [{
test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
use: {
loader: 'url-loader',
options: {
//10k
limit: 10240,
//生成资源名称
name: '[name].[hash:8].[ext]',
//生成资源的路径
outputPath: 'imgs/'
},
exclude: /node_modules/,
}
}]
}

  我们在css中给body添加一个小于10k的居中背景图片:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

1
2
3
4
5
6
7
body{
width: 100vw;
height: 100vh;
background: url(./bg.png) no-repeat;
background-size: 400px 400px;
background-position: center center;
}

  打包后查看body的样式可以发现图片已经被替换成base64格式的url了,代码在demo4。

url-loader.png

html-withimg-loader

  如果我们在页面上引用一个图片,会发现打包后的html还是引用了src目录下的图片,这样明显是错误的,因此我们还需要一个插件对html引用的图片进行处理:

1
npm i -D html-withimg-loader

  老样子还是在config中对html进行配置:

1
2
3
4
5
6
7
8
9
{
//省略其他配置
rules: [{
test: /\.(htm|html)$/,
use: {
loader: 'html-withimg-loader'
}
}]
}

  然鹅,打开页面发现却是这样的:

html-withimg-loader.png

  这是因为在url-loader中把每个图片作为一个模块来处理了,我们还需要去url-loader中修改:

1
2
3
4
5
6
7
8
use: {
loader: 'url-loader',
options: {
//10k
limit: 10240,
esModule: false
}
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  这样我们在页面上的图片引用也被修改了,代码在demo4中。

  html-withimg-loader会导致html-webpack-plugin插件注入title的模板字符串<%= htmlWebpackPlugin.options.title %>失效,原封不动的展示在页面上;因此,如果我们想保留两者的功能需要在配置config中把html-withimg-loader删除并且通过下面的方式来引用图片:

1
<img src="<%=require('./src/bg1.png') %>" alt="" srcset="">

vue-loader

  最后说一下一个比较特殊的vue-loader,看名字就知道是用来处理vue文件的。

1
2
npm i -D vue-loader vue-template-compiler
npm i -S vue

  我们首先来创建一个vue文件,具体代码在demo5中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//src/App.vue
<template>
<div id="app">
<div class="box" @click="tap">{{title}}</div>
</div>
</template>
<script>
export default {
name: 'app',
data(){
return {
title: 'app实例'
}
},
methods: {
tap(){
this.title = this.title.split('').reverse().join('')
}
}
}
</script>
<style>
#app{
font-size: 16px;
background: #ccc;
}
</style>

  然后在webpack的入口文件中引用它:

1
2
3
4
5
6
7
//src/main.js
import Vue from 'vue'
import App from './App.vue'

new Vue({
render: h => h(App)
}).$mount('#app')

  不过vue-loader和其他loader不太一样,除了将它和.vue文件绑定之外,还需要引入它的一个插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const VueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
module: {
rules: [
//省略其他loader
{
test: /\.vue$/,
loader: 'vue-loader'
}]
},
plugins: [
new VueLoaderPlugin(),
]
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  这样我们就能愉快的在代码中写vue了。

搭建开发环境

  在上面的demo中我们都是通过命令行打包生成dist文件,然后直接打开html或者通过static-server来查看页面的;但是开发中我们写完代码每次都来打包会严重影响开发的效率,我们期望的是写完代码后立即就能够看到页面的效果;webpack-dev-server就很好的提供了一个简单的web服务器,能够实时重新加载。

  首先在我们的项目中安装依赖:

1
npm i -D webpack webpack-dev-server

  webpack-dev-server的用法和wepack一样,只不过他会额外启动一个express的服务器。我们在项目中新建一个webpack.dev.config.js配置文件,单独对开发环境进行一个配置,相关代码在demo6中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
//省略其他配置
devServer: {
//启动服务器端口
port: 9000,
//默认是localhost,只能本地访问
host: "0.0.0.0",
//自动打开浏览器
open: false,
//启用模块热替换
hot: true,
//启用gzip压缩
compress: true
},
plugins: [
//热更新插件
new webpack.HotModuleReplacementPlugin({
})
]
}

  通过命令行webpack-dev-server来启动服务器,启动后我们发现根目录并没有生成任何文件,因为webpack打包到了内存中,不生成文件的原因在于访问内存中的代码比访问文件中的代码更快。

  我们在public/index.html的页面上有时候会引用一些本地的静态文件,直接打开页面的会发现这些静态文件的引用失效了,我们可以修改server的工作目录,同时指定多个静态资源的目录:

1
2
3
4
contentBase: [
path.join(__dirname, "public"),
path.join(__dirname, "assets")
]

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  热更新(Hot Module Replacemen简称HMR)是在对代码进行修改并保存之后,webpack对代码重新打包,并且将新的模块发送到浏览器端,浏览器通过新的模块替换老的模块,这样就能在不刷新浏览器的前提下实现页面的更新。

dev-server.png

  可以看出浏览器和webpack-dev-server之间通过一个websock进行连接,初始化的时候client端保存了一个打包后的hash值;每次更新时server监听文件改动,生成一个最新的hash值再次通过websocket推送给client端,client端对比两次hash值后向服务器发起请求返回更新后的模块文件进行替换。

  我们点击源码旁的行数看一下编译后的源码是什么样的:

source-map.png

  发现跟我们的源码差距还是挺大的,本来是一个简单add函数,通过webpack的模块封装,已经很难理解原来代码的含义了,因此,我们需要将编译后的代码映射回源码;devtool中不同的配置有不同的效果和速度,综合性能和品质后,我们一般在开发环境使用cheap-module-eval-source-map,在生产环境使用source-map

1
2
3
4
module.exports = {
devtool: 'cheap-module-eval-source-map',
//其他配置
}

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  其他各模式的对比:

devtool.png

plugins

  在上面我们也介绍了DefinePlugin、HtmlWebpackPlugin等很多插件,我们发现这些插件都能够不同程度的影响着webpack的构建过程,下面还有一些常用的插件,plugins相关代码在demo7中。

clean-webpack-plugin

  clean-webpack-plugin用于在打包前清理上一次项目生成的bundle文件,它会根据output.path自动清理文件夹;这个插件在生产环境用的频率非常高,因为生产环境经常会通过hash生成很多bundle文件,如果不进行清理的话每次都会生成新的,导致文件夹非常庞大;这个插件安装使用非常方便:

1
npm i -D clean-webpack-plugin

  安装后我们在config中配置一下就可以了:

1
2
3
4
5
6
7
8
9
10
11
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
//其他配置
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
})
]
}

mini-css-extract-plugin

  我们之前的样式都是通过style-loader插入到页面中去,但是生产环境需要单独抽离样式文件,mini-css-extract-plugin就可以帮我从js中剥离样式:

1
npm i -D mini-css-extract-plugin

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  我们在开发环境使用style-loader,生产环境使用mini-css-extract-plugin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
//其他配置
module: {
rules: [
{
test: /\.less/,
use: [{
loader: isDev ? 'style-loader' : MiniCssExtractPlugin.loader
},{
loader: 'css-loader'
},{
loader: 'less-loader'
}]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[hash:8].css",
})
]
}

  引入loader后,我们还需要配置plugin,提取的css同样支持output.filename中的占位符字符串。

optimize-css-assets-webpack-plugin

  我们可以发现虽然配置了production模式,打包出来的js压缩了,但是打包出来的css确没有压缩;在生产环境我们需要对css进行一下压缩:

1
npm i optimize-css-assets-webpack-plugin -D

  然后也是引入插件:

1
2
3
4
5
6
7
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
//其他配置
plugins: [
new OptimizeCSSAssetsPlugin(),
]
}

copy-webpack-plugin

  和demo6中一样,我们在public/index.html中引入了静态资源,但是打包的时候webpack并不会帮我们拷贝到dist目录,因此copy-webpack-plugin就可以很好地帮我做拷贝的工作了

1
npm i -D copy-webpack-plugin

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

  在config中配置我们需要拷贝的源路径和目标路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: 'public/js/*.js',
to: path.resolve(__dirname, 'dist', 'js'),
flatten: true,
}
]
}),
]
}

ProvidePlugin

  ProvidePlugin可以很快的帮我们加载想要引入的模块,而不用require。一般我们加载jQuery需要先把它import:

1
2
import $ from 'jquery'
$('.box').html('box')

  但是我们在config中配置ProvidePlugin插件后能够不用import,直接使用$

1
2
3
4
5
6
7
8
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
}),
]
}

  但是如果在项目中引入了太多模块并且没有require会让人摸不着头脑,因此建议加载一些常见的比如jQuery、vue、lodash等。

no-idea.png

loader和plugin的区别

  介绍了这么多loader和plugin,我们来回顾一下他们两者的区别:

loader:由于webpack只能识别js,loader相当于翻译官的角色,帮助webpack对其他类型的资源进行转译的预处理工作。
plugins:plugins扩展了webpack的功能,在webpack运行时会广播很多事件,plugin可以监听这些事件,然后通过webpack提供的API来改变输出结果。

总结

  最后,介绍了这么多,本文是webpack基础篇,还有很多生产环境的优化还没有写到;因此各位看官敬请期待优化篇。

参考:

谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

再来一打Webpack面试题
Webpack HMR 原理解析


谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里

本网所有内容文字和图片,版权均属谢小飞所有,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表。如需转载请关注公众号【前端壹读】后回复【转载】。