dev.toコードリーディング会に参加した
行ってきました。十数人で各々が好きなところから読み始めて、最後に見所や気づきを喋る流れでした。initializersを読んでいく人やモデル中心に見ていく人など様々。近日中に全員のメモが公開されるとのことですので、そのときはリンク追記します。会場を提供してくださったSmartHRさんありがとうございました。
先週金曜の dev\.to コードリーディング会に参加した皆さんのメモはこちらに公開されてますhttps://t.co/uipqG1pYxu
— ぷりんたい (@spacepro_be) 2018年8月13日
追記ここまで
以下は自分のメモ。やはりdev.toと言えば爆速なので、CSSをheadタグに埋め込んでいたり、キャッシュってどうやってんだろうなーといったところを中心に読みました。最後の所感にも書きましたが、爆速サイトをつくるためのイケてる最強プラクティスが詰まったコードを期待していた割に、泥臭さ100%のコードやら結構ひどいメソッドたちやらが溢れていて、歴史に勝てるものはおらんのじゃ...という気持ちになりました。users
テーブル辺りを見ると共感が得られると思います。やはり銀の弾丸は無く、歯を食いしばってひとつずつ問題をなぎ倒していくしか道は無いのです...。
なお、ハイライトは http://sushi.to というサイトが発見された瞬間でした。
headタグへのCSS埋め込み
render "layouts/styles"
- ページに必要なCSSをひたすら
.to_s.safe_html
- キャッシュ条件が結構ながい
- ここで
<% Rails.application.config.assets.compile = true %>
- 思っていたより泥臭いし、メンテできる気がしないけどどうなのか
- 大多数のページは
stylesheet_link_tag application
なので意外と大丈夫なのかも
- 大多数のページは
core_page?
で最小限のcssを読むか判定している- このcssはService Workerがはじめにキャッシュしている
- ページに必要なCSSをひたすら
Service Worker
- serviceworker-rails使ってる
- service-worker.js.erb
- serviceworker-companion.js
- 割と普通
- Homeにインストールの結果をGAに送っていた
どうやってCDNキャッシュしてるか
- bodyのdata属性にユーザ情報を埋め込んでいるが、これを非同期にやっているのでCDNにキャッシュできる
function initializePage(){ initializeLocalStorageRender(); initializeStylesheetAppend(); initializeFetchFollowedArticles(); callInitalizers(); } function callInitalizers(){ initializeLocalStorageRender(); initializeBodyData();
initializeBodyData
で/async_info/base_data
を叩いてbodyのdataに入れてる。入れたらlocalStorageにも入れてる。次回からはlocalStoreageから読み出してbodyのdataに入れてる。getUserData()
は15秒間待ってる。
function mouseoverListener(e) { if ($lastTouchTimestamp > (+new Date - 500)) { return // Otherwise, click doesn't fire } var a = getLinkTarget(e.target) if (!a || !isPreloadable(a)) { return } a.addEventListener('mouseout', mouseoutListener) if (!$delayBeforePreload) { preload(a.href) } else { $urlToPreload = a.href $preloadTimer = setTimeout(preload, $delayBeforePreload) } getImageForLink(a); }
マウスホバーでの先読みしてる。スマホはtouchstartListener
。
所感
CSS埋め込みとか、CDNエッジキャッシュするための工夫とか、もうちょっと高度な謎テクノロジーであのパフォーマンスを実現していると思っていたけど、読んでみたら想像を絶する泥臭さだった。総じて、この量のjsをテストほとんど無しで本当に書いたの...という気持ち。
PhoenixでPage Specific JavaScript
と言ってもすでにインタネットに情報があります。
まずはviewモジュール名(@view_module
)とテンプレート名(@view_template
)からjsファイルのパスが導出できるように取り決めしておき、bodyタグのdata属性にそのパスを吐き出しておく。で、DOMContentLoaded
イベントでそのパスを読み、webpackのrequire.context
でガバっと読み込んだ中から実行するものを決める、という寸法。
ただこのページにあるコードではjsのファイル名がイマイチで、もうちょい良くなりそう。
def unique_view_name(view_module, view_template) do [_elixir, _app | context_controller_template] = view_module |> Phoenix.Naming.humanize() |> String.split(".") [action, "html"] = view_template |> String.split(".") Enum.join(context_controller_template ++ [action], "_") end
例えばUserController
のshow
アクションとかなら、userview_show
になるし、Admin.UserController
のindex
アクションとかなら、admin_userview_index
になる。ただしこれだとjsファイルを同じディレクトリに全部並べなくてはならず、Elixirのコードと同じような階層構造をつくって整理できない。また、jsのファイル名はkebab-caseかCamelCaseがおそらく一般的で、少なくとも_
区切りは好まれない。あとuserview
のようにview
が全部に付くのもどうにかしたい。
ということで書いてみたのが以下。
def js_module_path(view_module, view_template) do [_elixir, _my_app_web | context_view] = view_module |> Atom.to_string() |> String.split(".") context_names = context_view |> List.delete_at(-1) resource_name = view_module |> Phoenix.Naming.resource_name("View") template_name = view_template |> String.replace_suffix(".html", "") (context_names ++ [resource_name, template_name]) |> Enum.map(&(&1 |> Phoenix.Naming.underscore |> String.replace("_", "-"))) |> Enum.join("/") end
Phoenix.Naming
便利。これならマックス意地悪な入力を与えても
js_module_path( MyAppWeb.GreatShop.TopSeller.FancyItemView, "member_only_index.html" ) #=> "great-shop/top-seller/fancy-item/member-only-index"
という出力になっていい感じ。
ちなみに、これまでPhoenixのモジュールバンドラはbrunchがデフォルトでしたが、v1.4.0からwebpackになります。
全部書いておく。
<!-- app.html.eex --> <html> ... <body data-js-path="<%= js_module_path(@view_module, @view_template) %>"> ... </body> </html>
# lib/my_app_web/views/layout_view.ex defmodule MyAppWeb.LayoutView do use MyAppWeb, :view def js_module_path(view_module, view_template) do [_elixir, _my_app_web | context_view] = view_module |> Atom.to_string() |> String.split(".") context_names = context_view |> List.delete_at(-1) resource_name = view_module |> Phoenix.Naming.resource_name("View") template_name = view_template |> String.replace_suffix(".html", "") (context_names ++ [resource_name, template_name]) |> Enum.map(&(&1 |> Phoenix.Naming.underscore |> String.replace("_", "-"))) |> Enum.join("/") end end
// assets/js/app.js import css from "../css/app.css" import "phoenix_html" import pageScript from './pages/index' window.addEventListener("DOMContentLoaded", () => { pageScript() })
// assets/js/pages/index.js const requireContext = require.context(".", true, /\.js$/) const modules = {} requireContext .keys() .filter(filename => filename !== './index.js') .forEach(filename => { const path = filename.replace('./', '').replace('.js', '') modules[path] = requireContext(filename) }) export default function() { const path = document.querySelector('body').dataset.jsPath const fun = modules[path] if (fun && fun.default) { fun.default() } }
GenStageのcastやcallもdispatchするんですね
以前ふむふむ眺めただけだったので触ってみたら微ハマりした。
defmodule Producer do use GenStage ... def enqueue(item) do GenStage.cast(__MODULE__, {:enqueue, item}) end def handle_cast({:enqueue, item}, state) do {:noreply, [item], state} end end
こういうとき、Producer.enqueue(:something)
するとconsumerへ1つイベントが流れていく。
こうやって見ると普通だけど、consumerへイベントを流す指示ができるのはhandle_demand
だと思いこんでいて、最初は↓の実装をしてた。
defmodule Producer do use GenStage ... def enqueue(item) do GenStage.cast(__MODULE__, {:enqueue, item}) end def handle_cast({:enqueue, item}, queue) do {:noreply, [], :queue.in(item, queue)} end def handle_demand(demand, queue) do case :queue.out(queue) do {:empty, queue} -> {:noreply, [], queue} {{:value, item}, queue} -> {:noreply, [item], queue} end end