とろろこんぶろぐ

かけだしR&Dフロントエンジニアの小言

【GCP】GCE/GKEを利用したWebサーバーを公開するまで

概要

GCP 上で Web サーバーを公開するまでにやったことを忘れないようにメモしておく。

Web サーバーは、

  • アプリケーション(Node.js)
  • DB (Postgresql)

で構築される。

やることまとめ

  1. アプリケーションをローカルで開発する
  2. アプリケーションをコンテナ化しておく
  3. ローカルで DB を立てる(on docker)
  4. ローカルで DB + アプリケーションを dockerize し動作を確認する(docker-compose)
  5. GCE で DB を構築する
  6. GKE でアプリケーションをデプロイする
  7. アプリケーションから DB を接続するためにファイアウォールを設定する
  8. GKE のサービスにドメインを設定する

詳細

1. アプリケーションをローカルで開発する

いつも通り node.js のアプリケーションを開発する。

npm install
npm run build
npm run start

とさえすれば動くようにしておく。

今回は graphql を利用したので、 tsc で dist ファイルに build する際に、

tsc && cp src/schema.graphql dist

として、schema.graphql もコピーした。

そうじゃないと以下のような実行時エラーが出る。

Error:
      Unable to find any GraphQL type definitions for the following pointers:

          - /Users/xxx/sample-server/dist/schema.graphql

    at prepareResult (/Users/xxx/sample-server/node_modules/@graphql-tools/load/index.cjs.js:591:15)
    at loadTypedefsSync (/Users/...

2. アプリケーションをコンテナ化しておく

のちに GKE でコンテナとして動かしたいので、 Dockerfile を用意しておく。

FROM node:14

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install

COPY . .

RUN npx prisma generate
RUN npm run build

EXPOSE 4000

CMD [ "npm", "run", "start" ]

今回アプリケーションに prisma を利用したため、 prisma generate を行って node_modules 配下に types を配置しないと build が通らない。

こんな感じのエラーが出る。

 > [6/6] RUN npm run build:
#10 4.414
#10 4.414 > sample-server@1.0.0 build /usr/src/app
#10 4.414 > tsc && cp src/schema.graphql dist/src
#10 4.414
#10 21.51 src/resolvers/Query/index.ts(13,25): error TS7006: Parameter 'item' implicitly has an 'any' type.

2. ローカルで DB を立てる(on docker)

あとでアプリケーションをのせる前提で、postgres だけ立てておく。

version: "3"
services:
  postgres:
    image: postgres:latest
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: xxx
      POSTGRES_DB: xxx
    volumes:
      - ./pgsql-tmp:/var/lib/postgresql/data
    ports:
      - 15432:5432

localhost:15432 で接続を確認しておく。

3. ローカルで DB + アプリケーションを dockerize し動作を確認する(docker-compose)

docker-compose.yaml にアプリケーション (app) を追加する。

  app:
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - 4000:4000
    environment:
      DATABASE_URL: "postgresql://dev:xxx@postgres:5432/sample?schema=public"
    depends_on:
      - postgres

docker-compose up -d --build とし、localhost:4000/graphql で Playground が見られることを確認しておく。

4. GCE で DB を構築する

GCP には新たな Project を作っておく。 この Project で GCE / GKE を使う。

Cloud SQL は有料しかないので、無料枠で楽しむために DB は GCE で立てる。

以下の記事を参考にすれば GCE 上に簡単に Postgresql を立てることができる。

Set up PostgreSQL on Compute Engine  |  Google Cloud Platform Community

5. GKE でアプリケーションをデプロイする

5.1 コンテナを GCR にあげる

まずアプリケーションの Docker コンテナを build し、 GCR (コンテナレジストリ)にアップロードする必要がある。

docker build -f Dockerfile -t gcr.io/${projectID}/sample-server:v1 .
docker push gcr.io/${projectID}/sample-server:v1

うまくいけば、 GCP の自分のプロジェクトの Container Registry であげた image を確認できる。

5.2 secret で環境変数を用意

ここから GKE になる。 あらかじめ Kubernetes クラスタ を新規で作成しておく。 ゾーンを GCE と揃えておかないと DB にアクセスできないので注意する。

kubectl を対象のクラスタ(sample-cluster)で使えるようにするため、以下コマンドを実行する。

gcloud container clusters get-credentials sample-cluster --zone us-central1

prisma環境変数で DATABASE_URL を指定するようになっている。 ローカルで開発する際は、 .env で管理するようになっている環境変数を、 k8s の service 実行時に指定する必要がある。

ただし DB のパスワードも含まれるため、これを k8s の secret で管理するようにする。

apiVersion: v1
kind: Secret
metadata:
  name: mysecret
type: Opaque
data:
  database_url:  XXX

base64 で指定するため、

 echo "postgresql://postgres:xxx@10.128.0.1:5432/sample?schema=public" | base64

のようにして得られた base64 の値を指定する。 IP は GCE の VM の内部 IP を指定する。

secret を k8s に apply する。

kubectl apply -f k8s/secret.yaml

5.3 deployment/service をデプロイ

deployment と service を示す yaml を書いて、 apply する。

kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml

作成したファイルは以下。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-server
  labels:
    app: sample-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-server
  template:
    metadata:
      labels:
        app: sample-server
    spec:
      containers:
        - name: sample-server
          image: gcr.io/sample/sample-server:v1
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: mysecret
                  key: database_url
          ports:
            - containerPort: 4000
          resources:
            requests:
              cpu: 200m

環境変数は上で作った mysecret から取得する。

kind: Service
apiVersion: v1
metadata:
  name: sample-server-service
spec:
  type: LoadBalancer
  selector:
    app: sample-server
  ports:
    - protocol: TCP
      port: 80
      targetPort: 4000

kubectl get service で得られる EXTERNAL-IP の外部 IP を使い、

XX.XX.XX.XX/graphql などで、Playground が見られることを確認する。

$ kubectl get service
NAME                  TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)        AGE
sample-server-service   LoadBalancer   10.128.0.1   XX.XX.XX.XX  80:30174/TCP   56m
kubernetes            ClusterIP      10.128.0.1     <none>           443/TCP        60m

おそらくこの時点では実際にクエリをリクエストしても DB にアクセスできない旨のエラーが表示される。

6. アプリケーションから DB を接続するためにファイアウォールを設定する

GKE のアプリケーションから GCE の DB にアクセスを許可するために少し設定が必要になる。

GCE に ssh で入り、 pg_hba.conf に Pod のアドレス範囲を許可する様に指定しておく。

Kubernetes クラスタ の詳細から、 ポッドのアドレス範囲 10.0.0.0/17 はわかる。

