背景
简单实现下模块打包器打包流程,大概了解webpack等工具打包核心流程
任务
首先,我们应当了解模块打包器做了什么?
简单来说,模块打包器打包过程就是读取需要打包的文件内容,构造一种依赖关系并将它们根据一定规则合并生成一个文件。
根据上述过程,我们可以得出一些任务:
- 文件读取:文件内容读取
- 获取依赖:根据文件内容分析文件的依赖
- 构造关系:根据上述的文件依赖构造一种关系
- 生成文件:生成文件,将上述关系进行一定规则写入到生成的文件
下面我们将一个简单目录结构的项目进行打包实现一下。
项目
目录
- mini-webpack
--src
-index.js
-foo.js
--build.js
--bundle.ejs
--package.json
src文件
index.js
import { foo } from './foo.js';
console.log('this is main process');
foo();
foo.js
export function foo() {
console.log('this is foo content');
}
流程图
实现
文件读取
这里我们借助node中的fs模块、path模快即可 。
// build.js
import { readFileSync, writeFileSync } from 'fs';
import path from 'path';
function getFileContent(filePath) {
// 打包目录 src
return readFileSync(path.resolve('./src', filePath), { encoding: 'utf-8'})
}
获取依赖
在上述文件读取后,我们可获得文件的内容,但如何通过内容获取到文件的依赖呢?
这里我们通过ast来处理,借助 @babel/parser、 @babel-traverse 可实现这个功能,实现如下:
// build.js
import { readFileSync, writeFileSync } from 'fs';
import path from 'path';
import parser from '@babel/parser';
import traverse from '@babel/traverse';
function getFileContent(filePath) {
// 打包目录 src
return readFileSync(path.resolve('./src', filePath), { encoding: 'utf-8'})
}
// 获取文件内容及依赖
function createAssets(entry) {
let assets = {}; // 这个对象记录当前文件的一些信息,以供后面使用
// 1.获取内容
let data = getFileContent(entry);
assets.filePath = entry;
assets.deps = [];
// 2. 获取依赖
// 2.1 转成ast
let ast = parser.parse(data, {
sourceType: "module",
});
assets.source = data; // 存储文件信息
// 2.2 ast生成树遍历
traverse.default(ast, {
// ImportDeclaration 可获取到 import 进来的文件路径
ImportDeclaration({node}) {
const { source } = node;
assets.deps.push(source.value); // 搜集依赖
}
});
return assets;
}
备注:
- astexplorer.net/:用于查看ast树生成内容节点
- babel.docschina.org/docs/en/bab…:parser工具包一些用法介绍
- www.npmjs.com/package/bab…:traverse工具包使用方法
构造关系
通过上述依赖关系,我们可以构建一种关系,即文件之间的关系。这里我们用数组来存储文件之间的关系。
// build.js
....
let entry = './index.js';
// 将文件依赖构造成图,通过递归方式
function creatGraph() {
let entryAssets = createAssets(entry);
// 用 quene 搜集依赖
let quene = [entryAssets];
for(const assets of quene) {
//遍历 deps
assets.deps.forEach((filePath) => {
let asset = createAssets(filePath);
quene.push(asset);
});
}
return quene;
}
文件生成
由于ESM模块浏览器运行会报错,我们需要考虑怎么样才能在浏览器下执行。
这里我们可以用一个IIFE(立即执行函数),并且模仿CommonJS中的导入导出来实现。参考如下:
// 例子
(function (map) {
function require(id) {
let asset = map[id];
let [fn, mapping] = asset;
function localRequire(path) {
return require(mapping[path]);
}
let module = {
exports: {}
};
fn(localRequire, module, module.exports);
return module.exports;
}
require('1');
})({
// 这里1,2为文件模块id,如果为路径的话存在不同文件下的同名文件会有问题
1: [function (require, module, exports) {
let { foo } = require("./foo.js");
console.log('this is main process');
foo();
// 加多一个map,用于映射路径与模块之间的关系
}, { "./foo.js": 2 }],
2: [function (require, module, exports) {
function foo() {
console.log('this is foo content');
}
module.exports = {
foo
}
}, {}],
})
由于 @babel/core支持将esm语法转换为commonJS语法 , 故上述想法可以实现。
这里我们将上面例子作为模板,模块内容作为数据,借用ejs.js来生成文件内容,模板代码如下:
//bundle.ejs
(function(map) {
function require(id) {
let asset = map[id];
let [fn, mapping] = asset;
function localRequire(path) {
return require(mapping[path]);
}
let module = {
exports: {}
};
fn(localRequire, module, module.exports);
return module.exports;
}
require('1');
})({
<% data.forEach(function(asset){ %>
<%-asset.id%>: [function(require, module, exports) {
<%-asset.source%>
}, <%-JSON.stringify(asset.mapping)%>],
<%})%>
})
build.js整体代码如下:
import { readFileSync, writeFileSync } from 'fs';
import * as parser from '@babel/parser';
import path from 'path';
import traverse from '@babel/traverse';
import ejs from 'ejs';
import { transformFromAst } from '@babel/core';
let entry = './index.js';
let id = 1;
function getFileContent(filePath) {
return readFileSync(path.resolve('./src', filePath), { encoding: 'utf-8'})
}
// 获取文件内容及依赖
function createAssets(entry) {
let assets = {};
// 1.获取内容
let data = getFileContent(entry);
assets.filePath = entry;
// 2. 获取依赖
// 2.1 转成ast
assets.deps = [];
let ast = parser.parse(data, {
sourceType: "module",
});
let { code } = transformFromAst(ast, null, {
presets: ['env']
});
assets.source = code;
assets.id = id;
assets.mapping = {};
// 2.2 ast生成树遍历
traverse.default(ast, {
ImportDeclaration({node}) {
const { source } = node;
assets.deps.push(source.value);
assets.mapping[source.value] = ++id;
}
});
return assets;
}
// 文件依赖转为图
function creatGraph() {
let entryAssets = createAssets(entry);
// 用 quene 搜集依赖
let quene = [entryAssets];
for(const assets of quene) {
//遍历 deps
assets.deps.forEach((filePath) => {
let asset = createAssets(filePath);
quene.push(asset);
});
}
return quene;
}
let quene = creatGraph();
/**
* 根据图关系生成浏览器可执行文件
* 1. 读取模板
* 2. 模板数据进行替换
* 2.1 由于文件是用的esm模式,需要改为common.js,此时需要用到 babel-preset, babel-preset-env
* 3. 生成文件
*/
// 获取模板
let template = readFileSync('./bundle.ejs', { encoding: 'utf-8'});
// 模板插入数据
let code = ejs.render(template, {
data: quene
});
// 生成文件
writeFileSync('./dist/bundle.js', code, { encoding: 'utf-8'});
备注:
- esm模块转为commonJS规范:借用@babel/core可实现
- ejs官网:ejs.bootcss.com/#docs
总结
通过打包器实现,可以了解到基本打包器流程,即读取文件内容->获得依赖 ->进行关系构造 -> 生成文件。
了解到通过babel相关包处理一些事情如文件内容转成ast,通过ast获取导入相关信息,ast将esm转为commonJS规范,以及模板ejs的使用。