做者:ROBIN WIERUCH翻譯:瘋狂的技術宅javascript
原文:https://www.robinwieruch.de/D...前端
未經容許嚴禁轉載java
Deno 是新的 JavaScript 和 TypeScript 運行時。Node.js 的發明者 Ryan Dahl 於 2020 年發佈了 Deno,做爲 Node.js 的改進。可是 Deno 不是 Node.js,而是全新的 JavaScript 運行時,同時也支持 TypeScript。與 Node.js 類似,Deno 可用於服務器端 JavaScript,但其目的是消除 Node.js 所犯的錯誤。它就像 Node.js 2.0 同樣,只有時間才能告訴咱們是否會像 2009 年使用 Node.js 同樣去使用它。node
Node(2009)和 Deno(2020)的發明者 Ryan Dahl 發佈了 Deno 做爲 JavaScript 生態系統的補充。當 Ryan 在會議上第一次宣佈 Deno 時,他談到了 Node.js 中的錯誤。Node.js 已經成爲 JavaScript 生態中不可或缺的工具,已被數百萬人使用,可是 Ryan Dahl 對當時作出的決定感到不滿。如今 Ryan Dahl 但願經過 Deno 解決 Node 的設計缺陷。 Deno 是由 V8 JavaScript 引擎、Rust 和 TypeScript 實現的用於安全服務器端的 JavaScript 和 TypeScript 全新運行時。git
如下各節將詳細介紹全部這些要點,同時從頭開始逐步實現一個小的 Deno 程序。以後咱們將繼續用 Deno 開發真實的 Web 應用。程序員
有多種方法來設置 Deno 應用程序。對你而言,這取決於你的操做系統和在計算機上安裝程序的工具鏈。例如我在 MacOS 上用 Homebrew 來管理計算機上的程序。對於你來講,可能還有其餘選擇,因此你應該從 Deno 網站獲取的這個方法列表中爲你的計算機使用適當的命令。這些命令應在集成終端或命令行界面中執行:golang
# Shell (Mac, Linux): curl -fsSL https://Deno.land/x/install/install.sh | sh # PowerShell (Windows): iwr https://Deno.land/x/install/install.ps1 -useb | iex # Homebrew (Mac): brew install Deno # Chocolatey (Windows): choco install Deno # Scoop (Windows): scoop install Deno # 用 Cargo 從源碼構建並安裝 cargo install Deno
安裝 Deno 後,能夠在命令行上驗證其安裝。你的版本可能比個人版本新,由於就我而言,我安裝了 Deno 的第一個發行版本 1.0.0。可是如下各節將假定你安裝了最新的 Deno 版本:面試
Deno --version -> Deno 1.0.0
若是要升級Deno的版本,可使用 Deno upgrade
。另外還能夠經過命令行執行下面的遠程 Deno 程序,來驗證 Deno 在你的計算機上是否可以正確運行:編程
Deno run https://Deno.land/std/examples/welcome.ts -> Welcome to Deno
這個 Deno 程序只是在你的命令行上輸出一段文本。可是它還向你展現瞭如何經過動態下載和編譯 Deno 程序來從遠程源執行該程序。若是你沒法在計算機上設置 Deno,請按照 Deno 官方網站 上的安裝說明進行操做。json
每次咱們學習新的編程語言知識時,都從 「Hello World」 示例開始。讓咱們的第一個 Deno 應用程也從這裏開始。在命令行中,爲你的 Deno 項目建立一個文件夾,進入到該文件夾,並建立一個新文件。你本身決定如何命名文件夾和文件:
mkdir Deno-project cd Deno-project touch index.js
而後在你喜歡的編輯器或 IDE 中打開新建立的 index.js 文件。輸入如下 JavaScript 代碼:
console.log('Hello Deno');
而後在命令行經過如下命令啓動 Deno 程序。
Deno run index.js -> Hello Deno
你的第一個 Deno 程序輸出 「Hello Deno」。你已經爲 Deno 項目建立了一個文件夾,爲實現細節建立了一個 JavaScript 文件,並在命令行上經過 Deno 運行了該文件。無需其餘設置。
如下各節將經過逐步介紹 Deno 的每一個方面,來改進咱們的第一個 Deno 程序。本節將討論 Deno 中的權限,由於 Deno 在默認狀況下是安全的。在本示例中,咱們將瞭解這究竟意味着什麼。
若是你想像我同樣隨時瞭解技術主題,你可能已經知道 Hacker News。你能夠在這個網站上閱讀有關技術的最新新聞。我喜歡在本身的教程中使用 Hacker News 的 API。爲了學習有關 Deno 和權限中的數據獲取的知識,咱們將用這個 API 來獲取數據。若是瀏覽 Hacker News API,則可以找到如下 URL 來請求有關某個主題下的文章:
http://hn.algolia.com/api/v1/search?query=...
咱們將在 Deno 項目的 index.js 文件中使用此URL,來獲取有關 JavaScript 的 Hacker News 文章:
const url = 'http://hn.algolia.com/api/v1/search?query=javascript';
接下來,用 Deno 內置的 fetch 函數處理 URL,該函數在 URL 上執行 HTTP GET 請求,並返回 JavaScript promise。你能夠經過將其轉換爲 JSON 並用日誌記錄語句輸出其結果來解決這個 promise:
const url = 'http://hn.algolia.com/api/v1/search?query=javascript'; fetch(url) .then((result) => result.json()) .then((result) => console.log(result.hits));
若是你用 JavaScript 寫過前端程序 ,則可能已經注意到,咱們所使用的瀏覽器 API 爲客戶端程序提供了相同的 fetch API(或至少使用相同實現細節的接口)。如前所述,Deno 嘗試與 Web 兼容,而且任何 Deno 程序在執行其代碼時都應該可以在瀏覽器中以相同的方式工做。所以 Deno 確保客戶端 JavaScript 程序中可用的 API 也能夠在服務器端 Deno 應用程序中使用。
如今,在命令行上再次啓動 Deno:
Deno run index.js
你應該會看到 Deno 提示的錯誤:「Uncaught PermissionDenied: network access to "http://hn.algolia.com/api/v1/search?query=javascript", run again with the --allow-net flag」。出現這個錯誤的緣由是,在默認狀況下 Deno 是安全的。若是咱們在 Deno 的域中操做,能夠無需授予Deno任何許可而作不少事情而。可是若是咱們想超越 Deno 的職責範圍,則須要明確容許它。在這種從遠程 API 獲取數據的狀況下,須要容許網絡請求:
Deno run --allow-net index.js
再次運行 Deno 程序後,你應該在命令行上看到一系列 Hacker News 文章。這個數組中的每一個項目都有許多信息,爲了便於閱讀,讓咱們精簡每一個項目(文章)的屬性。以後輸出會應更具可讀性:
const url = 'http://hn.algolia.com/api/v1/search?query=javascript'; fetch(url) .then((result) => result.json()) .then((result) => { const stories = result.hits.map((hit) => ({ title: hit.title, url: hit.url, createdAt: hit.created_at_i, })); console.log(stories); });
在本節中,你瞭解了 Deno 在默認狀況下是安全的。咱們必須容許本身可以訪問 Deno 領域之外的全部內容,多是網絡訪問或文件訪問,不然 Deno 將會拒絕工做。
前面你已經看到了怎樣在 Deno 中使用 fetch。咱們對瀏覽器中的 fetch API 是很熟悉的。因此在Deno 中能夠用與瀏覽器端徹底相同的接口,而沒必要爲 Deno 使用新的 API。在使用 Deno 時咱們不須要從新考慮本身的方法。
Deno 嘗試跟上現代 JavaScript 功能,不管是在客戶端仍是在服務器上。以 async/await 爲例,它僅在較新的 Node.js 版本中可用,默認狀況下在 Deno 中是可用的。你不只可使用 async/await,並且還可使用 async 的 top level await(這在 Node.js 中已經存在很長時間了):
const url = 'http://hn.algolia.com/api/v1/search?query=javascript'; const result = await fetch(url).then((result) => result.json()); const stories = result.hits.map((hit) => ({ title: hit.title, url: hit.url, createdAt: hit.created_at_i, })); console.log(stories);
做爲常規 promise 的 then 和 catch 塊的代替,能夠用 await 同步運行代碼。在 await 語句以後的全部代碼僅在 promise 解決後執行。若是這種實現要在函數中運行,則必須把函數聲明爲異步。開箱即用的 Deno 中提供了 Async/await 和 top level await。
Deno 帶有一組實用函數,這些函數被稱爲 Deno 的標準庫(簡稱:Deno std)。 Deno 並無從外部庫中導入全部內容,而是嘗試經過提供幾種內部解決方案來使其可用。接下來咱們嘗試用下面的標準庫解決方案之一來設置 Web 服務器:
import { serve } from 'https://Deno.land/std/http/server.ts'; const server = serve({ port: 8000 }); for await (const req of server) { req.respond({ body: 'Hello Deno' }); }
首先咱們用絕對路徑從標準庫中進行命名導入。在 Deno 中,全部庫導入(不管是從標準庫仍是從第三方庫)均使用指向專用文件的絕對路徑來完成。你從這個 以服務器文件形式存在的 http 庫 導出一個名爲served
的函數。
serve
函數爲咱們建立了一個 Web 服務器,可經過已定義的端口對其進行訪問。 JavaScript for await ... of 用於遍歷每一個傳入此服務器的請求。對於每一個請求,服務器在響應正文中返回相同的文本。
再次運行你的 Deno 程序,而後在瀏覽器中導航到 http://localhost:8000 。由於要再次使用網絡,因此須要受權:
Deno run --allow-net index.js
http://localhost:8000 和帶有結尾斜槓的 http://localhost:8000/ 這兩個 URL 在瀏覽器中的工做方式相同。當在瀏覽器中打開其中一個 URL 時,都會向 Deno 程序發出 HTTP GET 請求,而且該請求返回帶有 Hello Deno
正文的 HTTP 響應,而後該響應將顯示在瀏覽器中。
接下來用前面的代碼擴展該示例。咱們不會從服務器(Deno)上將硬編碼文本發送回客戶端(瀏覽器),而是從 Hacker News 獲取最重要的 JavaScript 文章並將其發送給客戶端:
import { serve } from 'https://Deno.land/std/http/server.ts'; const url = 'http://hn.algolia.com/api/v1/search?query=javascript'; const server = serve({ port: 8000 }); for await (const req of server) { const result = await fetch(url).then((result) => result.json()); const stories = result.hits.map((hit) => ({ title: hit.title, url: hit.url, createdAt: hit.created_at_i, })); req.respond({ body: JSON.stringify(stories) }); }
再次啓動 Deno 程序後,應該可以看到從 fetch 請求中獲得的結果以 JSON 的形式打印在瀏覽器中。之前咱們只在命令行上看到這個結果,可是如今有了 Web 服務器,能夠將結果發送到客戶端(瀏覽器)。
只依賴 Deno 的標準庫還不足以建立 Deno 程序,這就須要第三方庫(也稱爲外部庫或庫)發揮做用了。若是你再次從瀏覽器的最後一部分中檢查結果,可能會注意到 createdAt
的格式對人類很不友好,咱們將用 date-fns 庫來使其可讀:
Deno 中的庫經過絕對路徑直接從 Web 導入。在瀏覽器中再次打開 URL,並閱讀其中的源代碼,並檢查它是否真的導出了默認函數,即此處的 format
函數:
import { serve } from 'https://Deno.land/std/http/server.ts'; import format from 'https://Deno.land/x/date_fns/format/index.js'; const url = 'http://hn.algolia.com/api/v1/search?query=javascript'; const server = serve({ port: 8000 }); for await (const req of server) { const result = await fetch(url).then((result) => result.json()); const stories = result.hits.map((hit) => ({ title: hit.title, url: hit.url, createdAt: format( new Date(hit.created_at_i * 1000), 'yyyy-MM-dd' ), })); req.respond({ body: JSON.stringify(stories) }); }
format
函數有兩個強制性參數:日期和格式化日期的模式。咱們從 Hacker News API 收到的日期是一個 unix 時間戳 ,以秒爲單位;因此要先把它轉換爲毫秒,而後再從中建立 JavaScript 日期。爲函數第二個參數提供的模式使日期易於閱讀。
再次啓動 Deno 程序後,你會看到它從庫中下載了 format 函數以及全部依賴項。因爲使用了函數的直接 URL,因此只下載了庫的這一部分。若是試着包含整個庫路徑,將會看到整個庫將被下載, format 只是許多命名的導出(大括號)之一:
import { serve } from 'https://Deno.land/std/http/server.ts'; import { format } from 'https://Deno.land/x/date_fns/index.js'; const url = 'http://hn.algolia.com/api/v1/search?query=javascript'; const server = serve({ port: 8000 }); for await (const req of server) { ... }
當再次啓動 Deno 時,都會使用被緩存的庫,因此無需再次下載。它都會檢查全部的導入,將其下載並捆綁到一個可執行文件中。在 Deno 中導入庫的方式受到 Go 語言 的啓發。沒必要在文件中保留依賴項列表(例如,Node.js 的package.json),也不須要使全部模塊在項目中可見(例如,Node.js 的 node_modules)。
你已經瞭解到 Deno 的標準庫或第三方庫的導入是經過絕對路徑執行的。這種方法的靈感來自 Go 語言,由於它不會產生太多混淆的空間。由於你的 Deno 程序有多個文件,所以能夠用相對路徑導入它們。
來看看它是怎樣工做的:首先,在項目中建立一個名爲 stories.js 的文件,該文件應該與 index.js 文件在同一路徑下。在 stories.js 文件中,輸入如下代碼實現,這段代碼本質上上是咱們以前在其餘文件中所作的映射:
import { format } from 'https://Deno.land/x/date_fns/index.js'; export const mapStory = (story) => ({ title: story.title, url: story.url, createdAt: format( new Date(story.created_at_i * 1000), 'yyyy-MM-dd' ), });
在 stories.js 文件中進行命名導出,並在 index.js 文件中進行命名導入,並在稍後的代碼中使用該函數:
import { serve } from 'https://Deno.land/std/http/server.ts'; import { mapStory } from './stories.js'; const url = 'http://hn.algolia.com/api/v1/search?query=javascript'; const server = serve({ port: 8000 }); for await (const req of server) { const result = await fetch(url).then((result) => result.json()); const stories = result.hits.map(mapStory); req.respond({ body: JSON.stringify(stories) }); }
這就是在 Deno 中導出和導入文件的過程。與以前所用的絕對路徑不一樣,咱們用相對路徑來導入必要的內容。還要注意的是,不管絕對路徑仍是相對路徑,咱們都必須始終包含文件擴展名,由於不能留下任何產生歧義的餘地。
在編程的過程當中,測試不該該過後再去考慮,在 Deno 中也同樣,測試是必不可少的。接下來經過編寫第一個單元測試來了解其工做原理。首先建立一個新的 stories.test.js 文件。而後編寫如下代碼:
import { mapStory } from './stories.js'; Deno.test('maps to a smaller story with formatted date', () => { });
Deno 爲咱們提供了一個 test
功能,用來定義名稱、說明和實際的測函數。怎樣在函數主體中實現測試取決於咱們本身。咱們已經導入了要測試的函數(即 mapStory
),該函數實際上只接收一個文章列表數組,並返回具備較少屬性和格式化日期的新文章數組。咱們須要作的就是定義一個用於 mapStory
的文章列表和一個其假定爲該函數的輸出的文章列表:
import { assertEquals } from 'https://Deno.land/std/testing/asserts.ts'; import { mapStory } from './stories.js'; Deno.test('maps to a smaller story with formatted date', () => { const stories = [ { id: '1', title: 'title1', url: 'url1', created_at_i: 1476198038, }, ]; const expectedStories = [ { title: 'title1', url: 'url1', createdAt: '2016-10-11', }, ]; assertEquals(stories.map(mapStory), expectedStories); });
Deno 的標準庫提供了 assertEquals
函數用來聲明兩個值。第一個值是要測試的函數的輸出,第二個值是預期的輸出。若是二者都匹配,則測試應變爲綠色。若是它們不匹配,則測試應失敗並變爲紅色。在命令行上運行全部測試:
Deno test -> running 1 tests -> test maps to a smaller story with formatted date ... ok (9ms) -> test result: ok. (10ms) -> 1 passed; -> 0 failed; -> 0 ignored; -> 0 measured; -> 0 filtered out
測試變成綠色。用 Deno test
命令將拾取全部具備命名模式 test.{js,ts,jsx,tsx} 的文件。你也能夠用 Deno test <file_name>
僅測試特定文件,在本例中爲 Deno test stories.test.js
。
Deno 支持把 JavaScript 和 TypeScript 同時做爲第一語言。這就是爲何進行文件導入時要始終包含文件擴展名的緣由——不管這些文件是從 Deno 項目的相對路徑導入仍是從 Deno 標準庫或第三方庫絕對路徑導入。
因爲 Deno 把 TypeScript 做爲一等公民提供支持,因此能夠把 stories.js
文件重命名爲 stories.ts
。你會注意到須要調整全部導入——在 index.js 和 stories.test.js 中指向該文件,由於文件擴展名從 .js 被改成了 .ts。
帶有全部實現細節的 stories.ts 文件如今須要類型。咱們將經過 Story
和 FormattedStory
提供函數輸入和輸出的接口:
import { format } from 'https://Deno.land/x/date_fns/index.js'; interface Story { title: string; url: string; created_at_i: number; } interface FormattedStory { title: string; url: string; createdAt: string; } export const mapStory = (story: Story): FormattedStory => ({ title: story.title, url: story.url, createdAt: format( new Date(story.created_at_i * 1000), 'yyyy-MM-dd' ), });
如今這個 function 已徹底類型化了。經過將 stories.test.js 文件重命名爲 stories.test.ts,並將 index.js 文件重命名爲 index.ts,你能夠本身繼續把 JavaScript 轉換爲 TypeScript。這些新的 TypeScript文件並非都須要添加類型或接口,由於大多數類型是自動推導的。
若是要再次啓動 Deno 應用程序,這時必須調整 Deno 腳本的文件擴展名:
Deno run --allow-net index.ts
Deno 帶有默認的 TypeScript 配置。若是要自定義它,能夠添加自定義 tsconfig.json 文件。畢竟,因爲 TypeScript 和 JavaScript 同樣,都是一等的公民,因此由你本身決定爲未來的 Deno 項目選擇哪一種文件擴展名。
環境變量很是適合隱藏有關 Deno 程序的敏感信息。這能夠是 API 密鑰、密碼或他人不該該看到的數據。這就是咱們要經過建立 .env 文件來隱藏敏感信息的緣由。接下來咱們將建立這個文件,並把如下信息傳給服務器程序的端口:
PORT=8000
在 index.ts
文件中,咱們能夠把這個環境變量與第三方庫在一塊兒配合使用:
import { serve } from 'https://Deno.land/std/http/server.ts'; import { config } from 'https://Deno.land/x/dotenv/mod.ts'; import { mapStory } from './stories.ts'; const url = 'http://hn.algolia.com/api/v1/search?query=javascript'; const server = serve({ port: parseInt(config()['PORT']), }); for await (const req of server) { const result = await fetch(url).then((result) => result.json()); const stories = result.hits.map(mapStory); req.respond({ body: JSON.stringify(stories) }); }
config
函數從 .env 文件返回帶有有全部鍵值對的對象。咱們必須將 'PORT'
鍵的值解析爲數字,由於它能夠在對象中做爲字符串使用。如今該信息不會存在於源代碼中,而僅在環境變量文件中可用。
再次啓動 Deno 程序後,你應該在命令行上看到另外一個權限錯誤:"Uncaught PermissionDenied: read access to "/Users/mydspr/Developer/Repos/Deno-example", run again with the --allow-read flag"。能夠用另外一個權限標誌來容許訪問環境變量:
Deno run --allow-net --allow-read index.ts
重要提示:.env 文件不該在每一個人均可以看到的公共存儲庫中共享。若是你將源代碼公開(例如在GitHub上),請考慮將 .env 文件添加到 .gitignore 文件中。
畢竟服務器程序的端口不是敏感數據的最好例子。咱們使用端口是爲了瞭解環境變量。可是一旦你處理了Deno 程序的更多功能,最終可能會獲得源代碼所中使用的信息,這些信息對於其餘人不可見。
本文向你介紹了 Deno 全部的基礎知識。從小型腳本到功能完善的服務器應用,Deno 將在與 Node.js 相同的領域中使用,但其默認設置會大大改善。在默認狀況下,它是權限是安全的,並與許多客戶端的 API 兼容,有着諸如 top level await 等現代功能,並支持 JavaScript 和 TypeScript 。