電通総研 テックブログ

電通総研が運営する技術ブログ

Nuxt 3 の Stable 版がリリースされたので触ってみる

本記事は電通国際情報サービス Advent Calendar 2022の8日目の記事です。
執筆者は Xイノベーション本部 AI トランスフォーメーションセンター所属の山田です。

この記事では先日(2022年11月中旬)に安定版がリリースされた Nuxt 3 について紹介します。

なお、執筆時点での私たちのチームでは、本番プロジェクトへの Nuxt 3 の導入には至っていません。 本記事は、あくまで技術調査をしながら簡単なアプリケーションを開発してみた内容をまとめたものになります。

本記事で紹介するソースコードは以下のリポジトリで公開しています。

github.com

Nuxt 3 について

はじめに、簡単に Nuxt 3 について紹介しておきます。

Nuxt は Vue.js のフレームワークです。
冒頭でも述べましたが、2022年11月中旬に Nuxt 3 の安定版がついにリリースされました。

Announcing Nuxt 3.0 stable、 https://nuxt.com/v3

Nuxt 3 のリリースで大きいのは Vue 3 の正式なサポートです。

Vue 3 は 2020年9月にリリースされていましたが、Nuxt 2 ではサポートされていませんでした。
Vue 3 から追加された Compositon API などを利用するために、Nuxt 2 のプロジェクトで Nuxt Composition APIなどのパッケージを利用している人も多かったのではないでしょうか。

今回リリースされた Nuxt 3 は Vue 3 も公式にサポートしていますし、サーバエンジンも Nitro に刷新され、パフォーマンスも向上しています。

開発環境

本記事での利用した開発環境の情報を記載します。

  • 開発環境/IDEGitHub Codespaces
    • リージョン:Southeast Asia
    • スペック:CPU 4コア、メモリ 8 GB
  • Node.js のバージョン管理ツール:nvm
  • Node.js バージョン:v18.12.1
  • パッケージマネージャー・バージョン:Yarn / 1.22.19
  • ブラウザ:Google Chrome

せっかくなので、GitHub Codespaces を用いてみました。 Node.js のバージョン管理ソフトには GitHub Codespaces 上でデフォルトでインストールされていた nvm を利用しています。 Node.js のバージョンは LTS の最新バージョンである v18 系を、パッケージマネージャーには Yarn を使っています。

VSCode拡張機能は以下のものを利用します。

Chrome拡張機能は以下のものを利用します。

Nuxt 3 のプロジェクト作成と TypeScript の設定

Nuxt 3のプロジェクト作成はnpx nuxi initコマンドで行います。

npx nuxi init ${プロジェクト名}

プロジェクト作成段階でできるディレクトリ構成は以下のようになります。

プロジェクト名
├── README.md
├── app.vue
├── nuxt.config.ts
├── package.json
└── tsconfig.json

TypeScript の設定

Nuxt はデフォルトだと TypeScript の厳格な型チェックが有効になっていないので有効にしましょう。

まずは TypeScript と vue-tsc を devDependency に追加します。

yarn add -D typescript vue-tsc

次に nuxt.config.ts を以下のように編集します。

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
  typescript: {
    shim: false,
    strict: true,
    typeCheck: true
  },
});

shim: falseとしているのは、VSCode で TypeScript Vue Plugin (Volar) を利用するためです。
strict: trueにすることで、型チェックが厳格になります。
typeCheck: trueとしているのは、開発時から型チェックを有効にするためです。Nuxt ではビルドパフォーマンスを最適化するためにデフォルトでは型チェックが行われません。

このあたりの詳細については以下の公式ドキュメントを参照してください。

Nuxt - Installation、 https://nuxt.com/docs/getting-started/installation
Nuxt - type-checking、 https://nuxt.com/docs/guide/concepts/typescript

また Nuxt 3 からは CLI での型チェックが実行できます。

yarn nuxi typecheck

もしも開発中のホットリロード時にビルドパフォーマンスを求めるならば、typeCheck: falseとして CLI での手動でのチェックという選択肢もあります。
以下のように npm スクリプトに typecheck コマンドを登録しておいてもいいかもしれません。

{
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "typecheck": "nuxt typecheck"
  }
}

ESLint の設定

次に ESLint の設定をしましょう。

Nuxt では VSCode で開発する際には ESLint プラグインを利用することが推奨されています。
注意点として Prettier は無効にすることが勧められています。