host    all             all           [POD_ADDRESS]         md5

GKE のクラスタを作成すると、 自動的にファイアウォールが作成されていた。

gke-sample-cluster-xxxxxxx-all

これをみると、ソースフィルタ の IP 範囲がクラスタの Pod のアドレス範囲になっているためこれを適用するようにする。

ターゲットタグの gke-sample-cluster-xxxxxxx-node のようなタグをコピーし、 GCE の VM インスタンスのネットワーク タグに貼り付ける。

これによりファイアウォール側のコンソールに、インスタンスとして Postgres の VM が表示され、実際に IP ベースで Graphql にアクセス可能になっている。

7. GKE のサービスにドメインを設定する

静的 IP を取得する。

gcloud compute addresses create sample-server-ip --region us-central1

以下のコマンドで取得した IP を確認できる。

gcloud compute addresses describe sample-server-ip --region us-central1

service.yaml に上記で確認した IP を以下のように追加する。

kind: Service
...
      targetPort: 4000
+  loadBalancerIP: "XX.XX.XX.XX"

apply する。

kubectl apply -f k8s/service.yaml

確認する。

kubectl get service # EXTERNAL-IP を確認できる
curl http://XX.XX.XX.XX/

これにより固定化された IP で Web サービスにアクセスできる。

今回は、ドメインはもともと取得していたもののサブドメインに追加する。 Vercel で DNS の設定をしていたため、 Vercel の管理画面からサブドメインを IP 設定する A レコードを追加する。

api A XX.XX.XX.XX 60

これで http://api.sample.com/ のような形でアクセスできるようになる。

まとめ

とりあえず GKE / GCE で Web サーバーを動かすところまでやってみた。 ファイアウォール周り、cloud build 、terraform 、 https 化などもやってみたいがまた今度。

2021 HackDay フロントエンド開発振り返り

2021 HackDay

Yahoo 主催のハッカソン HackDay 2021 に参加しました。

https://hackday.jp/

開催日: 2021年03月20日 12:00 - 2021年03月21日 12:00 場所: オンライン 結果: 27日なのでまだ。

作ったモノ

「ぎょうれつや」

オンラインで行列に並んだり、自分で行列を作ることができるサービス。

  • Twitter でログインすると行列にならんだり作れたりする
  • 並ぶ時にコメントできて、みんなのコメントを眺めることができる
  • レコメンド機能でツイートや並んだ行列からおすすめの行列を提案してくれる

gyoretsuya.mocchiri.club

メンバーと担当

  • リーダー/インフラエンジニア
  • 機械学習エンジニア/バックエンドエンジニア
  • APP エンジニア/プレゼン/全体PM
  • デザイナー/フロントエンドエンジニア
  • フロントエンドエンジニア (←これ)

もともと4人が知り合いで、 フロントのエンジニアとして声をかけてもらい参加することになりました。

利用技術

FE

  • TypeScript: 型はスピードの足枷にはならず安定性の担保になった
  • Next.js (React): zero config は最強
  • styled-components: 使い慣れてるだけ
  • Firestore: 開発スピードの優先

細かくは下で書く。

BE/Infra

開発準備

フレームワーク選定

UI はゼロコンフィグで React を書ける Next.js にしました。

Next.js ならフレームワークとしてさまざまな形式に即座に対応可能だし、 もうひとりのフロントエンドエンジニアも React が書けるので迷わず決定しました。

さまざまな形式の例

- CSR のみ
- SSR + CSR 
- BFF で API ブリッジあり

結果的に CSR のみになりました。

デプロイ環境

k8s が用意されていたものの一から dockerfile 作るまでやれないと判断し、サービスに頼ることにしました。

Next.js なら Vercel でデプロイできるので、第一候補は Vercel でした。

push で環境をデプロイ追加可能だし、PR 環境もあるし最高と思っていたが、Vercel は GitHub Organizations だと Pro プラン($20/mo per member) に入る必要があることを知って断念しました。

Pricing – Vercel

同時並行で、データを保管するための DB として Firestore を利用する話がバックエンド側で挙がっていたため、このタイミングでデプロイ先として Firebase Functions / Firebase Hosting が候補に挙がりました。

Next.js の example にサンプルがありそうと判断しこのタイミングで Firebase をデプロイ先に決定しました。

next.js/examples/with-firebase at master · vercel/next.js · GitHub

next.js/examples/with-firebase-hosting at master · vercel/next.js · GitHub

認証技術検証

Firebase Authentication で Twitter 認証し、 Twitter API でツイートの取得を行うことにしたため、フロントから認証情報をどう取得できるのか確認しました。

これはドキュメント通りに組み込むだけだったので、簡単でした。Firestore のルールを設定すれば、認証情報のセッション管理を考えなくていいのが良かったかなと思います。

JavaScript による Twitter を使用した認証  |  Firebase

UI 技術検証

「ぎょうれつや」で一番 UI としてポイントになりそうだった "ミニマップ機能" は先に技術検証しました。

ミニマップは、VSCode のツールや Web ページなどでたまに見る、全体像を見れるようになっている部分のことです。

↓のような感じ。 Minimap - Chrome ウェブストア

ぎょうれつやは、長蛇の列を俯瞰して確認しスクロールやスワイプで行列を移動する、という UI を作り込むため、ミニマップを入れたいと思いました。

スワイプやドラッグによる処理を自前で書かないために、結果としてスマホ環境で一番安定して動いていた react-draggable を採用しました。

GitHub - react-grid-layout/react-draggable: React draggable component

以下が、完成したページです(ちょっとわかりづらいと思いますが...)。

f:id:ka2jun8:20210322113033p:plain

メインの行列とミニマップを用意し、

  • メインの行列は、横長のコンポーネントを用意。

    • 人数分の画像がとにかく横に並ぶコンポーネント
    • 横にスクロールした割合を state に保管する。
    • ミニマップが動かされたら、その割合分左右に scrollTo で動かす。
  • ミニマップは、画面幅を 100% として人数に応じた "表示枠" を用意。

    • 画面内に人数分の画像を詰め込むコンポーネント
    • 表示枠を人数に応じて大きさを変える。
    • react-draggable で掴んで動かせるようにする。
    • 左右に動かされたら、画面幅分の x 軸方向の割合を state に保管する。
    • メインの行列がスクロールされたら、その割合分左右に表示枠を動かす。

横方向のみにしか動かない前提で、細かいサイズやバグは無視してそれっぽく動いた時点で触るのをやめました。

フロント開発でやったこと

  1. フロントから Firestore のデータ読み書き API の開発
  2. ページ/コンポーネント開発
  3. Firebase Functions 開発
  4. 軌道修正とひたすらのバグ潰し

