前言
一起深入Wepcak , 尽管 Vite 非常火爆确实很香
,但是 Webpack 依然在企业占据主导地位以后我也不知道
, 学习深入非常有必要,主要介绍loader / plugin 和原理分析,而那些配置只介绍部分常用的,我希望您能学会看文档,中文 or 英文我看不懂,以后必学
所以
一位程序员的职业生涯大约十年,只有人寿命的十分之一。前端项目只是你生活工作的一部分,而你却是它的全部,你是他的灵魂。请放下长时间的游戏、工作时的摸鱼。多学习来以最完美的状态好好陪你项目!
正文
文章底部小彩蛋,请你一步一步看过去!!
知识点
- Webpack 前置基础
- Loader 机制(手写一个)
- Plugin 机制
- 小彩蛋(介绍部分Webpack原理分析)
Webpack 前置基础(配置)
const path = require('path');
module.exports = {
mode: "production", // "production" | "development" | "none"
// Chosen mode tells webpack to use its built-in optimizations accordingly.
entry: "./app/entry", // string | object | array
// 这里应用程序开始执行
// webpack 开始打包
output: {
// webpack 如何输出结果的相关选项
path: path.resolve(__dirname, "dist"), // string
// 所有输出文件的目标路径
// 必须是绝对路径(使用 Node.js 的 path 模块)
filename: "bundle.js", // string
// 「入口分块(entry chunk)」的文件名模板(出口分块?)
publicPath: "/assets/", // string
// 输出解析文件的目录,url 相对于 HTML 页面
library: "MyLibrary", // string,
// 导出库(exported library)的名称
libraryTarget: "umd", // 通用模块定义
// 导出库(exported library)的类型
/* 高级输出配置(点击显示) */
},
module: {
// 关于模块配置
rules: [
// 模块规则(配置 loader、解析器等选项)
{
test: /\.jsx?$/,
include: [
path.resolve(__dirname, "app")
],
exclude: [
path.resolve(__dirname, "app/demo-files")
],
// 这里是匹配条件,每个选项都接收一个正则表达式或字符串
// test 和 include 具有相同的作用,都是必须匹配选项
// exclude 是必不匹配选项(优先于 test 和 include)
// 最佳实践:
// - 只在 test 和 文件名匹配 中使用正则表达式
// - 在 include 和 exclude 中使用绝对路径数组
// - 尽量避免 exclude,更倾向于使用 include
issuer: { test, include, exclude },
// issuer 条件(导入源)
enforce: "pre",
enforce: "post",
// 标识应用这些规则,即使规则覆盖(高级选项)
loader: "babel-loader",
// 应该应用的 loader,它相对上下文解析
// 为了更清晰,`-loader` 后缀在 webpack 2 中不再是可选的
// 查看 webpack 1 升级指南。
options: {
presets: ["es2015"]
},
// loader 的可选项
},
{
test: /\.html$/,
test: "\.html$"
use: [
// 应用多个 loader 和选项
"htmllint-loader",
{
loader: "html-loader",
options: {
/* ... */
}
}
]
},
{ oneOf: [ /* rules */ ] },
// 只使用这些嵌套规则之一
{ rules: [ /* rules */ ] },
// 使用所有这些嵌套规则(合并可用条件)
{ resource: { and: [ /* 条件 */ ] } },
// 仅当所有条件都匹配时才匹配
{ resource: { or: [ /* 条件 */ ] } },
{ resource: [ /* 条件 */ ] },
// 任意条件匹配时匹配(默认为数组)
{ resource: { not: /* 条件 */ } }
// 条件不匹配时匹配
],
/* 高级模块配置(点击展示) */
},
resolve: {
// 解析模块请求的选项
// (不适用于对 loader 解析)
modules: [
"node_modules",
path.resolve(__dirname, "app")
],
// 用于查找模块的目录
extensions: [".js", ".json", ".jsx", ".css"],
// 使用的扩展名
alias: {
// 模块别名列表
"module": "new-module",
// 起别名:"module" -> "new-module" 和 "module/path/file" -> "new-module/path/file"
"only-module$": "new-module",
// 起别名 "only-module" -> "new-module",但不匹配 "only-module/path/file" -> "new-module/path/file"
"module": path.resolve(__dirname, "app/third/module.js"),
// 起别名 "module" -> "./app/third/module.js" 和 "module/file" 会导致错误
// 模块别名相对于当前上下文导入
},
},
devtool: "source-map", // enum
// 通过在浏览器调试工具(browser devtools)中添加元信息(meta info)增强调试
// 牺牲了构建速度的 `source-map' 是最详细的。
devServer: {
proxy: { // proxy URLs to backend development server
'/api': 'http://localhost:3000'
},
contentBase: path.join(__dirname, 'public'), // boolean | string | array, static file location
compress: true, // enable gzip compression
historyApiFallback: true, // true for index.html upon 404, object for multiple paths
hot: true, // hot module replacement. Depends on HotModuleReplacementPlugin
https: false, // true for self-signed, object for cert authority
noInfo: true, // only errors & warns on hot reload
// ...
},
plugins: [
// ...
],
// 附加插件列表
/* 高级配置(点击展示) */
}
上面内容高级 CV 操作来自Webpack官网仅贴出常用配置!这个不是主体,进入写一个环节
- entry:入口,Webpack 执行构建的第一步将从 entry 开始,可抽象成输入。
- module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 entry 开始递归找出所有依赖的模块。
- chunk:代码块,一个 chunk 由多个模块组合而成,用于代码合并与分割。
- loader:模块转换器,用于把模块原内容按照需求转换成新内容。
- plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。
你得了解上面基本信息后,才可以进入下一步
Loader 机制(手写一个)
简单来说 loader 是一个 可以获取你入口文件源代码的一个函数,函数本身参数就是源代码。
实现一个读取图片的 loader 并没有你想象的那么难
- 获取图片的buffer
- 转base64 / 写入 buffer 生成图片
动手试试
// webpack.config.js
module: {
rules: [
{
test: /\.(png)|(jpg)|(gif)$/, use: [{
loader: "./loaders/img-loader.js",
options: {
limit: 3000, //3000字节以上使用图片,3000字节以内使用base64
filename: "img-[contenthash:5].[ext]"
}
}]
}
]
}
获取模块配置项
在 Loader 中获取用户传入的 options,通过 loader-utils 的 getOptions 方法获取:
var loaderUtil = require("loader-utils")
function loader(buffer) { //给的是buffer
console.log("文件数据大小:(字节)", buffer.byteLength);
var { limit = 1000, filename = "[contenthash].[ext]" } = loaderUtil.getOptions(this);
if (buffer.byteLength >= limit) {
var content = getFilePath.call(this, buffer, filename);
}
else{
var content = getBase64(buffer)
}
return `module.exports = \`${content}\``;
}
loader.raw = true;
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据
module.exports = loader;
// 获取base 64 格式
function getBase64(buffer) {
return "data:image/png;base64," + buffer.toString("base64");
}
// 构建图片 生成路径
function getFilePath(buffer, name) {
var filename = loaderUtil.interpolateName(this, name, {
content: buffer
});
this.emitFile(filename, buffer);
return filename;
}
上面通过 this.emitFile 进行文件写入
同步与异步
Loader 有同步和异步之分,上面的 Loader 都是同步的 Loader,因为它们的转换流程都是同步的,转换完成后再返回结果。但有些场景下转换的步骤只能是异步完成的,例如你需要通过网络请求才能得出结果,如果采用同步的方式 网络请求 就会阻塞整个构建,导致构建非常缓慢。
module.exports = function(source) {
// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
var callback = this.async();
someAsyncOperation(source, function(err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast);
});
};
缓存加速
在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢。为此,Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其他依赖的文件没有发生变化时,是不会重新调用对应的 Loader 去执行转换操作的。
如果你想让 Webpack 不缓存该 Loader 的处理结果,可以这样:
module.exports = function(source) {
// 关闭该 Loader 的缓存功能
this.cacheable(false);
return source;
};
知道了 Webpack 核心loader 再来介绍一下 plugin
Plugin 机制
Plugin 可以干的活比Loader更多,更复杂,其本质是一个Class类
插件的基本结构 plugins 是可以用自身原型方法 apply 来实例化的对象。apply 只在安装插件被 Webpack 的 compiler 执行一次。apply 方法传入一个 webpck compiler 的引用,来访问编译器回调。
class HelloPlugin {
// 在构造函数中获取用户给该插件传入的配置
constructor(options) {
// ...
}
// Webpack 会调用 HelloPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler) {
// 在 emit 阶段插入钩子函数,用于特定时机处理额外的逻辑
compiler.hooks.emit.tap('HelloPlugin', compilation => {
// 在功能流程完成后可以调用 Webpack 提供的回调函数
});
// 如果事件是异步的,会带两个参数,第二个参数为回调函数,在插件处理完成任务时需要调用回调函数通知 Webpack,才会进入下一个处理流程
compiler.plugin('emit', function(compilation, callback) {
// 支持处理逻辑
// 处理完毕后执行 callback 以通知 Webpack
// 如果不执行 callback,运行流程将会一致卡在这不往下执行
callback();
});
}
}
module.exports = HelloPlugin;
使用插件时,只需要将它的实例放到 Webpack 的 Plugins 数组配置中:
const HelloPlugin = require('./hello-plugin.js');
module.exports = {
plugins: [new HelloPlugin({ options: true })],
};
先来分析以下 Webpack Plugin 的工作原理:
- 读取配置的过程中会先执行 new HelloPlugin(options) 初始化一个 HelloPlugin 获得其实例
- 初始化 compiler 对象后调用 HelloPlugin.apply(compiler) 给插件实例传入 compiler 对象
- 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件,并且可以通过 compiler 对象去操作 Webpack
在apply
的阶段你可以调用 compiler钩子
webpack的hoosk钩子其实是使用tapable直接注册在不同的阶段的,所以我们进行下一步分析
小彩蛋(介绍部分Webpack原理分析)
Webpack 本质是一个打包构建工具,我们不妨思考一下,它为我们做了什么。
- 读取
webpack.config.js
配置文件,找到入口 - 获取入口文件中的源代码 分析抽象语法树(babel实现)
- 分析过程 静态分析代码执行上下文和使用情况, 标记是否
Tree Shaking
- 核心的loader 和 plugin 在读取配置过程中执行函数,
tapable
注入钩子函数 - 最后输出在配置文件中的出口目录中
其实我们简易分析一下,也是非常好理解的
// 首先定义 Compiler
class Compiler {
constructor(options) {
// Webpack 配置
const { entry, output } = options;
// 入口
this.entry = entry;
// 出口
this.output = output;
// 模块
this.modules = [];
}
// 构建启动
run() {
// ...
}
// 重写 require 函数,输出 bundle
generate() {
// ...
}
}
使用 @babel/parser
和@babel/traverse
两个库分析源代码抽象语法树,找出所用模板依赖
而tapable
本质是一个javascript小型库, 内部是发布订阅模式。类似Node的EventEmitter。
Webpack 本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是 Tapable,Webpack 中最核心的负责编译的 Compiler 和负责创建 bundles 的 Compilation 都是 Tapable 的子类,并且实例内部的生命周期也是通过 Tapable 库提供的钩子类实现的。
总结
Webpack 本质是一个事件流机制的打包构建工具,从读取配置到分析语法树注册事件流输出文件的一个过程。其核心的loader
本质也是一个可以获取到源代码的一个函数,plugin
则是一个可以获取到整个事件生命周期的一个类~ 至此你应该对webpack有了更加深刻的认识,本文跳过了许多的细节问题,介绍核心知识,所以你还是得多看看配置的文档!
往期文章
【重拾落叶】Javascript执行期上下文、预编译
【重拾落叶】浏览器如何完整获取一个页面?(加载篇)
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!