Storybook.js (Vue) 文档模板输出

Storybook.js (Vue) Docs Template Output

使用 StoryBook.js,当我导航到一个组件,查看其“文档”并单击“显示代码”按钮时,为什么我得到的代码看起来像这样...

(args, { argTypes }) => ({
    components: { Button },
    props: Object.keys(argTypes),
    template: '<Button v-bind="$props" />',
})

...相对于此...

<Button type="button" class="btn btn-primary">Label</Button>

Button.vue

<template>
  <button
    :type="type"
    :class="'btn btn-' + (outlined ? 'outline-' : '') + variant"
    :disabled="disabled">Label</button>
</template>

<script>
export default {
  name: "Button",
  props: {
    disabled: {
      type: Boolean,
      default: false,
    },
    outlined: {
      type: Boolean,
      default: false,
    },
    type: {
      type: String,
      default: 'button',
    },
    variant: {
      type: String,
      default: 'primary',
      validator(value) {
        return ['primary', 'success', 'warning', 'danger'].includes(value)
      }
    }
  }
}
</script>

Button.stories.js

import Button from '../components/Button'

export default {
    title: 'Button',
    component: Button,
    parameters: {
      componentSubtitle: 'Click to perform an action or submit a form.',
    },
    argTypes: {
        disabled: {
            description: 'Make a button appear to be inactive and un-clickable.',
        },
        outlined: {
            description: 'Add a border to the button and remove the fill colour.',
        },
        type: {
            options: ['button', 'submit'],
            control: { type: 'inline-radio' },
            description: 'Use "submit" when you want to submit a form. Use "button" otherwise.',
        },
        variant: {
            options: ['primary', 'success'],
            control: { type: 'select' },
            description: 'Bootstrap theme colours.',
        },
    },
}

const Template = (args, { argTypes }) => ({
    components: { Button },
    props: Object.keys(argTypes),
    template: '<Button v-bind="$props" />',
})

export const Filled = Template.bind({})
Filled.args = { disabled: false, outlined: false, type: 'button', variant: 'primary' }

export const Outlined = Template.bind({})
Outlined.args = { disabled: false, outlined: true, type: 'button', variant: 'primary' }

export const Disabled = Template.bind({})
Disabled.args = { disabled: true, outlined: false, type: 'button', variant: 'primary' }

我以为我完全遵循了他们的指南,但我就是不明白为什么代码输出看起来不像我期望的那样。

我只是希望我的任何使用此组件的同事都能够从模板中复制代码并将其粘贴到他们的工作中,如果他们想使用该组件而不必小心他们 select来自代码输出。

对于遇到此问题的任何其他人,我发现这是一个 known issue for StoryBook with Vue 3

由于在撰写本文时我的项目目前是 green-field 项目,因此我通过将 Vue 降级到 ^2.6 来实施临时解决方法。

这对我来说没问题。无论如何,我正在使用选项 API 来构建我的组件,所以当 Storybook 解决了上述链接问题时,我会很乐意升级到 Vue ^3

一种可能的选择是使用我在 Simon K https://github.com/storybookjs/storybook/issues/13917 提到的 GH 问题中找到的当前解决方法:

.storybook 文件夹中创建文件 withSource.js,内容如下:

import { addons, makeDecorator } from "@storybook/addons";
import kebabCase from "lodash.kebabcase"
import { h, onMounted } from "vue";

// this value doesn't seem to be exported by addons-docs
export const SNIPPET_RENDERED = `storybook/docs/snippet-rendered`;

function templateSourceCode (
  templateSource,
  args,
  argTypes,
  replacing = 'v-bind="args"',
) {
  const componentArgs = {}
  for (const [k, t] of Object.entries(argTypes)) {
    const val = args[k]
    if (typeof val !== 'undefined' && t.table && t.table.category === 'props' && val !== t.defaultValue) {
      componentArgs[k] = val
    }
  }

  const propToSource = (key, val) => {
    const type = typeof val
    switch (type) {
      case "boolean":
        return val ? key : ""
      case "string":
        return `${key}="${val}"`
      default:
        return `:${key}="${val}"`
    }
  }

  return templateSource.replace(
    replacing,
    Object.keys(componentArgs)
      .map((key) => " " + propToSource(kebabCase(key), args[key]))
      .join(""),
  )
}

export const withSource = makeDecorator({
  name: "withSource",
  wrapper: (storyFn, context) => {
    const story = storyFn(context);

    // this returns a new component that computes the source code when mounted
    // and emits an events that is handled by addons-docs
    // this approach is based on the vue (2) implementation
    // see https://github.com/storybookjs/storybook/blob/next/addons/docs/src/frameworks/vue/sourceDecorator.ts
    return {
      components: {
        Story: story,
      },

      setup() {
        onMounted(() => {
          try {
            // get the story source
            const src = context.originalStoryFn().template;
            
            // generate the source code based on the current args
            const code = templateSourceCode(
              src,
              context.args,
              context.argTypes
            );

            const channel = addons.getChannel();

            const emitFormattedTemplate = async () => {
              const prettier = await import("prettier/standalone");
              const prettierHtml = await import("prettier/parser-html");

              // emits an event  when the transformation is completed
              channel.emit(
                SNIPPET_RENDERED,
                (context || {}).id,
                prettier.format(`<template>${code}</template>`, {
                  parser: "vue",
                  plugins: [prettierHtml],
                  htmlWhitespaceSensitivity: "ignore",
                })
              );
            };

            setTimeout(emitFormattedTemplate, 0);
          } catch (e) {
            console.warn("Failed to render code", e);
          }
        });

        return () => h(story);
      },
    };
  },
});

然后将这个装饰器添加到preview.js:

import { withSource } from './withSource'

...

export const decorators = [
  withSource
]