プログラミング
Nextjs+microCMSで10秒ごとに更新かつpropsを自動補完できるページの作り方
microCMSで、記事タイトル・スラッグ・サムネイル・本文を取得し、NextjsでISR(Incremental Static Rendering)します。その際、エディタでPropsを全部自動補完できるようにします。
環境VSCodeNode.js v16.14.0microCMS API v1microCMS側の仕様変更や画面デザインの変更があるかもしれないのでご注意ください。
Next.jsの準備npx create-next-app@latest --ts
Googleのサジェストでcreate-nuxt-appばっかり出てきて困りますね。Next使いはこれぐらい検索しないので出ないんでしょう(偏見)。
{
  "name": "nextjs-microcms-practice",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "12.0.10",
    "react": "17.0.2",
    "react-dom": "17.0.2"
  },
  "devDependencies": {
    "@types/node": "17.0.17",
    "@types/react": "17.0.39",
    "eslint": "8.9.0",
    "eslint-config-next": "12.0.10",
    "typescript": "4.5.5"
  }
}
▲昔と違ってeslint-config-next が入っています。今回はこれ以上lintはいじりません。
microCMSでAPIを用意
/api/v1/article というルートのエンドポイントを作りました。
ブログ的なサイトを作るので、リスト形式にします。
以下のようにフィールドを設定してください。
title / タイトル / テキストフィールド / 必須slug / スラッグ / テキストフィールド / 必須 / ユニーク / 後述の正規表現を適用thumbnail / サムネイル / 画像 / 任意body / 本文 / リッチエディタ / 必須
スラッグの詳細設定コンテンツIDをスラッグとして使うことも可能ですが、非エンジニアに触らせるのは危険すぎるので、別でユニークなスラッグフィールドを作りましょう。
ユニークの設定は「詳細設定」のところにあります。
「特定のパターンのみ入力を許可する」をONにしてください。パターンは^[a-z0-9]+(?:-[a-z0-9]+)*$でフラッグはなしです。
article-1 はOKですが、-artielce-1 みたいな先頭のハイフンはNGで、article--1 みたいな2回スラッグもNG、article-1- みたいな末尾のスラッグもNGになります。
お前こんなのどこで見つけたんだよって感じですが、
https://ihateregex.io/expr/url-slug/
i Hate Regex(正規表現なんて大っ嫌い)という神サイトがあってですね、ここに詳細な解説と一緒に例が載っています。
適当なデータを作成
適当な記事を書いて公開してください。プレースホルダに『ポラーノの広場』を使うとなんかデザイナー感出る。フォントに詳しそう。
microCMSと接続するnpm i microcms-js-sdk
ライブラリを追加。
APIキーをコピーします。
echo MICROCMS_SERVICE_DOMAIN=あなたのmicroCMSサービス名 >> .env.local
echo MICROCMS_API_KEY=$(pbpaste) >> .env.local
環境変数を書き出し。Linuxだとpbpasteじゃなくてxclip -selection clipboard -o を使えばいいそうです(ソース)
あと、クライアントサイドで使わないのでconfigで環境変数を登録する必要はありません。
mkdir lib && touch $_/microcms.ts
microcms.ts を作ります。
import { createClient } from 'microcms-js-sdk'
export const client = createClient({
  serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN!,
  apiKey: process.env.MICROCMS_API_KEY!,
})
これでデータ取得の用意が整いました。
で、ここでReact 18からSSRでもSuspenseが...とか語ってたらキリがないので、普通にSSRして、静的にページを書き出せる形で作ります。
(React世界にはいろいろなデータフェッチ方法があるよ!調べてみよう!)
型定義microcms-typescript っていうライブラリが新しいAPIに対応していなかったので、公式のSDKを使います。
API_NAME=article
mkdir types && touch $_/article.ts
API_NAME はあとでも使います。
import type { MicroCMSDate, MicroCMSImage, MicroCMSListResponse } from 'microcms-js-sdk'
export type ArticleType = {
  id: string
  title: string
  slug: string
  thumbnail?: MicroCMSImage
  body: string
} & MicroCMSDate
export type ArticleListType = MicroCMSListResponse<Article>
MicroCMSListResponse でtotalCount などを付けます。それ以外の部分を自動で書いてくれるライブラリを見つけられませんでした。(aspidaでもここは手動)
記事コンポーネントを作るORGANISMS_DIR=./components/organisms
mkdir -p $ORGANISMS_DIR && touch "_/$(echo "$API_NAME" | sed 's/.*/\u&/').tsx"
Atomic Designガン無視で突然「Organisms(生体)」が登場しましたね。宇宙の 法則が 乱れる!
あと、イキりすぎて全部コマンドで済ませようとしていますね、何をしているかというとcomponents/organisms/(API名の先頭だけ大文字).tsx ってファイルを作っているだけです。大文字にする方法は色々あるそうなんですが、bashのバージョン云々が面倒なので置換してしまいました。
import { ArticleType } from '../../types/article'
export default function Article(props: ArticleType) {
  return (
    <article>
      <h1>{props.title}</h1>
      {props.thumbnail && <img src={props.thumbnail.url} alt={`${props.title}のサムネイル`} />}
      <div dangerouslySetInnerHTML={{ __html: props.body }}></div>
    </article>
  )
}
記事コンポーネントです。Atomic Designガン無視で生体の中身を書いてしまいましたが、説明のためなので見逃してください。
記事一覧を作るtouch "$ORGANISMS_DIR/$(echo "$API_NAME" | sed 's/.*/\u&/')List.tsx"
ArticleList.tsx を作ります。
import { ArticleListType } from '../../types/article'
export default function ArticleList(props: ArticleListType) {
  return (
    <div>
      <ul>
        {props.contents?.map(
          (p) =>
            p && (
              <li key={p?.id}>
                <a href={`/articles/${p.slug}`}>{p.title}</a>
              </li>
            )
        )}
      </ul>
    </div>
  )
}
最低限の記事リストです。
一覧ページを作成するそれではNext.jsの真骨頂、SSRにおける型の推測をお見せしましょう。
import type { InferGetStaticPropsType, NextPage } from 'next'
import Head from 'next/head'
import ArticleList from '../components/organisms/ArticleList'
import { client } from '../lib/microcms'
import { ArticleType } from '../types/article'
export const getStaticProps = async () => {
const articlesData = await client.getList<ArticleType>({
endpoint: 'article',
queries: {
limit: 10,
orders: 'publishedAt',
},
})
return {
props: { articlesData: articlesData ?? null },
revalidate: 10,
}
}
type Props = InferGetStaticPropsType<typeof getStaticProps>
const Home: NextPage<Props> = (props) => {
return (
<div>
<Head>
<title>記事は{props.articlesData.totalCount}あります</title>
</Head>
<main>{props.articlesData ? <ArticleList {...props.articlesData} /> : <div>記事を取得できませんでした。</div>}</main>
</div>
)
}
export default Home
InferGetStaticPropsType<typeof getStaticProps> で、getStaticProps の返り値の方をReactのchildren と混ぜます。
するとHome のpropsにprops.articlesData が追加され、サーバーサイドの型をわざわざコンポーネント用に再度書く必要がなくなります。
こんな感じにタイトルに反映されます。
また、revalidate: 10 でISRが有効になり、適切なデプロイ環境なら10秒ごとにデータのキャッシュが切れます(10秒ごとにサーバーが動くわけではない。ここ重要)。
記事単体ページを作るmkdir -p pages/${API_NAME}s && touch $_/\[slug\].tsx
[slug].tsx はparams.slug を変数として取ります。
import type { GetStaticPaths, GetStaticPropsContext, InferGetStaticPropsType, NextPage } from 'next'
import Head from 'next/head'
import Article from '../../components/organisms/Article'
import { client } from '../../lib/microcms'
import { ArticleType } from '../../types/article'
export const getStaticPaths: GetStaticPaths = async () => {
  const allArticles = await client.getList<ArticleType>({
    endpoint: 'article',
    queries: {
      limit: 10,
      fields: ['slug'],
    },
  })
  const paths = allArticles.contents.map((c) => {
    return {
      params: {
        slug: c.slug,
      },
    }
  })
  return { paths, fallback: true }
}
export const getStaticProps = async ({ params }: GetStaticPropsContext) => {
  const articlesMatched = await client.getList<ArticleType>({
    endpoint: 'article',
    queries: {
      limit: 1,
      filters: 'slug[equals]' + params?.slug,
    },
  })
  if (articlesMatched.contents[0]) {
    return {
      props: { articleData: articlesMatched.contents[0] ?? null },
      revalidate: 10,
    }
  }
  return {
    props: {
      articleData: null,
    },
    notFound: true,
    revalidate: 10,
  }
}
type Props = InferGetStaticPropsType<typeof getStaticProps>
const ArticlePage: NextPage<Props> = (props) => {
  if (props.articleData) {
    const { title } = props.articleData
    return (
      <div>
        <Head>
          <title>{title}</title>
        </Head>
        <main>
          <Article {...props.articleData} />
        </main>
      </div>
    )
  }
  return <div>記事を取得できませんでした。</div>
}
export default ArticlePage
長いので分けて説明。
getStaticPathsexport const getStaticPaths: GetStaticPaths = async () => {
  const allArticles = await client.getList<ArticleType>({
    endpoint: 'article',
    queries: {
      limit: 10,
      fields: ['slug'],
    },
  })
  const paths = allArticles.contents.map((c) => {
    return {
      params: {
        slug: c.slug,
      },
    }
  })
  return { paths, fallback: true }
}
Next.jsのgetStaticPropsで、動的にURLを生成するための関数です。client.getList<ArticleType> で一覧からslugだけを取り出し、それらを[{params: slug: "スラッグ"}] といった配列に収めてpaths に返します。
fallback: true は、初回生成時にないページに訪れても、データを取得する設定です。正直ブログだと必要ないのですが、公開した記事を即見れるようにしたい場合に便利です。
getStaticPropsexport const getStaticProps = async ({ params }: GetStaticPropsContext) => {
  const articlesMatched = await client.getList<ArticleType>({
    endpoint: 'article',
    queries: {
      limit: 1,
      filters: 'slug[equals]' + params?.slug,
    },
  })
  if (articlesMatched.contents[0]) {
    return {
      props: { articleData: articlesMatched.contents[0] ?? null },
      revalidate: 10,
    }
  }
  return {
    props: {
      articleData: null,
    },
    notFound: true,
    revalidate: 10,
  }
}microCMSには2022年2月13日現在ユニークなフィールドからコンテンツ1つを返すAPIが用意されていません。そのため、filters: 'slug[equals]' + params?.slug で絞り込んで長さ0~1の配列をもらい、記事の有無を見ています。Strapiとか使えば自前でAPI作れるから楽しいんですけど、microCMSでは2022年2月13日現在こうするしかありません。
あと、params.slugの型は本来string | string[] | undefined ですから、文字列じゃない可能性を考慮するべきですが、文字列とプラスして文字列にしてしまえばMicroCMSQueries.filters?: string | undefined とたまたま合致し、型チェックが通ります。
しかもURL構造上配列になりえない(クエリパラメータじゃないのでカンマ書いても配列にならない)ので、これで動作に支障はきたしません。
notFound: true を返せば404になります。もちろん404ページはカスタマイズ可能です。
ページコンポーネントtype Props = InferGetStaticPropsType<typeof getStaticProps>
const ArticlePage: NextPage<Props> = (props) => {
  if (props.articleData) {
    const { title } = props.articleData
    return (
      <div>
        <Head>
          <title>{title}</title>
        </Head>
        <main>
          <Article {...props.articleData} />
        </main>
      </div>
    )
  }
  return <div>記事を取得できませんでした。</div>
}
export default ArticlePage
titleだけはページで使いたいので取り出します。ページ以外でheadをいじるのは人間のやることではない。
画像をNext/Imageにする
Do not use <img>. Use Image from 'next/image' instead. See: https://nextjs.org/docs/messages/no-img-elementnext/core-web-vitals っていうeslintプラグインが追加されていて、普通のimgタグだとwarnレベルの警告が出ます。このままでもビルドはできますが、練習がてらNext/Imageを使ってみましょう。
next.config.js  images: {
    domains: ['images.microcms-assets.io'],
  },
