4

简介

Storybook是一个UI组件的开发环境。

使用

初始化StoryBook环境

$ npx -p @storybook/cli sb init

storybook自动检测开发环境,安装依赖。

执行以上命令行会进行以下操作:

1. 自动生成以下目录结构:

├─.storybook // Storybook 全局配置文件
    ├─ main.js // 入口文件
    └─ preview.js // 页面展示、全局资源配置
└─stories // 示例代码
    └─assets

2. 更新pkg#run-scripts:

...
  "scripts": {
    "storybook": "start-storybook -p 6006 -h 0.0.0.0", // 启动本地服务以预览
    "build-storybook": "build-storybook"  // 构建
  },
...

核心文件

main.js

module.exports = {
  "stories": [  // 组件Stories目录所在 —— Storybook会载入配置路径下的指定文件渲染展示
    "../stories/**/*.stories.mdx",
    "../stories/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  "addons": [  // Storybook所用插件 —— Storybook功能增强
    "@storybook/addon-links",
    "@storybook/addon-essentials"
  ],
  "framework": "@storybook/vue3" // Storybook所用框架 —— Vue环境支持
}

该文件定义StoryBook与编译相关的配置。

preview.js

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  }
}

该文件引入全局依赖,定义StoryBook渲染相关的配置。

简单示例

入口配置

更新.storybook/main.js,将组件所在目录注册到入口文件声明中:

module.exports = {
  "stories": [  // 组件Stories目录所在 —— Storybook会载入配置路径下的指定文件渲染展示
    "../packages/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  ...
}

组件Story编写

import SubmitForm from "./index"; // 引入组件
import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";

const caseSchema = [ // 示例数据
  {
    key: "moduleName",
    name: "title",
    type: SchemaType.Text,
    label: "栏目名称",
    placeholder: "请输入栏目名称",
    attrs: {
      //
    },
    rules: [
      {
        required: true,
        message: "栏目名称必填~",
        trigger: RuleTrigger.Blur,
      },
    ],
  },
  ...
];

export default {
  title: "ui组件/SubmitForm", // 展示标题:使用路径定义命名空间 —— 分组、分类
  component: SubmitForm,
};

const Template = (args: any) => ({ // 渲染组件
  components: { SubmitForm },
  setup() {
    return {
      ...args,
    };
  },
  template: '<submit-form :schema="schema"></submit-form>',
});

export const 基本应用 = Template.bind({}); // 组件应用示例

(基本应用 as any).args = {
  schema: caseSchema,
  ref: "submitFormRef",
};

其中,

默认导出的是组件的元数据,包含归属组件、组件所属StoryBook文档分类、组件参数交互式声明...

更多配置参见:

ArgsTable配置

Contorl配置

命名导出的Story ( export const 基本应用 = Template.bind({}); ) 是一个函数,变量名为StoryBook文档展示的标题,另一种导出方式参考下文。

全局依赖配置

因为示例代码中依赖element-plus,通过上述展现的页面没有样式,所以,StoryBook渲染需要额外引入element-plus主题:

// .storybook/preview.js
import "element-plus/lib/theme-chalk/index.css";

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  }
}

启动本地服务

启动服务

更新pkg#storybook:

  // package.json
  "scripts": {
    "storybook": "start-storybook -p 6006 -h 0.0.0.0",
    "build-storybook": "build-storybook"
  },

命令行执行:

$ npm run storybook
效果展示

image

默认参数栏只展示两项,如需更多参数信息,修改 preview.js 文件:

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
  controls: {
    expanded: true // 展开所有参数信息
  }
}

image

在Stories中使用第三方UI库

以ElementPlus为例:

全局配置

如果 babel.config 没有配置按需加载,可直接编辑.storybook/preview.js

// .storybook/preview.js
import elementPlus from 'element-plus';
import { app } from '@storybook/vue3'

app.use(elementPlus);
export const decorators = [
  (story) => ({
    components: { story, elementPlus },
    template: '<elementPlus><story/></elementPlus>'
  })
];
import "element-plus/lib/theme-chalk/index.css";

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  }
}

