Node.jsでプラグインの仕組みを作る方法

この記事は[Node.js Advent Calendar 2016](http://qiita.com/advent-calendar/2016/nodejs)の5日目です。

昨日は……只今タイムトラベル中の会長 @yosuke_furukawa さんでした。
→タイムトラベルから戻られました。[Exploring Node.js Future というタイトルで jsconf.asia で発表してきました。](http://yosuke-furukawa.hatenablog.com/entry/2016/12/08/113158)

まえがき
---

マルチテナントシステムなんかで実装をシンプルにするために特定テナントでしか使わない処理をプラグインとして切り出すのはよくある設計だと思います。
Javaだとリフレクションつかって`完全修飾名`→`Class`に変換すると思いますが、JavaScriptだとどうやるのかな？ と思って考えてみました。

本記事を書くにあたってwebpackを参考にしようと思ったら思いの外複雑だったので、[ESLintのルール読み込みのソース](https://github.com/eslint/eslint/blob/master/lib/load-rules.js)を参考にしました。

作戦
---

1. プラグインが置かれているディレクトリのファイル一覧を取得
2. 適用プラグインリストを用いて、ファイル一覧から適用するプラグインを絞り込み
3. `require()`で読み込みながら処理する

サンプル
---

```js
const fs = require('fs');
const path = require('path');

const pluginsDir = path.join(__dirname, 'plugins');

// プラグインオブジェクトの入れ物
const pluginObjects = Object.create(null);

// プラグインファイルパスのオブジェクト化
fs.readdirSync(pluginsDir).forEach(file => {
  if (path.extname(file) !== '.js') {
    return;
  }
  pluginObjects[file.slice(0, -3)] = path.join(pluginsDir, file);
});
```

こうすれば`__dirname/plugins`にプラグインファイル`a.js`, `b.js`,`c.js`があった場合に`pluginObjects`に以下のように使用可能なプラグインのファイルパスが読み込まれます。キー名はファイル名の`.js`抜きです。

```js:pluginObjects
{ a: '/path/to/plugins/a.js',
  b: '/path/to/plugins/b.js',
  c: '/path/to/plugins/c.js' }
```

あとはプラグイン側に共通の関数を持っていることを前提にするなどして処理を行えばよいです。

すごくシンプルですが、読み込まれるプラグイン側をこのようにfilter()関数を持つオブジェクトにしておきます。

```js:plugins/a.js
module.exports = {
  filter: (data) => {
    // 与えられた文字列から1文字目を返す
    return data.substr(0, 1);
  }
};
```

適用するプラグインファイルを絞り込んで、`require()`して`filter()`を適用してきます。

```js
const data = 'hoge'; // フィルタ処理するデータ
const applyPlugins = ['a', 'c']; // 適用するプラグイン

Object.keys(pluginObjects).filter((pluginId) => {
  // 適用するプラグインを絞り込み
  return applyPlugins.includes(pluginId);

}).forEach((pluginId) => {
  // プラグインを読み込んで
  const plugin = require(pluginObjects[pluginId]);
  // filter()関数があれば実行
  if (typeof plugin.filter === 'function') {
    console.log(plugin.filter(data));
  }
});
```

読み込み側の全体像はこんな感じです。

```js:filter-by-plugins.js
const fs = require('fs');
const path = require('path');

const pluginsDir = path.join(__dirname, 'plugins');

// プラグインオブジェクトの入れ物
const pluginObjects = Object.create(null);

// プラグインファイルパスのオブジェクト化
fs.readdirSync(pluginsDir).forEach(file => {
  if (path.extname(file) !== '.js') {
    return;
  }
  pluginObjects[file.slice(0, -3)] = path.join(pluginsDir, file);
});


// フィルタ処理するデータ
const data = 'hoge';

// 適用するプラグイン
const applyPlugins = ['a', 'c'];

Object.keys(pluginObjects).filter((pluginId) => {
  // 適用するプラグインを絞り込み
  return applyPlugins.includes(pluginId);

}).forEach((pluginId) => {
  // プラグインを読み込んで
  const plugin = require(pluginObjects[pluginId]);
  // filter()関数があれば実行
  if (typeof plugin.filter === 'function') {
    console.log(plugin.filter(data));
  }
});
```

これを実行すると

```
h  <- プラグインa.jsにより1文字目が出力
g  <- プラグインc.jsにより3文字目が出力
```

となります。

### テナントごとに異なる処理を適用したいとき

`applyPlugins`をDBやJSONなどから読み込むようにすれば、テナントごとに適用するプラグインを設定出来るようになります。

### テナントごとに異なるパラメータをプラグインに渡したいとき

```js
const applyPlugins = {
  a: {param1: 'foo', param2: 'bar'},
  c: {}
}

Object.keys(pluginObjects).filter((pluginId) => {
  // applyPluginsのキー要素で、適用するプラグインを絞り込み判定
  return Object.keys(applyPlugins).includes(pluginId);

}).forEach((pluginId) => {
  // 省略
});
```

こんな感じで、`applyPlugins`をオブジェクトにして、キー要素で適用判定するようにすればプラグインへの引数もカンタンに持たせることが出来ます。

以上、参考になれば幸いです。

明日は @n0bisuke さんのNodeBotsネタです。
（この2人に挟まれるのってプレッシャーやな……）

参考
---

* [ESLint](https://github.com/eslint/eslint)
    * [lib/load-rules.js](https://github.com/eslint/eslint/blob/master/lib/load-rules.js)