Webpack简单入门学习

58 阅读46分钟

webpack0.jpg

Webpack简单入门

不管学习什么技术,开篇就应该问问它是什么? 它能(为我们)做什么? 那Webpack呢?

Q: 什么是Webpack?

A:本质上webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具

Q: Webpack能为我们做什么?

A: 当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles(它们均为静态资源,用于展示你的内容)

  • Webpack最主要的、最核心的功能就是模块打包;也就是:找出模块之间的依赖关系,按照一定的规则把这些模块组织、合并为一个JavaScript文件
  • Webpack认为一切都是模块,如JS文件、CSS文件、JPG和png图片等都是模块,Webpack会把所有这些模块合并为一个JS文件,这是它最本质的工作。 当然,我们可能并不想让它把这些模块都合并成一个JS文件,这时我们可以通过一些规则或工具来改变它最终打包生成的文件。

webpack核心概念

webpack中"一切皆模块"

  • 入口(entry)
  • 输出(output)
  • loader
  • 插件(plugin)

  • 模式(mode)
  • 浏览器兼容性(browser compatibility)
  • 环境(environment)
安装Webpack

Webpack有两种安装方式,分别为全局安装与本地安装。无论哪种安装方式,都需要安装两个npm包:

  1. webpack 是Webpack的核心npm包
  2. webpack-cli是命令行运行webpack命令所需的npm包(命令行工具包)

全局安装 npm install webpack webpack-cli -g 本地安装 npm install webpack webpack-cli --save-dev

Q: 全局安装与本地安装Webpack的区别 A: 全局安装的Webpack,可以在任何目录下执行webpack命令,调用webpack命令进行打包 本地安装的webpack, 必须先找到对应目录node_modules下的webpack命令文件,然后才能执行打包命令

  • 本地安装的webpack进行打包,如果不想拼接路径,可以使用命令npx webpack或者在package.json文件里下面的命令执行npm run dev,这两种方式都会自动执行node_modules下的webpack命令, 不需要拼接路径
// package.json中配置
"scripts": {
	"dev": "webpack"
}


// 执行: npm run dev

【注意】npx wbpack 命令里的npx是最新版Node.js里附带的命令。运行该命令的时候默认会找到node_modules/.bin/下的路径执行,与下面的命令等效。 Linux/UNIX命令如: node_modules/.bin/webpack

Windows的cmd命令行,如webpack-1\node_modules\.bin\webpack

npx webpack --entry ./a.js -o dist

// 上面命令的作用: 从a.js文件开始,按照模块引入的顺序把所有代码打包到一个JS文件里。 这个文件的默认名称是main.js
// Webpack会自动处理打包后代码的顺序与依赖关系
// --entry 用来指定打包入口文件
// -o 是out的意思,表示输出目录,这里使用dist目录作为打包后的输出目录。 
// 注意: webpack是打包命令,后面的内容是打包参数

Webpack打包模式

Webpack的打包模式有三种:

  1. production
  2. development
  3. none

这三种模式是通过mode参数指定的。 production和development两种模式分别按照线上生产环境和开发环境进行一些优化,而none保留原始的打包结果

  • 为了避免webpack额外的优化处理对我们学习 理解细节造成干扰,我们可以在webpack性能优化之前把mode参数设置为none模式来进行学习
npx webpack --entry ./a.js -o dist --mode=none
// 打包模式改为none模式,这样就不会压缩代码了。
Webpack的配置文件

Webpack默认的配置文件是项目根目录下的webpack.config.js文件,在我们执行npx webpack命令的时候,Webpack会自动查找该文件并使用配置信息进行打包,如果找不到 该文件,就使用默认参数打包 __dirname是Node.js的一个全局变量,表示当前文件的路径。 resolve(__dirname, '')表示的其实就是当前文件夹更目录的绝对路径

入口(entry)

入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部 依赖图(dependency graph) 的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的

  • entry的默认值是./src/index.js, 但是可以通过webpack configuraction中的配置项entry属性,来指定一个(或多个)不同的入口起点,如;
module.exports = {
  entry: './path/to/my/entry/file.js',
};
  • entry的配置可以有多种形式: 字符串、数组、对象、函数
  • 在使用字符串或数组定义单入口时,我们没有办法更改chunk name,只能用默认的main
  • 在使用对象来定义多入口时,则必须为每一个入口定义chunk name(即: 对象的属性名(key))
字符串类型入口
module.exports = {
	entry: {
		main: './src/index.js'
	}
}

// 简写形式如下
module.exports = {
	entry: './src/index.js'
}
数组类型入口

传入一个数组的作用是将多个资源预先合并,这样webpack在打包时会将数组中的最后一个文件作为实际(资源)的入口路径,数组的其余文件会被预先构建到入口文件中

module.exports = {
	entry: ['babel-polyfill', './src/index.js']
}


// 上面代码等同于
// webpack.config.js
module.exports = {
	entry: './src/index.js'
}

// index.js
import 'babel-polyfill'
对象类型入口

对象形式的entry又被称为多入口配置,多入口配置就是打包后生成多个JS文件; 如果想要定义多入口,则必须使用对象的形式。对象的属性名(key)是chunk name, 属性值是(value)是入口路径。

module.exports = {
	// chunk name为index, 入口路径为./src/index.js
	index: './src/index.js',
	// chunk name为lib, 入口路径为./src/lib.js
	lib: './src/lib.js'
}
  • entry对象的属性值也可以为字符串或数组,如:
module.exports = {
	entry: {
		index: ['babel-polyfill', './src/index.js'],
		lib: './src/lib.js'
	}
}
函数类型入口

我们也可以使用匿名函数来定义入口。其实非常简单,只要使用匿名函数的返回值为上述类型中任何一种配置形式即可,如:

// 返回一个字符串类型的入口
module.exports = {
	entry: ()=>'./src/index.js'
}


// 返回对象类型入口
module.exports = {
	entry: ()=>({
		index: ['babel-polyfill', './src/index.js'],
		lib: './src/lib.js'
	})
}
  • 使用函数的优点在于: 我们可以添加一些代码逻辑来动态地定义入口值。另外,函数也支持返回一个Promise对象来进行异步操作
module.exports = {
	entry: ()=> new Promise((resolve, reject)=>{
		setTimeout(()=>{
			resolve("./src/index.js");
		}, 1000);
	})
}
  • 无论是框架、库、还是各个页面的模块,都由app.js单一的入口进行引用。这样做的好处是只会产生一个JS文件,依赖关系清晰。 但这样做也有弊端,所有模块都打包到一起,当应用的规模上升到一定程度之后会导致产生的资源体积过大,降低用户的页面渲染速度。
提取vendor

为了解决该(单个JS文件体积过大)问题,我们可以使用提取vender的方法。vendor的字面量是"供应商",在webpack中则一般指工程所需要的库、框架 等第三方模块集中打包而产生的bundle。当第三方依赖较多时,可以用提取vender的方法将这些模块打包到一个单独的chunk中,以更有效地利用客户端缓存,加快页面渲染速度

module.exports = {
	entry: './src/app.js',
	vendor: ['react', 'react-dom', 'react-router']
}

// 通过这样的配置,app.js只产生业务相关的boundle, 从而达到我们提取vender的目标。由于vendor仅仅包含第三方模块,这部分不会经常变动,因此可以有效
地利用客户端缓存。在用户后续请求页面时加快整体的渲染速度。
多页面应用

对于多页面应用的场景,我们同样可以使用提取vendor的方法,将各个页面之间的公共模块提取

module.exports = {
	pageA: "./src/pageA.js",
	pageB: "./src/pageB.js",
	pageC: "./src/pageC.js",
	vendor: ['react', 'react-dom']
}

// 我们将react和react-dom 打包进vendor,之后再配置optimization.splitChunk,将它们从各个页面中提取处理,生成单独的boundle; 分离 app(应用程序) 和 vendor(第三方库) 入口

输出(output)

可以通过配置 output 选项,告知 webpack 如何向硬盘写入编译文件。注意,即使可以存在多个 entry 起点,但只能指定一个 output 配置

  • output就是资源出口配置项。output的值是一个对象,它有几个重要的属性: filenane、path、publicPath和chunkFilename
module.exports = {
	entry: './src/index.js',
	output: {
		// 输出资源的文件名
		filename: 'bundle.js',
		path: path.join(__dirname, 'assets'),
		publicPath: '/dist/'
	}
};

output的配置参数filename
  • filename的作用是控制输出资源的文件名,其形式为字符串
  • filename可以不仅仅是bundle的名字,还阔以是一个相对路径,即便路径中的目录不存在也没关系,因为webpack会在输出资源时创建该目录
  • filename支持类似变量的方式生成动态文件名,如[hash]-bundle.js,其中方括号代表占位符,里面的hash表示特定的动态值。 特定动态值除了[hash],还有[name]和[id]等 [name]表示的是chunk的名字,简单理解的话,在打包过程中,一个资源入口依赖的模块集合代表一个chunk,一个异步模块依赖代表一个chunk,另外代码拆分也会有单独的chunk
module.exports = {
	entry: './src/index.js',
	output: {
		filename: './js/main.js'
	}
}
  • 在多入口的场景中,我们需要为对应产生的每个bundle指定不同的名字。Webpack支持使用一种类似模板语音的形式动态地生成文件名,如:
module.exports = {
	entry: {
		app: './src/app.js',
		vendor: './src/vendor.js'
	},
	output: {
		filename: '[name].js'
	}
}

// 在资源输出时,上面配置的filename中的[name]会被替换为chunk name,因此最后项目中实际生成的资源是vendor.js 与 app.js
output的配置参数path
  • path表示资源打包后输出的位置,该位置地址需要的是绝对路径。如果没有设置,Webpack默认其为dist目录
  • path 可以指定资源输出的位置,要求值必须为绝对路径。如:
