ジンジャー研究室

長めのつぶやき。難しいことは書きません。

Netlify Functions + FaunaDB 使ってみた

個人開発でサクッと何か作りたいとき、 Heroku みたいに手軽に「git push はいリリース」なノリのやつがあると便利なんだけど Heroku は無料だと半日寝てるし東京に居ないしアドオンも少しまともに使うと値段が跳ね上がる。ので、なんか良い代替がないかなと探していたら、 Netlify Functions というものを見つけた。静的コンテンツなら Netlify が便利なのは言うまでもないとして、サーバーサイドも何か書きたいときにはこれを使って AWS Lambda を動かせると言うことらしい。従量課金でお金も節約できそう?というわけで、やってみた。

以下、「何も情報がないよりはマシだろう」程度のメモ。日記なので技術記事だと思って読まないほうがいいです。

Netlify Functions + FaunaDB

Netlify Functions 自体についての解説はググれば Qiita やら何やら出てくるので、そっちを当たってください。説明するの面倒臭いです。

せっかくなので裏に DB を置きたいんけど、 Lambda だからって裏を DynamoDB にすると結局 AWS に浸かることになってしまい面白くないので、もう少し調べてみるとこういう記事が出てきた。 www.netlify.com

FaunaDB というのは初めて聞いたけど、スケーラブルなドキュメント DB らしく「World's best serverless database, now with native GraphQL」っていう売り文句で、ちょっと面白いのでやってみるかと(結局 GraphQL 使ってないけど)。

作ったもの

github.com

REST API (Webhook) のエンドポイントを動的に追加して気軽にテストできるやつです(前回 Heroku で作っていたやつのマイグレーション)。メールのテストに使える MailSlurp というサービスがあって、それのパクリ。

感想・ハマったポイントなど

正直 Netlify でこんなにハマるとは予想してなかったというか、分かったら簡単なんだけど上手くレールに乗らないと死ぬ。ローカルテストとかスムーズに開発するために netlify-lambda というツールを使うんだけど、 TypeScript でやろうとすると結構ハマりポイントが多い(願わくば TypeScript デフォルトにして欲しい)。

babel 設定

ここ に書いてある通り .babelrc が必要。 TypeScript on Node で babel が必要なのかは疑問だが、内部的に使っているようなので仕方がない。で、 async/await を何の気なしに使っていると babel がエラーを吐くので、フロントエンドだと @babel/polifill を入れて解決するところだけど、今回は Node.js 8 以降で動かすのでターゲットの方を変える

webpack 設定

ここで webpack が登場するのは、 Lambda に zip を送信する都合上、必要な依存だけを node_module からかき集めるのが都合が良いという話だと思う。いつものフロントエンドじゃないか助けてくれ。

デバッグしてたらいきなり i is not a function と出るので(sourcemap どうすれば利くの)ここ に従って webpack.config を書くことでまずは minify を解除。すると require is not a function になるのでどうしたものかと調べると同じハマり方をしてる人がいて、早い話がこの Issue の UPDATE2 を書くと解決する。

Netlify Dev

netlify-lambda の後発として netlify dev という netlify-cli のコマンドがあって、バンドルはしないけど必要な依存は解決してかき集めてくれるらしい。というのを後から知って「じゃあ netlify-lambda もう要らないのか」と思ったんだけど、どうも TypeScript とかが必要な時は結局 netlify-lambda が必要らしきことが README に書いてある。うーん、なんかもうちょっと綺麗にまとめてくれないものだろうかと思うものの、 netlify dev はまだベータなので改善を待ちたい。

Express

実は Express が使える。FaaS と言えば基本的に1エンドポイントにつき1関数だと思うけど、全てのリクエストを1関数に集約すると、その中でルーティングできるようになる。それでいいのかよという気もするが、そんなに細かいチューニングがしたいわけでもないし、 Express でサクサク書きたい気持ちが上回ってしまうのだから仕方がない。

詳しくはこの記事に書いてある通り、 serverless-http というライブラリを使うとサクッと Lambda と express の辻褄を合わせてくれる。

Decoder

デコーダーと言えば Elm のあれなんだけど、 TypeScript でも同じようなことができるのでやってみた。何かと言うと any をバリデーションして型をつけてくれる。例えば下のように Decoder<User> を作って userDecoder.run(value) すると、成功時には anyUser になって返ってくるし、失敗時には例外が発生する。あとは express と組み合わせて 400 を返せば OK 。

export const userDecoder: Decoder<User> = object({
  age: number,
  name: string
});

使い方はこのリポジトリがわかりやすいと思う。まだ PoC なので実装は適当だけど、最低限は使えるはず。実務だと Swagger から JSON Schema を引っ張り出して ajv とかでバリデーションするんだけど、まあ軽い用途だとこのくらいで良いよねという感じ。

FaunaDB

https://fauna.com/ からサインアップしてドキュメントを読むと大体雰囲気がわかる。ダッシュボードはかなり使いやすい方だと思う。 GraphQL も使えるけど機能に制限があるらしいので、今回は FQL (Fauna Query Language) というものを使った。FQL はかなり癖が強くとっつきにくいが、慣れてしまえば API ドキュメントを読み読みしながら書けるようになる。以下は TypeScript の SDK を使って書いてみた様子。

q.Select(
  "data",
  q.Filter(
    q.Map(
      q.Paginate(q.Match("results_by_key_order_by_requestedAt", key), {
        size: 100000
      }),
      q.Lambda(["_", "ref"], q.Select("data", q.Get(q.Var("ref"))))
    ),
    q.Lambda("x", q.GTE(q.Select("requestedAt", q.Var("x")), from || 0))
  )
)

