虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Deno 1.14 へのアップデートと変更事項まとめ

皆さんこんにちは。最近、転居先を探しています。おっくんです。

去る 2021 年 9 月 14 日に Deno 1.14 がリリースされました。 今回も、リリースノートを参考に 変更事項の気になるところを紹介します。

実行環境

  • Docker イメージ denoland/deno:centos(確認時点では Deno 1.14.0 でした)

Deno 1.14

Deno 1.14 での変更事項をDeno 1.14 リリースノートを元に確認します。

Web Crypto API の追加

これまでのリリースでも少しづつ機能が拡張されていた Web Crypto API に新たな実装が追加されました。

  • crypto.subtle.exportKey():
    • HMAC キーは、JWK 形式と raw 形式でのエクスポート機能を追加
    • RSA キーを pkcs#8 形式でのエクスポート機能を追加
  • crypto.subtle.importKey():
    • HMAC キーは、JWK 形式と raw 形式でのエクスポート機能を追加
    • RSA キーを pkcs#8 形式でのエクスポート機能を追加
    • PBKDF2 キーの raw 形式でのインポート機能を追加
  • crypto.subtle.generateKey():
    • RSA-OAEP キーの生成のサポートを追加
    • ECDH キーの生成がサポートを追加
    • AES キーの生成がサポートを追加
  • crypto.subtle.deriveBits():
    • 導出アルゴリズムとして PBKDF2 のサポートが追加
    • 導出アルゴリズムとして HKDF のサポートが追加
  • crypto.subtle.verify():
    • ECDSA 署名検証のサポートが追加
  • crypto.subtle.encrypt():
    • RSA-OAEP 暗号化 のサポートが追加

継続して Web Crypto API の改善に取り組んでおり、年末までにAPIの完成を目指しているそうです。 Web Crypto API のロードマップが独立して issue として公開されています。

github.com

deno lint, deno fmt にカスタマイズオプションが追加されました

これまで、deno lint と deno fmt のカスタマイズ機能は提供されていませんでした。 しかし、1年間に渡ってユーザーからリクエストが寄せられており、 このリリースから特定の正当なニーズに対応して使用するオプションで調整を行った deno lint と deno fmt を実行できるようになりました。

しかし、基本的にはデフォルトオプションでの利用を推奨するそうです。 deno lint には、デフォルトで、recommended lint ルール が適用され実行されます。

deno lint には、次の3つのオプションが追加されます。

  • --rules-tags 適用するルールセットのタグ
  • --rules-exclude 除外するルール
  • --rules-include 適用するルール

deno fmt には、次の5つのオプションが追加されます。

  • --options-indent-width (default : 2)インデントに使用するスペースの個数
  • --options-line-width (default : 80)最大線幅
  • --options-prose-wrap (default : always)折り返し設定
  • --options-single-quote (default : false)一重引用符「'」を使用するかどうか?
  • --options-use-tabs (default : false)インデントにスペースの代わりにタブを使用するかどうか?

そして、これらの設定は tsconfig.json の拡張として記述して deno lint と deno fmt の実行時に --config で与えることができます。

リリースノートでは以下のように紹介されています。

[拡張前のtsconfig.json]

{
  "compilerOptions": {
    "allowJs": true,
    "lib": ["deno.window"],
    "strict": true
  }
}

[拡張されたtsconfig.json]

{
  "compilerOptions": {
    "allowJs": true,
    "lib": ["deno.window"],
    "strict": true
  },
  "lint": {
    "files": {
      "include": ["src/"],
      "exclude": ["src/testdata/"]
    },
    "rules": {
      "tags": ["recommended"],
      "include": ["ban-untagged-todo"],
      "exclude": ["no-unused-vars"]
    }
  },
  "fmt": {
    "files": {
      "include": ["src/"],
      "exclude": ["src/testdata/"]
    },
    "options": {
      "useTabs": true,
      "lineWidth": 80,
      "indentWidth": 4,
      "singleQuote": true,
      "proseWrap": "preserve"
    }
  }
}

現在は --config でファイルを与える必要がありますが、自動検出機能を提供する予定があるそうです。 あくまで必要に応じて使用する機能であり、使用の「必須」はなく、デフォルトオプションで最適に機能するとされています。 標準的な lint ルールとフォーマットルールを使用するのであれば、一切関与せずにおけるところがいいポイントだと感じます。

