22
18

More than 1 year has passed since last update.

Nuxt3 + 開発環境構築 + 基本

Last updated at Posted at 2023-02-16

はじめに

公式ドキュメントをしっかり読もう。
なんとなくで始めても良いけど、結局、公式ドキュメントに戻ることが多かった・・・。
理解度によるんだろうけど。

ハイドレーション

サーバーサイドレンダリング(SSR)などで事前にサーバー側で作られたHTMLを、JavaScriptで動的にしていくプロセスのこと。

①サーバーサイドで作られたHTMLをブラウザが受け取る。

②javascriptがサーバーサイドで作られたHTMLと期待しているHTML(ブラウザに表示されているHTML)をチェックする。
違っていれば、javascriptが再度レンダリングする。

③期待するHTMLに対してjavascriptのイベントを登録していく。

個人的に詳細まで理解できていないので、SEOが心配なときはcurlでhtmlを確認するのが良さそうな気がする。(心配性)

インストール

node.jsのバージョンは、16.11以上が必要です。node --version で確認してください。

①インストールはnpxコマンドで行います。npx nuxi init <project-name>
npxコマンドはnpmバージョン5.2.0より同梱されているコマンドで、ローカルにインストールしたコマンドを実行するために使われます。

nuxt 2とは違い、インタラクティブなやり取りはありません。最初からプロジェクト名を入力します。

node 18で利用できるようになったfetch APIでエラーが出ます。
ERROR (node:22556) ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time
ErrorなのかWarningなのかわかりずらいですが、内容的にはお知らせに近いです。

 Nuxt project is created with v3 template. Next steps:
 › cd nuxt3-sample
 › Install dependencies with npm install or yarn install or pnpm install
 › Start development server with npm run dev or yarn dev or pnpm run dev

と表示されているので次に進みます。

②パッケージをインストールします。npm install

これでインストールは完了です。

③development modeで確認 npm run dev -- -o

ブラウザが起動し、http://localhost:3000/ に「Welcome to Nuxt!」が表示されれば成功です。

※ npm run dev package.jsonに書かれているコマンドに引数を渡すときには -- [渡したい引数] となる。この場合 nuxt dev -o が実行される
-o はブラウザを開いてくれるオプション

Nuxt Bridgeを利用することで2系から3系にスムーズに移行できるようですが、今回は考慮しません。

ディレクトリ確認

node_modulesは省略

/
│  .gitignore
│  .npmrc
│  app.vue
│  nuxt.config.ts
│  package-lock.json
│  package.json
│  README.md
│  tsconfig.json
│
├─.nuxt
│  │  app.config.mjs
│  │  components.d.ts
│  │  imports.d.ts
│  │  nuxt.d.ts
│  │  nuxt.json
│  │  tsconfig.json
│  │
│  ├─dev
│  │      index.mjs
│  │      index.mjs.map
│  │
│  ├─dist
│  │  └─server
│  │          client.manifest.json
│  │          client.manifest.mjs
│  │          server.mjs
│  │
│  └─types
│          app.config.d.ts
│          imports.d.ts
│          nitro.d.ts
│          plugins.d.ts
│          schema.d.ts
│          vue-shim.d.ts
│
└─public
        favicon.ico

app.vueファイルのみであとはコンフィグファイルです。app.vueファイルを確認することにします。

app.vue

NuxtWelcomeコンポーネントは@nuxt/uiにあるそうです。

<template>
  <div>
    <NuxtWelcome />
  </div>
</template>

Remove this welcome page by replacing in app.vue with your own code.

と書かれているので、試してみます。

<template>
  <div>
    <p>Hello World</p>
  </div>
</template>

ブラウザの表示が変わります。(ホットリロード)

app.vueファイルは、3系のメインコンポーネントです。
複数のページを作成する場合はpagesディレクトリを利用しますがpagesディレクトリはオプションなので必ず利用する必要はありません。
ランディングページのみ、ルーティングが必要ないアプリケーションの場合は、app.vueファイルを利用するだけでアプリケーションを構築することができます。

app.vueファイルは必須ではありません。なかった場合、後述のdefaultのlayoutが呼ばれます。

pages

自動ルーティングをそのまま採用されているなら、ディレクトリ構造はそのままURLのパスに反映されます。手動でルーティングする場合はVue Routerを使用しますが、ここでは触れません。

アンダースコアで始まる名前 _id のファイルおよびディレクトリは、そこがパラメータになることを表しています。

[id]で囲んだ値を任意の名前をつけることでパラメータにすることができます。(Next.jsと同じですね。)

値は、routeオブジェクトを利用して取得することができます。

$route.params.id

scriptタグ内で参照するときは、useRoute関数を利用します。

<script setup>
const router = useRoute();
console.log(router.params.id);
</script>

話を戻します。index.vue ファイルを用意して、app.vue ファイルを編集し<NuxtPage />を追加します。

<template>
  <div>
    <NuxtPage />
  </div>
</template>

index.vueの内容が表示されるようになります。

aboutディレクトリを作成し、そこにindex.vueファイルを作成します。
http://localhost:3000/about
でaboutページが表示されることを確認します。

※ ルートにabout.vueを配置しても同じ結果になります。お好みで良いと思います。

layouts

レイアウトファイル。ベースとなるファイル。ASP.Netだとマスターページ、Ruby on Railsだとレイアウトテンプレートとか呼ばれます。

