BASE株式会社 Owners Experience Frontend チームのパンダ( @Panda_Program )です。 BASE では BASE の UI を構築するための社内コンポーネントライブラリ「BBQ」を使ってフロントエンドの開発をしています。 BBQ は Vue2 + Storybook v5 で作成されています。現在、フロントエンドの有志たちで Storybook のバージョンを最新の v6.2 にする対応をしています。 この記事では、Vue2 + Storybook v5 のコンポーネントを v6 向けに書き換える方法を紹介します。 なお、本記事ではStorybook v6 自体の機能の説明や、 main.js や preview.js の書き方といった Storybook の環境構築の方法には触れません。 Storybook コンポーネントを v5 から v6 に書き換える ここでは button-group を v5 から v6 に書き換えた例を紹介します。 まずは v5、v6 の書き方をそれぞれご覧ください。その後、変更点をそれぞれ解説をしていきます。 v5 の button-group.stories.js // bbq/stories/elements/button-group.stories.js import { action } from '@storybook/addon-actions' import { number, select, text, withKnobs } from '@storybook/addon-knobs' import { storiesOf } from '@storybook/vue' import { withInfo } from 'storybook-addon-vue-info' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' import README from '../../elements/buttonGroup/README.md' import { devices } from '../../values/Devices' // Storybook コンポーネント名 const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) buttonStories // addon-knob .addDecorator(withKnobs) // addon-info .addDecorator(withInfo) .add( 'ButtonGroup' , () => { return { components: { ButtonGroup } , // Vue Template template: ` <div : class = "'theme-'+device" > <p> <h2>デフォルト</h2> <div>アイコン+ラベル</div> <bbq-button-group :tag= "tag" :items= "iconsAndLabels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>アイコン</div> <bbq-button-group :tag= "tag" :items= "icons" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>ラベル</div> <bbq-button-group :tag= "tag" :items= "labels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> </p> <p> <h2>カスタムUI</h2> <bbq-button-group :tag= "tag" :items= "['a','b', 'c', 'd']" @change= "({index}) => change(index)" :selected= "selected" :width= "width" > <template v-slot= "{items, change}" > <button v- for = "(item, index) in items" @click= "change(index)" > {{ item }} : {{ index }} </button> </template> </bbq-button-group> </p> </div> `, // Data data() { return { device: select(`device`, devices, 'pc' ), selected: number( 'selected' , 0), width: select( 'width' , [ '' , 'full' ] ), } } , // Props props: { tag: { default : text( 'tag' , 'ul' ) } , } , // Computed computed: { icons() { return [{ icon: 'list' } , { icon: 'grid' }] } , labels() { return [{ label: 'ドラッグで並び替え' } , { label: '数値で並び替え' }] } , iconsAndLabels() { return [ { icon: 'list' , label: 'リストで並び替え' } , { icon: 'grid' , label: 'グリッドで並び替え' } , { icon: 'attentionCircle' , label: '念で並び替え' } , ] } , } , // methods methods: { change: action( 'change' ), } , } } , // Parameters { notes: README, } ) v6 の button-group.stories.js // bbq/elements/buttonGroup/button-group.stories.js import { devices } from '../../values/Devices' import ButtonGroup from './ButtonGroup' import README from './README.md' export default { // Storybook コンポーネント名 title: "V6/Elements/ButtonGroup/Vue" , // import したコンポーネントを指定 component: ButtonGroup, // parameters parameters: { notes: { README } , docs: { extractComponentDescription: ((_, { notes } ) => notes?.README) } } , argTypes: { // addon-knob の select で定義していた変数 device: { options: devices, defaultValue: devices [ 0 ] , control: { type: "select" } } , width: { options: [ "" , "full" ] , control: { type: "select" } } , // addon-action で定義していた関数 change: { action: 'changed' } } } ; const Template = (args, { argTypes } ) => ( { components: { ButtonGroup } , props: Object .keys(argTypes), template: ` <div : class = "'theme-'+device" > <div> <h2>デフォルト</h2> <div>アイコン+ラベル</div> <bbq-button-group :tag= "tag" :items= "iconsAndLabels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>アイコン</div> <bbq-button-group :tag= "tag" :items= "icons" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> <div>ラベル</div> <bbq-button-group :tag= "tag" :items= "labels" @change= "({index}) => {this.selected = index; change(index)}" :selected= "selected" :width= "width" /> </div> <div> <h2>カスタムUI</h2> <bbq-button-group :tag= "tag" :items= "['a','b', 'c', 'd']" @change= "({index}) => change(index)" :selected= "selected" :width= "width" > <template v-slot= "{items, change}" > <button v- for = "(item, index) in items" @click= "change(index)" > {{ item }} : {{ index }} </button> </template> </bbq-button-group> </div> </div> ` } ); export const Default = Template.bind( {} ) Default.args = { // Default コンポーネントに与える Props selected: 0, tag: "ul" , icons: [{ icon: 'list' } , { icon: 'grid' }] , labels: [{ label: 'ドラッグで並び替え' } , { label: '数値で並び替え' }] , iconsAndLabels: [ { icon: 'list' , label: 'リストで並び替え' } , { icon: 'grid' , label: 'グリッドで並び替え' } , { icon: 'attentionCircle' , label: '念で並び替え' } , ] , } ; なお、 devices の定義は const devices = ['pc', 'sp'] です。 Storybookv6の変更点 上記、新旧ファイルの変更点を抜粋してコードを比較します。 Storybook 上のコンポーネント名 Storybook で表示されるコンポーネント名の定義方法の変更点です。 // v5 import { storiesOf } from '@storybook/vue' const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) // v6 export default { title: "Elements/ButtonGroup" , // ... } v6 では @storybook/vue を import する必要がなくなりました。その代わりに、default export するオブジェクト内にコンポーネントのメタ情報を記述します。 表示するコンポーネントを指定 コンポーネントを指定する箇所も変更になっています。 // v5 import { storiesOf } from '@storybook/vue' import { ButtonGroup } from '../../elements/buttonGroup/button-group.vue' const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) buttonStories.add( 'ButtonGroup' , () => { return { components: { ButtonGroup } , // ... } } ) // v6 import ButtonGroup from './ButtonGroup' export default { // ... component: ButtonGroup, } v6 ではコンポーネントを default export するオブジェクトのプロパティに追加します。 parametersを定義する箇所の変更 v5 では add メソッドの第三引数だった parameters の定義箇所が、v6 では default export するオブジェクトに変更になりました。ここではマークダウンファイル README.md を v6 で読み込める書き方を紹介します。 // v5 import { storiesOf } from '@storybook/vue' import { withInfo } from 'storybook-addon-vue-info' import README from '../../elements/buttonGroup/README.md' const buttonStories = storiesOf( 'Elements/ButtonGroup' , module) buttonStories .addDecorator(withInfo) // decorator で addon-vue-info を活用している .add( 'ButtonGroup' , () => { ... } , { notes: README } ) // v6 import README from './README.md' export default { // ... parameters: { notes: { README } , docs: { extractComponentDescription: ((_, { notes } ) => notes?.README) } } , } v6 では Storybook 上の Docs タブで README.md を表示できます。このため、 @storybook/addon-notes 、 storybook-addon-vue-info は不要になります。 今回は v6 のコンポーネント内で定義しましたが、 preview.js に以下のように記述すると Storybook の全コンポーネントに parameters が追加されるため、 docs を各ファイルで記述すること避けられます( 「Migrating from notes/info addons」 )。 // preview.js import { addParameters } from '@storybook/client-api' ; addParameters( { docs: { extractComponentDescription: ((_, { notes } ) => notes?.README) } , } ); なお、今回は既存資産を活かすためにマークダウンファイルをそのまま使いましたが、 MDXを用いる方法も公式で紹介されています。 addon-knob の書き換え v5 では addon-knob を使うと Storybook 上でコンポーネントに与える値を画面上で変更できました。 v6 では addon-essentials に含まれている controls を使えば同様のことができます。 以下では knob の number 、 select 、 text 関数を書き換えています。 // v5 import { number, select, text, withKnobs } from '@storybook/addon-knobs' import { devices } from '../../values/Devices' buttonStories. add( // ... data() { return { device: select(`device`, devices, 'pc' ), selected: number( 'selected' , 0), width: select( 'width' , [ '' , 'full' ] ), } } , props: { tag: { default : text( 'tag' , 'ul' ) } , } , } ) // v6 import { devices } from '../../values/Devices' export default { // ... argTypes: { // select 関数で作成していた値 device: { options: devices, defaultValue: devices [ 0 ] , // 初期値の設定 control: { type: "select" } // この行は省略可能 } , width: { options: [ "" , "full" ] , control: { type: "select" } // この行は省略可能 } , } ; const Template = (args, { argTypes } ) => ( { ... } ); export const Default = Template.bind( {} ) Default.args = { selected: 0, // number 関数で作成していた値 tag: "ul" , // text 関数で作成していた値 } ; select 関数の代わりになる control: { type: "select" } で定義した値は、 defaultValue で初期値を設定できます。 なお、 export default の中で定義している device 、 width は以下のように Default.args で定義することも可能です。 // v6 Default.args = { device: devices, width: [ "" , "full" ] , selected: 0, tag: "ul" , } ; (参考: Dealing with complex values ) addon-actions の action 関数の書き換え addon-actions を使ったダミーのコールバック関数の定義方法も変更になりました。 // v5 import { action } from '@storybook/addon-actions' buttonStories .add( // ... () => { // ... methods: { change: action( 'changed' ), } , } } ) // v6 export default { argTypes: { change: { action: 'changed' } } } ; (参考: addon-actions ) ただし、以前のように action 関数を用いても問題なく動作するため、書き換えは必須ではありません。 コンポーネントに渡すデータの定義の変更 v5 で記述していた data, props, computed で定義していた値を Storybook の画面上で自由に変更したい場合は、 argTypes や args に集約可能です。 // v5 buttonStories .add( // ... () => { // ... // Data data() { return { device: select(`device`, devices, 'pc' ), selected: number( 'selected' , 0), width: select( 'width' , [ '' , 'full' ] ), } } , // Props props: { tag: { default : text( 'tag' , 'ul' ) } , } , // Computed computed: { icons() { return [{ icon: 'list' } , { icon: 'grid' }] } , labels() { return [{ label: 'ドラッグで並び替え' } , { label: '数値で並び替え' }] } , iconsAndLabels() { return [ { icon: 'list' , label: 'リストで並び替え' } , { icon: 'grid' , label: 'グリッドで並び替え' } , { icon: 'attentionCircle' , label: '念で並び替え' } , ] } , } , // ... // v6 export default { // ... argTypes: { // addon-knob の select で定義していた変数 device: { options: devices, control: { type: "select" } } , width: { options: [ "" , "full" ] , control: { type: "select" } } , } ; // ... export const Default = Template.bind( {} ) Default.args = { selected: 0, tag: "ul" , icons: [{ icon: 'list' } , { icon: 'grid' }] , labels: [{ label: 'ドラッグで並び替え' } , { label: '数値で並び替え' }] , iconsAndLabels: [ { icon: 'list' , label: 'リストで並び替え' } , { icon: 'grid' , label: 'グリッドで並び替え' } , { icon: 'attentionCircle' , label: '念で並び替え' } , ] , } ; ただし、data や props、computed をそのまま残すことも可能です。その場合、 props 以外は GUI 上で値を変更できません。Storybook の GUI 上で変更したい値であれば、args で記述すれば良いと思います。 BASE BBQ の.Storybook では様々なパターンがあるため、まずは v6 の書き換えを優先しています。このため、data 等で定義している値は一旦 args に集約し、コンポーネントごとの細かい調整は個別に対応する予定です。 以上のパターンで BASE の BBQ で作成された Storybook コンポーネントの大抵のケースを網羅しています。 その他、より詳しい変更点は、 Storybook 6 Migration Guide をご覧ください。 addon について v6 で利用可能な Essential addons には、今までの主要な.addon の機能がまとめられています。 Docs Controls Actions Viewport Backgrounds Toolbars & globals Storybook で開発するにあたり、 @storybook/addon-essentials は開発体験を向上させてくれるため、v6 からは必須といっても過言ではないでしょう。 ここでは、先程紹介した例で使用している addon のみ取り上げます。 addon-knob v6 では deprecated addon-essentials の controls を代わりに使う addon-info knob と同様に v6 では deprecated addon-essentials の docs を代わりに使う addon-action v7 で deprecated になる予定 v6 ではまだ使えるが、可能なら control に置き換えると次のバージョンアップがスムーズになる v6 で addon-action を使いたい場合は、以下のように記述すれば OK です。 const Template = (args, { argTypes } ) => ( { // ... template: `...`, methods: { change: action( "changed" ), } } ); 表示を確認する v6 の書き方に変更したコンポーネントを実際にStorybook で表示すると以下のようになります。 StorybookのButtonGroup ンポーネント control で値を変更して様々な props の表示ケースを確認できるようになりました。 また、「Docs」というタブをクリックすると README が表示されています。 StorybookのButtonGroupコンポーネントのDocs これで v6 への書き換えが完了しました。 Storybook v6 で向上した開発体験 v5 と異なり、v6 では以下の点で開発体験が向上しました。 コンポーネントに与えるデータを Vue の外(args, argTypes)で定義できる args として定義した値は addon-knob を使わなくても Storybook 上で値を書き換えられる 上記の例では取り上げていませんが、args を変えることでコンポーネントのバリエーションを容易に作成できる( using-args ) インストールする addon の数や、stories ファイルのボイラープレートが減った おわりに 今回は Vue2 + Storybook v5 の環境で Storybook を v6 にアップデートする詳細な方法を紹介しました。 Storybook のバージョンアップにあたり本記事の内容が参考になれば幸いです。 多くの方はお気づきだと思いますが、v5 から v6 への書き換えといってもパターンが決まっています。 このため、手順さえ分かってしまえばプログラムで機械的に置換するだけで対応できます。 この考え方をもとに、TypeScript Compiler API を使ってメタプログラミングで v5 のコンポーネントを v6 に書き換えたので、次回の記事でその方法をご紹介しようと思います。