URL パターンの検証手段の追加

このリリースで、URLをパターンマッチするためのWeb プラットフォーム APIである URLPattern API が追加されます。 このAPIは、人気ライブラリの、path-to-regexp の組み込み実装になっています。

github.com

リリースノートでは、以下のコードが紹介されています。

const pattern = new URLPattern({ pathname: "/books/:id" });

console.log(pattern.test("https://example.com/books/123"));
// => true

console.log(pattern.test("https://example.com/books/123/456"));
// => false

console.log(pattern.test("https://example.com/books"));
// => false

console.log(pattern.exec("https://example.com/books/123").pathname);
// => { input: "/books/123", groups: { id: "123" } }

こちらは、urltest.js と名前をつけて JavaScript としては実行可能ですが、urltest.ts と名前を付けて TypeScript としては、以下のエラーになります。

error: TS2531 [ERROR]: Object is possibly 'null'.
console.log(pattern.exec("https://example.com/books/123").pathname); // { input: "/books/123", groups: { id: "123" } }
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    at file:///usr/src/app/urltest.ts:6:13

exec が返すオブジェクトがpathnameを持たない可能性があるので、オプショナルチェーン演算子をつけ以下のようにします。

console.log(pattern.exec("https://example.com/books/123")?.pathname);

URLPattern は、パスだけでなくプロトコルや、ホスト名クエリ文字列でもパターンマッチができ、正規表現も以下のように使用できます。

// 正規表現を使う
let pattern = new URLPattern({ pathname: "/books/:id([1-9]*)" });

console.log(pattern.test("https://example.com/books/23412312"));
// => true

console.log(pattern.test("https://example.com/books/23412v312"));
// => false

// ホスト名をパターンマッチする
pattern = new URLPattern({ hostname: "hoge.com" });

console.log(pattern.test("https://example.com/books/1"));
// => false

console.log(pattern.test("https://hoge.com/books/1"));
// => true

URLPattern API は、現在 --unstable を必要とする機能となっています。 Chrome 95 でのURLPattern API の安定化と合わせて Deno 1.15 での安定化が予定されています。

MDNのドキュメントは、現在はプレビューのみ公開されており、近日正式に公開になるようです。

pr8734.content.dev.mdn.mozit.cloud

ネイティブ Web サーバーの WebSocketAPI が安定化

Deno 1.14 で ネイティブ Web サーバーが安定化しましたが、Deno 1.12 で登場したネイティブ Web サーバーの WebSocketAPI の Deno.upgradeWebSocket() が安定化しました。

安定化したので、実行時の --unstable が不要になりました。

Web Worker 間で コピーせずにArrayBuffer を転送てきるようになりました

Deno 1.14 では、WebWorker 間でコピーせずに ArrayBuffer を転送する機能が追加されています。 コピー処理が無いので、大きなサイズのバッファを高速に転送できます。

コピーせずに転送するには、postMessage() 関数のオプションで、transfer を設定します。 以下のようにして使います。

[WebWorker 呼び出し側 main.js]

const url = new URL("./worker.js", import.meta.url).href;

const worker = new Worker(url, { type: "module" });

const buffer = new ArrayBuffer(4);
const arr = new Int16Array(buffer);

arr[0] = 12
arr[1] = 36

worker.postMessage(buffer, [buffer]);
// postMessage の第一引数として与えたArrayBuffer を、配列に入れ第二引数にも設定します。

[worker.js]

self.onmessage = (e) => {
  console.log("got data:", e.data);
  const arr = new Int16Array(e.data);
  console.log("got Int16Array:", arr);
};

実行すると次のようになります。

$ deno run --allow-read main.js
got data: ArrayBuffer {}
got Int16Array: Int16Array(2) [ 12, 36 ]

この時点では、メインワーカーで設定した値をワーカースレッドで読み取っただけになります。 main.js を以下のように書き換えます。

[WebWorker 呼び出し側 main.js エラーパターン]

const url = new URL("./worker.js", import.meta.url).href;

const worker = new Worker(url, { type: "module" });

const buffer = new ArrayBuffer(4);
const arr = new Int16Array(buffer);

