Node.jsとの違いを感じながらDenoでChatアプリをサクッと作ってみる


この記事の目的

先日(1週間以上前になりますが)、Deno 1.0 がリリースされました!
DenoとNode.js、結局何が違うんだ?と疑問に思ったので、Learn Deno: Chat appのサイトをパクり参考にDenoとTypeScriptを用いてChatアプリを作りながら、実装した時に実感した違いをまとめてみます。

今回扱った技術要素(バージョン)

  • Deno 1.0.0
  • TypeScript
  • Preact

そもそもDenoとは

  • Javascriptのサーバーサイド実行環境です。
  • Rustの非同期ランタイムtokioで実行されます。(tokioの実行速度についてはこちらの記事で比較されています。)
  • Node.jsの欠点を克服するため、Node.jsの開発者が自身で作り直したものです。
  • 公式ドキュメントはこちら。また標準ライブラリも公式ドキュメントが用意されています。

今回はlocalhostのサーバーをDenoで立ててアプリを動かしてみます。

Denoを使うと何が嬉しいのか? Node.jsと何が違うのか?

前準備(Denoのインストール)

コマンドbrew install deno一発でインストールできました。

$ brew install deno
Updating Homebrew...
<>
==> Downloading https://homebrew.bintray.com/bottles/deno-1.0.0.mojave.bottle.tar.gz
<>
==> Summary
🍺  /usr/local/Cellar/deno/1.0.0: 9 files, 42.0MB <- 軽い

ビールでお祝いしてくれました。バージョンを確認すると、 1.0.0になっています!

$ deno --version
deno 1.0.0
v8 8.4.300
TypeScript 3.9.2

標準ライブラリに動作確認用のスクリプトも用意されています。

$ 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 🦕

【Node.jsとここが違う#1】 npmやnode_modulesでのライブラリ管理が不要

サーバーを立てる下記スクリプトを用意しました。

server.ts
import { listenAndServe } from "https://deno.land/std/http/server.ts";

listenAndServe({ port: 3000 }, async (req) => {
  if (req.method === "GET" && req.url === "/") {
    req.respond({
      status: 200,
      headers: new Headers({
        "content-type": "text/html",
      }),
      body: await Deno.open("./index.html"),
    });
  }
});

console.log("Server running on localhost:3000");

deno run server.tsでスクリプトを実行すると、初回のみ、実行に必要なdependencyをhttpモジュールから全てダウンロードします。(先ほどのwelcome.ts実行時も初回だったのでモジュールのダウンロードが走っています。)
npmなどのバージョン管理ツールに依存しないので、ユーザーが都度npm installを行う必要がありません。
一度ダウンロードされればキャッシュされるので、キャッシュをクリアしダウンロードし直したい場合はdeno --reloadコマンドを実行すると良いとのことです。
自身のレポジトリにライブラリを持たないので、モジュールをインポートしてコードが肥大化することもありません。
※ ただサードパーティから自身のレポジトリにアクセスできることを意味しているので、取り込んだ側がコントロールできないモジュールをインポートするには注意が必要です。

【Node.jsとここが違う#2】 package.jsonでの複雑なバージョン管理も不要に

前述のようにDenoでは、必要なライブラリはファイル実行時にダウンロードします。
その際、常に最新のライブラリを利用しても問題ない場合と、バージョンをきっちり管理したい場合とありますよね。
Denoでは、バージョン管理も簡単、importしているURLに@^${バージョン番号}を追加するだけです。
こんな感じで。

test.ts
import { camelCase } from 'https://cdn.pika.dev/camel-case@^4.1.1'

また、別にdeps.tsというファイルを用意し、そこでバージョンを一括管理することもできます。
インポートしたライブラリを複数ファイルで使用する時などに便利ですね。
例えばdeps.tsにバージョン指定でライブラリを読み込んでおけば、それぞれのファイルからはdeps.tsのメソッドを呼び出せばOKです。下記の場合、test.tsでは4.1.1のバージョンでcamelCaseがインポートされます。

deps.ts
export { camelCase } from "https://cdn.pika.dev/camel-case@^4.1.1";
test.ts
import { camelCase } from './deps.ts'

【Node.jsとここが違う#3】 セキュリティがより強固に

上記ファイルですが、そのまま実行すると下記のエラーが表示されます。
デフォルトではネットワークアクセスが許可されていないようです。

error: Uncaught PermissionDenied: network access to "0.0.0.0:3000", 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:261:20)
    at listenAndServe (https://deno.land/std/http/server.ts:28

Node.jsでは意図せず様々なファイル、フォルダへのアクセスが裏で行われていましたが、そのオープンさゆえに実装者が気をつけないとセキュリティのリスクに晒す可能性も潜んでいました。
しかしDenoでは、上記のようにデフォルトの設定では簡単にアクセスはできず、コマンドラインの引数で明示的にアクセス可能にしてあげる必要があります。つまりコマンドで機能ごとのアクセス制御ができるようになったのですね!
どのように制御できるか詳細はdeno run -hのコマンドで確認できます。
今回はdeno run --allow-net --allow-read server.tsと設定したコマンドで実行すると、無事にサーバーが立ち上がりました。

ターミナル
$ deno run --allow-net --allow-read server.ts
Compile file:///Users/shokokashiwagi/deno-chat/server.ts
Server running on localhost:3000

【Node.jsとここが違う#4】 標準ライブラリがリッチに

Node.jsでは外部ライブラリが続々といろんな人によって開発される分、インポートした時のディペンデンシーが重いのが課題でしたが、Denoでは様々な機能が標準ライブラリに実装されているので、自身のレポジトリの肥大化を防ぐことができます。
ためしに、今回のテーマであるchat機能を実装するためにwebsocketを導入してみます。
これも標準ライブラリからインポートし、サクッと使うことができます!

chat.ts
import {
  WebSocket,
  isWebSocketCloseEvent,
} from "https://deno.land/std/ws/mod.ts";
import { v4 } from "https://deno.land/std/uuid/mod.ts";

const users = new Map<string, WebSocket>();

function broadcast(message: string, senderId?: string): void {
  if (!message) return;
  for (const user of users.values()) {
    user.send(senderId ? `[${senderId}]: ${message}` : message);
  }
}

export async function chat(ws: WebSocket): Promise<void> {
  const userId = v4.generate();

  // Register user connection
  users.set(userId, ws);
  broadcast(`> User with the id ${userId} is connected`);

  // Wait for new messages
  for await (const event of ws) {
    const message = typeof event === "string" ? event : "";
    broadcast(message, userId);
    // Unregister user conection
    if (!message && isWebSocketCloseEvent(event)) {
      users.delete(userId);
      broadcast(`> User with the id ${userId} is disconnected`);
      break;
    }
  }
}

Goみたいだな、と思ったらやはりGoの影響を受けているらしいです。
公式ライブラリはこちら

【Node.jsとここが違う#4】 TypeScriptとも相性が良い

Denoは標準でTypeScriptをサポートしており、TypeScript実装するのに外部ツールは必要ありません。
TypeScriptを実装した場合、実行時にJavascriptへの変換が内部で行われます。
TypeScriptのconfigを独自に設定する場合は、tsconfig.jsonのファイルを作成し、下記のようにその設定ファイルを引数に該当ファイルを実行すると良いとのことです。

ターミナル
deno run -c tsconfig.json [ファイル名]

【Node.jsとここが違う#5】 テストも簡単に実行できる

テストファイルを書いて

test.ts
import { assertStrictEq } from "https://deno.land/std/testing/asserts.ts"; //アサート機能も標準ライブラリに実装されてます
import { camelize } from "./camelize.ts";

Deno.test("camelize works", async () => {
  assertStrictEq(camelize("this is an example"), "thisIsAnExample 🐪🐪🐪");
});

コマンドを実行するだけ!
これもGoと動きは似ています。

ターミナル
$ deno test
Compile file:///Users/shokokashiwagi/deno-chat/camelize.ts
running 1 tests
test camelize works ... ok (6ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (6ms)

まとめ

htmlファイル(index.html)を実装し、先ほどchat.tsで作成したコメント送信機能を呼び出します。
sercer.tsでlocalhostを立ち上げると、無事下記画面のようにチャットする画面ができました!
テキストを枠に入力し、~( ̄∇ ̄)ノアイヤイヤー ヘ( ̄∇ ̄)~アイヤイヤーのボタンを押すと画面上にテキストが表示されます。
ライブラリでUUIDを自動生成したり、文字をキャメルケースに変換したりしています。
コードの全量はLearn Deno: Chat appを参考に実装しています。

終わりに

2019年のJS Conf JPのセッションでDenoを知って以来、密かに興味をもちながら踏み出せずにいましたが、2020年5月13日、ついにDeno1.0.が正式にリリースされた!!!!!ということで、この機会なのでDenoを触ってみました。
インストール〜アプリ実装まで驚くほどスムーズでわかりやすいので、誰もがとっつきやすいのではないでしょうか。
Node.jsと構成が大きく異なるため代替として普及するにはまだ早いと言われていますが、個人的にはいずれはそうなるのではないか、と思いました。

参考