別にしんどくないブログ

技術のことや読書メモを書いています

Node.js v15 の主な変更点

f:id:Shisama:20200422011813p:plain

10/20 にリリースされた Node.js v15 の主な変更点を紹介します。

nodejs.org

15,000 文字以上あるので、適宜気になったところをお読みください。

npm v7 が同梱

github.com

先日 npm v7 がリリースされました 🎉

npm Blog Archive: Release v7.0.0

Node.js v15 には npm v7 が同梱されます。

npm v7 は workspace 機能が追加されたり yarn.lock を読むようになったりと大きな変更がされています。 詳しくは @watildeさんの記事を読んでください。

blog.watilde.com

V8 v8.6 ES2021 の機能追加

github.com

JavaScript エンジン V8 が新しくなりました。
詳細な V8 の変更点については公式ブログをご確認ください。

この記事では JavaScript に関する内容を紹介します。

v8.dev

V8 8.5 では ES2021 に入る JavaScript の新しい機能が実装されました。
V8 8.6 では Number.prototype.toString() のパフォーマンスが ~75% 改善されています。

この記事では V8 8.5 で追加された ES2021 の機能を紹介します。

Promise.any and AggregateError

developer.mozilla.org

developer.mozilla.org

Promise.any() は引数で渡された Promise オブジェクトの配列の中で最速で resolve された結果を取得します。配列の中の Promise がすべて reject されると AggregateError を投げます。

AggregateError は複数のエラーを 1 つのエラーにまとめたエラーオブジェクトです。Promise.any()のように 1 つの操作で複数エラーが発生する場合に使われます。

Promise.any()の動作

どれか 1 つの Promiseresolve されればエラーにならないという仕様を利用すれば、ある CDN からモジュールを Dynamic import で fetch を試みてエラーになったときに別の CDN にフォールバックするということも行えます。

try {
  const someModule = await Promise.any([
    import("https://primary.example.com/some-module"),
    import("https://secondary.example.com/some-module"),
  ]);
  // primary.example.com、secondary.example.com どちらかから取得したモジュール
  console.log(someModule.message);
} catch (error) {
  // すべてのPromiseがrejectされたらエラー
  console.assert(error instanceof AggregateError);
  console.log(error.errors);
}

Promise.any()を使わなければ、以下のように try-catch で書きます。この場合、並列ではなく直列でリクエストすることになります。

let someModule;
try {
  someModule = await import("https://primary.example.com/some-module");
} catch {
  someModule = await import("https://secondary.example.com/some-module") ;
}
console.log(someModule.message);

String.prototype.replaceAll

developer.mozilla.org

String.prototype.replaceAll() は文字列から指定したパターンの文字列をすべて置換するメソッドです。

const longUrl =
  "https://xxx.com?q=aaa+bbb+ccc+ddd+eee+fff+ggg+hhh+iii+jjj+kkk+lll+mmm+nnn+ooo+ppp+qqq+rrr+sss+ttt+uuu+vvv+www+xxx+yyy+zzz";

// '+' を ' '(半角スペース)に置換
const replacedUrl = longUrl.replaceAll(/\+/g, " ");
// "https://xxx.com?q=aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll mmm nnn ooo ppp qqq rrr sss ttt uuu vvv www xxx yyy zzz"

Logical assignment operators

developer.mozilla.org

次のように論理演算子 || &&Nullish coalescing operator ?? を使った x || (x = y) のような代入を x ||= y と簡潔に書けるようになります。xfalsy な値の場合 y が代入されます。

// x && (x = y)
// x と y が truthy な値の場合、y の値が代入される
x &&= y;

// x || (x = y)
// x が falsy な値の場合、y の値が代入される
x ||= y;

// x ?? (x = y)
// x が null または undefined な値の場合、y の値が代入される
x ??= y;

Web Crypto API の追加

github.com

WebCrypto APIJavaScript で暗号化や復号、署名やその検証等の処理を行うことができる Web 標準の API です。

