webpack4从入门到进阶——基础篇

683 阅读31分钟

前言

阅读此文需要结合webpack官方中文文档

1.什么是webpack?

webpack是一套基于Node.js的模块打包工具,在webpack刚推出的时候,它是一个单纯的js模块打包工具,可以将多个模块的js文件打包合并到一个文件中,随着时间的推移和众多开发者的追捧和贡献,现在的webpack不仅仅能打包js文件,还可以打包css/less/scss/图片等其它文件。

为什么要分模块?

由于在开发中,为了提升代码的可维护性和可复用性,我们会将不同的功能写到不同的模块中。但是这样就会面临一些问题,比如:
(1).导入资源变多了,请求的次数也变多了,性能就变差了;
(2).如何维护模块之间的关系?

如何解决这些问题呢?
(1).项目上线时将所有的模块都合并到一个文件中去
(2).在index.html中只引用主文件,在主文件中导入依赖模块

假设,现在有三个模块,它们的功能分别是创建了网页的头部、内容、底部区域:

文件结构

// header.js模块
function addHeader() {
    const header = document.createElement('div')
    header.innerText = '我是头部'
    document.body.appendChild(header)
}
// content.js模块
function addContent() {
    const content = document.createElement('div')
    content.innerText = '我是内容'
    document.body.appendChild(content)
}
// footer.js模块
function addFooter() {
    const footer = document.createElement('div')
    footer.innerText = '我是底部'
    document.body.appendChild(footer)
}

这时,我们想要在一个index.js中去使用这三个模块,就要先导入这三个模块,然后导入index.js,然后在index.js执行addHeader,addContent,addFooter这三个方法,这里就会有一个先后顺序:

// index.js

addHeader()
addContent()
addFooter()
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<script src="index.js"></script>
<script src="header.js"></script>
<script src="footer.js"></script>
<script src="content.js"></script>
</body>
</html>

如果模块加载的时候先加载index.js,后加载三个模块,肯定就会出问题:

错误信息

因此,当项目过大的时候,这种关系维护起来非常麻烦,所以我们要利用webpack来帮我们打包

2.webpack的安装

前面说过,由于webpack是基于Node.js的,因此在安装webpack前需要确保已经安装了node

先来初始化一个项目,生成package.json

// y是yes的意思,可以快速初始化,省去了npm init的时候一步一步敲回车的过程
npm init -y

然后开始安装webpack4+,需要两条命令:

npm install --save-dev webpack

npm install --save-dev webpack-cli

命令执行完以后,就可以看到package.json中多了两项,这就代表安装好了:

package.json

3.如何打包一个文件

我们将刚才的header、content、footer包装成真正的模块,暴露出来给index.js来使用:

// header.js模块
function addHeader() {
    const header = document.createElement('div')
    header.innerText = '我是头部'
    document.body.appendChild(header)
}
export default addHeader
// content.js模块
function addContent() {
    const content = document.createElement('div')
    content.innerText = '我是内容'
    document.body.appendChild(content)
}

export default addContent
// footer.js模块
function addFooter() {
    const footer = document.createElement('div')
    footer.innerText = '我是底部'
    document.body.appendChild(footer)
}

export default addFooter
// index.js
import headerModule from './header.js'
import contentModule from './content.js'
import footerModule from './footer.js'

headerModule()
contentModule()
footerModule()

然后打包index.js

// npx webpack 打包文件的名称
npx webpack index.js

index.js就是我们需要打包的文件,等待命令执行完以后,可以看到目录中多了一个dist文件夹,里面有一个main.js文件,这个main.js就是对index.js打包之后的结果:

打包后的文件结构

在html中引入main.js:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>

<script src="dist/main.js"></script>

</body>
</html>

打包后执行的最终结果:

浏览器中的效果

4.webpack的配置文件

什么是webpack的配置文件?
前面我们说可以通过npx webpack index.js命令去打包index.js文件,其实在webpack中,除了可以用命令行的形式去打包文件之外,还可以通过配置文件的形式来打包文件

官方文档说,配置是写在一个叫webpack.config.js的文件里面的,行,那我们先不管三七二十一,新建一个webpack.config.js文件,依样画葫芦

webpack.config.js

然后我们来看看webpack.config.js各项配置的含义:

// 这里需要用到node的path模块
const path = require("path")

module.exports = {
    /* 
    mode:指定打包的模式,模式有两种
    开发模式(development):不会对打包的js代码进行压缩
    生产模式(production):会对打包的js代码进行压缩
     */
    mode: "development", // "production" | "development"
    
    /*
    entry:指定需要打包的文件
     */
    entry: "./index.js",
    
    /*
    output:指定打包后文件输出的路径和输出文件的名称
     */
    output: {
        /*
        filename: 指定打包之后的JS文件的名称
         */
        filename: "bundle.js",
        /*
        path: 指定打包之后的文件存储到什么地方
        __dirname代表当前这个js文件所在目录的绝对路径
         */
        path: path.resolve(__dirname, "bundle")
    }
}

配置好以后,我们来看看效果,在命令行中输入npx webpack,当后面没有文件名的时候,会自动去查找webpack.config.js配置文件进行打包

npx webpack

执行完成后,可以看到根目录中多了一个bundle文件夹,文件夹中多了一个bundle.js,说明打包成功

打包后的文件

如果是开发模式(mode为development),bundle.js中的代码是没有压缩的

未压缩的bundle.js

如果是生产模式(mode为production),bundle.js中的代码是压缩过的

压缩后的bundle.js

接下来我们使用新打包生成的bundle.js文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>

<!--<script src="dist/main.js"></script>-->
<script src="bundle/bundle.js"></script>

</body>
</html>

可以看到效果和之前是一样的:

浏览器中的效果

webpack配置的注意事项
当我们去执行npx webpack命令的时候,webpack会自动去找到叫webpack.config.js的配置文件,根据配置去打包,如果找不到这个文件的话,会报错

那这个配置文件可以叫其它名字吗?当然可以,需要这么一条命令:

// npx webpack --config 文件名
npx webpack --config xxx

可是每次输入这么一条指令,我觉得又长又麻烦,我们可以利用package.jsonscript属性来保存指令,比如这样:

package.json

然后每次我们在命令行输入npm run build的时候,就等于输入了npx webpack --config new.config.js,或者直接点击前面绿色的三角形也可以执行这条命令

5.sourcemap

什么是sourcemap?

先来看,我们在index.js中写上这样一行代码

// index.js
console.log(a)

打包之后生成bundle.js,然后引用,结果肯定是报错,因为a并没有声明,但重点是报错文件显示在第97行,我明明只写了一行代码呀

错误信息提示

原因很简单,打包后的bundle.js会自己添加很多代码,那这样就有一个问题,如果我们写的代码出现问题了,这里定位起来就非常麻烦,非常不利于调试,因为如果运行webpack打包后的代码,错误提示的内容也是打包后文件的内容

所以为了降低调试的难度, 提高错误代码的阅读性, 我们就需要知道打包后代码和打包之前代码的映射关系

只要有了这个映射关系我们就能很好的显示错误提示的内容, 存储这个映射关系的文件我们就称之为sourcemap

如何开启sourcemap?
通过webpack的devtool属性可以开启,具体可以参考devtool

devtool的属性表

参照官方文档,devtool属性可以有很多取值。需要了解一下,在构建速度这一列中,+代表可以加快打包的速度,-代表会降低打包的速度。由于这个属性的取值非常多,我们可以根据这些它们的规律来了解:

(1).eval
devtool为none的时候,代码是这样的

devtool: "none"

bundle.js中的内容

我们把devtool设置成eval后,可以看到我们代码被放进了eval之中,并且还存储了它的映射关系

devtool: "eval"

bundle.js中的内容

总结:不会单独生成sourcemap文件, 会将映射关系存储到打包的文件中, 并且通过eval存储
优点:性能最好,但是在企业开发中不推荐
缺点:会让打包后的文件变得更加冗余,业务逻辑比较复杂时候提示信息可能不全面不正确

(2).sourcemap
如果是取值后面带有source-map的,就代表会将映射关系存储到单独的文件之中
比如我们将devtool设置成cheap-source-map

devtool: "cheap-source-map"

