本身 iMove
的定位就是 “一个逻辑可复用的,面向函数的,流程可视化的 JavaScript 工具库。"
对开发者而言, iMove
恰好是可以完成这些目标的理想工具。动动鼠标,写一下节点函数,代码导出,放到具体工程里就可以直接使用,是不是很方便?
那么,为什么我们还要做 iMove
在线执行代码功能呢?站在用户视角看,对于开发者来说,选中节点,右键在线执行代码是必要的。这样可以把 iMove
的工具属性做到极致,让开发者体验更好,操作更简单。
上次在《登上 Github 趋势榜,iMove 原理技术大揭秘!》一文中我们卖了个关子,今天就和大家分享下 iMove 是如何实现在线执行节点代码的~
故事还得从组内的一个小伙伴说起,当时她是第一次使用 iMove
,询问我如何测试运行刚写好的代码。二话不说,我就在她电脑上一通操作:
@imove/cli
imove -d
logic.invoke('trigger')
log
信息“我就想运行下刚写好的代码,看看输出结果是什么...”
『步骤繁琐』、『操作麻烦』、『有上手成本』、『运行结果不直观』... 此刻,脑中第一时间闪过的就是以上这些词汇,再看向小伙伴那疑惑的眼神,仿佛下一刻她就要被劝退了...
这可怎么行,体验必须优化,使用成本必须降下来!于是就有了右键在线执行代码的功能。
其实上述问题的诉求很简单: 写完节点的代码之后,旁边有个运行按钮,一点直接就能看到运行结果就行了。我们来看下就是这么一个优化都能带来哪些改变:
mock
输入,测试效率将得以极大的提升。想法是很美好,但具体该如何实现呢?接着往下看~
首先,要解决的第一个问题就是:节点代码在哪运行?
经过评估,我们认为主要有两个选择:
bundle
发送回浏览器端执行。对于以上两个方案, iMove
选择了前者,理由很简单:在线运行节点代码的初心就是为了降低上手成本,如果还需要本地起一个服务,使用者势必又要学习一个新命令,这无形中又提高了使用门槛。
然而,要想在浏览器中直接运行节点代码,还是有很多阻碍,并非一个 eval
就完事了~
看过 iMove
的出码就知道,流程图中的每个节点代码最终都会被编译成一个单独的 js
文件。因此,每个节点是支持 import
其他 npm
包的,这也是为什么不能直接调用 eval
的原因。
当然,也许聪明的你早已想到浏览器是支持 native ES module
的,包括之前挺火的 Vite 其实底层原理也是基于这个。为此,我们先来简单介绍下如何让 import/export
代码在浏览器中跑起来~
先来看 MDN
上的 官方文档:
可以看到,其实现代浏览器早就支持了,因此我们可以不用考虑兼容性问题。除此之外,继续浏览文档你会发现要让浏览器支持 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'
试试。
可以看到控制台报错了,原来是 lodash.get
被浏览器当做相对路径加载了,其实浏览器是支持 http
路径加载的,为此我们可以将其改成 import get from 'https://unpkg.com/lodash.get'
。
不幸的是,控制台还是报错了。错误的原因是 lodash.get
这个 package
遵循的是 cjs
规范,但浏览器只认 esm
规范,所以无法解析并执行这个 package
代码。
既然浏览器原生只支持 esm
规范的代码,那是否有办法支持 cjs
规范的代码呢?经过一番调研,我们发现了 SystemJS
这个库(传送门:https://github.com/systemjs/systemjs)。
据其介绍,它就是一款用来解决浏览器运行 ES Module
的工具。但是当我们按照其文档摸索一番后,发现加载 lodash.get
包时还是失败了,而且报的是相同的错误... 好在我们在它的 issue
区另有收获,发现了这么一个帖子:ES Modules and CommonJS? (PS:由此可见,该问题还是普遍存在的)
根据官方回复的内容,我们可以提取以下几点关键信息:
SystemJS
是支持 cjs
规范的,但是目前已经不再支持。cjs
主要是因为:之前的做法是先下载代码字符串,用正则匹配 require
解析依赖,最后才执行代码;而现在的编译解析工作依靠的是浏览器自身。为此,我们又找到 0.21
版本的文档:https://github.com/systemjs/systemjs/blob/0.21/docs/module-formats.md
可以看到该版本的 SystemJS
似乎可以满足我们的需求,但最终 iMove
并没有采用。因为官方已经不推荐这种方式而且也不再维护,所以这条路又断了...
上述的问题似乎走进了死胡同,但仔细想来问题的本质不就是『浏览器不支持 cjs
规范代码』吗?
可是反过来想,浏览器为什么要支持 cjs
规范代码呢? SystemJS
之前做的 cjs to esm
这个工作为什么要在浏览器侧做,而不是放在 CDN
上完成呢?狼叔的这篇《2021 再看 Deno(CDN for JavaScript modules的思考)》讲的很清楚,转换这事儿其实放在 CDN
上来做更合适~
简单介绍一下,jspm 做得就是这个事儿。
为了验证效果,我们来做个试验对比,以下分别是浏览器从 unpkg
和 jspm
上加载的 lodash.get
截图:
前文提到的『如何运行 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
转换 都能实现对应的效果,本文就不再展开详述。
行文到这,可以说是『万事俱备,只欠东风』了。经过刚才的多文件合并,我们只需运行代码,获取结果展示即可。这里需要注意的是,由于待执行的代码遵循 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
就是一个以锻炼团队为目的开源的项目,大家能够定义问题,能够解决问题,能够建立信心,能够激发技术热情,它的目的就达到了。