storybook-chrome-screenshotとzisuiとStorycapと
先日、Storycap というCLIを公開しました。 Storybookの各storyをキャプチャして画像化するだけのツールです。主として、reg-suit のようなツールと組み合わせ、画像回帰テスト環境を構築することを目的としています。
使い方はとてもシンプルで、CLIの引数としてStorybookのURLを与えるだけです。
$ npx storycap https://storybookjs-next.now.sh/official-storybook
以下のように、Storybook自体の起動コマンドを渡すことも可能です。
$ npx storycap --serverCmd "start-storybook --ci -p 9009" http://localhost:9009
CLIのオプションのみで、ある程度の制御は可能なようにしてありますが、StorybookのAddonとして利用することで、より細かくキャプチャの条件を指定することもできます。
/* src/components/MyComponent.stories.js */
import React from 'react';
import MyComponent from './MyComponent';
export default {
title: 'MyComponent',
parameters: {
screenshot: {
viewport: {
width: 375,
height: 668,
deviceScaleFactor: 2,
},
},
},
};
export const normal = () => <MyComponent />;
viewport以外にも「キャプチャ時に特定の要素をhover状態にする」「特定の関数(アセットの読み込みなど)が完了するまでキャプチャを待たせる」といったオプションがあります。 詳細はREADMEみてください。
誕生の経緯
1年ほど前に、同じ機能をもつzisuiというNPMパッケージを作って公開したのですが、Storycapはその後継です。
Storycapの前身に当たるNPMパッケージはもう1つ存在していて、@wadackel が作成・公開しているstorybook-chrome-screenshot(以下SCS)です。
時系列でいうと、SCSの方がzisuiよりも前から存在していましたし、僕もzisuiを作成する前からSCSの存在は知っていました。
僕自身が仕事でStorybookを利用することとなり、reg-suitと組み合わせる目的でSCSを導入したものの、CIで動作させた際に時間がかなりかかる等、運用する上での課題が生じました。 当初はSCSそのものを改修してどうにか解消しようと試みたものの、性能を優先すると、いくつかの機能(僕自身は必要としていない)をドロップさせたり、根幹からの修正を余儀なくされる気配があったため、泣く泣く別のツールとして、zisuiを作成するに至った、という経緯があります。
zisuiを作ったはいいものの、もともとSCSへもcontributeしていた経緯もあるし、そもそも端から見たらまるで同じツールにしか見えないと思います。 僕自身、「車輪を再開発してしまった」という妙に後ろ暗い気持ちもあったりしたのですが、wadackel君と飲む機会があり、その際に「SCSとzisuiを統合したいよねー」と話したのが切欠で、統合することとなりました。
最初はSCSのv2という形で考えていたのですが、breaking changeを許容するのであれば、同じタイミングでパッケージ名も変えてよいのでは?という流れになり、SCSでもzisuiでもなく、Storycapという名前で生まれ変わらせることになりました。star数とか、Storybook本家からリンクされている、等の都合を考慮した結果、レポジトリはSCSのそれをそのまま利用し続けています(organizationはreg-vizに移管させた)。
機能的にはSCSとzisuiのお互いに足りないところを補うような感じで作っています。
実際、今回の改修にあたって、まずは機能比較を行って、何を活かして何を捨てるかをissueにまとめてから作業する形をとりました。
例えば下記などは片方にしか存在してない機能だったのですが、Storycapは両方を取り込んでいます。
- zisuiはホストされているStorybookでもキャプチャ可能だが、SCSはローカルのStorybook
- SCSは1つのstoryに対して複数viewportのキャプチャを取得可能。zisuiは1 storyにつき1枚のPNGしか作れない
最終的に切り捨てた機能というのは殆どないのですが、一点挙げるとすると、SCSがサポートしていたStorybookのaddon-knobs対応くらいでしょうか。
移行について
SCS、もしくはzisuiを使っているユーザーに向けては、Migration guideを用意しています。
zisuiから見た場合、 Storycapのコードそのものはzisuiをベースにしているため、パッケージ名を変える程度で済むと思います。
逆にSCSからの場合、CLIのオプションなど、breakしている箇所が幾つかあるため、少し手間かもしれません。
いずれにせよ「うまいこと乗り換えできない!」となったら、issueなりtwitterなりで一声掛けてくれればと。
ちょっと内側の話
僕がそもそもzisuiを作ろうと思った要因の1つとして、仕事でのCI実行時間を短縮したかったというのがあります。 実際、SCSからzisuiに置き換えた際、仕事で運用していたStorybookには500枚程度のstoryがあったのですが、置き換え前後で10分→3分程度の短縮を達成しています。
SCSの方が実行コストが高いのは理由があります。 「SCSは1つのstoryに対して複数viewportのキャプチャを取得可能。zisuiは1 storyにつき1枚のPNGしか作れない」が関係しています。
Storybookの実装上、storyに付随するパラメータ(e.g. どのようなviewportにするか)というのは、一々そのstoryを実行しないと取得できません。
- zisui: 「1 story: 1画像」と割り切っている。1つのstoryをvisitするたびにPuppeteerのscreenshotメソッドを実行する
- SCS: すべてのstoryをvisitして、parameterの回収を行う。その後、回収したparameterからscreenshot対象のstoryにvisitしてscreenshotメソッドを実行する
各storyが最低2度visitされる特性上、SCSは実行に時間が掛かるわけですが、1つのstoryからPCとスマホ両方のviewportに相当するスクショ、のような設定が実現できていました。 一方、1 storyにつき1枚の画像があれば十分な場合、これはただの二度手間なため、zisuiではSCSの柔軟さを切り捨てて性能を取ったわけです。
さて、今回の統合で、設定の柔軟さと性能、どちらを優先したかと問われれば、両方ともとっています。
SCSもzisuiも、「事前にキャプチャ対象の一覧を用意してから、順次キャプチャを撮っていく」という実装だったわけです。
擬似コードで書くと下記のような感じですね。
const stories = await getStoriesToScreenshot();for(const story of stories) {
await visitAndCapture(story);
}
「キャプチャを撮る際に、必要であれば2枚目以降のスクショをキューに積んでおき、再訪させる」というアプローチにすれば、storyを実行する回数を抑えつつ、SCSが持っていた機能も残すことができます。 こういうのはgeneratorが便利ですね。上記のコードにおける stories
部分をgeneratorに置き換えるだけです。
const requests = await getStoriesToScreenshot();function* stories() {
while (requests.length > 0) {
yield requests.shift();
}
}const push = requests.push;for(const story of stories()) {
await visitAndCapture(story, push); // 必要に応じて追加のcaptureリクエストをvisit時にpushする
}
実際のコードでは並列処理なども組み込んでいるため、もう少し複雑ですが、基本的にはこのパターンで実装しています。 generatorで書き換えたお陰で、キャプチャにおけるタイムアウト時のリトライ処理なんかも同じ仕組みに載せられるので、スッキリした感があります。 日頃は生でgeneratorのコード書く機会もないですが、この手のジョブ管理みたいなのを書くのは楽しいものですね。
おわりに
Storycap自体はつい最近publishしたばかりのパッケージですが、SCSやzisuiで培われたノウハウが存分に生かされているため、十分実用に耐えるレベルで使えると自負しています。導入自体は簡単だと思うので、興味がある人は是非試してもらえればと。