Netlify Functions で古のアクセスカウンター（アクセサリー）をつくる

## はじめに

既にホスティングサービスが終了している懐かしの「ジオシティーズ」ですが、[スケジュール上では 2020/3/31 に全ファイルの削除](https://info-geocities.yahoo.co.jp/close/)が行われるようです。

自分にはジオシティーズ上で 2005年くらいまで更新していたサイトがありましたので、FTP でファイルを救出し [Netlify](https://www.netlify.com/) の無料枠にて記念に再ホストすることにしました。

…できたものの何かが足りない。

![2020-03-30_01-19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102286/e32bf371-2b95-52ce-2d4e-a342ec90ed19.png)

アクセスカウンターだ！

ということで Netlify に備わる Lambda なサービス Netlify Functions でアクセスカウンターをふとつくってみることにしました。

お気づきのように、Lambda のインスタンスが寝てしまうとカウンターが「飛ぶ」アクセスカウンター（アクセサリー）となっていますが、現代風に JSON や SVG などを使って処理するようになっています。

## Netlify Functions について

[Netlify Functions](https://docs.netlify.com/functions/overview/) は静的ファイルホスティングサービス Netlify が提供する、くだけて言えば Amazon AWS Lambda に対するプログラムの自動デプロイの仕組みで、Netlify のアカウントにて（クレジットカード登録が必要な AWS アカウントなしに）Lambda を利用することができます。

## この記事で分かること

アクセスカウンターの動きはどうだろうという感じですが、いくつかの処理が参考になるかもしれないと Qiita に投稿することにしました。何か使える部分があったら幸いです。

* Lambda でファイルシステムを操作する
* netlify-lambda で追加の webpack.config を構成する
* Lambda で jsdom を使って DOM を操作する

## ソースコードと動作デモ

ここで使われているソースコードを github にコミットしてあります。ソース全容を確認しながら読んでいただくと分かりやすいかも知れません。

ソースコード一式

> Netlify Functions Template. Includes joke access counter sample program.
>
> [https://github.com/h1romas4/netlify-functions-template](https://github.com/h1romas4/netlify-functions-template)

動作デモ（しばらくアクセスがないとカウントが `1` に戻ります）

> [https://maple4estry.netlify.com/](https://maple4estry.netlify.com/)

## Lambda でファイルシステムを操作する

Netlify Functions(Lambda) でも nodejsの `fs` オブジェクトを使ってファイルを操作することが出来ます。

`fs` で作成を行ったファイルは、この「アクセスカウンター」の動きどおり、インスタンスのリビルドなどのタイミングで消されますし、スケールした場合は値が分裂しますので、アルゴリズムとしてステートの性質をあてにしてはいけません。

API へのアクセス数をカウントする JSON ファイルを `/tmp` に書き込む処理抜粋:

```javascript
import fs from 'fs';
import * as common from '../common'

// async await 版の fs オブジェクト
const fsp = fs.promises;

// カウンターファイル JSON 出力パス
const counterJsonFile = common.tmp + "/counter.json";

/**
 * Lambda エンドポイント
 */
exports.handler = async (event) => {
    const now = new Date();
    let counterJson = {
        "createDate": now,
        "updateDate": now,
        "count": 1
    };
    try {
        // カウンターファイル存在確認
        const data = await fsp.readFile(counterJsonFile);
        // カウンターファイルが存在すれば JSON 解析してカウンターを更新
        counterJson = JSON.parse(data.toString('utf-8'));
        counterJson.updateDate = now;
        counterJson.count++;
    } catch(e) {
        // カウンターファイルがなければ例外を潰して新規作成
    }
    // カウンターファイル書き込み
    await fsp.writeFile(counterJsonFile, JSON.stringify(counterJson));
    // ...
}
```

Lambda のハンドラーを `exports.handler = async (event)` と `async` として `fs` の操作は `fs.promises` からもらったオブジェクトで `await` してあげると簡単でした。

ちなみにこの処理は `readFile` から `writeFile` までに間がありますのでアトミックなカウントアップはできません。また nodejs の fs の実装を確認していませんが、ジオシティーズ世代の CGI カウンターよろしく壊れる可能性もあるでしょうか？（懐かしい）。

さて、`/tmp` は Lambda で使えるテンポラリー領域となっていますが、開発を行うローカル環境でそのまま `/tmp` にかかれると確認などが面倒なためちょっと小細工をしています。

AWS で設定される環境変数の有無で `/tmp` の位置を切り替え:

```javascript
// カウンターファイルを作成するテンポラリーディレクトリ
export let tmp = '/tmp'
// AWS Lambda で動いていない場合はテンポラリーを dist/tmp に設定
if(!("AWS_REGION" in process.env)) {
    tmp = "./dist/tmp"
    try {
        fs.mkdirSync(tmp);
    } catch(e) { }
}
```

Netlify Functions から提供されている [netlify-lambda](https://github.com/netlify/netlify-lambda) SDK が `./dist/api` 以下にビルドを出力するため、合わせてテンポラリー領域を `./dist/tmp` に設定しています。

## netlify-lambda で追加の webpack.config を構成する

[netlify-lambda](https://github.com/netlify/netlify-lambda) SDK は内部的に webpack を使ってビルドしていますが、標準の webpack 設定をマージできる `--config` オプションが準備されています。

```
netlify-lambda --config ./webpack.functions.js build src/functions/endpoint
```

本アクセスカウンターでは、古では GIF 画像などで処理していたカウント画像に代わり、SVG/CSS/HTML を Lambda 上で処理し数字を描いてクライアントに返却したく、処理前の `.html` を文字列として `import` するため `raw-loader` を webpack に追加設定しています。

```javascript
// webpack.functions.js
module.exports = {
    optimization: { minimize: false },
    module: {
        rules: [
            {
                test: /\.html$/i,
                exclude: /node_modules/,
                use: 'raw-loader'
            }
        ],
    }
};
```

この設定によりプログラムから `html` 文字列変数として `import` ができます。

```javascript
// counter.js
import html from '../resources/fujilcd.html'
```

このような追加の webpack 設定がある場合は `--config` オプションを活用すると良さそうです。

なお、Lambda 上にデプロイするソースをミニマイズしてもあまり意味がないため、どのプロジェクトでもこのコンフィグで `optimization: { minimize: false },` を入れておくとビルド時間短縮に役立つかもしれません。

## Lambda で jsdom を使って DOM を操作する

nodejs 上で html などの DOM 操作を行う [jsdom](https://github.com/jsdom/jsdom) パッケージを Netlify Functions(Lambda?) 上で使う場合は少々コツが必要なようです。

通常通り `packege.json` に依存関係をいれると、

```
Error while initializing entrypoint: { Error: Cannot find module 'canvas'
```

というエラーで `canvas` パッケージを依存に入れても動作しませんでした。 issue を探ったところワークアラウンドがありましたので、この方法で回避しています。

> https://github.com/jsdom/jsdom/issues/1708#issuecomment-462990288
> 
> I was able to work around this by adding "canvas": "file:./canvas" to the dependencies section of the app's package.json and creating canvas/index.js containing simply module.exports = {}.

プロジェクト上に空の `canvas` モジュールをつくって依存に追加:

```javascript
// package.json
{
    "dependencies": {
        "jsdom": "^16.2.1",
        "canvas": "file:./src/functions/canvas",
        "utf-8-validate": "^5.0.2",
        "bufferutil": "^4.0.1"
    },
}
```

`jsdom` が導入できれば、先程 import したような文字列 HTML を `new JSDOM(html)` で DOM 化しウェブブラウザーと同様に `document.querySelector` などで操作することができます。

`import` したカウンター表示 [HTML](https://github.com/h1romas4/netlify-functions-template/blob/master/src/functions/resources/fujilcd.html#L1-L174)を DOM 操作して CSS クラスを付けてカウント数を表示:

```javascript
// counter.js
function updateLCD(html, number) {
    const dom = new JSDOM(html);
    const { document } = dom.window;

    let countString = number + ""
    countString = ("0".repeat(maxCount) + number);
    countString = countString.substring(countString.length - maxCount)

    for(let i = 0; i < countString.length; i++) {
        let number = countString.substring(i, i + 1);
        const digi = document.querySelector(`.digit-${i} svg`);
        digi.removeAttribute('class');
        digi.classList.add(`num-${number}`);
    }

    return dom.serialize();
}
```

`jsdom` は `dom.serialize();`にてできた DOM を文字列化できますので、これをアクセスしてきたウェブブラウザーに返却し、

```javascript
// counter.js
exports.handler = async (event) => {
    // ...
    // カウンター数 SVG を生成
    counterJson.html = updateLCD(html, counterJson.count);
    return {
        statusCode: 200,
        headers: {
            "Content-Type": "application/json; charset=utf-8",
        },
        body: JSON.stringify(counterJson)
    }
};
```

ウェブブラウザーは shadowDOM で画面にそのまま書き出しています。

`/v1/counter`として Netlify Functions にデプロイした Lambda を呼び出すクライアントのソース:

```html
<!DOCTYPE html>
<html>
<body>
<access-counter></access-counter>

<script>
  fetch('/v1/counter').then(function(response) {
    return response.json()
  }).then(function(json) {
    customElements.define('access-counter',
      class extends HTMLElement {
        constructor() {
          super();
          const shadowRoot = this.attachShadow({ mode: 'open' });
          shadowRoot.innerHTML = json.html;
      }
    })
  });
</script>
</body>
</html>
```

## 終わりに

現代版ジオシティーズとも言える Netlify を今回初めて使ってみたのですが、Netlify Functions 含めいろいろできて便利でした。

試しに検索エンジン API もつくってみましたので、自分のブログになってしまっていますが、気になる方がもしいらっしゃいましたらご覧ください!

> [Netlify Functions で検索エンジン API をつくる](https://another.maple4ever.net/archives/2847/)
>
> ふと思いつきまして Netlify で使える Lambda なサービス、Netlify Functions を用いて参照系検索 API を作成してみました。無料枠での挑戦です。

![netlify-01.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102286/d1f970fa-3dfe-58f0-99dd-ed8f3ea50ddd.jpeg)