arr[0] = 12;
arr[1] = 36;

worker.postMessage(buffer, [buffer]);

// 転送した ArrayBuffer をメインワーカーで再度使う
arr[1] = 123;
worker.postMessage(buffer, [buffer]); // <= この処理でエラーになる

このようにすると以下のようにエラーになります。

$ deno run --allow-read worker/main.js
got data: ArrayBuffer {}
got Int16Array: Int16Array(2) [ 12, 36 ]
thread 'worker-0' panicked at 'coding error: either js is polling or the worker is terminated', runtime/web_worker.rs:525:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

これは、単純に値の転送がされているわけではなく、ArrayBufferの所有権の譲渡が行われているために、メインワーカーで使用できなくなっている状態です。

詳しい説明は、MDN の Worker.prototype.postMessage()にもあるのでこちらも確認されるとわかりやすいと思います。

developer.mozilla.org

ファイルロックを行うためのAPIが追加

Deno 1.14 では、ファイルロックを行う以下の4つのAPIが追加されています。

  • Deno.flock
  • Deno.flockSync
  • Deno.funlock
  • Deno.funlockSync

動作確認してみます。

[lock1.ts]

const file = Deno.openSync("./lock/text")
Deno.flockSync(file.rid, true)

prompt("exit?")  // <= 入力を待って終了。ここまではロックが維持される

[lock2.ts]

const file = Deno.openSync("./lock/text")
Deno.flockSync(file.rid, true) // <= ロックを取得できるまで待機

console.log(Deno.readTextFileSync("./lock/text"))

以上の二つを用意し、二つのコンソールを用意しそれぞれ起動します。

deno run --unstable --A lock1.ts

# 別のコンソールで
deno run --unstable --A lock2.ts

起動すると、lock1.ts を終了するまで、lock2.tsはファイルの内容を表示せずに待ちます。 lock2.ts で、Deno.flockSync を記述しない場合、後続の出力処理は lock1.ts がロックを継続しても実行できました。

この動作は、deno doc の、Deno.flock の項目を見るとより詳細に理由が書かれています。

doc.deno.land

Deno.flock(及び Deno.flockSync)は、ロックのシステムにアドバイザリファイルシステムロックを使用しています。 このことにより、「ロックされているか?」参照せずに処理すれば、読み書き自体は可能です。

OS signals API が変更される

Deno 1.14 では、OS signals API が変更になります。 これまでは、引数に、Deno.Signal.SIGTERM のように Deno.Signal のメンバを渡していたものが、文字列で渡すことができるようになります。

リリースノートより、以下のように変更になります。

// Deno 1.14 未満
for await (const _ of Deno.signal(Deno.Signal.SIGTERM)) {
  console.log("got SIGTERM!");
}

// Deno 1.14 
for await (const _ of Deno.signal("SIGTERM")) {
  console.log("got SIGTERM!");
}

また、このリリースで Deno.signals 名前空間に用意されていた quit や terminate といったヘルパーメソッドは、削除されました。 この変更は、signals API の安定化のために行われているそうです。

fetch API が、相互 TLSをサポート

このリリースで、サーバーに対してクライアントを認証する相互 TLS を fetch API で使用できるようになります。 リリースノートでは、以下の使用例を挙げています。

  • Kubernetes API サーバーへのクライアント認証。
  • CloudflareAccess のような、ゼロトラストプラットフォームへのクライアント認証。

併せて、以下のように使用例が紹介されています。

const client = Deno.createHttpClient({
  certChain: Deno.readFileSync("./cert.pem"),
  privateKey: Deno.readFileSync("./key.pem"),
});
const resp = await fetch("https://example.com/", { client });

カスタマイズしたクライアントを渡す形で、使用します。

環境変数 DENO_AUTH_TOKENS が、Basic 認証をサポート

Deno 1.8 から DENO_AUTH_TOKENS に Bearer トークンを設定することで、認証が必要なサーバーからコードをフェッチできるようになっていました。 Deno 1.14 から、Basic 認証も設定できるようになりました。

リリースノートより以下のように設定をします。

DENO_AUTH_TOKENS=testuser123:testpassabc@127.0.0.1:4554

記述方法は、<ユーザー名>:<パスワード>@<ホスト名> となります。

