Closed21

Nuxt3(RC)&Vue3 + ESLint + Chakra-ui + Storybookの環境構築

disneyLadySangodisneyLadySango

https://v3.nuxtjs.org/community/roadmap

Nuxt3のstabeleは2022年Summer(なお、今は9月3日)
日本人の感覚ではあくまで夏とは8月末までの感覚。
しかし大学生という枠組みで見ると夏休みは9月末くらいまでと考えるときっと海外の夏も9月末までなんだろう(真顔)

なお、本日RC9がnpmに公開されているがRC8で環境構築をしているので要注意
(issueにはRC9、早速バグってるけど報告あるけど気にしない)
早くStable出てほしい・・・

Nuxtを触るのが今回初めてなので何が変わったのか、書き味が変わったのかとかは正直分かりません、許してください

disneyLadySangodisneyLadySango

普段React + Next.jsでChakara-uiを作っているのでゴールは下記

  • Chakra-uiでNuxt3を動かす
  • Chakra-uiでテーマを上書きする
  • 任意のコンポーネントでStorybookを動かす
disneyLadySangodisneyLadySango

nuxtのプロジェクトを作成
npx nuxi init <プロジェクト名>

そしてプロジェクトのディレクトリに入りyarn --frozen-lockfile
これでnode_modulesが入る

disneyLadySangodisneyLadySango

package.jsonがとてもシンプル
nuxtがdevDependenciesに入っている・・・stableになったらdevから消えるのかな・・・?
これ本番稼働させるなってことなのかな?

disneyLadySangodisneyLadySango

プロジェクトの構成はrootディレクトリにcomponentsとかpagesを切るらしい(ここはNext.jsも同じなので使い勝手は同じ)
とはいえ、rootに色々別のファイルとかディレクトリ切るので資材と混在すると見通しが悪いのでsrcディレクトリを切る

https://v3.nuxtjs.org/api/configuration/nuxt.config
設定がここにあるのでここを参考に nuxt.config.ts に設定を追加する

nuxt.config.ts
import { defineNuxtConfig } from 'nuxt'

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  srcDir: 'src/',
})
disneyLadySangodisneyLadySango

Nuxtには自動import機能があるらしい
https://v3.nuxtjs.org/guide/concepts/auto-imports

今までは明示的なimportだったのかな?
少なくとも普段使いのVue.jsだと明示的import

自動importとして使えるのは
componentsとcomposablesとのこと
composablesはReactでいうところのhooks
あと気をつけないといけないのはserverディレクトリ内では今動かないとのこと

Storybookと単体テストを書くのであればコンポーネントディレクトリ内に複数配置したいというのもあるので
これ使わない方が良さそうな気がしている。
機能をOFFにするので下記をconfigへ追加する

nuxt.config.ts
export default defineNuxtConfig({
  imports: {
    autoImport: false
  }
})
disneyLadySangodisneyLadySango

ということでこのタイミングで一回適当に動かしてみよう

まずは適当なコンポーネントを作る

src/components/HelloWolrd/HelloWorld.vue
<template>
  <div>Hello World!</div>
</template>

Reactチックに使いたい願望があるのでこいつをnamed exportさせる
(こうするとなんか問題あるのかな・・・今までvue.js使っていてそれには当たってない)

src/components/HelloWorld/index.ts
export { default as HelloWorld } from "./HelloWorld.vue";

そしてこいつを pagesのindexで読み込みさせてみる
と思ったけどimportの時にサジェストが出てこない・・・
aliasの設定もしたいのでconfigに下記を追加

nuxt.config.ts
import { defineNuxtConfig } from "nuxt";

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  srcDir: "src/",
  imports: {
    autoImport: false,
  },
  typescript: {
    tsConfig: {
      compilerOptions: {
        paths: {
          "@/*": ["src/*"],
        },
      },
    },
  },
});
src/pages/index.vue
<template>
  <div>
    <HelloWorld />
  </div>
</template>

<script lang="ts">
import { HelloWorld } from '@/components/HelloWorld'

export default {
  components: {
    HelloWorld
  }
}
</script>

よしちゃんと表示されている

disneyLadySangodisneyLadySango

