Deno 試してみた - 隅田川.js#1 LT

こんにちは、CADDi でフロントエンドエンジニアをしている桐生です。

少し前になりますが Deno 1.0 がリリースされ話題になったかと思います。まだ記憶に新しい方も多いのではないでしょうか。タイムリーにも 隅田川.js #1(オンライン)

隅田川.js

にてLTをさせて頂く機会があったので、話題の Deno について発表してきました。

本ブログはこの発表内容をブログ化したものになります。(一部、可読性向上のため内容を改変しています)

目次

[toc]

Deno とは

Deno ざっくり

Deno は Node.jsの作者である Ryan Dahl 氏が開発している TypeScript/JavaScript ランタイムです。

https://deno.land/ のトップによると

  • V8, Rust, Tokio 上に構築された、TypeScript/JavaScript ランタイム
  • デフォルトではファイルやネットワークへのアクセスが禁止されていてセキュア
  • デフォルトでTypeScriptをサポート
  • ビルトインでコードフォーマッタや各種ユーティリティを備えている

といった特徴があります。

Node.js との違い

V8 上の JavaScript ランタイムといえば Node.js です。Node.js があるにもかかわらず、なぜ新しい JavaScript ランタイムが必要なのでしょうか?背景には、Ryan Dahl氏の Node.js における設計ミスへの後悔 があります。興味のある方は見てみてください。

Comparison to Node.js によると

  • npm を使わない。module への URL か file path を使う。
  • package.json を使わない。
  • require() を使わない。Es module を使う。 import * as log from "https://deno.land/std/log/mod.ts";
  • 全ての非同期アクションは Promise を返す。
  • file, network, environment への明示的な許可をが必要
  • Uncaught error で停止する

といった違いがあります。

Deno で試したこと

そんな Deno について、習うより慣れろということでいろいろと試してみました。

Getting Started をなぞる

まずは https://deno.land/manual/getting_started に倣って、インストールと実行をやってみました。

$ brew install deno
$ deno run https://deno.land/std/examples/welcome.ts
Welcome to Deno 🦕

やったことはこれだけで非常に簡単でしたが、見事 Deno の第一歩を踏み出すことができました。

続いて HTTP Request を伴うモジュールの実行を試してみました。

$ deno run https://deno.land/std/examples/curl.ts https://example.com
error: Uncaught PermissionDenied: network access to "https://example.com/", run again with the --allow-net flag
    at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11)
    at Object.sendAsync ($deno$/ops/dispatch_json.ts:98:10)
    at async fetch ($deno$/web/fetch.ts:591:27)
    at async https://deno.land/std/examples/curl.ts:3:13

残念ながらエラーとなってしまいました。なぜでしょうか?冒頭でお伝えしたとおり、Deno はデフォルトで、ファイルやネットワークへのアクセスを禁止しているからです。この場合は --allow-net オプションを追加することで実行することができるようになりました。

$ deno run --allow-net https://deno.land/std/examples/curl.ts

他にもたくさんの --allow-xxx オプションがあり、用途に応じて追加することができます。

$ deno run -h
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
    ...

例えば複数追加する場合は、このような指定になります。

$ deno install \
  --unstable \
  --allow-net \
  --allow-read \
  --allow-write \
  --allow-run \
  -f -n das \
  https://deno.land/x/deno_app_setuper/cli.ts

環境構築をする

Getting Started のあと、環境構築を行うことにしました。私は普段 VSCode を使っているので、Deno 用の VSCode Extension をインストールすることにしました。(2020/5/19時点)

justjavac.vscode-deno

そして、ワークスペースに .vscode/.settings.json を用意して Deno Extenstion を有効化しました。

{
  "deno.enable": true
}

私が行った環境構築は以上で終わりです。

注意: 2020/6月現在、上記の extenstion は deprecated になっていました。代わりに以下が公式で出ているので、こちらをインストールしましょう。 denoland.vscode-deno

これ以外にも shell autocomplete の設定や、Jetbrains や Vim など向けのプラグインも提供されているので、お好みでセットアップしましょう。

https://deno.land/manual/getting_started/setup_your_environment

Style Guide を読む

基本的な実行と環境構築が終わり、コードを書ける状態になりました。しかし、どう書くのがベターか知っておきたいと思い、Style Guide を読むことにしました。

https://deno.land/manual/contributing/style_guide

一般

  • TypeScript を使う。
  • module という言葉を使う。library や package という言葉は使わない。
  • メタプログラミングは推奨しない。
  • 依存は最小に。循環参照を避ける。

呼称のブレをなくし module で統一する、というのはいいなと思いました。

ファイル命名規則まわり

  • ファイル名の区切りには underscore _ を使う。 ex) foo-bar.ts ではなく foo_bar.ts
  • _foo.ts など underscore から始まるファイル名は、internal module なので import してはいけない
  • index.ts / index.js を使わない。
  • もしエントリーポイントが必要なら mod.ts を使う。

