(téměř) komplexní průvodce používáním Storybook s Nuxt.js

Už nějakou dobu jsem chtěl Storybook implementovat do svých projektů Nuxt.js.

Pro ty, kteří nevědí, Storybook je open source nástroj pro vývoj komponent uživatelského rozhraní v izolaci. Podívejte se na jeho případy použití.

Použití Storybook s prostým Vue.js není žádný problém, ale s Nuxtem je to jiný příběh, protože nefunguje hned po vybalení. Informace tam jsou rozptýlené a musel jsem se prohrabat v repozitářích a příkladech jiných lidí, aby to fungovalo s Nuxtem, včetně toho, jak obvykle používám Obchod.

Myslel jsem, že to sepíšu a vytvořím robustnější vzorové úložiště pro ostatní, kteří chtějí začít používat Storyboook s Nuxtem.

Moje obvyklé nastavení projektu zahrnuje použití obchodu Vuex Store, modulu Axios společnosti Nuxt, TailwindCSS a vlastního SCSS.

To je zhruba to, co bych rád viděl ve spolupráci se Storybookem, aniž bych musel příliš měnit způsob, jakým Nuxt obecně používám.

Na konci tohoto příkladu budeme mít komponentu List, která načítá data externě z JSONPlaceholder.

Podívejte se, jak to bude vypadat zde.

Toto je obsáhlý průvodce, takže neváhejte přejít přímo do sekce, kterou hledáte. Celý tento kód můžete získat zde.

Počáteční nastavení

Protože je tento průvodce od základů, začínáme s novým projektem Nuxt pomocí create-nuxt-app :

npx create-nuxt-app nuxt-storybook

Také aktualizujeme Nuxt na nejnovější stabilní verzi 2.5.1:

npm rm nuxt && npm i -S nuxt

Chyba sestavení?

V době psaní tohoto článku má upgrade na Nuxt 2.5 za následek chybu při sestavování:

ERROR  Failed to compile with 1 errors                                                                                                                                          friendly-errors 13:29:07
[...]
Module parse failed: Unexpected token (7:24)                                                                                                                                     friendly-errors 13:29:07
[...]
| 
| var _0c687956 = function _0c687956() {
>   return interopDefault(import('../pages/index.vue'
|   /* webpackChunkName: "pages/index" */
|   ));

Pokud je to stále ten případ, na mém počítači™ (macOS) funguje na základě tohoto řešení následující:

rm -rf node_modules package-lock.json
npm i -D [email protected]
npm i

Spuštěn npm run dev by se nyní měla zobrazit výchozí uvítací stránka Nuxt.

Přidání knihy příběhů

Storybook a potřebné závislosti nainstalujeme ručně podle jejich pokynů pro Vue. Většina závislostí již existuje díky Nuxtu s babel-preset-vue jako jediný chybí.

// Add Storybook & dependencies
npm i -D @storybook/vue babel-preset-vue

Nyní vytvořte složku s názvem .storybook a přidejte soubor config.js v něm.

Config.js se používá jako „vstupní bod“, který říká Storybooku, kde hledat a načítat příběhy, a také importovat a používat další potřebné pluginy nebo doplňky pro použití s ​​Stories.

Podle pokynů Vue config.js bude zpočátku vypadat takto:

// /.storybook/config.js
import { configure } from '@storybook/vue';
function loadStories() {
  const req = require.context('../stories', true, /\.stories\.js$/);
  req.keys().forEach(filename => req(filename));
}
configure(loadStories, module);

To, co dělá, je iterování každého souboru končícího jako .stories.js ve složce příběhy. Protože mám rád své příběhy blízko svých komponent a ne všechny hromadně ve složce příběhů, jednoduše změním složku na komponenty a nechám funkci procházet každou složku v ní.

Vrátíme se k config.js později. Prozatím se ujistěte, že Storybook dokáže načíst jednoduchý příběh a zobrazit jej.

Přidávání našeho prvního příběhu

V adresáři komponent vytvořte novou složku s názvem list a v ní soubor s názvem List.vue s níže uvedeným kódem. Použijeme ho k vytvoření naší finální komponenty za pochodu.

// /components/list/List.vue

<template>
  <div class="list">
    I'm a list
  </div>
</template>

<script>
  export default {
    name: 'List'
  }
</script>

<style scoped>
  .list {
    background: #CCC;
  }
</style>

Všiml jsem si toho hodně, jen něco pro zobrazení našeho příběhu. Nyní do stejné složky přidejte soubor s názvem List.stories.js s následujícím kódem v něm:

// /components/list/List.stories.js
import Vue from 'vue'
import { storiesOf } from '@storybook/vue'
import List from './List'

storiesOf('List', module)
  .add('As a component', () => ({
    components: { List },
    template: '<List />'
  }))
  .add('I don\'t work', () => '<List />')

Nyní ke spuštění Storybook musíme přidat spouštěcí skript do package.json (chcete-li jej spustit na jiném portu, přidejte -p <port-number> )

“storybook”: “start-storybook”

Zadejte npm run storybook ve vašem terminálu a váš prohlížeč otevře novou kartu:

To běží Storybook. A protože používá rychlé načítání, budete moci vidět, že se vaše změny projeví okamžitě.

Všimli jste si, že druhý příběh nefunguje? Je to proto, že jsme Storybooku neřekli, aby použila náš Seznam komponentu pro tento příběh, jako jsme to udělali u prvního (otevření konzole prohlížeče zobrazí tyto chyby).

Seznam můžeme zaregistrovat jako globální komponent, stejně jako je registrujeme pomocí pluginů Nuxt, pouze v rámci config.js , takže to nakonec vypadá takto:

// /.storybook/config.js
import { configure } from '@storybook/vue';
import Vue from 'vue'
import List from '../components/list/List.vue'

Vue.component('List', List)

function loadStories() {
  const req = require.context('../components', true, /\.stories\.js$/);
  req.keys().forEach(filename => req(filename));
}

configure(loadStories, module);

Nyní funguje 2. příběh. To bylo jen pro představu, že některé z vašich komponent mohou používat jiné. Abychom je nemuseli vždy importovat, můžeme je definovat globálně, jako jsme to udělali my (zbývající příklad to nevyužije, takže to můžete smazat).

Nyní máte vanilkové nastavení Storybook pracující s Nuxtem. Ale zatím to není moc příběh.

Vylepšení naší komponenty Seznam a přidání obchodu

Nejprve trochu zkomplikujeme náš seznam komponenty a starat se o chyby, které na nás Storybook vrhne později.

Seznam by měl:

  • po připojení — načtení falešných uživatelů nebo falešných komentářů pomocí JSONPlaceholder;
  • iterujte každého uživatele/komentář a vykreslete jej pomocí komponenty ListItem;
  • využít Vuex k odesílání našich volání API;
  • vypadat hezčí pomocí TailwindCSS a některých vlastních stylů;

Styly

Pro styling použijeme některé třídy obslužných programů TailwindCSS a také některé vlastní styly, abychom doložili jeho použití s ​​Storybook. Používám SCSS, takže budeme muset přidat obvyklý node-sass &sass-loader :

npm i -D node-sass sass-loader

Seznam přijme prop zdroj takže ví, který zdroj dat chceme načíst. Necháme jej také připravené zavolat adekvátní akci Store k provedení volání API, jakmile je vytvoříme.

Seznam komponenta by nyní měla vypadat takto:

// /components/list/List.vue

<template>
  <div class="list p-5 rounded">
    I'm a {{ source }} list
  </div>
</template>

<script>
  export default {
    name: 'List',
    props: {
      source: {
        type: String,
        default: 'users'
      }
    },
    data() {
      return {
        entities: []
      }
    },
    mounted() {
      switch (this.source) {
        default:
        case 'users':
          this.loadUsers()
          break
        case 'comments':
          this.loadComments()
          break
      }
    },
    methods: {
      loadUsers() {
        //  Will call store action
        console.log('load users')
      },
      loadComments() {
        //  Will call store action
        console.log('load comments')
      },
    }
  }
</script>

<style lang="scss" scoped>
  $background: #EFF8FF;
  .list {
    background: $background;
  }
</style>

Přidání Store &API volání

Obvykle uchovávám svá volání API v akcích obchodu, takže je mohu snadno volat pomocí this.$store.dispatch .

.env :Naše koncové body ponecháme v .env soubor, takže abychom získali tyto hodnoty, nainstalujeme modul @nuxtjs/dotenv npm i -S @nuxtjs/dotenv a přidejte jej do nuxt.config.js moduly.

Vytvořte .env v kořenovém souboru projektu a přidejte:

USERS_ENDPOINT=https://jsonplaceholder.typicode.com/users
COMMENTS_ENDPOINT=https://jsonplaceholder.typicode.com/comments

Na přidávání akcí obchodu pro načtení uživatelů a komentářů. Přidejte actions.js soubor v existujícím adresáři úložiště s následujícím kódem:

// /store/actions.js
export default {
  async GET_USERS({ }) {
    return await this.$axios.$get(`${ process.env.USERS_ENDPOINT }`)
  },
  async GET_COMMENTS({ }) {
    return await this.$axios.$get(`${ process.env.COMMENTS_ENDPOINT }`)
  },
}

Nyní můžeme upravit náš Seznam metody komponenty pro volání těchto akcí, když je připojena, což nakonec vypadá takto:

// /components/list/List.vue

<template>
  <div class="list p-5 rounded">
    I'm a {{ source }} list
  </div>
</template>

<script>
  export default {
    name: 'List',
    props: {
      source: {
        type: String,
        default: 'users'
      }
    },
    data() {
      return {
        entities: []
      }
    },
    mounted() {
      switch (this.source) {
        default:
        case 'users':
          this.loadUsers()
          break
        case 'comments':
          this.loadUsers()
          break
      }
    },
    methods: {
      loadUsers() {
        this.$store.dispatch('GET_USERS')
        .then(res => {
          console.log(res)
        })
        .catch(err => {
          console.log('API error')
          console.log(err)
        })
      },
      loadComments() {
        this.$store.dispatch('GET_COMMENTS')
        .then(res => {
          console.log(res)
        })
        .catch(err => {
          console.log('API error')
          console.log(err)
        })
      },
    }
  }
</script>

<style lang="scss" scoped>
  // Pointless. Just for the sake of the example
  $background: #EFF8FF;
  .list {
    background: $background;
  }
</style>

Nyní získáme pole dat vrácených z každého koncového bodu. Pojďme si je zobrazit.

Přidání komponenty ListItem

V závislosti na tom, zda uvádíme uživatele nebo komentáře, zobrazíme variantu Položky seznamu komponent. Každá varianta bude mít také svou vlastní komponentu.

Vytvořte složku pod seznamem s názvem items a vytvořte soubor s názvem ListItem.vue . Zde je kód, který do něj přidáte:

// /components/list/items/ListItem.vue

<template>
  <div class="list-item rounded bg-blue-light px-5 py-3">
    <div v-if="itemType === 'users'">
      A user item
    </div>
    <div v-else>
      A comment item
    </div>
  </div>
</template>

<script>
  export default {
    name: 'ListItem',
    props: {
      itemType: {
        type: String,
        default: 'user'
      },
      data: {
        type: Object,
        default: () => {
          return {}
        }
      }
    }
  }
</script>

Zatím nic moc, za chvíli to změníme. Mezitím jsem upravil styl domovské stránky, abychom viděli oba naše seznamy vedle sebe:

Nyní skutečně použijeme naši Položku seznamu iterovat každou entitu vrácenou naším API a podle toho ji upravit.

Přidání komponenty User &Comment

Pro každou entitu vytvoříme komponentu na základě následující datové struktury:

// User
{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "[email protected]",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
},
// Comment
{
  "postId": 1,
  "id": 1,
  "name": "id labore ex et quam laborum",
  "email": "[email protected]",
  "body": "laudantium enim quasi est quidem magnam voluptate ipsam eos\ntempora quo necessitatibus\ndolor quam autem quasi\nreiciendis et nam sapiente accusantium"
}

Přidejte Comment.vue soubor v /components/list/items/ s kódem:

// /components/list/items/Comment.vue

<template>
  <div>
    <b>{{ name }}</b>
    <p>{{ body }}</p>
  </div>
</template>

<script>
  export default {
    name: 'Comment',
    props: {
      name: {
        type: String,
        default: ''
      },
      body: {
        type: String,
        default: ''
      }
    }
  }
</script>

Přidejte User.vue soubor v /components/list/items/ s kódem:

// /components/list/items/User.vue

<template>
  <div>
   <nuxt-link
      :to="{ name:'user' }"
      class="text-lg"
    >
      {{ name }} - "{{ username }}"
    </nuxt-link>
    <div class="flex flex-wrap justify-start my-2">
      <div class="w-1/2 mb-2">
        <span class="text-grey-dark font-bold">Email</span>
        <p class="p-0 m-0">{{ email }}</p>
      </div>
      <div class="w-1/2 mb-2">
        <span class="text-grey-dark font-bold">Phone</span>
        <p class="p-0 m-0">{{ phone }}</p>
      </div>
      <div class="w-1/2 mb-2">
        <span class="text-grey-dark font-bold">City</span>
        <p class="p-0 m-0">{{ address.city }}</p>
      </div>
      <div class="w-1/2 mb-2">
        <span class="text-grey-dark font-bold">Company</span>
        <p class="p-0 m-0">{{ company.name }}</p>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'User',
    props: {
      name: {
        type: String,
        default: ''
      },
      username: {
        type: String,
        default: ''
      },
      email: {
        type: String,
        default: ''
      },
      phone: {
        type: String,
        default: ''
      },
      address: {
        type: Object,
        default: () => {
          return {}
        }
      },
      company: {
        type: Object,
        default: () => {
          return {}
        }
      }
    }
  }