可以看到,bundle.js中多了bundle.js.map的文件,这个文件中就存储了映射关系

打包后的文件

总结:会单独生成sourcemap文件, 通过单独文件来存储映射关系
优点:提示信息全面,可以直接定位到错误代码的行和列
缺点:打包速度慢

(3).inline
如果将devtool设置成带inline的值会怎么样?

devtool: "inline-cheap-source-map"

打包完成之后,可以看到bundle.js下面没有sourcemap文件

打包后的文件

并且在bundle.js中,多了一个sourceMappingURL字段,在这个字段中以字符串的形式存储了映射关系

bundle.js

总结:不会单独生成sourcemap文件, 会将映射关系存储到打包的文件中, 并且通过base64字符串形式存储

(4).cheap
如果是取值中带cheap的,表示生成的映射信息只能定位到错误行不能定位到错误列

(5).module
如果是取值中带module的,就表示不仅希望存储我们代码的映射关系, 还希望存储第三方模块映射关系, 以便于第三方模块出错时也能更好的排错

在企业开发中,通常配置如下:
development: cheap-module-eval-source-map
只需要行错误信息, 并且包含第三方模块错误信息, 并且不会生成单独sourcemap文件

production: cheap-module-source-map
只需要行错误信息, 并且包含第三方模块错误信息, 并且会生成单独sourcemap文件

开启sourcemap之后,现在的报错提示可以让我们轻易的找到错误行了

错误信息

错误信息

6.file-loader

使用fileloader打包图片
前面说webpack不仅能打包js,还可以打包图片,那我们来试试,在index.js中往body插入一张图片(请注意这里打印了img)

// 引入一张图片
const img = require('./img/wechatheader.png')
// 打印img看看
console.log(img, 'img')

const imgNode = document.createElement('img')
imgNode.src = img
document.body.appendChild(imgNode)

然后执行npx webpack index.js,结果报错了,提示:你可能需要一个loader去处理文件类型,那问题来了,什么是loader?

错误信息提示

什么是loader?
由于webapck的本质是一个模块打包工具, 所以webpack默认只能处理JS文件,不能处理其他文件,因为其他文件中没有模块的概念;

但是在企业开发中我们除了需要对JS进行打包以外,还有可能需要对图片/CSS等进行打包,,所以为了能够让webpack能够对其它的文件类型进行打包,在打包之前就必须将其它类型文件转换为webpack能够识别处理的模块;

用于将其它类型文件转换为webpack能够识别处理模块的工具我们就称之为loader

怎么使用loader?
webpack中的loader都是用NodeJS编写的,但是在企业开发中我们完全没有必要自己编写,因为已经有现成的,我们只需要安装、配置、使用即可

由于import是用来导入模块的,所以我们需要把图片转成模块,需要用到file-loader

根据文档教程,我们先来安装file-loader

npm install --save-dev file-loader

安装好之后,需要在webpack.config.js配置文件中进行配置

注意:现在回头来看我们为什么要打印img。在file-loader 4.3.0之前,导入的img通过file-loader转换以后,输出的是图片的路径;但是从4.3.0开始默认使用了esModule语法,转换后输出的是一个Moudle对象

默认情况下控制台中img的输出结果

可以使用Module.default拿到图片的路径,或者设置esModule为false,让img变成图片的路径

module.exports = {
  /*
    module:告诉webpack如何处理webpack不能够识别的文件
     */
    module: {
        /*
        rules:rules表示规则,rules数组下的一个对象就代表一个规则
         */
        rules: [
            /*
              test:但凡正则表达式匹配到png、jpg、gif结尾的文件
              use:就使用file-loader进行处理
             */
            {
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            // file-loader新版本默认使用了esModule语法
                            esModule: false,
                        }
                    }
                ]
            }
        ]
    }
}

这时再来看看img打印的结果,就变成了打包之后的图片地址了

设置esModule为false以后img的输出结果

配置好规则以后,file-loader会将图片转换成模块,这样我们在index.js中去导入之后,再打包就不会报错,可以看到图片被打包到了bundle中了,名称变成了一个MD5的哈希值

打包后的文件

再来引入bundle.js看看效果,图片就被顺利加载出来了

浏览器中的效果

fileloader的其它配置
通过上一步可以看到,打包之后的图片名称是一个MD5的哈希值,那我们可以改变这个图片的名称吗?或者我们想要改变打包后图片的路径,这些可以通过配置来实现

 module: {
        /*
        rules:rules表示规则,rules数组下的一个对象就代表一个规则
         */
        rules: [
            /*
              test:但凡正则表达式匹配到png、jpg、gif结尾的文件
              use:就使用file-loader进行处理
             */
            {
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            // file-loader新版本默认使用了esModule语法
                            esModule: false,
                            /* 指定打包后文件名称
                            [name]表示打包后的文件名称和打包前一样
                            [ext]表示打包后的文件后缀和打包前一样
                             */
                            name: '[name].[ext]',
                            // 指定打包后文件存放目录
                            outputPath: 'images/',
                            // 指定托管服务器地址(统一替换图片地址)
                            publicPath: 'http://www.it666.com/images/'
                        }
                    }
                ]
            }
        ]
    }

打包完成之后,可以看到图片的名称和打包前一样,并且被放到了images文件夹下面

配置后打包生成的文件结构

这时候打印的img的地址,就变成了指定托管服务器地址,当我们把图片配置到第三方托管的时候,这样设置就可以拿到图片资源了

打印的img

7.url-loader

打包图片不仅可以用前一小节说到的file-loader,还可以用url-loader;我们在官方的loader文档中找到教程,loader的使用方式都是一样的,这里就不再赘述了,第一步还是需要安装

npm install --save-dev url-loader

安装完成后进行配置,由于url-loaderfile-loader的功能是差不多的,二者取其一就好,在配置url-loader之前先删掉原来的file-loader配置

module: {
        /*
        rules:rules表示规则,rules数组下的一个对象就代表一个规则
         */
        rules: [
            /*
              test:但凡正则表达式匹配到png、jpg、gif结尾的文件
              use:就使用file-loader进行处理
             */
            {
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            esModule: false,
                        },
                    }
                ]
            }
        ]
    }

打包以后的效果:

url-loader打包后的图片

打包后的文件结构:

url-loader打包图片后的文件结构

bundle文件夹中并没有看到图片文件,但在浏览器中还是可以看到图片,查看图片的路径发现图片被转成了base64的字符串

图片的src

url-loaderfile-loader的区别就是url-loader可以将图片转成base64的字符串,也可以不转,通过配置中的limit属性来决定:

如果图片的大小超过了limit限制的大小,图片就被保存成一个文件
如果图片的大小没有超过limit限制的大小,图片就会被转成base64的字符串

当前将limit配置为1024 * 100,也就是限制为100kb,图片的大小为232k,是大于limit的(其它的配置和file-loader是一样的)

module: {
        /*
        rules:rules表示规则,rules数组下的一个对象就代表一个规则
         */
        rules: [
            /*
              test:但凡正则表达式匹配到png、jpg、gif结尾的文件
              use:就使用file-loader进行处理
             */
            {
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            esModule: false,
                            /*
                            limit: 指定图片限制的大小
                            * */
                            limit: 1024 * 100,
                            // 指定打包后文件名称
                            name: '[name].[ext]',
                            // 指定打包后文件存放目录
                            outputPath: 'images/',
                        },
                    }
                ]
            }
        ]
    }

图片的大小

打包后,可以看到bundle文件夹下面产生了一张图片,因此可以证明,当图片大小大于limit的时候,会被保存为文件的形式:

图片大小大于limit后的文件结构

注意:
对于比较小的图片, 我们将图片转换成base64的字符串之后, 可以提升网页的性能(因为减少了请求的次数)
对于比较大的图片, 哪怕我们将图片转换成了base64的字符串之后, 也不会提升网页的性能, 还有可能降低网页的性能(因为图片如果比较大, 那么转换之后的字符串也会比较多, 那么网页的体积就会变大, 那么访问的速度就会变慢)

8.css-loader

和图片一样,webpack默认不能处理css文件,所以需要借助css-loader将css转换为wepack能够处理的类型,安装命令:

