知识大纲:(用于整理大纲思路,分享时可删除)
1、发展概述 > 模块化(ES6 Modul) > ES语法(Babel)> 框架(单文件组件) > Less(预编译) > TypeScript(预编译) > 工程化(引出webpack需要做的事情)
2、核心概念(5个核心概念理解(入口,出口、loader,插件,model))
3、快速上手(JS模块,CSS模块,初识Loader,Plugins,本地服务)
4、拆分配置(base、本地、开发、测试)和 merge、启动本地服务
5、处理loader(重点详解包含CSS、JS、图片& 字体,各种loader的应用场景,结合示例)
6、高级配置(多入口、抽离公共代码,代码压缩,UglifyJsPlugin等等)结合项目中用到的常用插件,进行详细讲解
7、性能优化(babel-loader开启缓存、避免无用引入、懒加载,CDN加速)
8、最佳实践 - 通用模板(一份 Webpack4/5 的通用配置文件模板)

前言

在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。
说到Webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为Webpack复杂的配置和五花八门的功能感到陌生。
当问你是否了解Webpack的时候,或许你可以说出一些的Webpack loader和plugin的名字,甚至还能说出插件和一系列配置做按需加载和打包优化。

那webpack 到底是什么?
简单的说,Webpack 用于编译 JavaScript 模块,它是一个模块打包工具。
所以,我们在系统的学习Webpack之前,需要了解一个很重要的概念,就是模块。
模块经历了几个过程的发展,现在一起来看看前端发展的历程,以及Webpack诞生的整个过程。

发展概述

模块

JS模块化

模块化的开发方式可以提高代码复用率,方便进行代码的管理。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前流行的js模块化规范有、AMD、CMD、ES6模块系统以及CommonJS。

CommonJS

CommonJS是一种使用广泛的JavaScript模块化规范,核心思想是通过require方法来同步加载依赖的其他模块,通过module.exports导出需要暴露的接口。

CommonJS的优点:

  • Node.js是CommonJS规范的主要实践者
  • 通过NPM发布的很多第三方模块都采用了CommonJS规范。 ```javascript // 定义模块math.js var basicNum = 0; function add(a, b) { return a + b; } module.exports = { //在这里写上需要向外暴露的函数、变量 add: add, basicNum: basicNum }

// 引用自定义的模块时,参数包含路径,可省略.js var math = require(‘./math’); math.add(2, 5);

// 引用核心模块时,不需要带路径 var http = require(‘http’); http.createService(…).listen(3000);

  1. CommonJS的缺点在于这样的代码无法直接运行在浏览器环境下,必须通过工具转换成标准的ES5
  2. <a name="V3hdy"></a>
  3. ##### AMD和require.js
  4. AMD也是一种JavaScript模块化规范,与CommonJS最大的不同在于它采用异步的方式加载依赖。AMD规范主要是为了解决针对浏览器环境的模块化问题,最具代表性的实现是`requirejs`
  5. 采用AMD导入和导出时:
  6. ```javascript
  7. //定义一个模块,进入导出
  8. define('module', ['dep'], function (dep) {
  9. return exports;
  10. });
  11. //导入和使用
  12. require(['module'], function (module) {
  13. });

AMD的优点:

  • 可在不转换代码的情况下直接在浏览器中运行
  • 异步加载依赖
  • 并行加载多个依赖
  • 代码可运行在浏览器环境和Node.js环境下

AMD的缺点在于JavaScript运行环境没有原生支持AMD,需要先导入实现了AMD的库后才能正常使用。

CMD和sea.js

CMD也是另一种JS模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。

  1. /** AMD写法 **/
  2. define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
  3. // 等于在最前面声明并初始化了要用到的所有模块
  4. a.doSomething();
  5. if (false) {
  6. // 即便没用到某个模块 b,但 b 还是提前执行了
  7. b.doSomething()
  8. }
  9. });
  10. /** CMD写法 **/
  11. define(function(require, exports, module) {
  12. var a = require('./a'); //在需要时申明
  13. a.doSomething();
  14. if (false) {
  15. var b = require('./b');
  16. b.doSomething();
  17. }
  18. });

ES6模块化

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,ES6逐渐取代CommonJS、AMD规范和CMD规范,成为浏览器和服务器通用的模块解决方案。
采用 ES6 模块化导入及导出时的代码如下:

  1. // 导入
  2. import {readFile} from 'fs';
  3. import React from 'react';
  4. //导出
  5. export function hello() {}
  6. export default {...};

ES6模块虽然是终极模块化方案,但它的缺点在于目前无法直接运行在大部分 JavaScript 运行环境下,必须通过工具转换成标准的 ES5 后才能正常运行。

样式文件中的模块化

除了 JavaScript 开始模块化改造,前端开发里的样式文件也支持模块化。
以LESS为例,把一些常用的样式片段放进一个通用的文件里,再在另一个文件里通过@import导入和使用这些样式片段。

  1. @charset "utf-8";
  2. // util.less文件
  3. // 定义样式片段
  4. @background: {background:red;};
  5. // main.less文件
  6. // 导入和使用util.less中定义的样式片段
  7. @import "util";
  8. #box {
  9. @background();
  10. }

模块化总结

在针对模块介绍中,提到了目前使用前端模块化的一些方案:AMD、CMD、CommonJS、ES6,LESS模块化。
但是在此之前,我们要想进行模块化开发,就必须借助于其他的工具,让我们可以进行模块化开发。
并且在通过模块化开发完成了项目后,还需要处理模块间的各种依赖,并且将其进行整合打包,过程相对繁 琐。

回到主题,webpack的最重要的一个核心理念,一切文件皆模块,并且会帮助我们处理模块间的依赖关系,更专注于帮助我们构建模块化项目。

当然 Webpack 的作用不止加载模块这么简单,前端的常用需求通常都可以实现:利用loader处理vue单文件组件、利用 Loader 转换 ES6 、 Sass/Less 、 Typescript ,还可利用插件。开发多页面应用,等等诸多强大功能,我们接着往下看。

新框架

在 Web 应用变得庞大复杂时,采用直接操作 DOM 的方式去开发将会使代码变得复杂和难以维护, 许多新思想被引入到网页开发中以减少开发难度、提升开发效率。

Vue

Vue 框架把一个组件相关的 HTML 模版、JavaScript 逻辑代码、CSS 样式代码都写在一个文件里,这非常直观。

  1. <!--HTML 模版-->
  2. <template>
  3. <div class="example">{{ msg }}</div>
  4. </template>
  5. <!--JavaScript 组件逻辑-->
  6. <script>
  7. export default {
  8. data () {
  9. return {
  10. msg: 'Hello world!'
  11. }
  12. }
  13. }
  14. </script>
  15. <!--CSS 样式-->
  16. <style>
  17. .example {
  18. font-weight: bold;
  19. }
  20. </style>

但是这种写法无法直接在任何现成的JavaScript引擎中运行,必须经过通过Webpack 配置对应的 vue-loader 处理才行。

React

React框架引入JSX语法到JavaScript中,以更灵活地控制视图的渲染逻辑。

  1. let has = true;
  2. render(has ? <h1>hello,react</h1> : <div>404</div>);

这种语法无法直接在任何现成的JavaScript引擎中运行,必须经过转换。

新语言

JavaScript 最初被设计用于完成一些简单的工作,在用它开发大型应用时一些语言缺陷会暴露出来。 CSS 只能用静态的语法描述元素的样式,无法像写 JavaScript 那样增加逻辑判断与共享变量。 为了解决这些问题,许多新语言诞生了。

ES6

ECMAScript 6.0(简称 ES6)是 JavaScript 语言的下一代标准。它在语言层面为 JavaScript 引入了很多新语法和 API ,使得 JavaScript 语言可以用来编写复杂的大型应用程序。例如:

  • 规范模块化
  • Class语法
  • let声明代码块内有效的变量,用const声明变量
  • 箭头函数
  • async函数
  • SetMap数据结构

通过这些新特性,可以更加高效地编写代码,专注于解决问题本身。但遗憾的是不同浏览器对这些特性的支持不一致,使用了这些特性的代码可能会在部分浏览器下无法运行。为了解决兼容性问题,需要把 ES6 代码转换成 ES5 代码,Babel 是目前解决这个问题最好的工具。 Babel 的插件机制让它可灵活配置,支持把任何新语法转换成 ES5 的写法。

TypeScript

TypeScript多用于开发大型项目,缺点是无法直接运行在浏览器或Node.js环境,需要编译器或babel转化为JavaScript代码,运行到浏览器上。

LESS/SCSS

LESS和SCSS都是一种CSS预处理器,基本思想是用和CSS相似的编程语言写完后再编译成正常的CSS文件。

  1. @blue: #1875e7;
  2. div {
  3. color: @blue;
  4. }
  5. // 编译后
  6. div {
  7. color: #1875e7;
  8. }

构建工具/工程化

