介绍:在前端工程中,有时候我们需要在浏览器中编译执行一些代码。这种需求在低代码场景中很常见。比如我们在构建的时候需要自定义一部分代码,这些代码需要在渲染的时候执行。为了方便起见,我们写的代码必须是ES6语法。如果要在浏览器中执行,则必须编译。下面是前端编译JS代码的一些实践。作者|场景来源|阿里巴巴科技公众号1总结在前端工程中,有时我们需要在浏览器中编译执行一些代码,这在低代码场景中很常见。比如我们在构建的时候需要自定义一部分代码,这些代码需要在渲染的时候执行。为了方便起见,我们写的代码必须是ES6语法。如果要在浏览器中执行,则必须编译。下面是前端编译JS代码的一些实践。2.需求描述在构建low-code时,需要自定义一部分代码。如果希望代码以多个文件的形式组织,可以使用ESModule导入/导出。三、需求分析1、在浏览器中编译代码必须使用babel;2、如果只有一个JS文件,可以直接使用babel的transform函数编译;3、如果有多个文件,文件中的变量必须相互隔离,文件之间可以通过某种形式相互引用,需要考虑文件之间的依赖关系;四核设计流程1变量隔离由于我们的需求是多文件编辑,所以每个文件中的变量要相互隔离。最简单的方法是将每个文件的内容转换成一个闭包,然后通过固定的接口连接每个文件。假设有a.js,内容如下:consta=1;常量b=2;函数总和(){返回a+b'}总和();它可以转换成下面的形式:(function(){consta=1;constb=2;functionsum(){returna+b'}sum();})();转换成这种形式后,每个文件中的变量将只存在于各自的闭包里面,互不影响。五文件引用文件之间的相互引用可以通过定义一个接口规则来实现:所有的文件引用都将通过全局变量模块进行;每个文件都会对应模块上的一个对象,key会根据文件名来确定。1导出原文件:`//a.jsexportconsta=1;`编译后:(function(){__filename='a.js';consta=1;varmod={};mod.a=a;module[__filename]=mod;})()2importsourcefile//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';console.log(一+2);这时候假设执行b的时候a还没有执行,那么b内部得到的a其实是undefined的,这显然不是我们想要的。所以这个时候一定要保证a先于b执行。但是这种使用方式在有循环引用的情况下无法解决,只能调整文件组织形式。其实,假设存在循环依赖,在函数内或者类内引用下面的方法是没有问题的。唯一的问题是直接使用://a.jsexportconsta=2//b.jsimport{a}from'a.js';exportfunctiontest(){returna+1;}这样,即使b对a有依赖,只要不立即执行函数,test就没有效果。七、编译1ESModule转换这个过程可以通过自定义一个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;})();//...3JS执行最后一步,只执行上面得到的编译结果,这一步可以直接使用new函数完成,例如:(假设上面的字符串内容被保存incompiledScript)constexec=newFunction(`varmodule={};${compiledScript};returnmodule;`);const模块=exec();module['a.js']//a.js模块['b.js']的导出内容//b.js的导出内容8总结至此,一个前端可执行的打包小工具就完成了实现了,可以在前端直接编辑执行多个文件。实时的,这个过程只适用于不方便使用服务器的场景。如果条件允许,编译过程最好在服务器端完成。你甚至可以使用webpack或rollup等打包工具来获得更好的编译效果。作为参考,我们目前在ali-lowcode-engine之上的源代码插件(@ali/lowcode-plugin-code-editor)中实现了多文件支持。目前我们只做最简单的实现:模块引用直接采用UMD规范,暂时不考虑循环依赖和执行顺序。后续优化将严格按照上述步骤进行。原文链接本文为阿里云原创内容,未经许可不得转载。