フロントから Firestore のデータ読み書き API の開発

ざっくりと決めた行列(event)のユーザー(user)のテーブルスキーマで、firestore の読み書き API を呼び出すラッパーの開発から行いました。

基本的には、行列とユーザーのデータに対して、

  • リアルタイムなデータ読出を行う subscirbe
  • ドキュメント id を指定した get 。
  • 新規書き込みを行う create
  • 更新を行う update

これらを行うためのリソースラッパーを作りました。コンポーネントやページから呼び出しやすいようにして、型も書きました。これが後で効いてくれたのでよかったです。

削除は、削除したくなったら直接コンソールから消せばいいと思い作りませんでした。

ページ/コンポーネント開発

この間にデザイナーの手によってできていたざっくりワイヤーをもとに、ページやコンポーネントをスタイル無視して、Firestore データの結合しながらページやコンポーネントを作りました。スタイルはあとから直していこうと思いました。

Firebase Functions 開発

Twitter にログインしてユーザーのオブジェクトを Firestore に作ったら、そのユーザーの一番最近発言した Tweet を取得して Firestore に追加する機能が欲しくなり、 Fibase Functions で書きました。

Cloud Firestore トリガー (リンク) が用意されていたので、サンプルを TypeScript 化して functions をぶち込みました。

仕様追加、軌道修正とひたすらのバグ潰し

ざっくり仕様で突き進んだので、途中で何度か仕様追加、スキーマ変更、軌道修正が行われました。 後半は、その変更によるデザイン崩れやバグ対応に追従していました。 あとはどこまで細かい仕様に合わせるかの勝負になっていき、最後は合わせきれない細かい仕様を残し、力尽きて完了となりました。

そのほか

認証とセキュリティ

途中で、レコメンドエンジンによるおすすめの行列一覧を取得するための REST API を作る話が挙がったが、それはやめてフロントのインターフェースを全て Firestore に寄せることにしました。

バックエンドチームは、ユーザーのデータが作られたり、行列に参加するタイミングで作られるデータをもとに、レコメンド検索を行い、別の Firestore コレクションにレコメンド結果を書き込む形にしました。

これにより、フロントは必要なすべてのデータを Firestore から得るようにすることができ、セキュリティは結果として Firestore のセキュリティルールだけ考えればよくなりました。

firebase という選択

firebase init functions , firebase init firestore とするだけで必要なファイルが生成され、すぐにでも動かせるようになりました。

また emulator をローカルで動かせばデプロイしなくても好きにデータをいじって確認できたのはめちゃ便利でした。

ハッカソンという短い期間での開発で、環境としては最適に感じました。

ただし、実際の業務として考えた時、firestore によるデータ管理は SSR に向かないし、データフェッチが安定せず、表示スピードの遅さが目立っていたのが気になりました。

Github Actions でのデプロイ

firebase へのデプロイになった早い段階で、GitHub Actions で firebase のデプロイを行うことにしました。

ハッカソンでデプロイ自動化に時間を割くか迷ったが、最後にバタバタしたときに誰でもデプロイできる状態にしておくことに価値があると思い、パイプラインを用意しました。

結果的に、マーケットにアクションが用意されていたため、速攻で組み込むことができたのでよかったです。

GitHub Action for Firebase · Actions · GitHub Marketplace · GitHub

TypeScript

ハッカソンという短い期間での開発でも、個人的には TypeScript での型は有用だった。 全体として、any を許容し関数の返り値の型はなくても良いルールにしました。とはいえ、any を使うことはほぼなく、型の恩恵に預かることが多かったです。tsc --noEmit でビルドに失敗することが先読みできたのも大きかったです。

ハッカソンはでき初めの頃よりも、後半仕様の追加と変更に追従する場面で、どのコードがどこに依存しているかわかりづらくなる場面で、型によって参照がわかりやすくなっていたのはとても開発体験がかなり良かったです。

特に終盤は眠気などもあり、頭がまともに働いてないのでエディタの方が賢い場面もあったと思いました。

反省点

なし!!

ハッカソンとしては、満点の働き方ができたと個人的には思っています。 ほかのメンバーがとにかく優秀だったので、ぼくの雑なコードに対するキャッチアップもはやかったし、プロマネ、バグ→機能修正や、スタイルあてなど、さまざまなタスクを拾ってもらいつつ、次に進む方向のディシジョンをしてもらえたから自分の反省点がないように見えてるんだと思います。

まとめ

最初「24時間寝ない」ハッカソンに若干抵抗はあったが、結果出てみてとても楽しかった。技術的には firestore のイベントドリブンなシステムに、モダンすぎる勢いを感じられたし、web サービスなんて作ってなんぼっていう感覚を思い出せたのはとてもよかったです。

ただ、完徹でサービス作るのは、あと数ヶ月はいいかなって思いました。

2020年振り返り

はじめに

すでに2021年がはじまってしまいましたが、気が向いたので去年を振り返ってみます。

本業

本業では1年間同じWebサービスの開発を継続して行いました。 一昨年の10月から開発を開始したサービスを昨年の2月にリリースし、 今も継続して開発を続けています。

ここまで長い間、リリースしてからも継続して 1つのサービスに携わる経験ははじめてなので嬉しく思っています。

技術的な話では、自由度が高い職場なのでAMPやSXG, Next.jsなどの モダンでチャレンジングな技術に触れながら、楽しく働けています。

職責的な話では、立場がメンバーからリーダー職になりました。 初期リリース後は開発業務ではなく、メンバーのPRレビューやコンディション、進捗管理、 また、案件相談やスケジュールの調整などがメインの業務になりました。

そうは言ってもコーディングしたいと思うことが多々あり、 自分でコードを読んで書く仕事を自ら作って、開発業務もサブで行っていました。 ただ、昨年はまだ仕事に慣れずメインの業務でコアタイムを使い切ってしまい、 サブの開発業務が残業に回りがちだったかなと思います。

2021年は効率的に仕事をこなし、 より一層ワークライフバランスを充実させられればなと思っています。 いまのサービスをこれまで通り続けられるか分かりませんが、 もし続けられるならよりサービスをグロースさせ、名の知れたサービスへと拡大していきたいです。

趣味開発/副業

趣味開発として、サービスを立ち上げようかと思っていました。 記事を書くためのサポートツールとして、クローリングした情報をピックアップできるツールを作り、それを用いたサービスを運用できないか考えていました。

最低限の機能でリリースするところまでできそうだったのですが、 サービスのグロースどころか運用さえもままならない状態になってしまったので、一旦潰しました。