Notes:配置按需加载后,import elementPlus from 'element-plus';导入elementPlus报错:elementPlus is not defined —— 全局加载、按需加载不能在同一项目中使用。

按需加载

在需要使用ElementPlus的Stories中直接引入即可:

// SubmitForm.stories.ts
import { ElButton } from 'element-plus';
import SubmitForm from "./index";
import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";

const caseSchema = [
  {
    key: "moduleName",
    name: "title",
    type: SchemaType.Text,
    label: "栏目名称",
    placeholder: "请输入栏目名称",
    attrs: {
      //
    },
    rules: [
      {
        required: true,
        message: "栏目名称必填~",
        trigger: RuleTrigger.Blur,
      },
    ],
  },
  ...
];

export default {
  title: "ui组件/SubmitForm",
  component: SubmitForm,
};

const Template = (args: any) => ({
  components: { SubmitForm, ElButton },
  setup() {
    return {
      ...args,
    };
  },
  template: '<submit-form :schema="schema" ref="submitFormRef"></submit-form><el-button @click="submit">提交</el-button>',
});
export const 基本应用 = Template.bind({});
(基本应用 as any).args = {
  schema: caseSchema,
};

补充已有示例交互

// SubmitForm.stories.ts
import { ElButton } from "element-plus";
import { ref } from "vue";
import SubmitForm from "./index";
import { SchemaType, RuleTrigger } from "./src/schemas/baseSchema";

const caseSchema = [
  {
    key: "moduleName",
    name: "title",
    type: SchemaType.Text,
    label: "栏目名称",
    placeholder: "请输入栏目名称",
    attrs: {
      //
    },
    rules: [
      {
        required: true,
        message: "栏目名称必填~",
        trigger: RuleTrigger.Blur,
      },
    ],
  },
  ...
];

export default {
  title: "ui组件/SubmitForm",
  component: SubmitForm,
};
const Template = (args: any) => ({
  components: { SubmitForm, ElButton },
  setup() {
    const { refName } = args;
    const submitFormRef = ref();
    function submit() {
      console.log(submitFormRef.value.values);
    }
    function onRuntimeChange(name: string, value: any) {
      console.log(name, " = ", value);
    }
    return {
      submit,
      onRuntimeChange,
      [refName]: submitFormRef,
      ...args,
    };
  },
  template: `
      <submit-form :schema="schema" :ref="refName" @runtimeChange="onRuntimeChange"></submit-form>
      <el-button @click="submit">提交</el-button>
    `,
});
export const 基本应用 = Template.bind({});

(基本应用 as any).args = {
  refName: "submitFormRef",
  schema: caseSchema,
};

这里做了两件事:

  • 增加提交按钮
  • 增加数据提交交互

配置参数文档

默认文档展示

默认查看到的文档是两栏展示:

image

更新 .storybook/preview.js 文件:

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
  controls: {
    expanded: true
  }
}

参数的所有配置都展示:

image

参数配置

通过配置argTypes可以补充参数信息:

// SubmitForm.stories.ts
...
export default {
  title: "ui组件/SubmitForm",
  component: SubmitForm,
  argTypes: {
    refName: {
      description: '表单组件引用',
      type: {
        required: true,
      },
      table: {
        defaultValue: {
          summary: 'defaultNameRef',
        }
      },
      control: {
        type: 'text'
      }
    },
    schema: {
      type: {
        required: true,
      },
      table: {
        type: {
          summary: '渲染表单所需JSON结构',
          detail: 'JSON结构包含表单渲染、交互所需要的必要字段,也包含表单的校验规则',
        },
        defaultValue: {
          summary: '[]',
          detail: `[
              {
                key: "moduleName",
                name: "title",
                type: SchemaType.Text,
                label: "栏目名称",
                placeholder: "请输入栏目名称",
                attrs: {
                  //
                },
                rules: [
                  {
                    required: true,
                    message: "栏目名称必填~",
                    trigger: RuleTrigger.Blur,
                  },
                ],
              }
            ]
          `
        }
      }
    },
    runtimeChange: {
      description: '实时监听表单的更新',
      table: {
        category: 'Events',
      },
    }
  }
};
...

