Next.jsをCloudflare workersから配信したい! Part 2

はじめに

Part 1では workder.devにデプロイするところまでやったよね

murajun1978.hatenadiary.com

Part2ではproductionにデプロイして、KVを使ってみましょう

Productionデプロイ

productionにデプロイする前にやることがあります。

それはAレコードの登録です。

例えば、"www.murajun1978.dev"へのリクエストをworkersに託すって設定をしないといけない。

でも、こんなのは5秒で終わる(反映するまでに5分以上かかるときがあります)

CloudflareのDNSにAレコードを追加します!

こんな感じ

f:id:murajun1978:20200608235714p:plain

次にデプロイ設定をwrangler.tomlに追記します。

ここでちょっと気をつけてほしいこと

デプロイでローカル環境で実行することはほぼないですよね?だいたいはCIでデプロイします。

CIでデプロイするってことは、GitHubなんかにデプロイするってことですよね?

そこにAPI Tokenをコミットすると。。。ね。。。

はい、wranglerには環境変数が用意されています。こちらを使うのが推奨というか、使おうね

僕はDokcerを使ってるので↓のように設定しました

# docker-compose.yml

version: '3.8'
services:
  node:
    build: .
    volumes:
      - .:/home/app
    ports:
      - 3000:3000
    env_file:
      - wrangler.env
# wrangler.env
CF_API_TOKEN=<YOUR API TOKEN>
CF_ACCOUNT_ID=<YOUR ACCOUNT ID>
CF_ZONE_ID=<YOUR ZONE ID>

んで、gitignoreにwrangler.envを追加しときましょー

CIにも↑の環境変数を設定すればOKです。

これで安心!

さ、productionの設定を追記しよう

# wrangler.toml

name = "dev-site"
type = "webpack"
workers_dev = true
route = ""

[site]
bucket = "./out"
entry-point = "workers-site"

[env.production] # プロダクション用の設定
route = "www.murajun1978.dev/*"

routeを追加するだけです!

この設定で、www.murajun1978.dev/のすべてのリクエストがworkersに転送されます

Productionデプロイ

$ npx wrangler publish --env production

やばい、簡単すぎる。。。

Cloudflareで確認するとこんな感じ

f:id:murajun1978:20200609001043p:plain

f:id:murajun1978:20200609001129p:plain

これでProductionにデプロイできました。

ここでちょっとKVを見てみましょ

f:id:murajun1978:20200609001341p:plain

ファイル名がKEYになってて、値にはファイルのHTMLやJS、CSSなんかが格納されています。

デプロイ時に、KVにKEY ファイル名 値: HTMLやJSを格納して、Edge serverから参照できるようにしているってことですね。

なので、アクセスするたびに、workersのアクセスカウントが++されていきますw

そう、HTML、JS、CSSファイルにアクセスするたびにねw

金額はささいなものなので、気にはなりませんが(少なくとも僕は)、完全無料でブログを公開したい、もしくはもっと手抜きしたい場合は、NetlifyVercelを使うって選択になりそうですね.。

GitHub連携できるのも敷居が一気にさがります。

KVにcurrent versionを設定する

これ、やろうと思ったのですが、なんか微妙だったので僕は見送りました。