const path = require('path')

module.exports = {
	entry: {
		app: './src/app.js',
		vendor: './src/vendor.js'
	},
	output: {
		filename: '[name].js',
		// 将资源输出位置设置为工程的dist目录
		path: path.join(__dirname, 'dist')
	}
}
  • path输出的位置表示的是磁盘上构建生成的真实文件存放的地址。我们在开发时,一般会用到webpack-dev-server开启一个本地服务器,这个服务器可以自动刷新和热加载等, 它生成的文件存放在内存中而不是在电脑磁盘中。对于该内存中的文件路径,我们会用webpack配置文件的deveServer配置项的publicPath表示,它虚拟映射了电脑磁盘路径
output的配置参数publicPath
  • publicPath是一个非常重要的配置项,并且容易与path混淆。它表示的是资源访问路径;从功能上来说,path用来指定资源的输出位置,publicPath则用来指定资源的请求位置。
  • path和publicPath的区别在于: path指定的是资源的输出位置,而publicPath指定的是间接资源的请求位置
  1. 输出位置:打包完成后资源产生的目录,一般将其指定为工程的dist目录
  2. 请求位置:由JS或CSS所请求的间接资源路径。页面中的资源分为两种,一种是由HTML页面直接请求的,比如通过script标签加载的JS;另一种是由JS或 CSS发起请求的间接资源,如图片、字体等(也包括异步加载的JS)。 publicPath的作用就是指定这部分间接资源的请求位置

资源输出位置表示的是本次打包完成后,资源存放在磁盘中的位置。 资源存放到磁盘后,浏览器如何知道该资源存放在什么位置呢? 这个时候需要我们指定资源的访问路径,这个访问路径就是用out.publicPath来表示的。 在Web开发中,配置项output.publicPath的默认值是auto,表示资源调用者与被调用者在同一目录下

  • publicPath有3种形式: 1、HTML相关 HTML相关,也就是说我们可以将publicPath指定为HTML的相对路径,在请求这些资源时,以当前页面HTML所在路径加上相对路径,构成实际的URL
// 假设当前HTML地址为 http://abc.com/app/index.html
// 异步加载的资源名为 0.chunk.js
publicPath: "" 				// 实际路径 http://abc.com/app/0.chunk.js
publicPath: "./js" 			// 实际路径 http://abc.com/app/js/0.chunk.js
publicPath: "../assets/" 	// 实际路径 http://abc.com/assets/js/0.chunk.js

2、Host相关 若publicPath的值以"/"开始,则代表此时publicPath是以当前页面的host name为基础路径的

// 假设当前HTML地址为 http://abc.com/app/index.html
// 异步加载的资源名为 0.chunk.js
publicPath: "/" 				// 实际路径 http://abc.com/0.chunk.js
publicPath: "/js/" 				// 实际路径 http://abc.com/js/0.chunk.js
publicPath: "/dist/" 			// 实际路径 http://abc.com/dist/js/0.chunk.js

3、CDN相关 publicPath我们也可以使用绝对路径的形式配置。这种情况一般在静态资源放在CDN上面,由于其域名与当前域名不一致,需要以绝对路径的形式进行 指定时发生。当publicPath以协议头或相对协议形式开始时,代表当前路径是CDN相关

// 假设当前HTML地址为 http://abc.com/app/index.html
// 异步加载的资源名为 0.chunk.js
publicPath: "http://cdn.com/" 				// 实际路径 http://cdn.com/0.chunk.js
publicPath: "https://cdn.com/" 				// 实际路径 https://cdn.com/0.chunk.js
publicPath: "//cdn.com/assets/" 			// 实际路径 //cdn.com/assets/0.chunk.js
  • webpack-dev-server的配置中也有一个publicPath,但是这个publicPath与webpack中的配置含义不同,它的作用的指定webpack-dev-server的静态资源服务路径。
const path = require("path");

module.exports = {
	entry: "./src/app.js",
	output: {
		filename: 'bundle.js',
		path: path.join(__dirname, 'dist')
	},
	devServer: {
		port: 3000,
		publicPath: '/assets/'
	}
}

// 从上例中可以看到配置中的output为dist目录,因此bundle.js应该存储在dist目录
// 当我们启动webpack-dev-server的服务后,访问localhost:3000/dist/bundle.js时会得到404, 这是因为devServer.publicPath配置项将资源位置指向了
localhost:3000/assets/,因此只有访问localhost:3000/assets/bundle.js才能得到期望的结果
  • 为了避免开发环境和生产环境产生不一致而造成开发的疑惑,我们可以将webpack-dev-server的publicPath与webpack配置中的output.path保持一致,这样在 任何环境下资源输出的目录都是相同的。如:
const path = require("path");

module.exports = {
	entry: "./src/app.js",
	output: {
		filename: 'bundle.js',
		path: path.join(__dirname, 'dist')
	},
	devServer: {
		port: 3000,
		publicPath: '/dist/'
	}
}
output的配置参数chunkFilename

chunkFilename也用来表示打包后生成的文件名,那么它和filename有什么区别在于:chunkFilename表示的是打包过程中非入口文件的chunk名称,通常在使用异步模块的时候,会生成非入口文件的chunk

hash、fullhash、chunkhash、和contenthash的区别
  • Webpack根据文件内容计算出特殊字符串的时候,使用的就是hash算法,这个特殊字符串一般叫做hash值
  • 在webpack里,我们通常用[hash:8]表示取值的前八位,
  1. fullhash与hash是一样的,fullhash是Webpack5提出的,它用来替代之前的hash.
  2. hash、chunkhash和contenthash这三者是根据文件内容计算出的hash值,只是它们计算的文件不一样
  3. hash是根据打包中的所有文件计算出的hash值。在一次打包中,所有资源出口文件的filename获得的[hash]都是一样的
  4. chunkhash是根据打包过程中当前chunk计算出的值。如果webpack配置是多个入口,那么通常生成多个chunk,每个chunk对应的资源出口文件的filename获得的[chunkname]是不一样的。 这样可以保证打包后每一个JS文件名都不一样
  5. contenthash有点像chunkhash,是根据打包时的内容计算出的hash值。在使用提取CSS文件的插件的时候,我们一般用contenthash。如:
plugins: [
	new miniExtractPlugin({
		filename: 'main.[contenthash:8].css'
	})
]
  • Webpack中的hash、fullhash、chunkhash、和contenthash主要与浏览器缓存行为有关; 浏览器在初次请求服务端资源的时候,网络服务器会为JS、CSS和图片等资源设置 一个较长的缓存时间,我们通过给资源名称添加hash值来控制浏览器是否继续使用本地磁盘中的文件。hash(fullhash)、chunkhash和contenthash这三者都是根据文件内容 计算出的hash值,hash是根据【全部参与打包的文件】计算出来的;chunkhash是根据【当前打包的chunk】计算出来的;contenthash主要用于计算CSS文件的hash值

预处理器Loader

Loader是 Webpack生态里一个重要的组成部分,我们一般称之为预处理器;它用于对模块的源代码进行解析转换的。loader 可以使你在 import 或 "load(加载)" 模块时预处理文件。因此,loader 类似于其他构建工具中“任务(task)”,并提供了处理前端构建步骤的得力方式。loader 可以将文件从不同的语言(如 TypeScript)转换为 JavaScript 或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS 文件!

  • loader就像webpack的翻译官。Webpack本身只认识JS文件、JSON文件,对于其他类型的资源,我们必须定义一个或多个loader对其进行转译,输出为Webpack能够(理解)接收的形式再继续进行,因此loader做的实际上是一个预处理的工作
  • 当webpack自身无法处理某种类型文件模块(如: 图片、CSS等)的时候,我们就可以(需要)通过配置特定的预处理器,赋予webpack处理该类型文件的能力(来处理该文件类型)
  • loader可以链式的,并且每一个loader都允许拥有自己的配置项
  • loader本质上(本身)是一个函数。第一个loader的输入是源文件,之后所有loader的输入是上一个loader的输出,最后一个loader则直接输出到webpack
  • 在配置loader时,实际上定义的是模块规则(module.rules),它主要关注两件事:
  1. 该规则对哪些模块生效(test、exclude、include配置)
  2. 使用哪些loader(use配置)
loader概述

loader是webpack中的一个核心概念,我们可以将其理解为一个代码转换的工具。每个loader本质上是一个函数,可以表示为以下形式:

output = loader(input)
// 这里的input可能是工程源文件的字符串,也可能是上一个loader转化后的结果,
// output则包含了转化后的代码、source-map和AST对象
// 如果是最后一个loader,结果将直接被送到Webpack进行后续处理,否则将作为下一个loader的输入向后传递
  • 举例,当我们使用babel-loader将ES6+的代码转化为ES5时,上面的公式如下:
ES5 = babel-loader(ES6+)
  • babel可以是链式的。我们可以对一种资源设置多个loader,第一个loader的输入是文件的源码,之后所有loader的输入都为上一个loader的输出。用公式表示如下:
output = loaderA(loaderB(loaderC(input)))

如在工程中编译SCSS,我们可能需要如下loader:
Style标签 = style-loader(css-loader(sass-loader(SCSS)))
  • loader的源码结构,大致如下:
module.exports = function loader(content, map, meta){
	var callback = this.async();
	var result = handler(content, map, meta)
	
	callback(
		null,				// error
		result.content,		// 转换后的内容
		result.map,			// 转换后的source-map
		result.meta			// 转换后的AST
	);
}
loader安装配置
  • loader都是一些第三方npm模块,Webpack本身并不包含任何loader,因此使用loader的第一步是先从npm安装它
  • 与loader相关的配置都在module对象中,其中module.rules代表了模块的处理规则。每条规则内部可以包含很多配置项, 这里我们只使用最重要的两项---test和use