npm install --save-dev css-loader

我们在index.js中创建一个<img>标签,然后引入图片和css样式,css样式设置图片的边框为红色

// index.js
import img from './img/wechatheader.png'
import './index.css'

let oImg = document.createElement("img")
oImg.src = img
oImg.setAttribute("class", "border")
document.body.appendChild(oImg)
// index.css
.border{
    border:1px solid red;
}

由于webpack默认情况下也是不能处理css文件的,现在打包肯定会报错:

错误信息

根据官方文档的教程,我们在webpack.config.js中添加配置:

module.exports = {
  module: {
    rules: [
      // 打包CSS规则
      {
        test: /\.css$/,
        /*
        css-loader:   解析css文件中的@import依赖关系
        style-loader: 将webpack处理之后的内容插入到HTML的HEAD代码中
        * */
        use: [ 'style-loader', 'css-loader' ]
      }
    ]
  }
}

配置中出现了一个style-loader,也是需要安装才能使用的,官方建议将style-loadercss-loader结合使用:

npm install style-loader --save-dev

css-loader:解析css文件中的@import依赖关系,比如a.css中引入了b.css文件,说明a.css依赖b.css,那css-loader就会把b.css的代码解析到a.css中。
style-loader:将webpack处理之后的内容插入到HTML的HEAD代码中

打包后可以看到图片多了一层红色边框,说明css样式生效了;在html中,css代码也确实被插入到了<head>标签中:

最终效果

html中的代码

css-loader注意点
(1).单一原则:一个loader只做一件事情
(2).多个loader会按照从右至左,从下至上的顺序执行

// 先执行css-loader解析依赖关系,然后执行style-loader将css代码插入<head>中
use: [ 'style-loader', 'css-loader' ]
// 先执行css-loader解析依赖关系,然后执行style-loader将css代码插入<head>中
 use: [
           {
               loader: "style-loader" // creates style nodes from JS strings
           },
           {
               loader: "css-loader" // translates CSS into CommonJS
           }
      ]

css-loader的模块化
现在我们新建一个custom.js的文件,在里面添加一个addImage的方法,这个方法又创建了一张图片,然后暴露出来给index.js使用,这样一来我们就创建了两个<img>标签:

// custom.js
import img from './img/wechatheader.jpg'

function addImage() {
    let oImg = document.createElement("img")
    oImg.src = img
    oImg.setAttribute("class", "border")
    document.body.appendChild(oImg)
}

export {addImage}
// index.js
import img from './img/wechatheader.jpg'
import {addImage} from './custom.js'
import './index.css'

let oImg = document.createElement("img")
oImg.src = img
oImg.setAttribute("class", "border")
document.body.appendChild(oImg)

// 调用custom模块的方法新增一张图片
addImage()
// index.css
.border{
    border:1px solid red;
    transform: translate(200px, 200px);
}

打包后就看到了两张图片都被加上红色边框:

执行后的效果

通过在index.js中使用import './index.css'这种方法引入的样式,会在index.jscustom.js中都生效,如果我们只想让引入的样式在一个模块中生效,就需要开启css-loader的模块化,在webpack.config.js中配置:

module: {
        rules: [{
            test: /\.css$/,
           use: [
                    {
                        loader: "style-loader"
                    },
                    {
                        loader: "css-loader",
                        options: {
                            // 开启css的模块化
                            modules: true,
                        }
                    }
                ]
        }]
    }

index.js中改变一下css样式文件的引用方式,同时打印一下拿到的cssModule:

// index.js
import img from './img/wechatheader.jpg'
import {addImage} from './custom.js'
// 改变引用方式
import cssModule from './index.css'

// 这里打印一下cssModule
console.log(cssModule, 'cssModule')


let oImg = document.createElement("img")
oImg.src = img
oImg.setAttribute("class", cssModule.border)
document.body.appendChild(oImg)

// 调用custom模块的方法新增一张图片
addImage()

打包完成后,可以看到只有一张图片被设置了红色边框,通过查看打印的cssModule,发现css样式被转换成了一个对象,通过cssModule.border就可以给图片设置样式:

最终效果

9.less-loader

less-loader的作用是自动将less转成css,安装:

npm install --save-dev less-loader less

配置:

module: {
        rules: [{
            test: /\.less$/,
            use: [{
                loader: "style-loader" // creates style nodes from JS strings
            }, {
                loader: "css-loader" // translates CSS into CommonJS
            }, {
                loader: "less-loader" // compiles Less to CSS
            }]
        }]
    }

10.sass-loader

sass-loader的作用是自动将scss转换为css,安装:

npm install sass-loader node-sass webpack --save-dev

配置:

module: {
    rules: [{
      test: /\.scss$/,
      use: [{
          loader: "style-loader" // 将 JS 字符串生成为 style 节点
      }, {
          loader: "css-loader" // 将 CSS 转化成 CommonJS 模块
      }, {
          loader: "sass-loader" // 将 Sass 编译成 CSS
      }]
    }]
  }

11.postcss-loader

postcss和sass/less不同,它不是css预处理器,它是一款使用插件去转换css的工具;postcss有许多非常好用的插件,例如:
(1).autoprefixer(自动补全浏览器前缀)
(2).postcss-pxtorem(自动把px代为转换成rem)

在webpack中也有postcss-loader可以实现这些功能,首先来安装:

npm i -D postcss-loader

webpack.config.js中添加配置:

module: {
    rules: [
     {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    },
                    {
                        loader: "css-loader"
                    },
                    {
                        loader: "postcss-loader"
                    }
                ]
            }
    ]
  }

autoprefixer(自动补全浏览器前缀)
配置好postcss-loader之后,我们还需要添加插件autoprefixer,安装:

npm i -D autoprefixer

安装好autoprefixer之后,我们在根目录创建一个postcss.config.js配置文件:

module.exports = {
    plugins: {
        "autoprefixer": {
            "overrideBrowserslist": [
                "ie >= 8", // 兼容IE7以上浏览器
                "Firefox >= 3.5", // 兼容火狐版本号大于3.5浏览器
                "chrome >=35", // 兼容谷歌版本号大于35浏览器
                "opera >= 11.5", // 兼容欧朋版本号大于11.5浏览器
            ]
        }
    }
}

接下来我们验证一下,在index.css中加入css3的属性transform,然后打包:

// index.css
.border{
    border:1px solid red;
    transform: translate(200px, 200px);
}

在浏览器中,<img>标签的transform属性被自动加上了各种前缀,说明autoprefixer生效:

最终效果

注意:可以在Can I use这个网站中查询浏览各个版本的浏览器是否需要添加前缀

postcss-pxtorem(自动把px代为转换成rem)
安装:

npm install postcss-pxtorem -D

然后在postcss.config.js中添加配置:

module.exports = {
    plugins: {
        "autoprefixer": {
            "overrideBrowserslist": [
                "ie >= 8", // 兼容IE7以上浏览器
                "Firefox >= 3.5", // 兼容火狐版本号大于3.5浏览器
                "chrome >=35", // 兼容谷歌版本号大于35浏览器
                "opera >= 11.5", // 兼容欧朋版本号大于11.5浏览器
            ]
        },
        "postcss-pxtorem": {
            rootValue: 100, // 根元素字体大小
            // propList: ['*'], // 可以从px更改到rem的属性,*代表所有属性
            propList: ["height"],
        }
    }
}

然后我们在index.less中将图片的高度设置成200px:

// index.less
body{
  img{
    height: 200px;
    border: 1px solid #4f47ff;
  }
}

此时webpack.config.js的配置:

module: {
    rules: [
     {
                test: /\.less$/,
                use: [
                    {
                        loader: "style-loader"
                    },
                    {
                        loader: "css-loader"
                    },
                    {
                        loader: "less-loader"
                    },
                    {
                        loader: "postcss-loader"
                    }
                ]
            }
    ]
  }

打包后可以看到,我们在index.less中写的height: 200px被转成了rem:

最终效果

12.打包字体图标iconfont

我们经常都会使用到iconfont,如图是font目录,里面除了有css文件,还有eot/js/json/svg/ttf/woff/woff2,除了js文件,其它都是webpack默认不能处理的格式,这里就需要用到file-loader来处理:

font目录

webpack.config.js中的配置:

 module: {
        /*
       rules:rules表示规则,rules数组下的一个对象就代表一个规则
        */
        rules: [
            {
                test: /\.(eot|json|svg|woff|woff2|ttf)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            // esModule: false,
                            // 指定打包后文件名称
                            name: '[name].[ext]',
                            // 指定打包后文件存放目录
                            outputPath: 'font/',
                        },
                    }
                ]
            },
        ]
    }

index.js中去使用iconfont

// index.js
// 引入iconfont的css文件
import './font/iconfont.css'

// 使用icon
document.body.innerHTML = "<i class='iconfont icon-2_shanchu' style='font-size: 50px'>"

打包后来看最终效果:

iconfont的使用

13.html-webpack-plugin

从这里开始我们会接触到webpack的插件(plugin),用于扩展webpack的功能;前面讲到的loader其实也是变相的扩展了webpack,但是它只专注于转化文件这一个领域,而plugin的功能更加丰富,不仅仅局限于资源的加载。

html-webpack-plugin的作用是什么呢?我们先来看一个现象:每次我们打包文件后,在bundle文件中的目录结构是这样的:

bundle文件目录

我们要手动在bundle文件夹中创建一个html,然后将bundle.js引入到html中。这个操作太麻烦,我们希望让webpack来帮我们做这件事情:

要使用html-webpack-plugin插件,首先需要安装:

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

webpack.config.js中修改配置:

// 引入HtmlWebpackPlugin插件
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    // 前面的配置省略
    
    /*
    告诉webpack需要新增一些什么样的功能
    */
    plugins: [new HtmlWebpackPlugin()],
}

删除掉原来的bundle文件夹,重新打包之后,这时看bundle目录中已经自动多了一个html文件,里面也自动引入了打包后的bundle.js

bundle目录

index.html

html-webpack-plugin在每次打包的时候,都为我们新增了一个html模板,并且将打包后的js文件引入其中;那现在又有一个新需求:如何让它使用我们自己的html模板,而不是重新去生成一个新的?

先来新建一个我们自己的html模板:

// index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>

<div>我是自己创建的html</div>
</body>
</html>

接着在webpack.config.js中配置:

// 引入HtmlWebpackPlugin插件
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    // 前面的配置省略
    
    /*
    告诉webpack需要新增一些什么样的功能
    */
     plugins: [new HtmlWebpackPlugin({
        template: "./index.html", // 配置模板
        minify:{
            collapseWhitespace: false, // 是否要压缩html文件
        }
    })],
}

然后开始打包,打包后可以看到,bundle文件中不仅自动多了一个index.html,这个html还是我们自己创建的模板,如果想要压缩这个html文件,设置collapseWhitespace属性为true就可以了,这里为了看到效果我就不压缩了:

index.html

14.clean-webpack-plugin

了解clean-webpack-plugin之前,我们先来看一个现象,在index.js中使用一张图片,然后打包:

// index.js
import img1 from './img/wechatheader.jpg'
// 引用样式
import './index.css'


let oImg = document.createElement("img")
oImg.src = img1
oImg.setAttribute("class", 'border')
document.body.appendChild(oImg)

打包后在bundle/images下就多了一张图片:

img1

然后我们修改一下index.js的代码,使用另外一张图片:

// index.js
import img2 from './img/c.jpg'
// 引用样式
import './index.css'


let oImg = document.createElement("img")
oImg.src = img2
oImg.setAttribute("class", 'border')
document.body.appendChild(oImg)

然后再来打包,这时第二张图片也被打包到了bundle/images目录中:

文件结构

现在并没有用到wechatheader.jpg这张图片,但它还是会存在于images中,我们在开发中可能会经常修改图片资源的使用,原来用到但是现在没有用到的图片,不手动删除的话,会一直存在于打包文件中,长期下去就会导致打包文件体积越来越大,使项目难以维护;因此,使用clean-webpack-plugin可以先清空目录,再打包。

clean-webpack-plugin不属于官方插件,所以在webpack官方中文文档中是找不到的,想要使用这个插件,首先也需要安装:

npm install --save-dev clean-webpack-plugin

webpack.config.js中添加配置:

// 引入HtmlWebpackPlugin插件
const HtmlWebpackPlugin = require('html-webpack-plugin')

// 引入CleanWebpackPlugin插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin')

module.exports = {
    // 前面的配置省略
    
    /*
    告诉webpack需要新增一些什么样的功能
    */
     plugins: [
        // 使用HtmlWebpackPlugin
        new HtmlWebpackPlugin({
        template: "./index.html",
        }),
        // 使用CleanWebpackPlugin
        new CleanWebpackPlugin(),
    ],
}

打包之后,可以看到bundle/images目录下面只有一张图片了,没有用到的已经被自动清理掉了:

文件目录

15.copy-webpack-plugin

在打包项目的时候除了JS/CSS/图片/字体图标等文件需要打包以外,可能还有一些相关的文档也需要打包,文档的内容是固定不变的,我们只需要将对应的文件拷贝到打包目录中即可,这个时候就可以使用copy-webpack-plugin来实现文件的拷贝

安装copy-webpack-plugin

npm install --save-dev copy-webpack-plugin

配置webpack.config.js

// 引入CopyWebpackPlugin插件
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
    // 前面的配置省略
    
    /*
    告诉webpack需要新增一些什么样的功能
    */
     plugins: [
        // 使用HtmlWebpackPlugin
        new HtmlWebpackPlugin({
        template: "./index.html",
        }),
        // 使用CleanWebpackPlugin
        new CleanWebpackPlugin(),
        new CopyWebpackPlugin([
            {from: "./doc", to: "doc"} // from表示文档原来的路径,to表示需要拷贝到bundle下的路径
        ])
    ],
}

打包后再来看文件结构,doc被拷贝到了bundle文件夹下了:

文件目录

16.mini-css-extract-plugin

mini-css-extract-plugin是一个专门用于将打包的css内容提取到单独文件的插件,前面我们通过style-loader打包的css都是直接插入到<head>中的

安装:

npm install --save-dev mini-css-extract-plugin

配置webpack.config.js,注意这里要替换掉原来的style-loader

// 引入MiniCssExtractPlugin插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
    // 前面的内容省略
    
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    {
                        // loader: "style-loader", // 原来是将css插入<head>
                        loader: MiniCssExtractPlugin.loader, // 使用MiniCssExtractPlugin的loader
                    },
                    {
                        loader: "css-loader",
                    },
                ]
            },
        ]
    },
    /*
    告诉webpack需要新增一些什么样的功能
     */
    plugins: [
        // 前面的内容省略
        
        new MiniCssExtractPlugin({
            filename: 'css/[name].css',
        }),
    ],
}

打包之后,可以看到目录中多了css文件,并且css也被自动引入到了html之中:

最终效果

如何压缩css?
前面我们说配置mode属性为production,会对打包的js代码进行压缩;那如果我们想要压缩打包后的css代码呢?

这里需要用到optimize-css-assets-webpack-plugin插件,首先来安装:

npm install --save -d optimize-css-assets-webpack-plugin

配置webpack.config.js,新增一个optimization属性(优化项):

// 引入MiniCssExtractPlugin插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

// 引入OptimizeCSSAssetsPlugin插件
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')

// 引入TerserJSPlugin插件
const TerserJSPlugin = require('terser-webpack-plugin')

module.exports = {
    /*
    optimization: 配置webpack的优化项
     */
    optimization: {
        minimizer: [new OptimizeCSSAssetsPlugin({})],
    },
    mode: "production", // 配置为生产模式
}

打包后再来看,css代码已经全部被压缩了:

css代码

但是文档中有这样一句话:

To minify the output, use a plugin like optimize-css-assets-webpack-plugin. Setting optimization.minimizer overrides the defaults provided by webpack, so make sure to also specify a JS minimizer:

大致意思是:如果想要压缩CSS,可以使用optimize-css-assets-webpack-plugin插件,但是会覆盖掉默认的webpack的配置,会覆盖掉默认的JS的压缩,我们可以看一下此时设置mode为production打包后的JS文件,确实没有被压缩:

bundle.js

我们需要额外的安装js代码压缩的插件:

npm install --save -d terser-webpack-plugin

修改webpack.config.js配置文件:

// 引入MiniCssExtractPlugin插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

// 引入OptimizeCSSAssetsPlugin插件
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')

// 引入TerserJSPlugin插件
const TerserJSPlugin = require('terser-webpack-plugin')

module.exports = {
    /*
    optimization: 配置webpack的优化项
     */
    optimization: {
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
    },
    mode: "production", // 配置为生产模式
}

打包后再来看,css和js都被压缩了:

最终效果

17.watch

之前我们打包文件之后,如果修改了代码,需要重新手动打包;启用Watch模式,可以让webpack监听打包文件的更改,当它们修改后会重新编译打包:

配置webpack.config.js

module.exports = {
    watch: true,
    watchOptions: {
        aggregateTimeout: 300, // 防抖,修改完成之后300ms再编译
        poll: 1000, // 每隔1000ms检查一次变动
        ignored: ['node_modules'], // 排除一些巨大的文件夹,不需要监控的文件夹,例如node_modules
    },
}

18.webpack-dev-server

webpack-dev-serverwatch一样可以监听文件变化,但区别是webpack-dev-server可以将打包好的程序运行在一个服务器环境下,还可以解决企业开发中开发阶段跨域的问题。

要使用webpack-dev-server,第一步需要安装:

npm install webpack-dev-server --save-dev

配置webpack.config.js,由于和watch功能相似,所以二者只能保留一个:

module.exports = {
  //...
 devServer: {
        contentBase: "./bundle",
        open: true, // 编译打包完以后是否会自动在浏览器打开网页
        port: 9000 // 端口号
    },
}

此时打包的命令也发生变化:

// 以前
npx webpack

// 现在
npx webpack-dev-server

通过npx webpack-dev-server命令打包完成后,网页自动打开,并且端口号被改成了9000:

执行后的结果

webpack-dev-server解决开发阶段的跨域问题

module.exports = {
    //...
    devServer: {
    
        // 第一种写法
        proxy: {
            /*
            当我们在代码中发送请求到/api和/login的时候,
            devServer就会自动将我们请求的地址替换为http://localhost:3000
            */
            "/api": {
                target: "http://localhost:3000",
                changeOrigin: true, // 当target为域名的时候,要开启域名跨域
                secure: false, // https跨域
                pathRewrite: {'/api' : ''} // 路径重写,将路径中的api替换为空
            },
             "/login": {
                target: "http://localhost:3000",
                changeOrigin: true, // 当target为域名的时候,要开启域名跨域
                secure: false, // https跨域
                pathRewrite: {'/api' : ''} // 路径重写,将路径中的api替换为空
            }
        }
        
        /***************************分割线*****************************/
        
        // 第二种写法
        proxy: [{
            context: ['/api', '/login'],
            target: "http://localhost:3000",
                changeOrigin: true, // 当target为域名的时候,要开启域名跨域
                secure: false, // https跨域
                pathRewrite: {'/api' : ''} // 路径重写,将路径中的api替换为空
        }]
        
    }
};

target:请求的地址;

changeOrigin:如果请求的地址是个域名(www.mi.com),就要开启域名跨域;

secure:如果是https协议(https://localhost:3000),需要开启https跨域;

pathRewrite:重写路径,例如{'/api' : ''},请求到 http://localhost:3000/api/xxx 现在会被代理到请求 http://localhost:3000/xxx

注意:webpack-dev-server只能解决开发阶段的跨域问题,并不能解决项目上线之后的跨域问题,因为项目上线之后是将打包好的文件上传到服务器,而打包好的文件中并没有webpack,更不会有webpack-dev-server,想要解决跨域问题,还是要依赖服务端。

19.热模块替换(热更新)插件

什么是热模块替换?(hot module replacement)
细心的读者可能会发现,在上一节中通过webpack-dev-server自动打包的文件并没有真正放到指定的目录中,因为读写磁盘是非常耗时耗性能的,为了提升性能,webpack-dev-server将打包好的内容直接放到了内存中(读写内存的速度比读写磁盘的速度快很多)。
通过webpack-dev-server可以实现实时监听打包内容的变化,每次打包之后都会自动刷新网页,但正是因为每当内容被修改时都会自动刷新网页,在开发阶段可能会带来不便,这时就需要需要通过热模块替换(HMR)插件来优化调试开发,使用热更新插件会在内容发生变化的时候更新修改的内容,但是不会重新刷新网页。

HMR的使用

配置webpack.config.js:

// 引入webpack
const webpack = require('webpack')

module.exports = {
    devServer: {
        hot: true, // 开启热更新,开启以后就不会自动刷新网页
        hotOnly: true, // 哪怕不支持热更新也不要刷新网页
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
                    {
                        // loader: "style-loader", // 原来是将css插入<head>
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                         //如果这里使用MiniCssExtractPlugin.loader,需要设置hmr: true,才能支持css代码的热更新
                            hmr: true,
                        }
                    },
                    {
                        loader: "css-loader",
                        options: {
                            // 开启css的模块化
                            // modules: true,
                        }
                    },
                    {
                        loader: "postcss-loader"
                    }
                ]
            },
        ]
    },
    plugins: [new webpack.HotModuleReplacementPlugin()] // 使用热更新插件
    
}

注意
对于css模块而言,在css-loader中已经帮我们实现了热更新,只要css代码被修改就会立即更新;但是对于js模块而言,默认并没有实现热更新,所以修改了js模块代码并不会立即更新

如何实现js模块的热更新呢

index.js中引入一个custom.js模块,当我们修改了custom.js代码的时候,会执行module.hot.accept()中的回调去更新内容:

// index.js

import img2 from '../img/c.jpg'
// 引用样式
import '../css/index.css'
// 引用js模块
import {addSpan} from './custom.js'


let oImg = document.createElement("img")
oImg.src = img2
oImg.setAttribute("class", 'border')
document.body.appendChild(oImg)

addSpan()

// 判断当前有没有开启热更新
if (module.hot) {
    // 告诉热更新需要监听哪一个js模块发生变化
    module.hot.accept('./custom.js', function () {
        // 删掉原来的span
        let oSpan = document.querySelector('span')
        document.removeChild(oSpan)
        // 重新添加span
        addSpan()
    })
}
// custom.js

function addSpan() {
    let oSpan = document.createElement("span")
    oSpan.innerText = '我是新增的span 的内容'
    document.body.appendChild(oSpan)
}

export {addSpan}

20.babel

在企业开发中为了兼容一些低级版本的浏览器,我们需要将ES6、ES7、ES8高级语法转换为ES5低级语法,否则在低版本浏览器中我们的程序无法正确执行,默认情况下webpack是不会将我们的代码转成ES5低级语法的,需要使用babel来转换。

如何使用babel
安装babel-loader及其核心库:

npm install -D babel-loader @babel/core @babel/preset-env webpack

配置webpack.config.js,我们可以通过配置presets的方式来告诉webpack我们需要兼容哪些浏览器,然后babel就会根据我们的配置自动调整转换方案,如果需要兼容的浏览器已经能够实现,就不转换了:

module.exports = {
    module: {
        rule: [
            {
                test: /\.js$/,
                exclude: /node_modules/, // 不处理哪一个文件夹
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            "presets": [['@babel/preset-env', {
                                // 配置哪些版本的浏览器需要转换
                                "targets": {
                                    "chrome": "58",
                                    "ie": "11"
                                }
                            }]]
                        }
                    }
                ]
            }
        ]
    }
}

如何利用babel实现低版本语法
对于ES6、ES7、ES8语法有对应关系的ES5语法而言,babel现在可以帮我们实现自动转换,但是对于没有对应关系的语法而言,按照现在的配置还不能实现自动转换,例如Promise,includes等方法就是新增的语法,ES5中没有对应关系,这时候就需要用到polyfill来让babel实现对应语法:

安装不存在代码的实现包:

npm install --save @babel/polyfill

在对应的js模块中引入:

// index.js

import "@babel/polyfill"

// *****

但是这样导入有个坏处,无论我们有没有用到不存在的语法都会打包到文件中,会增加了打包后文件的大小,我们希望按需加载,所以要在webpack.config.js中配置useBuiltIns属性:

module.exports = {
    module: {
        rule: [
            {
                test: /\.js$/,
                exclude: /node_modules/, // 不处理哪一个文件夹
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            "presets": [['@babel/preset-env', {
                                // 配置哪些版本的浏览器需要转换
                                "targets": {
                                    "chrome": "58",
                                    "ie": "11"
                                },
                                useBuiltIns: 'usage', // 按需加载
                            }]]
                        }
                    }
                ]
            }
        ]
    }
}

使用babel的注意点
先来打包一下,这时控制台有一个提示:

控制台信息

意思是如果配置了useBuiltIns: 'usage',就可以移除入口文件中的import "@babel/polyfill"

直接导入polyfill的方式只适用于一般项目的开发,但是如果在编写第三方模块的时候会有一些问题,因为这种方式是通过全局变量的方式来注入代码,会污染全局环境,所以我们再来看一下polyfill的第二种配置方式:

安装相关模块transform-runtime

npm install --save-dev @babel/plugin-transform-runtime

npm install --save @babel/runtime

配置webpack.config.js

module.exports = {
    module: {
        rule: [
            {
                test: /\.js$/,
                exclude: /node_modules/, // 不处理哪一个文件夹
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            "presets": [['@babel/preset-env', {
                                // 配置哪些版本的浏览器需要转换
                                "targets": {
                                    "chrome": "58",
                                    "ie": "11"
                                },
                                useBuiltIns: 'usage', // 按需加载
                            }]],
                            "plugins": [
                                [
                                    "@babel/plugin-transform-runtime",
                                    {
                                        "absoluteRuntime": false,
                                        "corejs": 2, // 设置为2,不污染全局环境
                                        "helpers": true,
                                        "regenerator": true,
                                        "useESModules": false,
                                        "version": "7.0.0-beta.0"
                                    }
                                ]
                            ]
                        }
                    }
                ]
            }
        ]
    }
}

但是这时候打包会报错,因为corejs属性设置为2,表示不污染全局环境,文档中可以看到,corejs配置为2或者3的时候,还要再单独安装包:

corejs

安装,然后再打包就没问题啦:

npm install --save @babel/runtime-corejs2

21.html-withimg-loader

此前我们通过file-loaderurl-loader已经可以将JS或者CSS中的图片打包到指定目录中了,但是并不能将HTML中引用的图片打包到指定目录中,所以我们需要html-withimg-loader来实现这一功能

安装html-withimg-loader

npm install html-withimg-loader --save

配置webpack.config.js

module.exports = {
    module: {
        rule: [
            {
                test: /\.(htm|html)$/i,
                loader: 'html-withimg-loader'
            },
        ]
    }

22.图片的压缩和合并

在企业开发中,为了提升网页的访问速度,我们除了会压缩JS/CSS/HTML以外,还会对网页上的图片进行压缩和合并,压缩可以减少网页的体积,合并可以减少请求次数。

压缩打包之后的图片
每次在打包图片之前,我们可以通过配置webpack对打包的图片进行压缩,以减少打包之后的体积

利用image-webpack-loader压缩图片,安装(如果安装后打包遇到image-webpack-loader报错,尝试使用cnpm):

npm install image-webpack-loader --save-dev

配置webpack.config.js

module.exports = {
    module: {
        rules: [
            // 打包jpg|png|gif的规则
            {
                test: /\.(jpg|png|gif)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            esModule: false,
                            // 指定打包后文件名称
                            name: '[name].[ext]',
                            // 指定打包后文件存放目录
                            outputPath: 'images/',
                        },
                    },
                    {
                        loader: 'image-webpack-loader',
                        options: {
                            // 代表不同类型文件的压缩规则
                            mozjpeg: {
                                progressive: true,
                                quality: 65, // 压缩的质量,100表示不压缩,值越小压缩得越多
                            },
                            // optipng.enabled: false will disable optipng
                            optipng: {
                                enabled: false,
                            },
                            pngquant: {
                                quality: [0.65, 0.90],
                                speed: 4
                            },
                            gifsicle: {
                                interlaced: false,
                            },
                            // the webp option will enable WEBP
                            webp: {
                                quality: 75
                            }
                        }
                    },
                ]
            },
        ]
    }    
}

压缩之前的图片大小:

压缩之前

压缩之后的图片大小:

压缩之后

合并打包的图片
过去为了减少网页请求的次数,我们需要UI设计师提供精灵图,并且在使用的时候还需要手动的去设置没张图片的位置,但是webpack可以让我们自己合成精灵图,并且会自动设置好图片的位置

利用postcss-sprites来合并图片,先来安装postcss-spritespostcss

npm install --save -d postcss-sprites postcss

在前面讲postcss-loader的时候我们在根目录创建一个postcss.config.js配置文件,所以和postcss相关的插件我们在postcss.config.js配置文件中配置:

module.exports = {
    plugins: {
        "autoprefixer": {
            "overrideBrowserslist": [
                "ie >= 8", // 兼容IE7以上浏览器
                "Firefox >= 3.5", // 兼容火狐版本号大于3.5浏览器
                "chrome >=35", // 兼容谷歌版本号大于35浏览器
                "opera >= 11.5", // 兼容欧朋版本号大于11.5浏览器
            ]
        },
        "postcss-pxtorem": {
            rootValue: 100, // 根元素字体大小
            // propList: ['*'], // 可以从px更改到rem的属性
            propList: ["height"],
        },
        "postcss-sprites": {
            // 告诉webpack合并之后的图片保存到什么地方
            spritePath: "./bundle/images"
        },
    }
}

现在我们编写一个简单的案例来测试一下:

// index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>

<div class="img1"></div>
<div class="img2"></div>
</body>
</html>
// index.css

div{
    height: 200px;
    width: 200px;
}

.img1{
    background: url("../img/wechatheader.jpg");
}

.img2{
    background: url("../img/c.jpg");
}
// index.js

import '../css/index.css'

打包之后再来看,bundle/images下面被合并成了一张图片,打包后的css代码也自动添加了位置:

打包后的文件

但这时候打开html,发现什么没有,查看图片的路径我们会发现一些问题:

图片路径

由于webpack打包之后给我们的都是相对路径,所以会导致在html中使用的图片能够正常运行,在css中的图片不能正常运行,例如:打包之后的路径是images/c.jpg,那么在html中会去html文件所在的路径下找images,能找到所以不会有问题;但是在css中,会去css文件下找images,找不到所以图片加载不出来,这就是为什么这里的路径变成了css/images的原因。

要想解决这个问题,我们需要找到file-loader的配置,在下面添加一个属性publicPath:

在开发阶段,将publicPath设置为dev-server服务器地址
在上线阶段,将publicPath设置为线上服务器地址

{
    loader: 'file-loader',
    options: {
        esModule: false,
        // 指定打包后文件名称
        name: '[name].[ext]',
        // 指定打包后文件存放目录
        outputPath: 'images/',
        publicPath: 'http://127.0.0.1:9000/images'
    },
},

重新打包以后就可以看到图片了:

合并后的最终效果

新的问题又来了,所有的图片都会被合并成一张图片,如果我想合并成两张图片,三张图片又该怎么做呢?可以通过groupBy属性来设置,让图片按照文件夹来合并:

先将图片按照文件夹分类:

此时的文件结构

配置groupBy属性,这个属性必须要返回一个Promise对象:

module.exports = {
    plugins: {
        "autoprefixer": {
            "overrideBrowserslist": [
                "ie >= 8", // 兼容IE7以上浏览器
                "Firefox >= 3.5", // 兼容火狐版本号大于3.5浏览器
                "chrome >=35", // 兼容谷歌版本号大于35浏览器
                "opera >= 11.5", // 兼容欧朋版本号大于11.5浏览器
            ]
        },
        "postcss-pxtorem": {
            rootValue: 100, // 根元素字体大小
            // propList: ['*'], // 可以从px更改到rem的属性
            propList: ["height"],
        },
        "postcss-sprites": {
            // 告诉webpack合并之后的图片保存到什么地方
            spritePath: "./bundle/images",
            // 告诉webpack合并图片的时候如何分组
            groupBy: function (image) {
                let path = image.url.substr(0, image.url.lastIndexOf('/'))
                let name = path.substr(path.lastIndexOf('/') + 1)
                return Promise.resolve(name)
            }
        },
    }
}

