跳至主要内容

[storybook] Storybook 介紹與實作

說明

如果團隊要檢查各個成員之間更改了哪些程式碼,可以使用版本控制工具,例如:Git。

但今天如果是想要看到的是各個元件之間,或是畫面的變化,例如:按鈕樣式、頁面樣式等,就可以使用 Storybook,來講述你元件與畫面的故事。

Storybook 也支援 Markdown 語法,所以針對一些教學需要用文字來表達的話也可以。

在開始之前,可以先到官方的Showcase,體驗一下使用 Storybook 帶來的好處。

安裝

要使用 Storybook,必須先有一份 React 專案,這邊用 Vite 快速建立一份 React 專案。

npm create vite@latest 你的專案名稱 -- --template react && npm install

接著在該專案目錄底下安裝 Storybook:

npx sb init --builder @storybook/builder-vite
備註

安裝的時間會有點久是正常的

安裝成功後會看到專案多了 .storybook 資料夾,在 src 裡面也可以看到 stories 資料夾,裡面已經有預設的 stories 了,執行以下指令來開啟 Storybook:

npm run storybook

Image

建立 stories

現在可以建立我們的第一個 stories,在 stories 資料夾內新增一個名為 Box.jsxBox.stories.jsx 的檔案。

Box.jsx
import React from "react";const Box = () => {  const style = {    width: "200px",    backgroundColor: "cyan",  };  return <div style={style}>Box</div>;};export default Box;
Box.stories.jsx
import Box from "./Box";export default {  title: "Box",  component: Box,};export const largeBox = () => <Box />;

現在在畫面上可以看到 Box 底下有一個 Large Box,也就是剛剛建立的 stories。

Image

如果日後 stories 太多需要分類的話,只需要在 title 的地方做更改即可。

