2021 再看 Deno(CDN for JavaScript modules的思考)

阿里巴巴 前端委员会智能化小组 @ 阿里巴巴

文/ 阿里淘系 F(x) Team - 狼叔

2018年,我曾经在 Deno 发布不久写过一篇《 Deno 不是下一代 Node.js!》的文章,正好最近有一些研究,站在2021年再来看看 Deno。

无疑,Deno 改变了大家的对包管理的看法。本身 Deno 够小,试错成本低,它确确实实引领了一个潮流方向。这个改进虽说不算新,但反响确实很好,大概是天下人苦 NPM(NPM开玩笑的说法是:你怕吗)久已,用法简单,高效,甚至是衍生出很多关于 CDN for JavaScript modules 的思考。

下面,我们就一起看一下吧。

缘起

我们做了一个 imove 的开源项目,iMove 是一个逻辑可复用的,面向函数的,流程可视化的 JavaScript 工具库。

目前已经支持的特性

  • : 上手简单,绘图方便,逻辑表达更直观,易于理解
  • : iMove 节点支持复用,单节点支持参数配置
  • : 仅需写一个函数,节点可扩展,支持插件集成
  • : 无语言编译出码限制(例: 支持 JavaScript, Java 编译出码)

其实,直白点讲,就是将运营配置的一套玩法给开发用。每个节点都是函数,可视化,可配置,可组装,可导出代码,做的是很克制的。基于 x6 图形和 json 协议,可以说是以最小的投入成本拿到最大的效果,从定位上看,还是相当精准的。我们自己在业务中使用落地,无论体验还是效果,也是非常好的。

最近为了开源,小伙伴提了2个优化点:

  1. 双击图形,可以编辑函数,这样操作更方便。已经做完了。
  2. 在这个界面上做到节点或流程可测试。确实会有这个问题,如果节点可测,功能上会更加实用。

第二点,我是非常认可这的。但问题来了,如何实现呢?

每个节点的代码等价于一个 js 文件,因此你不用担心全局变量的命名污染问题,甚至可以 import 现有的 NPM 包,但最后必须 export 出一个函数。需要注意的是,由于 iMove 天生支持节点代码的异步调用,因此 export 出的函数默认是一个 promise。

举例,就拿 是否登录 这个分支节点为例,我们来看下节点代码该如何编写:

import fetch from 'node-fetch';

export default async function (ctx) {
  return fetch('/api/isLogin')
    .then(res => res.json())
    .then(res => {
      const {success, data: {isLogin} = {}} = res;
      return success && isLogin;
    }).catch(err => {
      console.log('fetch /api/isLogin failed, the err is:', err);
      return false;
    });
}
复制代码

引申出

  1. 这是 ESM,基于es module 的主流写法。
  2. 支持外部包导入,不然很难能够应对复杂场景。

类似的 jsbin,或 codepen,或 codesandbox,可以使用 webpack 的 off-line 插件实现,也可以采用 webide初始化安装模块来实现,但这并不是好的方式。imove是要兼容浏览器和 node 的,直接运行,不需要本地安 NPM 包,也能够在 node里完美运行。这就导致,我们必须要往 http import 方向思考问题。System.js 就是一个极好的选择。

import-http

如果你去看 Deno 链接外部代码文档(Deno.land/manual/link…),它的做法是通过 --allow-net 参数选项,可以让 Deno 的 runtime 可以下载 imports 并将其缓存在磁盘上。

这其实只是缓存在系统目录中,比如 mac 上是 $HOME/Library/Caches/Deno。其实并没有啥本质提升。

通过代码地址来引用代码,确实是很爽的一件事儿。

No more node_modules bloat, no dependency to install.

在node世界里,也有人实现了类似的机制,即github.com/egoist/impo…。它是通过 webpack/rollup 编译时处理的。

看具体用法

先配置 webpack.config.js:

const ImportHttpWebpackPlugin = require('import-http/webpack')

module.exports = {
  plugins: [new ImportHttpWebpackPlugin()]
}
复制代码

然后就可以在代码直接使用了:

import React from 'https://unpkg.com/react'
import Vue from 'https://unpkg.com/vue'

console.log(React, Vue)
复制代码

原理:通过 webpack 的 compiler.resolverFactory.hooks.resolver 解析 import-http-resolver,即 import 里带有 http 和 https 的。然后通过 fileModuleCache 和 httpCache 对下载的内容进行缓存。

其实,Node.js 做这事儿也是很简单的。只要在 github.com/nodejs/node… 目录里,实现下载和缓存就可以解决。可是,历史包袱过重,想做到 no filesystem imports of any kind from https sources,还是有一段路要走的。不过这块,也是大家能够参与贡献 Node.js 源码的很好的点。

支持第三方 ESM loader 也快了,大家拭目以待吧,用法类似于下面的

node-dev --experimental-loader ts-node/ESM/transpile-only ./index.ts

esm.run

国外还有一个服务,名为 ESM.run,它的定位是:”A New-Age CDN for JavaScript modules“。这话说的已经相当直接了,它就是重新定义基于 CDN 的 JavaScript modules 的新的托管方式。

它的原理图。


以 NPM 和 github 作为源,同步到亚马逊 s3 上,继而代理到各种 CDN,为用户提供服务。

CJS to ESM

很早就有了 CJS 转 ESM 的工具。比如 github.com/standard-th…,自己实现大量 polyfill,过渡态,尝试还行,早晚还是要回归到内核中的。

The brilliantly simple, babel-less, bundle-less ECMAScript module loader.

// Set options as a parameter, environment variable, or rc file.
require = require("ESM")(module/*, options*/)
module.exports = require("./main.js")
复制代码

这是本地的做法,如果变成 http import,这件事儿本地是不需要做的,把这些都交给 cdn 类的服务来做更合适。事实上,pika.dev/skypack.dev/jspm.io都已经做了这件事儿。

借助 jspm.io(或其他类似服务)来将 commonjs 转换为兼容的 ESM 格式。

import cheerio from "https://dev.jspm.io/NPM:cheerio/index.js";
复制代码

引申一下,2个问题。

  1. 国内还没有类似的服务,既然有 cNPM,会不会有类似的服务呢?我想会有人做的。
  2. 传统 CDN 厂商下一步也会朝着这个方向走的,要么收购,要么自建。这其实是很好的生意。一方面满足开发者的诉求,另一方面也能够为传统 CDN 厂商提供增量业务。它也是新基建的组成部分。

总结

Deno 是一个很好的创新,上面讲的 import-http,esm.run 或模块转化服务,可以说都是 Deno 探索间接或直接作用的结果。

但如果说想替代 Node,目前的这些特性和性能提升,还不足以替代 node。Node 社区在 node 4之后接纳es特性之后还是很与时俱进的,CJS 和 ESM 处理曾经也很及时。那么既然时机已成熟,今天 node 拥抱 http import 还会远吗?



除文章外还有更多的团队内容等你解锁🔓