2021年7月14日水曜日

Deno と Node

Deno と Node.js を考えるとき、嫌でもモジュールについて考える必要が出てきます。



モジュールの扱いの違い

Deno はすべてがモジュールとして扱われるけど、Node.js は 基本的にモジュールとしては扱いません。 Node.js は .mjs 拡張子かつ、package.json で { "type" : "module" } を指定した場合に限りモジュールとして動作します。 つまり動作モードが 2つあるのが Node.js なのだけど、動作モードの違いで色々問題が起きる。

(1) モジュールとして動作させる場合 import で読み込み、top-level await が効く。 モジュールとして動作させない場合 require で読み込み、top-level await が効かない。 Node.js は現時点では require を使うのが基本になっているけど、今後は一体どちらをサポートすればいいのかわかりにくい (後述)。 モードの混在は言語仕様としてよろしくない。

(2) モジュールは当然ながら単体で動作します。 これが意味するところは Global Object は使えないということです。 例えば Node.js の __dirname などは Deno に存在しないし、 Node.js もモジュールとして動かそうとすると、__dirname などが使えなくなる。 __dirname は自分で定義しなければいけない。 そのへんが起因となって、モジュールの定義の仕方も違っている。

(3) 再掲しますが、モジュールとして扱うかどうかは package.json に { "type": "module" } があるかどうかで決定します。 package.json をわざわざ作らないといけないので、ちょっとしたスクリプトを書きたいときに困ります。

(4) Node.js では、混在するモードをどのように扱うのでしょうか。package.json 以外にも拡張子で判断します。 .mjs 拡張子で定義されるモジュール群からは、require などを使う .cjs / .js を以下のように呼び出して使えます。
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const cjs = require("test.js");
しかし .cjs から module を呼び出すのは相当大変みたい。 つまり現状では .mjs で作るべきというのはわかったのですが、 これでは既存の遺産がどうなるのかまったく信用できない言語になってるのは致命的と思う。 正直このようなことは考えたくないし、考えさせてはいけないと思う。

Node.js どうすんのこれ

2つのモードができて統一性がなくなってしまうと、無駄に覚えるコストが出てきて良くない。 ブラウザ側を見ても、Chrome と Firefox は既に top-level await に対応しており、Safari も 15 から対応する。 Node.js の require はブラウザの仕組みとズレてきている。

問題を解決しようとするプロジェクトも結構出てきているみたいです。 Deno プロジェクトを Node.js に変換するものであったり、Node.js から Deno globals を呼び出すものがある。 ただ top-level await に動作モードの違いがあるということは、動作モードの異なるライブラリがどんどん混在していくということでもある。

このへんの話を見ていると、Node より Deno のほうがだいぶ楽だなあと思いました。 みんながスーパー Node.js ユーザーではないので、のちのちどうやってサポートされるのかわからんものは困るのです。 Node.js はサポート期間が極めて短い言語なので、Node.js 14 がドロップされた時点で import 側にシフトするという事なのでしょうが、 正直こんなに不安定な状態で突き進んでいるようでは、少し問題を感じますね。 Deno は依存関係周りをどうするのか若干不透明感があるけど、エコシステムが安定しているので、それだけで信頼できる。

Deno はモジュールの作り方がすこし違う

Node.js の node_modules に過度に慣れてしまうと、とりあえず何でもデータをキャッシュしておけばどうにでもなります。 しかし Deno はキャッシュの概念がないので、実装方法も少し変わってきそうです。 例えば何らかのデータベースを読み込む必要がある場合、どうすべきでしょう。

おそらく (1) 実行時に fetch でロードするか、(2) git clone させて実行させるかのどちらか。 毎回ロードするのは面倒なので、2 を想定しているんじゃないかと思います。 ということでデータとモジュールが明確にわかれるのが Deno と言えます。 ただ少し面倒なので 1 もできるように実装して気が付くのは、 Node.js には fetch が存在しないため無駄にライブラリが必要になるということです。 まったく同じ実装にするのは非常に面倒。

で、ライブラリはどう書くのがいいの?

Node.js も Deno も初心者なのでよくわからないけど、以下のように書くのが良いと思いました。 命名規則は Node.js なら main.js / main.mjs、Deno なら mod.js / mod.ts をルートとするのが通例的に良さそう。

Node.js + module (main.mjs)

import * as fs from "fs";
import * as readline from "readline";
import { fileURLToPath } from "url";
import { dirname } from "path";

class YomiDict {
  static async load(filepath) {
    const dict = {};
    if (filepath) {
      const __filename = fileURLToPath(import.meta.url);
      const __dirname = dirname(__filename);
      filepath = __dirname + "/yomi.csv";
    }
    const fileReader = fs.createReadStream(filepath);
    const rl = readline.createInterface({ input:fileReader });
    for await (const line of rl) {
      const arr = line.split(',');
      const word = arr[0];
      const yomis = arr.slice(1);
      dict[word] = yomis;
    }
    const yomiDict = new YomiDict();
    yomiDict.dict = dict;
    return yomiDict;
  }

  constructor() {
    this.dict = {};
  }

  get(word) {
    return this.dict[word];
  }
}

export { YomiDict };

Node.js (main.js)

const fs = require("fs");
const readline = require("readline");

class YomiDict {
  static async load(filepath) {
    const dict = {};
    if (!filepath) {
      filepath = __dirname + "/yomi.csv";
    }
    const fileReader = fs.createReadStream(filepath);
    const rl = readline.createInterface({ input:fileReader });
    for await (const line of rl) {
      const arr = line.split(',');
      const word = arr[0];
      const yomis = arr.slice(1);
      dict[word] = yomis;
    }
    const yomiDict = new YomiDict();
    yomiDict.dict = dict;
    return yomiDict;
  }

  constructor() {
    this.dict = {};
  }

  get(word) {
    return this.dict[word];
  }
}

module.exports = YomiDict;

Deno (mod.js)

import { readLines } from "https://deno.land/std/io/mod.ts";

class YomiDict {
  static async fetch(url) {
    const dict = await fetch(url)
      .then((response) => response.text())
      .then((text) => {
        const d = {};
        text.split("\n").forEach((line) => {
          if (!line) return;
          const arr = line.split(",");
          const word = arr[0];
          const yomis = arr.slice(1);
          d[word] = yomis;
        });
        return d;
      }).catch((e) => {
        console.log(e);
      });
    const yomiDict = new YomiDict();
    yomiDict.dict = dict;
    return yomiDict;
  }

  static async load(filepath) {
    const dict = {};
    if (!filepath) {
      filepath = "./yomi-dict/yomi.csv";
    }
    const fileReader = await Deno.open(filepath);
    for await (const line of readLines(fileReader)) {
      const arr = line.split(",");
      const word = arr[0];
      const yomis = arr.slice(1);
      dict[word] = yomis;
    }
    const yomiDict = new YomiDict();
    yomiDict.dict = dict;
    return yomiDict;
  }

  constructor() {
    this.dict = {};
  }

  get(word) {
    return this.dict[word];
  }
}

export { YomiDict };
将来使うかも知れないので作り方をメモっておきます。 今まで無理やり同期化してたから気付かなかったけど、 ファイル読み込みするとコンストラクタから追い出されるのは、良いんだか悪いんだか…。

0 件のコメント: