Node.js終結者?青出於藍的Deno(二)

 Gordon Lau 劉偉中

Gordon Lau 劉偉中

2020-08-21

deno_paper.png

上回講到三個DenoNode.js優勝的地方,今次我們將會繼續介紹Deno這個初生之犢的優勢!

Node.js 使用NPM VS Deno 無中央資料庫

Node.js的模組(Module),主要是存在NPM之上,也許不少開發者不知道,其實NPM是一間公司,專為開發者提供存放私人模組(private module)的服務。而2020年3月時Github也收購了NPM,也就是代表現行大多數的Node.js 模組,都在Microsoft的控制之下。這無疑代表NPM本質也是一個中央資料庫,所以當間中NPM因技術問題而無法供開發者下載模組的話,Node.js開發者就會哀鴻遍野,因為NPM就是一個Single point of failure(單點故障)。

npmjs.png

Screen capture from npm.org

相較之下,Deno則完全沒有中央資料庫的概念,在Deno的設計之中。要安裝任何模組,有兩個不同的方法:

1. 使用URLimport模組,就像瀏覽器一樣,以下是一個由Opine程式庫的一段例子:

import {opine} from 'https://deno.land/x/opine@0.21.2/mod.ts';

const app = opine();

app.use((req,res)=>{
    res.send("Hello World");
});

app.listen(3000);

由這個例子可見,我們只需寫好URLDeno自動會從URL下載所需的程式碼,也會自動將程式碼存在快取(Cache),省卻下載時間。

2. 使用相對路徑(Relative Path)讀取其他本機檔案

import { sum } from './sum.ts'

sum(1,2);

因此,Deno的套件(Package)運作方式與瀏覽器很相似,沒有限制像必須使用NPM那樣的中央資料庫。

Node.js 使用node_modules VS Deno自動下載

Node_modules的問題很有名,每個Node.js 開發者都有這樣的經驗: 電腦磁碟空間快要耗盡了,原來是node_modules佔用了成GB的空間...網上還有這樣的meme:

node_modules.jpeg

Source

Node_modules的質量比黑洞更甚:)!

Deno由於摒棄了中央資料庫的做法,變相只需要下載所需的模組,無須每次下載新專案,都要運行npm install。 那在何時才會開始自動下載呢?答案是在第一次運行的時候。

gordon➜ ~ deno run --allow-net --allow-read opine.ts
Download https://deno.land/x/opine@0.21.2/mod.ts
...
Download https://deno.land/x/evt@v1.8.7/tools/Deferred.ts
Download https://deno.land/x/evt@v1.8.7/lib/Evt.loosenType.ts
Download https://deno.land/x/evt@v1.8.7/tools/safeSetTimeout.ts
Download https://raw.githubusercontent.com/garronej/run_exclusive/v2.2.13/deno_dist/lib/runExclusive.ts
Check file:///home/gordon/opine.ts

以後,再運行這個檔案,就會直接使用已在快取之檔案,因此既有瀏覽器的簡便,也不會每次重新下載。

gordon➜ ~ deno run --allow-net --allow-read opine.ts
Check file:///home/gordon/opine.ts

剩餘兩個其實算是小問題,不過既然Ryan提到了,就一併解釋吧。

Node.js require無須副檔名 VS Deno import 必須副檔名

Node.js中的require是不須副檔名的,因此如果你想require一個名為myfile.js的檔案,你只需寫:

const myfile = require('./myfile');

這為Node.js平添了不必要的複雜性,因為myfile是甚麼呢?是myfile.js? 還是myfile.ts? 隨著Node.js的用途增多,這樣的「方便」,反而模糊了背後的意思。

index.js有JavaScript 入口的概念

這是完全無用的功能,因為在package.json就可以控制main是那個檔案了,因此這個功能沒有實際作用。

{
    ...
    "main":"index.js",
    ...
}

其他未提及的優勢

不過,筆者在用Deno小試牛刀之後,感到Deno還有其他明顯優勢,而Ryan並沒有提及。大概是因為2018年時發展尚早,未有這樣想法。