せっかくなのでReactでいうhooks、Vue3のcompositionAPIとやらを試してみよう
あと何が嬉しいかってtemplateの下は複数返してもよくなったこと
これで今までdiv追加していたところが綺麗なコンポーネントを切れるようになった
これは大変嬉しい

ということでインクリメントする簡単なカウンターを用意する

src/components/Counter/Counter.vue
<template>
  <div>{{ state.count }}</div>
  <button @click="increment">increment</button>
</template>

<script lang="ts">
import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({ count: 0 })

    const increment = () => {
      state.count++
    }

    return {
      state,
      increment
    }
  }
}
</script>
src/components/Counter/index.ts
export { default as Counter } from "./Counter.vue";

これを先ほどのindexで読み込む

src/pages/index.vue
<template>
  <div>
    <HelloWorld />
    <Counter />
  </div>
</template>

<script lang="ts">
import { HelloWorld } from '@/components/HelloWorld'
import { Counter } from '@/components/Counter'

export default {
  components: {
      HelloWorld,
      Counter
  }
}
</script>

いい感じでできたぞ

disneyLadySangodisneyLadySango

と書いていて思った
自動整形全然されないw

ということでlintの設定をする
https://itnext.io/nuxt-3-first-steps-c23d142405c4

こちらのブログの設定を参考にしてみよう

.eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    es2021: true,
    node: true,
  },
  parserOptions: {
    ecmaVersion: 2020,
    sourceType: 'module',
    parser: '@typescript-eslint/parser',
  },
  extends: [
    'plugin:vue/vue3-recommended',
    'prettier',
    'plugin:@typescript-eslint/recommended',
    'plugin:nuxt/recommended',
  ],
  plugins: ['vue', '@typescript-eslint', '@typescript-eslint/eslint-plugin'],
  rules: {
    semi: ['error', 'never', { beforeStatementContinuationChars: 'never' }],
    quotes: ['error', 'single'],
    'no-unused-vars': 'off',
    'vue/multi-word-component-names': 'off',
    'vue/script-setup-no-uses-vars': 'off'
  },
}


下記コマンドを実行
yarn add -D eslint @nuxtjs/eslint-config @nuxtjs/eslint-module @typescript-eslint/eslint-plugin @typescript-eslint/parser @vue/eslint-config-prettier @vue/eslint-config-standard eslint eslint-config-prettier eslint-plugin-nuxt eslint-plugin-prettier eslint-plugin-vue prettier typescript typescript-eslint

色々試して見たけど明示的にtypescriptは追加した方がいいらしい

あとはprettierもいれるか

prettierrc.js

module.exports = {
  tabWidth: 2,
  singleQuote: true,
  semi: false,
  trailingComma: 'all',
  bracketSpacing: true,
  arrowParens: 'avoid',
  vueIndentScriptAndStyle: false,
}

あとは自動フォーマットの設定をworkspaceに追加

.vscode/settings.json
{
  "[vue]": {
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascript]": {
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "dbaeumer.vscode-eslint"
  },
  "[typescript]": {
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "dbaeumer.vscode-eslint"
  },
  "[json]": {
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "dbaeumer.vscode-eslint"
  },
  "[jsonc]": {
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "eslint.format.enable": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "vetur.format.enable": false,
}

あとは一応拡張子のやつも追加

.vscode/extenstions.json
{
  // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
  // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp

  // List of extensions which should be recommended for users of this workspace.
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "Vue.volar"
  ],
  // List of extensions recommended by VS Code that should not be recommended for users of this workspace.
  "unwantedRecommendations": [],
}

試しにセミコロンつけてみると・・・動く!

disneyLadySangodisneyLadySango

さあ、それではNuxtにChakraを導入する
https://vue.chakra-ui.com/

GetStartedを見ると、chakra-ui/nuxtというのがあるらしいがこれが動かなかった
同じくchakra-ui/vueも試してみるがこちらも動かない、エラーを見る限りVue3に対応してないような感じがする。

ということで当該のライブラリのVue3対応状況を確認する
issueを見るとchakra-ui-vue-nextというものでvue3対応とReactのchakra-v1に基づいて作成しているらしい。
これはとても嬉しい!!!

https://github.com/chakra-ui/chakra-ui-vue/issues/116

