【Deno1.16～】ローカルファイルの読み込み方法4種

Deno1.16以降、Denoにはローカルファイルの読み込み方法が4種類存在します。
それぞれの利用方法と使い分けについて解説します。

## 方法1：ローカルファイルのFetch

Deno1.16以降、ローカルファイルのFetchが導入されました。

```ts:raw.githubusercontent.com/ayame113/deno_test/76d60f19850d362e214998f4ec141572f43f39dc/a.ts
// import.meta.urlを使用して、このファイルからの相対パスを作成
const res = await fetch(new URL("./README.md", import.meta.url));
console.log(await res.text());
// console.log(await res.json()); // jsonの読み込み等も可能
```

#### メリット1：リモート実行に対応

この方法のメリットは、（例えば）github上のファイルを相対パスで読み込むことができることです。

```shell
> deno run --allow-net=raw.githubusercontent.com https://raw.githubusercontent.com/ayame113/deno_test/76d60f19850d362e214998f4ec141572f43f39dc/a.ts
# deno_test
```

上記のように`deno run https://～`の形で実行すると、ファイルからの相対パスも`https://`から始まるパスになります。ここで後述する`Deno.readFile`等を使ってしまうと、リモートファイルの読み込みに対応していないため、エラーが出てしまいます。
`deno run https://～`の形で実行する予定がある場合は（例えばCLIツール）、`fetch`を使いましょう。

なお、これは「自ファイルからの相対パス」を使う場合の問題で、「カレントディレクトリからの相対パス」でファイル読み込みする場合は関係ありません。

#### メリット2：ファイルのストリーミングが可能

もう一つのメリットは、サーバーにおいてファイルのストリーミングが可能な事です。

```ts:ファイルサーバー
import { serve } from "https://deno.land/std@0.114.0/http/mod.ts";

serve((request) => {
  const relativePath = `.${new URL(request.url).pathname}`;
  return fetch(new URL(relativePath, import.meta.url)); // Responseオブジェクトを返す
});
console.log("http://localhost:8000/");
```

ここで、`serve`関数に渡すコールバックは`Response`オブジェクトです。そして、`fetch`の返り値も`Response`オブジェクトです。
よって、実装としては、`fetch`の返り値をそのままretuenするだけでOKです。
（Web APIを使用するメリットが生きていて、この設計がとても好きです。）