下面以处理CSS为例

// 安装css-loader
npm install css-loader -D
// 配置
const path = require('path')

module.exports = {
	mode: 'development',
	entry: {
		app: './src/app.js',
		vendor: './src/vendor.js'
	},
	output: {
		filename: '[name].js'
	},
	module: {
		rules: [{
			// /\.css$/来匹配所有以.css结尾的文件
			test: /\.css$/,
			// 在这里我们只配置了一个css-loader,在只接收一个loader时也可以将其简化为字符串 use: "css-loader"
			use: ['css-loader']
		}]
	}
}
// test可接收一个正则表达式或者一个元素为正则表达式的数组,只有正则匹配上的模块才会使用这条规则。
// use可接受一个数组,数组包含该规则所使用的loader.

// css-loader的作用仅仅是处理CSS的各种加载语法(@import和url()函数等),如果要使用样式起作用还需要style-loader来把样式插入页面
// css-loader与style-loader通常是配置在一起使用的
链式loader
  • 很多时候,在处理某一类资源时我们都需要使用多个loader。如:对于SCSS类型的资源来说,我们需要:
  1. 使用sass-loader来处理其语法,并将其编译为css;
  2. 接着再用css-loader处理CSS的各类加载语法;
  3. 最后使用style-loader类将样式字符串包装成style标签插入页面
  • loader作为预处理器通常会给开发者提供一些配置项,在引入loader的时候可以通过options将它们传入;
const path = require('path')

module.exports = {
	mode: 'development',
	entry: {
		app: './src/app.js',
		vendor: './src/vendor.js'
	},
	output: {
		filename: '[name].js'
	},
	devServer: {
		host: 3000,
		publicPath: '/dist/'
	},
	module: {
		rules: [
			{
				test: /\.css$/,
				// use: ['style-loader', 'css-loader']	// 把style-loader加到css-loader前面,这是因为Webpack在打包时是按照数据从后往前的顺序将资源交给loader处理的,
因此要把最后生效的放在前面
				use: [
					'style-loader', 
					{
						loader: 'css-loader',
						options: {
							// css-loader的配置项
						}
					}
				]
			}
		]
	}
}
loader的更多配置
  • exclude(排除、不包含) 与 include(包含); exclude与include用于排除或包含指定目录下的模块,可接收正则表达式或者字符串(文件绝对路径), 或者由它们组成的数组
module: {
	rules: [
		{
			test: /\.css$/,
			use: ['style-loader', 'css-loader'],
			// node_modules中的模块不会执行这条规则。 该配置项通常是必加的,否则将拖慢整体的打包速度
			exclude: /node_modules/
		}
	]
}

  • 除exclude外,使用include配置也可以达到类似的效果
module: {
	rules: [
		{
			test: /\.css$/,
			use: ['style-loader', 'css-loader'],
			// 只对正则匹配到的模块生效。
			include: /src/
		}
	]
}
  • 当exclude和include同时存在时,exclude的优先级更高
module: {
	rules: [
		{
			test: /\.css$/,
			use: ['style-loader', 'css-loader'],
			exclude: /node_modules/,
			include: /node_modules\/awesome-ui/
		}
	]
}

// 此时node_modules已经被排除了
  • 假如我们想让上面规则对node_modules中的某一个模块生效,即便加上include也是无法覆盖exclude配置的。要实现这样的需求, 我们可以更改exclude中的正则表达式
module: {
	rules: [
		{
			test: /\.css$/,
			use: ['style-loader', 'css-loader'],
			// 排除node_modules中除了foo和bar以外的所有模块
			exclude: /node_modules/(?!(foo|bar)).*/
		}
	]
}

  • 由于exclude优先级更高,我们可以对include中的子目录进行排除。
module: {
	rules: [
		{
			test: /\.css$/,
			use: ['style-loader', 'css-loader'],
			// 排除src中的lib目录
			exclude: /src/lib/,
			include: /src/
		}
	]
}
  • resource 与 issuer用于更加精准地确定模块规则的作用范围。在Webpack中,我们认为被加载模块是resource,而加载者是issuer
// index.js
import './style.css'

// style.css被认为是resource
// index.js则是issuer
  • test exclude include本质上均属于对resource也就是被加载者的配置。
  • 如果要对加载者issuer也增加条件限制,则要额外写一些配置。
rules: [
	{
		test: /\.css$/,
		use: ['style-loader', 'css-loader'],
		exclude: /node_modules/,
		issuer: {
			test: /\.js$/,
			// 只有/src/pages/目录下面的JS文件引用CSS文件,这条规则才会生效,JSX文件引用则不会生效
			include: /src/pages/
		}
	}
]
  • enforce 用来指定一个loader的种类,只接收pre或post两种字符串类型的值
  • Webpack 中loader按照执行顺序可分为pre\inline\normal\post四种类型,
  1. 直接定义loader属于normal类型
  2. inline形式官方已经不推荐
  3. pre和post则需要使用enforce来指定
rules: [
	{
		test: /\.js$/,
		enforce: 'pre',
		use: 'eslint-loader'
	}
]

// 在配置中添加了一个eslint-loader来对源码进行质量检测,enforce的值为pre, 表示它将在所有正常loader之前执行,这样
可以保证其检测的代码不是别其他loader更改过的。

// 如果某个一个loader需要在所有loader之后执行,则指定enforce为post
// 事实上,我们可以不使用enforce而只要保证loader顺序是正确的即可。配置enfoce的目的是使用模块规则更加清晰,可读性强。

常用loader

处理css的loader

  • css-loader是必需的,它的作用是解析CSS文件,包括解析@import等CSS自身的语法。它的作用仅包括解析CSS文件,它会将解析后的CSS文件以字符串的形式打包到JS文件中。 不过,此时的CSS样式并不会生效,因为需要把CSS文件插入到HTML文件中才会生效

  • style-loader,它可以把JS里的样式代码插入到HTML文件中。它的原理很简单,就是通过JS动态生成style标签并将其插入HTML文件的head标签中

const path = require("path");

module.exports = {
	
	entry: './a.js',
	output: {
		path: path.resolve(__dirname, 'dist'),
		filename: 'bundle.js'
	},
	mode: 'none',
	module: {
		rules: [
			{
				test: /\.css$/,
				// 预处理器的执行顺序是从后向前执行,先执行css-loader,然后把css-loader的执行结果交给style-loader执行
				use: ['style-loader', 'css-loader']
			}
		]
	}
}

babel-loader
  • babel-loader用于处理ES6+ 并将其编译为ES5,它使我们能够在过程中使用最新的语言特性(甚至还在提案中),同时不必特别关心这些特性在不同平台的兼容问题
  • 在安装时推荐使用以下命令: npm install babel-loader @babel/core @babel/preset-env -D babel-loader: 它是使用Babel与Webpack协同工作的模块 @babel/core: 顾名思义,它是Babel编译器的核心模块 @babel/preset-env: 它是Babel官方推荐的预置器,可根据用户设置的目标环境自动添加所需的插件和补丁来编译ES6+代码

在配置babel-loader时有一些需要注意的地方。

rules: [
	{
		test: /\.js$/,
		exclude: /node_modules/,
		use: {
			loader: 'babel-loader',
			options: {
				cacheDirectory: true,
				presets: [[
					'env', {
						modules: false
					}
				]]
			}
		}
	}
]

// 由于该规则中的babel-loader会对所有JS后缀文件生效, 所以我们需要特别排除掉 node_modules目录,否则会令babel-loader编译其中所有的模块,
严重拖慢打包速度,甚至改变第三方模块的原有行为

// 对于babel-loader本身我们添加了cacheDirectory配置项,它会启用缓存机制,在重复打包未改变过的模块时防止二次编译,加快打包速度。
cacheDirectory可以接收一个字符串类型的路径来作为缓存路径,这个值也可以为true,此时其缓存目录会指向node_modules/.cache/babel-loader

// 由于@babel/preset-env会将ES6 Module转化为CommonJS的形式,这会导致Webpack中的tree-shaking特效失效

// 将@babel/preset-env的module配置项设置为false会禁用模块语句的转化,而将ES6 Module的语法交给Webpack本身处理
  • babel-loader支持.bablerc文件读取Babel配置,因此可以将presets和plugins从Webpack的配置文件中提取出来,效果是相同的
  • @babel/preset-react: 转译JSX语法
  • @babel/plugin-transform-runtime: 帮助Babel减少重复代码,缩小资源体积
  • mini-css-extract-plugin: 将样式提取为CSS文件
ts-loader
  • ts-loader与babel-loader的性质类似,它是用于连接Webpack与Typescript的模块,可使用如下命令安装
npm install ts-loader typescript -D

Webpack配置如下:

rules: [
	{
		test: /\.ts$/,
		use: 'ts-loader'
	}
]

需要注意,Typescript本身的配置并不在ts-loader中,而是必须放在工程目录下的tsconfig.json中,如:

{
	"compilerOptions": {
		"target": "es5",
		"sourceMap": true
	}
}

通过Typescript和ts-loader,我们可以实现代码类型检查。

sass-loader

处理Sass样式文件需要使用sass-loader预处理器,使用它需要先安装sass-loader这个npm包。 sass-loader底层依赖于Node Sass或Dart Sass进行处理, 它们对应的npm包的名称分别为node-sass和sass. 因为node-sass包在安装使用过程中容易遇到一些问题,所以我们推荐使用sass这个npm包。

  • sass-loader就是将SCSS语法编译为CSS,因此在使用时通常还有搭配css-loader和style-loader。类似我们安装babel-loader时还要安装babel-core, loader本身只是编译核心库与Webpack的连接器,因此这里处理sass-loader以外,还要安装sass,sass是真正用来编译SCSS的,而sass-loader只是骑到黏合作用

  • Sass有两种书写样式的方式,分别是Sass和Scss

postcss-loader

PostCSS是一个转换CSS的工具,但它本身没有提供具体的样式处理能力。我们可以认为它是一个插件平台,具体的样式处理能力由它转交给专门的样式插件来处理

  • 严格的说PostCSS并不能算是一个CSS预编译器,它只是一个编译插件的容器。它的工作模式是接收样式源代码并交由编译插件处理,最后输出CSS文件。 开发者可以自己指定使用哪些插件来实现特定的功能

  • 在webpack中使用PostCSS,需要安装postcss-loader这个包。在webpack文件里配置处理样式模块规则时,让postcss-loader在css-loader之前处理即可

  • 我们使用postcss-loader最重要的一个功能就是提供CSS样式浏览器厂商前缀,它是通过Autoprefixer来实现的。

npm install autoprefixer -D

  • PostCSS支持的插件非常多,对每一个插件的使用根据其文档配置即可,根据业务需求的不同,可以自行选择

  • 使用postcss-loader可以轻松地将PostCSS与Webpack连接起来。使用npm进行安装: npm install postcss-loader postcss -D

  • postcss-loader可以与css-loader结合使用,也可以单独使用,也就是说不配置css-loader也可以达到相同的效果。 唯一的不同是,单独使用postcss-loader时 不建议使用CSS中的@import语句,否则会产生冗余代码,因此官方推荐将postcss-loader放在css-loader之后使用

  • PostCSS要求必须有一个单独的配置文件,我们需要在项目的根目录下创建一个postcss.config.js。

  • PostCSS 一个最广泛的应用场景就是与Autoprefixer结合,为CSS自动添加厂商前缀。 Autoprefixer是一个样式工具,可以根据caniuse.com上的数据,自动觉得是否 要为某一特性添加厂商前缀,并且可以由开发者为其指定浏览器的范围。

npm install autoprefixer -D

在postcss.config.js中添加autoprefixer

const autoprefixer = require("autoprefixer");

module.exports = {
	plugins: [
		autoprefixer({
			grid: true,
			browsers: [
				'> 1%',
				'last 3 versions',
				'android 4.2',
				'ie 8'
			]
		})
	]
}
file-loader
  • file-loader是一个文件资源预处理器。
  • file-loader的作用,是解析文件导入地址并将其替换成访问地址,同时把文件输出到相应位置(其中文件导入语句包括JS的import...from '...'和CSS的url()); 导入地址包括了JS和CSS等导入语句的地址
  • file-loader 的本质功能是复制资源文件并替换访问地址,因此他不仅可以处理图片资源,还可以处理音视频等资源
  • file-loader用于打包文件类型的资源,并返回其publicPath, 安装命令如下:
npm install file-loader -D

Webpack配置如下:

const path = require("path");
module.exports = {
	entry: './app.js',
	output: {
		path: path.join(__dirname, 'dist'),
		filename: 'bundle.js'
	},
	module: {
		rules: [
			{
				test: /\.(png|jpg|gif)$/,
				use: 'file-loader'
			}
		]
	}
}

// 上面我们对png gif jpg这类图片资源使用file-loader,然后就可以再JS中加载图片了
// webpack打包完成后,dist目录下会生成名为asdfafafadfadf.jpg的图片文件。
// 由于配置中并没有指定output.publicPath,因此这里打印出的图片路径只是文件名,默认为文件的hash值加上文件后缀

import avatarImage from './avatar.jpg'
console.log(avatarImage) // asdfafafadfadf.jpg

  • 添加out.publicPath之后的情况
const path = require("path");
module.exports = {
	entry: './app.js',
	output: {
		path: path.join(__dirname, 'dist'),
		filename: 'bundle.js',
		publicPath: './assets/'
	},
	module: {
		rules: [
			{
				test: /\.(png|jpg|gif)$/,
				use: 'file-loader'
			}
		]
	}
}

// 此时图片路径会成为如下形式:
import avatarImage from './avatar.jpg'
console.log(avatarImage) // ./assets/asdfafafadfadf.jpg

  • file-loader也支持配置文件名以及publicPath(这里的publicPath会覆盖原有的output.publicPath),通过loader的options传入
rules: [
	{
		test: /\.(png|jpg|gif)$/,
		use: {
			loader: 'file-loader',
			options: {
				name: '[name].[ext]',
				publicPath: './another-path/'
			}
		}
	}
]

// 上面的配置会使图片路径成为如下形式:
import avatarImage from './avatar.jpg'
console.log(avatarImage) // ./another-path/asdfafafadfadf.jpg
Asset Modules
  • Asset Module通常被翻译为资源模块,它指的是图片和字体等一类型文件模块,它们无需使用额外的预处理器,Webpack通过一些配置就可以完成对它们的解析。该功能是Webpack 5新加入的,与file-loader等预处理器的功能很像

Asset Moduled 的几个配置项都放在module.rules里,关键配置项叫type,它的值有以下4种:

  1. asset/resource 与之前的file-loader很像,它处理文件导入地址并将其替换成访问地址,同时把文件输出到对应位置
  2. asset/inline 与之前的file-loader很像,它处理文件导入地址并将其替换为data URL,默认是Base64格式编码的URL
  3. asset/source 与raw-loader很像,以字符串形式导出文件资源
  4. asset 在导出单独的文件和dataURL间自动选择,可以通过修改配置项影响自动选择的标准 在资源模块type的取值asset的情况下,Webpack默认对于大于8 KB的资源会以asset/resource的方式处理,否则以asset/inline的方式处理 我们可以修改资源大小的阈值,在module.rule的parser.dataUrlCondition.maxSize中进行配置
  • 通过generator.filename 配置项来配置
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
	mode: "none",
	entry: "./a.js",
	devtool: 'source-map',
	output: {
		path: path.resolve(__dirname, "dist"),
		filename: "[id].js"
	},
	module: {
		rules: [
			{
				test: /\.(png|jpg)$/,
				type: 'asset/resource',
				generator: {
					filename: `static/[hash:8][ext][query]`
				}
			}
		]
	},
	plugins: [
		new HtmlWebpackPlugin({
			title: 'Asset测试',
			template: './index.html'
		})
	]
}

  • 通过output配置资源模块文件名
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
	mode: "none",
	entry: "./a.js",
	devtool: 'source-map',
	output: {
		path: path.resolve(__dirname, "dist"),
		filename: "[id].js",
		assetModuleFilename: 'static/[hash:4][ext][query]'
	},
	module: {
		rules: [
			{
				test: /\.(png|jpg)$/,
				type: 'asset/resource'
			}
		]
	}
}
  • Asset Modules是Webpack 5中新增的功能,用来替换之前使用的file-loader等预处理器
  • webpack5 也提供了另一种方式(asset/resource)来处理文件类型资源,可以用来替代file-loader。并且这种方式是内置的,使用起来更加便捷:
rules: [
	{
		test: /\.(png|jpg|gif)$/,
		type: 'assets/resource'
	}
]

url-loader
  • url-loader的作用于file-loader类似,唯一的不同在于,url-loader允许用户设置一个文件大小的阈值,当大于该阈值时 它会与file-loader一样返回publicPath,而小于该阈值时则会返回base64形式的编码

安装命令:npm install url-loader -D Webpack配置如下:

rules: [
	{
		test: /\.(png|jpg|gif)$/,
		use: {
			loader: 'url-loader',
			options: {
				limit: 10240,
				name: '[name].[ext]',
				publicPath: './another-path/'
			}
		}
	}
]

// url-loader可以接收与file-laoder相同的参数,如name和publicPath等,也可以接收一个limit参数。

// 图片的大小在阈值以下,经过url-loader转化后得到的是base64形式的编码
  • Webpack5也提供了内置的解决方案,可以替代url-loader来处理inline类型的资源
rules: [
	{
		test: /\.svg$/,
		type: 'asset/inline'
	}
]
增强版文件资源预处理器 url-loader
  • url-loader是file-loader的增强版,它除支持file-loader的所有功能外,还增加了Base64编码能力

  • url-loader的特殊能力是可以计算出文件的Base64编码,在文件提交小于指定值(单位为Byte)的时候,可以返回一个Base64编码 data URL来代替访问地址。 使用Base64编码的好处是可以减少一次网络请求,从而提升页面加载速度。

  • url-loader处理图片大小大于limit值的图片的时候,本质上是使用file-loader来进行处理的。

  • file-loader生成的文件默认文件名是"[contenthash].[ext]",contenthash是资源内容的hash值,ext是文件扩展名。我们可以通过设置name项来修改生成文件的名字。

  • file-loader除[contenthash]和[ext]这两个常用的占位符外,还有[hash]和[name]占位符;[hash]也是根据内容计算出的hash值,[name]是文件原始名称

  • file-loader默认使用output.publicPath作为资源访问地址,我们也可以在file-loader的配置项options里配置publicPath参数,它会覆盖output.publicPath

  • url-loader通过设置limit值的大小,当资源大小大于limit值的时候,url-loader使用file-loader来处理多媒体资源,当资源大小小于limit值的时候,url-loader 会极端出图片等多媒体资源的Base64编码,并将其直接打包到生成的JS或CSS文件里。 我们要设置合理的limint值,使大伯后的JS或CSS文件不要过大,也不要过小, 没必要为小于1KB的资源再单独请求一次网络资源。通常会在3~20KB范围内选择一个适合当前项目使用的值

自定义loader