ファイル名の単語の区切りにはハイフンではなくアンダースコアを使いましょうということで、私自身、普段ハイフンで区切ることが多いので、これは注意しておかなくてはと思いました。

またエントリーポイントの名前には index ではなく Rust に倣い mod を使おうというのも新鮮でした。

テストまわり

  • Module が公開している機能にはテストを書く。foo.ts -> foo_test.ts
  • Unit Test は明示的であるべき。

コードスタイルまわり

  • module には copyright header を入れる。
  • TODO コメントには Github の Issue や author name を入れる。
  • 公開機能には JSDoc を書く。
  • 関数の必須引数は最大で2つまで。オプション引数はオブジェクトとして渡す。
  • オプション引数だけが唯一、”プレーン”オブジェクトである。必須引数はプレーンオブジェクトとは区別ができる必要がある。(Array, Map, Date, class MyThing)
  • トップレベル関数には function を使う。Arrow function はクロージャーに限定する。

個人的に気になったのは下3つで、module を設計・実装するにあたって具体的な指針になると思いました。

Deno では関数のインターフェースを厳格に定めており、関数は必須パラメーターを最大2つまで指定できるとしています。さらに、必須でないその他の引数は全てオブジェクトに入れることとしています。このようにデザインすることで、オプションの位置が変更された場合にも、下位互換性を維持しつつ進化することができることのことです。

具体例を出しつつ見ていきましょう。

関数の必須引数は最大で2つまで。オプション引数はオブジェクトとして渡す。

// BAD: optional parameters not part of options object.
export function resolve(
  hostname: string,
  family?: "ipv4" | "ipv6",
  timeout?: number
): IPAddress[] {}

上記のコードはどこが悪いのでしょうか? 引数 family, timeout はオプション引数ですが、オブジェクトとして提供されていません。

// GOOD.
export interface ResolveOptions {
  family?: "ipv4" | "ipv6";
  timeout?: number;
}
export function resolve(
  hostname: string,
  options: ResolveOptions = {}
): IPAddress[] {}

引数 family, timeout を含む ResolveOptions として提供するのがよいとされます。

オプション引数だけが唯一、”プレーン”オブジェクトである。必須引数はプレーンオブジェクトとは区別ができる必要がある。(Array, Map, Date, class MyThing)

// BAD: `env` could be a regular Object
export interface Environment {
  [key: string]: string;
}
export function runShellWithEnv(
  cmdline: string,
  env: Environment
): string {}

一見問題なさそうですが、Enveronment は単なる key-value オブジェクトなので、実質的にオプションオブジェクトと同意ですが、必須オプションのように見えてしまいます。

// GOOD.
export interface RunShellOptions {
  env: Environment;
}
export function runShellWithEnv(
  cmdline: string,
  options: RunShellOptions
): string {}

RunShellOptions というオプションオブジェクトを定義し、その中に Enveronment を入れることで、オプションであることが自明となります。

これは陥りがちなポイントかもしれません。注意深く設計していく必要がありそうだと感じました。

トップレベル関数には function を使う。Arrow function はクロージャーに限定する。

// BAD
export const foo = (): string => {
  return "bar";
};
// GOOD.
export function foo(): string {
  return "bar";
}

確かに function を使ったほうが、関数であることが自明であったり、function.name で名前を取得できたりといった利点があったりするので、よいルールだと感じました。

私は普段 React で functional component を Arrow function で書きまくっているので、気をつけたいポイントです。

Deno API を眺める

続いて、Deno のAPI を見てみようと思い眺めてみると、基本的に Web compatible に設計されているおかげで、おなじみのAPIがたくさんありました。API名を見ただけでどんな処理ができるのか大体のあたりがついたので、一旦おいておくことにしました。普段 Web をやっている者にとっては、学習コストがかなり抑えられるので、非常に大きなメリットではないかと思いました。

https://doc.deno.land/https/github.com/denoland/deno/releases/latest/download/lib.deno.d.ts deno api

Third Party Modules を使う

そして最後に、いざプログラミングをしようと思ったときに、サードパーティモジュールは欠かせないということで、サードパーティモジュールはどう扱えば良いかを確認してみました。 https://deno.land/#third-party-modules

Web 上のどこからでも import できる

  • Github
  • 独自の Web Server
  • CDN
    • pika.dev
    • jspm.io
    • etc...

冒頭で説明した通り、module の import には URL かファイルパスを指定可能なので、論理的には Web上のどこからでも import が可能です。

Deno 公式サービス

これは Deno で動く ES Module をホストしているサービスです。試しに、ここにホストされている moment.js を使ってみることにしました。

// moment_sample.ts
import { moment } from 'https://deno.land/x/moment/moment.ts';
console.log(moment.now());

このように moment への URL を指定して import し、現在時間を出力するサンプルプログラムを作成し、実行してみると・・・

