Node-REDのカスタムノードを作成してみた

# はじめに
今回は、業務で必要になったため、Node-REDのカスタムノードを作成し、その過程を共有します。

Node-REDのカスタムノード作成方法は公式にわかりやすい記事があります。
以下はNode-REDの公式ドキュメントのリンクです。カスタムノードの作成手順が詳細に解説されています。

https://nodered.jp/docs/creating-nodes/first-node

本Qiitaアカウントで、Chrome拡張を取り上げたこともありますが、基本的な構成や考え方が非常に似ていると感じました。
Chrome拡張を作ったことがある方は抵抗なく始められるのではないでしょうか。

今回は、以前私が作成した以下の記事をChrome拡張の比較対象として取り上げます。

https://qiita.com/ydclab_0007/items/c1d235b524e8223fbf91

こちらの記事で取り上げた題材をNode-REDのカスタムノードで作り、Chrome拡張と似ていると感じた点を紹介していきます。
※以降の本文で登場する「参照元記事」はこちらの記事を指します。

# お題
参照元記事で取り上げたお題は

> Qiitaのトップページ内の「おすすめの記事」に表示される記事リストから「いいね数301以上の記事」をハイライトするChrome拡張

でした。
本記事では、

```
Qiitaのトップページに表示される「おすすめの記事」から
「いいね数100以上の記事」のタイトルとURLを返すNode-REDカスタムノード
```
を作成します。
※本記事作成時、300いいね以上の記事が一つだったため、今回は閾値を100にしました

以下はQiitaのトップページの画像です。
オレンジ枠の部分に表示される情報をターゲットとしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/1fcabfd6-0f9e-e448-c8f8-a7317fdbb7a5.png)


# 構成
以下の3ファイルで構成します。


### ①package.json
　Node.jsモジュールの内容を記述するための標準的に使われるファイルです。
　作成ノードをnpmモジュールとしてパッケージ化するため、モジュールの内容を記述します。

### ②sample.html
　カスタムノードの配置先や、設定画面をデザインします。

### ③sample.js
　カスタムノードの動作を実装します。

# 実装

package.jsonではモジュールの内容を定義します。

``` json:package.json
{
  "name": "sample",
  "version": "0.0.1",
  "description": "Qiitaトップページのおすすめ記事から100いいね以上の記事を抽出します",
  "main": "sample.js",
  "node-red": {
    "nodes": {
      "sample": "sample/sample.js"
    }
  },
  "dependencies": {
    "axios": "^0.21.1",
    "cheerio": "^1.0.0-rc.10"
  }
}
```

今回は
httpでのページ取得にaxios
取得したHTMLのDOMパースにcheerio
を用いるため、dependenciesに定義しています。


sample.htmlにはカスタムノードの所属カテゴリーと色、アイコンなどを定義するのみとしました。

``` html:sample.html
<script type="text/javascript">
    RED.nodes.registerType('sample', {
        category: 'function',
        color: '#a6bbcf',
        defaults: {
            name: {value: ""}
        },
        inputs: 1,
        outputs: 1,
        icon: "file.png",
        label: function() {
            return this.name || "sample";
        }
    });
</script>
```


sample.jsではqiitaのトップページからHTMLを取得しcheerioにてパースします。
取得結果から、likes変数にいいね数を取得し、100以上のものだけをarticles配列に追加します。titleとそのurlも同様に取得して返却します。

``` js:sample.js
module.exports = function(RED) {
  const axios = require('axios');
  const cheerio = require('cheerio');

  function QiitaScraperNode(config) {
    RED.nodes.createNode(this, config);
    const node = this;

    node.on('input', async function(msg) {
      try {
        const response = await axios.get('https://qiita.com');
        const $ = cheerio.load(response.data);

        const articles = [];
        $('div.style-1p44k52 > article.style-1w7apwp').each((index, element) => {
          const likes = $(element).find('div.style-1fvis2l span.style-qrq9vy').text().trim();
          if (likes >= 100) {
            const title = $(element).find('h2.style-1ws5e6r a.style-2vm86z').text().trim();
            const url   = $(element).find('h2.style-1ws5e6r a.style-2vm86z').attr('href');
            articles.push({title, url, likes});
          }
        });

        msg.payload = articles;
        node.send(msg);
      } catch (error) {
        node.error('Error fetching', error);
      }
    });
  }
  RED.nodes.registerType("sample", QiitaScraperNode);
}
```

Chromeのdeveloperツールで確認したDOMへアクセスしてみましたが正常に取得できず。
どうもonLoadなどで改変が入っているようで、本処理実装中にデバッグ情報を出力するなどしてelementの中身を確認し、本例のような実装としました。

:::note warn
本記事執筆段階（2024/11/20）ではこの処理にて記事一覧が取得できていますが、DOM構成やクラス名は変更される可能性があるため、実装時にはelement変数内を確認し、必要に応じてfindの引数を調整してください。
:::

# 動かしてみる
npmでNode-REDへインストールしフローを作成しました。
以下はNode-REDのフロー作成画面のスクリーンショットです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/213f4f2a-b4ea-514a-2830-476d89a9c9ce.png)


実際に動かすとデバッグ出力として以下が得られました。
以下はNode-REDのデバッグ出力ウィンドウのスクリーンショットです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2517800/e6385e37-e9b1-883c-5ab1-c2888a8d35ff.png)



# Chrome拡張と対応（筆者の感じ方）
今回Node-REDのカスタムノードを始めて作成しましたが、Chrome拡張を作っているときと同じ感覚で実装することができました。出来上がった内容を見ても、筆者はChrome拡張に非常に似ていると感じました。

#### package.json(Node-RED) ≒ manifest.json(Chrome拡張)
アプリケーション名や概要、与える権限など、定義すべき内容がほぼ同じような情報ですんなり理解することが出来ました。

#### sample.html(Node-RED)　≒　設定画面のデザイン(Chrome拡張)
ここは若干拡大解釈気味ですが、sample.htmlの内容が設定画面を主と捉えると、Chrome拡張のオプション画面を実装する際に用意する.htmlと近しい情報です。
今回はNode-REDのデザイン画面上に配置するための情報だけを記載したため、設定画面の定義はありません。Chrome拡張の設定画面不要時と同じような理解で進められました。

#### sample.js(Node-RED) ≒ content.js / background.js(Chrome拡張)
挙動を実装するという意味で近しい情報と捉えています。
Node-RED特有の書き方はありますが、axiosやcheerioなどのAPIを活用することでJavaScriptライクな実装もでき、今回の記事の内容程度であれば特に苦労することは無く理解できました。

もちろん、クライアントサイドで動作するChrome拡張とサーバーサイドで動作するNode-REDカスタムノードには、動作環境や使用言語に違いがあります。

それでも
Chrome拡張を知っていてNode-REDカスタムノードを作ろうとする
或いはその逆にトライするコストはそう大きくないと感じました。

同じような思想で作られているのはエンジニアとして非常にありがたいと思います。
引き続き利用していこうと思います。