在Webpack工程目录下使用相对路径安装,会在项目的node_moduels中创建一个指向实际force-strict-loader目录的软链,也就是说之后我们可以随时修改loader 源码并且不需要重复安装了

1、 loader初始化 创建一个force-strict-loader目录,然后在该目录下执行npm初始化命令npm init -y 创建index.js,也就是loader主体

module.exports = function(content){
	
	var useStrictPrefix = '\'use strict\';\n\n;
	return `'use strict';\n\n${content}`;
}

在Webpack工程中安装并使用这个loader:npm install ../force-strict-loader -D

  • 在Webpack工程目录下使用相对路径安装,会在项目的node_moduels中创建一个指向实际force-strict-loader目录的软链,也就是说之后我们可以随时修改loader 源码并且不需要重复安装了

Webpack配置

module: {
	rules: [
		{
			test: /\.js$/,
			use: 'force-strict-loader'
		}
	]
}

2、 loader启用缓存

当文件输入和其他依赖都没有发生变化时,应该让loader直接使用缓存,而不是重复进行转换工作。 在Webpack中可以使用this.cacheable进行控制,修改 我们的loader。

  • 通过启用缓存可以加快Webpack打包速度,并且可保证相同的输入产生相同的输出。
// force-strict-loader/index.js

module.exports = function(content){
	
	if(this.cacheable){
		this.cacheable();
	}
	
	var useStrictPrefix = '\'use strict\';\n\n;
	return `'use strict';\n\n${content}`;
}

3、获取options

loader的配置项通过use.options传进来,如:

rules: [
	{
		test: /\.js$/,
		use: {
			loader: 'force-strict-loader',
			options: {
				sourceMap: true
			}
		}
	}
]

上面我们为force-strict-loader传入了一个配置项sourceMap,接下来我们要在loader中获取它。

  • 首先需要安装一个依赖库loader-utils,它主要用于提供一些帮助函数。 在force-strict-loader目录下执行以下命令: npm install loader-utils -D

  • 接着更改loader

force-strict-loader/index.js

var loaderUtils = require("loader-utils");

module.exports = function(content){
	
	if(this.cacheable){
		this.cacheable();
	}
	
	// 获取和打印options
	var options = loaderUtils.getOptions(this) || {};
	console.log('options: ', options)
	
	// 处理content
	var useStrictPrefix = '\'use strict\';\n\n;
	return `'use strict';\n\n${content}`;
}

4、source-map

source-map指的是将编译、打包、压缩后的代码映射回源代码的过程。经过Webpack打包压缩后的代码基本上已经不具备可读性,此时若抛出了一个错误,要想回溯它的调用栈是非常困难的。 而有了source-map,再加上浏览器调试工具,要做到这一点就fico容易;

  • 开启source-map可以便于我们在浏览器的开发者工具中查看源码。 如果loader这里没有对source-map进行处理,最终Webpack也就无法生成正确的map文件,我们在浏览器的开发者工具中看到的源码就可能是错误的。

下面是支持source-map特性后的版本:

var loaderUtils = require("loader-utils");
var sourceNode = require("source-map").SourceNode;
var sourceMapConsumer = require("source-map").SourceMapConsumer;

module.exports = function(content, sourceMap){
	var useStrictPrefix = `'use strict';\n\n`;
	if(this.cacheable){
		this.cacheable();
	}
	
	// 获取和打印options
	var options = loaderUtils.getOptions(this) || {};
	console.log('options: ', options)
	
	
	if(options.sourceMap && sourceMap){
		var currentRequest = loaderUtils.getCurrentRequest(this);
		var node = SourceNode.fromStringWithSourceMap(content, new SourceMapConsumer(sourceMap))
		
		node.prepend(useStrictPrefix)
		
		var result = node.toStringWithSourceMap({ file: currentRequest })
		var callback = this.async();
		
		callback(null, result.code, result.map.toJSON());
	}
	
	// 处理content
	// var useStrictPrefix = '\'use strict\';\n\n;
	return useStrictPrefix + content;
}


// 首先,在loader参数中我们获取到sourceMap对象,这是由Webpack或者上一个loader传递下来的,只有当它存在时我们的loader才能进行继续处理
和向下传递

// 之后我们通过source-map这个库对map进行操作,包括接收和消费之前的文件内容source-map,对内容节点进行修改,最后产生新的source-map

// 在函数返回的时候要使用this.async获取callback函数(主要为了一次性返回多个值)
// callback函数的3个参数分别是抛出的错误、处理后的源码,以及source-map

Webpack插件

Webpack的plugins配置,plugins用于接收一个插件数组,可以使用Webpack内部提供的一些插件,也可以加载外部插件; Webpack为插件提供了各种API,使其可以在打包的各个环节中添加一些额外的任务

  • 插件是在Webpack编译的某些阶段,通过调用Webpack对外暴露出来的API来扩展Webpack的能力的

常用Webpack插件及操作

clean-webpack-plugin

clean-webpack-plugin是一个清除文件的插件。在每次打包之后,磁盘空间都会存有打包后的资源,在再次打包的时候,我们需要先把本地已有的打包后的资源清空, 来减少它们对磁盘空的占用。

copy-webpack-plugin
  • copy-webpack-plugin是用来复制文件的插件。有一些本地资源,如图片和音视频,在打包过程中没有任何模块使用它们,但我们想把它们存放到打包后的资源输出目录下。 copy-webpack-plugin就可以完成文件复制; 在使用copy-webpack-plugin时需要传入参数,该参数是一个对象。使用该插件进行文件复制时,最重要的是要告诉插件,需要 从哪个文件夹复制内容,以及要复制到哪个文件夹去

  • 参数对象的patterns属性就是设置从哪个文件夹复制到哪个文件夹去的。该属性是一个数组,数组每一项是一个对象,对象的from属性用于设置从哪个文件夹复制内容, to属性用于设置复制到哪个文件夹去。

  • 如果要从多个文件夹复制内容,就需要在patterns数组里设置多个对象

html-webpack-plugin插件
  • html-webpack-plugin是一个自动创建HTML文件的插件, 它可以自动引入打包后生成的CSS文件;
  • 要想Webpack配置文件里配置的插件参数title生效,需要修改模板文件
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title><%= htmlWebpackPlugin.options.title %></title> 
</head>

<body>
	<div id="container"></div>
</body>
</html>

// <title><%= htmlWebpackPlugin.options.title %></title>  重点就是这一行啦
mini-css-extrat-plugin

一般来说,在生产环境下,我们希望样式存在于CSS文件而不是style标签中,因为文件更有利于客户端进行缓存。

  • extra-text-webpack-plugin(适用于Webpack 4 之前的版本) 和 mini-css-extrat-plugin(适用于Webpack 4及以上版本),它们就是专门用于提取样式到CSS文件的
  • 使用mini-css-extra-plugin插件时有以下两个关键点:
  1. 它自身带有一个预处理器,在用css-loader处理完CSS模块后,需要紧接着使用MiniCssExtractPlugin.loader这个预处理器

  2. 需要在webpack配置文件的插件列表进行配置,执行new MiniCssExtraPlugin命令时需要传入一个对象,filename表示同步代码里提取的CSS文件名称,chunkFilename表示异步代码里提取的CSS文件名称

// mini-css-extrat-plugin的使用及配置
const path = require('path')
const MiniCssExtratPlugin = require('mini-css-extrat-plugin');
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
	mode: 'development',
	entry: {
		app: './src/app.js',
		vendor: './src/vendor.js'
	},
	output: {
		filename: '[name].js',
		publicPath: '/dist/'
	},
	devServer: {
		host: 'local-ip',
		port: 3000,
		open: true,
	},
	module: {
		rules: [
			{
				test: /\.css$/,
				use: [
					{
						loader: MiniCssExtraPlugin.loader,
						options: {
							publicPath: '../'
						}
					}, 
					'css-loader'
				]
			},
			{
				test: /\.js$/,
				use: 'force-strict-loader'
			}
		]
	},
	plugins: [
		new HtmlWebpackPlugin({ template: './src/index.html' }),
		new MiniCssExtratPlugin({
			filename: '[name].css',
			chunkFilename: '[id].css'
		})
	]
}

资源压缩插件

  • 资源压缩的主要目的是减小文件提交,以提升页面加载速度和降低带宽消耗等,资源压缩通常发生在生成环境打包的最后一个环境
  • 资源压缩主要是对JS和CSS文件进行压缩,常用的方式有把整个文件或大段的代码压缩成一行,把较长的变量名替换成较短的变量名,移除空格等
css-minimizer-webpack-plugin

在Webpack4时期,用来压缩css文件的插件通常是optimize-css-assets-webpack-plugin; Webpack 5推荐使用css-minimizer-webpack-plugin插件对CSS文件进行压缩

const { merge } = require("webpack-merge");
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const common = require("./webpack.common.js");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");


module.exports = merge(common, {
	module: {
		rules: [
			{
				test: /\.(css|scss)$/,
				use: [
					MiniCssExtractPlugin.loader, 
					'css-loader', 
					'postcss-loader', 
					'sass-loader'
				]
			}
		]
	},
	optimization: {
		minimize: true,
		// new TerserPlugin() 这个是对JS的压缩
		minimizer: [new TerserPlugin(), new CssMinimizerPlugin()]
	},
	plugins: [
		new MiniCssExtractPlugin({
			filename: `[name]-[contenthash:8].css`,
			chunkFilename: '[id].css'
		})
	]
});
terser-webpack-plugin

Webpack4之后我们可以使用terser-webpack-plugin插件进行JS文件压缩; 在webpack 5中,安装Webpack时会自动安装 terser-webpack-plugin插件,因此不需要我们单独安装

  • 使用terser-webpack-plugin插件进行JS文件压缩,有两种方案可以选择,
  1. 在wepack配置项plugins里面使用该插件进行压缩
