Next.jsをCloudflare workersから配信したい! Part 2
はじめに
Part 1では workder.devにデプロイするところまでやったよね
Part2ではproductionにデプロイして、KVを使ってみましょう
Productionデプロイ
productionにデプロイする前にやることがあります。
それはAレコードの登録です。
例えば、"www.murajun1978.dev"へのリクエストをworkersに託すって設定をしないといけない。
でも、こんなのは5秒で終わる(反映するまでに5分以上かかるときがあります)
CloudflareのDNSにAレコードを追加します!
こんな感じ
次にデプロイ設定を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で確認するとこんな感じ
これでProductionにデプロイできました。
ここでちょっとKVを見てみましょ
ファイル名がKEYになってて、値にはファイルのHTMLやJS、CSSなんかが格納されています。
デプロイ時に、KVにKEY ファイル名 値: HTMLやJSを格納して、Edge serverから参照できるようにしているってことですね。
なので、アクセスするたびに、workersのアクセスカウントが++されていきますw
そう、HTML、JS、CSSファイルにアクセスするたびにねw
金額はささいなものなので、気にはなりませんが(少なくとも僕は)、完全無料でブログを公開したい、もしくはもっと手抜きしたい場合は、NetlifyやVercelを使うって選択になりそうですね.。
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ってのは↓です
んじゃ、KVにセットしてみましょう
$ npx wrangler kv:key put --binding=CONTROL "putting test" "test"
はい、登録できましたね!
まとめ
Cloudflare workersを2記事にまとめてみました。
マジで簡単。
ちなみに、悶絶雑なページをlighthouseで計測してみました!
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ってご存じですか?
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です。
インストール
$ 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の存在を知ってしまったので、ちょっと試してみました😄
同一プロジェクト、または複数のプロジェクト間でportがかぶってちょっとずつ変えているなどなど
例) App1では3000ポートをApp2でも3000ポートを使っている
僕もいままでは、App2を4000ポートに変更して対応していました
"Simplify All the Things with Docker Compose" @mikesir87 さんのセッションを見て面白かったので試してみます
Thanks a lot, mikesir87👍
僕のサンプルアプリ
ちょっと調子のって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にアクセスしてみます
app2.localhost:3000にもアクセスしてみます
まとめ
どうでしょうか、めちゃ簡単に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: true
とpreviewDataL {}
が渡ってきます
ぼくのサンプルコードでは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のラベルが表示されるはずです
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;
このリンクをクリックすると、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のリポジトリ
これだけだとあっけないので、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にjsx
がno-unused-vars
だと怒られたりもします。。。
そこで、@emotion/babel-preset-css-prop
ですよ
@emotion/babel-preset-css-prop
は、Emotion用パーサーのbabelプラグインです
導入手順
- Installation
- @emotion/core
@emotion/babel-preset-css-prop
には@emotion/core
が必要なのでインストール
- @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
hello
と hi
メソッドをテストしたいよね?
でも、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!"
みなさんも、新しいロジックがひょこっと出てきたら、新しいモデルを作成しましょう。
そのモデルをテストすることで品質も担保できますしね。
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で確認してみましょう!
DOCSもみてみましょう
ちゃんと表示されてますね!
schemaDirectivesは自分で定義することもできるので、特定のフィールドを表示するかを認可で切り替えとかもできますね
Happy GraphQL ヘ(^o^)ノ