Deno能夠獨立執行

首先其中一個Deno的最大優勢,就是安裝Deno只是安裝了一個獨立的執行檔,也就是如果你去Deno的網站,運行以下的command:

# 如果你是用Mac/Linux
curl -fsSL https://deno.land/x/install/install.sh | sh

# 如果你是用 Windows PowerShell
iwr https://deno.land/x/install/install.ps1 -useb | iex

其實只是下載了一個二進位檔案(Binary file),也就是一個執行檔。在筆者的電腦,只是一個在~/.deno資料夾內的檔案。

gordon➜ ~ tree ~/.deno            
/home/gordon/.deno
└── bin
    └── deno

1 directory, 1 file

這個做法承襲自Golang(大概因為Ryan Dahl曾長時間運用Golang工作)而來,好處是使用deno,只有一個binary file的依賴,不會因為你有其他依賴未安裝,就無法使用deno,而且整個流程完全無須系統使用者權限(Administrator Right),實在令安裝過程簡單不少。

要運行Denocommand,只需打deno XXX。以下是deno help的輸出,

deno 1.3.0
A secure JavaScript and TypeScript runtime

...

USAGE:
    deno [OPTIONS] [SUBCOMMAND]

OPTIONS:
    -h, --help                     Prints help information
    -L, --log-level <log-level>    Set log level [possible values: debug, info]
    -q, --quiet                    Suppress diagnostic output
    -V, --version                  Prints version information

SUBCOMMANDS:
    bundle         Bundle module and dependencies into single file
    cache          Cache the dependencies
    completions    Generate shell completions
    doc            Show documentation for a module
    eval           Eval script
    fmt            Format source files
    help           Prints this message or the help of the given subcommand(s)
    info           Show info about cache or info related to source file
    install        Install script as an executable
    lint           Lint source files
    repl           Read Eval Print Loop
    run            Run a program given a filename or url to the module. Use '-' as a filename to read from stdin.
    test           Run tests
    types          Print runtime TypeScript declarations
    upgrade        Upgrade deno executable to given version

ENVIRONMENT VARIABLES:
    ...

由這裏可見,Deno有不少有用的subcommand,就等筆者介紹幾個特別令人感興趣的!

自帶測試工具

在寫Node.js的時候,開發者常常需要寫自動測試(Automated Tests),然而Node.js中有很多不同的測試程式庫: JestJasmineMochaChai都是其中的例子。因此要閱讀他人之Node.js專案時,如果別人正好用了你不常用的測試程式庫(Testing library),就變相要於不同專案使用不同之testing library,實在是不太方便。

Deno由於是後起之秀,今時今日自動測試早已是編程必要部份,因此原生支援testing,無需再思前想後要用那個library了。 以下是一個Deno測試的例子:

import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

Deno.test("1 + 1 should be 2", () => {
  assertEquals(1 + 1 , 2);
});

deno test直接運行,就會得到ok的結果。