WIPということでこれもrc版だろうが、標準的なAPIはある程度完成しているらしい
使うことで何も支障がなさそうなので使ってしまう、何か問題あれば後程対応する
基本的にはutilityなスタイルとアクセシビリティを意識したマークアップを行うために使うのでUIコンポーネントは別のものを使う想定

ということでインストールする
yarn add @chakra-ui/vue-next

disneyLadySangodisneyLadySango

そしたらchakraを動かせるようにする

https://next.vue.chakra-ui.com/

公式のChakra登録処理は下記のようになっている

main.js
import { createApp } from "vue";
import App from "./App.vue";
import ChakraUIVuePlugin, { chakra } from "@chakra-ui/vue-next";
import { domElements } from "@chakra-ui/vue-system";

const app = createApp(App);
app.use(ChakraUIVuePlugin);

domElements.forEach((tag) => {
  app.component(`chakra.${tag}`, chakra(tag));
});

app.mount("#app");

これをnuxt.jsでも実現させる必要がありそう

https://v3.nuxtjs.org/guide/directory-structure/plugins#vue-plugins

nuxtでVueプラグインを追加するにはこのようにするとのこと
なるほど、pluginsディレクトリを切ってそこに置けばいいのか

src/plugins/chakra-ui.ts
import ChakraUIVuePlugin, { chakra } from '@chakra-ui/vue-next'
import { domElements } from '@chakra-ui/vue-system'
import { defineNuxtPlugin } from 'nuxt/app'

export default defineNuxtPlugin(nuxtApp => {
  nuxtApp.vueApp.use(ChakraUIVuePlugin)
  // chakra.divなどを書けるようにする
  domElements.forEach(tag => {
    nuxtApp.vueApp.component(`chakra.${tag}`, chakra(tag))
  })
})

お、動いたけどSSRモードが入っていると動かない・・・plugins的には動くらしいと記載があるような気もするがおそらくまだ完全にはSSRに対応できてないという話だと思う
ということで今回はSSRモードをOFFに、SEOとか気にしないのでSPAでも何も問題ないというのもある

nuxt.config.ts
import { defineNuxtConfig } from 'nuxt'

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  ssr: false,
})

disneyLadySangodisneyLadySango

それじゃあ公式にあるコードをそのままコピペして使ってみる
とりあえず動けばいいだけなので下記をごっそりトップページに持ってくる

src/components/SampleChakraCard/SampleChakraCard.vue
<template>
  <CBox
    w="300px"
    font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';"
  >
    <CBox w="100%" shadow="md" rounded="lg" p="5">
      <chakra.img rounded="md" w="100%" src="https://bit.ly/2k1H1t6" />
      <CFlex align="baseline" mt="2">
        <CBadge color-scheme="purple"> Plus </CBadge>
        <CText
          ml="2"
          text-transform="uppercase"
          font-size="sm"
          font-weight="bold"
          color="pink.800"
        >
          Verified &bull; Cape Town
        </CText>
      </CFlex>
      <CText mt="2" font-size="xl" font-weight="semibold" line-height="short">
        Modern, Chic Penthouse with Mountain, City & Sea Views
      </CText>
      <CText mt="2"> $119/night </CText>
      <CFlex mt="2" align="center">
        <CIcon name="star" color="orange.400" />
        <CText ml="1" font-size="sm"> <b>4.84</b> (190) </CText>
      </CFlex>
    </CBox>
  </CBox>
</template>

<script>
import { CFlex, CBox, CBadge, CIcon, CText } from '@chakra-ui/vue-next'
export default {
  name: 'App',
  components: {
    CBadge,
    CBox,
    CFlex,
    CIcon,
    CText,
  },
}
</script>
src/components/SampleChakraCard/index.ts
export { default as SampleChakraCard } from './SampleChakraCard.vue'

コンポーネントを作ってindexで読む

src/pages/index.vue
<template>
  <div>
    <HelloWorld />
    <Counter />
    <SampleChakraCard />
  </div>
</template>

<script lang="ts">
import { HelloWorld } from '@/components/HelloWorld'
import { Counter } from '@/components/Counter'
import { SampleChakraCard } from '@/components/SampleChakraCard'

export default {
  components: {
    HelloWorld,
    Counter,
    SampleChakraCard,
  },
}
</script>

そしたら yarn devでプロジェクトを立ち上げる
http://localhost:3000へアクセス

お、ばっちりうごいた

