LoginSignup
3
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-29

はじめに

既にホスティングサービスが終了している懐かしの「ジオシティーズ」ですが、スケジュール上では 2020/3/31 に全ファイルの削除が行われるようです。

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

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

2020-03-30_01-19.png

アクセスカウンターだ!

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

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

Netlify Functions について

Netlify Functions は静的ファイルホスティングサービス 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

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

https://maple4estry.netlify.com/

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

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

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

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

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 の位置を切り替え:

// カウンターファイルを作成するテンポラリーディレクトリ
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 SDK が ./dist/api 以下にビルドを出力するため、合わせてテンポラリー領域を ./dist/tmp に設定しています。

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

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 に追加設定しています。

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

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

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

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

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

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

nodejs 上で html などの DOM 操作を行う 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 モジュールをつくって依存に追加:

// 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を DOM 操作して CSS クラスを付けてカウント数を表示:

// 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();
}

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

// 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 を呼び出すクライアントのソース:

<!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 をつくる

ふと思いつきまして Netlify で使える Lambda なサービス、Netlify Functions を用いて参照系検索 API を作成してみました。無料枠での挑戦です。

netlify-01.jpeg

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2