目录
2.1.1 babylon —— 解析 JavaScript 语法,生产 AST 语法树
2.1.2 babel-traverse —— 对 AST 进行遍历、转换的工具
2.1.3 transformFromAst —— 将 ES6、ES7 等高级的语法,转化为 ES5 的语法
2.4 自定义实现 require 方法,找到导出变量的引用逻辑
2.5 创建 dist 目录,将打包的内容写入 main.js 中
6. Webpack 热更新(Hot Module Replacement)
Webpack 是前端最常用的构建工具之一,为了解 Webpack 整体打包流程中:需要做的事,需要输出的结果,因此手写 mini 版 Webpack
深入浅出 Webpack · 深入浅出 Webpackhttp://webpack.wuhaolin.cn/
1. mini 版 Webpack 打包流程
- 从入口文件开始解析
- 查找入口文件引入了哪些 JavaScript 文件,找到依赖关系
- 递归遍历引入的其他 JavaScript 文件,生成最终的依赖关系图谱
- 将 ES6 语法转化成 ES5
- 最终生成一个可以在浏览器加载执行的 JavaScript 文件
- mini 版 Webpack 未涉及 loader、plugin 等复杂功能,只是一个非常简化的例子
2. 创建 minipack.js
2.1 需要用到的插件库
2.1.1 babylon —— 解析 JavaScript 语法,生产 AST 语法树
// babylon 解析 JavaScript 语法,生产 AST 语法树(AST 能把 JavaScript 代码,转化为 JSON 数据结构)
const babylon = require('babylon');
2.1.2 babel-traverse —— 对 AST 进行遍历、转换的工具
// babel-traverse 是一个对 AST 进行遍历、转换的工具
const traverse = require('babel-traverse').default;
2.1.3 transformFromAst —— 将 ES6、ES7 等高级的语法,转化为 ES5 的语法
// 将 es6、es7 等高级的语法,转化为 es5 的语法
const { transformFromAst } = require('babel-core');
2.2 读取文件内容,并提取它的依赖关系
-
// 每一个 JavaScript 文件,对应一个id
-
let
ID =
0;
-
-
/**
-
* 读取文件内容,并提取它的依赖关系
-
* @param {*} filename 文件路径
-
* @returns 文件id(唯一)、文件路径、文件的依赖关系、文件代码
-
*/
-
function
createAsset(
filename) {
-
const content = fs.
readFileSync(filename,
'utf-8');
-
-
// 获取该文件对应的 AST 抽象语法树
-
const ast = babylon.
parse(content, {
-
sourceType:
'module',
-
});
-
-
// dependencies —— 保存 所依赖模块的 相对路径
-
const dependencies = [];
-
-
// 通过查找 import 节点,找到该文件的依赖关系(也就是文件中 import 的其他文件)
-
traverse(ast, {
-
ImportDeclaration:
({ node }) => {
-
// 查找import节点
-
dependencies.
push(node.
source.
value);
-
},
-
});
-
-
// 通过递增计数器,为此模块分配唯一标识符,用于缓存已解析过的文件
-
const id =
ID++;
-
-
// 用 '@babel/preset-env' 将 代码 转换为 浏览器可以运行的内容
-
const { code } =
transformFromAst(ast,
null, {
-
// `presets` 选项是一组规则,告诉 `babel` 如何传输我们的代码
-
presets: [
'@babel/preset-env'],
-
});
-
-
// 返回此模块的相关信息
-
return {
-
id,
// 文件id(唯一)
-
filename,
// 文件路径
-
dependencies,
// 文件的依赖关系
-
code,
// 文件代码
-
};
-
}
2.3 递归获取项目的所有依赖(绘制项目依赖图谱)
-
/**
-
* 递归获取项目的所有依赖(绘制项目依赖图谱)
-
* @description 从入口文件开始,递归读取各个依赖文件
-
* @param {*} entry 项目入口文件
-
* @returns
-
*/
-
function
createGraph(
entry) {
-
// 读取 入口文件 内容,并提取它的依赖关系
-
const mainAsset =
createAsset(entry);
-
// 入口文件的信息(文件id(唯一)、文件路径、文件的依赖关系、文件代码),作为第一项放到数组里
-
const queue = [mainAsset];
-
-
for (
const asset
of queue) {
-
asset.
mapping = {};
-
// 获取 这个模块的 所在的目录
-
const dirname = path.
dirname(asset.
filename);
-
// 遍历 这个模块的 文件依赖关系
-
asset.
dependencies.
forEach(
(relativePath) => {
-
/**
-
* 获取 每个依赖文件的 绝对路径
-
* 通过将相对路径与父资源目录的路径连接,将相对路径转变为绝对路径
-
*/
-
const absolutePath = path.
join(dirname, relativePath);
-
// 递归解析其中所引入的其他资源
-
const child =
createAsset(absolutePath);
-
asset.
mapping[relativePath] = child.
id;
-
// 将 `child` 推入队列, 通过 递归 实现获取项目所有依赖
-
queue.
push(child);
-
});
-
}
-
-
// queue这就是最终的依赖关系图谱
-
return queue;
-
}
2.4 自定义实现 require 方法,找到导出变量的引用逻辑
-
/**
-
* 自定义实现 require 方法,找到导出变量的引用逻辑
-
* @param {*} graph 项目的所有依赖
-
* @returns
-
*/
-
function
bundle(
graph) {
-
let modules =
'';
-
graph.
forEach(
(mod) => {
-
modules +=
`${mod.id}: [
-
function (require, module, exports) { ${mod.code} },
-
${JSON.stringify(mod.mapping)},
-
],`;
-
});
-
const result =
`
-
(function(modules) {
-
function require(id) {
-
const [fn, mapping] = modules[id];
-
function localRequire(name) {
-
return require(mapping[name]);
-
}
-
const module = { exports : {} };
-
fn(localRequire, module, module.exports);
-
return module.exports;
-
}
-
require(0);
-
})({${modules}})
-
`;
-
return result;
-
}
2.5 创建 dist 目录,将打包的内容写入 main.js 中
-
// ❤️ 通过入口文件,递归获取项目的所有依赖
-
const graph =
createGraph(
'./example/entry.js');
-
// 自定义实现 require 方法,找到导出变量的引用逻辑
-
const result =
bundle(graph);
-
-
// 创建 dist 目录,将打包的内容写入 main.js 中
-
fs.
mkdir(
'dist',
(err) => {
-
if (!err) {
-
fs.
writeFile(
'dist/main.js', result,
(err1) => {
-
if (!err1)
console.
log(
'打包成功');
-
});
-
}
-
});
3. 测试 minipack.js
3.1 添加 minipack.js 同级文件/文件夹
在 minipack.js 同级的位置,添加:
- example 文件夹(相当于真实项目文件)
- package.json(使用 npm init -y 初始化)
- index.html(引入 minipack 打包后的文件,并展示)
3.2 初始化测试目录 example
name.js
export const name = 'mini Webpack By Lyrelion';
message.js
-
import { name }
from
'./name.js';
-
-
export
default
`hello ${name}!`;
entry.js(项目入口文件)
-
import message
from
'./message.js';
-
-
// 创建 <p></p> DOM节点
-
let p =
document.
createElement(
'p');
-
// 将 message 的内容显示到页面中
-
p.
innerHTML = message;
-
// 追加 DOM 节点
-
document.
body.
appendChild(p);
3.3 完善 package.json,安装必要依赖
根据前面写 minipack.js 时用到的依赖,补充 package.json 文件
-
{
-
"name":
"webpack",
-
"version":
"1.0.0",
-
"description":
"",
-
"main":
"entry.js",
-
"scripts": {
-
"test":
"echo \"Error: no test specified\" && exit 1"
-
},
-
"keywords": [],
-
"author":
"",
-
"license":
"ISC",
-
"dependencies": {
-
"@babel/core":
"^7.20.7",
-
"@babel/preset-env":
"^7.20.2",
-
"babel-core":
"^7.0.0-beta.41",
-
"babel-traverse":
"^6.26.0",
-
"babylon":
"^6.18.0",
-
"fs":
"^0.0.1-security",
-
"path":
"^0.12.7"
-
}
-
}
这里强调一下 babel,安装的时候报了很多错
解决方案:我是根据报错提示,调整 babel-core 版本,再重新安装,如下所示
3.4 完善 index.html,填充打包文件
在 minipack.js 中,写死了打包后输出的文件位置,因此在 index.html 里写死即可
-
<!DOCTYPE html>
-
<html>
-
<head>
-
<meta charset="utf-8" />
-
<title>mini Webpack
</title>
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
-
</head>
-
<body>
-
<!-- 引入打包后的 main.js -->
-
<script src="./dist/main.js">
</script>
-
</body>
-
</html>
3.5 项目打包、打包效果展示
打包前的目录结构:
执行打包命令:node minipack.js
打包成功后的目录结构:
打包效果:
4. 分析打包生成的文件 dist/main.js
main.js 里有一个立即执行函数,接收一个对象,该对象有三个属性
- 0 代表entry.js;
- 1 代表message.js;
- 2 代表name.js
文件执行过程:
- 从 require(0) 开始执行,调用内置的自定义 require 函数
- 执行 fn 函数
- 执行 require('./message.js'),执行 require(mapping['./message.js']),转化为 require(1),获取 modules[1],也就是执行 message.js 的内容
- 执行 require('./name.js'),转化为 require(2),执行 name.js 的内容
- 通过递归调用,将代码中导出的属性,放到 exports 对象中,一层层导出到最外层
-
最终通过 _message["default"] 获取导出的值,页面显示 hello mini Webpack By Lyrelion!
-
// 文件里是一个立即执行函数
-
(
function (
modules) {
-
function
require(
id) {
-
const [fn, mapping] = modules[id];
-
// ⬅️ 第四步 跳转到这里 此时 mapping[name] = 1,继续执行 require(1)
-
// ⬅️ 第六步 又跳转到这里 此时 mapping[name] = 2,继续执行 require(2)
-
function
localRequire(
name) {
-
return
require(mapping[name]);
-
}
-
const
module = {
exports: {} };
-
// ⬅️ 第二步 执行 fn
-
fn(localRequire,
module,
module.
exports);
-
return
module.
exports;
-
}
-
// ⬅️ 第一步 执行 require(0)
-
require(
0);
-
})({
-
// entry.js
-
0: [
-
function (
require, module, exports) {
-
"use strict";
-
-
// ⬅️ 第三步 跳转到这里 继续执行 require('./message.js')
-
var _message =
_interopRequireDefault(
require(
"./message.js"));
-
function
_interopRequireDefault(
obj) {
return obj && obj.
__esModule ? obj : {
"default": obj }; }
-
// 创建 <p></p> DOM节点
-
var p =
document.
createElement(
'p');
-
// ⬅️ 最后一步 将内容写到 p 标签中
-
// 将 message 的内容显示到页面中
-
p.
innerHTML = _message[
"default"];
-
// 追加 DOM 节点
-
document.
body.
appendChild(p);
-
},
-
{
"./message.js":
1 },
-
// message.js
-
],
1: [
-
function (
require, module, exports) {
-
"use strict";
-
-
Object.
defineProperty(
exports,
"__esModule", {
-
value:
true
-
});
-
exports[
"default"] =
void
0;
-
// ⬅️ 第五步 跳转到这里 继续执行 require('./name.js')
-
var _name =
require(
"./name.js");
-
var _default =
"hello ".
concat(_name.
name,
"!");
-
// ⬅️ 第八步 跳到这里 此时 _name 为 {name: 'mini Webpack By Lyrelion'}, 在 exports 对象上设置 default 属性,值为 'hello mini Webpack By Lyrelion!'
-
exports[
"default"] = _default;
-
},
-
{
"./name.js":
2 },
-
// name.js
-
],
2: [
-
function (
require, module, exports) {
-
"use strict";
-
-
Object.
defineProperty(
exports,
"__esModule", {
-
value:
true
-
});
-
exports.
name =
void
0;
-
var name =
'mini Webpack By Lyrelion';
-
// ⬅️ 第七步 跳到这里 在传入的 exports 对象上添加 name 属性,值为'mini Webpack By Lyrelion'
-
exports.
name = name;
-
},
-
{},
-
],
-
})
5. 真正的 Webpack 打包流程
Webpack 从项目的 entry 入口文件开始递归分析,调用 Loader 对不同文件进行编译;因为 Webpack 默认只能识别 JavaScript 代码,所以 .css 文件、.vue 文件等,必须要通过 Loader 解析成 JavaScript 代码,才能被 Webpack 识别
利用 babel(babylon)将 JavaScript 代码转化为 AST 抽象语法树;
通过 babel-traverse 对 AST 抽象语法树 进行遍历,找到文件的 import 引用节点(依赖关系);因为 依赖文件 都是通过 import 的方式引入,所以找到 import 节点,就找到了文件的依赖关系
给 每个模块 生成唯一标识 ID,并将解析过的模块缓存起来;如果其他地方也引入该模块,就无需重新解析
根据依赖关系,生成依赖图谱,递归遍历所有依赖图谱的模块,组装成一个个包含多个模块的 Chunk(块);
最后,将生成的文件,输出到 output 目录中
6. Webpack 热更新(Hot Module Replacement)
刷新一般分为两种:
- 一种是页面刷新,不保留页面状态,直接 window.location.reload()
- 一种是基于 WDS(webpack-dev-server)的模块热替换,局部刷新页面上发生变化的模块,同时保留当前页面的状态,比如复选框的选中状态、输入框的输入等
6.1 什么是 Webpack 热更新?
开发过程中,代码改变后,Webpack 会重新编译,编译后浏览器替换修改的模块,无需刷新整个页面,就能进行局部更新,提升开发体验
HMR 作为 Webpack 内置的功能,可以通过 HotModuleReplacementPlugin 或 --hot 开启
6.2 热更新原理 —— WebSocket
基本原理:
- 通过 WebSocket 实现,建立 本地服务 和 浏览器 的双向通信;
- 当代码发生变化,并重新编译后,通知浏览器 重新请求 需要更新的模块,替换 原有的模块;
通过 webpack-dev-server 开启 server 服务,本地 server 启动之后,再去启动 WebSocket 服务,建立 本地服务 和 浏览器 的双向通信
Webpack 每次编译后,会生成一个 Hash 值,Hash 代表每一次编译的唯一标识。本次输出的 Hash 值会编译新生成的文件标识,被作为下次热更新的标识
Webpack 监听文件变化(通过 文件的生成时间 判断是否有变化),当文件变化后,重新编译
编译结束后,通知浏览器请求变化的资源,同时将新生成的 Hash 值传给浏览器,用于下次热更新使用
浏览器请求到最新的模块后,用新模块 替换 旧模块,从而实现 局部刷新
7. 参考文章
转载:https://blog.csdn.net/Lyrelion/article/details/128497183