layoutsディレクトリを作成し、default.vueファイルを作成します。slot部分にapp.vueに指定されたの内容が展開されるイメージです。

(Nuxt 2では、<Nuxt />でした)

<template>
  <div>
    <h2>共通</h2>
    <slot />
  </div>
</template>

※ slotに名前を付けることができます。必要になったときに追記します。

app.vueファイルを編集し、<NuxtPage />タグを<NuxtLayout>でラップします。

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

正確にはラップでなくてもかまいません。

<template>
  <NuxtLayout><b>hoge</b></NuxtLayout>
  <i><NuxtPage /></i>
</template>

layoutファイルを適用したくないとき

definePageMeta関数で指定します。

<script setup>
definePageMeta({
  layout: false,
});
</script>

default.vue以外のlayout

definePageMeta関数で指定します。

<script setup>
definePageMeta({
  layout: 'ファイル名(拡張子除く)',
});
</script>

または、NuxtLayoutタグにname属性を付けます。

<template>
  <NuxtLayout name="sample">
    <NuxtPage />
  </NuxtLayout>
</template>

pageファイルで指定

app.vueではなく、pageファイルに<NuxtLayout>を定義することができます。ただし、ルートタグとしては設定できません。

<template>
  <div>
    <NuxtLayout>
      <h1>Page</h1>
    </NuxtLayout>
  </div>
</template>

動的にレイアウト変更

必要になったときに追記します。できるんだなと言うことを知っていればよいです。

components

コンポーネントとして切り出されたファイル。これらがページファイルでインポートして使われる。
Nuxt 3では、componentsディレクトリに保存したコンポーネントファイルは自動でimportされるためimport処理を行う必要がありません。
(個人的にこれは好きです。import, export地獄が緩和される?実際に開発してると別の弊害が出てくるのかもだけど)

Auto Importsを利用しないで通常のimportを利用することができます。

<script setup>
import GlobalNavi from '@/components/GlobalNavi';
</script>

ディレクトリのスペルミスで認識されずパニクリました。

GlobalNavi.vueファイルを作成します。

<template>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
  </nav>
</template>

必要なところで <GlobalNavi /> を定義します

ディレクトリ分けしたコンポーネント

componentsにディレクトリをつくった場合、ディレクトリ名 + コンポーネント名となります。

例えば、/componets/Global/Navi.vue であれば<GlobalNavi />となります。

Lazy Loading

componentsを必要な時にダウンロードすることができます。ブラウザに表示されたときに画像をロードさせるコンポーネント版だとざっくり思ってください。

コンポーネント名にLazyをつけます。

<LazyGlobalNavi />

必要になったときに追記します。こんなこともできるんだなぁという知識として持っておいてください。

NuxtLinkコンポーネント

aタグの代わりにNuxtLinkコンポーネントを利用することでページ全体を読み込むことなくページ遷移させることができます。
外部リンクのaタグと内部リンクのNuxtLinkコンポーネントといった使い分けをする必要はありません。
NuxtLinkコンポーネントが良い感じに処理してくれます。RailsのTurbolinksと思えば当たらず遠からず。

念のため、curlでhtmlソースを確認します。通常のaタグになっています。SEO上の不利益となることはなさそうです。
(ブラウザのソース表示でも良いですが、botにどう見えているかはcurlが確実です。)

assets

ビルドツール(Vite や webpack)に処理させたい全てのアセットをここに配置します。圧縮したいcssとか。

css

assetsディレクトリの下にcssディレクトリを作成し、cssファイルを作成します。

nuxt.config.tsファイルに利用するcssファイルを定義します。
(railsのapp/assets/stylesheets/application.css みたいな感じですかね。)

export default defineNuxtConfig({
  css: ['/assets/css/style.css'],
})

画像

assetsディレクトリの下に画像を置いて、コンポーネントからその画像を参照するときは注意が必要です。

<img src="~/assets/hello.ico" />

~が必要になります。assetsディレクトリは、_nuxt配下にコンパイルされます。

publicディレクトリ

web serverのドキュメントルートと思ってください。静的なファイルを配置します。

composables

Reactのカスタムフックに対応します。(カスタムフックとは、名前が ”use” で始まり、ほかのフックを呼び出せる JavaScript の関数のことです。)

composablesディレクトリに保存するとcomponents同様に自動importの対象になります。

と言われても、言葉だとピンとこないので、そんな時はサンプルを作ります。

composables/counter.ts を作成します

export const useCounter = (initialValue: number) => {
  const count = ref(initialValue);
  const increase = () => (count.value++);
  const decrease = () => (count.value--);
  return {
    count,
    increase,
    decrease,
  };
}

importを書かなくても使えます。

pages/index.vue

<script setup>
  const { count, increase, decrease } = useCounter(0);
</script>

<template>
  <div>
    <GlobalNavi />
    <p>Hello World</p>
    <div>Count:{{ count }}</div>
    <div>
      <button @click="() => increase()">increase</button>
      <button @click="() => decrease()">decrease</button>
    </div>
  </div>
</template>

話はそれていきますが、Compositon API でリアクティブなデータを定義するためにrefとreactiveが用意されています。こちらを簡単にメモします。
リアクティブなデータとは、変更があったら即時反応するみたいな理解で大丈夫です。上記の場合countが、増えたり減ったりするのがリアクティブって感じです。

