一文讀懂 Deno
在 Deno 之前,已經存在了 Node.js,但是由於 Node.js 中有很多難以解決的問題,所以 Ryan Dahl 決定重新開發一個全新的 runtime,並且解決掉之前存在的所有問題。所以我們可以把 Deno 看作是 Node.js 的升級版。
Deno 可以用更快、更安全的方式去做和 Node.js 相同的事情。
Node.js 非常成功,用戶呈指數級增長,但是這些都將會成爲 Dahl 改進 Node.js 的障礙。從一個乾淨的基礎開始,Dahl 可以使用很多創新技術和最佳架構實踐。 Deno 組成
Deno 構建在 JavaScript 的 V8 引擎和 Rust 的 Tokio 之上。 V8 引擎
V8 是 Google 開源的高性能 JavaScript 和 WebAssembly 引擎,用 C++ 編寫。目前主要用於 Chrome 和 Node.js。
他在真正執行之前會編譯並執行 JavaScript 來優化機器代碼。雖然最初設計只是爲了執行瀏覽器腳本,但是最新的版本已經允許服務端腳本。 Rust
Deno 最初是用 Go 來編寫的,但是由於性能問題和缺乏垃圾回收,很快就用 Rust 重寫了。Rust 允許我們將數據存儲在棧或堆上,並不需要在編譯時再進行分配。這種方法確保了訪問內存的高效,消除了對持續運行的垃圾回收的需求。通過直接訪問硬件,Rust 成爲了底層開發的最佳理想語言,在很多領域取代了 C++。除了技術方面,Rust 社區也非常活躍,我們可以很容易找到大量資料來學習 Rust。 Tokio
Tokio 上 Rust 的異步 runtime。它對 Deno 的意義,就像 libuv 對於 Node.js 的意義。由於使用多線程來調度程序,它可以用最小的開銷提供卓越的性能。Tokio 還有一組內存安全的 API,幫助我們防止和內存相關的錯誤。這些功能都爲 Deno 提供了堅實可靠的基礎。 Deno 的基礎功能
Deno 是用 Typescript 和 Rust 來編寫的,這兩門編程語言都是廣泛使用的語言,可以提供許多優勢來創建快速和高性能的應用程序。
下面是 Deno 的一些功能列表:
-
原生支持 Typescript,同時仍然可以使用 JavaScript。
-
支持 ES Module。
-
擁有非常多的庫。
-
沒有標準的包管理器,直接通過 URL 下載庫。
-
提供完整的內置工具,包括 bundling、debugging、testing 等。* 擁有明確的權限系統,默認情況下非常安全。 Deno 的核心功能
Deno 和 Node.js 的主要區別在於 Deno 背後的核心功能。它提供了豐富的功能來保障改進和流暢的開發體驗。
Deno 的目標是:Deno aims to be a productive and secure scriptin environment for the modern programmer.(Deno 旨在爲現代程序員提供高效且安全的腳本環境) 安裝在學習 Deno 的核心功能之前,我們需要先來安裝一下 Deno,以便運行一些示例代碼。
安裝 Deno 的方式非常簡單,下面是不同操作系統的安裝方式。 Mac、Linuxbash 方式是最通用的。
curl -fsSL https://deno.land/install.sh | sh 複製代碼
如果你有 Homebrew,也可以這樣。
brew install deno 複製代碼
Windows
Windows 推薦使用 PowerSehll 來安裝。
iwr https://deno.land/install.ps1 -useb | iex 複製代碼
安全
安全是 Deno 的一個核心功能。 沙盒安全層
默認情況下,任何 Deno 模塊都在沙盒安全層中執行,防止訪問磁盤、環境、網絡或者運行任何外部腳本的可能性,除非通過權限明確允許。在使用命令行運行代碼時,我們通過使用一些 flag 向程序授予權限。 示例
我們通過一個簡單的例子來更好地理解權限是如何工作的。
下面的命令作用是:執行一個遠程腳本文件 cat.ts,並授予它對目標文件的讀取權限。deno run --allow-read https://deno.land/std@0.123.0/examples/cat.ts test.txt 複製代碼
cat.ts 是 Deno 標準庫的一部分,我們可以直接從終端調用它。它接受一個或多個文件名作爲參數,並且打印它們的內容,以下是 cat.ts 的源碼:
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. import { copy } from "../streams/conversion.ts"; const filenames = Deno.args; for (const filename of filenames) { const file = await Deno.open(filename); await copy(file, Deno.stdout); file.close(); } 複製代碼
下面我們來解釋一下它關鍵的部分。
第 3 行:通過 Deno.args 訪問命令行中的參數,並賦值給 filenames。
第 4-8 行:通過循環遍歷所有的文件,並調用 Deno.open 方法打開文件,並調用 copy 將文件內容打印到 Deno.stdout。最後調用 file.close 方法關閉文件。
如果將 --allow-read 這個 flag 去掉,那麼就會在第 5 行拋出一個錯誤。因爲 Deno 默認是安全的,它要求我們必須提供權限。
Deno 的 flag 必須放在腳本名稱之前,否則它會被解析成腳本參數。 緩存模塊與 Node.js 不同的是,Deno 不使用 npm,而是使用 URL 來導入模塊。在使用 URL 導入模塊時,Deno 會下載模塊並將它換存在由環境變量定義的 DENO_DIR 中。默認值爲系統的緩存目錄,即 $HOME/.cache/deno。模塊被緩存之後,在之後每次執行過程中,Deno 都會先去緩存中查找請求的模塊是否存在,如果找到模塊,就會直接使用這個模塊,而不需要進行網絡拉取。當我們處於沒有網絡的開發環境下,這個方式將會非常有用。
現在嘗試重新運行上面的命令,可以看到它沒有再次下載 cat.ts 文件。
我們可以通過添加 --reload flag 來讓 Deno 重新通過網絡獲取模塊。 權限 flagDeno 的權限 flag 提供了非常好的粒度級別,在需要的時候可以縮小授權範圍。
比如我們可以通過 --allow-read=/folder 這個 flag 來限制對目標文件夾的讀取訪問。 完整的權限列表- -A/--allow-all:允許所有權限。
- --allow-net:允許訪問網絡。我們可以通過逗號分隔域名或 IP 制定可以訪問的網絡列表。單獨使用 --allow-net 表示可以訪問所有域名或 IP。
deno run --allow-net=github.com,gitlab.com code.js 複製代碼
-
--allow-env:允許讀寫環境變量。我們可以通過都好分割指定可以讀寫的環境變量。
-
--allow-hrtime:允許高分辨率時間測量。
-
--allow-read:允許文件系統讀取權限。可以通過逗號分隔授予多個文件或目錄讀取權限。
-
--allow-write:允許文件系統寫入權限。可以通過逗號分隔授予多個文件或目錄寫入權限。
-
--allow-run:允許允許子進程。但子進程不在沙箱中運行,子進程沒有安全限制。* --allow-ffi:允許加載動態庫。但是不在沙箱中運行。 TypeScript 支持
Deno 支持使用 JavaScript 或者 TypeScript 編寫 Deno 程序,但無論使用哪種方式,它都會自動編譯 TypeScript。
TypeScript 有很多好處,比如類型檢查和代碼提示等。使用 TypeScript 可以讓我們規避很多低級錯誤。 ES Module 支持Deno 可以從遠程 URL 或者本地路徑來導入 ES 模塊。所以不需要包管理器來導入遠程模塊。這樣意味着我們少了一個需要關心的技術組件。
加載遠程模塊示例:import { serve } from 'https://deno.land/std/http/server.ts' 複製代碼
加載本地模塊示例:
import { concat, split } from './utils/string-util.ts' 複製代碼
無論採用哪種方式導入文件,都需要提供完整的文件名,包括擴展名。因爲 Deno 的模塊解析有點類似於瀏覽器的模塊解析。
在導入遠程模塊時,如果不置頂特定版本,那麼就會下載最新版本的模塊。如果需要特定版本,可以在模塊路徑中添加指定版本。// 導入最新版本 // 不鼓勵使用這種方式 import { serve } from 'https://deno.land/std/http/server.ts' // 顯式指定了 0.65.0 版本 import { serve } from 'https://deno.land/std@v0.65.0/http/server.ts' const s = serve({ port: 8000 }); 複製代碼
因爲 Deno 採用分散的方式獲取這些庫,所以在 Deno 的項目中我們不需要 package.json 文件和 node_modules 文件夾。 顯示緩存位置
我們可以使用 deno info 命令來查看不同的緩存位置。
我們還可以使用 deno info TARGET_MODULE 來獲取和目標模塊相關的依賴模塊列表。當我們的項目變得龐大時,可以使用這個命令來確保不會錯誤的導入模塊。
我們可以使用下面的命令來測試。deno info https://deno.land/std@0.67.0/http/server.ts 複製代碼
分組導入
在多個文件中導入相同的遠程模塊會顯得重複而且低效。
Deno 中使用了類似桶文件的概念。桶文件提供一種將多個模塊的導出彙總到單個模塊的方法。Deno 使用 deps.ts 當作約定名,而不是 index.ts。
這種想法是將導入分組到一個集中的文件中,再根據需要將依賴導入到不同的文件中。因此,具有多個模塊的單個導入,而不是各種導入。
如果我們在導入中針對特定版本,這是一個方便的習慣。因爲我們只需要在一個地方更新版本號,確保在任何地方都是用相同的模塊版本。
下面是展示桶文件的代碼示例:
deps.tsexport { serve } from 'https://deno.land/std@v0.65.0/http/server.ts' // <- Change only here for a new version export { Cookie } from 'https://deno.land/std@v0.65.0/http/cookie.ts' export { SEP } from 'https://deno.land/std@v0.123.0/http/cookie.ts' 複製代碼
file1.ts
import { Cookie, serve, SEP } from './deps.ts'; const myCookie: Cookie; const currentSeparator = SEP; const s = serve({ port: 8000 }); 複製代碼
file2.ts
import { Cookie } from './deps.ts'; const myCookie: Cookie; 複製代碼
file3.ts
import { serve } from './deps.ts'; import { dateMod } from './utils/date-util.ts'; 複製代碼
遵循這種編碼方式,讓代碼維護更加容易。
如果我們需要將某個模塊的版本進行更新,只需要修改 depts.ts 中對應的一行代碼即可。而不需要修改多個文件的導入。
如果我們使用本地模塊來替換遠程模塊,我們可以只修改 depts.ts 中的導入路徑即可。只要導出的名稱和簽名與之前保持一致,就不會造成任何影響。 URL 導入的潛在風險從遠程 URL 中導入模塊會帶來潛在風險。比如存儲遠程模塊代碼的服務器可能會出現問題或被攻擊而不可用。
在這種情況下,Deno 的開發團隊建議將包含緩存模塊的文件夾添加到源代碼管理中。這樣即使遠程存儲庫出現問題,我們也可以確保所有的庫都可用。 頂級 await在 ECMAScript 中,async/await 可以讓我們通過更清晰、更易讀的方式來編寫異步代碼,而不需要顯式使用 Promise 的鏈式 then。
關鍵字 await 可以讓代碼停止執行,直到它的目標 Promise 被 reject 或 resolve 後才恢復執行。但是我們只能在 async 中使用它,否則會得到一個錯誤。 Deno 中的頂級 awaitDeno 支持頂級 await。
這意味着我們可以在 async 函數之外使用 await 關鍵字。
下面是示例:try { const decoder = new TextDecoder("utf-8"); const data = await Deno.readFile("README.md"); console.log(decoder.decode(data)); } catch (e) { console.error("An error occurrred while reading the file: ", e); } 複製代碼
不過需要注意,頂級 await 只能用於模塊。 Deno 庫
標準庫
Deno 的官方模塊由核心團隊提供支持。這些模塊就是標準庫。
標準庫涵蓋了大多數項目中常用的功能,並且在每個版本中都會擴展。
目前常見的有:-
Async:異步任務
-
bytes:字節切片操作
-
datetime:日期 / 時間
-
flags:解析命令行 flag
-
fs:文件系統
-
http:HTTP 客戶端 / 服務器功能
-
io:I/O 操作
-
path:路徑操作* wasi:WebAssembly 接口 導入模塊
我們應該使用特定版本的模塊,而不是最新版本的模塊。因爲最新版本可能是不穩定的,並且可能帶來意想不到的錯誤和副作用。
最佳實踐是始終導入特定版本來確保穩定性。 不穩定的 API並不是標準庫中的所有模塊都是穩定狀態,這意味着其中一些模塊會使用不穩定的 Deno API。
如果我們使用依賴了不穩定 API 的標準庫中的模塊,我們需要添加 --unstable flag 來防止在調用腳本時出現 TypeScript 錯誤。 第三方庫除了官方的標準庫外,deno.land/x 還提供了社區開發的庫的託管服務。
但是這些庫都未經 Deno 團隊的審覈。所以在使用第三方庫時應該小心。 測試測試可以確保我們的程序在部署到生產環境之前按照預期來工作,即使是在一些特殊情況下也應該如此。
單元測試可以幫助我們在開發過程中保持代碼的高質量,同時可以幫助我們檢測意外的副作用。
Deno 內置了測試運行器,所以編寫測試非常簡單。我們不需要引入任何第三方庫,或者編寫一堆配置。我們可以隨時開始編寫我們的測試代碼。 命名約定測試文件的命名必須滿足以下三點之一:
- 直接命名 test.ts
- 以 .test 結尾,比如 user.test.ts
- 以 _test 結尾,比如 user_test.ts
我們使用 deno test 命令來運行測試。
這條指令可以制定一個跟路徑路徑,測試運行器會以遞歸的方式在路徑中搜索和執行所有測試文件。如果不指定路徑,那麼就是運行命令的路徑。 測試風格下面是一段 Deno 的測試代碼。
我通過它展示了單元測試的不同風格。import { assertEquals } from "https://deno.land/std@0.123.0/testing/asserts.ts"; const textToTest = "Test this text!"; // 使用單元測試名字和函數 // 這類似於 Jasmine 的語法 Deno.test("Compact Form Test", () => { assertEquals(textToTest, "Test this text!"); }); // 使用命名函數 Deno.test(function namedFnTest() { assertEquals(textToTest, "Test this text!"); }); // 使用配置對象 Deno.test({ name: "Test definition", fn: () => { assertEquals(textToTest, "Test this text!"); }, }); // 可以添加配置對象作爲第二個參數 Deno.test("Additional Configuration", { permissions: { read: true } }, () => { assertEquals(textToTest, "Test this text!"); }); // 可以添加測試函數作爲第二個參數 Deno.test( { name: "Test Function as Param", permissions: { read: true } }, () => { assertEquals(textToTest, "Test this text!"); }, ); // 可以傳遞配置參數和命名函數作爲參數 Deno.test({ permissions: { read: true } }, function helloWorld6() { assertEquals(textToTest, "Test this text!"); }); 複製代碼
上面的代碼風格和很多主流的 Node.js 測試框架很相似。
我們來分析第一個單元測試(第 7-9 行)。- 第 7 行:我們使用 Deno.test 方法創建單元測試。Compact Form Test 作爲測試標題。
- 第 8 行:我們提供了一個斷言,這就是我們要測試的代碼部分。assertEquals 會比較第一個參數和第二個參數是否相等,會返回一個測試結果,成功或者失敗。
運行單元測試的命令如下(注意需要添加權限的 flag):
deno test --allow-read --allow-net test.ts 複製代碼
過濾測試
給單元測試設置一個有意義的名稱不僅可以理解它的作用,同時還可以使用 --filter flag 在一個組中執行它們。
比如我們開發瞭如下不同的單元測試:Deno.test({ name: "user-creation", fn: testCreation }); Deno.test({ name: "user-account-creation", fn: testAccount1 }); Deno.test({ name: "user-account-deletion", fn: testAccount2 }); Deno.test({ name: "user-account-update", fn: testAccount3 }); Deno.test({ name: "settings", fn: testSettings }); Deno.test({ name: "login", fn: testLogin }); 複製代碼
現在我們只想執行用戶相關的單元測試,就可以使用 --filter 來和測試名稱進行匹配。
deno test --filter "user" test.ts 複製代碼
如果我們只能執行用戶測試的子集怎麼辦?
Deno 還可以接受正則表達式作爲 filter 的參數進行匹配特定的測試。
比如我們要運行所有和賬戶相關的測試,可以使用下面的命令:$ deno test --filter "/user-account-\*d/" test.ts 複製代碼
單個測試
與 Jasmine 一樣,Deno 也提供了一種機制來臨時跳過所有其他測試,只運行一個測試。
在配置對象中設置 only 屬性爲 true,就可以讓測試運行器跳過其他所有測試,只運行這一個測試。Deno.test({ name: 'critical test', only: true, fn() { testComplexFeature() } }) 複製代碼
只要在任何測試中存在 only 選項,無論這個單元測試是否成功,整個測試都會失敗。因爲 only 是一種臨時方案,這種做法可以防止我們忘記刪除 only 選項。 斷言
斷言可以幫助我們來定義測試需要滿足的要求。Deno 內置了豐富的斷言庫,我們只需要從 deno.land/std@VERSION… 模塊中導入它們。
下面是一些常用的斷言方法:- assert:參數爲布爾值,true 爲成功,false 爲失敗
- assertEquals:兩個值相等
- assertNotEquals:兩個值不相等
- assertExists:驗證一個值不是 null 或 undefined
- assertStrictEquals:嚴格比較兩個值是否相等,對於非原始值,會比較引用
- assertStringIncludes:實際字符串中是否包含預期字符串
- assertArrayIncludes:在數組中查找一個值
- assertMatch:參數是否和正則表達式匹配
- assertNotMatch:參數不與正則表達式匹配
- assertObjectMatch:參數對象是否和預期對象的屬性匹配
- assertThrows:預期傳遞的函數拋出異常
- assertRejects:和 assertThrows 類似,但它需要返回一個 Promise 對象
-
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://juejin.cn/post/7126544389484052494