Nuxt3(RC)&Vue3 + ESLint + Chakra-ui + Storybookの環境構築
Nuxt3のstabeleは2022年Summer(なお、今は9月3日)
日本人の感覚ではあくまで夏とは8月末までの感覚。
しかし大学生という枠組みで見ると夏休みは9月末くらいまでと考えるときっと海外の夏も9月末までなんだろう(真顔)
なお、本日RC9がnpmに公開されているがRC8で環境構築をしているので要注意
(issueにはRC9、早速バグってるけど報告あるけど気にしない)
早くStable出てほしい・・・
Nuxtを触るのが今回初めてなので何が変わったのか、書き味が変わったのかとかは正直分かりません、許してください
普段React + Next.jsでChakara-uiを作っているのでゴールは下記
- Chakra-uiでNuxt3を動かす
- Chakra-uiでテーマを上書きする
- 任意のコンポーネントでStorybookを動かす
早速Nuxt3を環境作っていく
- Node.js
- LTSがいいらしい
- 14.16以降か、16.11以降とのこと
- 今回はNode16.17.0を使います
- VSCode
- 普段FE開発してあるのでヨシ!
- Volar extenstion
- インストールDONE
- Take Over Modeをオンにする必要があるらしい
- https://github.com/johnsoncodehk/volar/discussions/471
nuxtのプロジェクトを作成
npx nuxi init <プロジェクト名>
そしてプロジェクトのディレクトリに入りyarn --frozen-lockfile
これでnode_modulesが入る
package.jsonがとてもシンプル
nuxtがdevDependenciesに入っている・・・stableになったらdevから消えるのかな・・・?
これ本番稼働させるなってことなのかな?
プロジェクトの構成はrootディレクトリにcomponentsとかpagesを切るらしい(ここはNext.jsも同じなので使い勝手は同じ)
とはいえ、rootに色々別のファイルとかディレクトリ切るので資材と混在すると見通しが悪いのでsrcディレクトリを切る
設定がここにあるのでここを参考に nuxt.config.ts に設定を追加する
import { defineNuxtConfig } from 'nuxt'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
srcDir: 'src/',
})
Nuxtには自動import機能があるらしい
今までは明示的なimportだったのかな?
少なくとも普段使いのVue.jsだと明示的import
自動importとして使えるのは
componentsとcomposablesとのこと
composablesはReactでいうところのhooks
あと気をつけないといけないのはserverディレクトリ内では今動かないとのこと
Storybookと単体テストを書くのであればコンポーネントディレクトリ内に複数配置したいというのもあるので
これ使わない方が良さそうな気がしている。
機能をOFFにするので下記をconfigへ追加する
export default defineNuxtConfig({
imports: {
autoImport: false
}
})
ということでこのタイミングで一回適当に動かしてみよう
まずは適当なコンポーネントを作る
<template>
<div>Hello World!</div>
</template>
Reactチックに使いたい願望があるのでこいつをnamed exportさせる
(こうするとなんか問題あるのかな・・・今までvue.js使っていてそれには当たってない)
export { default as HelloWorld } from "./HelloWorld.vue";
そしてこいつを pagesのindexで読み込みさせてみる
と思ったけどimportの時にサジェストが出てこない・・・
aliasの設定もしたいのでconfigに下記を追加
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/*"],
},
},
},
},
});
<template>
<div>
<HelloWorld />
</div>
</template>
<script lang="ts">
import { HelloWorld } from '@/components/HelloWorld'
export default {
components: {
HelloWorld
}
}
</script>
よしちゃんと表示されている
せっかくなのでReactでいうhooks、Vue3のcompositionAPIとやらを試してみよう
あと何が嬉しいかってtemplateの下は複数返してもよくなったこと
これで今までdiv追加していたところが綺麗なコンポーネントを切れるようになった
これは大変嬉しい
ということでインクリメントする簡単なカウンターを用意する
<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>
export { default as Counter } from "./Counter.vue";
これを先ほどのindexで読み込む
<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>
いい感じでできたぞ
と書いていて思った
自動整形全然されないw
ということでlintの設定をする
こちらのブログの設定を参考にしてみよう
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もいれるか
module.exports = {
tabWidth: 2,
singleQuote: true,
semi: false,
trailingComma: 'all',
bracketSpacing: true,
arrowParens: 'avoid',
vueIndentScriptAndStyle: false,
}
あとは自動フォーマットの設定をworkspaceに追加
{
"[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,
}
あとは一応拡張子のやつも追加
{
// 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": [],
}
試しにセミコロンつけてみると・・・動く!
さあ、それではNuxtにChakraを導入する
GetStartedを見ると、chakra-ui/nuxtというのがあるらしいがこれが動かなかった
同じくchakra-ui/vueも試してみるがこちらも動かない、エラーを見る限りVue3に対応してないような感じがする。
ということで当該のライブラリのVue3対応状況を確認する
issueを見るとchakra-ui-vue-nextというものでvue3対応とReactのchakra-v1に基づいて作成しているらしい。
これはとても嬉しい!!!
WIPということでこれもrc版だろうが、標準的なAPIはある程度完成しているらしい
使うことで何も支障がなさそうなので使ってしまう、何か問題あれば後程対応する
基本的にはutilityなスタイルとアクセシビリティを意識したマークアップを行うために使うのでUIコンポーネントは別のものを使う想定
ということでインストールする
yarn add @chakra-ui/vue-next
そしたらchakraを動かせるようにする
公式のChakra登録処理は下記のようになっている
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でも実現させる必要がありそう
nuxtでVueプラグインを追加するにはこのようにするとのこと
なるほど、pluginsディレクトリを切ってそこに置けばいいのか
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でも何も問題ないというのもある
import { defineNuxtConfig } from 'nuxt'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
ssr: false,
})
それじゃあ公式にあるコードをそのままコピペして使ってみる
とりあえず動けばいいだけなので下記をごっそりトップページに持ってくる
<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 • 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>
export { default as SampleChakraCard } from './SampleChakraCard.vue'
コンポーネントを作ってindexで読む
<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
へアクセス
お、ばっちりうごいた
よし動いたので次はテーマをカスタムする
とりあえず動けばいいのでspaceをちょっと書き換えてみる
こちらを参考に作ってみた
先ほどのVue.useするところにオプションをつける
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がある、これを使うっぽい
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
})
問題は型定義が違うことだな・・・これは解決方法がない気もする(公式にも載ってない)
見つけたらここを更新したい(願望)
さて、それではここからリセットCSSなどをあてていきたい
まずはNuxtではレイアウト層というのが作れるらしい、よくあるヘッダーとフッターを持ったレイアウト的なやつだと思う
ここでResetさせておけば割と後が楽そうなのでレイアウトを実装します
<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>
<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>
さて、動いたのでStorybookを入れていく
公式にある通りとりあえず動かしてみよう
npx sb init
なんか動いたけど想定していた結果が得られていない・・・.storybook
とかが作られない・・・
コマンド叩くだけだとダメっぽいのでやり方を調査する
まずはStorybookの公式を見るとtypeを指定するっぽいことがわかった
npx sb init --type vue3
をする必要がありそう
試して見たが起動しても動かん。やっぱりダメ
vite使っているからかな?
調べたら--builderオプションがあるらしい
npx sb init --type vue3 --builder @storybook/builder-vite
が良さそう
調べたら下記の記事がヒットした、ありがたや・・・
実行したところいい感じに入った、これでいけそう
さて、これだとChakra-uiのコンポーネントが使えないので登録してしまう
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ではなく通常インストールしてくおく
これで実行すると動いた!
それではさっき作ったコンポーネントの一つでStoryを書いてみる
たまに一回エラーになると動かなくなったりするのでそういう時はキャッシュを削除する
node_modules/.cache にstorybookがあるのでディレクトリを消すと初期化されるっぽい
ということでまずはCounterのstoryを作ってみよう
(デフォルトのストーリーは不要なので消します)
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が効いてないのでデコレータを追加
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 />
`,
}),
]
次はChakraを使ってもちゃんと動くか試してみる
先ほどのカウンターを書き換えてみる
<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>
よし!ちゃんと動いてる!
これを今後Stableになったら再度適用させてみたいところではあるが今回はここまで(その時はちゃんと記事にしよう)
testを追加したかったが一向にうまくいかんので諦めます、プラグインの登録がうまくできていない