手撸 webpack 源码
简介
webpack是一个打包模块化 JavaScript 的工具,在 webpack里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件。 webpack专注于构建模块化项目。
打包流程
当webpack 的配置只有一个出口时,不考虑分包的情况,其实我们只得到了一个bundle.js的文件,这个文件里包含了我们所有用到的js模块,可以直接被加载执行。
Webpack的运行流程是一个基于事件流串行的过程,从启动到结束会依次执行以下流程:
- 定义 Compiler 类
- 解析入口文件,获取 AST
我们这里使用@babel/parser,这是 babel7 的工具,来帮助我们分析内部的语法,包括 es6,返回一个 AST 抽象语法树。
- 找出所有依赖模块
Babel 提供了@babel/traverse(遍历)方法维护这 AST 树的整体状态,我们这里使用它来帮我们找出依赖模块。
- AST 转换为 code
将 AST 语法树转换为浏览器可执行代码,我们这里使用@babel/core 和 @babel/preset-env。
- 递归解析所有依赖项,生成依赖关系图
- 重写 require 函数,输出 bundle
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
手撸一个webpack
1. 定义 Compiler 类
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {}
// 重写 require函数,输出bundle
generate() {}
}
2. 解析入口文件,获取 AST
这里我们会用到@babel/parser这个包,它负责将代码解析为AST抽象语法树:
// webpack.config.js
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'main.js'
}
}
const fs = require('fs')
const parser = require('@babel/parser')
const options = require('./webpack.config')
const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const ast = Parser.getAst(this.entry)
}
// 重写 require函数,输出bundle
generate() {}
}
new Compiler(options).run()
我们去看下@babel/parser的文档:
提供了两个API,而我们目前用到的是parse这个API。
它的主要作用是 parses the provided code as an entire ECMAScript program,也就是将我们提供的代码解析成完整的ECMAScript代码的AST。
再看看该API提供的参数:
我们暂时用到的是sourceType,也就是用来指明我们要解析的代码是什么模块。
一个完整的AST展示:
{
"type": "CallExpression",
"start": 24,
"end": 43,
"loc": {
"start": { "line": 3, "column": 2 },
"end": { "line": 3, "column": 21 }
},
"callee": {
"type": "MemberExpression",
"start": 24,
"end": 35,
"loc": {
"start": { "line": 3, "column": 2 },
"end": { "line": 3, "column": 13 }
},
"object": {
"type": "Identifier",
"start": 24,
"end": 31,
"loc": {
"start": { "line": 3, "column": 2 },
"end": { "line": 3, "column": 9 },
"identifierName": "console"
},
"name": "console"
},
"property": {
"type": "Identifier",
"start": 32,
"end": 35,
"loc": {
"start": { "line": 3, "column": 10 },
"end": { "line": 3, "column": 13 },
"identifierName": "log"
},
"name": "log"
},
"computed": false
},
"arguments": [
{
"type": "StringLiteral",
"start": 36,
"end": 42,
"loc": {
"start": { "line": 3, "column": 14 },
"end": { "line": 3, "column": 20 }
},
"extra": { "rawValue": "data", "raw": "'data'" },
"value": "data"
}
]
}
3. 找出所有依赖模块
现在我们需要遍历AST,将用到的依赖收集起来。什么意思呢?其实就是将用import语句引入的文件路径收集起来。我们将收集起来的路径放到deps里。
前面我们提到过,遍历AST要用到@babel/traverse依赖包。
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const options = require('./webpack.config')
const traverse = require('@babel/traverse').default
const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {};
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename);
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value);
dependecies[node.source.value] = filepath;
},
});
return dependecies;
};
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const { getAst, getDependecies } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
}
// 重写 require函数,输出bundle
generate() {}
}
new Compiler(options).run()
我们去看下@babel/traverse的文档:
它的主要作用是 We can use it alongside the babel parser to traverse and update nodes - "我们可以将它与 babel 解析器一起使用来遍历和更新节点"
4. AST 转换为 code**
将 AST 语法树转换为浏览器可执行代码,前面讲到过,执行这一步需要两个依赖包:@babel/core 和 @babel/preset-env。
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const options = require('./webpack.config')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {};
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename);
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value);
dependecies[node.source.value] = filepath;
},
});
return dependecies;
},
getCode:
ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
// 构建启动
run() {
const { getAst, getDependecies } = Parser
const ast = getAst(this.entry)
const dependecies = getDependecies(ast, this.entry)
const code = getCode(ast)
}
// 重写 require函数,输出bundle
generate() {}
}
new Compiler(options).run()
我们看看官网文档对@babel/core 的transformFromAst的介绍:
它的作用就是将我们传入的AST转化成我们在第三个参数里配置的模块类型。
你可能会有疑问,上一步的收集依赖在这里怎么没啥关系啊,确实如此。收集依赖是为了下面进行的递归操作。
5. 递归解析所有依赖项,生成依赖关系图
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const options = require('./webpack.config')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {};
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename);
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value);
dependecies[node.source.value] = filepath;
},
});
return dependecies;
},
getCode:
ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
run() {
// 解析入口文件
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 判断有依赖对象,递归解析所有依赖项
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 生成依赖关系图
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
// 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
)
},
build(filename) {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(filename)
const dependecies = getDependecies(ast, filename)
const code = getCode(ast)
return {
// 文件路径,可以作为每个模块的唯一标识符
filename,
// 依赖对象,保存着依赖模块路径
dependecies,
// 文件内容
code
}
},
// 重写 require函数,输出bundle
generate() {}
}
new Compiler(options).run()
6. 重写 require 函数,输出 bundle
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const options = require('./webpack.config')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')
const Parser = {
getAst: path => {
// 读取入口文件
const content = fs.readFileSync(path, 'utf-8')
// 将文件内容转为AST抽象语法树
return parser.parse(content, {
sourceType: 'module'
})
},
getDependecies: (ast, filename) => {
const dependecies = {};
// 遍历所有的 import 模块,存入dependecies
traverse(ast, {
// 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
ImportDeclaration({ node }) {
const dirname = path.dirname(filename);
// 保存依赖模块路径,之后生成依赖关系图需要用到
const filepath = './' + path.join(dirname, node.source.value);
dependecies[node.source.value] = filepath;
},
});
return dependecies;
},
getCode:
ast => {
// AST转换为code
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return code
}
}
class Compiler {
constructor(options) {
// webpack 配置
const { entry, output } = options
// 入口
this.entry = entry
// 出口
this.output = output
// 模块
this.modules = []
}
run() {
// 解析入口文件
const info = this.build(this.entry)
this.modules.push(info)
this.modules.forEach(({ dependecies }) => {
// 判断有依赖对象,递归解析所有依赖项
if (dependecies) {
for (const dependency in dependecies) {
this.modules.push(this.build(dependecies[dependency]))
}
}
})
// 生成依赖关系图
const dependencyGraph = this.modules.reduce(
(graph, item) => ({
...graph,
// 使用文件路径作为每个模块的唯一标识符,保存对应模块的依赖对象和文件内容
[item.filename]: {
dependecies: item.dependecies,
code: item.code
}
}),
{}
)
},
build(filename) {
const { getAst, getDependecies, getCode } = Parser
const ast = getAst(filename)
const dependecies = getDependecies(ast, filename)
const code = getCode(ast)
return {
// 文件路径,可以作为每个模块的唯一标识符
filename,
// 依赖对象,保存着依赖模块路径
dependecies,
// 文件内容
code
}
},
// 重写 require函数 (浏览器不能识别commonjs语法),输出bundle
generate(code) {
// 输出文件路径
const filePath = path.join(this.output.path, this.output.filename)
// 懵逼了吗? 没事,下一节我们捋一捋
const bundle = `(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports;
}
require('${this.entry}')
})(${JSON.stringify(code)})`
// 把文件内容写入到文件系统
fs.writeFileSync(filePath, bundle, 'utf-8')
}
}
new Compiler(options).run()
理解bundle 实现
我们创建了add.js文件和reduce.js文件,然后 在index.js中引入,再将index.js文件引入index.html。
add.js:
export const add = (a,b)=>{
return a+b;
}
reduce.js:
export const minus = (a,b)=>{
return a-b
}
index.js:
import {add} from "./add.js";
import {minus} from "./reduce.js";
const sum = add(1,2);
const division = minus(2,1);
console.log(sum);
console.log(division);
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>
我们在node中运行bundle.js,得到index.js和add.jscode内容:
// index.js
"use strict"
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum); console.log(division);
// add.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true});
exports["default"] = void 0;
var _default = function _default(a, b) { return a + b;};
exports["default"] = _default;
但是我们现在是不能执行index.js这段代码的,因为浏览器不会识别执行require和exports。
不能识别是为什么?不就是因为没有定义这require函数,和exports对象。那我们可以自己定义。
我们创建一个函数:
const bundle = (file) =>{
}
我们看下返回的这段代码
(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports;
}
require('${this.entry}')
})(${JSON.stringify(code)})
}
其实就是:
- 把字符串code传入一个立即执行函数。
- 将主模块路径传入require函数执行
- 执行reuire函数的时候,又立即执行一个立即执行函数,这里是把code的值传进去了
- 执行eval(code)。也就是执行主模块的code这段代码
我们再来看下code的值:
// index.js
"use strict"
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum); console.log(division);
执行这段代码的时候,又会用到require函数。此时require的参数为add.js的路径,不是绝对路径,需要转化成绝对路径。因此写一个函数localRequire来转化。怎么实现呢?我们来看下代码:
(function(graph){
function require(module){
function localRequire(relativePath){
return require(graph[module].dependecies[relativePath])
}
var exports = {};
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
return exports;
}
require('${this.entry}')
})(${JSON.stringify(code)})
}
实际上是实现了一层拦截。
- 执行require('./src/index.js')函数,实际执行了:
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
- 执行eval,也就是执行了index.js的代码。
- 执行过程会执行到require函数。
- 这时会调用这个require,也就是我们传入的localRequire.
(function(require,exports,code){
eval(code)
})(localRequire,exports,graph[module].code);
- 而执行localRequire就执行了return require(graph[module].dependecies[relativePath])这段代码,也就是执行了外面这个require:
在这里return require(graph[module].dependecies[relativePath]),我们已经对路径转化成绝对路径了。因此执行外面的require的时候就是传入绝对路径。
- 而执行require("./src/add.js")之后,又会执行eval,也就是执行add.js文件的代码。
这里其实是个递归。
这样就将代码整合起来了,但是有个问题,就是在执行add.js的code时候,会遇到exports这个还没定义的问题。如下所示
// add.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true});
exports["default"] = void 0;
var _default = function _default(a, b) { return a + b;};
exports["default"] = _default;
我们发现 这里它把exports当作一个对象来使用了,但是这个对象还没定义,因此我们可以自己定义一个exports对象。
我们增添了一个空对象 exports,执行add.js代码的时候,会往这个空对象上增加一些属性,
// add.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true});
exports["default"] = void 0;
var _default = function _default(a, b) { return a + b;};
exports["default"] = _default;
比如,执行完这段代码后:
exports = {
__esModule:{ value: true},
default:function _default(a, b) { return a + b;}
}
然后我们把exports对象return出去。
可见,return出去的值,被interopRequireDefault接收, interopRequireDefault再返回default这个属性给add,因此add = function _default(a, b) { return a + b;}
现在明白了,为什么ES6模块引入的是一个对象引用了吧,因为exports就是一个对象。
就是将我们早期收集的所有依赖作为参数传入到立即执行函数当中,然后通过eval来递归地执行每个依赖的code。
至此,处理require和exports两个关键词的功能就完整了。
我们的手写webpack核心原理就到此结束了。
最后我贴一下我跑的demo的结果。bundle.js的文件内容为:
(function(graph) {
// 重写require函数
function require(moduleId) {
// 找到对应moduleId的依赖对象,调用require函数,eval执行,拿到exports对象
function localRequire(relativePath) {
return require(graph[moduleId].dependecies[relativePath])
}
var exports = {};
(function(require, exports, code) {
eval(code);
})(localRequire, exports, graph[moduleId].code);
//暴露exports对象,即暴露依赖对象对应的实现
return exports;
}
require('./src/index.js')
})({
'./src/index.js': {
dependecies: { './add.js': './reduce.js' },
code: '"use strict"
var _add = _interopRequireDefault(require("./add.js"));
var _minus = require("./minus.js");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
var sum = (0, _add["default"])(1, 2);
var division = (0, _minus.minus)(2, 1);
console.log(sum); console.log(division);
},
...
})
})
转载自:https://juejin.cn/post/7374651496618868770