</script>

Poznámka :pro příklad jsem přidal nuxt-link . K tomu jsme také přidali příslušnou stránku /pages/user/index.vue . Není v tom nic, jen aby nuxt-link někam odkazoval.

Pojďme změnit naši Položku seznamu komponentu pro použití těchto nových komponent:

// /components/list/items/ListItem.vue

<template>
  <div class="list-item rounded bg-indigo-lightest shadow px-5 py-3 mb-3">
    <div v-if="itemType === 'users'">
      <User
        :name="data.name"
        :username="data.username"
        :email="data.email"
        :phone="data.phone"
        :address="data.address"
        :company="data.company"
      />
    </div>
    <div v-else>
      <Comment
        :name="data.name"
        :body="data.body"
      />
    </div>
  </div>
</template>

<script>
  import User from '@/components/list/items/User'
  import Comment from '@/components/list/items/Comment'

  export default {
    name: 'ListItem',
    components: {
      User,
      Comment
    },
    props: {
      itemType: {
        type: String,
        default: 'user'
      },
      data: {
        type: Object,
        default: () => {
          return {}
        }
      }
    }
  }
</script>

Nakonec musíme změnit List.vue takže odpověď volání API ve skutečnosti předáváme jako rekvizity, místo abychom ji pouze zaprotokolovali. Změňte metody tak, aby to vypadalo takto:

