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

i5ting發表於2021-01-04

2018年,我曾經在deno釋出不久寫過一篇《Deno不是下一代Node.js!》的文章,正好最近有一些研究,站在2021年再來看看deno。

無疑,deno改變了大家的對包管理的看法。本身deno夠小,試錯成本低,它確確實實引領了一個潮流方向。這個改進雖說不算新,但反響確實很好,大概是天下人苦npm(npm開玩笑的說法是:你怕嗎)久已,用法簡單,高效,甚至是衍生出很多關於CDN for JavaScript modules的思考。

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

緣起

我們做了一個imove的開源專案,iMove 是一個邏輯可複用的,面向函式的,流程視覺化的 JavaScript 工具庫。

目前已經支援的特性

  • [x] 流程視覺化: 上手簡單,繪圖方便,邏輯表達更直觀,易於理解
  • [x] 邏輯複用: iMove 節點支援複用,單節點支援引數配置
  • [x] 靈活可擴充套件: 僅需寫一個函式,節點可擴充套件,支援外掛整合
  • [ ] 多語言編譯: 無語言編譯出碼限制(例: 支援 JavaScript, Java 編譯出碼)

image.png

其實,直白點講,就是將運營配置的一套玩法給開發用。每個節點都是函式,視覺化,可配置,可組裝,可匯出程式碼,做的是很剋制的。基於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連結外部程式碼文件(https://deno.land/manual/link...),它的做法是通過--allow-net引數選項,可以讓deno 的runtime可以下載imports並將其快取在磁碟上。

這其實只是快取在系統目錄中,比如mac上是$HOME/Library/Caches/deno。其實並沒有啥本質提升。

通過程式碼地址來引用程式碼,確實是很爽的一件事兒。

No more node_modules bloat, no dependency to install.

在node世界裡,也有人實現了類似的機制,即https://github.com/egoist/imp...。它是通過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做這事兒也是很簡單的。只要在https://github.com/nodejs/nod...,實現下載和快取就可以解決。可是,歷史包袱過重,想做到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的新的託管方式。

它的原理圖。

image.png

以npm和github作為源,同步到亞馬遜s3上,繼而代理到各種CDN,為使用者提供服務。

cjs to esm

很早就有了cjs轉esm的工具。比如https://github.com/standard-t...,自己實現大量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都已經做了這件事兒。

藉助 http://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還會遠嗎?

相關文章