当我们对前端发展的过程有初步了解后,你一定会感叹前端技术发展之快,各种可以提高开发效率的新思想和框架被发明。
但是这些东西都有一个共同点:源代码无法直接运行,必须通过转换后才可以正常运行。
构建工具就是用来这件事情,把源代码转换成发布到线上的可执行 JavaScrip、CSS、HTML 代码,包括如下内容。

  • 代码转换:TypeScript编译成JavaScript、LESS编译成CSS等
  • 文件优化:压缩JavaScript、CSS、HTML代码,压缩合并图片等
  • 代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载
  • 模块合并:在采用模块化的项目里会有很多个模块和文件,需要构建功能把模块分类合并成一个文件
  • 自动刷新:监听本地源代码的变化,自动重新构建、刷新浏览器
  • 代码校验:在代码被提交到仓库前需要校验代码是否符合规范,以及单元测试是否通过
  • 自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统

构建其实是工程化、自动化思想在前端开发中的体现,把一系列流程用代码去实现,让代码自动化地执行这一系列复杂的流程。 构建给前端开发注入了更大的活力,解放了我们的生产力。

历史上先后出现一系列构建工具,它们各有其优缺点。由于前端工程师都比较熟悉 JavaScript ,Node.js 又可以胜任所有构建需求,所以大多数构建工具都是基于 Node.js 开发的。我们接下来会一一介绍它们。

Grunt

Grunt 是一个任务执行者。Grunt 有大量现成的插件封装了常见的任务,也能管理任务之间的依赖关系,自动化执行依赖的任务,每个任务的具体执行代码和依赖关系写在配置文件 Gruntfile.js 里,例如:

  1. module.exports = function (grunt) {
  2. // 所有插件的配置信息
  3. grunt.initConfig({
  4. // uglify 插件的配置信息
  5. uglify: {
  6. app_task: {
  7. files: {
  8. 'build/app.min.js': ['lib/index.js', 'lib/test.js']
  9. }
  10. }
  11. },
  12. watch: {
  13. another: {
  14. files: ['lib/*.js']
  15. }
  16. }
  17. });
  18. grunt.loadNpmTasks('grunt-contrib-uglify');
  19. grunt.loadNpmTasks('grunt-contrib-watch');
  20. };

在项目根目录下执行命令 grunt dev 就会启动 JavaScript 文件压缩和自动刷新功能。
Grunt的优点是:

  • 灵活,它只负责执行你定义的任务;
  • 大量的可复用插件封装好了常见的构建任务。

Grunt的缺点是集成度不高,要写很多配置后才可以用,无法做到开箱即用。

Gulp

Gulp是一个基于流的自动化构建工具。除了可以管理和执行任务,还支持监听文件、读写文件。
Gulp 设计得非常简单,通过下面5个方法就可以胜任几乎所有构建场景:

  • 通过gulp.task注册一个任务;
  • 通过gulp.run执行任务;
  • 通过gulp.watch监听文件变化;
  • 通过gulp.src读取文件;
  • 通过gulp.dest写文件。

Gulp的最大特点是引入了流的概念,同时提供了一系列常用的插件去处理流,流可以在插件之间传递。

  1. //引入Gulp
  2. var gulp = require('gulp');
  3. //引入插件
  4. var jshint = require('gulp-jshint');
  5. var sass = require('gulp-sass');
  6. var concat = require('gulp-concat');
  7. var uglify = require('gulp-uglify');
  8. //编译SCSS任务
  9. gulp.task('sass', function () { //读取文件通过管道传给插件
  10. gulp.src('./scss/*.scss') //SCSS插件把 scss 文件编译成 CSS 文件
  11. .pipe(sass())
  12. .pipe(gulp.dest('./css'));//输出文件
  13. });
  14. //合并压缩JS
  15. gulp.task('scripts', function () {
  16. gulp.src('./js/*.js')
  17. .pipe(concat('all.js'))
  18. .pipe(uglify())
  19. .pipe(gulp.dest('./dist'));
  20. });
  21. //监听文件变化
  22. gulp.task('watch', function () {
  23. // 当scss文件被编辑时执行SCSS任务
  24. gulp.watch('./scss/*.scss', ['sass']);
  25. gulp.watch('./js/*.js', ['scripts'])
  26. });

Gulp的优点是好用又不失灵活,既可以单独完成构建也可以和其他工具搭配使用。其缺点是和Grunt类似,集成度不高,要写很多配置后才可以使用,无法做到开箱即用。

Webpack

Webpack是一个打包模块化JavaScript工具,在Webpack里一切文件皆模块,通过Loader转文件,通过Plugin注入钩子,最后输出由多个模块组合成的文件。
Webpack帮你获得一些准备用于部署的 js 和 css 等,把它们转化为适合浏览器的可用的格式。
Webpack通过压缩、分离、懒加载等,来提升性能,提高开发效率。

一张官方图来了解Webpack
image.png

一切文件:JavaScript、CSS、SCSS、图片、模板,在 Webpack 眼中都是一个个模块,这样的好处是能清晰的描述出各个模块之间的依赖关系,以方便 Webpack 对模块进行组合和打包。 经过 Webpack 的处理,最终会输出浏览器能使用的静态资源。

Webpack的优点:

  • 专注于处理模块化的项目,能做到开箱即用
  • 通过Plugin扩展,完整好用又不失灵活
  • 使用场景不仅限于Web开发
  • 社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展;
  • 良好的开发体验

为什么学习Webpack?

我们来一个小章总结,用最简述的语言说一说为什么要学习 Webpack?

早期,在浏览器里运行 js,有二种方式:

  1. 直接引用 js 脚本程序,有多少个 js,就引用多少个 .js 文件;
  2. 直接一个大的 .js 文件,包含所有的 js 代码,但是文件大小体积就不可控。

后来,出现了使用立即执行函数表达式(IIFE)的方式,这种方式主要是用来解决大型项目的作用域的问题。针对这种做法,有一些工具 grunt、gulp,它们都是任务执行器,更多做的是项目文件的拼接。这类工具优化代码的能力比较弱,很难判断某个 js 方法是否被重复的引用,或是否未被引用。
Node.js 出来后,就出现了 JavaScript 的模块化开发。主要是引入了 require 机制,允许你在当前文件中加载和使用某个模块。
Webpack 最出色的功能,是它还可以引入任何其它类型的文件,包括非 js 类型的文件,可以用来引用应用程序中的所有的非 js 的内容,例如图片、css 等。Webpack 把这些都视为模块,这样每个模块都可以通过相互的引用(依赖)来表明它们之间的关系,就可以避免打包未使用的模块(资源)。
这就是 Webpack 存在的原因,也是学习 Webpack 的原因。

五大核心概念

学习使用Webpack之前,我们先重点理解一下Webpack中的五大核心概念。

Entry [ˈentri]

入口(Entry)是打包时,第一个被访问的源码文件。默认是 src/index.js (可以通过配置文件指定)。
Webpack 通过入口,加载整个项目的依赖。


image.png

(Webpack 入口)

Output

出口(Output)是打包后,输出的文件名称,默认是 dist/main.js(可以通过配置文件指定)。
如下图,出口是 dist/main.js:


image.png

(Webpack 出口)

Loader

加载器(Loader)是专门用来处理那些非 JavaScript 文件的工具(Webpack 默认只能识别 JavaScript),将这些资源翻译成 Webpack 能识别的资源。
Loader 的命名方式一般为 xxx-loader(css-loader | html-loader | file-loader),它们都是以 -loader 为后缀的 npm 包。
Loader 加载的基本逻辑:
image.png
(Webpack loader 加载的基本逻辑)

打包时,我们也可以将不同类型的文件,单独打包。

Plugins

插件(Plugins)用于实现 Loader 之外的其他功能,包括但不限于打包优化和压缩,重新定义环境中的变量等。Plugin 是 Webpack 的支柱,用来实现丰富的功能。
Plugins 的命名方式一般为 xxx-webpack-plugin(html-webpack-plugin),它们都是以 -webpack-plugin 为后缀的 npm 包,常用插件

Mode

模式(Mode)是用来区分环境的关键字,不同环境的打包逻辑不同,因此需要区分。
Mode 有三种固定的写法(名称固定,不能改):

  • development(开发环境:自动优化打包速度,添加一些调试过程中的辅助)
  • production(生产环境:自动优化打包结果)
  • none(运行最原始的打包,不做任何额外处理)

通过 process.env.NODE_ENV 可以获得当前的 Mode。

module chunk bundle 的区别

  • module:各个源码文件,Webpack 中一切皆模块。
  • chunk:多模块合并成的,如 entry、import() 异步加载的时候也生成一个 chunk、splitChunk 拆分代码块的时候定义 chunk。
  • bundle:最终的输出文件,由 chunk 构建分析完后输出。


image.png
(module chunk bundle)

快速上手

安装Node.js

在安装 Webpack 前请确保你的电脑已经安装了Node.js。

初始化一个项目

