TypeScript でジェネリクスの部分的な型推論ができない
const obj: any = { a: 1, b: "x" }; function foo<T, U>(x: U): [T, U] { return [obj[x], x]; }
こういうコードがあったとする。コードの良し悪しは置いといて、U
は引数から推論して、T
は呼び出す側から指定したいというケース。
// こう書きたいけどエラー foo<number>("a"); // Expected 2 type arguments, but got 1 // これは通る foo<number, string>("a");
これは今現在(TypeScript 4.0)ではできないみたいで、Proposal があがっていた。
https://github.com/microsoft/TypeScript/issues/26242
ワークアラウンドな方法を見つけたけどこれを使うぐらいなら素直に型引数書いたほうがよさそう。
今後のAdventar
メリークリスマス!Adventarを支える技術 Advent Calendar 2019 の25日目、最終日です。
最終日は今後の Adventar をどうしていきたいかについて技術編と機能編に分けて書こうと思います。
技術編
細かく直したいところはいっぱいありますが、大きめトピックだけいくつか書きます。
SSR のキャッシュを入れたい
SSR のキャッシュについては19日目の記事に詳しく書きました。今は毎回 Lambda を呼び出してレンダリングしているので、キャッシュすることでパフォーマンスをあげたいと思っていますが、コスト(お金)がかかる問題をどうにかしないと...。
gRPC-Web の Node.js 対応
5日目の記事に書きましたが、今 Node.js では gRPC-Web が使えないので SSR の際に仕方なく JSON API を使っていますが、完全に無駄なので gRPC-Web を Node.js でも使えるようにして同一コードでサーバー/クライアント両方動くようにしたいですね。
そのためには gRPC-Web の ptoroc プラグインに手をいれないといけなくて、C++ を書く必要がありそうです。できるのかな。
gRPC サーバーを Lambda で動かしてみる
gRPC サーバーを Lambda と API Gateway で動かすのは、たぶんやればできるんじゃないかなと思ってますが、確証はないです。これができると ECS などのサーバーが不要になり、真のサーバーレスにできます。真のサーバーレスになると圧倒的にコスト削減できるはずなので、取り組んで見る価値はあると思います。
GitHub Actions で CI/CD したい
今はテストは手元で実行するだけになっているし、デプロイも手元からやっているという、プロとして恥ずべき状態です。CircleCI とかでもいいんですが、GitHub Actions ちゃんと使ったことないのでやってみたい。
まあこのぐらいですね。どれも実際にやるかどうかはわかりません。
機能編
サービスをシンプルに保ちたいので、そんなにたくさん機能を追加するつもりはないのですが、現状でいくつかやりたいことはあります。
探しやすくする
今カレンダーを探すのはテキスト検索ぐらいしかないので、もう少しカレンダーを探しやすくするような機能をいれたいです。具体的にはカテゴライズ、スターを導入して人気順、募集中のものだけ検索、などを考えています。
ちなみにカテゴライズについては過去のカレンダーを手動でカテゴライズしてみたのですが、ジャンルが幅広すぎて、あってもなくてもあんまり変わらないのでは、という気になったので一回置きました。機械学習でいい感じにカテゴライズできたりするかな。
アイコン更新できない問題
地味な問題ではあるんですが、今年一番お問い合わせが多かった問題です。詳しくは8日目の記事に書きましたが、Firebase Authentication の仕様の問題で、Twitter などのアイコンを更新した際に Adventar 側のアイコンが更新されるタイミングが難しい感じになっています。
地味なんですけど、アイコン大事なのでどうにかしたいですね。
iOS/Android のアプリ
どちらかというと技術的な興味によるものです。Web よりもいい体験が提供できるんだろうか。ちょっとやってみないとわかりません。
ちなみに技術的には、Swift や Kotlin を学ぶために、それらの言語で作ってもいいし、React Native で作って、Web を React Native for Web で作り変えてみるなんていうのも技術的には面白そうではあります。
以上です。もし機能要望などがあれば Twitter や GitHub にお気軽に書いてください。GitHub の Issue は日本語でも大丈夫です。
まとめ
書ききりました。がんばった。全部の記事を読んでくれた方、一部の記事を読んでくれた方、Adventar を使ってくれた方、ありがとうございました。
今年はもう疲れたので何もしませんが、来年以降またがんばろうと思いますので、今後も Adventar をよろしくお願いします!
それでは良いお年を!
Adventar の技術変革の歴史
Adventarを支える技術 Advent Calendar 2019 の24日目です。
今日はこれまで Adventar が利用してきた技術がどのように変わってきたのかを書こうと思います。
2012年
リリースした年です。最初は Ruby と Rails を勉強したいと思い、何かいいサービスの題材はないかなあと思っていて、当時主に ATND で行われていた Advent Calendar が使い勝手が悪すぎて、Advent Calendar 専用のサービスを作ったら使いやすいんじゃないか、というので作りはじめました。(これは ATND が悪いわけではなくて、そもそも ATND はそういう目的のサービスではなかったというだけです)
なので初期実装は Rails でした。サーバーは、たしか YAPC Asia か何かで、当時ペパボに勤めていた刺身さんから、Sqale というペパボのホスティングサービス(Heroku みたいなやつ)のクーポンをもらったのがきっかけで Sqale を使ってホスティングしていました。
値段も安かったし簡単だったんですごくよかったんですけど、今はもう終了しちゃってます。
ちなみに Internet Archive で見つけてきた当時の様子はこんな感じでした。やる気がなさすぎるw
2013年
ほぼ一人でやっていたところに、june29、ayumikoという強力な仲間を得て、飛躍的に完成度が高まった年です。
june29 さんには僕の初心者 Rails コードをバシバシレビューしてもらい、ayumiko さんにはクソダサかった見た目を見違えるようなデザインにしてもらいました。
また、この年からサーバーは Heroku に移りました。明確な理由はよく覚えてないけど、june29 さんが Heroku に慣れていた、Redis とか memcached などのミドルウェアが Sqale で利用できなかった(ような気がする)あたりが理由だった気がします。
それとフロントエンドは Backbone.js を利用するようになったようです。当時流行ってたんですよ。
当時のデザインはこんな感じだったみたいです。
たしか12月になったらカレンダー一覧でなく記事一覧をトップに出したりしてました。背景の画像がなんかかっこいいですね。
2014年
この年は軽微な変更で、特に大きい変更や技術的チャレンジはないみたいでした。あんまり覚えてない。
2015年
Qiita にエントリを書いてました。
https://qiita.com/hokaccha/items/c5cd96c2ec002e27ff4b
サーバーは相変わらず Heroku だけど、フロントエンドを React で書き換えて、react-rails を使った Server Side Rendering を導入しました。
当時 React がちょうど流行りはじめぐらいで、試してみたいなーと思っていたので投入してみた。当時まだ hypernova とかもなくて、Server Side Rendering の知見はほぼない状態だったので色々と苦戦した覚えがあります。
react-rails は Turbolinks とまあまあ相性がよくて、Turbolinks の遷移時のイベントで React Component を Mount/Unmount できるし、初期描画のときだけ SSR、Turbolinks 遷移のときは CSR というふうにできて、ルーティングをクライアント側でやらずに Rails に乗っかれるし、アーキテクチャとしては今でも悪くない気がしてます。流行りはしないと思うけど。詳しくは前に発表したときの資料がありました。
https://speakerdeck.com/hokaccha/react-rails-1
2016年
軽微な変更のみで大きな変更はなかったみたいです。
2017年
この年は ECS にしてみました。
今年はHerokuからAWSに移してDocker/ECSにしてhttpsになったりHTTP/2に対応したりしました。
— hokaccha (@hokaccha) 2017年10月25日
当時仕事でも ECS を使い始めていて、基盤チームが色々整備してくれていたのだけど、自分では何もわからなかったので一回 ECS でインフラを作ってみたかったというのが動機です。
とにかく難しくて大変だった記憶があります。まあもちろん今年のほうが大変だけど。
2018年
ECS にして毎月1万円ぐらいかかることが判明して、ECS だいたい理解したし、できるだけ安価に運用したいということで雑な VPS に移して月1000円ぐらいで運用できるようにしました。
itamaeで構成管理したり、 Mackerel を使い始めたりしました。あとなぜかタイムスリップして古きよき Capistrano によるデプロイになりました。
Heroku に戻らなかった理由は覚えてないけど、なんでだっけな...。単純に VPS のほうが安かったからかな。
2019年
とにかくモダンな構成にしたいと思ってがんばって作り直しました。
Adventar、長年 Rails だったシステムを、GoでgRPCのAPIサーバー、Nuxt.jsでSPAのフロントエンド、grpc-web、Firebase Authentication, Lambdaでサーバーサイドレンダリング、Serverless Framework などを使って刷新してオープンソースにした https://t.co/Te0buXpA3j
— hokaccha (@hokaccha) 2019年10月31日
オーバーテクノロジーすぎてすでに Rails に戻りたい
— hokaccha (@hokaccha) 2019年10月31日
来年には Heroku に戻ってるかも知れないです。
まとめ
Adventar ができてからの歴史を振り返ってみました。2013年に基本的なかたちができてか大きい機能や見た目の変更はなく、個人サービスなので技術的なチャレンジを色々とやってみる実験台みたいになってます。
今後も大幅な機能変更とかの予定はないけど、できるだけ使いやすくはしたいし、長くサービスを継続できるようにがんばりたいし、技術的チャレンジももっとやっていきたいと思います。明日はいよいよ最終日です。今後のAdventarの技術的チャレンジや方向性について書こうと思います。
サービスをオープンソースにする
Adventarを支える技術 Advent Calendar 2019 の23日目です。
今年から Adventar はオープンソースにしました。
ツールやライブラリ、言語などのソフトウェアであれば今の時代オープンソースというのは山程ありますが、サービスがオープンソースというのはそんなに多くないと思うので今回はそうした理由や、いい点、悪い点などについて書こうと思います。
オープンソースにする理由
特にクローズドである意味もないので、オープンソースにしたいとは前々から思っていて、昔のコードはオープンにしづらい履歴もあるし、システムリニューアルのタイミングでちょうどいいので、このタイミングでオープンにしました。
オープンソースにして誰かの役に立てば嬉しいし、誰かが勝手にバグを直したり機能改善をしてくれるかもしれないし、Fastly や AWS などはオープンソース支援で料金を補助してくれたりするし(試しに申請してみる予定)、いいことばっかりです。
- https://docs.fastly.com/en/guides/accounts-and-pricing-plans#free-open-source-developer-accounts
- https://aws.amazon.com/blogs/opensource/aws-promotional-credits-open-source-projects/
ちなみにすでに実際に何件か Pull Request を頂いて、オープンにしてよかったと思えました。ありがとうございます。
https://github.com/adventar/adventar/pulls?q=is%3Apr+is%3Aclosed
(自分は master 直 push マンなのがバレる)
また、オープンにすることであまり雑にできない、という緊張が生まれるのは良いですね。実際コードを読んでる人はほとんどいないでしょうが、見られる可能性がある、誰かの参考にされる可能性がある、というだけで、ちゃんと書かないと、という意識になります。と、書いてて思ったけどこれってサービスに限らず普通の OSS でも同じですね。
ちなみに緊張感があるといいつつ、実際時間がなくて(いいわけ) Go のコードとかはけっこうひどい感じではあります。
懸念点
セキュリティ
少し大変なのは、オープンになることでセキュリティリスクが高まることかなと思います。ただ、世の中のはオープンソースのソフトウェアで溢れていて、それに対して脆弱性もばんばんでているわけで、アプリケーションのコードだけ隠しても劇的にセキュリティが強固になるとは思いません。もちろんオープンよりはクローズドのほうがセキュリティリスクは減ると思いますが。
また、クローズドだと雑にリポジトリ内に入れられるような情報を、オープンだと入れられない(ので環境変数などにする)、みたいなのは多少ありますが、より健全になるだけなのでこれについてはオープンのほうがいいですね。
ただ、terraform のコードをオープンにするかはけっこう迷いました。ネットワークまわりの設定を間違えていたりすると危険だし、あまり外に出したくない情報ではあります。
が、そういう理由もあり terraform でのインフラ構成はどは特に外には出さないので、こういうものこそ誰かの参考になれば、という気持ちでオープンにしました。危険な設定を見つけたらこっそり教えてください。
サービスを真似される危険
オープンソースにしたときに聞かれたことがあったので一応書いておきます。
営利目的だと話は違いますが、Adventar を真似て作られたところで痛くないし、むしろ Adventar より使われるようなサービスになった喜んで Adventar を閉じます。他には雑なスパムみたいなコピーサービスが増えるみたい可能性もありますが、それは別にクローズドでも見た目を真似ることはできるし、結局コンテンツがないと意味がないので心配してません。
ちなみに、サービスのコードをオープンにしているところはいくつかあって、有名どころだと dev.to や gitlab などがそうです。
これらのサイトはオープンですが特にそれを使って類似サービスがでたりはしてないし、心配するだけ無駄だと思っています。
まとめ
今日はサービスをオープンソースにして開発する意味や懸念点について書きました。明日は Adventar の歴史について書こうと思います。
細かすぎて伝わらない UI の工夫
Adventarを支える技術 Advent Calendar 2019 の22日目です。
さすがにネタ切れ気味なので、UI 系の細かいネタを投下します。
ユーザーアイコンが404のときにフォールバック
Adventar では Twitter などのソーシャルログインを使っていて、ユーザーのアイコンは各プロバイダから取得できる画像の URL をそのまま使っています。その URL がずっと使えるならいいのですが、ユーザーがプロバイダのほうでアイコンを更新した場合などに古いアイコンの URL がリンク切れになってしまいます。
そうなると、ひどいと以下のようなってしまいます。
これはひどい。せめてデフォルトアイコンを設定するようにしたいところです。しかし、アイコンの URL はアクセスしてみないと取得できるのかどうかわかりません。そこで img
要素の onerror
イベントをトリガーしてデフォルト画像にフォールバックしています。
<img src={{user.icon}} onerror="this.src = '/default.png'">
実際は Vue.js でやっていてもう少し複雑ですが、こんな感じのイメージです。実際のコードは以下です。
これでさっきの画面は以下のようになります。
だいぶマシですね。
部分的にローディング
API の呼び出し待ちなどにローディングを表示するのはユーザーに状況を伝えるのに重要なアクションですが、ローディングを表示するのはできるだけ小さい範囲に留めるようにしています。
例えばトップや検索画面のカレンダー一覧ですが、カレンダーの一覧取得には API の結果を待たないといけないので、返ってくるまでしばらくラグが発生します。そのとき全画面ローディングにしてもいいのですが、以下のように、必要な部分だけをローディングにすることで、可能な限りユーザーに速く画面を見せます。
また、8日目の記事に書いたのですが、最初のログイン処理にやや時間がかかってしまうため、その間ローディングを出したいのですが、これもログイン情報が必要なところは多くないので、必要なところだけローディングにして体験を損ねないようにしています。
これでだいぶ初期表示が速くなり、体験がよくなります。
吹き出しの位置のこだわり
これは本当に細かいのですが、カレンダーで登録情報を編集するポップアップがあるのですが、この吹き出しの位置に微妙なこだわりがあって、端の登録を押したとき、スマホなどの画面幅が狭い場合は以下のようになります。
可能な限り選択した吹き出しに近い位置に出して、吹き出しの三角は選択したセルを指します。
また、PC などの画面幅が広いデバイスで見た場合は左右に余白ができるので、端のセルでも中央にポップアップを表示したいところです。
書いてみると当たり前の動きな動きすぎてなんでこれを紹介しているのか自分でもよくわからなくなってきましたが、この動作をあらゆる画面幅で動作するように実装するのが思いの他大変だったので紹介したくなっただけです。
実装を見返してみると色々ハードコーディングしてあったりして泥臭くてだいぶひどい感じですが、がんばった後が見られますね...。
まとめ
今日はネタ切れ気味で細かい UI の話を書きました。本当に細かすぎてすいません。明日は Adventar をオープンソースで公開した話を書こうと思います。
View のモバイル対応
Adventarを支える技術 Advent Calendar 2019 の21日目です。
このご時世には信じがたい話ですが、去年まで Adventar はスマホで見ると PC View を縮小するだけで、スマホでは非常に使いづらいサービスでした。今年のシステムリニューアルでは機能追加したりや見た目を変える余裕はなかったのですが、さすがに恥ずかしい、アクセスも iOS が一番多いという言い訳のできない証拠があったので、モバイル対応だけはやることにしました。
レスポンシブと出し分け
モバイル対応(というかマルチデバイス対応)は、大きく分けて2つの手法があって、HTML は同一で、画面の大きさによって CSS を変更することで対応するレスポンシブ(Webデザイン)と言われる手法、ユーザーエージェントなどの情報を元に、スマホ用や PC 用などの HTML を出し分ける方法があります。
機能やレイアウトがガラっと変わるのであれば出し分けのほうがよいですが、スタイルの変更だけで要件がまかなえるのであればレスポンシブのほうが楽な場合が多いです。
個人的には、スマホで見たときと PC で見たときにコンテンツの位置が全然違ったり、PC にあった機能がスマホだとなくなっているという体験が嫌いなので、今回はレスポンシブで対応しました。
ブレイクポイントを決める
ブレイクポイントは感覚がよくわからないので Twitter Bootstrap を参考にしました。
https://getbootstrap.com/docs/4.3/layout/overview/#responsive-breakpoints
// Extra small devices (portrait phones, less than 576px) // No media query for `xs` since this is the default in Bootstrap // Small devices (landscape phones, 576px and up) @media (min-width: 576px) { ... } // Medium devices (tablets, 768px and up) @media (min-width: 768px) { ... } // Large devices (desktops, 992px and up) @media (min-width: 992px) { ... } // Extra large devices (large desktops, 1200px and up) @media (min-width: 1200px) { ... }
もともと Adventar の PC View は最大幅が 1000px で、992px, 1200px あたりは不要だったので、576px と 768px だけ採用し、以下の3パターンに絞りました。
サイズ | 幅 |
---|---|
Small | width < 576px |
Medium | 576px <= width < 768px |
Large | 768px <= width |
Small
Medium
Large
モバイルファーストで作る
PC 用のスタイルを画面の小さいスマホ向けに変更していくのはけっこう難しいのと、基本的に PC で開発するのでモバイルの確認が漏れがち、などの理由があり、モバイル向けの画面をプライマリで作り、そこから幅を広げた場合の画面を作っていくことにしました。
実装としては、メディアクエリに max-width
でなく min-width
を使うのがポイントです。例えば480pxをブレイクポイントにしてスタイルを変更するケースを考えてみます。max-width
を使うとこうです。
// PC 用 .btn { font-size: 20px; } // スマホ用 @media (max-width: 480px) { .btn { font-size: 14px; } }
min-width
の場合はこう。
// スマホ用 .btn { font-size: 14px; } // PC 用 @media (min-width: 481px) { .btn { font-size: 20px; } }
基本的には media query で元スタイルを上書きしていくほうがやりやすいので、min-width
を使うと自然と画面が小さいサイズ向けのスタイルがプライマリになります。
まとめ
UI をモバイル対応した話を書きました。明日は細かすぎて伝わらない UI の工夫について書こうと思います(ネタ切れです)。
Bugsnagを利用したエラートラッキング
Adventarを支える技術 Advent Calendar 2019 の20日目です。
今日はエラートラッキングについて書きます。
Bugsnag
エラートラッキングのサービスは色々あって、有名なのは Sentry や Airbrake あたりでしょうか。今回は Bugsnag というサービスを利用しました。これは Rails 時代から使っていて、無料で使える範囲が一番大きそう、という基準で選びました。
Bugsnag だと 250 events/day は無料枠で使えるので、Adventar ぐらいの規模であればスパイクしなければ余裕です。Sentry も今見たら無料で 5000 events/month なのでこっちでもいけそうな気がします(昔からこうだっけな)。
今回は Go の gRPC サーバー、Nuxt.js で SSR しているところに Bugsnag を使っています。フロントエンドの JS でも動くのですが、経験上フロントエンドでのエラートラッキングはノイズが多く、無料枠を食いつぶしてしまう可能性がありそうだったので今回は入れていません。もしかしたらそんなことはなくて、意外とさくっといける可能性はあります。
Rails では何もはまらずに使えていたのですが、Go と Node.js に有効にするのにけっこう苦労したので、それについて書いておきます。
Go/gRPCでの利用
Bugsnag の Go SDK はいくつかのフレームークに対応してますが、gRPC はありませんでした。
https://docs.bugsnag.com/platforms/go/
なのでOther Go appsを見て自力でどうにかする必要がありそうです。
Go のサーバーでエラーをトラッキングしたいのは主に
- 予期せぬエラーになった場合
- panic で死んだ場合
の2つで、この場合に
bugsnag.Notify(err, ctx)
を呼べばよさそうです。最終的には以下のようなコードを Interceptor に刺しこみました。
func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (_ interface{}, err error) { defer func() { if r := recover(); r != nil { err = grpc.Errorf(codes.Internal, "Internal Server Error") fmt.Printf("%s\n", r) if bugsnagAPIKey != "" { bugsnag.Notify(fmt.Errorf("%s", r), ctx) } } }() resp, err := handler(ctx, req) s, _ := status.FromError(err) if s.Code() == codes.Unknown { stacktrace := fmt.Sprintf("%+v\n", err) fmt.Print(stacktrace) if bugsnagAPIKey != "" { bugsnag.Notify(err, ctx, bugsnag.MetaData{"info": {"stacktrace": stacktrace}}) } err = grpc.Errorf(codes.Internal, "Internal Server Error") } return resp, err },
今回エラーハンドリングには xerrors を使っているのですが、xerrors で wrap したエラーを投げると、bugsnag 上でのエラーが全部 *xerrers.Wrap
になるの困っています。どうにかしたいのですが、エラーの量もそこまで多くなくて困らないので放置しています。
また、スタックトレースの情報がわかりづらく、例えば以下のエラーは
以下の箇所で発生していますが、entry.go:118
がスタックトレースに表示されません。
この問題はメタデータとして、fmt.Sprintf("%+v\n", err)
を送信することで一時しのぎしています。これは以下の表示されます。
わかりやすい...。もう少しちゃんとしたやり方があると思うので直したいところです。
Node.js/AWS Lambdaでの利用
SSR している Lambda では express を利用しているので、ドキュメントに沿って導入してみたが動きませんでした。どうやら bugsnag へのエラー送信が終わる前に Lambda が終了してしまうことが原因みたいだったようです。Issueもありました。
https://github.com/bugsnag/bugsnag-js/issues/495
エラーを送信する処理(bugsnagClient.notify
)を自分で実行して、その終了を待てばよさそうなのですが、必要な情報を自分で詰めないといけないのでややめんどうでした。例えば express plugin を使えばこのあたりでやってくれます。
とりあえず最低限の情報だけ詰めて対応しました。
app.use((err, req, _, next) => { const opt = { request: { headers: req.headers, httpMethod: req.method, url: req.url } }; // このコールバックでエラー送信の終了を待つ bugsnagClient.notify(err, opt, () => { next(err); }); });
また、もう一つの問題は source maps です。
https://docs.bugsnag.com/platforms/javascript/source-maps/
ブラウザの JS であれば、sourceMappingURL
に書いてある URL に取りに行ってくれるので便利ですが、Node.js の場合は source maps のファイルを別途 Bugsnag にアップロードする必要があります。がんばればできそうですが、今回はめんどうなので諦めました。いつかやるかもしれません。
まとめ
今回は Bugsnag を導入するのにはまったことなどを書きました。実際 Bugsnag はかなり役に立っていて、Bugsnag のおかげでいくつかのエラーを特定して潰すことができました。いくつか中途半端になっているところがあるので今後改善していきたいと思います。
明日は UI のスマホ対応について書きます。