毎年何かしら1つはサービスを作っては捨てていますが、 半分は勉強や経験、趣味を兼ねているので、問題ないと思いつつ、 何かうまくリリースして運用までいければいいなと(毎年)思ってます。 また今年も何かしら個人サービスを作って遊びたいなと思っています。

副業についてはお手伝いしているスタートアップがありましたが、 会社が少し大きくなりあまり手をかける必要がなくなってきたため、 2ヶ月ほど前に少し距離を置いて離れることにしました。

また副業するかどうかはわかりませんが、もし何かあればお声がけください。

プライベートな話

結婚式をあげました。

このような状況なので思っていた規模ではありませんでしたが、 人生のステージを1つ上げられたかなと思っています。 職場含めいろんな人に祝っていただき、嬉しく思いました。 正直こんなにたくさんの人たちからお祝いしてもらえると思っていなかったので、 実はとても驚きました。

買って/もらってよかったもの

洗濯乾燥機

値段が値段なので悩みましたが、日立の洗濯乾燥機にしました。

電気屋には3度行きました。 1度目に、縦置きとドラム式の違い、メーカーごとの違いなどを聞きざっくりとして知識を勉強しました。 一度帰って、ネットの情報と見聞きした情報を見比べて購入時期を検討しました。

2度目に、立ち寄った際に1度目に見た時との値段を比べました。 1度目はパナソニックフェアをやっていて、2度目はフェアがやっておらず、 パナソニックフェアのときがだいぶ安かったのだと知り、次のフェアを狙いました。

3度目に、日立フェアがあったので、このタイミングで1世代前の旧型の洗濯乾燥機を買いました。

結果、買ってよかったです。洗濯はほとんど洗濯乾燥機能に頼ることになりました。 夫婦共働きなので、基本的に乾燥までして干すことはなく畳むだけにしています。

日立で良くなかったなと思うことが2点だけあります。

1つは、ほこりがとても出ることです。 服やタオルにほこりがつくので、畳む際にほこりを取る必要があります。 他のメーカーでも同じなのかはわかりません。

2つ目は、初期不良なのか2回に1回くらいエラーで止まることがあります。 ただ、そのまま出しても大体乾いてるのであまり気にしていません。

それらの問題点を考慮しても非常に便利です。 電気代が高くなってるかも知れませんが、それも今のところ気になっていません。

https://amzn.to/2Mq3duzamzn.to

ホットクック

ホットクックは結婚祝いで頂きました。

食材を切って入れるだけなので簡単とは聞いていましたが、本当に楽でした。 煮込んでいる間、鍋や火の元を気にしなくていいので、その時間他のことに集中できます。

食材だけあらかじめ切ってタッパーに詰めて冷蔵/冷凍しておき、 必要に応じて食材を適宜入れてボタン1つで食事ができるというのはありがたいです。

細かいところで気になることはあります。 例えば、でかいので置き場に困る、めっちゃ喋るのでリモート会議中に作れない、意外と汚れがつくなど。 ただ、それらを補って余りあるほど、レシピは充実しているし、いろいろ作れるのが楽しいです。

https://amzn.to/3b9gHW3amzn.to

iPad Air

もともと Macbook ユーザーではありましたが、iPad は持っていませんでした。 Apple Pencil 第2世代が出たときに低価格帯が出たら買おうと思っていて、 iPad Air が今回発売されたので購入しました。

買う時にもしかしたら用途がないかもと思っていましたが、結果的にほとんど毎日使っています。

Kindle代わりに本やマンガを読む際に使う ・仕事中サブディスプレイに使う(ワイヤレスでツータッチでサブディスプレイになるのはすごい便利) ・メモや絵を描くのに使う

最近は勉強会がリモートで開催されることもあり、 YouTube ライブなどをiPadで流しながら Mac で開発するということもありました。

買ってよかったなと思っています。

https://amzn.to/3pRLHOeamzn.to

ソープディスペンサー

コロナ禍になり手洗いを心がけるようになったことをきっかけに、 お祝いでソープディスペンサーを頂きました。 手をかざすだけで石鹸が泡状で出てきてくれるので清潔ですし、ちょっと楽しいです。

1つ細かい難点があるとしたら、他のものを取ろうとした時や掃除しようとした時など、 関係ない時にたまに反応して泡が出てきてしまうことです。

https://amzn.to/3pMIGySamzn.to

ダイニングテーブル

こちらも夫婦ともにリモートワークの頻度が増えたことをきっかけに、 家庭内で働く場所を作る必要がありました。 これまでもこたつ机やミニテーブルなどはあったのですが、 これをきっかけにダイニングテーブルでも仕事ができるように買いました。 リモートワーク期間がすぐに終わっても、 半円にもできるので最悪物を置く棚としても利用できるかなと思いこのテーブルにしました。 今のところリモートワーク期間が続いているので引き続き、仕事スペースとして利用しています。

https://amzn.to/38jtzXJamzn.to

フォロドレッシング

リモート期間が増え、家庭内で美味しいものを食べたいが、 市販のものにも飽きてきたし、野菜も採った方がいい、というときに、 美味しいドレッシングを買ってみることにしました。

そこで出会ったフォロのドレッシングです。 少し値段が高いので、最初買って食べたときは、そうでもないなと思っていました。

しかし市販のドレッシングに戻ったときに、 「あれ、フォロの方が美味しいかも...」とクセになってしまったことに気がつきました。

今ではほとんどフォロのドレッシングを買っています。今のところ飽きていません。

https://amzn.to/38gWETMamzn.to

グラファイトトースター

これは実はふるさと納税で手に入れました。 兵庫県加西市ふるさと納税で5万円でした。 家にあった休めのトースターにガタがきていたので、 せっかくなら良いものを買いたいなと思った時期と、 ふるさと納税の期限が迫っていた時期が重なったので、グラファイトトースターを選びました。

トースターなんてどれも同じだろと思っていたんですが、 タイミングを見計らわないとこげまくっていた食パンがこげることなく、 外側サクっと中身ふわっとを簡単に実現してくれていて、正直かなり驚きました。

https://amzn.to/2L69T0Iamzn.to

まとめ

2021年も良い年にしたいです!

next.js x amp-mustache x storybook の罠

はじめに

この記事は Recruit Engineers Advent Calendar 2020 の18日目の記事です。

adventar.org

17日目は mic_psm さんの

KubernetesとIstioを使ったDX改善 - 後ろを向いて後退します

でした。

本記事の内容に入る前に、 本記事は複合的な条件が重なった際に表出する問題に注目したとてもニッチな内容なので、 Next.js や AMP を利用するケースが必ず起こりうるような問題ではないことにご注意ください。

