共计 10121 个字符,预计需要花费 26 分钟才能阅读完成。
一 目录
不折腾的前端,和咸鱼有什么区别
目录 |
---|
一 目录 |
二 前言 |
三 第一步 转换代码、生成依赖 |
四 第二步 生成依赖图谱 |
五 第三步 生成代码字符串 |
二 前言
返回目录
参考文章:实现一个简略的 Webpack
Webpack 的实质就是一个模块打包器,工作就是将每个模块打包成相应的 bundle
。
首先 ,咱们须要筹备目录:
+ 我的项目根门路 || 文件夹
- index.js - 主入口
- message.js - 主入口依赖文件
- word.js - 主入口依赖文件的依赖文件
- bundler.js - 打包器
- bundle.js - 打包后寄存代码的文件
最终的我的项目地址:all-for-one – 031- 手写 Webpack
如果小伙伴懒得敲,那能够看下面仓库的最终代码。
而后 ,咱们 index.js
、message.js
、word.js
内容如下:
index.js
// index.js
import message from "./message.js";
console.log(message);
message.js
// message.js
import {word} from "./word.js";
const message = `say ${word}`;
export default message;
word.js
// word.js
export const word = "hello";
最初 ,咱们实现一个 bundler.js
文件,将 index.js
当成入口,将外面牵扯的文件都本义并执行即可!
实现思路:
- 利用
babel
实现代码转换,并生成单个文件的依赖 - 生成依赖图谱
- 生成最初打包代码
上面分 3 章尝试这个内容。
三 第一步 转换代码、生成依赖
返回目录
这一步须要利用 babel
帮忙咱们进行转换,所以先装包:
npm i @babel/parser @babel/traverse @babel/core @babel/preset-env -D
转换代码须要:
- 利用
@babel/parser
生成 AST 形象语法树 - 利用
@babel/traverse
进行 AST 遍历,记录依赖关系 - 通过
@babel/core
和@babel/preset-env
进行代码的转换
而后增加内容:
bundler.js
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");
// 第一步: 转换代码、生成依赖
function stepOne(filename) {
// 读入文件
const content = fs.readFileSync(filename, "utf-8");
const ast = parser.parse(content, {sourceType: "module", // babel 官网规定必须加这个参数,不然无奈辨认 ES Module});
const dependencies = {};
// 遍历 AST 形象语法树
traverse(ast, {
// 获取通过 import 引入的模块
ImportDeclaration({node}) {const dirname = path.dirname(filename);
const newFile = "./" + path.join(dirname, node.source.value);
// 保留所依赖的模块
dependencies[node.source.value] = newFile;
},
});
// 通过 @babel/core 和 @babel/preset-env 进行代码的转换
const {code} = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"],
});
return {
filename, // 该文件名
dependencies, // 该文件所依赖的模块汇合 (键值对存储)
code, // 转换后的代码
};
}
console.log('--- step one ---');
const one = stepOne('./index.js');
console.log(one);
fs.writeFile('bundle.js', one.code, () => {console.log('写入胜利');
});
通过 Node 的形式运行这段代码:node bundler.js
:
--- step one ---
{
filename: './index.js',
dependencies: {'./message.js': './message.js'},
code:`
"use strict";
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { "default": obj};
}
// index.js
console.log(_message["default"]);
`,
}
- 入口
filename
:index.js
- 依赖
message.js
- 本义代码
code
所以 jsliang 将 code
提取到 bundle.js
中进行查看:
bundler.js
// ... 代码省略
fs.writeFile('bundle.js', one.code, () => {console.log('写入胜利');
});
bundle.js
"use strict";
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { "default": obj}; }
// index.js
console.log(_message["default"]);
解读下这个文件内容:
use strict
:应用严格模式_interopRequireDefault
:对不合乎babel
规范的模块增加default
属性,并指向本身对象以防止exports.default
出错
所以当初这份文件的内容是能够运行的了,然而你运行的时候会报错,报错内容如下:
import {word} from "./word.js";
^
SyntaxError: Unexpected token {
也就是说咱们执行到 message.js
,然而它外面的内容没法运行,因为 import
是 ES6
内容嘛。
咋整,持续看上面内容。
四 第二步 生成依赖图谱
返回目录
既然咱们只生成了一份本义后的文件:
--- step one ---
{
filename: './index.js',
dependencies: {'./message.js': './message.js'},
code:`
"use strict";
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { "default": obj};
}
// index.js
console.log(_message["default"]);
`,
}
那么咱们能够依据其中的 dependencies
进行递归,将整个依赖图谱都找进去:
bundler.js
// ... 省略后面内容
// 第二步:生成依赖图谱
// entry 为入口文件
function stepTwo(entry) {const entryModule = stepOne(entry);
// 这个数组是外围,尽管当初只有一个元素,往后看你就会明确
const graphArray = [entryModule];
for (let i = 0; i < graphArray.length; i++) {const item = graphArray[i];
const {dependencies} = item; // 拿到文件所依赖的模块汇合 (键值对存储)
for (let j in dependencies) {graphArray.push(stepOne(dependencies[j])); // 敲黑板!要害代码,目标是将入口模块及其所有相干的模块放入数组
}
}
// 接下来生成图谱
const graph = {};
graphArray.forEach((item) => {graph[item.filename] = {
dependencies: item.dependencies,
code: item.code,
};
});
return graph;
}
console.log('--- step two ---');
const two = stepTwo('./index.js');
console.log(two);
let word = '';
for (let i in two) {word = word + two[i].code + '\n\n';
}
fs.writeFile('bundle.js', word, () => {console.log('写入胜利');
});
所以当咱们 node bundler.js
的时候,会打印内容进去:
--- step two ---
{
'./index.js': {dependencies: { './message.js': './message.js'},
code: '"use strict";\n\nvar _message = _interopRequireDefault(require("./message.js"));\n\nfunction _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { "default": obj}; }\n\n// index.js\nconsole.log(_message["default"]);'},'./message.js': {dependencies: { './word.js': './word.js'},
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports["default"] = void 0;\n\nvar _word = require("./word.js");\n\n// message.js\nvar message = "say".concat(_word.word);\nvar _default = message;\nexports["default"] = _default;'},'./word.js': {dependencies: {},
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.word = void 0;\n// word.js\nvar word = "hello";\nexports.word = word;'
}
}
能够看到咱们将整个依赖关系中的文件都搜寻进去,并通过 babel
进行了转换,而后 jsliang 通过 Node
的 fs
模块将其写进了 bundle.js
中:
bundler.js
let word = '';
for (let i in two) {word = word + two[i].code + '\n\n';
}
fs.writeFile('bundle.js', word, () => {console.log('写入胜利');
});
再来看 bundle.js
内容:
bundle.js
"use strict";
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { "default": obj}; }
// index.js
console.log(_message["default"]);
"use strict";
Object.defineProperty(exports, "__esModule", {value: true});
exports["default"] = void 0;
var _word = require("./word.js");
// message.js
var message = "say".concat(_word.word);
var _default = message;
exports["default"] = _default;
"use strict";
Object.defineProperty(exports, "__esModule", {value: true});
exports.word = void 0;
// word.js
var word = "hello";
exports.word = word;
跟步骤一的解析差不多,不过这样子的内容是没法运行的,毕竟咱们塞到同一个文件中了,所以须要步骤三咯。
五 第三步 生成代码字符串
返回目录
最初一步咱们实现上面代码:
bundler.js
// 上面是生成代码字符串的操作
function stepThree(entry){// 要先把对象转换为字符串,不然在上面的模板字符串中会默认调取对象的 toString 办法,参数变成 [Object object],显然不行
const graph = JSON.stringify(stepTwo(entry))
return `(function(graph) {
// require 函数的实质是执行一个模块的代码,而后将相应变量挂载到 exports 对象上
function require(module) {
// localRequire 的实质是拿到依赖包的 exports 变量
function localRequire(relativePath) {return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code) {eval(code);
})(localRequire, exports, graph[module].code);
return exports; // 函数返回指向局部变量,造成闭包,exports 变量在函数执行后不会被捣毁
}
require('${entry}')
})(${graph})
`;
};
console.log('--- step three ---');
const three = stepThree('./index.js');
console.log(three);
fs.writeFile('bundle.js', three, () => {console.log('写入胜利');
});
能够看到,stepThree
返回的是一个立刻执行函数,须要传递 graph
:
(function(graph) {// 具体内容})(graph)
那么图谱(graph
)怎么来?须要通过 stepTwo(entry)
拿到了依赖图谱。
然而,因为步骤二返回的是对象啊,如果间接传进去对象,那么就会被本义,所以须要 JSON.stringify()
:
const graph = JSON.stringify(stepTwo(entry));
(function(graph) {// 具体内容})(graph)
那为什么这个函数(stepThree
)须要传递 entry
?起因在于咱们须要一个主入口,就好比 Webpack 单入口模式:
转变前后
// 转变前
const graph = JSON.stringify(stepTwo(entry));
(function(graph) {function require(module) {// ... 具体内容}
require('${entry}')
})(graph)
/* --- 分界线 --- */
// 转变后
const graph = JSON.stringify(stepTwo(entry));
(function(graph) {function require(module) {// ... 具体内容}
require('./index.js')
})(graph)
这样咱们就分明了,从 index.js
动手,而后再看外面具体内容:
function require(module) {
// localRequire 的实质是拿到依赖包的 exports 变量
function localRequire(relativePath) {return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(require, exports, code) {eval(code);
})(localRequire, exports, graph[module].code);
return exports; // 函数返回指向局部变量,造成闭包,exports 变量在函数执行后不会被捣毁
}
require('./index.js')
eval
是指 JavaScript 能够运行外面的字符串代码,eval('2 + 2')
会进去后果 4
,所以 eval(code)
就跟咱们第一步的时候,node bundle.js
一样,执行 code
外面的代码。
所以咱们执行 require(module)
外面的代码,先走:
(function(require, exports, code) {eval(code);
})(localRequire, exports, graph[module].code);
此刻这个代码中,传递的参数有 3 个:
require
:如果在eval(code)
执行代码期间,碰到require
就调用localRequire
办法exports
:如果在eval(code)
执行代码期间,碰到exports
就将外面内容设置到对象exports
中graph[module].code
:一开始module
是'./index.js'
,所以查找graph
中'./index.js'
对应的code
,将其传递进eval(code)
外面
有的小伙伴会好奇这代码怎么走的,咱们能够先看上面一段代码:
const localRequire = (abc) => {console.log(abc);
};
const code = `
console.log(456);
doRequire(123)
`;
(function(doRequire, code) {eval(code);
})(localRequire, code);
这段代码中,执行的 doRequire
其实就是传入进来的 localRequire
办法,最终输入 456
和 123
。
当初,再回头来看:
区块一:
bundle.js
function require(module) {
// localRequire 的实质是拿到依赖包的 exports 变量
function localRequire(relativePath) {return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function (require, exports, code) {eval(code);
})(localRequire, exports, graph[module].code);
return exports; // 函数返回指向局部变量,造成闭包,exports 变量在函数执行后不会被捣毁
}
require("./index.js");
它先执行 立刻执行函数 (function (require, exports, code) {})()
,再到 eval(code)
,从而执行上面代码:
区块二:
graph['./index.js'].code
"use strict";
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { "default": obj}; }
// index.js
console.log(_message["default"]);
在碰到 require("./message.js")
的时候,持续进去下面【区块一】的代码,因为此刻的 require
是:
function localRequire(relativePath) {return require(graph[module].dependencies[relativePath]);
}
所以咱们再调用本人的 require()
办法,将内容传递进去,变成:require('./message.js')
。
……以此类推,直到 './word.js'
外面没有 require()
办法体了,咱们再执行上面内容,将 exports
导出去。
这就是这段内容的运行流程。
至于其中细节咱们就不一一赘述了,小伙伴们如果还没看懂能够自行断点调试,这外面的代码口头形容的话 jsliang 讲得不是分明。
最初咱们看看输入整顿后的 bundle.js
:
bundle.js
(function (graph) {
// require 函数的实质是执行一个模块的代码,而后将相应变量挂载到 exports 对象上
function require(module) {
// localRequire 的实质是拿到依赖包的 exports 变量
function localRequire(relativePath) {return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function (require, exports, code) {eval(code);
})(localRequire, exports, graph[module].code);
return exports; // 函数返回指向局部变量,造成闭包,exports 变量在函数执行后不会被捣毁
}
require("./index.js");
})({
"./index.js": {dependencies: { "./message.js": "./message.js"},
code: `
"use strict";
var _message = _interopRequireDefault(require("./message.js"));
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { "default": obj}; }
// index.js
console.log(_message["default"]);
`,
},
"./message.js": {dependencies: { "./word.js": "./word.js"},
code: `
"use strict";
Object.defineProperty(exports, "__esModule", {value: true});
exports["default"] = void 0;
var _word = require("./word.js");
// message.js
var message = "say".concat(_word.word);
var _default = message;
exports["default"] = _default;
`,
},
"./word.js": {dependencies: {},
code: `
"use strict";
Object.defineProperty(exports, "__esModule", {value: true});
exports.word = void 0;
// word.js
var word = "hello";
exports.word = word;',
},
});
此时咱们 node bundle.js
,就能够获取到:
say hello
这样咱们就手撸实现了单入口的 Webpack 简略实现。
jsliang 的文档库由 梁峻荣 采纳 常识共享 署名 - 非商业性应用 - 雷同形式共享 4.0 国内 许可协定 进行许可。<br/> 基于 https://github.com/LiangJunrong/document-library 上的作品创作。<br/> 本许可协定受权之外的应用权限能够从 https://creativecommons.org/licenses/by-nc-sa/2.5/cn/ 处取得。