ファイルを読み込む
前のセクションではコマンドライン引数からファイルパスを取得して利用できるようになりました。このセクションでは渡されたファイルパスを元にMarkdownファイルを読み込んで、標準出力に表示してみましょう。
node:fs
モジュールを使ってファイルを読み込む
前のセクションで取得できるようになったファイルパスを元に、ファイルを読み込みましょう。Node.jsでファイルの読み書きを行うには、標準モジュールのnode:fs
モジュールを使います。まずは読み込む対象のファイルを作成しましょう。sample.md
という名前でmain.js
と同じnodecli
ディレクトリに配置します。
sample.md
# sample
node:fs
モジュール
node:fs
モジュールは、Node.jsでファイルの読み書きを行うための基本的な関数を提供するモジュールです。
node:fs
モジュールは同期APIと非同期APIの両方が提供されています。一方で、node:fs/promises
モジュールには非同期形式のAPIのみが提供されています。この書籍では分かりやすさのために、非同期形式のみのAPIを提供するnode:fs/promises
モジュールを利用します。
次のコードは、ECMAScriptモジュールのimport * as
構文を使って、node:fs/promises
モジュール全体をfs
オブジェクトとしてインポートしています。
// fs/promisesモジュール全体を読み込むimport *as fsfrom"node:fs/promises";
もちろん、次のように名前付きインポートを使って、node:fs/promises
モジュール全体ではなく一部のAPIだけを利用することもできます。
// fs/promisesモジュールからreadFile関数を読み込むimport { readFile }from"node:fs/promises";
node:fs/promises
の非同期APIは、モジュール名からもわかるようにPromiseを返します。ファイルの読み書きといった非同期処理が成功したときには、返されたPromise
インスタンスがresolveされます。一方、ファイルの読み書きといった非同期処理が失敗したときには、返されたPromise
インスタンスがrejectされます。
次のサンプルコードは、指定したファイルを読み込むnode:fs/promises
のreadFile
メソッドの例です。
// 非同期APIを提供するfs/promisesモジュールを読み込むimport *as fsfrom"node:fs/promises";fs.readFile("sample.md").then(file => {console.log(file);}).catch(err => {console.error(err);});
そして、次のサンプルコードは、同じく指定したファイルを読み込むnode:fs
モジュールのreadFileSync
メソッドの例です。Node.jsでは非同期APIと同期APIがどちらもあるAPIには、分かりやすくSync
がメソッド名の末尾に含まれています。
// 同期APIを提供するfsモジュールを読み込むimport *as fsfrom"node:fs";try {const file = fs.readFileSync("sample.md");}catch (err) {// ファイルが読み込めないなどのエラーが発生したときに呼ばれる}
Node.jsはシングルスレッドなので、他の処理をブロックしにくい非同期形式のAPIを選ぶことがほとんどです。Node.jsにはnode:fs/promises
モジュール以外にも多くの非同期APIがあるので、非同期処理に慣れておきましょう。
readFile関数を使う
それではnode:fs/promises
モジュールのreadFile
メソッドを使ってsample.md
ファイルを読み込んでみましょう。次のようにmain.js
を変更し、コマンドライン引数から取得したファイルパスを元にファイルを読み込んでコンソールに出力します。
main.js
// utilモジュールをutilオブジェクトとしてインポートするimport *as utilfrom"node:util";// fs/promisesモジュールをfsオブジェクトとしてインポートするimport *as fsfrom"node:fs/promises";// コマンドライン引数からファイルパスを取得するconst { positionals } = util.parseArgs({allowPositionals:true});const filePath = positionals[0];// ファイルを非同期で読み込むfs.readFile(filePath).then(file => {console.log(file);});
sample.md
を引数に渡した実行結果は次のようになります。文字列になっていないのは、fs.readFile
関数でファイルをオプションなしで読み込んだ結果は、ファイルの中身を表すBuffer
インスタンスとなるためです。Buffer
インスタンスはファイルの中身をバイト列として保持しています。そのため、そのままconsole.log
メソッドに渡しても人間が読める文字列にはなりません。
$node main.js sample.md<Buffer 23 20 73 61 6d 70 6c 65>
fs.readFile
関数は引数によってファイルの読み込み方を指定できます。ファイルのエンコードを第二引数であらかじめ指定しておけば、自動的に文字列に変換された状態でコールバック関数に渡されます。次のようにmain.js
を変更し、読み込まれるファイルをUTF-8として変換させます。
main.js
import *as utilfrom"node:util";import *as fsfrom"node:fs/promises";const { positionals } = util.parseArgs({allowPositionals:true});const filePath = positionals[0];// ファイルをUTF-8として非同期で読み込むfs.readFile(filePath, {encoding:"utf8" }).then(file => {console.log(file);});
先ほどと同じコマンドをもう一度実行すると、実行結果は次のようになります。sample.md
ファイルの中身を文字列として出力できました。
$node main.js sample.md#sample
エラーハンドリング
ファイルの読み書きは存在の有無や権限、ファイルシステムの違いなどによって例外が発生しやすいので、必ずエラーハンドリング処理を書きましょう。
次のようにmain.js
を変更し、readFile
の返り値であるPromiseオブジェクトに対してcatch
メソッドを追加するだけのシンプルなエラーハンドリングです。エラーが発生していたときにはエラーメッセージを表示し、process.exit
関数に終了ステータスを指定してプロセスを終了しています。ここでは、一般的なエラーを表す終了ステータスの1
でプロセスを終了しています。
main.js
import *as utilfrom"node:util";import *as fsfrom"node:fs/promises";const { positionals } = util.parseArgs({allowPositionals:true});const filePath = positionals[0];// ファイルを非同期で読み込むfs.readFile(filePath, {encoding:"utf8" }).then(file => {console.log(file);}).catch(err => {console.error(err.message);// 終了ステータス 1(一般的なエラー)としてプロセスを終了する process.exit(1);});
存在しないファイルであるnotfound.md
をコマンドライン引数に渡して実行すると、次のようにエラーが発生して終了します。
$node main.js notfound.mdENOENT: no such file or directory, open 'notfound.md'
これでコマンドライン引数に指定したファイルを読み込んで標準出力に表示できました。次のセクションでは読み込んだMarkdownファイルをHTMLに変換する処理を追加していきます。
[コラム] Node.jsのエラーファーストコールバック
Node.jsが提供するnode:fs
モジュールは同期APIと非同期APIを提供するという話を紹介しました。歴史的な経緯もあり、Node.jsではPromiseとエラーファーストコールバックの2種類の非同期APIを提供しているケースもあります。
node:fs/promises
モジュールでは、readFile
メソッドは、Promiseを返す非同期APIでした。一方で、node:fs
モジュールにもreadFile
メソッドがあり、このAPIはエラーファーストコールバックを扱う非同期APIです。
// fsモジュールにはエラーファーストコールバックを扱う非同期APIも含まれるimport *as fsfrom"node:fs/promises";// エラーファーストコールバックの第1引数にはエラー、第2引数 には結果が入るというルールfs.readFile("sample.md",(err, file) => {if (err) {console.error(err.message); process.exit(1);return; }console.log(file);});
エラーファーストコールバックについては、非同期の章でも紹介しています。エラーファーストコールバックは、PromisesがECMAScriptに入るES2015より前においては、非同期な処理を扱う方法として広く使われていました。Node.jsの多くのモジュールは、ES2015より前に作られているため、node:fs
モジュールのようにエラーファーストコールバックを扱うAPIもあります。
一方で、Promiseが非同期APIの主流となったため、Node.jsにもPromiseを扱うためのAPIが追加されました。しかし、node:fs
モジュールではエラーファーストコールバックを提供するメソッドと名前が衝突したため、fs.promises
というプロパティにPromiseのAPIがまとめられています。このままではPromiseのAPIが使いにくいため、PromiseのAPIのみをもつモジュールとしてnode:fs/promises
が新たに追加されました。また、Node.jsではエラーファーストコールバックを受け取る非同期APIをPromiseを返す非同期APIへとラップするutil.promisify
というメソッドも提供しています。
Node.jsでは、歴史的な経緯からエラーファーストコールバックとPromiseのAPIがどちらも提供されていることがあります。しかしながら、両方が提供されている場合はPromiseのAPIを利用するべきです。Promiseを扱うAPIには、他のPromiseを扱う処理との連携のしやすさ、Async Functionという構文的なサポート、エラーハンドリングの簡潔さなどのメリットがあります。
このセクションのチェックリスト
node:fs/promises
モジュールのreadFile
関数を使ってファイルを読み込んだ- UTF-8形式のファイルの中身をコンソールに出力した
readFile
関数の呼び出しにエラーハンドリング処理を記述した