前端模块化成为了主流的今天,离不开各种打包工具的贡献。webpack就是杰出代表!以下是官网对webpack介绍图。文章主要介绍概念思路,简单配置,更齐全配置一定要到官网查阅。
进入学习之前建议先了解一下:npm使用与package.json说明
|
webpack是什么 |
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
Webpack是一个开源的JavaScript模块打包工具,其最核心的功能是解决模块之间的依赖,把各个模块按照特定的规则和顺序组织在一起,最终合并为一个或多个bundle。这个过程就叫作模块打包。一般而言,一个入口对应一个bundle,但一个入口也可产出多个bundle(比如:使用了样式分离插件)。对于webpack来说,所有的资源(.js、.css、.png)都是module。
webpack与node或npm的关系 |
npm是于Node社区中产生的,是nodejs的官方包管理工具 , webpack是npm生态中的一个模块 。webpack是用来解决JavaScript模块之间的依赖,npm用来解决包之间依赖和版本的控制。
模块打包的作用 |
为什么不直接使用普通js等资源文件呢?干嘛要打包起来?
- 减少了网络请求连接
普通html文档引入了多个外部资源文件,那将会建立多个http请求连接,每个连接成本都是很大的。模块打包后,可以将多个资源文件合并成一个资源文件,从而减少了http请求。 - 解决模块间的依赖关系
html文档中引入多个js文件,假如某两个js文件存在上下依赖关系,只能人为控制先引入哪个js文件后引入哪个js文件,这样很容易导致不清楚具体代码逻辑的人搞错。模块打包工具会分析模块间关系,帮我们维护这些依赖关系。 - 多个模块之间的作用域是隔离的,彼此不会有命名冲突
每个script标签中,顶层作用域即全局作用域,如果没有任何处理而直接在代码中进行变量或函数声明,就会造成全局作用域的污染。
模块打包工具的工作方式 |
- 将存在依赖关系的模块按照特定规则合并为单个JS文件,一次全部加载进页面中。
- 在页面初始时加载一个入口模块,其他模块异步地进行加载。
webpack相比于其他模块打包工具有何特长 |
- Webpack默认支持多种模块标准,包括AMD、 Commonjs,以及最新的ES6模块,而其他工具往往只能兼容一到两种。
- Webpack有完备的代码分割( code splitting)解决方案。可以分割打包后的资源,首屏只加載必要的部分,不太重要的功能放到后面动态地加载。提升首页渲染速度。
- Webpack可以处理各种类型的资源。除了 Javascript以外, Webpack还可以处理样式、模板,甚至图片等。
- Webpack拥有庞大的社区支持。除了 Webpack核心库以外,还有无数开发者来为它编写周边插件和工具。
|
有几个概念有必要先了解,方便更好的了解webpack。先看一个例子:
关键文件目录结构
project
├── src/
| ├── index.css
| ├── index.js
| ├── common.js
| └── utils.js
└── webpack.config.js
文件内容
//index.css
body {background-color: red;}
//utils.js
export function square(x) {
return x * x;
}
//common.js
const {log}=console;
module.exports = {log}
//index.js
import './index.css'
import {log} from './common.js'
配置文件 webpack.config.js (看不懂没关系,后文会讲)
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports={
entry: {
index: "./src/index.js",
utils: './src/utils.js',
},
output: {
filename: "[name].bundle.js", // 输出 index.bundle.js 和 utils.bundle.js
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader', // css-loader 负责解析 CSS 代码, 处理 CSS 中的依赖
],
},
]
},
plugins: [
// 用 MiniCssExtractPlugin 抽离出 css 文件
new MiniCssExtractPlugin({
filename: '[name].bundle.css' // 输出的 css 文件名为 index.css
}),
]
}
编译
产出文件目录结构
project
├── dist/
├── index.bundle.css
├── index.bundle.js
└── utils.bundle.js
以下几个是官网说的核心概念:
入口(entry) |
资源打包的入口,Webpack从这里开始进行模块依赖的查找。指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。
输出(output) |
output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认输出路径为 ./dist
。
预处理器(loader) |
loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
主要属性:
- test ,接受正则表达式,用于标识出需要被转换文件。
- use ,表示进行转换时,应该使用哪个 loader。
插件(plugins) |
loader 被用于转换某些文件类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件可以完成loader完成不了的任务。
更多概念:module chunk bundle |
module |
对于webpack来说,所有的资源(.js、.css、.png)都是module。webpack能直接处理的只有js文件,其他文件需要借助loader或者plugin。
chunk |
chunk字面的意思是代码块,是webpack内部运行时的概念,在Webpack中可以理解成被抽象和包装过后的一些模块。
chunk是webpack根据功能拆分出来的,包含三种情况:
- 项目入口(entry)
- 通过
import()
动态引入的代码 - 通过
splitChunks
拆分出来的代码
bundle |
由chunk得到的打包产物称之为bundle。一般就是和chunk是一对一的关系,bundle就是对chunk进行编译压缩打包等处理之后的产出。
图片来源:卤蛋实验室
一般来说一个 chunk 对应一个 bundle,比如上图中的 utils.js -> chunks 1 -> utils.bundle.js;
但也有例外,比如上述例子中, MiniCssExtractPlugin 从 chunks 0 中抽离出了 index.bundle.css 文件。
module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:
我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。
|
安装webpack |
全局安装 |
npm install webpack webpack-cli -g
项目合作的时候,在自己电脑测试数据运行正常,换到别人电脑,假如webpack版本不一致,可能导致输出不一样。
项目内安装 |
npm install webpack webpack-cli --save-dev
//npm i webpack webpack-cli -D
通过 webpack-cli 可以使用终端配置 webpack,一般一起安装使用。
--save-dev
把依赖保存在开发环境, i
是 install
简写 -D
是--save-dev
简写
建立第一个webpack项目 |
初始化项目 |
mkdir webpack-app
cd webpack-app
npm init -y
-y
可选参数,可以让你跳过对项目信息的填写
安装webpack |
npm i webpack webpack-cli -D
创建两个有依赖关系的js模块(暂且在根目录创建) |
有依赖关系是为了方便查看模块打包后的效果。
//add_context.js
export default function(){
document.write('Hello Webpack');
}
//index.js
import addContent from './add_content.js'
document.write('Hello Joe<br/>');
addContent();
目前项目结构如下:
project
├── node_modules/
├── add_content.js
├── index.js
├── package.json
└── package-lock.json
进行模块打包 |
//入口文件为index.js 输出的整合文件名为bundle.js 运行模式为开发模式
npx webpack --entry=./js/index.js --output-filename=bundle.js --mode=development
npx
在node环境执行 webpack二进制文件(软件) 后面的就是对webpack的配置参数。npx webpack --help
查看更多的webpack配置参数。
执行完,根目录下生成一个 dist 文件夹,里面装有 bundle.js 文件。
新建html并引入bundle.js看页面效果 |
根目录新建index.html
<body>
<script src="./dist/bundle.js"></script>
</body>
打开网页,你会看到Hello Joe Hello Webpack。
不想每次启动都写这么多:
package.json
"scripts": {
"build": "webpack --entry=./index.js --output-filename=bundle.js --mode=development"
},
以后就可以 npm run build
这样启动
创建一个规范化的webpack项目 |
1.规范工程目录
//对上面入门案例文件目录进行修改 只写了关键几个
project
├── dist/ (存放项目的输出文件)
├── node_module/ (node资源文件)
├── src/ (存放项目的资源文件)
| ├── index.js (webpack默认源代码的入口文件)
| └── add_content.js
├── index.html
├── package.json (项目的描述文件 main:项目入口文件 dependencies:生产依赖 devDependencies:开发依赖)
├── package-lock.json
└── webpack.config.js (webpack默认配置文件)
2.添加webpack配置文件
//webpack.config.js
module.exports={
entry:'./src/index.js',
output:{
filename:'bundle.js',
},
mode:'development',
}
3.修改package.json中对webpack的启动脚本
"build": "webpack"
如此,webpack将会自动寻找根目录下的webpack.config.js文件来启动webpack!之后npm run build
即可。
webpack-dev-server 拒绝每次调试都要编译一次 |
之前每次改变资源文件都需要重新编译一次,甚是麻烦!
webpack-dev-serer是一个便捷的本地开发工具 ,有—项很便捷的特性就是live-reloading(自动刷新)。
1.安装
npm install webpack-dev-server --save-dev
2.配置到package.json的启动脚本中
"scripts": {
"build": "webpack",
"dev":"webpack-dev-server"
},
3.webpack配置文件添加对webpack-dev-server的配置
module.exports={
entry:'./src/index.js',
output:{
filename:'bundle.js',
},
mode:'development',
devServer:{
publicPath:'/dist'
}
}
4.启动看效果
rpn run dev
可见服务发布在本地8080端口,需要通过该端口访问才能看到自动刷新的效果,直接打开index.html
文件是没有这个效果的。因为webpack-dev-server并没有打包输出,而是将编译结果写在内存中,方便迅速刷新调试。
启动报错 Cannot find module 'webpack-cli/bin/config-yargs’
貌似高版本的webpack-cli没有config-yargs模块了,我把它卸载npm uninstall webpack-cli
了,然后安装了npm i webpack-cli@3.3.9 -D
版本即可!
|
资源处理流程 |
在一切流程的最开始,我们需要指定一个或多个入口(entry),也就是告诉Webpack具体从源码目录下的哪个文件开始打包。如果把工程中各个模块的依赖关系当作一棵树,那么入口就是这棵依赖树的根。这些存在依赖关系的模块会在打包时被封装为—个chunk,打包后生成的就是bundle。
配置资源入口 |
webpack通过context
和entry
这两个配置项来共同决定入口文件的路径。在配置入口时,实际上做了两件事:
- 确定入口模块位置,告诉Webpack从哪里开始进行打包。
- 定义chunk name。如果工程只有一个入口,那么chunk 默认名为“main”; 如果工程有多个入口,我们需要为每个入口定义chunk name,来作为该chunk的标识。一般而言,chunk name也是bundle name,但bundle name 往往重新定义输出。
context |
配置context的主要目的是让entry的编写更加简洁,尤其是在多入口的情况下。context可以省略,默认值为当前工程的根目录。context+entry的路径拼接成资源的完整路径。
entry |
主讲数组类型和对象类型入口。
📌对象形式
如果想要定义多入口,则必须使用对象的形式。对象的属性名(key)是chunk name,属性值(value)是入口路径。
module.exports={
entry:{
index:'./src/index.js',
other:'./src/other.js'
}
}
📌数组形式
传入一个数组的作用是将多个资源预先合并,在打包时webpack会将entry数组中的最后一个元素作为实际的入口路径。
module.exports={
entry:['./src/other_entry.js','./src/index.js'],
}
等价于
//index.js
import other_entry.js
module.exports={
entry:'./src/index.js',
}
优化资源入口 vendor |
vendor是用来提取公共且很少发生变化的模块,在webpack中vendor一般指的是工程所使用的库、框架等第三方模块集中打包而产生的bundle。
若工程只产生一个JS文件并且它的体积很大,一旦产生代码更新,用户都要重新下载整个资源文件,这对于页面的性能是非常不友好的。
entry:{
app:'./src/index.js',
vendor:['react','react-dom','react-router'],
},
通过这样的配置,app bundle将只包含业务模块,其依赖的第三方模块将会被抽取出来生成一个新的bundle。由于vendor仅仅包含第三方模块,这部分不会经常变动,因此可以有效地利用客户端缓存,在用户后续请求页面时会加快整体的渲染速度。
配置资源出口 |
filename |
//多入口多输出
module.exports={
entry:{
index:'./src/index.js',
other:'./src/other.js'
}
output:{
filename:'[name].js',
},
}
[name]
会被代替为chunk name,还有以下几种模板变量也可以部署到filename配置中。
上述变量一般有如下两种作用:
- 区分chunk。
- 控制客户端缓存。表中的[hash]和[chunkhash]都与chunk内容直接相关,在filename中使用了这些变量后,当chunk的内容改变时,可以同时引起资源文件名的更改,客户端检查发现请求文件chunkhash发生变化就下载新文件。[query]也可以起到类似的效果,只不过它与chunk内容无关,要由开发者手动指定。
如果要控制客户端缓存,最好还要加上[chunkhash],因为每个chunk所产生的[chunkhash]只与自身内容有关,单个chunk内容的改变不会影响其他资源,可以最精确地让客户端缓存得到更新。
hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值。采用hash计算的话,每一次构建后生成的哈希值都不一样,即使文件内容压根没有改变。这样子是没办法实现缓存效果
output:{
filename:'[name].[chunkhash].js',
},
path |
用来指定资源输出的位置,要求必须是绝对路径.
输出位置:打包完成后资源产生的目录,一般将其指定为工程中的dist目录。
output:{
filename:'[name].js',
path:path.resolve(__dirname,'dist'),
},
在Webpack 4之后,output.path
已经默认为dist目录,除非我们需要更改它,否则不必单独配置。
publicPath |
用来指定资源的请求位置.(默认值为空字符串''
)
请求位置:由JS或CSS所请求的间接资源路径。
output中的publicPath
用于给生成的静态资源路径添加前缀,也就是会在打包生成的html文件里面引用资源路径中添加前缀。
📌 页面中的资源分为两种 :直接资源 间接资源
直接资源:由HTML页面直接请求的,比如通过<script>
标签加载的JS;
间接资源:由JS或CSS请求的,如异步加载的JS、从CSS请求的图片字体等。
publicPath的作用就是指定这部分间接资源的请求位置。
首屏加载的JS资源地址是通过页面中的<script>
来指定的,而间接资源(通过首屏JS再进一步加载的JS)的位置则要通过output.publicPath
来指定。比如import('./bar.js')
使bar.js
成为了一个间接资源,我们需要配置publicPath
来告诉Webpack去哪里获取它。
publicPath有3种形式:
1.html相关的
与HTML相关,也就是说我们可以将publicPath
指定为HTML的相对路径,在请求这些资源时会以当前页面HTML所在路径加上相对路径,构成实际请求的URL。
// 假设当前HTML地址为 https://example.com/app/index.html
// 在该页面异步加载的资源名为 0.chunk.js
publicPath: "" // 实际路径https://example.com/app/0.chunk.js
publicPath: "./js" // 实际路径https://example.com/app/js/0.chunk.js
publicPath: "../assets/" // 实际路径https://example.com/aseets/0.chunk.js
2.host相关的
若publicPath
的值以/
开始,则代表此时publicPath是以当前页面的host name为基础路径的。
// 假设当前HTML地址为 https://example.com/app/index.html
// 异步加载的资源名为 0.chunk.js
publicPath: "/" // 实际路径https://example.com/0.chunk.js
publicPath: "/js/" // 实际路径https://example.com/js/0.chunk.js
publicPath: "/dist/" // 实际路径https://example.com/dist/0.chunk.js
3.CDN相关的
绝对路径的形式,一般发生于静态资源放在CDN上面时,因为域名不一样
// 假设当前页面路径为 https://example.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
拓展:devServer 中 publicPath |
webpack-dev-server下也有publicPath配置,是针对webpack-dev-server的静态资源服务路径。
devServer里面的publicPath
表示的是此路径下的打包文件可在浏览器中访问,若是devServer里面没有设置publicPath,则会认可是output里面设置的publicPath的值。
const path = require('path');
module.exports = {
entry: './src/app.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
},
devServer: {
publicPath: '/assets/',
port: 3000,
},
};
启动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保持一致,这样在任何环境下资源输出的目录都是相同的。
|
一个Web工程通常会包含HTML、JS、CSS、模板、图片、字体等多种类型的静态资源,并且这些资源之间都存在着某种联系。比如,JS文件之间有互相依赖的关系,在CSS中可能会引用图片和字体等。对于Webpack来说,所有这些静态资源都是模块,可以像加载一个JS文件一样去加载它们。
loader |
装载器(loader),它赋予了Webpack可处理不同资源类型的能力,如HTML、CSS、模板、图片、字体等,极大丰富了其可扩展性。
webpack本身只认识JavaScript,对于其他类型的资源必须预先定义一个或多个loader对其进行转译,输出为webpack能够接收的形式再继续进行,因此loader做的实际上是一个预处理的工作。loader本身只是编译核心库与webpack的连接器,有时还需要为loader补充额外的库。类似于我们装babel-loader时还要安装babel-core;编译sass除了sass-loader以外还要安装node-sass,node-sass是真正用来编译SCSS的,而sass-loader只是起到黏合的作用。
loader 执行顺序 |
webpack中的loader按照执行顺序可分为pre、inline、normal、post四种类型,我们直接定义的loader都属于normal类型(从后往前),inline形式官方已经不推荐使用,而pre和post则需要使用
enforce
配置项来指定。
module:{
rules:[{
test:/\.css$/,
use:['style-loader','css-loader'],
}]
}
use
数组是逆序加载的,因此要把最后生效的loader放在use
数组最前面。
loader 的使用案例 |
比如:在js文件引入css文件
import './style.css';
安装css-loader |
为了能在js文件引入css模块,第一步要把css-loader加到工程中。
npm i css-loader -D
配置loader,将css-loader引入工程中 |
module:{
rules:[{
test:/\.css$/,
use:['css-loader'],
}]
}
与loader
相关的配置都在module
对象中,其中module.rules
代表了模块的处理规则。test
可接收一个正则表达式,use
可接收一个数组,数组包含该规则所使用的loader。/\.css$/
匹配所有以.css结尾的文件。
此时,CSS的样式仍然没有在页面上生效。这是因为css-loader的作用仅仅是处理CSS的各种加载语法(@import
和url()
函数等),如果要使样式起作用还需要style-loader来把样式插入页面。
把style-loader加到工程 |
npm i style-loader -D
将style-loader引入工程中 |
module:{
rules:[{
test:/\.css$/,
use:['style-loader','css-loader'],
}]
}
把style-loader加到了css-loader前面,这是因为在webpack打包时是将资源按照use
数组逆序传给loader处理的,因此要把最后生效的放在前面。否则报错。
npm run build
模块打包,发现生成的 dist 文件夹里并没有css文件,因为样式一并打包到入口的bundle.js中了。原本你在普通html文档中,只要引入这个bundle.js 就可同时兼具样式的效果了。
样式预处理 |
样式预编译语言,如Sass(scss)、Less等。
sass,less编译后可生成css。
sass |
1.安装
npm install sass-loader node-sass
发现node-sass下载太慢,后面直接就是连不上,将node-sass库设为阿里源的node-sass库!
npm config set sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
2.配置
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
}
],
},
3.测试运行
//style.scss
$color='red'
body{
color: $color;
}
//index.js
import './style.scss'
打包后生成的css文件
body{
color: ‘red’;
}
less |
npm install less-loader less
其他步骤雷同
sourceMap |
背景:我们在打包中,将开发环境中源代码经过压缩,去空格,babel编译转化,最终可以得到适用于生产环境的项目代码,这样处理后的项目代码和源代码之间差异性很大,会造成无法debug的问题。
生产环境的代码都是经过压缩处理等的,调试的时候只能定位到压缩处理后的代码的位置,无法定位到开发环境中对应的源代码所在位置。
sourcemap就是为了解决上述代码定位的问题,简单理解,就是构建了处理前的代码和处理后的代码之间的桥梁。主要是方便开发人员的错误定位。这里的处理操作包括:
I)压缩,减小体积
II)将多个文件合并成同一个文件
III)其他语言编译成javascript,比如TypeScript和CoffeeScript等
假如我们想要在浏览器的调试工具里查看源码,需要分别为sass-loader和css-loader单独添加source map的配置项。
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
sourceMap: true,
},
}, {
loader: 'sass-loader',
options: {
sourceMap: true,
},
}
],
}
],
loader的其他配置项 |
exclude include |
exclude
与include
是用来排除或包含指定目录下的模块,可接收正则表达式或者字符串(文件绝对路径),以及由它们组成的数组。
module:{
rules:[{
test:/\.css$/,
use:['style-loader','css-loader'],
exclude:/node_modules/
}]
}
node_modules模块不会执行这条规则,避免遍历该模块,加快打包速度.
同时配置exclude include ,exclude 优先级高
resource issuer |
在Webpack中,被加载模块是resource,而加载者是issuer。
// index.js
import './style.css';
resource为/path/of/app/style.css,issuer是/path/of/app/index.js。
配置项test、exclude、include本质上属于对resource也就是被加载者的配置。
只有在/src/pages/目录下面的JS文件引用了CSS文件,这条rule才会生效
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
exclude: /node_modules/,
issuer: {
test: /\.js$/,
include: /src/pages/,
},
}
],
上述可读性较差,以下是等价的形式
{
use: ['style-loader', 'css-loader'],
resource: {
test: /\.css$/,
exclude: /node_modules/,
},
issuer: {
test: /\.js$/,
exclude: /node_modules/,
},
}
],
enforce |
webpack中的loader按照执行顺序可分为pre、inline、normal、post四种类型,上面我们直接定义的loader都属于normal类型(从后往前),inline形式官方已经不推荐使用,而pre和post则需要使用enforce来指定。
enforce用来指定一个loader的种类,只接收“pre”或“post”两种字符串类型的值。
“pre”,代表它将在所有正常loader之前执行
“post”,代表在所有loader之后执行
rules: [
{
test: /\.js$/,
enforce: 'pre',
use: 'eslint-loader',
}
],
在配置中添加了一个eslint-loader来对源码进行质量检测,其enforce的值为“pre”,代表它将在所有正常loader之前执行。
常用loader介绍 |
babel-loader |
npm install babel-loader @babel/core @babel/preset-env -D
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: [[
'env', {
modules: false,
}
]],
},
},
}
],
由于@babel/preset-env
会将ES6 Module转化为CommonJS的形式,这会导致Webpack中的tree-shaking
特性失效,将@babel/preset-env
的modules配置项设置为false会禁用模块语句的转化,而将ES6 Module的语法交给Webpack本身处理。
babel-loader支持从.babelrc
文件读取Babel配置,因此可以将presets和plugins从Webpack配置文件中提取出来,也能达到相同的效果。
html-loader |
html-loader用于将HTML文件转化为字符串并进行格式化,这使得我们可以把一个HTML片段通过JS加载进来。
npm install html-loader -D
rules: [
{
test: /\.html$/,
use: 'html-loader',
}
],
// header.html
<header>
h1>This is a Header.</h1>
</header>
// index.js
import headerHtml from './header.html';
document.write(headerHtml);
file-loader |
file-loader用于打包文件类型的资源(图片等),并返回其publicPath。
npm install file-loader -D
rules: [
{
test: /\.(png|jpg|gif)$/,
use: 'file-loader',
}
],
上面我们对png、jpg、gif这类图片资源使用file-loader,然后就可以在JS中加载图片了。
import avatarImage from './avatar.jpg';
console.log(avatarImage); // c6f482ac9a1905e1d7d22caa909371fc.jpg
url-loader |
url-loader与file-loader作用类似,唯一的不同在于用户可以设置一个文件大小的阈值,当大于该阈值时与file-loader一样返回publicPath,而小于该阈值时则返回文件base64形式编码。
npm install url-loader -D
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: 'url-loader',
options: {
limit: 10240,
name: '[name].[ext]',
publicPath: './assets-path/',
},
},
}
import avatarImage from './avatar.jpg';
console.log(avatarImage); // data:image/jpeg;base64,/9j/2wCEAAgGBg……
|
webpack具有大量的插件支持,详细看webpack plugins 以下只对几个常用插件做介绍。
mini-css-extract-plugin (样式分离) |
该插件将CSS提取到单独的文件中。
1.安装
npm i mini-css-extract-plugin -D
2.创建css文件
project
├── src/
| ├── common.css
| ├── index.css
| └── index.js
├── package.json
└── webpack.config.js
//common.css
body{text-align: center;}
//index.css
body{background-color: #00FFFF;}
//index.js
import './common.css'
import './index.css'
3.配置
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './app.js',
output: {
filename: '[name].js',
},
mode: 'development',
module: {
rules: [{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader,'css-loader'],
}],
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
})
],
};
4.运行
npx webpack
产出 main.css main.js
project
├── dist/
├── main.css
└── main.js
//main.css
body{text-align: center;}
body{
background-color: #00FFFF;
}
被index.js引入的两个css文件合并为一了。
terser-webpack-plugin (压缩JS) ~tree shaking |
Webpack 4中默认使用的压缩JavaScript的插件为terser-webpack-plugin。从Webpack 4之后,这项配置被移到了config.optimization.minimize
。(如果开启了mode:production
,则不需要人为设置)。
1.默认启动
mode:'production'
2.手动启动
optimization: {
minimize: true,
},
3.自定义配置
npm i -D uglifyjs-webpack-plugin
webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [ // 覆盖默认的 minimizer
new TerserPlugin({
/* your config */
test: /\.js(\?.*)?$/i,
exclude: /\/excludes/,
extractComments: true,//提取注释到一个独立文件
})
],
}
}
optimize-css-assets-webpack-plugin (压缩css) |
样式分离后才可压缩css。
npm i optimize-css-assets-webpack-plugin -D
webpack.config.js
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
module.exports = {
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin({
// 生效范围,只压缩匹配到的资源
assetNameRegExp: /\.optimize\.css$/g,
// 压缩处理器,默认为 cssnano
cssProcessor: require('cssnano'),
// 压缩处理器的配置
cssProcessorOptions: { discardComments: { removeAll: true } },
// 是否展示 log
canPrint: true,
})],
},
};
compression-webpack-plugin (资源压缩) |
这个和上面的压缩不同,这个是使用算法压缩,改变原有的文件格式,前端接收数据后需要解压。上面的2种压缩只是去空白,死代码等,打包后还是原来资源的格式。
npm i compression-webpack-plugin -D
webpack.config.js
const CompressionWebpackPlugin = require('compression-webpack-plugin')
module.exports = {
plugins: [
new CompressionWebpackPlugin({
filename: '[path][name].gz[query]',
algorithm: 'gzip',
test: /\.(js|css|json|ttf)(\?.*)?$/i,//压缩了代码和字体
threshold: 0,
minRatio: 0.8,
}),
]
};
html-webpack-plugin (生成注入依赖的html文件) |
html-webpack-plugin插件用于简化创建HTML文件,它会在body中用script标签来包含我们生成的所有bundles文件。不需要我们手动引入。
该插件的两个主要作用:
-
为html文件中引入的外部资源如script、link动态添加每次compile后的hash,防止引用缓存的外部文件问题,也不用每次打包后手动引入发生变化的资源文件(加了hash命名的文件)
-
可以生成创建html入口文件,比如单页面可以生成一个html文件入口,配置N个html-webpack-plugin可以生成N个页面入口
将 webpack中
entry
配置的相关入口chunk 和mini-css-extract-plugin
抽取的css样式 插入到该插件提供的template
或者templateContent
配置项指定的内容基础上生成一个html文件,具体插入方式是将样式link
插入到head
元素中,script
插入到head
或者body
中。
npm i html-webpack-plugin -D
var HtmlWebpackPlugin = require('html-webpack-plugin')
webpackconfig = {
...
plugins: [
new HtmlWebpackPlugin()
]
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Webpack App</title>
<link href="index-af150e90583a89775c77.css" rel="stylesheet"></head>
<body>
<script type="text/javascript" src="common-26a14e7d42a7c7bbc4c2.js"></script>
<script type="text/javascript" src="index-af150e90583a89775c77.js"></script></body>
</html>
输出的资源名绑定了chunkhash之后,资源改变,打包时资源名也会改变。
html-webpack-plugin会自动地将我们打包出来的资源名放入生成的[chunkname].html中,这样我们就不必手动地更新资源URL了。
配置多个html页面
...
plugins: [
new HtmlWebpackPlugin({
template: 'src/html/index.html',
excludeChunks: ['list', 'detail']
}),
new HtmlWebpackPlugin({
filename: 'list.html',
template: 'src/html/list.html',
chunks: ['common', 'list']
}),
new HtmlWebpackPlugin({
filename: 'detail.html',
template: 'src/html/detail.html',
chunks: ['common', 'detail']
})
]
...
我们也可以传入一个已有的HTML模板
<!DOCTYPE html>
<!-- template.html -->
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Custom Title</title>
</head>
<body>
<div id="app">app</div>
<p>text content</p>
</body>
</html>
// webpack.config.js
new HtmlWebpackPlugin({
template: './template.html',
})
|
mode |
提供mode
配置选项将告诉webpack采用相应环境下的内置优化。
有三个值:development ||production || none(退出任何默认优化选项)
development 模式下默认优化项
// webpack.development.config.js
module.exports = {
mode: 'development'
devtool: 'eval',
cache: true,
performance: {
hints: false
},
output: {
pathinfo: true
},
optimization: {
moduleIds: 'named',
chunkIds: 'named',
mangleExports: false,
nodeEnv: 'development',
flagIncludedChunks: false,
occurrenceOrder: false,
concatenateModules: false,
splitChunks: {
hidePathInfo: false,
minSize: 10000,
maxAsyncRequests: Infinity,
maxInitialRequests: Infinity,
},
emitOnErrors: true,
checkWasmTypes: false,
minimize: false,
removeAvailableModules: false
},
plugins: [
new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
]
}
production 模式下默认优化项
// webpack.production.config.js
module.exports = {
mode: 'production',
performance: {
hints: 'warning'
},
output: {
pathinfo: false
},
optimization: {
moduleIds: 'deterministic',
chunkIds: 'deterministic',
mangleExports: 'deterministic',
nodeEnv: 'production',
flagIncludedChunks: true,
occurrenceOrder: true,
concatenateModules: true,
splitChunks: {
hidePathInfo: true,
minSize: 30000,
maxAsyncRequests: 5,
maxInitialRequests: 3,
},
emitOnErrors: false,
checkWasmTypes: true,
minimize: true,
},
plugins: [
new TerserPlugin(/* ... */),
new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.NoEmitOnErrorsPlugin()
]
}
tree shaking |
tree shaking 树抖动,就会掉死叶子。在代码中,就是去无用代码的意思。
tree shaking本身只是为死代码添加上标记,真正去除死代码是通过压缩工具来进行的。使用我们前面介绍过的terser-webpack-plugin
即可。在Webpack 4之后的版本中,直接mode:production
也可以达到相同的效果。
ES6 Module依赖关系的构建是在代码编译时而非运行时。tree shaking功能,它可以在打包过程中帮助我们检测工程中没有被引用过的模块,这部分代码将永远无法被执行到,因此也被称为“死代码”。Webpack会对这部分代码进行标记,并在资源压缩时将它们从最终的bundle中去掉。
// index.js
import { foo } from './util';
foo();
// util.js
export function foo() {
console.log('foo');
}
export function bar() { // 没有被任何其他模块引用,属于“死代码”
console.log('bar');
}
在Webpack打包时会对bar()添加一个标记,在正常开发模式下它仍然存在,只是在生产环境的压缩那一步会被移除掉。
模块热替换(Hot Module Replacement,HMR) |
热更新 在我们每次改变代码,或者资源文件的时候,整个页面其实都会刷新。
热替换,直接替换更改后的依赖模块,而不用刷新整个页面,可以简单理解成局部更新。
许多Web开发框架和工具都提供了live reload来做热更新。模块热替换是 webpack 提供的最有用的功能之一。HMR对于大型应用尤其适用。试想一个复杂的系统每改动一个地方都要经历资源重构建、网络请求、浏览器渲染等过程,怎么也要几秒甚至几十秒的时间才能完成;
项目是基于webpack-dev-server开发时才可以启动模块热替换
const webpack = require('webpack');
module.exports = {
// ...
plugins: [
new webpack.HotModuleReplacementPlugin()
],
devServer: {
hot: true,
},
};
Webpack会为每个模块绑定一个module.hot对象,这个对象包含了HMR的API。
调用HMR API有两种方式,一种是手动地添加这部分代码;另一种是借助一些现成的工具,比如react-hot-loader、vue-loader等。
// index.js
import { add } from 'util.js';
add(2, 3);
if (module.hot) {
module.hot.accept();
}
index.js是应用的入口,那么我们就可以把调用HMR API的代码放在该入口中,这样HMR对于index.js和其依赖的所有模块都会生效。当发现有模块发生变动时,HMR会使应用在当前浏览器环境下重新执行一遍index.js(包括其依赖)的内容,但是页面本身不会刷新。
webpack打包原理 |
webpack打包原理是根据文件间的依赖关系对其进行静态分析,然后将这些模块按指定规则生成静态资源,当 webpack 处理程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
webpack有两种组织模块的依赖方式,同步、异步。异步依赖将作为分割点,形成一个新的块;在优化了依赖树之后,每一个异步区块都将作为一个文件被打包。
|
样式分离 MiniCssExtractPlugin |
通过loader可以将样式文件引入到js文件中,模块打包后输出的还是js文件(样式也直接写入到该js文件了)。此时,哪怕只是修改了js部分的代码,那么css模块也会被重新打包;或者只修改了css文件,js文件是没有变化的,但是他们都是在一个bundle中,所以都会被认为都有修改。
常用的分离样式的插件有两个:extract-text-webpack-plugin(webpack4以下) 和 mini-css-extract-plugin(webpack4以上)
我使用的"webpack": “^5.10.1”,想试用extract-text-webpack-plugin,结果折腾半天也是报错,罢了罢了,反正过期货(估计已经被版本pass掉了),官方推崇的也是mini-css-extract-plugin。
上文有对mini-css-extract-plugin的介绍了。
代码分片 SplitChunks |
代码分片(code splitting)可以把代码按照特定的形式进行拆分,按需加载。用户每次只加载必要的资源,优先级不太高的资源则采用延迟加载等技术渐进式地获取,这样可以保证页面的首屏速度。
手动提取公共模块 |
在Webpack中每个入口(entry)都将生成一个对应的资源文件,一些库和工具是不常变动,可以把它们放在一个单独的入口中,因此可以有效地利用客户端缓存,让用户不必在每次请求页面时都重新加载。(上文的vendor)
// webpack.config.js
entry: {
app: './app.js',//业务逻辑代码 常变
lib: ['lib-a', 'lib-b', 'lib-c']//工具类库 少变
},
output: {
filename: '[name].[chunkcode].js',
},
// index.html
<script src="dist/lib.1ac59013ea124.js"></script>
<script src="dist/app.43b659013ea12.js"></script>
lib.1ac59013ea124.js
chunkcode根据文件内容生成的,内容不变,这个也不变,这样就不会重新打包生成!
以后不必担心引入这个生成的名麻烦的问题,有html-webpack-plugin帮解决,下文会讲到。
自动提取公共模块 optimization.SplitChunks |
SplitChunks是Webpack 4内部自带的插件,用于将多个Chunk中公共的部分提取出来。
提取公共模块的好处:
- 开发过程中减少了重复模块打包,可以提升开发速度;
- 减小整体资源体积;
- 合理分片后的代码可以更有效地利用客户端缓存。
🌰案例对证
1.模块不作提取时
//index.js
import React from 'react'
document.write('index'+React.version)
//other.js
import React from 'react'
document.write('other'+React.version)
//webpack.config.js
const path=require('path')
module.exports={
context:path.resolve(__dirname,'./src'),
entry:{
index:'./index.js',
other:'./other.js',
},
output:{
filename:'[name].js',
},
}
可见index.js和other.js都分别加载了react模块。
2.使用SplitChunks
提取公共模块
SplitChunks是在webpack的优化项中的,查看更多优化项webpack.optimization
//webpack.config.js
optimization:{
splitChunks:{
chunks:'all',
}
}
chunks
的值为all
,这个配置项的含义是,SplitChunks将会对所有的chunks生效。默认情况下,SplitChunks只对异步chunks,比如import('xx.js')
生效。
ok!看下打包结果:
project
├── dist/
├── vendors-node_modules_react_index_js.js
├── index.js
└── other.js
多生成一个文件vendors-node_modules_react_index_js.js
,这个文件存储了原index.js和other.js的公共模块react。
webpack5自动分块需要满足以下条件:
- 可以共享新块,或者模块来自node_modules文件夹
- 新的块将大于20kb(在min + gz之前)
- 按需加载块时并行请求的最大数量将小于或等于30
- 初始页面加载时并行请求的最大数量将小于或等于30
当试图满足最后两个条件时,最好使用较大的块。
如果提取后的资源体积太小,那么带来的优化效果也比较一般。
同时加载过多的资源,每一个请求都要花费建立链接和释放链接的成本,更多需要花费更多时间和性能,为此做出了平衡。
看下splitChunks默认配置便可知:
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
automaticNameDelimiter: '~',
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
(1)SplitChunks工作模式
chunks配置SplitChunks的工作模式。它有3个可选值,分别为async(默认)、initial和all。async即只提取异步chunk,initial则只对入口chunk生效(如果配置了initial则上面异步的例子将失效),all则是两种模式同时开启。
(2)匹配条件
minSize、minChunks、maxAsyncRequests、maxInitialRequests都属于匹配条件
(3)命名
配置项name默认为true,它意味着SplitChunks可以根据cacheGroups和作用范围自动为新生成的chunk命名,并以automaticNameDelimiter分隔。如vendorsab~c.js意思是cacheGroups为vendors,并且该chunk是由a、b、c三个入口chunk所产生的。据在webpack5.10.3测试,automaticNameDelimiter并不生效
(4)cacheGroups
可以理解成分离chunks时的规则。默认情况下有两种规则——vendors和default。vendors用于提取所有node_modules中符合条件的模块,default则作用于被多次引用的模块。我们可以对这些规则进行增加或者修改,如果想要禁用某种规则,也可以直接将其置为false。当一个模块同时符合多个cacheGroups时,则根据其中的priority配置项确定优先级。
异步加载 import() |
将一些暂时使用不到的模块延迟加载,初次渲染的时候用户下载的资源尽可能小,后续的模块等到恰当的时机再去触发加载。因此一般也把这种方法叫作按需加载。
//bar.js
export function add(a,b){
return a+b;
}
//foo.js
import('./bar.js').then(({add})=>{console.log(add(1,2))})
webpack.config.js
const path=require('path')
module.exports={
context:path.resolve(__dirname,'./src/js/'),
entry:{
foo:'./foo.js',
bar:'./bar.js',
},
output:{
filename:'[name].[chunkhash].js',
publicPath:'/dist'
},
mode:'development',
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script src="src/js/foo.js"></script>
</body>
</html>
首屏加载的JS资源地址是通过页面中的script标签来指定的,而间接资源(通过首屏JS再进一步加载的JS)的位置则要通过output.publicPath来指定。上面我们的import()
相当于使bar.js成为了一个间接资源,我们需要配置publicPath来告诉Webpack去哪里获取它。
观察Network面板:
Initiator指向的是当前资源的的启动文件。
可以发现bar.js是由foo.js发起请求得到的结果,并且是在foo.js加载完后才加载的,实际是异步加载的。
代码压缩 |
上文插件部分分别介绍了js css gzip压缩!不累赘
缩小打包范围 |
exclude和include |
当exclude和include规则有重叠的部分时,exclude的优先级更高。
🌰include使babel-loader只生效于源码目录/src/scripts。
module: {
rules: [
{
test: /\.js$/,
include: /src\/scripts/,
loader: 'babel-loader,
}
],
},
noParse |
不去解析但仍会打包到bundle中.
有些库我们是希望Webpack完全不要去进行解析的,即不希望应用任何loader规则,库的内部也不会有对其他模块的依赖,那么这时可以使用noParse对其进行忽略。
module.exports = {
//...
module: {
noParse: /lodash/,
}
};
上面的配置将会忽略所有文件名中包含lodash的模块,这些模块仍然会被打包进资源文件,只不过Webpack不会对其进行任何解析。
IgnorePlugin |
exclude和include是确定loader的规则范围,noParse是不去解析但仍会打包到bundle中。IgnorePlugin,它可以完全排除一些模块,被排除的模块即便被引用了也不会被打包进资源文件中。
一些由库产生的额外资源我们用不到但又无法去掉,因为引用的语句处于库文件的内部。比如,Moment.js是一个日期时间处理相关的库,为了做本地化它会加载很多语言包,对于我们来说一般用不到其他地区的语言包,但它们会占很多体积,这时就可以用IgnorePlugin来去掉。
plugins: [
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/, // 匹配资源文件
contextRegExp: /moment$/, // 匹配检索目录
})
],
Cache |
有些loader会有一个cache配置项,用来在编译代码后同时保存一份缓存,在执行下一次编译前会先检查源码文件是否有变化,如果没有就直接采用缓存,也就是上次编译的结果。这样相当于实际编译的只有变化了的文件,整体速度上会有一定提升。
在Webpack 5中添加了一个新的配置项“cache:{type:"filesystem"}”
,它会在全局启用一个文件缓存。要注意的是,该特性目前仅仅是实验阶段,并且无法自动检测到缓存已经过期。目前的解决办法就是,当我们更新了任何node_modules中的模块或者Webpack的配置后,手动修改cache.version来让缓存过期。
|
优化构建 happyPack |
在打包过程中有一项非常耗时的工作,就是使用loader将各种资源进行转译处理。
工作流程概括如下:
1)从配置中获取打包入口;
2)匹配loader规则,并对入口模块进行转译;
3)对转译后的模块进行依赖查找(如a.js中加载了b.js和c.js);
4)对新找到的模块重复进行步骤2)和步骤3),直到没有新的依赖模块。
从步骤2)到步骤4)是一个递归的过程,此处的Webpack是单线程的。这里的问题在于Webpack是单线程的,假设一个模块依赖于几个其他模块,Webpack必须对这些模块逐个进行转译。虽然这些转译任务彼此之间没有任何依赖关系,却必须串行地执行。
HappyPack是一个通过多线程来提升Webpack打包速度的工具。适用于那些转译任务比较重的工程,类似babel-loader和ts-loader效果更佳。
npm i -D happypack
单个loader优化 |
const HappyPack = require('happypack');
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'happypack/loader',
}
],
},
plugins: [
new HappyPack({
loaders: [
{
loader: 'babel-loader',
options: {
presets: ['react'],
},
}
],
})
],
};
使用happypack/loader替换了原有的babel-loader,并在plugins中添加了HappyPack的插件,将原有的babel-loader连同它的配置插入进去即可。
多个loader优化 |
在使用HappyPack优化多个loader时,需要为每一个loader配置一个id,否则HappyPack无法知道rules与plugins如何一一对应。
const HappyPack = require('happypack');
module.exports = {
//...
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'happypack/loader?id=js',
},
{
test: /\.ts$/,
exclude: /node_modules/,
loader: 'happypack/loader?id=ts',
}
],
},
plugins: [
new HappyPack({
id: 'js',
loaders: [{
loader: 'babel-loader',
options: {}, // babel options
}],
}),
new HappyPack({
id: 'ts',
loaders: [{
loader: 'ts-loader',
options: {}, // ts options
}],
})
]
};
webpack-bundle-analyzer (图形化分析bundle的构成和内存) |
npm i webpack-bundle-analyzer -D
const Analyzer = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
// ...
plugins: [
new Analyzer()
],
};
npx webpack
占据终端并自动打开浏览器
webpack-dashboard (更直观的看到控制台的打包信息) |
npm i webpack-dashboard -D
const DashboardPlugin = require('webpack-dashboard/plugin');
plugins:[new DashboardPlugin()],
修改package.json
为了使webpack-dashboard生效还要更改一下webpack的启动方式,就是用webpack-dashboard模块命令替代原本的webpack或者webpack-dev-server的命令,并将原有的启动命令作为参数传给它。如:
"dev": "webpack-dashboard -- webpack-dev-server"
多个webpack配置文件 (webpack-merge) |
多个环境下的webpack配置文件如何切换?
这些名字是可以随意取得
webpack默认只认识webpack.config.js;
npx webpack --config webpack.dev.config
公共的配置提取出来,webpack-merge是配置合并的工具。
npm i webpack-merge -D
webpack.common.js
// webpack.common.js
module.exports = {
entry: './app.js',
output: {
filename: '[name].js',
},
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: 'file-loader',
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
],
}
],
},
};
webpack.prod.js 重写对 test: /\.css$/
规则的处理
// webpack.prod.js
const merge = require('webpack-merge');
const commonConfig = require('./webpack.common.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = merge.smart(commonConfig, {
mode: 'production',
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader', // css-loader 负责解析 CSS 代码, 处理 CSS 中的依赖
],
}
]
},
plugins: [
// 用 MiniCssExtractPlugin 抽离出 css 文件
new MiniCssExtractPlugin({
filename: '[name].bundle.css' // 输出的 css 文件名为 index.css
}),
]
});
参考文档:
《webpack实战 入门进阶与调优》
webpack官网
webpack 中,module,chunk 和 bundle 的区别是什么?