前端实现多文件编译器
一 概要
二 需求描述
-
低码搭建时需要自定义一部分代码 -
希望代码是以多文件形式组织的 -
可以使用 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.jsexport const a = 1;
(function() {__filename = 'a.js';const a = 1;var mod = {};mod.a = a;module[__filename] = mod;})()
2 导入
// b.jsimport { 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.jsexport const a = 2// b.jsimport { a } from 'a.js';console.log(a + 2);
但这种使用方式在存在循环引用时无法解决,只能调整文件组织形式。
事实上,假设存在循环依赖时,下面的在函数内或在类内引用方式是没有问题的,有问题的只是直接使用:
// a.jsexport const a = 2// b.jsimport { 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 等打包工具实现更好的编译效果。
参考
后续会严格按照以上步骤进行优化。
数据分析系统之数据管理与数据仓库
点击阅读原文查看详情
