Denoとは、Node.jsの開発者であるRyan Dahlさんによって作られたJavaScript/TypeScriptの新しいランタイム(JavaScript/TypeScriptを実行できる環境)です。v1.0が2020/5/13にリリースされました。Denoは、Ryan DahlさんがNode.jsの設計を見直して改めて開発したランタイムであり、「そのうちNode.jsがDenoにリプレイスされるんじゃないか!?」ということで注目を集めています。
本記事では、Denoの標準ライブラリを使って簡単なチャットアプリを作りながら、Node.jsと比較したDenoの特長を紹介していきます。
Denoのインストール
まずはHomebrewでDenoをインストールします
$ brew install deno
インストールが完了したら、バージョンを確認してみます
$ deno --version deno 1.0.3 v8 8.4.300 typescript 3.9.2
Deno、V8、TypeScriptのバージョンが表示されます。Denoは標準でTypeScriptをサポートしています。
DenoでJavaScript/TypeScriptを実行する際は、
$ deno run index.ts
上記のようにdeno run (ファイル名)
します。また、次のようにURLでファイルを指定して実行することもできます。
$ deno run https://deno.land/std/examples/welcome.ts Download https://deno.land/std/examples/welcome.ts Warning Implicitly using master branch https://deno.land/std/examples/welcome.ts Compile https://deno.land/std/examples/welcome.ts Welcome to Deno 🦕
あら、かわいい恐竜が…🦕。welcome.ts
は、公式のサンプルコードです。コードの内容は、URLをブラウザで開けば確認できます。
https://deno.land/std/examples/welcome.ts
ちなみに、二回目以降同じコードを実行すると、
$ deno run https://deno.land/std/examples/welcome.ts Welcome to Deno 🦕
ダウンロードやコンパイルは実行されず、キャッシュから実行されていることがわかります。
必要なファイル/ディレクトリを用意する
Denoのインストール、TypeScriptの実行が確認できたので、早速チャットアプリを作っていきます。
まず、index.html
、server.ts
の2ファイルを用意してください。
$ mkdir deno_app $ cd deno_app $ touch index.html server.ts
なんと、コードを書いてチャットアプリが動くようになるまで、必要なのは上記の2ファイルだけです。Denoでは、package.jsonもnode_modulesも必要ありません。外部モジュールを利用する場合は、コード内で外部モジュールとそのバージョンをURLで指定するだけでOKです(具体的な記述方法は後ほど紹介します)。
また、TypeScript→JavaScriptの変換もDenoが内部でよしなに行ってくれるため、tsconfig.jsonも必要ありません(独自に設定したい項目がある場合は、tsconfig.jsonを作成して設定することもできます)。
クライアント側を作る
index.htmlを編集していきます。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <title>Denoでチャットアプリ</title> </head> <body> <div> <input type="text" id="input" /> <button id="sendButton" disabled>送信</button> <button id="enterButton" disabled>入室</button> <button id="leaveButton" disabled>退室</button> </div> <ul id="timeline"></ul> <script> let ws; let isConnected = false; const input = document.getElementById("input"); const sendButton = document.getElementById("sendButton"); const enterButton = document.getElementById("enterButton"); const leaveButton =document.getElementById("leaveButton"); const timeline = document.getElementById("timeline"); input.addEventListener("keydown", sendMessageByEnterKey); sendButton.onclick = sendMessage; enterButton.onclick = connectWebSocket; leaveButton.onclick = leaveRoom; applyButtonState(); connectWebSocket(); function connectWebSocket() { if (ws) ws.close(); ws = new WebSocket(`ws://${location.host}/ws`); ws.addEventListener("open", () => { isConnected = true; applyButtonState(); }); ws.addEventListener("message", ({data}) => { timeline.appendChild(messageDom(data)); }); ws.addEventListener("close", () => { isConnected = false; applyButtonState(); }); } function sendMessageByEnterKey(e) { if (e.keyCode === 13) sendMessage(); } function sendMessage() { const msg = input.value; ws.send(msg); input.value = ""; } function messageDom(msg) { const li = document.createElement("li"); li.className = "message"; li.innerText = msg; return li; } function leaveRoom() { ws.close(); isConnected = false; applyButtonState(); } function applyButtonState() { sendButton.disabled = isConnected ? false : true; enterButton.disabled = isConnected ? true : false; leaveButton.disabled = isConnected ? false : true; } </script> </body> </html>
メッセージを入力するinput要素、送信・入室・退室のbutton要素、送受信したメッセージのタイムラインを表示するul要素を用意します。
script要素には、WebSocket接続の作成と管理、メッセージの送受信、画面の更新、ボタン要素のdisabledの制御に関する処理を記述します。ここはあまりDenoとは関係ないため、内容の解説は割愛します🦕。
サーバー側を作る
server.tsを編集していきます。
サーバーを用意する
Denoの標準ライブラリServerクラスのlistenAndServe関数を利用して、HTTPサーバーを用意します。下記の通り、listenAndServe関数をインポートし実行するコードを書きます。
import { listenAndServe } from "https://deno.land/std/http/server.ts"; listenAndServe({ port: 8080 }, async (request) => { if (request.method === "GET" && request.url === "/") { const file = await Deno.open("./index.html"); request.respond({ status: 200, headers: new Headers({ "content-type": "text/html", }), body: file, }); } if (request.method === "GET" && request.url === "/favicon.ico") { request.respond({ status: 302, headers: new Headers({ location: "https://deno.land/favicon.ico", }), }); } }); console.log("http://localhost:8080/");
このとおり、Denoでは外部モジュールをURLでimportすることができます。バージョンを指定して管理したい場合、
import { listenAndServe } from "https://deno.land/std@0.50.0/http/server.ts";
このように、@でバージョンを指定することもできます。lisetenAndServeの使い方は、こちらで確認できます。
ここではlisetenAndServeにオプションで8080ポートを設定し、ハンドラに下記のような処理を記述しています。
- GETメソッドで
/
がリクエストされた場合、index.htmlを返す - GETメソッドで
/favicon.ico
がリクエストされた場合、Deno公式のファビコンファイルにリダイレクトする
早速サーバーを動かしてみます
$ deno run server.ts Download https://deno.land/std/http/server.ts Download https://deno.land/std/ws/mod.ts 〜(省略)〜 Compile file://hoge/deno_app/server.ts http://localhost:8080/ error: Uncaught PermissionDenied: network access to "0.0.0.0:8080", run again with the --allow-net flag at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11) at Object.sendSync ($deno$/ops/dispatch_json.ts:72:10) at Object.listen ($deno$/ops/net.ts:51:10) at listen ($deno$/net.ts:152:22) at serve (https://deno.land/std/http/server.ts:252:20) at listenAndServe (https://deno.land/std/http/server.ts:272:18) at file://hoge/deno_app/server.ts:9:1
外部モジュールのインポート、server.tsのコンパイルのあと、エラーが出ました🦕。ここで出ているエラーは、「ネットワークにアクセスする権限がありませんので--allow-net
オプションをつけて再度実行してください」というものです。Denoは、デフォルトのままではネットワークにアクセスできないセキュリティルールになっています。
OK、--allow-net
オプションを付けて実行します。
$ deno run --allow-net server.ts http://localhost:8080/ error: Uncaught PermissionDenied: read access to "hoge/deno_app/index.html", run again with the --allow-read flag at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11) at Object.sendAsync ($deno$/ops/dispatch_json.ts:98:10) at async Object.open ($deno$/files.ts:37:15) at async file://hoge/deno_app/server.ts:16:18
またエラーが出ました🦕。「index.htmlにアクセスする権限がありませんので、--allow-read
オプションを付けて再度実行してください」とのことです。Denoは、デフォルトのままではローカルファイルにもアクセスできないセキュリティルールになっています。ンー、セキュア(適当)。
$ deno run --allow-net --allow-read server.ts Compile file://hoge/deno_app/server.ts http://localhost:8080/
サーバーが動き始めたようなので、ブラウザで確認してみます。