const { merge } = require("webpack-merge");
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const common = require("./webpack.common.js");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");


module.exports = merge(common, {
	module: {
		rules: [
			{
				test: /\.(css|scss)$/,
				use: [
					MiniCssExtractPlugin.loader, 
					'css-loader', 
					'postcss-loader', 
					'sass-loader'
				]
			}
		]
	},
	plugins: [
		new MiniCssExtractPlugin({
			filename: `[name]-[contenthash:8].css`,
			chunkFilename: '[id].css'
		}),
		new TerserPlugin()
	]
});
  1. 通过optimization配置项来配置该插件作为压缩器进行压缩
const { merge } = require("webpack-merge");
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const common = require("./webpack.common.js");
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const TerserPlugin = require("terser-webpack-plugin");


module.exports = merge(common, {
	module: {
		rules: [
			{
				test: /\.(css|scss)$/,
				use: [
					MiniCssExtractPlugin.loader, 
					'css-loader', 
					'postcss-loader', 
					'sass-loader'
				]
			}
		]
	},
	optimization: {
		minimize: true,
		minimizer: [new TerserPlugin()]
	},
	plugins: [
		new MiniCssExtractPlugin({
			filename: `[name]-[contenthash:8].css`,
			chunkFilename: '[id].css'
		})
	]
});

optimization.SplitChunks

optimization.SplitChunks(简称SplitChunks)是Webpack4为了改进CommonChunkPlugin而重新设计和实现的代码分片特性。它不仅比CommonChunkPlugin功能更强大,还更简单易用

module.exports = {
	entry: './foo.js',
	output: {
		filename: 'foo.js',
		publicPath: '/dist/'
	},
	mode: 'development',
	optimization: {
		splitChunks: {
			chunks: 'all'
		}
	}
}

// 使用optimization.SplitChunks替代了CommonChunkPlugin,并指定了chunks的值为all,表示SplitChunks将会对所有的chunks生效(默认情况下SplitChunks只对异步chunks生效,并且不需要配置)
// mode是Webpack4中新增的配置项,可以针对当前所处环境(如开发环境或生产环境)自动添加对应的一些webpack配置
  • splitChunk的默认配置
splitChunks: {
	chunks: 'async',
	minSize: 20000,
	minRemainingSize: 0,
	minChunks: 1,
	maxAsyncRequest: 30,
	maxInitialRequest: 30,
	enforceSizeThreshold: 50000,
	cacheGroups: {
		vendors: {
			test: /[\\/]node_modules[\\/]/,
			priority: -10
		},
		default: {
			minChunks: 2,
			priority: -20,
			reuseExistingChunk: true
		}
	}
}


// 
1) 匹配模式
通过chunks我们可以配置SplitChunks的工作模式。它有3个值,分别为async(默认)、initial和all。
async即只提取异步chunk
initial只对入口chunk生效(如果配置了initial则上面的异步的例子失效)
all同时开启两种模式

2) 匹配条件
minSize minChunks maxAsyncRequests maxInitialRequests都属于匹配条件
- minSize表示提取处理的chunk的最小体积,在Webpack5中默认的值是20000,表示20kb,只有达到这个值时才会被提取
- maxSize表示提取处理的chunk的最大体积,其默认值是0,表示不限制最大体积。它是一个可以违反的值,在被违反时起提示作用
- minChunks 默认值是1,表示拆分前至少被多少个chunks引用的模块才会被提取
- maxAsyncRequest 按需(异步)加载的最大并行请求数,其在Webpack5中的默认值是30,在Webpack4中的默认值是5
- maxInitialRequest 入口点的最大并行请求数,其在webpack 5中的默认值是30,在webpack4中的默认值是3
- cacheGroups 缓存组

3) cacheGroups
可以理解为分离chunks时的规则。默认情况下有两种规则----vendors和default. vendors用于提取所有node_modules中符合条件的模块,default则作用于被多次引用的模块;
我们可以对这些规则进行增加或者修改,如果想要禁用某种规则,也可以直接将其置为false。当一个模块同时符合多个cacheGroups时,则根据其中的priority配置确定优先级。
// cacheGroups 缓存组示例
cacheGroups: {
	toolVendors: {
		test: /(lodash|d3|moment|natty-fetch|util.js)/,
		priority: 25,
		name: 'tool-vendors',
		filename: 'js/tool-vendors.js',
		chunks: 'initial',
	},
	echartsVendor: {
		test: /echarts/,
		priority: 20,
		name: 'echart-vendor',
		filename: 'js/echart-vendor.js',
		chunks: 'initial',
	},
	rcVendors: {
		name: 'rc-vendors',
		filename: 'js/rc-vendors.js',
		test: /(react|react-dom|react-dom-router)/,
		priority: 15,
		chunks: 'initial',
	},
	antdVendors: {
		name: 'antd-vendors',
		filename: 'js/antd-vendors.js',
		test: /(antd|antv)/,
		priority: 12,
		chunks: 'initial',
	},
	vendors: {
		name: 'chunk-vendors',
		filename: 'js/chunk-vendors.js',
		test: /[\\/]node_modules[\\/]/,
		priority: 10,
		chunks: 'initial',
	},
	commons: {
		name: 'chunk-common',
		filename: 'js/chunk-common.js',
		minChunks: 2,
		priority: 5,
		chunks: 'initial',
		reuseExistingChunk: true,
	}
},
webpack-dev-server
  • webpack-dev-server通过开启一个本地服务器来加载构建完成的资源文件,它还有代理请求功能

  • 模块热替换是一个非常强大的功能,它可以在不刷新浏览器页面的情况下,直接替换修改代码部分的页面位置,能有效的提高我们的开发效率

  • 在调试的时候可以通过生成source map文件来观察对应的原始代码。 source map在生产环境下也是可以用的

  • webpack-dev-server是Webpack官方提供的一个Webpack服务估计,一般也称它为DevServer。 安装并启用webpack-dev-server后,它会在本地开启一个网络服务器, 可以用来处理网络请求

  • 在命令行根目录下执行npx webpack serve命令,就启动了webpack-dev-server; 在启动webpack-dev-server时,它会自动帮我们执行webpack 并读取本地的Webpack 配置文件,同时它会启用Webpack的文件监听模式

  • webpack-dev-server服务器默认使用过程目录下的index.html文件作为首页,如果工程根目录下没有index.html, 那服务器则加载的网页信息是工程目录

webpack-dev-server的常用参数: open:是否自动打开浏览器 port: 服务器端口号 compress: 是否为静态资源开启Gzip压缩 publicPath: Web服务器请求资源的路径

webpack-dev-server会自动开启文件监听模式,并且支持浏览器自动刷新功能

  • 模块热替换是不需要重新刷新整个页面,而是通过重新加载修改过的模块来实现实时预览。也被称为模块热更新; 在webpack 5中,将hot参数设置为true, 会自动添加webpack.HotModuleReplacementPlugin插件,不需要我们进行额外配置
webpack-bundle-analyzer

webpack-bundle-analyzer是一个非常有用的 webpack 打包体积优化分析工具,它通过可缩放图像的形式,帮我们分析打包后的资源体积大小,并可以分析该资源由哪些模块组成

  • Stat表示资源文件原始大小
  • Parsed表示经过Webpack基本处理后的资源大小(默认的)
  • Gzipped表示进行Gzip压缩后的资源文件大小
speed-measure-webpack-plugin

打包速度分析工具speed-measure-webpack-plugin帮我们分析Webpack在打包过程中预处理和插件花费的时间;

  • Webpack优化需要关注的除资源组成与大小外,还需要关注打包花费的时间,这关系到开发者的用户体验。
  • speed-measure-webpack-plugin使用只需要在前端项目里安装其npm包,然后在Webpack配置文件里调用其wrap方法即可
HMR原理

在本地开发环境下,浏览器就是客户端,webpack-dev-server(WDS)相当于我们的服务端。HMR的核心就是客户端从服务端拉取更新后的资源(准确地说,HMR拉取的不是整个资源,而是chunk diff), 即chunk需要更新的部分。

  1. 确定浏览器什么时候去来取更新
  • 这需要WDS对本地源文件进行监听。实际上WDS与浏览器之间维护了一个websocket,当本地资源发生变化时,WDS会向浏览器推送更新事件,并带上这次构建的hash,让客户端与上一次资源进行对比。 通过对比hash可以防止冗余更新的出现。因为很多时候源文件更改并不一定代表构建结果的更改(如添加了一个文件末尾空行等);
  1. 拉取什么
  • 有了恰当的拉取资源的时机,下一步就是要知道拉取什么。这部分信息并没有包含在刚刚的websocket中,因为刚刚我们只想知道这次构建的结果是不是和上一次一样。 现在客户端已经知道新的构建结果和当前有所差别,那么它会向WDS发起一个请求来获取更改文件的类别,即哪些模块有了改动。通常这个请求的名字为[hash].hot-update.json

  • 对于一个新项目来说,我们并不总是期望从零开始进行配置。这时就可以使用webpack-cli进行项目的初始化配置。它会询问项目将会使用的各种特性,如包管理器、编写样式的语言、JavaScript处理方案等, 然后更加我们的需求自动生成初始化目录、配置文件已经项目依赖等。 其初始化命令为: webpack-cli init

  • 如果将webpack和rollup进行比较,那么webpack的优势在于它更全面,基于"一切皆模块"的思想而衍生出丰富的可以满足各种场景的loader和plugin; 而Rollup则像一把手术刀,它更专注于javascript的打包。当然Rollup也支持许多其他类型的模块,但是总体而言在通用性上还是不如Webpack.

  • 如果当前项目需求仅仅是打包JavaScript,比如一个JavaScript库,那么Rollup很多时候是我们的第一选择

