🌐

clusterで使うためのjavascript講座(Cluster Scriptじゃないほう)

2022/12/09に公開

この記事は「clusterユーザーと創造する非公式 Advent Calendar 2022」の6日目の記事です
https://adventar.org/calendars/7589

Cluster Conference 2022で、Cluster ScriptでJavascriptを用いたギミック開発ができるようになったわけですが、非公式アドベンドカレンダーということで、いつもの平常運転で逆張りをしていこうと思います。

というわけでやっていきましょう。

はじめに

今回の記事は、Cluster Scriptを使わない、逆張りJS講座です。
相当雑ですし、参考にならないですし、参考にしなくていいです。
サンプルがクソコードなので、そこはご容赦ください。

もう一度言います。Cluster Scriptの解説はしません。
というわけで、やっていきましょう。
題して、「clusterのワールドで読めるポスターを返すAPIサーバーをnode.jsで作ろう」です。

ある意味去年のアドベンドカレンダーの記事の続きなので、こちらも読んでね。
https://zenn.dev/dolphiiiin/articles/0493543207d389#フォルダー上のランダムな画像を返却

今回の目標物

今日の主要都市の天気(18:00~0:00は明日の天気)のポスターをワールドに配置されたURL Textureに返すAPIを作っていきます。

URLにアクセスすると、ポスター画像がpngのblobとして返される

使うもの

とりあえず使うフレームワークとかライブラリです。

  • node.js
    JavaScriptを実行するためのサーバー
  • Express.js
    Webアプリケーションを作るためのフレームワーク
  • node-fetch
    外部APIをたたくためのライブラリ
  • puppeteer
    ヘッドレスなChromeを実行するためのライブラリ

とりあえず、node.jsのプロジェクトを作る

新規フォルダを作って、ターミナルで

npm init -y

これで、package.jsonというファイルが生成されます。

package.jsonを書く

今回のpackage.jsonです

{
  "name": "cluster-whether-news",
  "version": "1.0.0",
  "description": "whether image api",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.2",
    "node-fetch": "^2.6.7",
    "puppeteer": "^19.3.0"
  }
}

express.jsを使ってみよう

とりあえず、express.jsの使い方を少しだけ。

テキストを返す

//index.js
const express = require('express');
const app = express();

app.get('/', async (req, res) => {  //indexページにアクセスしたとき
  res.send("hello world");  //テキスト(hello world)をブラウザーに返す
});

// サーバーを起動する
app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

これを実行してみましょう。ターミナルで、次のように打つことで実行できます。

node index.js

実行したら、ブラウザーで、http://localhost:3000にアクセスします。

hello world!
ブラウザーにhello worldと表示されました。

htmlを返す

これは、単純なテキストを送信した例ですが、htmlを送信することで、装飾をすることもできます。

//index.js
const express = require('express');
const app = express();

app.get('/', async (req, res) => {
    //htmlを返す
    let html = `
        <body>
            <style>
                body {
                    background-color: antiquewhite;
                }
            </style>
            <h1>Hello World!</h1>
        </body>
    `;
    res.send(html);
});

// サーバーを起動する
app.listen(3000, () => {
    console.log('Server listening on port 3000');
});

実行してアクセスすると、黄色っぽい背景に太字でHello World!と表示されました。

画像をblobで返す

もうひとつやってみましょう。

//index.js
const express = require('express');
const app = express();

app.get('/', async (req, res) => {
  //base64でエンコードされた画像
  let buffer = "iVBORw0KGgoAAAANSUhEUgAAACEAAAAhCAYAAABX5MJvAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABrSURBVFhH7daxDcAgDERRk5aWJViGuTwPYhfGSSLFtwAUpPivQdf9wgXpftmG3rvNOWOtueI9igghQogQIoQIIUKIkF9EpDHG1qemlGI551hrkrtvRbTWrNYaaw03IUQIEUKEECFECBEfswegQxBLF8ZfLgAAAABJRU5ErkJggg"

  res.writeHead(200, {  //ブラウザーに返す形式を宣言
    'Content-Type': 'image/png',
    'Content-Length': Buffer.byteLength(buffer, 'base64')
  });
  res.end(buffer, 'base64');  //base64形式で画像を返す
});

// サーバーを起動する
app.listen(3000, () => {
    console.log('Server listening on port 3000');
});

実行してアクセスすると、灰色と白のチェッカー模様の画像が表示されます。

