Node.js 22新機能完全ガイド - ネイティブfetch、Webストリーム、テストランナー改善
Node.js 22新機能完全ガイド
Node.js 22は2024年4月にリリースされ、多くの画期的な機能が安定版として提供されました。本記事では、Node.js 22で追加・改善された主要機能を実践的なコード例とともに解説します。
目次
- ネイティブfetchの安定化
- Webストリームの完全サポート
- テストランナーの大幅改善
- パーミッションモデル
- V8エンジン12.4へのアップデート
- require(ESM)のサポート
- その他の改善点
1. ネイティブfetchの安定化
Node.js 18で実験的に導入されたネイティブfetch APIが、Node.js 22でついに安定版となりました。もう外部パッケージ(node-fetch、axios)は不要です。
基本的な使い方
// GET リクエスト
const response = await fetch('https://api.example.com/users');
const users = await response.json();
console.log(users);
// POST リクエスト
const newUser = {
name: 'Alice',
email: 'alice@example.com'
};
const postResponse = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newUser)
});
const createdUser = await postResponse.json();
console.log(createdUser);
エラーハンドリング
async function fetchUserData(userId) {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
if (error.name === 'TypeError' && error.message.includes('fetch')) {
console.error('ネットワークエラー:', error);
} else {
console.error('予期しないエラー:', error);
}
throw error;
}
}
タイムアウトの実装
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`リクエストが${timeout}msでタイムアウトしました`);
}
throw error;
}
}
// 使用例
try {
const response = await fetchWithTimeout('https://api.example.com/slow-endpoint', {}, 3000);
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error.message);
}
並列リクエスト
async function fetchMultipleUsers(userIds) {
const promises = userIds.map(id =>
fetch(`https://api.example.com/users/${id}`)
.then(res => res.json())
);
const users = await Promise.all(promises);
return users;
}
// 使用例
const userIds = [1, 2, 3, 4, 5];
const users = await fetchMultipleUsers(userIds);
console.log(users);
FormDataとファイルアップロード
import { readFile } from 'node:fs/promises';
async function uploadFile(filePath) {
const fileBuffer = await readFile(filePath);
const blob = new Blob([fileBuffer]);
const formData = new FormData();
formData.append('file', blob, 'image.png');
formData.append('description', 'My uploaded file');
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
body: formData
});
return response.json();
}
2. Webストリームの完全サポート
Node.js 22では、Web Streams API(ReadableStream、WritableStream、TransformStream)が完全にサポートされ、ブラウザとの互換性が大幅に向上しました。
ReadableStreamの基本
// カスタムReadableStreamの作成
const stream = new ReadableStream({
start(controller) {
controller.enqueue('Hello ');
controller.enqueue('World!');
controller.close();
}
});
// ストリームの読み取り
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(value);
}
fetchとストリーミング
async function streamLargeFile(url) {
const response = await fetch(url);
const reader = response.body.getReader();
let receivedLength = 0;
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
receivedLength += value.length;
console.log(`受信済み: ${receivedLength} バイト`);
}
// すべてのチャンクを結合
const chunksAll = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
chunksAll.set(chunk, position);
position += chunk.length;
}
return chunksAll;
}
TransformStreamでデータ変換
// テキストを大文字に変換するTransformStream
const uppercaseStream = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toString().toUpperCase());
}
});
// 使用例
const readableStream = new ReadableStream({
start(controller) {
controller.enqueue('hello ');
controller.enqueue('world');
controller.close();
}
});
const transformedStream = readableStream.pipeThrough(uppercaseStream);
const reader = transformedStream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(value); // "HELLO ", "WORLD"
}
Node.jsストリームとの相互変換
import { Readable, Writable } from 'node:stream';
// ReadableStreamからNode.js Readableへ
function webToNodeStream(webStream) {
return Readable.fromWeb(webStream);
}
// Node.js ReadableからReadableStreamへ
function nodeToWebStream(nodeStream) {
return Readable.toWeb(nodeStream);
}
// 使用例
import { createReadStream } from 'node:fs';
const nodeStream = createReadStream('./large-file.txt');
const webStream = Readable.toWeb(nodeStream);
// fetchのbodyとして使用可能
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
body: webStream,
duplex: 'half'
});
SSE(Server-Sent Events)の実装
import { createServer } from 'node:http';
createServer((req, res) => {
if (req.url === '/events') {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
// ReadableStreamを使ったSSE
const stream = new ReadableStream({
start(controller) {
let counter = 0;
const interval = setInterval(() => {
const data = `data: ${JSON.stringify({ count: counter++ })}\n\n`;
controller.enqueue(new TextEncoder().encode(data));
if (counter > 10) {
clearInterval(interval);
controller.close();
}
}, 1000);
}
});
// Web StreamをNode.jsストリームに変換してレスポンスにパイプ
const nodeStream = Readable.fromWeb(stream);
nodeStream.pipe(res);
}
}).listen(3000);
3. テストランナーの大幅改善
Node.js 20で導入されたテストランナーが、Node.js 22でさらに強化されました。
基本的なテスト
// test.js
import { test, describe, it } from 'node:test';
import assert from 'node:assert';
describe('数学関数のテスト', () => {
it('足し算が正しく動作する', () => {
assert.strictEqual(1 + 1, 2);
});
it('引き算が正しく動作する', () => {
assert.strictEqual(5 - 3, 2);
});
});
// または test() を直接使用
test('配列のテスト', (t) => {
const arr = [1, 2, 3];
assert.strictEqual(arr.length, 3);
assert.deepStrictEqual(arr, [1, 2, 3]);
});
非同期テスト
import { test } from 'node:test';
import assert from 'node:assert';
test('非同期処理のテスト', async () => {
const result = await Promise.resolve(42);
assert.strictEqual(result, 42);
});
test('fetchのテスト', async () => {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
assert.ok(response.ok);
assert.strictEqual(typeof data, 'object');
});
モックとスパイ
import { test, mock } from 'node:test';
import assert from 'node:assert';
test('モック関数のテスト', () => {
const mockFn = mock.fn((x, y) => x + y);
const result = mockFn(2, 3);
assert.strictEqual(result, 5);
assert.strictEqual(mockFn.mock.calls.length, 1);
assert.deepStrictEqual(mockFn.mock.calls[0].arguments, [2, 3]);
});
test('メソッドのモック', () => {
const obj = {
method: (x) => x * 2
};
const mockMethod = mock.method(obj, 'method', (x) => x * 3);
assert.strictEqual(obj.method(5), 15);
assert.strictEqual(mockMethod.mock.calls.length, 1);
// モックを復元
mockMethod.mock.restore();
assert.strictEqual(obj.method(5), 10);
});
カバレッジレポート
# カバレッジ付きでテスト実行
node --test --experimental-test-coverage
# 特定のファイルのみ
node --test --experimental-test-coverage src/**/*.test.js
スナップショットテスト
import { test } from 'node:test';
import assert from 'node:assert';
test('スナップショットテスト', (t) => {
const data = {
name: 'John',
age: 30,
hobbies: ['reading', 'coding']
};
// スナップショットと比較
t.assert.snapshot(data);
});
テストのフィルタリング
// only を使って特定のテストのみ実行
test('このテストは実行される', { only: true }, () => {
// テストコード
});
test('このテストはスキップされる', () => {
// テストコード
});
// skip を使ってテストをスキップ
test('このテストはスキップされる', { skip: true }, () => {
// テストコード
});
// 条件付きスキップ
test('条件付きスキップ', { skip: process.platform === 'win32' }, () => {
// Windowsでは実行されない
});
カスタムレポーター
// custom-reporter.js
export default class CustomReporter {
constructor() {
this.tests = [];
}
report(event) {
if (event.type === 'test:pass') {
console.log(`✓ ${event.data.name}`);
} else if (event.type === 'test:fail') {
console.log(`✗ ${event.data.name}`);
console.error(event.data.error);
}
}
}
# カスタムレポーターを使用
node --test --test-reporter=./custom-reporter.js
4. パーミッションモデル
Node.js 22では、Denoのようなパーミッションモデルが実験的に導入されました。
基本的な使い方
# ファイルシステムへの読み取りアクセスを制限
node --experimental-permission --allow-fs-read=/path/to/allowed app.js
# ファイルシステムへの書き込みアクセスを制限
node --experimental-permission --allow-fs-write=/path/to/allowed app.js
# 子プロセスの実行を制限
node --experimental-permission --allow-child-process app.js
# ネットワークアクセスを制限
node --experimental-permission --allow-worker app.js
パーミッションのチェック
// パーミッションがあるかチェック
import { permission } from 'node:process';
console.log(permission.has('fs.read', '/etc/passwd')); // false
console.log(permission.has('fs.read', '/allowed/path')); // true
// すべてのパーミッションをチェック
console.log(permission.has('fs')); // undefined (一部のみ許可)
セキュリティのベストプラクティス
// セキュアなファイル操作
import { readFile } from 'node:fs/promises';
import { permission } from 'node:process';
async function secureReadFile(path) {
if (!permission.has('fs.read', path)) {
throw new Error(`ファイル ${path} への読み取り権限がありません`);
}
return readFile(path, 'utf-8');
}
// 使用例
try {
const content = await secureReadFile('/allowed/path/file.txt');
console.log(content);
} catch (error) {
console.error(error.message);
}
5. V8エンジン12.4へのアップデート
Node.js 22にはV8エンジン12.4が搭載され、多くのパフォーマンス改善と新しいJavaScript機能が利用可能になりました。
Array.prototype.group(Stage 3)
const inventory = [
{ type: 'fruit', name: 'apple' },
{ type: 'vegetable', name: 'carrot' },
{ type: 'fruit', name: 'banana' },
{ type: 'vegetable', name: 'broccoli' }
];
// カテゴリごとにグループ化
const grouped = Object.groupBy(inventory, item => item.type);
console.log(grouped);
// {
// fruit: [
// { type: 'fruit', name: 'apple' },
// { type: 'fruit', name: 'banana' }
// ],
// vegetable: [
// { type: 'vegetable', name: 'carrot' },
// { type: 'vegetable', name: 'broccoli' }
// ]
// }
Promise.withResolvers
// 従来の方法
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// 新しい方法
const { promise, resolve, reject } = Promise.withResolvers();
// 使用例
setTimeout(() => resolve('完了'), 1000);
const result = await promise;
console.log(result);
String.prototype.isWellFormed
// 正しいUnicode文字列かチェック
const validString = 'Hello, World!';
const invalidString = 'Hello\uD800World'; // 不完全なサロゲートペア
console.log(validString.isWellFormed()); // true
console.log(invalidString.isWellFormed()); // false
// 正しい文字列に変換
console.log(invalidString.toWellFormed()); // "Hello�World"
ArrayBuffer.prototype.transfer
// ArrayBufferの所有権を移動
const buffer1 = new ArrayBuffer(8);
const view1 = new Uint8Array(buffer1);
view1[0] = 42;
// 所有権を移動(元のbufferは使用不可に)
const buffer2 = buffer1.transfer();
const view2 = new Uint8Array(buffer2);
console.log(view2[0]); // 42
console.log(buffer1.byteLength); // 0 (detached)
6. require(ESM)のサポート
Node.js 22では、CommonJSからESモジュールを直接requireできるようになりました(実験的機能)。
基本的な使い方
// CommonJS側 (app.cjs)
const esmModule = require('./esm-module.mjs');
console.log(esmModule.default);
console.log(esmModule.namedExport);
// ESモジュール側 (esm-module.mjs)
export default function() {
return 'デフォルトエクスポート';
}
export const namedExport = '名前付きエクスポート';
注意点
// トップレベルawaitを使っているESMはrequireできない
// esm-with-await.mjs
const data = await fetch('https://api.example.com/data');
export default data;
// これはエラーになる
// const module = require('./esm-with-await.mjs'); // Error!
7. その他の改善点
glob と globSync
import { glob, globSync } from 'node:fs';
// 非同期版
for await (const file of glob('**/*.js')) {
console.log(file);
}
// 同期版
const files = globSync('src/**/*.{js,ts}');
console.log(files);
// オプション付き
const testFiles = globSync('**/*.test.js', {
ignore: ['node_modules/**', 'dist/**']
});
WebSocket の改善
// Node.js 22では実験的なWebSocketサポートが改善
const ws = new WebSocket('wss://echo.websocket.org');
ws.addEventListener('open', () => {
console.log('接続しました');
ws.send('Hello Server!');
});
ws.addEventListener('message', (event) => {
console.log('受信:', event.data);
});
ws.addEventListener('error', (error) => {
console.error('エラー:', error);
});
ws.addEventListener('close', () => {
console.log('接続を閉じました');
});
watch モードの改善
# ファイル変更を監視して自動再起動
node --watch app.js
# 特定のファイルを監視対象から除外
node --watch --watch-path=./src app.js
パフォーマンス比較
fetch vs axios
import { performance } from 'node:perf_hooks';
// ネイティブfetchのベンチマーク
async function benchmarkFetch(iterations = 100) {
const start = performance.now();
for (let i = 0; i < iterations; i++) {
await fetch('https://api.example.com/data');
}
const end = performance.now();
return end - start;
}
const fetchTime = await benchmarkFetch();
console.log(`fetch: ${fetchTime}ms`);
// 結果: Node.js 22のネイティブfetchは外部ライブラリと同等以上のパフォーマンス
マイグレーションガイド
node-fetchからの移行
// Before (node-fetch)
import fetch from 'node-fetch';
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// After (Node.js 22)
// import文を削除するだけ
const response = await fetch('https://api.example.com/data');
const data = await response.json();
axiosからの移行
// Before (axios)
import axios from 'axios';
const { data } = await axios.get('https://api.example.com/data', {
headers: { 'Authorization': 'Bearer token' }
});
// After (Node.js 22)
const response = await fetch('https://api.example.com/data', {
headers: { 'Authorization': 'Bearer token' }
});
const data = await response.json();
ベストプラクティス
1. エラーハンドリング
async function robustFetch(url, options = {}) {
const maxRetries = 3;
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response;
} catch (error) {
lastError = error;
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
throw lastError;
}
2. リソース管理
// ストリームの適切なクリーンアップ
async function processStream(url) {
const response = await fetch(url);
const reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 処理
processChunk(value);
}
} finally {
reader.releaseLock();
}
}
3. テストの組織化
// tests/unit/math.test.js
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { add, subtract } from '../../src/math.js';
describe('Math Module', () => {
describe('add()', () => {
it('正の数を足す', () => {
assert.strictEqual(add(2, 3), 5);
});
it('負の数を足す', () => {
assert.strictEqual(add(-2, -3), -5);
});
});
describe('subtract()', () => {
it('正の数を引く', () => {
assert.strictEqual(subtract(5, 3), 2);
});
});
});
まとめ
Node.js 22は、以下の点で大きな進化を遂げました。
- ネイティブfetchの安定化: 外部ライブラリ不要でモダンなHTTPクライアント機能を提供
- Webストリーム対応: ブラウザとの互換性が向上し、コードの共有が容易に
- テストランナー改善: 外部テストフレームワークなしで本格的なテストが可能
- パーミッションモデル: セキュリティ強化のための新しいアプローチ
- V8更新: 最新のJavaScript機能とパフォーマンス改善
これらの機能により、Node.jsはよりモダンで、セキュアで、開発者フレンドリーなランタイムへと進化しました。既存のプロジェクトをNode.js 22にアップグレードする価値は十分にあります。