Box.stories.jsx
import Box from "./Box";export default {  //將Box分類到Component  title: "Component/Box",  component: Box,};export const largeBox = () => <Box />;

Image

PropTypes

如果要讓使用者能夠和 Component 互動的話,需要去定義 Component 的 PropTypes

Box.jsx
import React from "react";import PropTypes from "prop-types";const Box = () => {  const style = {    width: "200px",    backgroundColor: "cyan",  };  return <div style={style}>Box</div>;};export default Box;Box.propTypes = {  backgroundColor: PropTypes.string,  label: PropTypes.string,  size: PropTypes.oneOf(["sm", "md", "lg"]),  show: PropTypes.bool,};

重新整理後就會在 stories 的 control 中看到我們定義的 prop 。

Image

那要怎麼讓 Box Component 接收到這些 prop 呢?

一旦我們設定好 PropTypes 後,Box Component 其實就已經能接收到這些 prop 了。

Box.jsx
import React from "react";import PropTypes from "prop-types";const Box = ({ backgroundColor, label, size, show }) => {  console.log(backgroundColor, label, size, show);  const style = {    width: "200px",    backgroundColor: "cyan",  };  return <div style={style}>Box</div>;};export default Box;Box.propTypes = {  backgroundColor: PropTypes.string,  label: PropTypes.string,  size: PropTypes.oneOf(["sm", "md", "lg"]),  show: PropTypes.bool,};

打開 console 後,會發現只有label有值,其他的都是 undefined。

Image

這時候我們可以在 controls 的地方去選取 backgroundColor,size 的部分選擇 sm,show 的部分選擇 true。

Image

再看一次 console,就會看到我們的 prop 有成功輸出了。

Image

所以現在可以將傳入的參數帶進 Component 裡面並做一些判斷。

Box.jsx
import React from "react";import PropTypes from "prop-types";const Box = ({ backgroundColor, label, size, show }) => {  let scale = 1;  if (size == "sm") scale = 0.5;  if (size == "md") scale = 0.75;  const style = {    width: "200px",    padding: `${scale * 0.5}rem ${scale * 1}rem`,    backgroundColor,  };  return <>{show && <div style={style}>{label}</div>}</>;};export default Box;Box.propTypes = {  backgroundColor: PropTypes.string,  label: PropTypes.string,  size: PropTypes.oneOf(["sm", "md", "lg"]),  show: PropTypes.bool,};

Images

Default Props

如果我們希望 Component 在剛載入的時候就有預設 prop 的話,可以在 Component 中定義 defaultProps

Box.jsx
import React from "react";import PropTypes from "prop-types";const Box = ({ backgroundColor, label, size, show }) => {  let scale = 1;  if (size == "sm") scale = 0.5;  if (size == "md") scale = 0.75;  const style = {    width: "200px",    padding: `${scale * 0.5}rem ${scale * 1}rem`,    backgroundColor,  };  return <>{show && <div style={style}>{label}</div>}</>;};export default Box;Box.propTypes = {  backgroundColor: PropTypes.string,  label: PropTypes.string,  size: PropTypes.oneOf(["sm", "md", "lg"]),  show: PropTypes.bool,};Box.defaultProps = {  backgroundColor: "cyan",  label: "Hello I'm Box.",  show: true,  size: "lg",};

Image

建立多份 stories

現在可以建立基於 Box Component 的其他 stories,例如:尺寸較小的 Box(sm)、尺寸中型的 Box(md)。

我們可以先創建一個樣板(Template),樣板會接收到參數,之後將該參數傳至 Box Component,以此方式建立多份 story。

備註

💡 Template.bind({}) 是 標準的 JavaScript 技巧,用來複製函式。此技巧是用來讓每個輸出的 story 可以設定各自的參數,但使用完全相同的方法。

來源:https://storybook.js.org/tutorials/intro-to-storybook/react/zh-TW/simple-component/

Box.stories.jsx
import Box from "./Box";export default {  title: "Component/Box",  component: Box,};const Template = (args) => <Box {...args} />;export const largeBox = Template.bind({});largeBox.args = {  label: "Large Box",  size: "lg",};export const mediumBox = Template.bind({});mediumBox.args = {  label: "Medium Box",  size: "md",};export const smallBox = Template.bind({});smallBox.args = {  label: "Small Box",  size: "sm",};

Images

Controls

我們也可以自訂 control 的 type,像是在 label prop 的地方,我們想要呈現的是下拉式選單,並且有值能夠讓使用者自行選取。

Box.stories.jsx
import Box from "./Box";export default {  title: "Component/Box",  component: Box,  argTypes: {    label: {      options: ["選項1", "選項2", "選項3"],      control: { type: "select" },    },  },};const Template = (args) => <Box {...args} />;export const largeBox = Template.bind({});largeBox.args = {  label: "Large Box",  size: "lg",};export const mediumBox = Template.bind({});mediumBox.args = {  label: "Medium Box",  size: "md",};export const smallBox = Template.bind({});smallBox.args = {  label: "Small Box",  size: "sm",};

Images

Decorators

如果希望在每一個 story 底下的 Component 都能夠套樣相同的 CSS,則可以使用 Decorators 來達到該功能。

先建立一個 Center 的 HOC,在裡面撰寫置中的 CSS:

Center.jsx
import React from "react";const Center = ({ children }) => {  return (    <div      style={{        display: "flex",        justifyContent: "center",        alignItems: "center",      }}    >      {children}    </div>  );};export default Center;

之後打開 .storybook 資料夾內的 preview.jsx,直接 export decorators,decorators 內自帶參數 Story,可以把它想像成我們的所有 stories 底下的 Component。

所以現在是使用 Center HOC 將所有的 Component 包住,以此套用置中 CSS。

備註

如果是使用 vite 開發的話,preview 檔案的結尾記得要改成.jsx

preview.jsx
import Center from "../src/stories/Center";export const parameters = {  actions: { argTypesRegex: "^on[A-Z].*" },  controls: {    matchers: {      color: /(background|color)$/i,      date: /Date$/,    },  },};export const decorators = [  (Story) => (    <Center>      <Story />    </Center>  ),];

Image

Addon

我們也可以在 Storybook 專案內安裝額外的插件,這邊示範如何使用 addon-viewport 插件。

npm i @storybook/addon-viewport

接著打開 preview.jsx,在 parameters 的地方加上 viewport,並把 addon-viewport 的 INITIAL_VIEWPORTS 給帶入。

preview.jsx
import Center from "../src/stories/Center";import { INITIAL_VIEWPORTS } from "@storybook/addon-viewport";export const parameters = {  actions: { argTypesRegex: "^on[A-Z].*" },  controls: {    matchers: {      color: /(background|color)$/i,      date: /Date$/,    },  },  viewport: {    viewports: INITIAL_VIEWPORTS,  },};export const decorators = [  (Story) => (    <Center>      <Story />    </Center>  ),];

回到畫面,在上方的 size of preview 多了更多的尺寸讓我們去做選擇,方便進行測試。

Image

Chromatic

chromatic.yml
# Workflow name
name: "Chromatic Deployment"

# Event for the workflow
on: push

# List of jobs
jobs:
test:
# Operating System
runs-on: ubuntu-latest
# Job steps
steps:
- uses: actions/checkout@v1
- run: yarn
#👇 Adds Chromatic as a step in the workflow
- uses: chromaui/action@v1
# Options required for Chromatic's GitHub Action
with:
#👇 Chromatic projectToken, see https://storybook.js.org/tutorials/intro-to-storybook/react/en/deploy/ to obtain it
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}