显示器|前端实现多文件编译器

显示器|前端实现多文件编译器

文章图片

显示器|前端实现多文件编译器


一 概要 在前端工程中 , 有时我们需要在浏览器编译并执行一些代码 , 这种需求常见于低代码场景中 。 例如我们在搭建时需自定义一部分代码 , 这些代码需要在渲染时执行 。 为了方便起见 , 我们写的代码一定是 ES6 语法 , 如果要在浏览器执行 , 那么就必须经过编译 。 下面是前端编译 JS 代码的一些实践 。
二 需求描述








【显示器|前端实现多文件编译器】






































低码搭建时需要自定义一部分代码 希望代码是以多文件形式组织的 可以使用 ESModule 形式导入/导出 三 需求分析 1、在浏览器编译代码必然需要使用 babel 完成; 2、如果只有一个 JS 文件 , 那么可以直接使用 babel 的 transform 函数编译; 3、如果存在多文件 , 则文件内的变量必须相互隔离 , 且文件之间能够通过某种形式相互引用 , 并且需要考虑文件之间的依赖关系; 四 核心设计 流程 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 { hellofrom './a'hello(); 编译后 (function() { __filename = 'b.js'; var $$a = module['a.js'
; $$a.hello(); var mod = {; module[__filename
= mod;)() 六 依赖树解析 假设有一堆文件 , 我们通过解析(babel 或正则)后得到他们之间的关系如下: 他们之间存在循环依赖 根据这个依赖图可以梳理出几条依赖路线: A -B -D -C -F -循环依赖BA -B -E -F -循环依赖 BA -C -F -B -E -循环依赖 FA -C -G 从开始出现的第一个循环依赖截断依赖路线 , 分别统计统计每个节点的深度 , 按深度依次放入队列中 。如果两个节点深度相同 , 则分析两个节点的依赖关系 , 被依赖的先进队列 , 故最终形成的队列如下: F E B C D G A 为什么要得到一个编译顺序呢? 以上得出的编译顺序是为了尽可能解决如下的引用情况 , 但也不能解决所有: // a.jsexport const a = 2// b.jsimport { afrom 'a.js';console.log(a + 2); 这时候 , 假设执行 b 的时候 , a 还没被执行 , 那么 b 内部拿到的 a 实际上是 undefined , 显然不是我们所希望的 。 所以此时必须保证 a 先于 b 执行 。但这种使用方式在存在循环引用时无法解决 , 只能调整文件组织形式 。事实上 , 假设存在循环依赖时 , 下面的在函数内或在类内引用方式是没有问题的 , 有问题的只是直接使用: // a.jsexport const a = 2// b.jsimport { afrom 'a.js';export function test () { return a + 1; 这样 , 即使 b 有依赖 a , test 只要不是立即执行函数也不会产生影响 。七 编译 1 ESModule 转换 此过程可以通过自定义一个 Babel 插件完成 , 在语法编译时将文件编译成一个闭包 , 同时处理好 ESModule 语法 。该 Babel 插件很简单 , 在此就不展开去写了 。2 文件队列编译 对单个文件的编译可封装成一个方法 , 假设函数名为:compileFile 按照上面解析到的文件队列按照顺序逐个调用 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 执行 最后一步 , 执行上面得到的编译结果即可 , 此步骤可直接使用 new Function 的方式完成 , 例如: (假设以上的字符串内容保存在 compiledScript 中) const exec = new Functioon(` var module = {; ${compiledScript; return module;`);const module = exec();module['a.js'