【问题标题】:How can I make Vuex store work with Storybook?如何让 Vuex 商店与 Storybook 一起使用?
【发布时间】:2019-11-03 01:38:42
【问题描述】:

我有一个组件故事,需要由我的 Vuex 商店中的 ACTION 执行 API 调用。但是,Storybook 找不到该商店:Unhandled promise rejection TypeError: "this.$store is undefined"

我尝试通过 createdmounted Vue 生命周期钩子访问商店,但它们都返回了 undefined

我的 Vuex 商店在我的应用程序中正常运行。

我在 storybook 5.0.1 和 vuex 3.1.1 上运行。

这是我的故事书config.js

// Taken from https://davidwalsh.name/storybook-nuxt & https://github.com/derekshull/nuxt-starter-kit-v2/blob/master/.storybook/config.js
import { addParameters, configure } from '@storybook/vue';
import { withOptions } from '@storybook/addon-options';
import { setConsoleOptions } from '@storybook/addon-console';
import { create } from '@storybook/theming';
import Vue from 'vue';
import VueI18n from 'vue-i18n';

// Vue plugins
Vue.use(VueI18n);

setConsoleOptions({
  panelExclude: [],
});

// Option defaults:
addParameters({
  options: {
    /**
     * show story component as full screen
     * @type {Boolean}
     */
    isFullScreen: false,
    /**
     * display panel that shows a list of stories
     * @type {Boolean}
     */
    showNav: true,
    /**
     * display panel that shows addon configurations
     * @type {Boolean}
     */
    showPanel: true,
    /**
     * where to show the addon panel
     * @type {String}
     */
    panelPosition: 'bottom',
    /**
     * sorts stories
     * @type {Boolean}
     */
    sortStoriesByKind: false,
    /**
     * regex for finding the hierarchy separator
     * @example:
     *   null - turn off hierarchy
     *   /\// - split by `/`
     *   /\./ - split by `.`
     *   /\/|\./ - split by `/` or `.`
     * @type {Regex}
     */
    hierarchySeparator: /\/|\./,
    /**
     * regex for finding the hierarchy root separator
     * @example:
     *   null - turn off multiple hierarchy roots
     *   /\|/ - split by `|`
     * @type {Regex}
     */
    hierarchyRootSeparator: /\|/,
    /**
     * sidebar tree animations
     * @type {Boolean}
     */
    sidebarAnimations: true,
    /**
     * enable/disable shortcuts
     * @type {Boolean}
     */
    enableShortcuts: true,
    /**
     * theme storybook, see link below
     */
    theme: create({
      base: 'light',
      brandTitle: '',
      brandUrl: '',
      // To control appearance:
      // brandImage: 'http://url.of/some.svg',
    }),
  },
});

const req = require.context('../src/components', true, /\.story\.js$/)

function loadStories() {
  req.keys().forEach((filename) => req(filename))
}

configure(loadStories, module);

这是我的组件的故事:

import { storiesOf } from '@storybook/vue';
import { withReadme } from 'storybook-readme';
import { withKnobs } from '@storybook/addon-knobs';
import HandoffMainView from './HandoffMainView.vue';
import readme from './README.md';

storiesOf('HandoffMainView', module)
  .addDecorator(withReadme([readme]))
  .addDecorator(withKnobs)
  .add('Default', () => {
    /* eslint-disable */
    return {
      components: { HandoffMainView },
      data() {
        return {
          isLoading: true,
          component: {
            src: '',
            data: [],
          },
        };
      },
      template: '<handoff-main-view :component="component" />',
    };
  });

这是我的组件:

<template>
  <main class="o-handoff-main-view">
    <div class="o-handoff-main-view__content">
      <div
        :class="[
          'o-handoff-main-view__background',
          background ? `o-handoff-main-view__background--${background}` : false
        ]"
      >  
        <loader
          v-if="isLoading"
          :color='`black`'
          class="o-handoff-main-view__loader"
        />
        <div
          v-else
          class="o-handoff-main-view__ui-component"
          :style="getUiComponentStyle"
        >
          <img
            :src="uiComponent.src"
            alt=""
          >
          <handoff-main-view-layer-list
            :layers="uiComponent.data"
          />
        </div>
      </div>
    </div>
    <div class="o-handoff-main-view__controls">
      <handoff-main-view-zoom-handler
        :default-zoom-level="zoomLevel"
        :on-change="updateZoomLevel"
      />
    </div>
  </main>
</template>

<script>
  import { mapActions } from 'vuex';
  import Loader from '../../01-atoms/Loader/Loader.vue';
  import HandoffMainViewZoomHandler from '../HandoffMainViewZoomHandler/HandoffMainViewZoomHandler.vue';
  import HandoffMainViewLayerList from '../HandoffMainViewLayerList/HandoffMainViewLayerList.vue';

  export default {
    components: {
      Loader,
      HandoffMainViewZoomHandler,
      HandoffMainViewLayerList,
    },
    props: {
      background: {
        type: String,
        default: 'damier',
      },
      component: {
        type: Object,
        required: true,
      },
    },
    data() {
      return {
        isLoading: true,
        zoomLevel: 1,
        uiComponent: {
          src: null,
        }
      };
    },
    mounted() {
      this.setUiComponentImage();
    },
    methods: {
      ...mapActions('UiComponent', [
        'ACTION_LOAD_SIGNED_URLS'
      ]),
      async setUiComponentImage() {
        const uiComponentImg = new Image();
        const signedUrls = await this.ACTION_LOAD_SIGNED_URLS([this.component.id]);
        uiComponentImg.onload = () => {
          this.isLoading = false;
        };
        uiComponentImg.src = this.uiComponent.src;
      },
    },
  };