仕様: Web Cryptography API MDN: Web Crypto API - Web API | MDN

WebCrypto API ほとんどのブラウザで実装されています。

WebCrypto ブラウザ互換性 Can I use... Support tables for HTML5, CSS3, etc

Node.js には昔から Core API として crypto を提供してきていましたが、ブラウザ互換(Web 標準)ではなく独自の API でした。

nodejs.org

Node.js はブラウザとの互換性を重視するようになってきており、ブラウザ互換の API が増えてきています。

次のコードは WebCrypto APIRSA 暗号を使った公開鍵と秘密鍵のペアを生成するコードです。

const { subtle } = require("crypto").webcrypto;
const generateRsaKey = async () => {
  const { publicKey, privateKey } = await subtle.generateKey(
    {
      name: "RSASSA-PKCS1-v1_5",
      modulusLength: 4096,
      publicExponent: new Uint8Array([1, 0, 1]),
      hash: "SHA-256",
    },
    true,
    ["sign", "verify"]
  );

  return {
    public: publicKey,
    private: privateKey,
  };
};

MDN の SubtleCrypto.generateKey() を見てもらうとわかると思いますが、WebCrypto API はブラウザの APIインターフェイスが同じです。
また、従来の crypto は Promise を返さないので promisify する必要がありましたが、WebCrypto API は Promise を返します。

もちろん encrypt(暗号化)や decrypt(復号化)の API に関してもブラウザ互換になっています。その他の API についてはドキュメントをご確認ください。

nodejs.org

AbortController の追加

github.com

これもブラウザ互換(Web 標準)な API です。fetch() によるリクエストの中止(abort)ができます。 experimental な機能ですが、フラグを付けなくても使うことができます。
実装は @mysticateaさんの abort-controller をベースにしています。

github.com

AbortController はモジュールではなくグローバルのクラスのため、requreする必要はありません。

const ac = new AbortController();
ac.signal.addEventListener("abort", () => console.log("Aborted!"), {
  once: true,
});
ac.abort();
console.log(ac.signal.aborted);

ちなみに fetch() の Node.js への実装については継続議論中です。

github.com

EventTarget の追加

github.com

昔から EventEmitter というイベント駆動なコードを書くためのクラスがありました。しかし、このクラスはブラウザのイベントを扱う EventTarget とは互換性がありません。
ブラウザ互換が重要視されてきている Node.js にも EventTarget が追加されました。
v15 からユーザーも使うことができます。

nodejs.org

この EventTargetEvent API はグローバルなクラスのため次のように書くことが出来ます。

const target = new EventTarget();

target.addEventListener("foo", (event) => {
  console.log("foo is called");
});

target.dispatchEvent(new Event("foo"));

Node.js EventTarget vs. DOM EventTarget

Node.js の EventTarget はブラウザの EventTarget に対して以下の 2 つの違いがあります。

  • Node.js にはイベントの伝播はないため、EventTarget オブジェクトがネストされていてもイベントが階層を介して伝播しません。
  • イベントリスナーが Promise を返す場合、リジェクトされると同期的に throw されるリスナーと同じふるまいを行います。throw されたり reject された場合、promise.on('error') に転送されます。

https://nodejs.org/dist/latest-v14.x/docs/api/events.html#events_node_js_eventtarget_vs_dom_eventtarget

MessageChannel の追加

MessageChannelworker_thread モジュールの一部として読み込み可能でしたが、グローバルに晒されます。

// Node.js v15 から require する必要がなくなる
// const { MessageChannel } = require('worker_threads');
const { port1, port2 } = new MessageChannel();

port2.on("message", (message) => console.log(message));
port2.on("close", () => console.log("closed!"));

port1.postMessage("foobar");
port1.close();

Unhandled Rejections が発生したときエラーになるように変更(終了ステータスが 1 に変わる)

github.com

これまでは Promisereject されたときにハンドリングされていない Unhandled Rejections が発生したとき警告が表示されていました。
警告を表示するだけでエラーとしてプロセス終了はしませんでした。 なので、これまで Unhandled Rejections が発生していてもプロセス終了後の終了ステータスは 0 でした。

