做者:Flavio Copes翻譯:瘋狂的技術宅javascript
原文:https://www.freecodecamp.org/...html
未經容許嚴禁轉載前端
我每週都會探索一些新的項目,但不多會有像 Deno 這樣吸引個人。java
在本文中,我會讓你快速瞭解 Deno,並把它與 Node.js 進行比較,以此構建你的第一個 REST API。node
Deno 就像 Node,可是在不少方面都獲得了深刻的改善。先從 Deno 功能列表開始:git
await
fetch
和全局 window
對象咱們將在本指南中探索全部這些功能。程序員
在你開始使用 Deno 並瞭解了其功能以後,Node.js 看起來就像是「舊的」東西。github
特別是由於 Node.js API 是基於回調的,它是在 promise 和 async/await以前編寫的。 Node 中沒有可用於修改的餘地,這種修改的代價將會是巨大的。因此咱們只能用回調或大量的 API 調用。web
Node.js 很是棒,並將繼續成爲 JavaScript 世界中事實上的標準。可是我想咱們會逐漸看到 Deno 因爲其一流的 TypeScript 支持和現代標準庫而愈來愈被普遍的採用。面試
因爲沒有向後兼容性的報復,因此 Deno 能夠用現代技術編寫全部的東西。固然咱們沒法保證十年以內在 Deno 身上也會發生一樣的事情,而且會出現一項新技術,但這是目前的現實。
大約2年前,Node.js 的原始建立者 Ryan Dahl 在 JSConf EU 上宣佈了 Deno(油管上的演講視頻),這很是有趣,若是你常常用 Node.js 和 JavaScript,那麼它是必看的。
每一個項目經理都必須作出決定。 Ryan 對 Node 中的一些早期決定感到遺憾。此外,技術也在不斷髮展,現在的 JavaScript 與 2009 年 Node 創立時的語言已經徹底不一樣。好比現代的 ES6/2016/2017 功能等。
因此他開始了一個新項目,用來建立第二波基於 JavaScript 的服務器端程序。
我如今而不是兩年前寫本文的緣由是,技術須要大量時間才能成熟。咱們終於達到了 Deno 1.0(1.0 在2020年5月13日發佈),這是 Deno 正式宣佈穩定的初版。
這彷佛只是一個數字,但 1.0 表示直到 Deno 2.0 纔會有重大突破。當你採用一種新技術時,這很重要——你不想學習某些東西,由於它改變得太快。
這是一個大問題。
學習諸如 Deno 之類的新東西須要很大的努力。個人建議是,若是你如今開始使用服務器端 JS,而且還不瞭解 Node,而且從未編寫過任何 TypeScript 代碼,那麼就從 Node 開始。沒有人由於選擇 Node.js 而被解僱。
可是,若是你喜歡 TypeScript,想要在任何地方使用 await
,但不想依賴項目中龐大的 npm 包,那麼 Deno 可能就是你想要的。
答案是否認的。Node.js 是一項龐大的、完善的、得到了良好支持的技術,它將會持續數十年。
Deno 用 Rust 和 TypeScript 編寫,這兩種語言今天正在迅速發展。
特別是使用 TypeScript 意味着即便咱們選擇用純 JavaScript編寫代碼,也能夠得到 TypeScript 的不少好處。
使用 Deno 運行 TypeScript 代碼不須要編譯步驟——Deno 會自動爲你執行這一步驟。
你不會被迫使用 TypeScript 編寫代碼,可是 Deno 的核心是用 TypeScript 編寫的這一事實是明顯的。
首先愈來愈多的 JavaScript 程序員開始喜歡 TypeScript。
其次,你使用的工具能夠推斷出許多有關用 TypeScript 編寫的軟件的信息,例如 Deno。
這意味着,當咱們用 VS Code 進行編碼時(因爲兩者都是在 MicroSoft 上開發的,所以與 TypeScript 緊密集成),能夠在編寫代碼時得到類型檢查和高級 IntelliSense 功能。換句話說,編輯器可以以很是有用的方式幫助咱們。
因爲 Deno 基本上是 Node.js 的替代品,因此直接對二者比較很是有用。
類似之處:
差別:
npm
的官方包管理器。 Deno 沒有,而是讓你從 URL 導入任何 ES 模塊。沒有包管理器而且必須依靠 URL 來承載和導入包有利有弊。我真的很喜歡 pros:它很是靈活,咱們能夠建立軟件包而無需將其發佈到 npm 這樣的存儲庫中。
我認爲會有某種包管理器出現,可是尚未官方的消息。
Deno 網站爲第三方軟件包提供代碼託管(並經過 URL 分發):https://deno.land/x/
聊的夠多了!下面開始安裝 Deno。
在 Mac 上最簡單的方法是用 Homebrew:
brew install deno
一旦完成,你將能夠訪問 deno
命令。下面是你能夠用 deno --help
得到的幫助:
flavio@mbp~> deno --help deno 0.42.0 A secure JavaScript and TypeScript runtime Docs: https://deno.land/std/manual.md Modules: https://deno.land/std/ https://deno.land/x/ Bugs: https://github.com/denoland/deno/issues To start the REPL, supply no arguments: deno To execute a script: deno run https://deno.land/std/examples/welcome.ts deno https://deno.land/std/examples/welcome.ts To evaluate code in the shell: deno eval "console.log(30933 + 404)" Run 'deno help run' for 'run'-specific flags. 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 By default, subcommands print human-readable diagnostic messages to stderr. If the flag is set, restrict these messages to errors. -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 repl Read Eval Print Loop run Run a program given a filename or url to the module test Run tests types Print runtime TypeScript declarations upgrade Upgrade deno executable to newest version ENVIRONMENT VARIABLES: DENO_DIR Set deno's base directory (defaults to $HOME/.deno) DENO_INSTALL_ROOT Set deno install's output directory (defaults to $HOME/.deno/bin) NO_COLOR Set to disable color HTTP_PROXY Proxy address for HTTP requests (module downloads, fetch) HTTPS_PROXY Same but for HTTPS
請注意幫助中的 SUBCOMMANDS
部分,其中列出了咱們能夠運行的全部命令。都有哪些子命令呢?
bundle
把模塊和項目的依賴關係打包到單個文件中cache
緩存依賴項completions
生成 shell 補全doc
顯示模塊的文檔eval
用來評估一段代碼,例如 deno eval "console.log(1 + 2)"
fmt
內置的代碼格式化程序(相似於 Go 中的 gofmt
)help
打印此消息或給定子命令的幫助info
顯示有關緩存的信息或與源文件有關的信息install
把腳本做爲可執行文件進行安裝repl
讀取評估打印循環(默認)run
運行爲模塊指定文件名或 URL 的程序test
運行測試types
打印運行時 TypeScript 聲明upgrade
upgrade deno
to the newest versionupgrade
升級到最新版本的 deno
能夠運行 deno help
來獲取命令的特定其餘說明,例如 deno run --help
。
就像幫助所說的那樣,咱們能夠用這個命令使 deno 來啓動REPL(Read-Execute-Print-Loop),而無需任何其餘操做。
這與運行 deno repl
相同。
這個命令的一種更常見的使用方法是執行包含在 TypeScript 文件中的 Deno 程序。
你能夠同時運行 TypeScript(.ts
)文件與 JavaScript(.js
)文件。
若是你不熟悉 TypeScript,請不要擔憂:儘管 Deno 是用 TypeScript 編寫的,可是你也能夠用 JavaScript 編寫「客戶端」程序。
讓咱們運行第一個 Deno 應用程序。
我感到很是驚奇的是,甚至不須要寫一行代碼代碼——你能夠從任何 URL 運行命令。
Deno下載程序,進行編譯,而後運行:
固然,從互聯網上運行任意代碼不是一種建議作法。不過咱們是從 Deno 官方網站上運行它的,另外,Deno 還有一個沙箱,能夠阻止程序執行你不但願作的任何事情。稍後再詳細介紹。
這個程序很是簡單,只需調用 console.log()
便可:
console.log('Welcome to Deno 🦕')
若是用瀏覽器打開 URL https://deno.land/std/example... ,則會看到如下頁面:
奇怪吧?你可能但願拿到 TypeScript 文件,可是卻獲得了一個網頁。緣由是 Deno 網站的 Web 服務器知道你正在使用瀏覽器,併爲你提供了更加用戶友好的頁面。
例如,用 wget
下載相同的UR,它要求使用 text/plain
版本而不是 text/html
:
若是你想再次運行該程序,那麼如今它已由 Deno 緩存,不須要再次下載:
你可使用 `--reload
標誌來強制從新加載原始源:
deno run
有許多沒有在 deno --help
中列出的選項。你須要運行 deno run --help
來顯示它們:
flavio@mbp~> deno run --help deno-run Run a program given a filename or url to the module. By default all programs are run in sandbox without access to disk, network or ability to spawn subprocesses. deno run https://deno.land/std/examples/welcome.ts Grant all permissions: deno run -A https://deno.land/std/http/file_server.ts Grant permission to read from disk and listen to network: deno run --allow-read --allow-net https://deno.land/std/http/file_server.ts Grant permission to read whitelisted files from disk: deno run --allow-read=/etc https://deno.land/std/http/file_server.ts USAGE: deno run [OPTIONS] <SCRIPT_ARG>... OPTIONS: -A, --allow-all Allow all permissions --allow-env Allow environment access --allow-hrtime Allow high resolution time measurement --allow-net=<allow-net> Allow network access --allow-plugin Allow loading plugins --allow-read=<allow-read> Allow file system read access --allow-run Allow running subprocesses --allow-write=<allow-write> Allow file system write access --cached-only Require that remote dependencies are already cached --cert <FILE> Load certificate authority from PEM encoded file -c, --config <FILE> Load tsconfig.json configuration file -h, --help Prints help information --importmap <FILE> UNSTABLE: Load import map file Docs: https://deno.land/std/manual.md#import-maps Specification: https://wicg.github.io/import-maps/ Examples: https://github.com/WICG/import-maps#the-import-map --inspect=<HOST:PORT> activate inspector on host:port (default: 127.0.0.1:9229) --inspect-brk=<HOST:PORT> activate inspector on host:port and break at start of user script --lock <FILE> Check the specified lock file --lock-write Write lock file. Use with --lock. -L, --log-level <log-level> Set log level [possible values: debug, info] --no-remote Do not resolve remote modules -q, --quiet Suppress diagnostic output By default, subcommands print human-readable diagnostic messages to stderr. If the flag is set, restrict these messages to errors. -r, --reload=<CACHE_BLACKLIST> Reload source code cache (recompile TypeScript) --reload Reload everything --reload=https://deno.land/std Reload only standard modules --reload=https://deno.land/std/fs/utils.ts,https://deno.land/std/fmt/colors.ts Reloads specific modules --seed <NUMBER> Seed Math.random() --unstable Enable unstable APIs --v8-flags=<v8-flags> Set V8 command line options. For help: --v8-flags=--help ARGS: <SCRIPT_ARG>... script args
除了上面運行的示例外,Deno 網站還提供了一些其餘例子,你能夠在這找到它們:https://deno.land/std/examples/。
在撰寫本文時,咱們能夠找到:
cat.ts
: 打印做爲參數提供的文件列表的內容catj.ts
: 打印做爲參數提供的文件列表的內容chat/
: 一個聊天程序的實現colors.ts
: 一個例子curl.ts
: curl
的簡單實現,可打印做爲參數輸入的 URL 的內容echo_server.ts
: 一個 TCP 回顯服務器gist.ts
:一個將文件發佈到 gist.github.com 的程序test.ts
:測試的例子welcome.ts
:一個簡單的 console.log 語句(咱們在上面運行的第一個程序)xeval.ts
容許你爲收到的任何標準輸入行運行任何 TypeScript 代碼。 曾經是 deno xeval
命令,但此後已從官方命令中刪除。讓咱們寫一些代碼。
你使用 deno run https://deno.land/std/examples/welcome.ts
運行的 Deno 程序是別人寫的,因此你對 Deno 代碼的樣子一無所知。
咱們將從 Deno 官方網站上列出的默認示例開始:
import { serve } from 'https://deno.land/std/http/server.ts' const s = serve({ port: 8000 }) console.log('http://localhost:8000/') for await (const req of s) { req.respond({ body: 'Hello World\n' }) }
該代碼從 http/server
模塊導入 serve
函數。看到沒?咱們沒必要事先安裝它,也不會像 Node 模塊那樣將其存儲在本地計算機上。這是 Deno 安裝如此之快的緣由之一。
從 https://deno.land/std/http/se... 會導入模塊的最新版本。你可使用 @VERSION
導入特定版本,以下所示:
import { serve } from 'https://deno.land/std@v0.42.0/http/server.ts'
在這個文件中,serve
函數的定義以下:
/** * Create a HTTP server * * import { serve } from "https://deno.land/std/http/server.ts"; * const body = "Hello World\n"; * const s = serve({ port: 8000 }); * for await (const req of s) { * req.respond({ body }); * } */ export function serve(addr: string | HTTPOptions): Server { if (typeof addr === 'string') { const [hostname, port] = addr.split(':') addr = { hostname, port: Number(port) } } const listener = listen(addr) return new Server(listener) }
咱們繼續實例化一個調用 serve()
函數的服務器,該服務器傳遞帶有 port
屬性的對象。
而後,咱們運行這個循環來響應來自服務器的每一個請求。
for await (const req of s) { req.respond({ body: 'Hello World\n' }) }
請注意,因爲 Deno 實現了 top-level await,所以無需使用 await
關鍵字便可將其包裝到 async
函數中。
讓咱們在本地運行該程序。假設你用的是 VS Code,不過你可使用任何喜歡的編輯器。
我建議從 justjavac
安裝 Deno 擴展(我嘗試時有另外一個名稱相同,但已棄用的擴展——未來可能會消失)
該擴展將提供一些可以給 VS Code 帶來好處的實用工具,幫助你編寫程序。
下面在文件夾中建立一個 app.ts
文件,並粘貼上面的代碼:
而後用 deno run app.ts
運行:
Deno 首先下載咱們導入的依賴項,而後再下載所需的全部依賴項。
https://deno.land/std/http/se... 文件自己有多個依賴項:
import { encode } from '../encoding/utf8.ts' import { BufReader, BufWriter } from '../io/bufio.ts' import { assert } from '../testing/asserts.ts' import { deferred, Deferred, MuxAsyncIterator } from '../async/mod.ts' import { bodyReader, chunkedBodyReader, emptyReader, writeResponse, readRequest, } from './_io.ts' import Listener = Deno.Listener import Conn = Deno.Conn import Reader = Deno.Reader
而且這些都是自動導入的。
儘管最後咱們遇到了一個問題:
怎麼回事?咱們遇到了一個權限被拒絕的問題。
接下來要談談沙箱。
以前我曾經提到過,Deno 的沙箱能夠防止程序執行你不但願作的任何事情。
這是什麼意思?
Ryan 在 Deno 簡介演講中提到過,有時你想在 Web 瀏覽器以外運行 JavaScript 程序,但又不想讓它訪問系統上任何內容,或使用網絡與外界對話。
沒有什麼方法可以阻止 Node.js 應用獲取你係統上的 SSH 密鑰或任何其餘的東西,並將其發送到服務器。這就是爲何咱們一般只從受信任的源安裝 Node 軟件包的緣由。可是,咱們怎麼知道本身使用的項目是否遭到黑客入侵,而其餘人是否被黑客入侵呢?
Deno 嘗試複製與瀏覽相同的權限模型。除非你明確容許,不然在瀏覽器中運行的 JavaScript 不會在你的系統上作任何使人做嘔的事情。
回到 Deno,若是一個程序想要像之前那樣訪問網絡,那麼咱們須要給它權限。
能夠經過在運行命令時傳遞一個標誌來實現,在本例中是 --allow-net
:
deno run --allow-net app.ts
如今該程序能夠在端口 8000 上運行 HTTP 服務器了:
有不少容許 Deno 解鎖其餘功能的標誌:
--allow-env
容許環境訪問--allow-hrtime
容許高精度的時間測量--allow-net =
容許網絡訪問--allow-plugin
容許加載插件--allow-read =
容許文件系統讀訪問--allow-run
容許運行子進程--allow-write =
容許文件系統寫訪問--allow-all
容許全部權限(與 -A
相同)net
、 read
和 write
的權限能夠是細粒度的。例如你能夠用 --allow-read=/dev
來容許從特定目錄中讀取
我喜歡 Go 語言的一個緣由是 Go 編譯器附帶的 gofmt
命令。全部的 Go 代碼看起來都同樣。每一個人都在用 gofmt
。
JavaScript 程序員習慣於運行 Prettier,deno fmt
其實是在後臺運行的。
假設你有一個格式很亂的文件,以下所示:
運行 deno fmt app.ts
,它會自動被正確的格式化,還會在缺乏分號的地方自動添加:
儘管這個項目還很年輕,但 Deno 的標準庫仍然很龐大。
其中包括:
archive
: tar 存檔工具async
async utiltiesasync
:異步工具bytes
:用來操做字節片斷的輔助datetime
: 日期/時間解析encoding
:各類格式的編碼/解碼功能flags
: 解析命令行標誌fmt
: 格式化和打印fs
:文件系統 APIhash
:加密庫http
: HTTP服務器io
: I/O 庫log
: 日誌實用工具mime
:支持 multipart 數據node
: Node.js 兼容性層path
:路徑操做ws
: websockets讓咱們查看另外一個 Deno 應用示例:cat
:
const filenames = Deno.args for (const filename of filenames) { const file = await Deno.open(filename) await Deno.copy(file, Deno.stdout) file.close() }
這會將 Deno.args
的內容賦值給 filenames
變量,這一變量是包含發送到命令的全部參數的變量。
咱們遍歷它們,對於每個文件名咱們都用 Deno.open()
打開文件,而後使用 Deno.copy()
將文件的內容打印到 Deno.stdout
。最後關閉文件。
若是你這樣執行:
deno run https://deno.land/std/examples/cat.ts
那麼該程序會被下載並編譯,但沒有任何反應,由於咱們沒有指定任何參數。
如今試試
deno run https://deno.land/std/examples/cat.ts app.ts
假設你在同一文件夾中有上一個項目的 app.ts
,會看到權限錯誤:
由於在默認狀況下 Deno 是不容許訪問文件系統的。使用 --allow-read=./
能夠授予對當前文件夾的訪問權限:
deno run --allow-read=./ https://deno.land/std/examples/cat.ts app.ts
固然有相似的項目:
我想舉一個簡單的例子,說明如何用 Oak 構建 REST API。 Oak 之因此有趣,是由於它受到了流行的 Node.js 中間件 Koa 的啓發,所以,若是你之前用過,將會很是熟悉。
要構建的API很是簡單。咱們的服務器將會在內存中存儲帶有名稱和年齡的狗的列表。
咱們想:
咱們將用 TypeScript 進行這些操做,固然你也能夠用 JavaScript 編寫 API —— 只需去掉類型就能夠了。
建立一個 app.ts
文件。
首先從 Oak 導入 Application
和 Router
對象:
import { Application, Router } from 'https://deno.land/x/oak/mod.ts'
而後獲得環境變量 PORT 和 HOST:
const env = Deno.env.toObject() const PORT = env.PORT || 4000 const HOST = env.HOST || '127.0.0.1'
默認狀況下,咱們的程序將運行在 localhost:4000 上。
接下來建立 Oak 應用程序並啓動它:
const router = new Router() const app = new Application() app.use(router.routes()) app.use(router.allowedMethods()) console.log(`Listening on port ${PORT}...`) await app.listen(`${HOST}:${PORT}`)
如今程序應該能夠正常編譯了。
運行
deno run --allow-env --allow-net app.ts
而後 Deno 將會下載依賴項:
最後偵聽 4000 端口。
下次運行命令時,Deno 將會跳過安裝部分,由於這些軟件包已經被緩存了:
在文件開始,咱們爲狗定義一個接口,而後聲明一個初始的 Dog 對象 dogs
數組:
interface Dog { name: string age: number } let dogs: Array<Dog> = [ { name: 'Roger', age: 8, }, { name: 'Syd', age: 7, }, ]
如今開始實現 API。
準備就緒。在建立路由器後,讓咱們添加一些將被調用的功能:
const router = new Router() router .get('/dogs', getDogs) .get('/dogs/:name', getDog) .post('/dogs', addDog) .put('/dogs/:name', updateDog) .delete('/dogs/:name', removeDog)
看到了嗎?咱們定義瞭如下這些:
GET /dogs
GET /dogs/:name
POST /dogs
PUT /dogs/:name
DELETE /dogs/:name
讓咱們一一實現。
從 GET/dogs
開始,它返回全部狗的列表:
export const getDogs = ({ response }: { response: any }) => { response.body = dogs }
接下來,是經過名稱檢索一隻狗的方法:
export const getDog = ({ params, response, }: { params: { name: string } response: any }) => { const dog = dogs.filter((dog) => dog.name === params.name) if (dog.length) { response.status = 200 response.body = dog[0] return } response.status = 400 response.body = { msg: `Cannot find dog ${params.name}` } }
這是咱們添加新 dog 的方法:
export const addDog = async ({ request, response, }: { request: any response: any }) => { const body = await request.body() const dog: Dog = body.value dogs.push(dog) response.body = { msg: 'OK' } response.status = 200 }
注意,我如今使用 const body = await request.body()
來獲取 body 的內容,由於 name
和 age
的值是做爲 JSON 傳遞的。
這是更新狗年齡的方法:
export const updateDog = async ({ params, request, response, }: { params: { name: string } request: any response: any }) => { const temp = dogs.filter((existingDog) => existingDog.name === params.name) const body = await request.body() const { age }: { age: number } = body.value if (temp.length) { temp[0].age = age response.status = 200 response.body = { msg: 'OK' } return } response.status = 400 response.body = { msg: `Cannot find dog ${params.name}` } }
這是從列表中刪除狗的方法:
export const removeDog = ({ params, response, }: { params: { name: string } response: any }) => { const lengthBefore = dogs.length dogs = dogs.filter((dog) => dog.name !== params.name) if (dogs.length === lengthBefore) { response.status = 400 response.body = { msg: `Cannot find dog ${params.name}` } return } response.body = { msg: 'OK' } response.status = 200 }
如下是完整的代碼:
import { Application, Router } from 'https://deno.land/x/oak/mod.ts' const env = Deno.env.toObject() const PORT = env.PORT || 4000 const HOST = env.HOST || '127.0.0.1' interface Dog { name: string age: number } let dogs: Array<Dog> = [ { name: 'Roger', age: 8, }, { name: 'Syd', age: 7, }, ] export const getDogs = ({ response }: { response: any }) => { response.body = dogs } export const getDog = ({ params, response, }: { params: { name: string } response: any }) => { const dog = dogs.filter((dog) => dog.name === params.name) if (dog.length) { response.status = 200 response.body = dog[0] return } response.status = 400 response.body = { msg: `Cannot find dog ${params.name}` } } export const addDog = async ({ request, response, }: { request: any response: any }) => { const body = await request.body() const { name, age }: { name: string; age: number } = body.value dogs.push({ name: name, age: age, }) response.body = { msg: 'OK' } response.status = 200 } export const updateDog = async ({ params, request, response, }: { params: { name: string } request: any response: any }) => { const temp = dogs.filter((existingDog) => existingDog.name === params.name) const body = await request.body() const { age }: { age: number } = body.value if (temp.length) { temp[0].age = age response.status = 200 response.body = { msg: 'OK' } return } response.status = 400 response.body = { msg: `Cannot find dog ${params.name}` } } export const removeDog = ({ params, response, }: { params: { name: string } response: any }) => { const lengthBefore = dogs.length dogs = dogs.filter((dog) => dog.name !== params.name) if (dogs.length === lengthBefore) { response.status = 400 response.body = { msg: `Cannot find dog ${params.name}` } return } response.body = { msg: 'OK' } response.status = 200 } const router = new Router() router .get('/dogs', getDogs) .get('/dogs/:name', getDog) .post('/dogs', addDog) .put('/dogs/:name', updateDog) .delete('/dogs/:name', removeDog) const app = new Application() app.use(router.routes()) app.use(router.allowedMethods()) console.log(`Listening on port ${PORT}...`) await app.listen(`${HOST}:${PORT}`)
Deno 官方網站是 https://deno.land
可在 https://doc.deno.land 和 https://deno.land/typedoc/ind... 上獲得 API 文檔。
awesome-deno https://github.com/denolib/aw...
fetch
實現,與瀏覽器中的相匹配但願你喜歡這個 Deno 教程!