20210122のNode.jsに関する記事は6件です。

初めてのクラウド、初めての Azure -簡単なサーバーアプリをオンプレから移行-

初めに

*以下の記事を Qiita へ移植したものです。普段使用する技術情報プラットフォームが Qiita な方もいるかもということで。
(あとは単純に反応の違いとかあるのかという興味本位で)

https://zenn.dev/minominominoru/articles/fc40d09d89d24f

概要

皆さん、クラウド使ってますか?実際のところ使ったことはないという方も多いんじゃないかなと思ってたりします。そして使ってみたい、学んでみたいけど最初の一歩や学び方が分からない方も多いかなと思います。

やっぱり技術は実際に触ってみた方がイメージ掴みやすいし、学びやすいと(個人的に)思います。
なので、手元の PC でプログラムを実行した後に、クラウドに移行してみるまでの簡単なハンズオンを用意してみました。

なお、ちょっとしたツールのダウンロードや、アカウントの作成等は必要ですが、プログラミングの経験がない方でもできる限り簡単にできるように心がけて書いてみました。(指定したコマンドを入力するだけで試せますし、そんなに打つこともないです。)

所要時間はアカウントの作成や一部のツールのインストールを除けば 1 時間もあればできるんじゃないかなと思ってます。もちろん、無料で試せます。(アカウント作成でクレカは必要だったかと思いますが)

あくまでもちょっと体験してみるくらいにはなりますが、少し Web の知識の話とかも概要だけですが入れていたりするので、クラウドにこれから触るって方の最初の一歩の助けにでもなれたら良いなって思います。

対象の人

本記事は、以下の感じの方がメインのターゲットかなと思ってます。
- 「クラウドってよく聞くけど、実際のところ何が良いの?」って方
- 「クラウドや Azure 使ったことないけどちょっと試してみたい」って方
- プログラミングの経験はあまりないが体験してみたいって方
- 何でもよいからクラウドを体験してみたいって方

なので、例えば Azure や AWS 等に慣れている方からしたらあまり学びはないかもです。

ただ、上記の方々以外にも、node.js を使ったことがない方なんかにも、もしかしたら楽しんでいただけるのではないかなと思います。(欲を言うとちょっとでも気になったら読んでみて欲しいとは思ってます)

簡単な用語の説明

本当にごく一部、最小限知っててもらえたらという感じです。
厳密な定義というよりは、やんわりとイメージを持ってもらえたらくらいの解説にしてます。

一般的な web の用語

  • URL
    ザックリというとネットワーク上のリソースの場所です。例えば、https://www.google.com/ という URL を開くとよく見る Google の検索画面が表示されます。

  • HTTP リクエスト
    あるリソース(NW 上のサーバー等)に対して処理等を要求する。例えば、 Browserで URL へアクセスして Web ページを開くのはGETです。色んな種類の HTTP リクエストがありますが、ここでは GET と POST という2種類だけ出てきます。

  • エンドポイント
    リクエストを送る際に「そのサーバーのどの機能を使う?」って指定をするイメージです。例えば、www.hogehoge.com/pagea にアクセスしたら pageA,www.hogehoge.com/pageb にアクセスしたら pageb が出力されるみたいに、一つのサーバーに複数のエンドポイントを用意できます。

  • ローカル(ホスト)
    自分の PC 上でサーバーアプリを起動する際等に「ローカルで起動する」といった表現をします。

  • オンプレミス
    クラウドではなく、自分/自社のネットワーク環境のことだと思っていただけたらと。インターネットとかを経由しないのでクローズドなネットワークとかローカルネットワークとか言うこともあるかもです。(厳密な定義の差等はあるかもですが割愛)

Azure の用語

  • Azure
    そもそもですが、クラウドサービスの一つです。

  • App Service
    Azure のサービスの一種で Web アプリの作成等で使用します。
    https://azure.microsoft.com/ja-jp/services/app-service/

  • リソース
    あるサービスの自分が使用する個体です。例えば、上記の App Service を使って複数の Web サイトを作成する際には App Service のリソースを複数作成します。

  • リソースグループ
    上記のリソースを管理するために使用します。色んなサービスの色んなリソースを作成することになるので、うまく管理するのに使います。

  • Deploy
    ザックリというと自分の作成したアプリ等をクラウド上に配置することです。

準備

*私は windows 10 で実行しています。

前提条件

  • サーバーアプリ実行用の PC : スペックとかはそんなに必要ないです。
  • スマホ : 同上。ブラウザ(CHrome,Safari,Edge 等) を開けさえすれば大丈夫。
  • Wi-Fi 環境 : 上記の端末を同じネットワーク(Wi-Fi 環境)に繋げます。

準備作業

上記前提条件の上で。

  • Azure アカウント
    以下から Azure アカウントを作成します。クレカが必要だったかなと思います。
    https://azure.microsoft.com/ja-jp/free/

  • node.js
    ザックリいうと JavaScript という言語でサーバーアプリを作成する際によく使用します。
    Long Term Support(LTS) の方で自分の PC にあったものを入れてください。
    https://nodejs.org/ja/download/

  • VSCode
    コードの編集等で使用します。Azure との連携がよかったり、拡張機能で様々な機能が追加できます。
    https://code.visualstudio.com/Download

また、以下の拡張機能も追加してください。

  • Azure 拡張機能

Azure へのデプロイなどで使用します。各サービスごとに機能が出ていたりしますが、最低でも App Service の機能を入れてもらえたら大丈夫です。
https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack

  • REST Client

サーバーアプリの機能を試すのに便利な拡張機能です。個人的には普段からよく使ってます。
https://marketplace.visualstudio.com/items?itemName=humao.rest-client

オンプレでサーバーを実行

さて、前準備が終わったらまずはオンプレでサーバーアプリを実行してみましょう。
ちなみに、早くクラウドに触りたいって方は一応サンプルコードだけ下から落として、クラウドのところまで飛んでもらうのもありです。(ただ、node.js や Cloud が初めての方は頑張って書いたのでぜひ読んで欲しいです)