> node -p "Promise.reject()"
Promise { <rejected> undefined }
(node:76039) UnhandledPromiseRejectionWarning: undefined
(node:76039) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:76039) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
> echo $?
0 # 正常終了している

しかし Node.js v15 からはエラー扱いにして終了ステータスは 1 になります。

> node -p "Promise.reject()"
Promise { <rejected> undefined }
node:internal/process/promises:218
          triggerUncaughtException(err, true /* fromPromise */);
          ^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "undefined".] {
  code: 'ERR_UNHANDLED_REJECTION'
}
> echo $?
1 # 異常終了している

これまでもハンドルされない reject は非推奨でした。非推奨コード DEP0018 として警告を出力していました。 nodejs.org

また、これまでも --unhandled-rejections フラグを用いることでハンドルされない reject の挙動を変更できます。 フラグで指定しなかった場合、これまでは warn になっていましたが、throwされるようにデフォルトの挙動の変更になります。 また、この --unhandled-rejections フラグはこれまで通り使うことができます。

> node --unhandled-rejections=strict -p "Promise.reject()"
Promise { <rejected> undefined }
internal/process/promises.js:194
        triggerUncaughtException(err, true /* fromPromise */);
        ^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "undefined".] {
  code: 'ERR_UNHANDLED_REJECTION'
}
> echo $?
1 # 異常終了

その他、Unhandled Rejections の終了ステータスを変更する方法については以下の記事が参考になります。 efcl.info

QUIC の実験的実装

github.com

QUIC は HTTP/3 のトランスポート層にあたる次世代プロトコルです。

まず GoogleTCP 並の信頼性を持つ UDP 上で動作するプロトコルとして開発していました。その後 IETF が標準化しました。

先日、Chrome が HTTP/3 と IETF QUIC のサポートを開始すると発表しました。

blog.chromium.org

QUIC および HTTP/3 については @flano_yuki さんの HTTP/3 についての解説が詳しいです。

asnokaze.hatenablog.com

Node.js での QUIC の使い方など詳しい解説は @L_e_k_o さんがブログを書いてくれているのでぜひ読んで動かしてみてください。

blog.leko.jp

まだ実験的な機能で --experimental-quic フラグを付けて Node.js をコンパイルすることで利用可能になります。

> ./configure --experimental-quic
> make -j4

Node.js のビルドについては BUILDING.md を読むと詳しく説明されています。

node/BUILDING.md at master · nodejs/node · GitHub

// QUIC は TLS 必須なので鍵と証明書を取得する
const key = getTLSKeySomehow();
const cert = getTLSCertSomehow();

const { createQuicSocket } = require('net');

// ポート 1234 に紐づく QUIC ソケットの生成
const socket = createQuicSocket({ endpoint: { port: 1234 } });

socket.on('session', async (session) => {
  // ストリーム開始
  session.on('stream', (stream) => {
    stream.end('Hello World');

    // ストリームのイベントを定義
    stream.setEncoding('utf8');
    stream.on('data', console.log); // データを受け取ったとき
    stream.on('end', () => console.log('stream ended')); // ストリームが終了したとき
  });

  // QuicStream を新規生成 https://github.com/nodejs/node/blob/v15.0.0-proposal/doc/api/quic.md#quicstream
  const uni = await session.openStream({ halfOpen: true });
  uni.write('hi ');
  uni.end('from the server!');
});

// サーバー
// https://github.com/nodejs/node/blob/v15.0.0-proposal/doc/api/quic.md#quicsocketlistenoptions
(async function() {
  await socket.listen({ key, cert, alpn: 'hello' });
  console.log('The socket is listening for sessions!');
})();

timers/promises の追加

github.com

Node.js には古くから timers API があります。これは setTimeout()setInterval()setImmediate() といったスケジュール用の関数を提供しています。しかし、これらの関数はグローバルに晒されているので利用は稀だと思いますが、モジュールとしても提供されています。