// /components/list/List.vue
[...]
methods: {
  loadUsers() {
    this.$store.dispatch('GET_USERS')
    .then(res => {
      this.entities = res.data
    })
    .catch(err => {
      console.log('API error')
      console.log(err)
    })
  },
  loadComments() {
    this.$store.dispatch('GET_COMMENTS')
    .then(res => {
      this.entities = res.data
    })
    .catch(err => {
      console.log('API error')
      console.log(err)
    })
  },
}
[...]

Po několika drobných úpravách stylu by to nyní mělo vypadat takto:

Nyní jsme připraveni přejít na Storybook a uvidíme, co se stane.

Řešení stížností Storybook

Nyní vyřešíme každý z nastolených problémů při spuštění Storybook, první z nich je:

Modul nenalezen

Error: Can’t resolve ‘@/components/list/items/ListItem’

Když se podíváte na příklady Storybooku, uvidíte, že odkazuje na komponenty pomocí relativních cest. To je pro nás problém, když používáme Nuxt jako framework využívající @ alias.

Musíme nyní všude používat relativní cesty? Naštěstí ne. Nezapomeňte, že jsme nainstalovali babel-preset-vue dříve? Toto plus použití aliasu webpacku nám umožňuje tento problém vyřešit.

Nejprve vytvořte soubor v .příběhové knize složka s názvem .babelrc s následujícím:

// /.storybook/.babelrc
{
  "presets": [
    "@babel/preset-env",
    "babel-preset-vue"
  ]
}

Vytvořte další soubor s názvem webpack.config.js v .příběhové knize složka s následujícím:

// /.storybook/.webpack.config.js

const path = require('path')

module.exports = {
  resolve: {
    alias: {
      '@': path.dirname(path.resolve(__dirname))
    }
  }
}

Nyní byste měli být schopni nadále používat alias @ k importu komponent.

Abychom měli stručný kód, můžeme nyní změnit způsob importu Seznamu komponentu ve svém příběhu z import List from './List' na import List from '@/components/list/List' .

Analýza modulu selhala:zpracovává SCSS

Pohádková kniha nyní přináší:

Module parse failed: Unexpected character ‘#’ (69:13)
You may need an appropriate loader to handle this file type.

Je to proto, že jsme nespecifikovali, jak je načíst. Můžeme to vyřešit přidáním pravidla modulu pro CSS/SCSS na webpack, takže náš soubor nyní vypadá takto:

// /.storybook/.webpack.config.js

const path = require('path')

module.exports = {
  module: {
    rules: [
      {
        test: /\.s?css$/,
        loaders: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
        include: path.resolve(__dirname, '../')
      }
    ]
  },
  resolve: {
    alias: {
      '@': path.dirname(path.resolve(__dirname))
    }
  }
}

Musíme také přidat import '@/assets/css/tailwind.css na .storybook/config.js takže můžeme použít třídy obslužných programů Tailwind.

Znovu spusťte Storybook a tentokrát byste měli nechat prohlížeč otevřít novou kartu s tím nejhezčím:

Použití Vuex s Storybook

Pokud jste před tímto návodem postupovali podle pokynů Storybook's Vue, měli byste již importovat a používat Vuex v config.js.

Pokud ne, nyní by to mělo vypadat následovně:

// /.storybook/config.js

import Vue from 'vue'
import Vuex from 'vuex'
import { configure } from '@storybook/vue'
import '@/assets/css/tailwind.css'

Vue.use(Vuex)

function loadStories() {
  const req = require.context('../components', true, /\.stories\.js$/)
  req.keys().forEach(filename => req(filename))
}

configure(loadStories, module)

Ale jen tak to nevyřeší.

Komponenty Nuxt odkazují na Store jako this.$store a náš příběh o tom neví, proto musíme vytvořit nový obchod a předat jej naší komponentě.

Ale musíme znovu vytvořit celý Store? Naštěstí ne. Opravdu vytvoříme obchod, ale znovu použijeme všechny existující akce, getry, mutace nebo stavy, které náš stávající obchod má.

Za tímto účelem vytvoříme soubor s názvem store.js v .příběhové knize adresář s následujícím kódem:

// /.storybook/store.js

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'

// You can do the same for getters, mutations and states
import actions from '@/store/actions'

let store = new Vuex.Store({
  actions: actions
})

/*
  Bind Axios to Store as we don't have access to Nuxt's $axios instance here
*/
store.$axios = axios

/*
Remap Axios's methods to make use of $ symbol within the 
Store's actions so we don't re-write our Axios' calls
*/

store.$axios.$get = store.$axios.get
store.$axios.$post = store.$axios.post

export default store

Nyní můžeme importovat a předat tento obchod do našich Stories.

Prozatím máme pouze seznam příběhů Uživatelé, což je výchozí zdroj. Přidejte další příběh do seznamu komentářů a každý přejmenujte:

// /components/list/List.stories.js

import Vue from 'vue'
import { storiesOf } from '@storybook/vue'

import List from '@/components/list/List'

import store from '@/.storybook/store'

storiesOf('Lists', module)
  .add('Users', () => ({
    components: { List },
    store: store,
    template: '<List />'
  }))
  .add('Comments', () => ({
    components: { List },
    store: store,
    template: `<List :source="'comments'" />`
  }))