我们在电脑上找个地方,建一个文件夹叫 webpack-simple,然后在这里面完成一个简单的SPA(Single-page applications)应用。
打开命令行窗口,cd 到刚才建的 simple 目录。然后执行这个命令初始化项目:

  1. cd webpack-simple
  2. npm init

命令行会要你输入一些配置信息,我们这里一路按回车下去。

  1. package name: (webpack-simple)
  2. version: (1.0.0)
  3. description:
  4. entry point: (index.js)
  5. test command:
  6. git repository:
  7. keywords:
  8. author:
  9. license: (ISC)
  10. About to write to /Users/maizi/webpack-simple/package.json:
  11. {
  12. "name": "webpack-simple",
  13. "version": "1.0.0",
  14. "description": "",
  15. "main": "index.js",
  16. "scripts": {
  17. "test": "echo \"Error: no test specified\" && exit 1"
  18. },
  19. "author": "",
  20. "license": "ISC"
  21. }
  22. Is this OK? (yes)
  23. maizi@Mac-mini-M1 webpack-simple %

会生成一个默认的项目配置文件 package.json。
目录结果如下:

  1. └── package.json 项目配置信息

完善一些页面

我们写一个最简单的 SPA 应用来介绍 SPA 应用的内部工作原理。首先,建立 index.html 文件,内容如下:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title></title>
  7. </head>
  8. <body>
  9. <div id="app"></div>
  10. <!--导入 Webpack 输出的 JavaScript 文件-->
  11. <script src="./dist/bundle.js"></script>
  12. </body>
  13. </html>

它是一个HTML页面,通过 JavaScript 在网页中显示 今天是:2022年7月21日
注意在Webpack构建前,需要引入 Webpack 输出的 JavaScript 文件 <script src="./dist/bundle.js"></script>

index.js:

  1. // 创建 DOM 节点
  2. const titleTag = document.createElement('h1')
  3. titleTag.textContent = '今天是:2022年7月21日'
  4. const app = document.querySelector('#app')
  5. app.append(titleTag)

当前目录结构如下:

  1. ├── package.json 项目配置信息
  2. ├── src 我们的源代码
  3. ├── index.html 入口 html
  4. └── index.js 入口 js


页面代码这样就差不多搞定了,接下来我们进入 webpack 的安装和配置阶段。现在我们还没有讲 webpack 配置所以页面还无法访问,等会弄好 webpack 配置后再看页面实际效果。

安装 webpack

我们把 webpack 安装到项目:

  1. # npm i -D 是 npm install --save-dev 的简写,是指安装模块并保存到 package.json 的 devDependencies
  2. # 安装最新稳定版
  3. npm i -D webpack
  4. # 安装指定版本
  5. npm i -D webpack@<version>
  6. # 安装最新体验版本
  7. npm i -D webpack@beta

配置 webpack

当完成一些页面以及安装好Webpack后,接下来总算可以进入正题了。我们来创建 webpack 配置文件 webpack.config.js,注意这个文件是在 node.js 中运行的,因此不支持 ES6 的 import 语法,我们来看文件内容:

  1. const path = require('path');
  2. module.exports = {
  3. // JavaScript 执行入口文件
  4. entry: './index.js',
  5. output: {
  6. // 把所有依赖的模块合并输出到一个 bundle.js 文件
  7. filename: 'bundle.js',
  8. // 输出文件都放到 dist 目录下
  9. path: path.resolve(__dirname, './dist'),
  10. }
  11. };

最后需要通过 CommonJS 规范导出一个构建的 Object 对象。

此时项目目录如下:

  1. ├── package.json 项目配置信息
  2. ├── index.html 入口 html
  3. ├── index.js 入口 js
  4. └── webpack.config.js webpack 配置文件

一切文件就绪,在项目根目录下执行 webpack 命令运行 Webpack 构建

  1. webpack --config webpack.config.js

你会发现目录下多出一个 dist 目录,里面有个 bundle.js 文件, bundle.js 文件是一个可执行的 JavaScript 文件,它包含页面所依赖的脚本模块 index.js 和 webpackBootstrap 启动函数。
这时你用浏览器打开 index.html 网页将会看到 今天是:2022年7月21日

image.png

初识 Loader

上面使用 Webpack 快速构建了一个采用 CommonJS 规范的模块化项目,现在我们继续优化这个网页,编写CSS 代码让标题颜色变为红色。
我们已经学习CSS的预编译语言LESS,这里直接使用LESS文件。
index.less:

  1. @red:red;
  2. h1, h2 {
  3. color:@red
  4. }


Webpack 会把一切文件看作模块,CSS/LESS 文件也不例外,在入口文件index.js直接引入 less.css

  1. // 通过 CommonJS 规范导入 CSS 模块
  2. require('./index.less');
  3. // 创建 DOM 节点
  4. const titleTag = document.createElement('h1')
  5. titleTag.textContent = '今天是:2022年7月21日'
  6. const app = document.querySelector('#app')
  7. app.append(titleTag)

