こんにちは。ForSchool事業部の石上(id:shgam)です。今回はStorybookの話です。
7/17にリニューアルしたStudyplus for Schoolでしたが、このとき導入したStorybookを活用できていませんでした。そもそも整備が足りてなかったので、1日もらってStorybookを整理しました。
社内esaとの重複もありますが改めてチームメンバーへの共有も兼ねて、このブログ記事を書いています。
背景
コンポーネントカタログとして導入してあるStorybookでしたが、リリースに向けて忙しくなるうちに整理が後回しになり、ちゃんと活用できない状態になってしまっていました。
作業の話を始める前に、なぜコンポーネントカタログが必要か、そしてどんな問題があったかを整理しておきます。
そもそもなぜStorybookが必要なのか
- Storybookのようなものがないと、画面を実装する際使えるパーツを探すのが難しく暗黙知に頼るしかなくなります。整理されたStorybookは、画面実装の助けになると思います。
- Wikiみたいなところにテキストドキュメントで整理しようとすると、実装との乖離が発生しやすく整理するのは難しいです。Storybookなら実装したものをそのまま置く形になるので、現実のコンポーネントが確認できます。
問題と原因
以下のような問題がありました。
- Storybookに追加されているけど使い回せないコンポーネントがある
- 原因:開発初期のコンポーネント分割のスキル不足(Atomic Designの理解が浅いままやってしまっていた)
- Storybookに追加されてないコンポーネントがある問題
- 原因:いちいち追加するのが面倒
- Storybookの認識が曖昧(何に使うものなのか、今どうなってるのか)
- 原因:ちゃんと整理されておらず、活用方法も特に共有していない
これらを解決するのをゴールに、修正をはじめました。
やったこと
Storybookに追加されているけど使い回せないコンポーネントがある
使い回せないコンポーネントがいくつかStorybookに存在してしまっていました。
本来なら適切にまとめる(たとえば無駄な分割がされたコンポーネントを親のコンポーネントに含める)ことが必要ですが、今回はStorybookの整理なので一旦Storybookから削除することにしました。
Storybookに追加されてないコンポーネントがある問題
いちいち追加するのが面倒だというのが明らかでした。そこで、コンポーネントをつくるときに必ずstoryが追加されるよう、コンポーネント生成コマンドを用意することにしました。
ただそれ以前に、各ストーリーがstories/index.stories.tsx
にべた書きされていてファイルが分割できていませんでした。
// .storybook/config.ts function loadStories() { require('../stories/index.stories.tsx'); } configure(loadStories, module); // stories/index.stories.tsx storiesOf('atoms', module) .add('Hoge', () => <Hoge />) .add('Fuga', () => <Fuga />) // これが延々と続く
これでは、コンポーネント生成のスクリプトからストーリーを追加するときに面倒です。
以下のように修正しました。
// .storybook/config.ts import { configure } from '@storybook/react'; import "../src/styles/global.scss"; const loaderFn = () => { const req = require.context('../stories', true, /\.tsx$/); req.keys().forEach(fname => req(fname)); }; configure(loaderFn, module); // atoms/Tag.ts storiesOf('atoms', module) .add('Tag', () => { return ( <Tag tag={{ id: "hogehoge", name: "タグ" }} /> ) })
stories以下はこうなりました。
$ tree stories/ stories/ ├── atoms │ ├── Card.tsx │ ├── DoughnutChart.tsx │ │_____ ... └── molecules ├── EllipsisDropdown.tsx ├── SortLabel.tsx ├── ...
これなら、コンポーネントを作るときにストーリーファイルを生成するのも簡単です。atoms/Hoge
を作るなら、stories下にも同じ名前で雛形ファイルを作ってあげればいいだけです。以下のスクリプトを用意しました。
const fs = require('fs'); const path = require('path') const generateFile = (pathname, filename) => { const absolutePath = path.resolve(__dirname, pathname); const filePath = `${absolutePath}/${filename}`; if (!fs.existsSync(absolutePath)){ fs.mkdirSync(absolutePath); } if (fs.existsSync(filePath)){ console.log(`Error: ${filePath} already exists.`); return; } fs.appendFile(filePath, "// created by generator.", function(err) { if (err) { return console.log(err); } console.log(`${absolutePath}/${filename} generated.`); }); }; const generateComponent = (componentLevel, componentName) => { const filenames = ['index.tsx', 'styles.scss', 'styles.scss.d.ts']; filenames.forEach(filename => { generateFile(`../src/components/${componentLevel}/${componentName}`, filename); }); }; const generateStory = (componentLevel, componentName) => { generateFile(`../stories/${componentLevel}`, `${componentName}.tsx`); }; const run = () => { const [processName, scriptName, ...options] = process.argv; const [componentLevel, componentName, ...undefinedOpts] = options; const validComponentLevels = ['atoms', 'molecules']; if (validComponentLevels.includes(componentLevel)) { generateComponent(componentLevel, componentName); generateStory(componentLevel, componentName); } else { console.log(`Error: コンポーネントレベルは${validComponentLevels.join(', ')}のいずれかにしてください`); } } run();
実行すると、必要なファイルが生成されるようになりました。
~/boron-web node scripts/componentGenerator.js atoms Hoge /Users/gaaamii/boron-web/src/components/atoms/Hoge/styles.scss.d.ts generated. /Users/gaaamii/boron-web/src/components/atoms/Hoge/index.tsx generated. /Users/gaaamii/boron-web/src/components/atoms/Hoge/styles.scss generated. /Users/gaaamii/boron-web/stories/atoms/Hoge.tsx generated.
Storybookの認識が曖昧(何に使うものなのか、今どうなってるのか)
今回のこのブログを読んでもらって、ちゃんと整理できたので活用していきましょうという感じにしていきたいです。社内のesaにも、補足があればどんどん書き足していきたいです。
ついでに:latest(5.2.3)に対応
ついでに、Storybookのバージョンも最新に上げました。
まとめ
以上、今回は4つの作業を行いました。
当たり前にやるべきことをできてなかったという感じなので、ここで整理できてよかったです。
せっかくコンポーネントを分けているので、他の人が画面を実装するときには「Storybook見ながらコンポーネント組み合わせたら実装できた!」みたいな体験になればいいなと思っています。