別にしんどくないブログ

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

Node.js v18 の主な変更点

https://cdn-ak.f.st-hatena.com/images/fotolife/S/Shisama/20200422/20200422011813.png

Node.js v18がリリースされました 🎉

nodejs.org

この記事では Node.js v18 の主な変更点を抜粋して紹介します!

fetch() がフラグ無しで実行可能に (experimental)

github.com

Node.js で fetch() がフラグ無しで実行できるようになりました。

ブラウザの fetch() とインターフェースは合わせていますが、fetch() はもともとブラウザでリソースを取得するすべてのFetchingを JS の API として使えるようにする。XHRで行う API へのリクエストの代替というだけでなく、 <img> 要素などで取得も JS から行えるようにするものです。そのため、Node.js の fetch() がブラウザの fetch() と全く同じものというわけではありません。
とはいえ、HTTP クライアントとしての fetch() 関数自体のインターフェースは同じです。
また、Node.js のモジュールとして提供されているわけではなく、ブラウザの fetch() と同じようにグローバルに生えています。
ですから、require関数やimportを使わなくても次のようなコードが書けます( https://github.com/nodejs/node/pull/42262 から引用)。

const res = await fetch('https://nodejs.org/api/documentation.json');
if (res.ok) {
  const data = await res.json();
  console.log(data);
}

実は fetch() は Node.js v17.5.0 から利用可能です。しかし、次のように --experimental-fetch フラグを使って実行しなければいけませんでした。

node index.js --experimental-fetch

しかし、 Node.js v18.0.0 からはフラグなしでも利用可能です。

反対に fetch() を無効にしたいときは次のように --no-experimental-fetch フラグを使って実行しなければいけません。

node index.js --no-experimental-fetch

また、フラグなしで fetch() が使えるようになったとしても、まだ experimental (実験的な機能)であることに注意しなければいけません。
実験的な機能であるため、まだ安定していないので、プロダクトではまだ使わないほうが安全でしょう。

少し内部的な話をすると、この fetch() は undici という Node.js とは別のソフトウェアを内部では利用して実装されています。

github.com

もともと undici は Node.js のメンテナーの一人が個人開発していましたが、現在では nodejs organization へ移管されており、複数のメンテナーがいます。 開発初期から Node.js の http モジュールの代わりとして使えるように、nettls を使ってフルスクラッチで開発されました。

元々 http モジュールはレガシーなコードでメンテナンスも難しい状態でした。
その中でも HTTP パーサーはメンテナンスが難しい状態でした。

そこで undici では llhttp という次世代の HTTP パーサーを利用しています。
現在では、llhttp は Node.js 本体の HTTP パーサーとしても利用されており、nodejs organization でメンテナンスがされています。

github.com

llhttp はメンテナンス性を考慮して、TypeScript で開発されています。
しかし、高速に実行するために TypeScript のコードを C に変換しています。

また、Node.js や JavaScript からだけでなく、PythonRuby からも使えるようになっています。

undici はこの llhttp を Wasm へビルドされたものを利用しています。

また、undici 自体は次のように単体でも利用可能です。

import { request } from 'undici'

const {
  statusCode,
  headers,
  trailers,
  body
} = await request('http://localhost:3000/foo')

for await (const data of body) {
  console.log('data', data);
}

あと、fetch() に関連して、FormDataHeadersRequestResponse も追加されています。これらもグローバルに定義されています。

HTTP requestTiemout()のデフォルト値の変更

github.com

server.requestTimeoutタイムアウト時間のデフォルト値が 0 から 300000 (5分)になります。
これにより、これまでブラウザなどクライアントからのリクエストの待機時間が長くてもエラーにならなかったのが、5分でエラーになる可能性があります。
これまでデフォルトの値のままにしていた開発者は十分な時間を確保できる値を設定しなければいけません。
server.requestTimeout には任意の値を設定することができます。
しかし、0 に設定するのは推奨されていません。なぜなら、0 は永遠に待つことを意味しており、DoS攻撃などがあったときに攻撃が終わらないかぎりリクエスト待ち状態になります。
このような観点からデフォルトの値も5分へと変更になったようです。

node:test モジュール(テストランナー)の追加 (experimental)

github.com

Node.js の標準APIとしてテストランナーが追加されました。次のように test モジュールを読み込んで利用します( https://github.com/nodejs/node/pull/42262 から引用)。

import test from 'node:test';
import assert from 'assert/strict';

test('top level test', async (t) => {
  await t.test('subtest 1', (t) => {
    assert.strictEqual(1, 1);
  });

  await t.test('subtest 2', (t) => {
    assert.strictEqual(2, 2);
  });
});

また、skipconcurrencytodo といったオプションも用意されています。

test('skip option', { skip: true }, (t) => {
  // This code is never executed.
});

まだ、experimental な機能であり、単一のファイルの実行しかできないので、JestやMochaなどの既存のテストランナーから乗り換えるのは時期尚早だと思います。
しかし、将来的に stable になれば、ちょっとしたテストなら Node.js の標準テストランナーでも事足りるケースであれば、 Jest などの npm パッケージをインストールしなくてもよくなるかもしれません。

利用するにあたって注意点があります。
この node:test はこれまでの Node.js の標準 API とは違い、 importrequire でモジュールを読み込むときに node: プレフィックスが必須です。
もしプレフィックスを付けずに require('test') とするとユーザーランドの test モジュールを探索しにいきます。他の Node.js の標準 API とは異なる挙動なので注意しましょう。

V8 アップデートによる新しい JavaScriptAPI の追加

github.com

Node.js で利用される JavaScript エンジンの V8 が 10.1 にアップデートしました。
これによりいくつかの新しい JavaScriptAPI が利用可能になります。

Array#findLast(), Array#findLastIndex()

findLast()findLastIndex() は配列(Array)の新しいメソッドです。

find()findIndex() が配列の先頭から指定した条件に一致する要素を探すのに対して、findLast()findLastIndex() は末尾から探索します。

findLast() は配列の末尾から指定した条件に一致する要素の値を返し、findLastIndex() は末尾から条件に一致する要素番号を返します。

const arr = [1, 2, 3, 4, 5]; 

arr.findLast(el => el % 2 === 0);
// 結果は 4(2ではない)

arr.findLastIndex(el => el % 2 === 0)
// 結果は 3 (1ではない)

Intl.supportedValuesOf()

Intl.Locale オブジェクトに格納されている calendarscollationstimeZonesといったプロパティがあります。
たとえば、calendars というプロパティには、どの暦が使われているか一覧が配列で格納されています。

次のコードのように指定される Locale によってサポートされているプロパティは異なります。

const jpLocale = new Intl.Locale('ja');
jpLocale.calendars;
// ['gregory'] が格納されている。

jpLocale.timeZones
// 'ja' は timeZones をサポートしていないので、undefinedになる

Intl.supportedValuesOf() を利用すれば、それらのプロパティがどの Locale をサポートしているか確認できます。

Intl.supportedValuesOf('calendar');
// ['buddhist', 'chinese', 'coptic', 'dangi', 'ethioaa', 'ethiopic', 'gregory', 'hebrew', 'indian', 'islamic', 'islamic-civil', 'islamic-rgsa', 'islamic-tbla', 'islamic-umalqura', 'iso8601', 'japanese', 'persian', 'roc']

Intl.supportedValuesOf('timeZone');
//  ['Africa/Abidjan', 'Africa/Accra', ...]

その他の改善

V8 のアップデートによって、class fields や private class methods のパフォーマンス改善もされています。

Web Streams API のグローバルへの追加、実行時の警告の削除 (experimental)

github.com

github.com

Web Streams API の一連のクラスがグローバルに定義されるようになりました。また実行時の警告が削除されました。

Web Streams API は Web (ブラウザ)の Stream API と同じインターフェースや仕様に沿った新しい Stream の API です。
元々 Node.js には Stream API がありましたが、ブラウザとの互換性がない API でした。
Node.js は Web との互換性を重視するようになり、Web の API を積極的に Node.js 本体に実装しています。
そのため、従来の Node.js の Stream API は残しつつも、Web 互換な Stream API を実装しました。

この Web Streams API には、読み込み専用のReadableStream、書き込み用のWritableStream、変換用の TransformStream をはじめとした多くのクラスが存在します。
以前は 'node:stream/web' から ReadableStream などを import して利用しなければいけませんでしたが、v18 からは次のようにグローバルから読み込んで利用できるようになりました。

// Streamを読み込むためのオブジェクトを生成
const stream = new ReadableStream({
  start(controller) {
    controller.enqueue('a');
  },
});

// 変換処理を行うためのオブジェクトを生成
const transform = new TransformStream({
  transform(chunk, controller) {
    controller.enqueue(chunk.toUpperCase());
  }
});

const transformedStream = stream.pipeThrough(transform);

for await (const chunk of transformedStream)
  console.log(chunk);

Web Streams API は Node.js v16.5.0 から利用可能でしたが、experimentalな機能で利用時にはコンソールに警告が表示されていました。
しかし、v18 から警告の表示はされません。

まとめ

Node.js v18 の変更点を抜粋して紹介しました。
fetch()関数の追加や Web Streams API の安定版への移行など Web との互換が今回も目立った印象があります。
そのほかにも BlobBroadcastChannel が Node.js v18 からグローバルで利用できるようになっています。 これらもブラウザのようにグローバルで使えるようにするための変更です。
Node.js の Web 互換の流れはまだまだ続きそうです。

テストランナーは便利そうですね。まだまだ Jest や Mocha のように機能が充実しているわけではないですが、npm パッケージを別途インストールせずにテストコードを実行できるようになるのは便利です。 今後、既存のテストランナーにある機能を Node.js のテストランナーでも利用できるようにしてほしいといった要望も出てくるかもしれません。どういった機能が今後追加されていくのか楽しみです。

最後までお読みいただきありがとうございました。不備や質問があれば、お手数ですが@shisama_までお願いします。