Webpack打包流程

  1. 执行webpack打包命令 npx webpack

  2. 初始化编译参数,这些参数包括Shell命令的参数和Webpack配置文件中的参数

  3. 通过webpack-cli生成Compiler编译实例(Compiler是一个JS对象,包含了当前打包环境下的所有配置信息)

  4. 插件初始化

  5. 模块处理

  6. 输出打包后的资源

Webpack中的 source map

  • 想要在浏览器里直接看到打包前的代码,就需要使用source-map。 source-map是一个单独的文件,浏览器可以通过它还原出编译前的的原始代码
  • 开启source-map功能很简单,只需要在webpack配置文件里加一行配置就可以啦
// webpack.config.js
devtool: 'source-map'
  • devtool的取值为source-map时会生成单独的source map文件,而取一些其他的值时会把source map直接写到编译打包后的文件里,不过浏览器依然可以通过它还原出编译前的原始代码
开发环境与生产环境source map配置
  1. 开发环境 在开发环境中,我们可以对devtool取值eval-cheap-module-source-map,该配置能保留预处理器前的原始代码信息,并且打包速度也不慢,是一个较佳的选择

  2. 生产环境 生产环境中,我们通常不需要source-map,因为使用source map会泄漏原始代码的风险,除非使用者想要定位线上错误

  • 在webpack 5中,我们通常使用terser-webpack-plugin来压缩JS资源,使用css-minimizer-webpack-plugin来压缩CSS资源。这两个压缩插件支持的source map类型有
  1. source-map
  2. inline-source-map
  3. nosources-source-map
  4. hidden-source-map

source-map比较利于定位线上问题和调试代码,但其他人都可以通过浏览器开发者工具看到原始代码,有严重的安全风险,因此不推荐生产环境中使用这个类型。 基于同样的安全风险考虑,我们也不推荐使用inline-source-map

hidden-source-map是非常安全的选择。这种类型会打包输出完整的source map文件,但打包输出的bundle不会有source map的引用注释,因此在浏览器开发者工具里 看不到原始代码的。想要分享原始代码,我们通常会用一些错误监控系统,将source map文件上传到该系统中,然后通过JS出错后上报的错误信息,用该系统分析出 原始代码的错误堆栈。

需要注意的是,不要将source map文件部署到Web服务器上,而应上传到错误监控系统中。

关于webpack.config.js配置文件

  • 环境变量,指的是设定程序运行环境的一些参数。这里的程序也包括操作系统,操作系统本质上是一个大型程序,在Webpapak使用过程中,会遇到以下两种环境变量:
  1. Node.js环境的环境变量
  2. Webpack打包模块里的环境变量
环境配置的封装

生成环境的配置与开发环境有所不同,比如要设置模式、环境变量,为文件名添加chunkhash作为版本号等。一般来说有以下两种方式: 1、使用相同的配置文件 比如令webpack不管在什么环境下打包都使用webpack.config.js,只是在构建开始前将当前所属环境作为一个变量传进去,然后在webpack.config.js中通过各种判断条件来决定具体使用哪个配置,比如:

// package.json
{
	"scripts": {
		"dev": "webpack-dev-server --env development",
		"build": "webpack --env production",
	}
}


// webpack.config.js
const ENV = process.env.ENV;
const isProd = ENV === 'production';

module.exports = {
	output: {
		filename: isProd ? 'bundle@[chunkhash].js' : 'bundle.js'
	},
	mode: ENV
}

// 通过npm脚本命令传入了一个ENV环境变量,然后由webpack.config.js根据它的值来确定具体采用什么配置

2、为不同环境创建各自的配置文件。比如,我们可以单独创建一个生产环境webpack.production.config.js,开发环境则可以叫webpack.development.config.js,然后修改package.json

{
...
	"scripts": {
		"dev": "webpack-dev-server --config=webpack.development.config.js",
		"build": "webpack --config=webpack.production.config.js"
	}
}
合并配置工具webpack-merge
  • 在package.json文件里配置了两个npm命令,分别对应本地开发环境打包和生产环境打包
"scripts": {
	"dev": "cross-env NODE_ENV=development webpack serve",
	"build": "cross-env NODE_ENV=production webpack"
}
// 当执行npm run dev命令的时候,会将环境变量NODE_ENV设置为development,并开启本地Webpack开发服务。
// 当执行npm run build命令的时候,会将环境变量NODE_ENV设置为production后进行webpack打包

下面是可能出现的问题 Q: 项目启动失败 ,‘cross-env‘不是内部或者外部命令,也不是可运行的程序 A: 1、首先npm install 2、不行的话,就删除了node_modules和package-lock后重新install 3、还是不行的话,看看项目文件夹是不是中文的,改成英文后就可以正常启动项目 4、也还是不行的话,npm install cross-env -g

Q: [webpack-cli] You need to install 'webpack-dev-server' for running 'webpack serve' A: 可能webpack-cli版本过低,重新安装 npm install webpack-cli -D

合并webpack.common.js 是公共配置文件 && webpack.development.js && webpack.production.js
// package.json修改

"scripts": {
	"test": "echo \"Error: no test specified\" && exit 1",
	"dev": "cross-env NODE_ENV=development webpack serve --config webpack.development.js",
	"build": "cross-env NODE_ENV=production webpack --config webpack.production.js"
},
// webpack.common.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');


module.exports = {
	mode: 'none',
	entry: "./a.js",
	output: {
		path: path.resolve(__dirname, "dist"),
		filename: `bundle.js`
	},
	module: {
	},
	plugins: [
		new CleanWebpackPlugin(),
		new HtmlWebpackPlugin({
			template: './index.html',
			title: 'Webpack Study'
		})
	]
}
// webpack.development.js
const { merge } = require("webpack-merge");

const common = require("./webpack.common.js");

module.exports = merge(common, {
	module: {
		rules: [
			{
				test: /\.(css|scss)$/,
				use: ['style-loader', 'css-loader','postcss-loader', 'sass-loader']
			}
		]
	}
});
// webpack.production.js
const { merge } = require("webpack-merge");
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
const common = require("./webpack.common.js");

module.exports = merge(common, {
	module: {
		rules: [
			{
				test: /\.(css|scss)$/,
				use: [
					MiniCssExtractPlugin.loader, 
					'css-loader', 
					'postcss-loader', 
					'sass-loader'
				]
			}
		]
	},
	plugins: [
		new MiniCssExtractPlugin({
			filename: `[name]-[contenthash:8].css`,
			chunkFilename: '[id].css'
		})
	]
});

自定义开发webpack插件与预处理器

自定义Webpack预处理器

预处理器本质上是一个会对外导出函数的Node.js模块,我们使用module.exports来导出一个函数,当Webpack调用该预处理器解析相应的资源时会调用该函数

// src 表示的实际上是文件中内容,用content表示会更加合适一些; 
// Webpack会将初始传递的参数内容转换成字符串;接下来就是常规JS逻辑
module.exports = function(src){

}

导出的函数会接收webpack传递的参数,其第一个参数是资源的内容,链式调用的初始预处理器只会有这一个参数
  • Webpack在使用链式预处理器的最后一个预处理器做处理的时候,其处理结果应该为JS可解释的String或Buffer, 所以使用了module.exports = ${result} 作为返回结果
  • 多个预处理器链式调用时,只有最后一个预处理器需要使用字符串包裹moduele.exports这种模块输出形式的返回值,其他预处理器都是直接返回
module.exports = function(content){
	
	var result = '';
	if(content){
		result = content + 100;
	}
	
	return `module.exports = '${result}'`;
}
  • 预处理器文件处理直接返回内容结果外,也可以使用this.callback()方法; this.callback(null, result)

  • this.callback()是Webpack编译器提供的预处理器API,它最多可以接收4个参数。第1个与第2个参数是必需的,第1个参数可以是Error或null,第2个参数可以是String或Buffer类型的。 第3个与第4个参数是可选的,第3个参数是特殊形式的source map,第4个参数可以是任何值,Webpack不会使用它,开发者可以自定义

  • 自己开发的预处理器也可以支持配置参数,Webpack编译器提供了相应的API,我们可以(在自定义预处理文件中)通过this.query来获取,如:

var path = require("path");

module.exports = {
	mode: 'none',
	entry: './a.js',
	output: {
		path: path.resolve(__dirname, "dist"),
		filename: 'bundle.js'
	},
	module: {
		rules: [
			{
				test: /\.hi$/,
				use: {
					loader: './math-loader.js',
					options: {
						add: true
					}
				}
			}
		]
	}
}
// math-loader.js
module.exports = function(content){
	
	var result = '';
	if(this.query.add === true){
		result = content + 100;
	}
	
	return `module.exports = '${result}'`;
}
Webpack插件开发

Webpack插件的开发也是基于事件机制进行的,插件会监听Webpack构建过程中的某些节点,并做相应的处理; 一个简单的Webpack插件结构如下:

class HelloPlugin {
	// 构造方法
	constructor(options){
		// options就是从webpack.config.js中传递进来的配置项
		console.log(options)
	}
	
	
	apply(compiler){
		compiler.hooks.done.tap('HelloPlugin', ()=>{
			console.log('HelloPlugin')
		})
	}
}


module.exports = HelloPlugin;