正直これ AST を直書きしてるのと変わらないので、SQL みたいに文字で書きたくなってくる。とは言え、これはこれで composable なので上手くヘルパーを使えば楽になるかもしれない。一応、型もついてる。

あと、情報源がほぼ公式のドキュメントしかなくて、そこで理解できないとたちまち詰む。まあでもマイナー技術ってそんなものというか苦労するからこそ達成感を感じたりするし、作者も高確率でエゴサしてるので Twitter でちょっと呟くと飛びついてくるのが面白い(前例: Elm, ArangoDB, MailSlurp, etc.)。

全体としての感想

  • TypeScript で書こうとすると webpack や babel が絡んで難易度が上がる。TypeScript の開発がもう少しスムーズになるようにサポートして欲しい。その分だけ Heroku より手軽ではないが、 AWS を直に触るよりはかなり楽だと感じる。
  • FaunaDB は今後どうなるか未知数だが、 Netlify Functions と親和性は良さそうなので個人的にはもう少し使いたい。今のところ無料だし。
  • 手軽デプロイ系だと他には aws-cdk とか Firebase とかも気になってる。あと Netlify Edge 。

GW 進捗

割と捗った方だと思います。

4/27

  • DreamNotes
    • ScriptProcessorNode でノイズを再実装
    • 矩形選択を実装
    • ピアノロールのルーラーを実装
  • Starry Sky (Cupsule)

4/28

4/29

  • DreamNotes
    • プレイヤー実装
    • 「時の回廊」公開
  • リアル脱出ゲーム
    • 6/9 部屋
  • 海外旅行の計画
  • ゆるゆり 16 巻

4/30

  • DreamNotes
    • プリセットに organ1, organ2, base3, wind 追加
    • Octave パラメータ追加
  • 「FMシンセの新しいトリセツ」読了

5/1

  • DreamNotes
    • OSC / Fine 実装
    • FM / Detune 実装
    • プレーヤーの iOS / Safari 対応

5/2

  • DreamNotes
    • ノートのドラッグ実装
    • ノートのリサイズ実装
  • 複雑GUI

5/3

  • DreamNotes
    • アナライザー実装
    • パフォーマンス改善

5/4

5/5

5/6

  • DreamNotes
    • インサーションエフェクトの追加・削除
    • Distortion 実装
  • ドラム体験レッスン
  • シンセサイザーがわかる本」読了

GitHub Actions で Puppeteer をインストールして実行

分かってみれば全く大したことはない...けど誰かの役にたつかもしれないので足跡を残しておく。

.github/main.workflow

ワークフロー定義。カスタム定義の Action を使う(作り方はこの辺)。 UI では以下が上から串刺しになって見える。

workflow "Build and Test" {
  on = "push"
  resolves = ["Test"]
}

action "Install" {
  uses = "./puppeteer"
  runs = "npm"
  args = "install"
}

action "Test" {
  uses = "./puppeteer"
  needs = ["Install"]
  runs = "npm"
  args = "test"
}

コンテナは Action ごとに作られるけど、前の結果は引き継がれているように見える。

puppeteer/Dockerfile

Puppeteer を docker で使うためのテンプレの前半をコピペ。 同じディレクトリに entrypoint.sh を定義してもいいけど任意っぽい。

# https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-in-docker
FROM node:10

RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \
    --no-install-recommends \
    && rm -rf /var/lib/apt/lists/* \
    && rm -rf /src/*.deb

Puppeteer を root で使う時に --no-sandbox フラグが必要になるので、元ネタだとこの続きで USER を使っているんだけど、 GitHub Actions の制約で USER が使えないのでその部分は諦める。

index.js

仕方がないから CI の時だけ --no-sandbox つけようかということで、適当な環境変数を探す。

// https://developer.github.com/actions/creating-github-actions/accessing-the-runtime-environment/#environment-variables
const ci = !!process.env.GITHUB_ACTION;

// https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#setting-up-chrome-linux-sandbox
const options = ci
  ? { args: ["--no-sandbox", "--disable-setuid-sandbox"] }
  : {};
const browser = await puppeteer.launch(options);

おしまい。

CSS フレームワークを使いたくない

CSS フレームワークが辛い。

ここでいう CSS フレームワークとは Bootstrap とか Bulma とかそういうやつのことである。昔から自分はこういうのが苦手で、一定の便利さは感じつつもどうしても馴染めないという状態が続いていて、それでも「それは使い方が悪いだけで、ちゃんと使いこなせばペイするんだろう」と思って今までズルズル使ってきてしまったのだが、やっぱりそれでもどうしても辛くなり脱フレームワークしようと思う。

もちろん使いこなせる人には使いこなせるんだろうし「使うべきでない!」という主張をするつもりはない。頭のいい人には使えるんだろう。昔は「今すぐ〜すべき 10 の理由」みたいなことを適当に書いてたんだけど、どうせ自分がやってることは「 Web 系」のメインストリームからは外れてるんだろうし、合わせるつもりもなければ合わせさせるつもりでもない。使う理由も使わない理由も人それぞれだけど、少なくとも自分には無理でしたというだけの話。

前置きが長くなった。以下、個人的に CSS フレームワークをもう使いたくないなと思った理由。

CSS を書く方が楽

そもそもこれ。 CSS はモジュール化しにくいなどの欠点はあれど、生で書いてもそれなりに使いやすい。だからフレームワークで間に合わなかったり気に入らないスタイルがあるとすぐに CSS を書きたくなってしまう。しかし独自にスタイルを書いていくとフレームワークの世界観を壊してしまうので、なるべくフレームワークの文法とシステムを使って書こうと思うわけだが、これが全然思うように行かなくて歯がゆい。「ここを目的のスタイルにするためには、あのクラスとこのクラスを組み合わせて...」というのがとにかく難しくて「こんなの CSS 書けば一瞬で解決するじゃないか」という気持ちと戦いつつ、頑張ってフレームワークの文法を使って書いてみるも、結局思った通りにいかず最終的にぎこちなさの残ったスタイルになってしまう。

学習コストが高い

フレームワークを使うんだから楽がしたいのだが、学習コストがとにかく高い。もちろん上手くレスポンシブをやってくれたり、その辺はその道の達人が作ってるはずだから、払ったコストに対してペイするものだと思ってやっている。が、それでも難しい。なんどもなんども忘れて公式サイトを確認しにいくがそれでも忘れてしまう。 CSS もなんども調べなおすんだけど、こっちは Web 標準だからまだ良くて、フレームワークは本当にその場限りの知識になってしまうからガッツリ時間をかけて学習する気にもならない。

サンプルからルールが読み取れない

「こう言う感じのヘッダが作りたかったらこういう風に書くんだよ」というサンプルコードと絵が対になって紹介されているドキュメントが良くある。サンプルそのままの見た目でいいならそれをそのままコピペでいいんだけど、細かいところがちょっと予定と違っていたりして、その部分だけを変えようとすると途端にわからない。要は、 ABCDEFG という7種類のクラスが 30 行くらいの中にわっと書いてあって、そこからどういうルールでスタイルが適用されているのかを推測するゲームになる。

運が良ければ要素ごとに分解して書いてあるけど、それでも「こんな感じ」とサンプルがどーんと乗っかっていてかなり曖昧。わからないから「このクラスとこのクラスをネストさせるとこういう風になるんだろうな」と適当に想像したり、もっと知りたいときは「検証」で開発者ツールを開いて調べるんだけど、サンプル用に特別に当てられたスタイルとかもあって、自分のアプリ上に持っていくとその通りのスタイルにならなかったりする。で、開発中のアプリとドキュメントを行ったり来たりして「ここを変えたらここが変わったからつまり...」と何度かあれこれ試しながら見た目を整える作業がひたすら辛い。で、完成しても結局ルールはわからないまま。

意図のわからないルール

ルールを理解しようと思ったらコードを読めばいいんだが、 .foo > .bar:not(.is-hoge) .fuga みたいなセレクタを見て「あーそうですか」と理解できるならそもそもこんなフレームワーク使わないし、理解したとしてもその上でそのスタイルと整合性の合うように別のスタイルを組み合わせたりするのはさらに難しい。そもそも汎用的なコードというのはあらゆる組み合わせの可能性を考慮して書かれているので、普通のコードよりも複雑になりがちである。

で、コードを読んだ上でそれでも意図がわからないルールがあったりする。親要素がネガティブマージンになっていて子要素のスタイルと組み合わせることでプラマイ0になるようなやつとか、そんなトリッキーなことをして何がしたいのかが分からない。ドキュメントを探しても書いてないし、 StackOverflow には同じところで詰まってる人がいる。結局「よく分からないけど親要素にボーダーをつけたら上にはみ出るからダメなんだ」と納得しないまま進むしかなく、とても気持ち悪い。

HTML がどんどん汚くなる

本当はこれを一番に書いても良かったんだけど、話の流れから結果的にここになった。でもこれは本当に言いたい。そもそも CSS はドキュメントの構造とスタイルを分離しようという思想なんで、HTML にスタイル指定をモリモリ書いていくのはおかしいという感覚は前からあって、それでも「いや頭のいい人たちが OOCSS とか SMACCS とか色々考えた結果こうなったのだから、これも必要悪か」と渋々従っていたのだが、それでも読みにくいものは読みにくい。じゃあ古き良き CSS というか HTML にはスタイルを全く書かずに意味だけを記述してやっていくかというと、それはそれで今度は CSS の方が大変になって SASS を使ってやれ継承だという話になりそうなので、それも避けたい。

思うに、汎用性の高さゆえにクラスの記述が爆発している。例えば、開発中のアプリではカラムの幅の持たせ方は1種類しかないにも関わらずフレームワークは4種類をサポートしていると、そのうちのどれかを選ぶためにクラスの記述が必要になる。それがパラメータの数だけ掛け算になって <div class="columns is-x"><div class="column is-a is-b is-c"> ...</div></div> のように 5, 6 種類のクラスの組み合わせでようやく思ったのに近いスタイルになるが、 CSS を自分で書いていいのであれば多分1種類でいい。100 種類のボタンが用意されていても実際にアプリで使うのが 3 種類なら、残りの 97 はノイズなのだから綺麗に視界から消えて欲しい。

クラスも多いがネストも多い。例えば次のように foo-childbar を同居させて書きたい時に

<div class="foo">
  <div class="foo-child bar">
    <div class="bar-child">
    </div>
  </div>
</div>

これが何故かできなくて、次のようにネストさせないとスタイルが合わなかったりする。

<div class="foo">
  <div class="foo-child">
    <div class="bar">
      <div class="bar-bar">
      </div>
    </div>
  </div>
</div>

これが積もり積もって、頭の中では「2段にすればいいかなー」と思っていたら蓋を開けたら5段、6段になっていてゲッソリする。詳細は知らないが、これも汎用化するというフレームワークの指名ゆえに生まれた無駄だと思っている。

直感的でないレスポンシブの挙動

直感的かどうかというのは、主観的な話なのでその人のバックグラウンドによるんだとは思うが、ウインドウ幅を縮めた瞬間にガラッと見た目が変わってしまうのでやたらびっくりしてしまう。 Bulma なんかは、ちゃんとトップに「モバイルファースト」と大きく書いてあるので、それをあんまり気にせずにデスクトップファーストで作り始めた自分に非があるのは認める。だけど今までデスクトップファーストでしか作ったことないし、いきなり言われてもねというか、ブログとか簡単なサービスはいいとして高機能な UI を小さい画面で表現するのはかなりのデザイン力を要求されるし、なんかフレームワークを導入したから自動的にモバイル対応できましたにはならないと思う。

というわけで、ただでさえ設計力がいるんだけど、それはどの画面でどの UI を使うにしても常に考えていなければいけなくて、例えば「横に並べたいときはこのクラスあてりゃいいんだな」って思ってデスクトップサイズで開発してて、あとでモバイルサイズにしてみたらガッと縦になって「アイコン1個のために1行使うとかアホかー」になる。いやそれでも流石にレスポンシブフレームワークを謳うだけはあって、予想を裏切ってもそれなりに整った見た目にはなる。なるけどコレジャナイ感は拭えないし、何より自分で描いた覚えのない画面が急に出てくるのが気持ち悪いので最悪デスクトップと同じでもいいんじゃないかと思えてくる。

結局必要になるカスタム CSS

確かにフレームワークの方でカスタマイズできるように変数を用意してくれているので、ある程度はなんとかなる。けど、絶対にそれだけじゃ足りなくて必ず自前でなんらかの CSS は結局書くことになる。で、一度書き出すと「めっちゃ楽だ」ということに気づいてしまい、すぐに割れ窓になる。「こんなん横並びなんだから flex で済むだろ」でやってくと 100% 思い通りになる。で、気づくとレスポンシブがいつの間にか壊れている。既存のスタイルの何かと競合して壊れたんだろうが、面倒なので別のスタイルを上書きして直す。

あと大きいのが、デザイナーさんが居てくれる場合はほぼ 100% 自前のスタイルを書くことになるので、フレームワークがもともと用意したスタイルは最終的に影も形もなくなる。それは覚悟してるんだけどじゃあなんでフレームワークを使うかというと、苦手意識のあるレスポンシブをなんかよろしくやってくれるだろう期待があったりするんだけど、その期待は上で言った通り砕かれてしまった。変数も最初は「$danger がこの色で...」と当てはめていくだけなんだけど、そのうちに「薄い灰色よりももっと薄い灰色」とかが出てきて、最初は /* この部分は独自拡張 */ みたいにしていたけど、そのうちに独自拡張だらけになる。で、自分で CSS 書くを前提になると既存のスタイルがどんどん邪魔になってきて「なぜかスタイルが当たらない」と思うとだいたい優先度で負けていて !important をつけていくことになる。