好的,现在执行一个Webpack操作 webpack --config webpack.config.js

  1. ERROR in ./index.less 1:0
  2. Module parse failed: Unexpected character '@' (1:0)
  3. You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
  4. > @red:'red';
  5. | h1, h2 {
  6. | color:@red
  7. @ ./index.js 2:0-23
  8. webpack 5.73.0 compiled with 1 error and 1 warning in 1222 ms

执行后,会发现 Webpack 会报错,因为 Webpack 本身不支持解析 CSS 文件,更加不支持LESS文件。如果要支持非 JavaScript 类型的文件,需要使用 Webpack 的 Loader 机制。
我们现在修改webpack.config.js的配置:

  1. const path = require('path');
  2. module.exports = {
  3. // JavaScript 执行入口文件
  4. entry: './index.js',
  5. output: {
  6. // 把所有依赖的模块合并输出到一个 bundle.js 文件
  7. filename: 'bundle.js',
  8. // 输出文件都放到 dist 目录下
  9. path: path.resolve(__dirname, './dist'),
  10. },
  11. module: {
  12. rules: [
  13. {
  14. // 增加 'less-loader',注意顺序
  15. test: /\.(css|less)$/,
  16. use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader']
  17. }
  18. ]
  19. }
  20. };

Loader 可以看作具有文件转换功能的翻译员,配置里的 module.rules 数组配置了一组规则,数组执行顺序是从后往前执行,告诉 Webpack 在遇到哪些文件时使用哪些 Loader 去加载和转换。

如上面的配置告诉 Webpack 在遇到以 .css 或者.less 结尾的文件时先使用 css-loader 读取 CSS 文件,再交给 style-loader 把 CSS 内容注入到 JavaScript 里。

在处理样式的配置中,除了 CSS,同理还有 Less,Sass 等,都是差不多的思路(解析 Less 语法 => 解析 CSS 语法 => 转成 style)
在配置 Loader 时需要注意的是:

  • use 属性的值需要是一个由 Loader 名称组成的数组,Loader 的执行顺序是由后到前的;
  • 每一个 Loader 都可以通过 URL querystring 的方式传入参数,例如 css-loader?minimize 中的 minimize 告诉 css-loader 要开启 CSS 压缩。

一定要记得,我们配置 Loader 后,还需要安装对应的Loader

  1. npm i -D style-loader css-loader postcss-loader less-loader
  • less-loader: 用于处理编译 .less 文件,将其转为 css文件代码
  • postcss-loader:CSS 语法识别,处理浏览器兼容性
  • css-loader:将 .css 结尾的文件解析为 CSS(webpack 中一切皆模块,它不认识 .css 文件和 .js 文件的区别)
  • style-loader:将解析完的 CSS 插入到页面中(style 标签中)

这里划重点了哦,使用 less-loader 的话,必须安装 less,单独一个 less-loader 是没办法正常使用的。
所以,我们安装上面Loader 以后,还需要 安装 Less

  1. npm i -D less

安装成功后重新执行构建时,你会发现 bundle.js 文件被更新了,里面注入了在 index.less 中写的 CSS。
重新刷新 index.html 页面将会发现 2022年7月21日 变成红色了!

image.png

初识 Plugin [plʌgɪn]

Plugin 用于实现 Loader 之外的其他功能,Plugin 是 Webpack 的支柱,很多丰富的功能都由它来实现。
Plugin 和 Loader 的区别是,Loader 是在 import 时根据不同的文件名,匹配不同的 Loader 对这个文件做处理,
而 Plugin, 关注的不是文件的格式,而是编译的各个阶段,触发不同的事件,这个时候都可以使用Plugin来处理你想要的操作。

我们继续结合之前的代码,提出一些需求,继续往下看。

还记得我们在快速上手里面创建了一个入口页面 index.html,我们手动引入了Webpack打包编译后的文件,代码如下:
<script src="./dist/bundle.js"></script>

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title></title>
  7. </head>
  8. <body>
  9. <div id="app"></div>
  10. <!--导入 Webpack 输出的 JavaScript 文件-->
  11. <script src="./dist/bundle.js"></script>
  12. </body>
  13. </html>

试想如果有一个插件,可以自动引入打包后的所有资源(JS/CSS),那岂不是更加方便。
我们现在修改一下代码,找到index.html移除手动引入的js资源的代码
inde.html

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title></title>
  7. </head>
  8. <body>
  9. <div id="app"></div>
  10. </body>
  11. </html>

这里我们使用HtmlWebpackPlugin,直接修改配置文件
webpack.config.js

  1. const path = require('path');
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. module.exports = {
  4. // JavaScript 执行入口文件
  5. entry: './index.js',
  6. output: {
  7. // 把所有依赖的模块合并输出到一个 bundle.js 文件
  8. filename: 'bundle.js',
  9. // 输出文件都放到 dist 目录下
  10. path: path.resolve(__dirname, './dist'),
  11. },
  12. module: {
  13. rules: [
  14. {
  15. // 增加 'less-loader',注意顺序
  16. test: /\.(css|less)$/,
  17. use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader']
  18. }
  19. ]
  20. },
  21. plugins: [
  22. // 根据模板生成 HTML 文件
  23. new HtmlWebpackPlugin({
  24. template: './index.html',
  25. minify:false // 不开启自动压缩
  26. })
  27. ],
  28. };

这个插件的作用是,在输出的html文件中,通过插件中配置的 template 属性,复制 template 属性值所对应的 html 文件,并且自动引入打包好的所有资源(JS/CSS),然后把输出文件放在 dist/index.html 下面。

注意:
如果让新配置的插件代码运行起来,还是需要先安装引入的插件:

  1. npm i -D html-webpack-plugin

安装成功后重新执行构建时

  1. webpack --config webpack.config.js

这时会发现目录结果发生了变化!

此时项目目录如下:

  1. ├── dist 打包输出目录,只需部署这个目录到生产环境
  2. ├── bundle.js 公共函数库
  3. └── index.html 编译后的入口页面
  4. ├── package.json 项目配置信息
  5. ├── index.html 入口 html
  6. ├── index.js 入口 js
  7. └── webpack.config.js webpack 配置文件

当我们再打开 dist/index.html ,已经自动帮我们引入了打包后的资源。

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title></title>
  7. <script defer src="bundle.js"></script></head>
  8. <body>
  9. <div id="app"></div>
  10. </body>
  11. </html>

总结一下:
html-webpack-plugin 是用来打包入口 html 文件。
entry 配置的入口是 js 文件,webpack 以 js 文件为入口,遇到 import, 用配置的 loader 加载引入文件
但作为浏览器打开的入口 html, 是引用入口 js 的文件,它在整个编译过程的外面, 所以,我们需要 html-webpack-plugin 来打包作为入口的 html 文件。

接下来我们优化一些问题,继续深入一下配置。

还记得我们刚刚在上面使用Loader加载了Less文件。一起再来回顾一下。

  1. // 通过 CommonJS 规范导入 CSS 模块
  2. require('./index.less');
  3. // 创建 DOM 节点
  4. const titleTag = document.createElement('h1')
  5. titleTag.textContent = '今天是:2022年7月21日'
  6. const app = document.querySelector('#app')
  7. app.append(titleTag)

但是Webpack打包后,直接将CSS资源直接添加到了页面的<head></head>标签。
具体效果如下:
image.png

如果我们想提取 CSS 资源,最终让这些资源在 HTML 文件 元素中的以 标签的方式引入。
直接修改配置文件
webpack.config.js

  1. const path = require('path');
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. const MiniCssExtractPlugin = require("mini-css-extract-plugin");
  4. module.exports = {
  5. // JavaScript 执行入口文件
  6. entry: './index.js',
  7. output: {
  8. // 把所有依赖的模块合并输出到一个 bundle.js 文件
  9. filename: 'bundle.js',
  10. // 输出文件都放到 dist 目录下
  11. path: path.resolve(__dirname, './dist'),
  12. },
  13. module: {
  14. rules: [
  15. {
  16. // 将style-loader替换成MiniCssExtractPlugin.loader
  17. test: /\.(css|less)$/,
  18. use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader']
  19. }
  20. ]
  21. },
  22. plugins: [
  23. // 根据模板生成 HTML 文件
  24. new HtmlWebpackPlugin({
  25. template: './index.html',
  26. minify:false // 不开启自动压缩
  27. }),
  28. //
  29. new MiniCssExtractPlugin({
  30. filename: 'css/[name].[contenthash:7].css',
  31. }),
  32. ],
  33. };

配置好以后,记得安装新引入的插件:

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

安装成功后重新执行构建,你会发现 元素中的样式,已经变成link标签的方式引入到页面上了。
编译后的页面:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title></title>
  7. <script defer src="bundle.js"></script>
  8. <link href="css/main.e079919.css" rel="stylesheet"></head>
  9. <body>
  10. <div id="app"></div>
  11. </body>
  12. </html>

页面刷新后:
image.png

启动本地服务

通过上面的快速入门及 Loader及 Plugin的使用,我们基于 Webpack 工程化已经正常运行起来了。
回想上面的示例,我们每次修改源文件都需要再次打包,这种方式无疑对于开发者来说是有点不方便的,实际开发中你可能会需要:

  1. 提供 HTTP 服务,用于服务网页请求;
  2. 监听文件的变化并自动编译,自动刷新网页,做到实时预览;
  3. 支持 Source Map,以方便后续调试。

我们现在继续为之前的项目集成DevServer,具体如何使用:
0、在安装DevServer之前,需要具备以下环境要求:

  1. node >= v12.13.0
  2. webpack >= v4.37.0 #推荐使用 webpack >= v5.0.0
  3. webpack-cli >= v4.7.0

1、确认具备以上要求后,安装 DevServer

  1. npm i -D webpack-dev-server

2、配置webpack.config.js文件

  1. const path = require('path');
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. const MiniCssExtractPlugin = require("mini-css-extract-plugin");
  4. module.exports = {
  5. // JavaScript 执行入口文件
  6. entry: './index.js',
  7. output: {
  8. // 把所有依赖的模块合并输出到一个 bundle.js 文件
  9. filename: 'bundle.js',
  10. // 输出文件都放到 dist 目录下
  11. path: path.resolve(__dirname, './dist'),
  12. },
  13. module: {
  14. rules: [
  15. {
  16. // 增加 'less-loader',注意顺序
  17. test: /\.(css|less)$/,
  18. use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader']
  19. }
  20. ]
  21. },
  22. plugins: [
  23. // 根据模板生成 HTML 文件
  24. new HtmlWebpackPlugin({
  25. template: './index.html',
  26. minify:false // 不开启自动压缩
  27. }),
  28. // 将 CSS 提取到单独的文件中
  29. new MiniCssExtractPlugin({
  30. filename: 'css/[name].[contenthash:7].css',
  31. }),
  32. ],
  33. devServer: {
  34. open: true, // 自动打开浏览器
  35. compress: true, // 启动 gzip 压缩
  36. port: 8888,
  37. },
  38. };

3、修改package.json文件
在package.json文件的scripts节点中添加
“start”节点

  1. {
  2. "name": "webpack-simple",
  3. "version": "1.0.0",
  4. "description": "",
  5. "main": "index.js",
  6. "scripts": {
  7. "test": "echo \"Error: no test specified\" && exit 1",
  8. "start": "webpack serve --open"
  9. },
  10. "author": "",
  11. "license": "ISC",
  12. "devDependencies": {
  13. "css-loader": "^6.7.1",
  14. "html-webpack-plugin": "^5.5.0",
  15. "less": "^4.1.3",
  16. "less-loader": "^11.0.0",
  17. "mini-css-extract-plugin": "^2.6.1",
  18. "postcss-loader": "^7.0.1",
  19. "style-loader": "^3.3.1",
  20. "webpack": "^5.73.0",
  21. "webpack-cli": "^4.10.0",
  22. "webpack-dev-server": "^4.9.3"
  23. }
  24. }

4、执行命令npm start

  1. npm start

这时浏览器会自动打开一个网址
http://localhost:8888/
但是你会发现网页上有一行错误信息

  1. WARNING
  2. configuration
  3. The 'mode' option has not been set, webpack will fallback to 'production' for this value.
  4. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
  5. You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

这个出现这个warning问题的原因是在启动webpack的时候没有定义环境,我们只需要在在webpack.config.js文件中添加mode属性即可.

  1. const path = require('path');
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. const MiniCssExtractPlugin = require("mini-css-extract-plugin");
  4. module.exports = {
  5. mode: 'development', // development为开发者环境,production为生产环境变量 ,还有一个为none
  6. // JavaScript 执行入口文件
  7. entry: './index.js',
  8. output: {
  9. // 把所有依赖的模块合并输出到一个 bundle.js 文件
  10. filename: 'bundle.js',
  11. // 输出文件都放到 dist 目录下
  12. path: path.resolve(__dirname, './dist'),
  13. },
  14. module: {
  15. rules: [
  16. {
  17. // 增加 'less-loader',注意顺序
  18. test: /\.(css|less)$/,
  19. use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader']
  20. }
  21. ]
  22. },
  23. plugins: [
  24. // 根据模板生成 HTML 文件
  25. new HtmlWebpackPlugin({
  26. template: './index.html',
  27. minify:false // 不开启自动压缩
  28. }),
  29. // CSS 提取到单独的文件中
  30. new MiniCssExtractPlugin({
  31. filename: 'css/[name].[contenthash:7].css',
  32. }),
  33. ],
  34. devServer: {
  35. open: true, // 自动打开浏览器
  36. compress: true, // 启动 gzip 压缩
  37. port: 8888,
  38. },
  39. };

现在重新执行

  1. npm start

这时浏览器会自动打开一个网址
http://localhost:8888/
打开页面,效果如下:
image.png
当我们修改 index.js 文件中的内容时:

  1. // 通过 CommonJS 规范导入 CSS 模块
  2. require('./index.less');
  3. // 创建 DOM 节点
  4. const titleTag = document.createElement('h1')
  5. titleTag.textContent = '今天是:2022年7月24日'
  6. const app = document.querySelector('#app')
  7. app.append(titleTag)

页面会自动刷新,效果如下:
image.png
发现日期已经从 2022年7月21日变成了 2022年7月24日

我们现在试着修改一个LESS样式,看看是否会有变动呢?
修改 index.less,我们定一个新的@blue变量,并且应用到标签上。

  1. @red:red;
  2. @blue:blue;
  3. h1, h2 {
  4. color:@blue
  5. }

我们打开查看页面,发现页面上的文字并没有变成蓝色。
效果如下:
image.png

但是,当我们进行手动刷新时,页面的文字变成了蓝色。

image.png

这个问题是因为我们使用了mini-css-extract-plugin插件,它可以把CSS样式从JS文件中提取到单独的CSS文件中,但是CSS是单独缓存的,并不支持热替换,也就是当我们修改样式后,并不会自动应用到页面上,只能靠手动刷新。

所以为了更加方便的使用,我们一般在开发环境中并不进行样式提取,可以更加的方便页面的实时编译刷新,只有在生产环境中将页面中样式提取到单独文件,并且可以进行压缩等一些操作。

为什么要使用Webpack?
相信大家通过快速入门的示例讲解,已经对于这个工具都有了一些初步的认识。
我们通过编写JS模块,CSS模块,使用Loader进行处理,使用Plugins创建HTML并自动引入打包后的入口脚本,以及提取页面中的CSS进行单独引入等。我们还使用DevServer本地服务,可以做到页面的实时刷新等。

快速入门中的示例已经做了整理,感兴趣的可以直接下载运行 webpack-simple

事实上,Webpack能做的远不止这些,接下来我们会结合实际的项目场景,介绍一些最实用的配置。通过这些配置,我们会整理出一个开箱即用的Webpack模版。

以下内容是存粹的干货,一起来看。

Webpack 最佳实践

拆分配置和 merge

一般情况下我们刚接触Webpack,都会在项目中新建一个 webpack.config.js 文件,比如我们快速入门的示例中,就是这种传统方式,将所有配置汇总在一起,但是这种方式是极其不方便的。
但是从项目实际的角度考虑,我们会拆分配置,我们会将它分为三个文件:

  • webpack.common.js
  • webpack.dev.js
  • webpack.prod.js

与此同时修改 package.json 中的命令语句:

  1. "scripts": {
  2. "test": "echo \"Error: no test specified\" && exit 1",
  3. "dev-without-dev-server": "webpack --config build/webpack.dev.js",
  4. "dev": "webpack serve --config build/webpack.dev.js",
  5. "build": "webpack --config build/webpack.prod.js"
  6. }

webpack.common.js 基础配置代码:

  1. const path = require('path')
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. const { srcPath, distPath } = require('./paths')
  4. module.exports = {
  5. entry: path.join(srcPath, 'index'),
  6. plugins: [
  7. new HtmlWebpackPlugin({
  8. template: path.join(srcPath, 'index.html'),
  9. filename: 'index.html'
  10. })
  11. ],
  12. }

在 dev(开发环境配置)和 prod(生产环境配置)中分别都通过 merge(记得需要安装 webpack-merge 这个依赖) 将 common(公共配置)引进来。
webpack.dev.js 基础配置代码:

  1. const path = require('path')
  2. const webpack = require('webpack')
  3. const webpackCommonConf = require('./webpack.common.js')
  4. const { merge } = require('webpack-merge')
  5. const { srcPath, distPath } = require('./paths')
  6. module.exports = merge(webpackCommonConf, {
  7. mode: 'development',
  8. devServer: {
  9. // 这部分配置项省略,后面会单独详细讲……
  10. },
  11. plugins: [
  12. new webpack.DefinePlugin({
  13. // window.ENV = 'development'
  14. ENV: JSON.stringify('development')
  15. })
  16. ]
  17. })

webpack.prod.js 基础配置代码:

  1. const path = require('path')
  2. const webpack = require('webpack')
  3. const { CleanWebpackPlugin } = require('clean-webpack-plugin')
  4. const webpackCommonConf = require('./webpack.common.js')
  5. const { merge } = require('webpack-merge')
  6. const { srcPath, distPath } = require('./paths')
  7. module.exports = merge(webpackCommonConf, {
  8. mode: 'production',
  9. output: {
  10. filename: 'bundle.[contenthash:8].js', // 打包代码时,加上 hash 戳
  11. path: distPath,
  12. // publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
  13. },
  14. plugins: [
  15. new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
  16. new webpack.DefinePlugin({
  17. // window.ENV = 'production'
  18. ENV: JSON.stringify('production') // 这个插件是用来定义全局变量的
  19. })
  20. ]
  21. })

可以看到三份配置文件都引入了 paths.js,这是一个通用的 JS 文件,如果我们的项目中文件特别多,引入路径的时候会特别的混乱,所以这里我们把入口和出口的文件路径统一进行配置,它里面的完整代码是这样的:

  1. /**
  2. * @description 常用文件夹路径
  3. * @author wenyuan
  4. */
  5. const path = require('path')
  6. const srcPath = path.join(__dirname, '..', 'src')
  7. const distPath = path.join(__dirname, '..', 'dist')
  8. module.exports = {
  9. srcPath,
  10. distPath
  11. }

下面是拆分配置后的目录结构。

  1. ├── src/ # 项目源代码
  2. ├── paths.js # 常用文件夹路径,返回目录变量给其它文件用
  3. ├── webpack.common.js # 公共配置
  4. ├── webpack.dev.js # 开发环境配置
  5. ├── webpack.prod.js # 生产环境配置
  6. └── .babelrc # babel 配置

为了更加的清晰,我们把拆分的配置工具,统一放到build文件夹下。
更加完整的目录结果如下:

  1. └── build # Webpack 配置文件目录
  2. ├── paths.js # 常用文件夹路径,返回目录变量给其它文件用
  3. ├── webpack.common.js # 公共配置
  4. ├── webpack.dev.js # 开发环境配置
  5. └── webpack.prod.js # 生产环境配置
  6. ├── dist # 打包输出目录
  7. ├── src # 项目源代码
  8. ├── index.html
  9. └── index.js
  10. ├── package.json # 项目配置信息
  11. └── .babelrc # babel 配置

启动本地服务

关于本地服务的作用我们在快速上手的示例中已经讲过,它可以帮助我们启动一个HTTP服务,帮我们进行自动编译模块,刷新页面等等。
但是这个功能只需要在 dev 开发环境下使用,结合我们拆分后的环境配置,借助了 webpack-dev-server 这个依赖,我们将配置写在 webpack.dev.js 中:

  1. const path = require('path')
  2. const webpack = require('webpack')
  3. const webpackCommonConf = require('./webpack.common.js')
  4. const { merge } = require('webpack-merge')
  5. const { srcPath, distPath } = require('./paths')
  6. module.exports = merge(webpackCommonConf, {
  7. mode: 'development',
  8. devServer: {
  9. historyApiFallback: true, // 前端路由配置为 history 模式时用
  10. contentBase: distPath, // 根目录
  11. open: true, // 自动打开浏览器
  12. compress: true, // 启动 gzip 压缩
  13. hot: true, // 热更新
  14. port: 8080, // 启动端口
  15. // 设置代理,解决跨域访问 —— 如果有需要的话
  16. proxy: {
  17. // 将本地 /api/xxx 代理到 localhost:3000/api/xxx
  18. '/api': 'http://localhost:3000',
  19. // 将本地 /api2/xxx 代理到 localhost:3000/xxx
  20. '/api2': {
  21. target: 'http://localhost:3000',
  22. pathRewrite: {
  23. '/api2': ''
  24. }
  25. }
  26. }
  27. }
  28. })

我们在真实的项目开发中,和后端进行数据联调时,经常会存在跨域的问题,我们通过devServer提供的proxy可以进行设置代理,解决跨域访问。

Loader 配置

处理 ES6

由于我们在项目中使用了ES6最新的语言特性,所以处理 ES6 是最通用的功能,借助了 babel-loader 这个依赖,配置写在 webpack.common.js 中:

  1. const path = require('path')
  2. const { srcPath, distPath } = require('./paths')
  3. module.exports = {
  4. entry: path.join(srcPath, 'index'),
  5. module: {
  6. rules: [
  7. {
  8. test: /\.js$/,
  9. use: ['babel-loader'],
  10. include: srcPath,
  11. exclude: /node_modules/
  12. }
  13. ]
  14. }
  15. }

由于 babel-loader 使用到了 babel,因此还需要配置 .babelrc。一般简单配置就已经包含了 ES6、7、8 常用语法,特殊情况再配置 plugins。
所以常用配置如下:

  1. {
  2. "presets": ["@babel/preset-env"],
  3. "plugins": []
  4. }

处理样式

我们在项目开发过程中,CSS样式时必不可少的,处理样式也是非常通用的功能,借助了 css-loaderstyle-loaderpostcss-loader 这几个依赖,配置写在 webpack.common.js 中。

  • postcss-loader:CSS 语法识别,处理浏览器兼容性
  • css-loader:将 .css 结尾的文件解析为 CSS(webpack 中一切皆模块,它不认识 .css 文件和 .js 文件的区别)
  • style-loader:将解析完的 CSS 插入到页面中(style 标签中)

处理样式的配置如下:

  1. const path = require('path')
  2. const { srcPath, distPath } = require('./paths')
  3. module.exports = {
  4. entry: path.join(srcPath, 'index'),
  5. module: {
  6. rules: [
  7. {
  8. test: /\.css$/,
  9. // loader 的执行顺序是:从后往前
  10. use: ['style-loader', 'css-loader', 'postcss-loader']
  11. },
  12. {
  13. test: /\.less$/,
  14. // 增加 'less-loader',注意顺序
  15. use: ['style-loader', 'css-loader', 'less-loader']
  16. }
  17. ]
  18. }
  19. }

其中 postcss-loader 需要配置一份 postcss.config.js 文件,在这个文件里可以选择一些插件,此处我们安装并引入 autoprefixer 这个依赖(为 CSS 语法添加浏览器兼容性的前缀):

  1. module.exports = {
  2. plugins: [require('autoprefixer')]
  3. }

在处理样式的配置中,除了 CSS,同理还有 Less,Sass 等,都是差不多的思路(解析 Less 语法 => 解析 CSS 语法 => 转成 style),这些知识点我们在上面已经讲过。

处理图片

处理图片在 dev 环境和 prod 环境的思路不同。

  • dev 环境:借助 file-loader 依赖,直接引入图片 url
  • webpack.dev.js 中的配置如下: ```javascript const webpackCommonConf = require(‘./webpack.common.js’) const { merge } = require(‘webpack-merge’)

module.exports = merge(webpackCommonConf, { mode: ‘development’, module: { rules: [ // 直接引入图片 url { test: /.(png|jpg|jpeg|gif)$/, use: ‘file-loader’ } ] } })

  1. prod 环境:从性能优化的角度考虑,
  2. - 大图片借助 `file-loader` 依赖,直接引入图片 url,并打包到指定目录下;
  3. - 小图片转成 base64 的形式,可以减少一次 http 请求。
  4. `webpack.prod.js` 中的配置如下:
  5. ```javascript
  6. const webpackCommonConf = require('./webpack.common.js')
  7. const { merge } = require('webpack-merge')
  8. const { srcPath, distPath } = require('./paths')
  9. module.exports = merge(webpackCommonConf, {
  10. mode: 'production',
  11. output: {
  12. filename: 'bundle.[contenthash:8].js', // 打包代码时,加上 hash 戳,主要针对 JS 文件
  13. path: distPath,
  14. // publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名),这里暂时用不到
  15. },
  16. module: {
  17. rules: [
  18. // 图片 - 考虑 base64 编码的情况
  19. {
  20. test: /\.(png|jpg|jpeg|gif)$/,
  21. use: {
  22. loader: 'url-loader',
  23. options: {
  24. // 小于 5kb 的图片用 base64 格式产出
  25. // 否则,依然延用 file-loader 的形式,产出 url 格式
  26. limit: 5 * 1024,
  27. // 打包到 img 目录下
  28. outputPath: '/img/',
  29. // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
  30. // publicPath: 'http://cdn.abc.com'
  31. }
  32. }
  33. }
  34. ]
  35. }
  36. })

处理 SVG

可以直接把 SVG 文件当成一张图片来使用,方法和使用图片时完全一样。
在 webpack.common.js 中的配置如下:

  1. const path = require('path')
  2. const { srcPath, distPath } = require('./paths')
  3. module.exports = {
  4. entry: path.join(srcPath, 'index'),
  5. module: {
  6. rules: [
  7. // 处理 JavaScript
  8. {
  9. test: /\.js$/,
  10. use: ['babel-loader'],
  11. include: srcPath,
  12. exclude: /node_modules/
  13. },
  14. // 处理 SVG:导出一个资源的 data URI。也可以通过使用 file-loader 实现
  15. {
  16. test: /\.svg$/,
  17. type: 'asset/inline'
  18. },
  19. ]
  20. }
  21. }

处理字体文件

字体文件的使用,方法和使用图片时完全一样。
在 webpack.common.js 中的配置如下:

  1. const path = require('path')
  2. const { srcPath, distPath } = require('./paths')
  3. module.exports = {
  4. entry: path.join(srcPath, 'index'),
  5. module: {
  6. rules: [
  7. // 处理 JavaScript
  8. {
  9. test: /\.js$/,
  10. use: ['babel-loader'],
  11. include: srcPath,
  12. exclude: /node_modules/
  13. },
  14. // 处理 SVG:导出一个资源的 data URI。也可以通过使用 file-loader 实现
  15. {
  16. test: /\.svg$/,
  17. type: 'asset/inline'
  18. },
  19. // 处理字体:直接引入字体 url,之前通过使用 file-loader 实现
  20. {
  21. test: /\.(woff(2)?|eot|ttf|otf|svg|)$/,
  22. type: 'asset/resource',
  23. generator: {
  24. filename: 'static/fonts/[name].[hash:6][ext]'
  25. }
  26. }
  27. ]
  28. }
  29. }

处理 Vue

借助 vue-loader 即可,参考官网
需要先安装依赖包:

  1. npm i vue-loader

然后在 webpack.common.js 中添加对应的规则:

  1. const path = require('path')
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. const { srcPath, distPath } = require('./paths')
  4. module.exports = {
  5. entry: path.join(srcPath, 'index'),
  6. module: {
  7. rules: [
  8. {
  9. test: /\.js$/,
  10. use: ['babel-loader'],
  11. include: srcPath,
  12. exclude: /node_modules/
  13. },
  14. {
  15. test: /\.vue/,
  16. use: ['vue-loader'],
  17. include: srcPath
  18. }
  19. ]
  20. },
  21. plugins: [
  22. new HtmlWebpackPlugin({
  23. template: path.join(srcPath, 'index.html'),
  24. filename: 'index.html'
  25. })
  26. ]
  27. }

高级配置

抽离CSS 文件

在基础配置中,是把所有 CSS 文件全部写到页面的 style 标签里,这种方式在开发模式中问题不大,但在生产环境中显然不科学,因此我们需要把 CSS 文件单独抽离压缩。
首先在 webpack.common.js 中删除对 CSS 和 Less 的处理,只保留对 JS 文件的处理:

  1. const path = require('path')
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. const { srcPath, distPath } = require('./paths')
  4. module.exports = {
  5. entry: {
  6. index: path.join(srcPath, 'index.js')
  7. },
  8. module: {
  9. rules: [
  10. {
  11. test: /\.js$/,
  12. use: ['babel-loader'],
  13. include: srcPath,
  14. exclude: /node_modules/
  15. }
  16. ]
  17. },
  18. plugins: [
  19. new HtmlWebpackPlugin({
  20. template: path.join(srcPath, 'index.html'),
  21. filename: 'index.html'
  22. })
  23. ]
  24. }

其次将原来 CSS 和 Less 的处理逻辑放到开发环境下,即 webpack.dev.js 中:

  1. const path = require('path')
  2. const webpack = require('webpack')
  3. const webpackCommonConf = require('./webpack.common.js')
  4. const { merge } = require('webpack-merge')
  5. const { srcPath, distPath } = require('./paths')
  6. module.exports = merge(webpackCommonConf, {
  7. mode: 'development',
  8. module: {
  9. rules: [
  10. // 直接引入图片 url
  11. {
  12. test: /\.(png|jpg|jpeg|gif)$/,
  13. use: 'file-loader'
  14. },
  15. {
  16. test: /\.css$/,
  17. // loader 的执行顺序是:从后往前
  18. use: ['style-loader', 'css-loader', 'postcss-loader'] // 加了 postcss
  19. },
  20. {
  21. test: /\.less$/,
  22. // 增加 'less-loader' ,注意顺序
  23. use: ['style-loader', 'css-loader', 'less-loader']
  24. }
  25. ]
  26. },
  27. plugins: [
  28. new webpack.DefinePlugin({
  29. // window.ENV = 'development'
  30. ENV: JSON.stringify('development')
  31. })
  32. ],
  33. devServer: {
  34. // 此处省略 devServer 的配置
  35. }
  36. })

然后才是在 webpack.prod.js 中进行配置,这里对 CSS 和 Less 的处理逻辑与在开发环境中很不一样。需要安装三个插件 —— 用于抽离的 mini-css-extract-plugin,用于压缩的 terser-webpack-pluginoptimize-css-assets-webpack-plugin。配置代码如下:

  1. const path = require('path')
  2. const webpack = require('webpack')
  3. const { merge } = require('webpack-merge')
  4. const { CleanWebpackPlugin } = require('clean-webpack-plugin')
  5. const MiniCssExtractPlugin = require('mini-css-extract-plugin')
  6. const TerserJSPlugin = require('terser-webpack-plugin')
  7. const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
  8. const webpackCommonConf = require('./webpack.common.js')
  9. const { srcPath, distPath } = require('./paths')
  10. module.exports = merge(webpackCommonConf, {
  11. mode: 'production',
  12. output: {
  13. filename: '[name].[contenthash:8].js', // name 即多入口时 entry 的 key
  14. path: distPath,
  15. },
  16. module: {
  17. rules: [
  18. // 图片 - 考虑 base64 编码的情况
  19. {
  20. test: /\.(png|jpg|jpeg|gif)$/,
  21. use: {
  22. loader: 'url-loader',
  23. options: {
  24. // 小于 5kb 的图片用 base64 格式产出
  25. // 否则,依然延用 file-loader 的形式,产出 url 格式
  26. limit: 5 * 1024,
  27. // 打包到 img 目录下
  28. outputPath: '/img/',
  29. // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
  30. // publicPath: 'http://cdn.abc.com'
  31. }
  32. }
  33. },
  34. // 抽离 css
  35. {
  36. test: /\.css$/,
  37. use: [
  38. MiniCssExtractPlugin.loader, // 注意,这里不再用 style-loader
  39. 'css-loader',
  40. 'postcss-loader'
  41. ]
  42. },
  43. // 抽离 less
  44. {
  45. test: /\.less$/,
  46. use: [
  47. MiniCssExtractPlugin.loader, // 注意,这里不再用 style-loader
  48. 'css-loader',
  49. 'less-loader',
  50. 'postcss-loader'
  51. ]
  52. }
  53. ]
  54. },
  55. plugins: [
  56. new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
  57. new webpack.DefinePlugin({
  58. // window.ENV = 'production'
  59. ENV: JSON.stringify('production')
  60. }),
  61. // 抽离 css 文件,命名为 main + hash值
  62. new MiniCssExtractPlugin({
  63. filename: 'css/main.[contenthash:8].css'
  64. })
  65. ],
  66. optimization: {
  67. // 压缩 css
  68. minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
  69. }
  70. })

多入口配置

通过基本配置,我们在打包编译后产生的页面只是一个文件 index.html。如果在一个项目中想产生两个页面 index.htmlother.html(或多个页面),就需要进行多入口配置。
首先在 webpack.common.js 中建入口(entry)的时候就需要建立两个(或多个),其次在插件(plugins)中针对每一个入口都要 new 一个 HtmlWebpackPlugin 的实例。配置代码如下:

  1. const path = require('path')
  2. const HtmlWebpackPlugin = require('html-webpack-plugin')
  3. const { srcPath, distPath } = require('./paths')
  4. module.exports = {
  5. entry: {
  6. index: path.join(srcPath, 'index.js'),
  7. other: path.join(srcPath, 'other.js')
  8. },
  9. module: {
  10. rules: [
  11. // 此处省略处理 js、css、less 的配置
  12. ]
  13. },
  14. plugins: [
  15. // 多入口 - 生成 index.html
  16. new HtmlWebpackPlugin({
  17. template: path.join(srcPath, 'index.html'), // 对应到 index.html 这个模板文件
  18. filename: 'index.html', // 生成的文件,名字随便取
  19. // chunks 表示该页面要引入哪些 JS 文件(即 entry 中配置的 JS 文件),默认全部引用
  20. chunks: ['index'] // 只引用 index.js
  21. }),
  22. // 多入口 - 生成 other.html
  23. new HtmlWebpackPlugin({
  24. template: path.join(srcPath, 'other.html'), // 对应到 other.html 这个模板文件
  25. filename: 'other.html', // 生成的文件,名字随便取
  26. chunks: ['other'] // 只引用 other.js
  27. })
  28. ]
  29. }

其次修改 prod(生产环境配置)中的 output,将 webpack.prod.jsoutput.filename 的固定值 bundle 修改为变量 [name]。配置代码如下:

  1. const path = require('path')
  2. const webpack = require('webpack')
  3. const { CleanWebpackPlugin } = require('clean-webpack-plugin')
  4. const webpackCommonConf = require('./webpack.common.js')
  5. const { merge } = require('webpack-merge')
  6. const { srcPath, distPath } = require('./paths')
  7. module.exports = merge(webpackCommonConf, {
  8. mode: 'production',
  9. output: {
  10. // filename: 'bundle.[contenthash:8].js', // 打包代码时,加上 hash 戳
  11. filename: '[name].[contenthash:8].js', // name 即多入口时 entry 的 key
  12. path: distPath,
  13. },
  14. module: {
  15. rules: [
  16. // 此处省略处理图片的配置
  17. ]
  18. },
  19. plugins: [
  20. new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
  21. new webpack.DefinePlugin({
  22. // window.ENV = 'production'
  23. ENV: JSON.stringify('production')
  24. })
  25. ]
  26. })

抽离公共代码

为了提升性能,我们需要在打包时将第三方模块公用引用的代码单独拆分出去。
既然是在打包时进行的优化,就是修改首先在 webpack.prod.js 这份生产环境的配置文件。在 optimization 中添加分割代码块的逻辑,配置代码如下:

  1. const webpack = require('webpack')
  2. const { merge } = require('webpack-merge')
  3. const { CleanWebpackPlugin } = require('clean-webpack-plugin')
  4. const MiniCssExtractPlugin = require('mini-css-extract-plugin')
  5. const TerserJSPlugin = require('terser-webpack-plugin')
  6. const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
  7. const webpackCommonConf = require('./webpack.common.js')
  8. const { srcPath, distPath } = require('./paths')
  9. module.exports = merge(webpackCommonConf, {
  10. mode: 'production',
  11. output: {
  12. filename: '[name].[contenthash:8].js', // name 即多入口时 entry 的 key
  13. path: distPath,
  14. },
  15. module: {
  16. rules: [
  17. // 图片 - 考虑 base64 编码的情况
  18. {
  19. test: /\.(png|jpg|jpeg|gif)$/,
  20. use: {
  21. loader: 'url-loader',
  22. options: {
  23. // 小于 5kb 的图片用 base64 格式产出
  24. // 否则,依然延用 file-loader 的形式,产出 url 格式
  25. limit: 5 * 1024,
  26. // 打包到 img 目录下
  27. outputPath: '/img/',
  28. // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
  29. // publicPath: 'http://cdn.abc.com'
  30. }
  31. }
  32. },
  33. // 抽离 css
  34. {
  35. test: /\.css$/,
  36. use: [
  37. MiniCssExtractPlugin.loader, // 注意,这里不再用 style-loader
  38. 'css-loader',
  39. 'postcss-loader'
  40. ]
  41. },
  42. // 抽离 less
  43. {
  44. test: /\.less$/,
  45. use: [
  46. MiniCssExtractPlugin.loader, // 注意,这里不再用 style-loader
  47. 'css-loader',
  48. 'less-loader',
  49. 'postcss-loader'
  50. ]
  51. }
  52. ]
  53. },
  54. plugins: [
  55. new CleanWebpackPlugin(), // 会默认清空 output.path 文件夹
  56. new webpack.DefinePlugin({
  57. // window.ENV = 'production'
  58. ENV: JSON.stringify('production')
  59. }),
  60. // 抽离 css 文件
  61. new MiniCssExtractPlugin({
  62. filename: 'css/main.[contenthash:8].css'
  63. })
  64. ],
  65. optimization: {
  66. // 压缩 css
  67. minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})],
  68. // 分割代码块
  69. splitChunks: {
  70. chunks: 'all',
  71. /**
  72. * initial 入口 chunk,对于异步导入的文件不处理
  73. * async 异步 chunk,只对异步导入的文件处理
  74. * all 全部 chunk,一般选择 all 模式
  75. */
  76. // 缓存分组
  77. cacheGroups: {
  78. // 第三方模块
  79. vendor: {
  80. name: 'vendor', // chunk 名称
  81. priority: 1, // 权限更高,优先抽离(例如第三方模块同时也作为公共模块在多处引用时,按第三方模块的规则进行抽离)
  82. test: /node_modules/, // 检查模块是否位于 node_modules/ 目录下
  83. minSize: 30000, // 大小限制(Byte),太小的不用抽离
  84. minChunks: 1 // 最少复用过几次(第三方模块只要引用过一次就抽取出来)
  85. },
  86. // 公共的模块
  87. common: {
  88. name: 'common', // chunk 名称
  89. priority: 0, // 优先级
  90. minSize: 50000, // 公共模块的大小限制(此处设置 50KB)
  91. minChunks: 2 // 公共模块最少复用过几次
  92. }
  93. }
  94. }
  95. }
  96. })

