当前位置: 首页 > 科技观察

多文件编译器的前端实现

时间:2023-03-18 00:58:57 科技观察

作者|场景一、概述在前端工程中,有时我们需要在浏览器中编译并执行一些代码。这种需求在低代码场景中很常见。比如我们在构建的时候需要自定义一部分代码,这些代码需要在渲染的时候执行。为了方便起见,我们写的代码必须是ES6语法。如果要在浏览器中执行,则必须编译。下面是前端编译JS代码的一些实践。2.需求描述在构建低代码时,需要自定义部分代码。如果希望代码以多个文件的形式组织,可以使用ESModule导入/导出3.需求分析在浏览器中编译代码必须使用babel来完成;如果只有一个js文件,那么可以直接使用babel的transform函数编译;如果有多个文件,文件中的变量必须相互隔离,文件之间可以通过某种形式相互引用,需要考虑文件之间的依赖关系;四、核心设计流程1、变量隔离由于我们的需求是多文件编辑,所以每个文件中的变量要相互隔离。最简单的方法是将每个文件的内容转换成一个闭包,然后通过固定的接口连接每个文件。假设有a.js,内容如下:consta=1;常量b=2;函数总和(){返回a+b'}总和();它可以转换成下面的形式:(function(){consta=1;constb=2;functionsum(){returna+b'}sum();})();转换成这种形式后,每个文件中的变量将只存在于各自的闭包里面,互不影响。5、文件引用文件之间的相互引用可以通过定义一个接口规则来实现:所有的文件引用都将通过全局变量模块进行;每个文件都会对应模块上的一个对象,key会根据文件名来确定。1.导出原文件:https://back-media.51cto.com/editor/h6e90be6-D8rA67LO编译:(function(){__filename='a.js';consta=1;varmod={};mod.a=a;module[__filename]=mod;})()2.导入源文件://b.jsimport{hello}from'./a'hello();编译后:(function(){__filename='b.js';var$$a=module['a.js'];$$a.hello();varmod={};module[__filename]=mod;})()六、依赖树解析假设有一堆文件,解析后我们得到它们之间的关系(babel或regular)如下:它们之间存在循环依赖。根据这张依赖图,可以梳理出几条依赖路径:A->B->D->C->F->循环依赖BA->B->E->F->循环依赖BA->C->F->B->E->循环依赖FA->C->G从最开始出现的第一个循环依赖切断依赖路由,分别统计每个节点的深度,放入队列中按深度排序。如果两个节点的深度相同,分析两个节点的依赖关系和依赖的高级队列,所以最终的队列如下:FEBCDGA为什么需要编译顺序?上面得到的编译顺序是尽可能解决下面参考的情况,但是不能解决全部://a.jsexportconsta=2//b.jsimport{a}from'a.js';控制台日志(a+2);这个时候,假设要执行b的时候,a还没有执行,那么b得到的a其实是undefined的,这显然不是我们想要的。所以这个时候一定要保证a先于b执行。但是这种使用方式在有循环引用的情况下无法解决,只能调整文件组织形式。其实,假设存在循环依赖,在函数内或者类内引用下面的方法是没有问题的。唯一的问题是直接使用://a.jsexportconsta=2//b.jsimport{a}from'a.js';exportfunctiontest(){returna+1;}这样,即使b对a有依赖,只要不立即执行函数,test就没有效果。七、编译1.ESModule转换这个过程可以通过自定义一个Babel插件,在语法编译时将文件编译成闭包,同时处理ESModule语法来完成。Babel插件非常简单,这里就不展开了。2.文件队列编译单个文件的编译可以封装成一个方法,假设函数名为:compileFile。根据上面解析出来的文件队列,依次调用compileFile进行编译,结果直接拼接在一起,形成了一个巨大的字符串。该字符串应类似于以下格式:(function(){__filename='b.js';var$$a=module['a.js'];//...varmod={};module[__filename]=mod;})();(function(){__filename='a.js';var$$b=module['b.js'];//...varmod={};module[__filename]=mod;})();//...3.JS最后执行第一步是执行上面得到的编译结果。这一步可以直接使用newFunction完成,例如:(假设上面的字符串内容存放在compiledScript中)。constexec=newFunction(`varmodule={};${compiledScript};returnmodule;`);const模块=exec();module['a.js']//导出a.js的内容module['b.js']//导出b.js的内容8.总结至此,一个可以在前端执行的小包工具end已经实现,可以在前端直接编辑执行多个文件。实时的,这个过程只适用于不方便使用服务器的场景。如果条件允许,编译过程最好在服务器端完成。你甚至可以使用webpack或rollup等打包工具来获得更好的编译效果。作为参考,我们目前在ali-lowcode-engine之上的源代码插件(@ali/lowcode-plugin-code-editor)中实现了多文件支持。目前我们只做最简单的实现:模块引用直接采用UMD规范,暂时不考虑循环依赖和执行顺序。后续优化将严格按照上述步骤进行。