更多相关配置见:

ArgsTable配置

Contorl配置

StoryBook功能模块

一个Story是以函数形式描述如何渲染组件的方式。

args提供动态参数,提供省时的便利:

  • 参数可在Controls面板实时编辑,可检测组件在不同参数下的状态。
  • 事件可在Actions面板查看日志输出。

    • 需要配置actions

自定义Stories展示名称

命名模块

export const 自定义名称 = () => ({
  components: { Button },
  template: '<Button primary label="Button" />',
});

【推荐】storyName属性设定

export const Primary = () => ({
  components: { Button },
  template: '<Button primary label="Button" />',
});
Primary.storyName = '自定义名称';

Args

提供交互动态修改Props、Slots、styles、inputs...的方式,允许在Storybook交互界面中实时编辑,不必改动底层组件代码。

通过Storybook交互界面指定Args

界面直接修改

通过URL设定Args

?path=/story/avatar--default&args=style:rounded;size:100

由于安全策略(XSS攻击)和特殊值,传入URL需要处理,见详情

全局依赖

// .storybook/preview.js
import { app } from '@storybook/vue3';
import Vuex from 'vuex';

//👇 Storybook Vue app being extended and registering the library
app.use(Vuex);

export const decorators = [
  (story) => ({
    components: { story },
    template: '<div style="margin: 3em;"><story /></div>',
  }),
];

静态资源访问

// .storybook/main.js

module.exports = {
  stories: [],
  addons: [],
  staticDirs: ['../public'],
};

// .storybook/main.js

module.exports = {
  staticDirs: [
     { 
        from: '../my-custom-assets/images', 
        to: '/assets' 
     }
  ],
};

查看更多配置

定制化主题

修改Logo

安装依赖:

npm i -D @storybook/addons @storybook/theming

修改pkg#scripts

// pkg#scripts
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public",

新建.storybook/manager.js文件:

import { addons } from "@storybook/addons";
import theme from "./themes/theme";

addons.setConfig({
  theme: theme
})

创建./storybook/themes/theme.js:

// .storybook/themes/theme.js
import { create } from '@storybook/theming';

export default create({
  base: 'light',
  brandTitle: 'Custom StoryBook', // logo不展示时,替代文本alt
  brandImage: '/logo.png',
});

Notes:brandImage和brandTitle同时配置的情况下,只有一项起作用,优先级brandImage > brandTitle

Notes:自定义主题时,base配置是必填的。

// package.json
{
  ...
  "scripts": {
    "replace": "rimraf storybook-static/favicon.ico && cpr .storybook/themes/favicon.ico storybook-static/favicon.ico",
    "storybook": "start-storybook -p 6006 -h 0.0.0.0",
    "build-storybook": "build-storybook && npm run replace"
  },
}

Notes:打包的话,需要用本地图标替换storybook包内的默认图标。

这里使用了Cli参数-s指定静态文件访问地址,更推荐在main.js中配置:

// .storybook/main.js

module.exports = {
  stories: [],
  addons: [],
  staticDirs: ['/public'],
};

修改站点Title、favicon

新增.storybook/manager-head.html:

<link rel="shortcut icon" href="/favicon.ico">
<script>
  var observer = new MutationObserver(function(mutations) {
    if (document.title.match(/Storybook$/)) {
      document.title = "M.UI | GameCenter";
    }
  }).observe(document.querySelector("title"), {
    childList: true,
    subtree: true,
    characterData: true
  });
</script>

参见更多配置

组件样式—Scss

组件样式需要storybook-addon的支持

npm i -D @storybook/preset-scss

修改.storybook/main.js

module.exports = {
  "stories": [
    "../packages/**/*.stories.@(js|jsx|ts|tsx)",
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/preset-scss"
  ],
  "framework": "@storybook/vue3"
}

Rem支持

替换@storybook/preset-scss为@storybook/addon-postcss:

npm uninstall -D @storybook/preset-scss
npm i -D @storybook/addon-postcss
# 修改webpack内核为版本5
# 初始化环境时修改
npx -y sb init --builder webpack5
# 初始化时未设定,后续修改
npm i -D @storybook/builder-webpack5
npm i -D @storybook/manager-webpack5
// 修改.storybook/main.js
module.exports = {
  "stories": [
    "../packages/**/*.stories.@(js|jsx|ts|tsx)",
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    {
      name: '@storybook/addon-postcss',
      options: {
        postcssLoaderOptions: {
          implementation: require('postcss'),
        },
        sassLoaderOptions: {
          implementation: require('sass'),
        }
      },
    }
  ],
  core: {
    builder: 'webpack5',
  },
  webpackFinal: (config) => {
    config.module.rules.push({
      test: /\.scss$/,
      use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
    });
    return config
  },
  "framework": "@storybook/vue3"
}

在preview.js中引入flexible.js,并定义视窗:

import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
import '../assets/js/flexible.all';
import './assets/stylesheets/sb.scss';
import '../assets/stylesheets/reset.scss';

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
  viewport: {
    viewports: INITIAL_VIEWPORTS, // newViewports would be an ViewportMap. (see below for examples)
    defaultViewport: 'iphone6',
  },
  controls: {
    expanded: true
  }
}

详细内容参见文章地址

Decorator自定义画布、文档样式

Decorator通过包裹Story来增强其表现形式。
作用域:全局 > Component > Story(按执行顺序排列)

// js示例
export default {
  title: 'YourComponent',
  component: YourComponent,
  decorators: [() => ({ template: '<div style="margin: 3em;"><story/></div>' })],
};
// mdx示例
<Meta
  title="YourComponent"
  component={YourComponent}
  decorators={[
    () => ({
      template: '<div style="margin: 3em;"><story /></div>',
    }),
  ]}
/>

Decorator可在.storybook/preview.js、组件声明、Story声明中定义,最终会合并,执行顺序为 Global > Component > Story,可通过定义ClassName的命名空间来定制样式。

更多配置: https://storybook.js.org/docs...

画布、文档的分离

默认文档中包含画布信息,若不想文档中渲染Story,需要写.stories.js和.stories.mdx两个文件
其中,.stories.js中默认导出不添加任何元数据,转移至.stories.mdx中,示例如下:

// *.stories.ts
export default {}
const Template = (args: any) => ({
  components: {
    Drawer
  },
  props: Object.keys(args),
  methods: {
    onClose: action('onClose'),
    onStretched: action('onStretched')
  },
  setup () {
    const state = reactive({
      ...args
    })
    function toogleVisible () {
      state.visible = true
    }
    return {
      state,
      toogleVisible
    }
  },
  template: `
    <Drawer v-bind="state" v-model:visible="state.visible" @close="onClose" @stretched="onStretched">
    </Drawer>
    <button @click="toogleVisible">切换可见性</button>
  `
})
export const Primary = Template.bind({}) as any
(Primary as any).args = {
  visible: false,
  title: '示例'
}
Primary.parameters = { docs: { disable: true } };


// *.stories.mdx
import { Meta, Story } from '@storybook/addon-docs'
import { Primary } from './Drawer.stories.ts';
import Drawer from './index'
export const argsType = {
  visible: {
    description: '是否显示Dialog,支持.sync修饰符',
    type: {
      required: true
    },
    table: {
      type: {
        summary: 'Boolean'
      },
      defaultValue: {
        summary: false
      }
      // category: 'Boolean' // 参数分组
      // subcategory: 'Button colors', // 子分组
    },
    control: 'boolean'
  },
  ...
}
# Drawer

## 基本用法
<Story
  name="Drawer"
   decorators={[
    () => ({
      template: '<div id="custom-root" style="background: red;"><story /></div>',
    })
  ]}
  story={Primary} />

## 自定义内容

## 参数文档

<Meta 
  title="组件/Basic/Drawer" 
  component={Drawer} 
  argTypes={{
    ...argsType
  }}
/>

parameters = { docs: { disable: true } }可在docs中禁止渲染Story
https://storybook.js.org/docs...
https://github.com/storybookj...
https://github.com/storybookj...