URL 解析が3倍高速になりました。

数週間前に、「URLの解析が遅い」という報告があったそうです。 URLの解析は、サーバーアプリケーションでは頻繁に行われる操作であるため大きな問題で、改善できればWeb サーバーのパフォーマンスを良くできます。

Deno 1.14 は、Deno 1.13に比べて3倍高速になったそうです。 内部の改善で3倍性能が向上するというのは、見習うべき点があると感じます。

子プロセスに、gid(group id)とuid(user id)を指定できるようになりました

こちらも --unstable フラグを要求する機能として、子プロセスに gidとuidを指定できるようになりました。 リリースノートでは以下のように紹介されています。

Deno.run({
  cmd: [
    "echo",
    "Hello from root user",
  ],
  uid: 0,
});

Deno.run({
  cmd: [
    "echo",
    "Hello from root group",
  ],
  gid: 0,
});

ここで気になるのは存在しない gid か uid を設定した時にどうなるのか?です。 確認のため /etc/passwd に載っていない uid を設定しても実行できました。

std/http モジュール がより高速になりました。

std ライブラリの 0.107.0 では、http モジュールが大きく改修されます。 Deno 1.13 では、ネイティブ HTTP サーバー API が安定化になりました。それに合わせて http/server.ts モジュールはこちらを使うようになりました。 このことで、よりユーザーフレンドリーなAPIになったそうです。

std/http 0.107.0 の http/server.ts を使った サーバーの実装は、次のようになります。

[app.ts]

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

listenAndServe(":8080", () => new Response("Hello World"));

非常にシンプルです。 https://deno.land/std@0.107.0/http/server.ts には、listenAndServeTls といったメソッドも公開されています。

比較として、std/http 0.106.0 では以下のようになっていました。

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

今までよく見ていた実装ですね。 ちなみに、この旧モジュールは、std/http 0.107.0 でも http/server_legacy.ts と名を変えて残っています。 ただし、非推奨になっているので注意が必要です。

ここで、http/server.ts を使って簡単にアプリケーションを作ってみました。

[app.ts]

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

const now = (req: Request): Response => {
  return new Response(
    new Date().toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" })
  );
};

const root = (req: Request): Response => {
  return new Response("Hello World");
};

const notFound = (req: Request): Response => {
  return new Response("Not Found", { status: 404 });
};

const router = (req: Request): Response => {
  if (new URLPattern({ pathname: "/now" }).test(req.url)) {
    return now(req);
  } else if (new URLPattern({ pathname: "/" }).test(req.url)) {
    return root(req);
  } else {
    return notFound(req);
  }
};

listenAndServe(":8080", router);

deno run --unstable --allow-net app.ts で起動します。 URLPattern も使用してみました。 これまでに比べれば、かなり見やすいように感じます。

TypeScript 4.4 が導入されました

Deno 1.14 では、TypeScript 4.4 が導入されました。

TypeScript 4.4 は、静的初期化ブロックをクラスに記述できるなどの機能追加がされています。 詳しくは、こちらのAnnouncing TypeScript 4.4を参照ください。

devblogs.microsoft.com

v8 が 9.4 にアップデートされました。

v8 のバージョンアップにより、バグフィックスとともにこちらも、静的初期化ブロックの導入が含まれています。
v8 のバージョンアップと Typescript 4.4 はセットで必要だったようです。

その他

  • VsCode 拡張機能のアップデート

まとめ

ネイティブwebサーバーAPIのwebsocket対応が、 --unstable が外れ、std/httpライブラリで旧APIが別名で登録されたりなど大きな転換点を感じました。 また、v8とtypescriptでの、静的初期化ブロックの導入で今後実際にコーディングしていくときにどの様にしていくのがベターとされるのか興味が湧きます。

ではまた、次のリリースでお会いしましょう。

P.S.

採用情報

■募集職種
yumenosora.co.jp

カジュアル面談も随時開催中です

■お申し込みはこちら!

news.toranoana.jp

■ToraLab.fmスタートしました!

メンバーによるPodcastを配信中! 是非スキマ時間に聞いて頂けると嬉しいです。 anchor.fm

■Twitterもフォローしてくださいね!

ツイッターでも随時情報発信をしています
twitter.com