ref

プリミティブな値(string, number など)を引数にとりリアクティブなデータを定義します。
ref メソッドの返り値の型は Ref(T は引数に渡した値の型)というオブジェクトになります。

ref で定義した値にアクセスするためにはvalue というプロパティにアクセスする必要があります。
<template> 内で使うときには .value を省略できます。

reactive

reactive メソッドはオブジェクトを引数に受け取り、リアクティブにしたコピーを返します。

const user = reactive({
  firstName: 'Stephen',
  lastName: 'Curry',
  age: 34,
});

const firstName = ref('Stephen');
const lastName = ref('Curry');
const age = ref(34);

reactive メソッドの返り値の型は元のオブジェクトのままです。また、分割代入するとリアクティブ性が失われます。

let { fristName, latName, age } = user;

分割代入を利用したい場合には toRefs を使って ref に変換した値として使う必要があります。

let { fristName, latName, age } = toRefs(user);

plugins

pluginsディレクトリを作成してプラグインファイルを作成するとアプケーションの起動時に自動で登録を行ってくれるため登録作業を行う必要がありません。
Nuxt2ではnuxt.config.tsに利用するプラグインを記述していましたが、Nuxt3では不要です。

個人的にプラグインと言うとサードパーティのモジュールを利用するのをイメージしてしまいますが、他のフレームワークで言うhelperメソッドだと思えば、スッキリするかもしれません。

ファイル名にclientをつけることでクライアントのみで使用することができます。

例えば、通貨(円)を表示したい場合、以下のようなpluginを作成します。

export default defineNuxtPlugin(() => {
  return {
    provide: {
      yen(value: string){
        if (!value) return '';
        const number = Number(value);
        if (isNaN(number)) return '';
        return number.toLocaleString() + '';
      }
    }
  }
});

index.vueでcompsablesのuseNuxtApp関数を実行してプラグイン$yenを取り出します。

<script setup>
  const { $yen } = useNuxtApp();
</script>

<template>
  <div>
    <p>1回{{ $yen(1000) }}</p>
  </div>
</template>

middleware

ミドルウェアはページルーティングが発生する際に実行されます。
ページコンポーネント内に埋め込む方法もありますが、ここでは、middlewareディレクトリを作成する方法についてみていきます。

例えば、会員画面でクエリーストリングにtokenが無ければ表示不可とします。
middlewareディレクトリに、以下auth.tsファイルを作成します。

不正があれば他のところに遷移させる定義を追加していくイメージ。

export default defineNuxtRouteMiddleware((to, from) => {
  const { token } = to.query;
  if (!token) {
    return navigateTo('/login')
  }
  if ((Array.isArray(token) ? token[0] : token) !== 'hoge') {
    return navigateTo("/login");
  }
  /*
  return abortNavigation(
    createError({ statusCode: 403, message: '会員ではありません' })
  );
  */
});

navigateToでexternal URLに飛ばすときは、await navigateTo('https://nuxt.com', { external: true }); external: trueとしなければならない。
301なのか302なのかも指定できる。いつものごとくreference読まずに進めてると余計な時間がかかる。reference確認。

会員ページでsetup
definePageMeta Composableで対象ミドルウェアを指定します(拡張子は省略)

<script setup>
  definePageMeta({
    middleware: 'auth',
  })
</script>

<template>
  <div>会員ページ</div>
</template>

すべてのページに設定したい場合にはファイル名にglobalをつけることで自動で設定されます。
auth.global.tsとします。

Nuxt Config

nuxt.config.tsの設定項目を確認します

Runtime Config

アプリケーションの設定は基本的にはRuntime Configを使います。

export default defineNuxtConfig({
  runtimeConfig: {
    // public設定
    public: {
      hoge: 'hoge-setting',
    },
    // private設定
    secret: 'secret-value',
    db: {
      user: 'hoge',
      password: 'pass'
    },
  }
})

publicな設定は、サーバーサイド、クライアントサイド両方で参照可能です。

デフォルトはprivateでサーバーサイドのみで参照可能です。
実運用ではsecret情報をここに書くことはないでしょう。ローカル開発環境用だけですかね。
少なくともステージング、プロダクション環境では.envファイルを用意したり、クラウドのパラメータストアを使ったりするでしょう。

参照は以下のように行います

<script setup lang="ts">
  const runtimeConfig = useRuntimeConfig();
  console.log(runtimeConfig.public.hoge);
  // server側かどうか確認
  const env = process.server ? 'Server' : 'Client';
  console.log(env);
</script>

<template>
  <div>runtime</div>
</template>

※ processが見つからなくて以下のようなエラーがlintから返された。

Do you need to install type definitions for node? Try `npm i @types/node` and then add `node` to the types field in your tsconfig.

無視しても開発環境では動作します。ですが、VS Codeで赤くなるのは気持ち悪い。
そんな時は、とりあえず、npm i @types/node とnpm run buildしてやれば直りました。

tsconfig.jsonには以下のように書かれていて、.nuxt/tsconfig.jsonが何らかの理由で更新されなかったのかなと思い、明示的にbuildしました。
他にいい方法があるかもしれません。

 "extends": "./.nuxt/tsconfig.json"

.envを利用する場合

以下のように定義することもできるし、もっと簡素化もできる。

