前端实现多文件编译器
一 概要
二 需求描述
-
低码搭建时需要自定义一部分代码 -
希望代码是以多文件形式组织的 -
可以使用 ESModule 形式导入/导出
三 需求分析
四 核心设计
1 变量隔离
假设有 a.js,内容如下:
const a = 1;
const b = 2;
function sum () {
return a + b'
}
sum();
(function() {
const a = 1;
const b = 2;
function sum () {
return a + b'
}
sum();
})();
五 文件引用
-
所有文件的引用都将通过全局变量 module 进行;
-
每个文件都将对应到 module 上的一个对象,key 根据文件名而定。
1 导出
// a.js
export const a = 1;
(function() {
__filename = 'a.js';
const a = 1;
var mod = {};
mod.a = a;
module[__filename] = mod;
})()
2 导入
// b.js
import { hello } from './a'
hello();
(function() {
__filename = 'b.js';
var $$a = module['a.js'];
$$a.hello();
var mod = {};
module[__filename] = mod;
})()
六 依赖树解析
他们之间存在循环依赖
根据这个依赖图可以梳理出几条依赖路线:
A -> B -> D -> C -> F -> 循环依赖B
A -> B -> E -> F -> 循环依赖 B
A -> C -> F -> B -> E -> 循环依赖 F
A -> C -> G
从开始出现的第一个循环依赖截断依赖路线,分别统计统计每个节点的深度,按深度依次放入队列中。
F E B C D G A
为什么要得到一个编译顺序呢?
以上得出的编译顺序是为了尽可能解决如下的引用情况,但也不能解决所有:
// a.js
export const a = 2
// b.js
import { a } from 'a.js';
console.log(a + 2);
但这种使用方式在存在循环引用时无法解决,只能调整文件组织形式。
事实上,假设存在循环依赖时,下面的在函数内或在类内引用方式是没有问题的,有问题的只是直接使用:
// a.js
export const a = 2
// b.js
import { a } from 'a.js';
export function test () {
return a + 1;
}
七 编译
1 ESModule 转换
该 Babel 插件很简单,在此就不展开去写了。
2 文件队列编译
按照上面解析到的文件队列按照顺序逐个调用 compileFile 进行编译,并将结果直接拼接起来,形成一个巨大的字符串,该字符串的样子应该是如下的格式:
(function() {
__filename = 'b.js';
var $$a = module['a.js'];
// ...
var mod = {};
module[__filename] = mod;
})();
(function() {
__filename = 'a.js';
var $$b = module['b.js'];
// ...
var mod = {};
module[__filename] = mod;
})();
// ...
3 JS 执行
(假设以上的字符串内容保存在 compiledScript 中)
const exec = new Functioon(`
var module = {};
${compiledScript};
return module;
`);
const module = exec();
module['a.js'] // a.js 的导出内容
module['b.js'] // b.js 的导出内容
八 总结
实时上,此过程仅适用于不方便借助服务器的场景,如果有条件允许可以借助服务器,那么编译过程最好在服务端完成,甚至还可以借助 webpack 或 rollup 等打包工具实现更好的编译效果。
参考
后续会严格按照以上步骤进行优化。
数据分析系统之数据管理与数据仓库
点击阅读原文查看详情