</script>

【问题讨论】:

  • IMO 你不应该需要 vuex 来写故事书。 Storybook 中的组件应该向调用 store 的父组件发送事件。故事书中几乎所有的数据都应该是虚拟数据。

标签: vue.js vuex storybook


【解决方案1】:

你可以尝试使用装饰器

import { createStore } from 'vuex';

const _vue = require("@storybook/vue3");
const _addons = require("@storybook/addons");

const withVueRouter = function withVueRouter() {
  const store = arguments?.[0] || createStore({ state: {} });
  return _addons.makeDecorator({
    name: 'withStore',
    parameterName: 'withStore',
    wrapper: (storyFn, context) => {
      _vue.app.use(store);
      return storyFn(context);
    }
  });
};

export default withVueRouter;

用法

import withStore from '../../../config/storybook/decorators/withStore';
import { createStore } from 'vuex';

const store = createStore({
  state: {
    film: films[0],
  },
});

export default {
  title: 'film-details/FilmDetails',
  decorators: [withStore(store)]
};

const FilmDetailsTemplate = (args) => ({
  components: { FilmDetails },
  template: '<FilmDetails/>',
});

export const template = FilmDetailsTemplate.bind({
});

【讨论】:

    【解决方案2】:

    我打赌在你的应用程序的某个地方,可能是main.js,你正在做类似的事情:

    import Vuex from 'vuex';
    Vue.use(Vuex);
    
    const store = new Vuex.Store({
      state,
      mutations,
      getters,
    });
    

    然后,在创建 Vue 应用程序时,您调用 new Vue({store, i18n...})

    您已经在 config.js 中使用“i18n”模块来锻造 Vue。您还需要导入 Vuex 和那里的商店。


    现在,必须在 Storybook 设置中导入您的商店 - 或模拟它 - 可能是因为您的组件太大,或者与您的商店过于耦合。

    通常,故事书更倾向于展示显示具有专用功能的内容(表单控件、事物列表...)的组件。此类组件通常通过道具和事件与应用程序的其余部分进行通信。我们称它为展示组件。

    相反,与 store 通信的组件通常是视图或页面,它们协调状态并与后端通信,并为前者提供数据。

    我认为您应该在故事书展示中仅显示演示组件,并避免谈论其中的全局模块。至少,我相信这是故事书背后的精神,也是故事书的主要用途。这可能是原因,因为您没有找到太多关于如何在故事书中模拟您的商店的文档:我认为,故事书项目通常不会首先连接到 vuex。

    【讨论】:

    • 确实,我有一个main.js,其中包含了我所有的 Vue 配置,尤其是我的 Vuex 商店。我还认为我应该将我的商店导入到我的故事书config.js 文件中,但我不知道下一步该做什么。我开始认为我需要模拟我的应用商店的特定部分并将它们放在我的组件故事中。但根本不清楚。我找不到任何相关文档。
    • 更新了我对此的回答。
    • 我完全明白你的意思,我想我可能会同意你的观点。这让我想起了前段时间看到的这篇文章:medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0
    【解决方案3】:

    如果您使用的是 Nuxt.js,您可以这样做:

    ./storybook/store.js

    import Vue from "vue";
    import Vuex from "vuex";
    
    Vue.use(Vuex);
    
    const store = new Vuex.Store({
        state: require("../store/index.js").state,
        getters: require("../store/index.js").getters,
        actions: require("../store/index.js").actions,
        mutations: require("../store/index.js").mutations,
    
        modules: {
            ads: {
                namespaced: true,
                state: require("../store/ads.js").state,
                getters: require("../store/ads.js").getters,
                actions: require("../store/ads.js").actions,
                mutations: require("../store/ads.js").mutations
            },
    
            features: {
                namespaced: true,
                state: require("../store/features.js").state,
                getters: require("../store/features.js").getters,
                actions: require("../store/features.js").actions,
                mutations: require("../store/features.js").mutations
            },
    
    
            user: {
                namespaced: true,
                state: require("../store/user.js").state,
                getters: require("../store/user.js").getters,
                actions: require("../store/user.js").actions,
                mutations: require("../store/user.js").mutations
            },
        }
    });
    
    export default store
    

    那么在你的故事中:

    // ...
    import store from '@/.storybook/store';
    
    export default {
        title: 'MyComponent'
    };
    
    export const MyComponentStory = () => ({
        store: store,
        // ...
    })
    

    【讨论】:

    • 在我的设置上苦苦挣扎了几个小时后,我只需要按照您的建议添加 store: store,
    【解决方案4】:

    在故事中传递新的商店实例(或模拟)

    import Vuex from "vuex";
    import { storiesOf } from '@storybook/vue';
    import { withReadme } from 'storybook-readme';
    import { withKnobs } from '@storybook/addon-knobs';
    import HandoffMainView from './HandoffMainView.vue';
    import readme from './README.md';
    
    storiesOf('HandoffMainView', module)
      .addDecorator(withReadme([readme]))
      .addDecorator(withKnobs)
      .add('Default', () => {
        /* eslint-disable */
        return {
          components: { HandoffMainView },
          data() {
            return {
              isLoading: true,
              component: {
                src: '',
                data: [],
              },
            };
          },
          template: '<handoff-main-view :component="component" />',
          store: new Vuex.Store({ // here
            modules: {
              namespaced: true,
              actions: ... 
            }
          }
        };
      });
    

    【讨论】:

      猜你喜欢
      • 2020-01-21
      • 2020-06-27
      • 2018-03-31
      • 2022-06-15
      • 1970-01-01
      • 2022-01-03
      • 1970-01-01
      • 1970-01-01
      • 2019-08-18
      相关资源
      最近更新 更多