webpack
是一个用于现代 JavaScript
应用程序的 静态模块打包工具。它主要解决的问题是将多个模块(Module)打包成一个或多个文件,并且在这个过程中还支持一些特性,如代码分离、文件压缩等。
我们先将着重点落在 静态模块打包工具
上,为什么是 静态模块打包工具
?
是因为它可以将多个模块(JavaScript 文件
、CSS 文件
、图片
等)打包成一个或多个静态资源文件。静态资源文件包含了应用程序中的所有依赖关系和逻辑,可以直接在浏览器中加载和运行。
下面我们就来演示一个最简单的 webpack 打包示例:
npm
,然后 在本地安装 webpack
,接着安装 webpack-cli
(注意:此工具用于在命令行中运行 webpack
):shmkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev
add.js
:
jsexport function add(n1, n2) {
return n1 + n2;
}
sub.js
jsexport function sub(n1, n2) {
return n1 - n2;
}
index.js
jsimport { add } from "./add";
import { sub } from "./sub";
console.log(add(1, 2));
console.log(sub(1, 2));
可以看到,我们做的仅仅定义了两个工具文件 a.js
和 b.js
,并在 index.js
中导入并且调用了两个函数。
npx webpack
命令进行打包,打包结果如下图所示:可以这样说,执行 npx webpack
,会将我们的脚本 src/index.js
作为 入口起点,也会生成 dist/main.js
作为 输出。Node 8.2/npm 5.2.0
以上版本提供的 npx
命令,可以运行在初次安装的 webpack package
中的 webpack
二进制文件(即 ./node_modules/.bin/webpack
)。
在上面的例子中,我们仅仅演示了 es6
的模块化导入导出,事实上除了 import
和 export
,webpack
还能够很好地支持多种其他模块语法比如 CommonJS
、AMD
等
在 webpack v4
中,可以无须任何配置,然而大多数项目会需要很复杂的设置,这时候就需要一个配置文件来拯救我们了,因为这比在 terminal(终端)
中手动输入大量命令要高效的多
webpack-demo/webpack.config.js
jsconst path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
npx webpack --config webpack.config.js
。可以看到生成的新的文件名为 bundle.js
说明我们的配置文件已经生效。
事实上,如果 webpack.config.js
存在,则 webpack
命令将默认选择使用它,所以我们依然可以使用 npx webpack
进行打包。这里使用 --config
选项只是表明可以传递任何名称的配置文件,这对于需要拆分成多个文件的复杂配置是非常有用的。
考虑到用 CLI
这种方式来运行本地的 webpack
副本并不是特别方便,我们可以设置一个快捷方式。调整 package.json
文件,添加一个 npm script
:
"build": "webpack"
现在,可以使用 npm run build
命令,来替代我们之前使用的 npx
命令。注意,使用 npm scripts
,我们可以像使用 npx
那样通过模块名引用本地安装的 npm packages
。这是大多数基于 npm
的项目遵循的标准,因为它允许所有贡献者使用同一组通用脚本。
在这一题中,我们使用了一个最简单的案例来解释了 webpack
是什么东西,它的本质就是一个模块化的打包工具,我想大家应该对它的概念都能有一个基本的了解了。
Webpack
的核心概念主要包括以下几个:
入口起点(entry point) 定义 webpack
从哪个文件开始构建依赖关系图,进入入口起点后,webpack
会找出有哪些模块和库是入口起点(直接和间接)依赖的。
它的默认值是 ./src/index.js
,但是我们可以通过在配置文件中配置 entry
属性来指定一个(或多个)不同的入口起点。例如:
webpack.config.js
jsmodule.exports = {
entry: './path/to/my/entry/file.js',
};
output
属性告诉 webpack
在哪里输出它所创建的 bundle
,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js
,其他生成文件默认放置在 ./dist
文件夹中。
你可以通过在配置中指定一个 output
字段,来配置这些处理过程:
jsconst path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js',
},
};
在上面的示例中,我们通过 output.filename
和 output.path
属性,来告诉 webpack bundle
的名称,以及我们想要 bundle
生成(emit
)到哪里。
webpack
只能理解 JavaScript
和 JSON
文件。loader
的作用就是让让 webpack
能够去处理其他类型的文件,并将它们转换为 JavaScript
文件。
在 webpack
的配置中,loader
有两个属性:
test
属性,识别出哪些文件会被转换。use
属性,定义出在进行转换时,应该使用哪个 loader
。webpack.config.js
jsconst path = require('path');
module.exports = {
output: {
filename: 'my-first-webpack.bundle.js',
},
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
};
以上配置中,对一个单独的 module
对象定义了 rules
属性,里面包含两个必须属性:test
和 use
。这告诉 webpack 编译器(compiler)
如下信息:
“嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.txt' 的路径」时,在你对它打包之前,先 use(使用) raw-loader 转换一下。”
webpack 中 loader
用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。插件目的在于解决 loader
无法实现的其他事。
想要使用一个插件,只需要 require()
它,然后把它添加到 plugins
数组中。多数插件可以通过选项(option
)自定义。也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new
操作符来创建一个插件实例。
webpack.config.js
jsconst HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件
module.exports = {
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
在上面的示例中,html-webpack-plugin
为应用程序生成一个 HTML
文件,并自动将生成的所有 bundle
注入到此文件中。
Webpack
有三种模式:development
、production
和 none
。通过设置不同的模式,可以启用不同的内置优化。
Webpack
支持所有符合 ES5
标准 的浏览器(不支持 IE8 及以下版本)。
Webpack 5
运行于 Node.js v10.13.0+
的版本。
chunk
可以理解为 webpack
构建输出的一个文件,通常包含了在构建过程中生成的一些代码块。webpack
将所有相关的模块打包在一起,以此生成一个或多个chunk 。这些 chunk
可以是 JavaScript文件、CSS文件、图片等,也可以是异步加载的代码块。
webpack
将代码分割成多个 chunk
的主要目的是优化应用程序的性能。通过将代码拆分成多个小块,webpack
可以减少初始加载时间,并提高应用程序的性能。
Webpack
会将所有的文件都看作一个个模块,每个模块都有自己的依赖关系,Webpack
会根据这些依赖关系构建出一个依赖关系图。
Webpack
的打包原理可以简单地概括为以下几个步骤:
解析入口文件:Webpack
通过指定的入口文件开始打包,从该文件开始分析和解析整个项目的依赖关系。
依赖分析:Webpack
根据模块之间的依赖关系,递归地分析和收集所有需要打包的模块,包括 JavaScript
、CSS
、图片等资源文件。
模块转换:Webpack
在解析模块的过程中,会根据不同的模块类型,将它们转换成 JavaScript
代码,以便在浏览器中执行。
生成 Chunk
:Webpack
会将所有模块打包成一个或多个 Chunk
,每个 Chunk
包含了一组模块,以及这些模块之间的依赖关系。
生成 Bundle
:最后,Webpack
会将所有的 Chunk
生成对应的静态资源文件,例如 JavaScript、CSS、图片等,供浏览器加载和执行。
在整个打包过程中,Webpack
提供了很多插件和配置选项,可以帮助我们自定义打包过程,例如代码压缩、分离 CSS
文件、处理图片 等。同时,Webpack
还支持使用各种 Loader
来处理不同类型的模块,例如使用 Babel Loader
来处理 ES6
语法的模块。
chunk:chunk
是 Webpack
打包生成的一个或多个 JavaScript
文件,它包含了一组相关的模块的代码,可以看做是模块的集合。在实际开发中,chunk
可以用于实现代码分割、按需加载、懒加载等功能,从而优化代码的加载和执行,提高应用的性能和用户体验。
bundle:bundle
是 Webpack
打包生成的一个或多个静态资源文件,它包含了所有模块的代码,可以看做是代码的最终打包结果。在实际开发中,bundle
可以用于发布到生产环境,供浏览器加载和执行。
简单来说,chunk
是 Webpack
在打包过程中产生的中间文件,bundle
是最终生成的静态资源文件。在代码分割和按需加载的场景下,Webpack
会生成多个 chunk
文件,每个文件包含一部分模块的代码,然后将这些 chunk
文件合并成一个或多个 bundle
文件,以便在浏览器中加载和执行。
代码分离(Code Spliting) 是 webpack
一个非常重要的特性,它主要的目的是将代码剥离到不同的 bundle
中,之后我们可以按需加载,或者并行加载这些文件。
webpack
常用的代码分离方式有三种:
entry
配置手动分离代码;EntryDependencies
或者 SplitChunksPlugin
去重和分离代码:这是迄今为止最简单直观的分离代码的方式。不过,这种方式手动配置较多,并有一些隐患,我们将会解决这些问题。
先来看看如何从 main bundle
中分离 another module
(另一个模块)
创建一个小的 demo
:
npm
,然后在本地安装 webpack
、webpack-cli
、loadsh
shellmkdir webpack-demo cd webpack-demo npm init -y npm install webpack webpack-cli lodash --save-dev
src/index.js
:jsimport _ from "lodash";
console.log(_);
src/another-module.js
:jsimport _ from 'lodash';
console.log(_);
webpack.config.js
:const path = require("path"); module.exports = { mode: "development", entry: "./src/index.js", output: { path: path.resolve(__dirname, "dist"), filename: "main.js", }, };
package.json
中添加命令:json"scripts": {
"build": "webpack"
},
shellnpm run build
可以看到此时生成了一个 554KB
的 main.js
文件
接下来我们从 main bundle
中分离出 another module
(另一个模块)
webpack.config.js
diffconst path = require("path");
module.exports = {
mode: "development",
- entry: './src/index',
+ entry: {
+ index: './src/index',
+ another: './src/another-module.js'
+ },
output: {
path: path.resolve(__dirname, "dist"),
- filename: "main.js",
+ filename: "[name].main.js",
},
};
我们发现此时已经成功打包出 another.bundle.js
和 index.bundle.js
两个文件了,但是文件的大小似乎有些问题,怎么两个都是 554KB
?
正如前面提到的,这种方式存在一些隐患:
chunk
之间包含一些重复的模块,那些重复模块都会被引入到各个 bundle
中。以上两点中,第一点对我们的示例来说无疑是个问题,因为之前我们在 ./src/index.js
中也引入过 lodash
,这样就在两个 bundle
中造成重复引用。在下一小节我们将移除重复的模块。
在通过多入口分离代码的方式中,我们可以通过配置 dependOn
这个选项来解决重复模块的问题,它的原理就是从两个文件中抽出一个共享的模块,然后再让这两个模块依赖这个共享模块。
webpack.config.js
配置文件:diff const path = require('path');
module.exports = {
mode: 'development',
entry: {
- index: './src/index.js',
- another: './src/another-module.js',
+ index: {
+ import: './src/index.js',
+ dependOn: 'shared',
+ },
+ another: {
+ import: './src/another-module.js',
+ dependOn: 'shared',
+ },
+ shared: ['lodash'],
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
};
可以看到 index.mian.js
和 another.mian.js
中重复引用的部分被抽离成了 shared.main.js
文件,且 index.mian.js
和 another.mian.js
文件大小也变小了。
另外一种分包的模式是 splitChunks
,它底层是使用 SplitChunksPlugin
来实现的:
SplitChunksPlugin
插件可以将公共的依赖模块提取到已有的入口chunk
中,或者提取到一个新生成的chunk
。
因为该插件 webpack
已经默认安装和集成,所以我们并 不需要单独安装和直接使用该插件;只需要提供 SplitChunksPlugin
相关的配置信息即可
webpack
提供了 SplitChunksPlugin
默认的配置,我们也可以手动来修改它的配置:
chunks
仅仅针对于异步(async
)请求,我们可以设置为 initial
或者 all
,1.1.2
的基础上修改 webpack.cofig.js
:diff const path = require('path');
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
another: './src/another-module.js',
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
},
+ optimization: {
+ splitChunks: {
+ chunks: 'all',
+ },
+ },
};
使用 optimization.splitChunks
配置选项之后,现在应该可以看出,index.bundle.js
和 another.bundle.js
中已经移除了重复的依赖模块。需要注意的是,插件将 lodash
分离到单独的 chunk
,并且将其从 main
bundle
中移除,减轻了大小。
除了 webpack
默认继承的 SplitChunksPlugin
插件,社区中也有提供一些对于代码分离很有帮助的 plugin
和 loader
,比如:
mini-css-extract-plugin
: 用于将 CSS 从主应用程序中分离。关于 optimization.splitChunks
文档上有很详细的记载,我这里讲你叫几个常用的:
1. Chunks:
async
initial
,表示对通过的代码进行处理all
表示对同步和异步代码都进行处理2. minSize
:
minSize
,那么这个包就不会拆分;3. maxSize
:
4. cacheGroups:
lodash
在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包;test
属性:匹配符合规则的包;name
属性:拆分包的 name
属性;filename
属性:拆分包的名称,可以自己使用 placeholder
属性;webpack.config.js
jsconst path = require("path");
module.exports = {
mode: "development",
entry: {
index: "./src/index.js",
another: "./src/another-module.js",
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
},
optimization: {
splitChunks: {
chunks: "all",
// 拆分包的最小体积
// 如果一个包拆分出来达不到 minSize,那么这个包就不会拆分(会被合并到其他包中)
minSize: 100,
// 将大于 maxSize 的包,拆分成不小于 minSize 的包
maxSize: 10000,
// 自己对需要拆包的内容进行分组
cacheGroups: {
自定义模块的name: {
test: /node_modules/,
filename: "[name]_vendors.js",
},
},
},
},
};
另外一个代码拆分的方式是动态导入时,webpack
提供了两种实现动态导入的方式:
ECMAScript
中的 import()
语法来完成,也是目前推荐的方式;webpack
遗留的 require.ensure
,目前已经不推荐使用;动态 import
使用最多的一个场景是懒加载(比如路由懒加载)
接着从 1.1.2
小节代码的基础上修改:
webpack.confg.js
:jsconst path = require("path");
module.exports = {
entry: "./src/index.js",
mode: "development",
entry: {
index: "./src/index.js",
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
},
};
删除 src/another-module.js
文件
修改 src/index.js
,不再使用 statically import
(静态导入) lodash
,而是通过 dynamic import
(动态导入) 来分离出一个 chunk
:
jsconst logLodash = function () {
import("lodash").then(({ default: _ }) => {
console.log(_);
});
};
logLodash();
之所以需要 default
,是因为 webpack 4
在导入 CommonJS
模块时,将不再解析为 module.exports
的值,而是为 CommonJS
模块创建一个 artificial namespace
对象。
由于 import()
会返回一个 promise
,因此它可以和 async
函数一起使用。下面是如何通过 async
函数简化代码:
jsconst logLodash = async function () {
const { default: _ } = await import("lodash");
console.log(_);
};
logLodash();
因为动态导入通常是一定会打包成独立的文件的,所以并不会再 cacheGroups
中进行配置;
它的命名我们通常会在 output
中,通过 chunkFilename
属性来命名:
webpack.config.js
diffconst path = require("path");
module.exports = {
entry: "./src/index.js",
mode: "development",
entry: {
index: "./src/index.js",
},
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
+ chunkFilename: "chunk_[name].js""
},
};
如果对打包后的 [name]
不满意,还可以通过 magic comments
(魔法注释)来修改:
1, 修改 src/index.js
:
jsconst logLodash = async function () {
const { default: _ } = await import(/*webpackChunkName: 'lodash'*/ "lodash");
console.log(_);
};
logLodash();
什么是 Tree Shaking
?
Tree Shaking
是一个术语,在计算机中表示消除死代码(dead_code
);LISP
,用于消除未调用的代码(纯函数无副作用,可以放心的消除,这也是为什么要求我们在进行函数式编程时,尽量使用纯函数的原因之一);Tree Shaking
也被应用于其他的语言,比如 JavaScript
、Dart
;JavaScript
的 Tree Shaking
:
JavaScript
进行 Tree Shaking
是源自打包工具 rollup
;Tree Shaking
依赖于 ES Module
的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系);webpack2
正式内置支持了 ES2015
模块,和检测未使用模块的能力;webpack4
正式扩展了这个能力,并且通过 package.json
的 sideEffects
属性作为标记,告知 webpack
在编译时,哪里文件可以安全的删除掉;webpack5
中,也提供了对部分 CommonJS
的 tree shaking
的支持;
✓ https://github.com/webpack/changelog-v5#commonjs-tree-shakingwebpack
实现 Tree Shaking
采用了两种不同的方案:
usedExports
:通过标记某些函数是否被使用,之后通过 Terser
来进行优化的;sideEffects
:跳过整个模块/文件,直接查看该文件是否有副作用;usedExports
按 sideEffects
这两个东西的优化是不同的事情。
引用官方文档的话: The sideEffects and usedExports(more konwn as tree shaking)optimizations are two different things
下面我们分别来演示一下这两个属性的使用
webpack-demo
。shellmkdir webpack-demo cd webpack-demo npm init -y npm install webpack webpack-cli lodash --save-dev
src/math.js
文件:jsexport const add = (num1, num2) => num1 + num2;
export const sub = (num1, num2) => num1 - num2;
在这个问价中仅是导出了两个函数方法
src/index.js
文件:、jsimport { add, sub } from "./math";
console.log(add(1, 2));
在 index.js
中 导入了刚刚创建的两个函数,但是只使用了 add
webpack.config.js
:jsconst path = require("path");
module.exports = {
mode: "development",
devtool: false,
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "main.js",
},
optimization: {
usedExports: true,
},
};
为了可以看到 usedExports
带来的效果,我们需要设置为 development
模式。因为在 production
模式下,webpack
默认的一些优化会带来很大的影响。
usedExports
为 true
和 false
对比打包后的代码:仔细观察上面两张图可以发现当设置 usedExports: true
时,sub
函数没有导出了,另外会多出一段注释:unused harmony export mul
;这段注释的意义是会告知 Terser
在优化时,可以删除掉这段代码。
这个时候,我们将 minimize
设置 true
:
usedExports
设置为 false
时,sub
函数没有被移除掉;usedExports
设置为 true
时,sub
函数有被移除掉;所以,usedExports
实现 Tree Shaking
是结合 Terser
来完成的。
在一个纯粹的 ESM
模块世界中,很容易识别出哪些文件有副作用。然而,我们的项目无法达到这种纯度,所以,此时有必要提示 webpack compiler
哪些代码是“纯粹部分”。
通过 package.json
的 "sideEffects"
属性,来实现这种方式。
json{
"name": "your-project",
"sideEffects": false
}
如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false
,来告知 webpack
它可以安全地删除未用到的 export
。
"side effect(副作用)"
的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个export
或多个export
。举例说明,例如polyfill
,它影响全局作用域,并且通常不提供export
。
如果你的代码确实有一些副作用,可以改为提供一个数组:
json{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
}
注意,所有导入文件都会受到
tree shaking
的影响。这意味着,如果在项目中使用类似css-loader
并import
一个CSS
文件,则需要将其添加到side effect
列表中,以免在生产模式中无意中将它删除:
json{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js", "*.css"]
}
上面将的都是关于 JavaScript
的 Tree Shaking
,对于 CSS
同样有对应的 Tree Shaking
操作。
在早期的时候,我们会使用 PurifyCss
插件来完成 CSS
的 tree shaking
,但是目前该库已经不再维护了(最新更新也是在 4
年前了);
目前我们可以使用另外一个库来完成 CSS
的 Tree Shaking
:PurgeCSS
,也是一个帮助我们删除未使用的 CSS
的工具;
PurgeCss
的 webpack
插件:shellnpm install purgecss-webpack-plugin -D
webpack.config.js
中配置 PurgeCss
new PurgeCSSPlugin({ paths: glob.sync(`${path.resolve(__dirname, '../src')}/**/*`, { nodir: true }), only: ['bundle', 'vendor'] })
paths
:表示要检测哪些目录下的内容需要被分析,这里我们可以使用 glob
;Purgecss
会将我们的 html
标签的样式移除掉,如果我们希望保留,可以添加一个 safelist
的属性;purgecss
也可以对 less
、sass
文件进行处理(它是对打包后的 css
进行 tree shaking
操作);
Webpack
实现 热更新 Hot Module Replacement(HMR)
的主要原理是通过在运行时替换被修改的模块,而不需要重新加载整个页面或应用。
具体来说,Webpack HMR
的实现流程如下:
首先,在入口文件中添加对 HMR
的支持,例如使用 module.hot.accept
方法或 webpack-dev-server
等工具提供的 HMR API
。
当某个模块发生变化时,Webpack
会构建新的模块代码,并将其传递给 HMR runtime
。
HMR runtime
会将新的模块代码与当前运行的模块进行比较,找出发生变化的部分,然后将其应用到当前运行的模块上。
如果当前模块依赖其他模块,HMR runtime
会检查这些依赖模块是否也发生了变化,如果有,会递归执行上述操作,直到所有相关的模块都被更新为止。
最后,HMR runtime
会通知应用程序,告诉它哪些模块已经被更新,并提供一些回调函数,让应用程序可以根据需要进行一些额外的操作。
总之,Webpack HMR
通过在运行时替换被修改的模块,使应用程序可以保持运行状态,同时也提高了开发效率和用户体验。要实现 HMR
,我们需要在入口文件中添加对 HMR
的支持,并使用 webpack-dev-server
或其他工具来提供 HMR runtime
的支持。
HtmlWebpackPlugin
:该插件可以根据指定的模板生成 HTML
文件,并自动将生成的 JS
和 CSS
文件引入到 HTML
中。
CleanWebpackPlugin
:该插件可以在每次构建之前清空输出目录,避免旧文件对新构建结果的影响。
MiniCssExtractPlugin
:该插件可以将 CSS
文件提取出来并单独打包成一个或多个文件,以便在浏览器中异步加载。
Webpack.DefinePlugin
:该插件可以定义全局变量,可以用来区分开发环境和生产环境等场景。
TerserWebpackPlugin
:该插件可以压缩 JavaScript
代码,减小文件大小,加快加载速度。
CopyWebpackPlugin
:该插件可以将某些文件或文件夹复制到输出目录中,例如静态资源文件等。
webpack
的优化策略包括:
Tree Shaking
:通过静态代码分析,找出未被引用的代码并在打包时剔除掉,以减少打包后的代码体积。Scope Hoisting
:将模块中的所有函数作用域合并到一个函数作用域中,以减少代码体积和函数声明语句执行时间。Code Splitting
:将代码按照路由或者功能进行拆分,实现按需加载,减少首屏加载时间。本文作者:叶继伟
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!