3
作者:菉竹

本身 iMove 的定位就是 “一个逻辑可复用的,面向函数的,流程可视化的 JavaScript 工具库。"

对开发者而言, iMove 恰好是可以完成这些目标的理想工具。动动鼠标,写一下节点函数,代码导出,放到具体工程里就可以直接使用,是不是很方便?

那么,为什么我们还要做 iMove 在线执行代码功能呢?站在用户视角看,对于开发者来说,选中节点,右键在线执行代码是必要的。这样可以把 iMove 的工具属性做到极致,让开发者体验更好,操作更简单。

上次在《登上 Github 趋势榜,iMove 原理技术大揭秘!》一文中我们卖了个关子,今天就和大家分享下 iMove 是如何实现在线执行节点代码的~

引子

故事还得从组内的一个小伙伴说起,当时她是第一次使用 iMove,询问我如何测试运行刚写好的代码。二话不说,我就在她电脑上一通操作:

  1. 安装 @imove/cli
  2. 在项目根目录执行命令: imove -d
  3. 找到项目中的组件,添加代码 logic.invoke('trigger')
  4. 启动项目,打开控制台面板,筛选 log  信息
  5. ... ...

“我就想运行下刚写好的代码,看看输出结果是什么...”

『步骤繁琐』、『操作麻烦』、『有上手成本』、『运行结果不直观』... 此刻,脑中第一时间闪过的就是以上这些词汇,再看向小伙伴那疑惑的眼神,仿佛下一刻她就要被劝退了...

这可怎么行,体验必须优化,使用成本必须降下来!于是就有了右键在线执行代码的功能。

image

其实上述问题的诉求很简单: 写完节点的代码之后,旁边有个运行按钮,一点直接就能看到运行结果就行了。我们来看下就是这么一个优化都能带来哪些改变:

  • 无安装工具要求,0 上手成本: 代码在浏览器端就能运行,不再需要安装命令行工具,大大降低了学习成本。
  • 结果可视化,简单明了: 开发调试过程中最常用到的就是日志,每次都要打开控制台极不方便,直接将节点的运行结果用可视化的方式展示出来,简单直观。
  • 在线 mock 输入,方便测试: 每个节点的代码都会有各种可能的输入,如果可以直接 mock 输入,测试效率将得以极大的提升。
  • 测试用例可保存,保证代码质量: 除了方便测试节点代码之外,如果可以将成对的输入/输出作为测试用例加以保存,渐而形成完备的测试用例集,将能进一步保证节点的代码质量。
  • ... ...

想法是很美好,但具体该如何实现呢?接着往下看~

探索 1:节点代码在哪运行

首先,要解决的第一个问题就是:节点代码在哪运行?

经过评估,我们认为主要有两个选择:

  1. 浏览器端直接运行节点代码;
  2. 本地起一个服务,将节点信息发送至本地,本地构建编译完之后将 bundle 发送回浏览器端执行。

对于以上两个方案, iMove 选择了前者,理由很简单:在线运行节点代码的初心就是为了降低上手成本,如果还需要本地起一个服务,使用者势必又要学习一个新命令,这无形中又提高了使用门槛。

然而,要想在浏览器中直接运行节点代码,还是有很多阻碍,并非一个 eval 就完事了~

探索 2:如何运行 import/export

看过 iMove 的出码就知道,流程图中的每个节点代码最终都会被编译成一个单独的 js 文件。因此,每个节点是支持 import 其他 npm 包的,这也是为什么不能直接调用 eval 的原因。

1)浏览器原生支持 ES Module

当然,也许聪明的你早已想到浏览器是支持 native ES module 的,包括之前挺火的 Vite 其实底层原理也是基于这个。为此,我们先来简单介绍下如何让 import/export 代码在浏览器中跑起来~

先来看 MDN 上的 官方文档

image

image

