webpack是什么
Webpack是基于模块化打包的⼯具: ⾃动化处理模块,webpack把⼀切当成模块,当 webpack 处理应⽤程序时,它会递归地构建⼀个依赖关系图 (dependency graph),其中包含应⽤程序需要的每个模块,然后将所有这些模块打包成⼀个或多个 bundle。
为什么会有webpack
Webpack 的出现是为了解决前端模块化开发的问题。在没有模块化的开发环境中,JavaScript 代码通常是通过 script 标签引入的方式来进行管理的,这种方式容易导致全局变量污染、代码难以维护等问题。而采用模块化开发可以将代码分解成小的、独立的模块,从而降低代码耦合度,提高代码的可维护性。
Webpack 的主要作用是将应用程序的所有模块打包成一个或多个 bundle,同时还可以进行代码转换、压缩、优化等操作。它具有以下优点:
- 支持多种模块化规范:Webpack 支持 CommonJS、AMD、ES6 等多种模块化规范,可以让开发者按照自己的喜好来组织代码。
- 可以处理多种类型的文件:Webpack 可以处理 JavaScript、CSS、图片、字体等多种类型的文件,使开发者可以在一个工具中完成多种任务。
- 提供丰富的插件:Webpack 提供了丰富的插件,可以进行代码压缩、代码优化、错误处理等操作,同时也可以扩展 Webpack 的功能。
- 支持开发和生产环境的构建:Webpack 可以根据不同的环境进行配置,从而达到开发和生产环境的最佳实践。
怎样提高性能和用户体验
Webpack 可以提高应用程序的性能和用户体验,主要体现在以下方面:
- 减少网络请求:Webpack 可以将多个模块打包成一个或多个 bundle,从而减少 HTTP 请求的次数,提高页面的加载速度。
- 代码压缩:Webpack 可以对 JavaScript 代码进行压缩,从而减小代码的体积,提高页面的加载速度。
- 代码优化:Webpack 可以进行代码优化,比如去除未使用的代码、提取公共代码等,从而减小代码的体积,提高页面的加载速度。
- 使用 Tree Shaking:Webpack 可以使用 Tree Shaking 技术,只将应用程序中用到的代码打包到 bundle 中,从而减小 bundle 的体积,提高页面的加载速度。
- 使用 Code Splitting:Webpack 可以根据业务需求将代码拆分成多个 bundle,从而提高页面的加载速度。
- 使用缓存:Webpack 可以使用缓存技术,缓存已经生成的代码,从而减少重新生成代码的时间,提高页面的加载速度。
- 使用热更新:Webpack 可以使用热更新技术,在代码修改后不需要刷新页面即可实时预览,从而提高开发效率和用户体验。
打包的原理
详细描述:
步骤概述:
(1)搭建结构,读取配置参数
根据 Webpack 的用法可以看出, Webpack 本质上是一个函数,它接受一个配置信息作为参数,执行后返回一个 compiler 对象,调用 compiler 对象中的 run 方法就会启动编译。run 方法接受一个回调,可以用来查看编译过程中的错误信息或编译信息。
(2)用配置参数对象初始化 Compiler 对象
//Compiler其实是一个类,它是整个编译过程的大管家,而且是单例模式
class Compiler {
+ constructor(webpackOptions) {
+ this.options = webpackOptions; //存储配置信息
+ //它内部提供了很多钩子
+ this.hooks = {
+ run: new SyncHook(), //会在编译刚开始的时候触发此run钩子
+ done: new SyncHook(), //会在编译结束的时候触发此done钩子
+ };
+ }
}
//第一步:搭建结构,读取配置参数,这里接受的是webpack.config.js中的参数
function webpack(webpackOptions) {
//第二步:用配置参数对象初始化 `Compiler` 对象
+ const compiler = new Compiler(webpackOptions)
return compiler;
}
(3)挂载配置文件中的插件
Webpack Plugin 其实就是一个普通的函数,在该函数中需要我们定制一个 apply 方法。当 Webpack 内部进行插件挂载时会执行 apply 函数。我们可以在 apply 方法中订阅各种生命周期钩子,当到达对应的时间点时就会执行。
//自定义插件WebpackRunPlugin
class WebpackRunPlugin {
apply(compiler) {
compiler.hooks.run.tap("WebpackRunPlugin", () => {
console.log("开始编译");
});
}
}
//自定义插件WebpackDonePlugin
class WebpackDonePlugin {
apply(compiler) {
compiler.hooks.done.tap("WebpackDonePlugin", () => {
console.log("结束编译");
});
}
}
(4)执行 Compiler 对象的 run 方法开始执行编译
在正式开始编译前,我们需要先调用 Compiler 中的 run 钩子,表示开始启动编译了;在编译结束后,需要调用 done 钩子,表示编译完成。编译这个阶段需要单独解耦出来,通过 Compilation 来完成。
class Compiler {
//省略其他
run(callback) {
//省略
}
compile(callback) {
//虽然webpack只有一个Compiler,但是每次编译都会产出一个新的Compilation,
//这里主要是为了考虑到watch模式,它会在启动时先编译一次,然后监听文件变化,如果发生变化会重新开始编译
//每次编译都会产出一个新的Compilation,代表每次的编译结果
+ let compilation = new Compilation(this.options);
+ compilation.build(callback); //执行compilation的build方法进行编译,编译成功之后执行回调
}
}
+ class Compilation {
+ constructor(webpackOptions) {
+ this.options = webpackOptions;
+ this.modules = []; //本次编译所有生成出来的模块
+ this.chunks = []; //本次编译产出的所有代码块,入口模块和依赖的模块打包在一起为代码块
+ this.assets = {}; //本次编译产出的资源文件
+ this.fileDependencies = []; //本次打包涉及到的文件,这里主要是为了实现watch模式下监听文件的变化,文件发生变化后会重新编译
+ }
+ build(callback) {
+ //这里开始做编译工作,编译成功执行callback
+ callback()
+ }
+ }
(5)根据配置文件中的 entry 配置项找到所有的入口
(6)从入口文件出发,调用配置的 loader 规则,对各模块进行编译
Loader 本质上就是一个函数,接收资源文件或者上一个 Loader 产生的结果作为入参,最终输出转换后的结果。
(7)找出此模块所依赖的模块,再对依赖模块进行编译
(8)等所有模块都编译完成后,根据模块之间的依赖关系,组装代码块 chunk
一般来说,每个入口文件会对应一个代码块chunk,每个代码块chunk里面会放着本入口模块和它依赖的模块,这里暂时不考虑代码分割。
(9)把各个代码块 chunk 转换成一个一个文件加入到输出列表
(10)确定好输出内容之后,根据配置的输出路径和文件名,将文件内容写入到文件系统
(11)在执行上述步骤的同时,webpack会触发各种钩子函数,plugin会监听这些钩子函数,并在适当的时候运行。
热更新原理
bundle,chunk,module是什么?
module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:
我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。
Loader和Plugin的不同?
定义不同:
Loader:加载器,加载项目的各种文件,因为webpack原生只能解析js文件,要打包其他文件,就要用loader,其作用是为了让wepack拥有拥有了加载和解析⾮JavaScript⽂件的能⼒
Plugin:插件,用于拓展或者增强webpack的功能,webpack运行的生命周期中会广播许多事件,相当于生命周期钩子,Plugin会监听这些事件,在适当的时候就运行。
用法不同:
Loader:在module.rules中配置。接test、use、loader、options
module: {
rules: [
{
test: /.js$/,
use: [
{
loader: "simpleLoader",
options: {
/* ... */
},
},
],
},
],
},
Plugin:单独配置,与module同级。
plugins: [
// 代码压缩
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /.css$/g,
cssProcessor: cssnano,
}),
// 提取公共资源包-速度优化(基础包cdn)
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: 'https://11.url.cn/now/lib/16.2.0/react.min.js',
global: 'React',
},
{
module: 'react-dom',
entry: 'https://11.url.cn/now/lib/16.2.0/react-dom.min.js',
global: 'ReactDOM',
},
],
}),
],
常见的Loader
- file-loader:把⽂件输出到⼀个⽂件夹中,在代码中通过相对 URL 去引⽤输出的⽂件
- url-loader:和 file-loader 类似,但是能在⽂件很⼩的情况下以 base64 的⽅式把⽂件内容注⼊到代码中去
- source-map-loader:加载额外的 Source Map ⽂件,以⽅便断点调试
- image-loader:加载并且压缩图⽚⽂件
- babel-loader:把 ES6 转换成 ES5
- css-loader:加载 CSS,⽀持模块化、压缩、⽂件导⼊等特性
- style-loader:把 CSS 代码注⼊到 JavaScript 中,通过 DOM 操作去加载 CSS。
- eslint-loader:通过 ESLint 检查 JavaScript 代码
常见的Plugin
- define-plugin:定义环境变量
- html-webpack-plugin:简化html⽂件创建
- uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码
- webpack-parallel-uglify-plugin: 多核压缩,提⾼压缩速度
- webpack-bundle-analyzer: 可视化webpack输出⽂件的体积
- mini-css-extract-plugin: CSS提取到单独的⽂件中,⽀持按需加载
打包后的bundle分析
app.bundle.js里面其实就是个立即执行函数,类似如下结构:
(function (modules){
console.log(modules['app'])
})
({
'app': function(){
return 20
},
'test': function(){
return 40
}
})
事实上,其函数体就是webpack生成的运行时函数,参数就是个 键值对 对象。
手写Loader
手写Plugin
webpack是怎样运行plugin的?
if (options.plugins && Array.isArray(options.plugins)) {
//这里的options.plugins就是webpack.config.js中的plugins
for (const plugin of options.plugins) {
plugin.apply(compiler); //执行插件的apply方法
}
}
所以,我们就写一个apply函数就行了。
// demo-plugin.js
class DemoPlugin {
apply(compiler) {
//在done(构建完成后执行)这个hook上注册自定义事件
compiler.hooks.done.tap("DemoPlugin", () => {
console.log("DemoPlugin:编译结束了");
});
}
}
module.exports = DemoPlugin;
// webpack.config.js
const DemoPlugin = require("./plugins/demo-plugin");
module.exports = {
mode: "development",
entry: "./src/index.js",
devtool: false,
plugins: [new DemoPlugin()],
};
// yarn build
yarn build
$ webpack
DemoPlugin:编译结束了
asset main.js 643 bytes [emitted] (name: main)
./src/index.js 476 bytes [built] [code generated]
webpack 5.74.0 compiled successfully in 71 ms
✨ Done in 0.64s.
优化配置:
如何编写可维护的webpack配置:
Vite 和 Webpack 对比
相同点:
- 都可以处理各种类型的文件,如HTML、CSS、JavaScript、图片等。
- 都支持模块化开发。
- 都支持代码压缩、优化和 Tree-shaking 等功能。
- 都可以通过插件扩展其功能。
- 都可以在开发和生产环境中使用。
不同点:
- 构建速度 Vite 的开发服务器会提前解析和编译 ES Modules 并且缓存起来,当需要重新构建时只需要构建修改部分,大幅加快了构建速度。而 Webpack 在每次构建时需要遍历整个依赖树,并进行一系列复杂的操作,因此构建速度较慢。
- 原理 Vite 使用浏览器原生 ES Modules 方式加载模块,不需要像 Webpack 一样将所有模块打包成一个或多个 bundle。这种方式可以避免冗余的代码和性能瓶颈,同时使得开发阶段可以直接使用原生的 ESM 语法,而打包后仍然可以兼容老旧浏览器。
- 插件系统 Vite 的插件系统更为简单,只需要编写一个普通的 JavaScript 模块即可。而 Webpack 的插件系统设计更为复杂,需要了解其内部机制才能编写高质量的插件。
- 生态环境 Webpack 的生态环境非常丰富,有大量的社区支持和第三方插件可以使用。而 Vite 相对较新,在生态环境上还不如 Webpack 成熟。
webpack5 的新特性
- 支持WebAssembly模块
Webpack 5支持将WebAssembly模块打包到JavaScript bundle中,以便于在浏览器中使用。
- 持久化缓存
Webpack 5引入了一种名为“持久化缓存”的新功能,该功能可加快构建时间。这个功能会将构建结果缓存在本地磁盘上,然后在下次重新构建时进行比较,从而避免重复构建。
- 更好的Tree Shaking
Tree shaking是一种用于剪裁未使用代码的技术。Webpack 5通过改进其内部算法,使得Tree shaking更加有效。
- Module Federation
Module Federation是一种新的功能,它可以让多个Webpack应用程序共享模块。这个功能可以帮助开发者更容易地将多个单独的应用程序组合成一个整体。
- 优化输出文件大小
Webpack 5引入了一种名为“资源模块”的新类型,可以让Webpack更好地优化输出文件的大小。这个功能使得开发者能够更轻松地管理和压缩他们的输出文件。
- 支持ES模块导入语法
Webpack 5支持ES模块导入语法,这意味着开发者可以直接使用导入和导出语法来管理他们的模块依赖关系。
- 支持顶层await
Webpack 5支持顶层await,这意味着开发者可以在顶层上下文中使用await关键字。