別にしんどくないブログ

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

Node.js v17 の主な変更点

f:id:Shisama:20200422011813p:plain

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

nodejs.org

QUICがサポートされたOpenSSLにアップデート

Node.jsのHTTPSなどのAPIで使われるOpenSSLのバージョンが1.1.0から3.0.0にアップデートされました。
OpenSSL-3.0.0はQUICをサポートしています。これによりNode.jsでもQUICをサポートできるようになります。
実は過去にQUICのサポートを実験的に行ったことがあります。
それについては以下の記事をお読みください。

blog.leko.jp

しかし、後にこのQUIC実装は削除されます。

OpenSSLのメンテナンスに問題があったためです。OpenSSLのメンテナンスが滞っているため、QUIC実装のためにNode.js側でパッチをあてて対応していました。しかし、この先Node.js側でパッチをあて続けるのも困難なため、一度削除されて別の道を模索するようになりました。
QUIC実装の削除については、以下の記事にまとめられています。

zenn.dev

今回のvNode.js v17では、AkamaiMicrosoftが本家OpenSSLをforkしたquictls/opensslを使っています。

github.com

このforkされたOpenSSLはQUICを有効にするためにforkされて開発されています。

Node.jsで利用するOpenSSLのメンテナンスについては次のドキュメントに詳細が書かれています。

github.com

また、OpenSSL-3.0からは新しいFIPSモジュールが採用されており、Node.jsではビルド時のフラグにてFIPSを有効にするか指定できます。

$ ./configure --openssl-is-fips && make -j8 test

また、OpenSSL-3.0はNode.js v16以前で利用していたOpenSSL-1.1と互換性がない機能もあります。そのため、Node.js v17を使用していてERR_OSSL_EVP_UNSUPPORTEDのエラーが発生したときのためのオプションが用意されています。
次のように--openssl-legacy-providerオプションを付けてNode.jsを実行することで古いOpenSSLのプロだバイダーを利用できます。

node index.js --openssl-legacy-provider

V8 が 9.5 にアップデート

JavaScriptエンジンのV8が9.5にアップデートされました。V8 9.5ではIntl APIの強化やWebAssemblyの例外のハンドリングがサポートされています。

Intl.DisplayNamesは言語、地域、文字体系の表示名を指定した言語で翻訳する機能で、V8 8.1からサポートされています。

Intl.DisplayNames

MDNからの抜粋ですが、次のように地域の名前を指定した言語、ここでは英語と繁体字(中国語)で表示します。

const regionNamesInEnglish = new Intl.DisplayNames(['en'], { type: 'region' });
const regionNamesInTraditionalChinese = new Intl.DisplayNames(['zh-Hant'], { type: 'region' });

console.log(regionNamesInEnglish.of('US'));
// expected output: "United States"

console.log(regionNamesInTraditionalChinese.of('US'));
// expected output: "美國"

V8 9.5では言語や地域に加えて、カレンダーや日付フィールドがサポートされました。次の例はV8の記事からの抜粋ですが、"calendar""dateTimeField"typeに指定できます。

const esCalendarNames = new Intl.DisplayNames(['es'], { type: 'calendar' });
const frDateTimeFieldNames = new Intl.DisplayNames(['fr'], { type: 'dateTimeField' });
esCalendarNames.of('roc');  // "calendario de la República de China"
frDateTimeFieldNames.of('month'); // "mois"

また言語の表示が強化され、方言の表示を指定できるようになりました。次のようにtype: 'language'と一緒にlanguageDisplay: 'standard'またはlanguageDisplay: 'dialect'を指定します。

const jaDialectLanguageNames = new Intl.DisplayNames(['ja'], { type: 'language' });
const jaStandardLanguageNames = new Intl.DisplayNames(['ja'], { type: 'language' , languageDisplay: 'standard'});
jaDialectLanguageNames.of('en-US')  // "アメリカ英語"
jaDialectLanguageNames.of('en-AU')  // "オーストラリア英語"
jaDialectLanguageNames.of('en-GB')  // "イギリス英語"

jaStandardLanguageNames.of('en-US') // "英語 (アメリカ合衆国)"
jaStandardLanguageNames.of('en-AU') // "英語 (オーストラリア)"
jaStandardLanguageNames.of('en-GB') // "英語 (イギリス)"

Intl.DateTimeFormat

Intl.DateTimeFormatは、指定した言語に合わせた日時表示をするAPIです。

const date = new Date();
const df = new Intl.DateTimeFormat('en-US', {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    timeZone: 'America/Los_Angeles',
    timeZoneName: 'short'
});
console.log(df.format(date))
// Tuesday, October 19, 2021, PDT

このAPI自体は古くからありますが、今回のV8 9.5からtimeZoneNameに指定できる値が増えて、次の値を指定できるようになりました。

  • 'shortGeneric'
  • 'longGeneric'
  • 'shortOffset'
  • 'longOffset'

'shortOffset''shortGeneric'を使った例は次のとおりです。タイムゾーンの表記が'short'と異なっていることがわかります。

const date = new Date();
const df = new Intl.DateTimeFormat('en-US', {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    timeZone: 'America/Los_Angeles',
    timeZoneName: 'shortOffset'
});
console.log(df.format(date))
// Tuesday, October 19, 2021, GMT-7
const date = new Date();
const df = new Intl.DateTimeFormat('en-US', {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    timeZone: 'America/Los_Angeles',
    timeZoneName: 'shortGeneric'
});
console.log(df.format(date))
// Tuesday, October 19, 2021, PT

V8 9.5に関するアップデート内容はV8のブログに詳しく記載されています。

v8.dev

Promiseベースのreadline APIの追加

Node.jsのここ数バージョンでPromiseベースのNode.jsの標準APIが増えていっています。たとえば、timers/promisessetTimeoutなどのPromiseベースのAPIを提供していたり、fs/promisesreadFileなどのPromiseベースのAPIを提供していたりします。

Node.js v17では、新たにreadlineにPromiseベースのAPIが追加されています。
Promiseベースのため、次のようにawaitを使うこともできます。

import * as readline from ‘node:readline/promises’;
import { stdin as input, stdout as output } from ‘process’;
const rl = readline.createInterface({ input, output });

// Promiseベースのため、async/awaitで記述できる
const answer = await rl.question('What is your favorite food? ');
console.log(`Oh, so your favorite food is ${answer}`);
rl.close();

また、AbortControllerを使ったキャンセルもできます。

import * as readline from ‘node:readline/promises’;
import { stdin as input, stdout as output } from ‘process’;
const rl = readline.createInterface({ input, output });

// AbortControllerからAbortSignalを取得
const ac = new AbortController();
const signal = ac.signal;

// AbortSignalを設定し、キャンセル可能にする
const answer = await rl.question('What is your favorite food? ', { signal });
console.log(`Oh, so your favorite food is ${answer}`);

// キャンセル時に一度だけコンソールにメッセージを表示
signal.addEventListener('abort', () => {
  console.log('The food question timed out');
}, { once: true });

// 10秒(10000ミリ秒)後にキャンセルする
setTimeout(() => ac.abort(), 10000);

WHATWG Stream との互換性の強化

Node.jsには古くからStream APIがありますが、このAPIWeb標準であるWHATWG Streamとは互換性のないAPIでした。
この数年のNode.jsはWeb標準との互換性を重視しています。たとえば、cyrpto APIもWeb Crypto APIが追加されていたりします。
StreamもWHATWG Streamと互換のあるWeb Streams APIがNode.js v16.5.0から追加されています。

しかし、古いStream APIとWeb Streams APIでは互換性がないため、古いStreamを使ったコードやライブラリから今後入ってくるWeb標準互換のAPIやライブラリへの移行が大変です。

そこでNode.js v17では、古いStream APIとWeb Streams APIで互換性を強化するために、fromWeb関数とtoWeb関数が追加されています。

import {Readable} from 'node:stream';
import {fetch} from 'undici';

const response = await fetch(url);
// Web StreamからNode.jsの古いStreamへ変換
const readableNodeStream = Readable.fromWeb(response.body);
// 古いStreamからWeb Streamへ変換
const readableWebStream = Readable.toWeb(readableNodeStream);

このように変換機能を使うことで古いStreamとWeb Streams APIを両方使うことができるようになります。今後Node.jsにはfetchのようなWeb標準APIが追加されていく予定です。そのため、このような古いAPIとの互換性は重要な課題となっています。

ディープクローンが簡単になる structuredClone の追加

structuredClone関数はJavaScriptの値をコピーするための関数です。
この関数を使うことでディープクローンが簡単に行えるようになっています。
この関数はWHATWGのHTML Standardに仕様定義されており、Web標準と互換性のあるAPIになっています。

const original = {
  foo: {
    bar: 1,
    hoge: {
      message: "Hello"
    }
  }
};
original.self = original;
const clone = structuredClone(original);

console.log(clone.foo.bar);
// 1
console.log(clone.foo.hoge.message);
// Hello

clone.foo.bar = 2;
console.log(original.foo.bar);
// 1
console.log(clone.foo.bar);
// 2

structuredCloneはオブジェクトに限らず、numberstringなどのプリミティブ値やDateArrayBufferRegExpMapErrorArrayなど様々な型をサポートしています。

もし、Promiseなどサポートしていない型を含む場合、DOMExceptionがスローされます。

structuredCloneに関する詳しい解説は以下の記事をお読みください。

zenn.dev

structuredCloneで使われている構造化複製アルゴリズムについては以下の記事をお読みください。

developer.mozilla.org

Node.js v16からの機能

Node.js v17は当たり前ですが、v16の機能をすべて含んでいます。
Node.js v16では、V8のアップデートによるJavaScriptの機能が追加されていたり、パッケージマネージャー管理ツールであるCorepackの追加など大きな変化が含まれています。

次のようなJavaScriptの機能が追加されています。

パッケージマネージャー管理ツールのCorepackについてはNode学園 37時限目で話したので、そちらの資料をお読みください。

speakerdeck.com

その他のNode.jsの変更点については以下の記事にも書いています。

shisama.hatenablog.com

まとめ

ユーザー視点で見ると、Node.js v17はとても嬉しい機能追加はないかもしれません。しかし、内部的にはずっと問題になっていたOpenSSLのアップデート、Web標準との互換性のための機能といった今後のNode.jsの進化に必要なものが追加されたと思います。 Node.js v17はLTSにならないバージョンのため、使う人は少ないかもしれませんが今後大きな機能が追加されていき、その後のLTSになるv18にとっても大きな機能追加があるかもしれません。

参考資料