明天来实现一个简略的打包工具
文件依赖
src
├─ a.js
├─ b.js
├─ c.js
└─ index.js
文件内容如下
// src/index.js
import {aInit} from './a.js'
aInit()
// src/a.js
import b from './b.js'
import c from './c.js'
export const aInit = () => {console.log('a init')
}
// src/b.js
console.log('b import')
// src/c.js
console.log('c import')
思路
外围原理是三大阶段 解析 - 转换 - 生成
解析
解析的是文件的依赖关系,将其全副收集起来,用于之后的转换
转换
- 将 es module 导入语法转换为 commonjs 导入语法
- 利用 iife 实现模块化
生成
将转换完的代码输入到 bundle.js 文件
总结
有了宏观上的三大流程,咱们就能够思考怎么去实现这个打包工具了:
- 读取文件内容
- 剖析文件内容,找出它的子依赖
- 保留以后文件的 code 和 子依赖列表,并且如果有子依赖则回到第一步挨个解决子依赖,全副处理完毕后生成
依赖关系图
- 将 es module 转换为 common js, 以兼容浏览器
- 生成 iife 代码,以实现模块化
- 输入为 bundle.js
开始入手
首先咱们在根目录创立一个 mini-webpack.js
文件,用来编写咱们的 mini-webpack
1. 读取文件内容
利用 fs 模块,并将入口文件地址传入,并将文件门路和读取到的内容导出
const fs = require('fs');
function createAsset(filePath) {const originCode = fs.readFileSync(filePath, 'utf8')
return {
filePath,
code: originCode,
}
}
createAsset('./src/index.js')
2. 剖析文件内容,找出它的子依赖
如何能力晓得一个文件的依赖呢,大体上有两个计划
- 应用正则去匹配
import
from
等关键字,毛病就是比拟不灵便 - 应用 ast
咱们这里当然应用 ast,说到 ast 那 babel 就是好货色了,对 babel 不相熟的同学能够看之前写的这篇文章: 一文看懂 webpack 的所有 source map!🤔
利用 @babel/parser
生成 ast,而后利用 @babel/traverse
对 ast 进行解决, 将剖析到的依赖退出 deps 数组中,并导出
const parser = require('@babel/parser')
const traverse = require('@babel/traverse')
function createAsset(filePath) {const originCode = fs.readFileSync(filePath, 'utf8')
const ast = parser.parse(originCode, {
// 须要申明是 es module
sourceType: 'module'
});
// 收集到外部 import 依赖
const deps = []
traverse.default(ast, {
// 找到 import
ImportDeclaration(path) {
const node = path.node
// 收集到依赖数组
deps.push(node.source.value)
}
})
return {
filePath,
code,
deps
}
}
3. 保留以后文件的 code 和 子依赖列表,并且如果有子依赖则回到第一步挨个解决子依赖
利用广度优先遍历,以入口为终点,将所有资源的依赖收集到 graph 中
function createGraph (entryFile) {const entryAsset = createAsset(entryFile)
// 首先是将入口文件退出
const graph = [entryAsset]
// 遍历图中所有资源
for (const asset of graph) {
// 如果以后资源存在子依赖
asset.deps.forEach(relativePath => {const childAsset = createAsset(path.join(__dirname, 'src', relativePath))
// 将子依赖资源退出到 graph 数组,后续 for 循环天然也会将其解决
graph.push(childAsset)
})
}
return graph
}
// 调用
const graph = createGraph('./src/index.js')
咱们看一下此时 graph 的输入后果
咱们拿到了每个文件的文件门路,文件内容,还有子依赖
4. 将 es module 转换为 common js, 以兼容浏览器
这个转换,babel 同样能够做到,此时咱们批改一下 createAsset
办法,利用 @babel/core
的transformFromAst
,在将 ast 转回 code 的阶段应用@babel/preset-env
实现转换
const {transformFromAst} =require("@babel/core")
function createAsset(filePath) {const originCode = fs.readFileSync(filePath, 'utf8')
const ast = parser.parse(originCode, {sourceType: 'module'});
// 收集到外部 import 依赖
const deps = []
traverse.default(ast, {ImportDeclaration(path) {
const node = path.node
// 检测到的依赖都增加到数组里
deps.push(node.source.value)
}
})
// 将 es module 转换为 common js, 以兼容浏览器
const {code} = transformFromAst(ast, originCode, {"presets": ["@babel/preset-env"]
})
return {
filePath,
code,
deps
}
}
再来看一下 graph 的输入后果
能够看到外面 code , 曾经不再是上一阶段的 import , 而是应用 require 进行引入
5. 生成 iife 代码,以实现模块化
尽管咱们又了文件的依赖关系,也将 es module 转换为 common js,但目前还是无奈在浏览器中应用,为此咱们要实现 iife 模块化,咱们来看看如何编写
首先,咱们的目标是把下图的这些文件,都打入到最终的 bundle.js
所以咱们先手动把所有文件内容都放入 bundle.js 看看
// index.js
import {aInit} from './a.js'
aInit()
// a.js
import b from './b.js'
import c from './c.js'
export const aInit = () => {console.log('a init')
}
// b.js
console.log('b import')
// c.js
console.log('c import')
显然,这样间接放在一起是无奈运行的
首先须要将 esmodule 手动改为 common js
// index.js
const {aInit} = require('./a.js')
aInit()
// a.js
const b = require('./b.js')
const c = require('./c.js')
module.exports.aInit = () => {console.log('a init')
}
// b.js
console.log('b import')
// c.js
console.log('c import')
改为 require 后同样是无奈运行的,因为浏览器里基本没有 require 办法,所以咱们须要给他一个
// index.js
function indexJs(require, module, exports) {const { aInit} = require('./a.js')
aInit()}
// a.js
function aJs(require, module, exports) {const b = require('./b.js')
const c = require('./c.js')
module.exports.aInit = () => {console.log('a init')
}
}
// b.js
function bJs(require, module, exports) {console.log('b import')
}
// c.js
function cJs(require, module, exports) {console.log('c import')
}
把每个文件都看成是一个 function , 它会接管 require, module, exports 三个参数, 当初 require 有中央取了,然而咱们还没做具体实现,所以咱们须要持续革新。
(function (moduleMap) {function require(filePath) {const fn = moduleMap[filePath]
const module = {exports: {}
}
fn(require, module, module.exports)
return module.exports
}
require('./index.js')
}({'./index.js': function (require, module, exports) {const { aInit} = require('./a.js')
aInit()},
'./a.js': function (require, module, exports) {const b = require('./b.js')
const c = require('./c.js')
module.exports.aInit = () => {console.log('a init')
}
},
'./b.js': function (require, module, exports) {console.log('b import')
},
'./c.js': function (require, module, exports) {console.log('c import')
}
}))
应用 iife 进行包裹,将文件映射作为参数传入,并在函数内结构 require 办法,其外部实质是从文件映射中获取相应的 function, 结构一个 module 对象,调用 function 并传入
并且最初调用一下入口文件
当初咱们在浏览器里加载一下这个代码,看看是否运行
ok , 没有问题
6. 输入为 bundle.js
如何动静结构 iife 呢,比拟不便的办法就是应用模版引擎
咱们编写一个 build 办法, 应用 bundle.ejs 模版进行渲染
function build(graph) {const template = fs.readFileSync('./template/bundle.ejs', { encoding: 'utf-8'})
const ejsData = graph.map(asset => ({
filePath: asset.filePath,
code: asset.code
}))
console.log(ejsData)
const code = ejs.render(template, { ejsData})
fs.writeFileSync('./dist/bundle.js', code)
}
const graph = createGraph('./src/index.js')
build(graph)
bundle.ejs 模版内容如下【ejs 模版引擎具体语法可取官网文档查看】
// bundle.ejs
(function (moduleMap) {function require(filePath) {const fn = moduleMap[filePath]
const module = {exports: {}
}
fn(require, module, module.exports)
return module.exports
}
require('./index.js')
}({
<% ejsData.forEach(item => { %>
"<%- item["filePath"] %>": function (require, module, exports) {<%- item["code"] %>
},
<% }) %>
}))
ejsData 是咱们须要注入的变量,就是咱们的 资源关系依赖图,对其进行 forEach , 循环出所有资源
此时最终文件如下:
(function (moduleMap) {function require(filePath) {const fn = moduleMap[filePath];
const module = {exports: {},
};
fn(require, module, module.exports);
return module.exports;
}
require("./index.js");
})({"./src/index.js": function (require, module, exports) {
"use strict";
var _a = require("./a.js");
(0, _a.aInit)();},
"/Users/nxl3477/Documents/study_space/mini-series/mini-webpack/my-mini-webpack/src/a.js":
function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {value: true,});
exports.aInit = void 0;
var _b = _interopRequireDefault(require("./b.js"));
var _c = _interopRequireDefault(require("./c.js"));
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj};
}
var aInit = function aInit() {console.log("a init");
};
exports.aInit = aInit;
},
"/Users/nxl3477/Documents/study_space/mini-series/mini-webpack/my-mini-webpack/src/b.js":
function (require, module, exports) {
"use strict";
console.log("b import");
},
"/Users/nxl3477/Documents/study_space/mini-series/mini-webpack/my-mini-webpack/src/c.js":
function (require, module, exports) {
"use strict";
console.log("c import");
},
});
咱们发现产出的文件有一些问题,
咱们理论 require 的是文件的相对路径,而传入的 moduleMap
很多资源是绝对路径,所以,比方 require(‘./a.js’) 是找不到的,所以咱们得再想方法革新一下
iife 代码革新为如下,咱们赋予所有的 资源模块一个 id , 并携带一个用于 相对路径映射理论 id 的对象
(function (moduleMapping) {function require(moduleId) {
// 取出函数和 映射对象
const [fn, mapping] = moduleMapping[moduleId]
// 对 require 包装一层,先通过本身的 mapping 取出理论的 id,而后再调用 require 取出理论的资源
function localRequre(filePath) {const _moduleId = mapping[filePath]
return require(_moduleId)
}
const module = {exports: {}
}
fn(localRequre, module, module.exports)
return module.exports
}
require(0)
}({
// 改为传入数组,第一个元素为函数,第二个是用来映射相对路径的对象
// ./index.js
0: [function (require, module, exports) {const { aInit} = require('./a.js')
aInit()}, {'./a.js': 1}],
// ./a.js
1: [function (require, module, exports) {const b = require('./b.js')
const c = require('./c.js')
module.exports.aInit = () => {console.log('a init')
}
}, {'./b.js': 2, './c.js': 3}],
// './b.js'
2: [function (require, module, exports) {console.log('b import')
}, {}],
// './c.js'
3: [function (require, module, exports) {console.log('c import')
}, {}]
}))
依据下面这个代码,咱们更新一下 ejs 模版
// bundle.ejs
(function (moduleMapping) {function require(moduleId) {const [ fn, mapping] = moduleMapping[moduleId]
function localRequre(filePath) {const _moduleId = mapping[filePath]
return require(_moduleId)
}
const module = {exports: {}
}
fn(localRequre, module, module.exports)
return module.exports
}
// 入口 Id 为 0
require(0)
}({
<% ejsData.forEach(item => { %>
"<%- item["id"] %>": [function (require, module, exports) {<%- item["code"] %>
}, <%- JSON.stringify(item.mapping) %> ],
<% }) %>
}))
mini-webpack.js
也要进行调整:
- 为每个资源模块生成一个 id
- 为每个资源模块结构 mapping 用来映射子依赖的模块 id
let uid = 0
function createAsset(filePath) {const originCode = fs.readFileSync(filePath, 'utf8')
const ast = parser.parse(originCode, {sourceType: 'module'});
// 收集到外部 import 依赖
const deps = []
traverse.default(ast, {ImportDeclaration(path) {
const node = path.node
// 检测到的依赖都增加到数组里
deps.push(node.source.value)
}
})
const {code} = transformFromAst(ast, originCode, {"presets": ["@babel/preset-env"]
})
return {
filePath,
code,
// 新增 mappping
mapping: {},
deps,
// 新增 id
id: uid++
}
}
// 欠缺 mapping 数据
function createGraph (entryFile) {const entryAsset = createAsset(entryFile)
const graph = [entryAsset]
// 遍历图中所有资源
for (const asset of graph) {
// 编译每个资源的依赖
asset.deps.forEach(relativePath => {const child = createAsset(path.join(__dirname, 'src', relativePath))
// 将子依赖的 id 写入父资源模块的
asset.mapping[relativePath] = child.id
graph.push(child)
})
}
return graph
}
// 欠缺 ejsData 的数据
function build(graph) {const template = fs.readFileSync('./template/bundle.ejs', { encoding: 'utf-8'})
const ejsData = graph.map(asset => ({
filePath: asset.filePath,
code: asset.code,
id: asset.id,
mapping: asset.mapping
}))
console.log(ejsData)
const code = ejs.render(template, { ejsData})
fs.writeFileSync('./dist/bundle.js', code)
}
好此时,咱们再来看看生成的代码
(function (moduleMapping) {function require(moduleId) {const [fn, mapping] = moduleMapping[moduleId];
function localRequre(filePath) {const _moduleId = mapping[filePath];
return require(_moduleId);
}
const module = {exports: {},
};
fn(localRequre, module, module.exports);
return module.exports;
}
require(0);
})({
0: [function (require, module, exports) {
"use strict";
var _a = require("./a.js");
(0, _a.aInit)();},
{"./a.js": 1},
],
1: [function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {value: true,});
exports.aInit = void 0;
var _b = _interopRequireDefault(require("./b.js"));
var _c = _interopRequireDefault(require("./c.js"));
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj};
}
var aInit = function aInit() {console.log("a init");
};
exports.aInit = aInit;
},
{"./b.js": 2, "./c.js": 3},
],
2: [function (require, module, exports) {
"use strict";
console.log("b import");
},
{},],
3: [function (require, module, exports) {
"use strict";
console.log("c import");
},
{},],
});
而后去浏览器执行一下
ok,完满,一个简略的打包工具就实现了
残缺代码已上传至 github: mini-webpack
参考资料:
手摸手带你实现打包器 仅需 80 行代码了解 webpack 的外围