Next.js と AMP オプションを使った Web アプリケーションに Storybook を導入した際に躓いた部分があったので、 備忘の意味を込めて記事に残し、調査した内容を共有します。 記事執筆時点でまだスマートな解決策は見つかっていません。

概要

Next.js の AMP オプションを使って SSR で配信している Web アプリケーションに、 デザインシステムや VRT(ビジュアルリグレッションテスト) のために Storybook を導入しました。 しかし AMP の amp-list / amp-mustache で CSR を行っていると、 Storybook 上では思ったように描画しないことがありました。

結論からいうと、 amp-mustache で利用される template タグが React で利用できないため、 Storybook では描画できていませんでした。

Next.js x AMP について

Next.js では言わずと知れた React (react.js) を活用した Web フレームワークです。 導入した瞬間に煩わしい開発環境の設定がほとんど必要ない状態で React を書き始めることができます。

Next.js by Vercel - The React Framework

Next.js では AMP での開発を公式にサポートしています。

next/amp | Next.js

AMP 化したいページに対して export const config = { amp: true } を 1行足すだけで、 AMP コンポーネントに最低限必要なスタイルやスクリプトを追加してくれたり、 amphtml-validator や AMP optimizer といった周辺ツールを自動的に利用してくれます。

以下 GitHub のリンクです。

amphtml/validator/js/nodejs at master · ampproject/amphtml · GitHub

amp-toolbox/packages/optimizer at main · ampproject/amp-toolbox · GitHub

AMP optimizer に関してはこちらの Qiita 記事が詳しいです。

詳解 AMP Optimizer - Qiita

Next.js で AMP を利用する場合、 AMP は任意の JavaScript を利用できないため、React での CSR は一切行われません。 つまり React をテンプレートエンジンとして利用しているような形になります。

Storybook について

Storybook はデザインシステムや UI コンポーネントのカタログとして利用できる Web アプリケーション開発者向けツールです。

Storybook: UI component explorer for frontend developers

React の場合は @storybook/react のパッケージを導入することで React で作られたコンポーネントを管理することができます。 React 以外でも Vue や Angular、 Web Components などでも利用でき、フレームワークに限定したツールではありません。

また現在実際に開発時に利用しているユースケースの1つとして、 reg-suit というツールを Storybook と連携させることで、VRT(ビジュアルリグレッションテスト)にも利用しています。

reg-suit

Storybook で管理している UI コンポーネントスクリーンショットをコミットごとに比較することで、 コードの修正による不必要な UI 変更を検出しやすくなります。

情報が若干古いかもしれませんが、参考記事としてこちらをあげておきます。

Storybookとreg-suitで気軽にはじめるVisual Regression Testing - wadackel.me

React と AMP と Storybook の組み合わせによる問題

描画に関する違い

React で AMP コンポーネントをラップする形で個々のコンポーネントを開発し、 (Next.js の特徴である) pages 配下のファイルにそれらのコンポーネントを配置して Web ページの開発を行っています。

ただし繰り返しになりますが Next.js から実際に Web ページを配信する際には、 最終的に next.js 内で (おそらく) ReactDOM.renderToString()SSR された HTML を配信しています。

一方 @storybook/react は React を Storybook 上で CSR で描画を行っています。

storybook/render.tsx at next · storybookjs/storybook · GitHub

Storybook からすれば React で書かれているコンポーネントなので当然といえば当然です。

98%は問題ない

AMP のコンポーネントは Web Components ですが、Next.js 、つまり React で書いて利用することも可能です。 Next.js で公式に AMP をサポートしていることもあり、 Next.js で AMP コンポーネントを書く際にそこまで書き方を工夫する必要はありません(※1)。

Storybook においても、 React で作られてさえいれば Story に配置することは簡単にできます。 AMP を正しく動作させるために必要なスタイルや JavaScriptpreview-head.html に書いておくことで、 各 Story が読み込まれる際に head タグの中に差し込んでくれます。

Story rendering

ただし AMP として公式に扱ってもらえるわけではないので、 不要な JavaScript の読み込みが発生してしまい AMP valid な状態で表示することはできません。 これにより、 Story を行き来していると正しく動作しないことがあります。

しかし今回あげている VRT として利用するのであれば、初期描画の見た目が想定通りに表示されれば問題ありません。

※1 React で書く AMP コンポーネントを、TypeScript や styled-components などの言語やライブラリと組み合わせた場合には、 多少工夫が必要になることがあります。詳しく知りたい方は以下の記事をご覧ください。

モダンなWebフロントエンドの技術とAMP - Tech Blog - Recruit Lifestyle Engineer

快適なUXの裏には泥臭さがあった? 新サービス開発に「React×Next.js×AMP」採用のワケ (1/2):CodeZine(コードジン)

残り 2% の問題

AMP は JavaScript を任意に動かすことができないため、クライアント側で任意の描画を行わせるためには AMP コンポーネントを利用する必要があります。 AMP のコンポーネントの中で amp-list と amp-mustache というコンポーネントを組み合わせることで、任意の CSR を実現することができます。 amp-list はデータを動的に取得することができるコンポーネントで、 amp-mustache は上で取得したデータを利用して動的に DOM を描画することができるコンポーネントです。 詳しい利用方法についてはドキュメントをご参照ください。

ドキュメント:<amp-list> - amp.dev

ドキュメント:<amp-mustache> - amp.dev

amp-mustache では HTML5 の template タグを利用します(※2)。 例えば、以下のような書き方になります。

<template type="amp-mustache">
  <div class="greeting">Hello {{world}}!</div>
</template>

余談ですが、 {{}} で変数を囲う記法が mustache の書き方で、 amp-mustache では mustache.js が利用されています。

amphtml/mustache.js at master · ampproject/amphtml · GitHub

GitHub - janl/mustache.js: Minimal templating with {{mustaches}} in JavaScript

この amp-mustache を利用したコンポーネントは React で SSR した HTML では正しく描画されますが、 React で CSR した際には正しく描画されません。

※2 amp-mustache は script タグでも記述できます。それについては後述します。

原因調査

template タグを使って記述すると template タグ配下が DocumentFragment として扱われます。 template タグや DocumentFragment については MDN とともに以下の記事が分かりやすいです。

<template>: コンテンツテンプレート要素 - HTML: HyperText Markup Language | MDN

DocumentFragment - Web API | MDN

七章第四回 ノードをまとめて扱う:DocumentFragment — JavaScript初級者から中級者になろう — uhyohyo.net

これにより amp-mustache タグは CSR を実現しています。