//base64でエンコードされた画像
  let buffer = "iVBORw0KGgoAAAANSUhEUgAAACEAAAAhCAYAAABX5MJvAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABrSURBVFhH7daxDcAgDERRk5aWJViGuTwPYhfGSSLFtwAUpPivQdf9wgXpftmG3rvNOWOtueI9igghQogQIoQIIUKIkF9EpDHG1qemlGI551hrkrtvRbTWrNYaaw03IUQIEUKEECFECBEfswegQxBLF8ZfLgAAAABJRU5ErkJggg"

  res.writeHead(200, {  //ブラウザーに返す形式を宣言
    'Content-Type': 'image/png',
    'Content-Length': Buffer.byteLength(buffer, 'base64')
  });
  res.end(buffer, 'base64');  //base64形式で画像を返す

この部分が肝になっていて、画像データをbase64という文字データに変換(エンコード)して、ブラウザーに返しています。
imgタグを用いたhtmlをブラウザーに返しても画像を表示することができます。
が、URL Textureで使用する場合は、画像をエンコードして返すことで確実に画像を表示できるでしょう。

外部APIから情報を受け取る

外部のAPIから情報を取得してみましょう。
今回は、天気のデータを取得したいので、気象庁のAPIを使用します。
例えば、札幌の天気を取得したい場合、

https://www.jma.go.jp/bosai/forecast/data/forecast/016000.json

試しにブラウザーでアクセスすると、札幌の天気がJSON形式で帰ってきます。

APIを叩くときは、FireFoxでたたくと、JSONが見やすくて便利
では、ブラウザーではなく、node.jsで札幌の天気を取得してみましょう。

const fetch = require('node-fetch');

const getWeather = async (code) => {
    const res = await fetch(`https://www.jma.go.jp/bosai/forecast/data/forecast/${code}.json`);
    const json = await res.json();
    return json;
}

(async () => {
    const json = await getWeather("016000");
    console.log(json);
})();

node-fetchライブラリを使用して、APIにアクセスし、取得したjsonをコンソールに出力しています。
APIにアクセスするのは一瞬では終わらないので、async/awaitを使って、順次処理するようにしましょう。
コンソールには、

[
  {
    publishingOffice: '札幌管区気象台',
    reportDatetime: '2022-12-07T17:00:00+09:00',
    timeSeries: [ [Object], [Object], [Object] ]
  },
  {
    publishingOffice: '札幌管区気象台',
    reportDatetime: '2022-12-07T17:00:00+09:00',
    timeSeries: [ [Object], [Object] ],
    tempAverage: { areas: [Array] },
    precipAverage: { areas: [Array] }
  }
]

と表示され、取得できていることがわかります。

ポスター画像を作る

データをAPIから取得して、ポスター画像を生成するための、部分の流れとしては、以下のようになります。

  1. ポスターサイズでhtml、cssを書く
  2. テキストを外部APIから取得した情報を基にreplaceしたりしておいていく。
  3. puppeteerで、htmlをレンダリング
  4. puppeteerでページ全体のスクリーンショットを撮影
    本当は、canvasを使って書いた方がいいんでしょうけど、canvasと仲が悪いので、htmlで生成しています。
    なんなら、htmlもAdobe XDとプラグインで自動生成して、整形しています。

    XDで試作してた時の

1~2は割愛して、3~4の部分はこんな感じです。

const browser = await puppeteer.launch();  //ブラウザーの起動
const page = await browser.newPage();   //ページの作成

await page.setContent(html);    //htmlをページにセット

// ページ全体が表示されるように、画像のサイズを指定する
const dimensions = await page.evaluate(() => {
	return {
        width: document.body.scrollWidth,
        height: document.body.scrollHeight
    };
});

// htmlをpng形式の画像に変換する
const image = await page.screenshot({
    type: 'png',
    width: dimensions.width,
    height: dimensions.height,
    fullPage: true
});

// 画像をブラウザーに返す
res.set('Content-Type', 'image/png');
res.send(image);

await browser.close();  //ブラウザーを閉じる

完成したもの

これらを色々とねれねりして、完成したものが記事の初めに出した画像です。
https://gist.github.com/Dolphiiiin/9391f470017322043f4bffd7fe308317
途中相当クソみたいなコードになっていますが、許してください。

実運用はしないの?

サーバーを用意できなかったので、この天気ポスターAPIは実運用はしないです。
Vercelで運用したかったんですけど、puppeteerがどうしてもうまく動きませんでした。
最初からhtmlじゃなくてcanvasと仲良くなってcanvasで実装すれば、Vercelで運用できた気もしたけど疲れました

Discussion