Nuxt - use-eslint、 https://nuxt.com/docs/community/contribution#use-eslint

ということで、まずは ESLint を devDependency に追加します。

yarn add -D @nuxtjs/eslint-config eslint

そして ESLint の設定ファイル.eslintrcを作成します。

touch .eslintrc

.eslintrcは以下のように記述します。

{
  "extends": [
    "@nuxtjs/eslint-config-typescript"
  ]
}

すでに Prettier を導入している開発者に配慮して、Prettier によるフォーマットがかからないようにしましょう。 .prettierignoreを用意し、プロジェクト内のすべてのファイルをフォーマット対象外にしてしまいましょう。

以下が.prettierignoreの記述内容になります。

# プロジェクト内のすべてのファイルをPrettierの対象外にする
**

.vscode/settings.jsonを設定し、保存時に ESLint によるフォーマットがかかるようにしましょう。

{
  "editor.codeActionsOnSave": {
    "source.fixAll": false,
    "source.fixAll.eslint": true
  }
}

最後に npm スクリプトに lint コマンドを登録しておきましょう。

{
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "typecheck": "nuxt typecheck",
    "lint": "eslint --ext .ts,.js,.vue .",
  },
}

これにより以下のコマンドで ESLint によるコードの静的解析、フォーマットを実施できます。

# 静的解析
yarn lint

# フォーマット
yarn lint --fix

簡単なアプリケーションを開発してみる

環境が整ったので、ここからは簡単なアプリケーションを開発しながら Nuxt 3 について触れていきましょう。

今回は The Cat API を利用した、猫を愛でることができるアプリケーションを作ります。

The Cat API - Cats as a Service. https://thecatapi.com/

The Cat API では API キーがなくても、10枚まではランダムに猫の画像が取得できます。 完成形のイメージとしては、取得した猫画像をグリッド形式で表示するアプリケーションとします。

作成するアプリケーションの画面イメージ

コンポーネントの設計

はじめにコンポーネントを設計しましょう。
Vue.js はコンポーネント指向なフレームワークですので、画面デザインからコンポーネントを設計しておくのが大事です。

コンポーネントの構成

画像出典:コンポーネントの基本 - コンポーネントの構成、 https://v3.ja.vuejs.org/guide/component-basics.html

今回は手始めに以下のようなコンポーネント構成を考えましょう。

ツリー形式で可視化すると以下のようになります。

設計したコンポーネントのツリービュー

pages/index.vueの作成

はじめにpagesコンポーネントindex.vueの追加しましょう。
Nuxt 3 では nuxi addコマンドで簡単にpagesコンポーネントを追加できます。

npx nuxi add page index

作成時点では以下のような形になっています。

<script lang="ts" setup></script>

<template>
  <div>
    Page: foo
  </div>
</template>

<style scoped></style>

Nuxt 2 までの単一ファイルコンポーネント内では、

  1. template
  2. script
  3. style

という順番で記述されるのが一般的でしたが、Nuxt 3では

  1. script
  2. template
  3. style

の順で記述するのが一般的なようです。

また script 部分は<script setup>構文での記述がベースとなっています。 <script setup>構文の詳細については Vue.js 3 の公式ドキュメントを参照してください。

Vue.js - SFC <script setup>https://v3.ja.vuejs.org/api/sfc-script-setup.html

app.vue の削除

Nuxt ではpages配下の Vue ファイルでアプリケーションのルーティングが生成されます。 そのため、プロジェクト作成時点でのエントリポイントであるapp.vuepagesコンポーネントを作成後は不要になるため削除します。

rm app.vue

API 呼び出しでのデータ取得

pages/index.vueで The Cat API を呼び出して、データを取得する処理を記述します。
外部API呼び出しには Nuxt 組込みのuseFetchを利用します。

useFetch、https://nuxt.com/docs/api/composables/use-fetch

呼び出し前に取得するデータの型を定義しておきましょう。
今回、利用する The Cat API のレスポンスは以下のようになっています。

[
  {
    "id": "5ni",
    "url": "https://cdn2.thecatapi.com/images/5ni.jpg",
    "width": 500,
    "height": 375
  }
]

https://api.thecatapi.com/v1/images/search

この情報をもとに型情報を定義しましょう。
型定義はtypesディレクトリ配下に記述することにします。

# typesディレクトリの作成
mkdir types

