徒然技術日記

Object.prototype.__noSuchMethod__

使われていないCSSルールを検出する stylelint-no-unused-selectors を作った

github.com

背景

発端はあるツイートより.

たしかに CSS Modules は webpack の恩恵により,styled-components や emotion の場合にはその仕組みゆえに,どれも CSS の dead code elimination (使われていない CSS ルールが最終的な CSS ファイルに入らないようにすること) は自然と行われます.

一方で, CSS in JS なアプローチを取らない場合には往々にして challenging なものとなります.既存のツールとしては UnCSS, DropCSS, PurifyCSS, PurgeCSS などがあるものの,以下のような理由から自作することにしました.

  • コンポーネントごとに CSS ファイルを分けて書いている場合 に使いづらいものが多かった
    • 入力として HTML しか対応していないもの (UnCSS, DropCSS), アプリケーション全体で使われていない CSS を検出するもの (PurifyCSS, PurgeCSS) など微妙にユースケースが合わなかった
    • コンポーネントごとに1対1で対応するテンプレートファイルと CSS ファイル間でチェックしたかった
  • jsx, tsx, CSS Modules, classnames にも対応したい
  • コードを書いているときにリアルタイムで警告されてほしい
    • stylelint の仕組みに乗っかるのが簡単そう

というわけで,以下のようなコンポーネントがある場合に,FooComponent.css と FooComponent.jsx のクラスを比較して, jsx 側で使われていないクラスがあったときに警告を出す stylelint プラグインである stylelint-no-unused-selectors が完成しました.

FooComponent
├── index.js
├── FooComponent.jsx
└── FooComponent.css

使い方

stylelint と stylelint-no-unused-selectors をインストール.

yarn add -D stylelint stylelint-no-unused-selectors

その後 .stylelintrc に設定を追加すれば動くようになります.詳しくは README を参照してください.

{
  "rules": {
    "plugin/no-unused-selectors": true
  }
}

stylelint のプラグインがエディタで有効になっていれば,未使用の CSS を発見した場合に以下のようなエラーが出るはずです.

stylelint-no-unused-selectorsによりエディタで未使用CSSが検出されている様子

ターミナルで実行するとこんな感じ.

ターミナルでstylelintを実行している様子

コード解析には @babel/parser と typescript を使っているので,TC 39 stage 4 未満の機能を使っている場合や,TypeScript で書かれている場合もサポートしています.

ちなみに600近くのコンポーネントがある某プロダクトでも試してみましたが,問題なく動作しました.

技術的な話

AST を本格的に扱うのは初めてだったので,どれから始めていいのかわからず途中まで acorn で作ってから後で @babel/parser (babylon) に移行するなど迷走してました. この界隈は玄人向けなのかあまりドキュメントがなく,かといって AST の仕様書やコードを直接読むのはかなりしんどいので,AST Explorer でいろいろいじくりまわして雰囲気で進めるのがよさそう.

JS/TS と比べると PostCSS 界隈はさらに情報が少なく,既存のプラグインのコードから挙動を推測する感じだったのが辛かったです...

おわりに

publish してから時間がなかなか取れずに記事を書けないでいたところ,なんと PostCSS の公式 Twitter が紹介してくれたのにはとても驚きました.

しっかりしたドキュメントを書くことの重要性を改めて感じました(なかなか面倒臭がってしまいますが...).記事も英語版をどこかに投稿しないと,と思っています.

主に自分たちの課題を解決するために作り始めたプラグインですが,もし便利に思っていただける方がいましたらとても嬉しく思います!

web.dev の Fast load times を読んだ

パフォーマンス改善のための基礎知識をつけるべく,Chrome Dev Summit 2018 で紹介された web.dev に書かれているドキュメントのうちパフォーマンスに関するもの

web.dev

を一通り読んだので,簡単なまとめとして読書メモを書いた.

デブサミには幸運にも会社から派遣メンバーとして選ばれて参加したが,全体的にパフォーマンスに関する話題が多く,Google としてもパフォーマンス向上に力を入れていることが強いメッセージとして伝わってきた. web.dev はその啓蒙活動の一環としてドキュメントと Lighthouse の Web 版が提供されている.

Discover performance opportunities with Lighthouse

  • lighthouse は metrics (現在のページのパフォーマンスの状態を表す指標) と opportunities (パフォーマンスの改善ポイント) を提供する
  • web.dev または Chrome の DevTools から実行できる
  • スコアについての詳細は https://developers.google.com/web/tools/lighthouse/v3/scoring に書かれている
  • https://web.dev/measure から Profile にサイトを登録することで, daily に Lighthouse のレポートを測定してくれるようになる

Use Imagemin to compress images

  • imagemin で適切に画像を圧縮する話
  • ビルド時に毎回圧縮するのは無駄が多いので,画像追加時に一回だけやりたい
    • PR 時に適切に圧縮されているかどうか調べたい
    • GitHub Actions でチェックできるかも?

Replace animated GIFs with video for faster page loads

  • ffmpeg で gif を webm/mp4 に変換して video タグに置き換えればファイルサイズがずっと小さくなる(例では1/10になっている)

Use lazysizes to lazyload images

  • 画像を lazyload することについて
  • Intersection Observer の活用を頑張らなければ...

Serve Responsive Images

  • viewport の大きさごとに最適なサイズの画像を配信することについて
  • sharpImageMagick を使うことで画像のリサイズが可能
  • img 要素の srcset 属性で width descriptors を使うことでブラウザに画像サイズを伝えることができる
    • ブラウザ側が最適な大きさの画像を取得してくれる
    • バイスの解像度に応じて画像を変える density descriptors もある
  • width descriptors を使う場合は sizes 属性も同時に使う必要がある
    • 画像が表示される大きさをブラウザに伝える
  • picture 要素を使うとより細かい制御が行える

Serve images with correct dimensions

  • 適切な画像サイズをどう判断したら良いかについて
  • まずは不適切なサイズの画像を探す: Lighthouse の "Properly size images" audit で調べられる
  • Good approach
    • 絶対単位で大きさが指定されている場合
      • 表示されるサイズにリサイズする
    • 相対単位で大きさが指定されている場合
      • どのデバイスでも適切に表示されるサイズにリサイズする.デバイスのスクリーンサイズは GA などから判断できる
  • Better approach
    • 絶対単位で大きさが指定されている場合
      • srcsetsizes 属性を使ってデバイスの解像度に応じた適切な画像が配信されるようにする
        • おそらく density descriptors のことを指している
    • 相対単位で大きさが指定されている場合
      • srcsetsizes 属性を使ってスクリーンサイズに応じた適切な画像が配信されるようにする
        • おそらく width descriptors のことを指している

Use WebP images

  • webp は 25-35% ファイルサイズが小さく,YouTube ではページ読み込みが 10% 早くなった
  • cwebp または Imagemin で生成可能
  • picture 要素を使って配置する
  • Lighthouse の "Serve images in next-gen formats" audit で調査可能

Apply instant loading with the PRPL pattern

  • critical なリソースを preload する
    • <link rel=preload>
    • リソースのリクエスト優先度をあげることができる
  • First Paint をできる限り早く render する
  • Service Worker を用いてアセットを pre-cache する
  • 必要ないリソースは lazyload する
    • Code splitting することで JS を lazyload できる
      • 重要度の高い chunk は preload の設定をする
    • 画像も lazyload できる

Preload critical assets to improve loading speed

  • <link rel=preload> で優先度を上げられる
    • ブラウザがリソースを発見するのに時間がかかるものの first paint に重要なリソースに対して使うと効果が高い
      • 例: @font-face に指定されるリソースは CSS のパースが終わらないとブラウザが認識できない
    • webpack は /* webpackPreload: true */ を使うと preload を挿入できる
  • <link rel=prefetch>: 現在のページのリクエストが全て完了した後に,次のページ遷移を高速化するための準備を定義できる
    • /* webpackPrefetch: true */ もある

Reduce JavaScript payloads with code-splitting

  • Code-splitting について
  • route もしくは component レベルで splitting するのがシンプルな方法

Remove unused code

  • Lighthouse の "Unused JavaScript" audit で実行されなかったコードを解析できる
  • webpack-bundle-analyzer でバンドルに含まれるモジュールを可視化できる
  • import する対象を限定したり,ライブラリ自体を使わないようにすることでコード量を減らせる

Minify and compress network payloads

  • Minification
    • UglifyJS
  • Data compression
    • gzip, brotli
    • 大抵 CDN がやってくれることが多いはず
    • Dynamic compression
      • リクエストされたタイミングで圧縮する
        • シンプルだがレスポンスを返すのに余計な時間がかかる
    • Static compression
      • 事前に圧縮しておく
        • BrotliWebpackPlugin, CompressionPlugin などの webpack plugin がある

Serve modern code to modern browsers for faster page loads

  • @babel/preset-env
  • <script type=module>
    • preset-env で targets: esmodules: true を設定すると ESM が使えるブラウザのみをターゲットにできる
    • type=module をセットすれば ESM を解釈できるブラウザのみを対象にファイルを配信できる

Avoid invisible text during font loading

  • "flash of invisible text" の代わりに "flash of unstyled text" を目指す
  • font-display: swap; を使う
    • 対応ブラウザが限られる
  • フォントファイルが読み込まれるまで system font を適用しておき,読み込まれたことをトリガーにして custom font を当てるようにする

Using the Chrome UX Report to look at performance in the field

  • CrUX はオプトインしたユーザーから収集した実際の First Contentful Paint (FCP), DOM Content Loaded (DCL), First Input Delay (FID) のデータセット
  • CrUX Dashboard, PageSpeed Insights, BigQuery の3つ手段でデータにアクセスできる
  • CrUX Dashboard
    • Data Studio を使ったパフォーマンスの時間変化を可視化できるツール
  • PageSpeed Insights
    • URL を指定すると直近30日間のデータを集約して表示する
    • API もある
  • BigQuery
    • 自前でいろいろ分析したい時に

Using the CrUX Dashboard on Data Studio

  • Data Studio の Community Connectors を基に作られているツール
  • https://g.co/chromeuxdash にページの origin URL を入れれば dashboard を作成できる
  • dashboard は FCP, デバイス分布,接続状況分布から構成されている
  • FCP
    • Fast (<1s), Average (1~2.5s), Slow (>2.5s) の比率が表示される
  • バイス分布
  • 接続状況分布
    • 4G, 3G, 2G, Slow 2G, Offline の比率が表示される
  • これ以外のメトリクスや国ごとのデータなどを見たい場合は BigQuery を使う必要がある

Using the Chrome UX Report on PageSpeed Insights

  • PageSpeed Insights は Lighthouse と CrUX の結果を一度に見られるようにしたもの
  • Lab data (Lighthouse の結果) と Field data (CrUX のデータ) に分かれて表示される
  • Field data には FCP と FID が Fast, Average, Slow に分けて表示される
    • FCP: Fast (<1s), Average (1-2.5s), Slow (>2.5s)
    • FID: Fast (<50ms), Average (50-250ms), Slow (>250ms)
    • 入力したページのデータの他に, origin の結果(そのページが属する origin の全てのページのデータを集約したもの)も表示される
    • PSI のデータは過去30日のもの.これは月単位でデータを集約する BigQuery とは集計の仕方が異なっているので注意する必要がある
  • PSI は以下の利点がある
    • ページ単位でのパフォーマンスが見られる唯一のツール
    • 日単位でデータを集計してくれる唯一のツール
    • API がある
  • その代わり過去のデータは見られないし,メトリクスも FCP と FID しかない

Using the Chrome UX Report on BigQuery

  • SQL を使って生データを解析できる
  • 国ごとのデータ,月ごとのデータがテーブルとして提供されている
  • 無料の範囲は1ヶ月あたり 1TB で,それ以降は $5/TB の料金がかかる

Performance budgets 101

  • performance budgets
    • パフォーマンスに影響するメトリクスに対する制限
    • 設定した制限が,機能追加・デザイン・技術選定・ライブラリ選定を行う際の判断基準の一つになる
  • 定量的なメトリクス
    • 開発の初期段階で有効
      • サイズが大きくなるのを防ぐことができる
    • 画像サイズ,Web フォントの数,JS のサイズ,外部リソースの数, etc.
    • これらはユーザーエクスペリエンスを直接表していないので注意する必要がある
      • 例えばサイズが小さくても critical path のスクリプトの読み込み優先度が低かったらユーザーになかなか内容が表示されなくなってしまう
  • Milestone timings
  • ルールベースのメトリクス
    • Lighthouse, WebPageTest が提供するスコア
  • どうやって目標値を決定するのか?
    • まずは計測してみる.競合のサイトのスコアも計測してみよう.
    • もしそうした調査にかける時間がない場合には,TTI 5秒以下,critical path のリソースが 170KB 以下というのが一つの目安
      • これは 3G の通信環境を考慮に入れて考えられた数字
  • performance budget の値はページごとに異なりうる
  • 使えるツール
  • 継続的に数値を追うのがよい

Your first performance budget

Performance Budget の決め方について

  1. 最も重要なページがどこかを決める
  2. そのページのメトリクスを Lighthouse で測定する
  3. 競合のメトリクスを調査する
  4. 10サイトくらいは調査した方がいい
  5. milestone timings の budget を決める
  6. 20% ルールが有効
    • レスポンスタイムに 20% の違いがあるとユーザーが気がつくという研究に基づく
  7. initial はまず現在の値から 20% 改善を目指し,最終的には競合より 20% よい値を目指す
  8. 他の種類の budget を決める
  9. 定量的メトリクス
    • 一般的に critical path のリソースは 170KB 以下に保つべき
      • 低スペックで 3G な環境でも快適になる
      • 4G をターゲットにするならもう少し余裕ができる(表を参照
    • コンテンツの種類によっても変わってくるので,最終的な値はその辺りを考慮して決める必要がある
      • e-commerce で画像が多いなら JS の制限をきつくするなど
  10. ルールベースのメトリクス
    • Lighthouse で 85 点以上など
  11. 優先度を考える
  12. ニュースサイトであれば FCP, 検索サイトであれば TTI など重要なメトリクスはサイトによって異なる
  13. CrUX で競合サイトの状況を見てみるのもよい

Incorporate performance budgets into your build process

Performance Budget をビルドプロセスに組み込む方法について

  • Webpack Performance Hints
    • 非圧縮のサイズなのに注意
    • ただし圧縮は転送速度を早めるだけで,特にモバイルで顕著に影響する parse の速度を早めることはないので,非圧縮のサイズを気にすることも大事
  • Bundlesize
    • CLI で使えるほか,CI に組み込める
    • gzip, brotli, none から圧縮方法を選択して,そのサイズに対する budget を設定できる
  • Lighthouse Bot
    • Travis でしか使えないが,budget に違反している PR を block できる

Using bundlesize with Travis CI

  • Travis で bundlesize を使う方法
    • このページは正直 bundlesize の ReadMe を見れば充分 😗

Using Lighthouse Bot to set a performance budget

  • Lighthouse Bot を使う方法
    • localhost ではなくどこかのサーバーに deploy した方がリアルなデータが得られる

リーダブルコードを読んだ

社会人になる前に読もうと思って春休みに購入し,そのまま積んでいたリーダブルコードを今更ながら読んだ.

よく新卒になる前に/なったら読んでおけという本のリストに必ずといっていいほど入っている本であるが,結論から言うとやはりもっと早く読んでおくべきだったと若干後悔している.

以下読書メモ.

第1章: 理解しやすいコード

  • 理解しやすいコードとは何か?
    • 他人が最短時間で理解できるコード
    • 簡潔なコードが理解しやすいとは限らない
  • 自分は入社してから「意図が伝わりやすいコード」を心がけるようになった
    • 「他人が最短時間で理解できるコード」と同じような感じかもしれない
    • コードに加えて,コミットメッセージ・コミットの内容も「意図が伝わる変更」を意識している
      • 例えば:
        • バグ修正の時は先に fail するテストをコミットしてから修正のコミットをする
        • リファクタリングでは各コミットでテストが通ることを確認する(その旨を PR のメッセージも書いておく)

第2章: ネーミング

第3章: ネーミングその2: 誤解されない名前の付け方

  • check が曖昧なのはよく知られているが,他にも例示されていたので勉強になった
    • filter -> select / exclude: JS の Array#filter があるので意識したことがあまりなかったが,言われてみると確かに曖昧
    • clip -> truncate: truncate はなかなか思いつかない単語なので脳内リストに入れておきたい
    • first/last の last は inclusive だが begin/end の end は non-inclusive なのは知らなかった
    • ブール値の値に is/has をつけるのは意識しているものの, 否定形の単語を避ける のは意識していなかったので注意したい
      • 自分の観測範囲では日本人が書くコードで is + 形容詞 + 名詞 となっているのをよく見かける.これはなぜなのか...
        • 形容詞によっては意味が通ることもあるが,コードで出てくる場合はだいたい叙述用法 is + 名詞 + 形容詞 が適切な場合が多い

第4章: コードの美しさ

  • この章は形式的な美しさのみを扱っている
  • 今日では formatter が解決する部分が多い
    • 書かれている内容が実現できない場合もあるものの(縦で揃えるなど),しばしば bikeshedding な議論になるので formatter に任せるのが便利
  • コードを意味段落ごとに改行する,宣言を意味ごとにまとめる,などは割とやっているかも

第5章: コメントに書くべきこと,書いてはいけないこと

  • コメントには「自分の考えを記録する」
    • 5.2. 定数にコメントをつける
      • これは意識できていなかったので注意したい
      • 確かにコメントがないとその定数を変えるのが難しくなる...
    • 5.3. 全体像のコメント,要約コメント
      • 個人的にはこの辺りは適切なネーミング(ファイル名,クラス名,変数名)とメソッドへの分割で担保したいと考えている部分も大きい
      • どちらがいいのかは自分の中でまだ結論が出ていない...

第6章: 簡潔なコメントを書く方法

  • 最近はコメントを英語で書いているので,ここの部分は原著で読んだほうがよかったかもしれないと思った
  • 6.4. 関数の動作を正確に記述する
    • 下手すると関数の中身を逐一説明することになりそうなのでいいコメントが書けるようになるには修練が必要そうに感じた
  • 6.5. コーナーケースの例を書く
    • ユニットテストで例示する方法もありかなと思った
      • コメントに例を書く方法では常に正しい例なのかどうかが検証できないが,テストコードではその心配がない

第7章: 制御フローを読みやすくする

  • if condition の順序,if/else の順序,ネストの深さ,early return など
    • この章の内容は馴染み深いものだった

第8章: 式の分割

  • 8.1, 8.2. 説明変数,要約変数
    • 今までも無意識にやっていたけれど,概念に名前がつくと意識できてよい
    • 機能追加が重なっていくと複雑な if になることが結構あるので,意識的に変数で説明的にしていきたい
  • 8.5. if の条件を考えるのに「反対から考える」と条件が簡潔になることがある
    • e.g.) 2つのオブジェクトが「重なる条件」→「重ならない条件」を考える
    • 数学の「余事象」に似ていると思った
      • コーディングしているときは大抵「やりたいことをそのまま表現できるコードを」と思って書いているので,余事象を考えるのは割と盲点だった

第9章: 変数と読みやすさ

  • 不要な変数の削除,変数のスコープを小さく保つこと, const を使うこと
    • JS に const が導入された当初はいわゆる「定数」のみ const で定義することが多かったように記憶しているけれど,ここまで「必要がなければ const で定義する」ことが広まったのは感慨深い
    • Object.freeze and Object.seal syntax がもし入ればそれを使うことが一般化するのだろうか...?

第10章: 無関係の下位問題を抽出する

  • プロジェクト固有のコードから汎用コードを分離することについて
    • utils/ に切り出すようなイメージ
  • 「無関係の下位問題」という概念を初めて知った
    • これまで結構やらかしてそうで怖い
  • 気になった箇所 (p.137, 強調は自分):

make_url_friendly() はどこに置けばいいのだろう? 汎用的な関数なので util/ ディレクトリに入れてもいいと思う.でも,この正規表現アメリカのビジネス名だけを対象にしているので.元のファイルと同じ場所に置いたほうがいいかもしれない. 大切なことじゃないので,あとで決めてもいいだろう.

「どこにファイルを置くか」というのは 大切な問題 と思う.ビジネスの特定ドメインに紐つく処理なのか,そうではなくもっと汎用的な処理なのかは厳密に区別されるべきで,これが曖昧になるとアプリケーションの規模が大きくなった時にどこに何があってどれがどれに依存しているのか判断しにくくなってしまう.

