乐趣区

关于前端:前端实现多文件编译器

简介:在前端工程中,有时咱们须要在浏览器编译并执行一些代码,这种需要常见于低代码场景中。例如咱们在搭建时需自定义一部分代码,这些代码须要在渲染时执行。为了不便起见,咱们写的代码肯定是 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 标准,临时也没有思考循环依赖和执行程序。

后续会严格依照以上步骤进行优化。

原文链接
本文为阿里云原创内容,未经容许不得转载。

退出移动版