export default defineNuxtConfig({
  runtimeConfig: {
    apiKey: process.env.API_KEY,
    public: {
      publicConfig: process.env.PUBLIC_CONFIG,
    },
  },
});

.envに特定のプリフィックスNUXT_またはNITRO_をつけると空の定義だけで上書いてくれる。
(個人的には好きではない。知っていれば使いたくなるが、知らない人がこれを見た時に何なのかわからない)

.env

NUXT_API_KEY=hoge
NUXT_PUBLIC_CONFIG=hogehoge
export default defineNuxtConfig({
  runtimeConfig: {
    apiKey: '',  // 定義していると上書きされる
    public: {
      publicConfig: '' // 定義していると上書きされる
    }
  }
}

Page Transitions

ページを移動する際にアニメーションを設定したい場合にPage Transitionsを利用することができます。
必要になったときに追記します。

header情報設定

アプリケーション全体でheaderタグ内に設定するにはnuxt.config.tsファイルのapp.headで行います。
ページ毎に設定する場合にはcomposablesのuseHead関数を利用します。

titleやmeta dscription、canonicalとかは共通にすることはないので使わないかなぁ。

app:{
    head: {
      title: "hoge",
      meta: [
        { charset: "utf-8" },
        { name: "viewport", content: "width=device-width, initial-scale=1" },
      ],
      link: [
        { rel: "icon",type: "image/x-icon", href: "/favicon.ico" },
        { rel: "stylesheet",href: "https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" }
      ],
    },
  }

useHead関数

pageファイルでscriptタグで実装

<script setup>
useHead({
  title: 'トップページ',
  meta: [
    {
      name: 'description',
      content: 'トップページの説明',
    },
  ],
});
</script>

app.vueファイル、レイアウトファイルでuseHead関数を利用してタイトルに共通suffixをつけることもできる

<script setup>
useHead({
  titleTemplate: (title) => {
    return title ? `${title} - Nuxt 3` : 'Nuxt 3';
  },
});
</script>

useMeta関数

動的にdescriptionを変更

<script setup>
  const router = useRoute();
  useMeta({
    title: 'Nuxt 3',
    meta: [
      {
        name: 'description',
        content: `User Id: ${router.params.id}`,
      },
    ],
  });
</script>

App Config

App Configは、nuxt.config.tsではなく、専用のファイルapp.config.tsを作成して記載することもできます。

export default defineAppConfig({
  hoge: 'hogehoge',
})

すべてpublicとなりクライアントサイドからもサーバーサイドからもアクセスできるので注意してください。

参照は以下のように行います

<script setup lang="ts">
  const appConfig  = useAppAppConfig();
  console.log(appConfig.hoge);
</script>

<template>
  <div>app config</div>
</template>

axiosは使わずuseFetch

Nuxt3からはaxiosを使用しなくてもAPIを実行できます。useFetch, useLazyFetch, useAsyncData, useLazyAsyncDataの4つが用意されています。

まずはモックapiを作成します。

useFetch

オプションなど詳細はリファレンスを確認してください。

const { data, pending, error, refresh } = await useFetch('/api/list');

dataには取得したデータ、errorにはエラーメッセージ(エラーがある場合)、pendingにはデータの取得中かどうかBoolean値が含まれています。
execute, refreshは関数なので実行することができ、実行すると再度データ取得を行うことができます。

<script setup>
  const { data, pending, error, refresh } = await useFetch('/api/list');
</script>
<template>
  <p v-if="error">{{ error }}</p>
  <button @click="refresh()">再取得</button>
  <table border="1">
    <thead>
      <tr><th>ID</th><th>Title</th><th>Summary</th><th>Created by</th><th>Created at</th></tr>
    </thead>
    <tbody>
      <tr v-for="article in data?.articles" :key="article.id">
        <td>{{ article.id }}</td>
        <td>{{ article.title }}</td>
        <td>{{ article.summary }}</td>
        <td>{{ article.created_by }}</td>
        <td>{{ article.created_at }}</td>
      </tr>
    </tbody>
  </table>
</template>

useAsyncData

ページやコンポーネント、プラグイン内にて、非同期でデータを取得するために使うことができる。・・・関数名から想像つきますよね。

const { data, pending, error, refresh } = await useAsyncData(
  'list',
  () => $fetch('/api/list')
)

第一引数にはキーを指定する。useAsyncDataは内部でキャッシュを保持しキャッシュを利用することで2回目以降はAPIにリクエストを送らずとも前の値と同じ結果を返してくれる。
このキャッシュはNuxt内部で保持するため、キャッシュを区別するための一意なキーが必要となる。

第二引数には非同期処理を行う関数を指定する。このコールバック内でWeb APIにリクストを送信し、useAsyncDataの結果として値を返すことができる。

キーは省略することは可能で省略した場合は自動でキーが設定されます。その場合のキーはファイル名と行番号を利用して作成されます。

$fetchはヘルパー関数で内部ではofetchライブラリを利用しています。

useLazyFetch

誤解を恐れずに言うなら、ページ内容をバッファするかどうかがuseFetchとの違い。
php.iniのoutput_buffering = 0 に設定したり、昔のプログラムを経験したことがある人ならClassic ASPでResponse.Buffer = FALSE なんて設定をした人がいるかもしれない。
バッファさせたほうが処理は早くなるのだけど、レスポンスタイム(画面の描画が始まるの)が遅くなる。
バッファしなければとりあえず、時間のかかっているところより上は表示できる。

useFetchにオプションを指定すれば同じ処理をできる。

具体的には、pendingの戻り値boolを利用して、ローディング中みたいなメッセージを出したりする

<script setup>
  const { data, pending, error, refresh } = await useLazyFetch('/api/list');
</script>
<template>
  <p v-if="error">{{ error }}</p>
  <p v-if="pending">ローディング中</p>
  <button @click="refresh()">再取得</button>
  <table border="1">
    <thead>
      <tr><th>ID</th><th>Title</th><th>Summary</th><th>Created by</th><th>Created at</th></tr>
    </thead>
    <tbody>
      <tr v-for="article in data?.articles" :key="article.id">
        <td>{{ article.id }}</td>
        <td>{{ article.title }}</td>
        <td>{{ article.summary }}</td>
        <td>{{ article.created_by }}</td>
        <td>{{ article.created_at }}</td>
      </tr>
    </tbody>
  </table>
</template>

useLazyAsyncData

想像の通りだと思うので省略します。

state管理

コンポーネント間やページ間で状態管理(データ共有)したい場合にcomposablesのuseStateを利用することができます。

Nuxt 3 では Vuex がサポートされていません。大規模なアプリケーションでなければ useState と Composables (Composition Function) を組み合わせることで簡単に State 管理をすることが可能になります。
(Next.jsでもreduxを使わなくてもuseStateでほとんどの場合は大丈夫ですね。)

最初これを読んだとき、composablesでリアクティブなデータを扱ってたじゃん。同じようにuseで始まってたし。それとは違うの?と思いました。

使いどころを簡単に理解すると、「ref関数ではページ間の移動を行うと値がクリアされ保持することができない。ページを移動しても値を保持したい場合にuseStateを利用する」です。

もう少し詳しく確認します。
refやreactiveという状態管理用のAPIは一般的にコンポーネント内で使用するものです。
また、状態をサブコンポーネントで利用する場合は、通常はpropsを通じて渡してあげます。
サブコンポーネントで状態を更新する場合は、イベントを発火して、データを主管する親コンポーネントでデータ更新をします。
コンポーネントツリーが3階層、4階層と深くなってくると冗長で面倒な作業になります(よくバケツリレーと言われています)。

(Next.jsで経験したいやな過去を思い出しました。Next.jsが悪いわけではなく、そのプロジェクトのテックリードがおかしかった)

export declare function useState<T>(key?: string, init?: (() => T | Ref<T>)): Ref<T>;
export declare function useState<T>(init?: (() => T | Ref<T>)): Ref<T>;

useStateは状態の作成と取得の両方を兼ねています。キー(key)と初期化処理(init)をもとに、現在の状態を返します。

keyを省略した場合は、ランダムな値が採番されます。コンポーネント内でのみ使用する状態で利用できます。
initは状態が初期化されてない場合のみ実行されます。これはサーバーサイドで実行済みの場合も含まれます。
つまり、サーバーサイドでinitが実行されている場合は、クライアンサイドではinitは実行されません。

具体的な使いどころは、「ログインユーザー情報を複数コンポーネントで使えるよう、useStateでグローバルに状態変数を定義する。」などが考えられるでしょう。

<script lang="ts" setup>
  // api認証のoauthとかで得られる情報とかを入れる
  const user = useState<{id: string, name: string}>('login-user', () => {
    return { id: '1', name: 'hoge', }
  });
</script>

別ページでつかってみる

<script setup>
  const user = useState('login-user')
</script>
<template>
  <div>
    <p>ようこそ{{ user.name }}さん</p>
  </div>
</template>

引き継がれます。local strageとか古いところだとcookieなんかを駆使して頑張った過去を思い出した。

簡単に確認しましたが、実運用だとこれでは困ることがあります。

  • 利用した側のページでは初期化処理をしていないので、リンク移動以外の手段でページを表示しようとするとサーバー側でエラーとなる
    (ブラウザの更新ボタン、URLの直打ちでサーバーエラーとなる)

  • キーを文字列管理しているのでミスが発生する

  • 使いたいところで使っていると管理が煩雑になる

この解決方法として公式ドキュメントに提案がある。

グローバルstate

composables に states.ts を作成して一元管理する。

export const useLoginUser = () =>
  useState<{ id: string; name: string; }>('login-user', () => {
    // api認証のoauthとかで得られる情報とかを入れる
    return { id: '1', name: 'hoge', }
  });

<script setup>
  const user = useLoginUser();
</script>
<template>
  <div>
    <p>ようこそ{{ user.name }}さん</p>
  </div>
</template>

Composition API

Nuxt 3と言うわけではなく、vue.js 3のComposition APIについて簡単に確認します。

computedって

日本語で算出プロパティというらしいですが日本語にする必要はないかな。値が変わるとその値に依存しているすべてのバインディングが更新されます。

「ユーザーからのイベント発生 => 値が変更される => その値に変更があった場合、動的に何かしらの処理を実行したい」って時に利用します。

composablesで、以下のようなカウンターのメソッドを作成しました。

export const useCounter = (initialValue: number) => {
  const count = ref(initialValue);
  const increase = () => (count.value++);
  const decrease = () => (count.value--);
  return {
    count,
    increase,
    decrease,
  };
};

これにcomputedを追加します。

export const useCounter = (initialValue: number) => {
  const count = ref(initialValue);
  const increase = () => (count.value++);
  const decrease = () => (count.value--);
  const double = computed(() => count.value * 2);
  const triple = (): number => { return count.value * 3; }
  return {
    count,
    increase,
    decrease,
    double,
    triple,
  };
};

使うときは、プロパティなので()は必要ありません。

    <div>Count:{{ count }}</div>
    <div>Double:{{ double }}</div>
    <div>Triple:{{ triple() }}</div>

サンプルが悪いのか、イマイチ使いどころがわからないです。methodで定義したtripleとの違いはmethodなのかpropertyなのかと言うところです。

内部での違いは、methodとcomputedの違いはcomputedは処理内容をキャッシュするが、methodは処理内容をキャッシュしません。
つまり、定義したpropertyの内容が変わらない場合は、キャッシュが利用されるのでcomputedのほうが速い!・・・らしいです。

watchって

リアクティブな変数を自動的に監視します。
データの変更を監視して、それをトリガーに非同期処理や複雑な処理を行う必要がある時に使います。

  const count = ref(initialValue);
  watch(count, (currentCount, prevCount) => { console.log(`今:${currentCount} 前:${prevCount}`) });

computedではなくwatchを使うのは

  • computedプロパティでは処理できない非同期通信などの複雑な処理を行う場合
  • 更新前と更新後の値を使う場合
  • 処理を実行しても、データは返さない場合

props / emit

超基本的な話。props / emitって?メモしときます。

  • props ... 「親コンポーネントの値を渡すもの」

  • emit ... 「親コンポーネントの関数を使えるようにするもの」「子コンポーネントの値を親に伝えるもの」

と書いてみたけど、他のフレームワークでもあるのでそんなに構える必要はないかな。railsのパーシャルテンプレートにlocalsとかcollectionとかでパラメータを渡す。
.net core mvcだとパーシャルビューにモデルを渡したりします。

props

Helloコンポーネントにデータを渡したい場合はHelloタグの中に任意の名前の属性名(message)設定し渡したい値を設定します。

components/Hello.vue

<script setup lang="ts">
const props = defineProps({
    message:String
})
</script>

<template>
  <p>props試します{{ props.message }}</p>
</template>
 <Hello message="Hello World" />

デフォルト値を設定したり、必須とすることもできます。

<script setup lang="ts">
const props = defineProps({
    message: { type String, default: 'default Hello' },
})
</script>
<script setup lang="ts">
const props = defineProps({
    message: { type String, required: true },
})
</script>

emit

propsとして渡されたデータは子コンポーネントでは更新してはいけないというルールがあります。
そのため親コンポーネントが持つreactiveな変数は親コンポーネントが更新しなければいけません。
もし子コンポーネントで行うユーザのアクションによって親のreactiveな変数を更新する必要が出た場合は子コンポーネントから親コンポーネントに対象となるreactiveな変数を更新して欲しいと伝えるための通知の仕組みが必要となります。
その仕組みに利用するのがemitです。

と言ってもなかなかピンと来ないと思います。そんな時は簡単なサンプルを作ります。

子コンポーネントのbuttonが押されたら親コンポーネントに通知します。
$emit引数に親コンポーネントで指定するイベント名を文字列指定します。

<script setup lang="ts">
const props = defineProps({
    message:String
})
</script>

<template>
  <div>
    <p>props試します{{ props.message }}</p>
    <button @click="$emit('notification')">通知</button>
  </div>
</template>

親コンポーネントでは、emitで指定したイベント名を使ってイベントハンドラーを実装します。

<script setup lang="ts">
  const handleEvent = (): void => {
    console.log('子コンポーネントからの通知が届きました');
  };
</script>
<template>
  <Hello @notification="handleEvent" />
</template>

簡単に確認しましたが、イベントを実装するだけでなく、値を渡すこともできます。こちらは必要になったときに追記します。

cookie

cookieをサーバーサイドで読んだり、書いたりするのにuseCookieが使えます。
setup か Lifecycle Hooksで利用できます。

useCookieにより得たcookieはrefオブジェクトとなっており、valueに値を書き込むことでdocument.cookieに同期される。

話はそれますが、HttpOnly 属性を持つ Cookie は、 JavaScript の Document.cookie API にはアクセスできません。
例えば、サーバー側のセッションを持続させる Cookie は JavaScript で利用する必要はないので、 HttpOnly 属性をつけるべきです。
この予防策は、クロスサイトスクリプティング (XSS) 攻撃を緩和するのに役立ちます。

と言うことで、accesstokenなんかは、HttpOnly trueにすべきなようです。

<script setup>
  const counter = useCookie('counter', { maxAge: 60 * 60 * 24, });
  counter.value = counter.value ?? Math.round(Math.random() * 1000);
  // 削除
  // counter.value = null;
</script>
<template>
  <div>
    <p>{{ counter }}</p>
  </div>
</template>

他にも使い方があるので、必要に応じて追記する。

余談

OAuth 2.0で外部認証するときに、CSRF対策としてstateパラメータを使うことがあると思います。詳細は割愛しますが、認証サーバーに送ったstateパラメータと返ってきたstateパラメータが一致することを確認します。
stateパラメータが一致することを確認するのに、一時的にstateパラメータを保存しないといけないのですが、sessionを使ったりcookie使ったりするのが一般的なのかなと思います。
そこでふと、サーバー側でredirectするのにcookieってつけられるんだっけ?と疑問に思いました。

結論から言うと、redirectでも問題なくcookieはつけられます。

あたり前な話ですが、サーバーからブラウザに対しては、302のステータスコードとLocation情報を送って、それを受け取ったブラウザがLocationのページにRequest情報を送りなおす。つまりは、302のステータスコードとLocation情報をブラウザに送っているHeaderにcookieの情報がついていればブラウザはcookieを保存してくれる。

何が言いたかったというと、ブラウザとwebサーバーのやりとりrequest / response って意識する必要ないけど、まったくわかってないと変な迷路に入ってしまいます。

OAuth 2.0ではリダイレクトURIを事前に登録する必要があって、登録できるURI数に制限があって、正規表現が使えなかったりするのが通常です。そのためパラメータで動的に変更になるページをリダイレクト先にしたい場合に悩むことがあります。

そんな時にもcookieに情報を持たせることができるかもしれません。

エラー

遭遇したエラーとその対処法をメモする。

  • Hydration children mismatch in

ハイドレーションのエラーはたいていがHTMLのタグの組み方がおかしい時に起こります。
表示上問題ないので、放置しがちですが、パフォーマンスに影響が出るので修正するようにしましょう。
Next.js 13でも良く起きるのですが、サーバー側で作ったHTMLとクライアント側(ブラウザでvue.js)で作ったHTMLが異なるよ。と言うエラー(ワーニング)です。

<p>タグの中に<table>は使ってはだめ。

<table> には <thead> <tbody> も定義する。

<p>タグの中には<div>タグは使えない。

HTMLに詳しい人からすると当然のことなんだろうけど、なんとなくで作ってた人には、warningを見て何をしないといけないのかわからないことがある。

Error haundling

NuxtErrorBoundary

クライアント側で発生するエラーをキャッチして事前に設定したエラー内容をブラウザ上に表示させたい時に利用できるコンポーネントです。

<script setup lang="ts">
  const log = (err: any) => console.log(err)
  const clearError = (error: any) => {
    error.value = null;
  };
</script>

<template>
  <div>
    <NuxtErrorBoundary @error="log">
      <GlobalNavi />
      <template #error="{ error }">
        エラーが発生しました。
        {{ error }}
        <button @click="clearError(error)">Clear Error</button>
      </template>
    </NuxtErrorBoundary>
  </div>
</template>

コンポーネントをNuxtErrorBoundaryでラップします。

カスタムエラーページ

プロジェクトルート直下にerror.vueを作成します。

<script setup lang="ts">
import { NuxtApp } from "#app";

const props = defineProps<{ error: NuxtApp["payload"]["error"] }>();
const handleError = () => clearError({redirect: '/'})
const isDev = process.dev;
</script>

<template>
  <p>エラーが発生しました</p>
  <button @click="handleError">トップページに戻る</button>
  <div v-if="isDev">
    {{ error }}
  </div>
</template>

vueApp.config.errorHandler

pluginsフォルダに任意の名前のファイルを作成します。slackに通知やメール通知などお好みで実装します。

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.config.errorHandler = (error, context) => {
    console.log('エラー', error);
    console.log('コンテキスト', context);
  };
});