index.htmlが表示されていることが確認できました。WebSocket接続を開いただけでそれ以外の処理をなにも用意していないので、まだ何もできません。
チャットアプリの処理を用意する
次に、WebSocket接続のリクエストを受けたときの処理をserver.tsに追記していきます
import { listenAndServe } from "https://deno.land/std/http/server.ts"; import { acceptWebSocket, acceptable, WebSocket, isWebSocketCloseEvent, } from "https://deno.land/std/ws/mod.ts"; listenAndServe({ port: 8080 }, async (request) => { if (request.method === "GET" && request.url === "/") { const file = await Deno.open("./index.html"); request.respond({ status: 200, headers: new Headers({ "content-type": "text/html", }), body: file, }); } if (request.method === "GET" && request.url === "/favicon.ico") { request.respond({ status: 302, headers: new Headers({ location: "https://deno.land/favicon.ico", }), }); } if (request.method === "GET" && request.url === "/ws") { if (acceptable(request)) { acceptWebSocket({ conn: request.conn, bufReader: request.r, bufWriter: request.w, headers: request.headers, }).then(wsHandler); } } }); console.log("http://localhost:8080/"); const clients = new Map<number, WebSocket>(); let clientId = 0; async function wsHandler(ws: WebSocket): Promise<void> { const id = ++clientId; clients.set(id, ws); dispatch(`[${id}]さんが入室しました`); for await (const msg of ws) { if (typeof msg === "string") { dispatch(`[${id}] > ${msg}`); } else if (isWebSocketCloseEvent(msg)) { clients.delete(id); dispatch(`[${id}]さんが退室しました`); break; } } } function dispatch(msg: string): void { for (const client of clients.values()) { client.send(msg); } }
Denoの標準ライブラリからWebSocketの各種モジュールをインポートし、WebSocketに関する処理をいろいろと記述しています。
まず、listenAndServe関数のハンドラ内に追記した、/ws
をリクエストされたときの処理について、acceptable関数でリクエストのヘッダーがWebSocketで受け入れ可能かどうかを検査し、acceptWebSocket関数でTCP接続をWebSocket接続にアップグレードしています。成功した場合、wsHandler関数を実行します。
wsHandler関数には、number型の変数をキー・WebSocket型の変数をバリューとするMapオブジェクトclientsに、ユーザーID(連番)の変数id
・acceptWebSocket関数から渡されたWebSocket接続ws
をclientsに追加する処理を記述しています。clientsへの追加の他に、メッセージを受信した場合はメッセージをsendするdispatch関数を実行する、isWebSocketCloseEvent関数でcloseイベントを検知した場合は退室の処理を行う、などの処理を記述しています。
dispatch関数でclient.send(msg)が実行されたとき、index.htmlのscript要素で定義したconnectWebSocket関数内の処理
ws.addEventListener("message", ({data}) => { timeline.appendChild(messageDom(data)); });
が実行され、画面上に入退室の通知、送受信したメッセージが表示されます。
さっそくdeno run --allow-net --allow-read server.ts
し、動作を確認してみましょう。

おぉ〜🦕🦕🦕🦕
いったんここで、チャットアプリは完成とします。匿名での入退室、メッセージの送受信が可能なチャットアプリです。
まとめ
Deno、TypeScriptがすぐ動いたり、npmで苦しむことがなかったりで、かなりよいですね。今回は標準ライブラリだけでチャットアプリを作りましたが、サードパーティのモジュールもたくさんあるようなので、それらも使ってみたいですね。