Notes:

  • mdx文件中jsx和markdown语法之间要用空行分隔;
  • jsx定义对象,尤其是空对象,不能有多余的空行;
export const argTypes = {

}
# 这样会报错

正确写法:

export const argTypes = {}

# 这样才能正确解析

侧边栏忽略子节点

若不想要侧边栏创建子节点,可以定义Story.storyName与export default的组件title保持一致:

// 示例
export default {
  title: '组件/Basic/Drawer'
}
export const Primary = Template.bind({}) as any
Primary.storyName = 'Drawer';

调整docs优先展示权

默认优先展示stories,可以通过优先展示docs

parameters = {
  docs: {
    disable: true
  },
  viewMode: 'docs'
}

https://github.com/storybookj...

隐藏Canvas

  previewTabs: {
    canvas: {
        hidden: true,
    }
  },

可以隐藏当前Stories的Canvas面板

修改Logo跳转地址

// .storybook/themes/theme.js
import { create } from '@storybook/theming';

export default create({
  base: 'light',
  brandTitle: 'StoryBook',
  brandUrl: '/?path=/docs/快速开始--primary',
  brandImage: '/logo.png',
});

关闭Addons

showPanel无论设置在哪一层级,都是全局的

parameters = {
  docs: {
    disable: true
  },
  controls: {
    disable: true
  },
  options: {
    showPanel: false
  }
}

MDX写法

动机

上述的写法为CSF,component story format,是 Storybook 官方推荐的一种基于 ES module 的 stories 编写方法,由一个 export default 和一个或多个 export 组成。

它是Storybook 提供了一些封装过后的组件供使用,让我们能够较为快速的生成 stories。

代价是灵活度会相对的没有高,当然,如果只是简单的展示组件及其接收参数,那其实已经完全足够了。

可如果在展示组件之余,还想要编写一个额外的文档,比如介绍一下组件封装的背景,用到的技术等,CSF 就不是那么好用了。

基于这样的需求,Storybook 也支持使用 MDX 格式编写 stories。

MDX,如同 TSX,就是一种能够在 Markdown 文档中写 JSX 的格式。使用 MDX 格式编写 stories,文字部分内容的编写会更加灵活,没有了官方预置的内容,真正的所写即所得。

MDX示例

import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs';
import { action } from '@storybook/addon-actions'
import { reactive } from 'vue'
import Dialog from './index'

# Dialog

export const argsType = {
  visible: {
    description: '是否显示Dialog,支持.sync修饰符',
    type: {
      required: true
    },
    table: {
      type: {
        summary: 'Boolean'
      },
      defaultValue: {
        summary: false
      }
      // category: 'Boolean' // 参数分组
      // subcategory: 'Button colors', // 子分组
    },
    control: 'boolean'
  },
  showCancel: {
    description: '展示独立的关闭按钮X',
    table: {
      type: {
        summary: 'Boolean'
      },
      defaultValue: {
        summary: true
      }
    },
    control: 'boolean'
  },
  title: {
    description: 'Dialog的标题,也可通过具名slot(见下表)传入',
    table: {
      defaultValue: {
        summary: '示例Dialog'
      }
    },
    control: 'text'
  },
  rawHtml: {
    description: 'dialog主体内容',
    table: {
      type: {
        summary: 'string / htmlString'
      },
      defaultValue: {
        summary: 'DJ小能手',
        detail: '<span style="color: red;">DJ</span>小能手'
      }
    },
    control: 'text'
  },
  confirm: {
    description: '确认相关的配置项目',
    mapping: {
      label: '确定按钮的文本内容',
      handler: '确认的回调'
    },
    options: ['label', 'handler'],
    table: {
      type: {
        summary: 'Object',
        detail: `
          confirm.label: 确定按钮的文本内容;
          confirm.handler: 确认的回调;
        `
      }
    },
    control: 'object'
  },
  cancel: {
    description: '取消相关的配置项目',
    table: {
      type: {
        summary: 'Object',
        detail: `
        cancel.label: 取消按钮的文本内容;
        cancel.handler: 取消的回调;`
      }
    },
    control: 'object'
  },
  header: {
    description: 'Dialog标题区的内容',
    table: {
      type: {
        summary: 'Vnode'
      },
      defaultValue: ''
    },
    control: 'text',
    category: 'Slots'
  },
  default: {
    description: 'Dialog的内容',
    table: {
      type: {
        summary: 'Vnode'
      },
      defaultValue: ''
    },
    control: 'text',
    category: 'Slots'
  },
  'update:visible': {
    table: {
      disable: true
    }
  }
}