vue.js 初心者的メモ

これなんだっけ?と思うことが最初はよくあるのでメモします。

@ってなに?

v-onディレクティブはVue.jsで多用する処理のため簡単に記述する省略形が用意されています。

<button v-on:click="counter++">

<button @click="counter++">

#ってなに?

v-slotの省略記法です。

  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

:ってどういうこと?

v-bindの省略記法です。

<a v-bind:href="url">リンク</a>

<a :href="url">リンク</a>

$って何?

(ドル記号)[https://v2.ja.vuejs.org/v2/cookbook/adding-instance-properties.html]は、ざっくり「vueが元々持っているネイティブのプロパティを使う場合に必要となるもの」です。
通常は、(プラグイン)[#plugins]を使うときに用います。
あまり深く考えずに変数名に_を使うのと同じって考えていても大丈夫な気がします。

VS Code

拡張機能

  • Vue Language Features (Volar)

veturではなくVolarをインストールしましょう。Take Over Modeを有効にします。*.vueファイルの型チェックを強化してくれます。
拡張機能のタブで@builtin typescriptと入力し、TypeScript and JavaScript Language FeaturesをDisable(Workspace)とします。

nuxt.config.tsを編集します

export default defineNuxtConfig({
  typescript: {
    shim: false
  }
})
  • ESLint
  • Prettier

Lintです。お好みでインストールしてください

debug

vscodeで以下launch.jsonでデバッグできました。
typescriptのままブレークできて開発に支障出なくてよかった。

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Nuxtjs: Launch Chrome",
      "url": "http://localhost:3000",
      "webRoot": "${workspaceFolder}"
    }
  ]
}