disneyLadySangodisneyLadySango

よし動いたので次はテーマをカスタムする
とりあえず動けばいいのでspaceをちょっと書き換えてみる

https://vue.chakra-ui.com/extending-theme

こちらを参考に作ってみた

先ほどのVue.useするところにオプションをつける

src/plugins/chakra-ui.ts
import ChakraUIVuePlugin, { chakra } from '@chakra-ui/vue-next'
import { domElements } from '@chakra-ui/vue-system'
import { defineNuxtPlugin } from 'nuxt/app'

import { extendTheme } from '@/styles/chakra'

export default defineNuxtPlugin(nuxtApp => {
  nuxtApp.vueApp.use(ChakraUIVuePlugin, {
     extendTheme: {
        space: {
           4: '4px'
        }
     }
  })
  domElements.forEach(tag => {
    nuxtApp.vueApp.component(`chakra.${tag}`, chakra(tag))
  })
})

が、明らかにスタイルが変わらない・・・なんでだ・・・
と思ってライブラリのAPIを覗いていたらextendsThemeというAPIがある、これを使うっぽい

src/plugins/chakra-ui.ts
import ChakraUIVuePlugin, { chakra, extendTheme } from '@chakra-ui/vue-next'
import { domElements } from '@chakra-ui/vue-system'
import { defineNuxtPlugin } from 'nuxt/app'

import { extendTheme } from '@/styles/chakra'

export default defineNuxtPlugin(nuxtApp => {
  nuxtApp.vueApp.use(ChakraUIVuePlugin, {
     extendTheme: extendTheme({
        space: {
           4: '4px'
        }
     })
  })
  domElements.forEach(tag => {
    nuxtApp.vueApp.component(`chakra.${tag}`, chakra(tag))
  })
})

お!今度は4と指定したところがちゃんとpxで表示されるようになった。
あとはディレクトリ構成をうまいことして上書きしたいものを上書きしていけば良さそう

ただ、これだとデフォルトで設定されているパラメータが外せないっぽいな・・・
と思ったらextendThemeの第二引数にbaseとなるテーマを渡せるみたい
第一引数はあくまでoverrides(上書き)するもので、第二引数がベースっぽい
例えばcolor定義を自前のものだけにしたければ第二引数に下記のように渡すと良さそう

const color = {
  black: '#222',
  white: '#fff'
}
const customTheme = extendTheme({
  color
}, {
  ...extendTheme({}),
  color
})
disneyLadySangodisneyLadySango

問題は型定義が違うことだな・・・これは解決方法がない気もする(公式にも載ってない)
見つけたらここを更新したい(願望)

disneyLadySangodisneyLadySango

さて、それではここからリセットCSSなどをあてていきたい

まずはNuxtではレイアウト層というのが作れるらしい、よくあるヘッダーとフッターを持ったレイアウト的なやつだと思う
ここでResetさせておけば割と後が楽そうなのでレイアウトを実装します

/src/layouts/BaseLayout.vue
<template>
  <CReset />
  <header>ここにヘッダー</header>
  <main>
    <slot />
  </main>
  <footer>ここにフッター</footer>
</template>

<script lang="ts">
import { CReset } from '@chakra-ui/vue-next'

export default {
  components: {
    CReset,
  },
}
</script>
src/pages/index.vue
<template>
  <BaseLayout>
    <HelloWorld />
    <Counter />
    <SampleChakraCard />
  </BaseLayout>
</template>

<script lang="ts">
import { BaseLayout } from '@/layouts/BaseLayout'
import { HelloWorld } from '@/components/HelloWorld'
import { Counter } from '@/components/Counter'
import { SampleChakraCard } from '@/components/SampleChakraCard'

export default {
  components: {
    BaseLayout,
    HelloWorld,
    Counter,
    SampleChakraCard,
  },
}
</script>

disneyLadySangodisneyLadySango

さて、動いたのでStorybookを入れていく

公式にある通りとりあえず動かしてみよう
npx sb init

なんか動いたけど想定していた結果が得られていない・・・.storybookとかが作られない・・・
コマンド叩くだけだとダメっぽいのでやり方を調査する

まずはStorybookの公式を見るとtypeを指定するっぽいことがわかった
https://storybook.js.org/docs/vue/get-started/install#troubleshooting

