Phoenixでresourcesをネストさせるときにaliasも設定できる
普通にやるならこうする。
scope "/v1", MyApp do pipe_through :api resources "/users", UserController, only: [:index] do resources "/articles", ArticleController, only: [:index] end end
生成されるルーティングはこれ。
$ mix phx.routes user_path GET /v1/users MyApp.UserController :index user_article_path GET /v1/users/:user_id/articles MyApp.ArticleController :index
ただしこれだと/v1/articles
を入れたときに
scope "/v1", MyApp do pipe_through :api resources "/users", UserController, only: [:index] do resources "/articles", ArticleController, only: [:index] end resources "/articles", ArticleController, only: [:index] end
$ mix phx.routes user_path GET /v1/users MyApp.UserController :index user_article_path GET /v1/users/:user_id/articles MyApp.ArticleController :index article_path GET /v1/articles MyApp.ArticleController :index
というルーティングになるので、/v1/users/:user_id/articles
と同じコントローラを使うことになる。それでいいならOKだけど、別にしたいときはscope/2
でaliasを入れてあげる。
scope "/v1", MyApp do pipe_through :api resources "/users", UserController, only: [:index] do scope alias: User do resources "/articles", ArticleController, only: [:index] end end resources "/articles", ArticleController, only: [:index] end
$ mix phx.routes user_path GET /v1/users MyApp.UserController :index user_article_path GET /v1/users/:user_id/articles MyApp.User.ArticleController :index article_path GET /v1/articles MyApp.ArticleController :index
MyApp.User.ArticleController
とMyApp.ArticleController
に分けられる。ここまでは何も特別なことは言ってなくて、ドキュメントに書いてあるしわざわざブログ書くようなことじゃない。自分もこんな風にscope/2
使うのが普通と思っていたんだけど、最近うろ覚えで「こうだっけ?」って↓のように書いたら同じ動きをした。
scope "/v1", MyApp do pipe_through :api resources "/users", UserController, only: [:index], alias: User do # ここに alias resources "/articles", ArticleController, only: [:index] end resources "/articles", ArticleController, only: [:index] end
$ mix phx.routes user_path GET /v1/users MyApp.UserController :index user_article_path GET /v1/users/:user_id/articles MyApp.User.ArticleController :index article_path GET /v1/articles MyApp.ArticleController :index
ソースを追っていくとhttps://github.com/phoenixframework/phoenix/blob/v1.4.1/lib/phoenix/router/resource.ex#L32にたどり着いて、確かにresources
もalias
オプション受け入れてくれるんだなーって感じ。でもhttps://hexdocs.pm/phoenix/1.4.1/Phoenix.Router.html#resources/4には言及がなかったので、とりあえずプルリク投げといた。
PipenvとDocker Compose
最近Djangoのプロジェクトをつくっているのですが、docker-compose up
で立ち上がるようにしておこうとググったところ、なんかイマイチじゃない?ってのが多く出てきたので、自分なりのやつを書いておきます。
Dockerfile
FROM python:3.7-slim RUN apt-get update -qq ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 ENV PIPENV_VENV_IN_PROJECT 1 WORKDIR /app RUN pip install pipenv
docker-compose.yml
version: '3.7' services: db: image: postgres:11.1-alpine volumes: - postgresql:/var/lib/postgresql/data app: build: . command: pipenv run server volumes: - .:/app:cached ports: - 7000:7000 depends_on: - db volumes: postgresql:
Pipfile
[[source]] name = "pypi" url = "https://pypi.org/simple" verify_ssl = true [dev-packages] [packages] [requires] python_version = "3.7" [scripts] server = "python manage.py runserver 0.0.0.0:7000" test = "python manage.py test"
ポイントとしてはPIPENV_VENV_IN_PROJECT=1
にして、.venv
をDocker Composeのvolumesに入るようにするとこ。ググって出てきたイマイチっぽいやつは、Dockerfileの中でPipfileとPipfile.lockをCOPY
してpip install
をやっちゃうやつ。これだと新しい依存を追加するたびにbuildしないといけないんじゃないのって感じでお見送り。
ハチャメチャに速いサイトをつくりたい
ということを思って、もうだいぶ前だけどhttps://surisuri.ninjaっていうのをつくった。半端になってるページもあるけど、やりたいことはざっとできたので趣味としては満足。Herokuで動いてます。ソースはこちら。いま仕事で開発してるサービスの公開APIで本家をマネする企画がことの始まり。
やったこと
CDNのエッジキャッシュを使う
まず前提として、HTMLは全部CDNにキャッシュする。後述するように動的に変わるコンテンツはキャッシュに載せなかったので、そういうコンテンツは表示されなかったとしても、ヘッダとか静的な部分まで表示されず画面真っ白というのはハチャメチャに速いとは言えない。なのでオリジンのサーバはなるたけコンテンツを載せたtext/htmlを返すようにする。ほぼ空っぽのHTMLに<script>
がぽつんとあるSPAみたいにはしない。
APIとフロントを分割する
いわゆるモノリシックなWebアプリにはしなかった。JSON APIとフロントが分かれていたほうが個人的に好みだし、チームで開発する場合も適していると思うのでそうした。ただそうなるとtext/htmlをつくるのはフロント側になるので、SSRする仕組みが必要になる。SSRをサポートしてくれるフレームワークはNuxtとかNextとかこの頃興味を持っていたSvelteのSapperなどがあるけど、Nextはpathパラメータの扱いが苦手そう?に見えたのでやめて、Sapperは後述するaタグでの遷移ができなかったのでやめてNuxtを選んだ。仕事でもVueを使っていて手に馴染んでるのでちょうどよかった。
普通のaタグを使う
普通じゃないaタグって何だよって感じだけど、SPA的な画面遷移をしないということ。リンクをタップしてからAPI叩いて必要な情報が揃ってから画面書き換え、ということをするよりも、CDNのエッジキャッシュに載ってるHTMLを取ってくる方が速いんじゃねーのと考えた。もちろん代替案はいくつも考えられて、
など。前者はちょっと記憶が曖昧だけど、Vueのmounted
を使えば遷移してからAPIを叩けるけど、そのページをSSRしたときにコンテンツが載ってこない。だからと言ってNuxtのasyncData
やfetch
を使うと、こいつらは遷移前に呼ばれるので、リンクをタップしてから遷移するまで時間がかかってしまう。そんな具合にサーバサイドとクライアントサイド両方でいい感じに叩く手段がなかったので、サーバサイドに振り切ったということ。後者は無駄な通信が発生する可能性がありギガに優しくないなと思ったのでやめた。
その他
CSSはheadタグの中にstyleタグで書いてCSSOMの準備が早くできるようにするとか、assetなどURLと内容が対応してて変化しないものはService WorkerでNetwork Firstなキャッシュをしてオフライン対応するとか、いくつか考えていたけど、この辺はNuxtがやってくれちゃったので頑張った感じはない。詳しくはhttps://github.com/nuxt-community/pwa-moduleなど参照のこと。
やらなかったこと
動的なコンテンツのキャッシュはしなかった。CDNによってはクッキーの値をキャッシュキーに含めることができるらしいけど、こちらが意図していない、また把握できないキャッシュをつくられる可能性があるのでやめた。なのでユーザごとのコンテンツやユーザによって作成・更新されるものはブラウザからAPIを叩いて取得する。dev.toなんかはそういうコンテンツもキャッシュに載せていて、ユーザによって更新されたタイミングでCDNのキャッシュを消している。なので絶対無理ってわけでは無いのだけど、これはこれで大変なのでやらなかった。
まとめ
そんなところで、趣味レベルだけど一度キャッシュに載ってくれればそこそこ速いものはできた。ただ例えばトップは時間によって内容が変わるので3時間でキャッシュ消すようにしてるので、キャッシュ切れててHerokuのdynoが寝てるときの描画はかなり遅いし、やっぱりAPI叩くところはどうしてもモッサリするので、もうひと押しって感じですね。