以上の設定を加えてください。microCMS以外の画像を使う場合は、適宜追加してください。
mkdir ./components/atoms && touch $_/MicroCMSImage.tsx
やっと原子を作ります。順番がおかしい。
MicroCMSImage.tsximport type { MicroCMSImage } from 'microcms-js-sdk'
import Image from 'next/image'
export default function MicroCMSImage(props: { image: MicroCMSImage; alt: string }) {
  const { width, height, url } = props.image
  return <Image width={width} height={height} src={url} alt={props.alt} />
}
next/image用にコンポーネントを分けるのは、サイズの取得が面倒だからです。
Article.tsximport { ArticleType } from '../../types/article'
import MicroCMSImage from '../atoms/MicroCMSImage'
export default function Article(props: ArticleType) {
  return (
    <article>
      <h1>{props.title}</h1>
      {props.thumbnail && <MicroCMSImage image={props.thumbnail} alt={`${props.title}のサムネイル`} />}
      <div dangerouslySetInnerHTML={{ __html: props.body }}></div>
    </article>
  )
}
これで画像部分を最適化できました。
HTMLがすごいことになってたら成功です。
参考microCMS: https://dev.classmethod.jp/articles/remix-with-microcms/InferGetStaticPropsType: https://zenn.dev/catnose99/articles/7201a6c56d3c88
内容を見る
プログラミング
4200000000Hzでゴリ押す!!!(2)
今回はアルゴリズムとCPUのパワーでパズルを全力で解きます。
Exponential Idle(Android版 / iOS版)というクリッカー系放置ゲーム中のミニゲームである、矢印パズルを攻略します。
こちらのnoteの記事にめっちゃ影響を受けて書くことにしました。
と言っても、初級と中級は私でも頑張れば解けたのと、上級も先行研究があるので、エキスパートに絞って話を進めていきます(まあ上級も同じプログラムをちょちょっといじれば簡単に解けますが)。訂正とお詫び前回のサムネイルの一部に「♪ラデツキー行進曲(1.5倍速)」とありましたが、正しくは1.543倍速です。謹んでお詫び申し上げます。ルール説明(エキスパート)図1:矢印パズル(エキスパート)のルール六角格子状に並べられたタイルをタップすると、そのタイルを含む周囲(最大)7個のタイルがそれぞれ時計回りに60°回転します。
それを繰り返してすべてのタイルの向きを上向きに揃える、というルールです。目標盤面を入力として受け取り、各タイルのタップ回数を出力する。基本の考え方1回タップすることに60°回転するので、6回同じ場所をタップすれば盤面の状態は変わらず、加法巡回群をなすような6元集合、$\mathbb{Z}/6\mathbb{Z}$ を持ってくる。解法案1参考文献[3]と同様に、$\mathbb{Z}/6\mathbb{Z}$ 上の37次の線形方程式を解くことに帰着させる方法です。が。
聡明な読者の方ならお気づきでしょう。$\mathbb{Z}/6\mathbb{Z}$ は体でしょうか。2,3,4は乗法逆元を持たないので体ではないですね。
すると線形方程式を解くときに除法が使えないということになり、掃き出し法は使えないことになります(ピボットを1にできないだけで掃き出せないわけではないだろうが、一般的なアルゴリズムをそのまま持ってくるのは厳しそう)。2掃き出しで解けない(難しい)と言っても、元は有限個しかないので $6^{37}=6.2 \times 10^{28}$ 通りすべてを試してしまえという考え方。猿。
いや無理やけど。もし1パターンを1nsで検証できたとしても6.2e19 s掛かります。実際は1パターンの検証で約7x37回の足し算をすることになるのでもっとかかります。
imos法で最適化すればどうにかなるようなレベルの話ではないです。やるだけ無駄。3大本命。DFSです。DFS自体は木の全探索なのですが、探索の必要がない部分を枝刈りすればだいぶ計算量が減ります。どれぐらい絞れるかは後述。
今回はこれを採用。プログラムこれが解を探索するプログラムです。
入力の数字は、ディスプレイモードを数字にして表示された数字ということになっています(内部的には-1して持っているが)。#include <iostream>
#include <vector>
#include <tuple>
#include <algorithm>
using index_t = std::tuple<int, int>;
using board_t = std::vector<std::vector<int>>;
const int mod = 6;
const size_t board_row = 7, board_col = 7;
void dfs(board_t &board, board_t &hands, index_t index);
void flip_around(board_t &board, index_t index, int amount);
index_t next(index_t index);
bool is_in_board(index_t index);
void print_board(const board_t board);
const index_t end_index = next({ 6, 6 });
int main() {
  board_t board(board_row, std::vector<int>(board_col));
  board_t hands(board_row, std::vector<int>(board_col));
  for (index_t i = { 0, 0 }; i != end_index; i = next(i)) {
    const auto [r, c] = i;
    std::cout << r << " " << c << ": " << std::flush;
    std::cin >> board[r][c];
    board[r][c]--;
  }
  dfs(board, hands, { 0, 0 });
  return 0;
}
void dfs(board_t &board, board_t &hands, index_t index) {
  if (index == end_index) {
    print_board(hands);
    exit(0);
    return;
  }
  auto [r, c] = index;
  // 右端辺での枝刈り
  if (c == 6 && r > 3 && board[r - 1][c - 1] != board[r - 1][c]) {
    return;
  }
  // 下端辺での枝刈り
  if (r == 6 && c > 3 && board[r - 1][c - 1] != board[r][c - 1]) {
    return;
  }
  // 上端、左端辺以外での枝刈り
  if (r != 0 && c != 0) {
    // 左上マスを0にするように動かす
    const int hand = (mod - board[r - 1][c - 1]) % mod;
    hands[r][c] = hand;
    flip_around(board, index, hand);
    dfs(board, hands, next(index));
    flip_around(board, index, mod - hand);
  } else {
    for (int hand = 0; hand < mod; hand++) {
      hands[r][c] = hand;
      flip_around(board, index, hand);
      dfs(board, hands, next(index));
      flip_around(board, index, mod - hand);
    }
  }
}
void flip_around(board_t &board, index_t index, int amount) {
  // 正体不明だがちゃんと最初だけ初期化処理が走る
  static std::array<index_t, 7u> dpos{
    dpos[0] = { -1, -1 },
    dpos[1] = { -1, 0 },
    dpos[2] = { 0, -1 },
    dpos[3] = { 0, 0 },
    dpos[4] = { 0, 1 },
    dpos[5] = { 1, 0 },
    dpos[6] = { 1, 1 }
  };
  const auto [r, c] = index;
  for (const auto [dr, dc] : dpos) {
    if (!is_in_board({ r + dr, c + dc })) continue;
    board[r + dr][c + dc] += amount;
    board[r + dr][c + dc] %= mod;
  }
}
index_t next(index_t index) {
  auto [r, c] = index;
  if (c == 6 || c - r >= 3) {
    r++;
    c = std::max(r - 3, 0);
  } else {
    c++;
  }
  return { r, c };
}
bool is_in_board(index_t index) {
  auto [r, c] = index;
  return std::abs(r - 3) <= 3 && std::abs(c - 3) <= 3 && std::abs(c - r) <= 3;
}
void print_board(const board_t board) {
  for (size_t i = 0; i < board_row; i++) {
    for (size_t j = 0; j < board_col; j++) {
      if (is_in_board({ i, j })) {
        std::cout << board[i][j] << " ";
      } else {
        std::cout << "  ";
      }
    }
    std::cout << "\n";
  }
  std::cout << std::endl;
}
こんなに const とか & とか書くんだったら最初からRustでやればよかったなと若干の後悔。
ぼちぼち解説していきます。
まずここ。using index_t = std::tuple<int, int>;
using board_t = std::vector<std::vector<int>>;index_t というエイリアスに std::tuple<int, int> を当てています。
index_t index = {row, column}; となっているならば、index は、row 行 column 列のパネルの位置を表します。
board_t には std::vector<std::vector<int>> を当てていますが、
board_t board(R, std::vector<int>(C)); としたならば、board は、R 行 C 列のパネルの二次元配列を表します。(入力と)パネルの現状態と出力する手数に使います。
図2:indexの指し方
次はここ。index_t next(index_t index) {
  auto [r, c] = index;
  if (c == 6 || c - r >= 3) {
    r++;
    c = std::max(r - 3, 0);
  } else {
    c++;
  }
  return { r, c };
}next関数はその名の通り、次に探索する(タップ回数を決定する)点を決定する関数で、DFSでの探索順に直接関わってきます。
定義はまあ見りゃわかりますね(ifの条件に c - r >= 3 とか書いてあるが、これは条件をいっぱい書きたくなかったからうまいこと纏めてあるだけ)。
2行目の auto [r, c] = index; ってのは構造化束縛というC++17以降で追加された機能です。分割代入だと思って大体問題ありません。
一応探索順を図に示しておくとこうです。図3:DFSの探索順この順序にしているのは、単純に分かりやすいという以外にもう一点理由があって、枝刈りがしやすいという点。
枝刈りの方法はdfs関数を見ればわかります。
void dfs(board_t &board, board_t &hands, index_t index) {
  if (index == end_index) {
    print_board(hands);
    exit(0);
    return;
  }
  auto [r, c] = index;
  // 右端辺での枝刈り
  if (c == 6 && r > 3 && board[r - 1][c - 1] != board[r - 1][c]) {
    return;
  }
  // 下端辺での枝刈り
  if (r == 6 && c > 3 && board[r - 1][c - 1] != board[r][c - 1]) {
    return;
  }
  // 上端、左端辺以外での枝刈り
  if (r != 0 && c != 0) {
    // 左上マスを0にするように動かす
    const int hand = (mod - board[r - 1][c - 1]) % mod;
    hands[r][c] = hand;
    flip_around(board, index, hand);
    dfs(board, hands, next(index));
    flip_around(board, index, mod - hand);
  } else {
    for (int hand = 0; hand < mod; hand++) {
      hands[r][c] = hand;
      flip_around(board, index, hand);
      dfs(board, hands, next(index));
      flip_around(board, index, mod - hand);
    }
  }
}私もそこまでアホではないし、この関数は深い再帰を伴うので引数の大部分は参照で受けるようにしました。
部のC#erの皆さんが卒倒するかもしれませんが、vector<T>は値型です。というか、C/C++に値型・参照型という分け方はありません。
明示しない限り常にすべて値渡しになるので、ちゃんと参照渡しになるよう指定してやります。
さておき、枝刈りの話。これも図に示したほうが早いでしょう。図4:dfs関数の枝刈り図3に示した探索順から分かるように、27,32,36のノードでは、そのノードを操作した後は左上と真上の変化がないので、その2箇所の状態は必ず同じでなければならず、そうでなければ後退して探索し直すことになります。34,35,36でも左上と左下で同じことが起こります。
また図3から分かるように、ある時点で探索しているパネルの左上にパネルがあったとき、探索しているパネルを動かした後に再び左上のパネルが変わることはないので、左上は0にならなくてはなりません。よって左上のパネルを元にただ一通りに決まります。逆に言えば、0~5の中から自由に選べるのは左上にパネルが存在しない0,1,2,3,4,9,15のたった7個のパネルのタップ回数のみです。したがって、検証の必要なパターン数は $6^7 \approx 10^{5.45}$ 通りだけになります(ただし、関数呼び出し自体が重いことと、これらの点の不正性が分かるまでに再帰の深いところに潜ることがあるのでやはりある程度計算に時間はかかる)。
こんな感じで殆どの点で枝刈りが行えることがわかりますね。
if (index == end_index) {
  print_board(hands);
  exit(0);
  return;
}で、最後36番が終わればここの分岐にたどり着いて終わりです(なんでこの人exitとreturn両方書いてるんだろう)。
でも図4を見てもらえばわかりますが、36番ノードだけチェックが入ってないんですよね。
これでいいのかと言われれば良くて、解が存在するならば、36番ノードだけが非0であることはありません。
流石に解が存在しない問題は生成されないだろうということでここは一つ。
flip_around(board, index, hand);
dfs(board, hands, next(index));
flip_around(board, index, mod - hand);ところでこの部分ですが、再帰DFSにおける常套手段です。
ある変換(ここではある1パネルをn回タップすること)が可逆であるならば、dfs呼び出しから帰ってきた後に逆変換(ここでは同じパネルを6-n回タップすること)を行えばうまいことDFSになります。是非覚えておいてください。
以上。閑話void flip_around(board_t &board, index_t index, int amount) {
  // 正体不明だがちゃんと最初だけ初期化処理が走る
  static std::array<index_t, 7u> dpos{
    dpos[0] = { -1, -1 },
    dpos[1] = { -1, 0 },
    dpos[2] = { 0, -1 },
    dpos[3] = { 0, 0 },
    dpos[4] = { 0, 1 },
    dpos[5] = { 1, 0 },
    dpos[6] = { 1, 1 }
  };
  ...
}この部分、何ですか。自分で書いたけどどういう理屈で初期化されているのかわかりません。C++プロの方、教えて下さい。
aggregateならメンバ名を指定して初期化する方法があるけど、その延長と考えていいんですかね。参考文献[1]Conic Games「Exponential Idle - Google Play のアプリ」<https://play.google.com/store/apps/details?id=com.conicgames.exponentialidle>
[2]Gilles-Philippe Paille「「Exponential Idle」をApp Storeで」<https://apps.apple.com/jp/app/exponential-idle/id1538487382>
[3]ちゃそ「Exponential Idle #2 矢印パズル攻略法(と二元体GF(2)上の線形方程式について)」<https://note.com/so_ra_64/n/n9c2eb6a5ef6f>
[4]ますたー。/繰り上げP「焼き甜花ちゃんシリーズの作り方&素材」<https://www.nicovideo.jp/watch/sm37861810>
[5]いもす研「いもす法」<https://imoz.jp/algorithms/imos_method.html>
内容を見る
プログラミング
LaravelをCloud Runにデプロイして効率よくバックエンドを開発しよう!
何円ぐらいで済むの?
以下の手順で60.77時間、動かしてみました。リージョンは全て東京(asia-northeast1)です。
結果MySQL 実行: 97円MySQL ストレージ: 11円ストレージ(NA-APAC間の通信): 5円以上、113円でした。
一時間1.85円として計算すると、1ヶ月で1338円になります。
お詫びすいません、ケチれませんでした!!!!!
ただ、GitHubからデプロイすることで効率の良い開発スケール他GCPサービスとの円滑な連携が可能になる点は見逃せません。余裕のある方は試してみてください。
下準備GCPのセットアップgcloud CLIをインストールしておく。環境によって手順が異なるので省略。
プロジェクトを作り、支払いアカウントを紐づける。
gcloud auth loginまずはログインする。
PROJECT_NAME=(表示上のプロジェクトネーム)
PROJECT_ID=$(gcloud projects list --format 'value(projectId)' --filter name=$PROJECT_NAME)
export PROJECT_ID
gcloud config set project $PROJECT_ID
gcloud services enable \
    containerregistry.googleapis.com \
    cloudresourcemanager.googleapis.com \
    iam.googleapis.com「表示上のプロジェクトネーム」はGCPの画面に出ているプロジェクト名。プロジェクトをセットし、APIを有効化する。