打包后再来看bundle/images目录,生成了两张精灵图:

文件目录

还有一个filterBy属性需要了解,这个属性是用来做过滤的,用来设置哪些图片需要合并,哪些图片不需要合并:

module.exports = {
    plugins: {
        "autoprefixer": {
            "overrideBrowserslist": [
                "ie >= 8", // 兼容IE7以上浏览器
                "Firefox >= 3.5", // 兼容火狐版本号大于3.5浏览器
                "chrome >=35", // 兼容谷歌版本号大于35浏览器
                "opera >= 11.5", // 兼容欧朋版本号大于11.5浏览器
            ]
        },
        "postcss-pxtorem": {
            rootValue: 100, // 根元素字体大小
            // propList: ['*'], // 可以从px更改到rem的属性
            propList: ["height"],
        },
        "postcss-sprites": {
            // 告诉webpack合并之后的图片保存到什么地方
            spritePath: "./bundle/images",
            // 告诉webpack合并图片的时候如何分组
            groupBy: function (image) {
                // image是一个对象,通过image.url拿到图片的文件夹名称
                let path = image.url.substr(0, image.url.lastIndexOf('/'))
                let name = path.substr(path.lastIndexOf('/') + 1)
                return Promise.resolve(name)
            },
            // 告诉webpack哪些图片需要合并,哪些不需要合并
            filterBy: function (image) {
                let path = image.url
                // 如果是png结尾的才合并
                if (!/\.png$/.test(path)) {
                    return Promise.reject()
                } else return Promise.resolve()
            }
        },
    }
}

23.ESLint

ESLint是一个插件化的JavaScript代码检测工具,它可以用于检查常见的JavaScript代码错误,也可以进行代码规范的检查;在企业开发中项目负责人会定制一套ESLint规则,然后应用到所编写的项目上,从而实现辅助编码规范的执行,有效控制项目代码的质量。在编译打包时如果语法有错误或者不符合规范就会报错,并且提示相关错误信息。

如何使用ESLint
安装ESLint和对应eslint-loader

npm install eslint --save-dev

npm install eslint-loader --save-dev

修改配置文件webpack.config.js,由于loader是从下往上执行的,所以检查编码规范的规则要写在最下面,让它最先执行(或者可以设置enforce属性为pre):

module.exports = {
    module: {
        rules: [
            // 前面的配置省略......
            
            // 检查编码规范的规则
            {
                enforce: 'pre', // 当前loader最先执行
                test: /\.js$/,
                exclude: /node_modules/, // 排除不需要检查的目录
                include: path.resolve(__dirname, 'src'),
                loader: 'eslint-loader',
                options: {
                    // eslint options (if necessary)
                },
            },
        ]
    }
}

配置webpack.config.js完成以后,我们还需要配置对应的规则,ESLint中文文档说还需要创建一个.eslintrc.*文件,所以我们根据提示,创建一个.eslintrc.js文件,配置对应的规则:

// .eslintrc.js
// https://eslint.org/docs/user-guide/configuring
module.exports = {
  /*
  不重要,永远写true
   */
  root: true,
  parserOptions: {
    // parser: 'babel-eslint',
    /*
    默认设置为 3,5(默认), 你可以使用 6、7、8、9 或 10 来指定你想要使用的 ECMAScript 版本
     */
    "ecmaVersion": 10,
    /*
    设置为 "script" (默认) 或 "module"(如果你的代码是 ECMAScript 模块)。
     */
    "sourceType": "module",
    /*
    ecmaFeatures - 这是个对象,表示你想使用的额外的语言特性:
    globalReturn - 允许在全局作用域下使用 return 语句
    impliedStrict - 启用全局 strict mode (如果 ecmaVersion 是 5 或更高)
    jsx - 启用 JSX
     */
    "ecmaFeatures": {}
  },
  // 指定代码运行的宿主环境
  env: {
    browser: true, // 浏览器
    node: true, // node
    /*
    支持 ES6 语法并不意味着同时支持新的 ES6 全局变量或类型(比如 Set 等新类型)。
    对于 ES6 语法,使用 { "parserOptions": { "ecmaVersion": 6 } }
     */
    es6: true,
  },
  extends: [
      /*
      引入standard代码规范
       */
    // https://github.com/standard/standard/blob/master/docs/RULES-en.md
    'standard'
  ],
  /*
  扩展或覆盖规则
   */
  rules: {
    // 强制语句结束添加,分号
    semi: ["error", "always"],
    // 强制缩进2个空格
    indent: ["error", 4],
    // 方法名和刮号之间不加空格
    'space-before-function-paren': ['error', 'never'],
    "no-unexpected-multiline": "off"
  }
};

由于这里用到了standard标准,因此还需要安装:

npm install standard --save-dev

配置完成以后,此时我们打包,webpack就会先根据我们配置的规则检查语法,如果语法不正确就会报错,打包失败;除此之外,我们可以配合编码工具来使用eslint,当我们在编码阶段写出语法错误或者不符合eslint规则的代码时,编码工具就会提示错误,这里以webstorm为例,打开setting,找到eslint,按照图上这样设置,就可以开启webstorm的语法规则提示了:

eslint设置

24.配置文件完整代码

webpack.config.js:

const path = require('path'); // 这里需要用到node的path模块
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 引入HtmlWebpackPlugin插件
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); // 引入CleanWebpackPlugin插件
const CopyWebpackPlugin = require('copy-webpack-plugin'); // 引入CopyWebpackPlugin插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 引入MiniCssExtractPlugin插件
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); // 引入OptimizeCSSAssetsPlugin插件
const TerserJSPlugin = require('terser-webpack-plugin'); // 引入TerserJSPlugin插件
const webpack = require('webpack');