おそらく原著がドメイン駆動設計の「再発見」より前に書かれていることと,本の流れとしてファイルを置く場所について言及すると脱線気味になってしまうことがさらっと流している理由かなと思うものの,自分は 大切なことじゃない という意見には共感できなかった.

第11章: タスク単位での分解

  • コードをタスク単位で小さく分割し,ブロックやメソッドをタスク単位で記述することについて
    • だらだらと処理が書かれているコードはなんとなく読みにくいと感じていたが,それは「複数のタスクが入り混じっているコードだから」と腑に落ちた
    • 今後わかりにくいコードを見つけたらタスク単位で分解するのを試してみたい

第12章: コードに思いを込める

  • 行いたい処理の内容を簡潔な言葉で説明することでリファクタリングを行う方法について
    • 頭の中だけでは考えきれない場合にノートに図などを書いて整理することはやっていたものの,リファクタリングに対してはやってみたことがなかった
    • リファクタに限らずコードが複雑になりそうな場合は言葉で説明してみるというのは有用そうに思った

第13章: 短いコードを書く

  • いかにコードを書かないで済ませるか?という話
    • 要求を正確に捉えて,無駄な処理を実装しない
      • 要は YAGNI の原則を説明したものと理解した
    • 言語機能,ライブラリなどで使えるものはないかを意識する
      • 新しく使う言語,慣れていない言語では重要そう

第14章: テストと読みやすさ

  • コード改善の実例: テストコードの改善方法について
    • テストコードの読みやすさを気にかける余裕がなかなかないのが辛いところ...
    • 「一つの巨大なテストケース」は生み出したことがあるので反省(この前ちょうど1つ改善した)

Ginger を使った校正用ルールなど,英語向けの textlint ルールをいくつか作ってみた

最近ではちょっとしたものを公開する場合でも,英語でコミットメッセージや ReadMe を書くのが主流になってきているように感じます. その風潮に乗って一生懸命英語を書くわけですが,三単現を忘れたり,a/the が適当だったりと,残念な英語しか書けない自分の英語力のなさが露呈して恥ずかしい限りです.

そうした恥ずかしい文章の公開を未然に防ぐべく,自動的に校正を書けてくれるツールとして azu さんが作られている textlint があります.これは,自然言語に対する ESLint のような linter であり,Atomプラグインなどを入れれば,エディタで書いているそばからおかしい部分を指摘してくれるようになるというすぐれものです.

ルールを探して Collection of textlint rule を覗いてみたところ,どうやら英語向けのルールは少々手薄のようだったので,いくつかルールを作成してみました.

textlint-rule-ginger

一時期有名になった英語校正ツール Ginger で校正を行うルールです.自動修正にも対応しています.

f:id:Nodaguti:20160317171951p:plain

textlint-rule-spellchecker

OS ネイティブのスペルチェッカーを使用してスペルチェックを行うルールです.使っている OS によって NSSpellChecker, Hunspell, Windows 8 Spell Check API.aspx) のどれかが使われます.

これも自動修正に対応しており,修正候補が1つしか出てこない場合にのみ修正します.

内部的には,Atom に付属しているスペルチェッカーである spell-check プラグイン が使っているのと同じパッケージ node-spellchecker を使用しています.

これだけ Atomプラグイン上ではエラーが出てしまったため,スクリーンショットコマンドラインのものです.(おそらく spell-checktextlint-rule-spellchecker とで競合してしまったのでしょう)

f:id:Nodaguti:20160317172003p:plain

textlint-rule-write-good

azu さん謹製のルール textlint-rule-rousseaurousseau と同じような校正ツール write good で校正を行うルールです.

一応スター数的には write good の方が人気のようです(rousseau: 76 stars, write good: 1853 stars).ちなみに私はどちらも知りませんでした.

f:id:Nodaguti:20160317172009p:plain

textlint-rule-no-dead-link

これは英語に関係ないですが,デッドリンクがないかどうかチェックするルールです.主に Markdown で使用することを想定していますが,プレーンテキストでも正規表現によって URL っぽい部分を抜き出してチェックします.

f:id:Nodaguti:20160317171958p:plain

作ってみて

意外と簡単に書けるというのが率直な感想です.既存の適当なルールからソースをコピペしてきて,ちょちょっと変えるだけでルールが完成します.textlint-rule-helper や textlint-tester など,ルールを書くための周辺パッケージも揃っているため,気軽にルールとテストを書くことができます.

ハマった部分としては,textlint-rule-spellchecker にて TravisCI テストを OSX 上で動かそうとして NodeJS のインストールに詰まった とか,textlint-rule-no-dead-link にて HEAD リクエストを飛ばす際に gzip を OFF にしないと Node 5.x で時々エラーが出る とかですが,まあ本質的ではないので詳細は割愛します.

まとめ

もっとみんなルールを書いて textlint を盛り上げていこう!

Nightmare v2.1.0 以降では type() を文字入力のために使ってはいけない

github.com

TL;DR

  • Nightmare は v2.1.0 から type() の挙動が変わったので,文字列入力のために用いてはいけない.
  • 使用する場合は,キーボードイベントの発火が完了したことが保証されないので,適切に wait する必要がある.

詳細