可以看到,其实现代浏览器早就支持了,因此我们可以不用考虑兼容性问题。除此之外,继续浏览文档你会发现要让浏览器支持 ES Module 很关键的一点是要在 script 标签上添加 type="module" 属性。来看一个例子:

# 文件目录
index.html
main.mjs

<!--index.html-->
<script type="module">
  import sayHello from './main.js';
  say('Hello iMove!');
</script>

// main.js 
const say = (words) => console.log(words);

注意: 运行上述例子时不要在本地直接打开 index.html 文件,因为浏览器会默认用 file:// 协议打开,请求 main.js 资源时会跨域。该问题可以在本地开一个 http 服务解决,或在 codesandbox 上尝试也可。

如上所示,要让浏览器跑 ES Module 代码也太简单了,可以说几乎没有成本,但问题真解决了吗?可以在代码中加一行 import get from 'lodash.get' 试试。

image

可以看到控制台报错了,原来是 lodash.get 被浏览器当做相对路径加载了,其实浏览器是支持 http 路径加载的,为此我们可以将其改成 import get from 'https://unpkg.com/lodash.get'

image

不幸的是,控制台还是报错了。错误的原因是 lodash.get 这个 package 遵循的是 cjs 规范,但浏览器只认 esm 规范,所以无法解析并执行这个 package 代码。

2)SystemJS 尝试

既然浏览器原生只支持 esm 规范的代码,那是否有办法支持 cjs 规范的代码呢?经过一番调研,我们发现了 SystemJS 这个库(传送门:https://github.com/systemjs/systemjs)。

image

据其介绍,它就是一款用来解决浏览器运行 ES Module 的工具。但是当我们按照其文档摸索一番后,发现加载 lodash.get 包时还是失败了,而且报的是相同的错误... 好在我们在它的 issue 区另有收获,发现了这么一个帖子:ES Modules and CommonJS? (PS:由此可见,该问题还是普遍存在的)

image

根据官方回复的内容,我们可以提取以下几点关键信息:

  • 0.21版本之前的 SystemJS 是支持 cjs 规范的,但是目前已经不再支持。
  • 之前的版本支持 cjs 主要是因为:之前的做法是先下载代码字符串,用正则匹配 require 解析依赖,最后才执行代码;而现在的编译解析工作依靠的是浏览器自身。
  • 以前的这种解法其实存在性能隐患,故新版本中将不再考虑。

为此,我们又找到 0.21 版本的文档:https://github.com/systemjs/systemjs/blob/0.21/docs/module-formats.md

image

image

可以看到该版本的 SystemJS 似乎可以满足我们的需求,但最终 iMove 并没有采用。因为官方已经不推荐这种方式而且也不再维护,所以这条路又断了...

3)新一代 JS 模块 CDN 托管

上述的问题似乎走进了死胡同,但仔细想来问题的本质不就是『浏览器不支持 cjs 规范代码』吗?

可是反过来想,浏览器为什么要支持 cjs 规范代码呢? SystemJS 之前做的 cjs to esm 这个工作为什么要在浏览器侧做,而不是放在 CDN 上完成呢?狼叔的这篇《2021 再看 Deno(CDN for JavaScript modules的思考)》讲的很清楚,转换这事儿其实放在 CDN 上来做更合适~

简单介绍一下,jspm 做得就是这个事儿。

image

image

为了验证效果,我们来做个试验对比,以下分别是浏览器从 unpkgjspm 上加载的 lodash.get 截图:

image

image

探索 3:多文件合并成单文件执行

前文提到的『如何运行 import/export 问题』看起来似乎已经完美解决,但实操起来我们却发现还是遗漏了一个细节:iMove 的出码是一个多文件的组织形式,因此浏览器将会以相对路径的形式引入其他文件,这意味着还需一个 http 服务来提供这些资源的加载,这是我们不能接受的。

如何解?最简单的办法其实就是将 多文件合并成单文件执行,这样天然地就消灭了相对路径的问题。我们再来看个例子:

// a.js
import get from 'lodash.get';
const obj = {
  text: 'a',
  say: () => console.log(get(obj, 'text'))
};
export default obj;

// b.js
import get from 'lodash.get';
const obj = {
  text: 'b',
  say: () => console.log(get(obj, 'text'))
};
export default obj;

// main.js
import a from './a';
import b from './b';
a.say();
b.say();

上述是合并前的多文件组织形式,要合并它们其实并不难,只需注意以下 2 点:

  • import 某个文件时,该文件的代码需要立即执行
  • 不同文件中的全局变量名可能相同,需要解决命名污染的问题
// merged.js
const run = async () => {
  const modA = await (async () => {
    const get = (await import('https://jspm.dev/lodash.get')).default;
    const obj = {
      text: 'a',
      say: () => console.log(get(obj, 'text'))
    };
    return obj;
  })();
  const modB = await (async () => {
  const get = (await import('https://jspm.dev/lodash.get')).default;
  const obj = {
    text: 'b',
    say: () => console.log(get(obj, 'text'))
  };
  return obj;
  })();
  modA.say();
  modB.say();
};
run();

需要注意的是原来的 import 由于合并的原因出现在了函数体当中,所以需要使用 dynamic import 的方式来加载网络包。剩下的只要用 正则表达式匹配替换 或者 AST 转换 都能实现对应的效果,本文就不再展开详述。

探索 4:Script 间如何传值通信

行文到这,可以说是『万事俱备,只欠东风』了。经过刚才的多文件合并,我们只需运行代码,获取结果展示即可。这里需要注意的是,由于待执行的代码遵循 esm 规范,如果想用 eval 执行代码就有一个前提:当前代码必须也在 type="module"script 标签下。不过我们可以换个思路,不用 eval 也能执行字符串代码:

const script = document.createElement('script');
script.type = 'module';
script.text = 'code here';
document.body.appendChild(script);

如上所示,我们可以通过动态插入 script 标签的方式来执行代码。可是随之而来的问题又变成了:我们该如何在代码运行完之后触发回调拿到结果展示呢?何况现在这两段代码还是在两个 script 标签下。

最容易想到的办法就是用类似 jsonp 的方式,一方在全局 window 下注册唯一方法,另一方在代码执行完之后调用该方法。在这里,我们介绍一种更优雅的方式,可以用事件通信的方式来解决~

MDN 官方文档传送门:Custom Events

// 监听事件 document.addEventListener('customEvent', function (evt) { console.log(evt.detail); }, false); // 发送事件 document.dispatchEvent(new CustomEvent('customEvent', {detail: {text: 'iMove'} }));

总结

本文先后介绍了 iMove 在线运行节点代码这一路所踩过的坑,最终主要还是依靠 http-import 的思想来解决问题。无疑, deno 改变了大家的对包管理的看法。本身 deno 够小,试错成本低,它确确实实引领了一个潮流方向。这个改进虽说不算新,但反响确实很好,大概是天下人苦 npm(npm 开玩笑的说法是:你怕吗)久已,用法简单,高效,甚至是衍生出很多关于 CDN for JavaScript modules 的思考。如果你有更好的想法,欢迎一起交流。

另外,做 iMove 在线执行代码功能是用户视角做的决定,小伙伴们都非常认可这个决策,并在落地过程中,能够探索出一种全新的方式,这是非常值得称赞的。本身 iMove 就是一个以锻炼团队为目的开源的项目,大家能够定义问题,能够解决问题,能够建立信心,能够激发技术热情,它的目的就达到了。

iMove 系列推荐阅读

  1. 《2021 年前端趋势预测》
  2. 《F2C能否让前端像运营配置一样开发?》
  3. 《登上 Github 趋势榜,iMove 原理技术大揭秘!》
  4. 《iMove 基于 X6 + form-render 背后的思考》
  5. 《所见即所得! iMove 在线执行代码探索》

小石头若海
1.4k 声望1.4k 粉丝

努力不一定成功,但不努力会很轻松哦~