切り戻したいときはrevertすればいいかなと、とはいえKVに値をセットしてみましょ(とりあえず

まずは、KVのnamespaceを設定する必要があります

# wrangler.toml

kv-namespaces = [ 
  { binding = "CONTROL", id = <YOUR KV ID>}
]

[env.production]
route = <YOUR ROUTE>

kv-namespaces = [ 
  { binding = "CONTROL", id = <YOUR PRODUCTION KV ID>}
]

KV IDってのは↓です

f:id:murajun1978:20200609003540p:plain

んじゃ、KVにセットしてみましょう

$ npx wrangler kv:key put --binding=CONTROL "putting test" "test"

f:id:murajun1978:20200609004341p:plain

はい、登録できましたね!

まとめ

Cloudflare workersを2記事にまとめてみました。

マジで簡単。

ちなみに、悶絶雑なページをlighthouseで計測してみました!

f:id:murajun1978:20200609005844p:plain

Next.js + Preactなら楽勝で100Pointsですよw

近々、ブログもこちらに移行する予定です

おそらく、Vercelを使うことになると思います。

次はCloudflareのImage Resizingを使ってみようかなー

サイトはVercelから配信して、画像はS3やCloud Storageにアップロードしたものを、Cloudflareから配信するって感じです。

pre-renderするときに、画質を落としてぼかした画像を表示しておいて、裏でダウンロードが完了したら、ちゃんとした画像に差し替えるって感じですかね。(Mediumと同じやつです

こうご期待w

Enjoy Cloudflare workers ヘ(^o^)ノ

Next.jsをCloudflare workersから配信したい! Part 1

みなさま、Cloudflare workersってご存じですか?

workers.cloudflare.com

90カ国 200都市のデータセンターで動いてるEdge serverです。

service workerのEdge server版ってとこです。

S3やCloud StorageのファイルをCDN経由で配信?

ま、それも悪くないですが、Edge serverでパッと処理してシュッと返してあげましょ!S3もCloud Storageも不要です(ヤッタ

Edge serverだから、クライアントの一番近いところで動いているので、爆速でHTMLをreponseを返すことができます。

キャッシュコントロールも可能なので、さらに爆速!Preactで更にドン!

そんな誘惑にこころ動かされたので試してみます。

注意!Workers Sitesを使うので、Cloudflare workersの有料プランである必要があります💰($5/月)

ではやってみましょう💨

Next.js

$ npx create-next-app

以上

wrangler

Cloudflare workersのCLIです。

github.com

インストール

$ npm install -D @cloudflare/wrangler

初期設定

CloudflareのアカウントIDとゾーンIDとAPI Tokenが必要です。

アカウントIDとゾーンIDは、Cloudflareのダッシュボードに表示されてるのでメモ。

API Tokenはworkers用のトークンを作成します。

こちらもダッシュボードに"API トークンを取得"ってリンクがあるので、そこからAPIトークンを作成します。

$ npx wrangler config

Enter API Token:

さきほど作成したAPI Tokenを入力して終了でっす。

Siteの作成

workers siteを作成します。

$ npx wrangler init --site hello-next-worker

workers-siteってディレクトリとwrangler.tomlが作成されたはずです。

name = "hello-next-worker"
type = "webpack"
account_id = <あなたのアカウトID>
workers_dev = true
route = ""
zone_id = <あなたのゾーンID>

[site]
bucket = "./out"
entry-point = "workers-site"

account_id, zoon_idを設定します。

bucketにはNext.jsのbuildディレクトリを指定します。

これでwranglerの設定は完了です。簡単!

ビルド

Next.jsでbuildします📦

$ npx next build
$ npx next export

Publish

workers devにpublishします。ProductionへのpublishはPart 2で。

$ npx wrangler publish

これで、https://hello-next-worker.<あなたのドメイン>.workers.dev にpublishできるはずです🎉

まとめ

以上で、Next.jsのbuildしたファイルをCloudflare workers siteにpublishして、Edge serverから配信することができました!

どうでしょう?簡単でしたよね?

Part 2では、ProductionへのpublishとKVを使って、前のバージョンに切り戻したりしてみます。

Enjoy Cloudflare workers ヘ(^o^)ノ

Traefikでdocker-composeのポート重複を解決する

昨日のDockerCon LiveのセッションでTraefikの存在を知ってしまったので、ちょっと試してみました😄

github.com

同一プロジェクト、または複数のプロジェクト間でportがかぶってちょっとずつ変えているなどなど

例) App1では3000ポートをApp2でも3000ポートを使っている

僕もいままでは、App2を4000ポートに変更して対応していました

"Simplify All the Things with Docker Compose" @mikesir87 さんのセッションを見て面白かったので試してみます

Thanks a lot, mikesir87👍

docker.events.cube365.net

github.com

僕のサンプルアプリ

github.com

ちょっと調子のってDenoで書いてみましたw

1つのプロジェクトにApp1とApp2のサーバが3000ポートでサーバが立ってます

これをTraefikを使ってProxyしてみます

サンプルコードをみてもらったら、一目瞭然なので詳しい説明はしませんが(しないのかよ!)

ハマったこと

僕がちょっとハマったことを書いておきます

僕の開発環境はLinux + Rootless dockerです

Rootless dockerがちょっとハマりました。。。

公式ドキュメントのサンプルとかでは、80番ポートでProxyしてるのですが、Rootless dockerはrootユーザで起動してないので、 localhostの80番ポートが使えなかったです😭

なので、僕のサンプルコードでは3000番ポートをリッスンするようにしてます

あとは、socketファイルのパスが違うことですね、DOCKER_HOSTを確認してくださいね

$ echo $DOCKER_HOST
unix:///run/user/1000/docker.sock

動作確認

コンテナを起動します

$ docker-compose up -d

app1.localhost:3000にアクセスしてみます

f:id:murajun1978:20200529154714p:plain

app2.localhost:3000にもアクセスしてみます

f:id:murajun1978:20200529154805p:plain

まとめ

どうでしょうか、めちゃ簡単にProxyできましたね👏

同一プロジェクトでは旨味はありませんが、プロジェクトをまたがる場合は重宝しそうです

プロジェクトをまたぐ場合は、Traefikをdeamonで動作させて、Traefikのnetworkをexportしておけばいいと思います

各プロジェクトでは、このネットワークに接続することで、Proxyできるようになります

もうポートを気にする必要はない!

Enjoy Traefik ヘ(^o^)ノ

Next.jsのPreview modeでページのプレビューを表示する

Next.js v9.3でpreview modeが追加されました

CMSっぽいの作ろうと思ってるので、ちょっと試してみました

めちゃシンプルで簡単にPreviewできたのでメモ

ドキュメントは↓ nextjs.org

サンプルコードは↓ github.com

Preview modeのAPIを用意する

ヘッドレスCMDなどからプレビューページを表示するためのエンドポイントを作成します

ぼくのサンプルコードではこんな感じ

// /pages/api/preview.ts

export default (req, res) => {
  const id = req.query.id;

  res.setPreviewData({});
  res.writeHead(307, { Location: `/posts/${id}` });
  res.end();
};

クエリパラメータのidで対象のURLへリダイレクトしているだけです

setPreviewDataには任意のデータを渡せますが、cookieで保持しているのでサイズには上限があります

previewDataのリミットは2KBだそうです

Pagesでpreview mdoeの振る舞いを記述する

Preview modeだとgetStaticPropsのpropsにpreview: truepreviewDataL {}が渡ってきます

ぼくのサンプルコードではpreviewをcomponentに渡してラベルを表示するようにしました

// /pages/posts/[id].tsx

import { useRouter } from 'next/router';

export const getStaticPaths = async () => {
  return {
    paths: [{ params: { id: '1' } }],
    fallback: true,
  };
};

export const getStaticProps = async ({ params, preview }) => {
  const isPreview = preview || false;
  return { props: { id: params.id, previewMode: isPreview } };
};

const Post = ({ id, previewMode }) => {
  const router = useRouter();

  if (router.isFallback) {
    return <div>Loading...</div>;
  }

  return (
    <>
      {previewMode ? 'Preview mode' : ''}
      <h1>{`Page's ID is ${id}`}</h1>
    </>
  );
};

export default Post;

これだけです!

http://localhost:3000/api/preview?id=1にアクセスすると/pages/1にリダイレクトされて、Preview modeのラベルが表示されるはずです

f:id:murajun1978:20200525020216p:plain

Previewモードを解除する

Preview modeを解除するには、cookieをクリアしてあげればOKです

解除用のエンドポイントを作成します

// /pages/api/preview-disable.ts

export default (req, res) => {
  const location = req.headers.referer || '/';

  res.clearPreviewData();
  res.writeHead(307, { Location: location });
  res.end();
};

pageにも解除用のリンクを追加します

// /pages/posts/[id].tsx

import { useRouter } from 'next/router';

export const getStaticPaths = async () => {
  return {
    paths: [{ params: { id: '1' } }],
    fallback: true,
  };
};

export const getStaticProps = async ({ params, preview }) => {
  const isPreview = preview || false;
  return { props: { id: params.id, previewMode: isPreview } };
};

const Post = ({ id, previewMode }) => {
  const router = useRouter();

  if (router.isFallback) {
    return <div>Loading...</div>;
  }

  return (
    <>
      {previewMode ? 'Preview mode' : ''}
      <h1>{`Page's ID is ${id}`}</h1>
      {previewMode ? (
        <a href="/api/preview-disable">Disable preview mode</a>
      ) : null} // リンクを追加
    </>
  );
};

export default Post;

f:id:murajun1978:20200525020707p:plain

このリンクをクリックすると、posts/1にリダイレクトしてPreview modeのラベルやリンクが表示されないはずです

まとめ

すごくシンプルで簡単にPreviewを実装できました

実際の運用では、previewのデータを取得する実装が必要になりますね

公開されているデータを取得するclinetとPreviewのデータを取得するclientを作って、getStaticPropsで切り替えるとかね

Enjoy Next.js ヘ(^o^)ノ

Next.jsでTailwind CSSとEmotionを使う

Next.jsでTailwind CSSとEmotionを使える環境を作ってみます

$ npm init next-app --example with-tailwindcss-emotion [アプリケーション名]

create-next-appを使って、exampleをダウンロードするだけでOKです

Exampleのリポジトリ

github.com

これだけだとあっけないので、Tips的なものを1つ

[ご注意] ここからは↑で作成したexampleを前提に書いてます。他の環境では動作しないかもです

こんなコンポーネントがあったとします

/** @jsx jsx */
import { jsx } from '@emotion/react'
import tw from '@tailwindcssinjs/macro'

const styles = {
  button: tw`
    relative
  `
}

const Button = () => <button css={styles.button}>button</button>
export default Button

JSX Pragmaなるもの(/** @jsx jsx */)とimport { jsx } from '@emotion/react'を毎回かかないといけません。。。

しかも、ESLintにjsxno-unused-varsだと怒られたりもします。。。

そこで、@emotion/babel-preset-css-propですよ

emotion.sh

@emotion/babel-preset-css-propは、Emotion用パーサーのbabelプラグインです

導入手順

  • Installation
    • @emotion/core
      • @emotion/babel-preset-css-propには@emotion/coreが必要なのでインストール
  $ npm install @emotion/core
  
  or 
  
  $ yarn add @emotion/core
  • @emotion/babel-preset-css-prop
  $ npm install @emotion/babel-preset-css-prop
  
  or 
  
  $ yarn add @emotion/babel-preset-css-prop
  • Modify babel config
{
  "presets": [
    "next/babel",
    "@emotion/babel-preset-css-prop" # この行を追加
  ],
  "plugins": [
    "macros",
    "@emotion/babel-plugin"
  ]
}

これで ↓ の2行が不要になります

/** @jsx jsx */
import { jsx } from '@emotion/react'

やったね!

あ、TypeScriptだとcssってpropsの型しらねーって怒られるので、型定義も追加する必要があります

# emotion-env.d.ts (ファイル名はなんでもOK)
/// <reference types="@emotion/core"/>

Happy Tailwind CSS and Emotion ヘ(^o^)ノ

Railsのmodelでprivateメソッドのロジックをテストしない

みなさんはRailsのモデルでprivateメソッドをテストしますか?

僕はしません。

理由はpublicメソッドではないからです。

Privateメソッドをテストするって理由の一つが、「privateメソッドのロジックをテストしたい」だと思います。

ふーん、でもそのロジックはそのモデルにあるべきなんでしょうか?そして、そのモデルでテストするべきなのでしょうか?

他のモデルで同じロジックを使いたいときはどうするのでしょうか?

Railsにはconcernという機能がありますが、僕は基本使わなくなりました。

なせ?単純にいらないからですw

嘘です、必要です。そもそもconcernの存在意義が、ロジックを共通化するものではないというだけです。

では、どうするのか?

そのロジック用のモデルを作成することで解決できます。

具体的にやってみましょう。

Humanモデルにロジックを書いてみる

class Creature
  include ActiveModel::Model
  include AcitveMode::Attributes

  attribute :name, :string
end

class Human < Creature
  private

  def hello
    "Hello #{name}!"
  end

  def hi
    "Hi #{name}!"
  end
end

hellohi メソッドをテストしたいよね?

でも、privateメソッド、、

ならば!

Greetモデルを作成してみる

class Human < Creature
  private

  def greet
    @greet ||= Greet.new(creature: self)
  end
end
class Greet
  include ActiveModel::Model
  include AcitveMode::Attributes

  attribute :creature

  def hello
    "Hello #{creature&.name}!"
  end

  def hi
    "Hi #{creature&.name}!"
  end
end

どうでしょうか?

これでprivateメソッドのテストは不要になりますね。(Greetモデルのテストで担保するため)

さらに移譲してみましょう。

class Human < Creature
  delegate :hello, :hi, to: :greet

  private

  def greet
    @greet ||= Greet.new(creature: self)
  end
end
irb>human = Human.new(name: 'murajun1978')
irb>human.hello #=> "Hello murajun1978!"
irb>human.hi #=> "Hi murajun1978!"

みなさんも、新しいロジックがひょこっと出てきたら、新しいモデルを作成しましょう。

そのモデルをテストすることで品質も担保できますしね。

みなさんも、Railsでよきオブジェクト指向を満喫してください。

Apollo serverのSchema directivesを使ってdeprecation warningを表示する

Apollo serverでリファクタリングや仕様変更などで、特定のフィールドをdeprecatedにしたい場合があります

Apollo serverの @deprecated を使えば簡単にdeprecation warningを表示できます

では、やってみましょう

// src/index.js
const { ApolloServer, gql } = require("apollo-server");

const typeDefs = gql`
  type Author {
    name: String
  }

  type Book {
    title: String
    author: Author
    authorName: String
  }

  type Query {
    books: [Book]
  }
`;

const resolvers = {};
const server = new ApolloServer({
  typeDefs,
  mocks: true
});

server.listen(4001).then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

BookのauthorNameをdeprecatedにしたいので、deprecation warningを表示したいと思います

type Book {
  title: String
  author: Author
  authorName: String
    @deprecated(
      reason: "\`authorName\` is deprecated. Use \`author\` instead."
    )
}

これだけです!超簡単!

Playgroundで確認してみましょう!

f:id:murajun1978:20200311235454p:plain

DOCSもみてみましょう

f:id:murajun1978:20200311235619p:plain

ちゃんと表示されてますね!

schemaDirectivesは自分で定義することもできるので、特定のフィールドを表示するかを認可で切り替えとかもできますね

Happy GraphQL ヘ(^o^)ノ