Terraformのインストールいつか使うんだろうなーと思っていた。入れてなかったのでここで入れておく。
brew install tfenv
tfenv install 0.12.6
tfenv use 0.12.6tfenvを使うのは、以下のテンプレートが0.12.6を想定していたから。随時変更しよう。
ステップ1. テンプレートを準備自分でやろうとしたがあまりにめんどくさかったので、以下のテンプレートを使う。
https://github.com/villers/laravel-cloud-run-sql
これをフォークしよう。
クローンしてビルドgit clone git@github.com:あなたのユーザー名/laravel-cloud-run-sql.git
cd laravel-cloud-run-sql
docker-compose build
euをasiaに東京で動かすので、レポジトリ内のeu.gcr.ioをasia.gcr.io に置換。
ローカルで試すdocker-compose up
起動の様子。なんでもDockerでやれば、なんかすごいことやってるみたいに見える。
docker-compose run --rm app php artisan migrate --seed
マイグレーションする。
http://localhost:8080/ で動作確認しよう。
ステップ2. GCPにコンテナをアップするサービスアカウントを用意するサービスアカウントを作り、Owner権限を与える。キーをダウンロードしてterraform-key.json として保存し、環境変数に設定、サービスアカウントとして認証する。それらが以下のコマンドでできる。ありがとうgcloud CLI!
gcloud iam service-accounts create deployer \
    --display-name "Terraformのサービスアカウント"
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
  --member serviceAccount:deployer@${PROJECT_ID}.iam.gserviceaccount.com \
  --role roles/owner