この段階になると「半分くらいのスタイルがフレームワーク由来、残りはそれを上書きして拡張したものか完全に独自のスタイル」という状態になり、フレームワークと自前 CSS の境界がとても曖昧になる。HTML もフレームワーク由来のクラスと独自クラスが入り乱れて、非常に混乱してくる。

コードをすぐに参照できる場所に置いておきたい

フレームワークのコードは node_modules の奥深くに埋もれていていてなかなか目にする機会がないし、膨大なコードの中からアプリケーションで実際に使われているわずかな情報を抽出するのも難しい。100% 自分が意図して必要な分だけスタイルを記述して src 以下に置いておくのが、やっぱり一番コンパクトで良いのではないかと思う。「それができりゃ苦労しねえ!」って話なんだろうけど、トータルで見るとそのほうが辛くない気がする。

CSS の進化

CSS フレームワークの概念自体が Bootstrap で広がったんだと思ってるんだけど、その頃から比べたら CSS はかなり進化しているし、少なくとも「何かしらの hack をしないと使い物にならない CSS 」ではなくなったと思う。 IE11 以上でよければ Grid も使える。ブラウザの差異を吸収するのは post css とかもやってくれるし、まあそもそもあまりマニアックな機能は使わないように気をつけていれば崩れないと思う。というわけで、臭い CSS に蓋をする役割としてはどんどん価値が減っていると思う。

フレームワークをやめたら綺麗になった

ともかく辛さから逃れたい一心でフレームワークを削っていったら、HTML のネストはどんどん減るし、クラスも激減してコードも見やすくなった。それだけではなく、今まで微妙だなと思いながら放置していたスタイル崩れがどんどん解消していった。「フレームワークの流儀で書かなくてはいけない」という縛りプレイが悪い方向に働いて思うように書けなかったのと、HTML が複雑化してスタイルを直すのも困難だったことが原因だったと思われる。

何がいけなかったのか?

反省点としては、「とりあえず CSS フレームワークは導入しておけば楽になるだろ」という思考停止、「自分の書く CSS なんてたかが知れてるだろう」という自信の無さ、「自分が気づいていない何かをフレームワークがカバーしてくれるんじゃないか」という淡い期待を抱いてしまったことだろうと思う。確かにメディアクエリの知識とかは足りないんだけど、それはやりながら覚えればいい話。

自前で書くのは果たして楽なのか?

そんなことはないと思う。あと当たり前だけど CSS のスキルとコツが要る。ここではフレームワークを dis ってるんだけど、考え方は盗んだ方がいい。という意味では、やったこと自体は無駄にはなってないんだけど、ちょっと遠回りしすぎたねという感じ。地道に CSS スキルをつけていこうと思う。関係ないけど Grid 最高。

Elm 本書きました

f:id:jinjor:20190225233153p:plain

Elm の本を書いていました。

基礎からわかる Elm

