10/20 にリリースされた Node.js v15 の主な変更点を紹介します。
15,000 文字以上あるので、適宜気になったところをお読みください。
- npm v7 が同梱
- V8 v8.6 ES2021 の機能追加
- Web Crypto API の追加
- AbortController の追加
- EventTarget の追加
- Unhandled Rejections が発生したときエラーになるように変更(終了ステータスが 1 に変わる)
- QUIC の実験的実装
- timers/promises の追加
- stream/promises の追加
- require('assert').strict を require('assert/strict') で読み込む
- require('dns').promises を require('dns/promises') で読み込む
- file URL の仕様追随
- Node.js v15 に関するその他記事
- 最後に
npm v7 が同梱
先日 npm v7 がリリースされました 🎉
Node.js v15 には npm v7 が同梱されます。
npm v7 は workspace 機能が追加されたり yarn.lock を読むようになったりと大きな変更がされています。 詳しくは @watildeさんの記事を読んでください。
V8 v8.6 ES2021 の機能追加
JavaScript エンジン V8 が新しくなりました。
詳細な V8 の変更点については公式ブログをご確認ください。
この記事では JavaScript に関する内容を紹介します。
V8 8.5 では ES2021 に入る JavaScript の新しい機能が実装されました。
V8 8.6 では Number.prototype.toString()
のパフォーマンスが ~75% 改善されています。
この記事では V8 8.5 で追加された ES2021 の機能を紹介します。
Promise.any and AggregateError
Promise.any()
は引数で渡された Promise
オブジェクトの配列の中で最速で resolve
された結果を取得します。配列の中の Promise
がすべて reject
されると AggregateError
を投げます。
AggregateError
は複数のエラーを 1 つのエラーにまとめたエラーオブジェクトです。Promise.any()
のように 1 つの操作で複数エラーが発生する場合に使われます。
どれか 1 つの Promise
が resolve
されればエラーにならないという仕様を利用すれば、ある 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
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
次のように論理演算子 ||
&&
と Nullish coalescing operator ??
を使った x || (x = y)
のような代入を x ||= y
と簡潔に書けるようになります。x
が falsy
な値の場合 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 の追加
WebCrypto API は JavaScript で暗号化や復号、署名やその検証等の処理を行うことができる Web 標準の API です。
仕様: Web Cryptography API MDN: Web Crypto API - Web API | MDN
WebCrypto API ほとんどのブラウザで実装されています。
Can I use... Support tables for HTML5, CSS3, etc
Node.js には昔から Core API として crypto
を提供してきていましたが、ブラウザ互換(Web 標準)ではなく独自の API でした。
Node.js はブラウザとの互換性を重視するようになってきており、ブラウザ互換の API が増えてきています。
次のコードは WebCrypto API で RSA 暗号を使った公開鍵と秘密鍵のペアを生成するコードです。
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 についてはドキュメントをご確認ください。
AbortController の追加
これもブラウザ互換(Web 標準)な API です。fetch()
によるリクエストの中止(abort)ができます。
experimental な機能ですが、フラグを付けなくても使うことができます。
実装は @mysticateaさんの abort-controller
をベースにしています。
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 への実装については継続議論中です。
EventTarget の追加
昔から EventEmitter
というイベント駆動なコードを書くためのクラスがありました。しかし、このクラスはブラウザのイベントを扱う EventTarget
とは互換性がありません。
ブラウザ互換が重要視されてきている Node.js にも EventTarget
が追加されました。
この 2 つは Node.js v14 から実装されていましたが、ユーザーに公開されていませんでした。
v15 からユーザーも使うことができます。
この EventTarget
と Event API
はグローバルなクラスのため次のように書くことが出来ます。
const target = new EventTarget(); target.addEventListener("foo", (event) => { console.log("foo is called"); }); const ev = new Event("foo"); 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')
に転送されます。
MessageChannel
は worker_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
に変わる)
これまでは Promise
が reject
されたときにハンドリングされていない 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 の実験的実装
QUIC は HTTP/3 のトランスポート層にあたる次世代プロトコルです。
まず Google が TCP 並の信頼性を持つ UDP 上で動作するプロトコルとして開発していました。その後 IETF が標準化しました。
先日、Chrome が HTTP/3 と IETF QUIC のサポートを開始すると発表しました。
QUIC および HTTP/3 については @flano_yuki さんの HTTP/3 についての解説が詳しいです。
Node.js での QUIC の使い方など詳しい解説は @L_e_k_o さんがブログを書いてくれているのでぜひ読んで動かしてみてください。
まだ実験的な機能で --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 の追加
Node.js には古くから timers
API があります。これは setTimeout()
、setInterval()
、setImmediate()
といったスケジュール用の関数を提供しています。しかし、これらの関数はグローバルに晒されているので利用は稀だと思いますが、モジュールとしても提供されています。
この 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 の追加
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 から使えることを以前の記事で紹介しました。
このようなエイリアスに関して他の API でも対応が進められています。
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' が厳密には違うのでエラーになる
require('dns').promises を require('dns/promises') で読み込む
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 が出ています。
https://github.com/nodejs/node/pull/34055github.com
https://github.com/nodejs/node/pull/34962github.com
file URL の仕様追随
WHATWG の URL の仕様に対する実装漏れなどが修正されています。
先日、file URL のノーマライゼーションに関して仕様の修正が行われました。
この変更は破壊的変更です。もし 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 リポジトリをウォッチすると得ることができます。
最後までお読みいただきありがとうございました。不備や質問がございましたら、@shisama_までメンションするかブコメなどでコメントください。