chrome

chrome拡張機能 Vue用のDevTools

typescript

Nuxt 3では、typescriptを意識しなくても開発出来てすごくいい。注意点は、いろんなサイトを見ながら作ってると<script setup>と書いてしまう。
<script lang="ts" setup>と書いていないとハマることがある。

具体的にはreturnの型を定義したとき。 < >が Unhandled error during execution of setup function となる。

<script setup>
const user = useState<{ id: string, name: string, }>('login-user', () => {
  console.log('retrieving user info...')
  return {
    id: '1',
    name: 'hoge',
  };
})
</script>

演算子

たまに忘れる演算子をメモする

  • ?? 演算子 || 演算子より厳密に undefined や null を判定してくれる。Null結合演算子という。
    || (OR演算子)との違いは、OR演算子は左辺が false として判定される場合は右辺が返るのに対し、 ?? では 左辺がnull または undefined に限定されること。
  • ?. 演算子 undefined または null な参照の場合に、エラーではなく undefined にしてくれる、RubyでいうところのNull条件演算子

コマンド

コマンド 説明
nuxt dev devをつけるようになった。逆にこっちのほうがスッキリする開発サーバーを起動します。このコマンドで起動したサーバーはソースに変更があった場合は即座に反映される。(ホットリロード)
nuxt build アプリをWebpackとしてビルドします。このコマンドは「nuxt startコマンド」の前に実行する必要があります。
nuxt generate nuxt.config で target: 'static' を設定している場合、HTML ファイルを .output/public ディレクトリに生成
nuxt preview ビルドコマンドの実行後にNuxtアプリケーションをプレビューするためのサーバーを起動します。ローカルPCで本番環境を確認するには、buildコマンドを実行後にpreviewコマンドを実行します。
nuxt prepare .nuxtフォルダを作成してくれます。CI環境を作るときなどに利用します。