実は 2017 年時点で Amazon に存在していたのですが「出した直後に Elm がバージョンアップして紙くずになりました」という事態をどうしても避けたかったので 0.19 が出るのをひたすら待ってました。0.18 から半年くらいで出るだろうと踏んでいたんですが、蓋を開けたら1年半経ってました。実際バージョンアップで elm-makeelm make に、 elm-package.jsonelm.json に、 elm-lang/*elm/* に、toStringDebug.toStringString.fromInt に、Html.programBrowser.element に、と色々変わってしまったので、あの時点で出していたら本当に紙くずになってましたね。

内容

Elm は公式ガイドがかなりよくまとまっいるし今は日本語訳もある中で、紙の本に何を書いたらメリットが出るかというのはものすごく悩んだんですが、逆に公式がすっ飛ばしている部分をギチギチに埋めていくことにしました。特に ML 系の文法とか型に慣れていないと色々戸惑うことが多いと思っていて、type MyType = Foo | Bar と書けば「この Foo という型はどこに定義されてるんですか」みたいな話になるし、 onInput Input みたいにさらっと部分適用するし、「Html msgHtml Msg の違いは何ですか?」とかも FAQ ですね。Elm は最初とっつきやすい割に途中から「型を見て全てを理解しろ」というノリになってくるところがあって、そこで振り落とされると「サンプルコードがないからわからない」状態になって死にます。なので、この本の目標としては関数型言語の初心者が API ドキュメントの型をスラスラ読めるようになること」です。型が読めれば自力で前に進むことができます。SPA 設計とかについても一応書いたんですが基礎力がないと結局応用できなくて死んでしまうので、最初の方ほど重要度が高いです。ぶっちゃけ Elm やり込んでる人にとっては当たり前のことしか書いてないですが。

あとは紙の本の場合は「それ自身で情報が完結する(self contained)」というところメリットを出さないといけないと思っていて、ネットだとどうしても公式サイトから GitHub 、Qiita 、 Slack と情報が散らばってしまうところを一冊読めば必要な情報は全部網羅できる状態にしたつもりです。 最後には @ruicc @arowM_ の両氏に分担して内容を確認してもらいました。ほとんど時間なかったんですが、的確な指摘をたくさんもらったのでかなり助かりました。🙏

感想

ええと正直ですね、めちゃくちゃしんどかったです。技術書の出版は大したお金にもならないのは知っていたのでそれは別にいいんですが、とにかく時間が削られるので、週末がくるたびに「原稿進めないと」と鬱になっていたし、その間ほかにやりたいことが何1つ進められなくて、それでも勉強になるならいいんですが自分の知ってることを吐き出してるだけなので、本当に割に合わない仕事ですねこれ。 まあこうして形になってみると「本っぽい!本当に技術書っぽい!」という感慨はあるんですが、中を読むと辛い思い出が蘇りそうなので未だに開けずにいます。

でも中身は多分最高なので、ぜひ読んでくださいね!

JavaScript フレームワークを巡った話

ポエムです。

自分の今の立場としては「Elm の人」ということになってるんだけど、どういう変遷でここまできて今どういうスタンスなのかっていうのはあんまり話す機会がない。だから整理のために考えてることを書いていくよ、というのがこの記事の趣旨。

非 Web の立場から

そもそも自分は「Web 系」の出身ではない。新卒入社したワークスでは ERP パッケージを提供するのに画面を Web 技術で作ってるというだけで、別に SEO の順位を競ったり広告をどうという話ではないし、瞬時に画面が表示されないと離脱率が〜という話でもない。ただ、画面はとにかく複雑で設定項目とががうじゃうじゃある。

あと、学生時代に PC に触れたのが Windows で「黒画面なにそれ美味しいの?」くらいに GUI に染まりきってたというのがある。工学系の研究を効率化するために C#GUI を作ってたら、なんかソフトウェア業界に入ることになってしまった。 CLI は今でも得意でなく、黒画面を前に「今なにをしようとしてたんだっけ」と手が止まってしまう。

話を戻すと、とにかく複雑で高機能な画面を簡単に作りたいわけである。仕事もそうだし、趣味の話で言えば音楽制作ソフトの華である DAW とかが作りたかったりする。

jQuery

最初は Java でスタンダードな画面遷移する Web アプリの作り方を学んでたりしたんだけど、なんかのタイミングで「 JavaScript を使えばグリグリ動く画面が作れるらしい」「とにかく jQuery を入れるんだ」という情報を仕入れて、まあやってみたら本当にグリグリ動いた。 あと社内でアリエルの製品を使ってて、かなり高機能な画面が JavaScript でゴリゴリ動いてるのを知っていたので、「そうかイケてる画面はこうやって作るのか〜」とか思いつつ、とりあえず目標としては JavaScript で高機能な画面を作ることとなった。

ところが自分が当時関わっていた社内システムは理想とは程遠く、よくある「とりあえずサーバー側で作った画面を jQuery でいじり倒す」という行き当たりばったり状態。まあそもそも JavaScript もほとんど分からない状態だったので、ポップアップを出すライブラリを適当に取ってきて用途に合わない動きをちょっとハックしてなんとか動かすみたいなことをしてた。

「このままじゃ全然イケてる画面になる気がしない・・・」と思っていた矢先、社内で「クライアントサイドの基盤でも作ろうか」みたいな話が持ち上がった。一応、技術基盤をやる部署だったので。

Ember.js

最初に検証の対象に持ち上がったのが Ember.js である。なぜこいつが最初だったのかはよく覚えていないが、確かにチュートリアル通りに手を動かすとイケてる感じのポップアップがポンと出てきて「お〜」みたいな感じになった。 これは本当にちょっとしかやってないからほとんど覚えてないけど、まあ JavaScript 自体が初心者ということもあり使い方が複雑で難しく、自力で組み立てようにもまずフレームワークが難しいような状態で断念。(あとマスコットキャラクターもあんまり好みじゃなかった。)

コンポーネント

フレームワークはよく分からなかったが、とりあえず目標としているのはネイティブ GUI のような高機能画面で、それらの特徴は「四角い部品」の寄せ集めで出来ているということだった。 当時「コンポーネント」という言葉を使ってたかは忘れたが、とにかく jQuery で HTML を直にいじるのはよくないから、もっと粒度の大きい部品を作って API 越しにアクセスすればいいんじゃないかという考えになった。で、そのコンポーネントを全体でツリー状にネストさせれば良いのだと。

Haxe

順番は忘れたが、 AltJS の波が来て「 Haxe っていうイケてる言語があるらしい」ということで、上記のコンポーネントはそれで作っていた。この頃には前よりいくらか JavaScript にも慣れていて Haxe 製の Haxe エディタなども作ったりした。

型もついて一件落着かと思ったが、「イベントをやり取りするのがめんどくさい」という課題が残った。例えば、「クリックしたら兄弟コンポーネントも更新する」ような場合に、一度親がイベントを拾って兄弟を描画し直さないといけない。あとは、遠く離れたコンポーネントを更新する場合には一度グローバルにイベントを渡すようなトリックも必要になった。 つまり、粒度は大きくなったが、やっていることとしては jQuery と同じ「ビューからイベントを受け取って別のビューを更新する」というフローになっていた。

これを打破するために必要になったのが MVC である。

Backbone.js

もはや名前も聞かないが、当時 MVC フレームワークと言えばとりあえずメジャーな Backbone.js かなという雰囲気があった。jQuery を使うのが前提になっているのも、その時代という感じがする。 こいつは何かというと「モデルが更新されたらビューが自動で更新されるようにしましょう」が軸で、手段としてはオブザーバーを使う。ビューはモデルを listen し、モデルのプロパティが更新されたという通知を受けたらなんらかの方法で再描画する。

仕事では試す機会がなかったので、趣味で Backbone.js で色々作ってみることにした。確か WebAudio API の UI を作ったのはこれ。で、色々問題が見つかった。

まず、イベントハンドリングがスパゲッティになる。モデルやそれの集合体であるコレクションを更新するととにかくイベントが飛びまくる。「1回の更新のはずなのになぜか3回再描画が走る」などのトラブルが起こり、原因を突き止めるのがとにかく大変。 次に、リスナーの削除が漏れる。ビューのライフサイクルの中できちんと unlisten できればいいのだが、上位コンポーネントが雑に innerHTML とかでまるっと消してしまうと、リスナーが削除されずに溜まっていく。 最後に、モデルを更新したら自動的にサーバー側と同期をとる仕組みが意味不明だった。そんなに上手くいくんだろうか? Rails の何かを真似した仕組みっぽいけど、 Rails の経験はなかったし(今もない)、そもそもサーバーと同じ仕組みで動くものだろうか。

あとは Backbone.js に何かの仕組みを付け足した派生みたいの(Marionet とか)が結構あったけど、どれも大成功しているようには見えず、モヤモヤしていた。

AngularJS

当時はとにかく AngularJS の登場が衝撃的で「すげー!」となった。(登場というか、当時はそんなにネットで情報収集していなかったので、すでに登場していたのかもしれない) もちろん Angular 1 のことである。

仕組みは Backbone.js よりもかなり単純で、モデルを更新したら次のターンでビューを描画する。テンプレートを使い、今の Virtual DOM に似た dirty checking という方法で変更差分だけを DOM に反映する。ただ、非同期処理が入ると描画がトリガーされないので、そこだけは上手く専用の API を呼ぶ必要がある。

つかみは非常に良かったのだが、それ以上複雑なことをしようとすると、どうすればいいのかたちまち分からなくなった。コンポーネントをツリー状にすれば良いんだろうと思っていたが、そんな説明はドキュメントのどこにもない。directive というカスタム要素を作る方法はあったが、ちょっとした用途向きで無限にネストすることは考えられていない。他にもコントローラーとかインジェクションとかも色々あったが、とにかくどう使えば良いのか分からない道具が並んでいるという印象。

当時「AngularJS を使っていると何度か精神的にアップダウンするけど、最終的には最高の状態に行きつける」みたいな言説があったけど、自分はアップダウンしている間に燃え尽きてしまった。

Polymer

Google が「時代は Web Components !」と言わんばかりに Polymer を突っ込んできた。 Web Components は自分の当初やりたかったコンポーネント構想に近かったのもあるし、 Web 標準にもなるというのでかなり期待が高かった。Chrome 35 だか 36 の時に Custom Elements が実装されて大はしゃぎした記憶がある。

もう記憶がおぼろげだが、 Polymer は単なるポリフィルというよりもフレームワーク的なこともお世話してくれていた気がする。今は虫の息と言われている HTML Imports とかも、本番用に結合して配信するための vulcanize とかいうツールが作られたりした。作ったコンポーネントをシェアするためのリジストリなんかもあった。 Polymer のカンファレンスもあった。とにかく「これからみんな Polymer でやってくぞ」という雰囲気で宣伝も激しかった。

で、自分のホームページを Polymer で構築してみた。 ・・・当時は「未来の Web 標準」なんて Google が言い出したものなら「なるほどそうなのか!」と言って無邪気に信じたし、ライブラリだって「凄い人たちが作った凄いもので自分は使うだけ」と思ってたから、まさか数ヶ月後に突然ホームページが真っ白になるとは思わなかった。多分 Chrome の何かのアップデートで polyfill の一部が壊れたのだろう。 すっかり懲りてしまって、「まあ Google がなんか言っても話半分に聞いとくか」くらいのスタンスになった。

リアクティブプログラミング

そんなこんなしている時に、ひょんなことから次の記事が目に留まった。

なぜリアクティブプログラミングは重要か。 - Conceptual Contexture

概念としてすごく面白そうだと思って「これ MVC フレームワークに応用できるんじゃね?」となった。 アイデアとしては、モデルが変更されたら自動でビューが更新されるように連鎖してくれれば言いわけだ。AngularJS とかだとモデルとビューは1対1なんだけど、リアクティブプログラミングであれば「モデル => モデル => モデル => ビュー」のようにスケールしそうな気がする。

というわけで、それができそうな Bacon.js を触ってみる。だけど Bacon.js はイベントの起点が DOM になっていて、なんかちょっと違うなと思った。(少なくとも当時はそう思っていたけど、今は別におかしくないと思う) で、もう少し調べていると、なにやら Elm という言語が FRP(関数型リアクティブプログラミング)というのでそれっぽいことをしているらしい。こんにちは、FRP

ただ、当時の Elm は難しかった。Haskell を知っているのが前提みたいな空気があったし、何と言っても HTML タグが書けなかった。頑張って習得したところで canvas に5角形を書くくらいしかできない。「TodoMVC をなんとか Elm でも書けたぞ!」と Evan が得意げに YouTube に投稿するくらいである。 当然「流石にこれはオモチャでしょ...」となった。

React

これはもう魂が震えてしまったね。もはや説明の余地もない「宣言的ビュー万歳」である。 最初は「そんなバカスカ Virtual DOM 生成していいものか」という不安はあったが、使ってみてすぐにその懸念は払拭された。コネで CodeZine に2本ほど記事も書いた。

もちろん全てに満足していたわけではなくて、「わざわざシンタックスのためだけに JSX とか入れる?」とか、「 JS で Immutable 守り通すのきつくね?」とか、まあ色々思うところはあったが、出発点が jQuery のカオスだっただけに「まあ言っても昔に比べりゃ全然 OK なレベル」と概ね満足していた。

Elm

そんなところに「Elm が Virtual DOM を導入したらしい」というニュースが入ってきた。(これもニュースというよりは、少し経ってから知ったのだが)

「Elm で HTML が書けるってマジ?」

元々関数型も少しかじってたし面白そうだからやってみるか。で、試しに色々作ってたら「なんかこれ普通に実戦投入できそうじゃね?」になって今に至る。その間に Elm は FRP であることをやめてしまった。さようなら、 FRP! あと、ミートアップに顔を出したり主催してたら Idein 社に呼んでもらえた。ごめんな ERPFRP もやめてしまって。

仕事でも Elm を快適に使えているし、大体の用事は Elm でなんとかなる。 そのことに特に不満はない。シンプルながら最強の型システムを持ち、コンパイルは爆速、ライブラリはフロントエンドに特化しているし、関数単位でデッドコードを除去しながらバンドルまでしてくれて、いざとなれば JavaScript と連携可能である。これだけあって、至らない部分に文句を言うのは贅沢というものである。

ただ、個人的な問題としては、Elm Way が完成されすぎてて「もっとこうしたらどうか」と自由に遊べない。「遊んで時間を浪費するよりも余計なことを考えずにトレードオフを受け入れろ」という、なんとも仕事向けな奴なのである。(言語機能は汎用的だが、主要ライブラリの仕組みごと作り変えるコストが高い) 「宣言的ビューは善である」という仮説をある程度検証することは出来たけど、「じゃあ他の可能性はなかったの?」と思わなくはないわけである。

Web Components

Edge が開発を断念し、世はまさに Chromium 一強時代に突入しようとしているらしい。 となると、 Web Components (主に Custom Elements と Shadow DOM )が意外と早く陽の目を見るのでは、という見方からパラメーター配分を変えているのが現在の状況である。なんとなく v1 はこのまま行きそうだし、 Polymer に頼ることもなさそうだ。

Web Components はフレームワークではないので、素のまま使うのは難易度が高いかもしれないが、今はあえて「何も使わずにやる」という方針にしている。というのは、今のフレームワークって全部 Web Components がない前提で作られたものばかりなので、 Web Components があるとまた違った景色が見えてくるかもしれないから。「素のまま頑張るとこんなもんか〜」と生 DOM の感触を確かめつつ(一応 TypeScript で)色々と試している。

今のところの感想としては、素の Web Components は Backbone.js と書き味が近い。 ただ(自分の理解では) unconnectedCallback が必ず呼ばれるはずなのでリスナーの解除し忘れがないのが嬉しい。独自の仕組みとしてイベントに EventTarget を使っているが、モデルとは紐付けないようにしている。HTML をテンプレートで書くときも、VSCode の lit-html プラグインを使うとシンタックスハイライトが効いたりする。

その先

どうなるかは全然読めないけど、とりあえず Web Components をベースにガチャガチャやっていくことになるんだと思う。CSS は Shadow DOM の中で干渉なく使えて css-loader とかの存在意義が薄れる。モジュールは基本 ES6 Modules で書かれるけど HTTP/2 でサーバープッシュするプランが怪しいので、引き続きバンドラーを使うことにはなりそう。言語はそのうち TypeScript をブラウザが解釈するようになるんじゃないか、などと妄想しておく。

自分の探求する領域としては、 Virtual DOM alternative を考えようかなと思っているところ。 lit-html や hyperHTML みたいなものかもしれないし、別のかもしれない。Elm は仕事で継続してベストプラクティスを探求しつつ、 Web Components との組み合わせも試したい。そんな感じ。

おまけ:その他のライブラリ

  • jQuery UI: 単品でいくつか使ったことがある。
  • dojo: 聞いたことある程度。
  • Knockout.js: 聞いたことある程度。
  • ExtJS: 前職で使われていて良さそうだったが、あまり触ってない。
  • Google Closure Library: これも前職やアリエルで使われていたが、玄人向けで使いこなせる気がしなかった。
  • RxJS: 気になりつつ触ってない。
  • Angular(2-): ちょっと何がしたいのか良く分からない。
  • Redux: だいたい知ってるけど触ってない。昔の Elm の名残りで副作用周りがミドルウェアに追い出されてるように見える。
  • Vue: チュートリアルはやった。ルールが多いのとオブザーバーへのトラウマから食わず嫌い中。
  • Riot.js: 正規表現でゴリゴリパースしてるという話だけ聞いた。
  • Dart: 初代をちょっと触った。
  • Flutter: よく知らない。
  • Preact: よく知らない。
  • Mithril: 一時期速いって言われてたことだけ知ってる。
  • choo: Elm 再現系ね。
  • Cycle.js: あの人最近何やってるの。
  • Meteor: 密結合が怖くて触れなかった。
  • CoffeeScript: 触る前に消えた。2は知らない。
  • Reason (React): 型はあるけど Elm みたいに API ドキュメントが見当たらなくて辛かった。
  • PureScript (Halogen): 自分には難しそう。
  • ClojureScript (om): よく知らない。
  • Scala.js: 触ってない。
  • js_of_ocaml: あったね。

deno で Elm の live reload を作ってみた + 感想

deno はこれ。

github.com

Node.js 作った人が今度は TypeScript で作り直してるっていう話らしい。

yosuke-furukawa.hatenablog.com

で、今はまだまだ実用段階ではないんだけど、一応それなりには動く模様。

まじで、置いとけば動くの。

f:id:jinjor:20181223092317p:plain

うおー本当だ。なにこれ楽しい!

試しに作ってみた

github.com

deno で Elm のライブリロード。 こんな感じで起動して、ソースの .elm ファイルを更新するとコンパイルしたついでに画面が更新される。

deno ../index.ts src/Main.elm src/index.html  --port=3001 --allow-net --allow-run

以下が一通り試せるので、お題の選定として最適っぽい。

  • 引数処理
  • ファイル読み込み
  • プロセスの実行
  • HTTP サーバー

感想

ついさっき知ったことをそのまま横流しに紹介するスタイル。 ちゃんとした紹介はあとで誰かにしてほしい。

deno https://...

一番面白い点としては、やはりモジュールがインターネットから降ってくるところ。1回目でフェッチして2回目以降はキャッシュを使うのでオフラインでも大丈夫。--reload をつけると読み直す。~/.deno につないだ場所とか履歴とか生成した JS とか source map とか諸々置いてある。

「いやいやそんな適当なモジュール管理でいいわけないでしょ」っていう反応に対していくつか考えが述べられている(けど、もう少し詳細に知りたい気持ちはある)。 https://github.com/denoland/deno/blob/master/Docs.md#linking-to-third-party-code

バージョンは URL で一意に決まるから npm とか package.json みたいなやつはいらなくて、その辺は例えば package.ts を用意するとかしてアプリケーションコードで管理してくださいとのこと。随分と思い切るなー。

特定の操作に許可が必要

フラグをつけないとファイルに書き込んだりネットにアクセスしたりできない。V8 のサンドボックス機能を活かした形。

        --allow-write   Allow file system write access.
        --allow-net     Allow network access.
        --allow-env     Allow environment access.
        --allow-run     Allow running subprocesses.

サンドボックスの外とはシリアライズされたデータでのみやりとりが出来る。

deno/msg.fbs at master · denoland/deno · GitHub

とにかく Promise (async/await) を使う

標準から Promise API で溢れているので、もうとにかく async/await し放題。将来的にはトップレベルでも await したいとのこと。あと Unhandled なんとかはもれなくエラー。

型付き API ドキュメント

嬉しい!

https://deno.land/typedoc/

まだ洗練されていない印象はあるものの、ちゃんと型を辿っていけば正解にたどり着けるようになっていた。すごい。

まだ楽に色々出来る状況ではない

今提供されている API はかなり最低限のもので、ファイルを読むにもオープンしてバッファにデータ詰めてデコードして最後にクローズ、というのをやる必要があり結構だるい。TextDecoder / TextEncoder とか知らないと辛いんだけど、調べてもすぐに出てこないので「目の前に string があるけど API が要求しているのは UInt8Array でどうしよう」みたいな事に、よくなる。

HTTP サーバーはなんとか動くレベル

一応、標準ライブラリがある。

github.com

が、 HTTP サーバーは基本機能もまだ怪しくて、headersContent-Length を正しく入れてやらないとコネクションが閉じず、ブラウザから1秒おきに fetch() すると7リクエスト目から通信不可能になってしまった。あと最近まで POST の body が読まれないことがあったらしいとか、そういう状況。

引数処理が楽

同じく標準ライブラリの flags なんだけど、これだけのために deno を使いたいくらい良く出来てる。 これが npm の argv 相当で、他にも npm の chalk 相当の color とか、「それが欲しかったんだよ!」っていうのが用意されててとにかく嬉しい。

他に面白そうな要素

今回は通らなかったけど、 deno のネイディブ側は Rust で動いているらしく、deno を Rust 内で使うにはこのクレートを使えばよいそうだ。楽しそう。

貢献のチャンス

今なら既存の何かを deno 用に書き直すだけでワンチャンありそうだし、 deno 独自の特徴を活かした面白いものが色々作れるかも。 腕に自信のある方は是非。