module.exports = {
    /*
        mode:指定打包的模式,模式有两种
        开发模式(development):不会对打包的js代码进行压缩
        生产模式(production):会对打包的js代码进行压缩
     */
    mode: 'development', // "production" | "development"

    /*
        entry:指定需要打包的入口文件
     */
    entry: './src/js/index.js',

    /*
        output:指定打包后文件输出的路径和输出文件的名称
     */
    output: {
        /*
            filename: 指定打包之后的JS文件的名称
         */
        filename: 'js/bundle.js',
        /*
            path: 指定打包之后的文件存储到什么地方
            __dirname代表当前这个js文件所在目录的绝对路径
         */
        path: path.resolve(__dirname, 'bundle')
    },

    /*
        watch和devServer功能相似,一般只用devServer就够了
     */
    // watch: true,
    // watchOptions: {
    //     aggregateTimeout: 300, // 防抖,修改完成之后300ms再编译
    //     poll: 1000, // 每隔1000ms检查一次变动
    //     ignored: ['node_modules'], // 排除一些巨大的文件夹,不需要监控的文件夹,例如node_modules
    // },
    devServer: {
        contentBase: './bundle',
        open: true, // 编译打包完以后是否会自动在浏览器打开网页
        port: 9000, // 端口号
        hot: true, // 开启热更新,开启以后就不会自动刷新网页
        hotOnly: true // 哪怕不支持热更新也不要刷新网页
    },
    /*
        optimization: 配置webpack的优化项
     */
    optimization: {
        /*
            OptimizeCSSAssetsPlugin:压缩css
            TerserJSPlugin:压缩js
         */
        minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})]
    },
    /*
        development:cheap-module-eval-source-map,只需要行错误信息, 并且包含第三方模块错误信息, 并且不会生成单独sourcemap文件
        production: cheap-module-source-map,只需要行错误信息, 并且包含第三方模块错误信息, 并且会生成单独sourcemap文件
     */
    devtool: 'cheap-module-eval-source-map',
    /*
        module:告诉webpack如何处理webpack不能够识别的文件
     */
    module: {
        /*
       rules:rules表示规则,rules数组下的一个对象就代表一个规则
        */
        rules: [
            // 打包jpg|png|gif的规则
            {
                test: /\.(jpg|png|gif)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            // 新版本默认使用了esModule语法
                            esModule: false,

                            // 指定打包后文件名称
                            name: '[name].[ext]',

                            // 指定打包后文件存放目录
                            outputPath: 'images/',

                            // 指定托管服务器地址(统一替换图片地址)
                            publicPath: 'http://127.0.0.1:9000/images'
                        }
                    },
                    // 图片的压缩
                    {
                        loader: 'image-webpack-loader',
                        options: {
                            mozjpeg: {
                                progressive: true,
                                quality: 65
                            },
                            optipng: {
                                enabled: false
                            },
                            pngquant: {
                                quality: [0.65, 0.90],
                                speed: 4
                            },
                            gifsicle: {
                                interlaced: false
                            },
                            webp: {
                                quality: 75
                            }
                        }
                    }
                ]
            },
            // 处理css的规则
            {
                test: /\.css$/,
                use: [
                    {
                        // loader: "style-loader", // 原来是将css插入<head>
                        loader: MiniCssExtractPlugin.loader, // 如果配置了MiniCssExtractPlugin插件,就要使用MiniCssExtractPlugin.loader
                        options: {
                            hmr: true // 开启css的热更新
                        }
                    },
                    {
                        loader: 'css-loader',
                        options: {
                            // 开启css的模块化
                            modules: true
                        }
                    },
                    {
                        loader: 'postcss-loader'
                    }
                ]
            },
            // 处理less的规则
            {
                test: /\.less$/,
                use: [
                    {
                        loader: 'style-loader' // 将 JS 字符串生成为 style 节点
                    },
                    {
                        loader: 'css-loader' // 将 CSS 转化成 CommonJS 模块
                    },
                    {
                        loader: 'less-loader' // 将 less 编译成 CSS
                    }
                ]
            },
            // 处理sass的规则
            {
                test: /\.scss$/,
                use: [
                    {
                        loader: 'style-loader' // 将 JS 字符串生成为 style 节点
                    },
                    {
                        loader: 'css-loader' // 将 CSS 转化成 CommonJS 模块
                    },
                    {
                        loader: 'sass-loader' // 将 sass 编译成 CSS
                    }
                ]
            },
            // 处理html
            {
                test: /\.(htm|html)$/i,
                loader: 'html-withimg-loader' // 将html中的图片打包到指定目录
            },
            // 打包字体图标的规则
            {
                test: /\.(eot|json|svg|woff|woff2|ttf)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            // 指定打包后文件名称
                            name: '[name].[ext]',
                            // 指定打包后文件存放目录
                            outputPath: 'font/'
                        }
                    }
                ]
            },
            // 处理js文件
            {
                test: /\.js$/,
                exclude: /node_modules/, // 不处理哪一个文件夹
                use: [
                    {
                        loader: 'babel-loader',
                        options: {
                            presets: [['@babel/preset-env', {
                                // 配置哪些版本的浏览器需要转化
                                targets: {
                                    chrome: '58',
                                    ie: '11'
                                },
                                useBuiltIns: 'usage'
                            }]],
                            plugins: [
                                [
                                    '@babel/plugin-transform-runtime',
                                    {
                                        absoluteRuntime: false,
                                        corejs: 2, // 设置为2,不污染全局变量
                                        helpers: true,
                                        regenerator: true,
                                        useESModules: false,
                                        version: '7.0.0-beta.0'
                                    }
                                ]
                            ]
                        }
                    }
                ]
            },
            // 检查编码规范的规则
            {
                enforce: 'pre', // 当前loader最先执行
                test: /\.js$/,
                exclude: /node_modules/, // 排除不需要检查的目录
                include: path.resolve(__dirname, 'src'),
                loader: 'eslint-loader',
                options: {
                    // eslint options (if necessary)
                    fix: true
                }
            }
        ]
    },
    /*
    告诉webpack需要新增一些什么样的功能
     */
    plugins: [
        // 自动生成html
        new HtmlWebpackPlugin({
            template: './src/index.html'
        }),
        // 清除打包后没有使用的文件
        new CleanWebpackPlugin(),
        // 实现自动拷贝
        new CopyWebpackPlugin([
            { from: './doc', to: 'doc' } // from表示文档原来的路径,to表示需要拷贝到bundle下的路径
        ]),
        // css文件的单独生成、压缩等功能
        new MiniCssExtractPlugin({
            filename: 'css/[name].css'
        }),
        // 热更新
        new webpack.HotModuleReplacementPlugin()
    ]
};

postcss.config.js:

module.exports = {
    plugins: {
        // 自动添加浏览器前缀
        autoprefixer: {
            overrideBrowserslist: [
                'ie >= 8', // 兼容IE7以上浏览器
                'Firefox >= 3.5', // 兼容火狐版本号大于3.5浏览器
                'chrome >=35', // 兼容谷歌版本号大于35浏览器
                'opera >= 11.5' // 兼容欧朋版本号大于11.5浏览器
            ]
        },
        // px转换成rem
        'postcss-pxtorem': {
            rootValue: 100, // 根元素字体大小
            // propList: ['*'], // 可以从px更改到rem的属性,*表示全部
            propList: ['height']
        },
        'postcss-sprites': {
            // 告诉webpack合并之后的图片保存到什么地方
            spritePath: './bundle/images',

            // 告诉webpack合并图片的时候如何分组
            groupBy: function(image) {
                const path = image.url.substr(0, image.url.lastIndexOf('/'));
                const name = path.substr(path.lastIndexOf('/') + 1);
                return Promise.resolve(name);
            },

            // 告诉webpack合并图片的时候如何分组
            filterBy: function(image) {
                const path = image.url;
                // 如果是png结尾的才合并
                if (!/\.png$/.test(path)) {
                    return Promise.reject();
                } else return Promise.resolve();
            }
        }
    }
};

.eslintrc.js:

module.exports = {
    /*
    不重要,永远写true
     */
    root: true,
    parserOptions: {
        // parser: 'babel-eslint',
        /*
        默认设置为 3,5(默认), 你可以使用 6、7、8、9 或 10 来指定你想要使用的 ECMAScript 版本
         */
        "ecmaVersion": 10,
        /*
        设置为 "script" (默认) 或 "module"(如果你的代码是 ECMAScript 模块)。
         */
        "sourceType": "module",
        /*
        ecmaFeatures - 这是个对象,表示你想使用的额外的语言特性:
        globalReturn - 允许在全局作用域下使用 return 语句
        impliedStrict - 启用全局 strict mode (如果 ecmaVersion 是 5 或更高)
        jsx - 启用 JSX
         */
        "ecmaFeatures": {}
    },
    // 指定代码运行的宿主环境
    env: {
        browser: true, // 浏览器
        node: true, // node
        /*
        支持 ES6 语法并不意味着同时支持新的 ES6 全局变量或类型(比如 Set 等新类型)。
        对于 ES6 语法,使用 { "parserOptions": { "ecmaVersion": 6 } }
         */
        es6: true,
    },
    extends: [
        /*
        引入standard代码规范
         */
        // https://github.com/standard/standard/blob/master/docs/RULES-en.md
        'standard'
    ],
    /*
    扩展或覆盖规则
     */
    rules: {
        // 强制语句结束添加,分号
        semi: ["error", "always"],
        // 强制缩进2个空格
        indent: ["error", 4],
        // 方法名和刮号之间不加空格
        'space-before-function-paren': ['error', 'never'],
        "no-unexpected-multiline": "off"
    }
}

小技巧
使用WebStorm的Macros宏指令,实现保存的同时格式化代码,并跳至行尾(简单来说就是每次ctrl+s的时候会保存代码然后自动修复eslint然后光标跳至行尾)

(1).Edit>Macros>Start Macros Recording

Start Macros Recording

(2).开启录制以后会有如下的状态
按下Ctrl+s(保存)然后按下Ctrl+Alt+L(格式化),然后按下end(跳转行尾)然后点击右下角录制按钮结束录制,输入你想要保存的名字。

(3).进入设置页面添加快捷键
按下Ctrl+Alt+S或者File>Settings,在搜索框输入你刚才保存的名字,然后添加快捷键。如果提示警告该快捷键已存在,不用管直接移除,然后apply>OK.

如果这篇文章对你有帮助,请点个赞再走吧