Nightmare の type(selector, text) は文字の入力を行うための基本的な API の一つであり, 公式サイトのサンプルでも一番初めにこのような例が出てきます:

var Nightmare = require('nightmare');
var vo = require('vo');

vo(function* () {
  var nightmare = Nightmare({ show: true });
  var link = yield nightmare
    .goto('http://yahoo.com')
    .type('input[title="Search"]', 'github nightmare')
    .click('.searchsubmit')
    .wait('.ac-21th')
    .evaluate(function () {
      return document.getElementsByClassName('ac-21th')[0].href;
    });
  yield nightmare.end();
  return link;
})(function (err, result) {
  if (err) return console.log(err);
  console.log(result);
});

Search という title 属性がついた入力欄に github nightmare という文字列を入力するため, type 関数を使用しています.

しかし,type() 関数の挙動が,一週間前くらいにリリースされた v2.1.0 から変化しており上記例の通りにテストを書いたところハマってしまったので,注意喚起も込めて書きます.

以前の実装では,要素の value プロパティに与えられたテキストを代入するという実装でした.以下は v2.0.9 時点での type() 関数の実装です.

exports.type = function(selector, text, done) {
  debug('.type() %s into %s', text, selector);
  this.evaluate_now(function (selector, text) {
    var elem = document.querySelector(selector);
    elem.focus();
    elem.value = text;
    elem.blur();
  }, done, selector, text);
};