gcloud iam service-accounts keys create ~/terraform-key.json \
  --iam-account deployer@${PROJECT_ID}.iam.gserviceaccount.com
export GOOGLE_APPLICATION_CREDENTIALS=~/terraform-key.json
gcloud auth activate-service-account --key-file=$GOOGLE_APPLICATION_CREDENTIALS
ビルド(M1注意)export SERVICE_NAME=laravel-app-service
docker build --platform linux/amd64 -f .cloud/docker/php/Dockerfile -t asia.gcr.io/${PROJECT_ID}/${SERVICE_NAME} .
上記コマンドでアジア用タグをつけたイメージをビルドする。間違ったタグをつけたりしたら、docker images からのdocker rmi [ID] で消せる。それか、Docker Desktopのイメージ画面を使う。
--platform linux/amd64 はM1用。ARMでビルドしてもデプロイで死ぬ。このせいで数時間が無駄になった。
レジストリにプッシュgcloud auth configure-docker
docker push asia.gcr.io/${PROJECT_ID}/${SERVICE_NAME}コンテナレジストリにログインして、さっきのをプッシュする。
ステップ3. 自動でCloud SQLとLaravelをデプロイTerraform初期化cd .cloud/terraform
terraform init
ついに初回デプロイexport REGION=asia-northeast1
export INSTANCE_NAME=laravel-app-service
terraform apply \
  -var "region=${REGION}" \
  -var "service=${SERVICE_NAME}" \
  -var "project=${PROJECT_ID}" \
  -var "instance_name=${INSTANCE_NAME}"
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.
  Enter a value:
yesと打てば、Secret Managerでキーを作り`laravel`というサービスアカウントができCloud SQLにMySQLをセットアップCloud RunでLaravelをデプロイの4つが行われる。
Cloud SQLの部分でかなり時間がかかる。
Apply complete! Resources: 26 added, 0 changed, 0 destroyed.
Outputs:
result =     The laravel-app-service is now running at https://<ここにURL>
    If you haven't deployed this service before, you will need to perform the initial database migrations:
    cd ../..
    gcloud builds submit --project <ここにプロジェクトID> --config .cloudbuild/build-migrate-deploy.yaml \
        --substitutions _APP_ENV=dev,_APP_DEBUG=true,_REGION=asia-northeast1,_INSTANCE_NAME=laravel-app-service,_SERVICE=laravel-app-service
    The username and password are stored in these secrets:
    gcloud secrets versions access latest --secret DATABASE_URL
    ✨
終わるとこんなんが出てくる。
Cloud Runの画面でもURLが確認できる。マイグレーションは後でやる。
GitHubと連動してビルド・デプロイさせる実はTerraformを使うのは初回だけでいい。あとはCloud Buildを活用しよう。
ルートにcloudbuild.yaml を作る。
cloudbuild.yamlsteps:
  - id: 'build_and_push'
    name: 'gcr.io/kaniko-project/executor:latest'
    args:
      [
        # https://github.com/GoogleContainerTools/kaniko/issues/1427
        "--dockerfile=.cloud/docker/php/Dockerfile",
        "--cache=true",
        "--cache-ttl=6h",
        "--destination=asia.gcr.io/${PROJECT_ID}/${_SERVICE}",
      ]
  
  - id: 'migrate'
    name: 'gcr.io/google-appengine/exec-wrapper'
    args: ['-i', 'asia.gcr.io/${PROJECT_ID}/${_SERVICE}',
           '-s', '${PROJECT_ID}:${_REGION}:${_INSTANCE_NAME}',
           '-e', 'PROJECT_ID=${PROJECT_ID}',
           '-e', 'APP_ENV=${_APP_ENV}',
           '-e', 'APP_DEBUG=${_APP_DEBUG}',
           '--', 'php', 'artisan', 'migrate']
  - id: 'deploy'
    name: 'gcr.io/cloud-builders/gcloud'
    args: ["run", "deploy", "${_SERVICE}",
           "--platform", "managed",
           "--region", "${_REGION}",
           "--image", "asia.gcr.io/$PROJECT_ID/${_SERVICE}"]
options:
  substitutionOption: ALLOW_LOOSE
substitutions:
  _APP_ENV: prod
  _APP_DEBUG: "false"
  _SERVICE: laravel-app-service
  _REGION: asia-northeast1
  _INSTANCE_NAME: laravel-app-service
ビルドのトリガー設定を変更(重要)
次に、Cloud Runのページを開く。
このボタンでビルドのトリガーを作れる。
GitHubのレポジトリと接続すればいいが、Dockerfile は間違いなので修正が必要。
「EDIT CONTINUOUS DEPLOYMENT」から設定を開き、LocationをRepository に変える。 Cloud Build configuration file (yaml or json) を選択する。/cloudbuild.yaml になっているか確認。ブランチ名を変えた場合は、適宜トリガーの設定も変える。
プッシュすればビルド、プッシュ、マイグレーション、デプロイがされる。こちらのリンクからビルド履歴が見れる。
初回は時間がかかるが...
2回目以降はKanikoがキャッシュを活用してくれる。大体二分で終わる。
え? FTPで差分をアップした方が早い? 確かにそうかもしれない。だが、全部自動化バージョンを戻せる環境を簡単に再現という点で明らかに効率がいい。積極的に活用していこう。
内容を見る