`Response`オブジェクトはファイルの内容をストリーミングできるので、巨大ファイルの配信に適しています。
サーバーから静的ファイルを配信する場合、ストリーミング可能な`fetch`を使いましょう。
Responseに対してContent-Typeを設定する方法は、[長くなったので別記事に書きました](https://qiita.com/access3151fq/items/a097acfc4e15f35b307a)。

また、標準ライブラリの関数を利用して、1行ずつの読み込みなども行うことができます。

```ts
import { readerFromStreamReader } from "https://deno.land/std@0.114.0/streams/mod.ts";
import { readLines } from "https://deno.land/std@0.114.0/io/mod.ts";

const response = await fetch("https://www.yahoo.co.jp/");
if (!response.body) {
  throw new Error("cannot read response body");
}
const reader = readerFromStreamReader(response.body.getReader());
for await (const line of readLines(reader)) {
  // 1行ごとに読み込み
  console.log(line);
}
```

#### メリット3：deno deploy 互換性

ローカルファイルfetchはdeno deployでも利用可能です。
（2022年1月ごろに対応したようです。）
deno deployで`file://`始まりのURLへのfetchを行うと、GitHubリポジトリ内のファイルを読み込むことができます。

## 方法2：`Deno.readTextFile` / `Deno.readFileSync`を使う

`Deno.readTextFile`はテキストファイルを読み取る関数です。

```ts
// 非同期実行
const content1 = await Deno.readTextFile("./test.csv"); // カレントディレクトリからの相対パス
const content2 = await Deno.readTextFile(new URL("./test.csv", import.meta.url)); // 自ファイルからの相対パス

// 同期実行
const content3 = Deno.readTextFileSync("./test.csv"); // カレントディレクトリからの相対パス
const content4 = Deno.readTextFileSync(new URL("./test.csv", import.meta.url)); // 自ファイルからの相対パス

console.log(content1);
console.log(content2);
console.log(content3);
console.log(content4);
```

ファイルの内容全体を1度に読み取ってメモリに格納するため、大きいファイルを読むのには向いていません。

#### メリット1：同期APIが用意されている

`Deno.readTextFile`は非同期関数なので、結果を`await`する必要があります。何らかの事情で非同期関数が使用できない時は、同期版の`Deno.readTextFileSync`が使えます。

#### メリット2：`fetch`に比べて`await`が少なく済む

fetchの例では`await`を2回使用しています。

```ts
const res = await fetch(new URL("./README.md", import.meta.url));
console.log(await res.text());
```

一方、`Deno.readTextFile`を使うと`await`が一回で済みます。

```ts
const content = await Deno.readTextFile(new URL("./README.md", import.meta.url));
console.log(content);
```

`Deno.readTextFile`を使うとコードの見通しがよくなった感じがします。（これは好みの問題ですが）


## 方法3：`Deno.readFile` / `Deno.readFileSync`を使う

`Deno.readFile`は`Deno.readTextFile`と似ていますが、返り値は文字列ではなく`Uint8Array`です。画像等のバイナリデータを読むのに向いています。

```ts
// 非同期実行
const content1 = await Deno.readFile("./test.csv"); // カレントディレクトリからの相対パス
const content2 = await Deno.readFile(new URL("./test.csv", import.meta.url)); // 自ファイルからの相対パス

// 同期実行
const content3 = Deno.readFileSync("./test.csv"); // カレントディレクトリからの相対パス
const content4 = Deno.readFileSync(new URL("./test.csv", import.meta.url)); // 自ファイルからの相対パス

// Uint8Arrayを文字列に変換するやつ
const decoder = new TextDecoder();

console.log(decoder.decode(content1));
console.log(decoder.decode(content2));
console.log(decoder.decode(content3));
console.log(decoder.decode(content4));
```

[`Uint8Array`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)を文字列に変換したい場合は、`TextDecoder`を使います。
`Deno.readTextFile`と同様、こちらも同期API（`Deno.readFileSync`）が存在しています。

## 方法4：`Deno.open` / `Deno.openSync`を使う

`Deno.open`と標準ライブラリの組み合わせでも、ローカルファイルの読み込みができます。

```ts:1行ずつ読み込み
import { readLines } from "https://deno.land/std@0.114.0/io/mod.ts";

const file = await Deno.open(new URL("./test.csv", import.meta.url));
try {
  for await (const line of readLines(file)) {
    console.log(line);
  }
} catch (error) {
  file.close();
  throw new Error("failed to read file", { cause: error });
}
```

```ts:ファイルの内容全体を読み込み
import { readAll } from "https://deno.land/std@0.114.0/streams/mod.ts";

const file = await Deno.open(new URL("./test.csv", import.meta.url));
const decoder = new TextDecoder();
try {
  // await readAllの代わりにreadAllSyncも可能
  const content = await readAll(file);
  console.log(decoder.decode(content));
} catch (error) {
  file.close();
  throw new Error("failed to read file", { cause: error });
}
```

```ts:チャンクごとに分割して読み込み
import { iterateReader } from "https://deno.land/std@0.114.0/streams/mod.ts";

const file = await Deno.open(new URL("./test.csv", import.meta.url));
try {
  for await (const chunk of iterateReader(file)) {
    console.log(chunk);
  }
} catch (error) {
  file.close();
  throw new Error("failed to read file", { cause: error });
}
```

```ts:ReadableStreamに変換してサーバーから配信
import { serve } from "https://deno.land/std@0.114.0/http/mod.ts";
import { readableStreamFromReader } from "https://deno.land/std@0.114.0/streams/mod.ts";

serve(async (request) => {
  try {
    // TODO: パスがディレクトリの場合を検出する
    const file = await Deno.open(`.${new URL(request.url).pathname}`);
    // ReadableStreamに変換
    return new Response(readableStreamFromReader(file));
  } catch {
    return new Response("404 Not Found", { status: 404 });
  }
});

console.log("http://localhost:8000/");
```

`Deno.open`はファイルを開き、ファイルハンドラーを返す関数です。ファイルに対する操作が終わったら自分で`file.close()`を呼ぶ必要があります。

`Deno.open`はgoライクな`Deno.reader`インターフェースを返します。
標準ライブラリの関数のうち`Deno.reader`に対応する関数を使って、ファイルのストリーミングをすることができます。特に`readLines`が便利です。

2022年1月ごろから、deno deployでも利用できるようになりました。

## まとめ

||`fetch`|`Deno.readTextFile`|`Deno.readTextFileSync`|`Deno.readFile`|`Deno.readFileSync`|`Deno.open`|`Deno.openSync`|
|--|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|deno deploy互換性|○|○|×|○|×|○|×|
|リモートファイル読み込み（`deno run https://～`互換）|○|×|×|×|×|×|×|
|ストリーミング|○|×|×|×|×|○|○|
|同期or非同期|非同期|非同期|同期|非同期|同期|非同期|同期|
|戻り値|`Promise<Request>`|`Promise<string>`|`string`|`Promise<Uint8Array>`|`Uint8Array`|`Promise<Deno.Reader&Deno.ReaderSync>`|`Deno.Reader&Deno.ReaderSync`|
|備考|Deno1.16以降のみ|||||標準ライブラリと組み合わせて使用|標準ライブラリと組み合わせて使用|


使い道に合わせた方法を選びましょう。
特に、大きなファイルを扱う場合は、パフォーマンスの観点からストリーミングに対応している`fetch`や`Deno.open`を使ったほうがよいと思います。

また、deno deployではGitHubリポジトリ内のファイルをローカルファイルのように扱うことができます。deploy内では同期APIは使えず、非同期APIのみ対応しています。