(https://github.com/segmentio/nightmare/blob/d203ec44deac766d7ea4dd7d1f1713441770f21b/lib/actions.js#L120 より引用)

この実装では keyup などのキーボード関連イベントが発火しないという問題があり,Electron の sendInputEvent を使うようにする pull request により v2.1.0 では以下のように実装が変わりました.

exports.type = function(selector, text, done) {
  debug('.type() %s into %s', text, selector);
  var child = this.child;

  this.evaluate_now(function (selector) {
    document.querySelector(selector).focus();
  }, function() {
    child.once('type', done);
    child.emit('type', text);
  }, selector);
};

(https://github.com/segmentio/nightmare/blob/6afdc411d1d8d979624870caa4e57e2922c2037e/lib/actions.js#L120 より引用)

type イベントが発火されると,Nightmare は Electron に対して sendInputEvent というイベントを発行します.

この sendInputEvent というのがくせもので,こいつは Electron の renderer に対して「イベントを発行せよ」という通信は行うものの,どうやら実際に発火したかまでは待たないようです.すなわち,上記のコードに変化したことによって,type() 関数では文字列が確実に入力されたかどうかが保証されなくなりました

実験

実際,type に長い文字列を渡してみると,入力しきれない場合が出てきます. 挙動説明のために以下のサンプルアプリを作成しました. npm test すると Nightmare を使ってテストを実施します.

github.com

テキストエリアに入力して Submit すると,その内容が上に表示されるというだけの簡単なアプリケーションですが,1000 文字入力しようとするだけで type が追いつかず,テストが失敗してしまうことが分かります.

  1) nightmare can type a long string into a textarea:

      AssertionError:   # test/index.js:21
  
  assert(message === MESSAGE)
         |       |   |       
         |       |   "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
         |       false       
         "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
  
  --- [string] MESSAGE
  +++ [string] message
  @@ -878,123 +878,4 @@
   7890
  -12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789

環境によってはもっと短い文字(10文字程度)でも発生することがあり,例えばログインページのテストでは ID が最後まで入力できなくてログインが成功したりしなかったりするなど,影響はかなり大きいと言えます.

対処

v2.0.9 以前の element.value = text 方式であれば,同期的にテキストを入れるのでこのようなことは起こりません.

幸い,Nightmare は Nightmare.action() を使うことでカスタムアクションを定義できますので,v2.0.9 方式を再現する以下のようなアクションを追加すれば問題ありません.

Nightmare.action('populate', function(selector, text, done) {
  debug('populate() %s into %s', text, selector);
  this.evaluate_now(function(selector, text) {
    var element = document.querySelector(selector);
    element.value = text;
  }, done, selector, text);
});

また,この件については Issue に登録し,上記アクションを追加する Pull Request も投げておきました.うまく行けば次のバージョンでは populate アクションが追加されるでしょう.

github.com

結論

  • v2.1.0 以降の type() は,キーボードショートカットやインクリメンタルサーチのテストなど,キーイベントを発火したいときに使うべきで,文字入力には適さない.
  • 以前の挙動に戻すカスタムアクションを定義して,そっちを使うべき.
  • 運がよければ次バージョンで文字入力のためのアクションが追加されるかも.

3ヶ月間インターンシップに参加して実感したドワンゴの魅力

f:id:Nodaguti:20151224192227j:plain

10月から12月にかけてドワンゴインターンに行ってきました.

参加の経緯は

nodaguti.hatenablog.com

をご覧ください.

なお,この記事は ドワンゴ Advent Calendar第2のドワンゴ Advent Calendar の記事ではありません.枠争奪戦に敗北しました.

インターンの経過

10月〜11月

面接での希望通り,ニコニコ動画を開発しているチームに配属となりました!

元々は JavaScript をバリバリ書いてプレイヤーを改善していくぞ!💪💪💪 という意気込みでいたのですが,タイミング悪くプレイヤー関連では特に大きなタスクがなかったらしく,11月末あたりまでは総合トップのデザイン変更という案件に関わらせていただきました.

https://blog.nicovideo.jp/2015/11/post_268.php

「デザインの変更」というと CSS (とせいぜい DOM)の変更が主という印象を受けるかもしれませんが,実際は

  • PHP フレームワークのバージョンアップと,それに伴うテンプレート・CSS・JS の全書き換え
  • 新しい情報が出る部分のサーバ側(PHP)実装とキャッシュ構築

という非常に大規模な案件でした.

このうち,デザイン,static な DOM 構築および CSS はデザイナーの方が担当され,私は

  • 既存の総合トップとその周辺の PHP をコードリーディング
  • 新規枠向けの PHP 実装 (70%ほど)
  • ページのベースとなるテンプレートを作成するなど,テンプレートの「継承」における基礎部分を構築
  • デザイナーの方が構築された DOM をテンプレートに繋ぎこみ (70%ほど)
  • JavaScript の実装(検索窓, タブなど)

など,インターン生にも関わらずかなりの部分を担当させていただきました.

さらに,初めてコードレビューを「する」側としてレビューに参加させていただきました.新卒ですらないのにコメントしていいのだろうかと思いながら,これまで独学で学んできた謎の美意識に基づいて好き勝手書かせていただきました.そうしたレビューを許容してくださったチームメンバーの懐の広さにとても感謝しています.

まだまだ細かなコーディングスタイルやネーミングなど狭い視野でのコメントにとどまり,なかなかコード全体から設計などを考えることができないと実感させられました.

また,社員の方からのレビューコメントや,口頭での議論を通して,「よりよい設計とは何か?」ということを深く考えるきっかけを得られました.もっともっと精進していきたいです.

得たもの

ニコニコ動画の歴史ある重厚なコードを存分に読むことができ,(RC) の頃から愛用しているサービスの中身を垣間見れたことがとても貴重な経験でした.

AB テストリリース直前にはいろいろバグも出してしまい,ご迷惑をおかけしてしまったのですが,大人数がアクセスする総合トップに関わることができ,実際にユーザーの方の反応が得られた時には,感無量でした.

なお,PHP は「あらゆることをトップレベルの関数で行う」という部分が生理的に合わず,これまで避けてきたのですが,今回ついに触ることになりました.

その感想としては,http://qiita.com/nori0620/items/08bba8649fa5b608f695 という感じです.おそらく趣味ではお世話になることはないかと思います,PHP さんありがとうございました.

12月

総合トップの AB テストリリースが終わった12月からは,とある案件のビルド・テスト環境の構築を行いました.

具体的には, gulpfile.babel.js や webpack.config.js を書きまくるタスクです.CA の時にはこれらのファイルはすでに存在していましたし,趣味プログラミングでも本格的に構築したことはなかったため,非常に勉強になりました.一つ一つのパッケージを調べて導入していったことで,ビルドやテスト周りについては基礎知識がついたように思います.

テスト環境周りを構築していた時に,espower-babel のバグを発見して Pull Request を送ったり(人生初の OSS へのコミット),electron-mocha を Gulp から使えるようにする gulp-electron-mocha というパッケージを作成したり(人生初の npm パッケージ公開)しました!

これからも積極的にプロダクトを作っていきたいという気持ちが高まりました.

勤務環境

一言で言うと,「自由」です.そして,その自由さが最大の魅力だと感じました.

ドワンゴでは午前10時は早朝」 という言葉を以前読んだことがあったものの,正直インターンを開始するまでは半信半疑でした.しかし,実際は「午前11時でも早朝」なのでは?という程の人のいなさ加減でした(おそらくチームによっても異なるのだとは思いますが).ちなみに社員の方は「ドワンゴには DST (Dwango Standard Time) というものがあってね...」と話していましたw

また,社内に葉っぱが生えていたり,机の上がダンボールハウスと化していたり,机に芝生が生えていたり,天井からメイド服や謎の提灯がぶら下がっていたりと,かなりカオス自由な環境だったのが特徴的でした.この辺りは以下の記事の写真をご覧いただくと雰囲気がより伝わりやすいのではないかと思います.

http://hrnabi.com/2014/07/25/2102/hrnabi.com

b.hatena.ne.jp

また開発環境については,ディスプレイこそ DELL の22インチディスプレイでしたが,PC は MacBook Pro 13-inch, Early 2015, メモリ 16GB,すなわち最新の MBP (!) を支給していただき,とても快適にプログラミングをすることができました.

おわりに

今回のインターンは前回と同じく週3勤務とはいえ3ヶ月間お世話になったこともあり,総合トップのデザイン変更という大きな案件に関わらせていただくことができました.

また,年末の忘年会にも参加させていただき,改めて,いつの間にかドワンゴは「イベント会社」になっていたのだなあと実感させられました.(なお,福引は外れました)

ディファ有明で行われた忘年会での一幕 (写真はカラオケ大会の様子)

3ヶ月間,目一杯ドワンゴを満喫させていただき,大変お世話になりました.本当にありがとうございました.

ドワンゴの独特な空気は,実際に体験してみてこそです.来年インターンをされる方にはぜひお勧めです!

サイバーエージェントにインターンに行ってきた話

f:id:Nodaguti:20151016003335j:plain

インターンが終わってからだいぶ間が空いてしまいましたが,

nodaguti.hatenablog.com

で書いたとおり9月いっぱいサイバーエージェントインターンに行ってきました.

ちなみにハッカソン(TECH CAMP)ではなく, 就業型インターン(弟子入り) の方です.

業務内容

www.amebaownd.com

アメブロと比べて残念ながら知名度が低めなサービス(自分もインターンに応募するまで知らなかった) Ameba Ownd のチームにフロントエンドエンジニアとしてジョインしました. フロントエンドだけで自分以外に7人もいるチームでした.全体では30人くらいだったと思います.

中身は CoffeeScript + Angular.js + Webpack + Jade + Stylus という構成で, 規模としては gulp build でフルビルドすると2-3分かかるくらいでした. というわけで,コーヒーコーヒーした1ヶ月間でした.

一方自分のスキルセットは

  • JavaScript は書ける.
  • CoffeeScript は食わず嫌いしてた.
  • Angular.js, Webpack, Jade, Stylus, Gulp, etc.: 名前は知っているけれど...
  • git: pull req したことない.

という惨状でした.

言ってみれば自分のスキルは10年くらい前のフロントエンジニアみたいな感じであり,最初のうちは割ときつかったです.

しかし,その分非常に勉強になったと感じています. はてブに流れてくるものを読むだけでなく,とりあえず手を動かしてフレームワークに触れてみることの重要性を実感しました.

より具体的に

環境構築が終わった段階で,まずはコードリーディングとウォーミングアップを兼ねた簡単めなタスクということで, ヘッダー部分のナビゲーションをウィンドウの幅にあわせて "More" に収納する部分の修正を割り振っていただきました.

しかしながら,その場の全員(自分含む)が軽いタスクだと見積もっていたこれが実は罠案件で, 最終的には該当部分の Controller を一からリライトし, かつ各テーマの DOM 構造および CSS を変更しなければならないという割と大変なタスクでした... 結局リリースできたのはインターン最終日あたりだったと思います.

他には,何件かのバグ修正と,詳しくは書けないですが新規ページの実装を行いました.

1ヶ月は長いかと思っていたのですが,他のバイトとの兼ね合いから週3での出勤だったこと, シルバーウィークがあったことなどから,実質的な出社日数は10日あまりでした. 期間が短かったことと,予想以上に1stタスクが伸びてしまった影響で新機能のリリースまでいけなかったのが残念です. (未完成状態で他のメンバに引き継ぐ形になりました)

また,自分がいた時期は,万行単位の機能が1週間に最低1つは投入されるような状況で, まわりが圧倒的スピードで開発しているのに気押されていたというのもあります. 逆に言うとよく自分なんかを受け入れてくれたなーという感じで, 忙しい合間をぬっていろいろアドバイスをくださったメンバの方々には感謝してもしきれないです.

インターンシップの環境について

MBP (機種がちょっと古めだったのと,キー配列がJIS配列だったのがちょっと残念だった) と Thunderbolt Display を貸与されました. 自宅のEIZO 24inchディスプレイ よりも遥かに快適だったので,やはりディスプレイは大きければ大きいほどよいという印象です.

無料のウォーターサーバーとドリンクサーバー(紙コップに飲み物が注がれるやつ)があり, 無限コーヒーと無限紅茶花伝で飲み物には困りませんでした. また,社内販売のお弁当があり,400円程度でおいしい弁当を食べられました. 1つ上の階にあった全品50円のアイス自販機でアイスを買いそびれたのが唯一心残りな点です :)