nuxt startがnuxt pureviewに代わった。nuxtだけだったのがnuxt devとなったとざっくり理解で大丈夫と思います。
Nuxt CLI コマンドである nuxi が導入されているので、nuxtをnuxiと変更するらしいですが、インストール時はnuxtとなっています。

Nitro

Nuxtのサーバーエンジンです。
デフォルトではNode.js Serverをターゲット環境としていますが、NitroはユニバーサルJavaScriptエンジンなので、AWS Lambda等の環境でも実行可能です。

APIの実装でもNuxt3のNitroを使うと、サーバーサイドレンダリングではHTTP通信でなく直接APIコールとなります。クライアントサイドからはHTTP通信となります。自動で切り替えられるのは魅力です。大規模なシステムの場合、APIサーバーとWEBサーバーは別にしたりしますが、小規模なシステムの場合はNuxt3のNitroを使ってすべて作ってしまうのはありかもしれない。

deploy

基本的にはデフォルトのユニバーサルレンダリングですね。

デフォルトのユニバーサルレンダリング(プリレンダリング無効:target->server)

npm run build
# Nitroサーバーエンジン起動
node .output/server/index.mjs

ユニバーサルレンダリング(プリレンダリング有効:target->static) または クライアントサイドレンダリング(SPA)

npm run generate
# dist以下をホスティング