性能优化

优化 babel-loader

  • 开启缓存:在原配置的基础上增加一个 ?cacheDirectory 开启缓存,只要是 ES6 代码没有改动的部分,就不会重新编译。
  • 明确范围:通过 includeexclude 明确打包范围,两者选一个即可。
    1. {
    2. test: /\.js$/,
    3. use: ['babel-loader?cacheDirectory'], // 开启缓存
    4. include: path.resolve(__dirname, 'src') // 明确范围
    5. // 排除范围,include 和 exclude 两者选一个即可
    6. // exclude: path.resovle(__dirname, 'node_modules')
    7. }

    IgnorePlugin 避免引入无用模块-按需加载

    例如我们在项目中引入了 Moment.js 这个日期处理类库 import moment from 'moment',该库有多国语言支持,默认会引入所有语言的 JS 代码,导致体积庞大。
    如果我们只想打包进中文语言的代码,就需要启用 IgnorePlugin 插件。
    修改 webpack.prod.js 文件,在 plugins 中追加配置:
    1. module.exports = {
    2. plugins: [
    3. // 忽略 moment 下的 /locale 目录
    4. new webpack.IgnorePlugin(/\.\/locale/, /moment/),
    5. ]
    6. }
    通过上述配置后,在打包时 moment 库的 locale 这个文件夹就被跳过了。那么在使用时,为了能显示语言,就要动态引入: ```javascript import moment from ‘moment’ import ‘moment/locale/zh-cn’ // 手动引入中文语言包 moment.locale(‘zh-cn’)

console.log(moment().format(‘ll’))

  1. <a name="ETLwT"></a>
  2. #### bundle 加 hash
  3. 在生产环境下(`webpack.prod.js`),对于出口文件,根据文件的内容计算出一个 hash 值(下述示例为 8 位 hash),如果文件内容更新后,缓存会失效,重新请求新的文件;如果代码没有变化,hash 值不变,就会使用缓存,从而提高加载效率。如下代码所示:
  4. ```javascript
  5. const { merge } = require('webpack-merge')
  6. const webpackCommonConf = require('./webpack.common.js')
  7. const { srcPath, distPath } = require('./paths')
  8. module.exports = merge(webpackCommonConf, {
  9. mode: 'production',
  10. output: {
  11. // filename: 'bundle.[contenthash:8].js', // 打包代码时,加上 hash 戳
  12. filename: '[name].[contenthash:8].js', // name 即多入口时 entry 的 key
  13. path: distPath,
  14. }
  15. })

