Deno 是一個類似于 Node.js 的 JavaScript 和 TypeScript 運行時,基于 Rust 和 JavaScript V8 引擎構建。它由 Node.js 創始人 Ryan Dahl 創建,以彌補他在 2009 年最初設計和發布 Node.js 時所犯的錯誤。
Ryan's 有關 Node.js 的遺憾在 2018 年 JSConf EU 上著名的演講 “我對 Node.js 遺憾的十件事” 有充分的記錄。總而言之,他感嘆缺乏對安全性的關注、通過 node_modules 解析模塊、瀏覽器工作方式的各種偏差以及其他問題,他開始在 Deno 中修復這些錯誤。
在本文中,我們將討論 Deno 的創建原因以及它與 Node.js 相比的優缺點。還將對 Deno 的怪癖(quirks)和功能做一個實用概述,以便您決定它是否適合于您的下一個項目。
本文主題
安裝 Deno
Deno 做為一個獨立的、自包含的二進制文件沒有任何依賴。你可以通過多種方式安裝 Deno,具體取決于你的操作系統。最簡單的方法是下載并執行一個 shell 腳本,如下所示:
# Linux and macOS $ curl -fsSL https://deno.land/x/install/install.sh | sh # Windows PowerShell $ iwr https://deno.land/x/install/install.ps1 -useb | iex
一旦你為你的操作系統執行了適當的命令,Deno CLI 二進制文件將會下載到你的計算機。根據你選擇的安裝方法,你需要添加這個二進制文件位置到你的 PATH。
你可以在 Bash 中添加以下行到你的 $HOME/bash_profile 文件來執行此操作。你可能需要開啟一個新的 shell 會話來使更改生效。
export DENO_INSTALL="$HOME/.deno" export PATH="$DENO_INSTALL/bin:$PATH"
運行以下命令,驗證 Deno 的安裝版本。如果這個 CLI 已經下載成功并且添加到您的 PATH,控制臺應該會打印輸出 Deno 版本信息。
$ deno --version deno 1.14.2 (release, x86_64-unknown-linux-gnu) v8 9.4.146.16 typescript 4.4.2
如果你的 Deno 版本已過時,可以通過 upgrade 子命令升級到最新的 release 版本。
$ deno upgrade Looking up latest version Found latest version 1.14.2 Checking https://github.com/denoland/deno/releases/download/v1.14.2/deno-x86_64-unknown-linux-gnu.zip 31.5 MiB / 31.5 MiB (100.0%) Deno is upgrading to version 1.14.2 Archive: /tmp/.tmpfdtMXE/deno.zip inflating: deno Upgraded successfully
接下來,編寫一個常用的 Hello world 程序來驗證是否可以正常工作。可以為你的 Deno 項目創建一個目錄并在該目錄下創建一個 index.ts 文件放入以下代碼:
function hello(str: string) { return `Hello ${str}!`; } console.log(hello("Deno"));
在 deno run 子命令后將文件名做為參數執行,如果輸出 “Hello Deno!”,意味著你已經成功的安裝了 Deno。
$ deno run index.ts Check file:///home/ayo/dev/deno/index.ts Hello Deno!
要了解 Deno CLI 提供的其它功能及選項,使用 --help 標志:
$ deno --help
一流的 TypeScript 支持
Deno 相較于 Node.js 的一大賣點是它對 TypeScript 的一流支持。
正如你所看到的,除了安裝 Deno CLI 之外,你不需要做任何的事情讓它來工作。與其前身一樣,使用 V8 引擎運行時來解析解釋和執行 JavaScript 代碼,但它還在其可執行文件中使用 TypeScript 編譯器以實現對 TypeScript 的支持。
在后臺,TypeScript 代碼被檢查和編譯。生成的 JavaScript 代碼會緩存在文件系統上的一個目錄中,可以再次執行而無需從頭編譯。你可以使用 deno info 檢查緩存目錄位置和其它包含 Deno 管理文件的目錄。
在使用 TypeScript 時 Deno 不需要任何配置,但如果你想調整 TypeScript 編譯器解析代碼的方式,你可以提供一個 JSON 文件指定 TypeScript 的編譯選項。盡管 tsconfig.json 是用 TSC 編譯時的一個常見選擇方式,但是 Deno 團隊建議使用 deno.json 因為其它特定于 Deno 的配置選項也可以放置在這里。
請注意 Deno 不支持所有的 TypeScript 編譯選項。在 Deno 的文檔中提供了一個完整的可用的的選項和它們的默認值。對于 Deno 的示例配置文件如下所示:
{ "compilerOptions": { "checkJs": true, "noImplicitReturns": true, "noUnusedLocals": true, "noUnusedParameters": true, "noUncheckedIndexedAccess": true } }
在撰寫本文時,Deno 不會自動檢測 deno.json 文件,因此必須通過 --config 標志指定它。然后,這個功能計劃在未來的一個版本發布。
$ deno run --config deno.json index.ts
當 Deno CLI 遇到類型錯誤時,它會停止腳本編譯并以非 0 狀態碼退出終止。你可以通過以下方式繞錯錯誤:
在錯誤發生的地方使用 //@ts-ignore 或 //@ts-expect-error // @ts-nocheck 在一個文件的開始忽略所有的錯誤。
Deno 還提供了一個 --no-check 標志來完全禁用類型檢查。這有助于防止 TypeScript 編譯器在快速迭代問題時減慢你的速度。
$ deno run --no-check index.ts
權限功能
Deno 以成為 JavaScript 和 TypeScript 的安全運行時而驕傲。它維護安全性部分的方式是通過權限功能完成的。為了演示權限功能在 Deno 中的工作方式,添加以下腳本到您的 index.ts 文件。這個腳本會從 disease.sh 獲取最新的全球 Covid-19 統計數據。
async function getCovidStats() { try { const response = await fetch("https://disease.sh/v3/covid-19/all"); const data = await response.json(); console.table(data); } catch (err) { console.error(err); } } getCovidStats();
當你嘗試執行腳本時,它應該顯示 PermissionDenied 錯誤:
$ deno run index.ts PermissionDenied: Requires net access to "disease.sh", run again with the --allow-net flag # PermissionDenied:需要對 “disease.sh” 的網絡訪問,使用 --allow-net 標志再次運行。
上面的錯誤消息表明腳本沒有給予網絡訪問權限,它建議在命令中包含 --allow-net 標志以授予訪問權限。
$ deno run --allow-net index.ts ┌────────────────────────┬───────────────┐ │ (idx) │ Values │ ├────────────────────────┼───────────────┤ │ updated │ 1633335683059 │ │ cases │ 235736138 │ │ todayCases │ 32766 │ │ deaths │ 4816283 │ │ todayDeaths │ 670 │ │ recovered │ 212616434 │ │ todayRecovered │ 51546 │ │ active │ 18303421 │ │ critical │ 86856 │ │ casesPerOneMillion │ 30243 │ │ deathsPerOneMillion │ 617.9 │ │ tests │ 3716763329 │ │ testsPerOneMillion │ 473234.63 │ │ population │ 7853954694 │ │ oneCasePerPeople │ 0 │ │ oneDeathPerPeople │ 0 │ │ oneTestPerPeople │ 0 │ │ activePerOneMillion │ 2330.47 │ │ recoveredPerOneMillion │ 27071.26 │ │ criticalPerOneMillion │ 11.06 │ │ affectedCountries │ 223 │ └────────────────────────┴───────────────┘
你可以提供一個以逗號分隔的主機名或 IP 地址的允許列表做為 --allow-net的參數,以便腳本僅可以訪問指定的網站,而不是授予腳本訪問所有網站的權限(如上所示)。如果腳本嘗試鏈接不在允許列表中的列,Deno 將會阻止它的鏈接,并且腳本將會執行失敗。
$ deno run --allow-net='disease.sh' index.ts
這個功能是 Deno 對 Node.js 的改進之一,任何腳本都可以通過網絡訪問任何資源。讀取和寫入文件系統也存在類似的權限。如果一個腳本需要執行任一任務,你需要分別指定 --allow-read 和 --allow-write 權限。兩個標志允許你為腳本設置特定可訪問的目錄,以便文件系統的其它部分免受篡改。Deno 還提供了一個 --allow-all 標志,如果需要,可以為一個腳本開啟所有的安全敏感功能。
與瀏覽器 API 的兼容性
Deno 的主要目標之一是盡可能與 Web 瀏覽器兼容。這反映在它使用 Web 平臺 API,而不是為某些操作創建特定于 Deno 的 API。例如,我們在上一節中看到了 Fetch API 的實際應用。這是一個在瀏覽器中使用的確切 Fetch API,在必要時有一些偏差以說明 Deno 中獨特的安全模型(這些更改大多無關緊要)。
Deno 的在線文檔中列出了所有已實現的瀏覽器 API。
依賴管理
Deno 管理依賴方式可能是它與 Node.js 的最大不同之處。
Node.js 使用 npm 或 yarn 之類的包管理器將第三方包從 npm 注冊表下載到 node_modules 目錄和 package.json 文件以跟蹤項目的依賴關系。Deno 摒棄了這些機制,轉而采用更加以瀏覽器為中心的方式來使用第三方包:URLs。
這里有一個使用 Deno web 應用程序框架 Oka 來創建基礎 web server 的示例:
import { Application } from "https://deno.land/x/oak/mod.ts"; const app = new Application(); app.use((ctx) => { ctx.response.body = "Hello Deno!"; }); app.addEventListener("listen", ({ hostname, port, secure }) => { console.log(`Listening on: http://localhost:${port}`); }); await app.listen({ port: 8000 });
Deno 使用 ES 模塊,與 Web 瀏覽器中使用的模塊系統相同。只要引用的腳本(模塊)導出方法或其它值,就可以從絕對路徑或相對路徑導入模塊。值得注意的是,文件擴展名必須始終存在,無論您是從絕對路徑還是相對路徑導入。
雖然您可以從任何 URL 導入模塊,但許多專門為 Deno 構建的第三方模塊都通過 **deno.land/x** 進行緩存。每次發布模塊的新版本時,它都會自動緩存在該位置并使其不可變,以便模塊的特定版本內容保持不變。
假設您運行上一個片段中的代碼。在這種情況下,它將下載模塊及其所有依賴項并將它們緩存在本地 DENO_DIR 環境變量指定的目錄中(默認應為 $HOME/.cache/deno)。下次程序運行時,將不會再次下載,因為所有依賴項都已緩存在本地。這類似于 Go 模塊系統的工作方式。
$ deno run --allow-net index.ts Download https://deno.land/x/oak/mod.ts Warning Implicitly using latest version (v9.0.1) for https://deno.land/x/oak/mod.ts Download https://deno.land/x/oak@v9.0.1/mod.ts
對于生產應用程序,Deno 創建者建議檢查您的依賴性已進入代碼版本控制系統,以確保持續可用(即時模塊的源代碼不可用,無論處于何原因)。將 DENO_DIR 環境變量指向項目中的本地目錄(例如 vendor),您可以將其提交給 Git。
例如,下面的命令會將腳本的所有依賴項下載到項目中的 vendor 目錄中。隨后,您可以提交該文件夾以在您的生產服務器中一次將其全部拉下。您還需要將 DENO_DIR 變量設置為從服務器上的 vendor 目錄中讀取,而不是重新下載它們。
$ DENO_DIR=$PWD/vendor deno cache index.ts # Linux and macOS $ $env:DENO_DIR="$(get-location)\vendor"; deno cache index.ts # Windows PowerShell
Deno 還支持對依賴項進行版本控制,以確保可重現的構建。目前,我們已經從 https://deno.land/x/oak/mod.ts 導入了 Oak。這始終會下載最新版本,將來可能與您的程序不兼容。當你第一次下載模塊時,它還會導致 Deno 產生警告:
Warning Implicitly using latest version (v9.0.1) for https://deno.land/x/oak/mod.ts
最佳實踐是引用一個特定版本,如下所示:
import { Application } from 'https://deno.land/x/oak@v9.0.1/mod.ts';
如果您在代碼庫的許多文件中引用一個模塊,升級它可能會變得乏味,因為您必須在許多地方更新 URL。為了規避這個問題,Deno 團隊建議將您的外部依賴項導入到一個集中的 deps.ts 文件中,然后重新導出它們。這是一個 deps.ts 示例文件,它從 Oak 庫中導出我們需要的內容。
export { Application, Router } from "https://deno.land/x/oak@v9.0.1/mod.ts";
然后在您的應用程序代碼中,您可以按如下方式導入它們:
import { Application, Router } from "./deps.ts";
此時,更新一個模塊變得很簡單,只需將 deps.ts 文件中的 URL 更改為指向新版本即可。
標準庫
Deno 提供了標準庫(stdlib)旨在成為 Go 標準庫的松散端口(譯者注:這里原句為 “aims to be a loose port of Go's standard library”)。標準中包含的模塊由 Deno 團隊審核并隨著每一次 Deno release 而更新。提供標準庫的目的是讓您立即創建有用的 Web 應用程序,而不需要任何第三方包(這是 Node.js 生態系統中的規范)。
你可能會發現一些有幫助的第三方模塊,示例如下:
- HTTP:Deno 的 HTTP 客戶端和服務端實現。
- Fmt:包括用于打印格式化輸出的助手。
- Testing:提供了用于代碼測試和基準測試的基本實用工具。
- FS:文件系統。
- Encoding:提供處理各種文件格式的助手,例如 XML、CSV、base64、YAML、二進制等。
- Node: Node.js 標準可兼容層。
這是一個示例(取自 Deno 官方文檔)它取自 Deno 標準庫中的 HTTP 模塊用于創建一個基本的 Web Server。
import { listenAndServe } from "https://deno.land/std@0.109.0/http/server.ts"; const addr = ":8080"; const handler = (request: Request): Response => { let body = "Your user-agent is:\n\n"; body += request.headers.get("user-agent") || "Unknown"; return new Response(body, { status: 200 }); }; console.log(`HTTP webserver running. Access it at: http://localhost:8080/`); await listenAndServe(addr, handler);
通過以下命令開啟這個服務:
$ deno run --allow-net index.ts HTTP webserver running. Access it at: http://localhost:8080/
在不同終端,通過以下命令訪問這個正在運行的服務。
$ curl http://localhost:8080 Your user-agent is: curl/7.68.0
請注意,標準庫中的模塊當前標記為不穩定(如版本號所示)。這意味著您不應該僅僅依賴它們來進行嚴肅的生產應用程序。
在 Deno 中使用 NPM
不可否認 Node.js 成功的主要原因是可以在項目下載使用大量的 NPM 包。如果你正在考慮切換到 Deno,你也許想知道是否要放棄所有你知道且喜愛的 NPM 包。
最簡單的答案是 “不”。如果某些 NPM 包依賴于 Node.js API,您可能無法在 Deno 中使用它們(特別是如果 Deno 的 Node.js 兼容層不支持特定 API),但許多 NPM 包可以通過 CDN 在 Deno 中使用,例如 esm.sh 和 skypack.dev。這兩個 CDN 都將 NPM 包作為 ES 模塊提供,即使包的作者沒有專門針對 Deno 進行設計,也可以隨后在 Deno 腳本中使用這些包。
這是一個在 Deno 腳本中從 Skypack 到入 NPM 包的示例:
import dayjs from "https://cdn.skypack.dev/dayjs@1.10.7"; console.log(`Today is: ${dayjs().format("MMMM DD, YYYY")}`);
$ deno run index.ts Today is: October 05, 2021
為確保 Deno 可以發現與包關聯的類型,請確保在使用 Skypack CDN 時在包 URL 的末尾添加 ?dts后綴。這會使 Skypack 設置 X-TypeScript-Types 標頭,以便 Deno 可以自動發現與包關聯的類型。Esm.sh 默認包含此標頭,但您可以通過在包 URL 末尾添加 ?no-check 后綴來選擇退出。
import dayjs from "https://cdn.skypack.dev/dayjs@1.10.7?dts";
工具
Deno CLI 附帶了幾個有價值的工具,可以提高開發人員的使用體驗。與 Node.js 一樣,它帶有一個 REPL(交互式解析器),您可以使用 deno repl 訪問它。
$ deno repl Deno 1.14.2 exit using ctrl+d or close() > 2+2 4
它還有一個內置的文件觀察器,可以與它的幾個子命令一起使用。例如,您可以使用 --watch 標志將 deno run 配置為在文件更改后自動重建和重新啟動程序。在 Node.js 中,這個功能一般是通過一些第三方包來實現的,比如 nodemon。
$ deno run --allow-net --watch index.ts HTTP webserver running. Access it at: http://localhost:8080/ Watcher File change detected! Restarting! HTTP webserver running. Access it at: http://localhost:8080/
使用 Deno 1.6,您可以通過 compile 子命令將腳本編譯為不需要安裝 Deno 的獨立可執行文件(您可以在 Node.js 中使用 pkg 執行相同操作)。您還可以通過--target 標志為其他平臺(交叉編譯)生成可執行文件。編譯腳本時,您必須指定其運行所需的權限。
$ deno compile --allow-net --output server index.ts $ ./server HTTP webserver running. Access it at: http://localhost:8080/
請注意,通過此過程生成的二進制文件非常龐大。在我的測試中,deno compile 為一個簡單的 “Hello world” 程序生成了一個 83MB 的二進制文件。然而,Deno 團隊目前正在努力減少文件大小以使其更易于管理。
發布 Deno 程序的另一種方法是通過 bundle 子命令將其打包到單個 JavaScript 文件中。該文件包含程序的源代碼及其所有依賴項,可以通過 deno run 執行,如下所示:
$ deno bundle index.ts index.bundle.js Check file:///home/ayo/dev/demo/deno/index.js Bundle file:///home/ayo/dev/demo/deno/index.js Emit "index.bundle.js" (7.39KB) $ deno run --allow-net index.bundle.js HTTP webserver running. Access it at: http://localhost:8080/
Deno 附帶的另外兩個很棒的工具是內置的 linter (deno lint) 和 formatter (deno fmt)。在 Node.js 生態系統中,linting 和格式化代碼通常分別由 ESLint 和 Prettier 處理。
使用 Deno 時,您不再需要安裝任何東西或編寫配置文件來獲取 JavaScript、TypeScript 和其他支持的文件格式的 linting 和格式化。
單元測試
Deno 內置了對 JavaScript 和 TypeScript 代碼的單元測試支持。當您運行 deno test 時,它會自動檢測任何以 _test.ts 或 .test.ts 結尾的文件(也支持其他文件擴展名)并在其中執行任何定義的測試。
要編寫您的第一個測試,請創建一個 index_test.ts 文件并使用以下代碼填充它:
import { assertEquals } from "https://deno.land/std@0.109.0/testing/asserts.ts"; Deno.test("Multiply two numbers", () => { const ans = 2 * 2; assertEquals(ans, 4); });
Deno 提供了用于創建單元測試的 Deno.test 方法。它將測試的名稱作為其第一個參數。它的第二個參數是測試運行時執行的函數。
還有第二種風格接受一個對象而不是兩個參數。除了測試名稱和功能之外,它還支持其他屬性來配置測試是否應該運行或如何運行。
Deno.test({ name: "Multiply two numbers", fn() { const ans = 2 * 2; assertEquals(ans, 4); }, });
assertEquals() 方法來自標準庫中的測試模塊,它提供了一種輕松檢查兩個值是否相等的方法。
繼續運行測試:
$ deno test test Multiply two numbers ... ok (8ms) test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (37ms)
Deno 語言服務協議
選擇編程語言或環境的主要考慮因素之一是它與編輯器和 IDE 的集成。在 Deno 1.6 中,內置語言服務協議(deno lsp) 被添加到運行時以提供以下功能:
- 代碼自動補全
- 跳轉到定義處
- 代碼 lint 和格式化集成
以及任何支持語言服務協議 (LSP) 的編輯器的其他語言智能。你可以在 Deno 的在線文檔中了解有關在編輯器中設置 Deno 支持的更多信息。
總結:Deno 還是 Node.js ?
在本文中,我們考慮了 Deno 運行時的許多方面,以及它是對 Node.js 的升級的方式。
關于 Deno 及其生態系統還有很多話要說,但對于考慮將 Deno 用于新項目的 Node.js 開發人員來說,這應該是一個有用的介紹。Deno 第三方軟件包的可用性較低是它明顯不足的一個方面,因為它在現實世界中由于年齡太小而沒有像 Node.js 那樣經過實戰測試(Deno 1.0 于 2020 年 5 月發布)。
比較 Node.js 和 Deno 之間的性能可以發現,在大多數情況下,它們在同一個范圍內,盡管在少數情況下 Node.js 表現出更出色的性能。隨著 Deno 變得更加成熟,所測得的差異勢必會有所改善。
在選擇 Node.js 和 Deno 時,同樣重要的是要記住,Deno 提供的一些好處也可以在第三方包的幫助下在 Node.js 中使用。因此,如果您對 Deno 只有一兩點欣賞,那么您很可能在 Node.js 中也可獲得類似的結果,盡管不是那么無縫。
譯者:五月君
作者:ayooluwa-isaiah
https://blog.appsignal.com/2022/02/09/an-introduction-to-deno-is-it-better-than-nodejs.html
原文地址:https://mp.weixin.qq.com/s/FUIaBBv5-lc6OZcFDbc9Cg