server処理

Nitroサーバによりクライアント側からアクセス可能なAPI Routeを作成することができます。
サーバとしてデータベースへの接続も行えるのでデータベースを利用した環境でデータの取得と追加の動作確認も行えます。
しかしながら、フロントエンドエンジニアとサーバーエンジニアはわかれていることが多く、API開発はサーバーエンジニアが行うことが多いためここでは詳細まで確認しないこととします。

サーバーエンジニアがAPIを開発している間にモックを用意してくれればよいのだけど、そんなの自分でJsonファイル置いとけばいいでしょ?と言われた場合を想定して固定のJSONを返してみます。

serverディレクトリを作成します。その下にAPIディレクトリを作成し、list.ts ファイルを作成します。

export default defineEventHandler(() => {
  return {
    data: [
      { id: 1,
        title: 'nuxt.jsとは',
        summary: 'nuxt.jsのサマリー',
        contents: 'nuxt.jsについての全文',
        created_by: 'hoge',
        created_date: '2022-02-01 12:00:00.000'
      },
      { id: 2,
        title: 'next.jsとは',
        summary: 'next.jsのサマリー',
        contents: 'next.jsについての全文',
        created_by: 'hogehoge',
        created_date: '2022-02-01 13:00:00.000'
      },
      { id: 3,
        title: 'nuxt.jsとnext.jsの違い',
        summary: 'nuxt.jsとnext.jsの違いのサマリー',
        contents: 'nuxt.jsとnext.jsの違いの全文',
        created_by: 'moge',
        created_date: '2022-02-02 12:00:00.000'
      },
    ]
  }
});

これだけで完了。content-type: application/json になってます。

npx

npxコマンドはnpmバージョン5.2.0より同梱されているコマンドで、ローカルにインストールしたコマンドを実行するために使われます。

npmを使ってスクリプトを実行するにはpackage.jsonのscriptsへ予めスクリプトを定義しておく必要があります。
一方で、npxであれば、インストールされていないコマンドであっても自動的に探してインストール、実行まで行ってくれます。
実行したあとはパッケージの除去まで行ってくれます。

  • インストール済みモジュールの実行

npx <インストール済みモジュール名>

  • インストールしていないモジュールを実行(実行後に自動削除)

npx <未インストールのモジュール名>

  • GitHub上の特定リポジトリを明示的に指定して実行

npx github:<リポジトリ名>

  • npm用の依存関係アップデート確認

npx npm-check-updates

  • 更新があるパッケージを確認

npx npm-check-updates -u

上記実行後 npm install を実行で更新

  • トランスパイル

npx tsc

22
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
18