また,よく「サイバーの社員はリア充」と言われますが, 机にフィギュアが置いてあったり,Macに美少女キャラのステッカーが貼ってあったりという事象は 確かに観測できなかったので,ある意味エンジニアの方に対しても当てはまるのかもしれません.

人事の強力なバックアップ体制

インターンに関して,人事の方が強力にバックアップしてくださったのが特徴的でした.例えば:

  • 週1の面談

    人事の担当の方と毎週面談があり,目標,現状の課題,悩みなどを共有しました.

    こう聞くと面倒臭く感じるかもしれませんが,むしろ漫然と目の前のタスクをこなすのではなく, 現状の整理をこまめに行うことができ,明確な目的意識を持ってインターンに取り組めるという点で有意義でした.

  • インターン生交流ランチ・飲み会

    人事の方が他のインターン生や社員の方との飲み会やランチをセッティングしてくださり, 積極的に交流の機会を持つことができました.タダ飯最高!

最後に

最終日に Ownd チームの方全員から無事に修了したことをお祝いしていただきました. 賞状,フロントチームからの寄せ書き,高級そうなボールペンをいただきました.

f:id:Nodaguti:20151016003303j:plain

写真には写っていませんが賞状の裏にはメンターと人事の方からメッセージがびっしり書かれています. 短い間しかいなかったにも関わらずいろいろ用意してくださり,本当にありがとうございました!(ボールペンは早速愛用しています)

サイバーエージェントインターンは,cutting-edge な技術を用いたスピード感のある開発を経験できるのみならず, 他のインターン生や社員の方との人脈を広げることのできる貴重な機会だと思います.

来年インターンを考えている方は,ぜひサイバーエージェントにもエントリーしてみることをおすすめします!