以下の Github にサンプルコードを用意しました。
https://github.com/MinoMinoMinoru/simple-restify-server/tree/master

git で clone する方法もありますが、そういった方法に慣れない方は zip でダウンロードすることもできます。

image.png

とりあえず、プロジェクトファイル一式がある場所で Powershell(適宜好みのコマンド ツール)を開きましょう。Windows の方は、以下の画像のようにエクスプローラーから開くこともできます。

image.png

image.png

serverアプリの実行

以下のコマンドで必要な module を install します。(package.json にそういう感じのことを書いてます。気になる方は npm というパッケージ管理ツールについて調べてみてください。)

npm install

続けて以下のコマンドでサーバープログラムを実行します。

npm start

まずはブラウザで以下の URL にアクセスしてみましょう。

http://localhost:3978

以下のように私が雑に作ったページが出力されます。

image.png

次に、以下の URL にアクセスしてみましょう。

http://localhost:3978/test-get


image.png

なんか違うページが表示されているかと思います。
では、ちょっとだけ実行されているコードを見てみましょう。

const restify = require('restify');
const fs = require('fs');

// Create HTTP server
const server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, () => {
    console.log("Please open the following URLs in Browser")
    console.log("[URL A]");
    console.log("http://localhost:3978")

    console.log("[URL B]");
    console.log("http://localhost:3978/test-get")

    console.log("[URL C]");
    console.log("http://<Your Localhost Address>:3978/test-get");
});

// Listen for incoming requests.
server.get('/', (req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.write("Welcome to the start test page");
    res.end();
});

server.get('/test-get', (req, res) => {
    fs.readFile("./index.html", 'UTF-8',
        function (err, data) {
            res.writeHead(200, { 'Content-Type': 'text/html' });
            res.write(data);
            res.end();
        });
});

server.post('/test-post', (req, res) => {
    res.send({ TestBody: "Test Post Response Body" });
    res.end()
});

server.get() という風に書いている部分が 2 箇所あるかと存じますが、server.get('/',...) と書かれているところと、server.get('/test-get',...) と書かれているところがありますね。

'/' のエンドポイント方ではプログラム内で用意した文字列を返していて、'/test-get' のエンドポイント方では index.html というファイルを readFile していることが何となくわかるかなと思います。 (細かい話は node.js の話になるので、何となくで感じていただけたらと。)

HTML 編集

index.html の中身を少し編集してみてください。以下が標準状態(というか私が用意したまんまの状態)です。

の間とかを自由に編集した上で、/test-get にアクセスしてみましょう。
<html>
    <p>Hello from index.html</p>
</html>

localhost の IPアドレスで実行

Powershell で以下のコマンドを実行してください。

ipconfig

image.png

ここで出力される IPv4 アドレスを使用して http://:3978 にアクセスしましょう。
すると、最初にアクセスしたのと同じページにアクセスできます。

ローカルで POST 実行

先ほど実行したのは HTTP の GET リクエストです。POST リクエストに関しても試してみましょう。

落としてきたファイル群の中にある test.http というファイルを VSCode で開きます。
その後、ファイルの一番上にある "Send Request" のボタンをクリックしてみましょう。
すると以下の画像のようになります。

image.png

画面の右側は POST リクエストを送信した結果になります。
今は同じ PC でserverを立ててリクエストを送信してますが、実際には異なる serverに POST リクエストを送ることで、処理を分けたりすることができます。

ちなみに、実際には POST の際に何等かのデータを追加して送ることで、そのデータを基にサーバー側で処理をしてもらうってことが多いです。例えば、IoT システムである地点で取得した温度のデータを POST リクエストでサーバーに投げると、サーバー側でデータベースに保存する処理をする。等が考えられます。

スマホからアクセス(GET リクエスト)

先ほどまでは同じPC から HTTP リクエストを送信していました。今度はスマホを使ってリクエストを送ってみましょう。

サーバーのアプリは起動したままで次の処理へ入りましょう。

Wi-Fi に繋いで

まずはスマホを PC と同じ Wi-Fi に接続させた状態で、http://:3978 (もしくは /test-get) にアクセスしてみます。

image.png

こんな感じで PC で試した時と同じ表示がされますね。

Wi-Fi から外して

次に、スマホの Wi-Fi をオフにして同じページを開いてみます。
すると、以下のようになるかと思います。

image.png

さて、ここで先ほど PC で確認したページに繋がらない事象が発生するかと思います。
なんででしょうか?

はい、これはスマホとサーバ(PC) が異なるネットワークに存在しているために発生しています。
Wifi に接続していた際には同じネットワーク内に存在していたので、ローカルの IP アドレスでアクセス出来ていたのですが、今スマホは Wifi に接続されていません。

筆者が雑に作ったイメージ図です。イメージ図です。(大事なことなので)

オンプレでの実行時はこんな感じです。
image.png

IP アドレスからスマホ君がどこへアクセスしたいのかをお家の Wifi を管理してくれてる機器がうまく制御してくれます。