nodejs.org

この timers の関数を Promise な関数として使うことができるようになります。

これまでは util.promisify() を利用しなければいけませんでした。

const util = require("util");
const wait = util.promisify(setTimeout);
const main = async () => {
  console.log("start");
  await wait(10000); // 10秒待つ
  console.log("waited 10 seconds");
};

Node.js v15 移行は次のように Promise オブジェクトを読み込むことができます。

const { setTimeout: wait } = require("timers/promises");
const main = async () => {
  console.log("start");
  await wait(10000); // 10秒待つ
  console.log("waited 10 seconds");
};

stream/promises の追加

github.com

Stream API にも同じように Promise 化された関数が追加されました。

const { pipeline } = require("stream/promises");
const fs = require("fs");
const zlib = require("zlib");

const main = async () => {
  const rs = fs.createReadStream("some.txt");
  const ws = fs.createWriteStream("some.txt.gz");
  const gzip = zlib.createGzip();
  let finished = false;
  ws.on("finish", () => {
    finished = true;
  });
  await pipeline(rs, gzip, ws);
  console.log(finished); // true
};

require('assert').strict を require('assert/strict') で読み込む

require('fs').promisesエイリアスである require('fs/promises') に関してはすでに Node.js v14 から使えることを以前の記事で紹介しました。

shisama.hatenablog.com

このようなエイリアスに関して他の API でも対応が進められています。

github.com

require("assert").strict === require("assert/strict"); // true

Node.js の assert には deepEqual()deepStrictEqual() があります。次のようにこの strict プロパティを用いることで deepEqual()deepStrictEqual() のように振る舞います。

const assert = require('assert').strict;

assert.deepEqual([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]);
// AssertionError: Expected inputs to be strictly deep-equal:
// 3 と '3' が厳密には違うのでエラーになる

nodejs.org

require('dns').promises を require('dns/promises') で読み込む

github.com

dns.promises 自体は Node.js v10 からある API です。

require("dns").promises === require("dns/promises"); // true
const { Resolver } = require('dns/promises');
const resolver = new Resolver();

(async function() {
  const addresses = await resolver.resolve4('example.org');
})();

そのほかにも require('util/types');require('path/posix'); など今回はリリースされませんでしたが PR が出ています。

github.com

github.com

file URL の仕様追随

github.com

WHATWG の URL の仕様に対する実装漏れなどが修正されています。

先日、file URL のノーマライゼーションに関して仕様の修正が行われました。

https://github.com/whatwg/url/pull/544github.com

この変更は破壊的変更です。もし file:/// から始まる URL を使った実装になっている場合、影響を受けるかも知れません。

繰り返しになりますが、Node.js は Web 標準に準拠していこうとしています。
なので、もし Node.js のブラウザ や ECMA の仕様と合っていない箇所を見つけたら、Node.js に issue を送っていただけると幸いです。

Node.js v15 に関するその他記事

他の方も Node.js v15 について紹介してくれているので、ぜひ御覧ください!(見つけたら更新します)

最後に

Node.js v15 は npm v7 になったり、WebCrypto API や EventTarget や AbortController が使えるようになり Web 標準を追従した API が増えてきました。
今年は ES Modules がフラグなしでも使えるようになりブラウザ互換がかなり進んだ印象です。今後は fetch() なども実装が始まっていくと思います。(過去に node-fetch を使った実装の PR も出ていましたが、閉じられました。https://github.com/nodejs/node/pull/27979

Node.js は 4 月末と 10 月末にメジャーバージョンのリリースを行っています。また v16 は来年の 4 月末ごろにリリースされると思います。その頃には v10 のメンテナンスも終了します。リリース日についての最新情報は nodejs/release リポジトリをウォッチすると得ることができます。

https://github.com/nodejs/Releasegithub.com

最後までお読みいただきありがとうございました。不備や質問がございましたら、@shisama_までメンションするかブコメなどでコメントください。