懒加载

通过 import 语法,先加载重要的文件,然后异步加载大的文件。这个逻辑与 Vue 和 React 中组件的异步加载类似,如下代码所示:

  1. setTimeout(() => {
  2. // 定义 chunk
  3. import('./dynamic-data.js').then(res => {
  4. console.log(res.default.message)
  5. })
  6. }, 1500)

使用 CDN 加速

在生产环境下(webpack.prod.js),设置 output.publicPath 后,打包出来的 html 里都会引用 CDN 的静态资源文件。
需要注意,在打包完后,需要将结果文件(dist 目录)都上传到 CDN 服务器,保证这些静态资源都是可访问的。

  1. const { merge } = require('webpack-merge')
  2. const webpackCommonConf = require('./webpack.common.js')
  3. const { srcPath, distPath } = require('./paths')
  4. module.exports = merge(webpackCommonConf, {
  5. mode: 'production',
  6. output: {
  7. // filename: 'bundle.[contenthash:8].js', // 打包代码时,加上 hash 戳
  8. filename: '[name].[contenthash:8].js', // name 即多入口时 entry 的 key
  9. path: distPath,
  10. publicPath: 'http://cdn.abc.com' // 修改所有静态文件 url 的前缀(如 cdn 域名)
  11. }
  12. })