次に、Wifi から外した時のイメージ図です。イメ(ry


image.png

ここで問題なのは、この IP アドレスはローカルなネットワークでしか理解できないものですので、異なるネットワークから接続しようとしてもどこにあるのか分かりません。東京のおまわりさんに奈良の住所を聞いても場所のイメージがつかないようなものです(?) なお、実際にはサーバーに繋がろうとスマホ君が頑張っていたことすら、サーバー君には届きません。

逆に言うと、何等かの方法で別のネットワークからでもアクセスできるところにサーバー君がいれば、スマホ君からアクセスができますね。

image.png

このことから、Web アプリを作成した際にはアプリの公開方法を意識する必要があるということが分かりますね。例えば、自社の人のみが使用する Web アプリであれば自社のネットワークにいたらアクセスできることが目的になるかもですが、一般の人が利用するアプリ等であればインターネット等で接続できるようにしなければいけません。

今回はその解決策の一つとして、クラウドサービスを使います。

Azure 使う

いよいよクラウドを使います。「ここまで長かったな。。。」って思った方もいらっしゃるかと思いますが、あと一息ですので一緒にがんばりましょう。僕も書いてて疲れてきましたががんばります。

リソース作成

Azure に作成したアカウントでログインして、Portal の上の検索バーで「App Service」って検索して、新規に App Service のリソースを作成します。
image.png

公開、ランタイム スタック、オペレーティング システムは以下に揃えてもらうと確実かなと思います。

image.png

リソース名やリソースグループ等はご自由に編集してください。そしたら、作成まで進めてください。

Deploy

先ほど作ったリソースに対して作成したアプリを Deploy します。もし気になる方は以下も参考にしてください。

https://docs.microsoft.com/ja-jp/azure/app-service/quickstart-nodejs?pivots=platform-windows

VSCode でプロジェクト一式を開きましょう。先ほど npm start 等のコマンドを入力した時のように、
Powershell 等で該当のディレクトリ(フォルダ階層) にいる場合は以下のコマンドでも開けます。

code .

(Code は VSCode のコマンド、"." は現在のディレクトリのファイル全てを指定しているイメージです。)

もしなんらかの理由でコマンドが実行できない人は以下のようにVSCode を開いてからフォルダを指定して開いてください。
image.png

左側から、追加した Azure の拡張機能をクリックして、画像のように作成した App Service のリソースを見つけます。サブスクリプション等があっているかを確認して探しましょう。(最初は Azure へのログイン等も要求されるかと思います。)

image.png

そこで右クリックをして「Deploy to Web App...」 を実行します。
image.png

その後、 Deploy するか等の確認がされますので従ってクリックをして Deploy 完了まで待ちましょう。

App Service 上の Web アプリにアクセスする

さて、Deploy が完了したらいよいよ大詰めです。実際に App Service に Deploy したアプリにアクセスしてみましょう。

該当の App Service リソースを開くと、以下のように URL の欄があるのでこちらをクリックしてもらうと
image.png

先ほどローカルで実行していたページを同じページが開けます。
image.png

他のエンドポイントに関しても以下の通り!
image.png

スマホからアクセス

今度はスマホのWi-Fi をオフにして App Service へアクセスしてみましょう。ローカルで実行していた時にはアクセスできなかったのが、アクセスできているのではないでしょうか。


image.png

こんな風にパブリックにアクセスできる(異なるネットワークからアクセスできる)リソースを用意できるのはクラウドの魅力の一つかと思います。(この一文のためにかなり回り道をしました)

HTML 編集

こちらは任意ですが、ローカルの時同様に HTML を編集したい場合には、以下の App Service Editor を使用できます。実際には、こちらはプレビュー機能であるためお勧めはできませんが、そもそも本来は*開発はローカルで行って、エラー等が出ない状態にしてから クラウドへ Deploy *というイメージが多いかなとかと思います。(CI/CD とかそういう話は抜きにしてます。)

image.png

クラウドに POST

念のために POST も試してみましょう。ローカルの際と同様に .http ファイルから POST リクエストを送ってみましょう。

先ほどは http://localhost:3978/test-post だったかと思いますが、今回は先ほどの https://.azurewebsites.net/test-post に対してリクエストを送ってみて、先ほどと同様の動作を確認してみてください。

まとめ

今回はクラウドを体験しやすいものとして、サーバアプリ(node.js 製)をローカルとクラウドのどちらでも実行してみるハンズオンとしてみました。もちろん、クラウドの利点は他にもたくさんありますが、イメージの一端でも伝わってますと幸いです。

クラウドのメリットとして今回の例では、クラウドを使うとサーバーアプリにアクセスしやすかったり、手元でサーバを起動する時のように常に手元の PC でサーバアプリを起動する必要等がない点がありましたが、実際にはそもそも個人手では開発が難しい機能等をクラウドを利用すると開発のハードルが低くなるといったケースもあります。

ただ、オンプレミスの方が良い場合等もあるかと思います。今回のハンズオンでいうと誤差の範囲化と思いますが、ネットワークの距離的な問題から処理の速度がオンプレの方が早いこともあります。また、社外につなげないような環境であればオンプレの環境の方が良いこともあります。

結局のところ、技術に至高なんてものは存在しないのかなと思います。ベストプラクティスなんて状況に応じて変化すると思います。なので、クラウドやオンプレ等の単語で反応して否定するよりは、それぞれの強みを理解して状況に応じて使い分けるのが大切なのかもしれませんね。(雑なまとめ)

とりあえず、僕としては皆さんが楽しく学習出来たら良いなって思ってます。では。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Githubプロフィールをデコる画像を動的に生成する

巷ではGithubのプロフィールをデコるのが流行っています。
具体的にはGitHub Readme Stats を利用してGitHubプロフィールをカッコよくするのような記事を読んでもらえばいいんですが、ここで紹介されている物以外を載せたい場合があると思います。

そこで、今回は試しにTwitterのプロフィール情報を取得し画像にして返すtwitter-profile-cardというものを書きました。
Vercelにデプロイされており、URLにidのクエリを付けて叩くとTwitterのプロフィールの画像が返ってきます。
Twitterの情報を取得する手順で他のデータを取得すれば応用が利くはずです。

記事では書いたCSSの詳細は省いていますが、この様な感じで表示されます。(画像はpngにした物ですが、リンク先は生成された物になっています。)
詳しくはリポジトリを見て下さい。

twitter-profile-card-default

やりたいこと

  1. Twitter APIを叩いて、プロフィールを取得
  2. HTMLElementにデータを埋め込んでスタイリング1
  3. PuppeteerでHTMLElementのスクリーンショットを撮る
  4. svgの配下のimageタグにスクリーンショットを埋め込んだレスポンスを返す
  5. これらをデプロイ

構成

.
├── README.md
├── package.json
├── vercel.json
├── api
│   └── index.ts
├── src
│   ├── createCard.ts
│   ├── createElement.tsx
│   └── getTwitterData.ts
└── tsconfig.json
  • /apiがエンドポイントになります

データの取得

好きなように取得してください。
今回はTwitterのプロフィール情報を取得し返す、と言う事で適当に取得します。

src/getTwitterData.ts
export function getTwitterData({ id }) => {
  const headers = {
    Authorization: `Bearer ${process.env.TWITTER_BEARER_TOKEN}`
  }

  const params = {
    screen_name: id
  }

  const userShowEndoPoint = 'https://api.twitter.com/1.1/users/show.json'

  return new Promise((resolve, reject) => {
    axios
      .get(userShowEndoPoint, { headers, params })
      .then((response) => resolve(response.data))
      .catch(async (err) => {
        return reject(err.response)
      })
  })
}

データをスタイリングし、Puppeteerでスクリーンショットを撮る

VercelAWS Lambda上で動いてる為、デプロイパッケージのサイズに制限があります。
Puppeteerに同梱さているChrome単体パッケージが単体で250MBもあるので、そのままVercelにデプロイしてしまうと、サイズ上限に引っかかってしまいます。

そこで、サイズ上限を回避するためにchrome-aws-lambdapuppeteer-coreを利用します。2
この2つはバージョンを合わせる必要があるので注意が必要です。

src/createCard.ts
import chrome from 'chrome-aws-lambda'
import puppeteer from 'puppeteer-core'

また、ローカル環境ではchrome-aws-lambdaが働いてくれないので、サーバー上で動いているかを条件分けし、ローカルでは自分のPCに入っているChromeを使うようにします。

src/createCard.ts
  const browser = await puppeteer.launch(
    process.env.AWS_REGION
      ? {
          args: chrome.args,
          executablePath: await chrome.executablePath,
          headless: chrome.headless
        }
      : {
          args: [],
          executablePath:
            process.platform === 'win32'
              ? 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
              : process.platform === 'linux'
              ? '/usr/bin/google-chrome'
              : '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
        }
  )

データを表示する為に、htmlを書きます。
スタイリングにインテリセンスが効かないと辛いので、.tsx | .jsx拡張子のファイルを作り、JSX.Elementを返す関数を書きます。
全てを書くと長いので、ここではヘッダーを表示する部分だけ書きます。

src/createElement.tsx
import * as React from 'react'

export function createElement(tweetData) {

  const header = {
    height: '33%',
    width: '100%',
    overflow: 'hidden'
  }

  const headerImage = {
    height: '100%',
    width: '100%',
    objectFit: 'cover'
  }

  return (
    <div style={header}>
        <img
          src={tweetData.profile_banner_url}
          alt="header image"
          height="100px"
          width="300px"
          style={headerImage}
        />
    </div>
  )
}

この様にinline-styleでスタイリングしていきます。

このJSX.Elementreact-dom/serverrenderToStringを使う事でstring型に変換でき、それをPuppeteerで読み込み、スクリーンショットを撮ります。

src/createCard.ts
import { renderToString } from 'react-dom/server'
import { createElement } from './createElement'
src/createCard.ts
const element = createElement(tweetData)

const page = await browser.newPage()
await page.setContent(
  `<html>
      <head>
        <style>
          body {
            width: "${width}";
            height: "${height}";
          }
        </style>
      </head>
      <body>${renderToString(element)}</body>
    </html>
  `
)

const image = await page.$('body')
const buffer = await image.screenshot({ encoding: 'base64' })

補足

日本語が混じったDOMをそのままスクリーンショットしてしまうと、日本語のフォントがPuppeteerに存在しないので文字化けしてしまいます。
ですのでgooglefontsなどのcdnからfontを読み込む様にします。

src/createCard.ts
await chrome.font(
  'https://rawcdn.githack.com/googlefonts/noto-cjk/be6c059ac1587e556e2412b27f5155c8eb3ddbe6/NotoSansCJKjp-Regular.otf'
)
await chrome.font(
  'https://rawcdn.githack.com/googlefonts/noto-fonts/ea9154f9a0947972baa772bc6744f1ec50007575/hinted/NotoSans/NotoSans-Regular.ttf'
)

撮ったスクリーンショットをクライアントに返す

上述しましたが、クライアント側のエンドポイントは/apiになります。
(いくつかのレポジトリでこうなってたので参考にしました。)
./api/index.tsで、リクエストを受けレスポンスを返す処理を行います。

export default async (req,res) => {
  const { id } = req.query
  res.send(id)
}

req.queryに叩いたURLのqueryが入ります。
(例えば/api?id=Twitterとすると、req.query.idにTwitterが入ります。)
res.sendでクライアントにレシポンスを返すことができ、res.setHeaderでHeaderを設定します。

先ほど撮ったスクリーンショットをsvgに埋め込み、クライアントに返します。

src/createCard.ts
return `
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="${width}"
    height="${height}"
    viewport="0 0 ${width} ${height}"
    fill="none"
  >
    <image href="data:image/jpeg;base64,${buffer}" x="0" y="0" width="100%" height="100%"/>
  </svg>
`
api/index.ts
import { createCard } from '../src/createCard'
import { getTwitterData } from '../src/getTwitterData'

export default async (req,res) => {
  const result = await getTwitterData(req.query) // Twitterのデータ取得
  const svgImage = await createCard()  // svg画像のHTMLelementを取得

  res.setHeader('Content-Type', 'image/svg+xml') // svgを指定
  res.setHeader('Cache-Control', `public, max-age=${60 * 60 * 12}`) // データの変化があまりないのでキャッシュを12時間に
  res.send(svgImage) // データを返す
}

これでクライアントにsvg画像を返すことができました。

確認、デプロイ

vercel.jsonを設定し、Vercel devでローカルサーバーを起動できます。
http://localhost:3000/api?id=Twitterで画像が表示されれば完成です。

あとはvercelコマンドでデプロイできます、簡単ですね。

まとめ

参考


  1. スタイリングにインテリセンスが効かないと辛いので(1敗)、スタイリングはjsxで書いていき、ReactDOMServer.renderToString()を使って変換します 

  2. chrome-aws-lambdaの配下にPuppeteerが存在し、それを利用すれば一見動きそう何ですが、puppeteer-coreが依存関係にあり、両方必要です 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Githubのプロフィールをカッコよくさせる画像を動的に生成する

巷ではGithubのプロフィールをデコるのが流行っています。
具体的にはGitHub Readme Stats を利用してGitHubプロフィールをカッコよくするのような記事を読んでもらえばいいんですが、ここで紹介されている物以外を載せたい場合があると思います。

そこで、今回は試しにTwitterのプロフィール情報を取得し画像にして返すtwitter-profile-cardというものを書きました。
Vercelにデプロイされており、URLにidのクエリを付けて叩くとTwitterのプロフィールの画像が返ってきます。
Twitterの情報を取得する手順で他のデータを取得すれば応用が利くはずです。

記事では書いたCSSの詳細は省いていますが、この様な感じで表示されます。(画像はpngにした物ですが、リンク先は生成された物になっています。)
詳しくはリポジトリを見て下さい。

twitter-profile-card-default

やりたいこと

  1. Twitter APIを叩いて、プロフィールを取得
  2. HTMLElementにデータを埋め込んでスタイリング1
  3. PuppeteerでHTMLElementのスクリーンショットを撮る
  4. svgの配下のimageタグにスクリーンショットを埋め込んだレスポンスを返す
  5. これらをデプロイ

構成

.
├── README.md
├── package.json
├── vercel.json
├── api
│   └── index.ts
├── src
│   ├── createCard.ts
│   ├── createElement.tsx
│   └── getTwitterData.ts
└── tsconfig.json
  • /apiがエンドポイントになります

データの取得

好きなように取得してください。
今回はTwitterのプロフィール情報を取得し返す、と言う事で適当に取得します。

src/getTwitterData.ts
export function getTwitterData({ id }) => {
  const headers = {
    Authorization: `Bearer ${process.env.TWITTER_BEARER_TOKEN}`
  }

  const params = {
    screen_name: id
  }

  const userShowEndoPoint = 'https://api.twitter.com/1.1/users/show.json'

  return new Promise((resolve, reject) => {
    axios
      .get(userShowEndoPoint, { headers, params })
      .then((response) => resolve(response.data))
      .catch(async (err) => {
        return reject(err.response)
      })
  })
}

データをスタイリングし、Puppeteerでスクリーンショットを撮る

VercelAWS Lambda上で動いてる為、デプロイパッケージのサイズに制限があります。
Puppeteerに同梱さているChrome単体パッケージが単体で250MBもあるので、そのままVercelにデプロイしてしまうと、サイズ上限に引っかかってしまいます。

そこで、サイズ上限を回避するためにchrome-aws-lambdapuppeteer-coreを利用します。2
この2つはバージョンを合わせる必要があるので注意が必要です。

src/createCard.ts
import chrome from 'chrome-aws-lambda'
import puppeteer from 'puppeteer-core'

また、ローカル環境ではchrome-aws-lambdaが働いてくれないので、サーバー上で動いているかを条件分けし、ローカルでは自分のPCに入っているChromeを使うようにします。

src/createCard.ts
  const browser = await puppeteer.launch(
    process.env.AWS_REGION
      ? {
          args: chrome.args,
          executablePath: await chrome.executablePath,
          headless: chrome.headless
        }
      : {
          args: [],
          executablePath:
            process.platform === 'win32'
              ? 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
              : process.platform === 'linux'
              ? '/usr/bin/google-chrome'
              : '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
        }
  )

データを表示する為に、htmlを書きます。
スタイリングにインテリセンスが効かないと辛いので、.tsx | .jsx拡張子のファイルを作り、JSX.Elementを返す関数を書きます。
全てを書くと長いので、ここではヘッダーを表示する部分だけ書きます。

src/createElement.tsx
import * as React from 'react'

export function createElement(tweetData) {

  const header = {
    height: '33%',
    width: '100%',
    overflow: 'hidden'
  }

  const headerImage = {
    height: '100%',
    width: '100%',
    objectFit: 'cover'
  }

  return (
    <div style={header}>
        <img
          src={tweetData.profile_banner_url}
          alt="header image"
          height="100px"
          width="300px"
          style={headerImage}
        />
    </div>
  )
}

この様にinline-styleでスタイリングしていきます。

このJSX.Elementreact-dom/serverrenderToStringを使う事でstring型に変換でき、それをPuppeteerで読み込み、スクリーンショットを撮ります。

src/createCard.ts
import { renderToString } from 'react-dom/server'
import { createElement } from './createElement'
src/createCard.ts
const element = createElement(tweetData)

const page = await browser.newPage()
await page.setContent(
  `<html>
      <head>
        <style>
          body {
            width: "${width}";
            height: "${height}";
          }
        </style>
      </head>
      <body>${renderToString(element)}</body>
    </html>
  `
)

const image = await page.$('body')
const buffer = await image.screenshot({ encoding: 'base64' })

補足

日本語が混じったDOMをそのままスクリーンショットしてしまうと、日本語のフォントがPuppeteerに存在しないので文字化けしてしまいます。
ですのでgooglefontsなどのcdnからfontを読み込む様にします。

src/createCard.ts
await chrome.font(
  'https://rawcdn.githack.com/googlefonts/noto-cjk/be6c059ac1587e556e2412b27f5155c8eb3ddbe6/NotoSansCJKjp-Regular.otf'
)
await chrome.font(
  'https://rawcdn.githack.com/googlefonts/noto-fonts/ea9154f9a0947972baa772bc6744f1ec50007575/hinted/NotoSans/NotoSans-Regular.ttf'
)

撮ったスクリーンショットをクライアントに返す

上述しましたが、クライアント側のエンドポイントは/apiになります。
(いくつかのレポジトリでこうなってたので参考にしました。)
./api/index.tsで、リクエストを受けレスポンスを返す処理を行います。

export default async (req,res) => {
  const { id } = req.query
  res.send(id)
}

req.queryに叩いたURLのqueryが入ります。
(例えば/api?id=Twitterとすると、req.query.idにTwitterが入ります。)
res.sendでクライアントにレシポンスを返すことができ、res.setHeaderでHeaderを設定します。

先ほど撮ったスクリーンショットをsvgに埋め込み、クライアントに返します。

src/createCard.ts
return `
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="${width}"
    height="${height}"
    viewport="0 0 ${width} ${height}"
    fill="none"
  >
    <image href="data:image/jpeg;base64,${buffer}" x="0" y="0" width="100%" height="100%"/>
  </svg>
`
api/index.ts
import { createCard } from '../src/createCard'
import { getTwitterData } from '../src/getTwitterData'

export default async (req,res) => {
  const result = await getTwitterData(req.query) // Twitterのデータ取得
  const svgImage = await createCard()  // svg画像のHTMLelementを取得

  res.setHeader('Content-Type', 'image/svg+xml') // svgを指定
  res.setHeader('Cache-Control', `public, max-age=${60 * 60 * 12}`) // データの変化があまりないのでキャッシュを12時間に
  res.send(svgImage) // データを返す
}

これでクライアントにsvg画像を返すことができました。

確認、デプロイ

vercel.jsonを設定し、Vercel devでローカルサーバーを起動できます。
http://localhost:3000/api?id=Twitterで画像が表示されれば完成です。

あとはvercelコマンドでデプロイできます、簡単ですね。

まとめ

参考


  1. スタイリングにインテリセンスが効かないと辛いので(1敗)、スタイリングはjsxで書いていき、ReactDOMServer.renderToString()を使って変換します 

  2. chrome-aws-lambdaの配下にPuppeteerが存在し、それを利用すれば一見動きそう何ですが、puppeteer-coreが依存関係にあり、両方必要です 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Githubプロフィールに貼れる画像を動的に生成し、返すエンドポイントを作る

巷ではGithubのプロフィールをデコるのが流行っています。
具体的にはGitHub Readme Stats を利用してGitHubプロフィールをカッコよくするのような記事を読んでもらえばいいんですが、ここで紹介されている物以外を載せたい場合があると思います。

そこで、今回は試しにTwitterのプロフィール情報を取得し画像にして返すtwitter-profile-cardというものを書きました。
Vercelにデプロイされており、URLにidのクエリを付けて叩くとTwitterのプロフィールの画像が返ってきます。
Twitterの情報を取得する手順で他のデータを取得すれば応用が利くはずです。

記事では書いたCSSの詳細は省いていますが、この様な感じで表示されます。(画像はpngにした物ですが、リンク先は生成された物になっています。)
詳しくはリポジトリを見て下さい。

twitter-profile-card-default

やりたいこと

  1. Twitter APIを叩いて、プロフィールを取得
  2. HTMLElementにデータを埋め込んでスタイリング1
  3. PuppeteerでHTMLElementのスクリーンショットを撮る
  4. svgの配下のimageタグにスクリーンショットを埋め込んだレスポンスを返す
  5. これらをデプロイ

構成

.
├── README.md
├── package.json
├── vercel.json
├── api
│   └── index.ts
├── src
│   ├── createCard.ts
│   ├── createElement.tsx
│   └── getTwitterData.ts
└── tsconfig.json
  • /apiがエンドポイントになります

データの取得

好きなように取得してください。
今回はTwitterのプロフィール情報を取得し返す、と言う事で適当に取得します。

src/getTwitterData.ts
export function getTwitterData({ id }) => {
  const headers = {
    Authorization: `Bearer ${process.env.TWITTER_BEARER_TOKEN}`
  }

  const params = {
    screen_name: id
  }

  const userShowEndoPoint = 'https://api.twitter.com/1.1/users/show.json'

  return new Promise((resolve, reject) => {
    axios
      .get(userShowEndoPoint, { headers, params })
      .then((response) => resolve(response.data))
      .catch(async (err) => {
        return reject(err.response)
      })
  })
}

データをスタイリングし、Puppeteerでスクリーンショットを撮る

VercelAWS Lambda上で動いてる為、デプロイパッケージのサイズに制限があります。
Puppeteerに同梱さているChrome単体パッケージが単体で250MBもあるので、そのままVercelにデプロイしてしまうと、サイズ上限に引っかかってしまいます。

そこで、サイズ上限を回避するためにchrome-aws-lambdapuppeteer-coreを利用します。2
この2つはバージョンを合わせる必要があるので注意が必要です。

src/createCard.ts
import chrome from 'chrome-aws-lambda'
import puppeteer from 'puppeteer-core'

また、ローカル環境ではchrome-aws-lambdaが働いてくれないので、サーバー上で動いているかを条件分けし、ローカルでは自分のPCに入っているChromeを使うようにします。

src/createCard.ts
  const browser = await puppeteer.launch(
    process.env.AWS_REGION
      ? {
          args: chrome.args,
          executablePath: await chrome.executablePath,
          headless: chrome.headless
        }
      : {
          args: [],
          executablePath:
            process.platform === 'win32'
              ? 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
              : process.platform === 'linux'
              ? '/usr/bin/google-chrome'
              : '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
        }
  )

データを表示する為に、htmlを書きます。
スタイリングにインテリセンスが効かないと辛いので、.tsx | .jsx拡張子のファイルを作り、JSX.Elementを返す関数を書きます。
全てを書くと長いので、ここではヘッダーを表示する部分だけ書きます。

src/createElement.tsx
import * as React from 'react'

export function createElement(tweetData) {

  const header = {
    height: '33%',
    width: '100%',
    overflow: 'hidden'
  }

  const headerImage = {
    height: '100%',
    width: '100%',
    objectFit: 'cover'
  }

  return (
    <div style={header}>
        <img
          src={tweetData.profile_banner_url}
          alt="header image"
          height="100px"
          width="300px"
          style={headerImage}
        />
    </div>
  )
}

この様にinline-styleでスタイリングしていきます。

このJSX.Elementreact-dom/serverrenderToStringを使う事でstring型に変換でき、それをPuppeteerで読み込み、スクリーンショットを撮ります。

src/createCard.ts
import { renderToString } from 'react-dom/server'
import { createElement } from './createElement'

src/createCard.ts
const element = createElement(tweetData)

const page = await browser.newPage()
await page.setContent(
  `<html>
      <head>
        <style>
          body {
            width: "${width}";
            height: "${height}";
          }
        </style>
      </head>
      <body>${renderToString(element)}</body>
    </html>
  `
)

const image = await page.$('body')
const buffer = await image.screenshot({ encoding: 'base64' })

撮ったスクリーンショットをクライアントに返す

上述しましたが、クライアント側のエンドポイントは/apiになります。
(いくつかのレポジトリでこうなってたので参考にしました。)
./api/index.tsで、リクエストを受けレスポンスを返す処理を行います。

export default async (req,res) => {
  const { id } = req.query
  res.send(id)
}

req.queryに叩いたURLのqueryが入ります。
(例えば/api?id=Twitterとすると、req.query.idにTwitterが入ります。)
res.sendでクライアントにレシポンスを返すことができ、res.setHeaderでHeaderを設定します。

先ほど撮ったスクリーンショットをsvgに埋め込み、クライアントに返します。

src/createCard.ts
return `
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="${width}"
    height="${height}"
    viewport="0 0 ${width} ${height}"
    fill="none"
  >
    <image href="data:image/jpeg;base64,${buffer}" x="0" y="0" width="100%" height="100%"/>
  </svg>
`
api/index.ts
import { createCard } from '../src/createCard'
import { getTwitterData } from '../src/getTwitterData'

export default async (req,res) => {
  const result = await getTwitterData(req.query) // Twitterのデータ取得
  const svgImage = await createCard()  // svg画像のHTMLelementを取得

  res.setHeader('Content-Type', 'image/svg+xml') // svgを指定
  res.setHeader('Cache-Control', `public, max-age=${60 * 60 * 12}`) // データの変化があまりないのでキャッシュを12時間に
  res.send(svgImage) // データを返す
}

これでクライアントにsvg画像を返すことができました。

確認、デプロイ

vercel.jsonを設定し、Vercel devでローカルサーバーを起動できます。
http://localhost:3000/api?id=Twitterで画像が表示されれば完成です。

あとはvercelコマンドでデプロイできます、簡単ですね。

まとめ

参考


  1. スタイリングにインテリセンスが効かないと辛いので(1敗)、スタイリングはjsxで書いていき、ReactDOMServer.renderToString()を使って変換します 

  2. chrome-aws-lambdaの配下にPuppeteerが存在し、それを利用すれば一見動きそう何ですが、puppeteer-coreが依存関係にあり、両方必要です 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Githubプロフィールに貼れる画像を動的に生成して返すエンドポイントを作る

巷ではGithubのプロフィールをデコるのが流行っています。
具体的にはGitHub Readme Stats を利用してGitHubプロフィールをカッコよくするのような記事を読んでもらえばいいんですが、ここで紹介されている物以外を載せたい場合があると思います。

そこで、今回は試しにTwitterのプロフィール情報を取得し画像にして返すtwitter-profile-cardというものを書きました。
Vercelにデプロイされており、URLにidのクエリを付けて叩くとTwitterのプロフィールの画像が返ってきます。
Twitterの情報を取得する手順で他のデータを取得すれば応用が利くはずです。

記事では書いたCSSの詳細は省いていますが、この様な感じで表示されます。(画像はpngにした物ですが、リンク先は生成された物になっています。)
詳しくはリポジトリを見て下さい。

twitter-profile-card-default

やりたいこと

  1. Twitter APIを叩いて、プロフィールを取得
  2. HTMLElementにデータを埋め込んでスタイリング1
  3. PuppeteerでHTMLElementのスクリーンショットを撮る
  4. svgの配下のimageタグにスクリーンショットを埋め込んだレスポンスを返す
  5. これらをデプロイ

構成

.
├── README.md
├── package.json
├── vercel.json
├── api
│   └── index.ts
├── src
│   ├── createCard.ts
│   ├── createElement.tsx
│   └── getTwitterData.ts
└── tsconfig.json
  • /apiがエンドポイントになります

データの取得

好きなように取得してください。
今回はTwitterのプロフィール情報を取得し返す、と言う事で適当に取得します。

src/getTwitterData.ts
export function getTwitterData({ id }) => {
  const headers = {
    Authorization: `Bearer ${process.env.TWITTER_BEARER_TOKEN}`
  }

  const params = {
    screen_name: id
  }

  const userShowEndoPoint = 'https://api.twitter.com/1.1/users/show.json'

  return new Promise((resolve, reject) => {
    axios
      .get(userShowEndoPoint, { headers, params })
      .then((response) => resolve(response.data))
      .catch(async (err) => {
        return reject(err.response)
      })
  })
}

データをスタイリングし、Puppeteerでスクリーンショットを撮る

VercelAWS Lambda上で動いてる為、デプロイパッケージのサイズに制限があります。
Puppeteerに同梱さているChrome単体パッケージが単体で250MBもあるので、そのままVercelにデプロイしてしまうと、サイズ上限に引っかかってしまいます。

そこで、サイズ上限を回避するためにchrome-aws-lambdapuppeteer-coreを利用します。2
この2つはバージョンを合わせる必要があるので注意が必要です。

src/createCard.ts
import chrome from 'chrome-aws-lambda'
import puppeteer from 'puppeteer-core'

また、ローカル環境ではchrome-aws-lambdaが働いてくれないので、サーバー上で動いているかを条件分けし、ローカルでは自分のPCに入っているChromeを使うようにします。

src/createCard.ts
  const browser = await puppeteer.launch(
    process.env.AWS_REGION
      ? {
          args: chrome.args,
          executablePath: await chrome.executablePath,
          headless: chrome.headless
        }
      : {
          args: [],
          executablePath:
            process.platform === 'win32'
              ? 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
              : process.platform === 'linux'
              ? '/usr/bin/google-chrome'
              : '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
        }
  )

データを表示する為に、htmlを書きます。
スタイリングにインテリセンスが効かないと辛いので、.tsx | .jsx拡張子のファイルを作り、JSX.Elementを返す関数を書きます。
全てを書くと長いので、ここではヘッダーを表示する部分だけ書きます。

src/createElement.tsx
import * as React from 'react'

export function createElement(tweetData) {

  const header = {
    height: '33%',
    width: '100%',
    overflow: 'hidden'
  }

  const headerImage = {
    height: '100%',
    width: '100%',
    objectFit: 'cover'
  }

  return (
    <div style={header}>
        <img
          src={tweetData.profile_banner_url}
          alt="header image"
          height="100px"
          width="300px"
          style={headerImage}
        />
    </div>
  )
}

この様にinline-styleでスタイリングしていきます。

このJSX.Elementreact-dom/serverrenderToStringを使う事でstring型に変換でき、それをPuppeteerで読み込み、スクリーンショットを撮ります。

src/createCard.ts
import { renderToString } from 'react-dom/server'
import { createElement } from './createElement'
src/createCard.ts
const element = createElement(tweetData)

const page = await browser.newPage()
await page.setContent(
  `<html>
      <head>
        <style>
          body {
            width: "${width}";
            height: "${height}";
          }
        </style>
      </head>
      <body>${renderToString(element)}</body>
    </html>
  `
)

const image = await page.$('body')
const buffer = await image.screenshot({ encoding: 'base64' })

補足

日本語が混じったDOMをそのままスクリーンショットしてしまうと、日本語のフォントがPuppeteerに存在しないので文字化けしてしまいます。
ですのでgooglefontsなどのcdnからfontを読み込む様にします。

src/createCard.ts
await chrome.font(
  'https://rawcdn.githack.com/googlefonts/noto-cjk/be6c059ac1587e556e2412b27f5155c8eb3ddbe6/NotoSansCJKjp-Regular.otf'
)
await chrome.font(
  'https://rawcdn.githack.com/googlefonts/noto-fonts/ea9154f9a0947972baa772bc6744f1ec50007575/hinted/NotoSans/NotoSans-Regular.ttf'
)

撮ったスクリーンショットをクライアントに返す

上述しましたが、クライアント側のエンドポイントは/apiになります。
(いくつかのレポジトリでこうなってたので参考にしました。)
./api/index.tsで、リクエストを受けレスポンスを返す処理を行います。

export default async (req,res) => {
  const { id } = req.query
  res.send(id)
}

req.queryに叩いたURLのqueryが入ります。
(例えば/api?id=Twitterとすると、req.query.idにTwitterが入ります。)
res.sendでクライアントにレシポンスを返すことができ、res.setHeaderでHeaderを設定します。

先ほど撮ったスクリーンショットをsvgに埋め込み、クライアントに返します。

src/createCard.ts
return `
  <svg
    xmlns="http://www.w3.org/2000/svg"
    width="${width}"
    height="${height}"
    viewport="0 0 ${width} ${height}"
    fill="none"
  >
    <image href="data:image/jpeg;base64,${buffer}" x="0" y="0" width="100%" height="100%"/>
  </svg>
`
api/index.ts
import { createCard } from '../src/createCard'
import { getTwitterData } from '../src/getTwitterData'

export default async (req,res) => {
  const result = await getTwitterData(req.query) // Twitterのデータ取得
  const svgImage = await createCard()  // svg画像のHTMLelementを取得

  res.setHeader('Content-Type', 'image/svg+xml') // svgを指定
  res.setHeader('Cache-Control', `public, max-age=${60 * 60 * 12}`) // データの変化があまりないのでキャッシュを12時間に
  res.send(svgImage) // データを返す
}

これでクライアントにsvg画像を返すことができました。

確認、デプロイ

vercel.jsonを設定し、Vercel devでローカルサーバーを起動できます。
http://localhost:3000/api?id=Twitterで画像が表示されれば完成です。

あとはvercelコマンドでデプロイできます、簡単ですね。

まとめ

参考


  1. スタイリングにインテリセンスが効かないと辛いので(1敗)、スタイリングはjsxで書いていき、ReactDOMServer.renderToString()を使って変換します 

  2. chrome-aws-lambdaの配下にPuppeteerが存在し、それを利用すれば一見動きそう何ですが、puppeteer-coreが依存関係にあり、両方必要です 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Typescript】 Error: Cannot find module 'express' の解決

あるファイルを読み込もうとしたら、エラーメッセージが出た。

node index.js

internal/modules/cjs/loader.js:638
    throw err;
    ^

Error: Cannot find module 'express'
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:636:15)
    at Function.Module._load (internal/modules/cjs/loader.js:562:25)
    at Module.require (internal/modules/cjs/loader.js:692:17)
    at require (internal/modules/cjs/helpers.js:25:18)
    at Object.<anonymous> (/home/ec2-user/environment/photo/realitycapture/reality.capture-nodejs-photo.to.3d/index.js:18:15)
    at Module._compile (internal/modules/cjs/loader.js:778:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:789:10)
    at Module.load (internal/modules/cjs/loader.js:653:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)
    at Function.Module._load (internal/modules/cjs/loader.js:585:3)

解決策

npm insatll express

expressを入れましょう。以上。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む