はじめに
既にホスティングサービスが終了している懐かしの「ジオシティーズ」ですが、スケジュール上では 2020/3/31 に全ファイルの削除が行われるようです。
自分にはジオシティーズ上で 2005年くらいまで更新していたサイトがありましたので、FTP でファイルを救出し Netlify の無料枠にて記念に再ホストすることにしました。
…できたものの何かが足りない。
アクセスカウンターだ!
ということで 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.
動作デモ(しばらくアクセスがないとカウントが 1
に戻ります)
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();
}
jsdom
は dom.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 を作成してみました。無料枠での挑戦です。