同理,图片也可以设置 CDN 地址,如下代码所示:

  1. // 图片 - 考虑 base64 编码的情况
  2. {
  3. test: /\.(png|jpg|jpeg|gif)$/,
  4. use: {
  5. loader: 'url-loader',
  6. options: {
  7. // 小于 5kb 的图片用 base64 格式产出
  8. // 否则,依然延用 file-loader 的形式,产出 url 格式
  9. limit: 5 * 1024,
  10. // 打包到 img 目录下
  11. outputPath: '/img/',
  12. // 设置图片的 cdn 地址(也可以统一在外面的 output 中设置,那将作用于所有静态资源)
  13. publicPath: 'http://cdn.abc.com'
  14. }
  15. }
  16. }

Webpack 示例模版

webpack-simple

包含了快速上手的示例Demo,可用于Webpack基础知识的学习,具体使用方式如下:

  1. # 安装
  2. npm i
  3. # 使用
  4. npm start

webpack-template

一份 Webpack5 的通用模板。

安装

  1. cd webpack-template/
  2. npm install

使用

开发环境运行
  1. npm run serve

开发环境下服务器将运行在 localhost:8080

生产环境打包
  1. npm run build

注意: 需要全局安装 http-server 来部署一个简易的 http 服务器。

  1. npm install http-server -g

通过在 dist 目录中创建一个服务器来查看打包后的页面效果。

  1. cd dist/ && http-server

依赖包

这份通用模板用到的依赖包及具体用途如下:

下述依赖包均是通过 npm install <package-name> --save-dev 进行安装的。

Webpack

最基础的依赖。

  • [webpack](https://github.com/webpack/webpack):包含 Webpack 核心内容
  • [webpack-cli](https://github.com/webpack/webpack-cli):包含 Webpack 操作的常见命令
  • [webpack-dev-server](https://github.com/webpack/webpack-dev-server):Webpack 的开发服务器
  • [webpack-merge](https://github.com/survivejs/webpack-merge):提供合并函数,用于合并配置文件

    Babel

    Babel 用于将 ES6+ 语法编写的代码转换为向后兼容的 JavaScript 语法。

  • [@babel/core](https://www.npmjs.com/package/@babel/core):包含 Babel 转换的核心 API

  • [@babel/preset-env](https://babeljs.io/docs/en/babel-preset-env):Babel 的默认设置,包含最近的 JavaScript 语法转换规则

    Loaders

    专门用来处理那些非 JavaScript 文件的工具(Webpack 默认只能识别 JavaScript),将这些资源翻译成 Webpack 能识别的资源。

  • 转换 JS

    • [babel-loader](https://webpack.js.org/loaders/babel-loader/):用来转换 ES6+ 语法,需要创建一个 .babelrc 配置文件
  • 转换 vue
    • [vue-loader](https://www.npmjs.com/package/vue-loader):用来转换 Vue的单文件组件
  • 处理 CSS
    • [css-loader](https://webpack.js.org/loaders/css-loader/):负责遍历 CSS 文件,将 CSS 转化为 CommonJS(将 CSS 输出到打包后的 JS 文件中)
    • [style-loader](https://webpack.js.org/loaders/style-loader/):负责把包含 CSS 内容的 JS 代码,挂载到页面的 style 标签中
  • 处理 LESS
    • [less](https://webpack.js.org/loaders/less-loader/):安装 Less.js,使项目支持 LESS 语法
    • [less-loader](https://webpack.js.org/loaders/less-loader/):负责将 Less 编译为 CSS
  • 处理 CSS 浏览器兼容性

    • [postcss-loader](https://webpack.js.org/loaders/postcss-loader/):CSS 语法识别,需要创建一个 postcss.config.js 配置文件
    • [postcss-preset-env](https://www.npmjs.com/package/postcss-preset-env):在 postcss.config.js 配置文件中添加的插件,用于为 CSS 语法添加浏览器兼容性的前缀。该插件集成了 autoprefixer 且做了优化。
      Plugins
  • [clean-webpack-plugin](https://github.com/johnagan/clean-webpack-plugin):每次打包之前,先删除输出目录中的历史文件(保证输出目录中的打包文件是最新的)

  • [copy-webpack-plugin](https://github.com/webpack-contrib/copy-webpack-plugin):不需要处理的其他文件,可以直接复制到输出目录
  • [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin):用于从模板创建 HTML 文件,创建的 HTML 文件默认引入打包后的所有资源文件
  • [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin):抽离 CSS 到独立的文件
  • [css-minimizer-webpack-plugin](https://webpack.js.org/plugins/css-minimizer-webpack-plugin/):优化和压缩 CSS(跟 optimize-css-assets-webpack-plugin 一样,但有一些优势)