gordon➜ ~ deno test test.ts 
Download https://deno.land/std/testing/asserts.ts
Warning Implicitly using latest version (0.65.0) for https://deno.land/std/testing/asserts.ts
Download https://deno.land/std@0.65.0/testing/asserts.ts
Download https://deno.land/std@0.65.0/fmt/colors.ts
Download https://deno.land/std@0.65.0/testing/diff.ts
Check file:///home/gordon/.deno.test.ts
running 1 tests
test 1 + 1 should be 2 ... ok (2ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (3ms)

自帶Linter及formatter

Node.jsLinterFormatter都是需要額外安裝及設置,著名的prettier就是其中的好例子。Deno則早已包含在執行檔之內。

直接使用Linter:

gordon➜ ~ deno lint test.ts 

直接使用Formatter:

gordon➜ ~ deno fmt test.ts            
/home/gordon/test.ts

將這些本身是外部套件的功能加至原生支援(Native support),最大的好處在於開發者無須為選擇程式庫煩惱,而且程式碼的樣式也因而非常統一(Consistent), 非常方便維護。

自帶Bundler

還有一個筆者認為Deno非常強大的功能,就是deno的封裝(bundle)功能。現時要開發React應用時,Webpack是必不可少的一環,因為React當中大量使用了瀏覽器不支援的格式如JSXTypeScriptSCSS等。因此React開發一個必備的過程就是Build的步驟,也就是將程式碼由Node.js的世界﹐「封裝」(Bundle)成Browser的世界。

Webpack 網站banner很準確地概括了Bundler這個概念。

webpack.png

Screen capture from webpack.js.org

Deno對此的解決方法如上面雷同,就是將封裝功能一併放到執行檔之中。 如果用deno bundle將剛才的test.ts封裝

gordon➜ ~ deno bundle test.ts  test-bundle.js 
Bundle file:///home/gordon/test.ts
Emit "test-bundle.js" (25.27 KB)

test-bundle.js的開頭大概是這樣:

/ Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.

// This is a specialised implementation of a System module loader.

"use strict";

// @ts-nocheck
/* eslint-disable */
let System, __instantiate;
(() => {
  const r = new Map();

  System = {
    register(id, d, f) {
      r.set(id, { d, f, exp: {} });
    },
  };
  async function dI(mid, src) {
    let id = mid.replace(/\.\w+$/i, "");
    if (id.includes("./")) {
      const [o, ...ia] = id.split("/").reverse(),
        [, ...sa] = src.split("/").reverse(),
        oa = [o];
      let s = 0,
        i;
    ...
    // 以下省略

這個檔案可以直接用deno.run運行,裏面有齊了整個test.ts的依賴,不過由於有Deno的依賴,這個檔案不能在瀏覽器內運行。

Deno的弱點

談了那麼多Deno的好處,那Deno相比起Node.js,有明顯的弱點嗎? 筆者想到兩點比較明顯的弱點,都與Deno 的兼容性有關的。

與現有Node.js專案不兼容

Deno完全摒棄package.jsonnode_modules的做法,因此你不可能直接使用deno去運行現有的Node.js專案,因為連import的運作模式,也不盡相同。 因此除非你是重新開始一個新專案,也是所謂的Green Field Project,否則你不能在一個專案中漸進使用Deno

筆者認為這是Deno成為主流的最大障礙,因為對大多數公司而言,大量由Node.js所寫成的程式碼有很大的商業價值,因此要由零開始乃是不切實際。我們可以由KotlinScala的發展觀察到這個現象,ScalaKotlin都是以Better Java的姿態面世,縱然兩者都是JVM LanguagesScala基本上與Java不相容,Java專案要變成Scala專案也很不容易,因此Scala一直都只是在Big Data方面得到長足發展。Kotlin則相反,設計之初就以漸進使用為賣點,更可以在一個Java專案只是少數幾個檔案使用Kotlin,結果今日的發展大家都知道,Kotlin因其兼容性,真正達到了Better Java的特點。 所以筆者認為Deno要得到更大成功,能夠漸進在Node.js中使用,是至為必要的。

Deno無法得益於Node.js龐大的套件庫

另外,DenoNPM本身不相容,因此Deno開發者無法在NPM上直接使用已存在的NPM套件。這也是對Deno的發展的另一障礙。因為NPM packages坐擁世上最多的套件數目,遠遠拋離其他程式語言的套件管理員,無法從此得益,實屬可惜。

module_counts.png

Screen capture from modulecounts.com

由網站Module CountsNPM有超過125萬個套件,如果Deno能夠運用套件,絕大多數的功能就已告齊備。

結語

返回文章的題目,Deno能否取代Node.js成為JavaScript的主要開發環境呢?筆者認為短期兩至三年內可能性都非常低,但如果未來Deno能夠解決筆者所言的兩個局限性,筆者也會躍躍欲試,將手頭上的Node.js專案加入Deno了。

留言

延伸閱讀

Node.js終結者?青出於藍的Deno(一)

SQL首部曲:NoSQL? No! SQL!

Dart vs JavaScript vs TypeScript

到底React Hooks 有何特別?