React が SSR する際は template タグの配下で DocumentFragment の要素として mustache で記述した内容が正しく描画されるため問題なく動作します。 しかし、 React の CSR では残念ながら DocumentFragment は template タグ内の DocumentFragment の要素として描画されません。

f:id:ka2jun8:20201213121053p:plain
SSRの場合

f:id:ka2jun8:20201213121105p:plain
CSRの場合

図では少し分かりづらいですが、 CSR の場合に template タグの DocumentFragment の要素として見なされていないことがわかります。 React は template タグの場合でも DOM 要素を子要素に追加していくため、 DocumentFragment の要素として内容を保持してくれません。 React からすれば React そのものが CSR するための仕組みなので、 template タグを使う必要はなく、「哲学が違うから対応しない」と言われてしまえば「すまん、そうだよな...」という気持ちになります。 それについては以下のリンク先の Issue や Stack Overflow でも触れられています。

Better support <template> tags · Issue #19932 · facebook/react · GitHub

reactjs - How can I render a <template /> tag with react? - Stack Overflow

template タグの DocumentFragment が正しく書き出されていない以上、 amp-mustache で以下のように参照しても空の情報が返ってきてしまうため描画できません。

  return template.content.cloneNode(true);

amphtml/dom.js at 07551bbf822dceef05e93e17cbb8e86acea7c467 · ampproject/amphtml · GitHub

解決策の調査

解決案1「template タグ部分で SSR する」

上記 Stack Overflow の記事に書かれている通り、 React では amp-mustache として描画したい部分で template タグを dangerouslySetInnerHTML で文字列として書き出せば動きます。

しかしながら、SSR では正しく動作しているプロダクションコードに対して、 Storybook 上で動かない問題を解決するために、 dangerouslySetInnerHTML と ReactDOM.renderToString を用いたコードに変更するのは、 大掛かりでリスクも高そうなため、解決策としては適さないと判断しました。

解決案2「template タグを使わない」

amp-mustache は template タグを利用する代わりに script タグでも記述することができます。 公式のドキュメントにも書かれている通り推奨されていないものの、以下の template タグのものは、

<template type="amp-mustache">
  Hello {{world}}!
</template>

下記のような script タグに書き変えても動作します。

<script type="text/plain" template="amp-mustache">
  Hello {{world}}!
</script>

しかし、以下のようなコードは

<template type="amp-mustache">
  <div class="greeting">Hello {{world}}!</div>
</template>

React では以下のように書き換えても問題は解決しません。

<script type="text/plain" template="amp-mustache">
  <div class="greeting">Hello {{world}}!</div>
</script>

上記のコードを動作させてみると、template から書き出された DOM には class 属性が描画されず、 DOM の情報が抜け落ちていることがわかります。

f:id:ka2jun8:20201213124600p:plain
class 名が描画されない

これは amp-mustache が内部で textContent を参照して template の中身を読み取る処理が書かれているのですが、 React では script の中の要素も DOM として判断されてしまうため、 textContent では DOM 内のテキスト情報(上の例では Hello {{world}}! )のみが取得されてしまうからです。

amphtml/amp-mustache.js at 07551bbf822dceef05e93e17cbb8e86acea7c467 · ampproject/amphtml · GitHub

つまりこの script タグを用いる場合も解決案1と同様に dangerouslySetInnerHTML と ReactDOM.renderToString を利用するような React コンポーネントの文字列化が必要になってしまい、 正しく動作させるための修正が大掛かりになります。

解決案3「Storybook で SSR する」

Next.js 側のコードを修正して解決できなさそうなのであれば、 Storybook 側でなんとかできないか考えます。 Storybook が @storybook/react で template タグを React で CSR しようとしてしまうことがそもそも問題なので、 React で SSR した結果の HTML を Storybook から参照できないか検討してみました。

Storybook ではサーバサイドで動作させることを前提にした @storybook/server があります。 もしくは @storybook/html では html をコンポーネント単位として管理する方法もあります。

storybook/app/server at next · storybookjs/storybook · GitHub

storybook/app/html at next · storybookjs/storybook · GitHub

サンプルが参考になります。

storybook/examples/server-kitchen-sink at next · storybookjs/storybook · GitHub

storybook/examples/html-kitchen-sink at next · storybookjs/storybook · GitHub

ちなみに、@storybook/server の README に書かれている npx -p @storybook/cli sb init -t server は動きません。 -t のオプションは type 指定ですが、以下の type 一覧内に server がないのでそんな type ないぜ、と怒られて動作しないようです。

storybook/project_types.ts at next · storybookjs/storybook · GitHub

@storybook/server を使うために SSR した HTML を返すサーバーを書くか、 @storybook/html を使うために SSR した HTML をどこかに吐き出して、参照するようにすればできそうです。

ただし、どちらにせよこれまで書き溜めてきた @storybook/react 向けの story が一切利用できなくなります。 また、@storybook/server だとしたら以下のような server.js で コンポーネントSSR して html を返すようなコードを書く必要がありそうです。

storybook/server.js at next · storybookjs/storybook · GitHub

大変な修正になりそうなので、対応することを断念しました...。 もしトライしたらまた別の記事にしようと思います。

余談「@storybook/react で SSR するなら」

@storybook/react のオプションとして SSR オプションがあったらどうだろうか検討してみました。

現在の @storybook/react の render は以下で呼び出されています。

storybook/render.tsx at 981e6c8b5dfde9613d4820c240e9c8f4e49a7f96 · storybookjs/storybook · GitHub

ここで、 ReactDOM.render() の代わりに ReactDOMServer.renderToString() を使うように変えてみます。

  const html = ReactDOMServer.renderToString(element);
  rootEl.innerHTML = '';
  const div = document.createElement('div')
  div.innerHTML = html;
  rootEl.appendChild(div);

余談の余談ですが、 ReactDOMServer の renderToString はクライアント側でも利用することができます。

以下のメソッドはサーバとブラウザの両方の環境で使用できます:

  • renderToString()
  • renderToStaticMarkup()

ReactDOMServer – React

上記のように ReactDOMServer.renderToString を利用した @storybook/react をローカルでビルドし、 開発中の Web アプリケーションで node_modules の @storybook 配下の render.js と置き換えて動作を確認してみたところ、 きちんと React コンポーネントSSR され、 Storybook でも amp-mustache が描画されることが確認できました。

しかし、 HTML が正しく描画されても CSS は描画されませんでした。 なぜなら、アプリケーションが CSS in JS である styled-components を利用しているからでした...。 そのため本格的にこの方法で取り込む場合は仮に @storybook/react で SSR オプションが導入されたとしても、 アプリケーションの Storybook 側でコンポーネントで利用している適切な CSS を収集して SSR 時に style タグを注入する必要があります。 以下はイメージですが、 コンポーネントstories 側で、今まで以下にしていた部分を、

