freeCodeCamp Blog: How to Read and Write Files with Node.js を読んだメモ

## はじめに

この記事は、[freeCodeCamp](https://freecodecamp.org/) に関連して、アドベントカレンダー形式で学習た内容をメモしたものになります。
12月2日は、Ashutosh Krishna さんが書かれたブログ記事「[How to Read and Write Files with Node.js](https://www.freecodecamp.org/news/how-to-read-and-write-files-with-nodejs/)」を読んだ内容をざっと紹介してみます。

## なぜこれを取り上げたの？

ここ数年はサポートのお仕事が中心になり、コードを書く機会が減ってしまいました。
また、最近のフレームワークといったものは全く付いていけず、リハビリを兼ねて、小さく動きを試せる内容がよかった、というのが、Node.js を素材にした理由です。

はずかしながら、やってみると忘れているものですね...。

## ざっくりの内容は？

それぞれ別の json ファイルの内容を読み取り、マージして1つのファイルに書き出す、といった処理を、同期と非同期のパターンでのファイル操作の違いを交えて実装する内容になっています。
こちらも、この投稿で逐一内容をなぞることは控えますが、タイトルや、これらのキーワードから興味を持たれた方は、ぜひお読みいただけたらと思います！

## 英語の記事ですが、複雑な記事ですか？

今回も恥ずかしながら、わたしは Google 翻訳などを併用して拝読しています。
英語ではありますが、実際にコードを交えての解説になっていますので、シンプルな Google 翻訳でも、理解しやすい内容かと思います。
また、実際に試してみるにしても、Node.js が動く環境ならそこまで特別なライブラリなどの追加の必要はありません。

なお、今回わたしは手元のノート PC での Node.js の準備ができなかったので、Azure ポータル上の ``Cloud Shell`` で動かしてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27976/8fb36b53-8508-c961-7d5c-f2a533dfe0f7.png)


## 動かしてみたら？

ひとまず記事に倣って簡単なファイルを作成します。

```JavaScript

const fs = require("fs").promises;

async function readJSONFile(filename) {
  try {
    const data = await fs.readFile(filename, "utf8");
    return JSON.parse(data);
  } catch (error) {
    console.error(`Error reading ${filename}: ${error}`);
    return [];
  }
}

async function main() {
  try {
    // promsie を待って値を取り出せるようにします。
    // promise を待たないと、names.json の内容を読み取る前に、次の address.json のファイル読み取りの処理に移ってしまうので...。
    const names = await readJSONFile("names.json");
    const addresses = await readJSONFile("address.json");

    const bioData = names.map((name) => {
      const matchingAddress = addresses.find(
        (address) => address.id === name.id
      );
      return { ...name, ...matchingAddress };
    });

    await fs.writeFile("bio.json", JSON.stringify(bioData, null, 2));
    console.log("bio.json created successfully!");
  } catch (error) {
    console.error("Error combining data:", error);
  }
}

main();
```

内容は、names.json と address.json をそれぞれ読み取りして、それぞれ同じ id に対して値をその時の name と address の組にして、新しくファイルに書き出すというものです。

動かしてみます。

```bash
🐹  work $ node sample.js 
bio.json created successfully!

🐹  work $ cat bio.json 
[
  {
    "id": 1,
    "name": "Alice",
    "address": "123 Main St"
  },
  {
    "id": 2,
    "name": "Bob",
    "address": "456 Elm St"
  },
  {
    "id": 3,
    "name": "Charlie",
    "address": "789 Oak St"
  }
```

うまくいきました。

## fs.readFile() と fs.readFileSync() 

ブログ記事では、まずは実装のあとで fs.readFile() と fs.readFileSync() についての記載がありました。
このあたりもおさらいとして、大変ありがたい限り。

**fs.readFile()**

* Node.js においては非同期処理
* 他の処理をブロックすることなくファイルを読み取る
* 基本はノンブロッキングな I/O 操作が必要な際に利用すると良い
* パラメーターとしては3つ
    * パス
    * エンコーディング（オプション）
    * コールバック関数（オプション）
        * コールバック関数では error, data, buffer の3つの引数を取る

**fs.readFileSync()**

* Node.js においては同期処理
* 同期的にファイルを読み取りするので、ファイル読み取りが完了するまでは他の操作はブロックされる
* 同期的な処理が求められる時に使うと良い
* パラメーターとしては2つ
    * パス
    * エンコーディング（オプション）

**動作の違いについての解説**

**readFile の場合**

```JavaScript
/*
example.txt を用意。こんな内容です。
----
freeCodeCamp is awesome!
----

*/

const fs = require("fs");

fs.readFile("data.txt", "utf8", (err, data) => {
  console.log("Content from readFile:", data);
});

console.log("Completed reading file content asynchronously");

```

動かしてみます。ファイル読み取りと値取得を待たずに、先に "Completed..." の内容が出力されてしまいます。


```bash
🐹  work $ node sample1.js 
Completed reading file content asynchronously
Content from readFile: freeCodeCamp is awesome!
```

**readFileSync の場合**

```JavaScript
/*
example.txt を用意。こんな内容です。
----
freeCodeCamp is awesome!
----

*/

const fs = require("fs");

const data = fs.readFileSync("data.txt", "utf8");
console.log("Content from readFileSync:", data);

console.log("Completed reading file content synchronously");
```

動かしてみます。同期的な処理になるので、ファイルの読み取りとファイルの内容の出力のあとに、"Completed...." のメッセージが出力されました。


```bash
🐹  work $ cnode sample1.js 
Content from readFileSync: freeCodeCamp is awesome!

Completed reading file content synchronously
```

なるほど。
ちゃんと気をつけないと、想定通りの順番で処理が行われないことも想定されますね！
改めて注意が必要という点を実感です。

## fs.promises について

fs.promisesNode.js を使うと、コールバックに比べて非同期操作をより読みやすく効率的に処理できる、とのこと。
実際に書き換えたコードを見てみると、なるほど、スッキリです。

```JavaScript

const fs = require("fs").promises;

// async - await が使えます！
async function readTextFile() {
  try {
    const data = await fs.readFile("data.txt", "utf8");
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

readTextFile();
```

上記の readFile() を使ったコードも、fs.readFile() を使いつつファイルの読み取りを待ってから次に進めたい場合も、シンプルに書けそうです。

```JavaScript


/*
// このパターンだとファイルび読み取りを待たずに Completed ... が出力されてしまう
const fs = require("fs");

fs.readFile("data.txt", "utf8", (err, data) => {
  console.log("Content from readFile:", data);
});

console.log("Completed reading file content asynchronously");
*/

// こちらだと fs.readFile() でもファイル読み取りを待ってから順に処理できる。
const fs = require("fs").promises;
async function readTextFile() {
  const data = await fs.readFile("data.txt", "utf8");
  console.log("Content from readFile:", data);
  console.log("Completed reading file content asynchronously");
}

readTextFile();
```
ブログ記事でも、以下のように紹介されています。

> We also learned about fs.promises, which is a more elegant way to handle file operations using asynchronous functions.
> ざっくりの訳：fs.promises を使うと、非同期関数をもっとエレガントに扱えますよ！

## fs.stat と fs.promises.access でファイルの存在チェック

ブログ記事中の FAQ として、ファイルの読み書きの前にファイルを存在するチェックの方法として、fs.stat と fs.promises.access が紹介されていました。
コードの例はなかったので、ブログ中のサンプルコードに少し手をくわえて試してみます。

**fs.stat の場合**

```JavaScript


const fs = require("fs");

fs.stat("data.txt", (err, stats) => {
  if (err) {
    console.log("data.txt does not exist.");
  } else {
    console.log("data.txt exists");

    fs.readFile("data.txt", "utf8", (err, data) => {
      console.log("Content from readFile:", data);
    });
  }
});

console.log("Completed reading file content asynchronously");
```

実行してみます。

```bash

$ mv data.txt data.txt.back
$ node sample1.js 
Completed reading file content asynchronously
data.txt does not exist.

$ mv data.txt.back data.txt
$ node sample1.js 
Completed reading file content asynchronously
data.txt exists
Content from readFile: freeCodeCamp is awesome!
```

**fs.promises.access の場合**

```JavaScript
const fs = require("fs").promises;
async function readTextFile() {
  try {
    await fs.access("data.txt", fs.constants.R_OK);
    console.log("data.txt exists");
    const data = await fs.readFile("data.txt", "utf8");
    console.log("Content from readFile:", data);
    console.log("Completed reading file content asynchronously");
  } catch (err) {
    console.log("data.txt does not exist.");
  }
}

readTextFile();
```

実行してみます。

```bash

$ mv data.txt data.txt.back
$ node sample1.js 
data.txt does not exist.

$ mv data.txt.back data.txt
$ node sample1.js 
data.txt exists
Content from readFile: freeCodeCamp is awesome!

Completed reading file content asynchronously
```

やはり fs.promise を利用すると想定の順番通りで処理しやすくていいなと感じました。

## まとめ：読むだけでなく動かしてみるといろいろ発見がありました！

1日目の記事で、毎日少しでも続ける、コードを書いてみることの大切さを触れていますが、やはりここしばらく離れていたので、いろいろなことを忘れているものだと痛感しました。
リファレスを調べる際には、Node.js の最新版が 23 以上だとかも驚きです。
(※ Azure ポータル上での Cloud Shell では、この時点で LTS 版の 18 が入っていました)

お仕事内容でも変わってくるのですが、API の動作確認のためにスクリプトを組むことは時々あります。そうした際に、シンプルに試せる方法を1つか2つ身につけておくと、お仕事であっても「うまくいった！」というタイミングで非常に楽しいものがあります。

完全に覚えている必要はないので、「たしかこんな関数だった、API だったかな」「呼び出し方はこうだったかな」といったものを、ぼんやりとでも思い出せるレベルであれば、なんとかなるかとも思いました。