export const actionData = {
  updateVisible: action('update:visible')
}

## 参数文档

<Meta
  title="组件/Basic/Dialog"
  component={Dialog}
  argTypes={{
    ...argsType
  }}
/>

<ArgsTable story="基本用法" />

## 基本用法

export const argsData = {
  visible: false,
  showCancel: true,
  confirm: {
    label: '确定',
    handler () {
      console.log('确定')
    }
  },
  cancel: {
    handler () {
      console.log('X')
    }
  }
}

export const HTMLTemplate = `
  <Dialog v-bind="state" v-model:visible="state.visible" @update:visible="updateVisible">
    <template v-slot:header v-if="state.header">
      <header v-html="state.header"></header>
    </template>
    <template v-slot:default v-if="state.default">
      <main v-html="state.default"></main>
    </template>
  </Dialog>
  <button @click="toggleVisible">切换可见性</button>
`

export const ConstructorFactory = (args) => ({
  components: { Dialog },
  props: Object.keys(args),
  setup () {
    const state = reactive({
      ...args
    })
    function toggleVisible () {
      state.visible = true
    }
    return {
      state,
      toggleVisible
    }
  },
  methods: {
    ...actionData
  },
  template: HTMLTemplate
})

<Canvas
  mdxSource={HTMLTemplate}
>
  <Story
    name="基本用法"
    args={{...argsData}}
  >
    {
      ConstructorFactory.bind({})
    }
  </Story>
</Canvas>

内置组件

Meta

声明本 MDX 文件渲染的页面的标题,对应的组件等。作用和 CSF 写法中的 export default 一致;

ArgsTable

自定义arguments类型,用于Props、Slots、Events展示与交互。

属性示例属性说明属性值属性值示例
of<ArgsTable of={ComponentObj} />自动解析组件的Props、Slots、Events等声明,依据Storybook内置类型声明输出Storybook文档import导入的组件对象import Dialog from './index'的"Dialog"
story<ArgsTable story="StoryNameString" />自定义arguments,需要使用story属性承接arguments类型声明。Story标签的name属性值<Story name="storyName">的"storyName"

Canvas

生成一个 Canvas 画板,用于展示我们自己编写的组件。

画布会提供一些便捷的功能,比如展示当前组件的源代码等;

Canvas默认将Stroybook的Template函数作为源码输出,若想自定义源码输出,将源码作为属性mdxSource的值即可。

点击链接查看更多属性配置

Story

生成一个 story。

Notes:并不是一定要把 story 写在 *<Cavas />*组件中,Canvas 组件只是能够提供一些其他的功能(展示源码、复制代码之类的)。直接编写 story 的话,组件也能正常渲染。

image

区别在于,写在<Canvas />中,Canvas会提供便捷的功能:工具栏、源代码查看...

vscode语法提示插件

名称: MDX

ID: silvenon.mdx
说明: Provides syntax highlighting and bracket matching for MDX (JSX in Markdown) files.
版本: 0.1.0
发布者: Matija Marohnić
VS Marketplace 链接: https://marketplace.visualstu...

名称: MDX Preview

ID: xyc.vscode-mdx-preview
说明: MDX Preview
版本: 0.3.3
发布者: Xiaoyi Chen
VS Marketplace 链接: https://marketplace.visualstu...

名称: Vue Storybook Snippets

ID: megrax.vue-storybook-snippets
说明: Storybook Snippets for Vue
版本: 0.0.7
发布者: Megrax
VS Marketplace 链接: https://marketplace.visualstu...

同类对比


米花儿团儿
1.3k 声望75 粉丝