touch types/index.ts

interfaceでレスポンスの型を定義します。

export interface CatResponse {
  id: string;
  url: string;
  width: number;
  height: number;
}

そして定義した型情報をインポートしつつ、useFetchでのAPI呼び出しのコードを記述します。

<script lang="ts" setup>
import { CatResponse } from '~/types'

const { pending, error, data } = useFetch<CatResponse[]>(
  'https://api.thecatapi.com/v1/images/search?limit=10'
)
</script>

<template>
  <div>
    Page: foo
  </div>
</template>

<style scoped></style>

useFetchを利用する際に、インポート文を書いていないことに違和感がある方もいるのではないでしょうか?
Nuxt 3 では、Auto Imports 機能があり、Vue.js標準のrefcomputed Nuxt 組込みのuseFetchなどの関数を明示的にインポートすることなく記述できます。

Nuxt - auto-imports、 https://nuxt.com/docs/guide/concepts/auto-imports

動作確認

ここまででアプリケーションを起動し、Chrome の Vue.js devtoolsを確認しましょう。

# 開発サーバでアプリケーションを起動
yarn dev

Chrome の Vue.js devtoolsを確認すると、きちんとデータ取得できていることがわかります。

Devetoolsでの値チェックの様子

コンポーネントの作成

続いてコンポーネントを作成していきましょう。
コンポーネントnuxi addコマンドで簡単に追加できます。

# CatCardListコンポーネントの作成
npx nuxi add component CatCardList

# CatCardコンポーネントの作成
npx nuxi add component CatCard

CatCard コンポーネントの作成

まずはより小さい階層のCatCardコンポーネントを作成します。
CatCardコンポーネントでは、propsを介して1つのCatResponseオブジェクトを受け取ります。 そして画像のURLを<img>タグのsrcに渡します。

以下がCatCardコンポーネントのコードになります。

<script lang="ts" setup>
import { CatResponse } from '~/types'

interface Props {
  catData: CatResponse;
}

defineProps<Props>()
</script>

<template>
  <div>
    <img
      class="card-img"
      :src="catData.url"
      alt="cute cat"
      :data-testid="catData.id"
    >
  </div>
</template>

<style scoped>
.card-img {
  border-radius: 8px;
  width: 320px;
  height: 320px;
  object-fit: cover;
}
</style>

CatCardList コンポーネントの作成

次にCatCardListコンポーネントを作成します。 CatCardListコンポーネントでは、propsを介してCatResponseオブジェクトの配列受け取ります。
そしてv-for構文で配列を展開し、CatCardコンポーネントを複数描画します。

以下がCatCardListコンポーネントのコードになります。

<script lang="ts" setup>
import { CatResponse } from '~/types'

interface Props {
  catList: CatResponse[];
}

defineProps<Props>()
</script>

<template>
  <div class="card-list" data-testid="cat-list">
    <cat-card v-for="(cat, i) in catList" :key="i" :cat-data="cat" />
  </div>
</template>

<style scoped>
.card-list {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 16px;
}
</style>

pages/index.vueから作成したコンポーネントを呼びだす

作成したCatCardListコンポーネントindex.vueから呼び出します。
コンポーネントについても Nuxt 側での Auto Imports が有効になっているため、明示的にインポートする必要はありません。

<script lang="ts" setup>
import { CatResponse } from '~/types'

const { pending, error, data } = useFetch<CatResponse[]>(
  'https://api.thecatapi.com/v1/images/search?limit=10'
)
</script>

<template>
  <div v-if="!pending && data" class="content">
    <cat-card-list :cat-list="data" />
  </div>
</template>

<style scoped>
.content {
  margin: auto;
}
</style>

CatCardList は dataの値をプロパティにわたす必要があるためv-ifで条件付きレンダリングにします。

あとは、少しレイアウトを整えれば以下のようなアプリケーションが完成します。

作成したアプリケーションの画面

isid.github.io

テスト

最後にテストについても触れましょう。
Vue.js 3 系からはテストツールとして Vitest が推奨されています。

Vue.js - テスト #推奨事項、 https://ja.vuejs.org/guide/scaling-up/testing.html#recommendation

Nuxt 3 の公式ドキュメントではテスト用ツールに@nuxt/test-utils-edgeが紹介されていますが、こちらは開発中で不安定です。
なので今回はコンポーネントレベルのテストだけを実施する Vue Testing Library(@testing-library/vue)を用いる方法を紹介します。