const Template: Story<React.ComponentProps<typeof MustacheComponent>> = (
  args
) => <MustacheComponent {...args} />;

以下のように対象の CSS を書き出す形に変える必要がありそうです。

const Template: Story<React.ComponentProps<typeof MustacheComponent>> = (
  args
) => {
  const sheet = new ServerStyleSheet();
  try {
    const StyledComponent = sheet.collectStyles(
      <MustacheComponent {...args} />
    );
    // style 収集のために一度 render する
    renderToString(StyledComponent);
    return (
      <>
        {sheet.getStyleElement()}
        {StyledComponent}
      </>
    );
  } finally {
    sheet.seal();
  }
};

結局こちらもあまりスマートな解決策とは言えなさそうです...。

解決策検討の結果

  • 解決案1「template タグ部分で SSR する」: Next.js 側のコードにリスキーな変更を加える必要があり有効ではない
  • 解決案2「template タグを使わない」: Next.js 側のコードにリスキーな変更を加える必要があり有効ではない
  • 解決案3「Storybook で SSR する」: Storybook の資産を流用できないため大幅な改修が必要

最終的にスマートに解決する方法は見つかりませんでした。 その中で、解決案2の script タグを利用した template の書き方を、 影響範囲の少ないところから取り込んでいくことが良さそうかなと感じています。

今から同様の構成で新規で Storybook を取り込む場合には、 上記について検討した上で @storybook/server や @storybook/html で Storybook を取り入れる形式を選択すべきかなと思います。

私の方でもまだ調査しきれていませんが、 ampproject でも storybook の addon を開発中のようです。 こちらは React ではなく preact で使われているようなので、React ではそのまま利用できないかもしれませんが、 このようなツールの導入を検討してみるのも良いかもしれません。

GitHub - ampproject/storybook-addon-amp: The storybook AMP addon

まとめ

Next.js (React) と AMP と Storybook を組み合わせた際に起こりうるニッチな問題について、調査した内容をブログ記事にしました。

Next.js の AMP オプションを使って SSR で配信している Web アプリケーションに、 デザインシステムや VRT のために Storybook を導入しました。 しかし AMP の amp-list / amp-mustache で CSR を行っていると、 Storybook 上では思ったように描画しないことがありました。

これは amp-mustache で利用される template タグが React で利用できないためでした。 その代わりの解決策はいくつかありそうでしたが、 結果的にスマートな解決策が見つけられませんでした。 もし何かアドバイスありましたらご教示いただけると幸いです。

Gatsby.js で AMP 化するときの注意点

Gatsby.js

React.js + GraphQL ベースで簡単に静的サイトが作れるフレームワークWordPress よりも軽量で柔軟で開発者初心者でも扱いやすく、 markdown を書くだけでオリジナルのブログが簡単に作れるため流行ってきています。

www.gatsbyjs.org

実際に使ってみると確かにほとんど自分でコードを書かなくてもサンプルは揃っているし、 デザインテンプレートも充実しているのですぐにブログを公開することができます。

しかし markdown で書く場合は、 markdown to html が自動的に行われてしまうので、 実際のブログ記事本文に対して CSS をあてることに苦労します。 例えば、ブログの記事ごとに内部のリンクや太字に細かい修正を与えたくても、 基本的にはクラスやIDを振れるわけではないので、細かなスタイリングはできません。 もちろん CSS を global に与えることはできるので、全体として統一感のあるスタイルは自分で定義できます。

gatsby-config.js にいろいろプラグインを足していけば自分で意識することなくいろんなことができます。 例えば、静的ファイルの配信や画像の加工と最適化(←これ結構すごい)、サイトマップ生成やPWA化までできます。

TypeScript も苦労なく取り入れられるので開発者にとってもありがたいです。 Netlifyなどと連携すれば思い立ってからデプロイまで高速にできるのでありがたいフレームワークになっています。

AMP

AMP はブログのような記事を公開する静的サイトと相性がよく、 Google が検索結果の SERP を優遇することがあるため SEO 的にも有利とされており、 WordPress などでもよく取り入れられることがあります。

Gatsby でも使いたくなる人が結構いそうで、 GitHub 上でも Gatsby での AMP 化について以下に詳しく議論されています。

Consider adding first-class support for AMP · Issue #13454 · gatsbyjs/gatsby · GitHub

しかし現時点で具体的に AMP 化を Gatsby 本体でサポートするには至っていません。

そのため AMP 化する一番簡単な方法は、 html2amp というツールを使った plugin を使うことだと思います。 html2amp は HTML のタグを適切にAMP用のタグに変更して AMP HTML を吐き出してくれるツールです。 例えば img タグを amp-img タグにするといった簡易的な変換ツールといった感じです。

gatsby-plugin-html2amp | GatsbyJS

GitHub - tomoyukikashiro/html2amp: html2amp is simple converter from HTML into AMP(Accelerated Mobile Pages).

これを使えば gatsby build 時に AMP 化した HTML を同時に生成することができます。 このプラグインの場合、 gatsby develop では AMP 化したものを確認できないので注意が必要です。

また内部で画像サイズなどを取得して最適化してくれようとするので、 過去の記事の画像が存在しない 404 になってしまうと html2amp が落ちてビルドに失敗します。 同様に何らかの原因で html2amp が落ちてしまうと build そのものに失敗してしまうので、 gatsby develop で確認し CI上ではじめて build が走るようにしているとデプロイできないことになるので注意が必要です。

基本的に AMP バリデータを自分でかけながら確認しているわけではないので、 デプロイしてから AMP のルールに基づいてなかったということに気付いたり、 画像が思ったように表示されてなかったりということに気付いたりします。

html2amp は files: ["**/index.html", "index.html"], とオプションで指定するとすべての記事を AMP 化しようとするので、 もし AMP 化を試みる場合は意識しながら記事やReactテンプレートを書く必要があるかなと思います。

ただ serviceWorker や optimizer もオプショナルに利用できるようになっており、 細かなニーズに応えてくれている OSS なので、今後より使い勝手のイイものになってくれるといいなと思います。

Web story (AMP story)が使えない

Web ストーリー(元AMPストーリー)は、AMP が提供している最もリッチなコンポーネントです。 AMPストーリーは instagram などで最近よくみる全画面でスライドショー的に画像や動画が順に表示されるコンテンツです。

これを取り入れようとしても Gatsby.js では Web ストーリーを利用できません。 テンプレートとして新たにAMPストーリー用のページを作る場合は、 AMP のスクリプトを react-helmet 経由で head タグに差込み、 以下のように amp-story を直接書いたとします。