npx sb init --type vue3をする必要がありそう
試して見たが起動しても動かん。やっぱりダメ

vite使っているからかな?
https://ja.vitejs.dev/

調べたら--builderオプションがあるらしい
https://storybook.js.org/blog/storybook-for-vite/

npx sb init --type vue3 --builder @storybook/builder-vite
が良さそう

調べたら下記の記事がヒットした、ありがたや・・・
https://qiita.com/akagire/items/17fb8d05bf72cb8c3041

実行したところいい感じに入った、これでいけそう

disneyLadySangodisneyLadySango

さて、これだとChakra-uiのコンポーネントが使えないので登録してしまう
preview.jsを下記のように書き換える

.storybook/preview.js
import { app } from '@storybook/vue3'
import ChakraUIVuePlugin, { chakra ,extendTheme } from '@chakra-ui/vue-next'
import { domElements } from '@chakra-ui/vue-system'

import { extendTheme } from '../src/styles/chakra'

// Chakraの登録
app.use(ChakraUIVuePlugin, {
  extendTheme: extendTheme({})
})
domElements.forEach(tag => {
  app.component(`chakra.${tag}`, chakra(tag))
})

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

と思ったらエラーが出る・・・
多分 @chakra-ui/vue-systemが明示的にインストールされていないから
ということで追加
yarn @chakra-ui/vue-system
plugins側でも使うし一応devではなく通常インストールしてくおく

これで実行すると動いた!

disneyLadySangodisneyLadySango

それではさっき作ったコンポーネントの一つでStoryを書いてみる

たまに一回エラーになると動かなくなったりするのでそういう時はキャッシュを削除する
node_modules/.cache にstorybookがあるのでディレクトリを消すと初期化されるっぽい

ということでまずはCounterのstoryを作ってみよう
(デフォルトのストーリーは不要なので消します)

src/components/Counter/__stories__/Counter.stories.ts
import { Meta, Story } from '@storybook/vue3'
import { Counter } from '..'

export default {
  title: 'components/Counter',
  component: Counter,
} as Meta<typeof Counter>

const Template: Story<typeof Counter> = () => ({
  components: { Counter },
  template: '<Counter />',
})

export const Default = Template.bind({})
Default.storyName = 'インクリメントカウンター'

いい感じで表示されている

がこれだとResetが効いてないのでデコレータを追加

.storybook/preview.js
import { app } from '@storybook/vue3'
import ChakraUIVuePlugin, {
  chakra,
  extendTheme,
  CReset,
} from '@chakra-ui/vue-next'
import { domElements } from '@chakra-ui/vue-system'

// Chakraの登録
app.use(ChakraUIVuePlugin, {
  extendTheme: extendTheme({}),
})
domElements.forEach(tag => {
  app.component(`chakra.${tag}`, chakra(tag))
})

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

export const decorators = [
  story => ({
    components: { CReset },
    template: `
      <CReset />
      <story />
    `,
  }),
]

disneyLadySangodisneyLadySango

次はChakraを使ってもちゃんと動くか試してみる
先ほどのカウンターを書き換えてみる

src/components/Counter/Counter.vue
<template>
  <chakra.div display="flex" gap="12" align-items="center">
    <chakra.p font-size="20" font-weight="bold"
      >件数: {{ state.count }}</chakra.p
    >
    <CButton
      @click="increment"
      type="button"
      mt="1"
      color="white"
      background-color="blue"
      :_hover="{}"
      :_active="{
        backgroundColor: 'blue.500',
        outline: 'none',
      }"
    >
      increment
    </CButton>
  </chakra.div>
</template>

<script lang="ts">
import { reactive } from 'vue'
import { CButton } from '@chakra-ui/vue-next'

export default {
  name: 'Counter',
  components: {
    CButton,
  },
  setup() {
    const state = reactive({ count: 0 })

    const increment = () => {
      state.count++
    }

    return {
      state,
      increment,
    }
  },
}
</script>

よし!ちゃんと動いてる!

disneyLadySangodisneyLadySango

これを今後Stableになったら再度適用させてみたいところではあるが今回はここまで(その時はちゃんと記事にしよう)
testを追加したかったが一向にうまくいかんので諦めます、プラグインの登録がうまくできていない

このスクラップは2022/09/04にクローズされました