// /components/list/List.vue
[...]
if ('data' in res) {
  this.entities = res.data
} else {
  this.entities = res
}
[...]

Po provedení výše uvedených kroků bychom nyní měli vidět oba příběhy pro naši komponentu Seznam:

Obsluha nuxt-link

Konečně můžeme něco vidět! Ale naše odkazy chybí..

Pokud otevřete konzolu prohlížeče na kartě Storybook, uvidíte, že neví, co nuxt-link je (také se tam vždy můžete podívat na potenciální chyby, pokud věci nefungují správně).

Aby byly funkční a funkční, je vyžadována konečná úprava Storybooku.

K tomu potřebujeme nainstalovat @storybook/addon-actions závislost:npm i -D @storybook/addon-actions a přidejte je do Storybook vytvořením souboru addons.js v .příběhové knize adresář s řádky:

// /.storybook/addons.js
import '@storybook/addon-actions'
import '@storybook/addon-actions/register'

Nakonec potřebujeme import { action } from '@storybook/addon-actions v config.js a zaregistrujte vylepšenou komponentu nuxt-link do Vue. Naše config.js soubor by měl nyní vypadat takto:

// /.storybook/config.js

import Vue from 'vue'
import Vuex from 'vuex'
import { configure } from '@storybook/vue'

import { action } from '@storybook/addon-actions'

import '@/assets/css/tailwind.css'

Vue.use(Vuex)

Vue.component('nuxt-link', {
  props:   ['to'],
  methods: {
    log() {
      action('link target')(this.to)
    },
  },
  template: '<a href="#" @click.prevent="log()"><slot>NuxtLink</slot></a>',
})

function loadStories() {
  const req = require.context('../components', true, /\.stories\.js$/)
  req.keys().forEach(filename => req(filename))
}

configure(loadStories, module)

To nahradí všechny instance s běžným prvkem kotvy a také nastavením metody protokolu zobrazující cestu vlákna při kliknutí na něj.

Poté bychom již neměli vidět žádnou chybu na konzole prohlížeče a mít skutečné klikatelné odkazy na jména našich uživatelů:

Pohádková kniha ve spolupráci s Nuxtem!

Chvíli to trvalo, ale podařilo se nám, aby Storybook dobře fungoval s komponentami Vue.js v rámci projektu Nuxt.js.

Toto není plnohodnotný průvodce, protože nám chybí testy a klíčové aspekty Nuxtu, jako je (Také by mě zajímalo, jak mohou asyncData a Storybook nakonec spolupracovat).

Bonus:nasazení Storybook do Netlify

Při spuštění Storybook získáte IP adresu, kterou můžete sdílet s ostatními ve vaší místní síti, a to je skvělé, pokud jste na stejné WiFi. Ale co když to chcete sdílet se svými klienty, aby vám mohli poskytnout zpětnou vazbu k iteraci z minulého týdne?

V takovém případě jej hostujte na Netlify. Jednoduše přidejte níže uvedený skript do souboru package.json soubor, který vygeneruje statický Storybook v adresáři Storybook-static:

"build-storybook": "build-storybook -c .storybook"

Poté přejděte na Netlify a vyberte své úložiště. Definujte příkaz build jako npm run build-storybook a publikační adresář jako storybook-static .

Poté byste měli mít svůj Storybook aktivní a aktualizovaný pokaždé, když jej vložíte/sloučíte do master větev. Podívejte se na to!

Finální repo a zdroje

Neváhejte a vezměte si kód na Github https://github.com/mstrlaw/nuxt-storybook a prohlédněte si tento materiál ke čtení a další úložiště, která byla užitečná pro vytvoření této příručky:

  • Průvodce Vue Storybook;
  • learnstorybook.com (průvodce Vue);
  • Tento příspěvek na blogu (čínština) a toto úložiště;
  • Blogový příspěvek Davida Walshe, který se ponoří do skutečných testů pomocí Jestu;
  • Tato otázka;

Připojte se a zanechte své myšlenky a návrhy v komentářích níže.

Původně zveřejněno na médiu