return (
  <>
      <amp-story
        standalone
        title="Joy of Pets"
        publisher="AMP tutorials"
        publisher-logo-src="/salty_egg.jpg"
        poster-portrait-src="/salty_egg.jpg"
      >
        <amp-story-page id="cover">
          <amp-story-grid-layer template="fill">
            <amp-img
              src="/salty_egg.jpg"
              width="720"
              height="1280"
              layout="responsive"
            ></amp-img>
          </amp-story-grid-layer>
        </amp-story-page>
      </amp-story>
   </>
)

しかし実際に実行してアクセスしてみても以下のようなエラーを出して表示されません。

index.js:2177 Render timeout waiting for service amp-story-render to be ready.<200b><200b><200b>
...
log.js:710 Uncaught Error: Render timeout waiting for service amp-story-render to be ready.<200b><200b><200b>
    at cb (log.js:710)
    at Za.f.createError (log.js:359)
    at timer-impl.js:142
    at timer-impl.js:83

AMP のエラーは深堀りしてませんが、まともに議論されていなさそうです。 面倒なので大してこれ以上調べてません。

Render timeouts waiting for service to load · Issue #6569 · ampproject/amphtml · GitHub

Uncaught Error: Render timeout waiting for service amp-story-render to be ready.​​​ · Issue #22774 · ampproject/amphtml · GitHub

Web story は body 直下に置くことを前提に作られているコンポーネントです。 逆にいうと amp-story しか置けないようになっています(AMP validator でもひっかかる)。 おそらくそれが原因で上記エラーになってしまったのかなと予想しています。

Gatsby は body 直下に

を置いて、その配下に各ページの HTML を書き出しています。 つまり amp-story タグを body 直下に置くことは実質不可能だと思われます。 (試しに devtools で
あたりを外して amp-story を body 直下にしたら表示されました。)

gatsby で HTML 構造の話もされていましたが、こちらも特に進捗はないようです。 No HTML markup for programmatically created pages · Issue #7648 · gatsbyjs/gatsby · GitHub

そうなると、残念ながら gatsby では amp-story を動かすことは実質不可能ということになります。

まとめ

GatsbyGatsby + AMP 化について調べてみました。 Gatsby で簡単に記事を書いてデプロイするまでは超高速にできそうなので、 簡易的なブログやドキュメントを公開するにはとても良さそうに思いました。

AMP 化についてはギリギリ使えるか使えないか、といったところかなという印象です。 単純なブログ記事のAMP化については問題なさそうですが、 公式サポートされているわけではないので入れるメリットより入れたときの不安定さの方が強そうかなと思いました。

Webストーリーを使うことは難しそうです。 今回はWebストーリーしかみてませんが、 他のAMPコンポーネントを使おうとしても苦しむことがあ流かもしれないなと思いました。

画像をタイル上に並べる方法

概要

サイズの異なる画像をタイル上に並べる際に、 column-gapcolumn-count が使えるということを知ったのでメモ。

タイル上と言っているのはこんな感じ↓の画像の配置のこと。

f:id:ka2jun8:20200314164603p:plain:w300

本記事の内容は下記のページの焼き直し。

css-tricks.com

HTML + JS

下記は 100px から 400px までのランダムな大きさの画像を 25枚並べているだけ。

<div id="photos" class="photo"></div>
function getRandomSize(min, max) {
  return Math.round(Math.random() * (max - min) + min);
}

for (var i = 0; i < 25; i++) {
  var width = getRandomSize(100, 400);
  var height =  getRandomSize(100, 400);
  $('#photos').append('<div class="item"><img src="//placehold.jp/'+width+'x'+height+'.png" alt="sample image"></div>');
}

CSS(SCSS)

適用する css (簡単のため scss )は以下。

.photos {
  column-gap: 0;
  column-count: 4;
  line-height: 0;
  width: 100%;

  .item {
    padding: 1px;

    img {
      width: 100%;
      height: auto;
    }
  }
}

column-gapcolumn-count を使う。 column-gap でカラムとカラムの間を 0px とし、 column-count で列数(今回では4)を指定する。

基本的にはこれだけでタイル上に画像が並ぶ。 あとは、細かいスタイル修正を行えばok

例えば、 line-hight を 0 として縦間隔を狭めたり、padding をとって間をあけたりなど。 画像に対しては縦横比を揃えたまま表示するようにしておく。

参考:

column-gap (grid-column-gap) - CSS: カスケーディングスタイルシート | MDN

column-count - CSS: カスケーディングスタイルシート | MDN

サンプルコード

HTML+CSSで右上に三角のラベルをつける方法

概要

あるコンポーネントの右上にラベルやバッジをつけるHTML/CSSの書き方を学んだので備忘。 もっと良い方法がある場合は教えて欲しい。 サンプルは以下のようなもの。

f:id:ka2jun8:20200314152227p:plain:w200

HTML

HTMLでは親のDOM(class: badge)に、 - 三角形を描くDOM(class: triangle)と、 - その上に表示するラベルを描くDOM(class: label) をそれぞれ用意する。 子要素をそれぞれ before, after の擬似要素で用意しても書けそう。 必要以上にDOMを増やしたくない場合はその方がいいかも。

    <div class="badge">
      <span class="triangle"></span>
      <span class="label">ラベル</span>
    </div>

CSS(SCSS)

CSS(今回簡略化のためSCSS)は以下。

.badge {
  position: relative;
  
  .triangle {
    position: absolute;
    border-bottom: 60px solid transparent;
    border-left: 60px solid red;
  }

  .label {
    position: absolute;
    top: 10px;
    left: 5px;
    font-size: 14px;
    color: #fff;
    transform: rotate(-45deg);
  }
}

まず親となる badge に position: relative を持たす。 そして子要素たちに position: absolute を持たす。

次にtriangleで三角形を作る。 cssで三角形の作り方は下記を参照。

www.granfairs.com

上記に書かれている通り、以下で実現できる。サイズや色は適当。

    border-bottom: 60px solid transparent;
    border-left: 60px solid red;

最後にlabelで文字を上に描く。

  .label {
    position: absolute;
    top: 10px;
    left: 5px;
    font-size: 14px;
    color: #fff;
    transform: rotate(-45deg);
  }

transform: rotate(-45deg); で45度回転させて文字を表示する。 top, left を使って場所を調節する。 font-size や color は適当。

ラベルは下におかれるコンポーネントの上に載せないといけないので、 必要に応じて z-index を指定する必要があると思われる。

サンプルコード

完成形のサンプルコードは以下。