简介:在前端工程中,有时咱们须要在浏览器编译并执行一些代码,这种需要常见于低代码场景中。例如咱们在搭建时需自定义一部分代码,这些代码须要在渲染时执行。为了不便起见,咱们写的代码肯定是 ES6 语法,如果要在浏览器执行,那么就必须通过编译。上面是前端编译 JS 代码的一些实际。
作者 | 景遇
起源 | 阿里技术公众号
一 概要
在前端工程中,有时咱们须要在浏览器编译并执行一些代码,这种需要常见于低代码场景中。例如咱们在搭建时需自定义一部分代码,这些代码须要在渲染时执行。为了不便起见,咱们写的代码肯定是 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.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;
})()
六 依赖树解析
假如有一堆文件,咱们通过解析 (babel 或正则) 后失去他们之间的关系如下:
他们之间存在循环依赖
依据这个依赖图能够梳理出几条依赖路线:
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);
这时候,假如执行 b 的时候,a 还没被执行,那么 b 外部拿到的 a 实际上是 undefined,显然不是咱们所心愿的。所以此时必须保障 a 先于 b 执行。
但这种应用形式在存在循环援用时无奈解决,只能调整文件组织模式。
事实上,假如存在循环依赖时,上面的在函数内或在类内援用形式是没有问题的,有问题的只是间接应用:
// a.js
export const a = 2
// b.js
import {a} from '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'] // a.js 的导出内容
module['b.js'] // b.js 的导出内容
八 总结
至此,一个前端可执行的小型打包工具就已实现,能够间接在前端进行多文件的编辑和执行。
实时上,此过程仅实用于不不便借助服务器的场景,如果有条件容许能够借助服务器,那么编译过程最好在服务端实现,甚至还能够借助 webpack 或 rollup 等打包工具实现更好的编译成果。
参考
目前咱们在 ali-lowcode-engine 之上的源码插件(@ali/lowcode-plugin-code-editor)外部实现了多文件的反对,目前仅做了最简略的实现:模块援用间接采纳了 UMD 标准,临时也没有思考循环依赖和执行程序。
后续会严格依照以上步骤进行优化。
原文链接
本文为阿里云原创内容,未经容许不得转载。