// 我们在Webpack配置文件里配置插件时,是使用new命令实例化一个构造函数来获取实例对象的。因此,我们通常用ES6的class类来定义一个Webpack插件,在内部通过constructor构造方法可以获取插件参数
// apply 方法在插件初始化时会被Webpack编译器调用一次,其方法参数就是Webpack编译器的Compiler对象引用。
// 在apply内部,我们通过Compiler的Hook对象上的方法注册回调函数,以便在Webpack特定的编译时机执行特定任务。
// compiler.hooks是一个由tapable扩展而来的对象,它支持非常多的事件钩子。 上面代码中tap方法的第一个参数表示插件名称,第二个参数是回调函数,在回调函数里可以获取Compilation对象

关于Webpack性能

在使用Webpack将代码打包到线上生产环境的时候,我们需要观察打包后的资源大小是否合适,如果文件太大,就需要减小其体积以便提升页面加载速度

  • 在Webpack配置文件里,可以用perfomance配置项进行性能提示;例如: 如果一个资源超过512KB,Webpack会输出一个警告来通知使用者

Performance有是个参数: hintsmaxEntryPointSizemaxAssetSizeassetFilter

  • hints 用来配置Webpack如何提示信息。它有三种可配置值,warning error false; 默认为warning; 当配置为warning或error时,Webpack会进行警告或错误提示;若配置为false时, 则不进行信息提示

  • maxEntryPointSize 用来配置Webpack 入口资源的最大体积,超过该值就会进行信息提示,默认为 250 000 单位是Byte,即250KB

  • maxAssetSize 用来配置Webpack打包资源的最大体积,超过该值就会进行信息提示,默认为 250 000 单位是Byte,即250KB

  • assetFilter 用来配置哪些文件会被webpack进行性能提示,该参数值是一个函数,默认值如下,一般不需要修改:

function assetFilter(assetFiltername){
	return !/\.map$/.test(assetFiltername)
}
代码分片

实现高性能应用的重要的一点就是尽可能地让用户每次只加载必要的资源,对于优先级不太高的资源则采用延迟加载等技术渐进式获取,这样可以保证页面的首屏加载速度。

  • 代码分片是Webpack作为打包工具所特有的一项技术,通过这项技术,我们可以把代码按照特定的形式进行拆分,使用户不必一次加载全部代码,而是按需加载;
资源异步加载
  • 资源异步加载主要解决的问题是,当模块数量过多、资源体积过大时,可以延迟加载一些暂时使用不到的模块。这样可以使用户初次渲染的时候下载尽可能小的资源,等到恰当的时机再去触发加载后续的模块。 因此一般把这种方法叫做按需加载

  • 在Webpack中有两种异步加载的方式----import函数和require.ensure。require.ensure是Webpack 1支持的异步加载模块;import函数从Webpack 2开始引入,并得到官方推荐,这里只介绍这个

  • 与正常ES6的import语法不同,通过import函数加载的模块及其依赖会被异步地加载,并返回一个Promise对象

  • output.chunkFilename用来指定异步chunk的文件名。

optimization.splitChunks

代码分割是webpack优化中非常重要的一部分,webpack里主要有三种方法进行代码分割:

  1. 入口entry: 配置entry 入口文件,从而手动分割代码 import('xxxx')

  2. 动态加载:通过import等方法进行按需加载

  3. 抽取公共代码: 使用splitChunks等技术抽取公共代码

  • splitChunks指的是Webpack插件SplitChunksPlugin,在Webpack的配置项optimization.splitChunks里面直接配置即可,无须单独安装
  • 在webpack4之前,webpack是通过CommonChunkPlugin插件来抽取公共代码的; Webpack4之后使用的是SplitChunksPlugin插件
  • splitChunks的配置参数非常多,以下是该插件的默认值:
module.exports = {
	optimization: {
		splitChunks: {
			chunks: 'async',
			minSize: 20000,
			minRemainingSize: 0,
			minChunks: 1,
			maxAsyncRequest: 30,
			maxInitialRequests: 30,
			enforceSizeThreshold: 50000,
			cacheGroups: {
				defaultVendors: {
					test: /[\\/]node_modules[\\/]/,
					priority: -10,
					reuseExistingChunk: true
				},
				default: {
					minChunks: 2,
					priority: -20,
					reuseExistingChunk: true
				}
			}
		}
	}
}
配置预处理器的exclude与include 缩小查找范围

在预处理器解析模块时,有两个配置项可以额外配置: exclude 与 include; exclude可以排除不需要该预处理器解析的文件目录,include可以设置该预处理器 只对哪些目录有效,这样可以减少不需要预处理器处理的文件模块,从而提升构建速度

module.exports = {

	module: {
		rules: [
			{
				test: /\.js$/,
				// 将node_modules目录排除在babel-loader的处理范围外
				exclude: /node_modules/,
				use: {
					loader: 'babel-loader'
				}
			}
		]
	}
}
module.noParse 忽略部分模块

在进行前端开发时,有些模块不需要被任何处理器解析,例如:jQuery与Lodash这一类工具库。 通过配置module.noParse可以告诉Webpack这些模块不需要被解析处理,保留这些模块原始的样子即可

  • 被忽略的模块中不应该有import 和 require等任何模块导入的语法,即这些被忽略的模块不能依赖于其他模块;
  • 需要注意的是,虽然module.noParse指定的模块不会被解析,但使用的模块任然会被打包到bundle.js里
  • module.npParse的取值可以是字符串、正则表达式和数值
module.exports = {
	module: {
		noParse: /jquery|lodash/
	}
}
resolve.extensions 缩短匹配过程
  • resolve.extensions 用于Webpack匹配文件后缀名。 在我们引入其他模块时,有时候没有写后缀名
import { name } from './b';

更准确的写法应该是

import { name } from './b.js';
  • resolve.extensions 是Webpack识别不带后缀名文件的关键,Webpack会尝试使用resolve.extensions指定的后缀名来解析文件。
  • resolve.extensions 的值是一个数组,Webpack会按数组元素从头到尾的顺序尝试解析,如果匹配到文件,则会使用该文件并跳过剩下后缀名的匹配,如:
resolve: {
	extensions: ['.js', '.json', '.jsx']
}

// 在解析 import { name } from './b' 时,会首先寻找'./b.js'文件,如果找到就使用'./b.js',并结束对'./b'的查找; 如果没有找到'./b.js',则尝试寻找'./b.json'文件,以此类推
  • 从上面的搜寻过程可以看出,如果resolve.extensions数组项数量越多并且靠前的后缀名没匹配到,那么Webpack尝试搜寻的次数就越多,这会影响Webpack的解析速度, 因此需要合理配置resolve.extensions. 合理配置resolve.extensions有以下两个关键点:
  1. 出现频率高的后缀名放在数组签名,以便尽快结束匹配过程,通常会把'.js'放在第一项

  2. 缩短数组长度,用不到的后缀名不要放到数组中

另外,在我们写代码的过程中,模块导入语句中应尽量带上后缀名,这样可以避免匹配过程

使用Tree Shaking摇树优化

摇树优化是Webpack里非常重要的优化措施,它的优化效果在Webpack 5中得到了进一步提升; Tree Shaking可以帮我们检测模块中没有用到的代码,并在Webpack打包时将没有使用到的代码移除,减小打包后的体积。 它的名字也非常形象,通过摇晃树把树上干枯的叶子摇掉;

  • 使用Tree Sharking一共分为两个步骤
  1. 标注未使用的代码
var path = require("path");

module.exports = {
	mode: 'none',
	entry: './a.js',
	output: {
		path: path.resolve(__dirname, "dist"),
		filename: 'bundle.js'
	},
	optimization: {
		// 这一句
		usedExports: true
	}
}
  1. 对未使用的代码进行删除;若需要删除未使用的代码,使用Webpack5自带的TerserPlugin即可完成该操作
var path = require("path");
var TerserPluin = require("terser-webpack-plugin");

module.exports = {
	mode: 'none',
	entry: './a.js',
	output: {
		path: path.resolve(__dirname, "dist"),
		filename: 'bundle.js'
	},
	optimization: {
		usedExports: true,
		minimize: true,
		minimizer: [ new TerserPluin() ]
	}
}

Webpack中的缓存

在使用Webpack开发前端过程时,涉及的缓存主要有两类: 1、访问Web页面时的浏览器缓存,我们称之为长期缓存(为了提示用户体验而设计的)

2、Webpack构建过程中的缓存,我们称之为持久化缓存或编译缓存(为了提升Webpack构建速度进而提升开发体验)

  • 在Webpack5之前,Webpack自身没有提供持久化缓存,开发时经常需要使用cache-loader或dll动态链技术来做缓存方面的处理;

  • Webpack5提供了持久化缓存,它通过使用文件系统缓存,极大地减少了再次编译的时间

  • 在webpack5中,使用文件系统缓存是非常容易的,只需要在Webpack的配置文件中增加如下配置即可:

module.exports = {
	cache: {
		type: 'filesystem'
	}
}

// 配置项cache用于对Webpack进行缓存配置,当把cache.type设置为filesystem时就开启了文件系统缓存
// 也可以将cache.type设置为memory,表示会打包生成的资源存放于内存中。
  • cache的值除了对象类型外还支持布尔值。在开发模式下,cache的默认值是true,这与将cache.type设置为memory的效果是一致的。

  • 在生产环境下,cache默认值是false,会禁用缓存

  • 文件系统缓存的使用极大地减少了再次编译所消耗的时间

tapable

Webpack是建立于插件系统之上的事件流工作系统,而插件系统是基于tapable实现的。

  • tapable通过观察者模式实现了事件监听与触发,这和Node.js里的EventEmitter事件对象很像,但tapable更适合Webpack
  • tapable是npm包对外提供了许多Hook(钩子)类,这些Hook类可以生成Hook实例对象

Module Federation

Webpack 5引入的一个重要特性就是Module Federation,它使得JS应用可以动态调用其他JS应用中的代码,从而解决多个应用代码共享的问题