なお Vue.js のテストツールとしては、 Vue Test Utils もありますが、コンポーネントレベルのテストでは@testing-library/vueの利用が推奨されています。

Vue.js - テスト コンポーネントのテスト#推奨事項、 https://ja.vuejs.org/guide/scaling-up/testing.html#recommendation-1

準備

まずは必要なパッケージをインストールします。
テスト時にコンポーネントを描画するDOM環境にはhappy-domを使用します。

yarn add -D vitest @testing-library/vue happy-dom

Vitest の設定ファイルvitest.config.tsを作成します。

/// <reference types="vitest" />
import { defineConfig } from 'vitest/config'
import Vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [Vue()],
  resolve: {
    alias: {
      '~': `${__dirname}`
    }
  },
  test: {
    root: '.',
    globals: true,
    environment: 'happy-dom'
  }
})

npm スクリプトに test コマンドを追加します。

{
    "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "typecheck": "nuxt typecheck",
    "lint": "eslint --ext .ts,.js,.vue .",
    "test": "vitest",
  },
}

コンポーネントレベルのテスト

CatCardコンポーネントのテストを記述しましょう。 CatResponse形式のオブジェクトをpropsに渡した際に、コンポーネントが描画できるかテストします。

テストコードはtestsディレクトリ配下に*.spec.tsの形で配置します。

@testing-library/vueでのテストコードは以下のようなります。
コンポーネントが描画できているかの判断にはdata-testid属性を使っています。

import { describe, expect, test } from 'vitest'
import { render } from '@testing-library/vue'

import CatCard from '~/components/CatCard.vue'
import { CatResponse } from '~/types'

describe('CatCard', () => {
  test('コンポーネントの描画ができること', () => {
    const catData: CatResponse = {
      id: 'test',
      url: 'https://example.com',
      width: 100,
      height: 100
    }
    const { getAllByTestId, html } = render(CatCard, { props: { catData } })
    const results = getAllByTestId(catData.id)

    expect(results.length).toBe(1)
    expect(html()).contain(`data-testid="${catData.id}"`)
  })
})

テストを実行してみましょう。

yarn test

きちんとテストが通れば、以下の出力が得られます。

 RERUN  tests/components/CatCard.spec.ts x20

 ✓ tests/components/CatCard.spec.ts (1)

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  09:40:00
   Duration  230ms


 PASS  Waiting for file changes...
       press h to show help, press q to quit

Vitest の VSCode プラグインデバッグ

テストコードを書くことによって、小さい範囲でコードを動かせるようになりました。
さらにデバッグもできると開発がより捗ります。

Vitest では公式で VSCodeプラグインをリリースしており、こちらを活用することでテストコードをもとにデバッガーを起動できます。

Vitest、 https://marketplace.visualstudio.com/items?itemName=ZixuanChen.vitest-explorer
VItes - IDE Integrations 、 https://vitest.dev/guide/ide.html

テストコードにブレークポイントを仕込んで、テストタブから「テストのデバッグ」を選択するとデバッガーが起動できます。
これで開発環境はバッチリですね。

デバッガーを起動している様子

まとめ

本記事では、Nuxt 3 について紹介しました。

今回、記事を書きながら 、これまでの Nuxt の良さを引き継ぎながら正当に進化していると感じました。
nuxiコマンドの充実や TypeScript、VSCodeとの親和性の向上、Nitroによるビルド速度の向上など開発がより楽しくなる可能性を感じました。

一方でテストの部分はまだ不安定だと言わざるを得ません。
Nuxt 側の Auto Imports 機能と Vitest の連携がうまくいかなかったり @nuxt/test-utils-edge モジュールが開発中であるため Nuxt 3 のアプリケーションをテストする方法が確立していません。

support for unit testing in a nuxt environment #2465、 https://github.com/nuxt/framework/issues/2465
Nuxt - Testing、https://nuxt.com/docs/getting-started/testing#testing

直近で Nuxt 3 の導入を検討しているならば、この部分には注意をする必要があります。


最後に、私たちAIトランスフォーメーションセンターでは、一緒に働いてくれる仲間を探しています。 AI 製品開発に興味がある方のご応募をお待ちしております。

執筆:@yamada.y、レビュー:寺山 輝 (@terayama.akira)Shodoで執筆されました