$ deno run --reload moment_sample.ts
Compile file:///Users/xxx/yyy/zzz/moment_sample.ts
Download https://deno.land/x/moment/moment.ts
Download https://deno.land/x/moment/vendor/moment.js
1589849822499

無事実行することができました。

確かに、URL をしっかりと指定すればサードパーティモジュールを使うことができるようです。

ただし、上記のプログラムには1点問題がありました。moment の型がなぜか any となっており、せっかく TypeScript で書いているのにコード補完が効かなかったのです。

module の型の指定

この問題を解消するには、型定義ファイルを使用すればよいと考え、Deno での型定義ファイルの適用方法を調べてみました。すると、// @deno-types="" という pragma を指定すればよいことがわかりました。 https://deno.land/manual/getting_started/typescript#compiler-hint

moment 用の型定義ファイルは deno.land にはホストされていなかったため、GitHub から直接 import することにしました。さらに、型定義があったとしても、deno.land の moment を使っている限りはなぜかコード補完が効かなかったため、こちらも GitHub のものを使うように変更しました。すると、ようやくコード補完が効くようになりました。

// @deno-types="https://raw.githubusercontent.com/moment/moment/develop/ts3.1-typings/moment.d.ts"
import moment from "https://raw.githubusercontent.com/moment/moment/develop/dist/moment.js";
console.log(moment.now());

単に module への URL を指定するだけで簡単に使えると思っていたのですが、実際に期待する動作を得るまでにかなり苦労しました。

Third Party Modules まとめ

その後 moment 以外にも様々な module の import を試し、いろいろと試行錯誤した結果わかったことをまとめます。

  • Third Party Module を使う場合
    • CDN や GitHub などに公開されている module の URLを指定する。
    • ただし、import できる module には制限あり。
    • Node の Module Resolution に依存していないもの(外部 module を import していないもの、あるいは、import に 明示的なパスやURLが記述されているもの)は import 可能。そうでないものは Deno がパス解決できないため無理。
  • TypScriptの型のサポートを得たい場合
    • // deno-types="" で CDN や GitHub などにホストされている型定義ファイル の URLを指定する。
    • ただし、サポートする型定義ファイルには制限あり
    • Node の Module Resolution に依存していないもの(外部の型定義 を import していないもの、あるいは、import に 明示的なパスやURLが記述されているもの)は import 可能。そうでないものは Deno がパス解決できないため無理。 ex) React の 型定義ファイル 内部では以下の参照が含まれているので Deno では扱えない。 import * as CSS from 'csstype'; // パス解決できない import * as PropTypes from 'prop-types'; // パス解決できない
  • Deno は import に 明示的なパス指定を要求するため、npm package は(Node Module Resolution に依存しているので)使えない可能性が高い。

  • npm の @types に相当するものはないのか?

  • deno.land に置いてある module は TypeScript としてホストされているので、型定義なしでも使えるはずでは?
    • そんなこともなかった。型がついていなかったりする。少なくとも moment に型はついていなかった。 deno.land/moment
  • Deno が扱える module の正しい URL を見つけ出して指定するのが辛い。

  • pika.dev は Deno フレンドリー。検索性が高く探しやすい。module を import すると一緒に型定義ファイルもダウンロードできる仕組みを提供している。

余談: TypeScript の Top level "for-await" が実装されていない

// server.ts
import { serve } from "https://deno.land/std@0.50.0/http/server.ts";
const s = serve({ port: 8000 });
for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}

上記は公式サイトに載っているサンプルコードですが、このサンプルコードの bundle は成功するものの、bundle 後の run が失敗してしまいます。

$ deno bundle server.ts server.bundle.js
$ deno run --allow-net server.bundle.js

error: Uncaught SyntaxError: Unexpected reserved word
        for await (const req of s) {

なぜだろうと思い調べてみると、Issue が挙がっていました。 Top-level for-await not working in bundles #4207

この Issue のコメントを読んでいくと原因が判明しました。なんと、そもそも TypeScript で Top Level "for-await" が実装されていない、というオチでした。。 Top Level "for await" not supported, but should be #37402

ということで、しばらくは Top Level "for-await" は使わないようにしましょう。

Deno 所感

Getting Started から Third Party Module を使ってみるところまでをやってみて感じたことは、

  • Install からサンプルを動かすまではあっという間。
  • API は Web フレンドリーでとっつきやすい。
  • Style Guide は目を通しておいたほうがいい。
  • Module の import はちょっとツライと思ったが pika.dev のおかげでだいぶ和らぎそう。

という感じでした。現時点では Node.js を置き換えるほどではありませんが、将来がとても楽しみです。今後の動向に注目していきたいと思います。

というわけで、Deno 知らなかった、興味はあるけど触ったことなかった、という方はこれを期にぜひトライしてみてください。

Let’S Try Deno!