1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Nuxt2→Nuxt3 移行メモ

Posted at

あくまで「動いた」レベルのもので、production用の調整はしていないです。
少し前に書いたものなのでその後Nuxt3のバージョン上がって状況少し異なります。

Nuxt2での状態

主な利用パッケージ

  • nuxt@^2.15.8
  • @nuxtjs/composition-api@~0.32.0
  • vue@~2.6.14
  • vuex@^3.6.2
  • vuetify@^2.6.8
  • @nuxtjs/vuetify@^1.12.3
  • @nuxtjs/axios@^5.13.6
  • @nuxtjs/pwa@^3.3.5
  • @nuxtjs/auth-next@^5.0.0-1648802546.c9880dc
  • @nuxt/content@^1.15.1

package.json

{
  "scripts": {
    "dev": "nuxt",
    "dev:browser": "NODE_ENV=development node -r dotenv/config --inspect --nolazy node_modules/.bin/nuxt",
    "build": "nuxt build",
    "generate": "nuxt generate",
    "start": "nuxt start",
    "analyze": "nuxt build --analyze",
    "lint:js": "eslint --ext \".js,.ts,.vue\" --ignore-path .gitignore .",
    "lint:prettier": "prettier --check .",
    "lint": "yarn lint:js && yarn lint:prettier",
    "rm-cache:hs": "rm -rf node_modules/.cache/hard-source/",
    "rm-cache:yarn": "yarn cache clean --force",
  },
  "dependencies": {
    "@mdi/js": "^6.9.96",
    "@nuxt/content": "^1.15.1",
    "@nuxtjs/auth-next": "^5.0.0-1648802546.c9880dc",
    "@nuxtjs/axios": "^5.13.6",
    "@nuxtjs/composition-api": "~0.32.0",
    "@nuxtjs/pwa": "^3.3.5",
    "core-js": "^3.24.1",
    "nuxt": "^2.15.8",
    "vue": "~2.6.14",
    "vue-server-renderer": "~2.6.14",
    "vue-template-compiler": "~2.6.14",
    "vuetify": "^2.6.8",
    "vuex": "^3.6.2",
    "webpack": "^4.46.0",
    "webpack-node-externals": "^3.0.0",
  },
  "devDependencies": {
    "@babel/eslint-parser": "^7.16.3",
    "@commitlint/cli": "^17.0.3",
    "@commitlint/config-conventional": "^17.0.3",
    "@nuxt/types": "^2.15.8",
    "@nuxt/typescript-build": "^2.1.0",
    "@nuxtjs/eslint-config-typescript": "^10.0.0",
    "@nuxtjs/eslint-module": "^3.1.0",
    "@nuxtjs/stylelint-module": "^4.1.0",
    "@nuxtjs/vuetify": "^1.12.3",
    "@vue/test-utils": "^1.3.0",
    "babel-core": "7.0.0-bridge.0",
    "babel-jest": "^27.4.4",
    "eslint": "^8.4.1",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-nuxt": "^3.1.0",
    "eslint-plugin-vue": "^8.7.1",
    "husky": "^8.0.1",
    "jest": "^27.4.4",
    "lint-staged": "^13.0.3",
    "nuxt-purgecss": "^1.0.0",
    "postcss-html": "^1.5.0",
    "prettier": "^2.7.1",
    "sass": "^1.54.3",
    "stylelint": "^14.1.0",
    "stylelint-config-prettier": "^9.0.3",
    "stylelint-config-recommended-vue": "^1.1.0",
    "stylelint-config-standard": "^26.0.0",
    "ts-jest": "~27.1.5",
    "vue-jest": "^3.0.4"
  },
}

Nuxt3後の状態

主な利用パッケージ

  • nuxt@^3.4.3
  • @nuxt/devtools@^0.6.0
  • vuex@^4.1.0
  • vuetify@^3.2.3
  • @kevinmarrec/nuxt-pwa@^0.17.0
  • @sidebase/nuxt-auth@^0.6.0-beta.2
  • @nuxt/content@^2.6.0

package.json

{
  "scripts": {
    "analyze": "npx nuxi analyze --no-serve",
    "rm-cache:yarn": "yarn cache clean --force",
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare",
    "lint:js": "eslint --ext \".js,.ts,.vue\" --ignore-path .gitignore .",
    "lint": "yarn lint:js",
  },
  "dependencies": {
  },
  "devDependencies": {
    "@babel/eslint-parser": "^7.21.8",
    "@kevinmarrec/nuxt-pwa": "^0.17.0",
    "@mdi/js": "^7.2.96",
    "@nuxt/content": "^2.6.0",
    "@nuxt/devtools": "^0.6.0",
    "@nuxt/eslint-config": "^0.1.1",
    "@nuxtjs/eslint-config-typescript": "^12.0.0",
    "@sidebase/nuxt-auth": "^0.6.0-beta.2",
    "@types/node": "^18",
    "eslint": "^8.41.0",
    "eslint-plugin-vuetify": "^2.0.1",
    "nuxt": "^3.4.3",
    "ofetch": "^1.1.0",
    "prettier": "^2.8.8",
    "sass": "^1.62.1",
    "typescript": "^5.0.4",
    "vite-plugin-vuetify": "^1.0.2",
    "vue-tsc": "^1.6.5",
    "vuetify": "^3.2.3",
    "vuex": "^4.1.0"
  },
}

移行メモ

nuxt 3

nuxt3では待望のVue3対応/TypeScript正式対応。
これまではComposition API用のパッケージ入れてなんちゃってVue3対応したり、TypeScript用のパッケージ入れてなんちゃって対応したりしていたけど、それらの煩わしさから解放された。
tsconfig.jsonもこれまでは状況を見つつ適当に頑張って作成する必要あったがそれも無くなった。
postinstallで.nuxtディレクトリを作成するようになり、そこではtsconfig.jsonや必要な型情報が自動生成される。プロジェクト自体のtsconfig.jsonの継承元が.nuxtディレクトリ内となる。

移行する手順としては、

  1. プロジェクトをnpxで新たに作り直して
  2. 必要なパッケージを個別に移植し
  3. 独自のコードのみを移植し
  4. ビルドエラー/eslintエラーを頼りに修正してく

という感じが良さそう。
ただその前に、パッケージごととかでテストプロジェクトを作ってセットアップ内容を決め、最低限必要なコードが動作することを確認していくのが良さそう。

今回だとまず、volar/eslintをセットアップして静的解析と整形環境を整え、componentsのprops/emits含めた定義方法を動作確認し、middleware/pluginsの定義方法を動作確認し、それらを踏まえてvuex/vuetifyの移植をテストプロジェクトで少しずつ検証し、テストプロジェクトでcontent/pwa/authの移植検証を経て、ようやく本移植へと移った。

以降、Vue3での変更を切り分けずnuxt3の変更として言及。

プロジェクト作成

OOBで動作させるには次の通り。nuxt2の時と違い細かなカスタム設問は無いみたい。

npx nuxi@latest init my-app
cd my-app
yarn install
yarn dev

OOB

デフォルトだと.nuxt/public/serverディレクトリくらいしか無い。バックエンドは移行対象が別プロジェクトとしてexpressで実装しているのでserver(nitro)は今回は扱わない。ただ確かこれを使うことにより型がフロントとバックで自動で共有できるとか。型用パッケージを別出しにして共有しているが面倒なのでもっと早く出会いたかった。

ディレクトリ構成

nuxt3で使えるディレクトリは.nuxt/public/serverの他には、components/pages/layouts/middleware/plugins/composablesがある。nuxt2で使用していたディレクトリは全て共通のため、そのまま移植できた。新たにcomposablesというのが増えているがこれは、リロードしても消えないStateらしい。

.nuxt

実装に必要な補助情報が入っているぽい。実装内容や利用している関連パッケージの型情報が自動生成されるなど。
tsconfig.jsonは次のような定義となる。

tsconfig.json

{
  "extends": "./.nuxt/tsconfig.json",
}

注意としては、tsconfig.jsonはオプションの一部をオーバーライドして変更ができないみたいで、一部を変更したい場合大部分を書き直す必要がありそう?さすがに不便なのでNuxt側のオプションがあってもよさそう。

vue-shim.d.ts は必須じゃなくなった?楽ちん。

router

before

$router.go({ path: '/', force: true })

forceはもしかしたらexternalに相当する?
after

navigateTo('/')

nuxt.config.ts

nuxt.config.jsがTypeScript対応になってnuxt.config.tsになった。
中身はかなり異なるので作り直して個別に移植する方がよい。

before

  publicRuntimeConfig: {
    BACK_URL: 'http://127.0.0.1:3001',
  },
  privateRuntimeConfig: {
    NUXT_HOST: process.env.NUXT_HOST,
    NUXT_PORT: process.env.NUXT_PORT,
  },
  typescript: {
    shim: false,
    strict: true,
    // typeCheck: true,
  },
  build: {
    rules: [{ test: /\.txt$/, use: 'raw-loader' }],
    extend(config, ctx) {
      if (ctx.isDev && ctx.isClient) {
        config.devtool = 'eval-source-map'
      }
    },
    parallel: process.env.NODE_ENV === 'development',
    cache: true,
    hardSource: true,
    publicPath: '/_nuxt',
    analyze: false,
    devtools: process.env.NODE_ENV === 'development',
  },

configの定義方法が変わってbuild内容はほぼ不要に
after

  runtimeConfig: {
    public: {
      BACK_URL: process.env.BACK_URL,
    },
    private: {
      NUXT_HOST: process.env.NUXT_HOST,
      NUXT_PORT: process.env.NUXT_PORT,
    },
  },
  build: {
    analyze: false,
  },

configの使用方法

before

(globalThis as unknown as Window).$nuxt.context.$config.UPLOAD_DIR

after

useRuntimeConfig().public.UPLOAD_DIR;

app.vue

nuxt2には無い要素でメインコンポーネントでエントリーポイント。

デフォルトだとこんな感じの定義になっている。

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

これ1つで成り立ち、layouts/pagesはoptional要素。ランディングページなど1枚ページに便利に。

従来通りlayouts/pagesを使用することも可能。
その場合はapp.vueからNuxtLayout/NuxtPageで呼び出す

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

Composition API <script setup>

Vue2のComposition API(@vue/composition-api@nuxt/composition-api)では
<script>
記法があったが、それを簡略化できるようになった。
以降、nuxt2との比較はscript setup記法について言及。

before

<script lang="ts">
import {
  defineComponent
} from '@nuxtjs/composition-api'
export default defineComponent({
  components: {},
  setup() {
    return { }
  },
})
</script>

after

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

script setup内ではuseXXX()系の必要な関数がインポート無しに全て使える。楽ちん。

Components

ref/computedなどを@nuxt/composition-apiからimportする必要が無くなった。あんまり使わない型などで面倒だったのでかなり楽ちんに。

昔のdata相当のものをこれまではreturn { xxx }してやる必要があったが必要無くなった。つまり全変数がtemplateに公開されるようになったかも。

(auto-importオフの場合)Componentをimportしてからcomponents: {}にセットしてやる必要あったが必要無くなった。importしただけでtemplateで使用可能に。

v-model

v-model用のpropsであるvalueがmodelValueに名称変更された。修正が地味に面倒。同様にinputイベントがupdate:modelValueに名称変更された。そしてこの名称をコンポーネント別に簡単に変更可能になったみたい?まぁでもここを変更すると混乱の元なので変更することはあんまり無さそう。

また、これまではv-model的なことができるのは1プロパティのみだったが、複数プロパティで可能になった模様。移植には一旦関係なし。

props

script setup記法&TypeScriptによりかなり書き方が変わった。従来寄りの宣言方法は実行時の宣言(runtime declaration)、TypeScript寄りの宣言方法は型ベースの宣言(type-based declaration)というみたい。今回はTypeScriptに寄せたいので型ベースの宣言に置き換える。

before

<script lang="ts">
import { defineComponent } from '@nuxtjs/composition-api'
export default defineComponent({
  props: {
    color: {
      type: String,
      required: false,
      default: 'info',
    },
    text: {
      type: String,
      required: true,
    },
  },
  setup(props) {
  },
})
</script>

after

<script lang="ts" setup>
  interface Props {
    color?: string;
    text: string;
  }
  const props = withDefaults(
    defineProps<Props>(), { color: 'info' }
  );
</script>

型ベースの場合はpropsのバリデーションが記載できない気がする。使ってないけど実行時宣言にしたい場合はこんな感じ?

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

emits

これまでイベントは定義できなかったけど定義可能に。さらに型付けも可能でかなり便利に。
従来のemit()の記載をしてしまうと、なぜかイベントが2回発生したりなどする。謎。
移植がうまくいかない場合はイベントが意図せず複数回発生してないか要確認。

before

export default defineComponent({
  setup({ emit }) {
      emit('name-change', 'xxxx')
  },
})
</script>

after

<script lang="ts" setup>
interface Emits {
  (e: 'name-change', v: string): void;
}
const emits = defineEmits<Emits>();
emits('name-change', 'xxxx')
</script>

watch

watchには移植に関する変更はなさそう?
新たにwatchEffectという方式が増えたみたい。

head

head等のメタ情報の定義方法は調査しきれず。

pages

これまで通り

layouts

ページ読み込みの指定がnuxtからslotに変更
before

<template>
  <nuxt />
</template>

after

<template>
  <slot />
</template>

middleware

定義方法がかなり変わってnuxt.confing.jsでの定義は無くなった。
共通ミドルウェアはファイル名をxxxx.global.tsとする。
個別ミドルウェアはファイル名をこれまで通りxxxx.tsとして、ページ内でセット。
パラメータにも変更があってこれまではcontextが渡されてたけどrouteのfrom/toのみに。

before
xxxx.ts

import { Middleware } from '@nuxt/types';

const myMiddleware: Middleware = async (context) => {
  console.log(context.route.fullPath);
  if(context.route.fullPath === '/test') {
    return redirect('/')
  }
};

export default myMiddleware;

after
xxxx.global.ts

import { RouteMiddleware } from 'nuxt/app';
const myMiddleware: RouteMiddleware = defineNuxtRouteMiddleware(async (to, from) => {
 if(to.fullPath === '/test') {
    return navigateTo('/')
 }
});

export default myMiddleware;

個別の場合はglobalを取ってページ側でこう。

<script setup>
definePageMeta({
  middleware: ["xxxx"]
})
</script>

plugins

pluginsもmiddleware同様nuxt.config.jsの定義は不要になった。

before

import { Plugin } from '@nuxt/types';
declare module '@nuxt/types' {
  interface Context {
    $xxxx(): string;
  }
}
let count = 0;
const xxxx = (): string => {
  return String(count++);
};
const plugin: Plugin = (context: unknown, inject) =>
  inject('xxxx', xxxx);

export default plugin;

after

export default defineNuxtPlugin((nuxtApp) => {
  let count = 0;
  const xxxx = (): string => {
    return String(count++);
  };
  return {
    provide: {
      xxxx: xxxx,
    },
  };
});

composables

新キャラ。単純移行のため未使用。リロードしても消えないStateらしいので使い勝手良さそう。。

lint+整形

Nuxt2の頃はeslint+prettierで静的解析&整形をするケースが多かったはずだが、Nuxt3ではeslint単体での静的解析&整形が推奨されている。但し将来的にはprettierに対応するかもとのこと。

@nuxt/eslint-configを使うわけだがissueにもあるようにREADME.mdがバグってて、パッケージも実は@nuxt/eslint-config@nuxtjs/elisnt-config(-typescript)の2つある状態。js無し版が最新版のようだがテスト版のようで整形結果がゴミ。しょうがないので古い方の@nuxtjs/eslint-config-typescriptを利用することにする。新規開発があまり活発ではないように見えるので将来的な不安要素。

yarn add -D eslint
yarn add -D @nuxtjs/eslint-config

.eslintrc

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

Marketplaceから「ESLint」(dbaeumer.vscode-eslint)をインストールし
settings.jsonに

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

vueのpropsの初期値など、今まで問題なかった個所にエラーが出たりしたので指摘に合わせて調整。
また、もともと

        '@typescript-eslint/no-floating-promises': 'error',
        '@typescript-eslint/no-misused-promises': 'error',

を使っていたがこれを有効にするとうまく動かず、どうにもならなかったので利用を断念した。

実際は拡張子(js/ts/vue)別に定義を変えているがここでは割愛。

axios

大破壊的変更の一つ。axiosはサポートされなくなり$fetchに置き換えられた(泣きたい)。$fetchは内部的にはofetchというパッケージで、ofetchはJS標準のfetchのラッパーの模様。基本的にできることは同じだが、axiosはXMLHttpRequestで$fetchはFetch APIという違いがある。
nuxt3ではuseFetch()/useAsyncData()など$fetchのラッパーがいくつがあるっぽいが、axiosからの単純移植ということと自分の使い方的には$fetchを直接使うやりかたのみ必要っぽかったのでそこに注目して移植した。
これまではnuxt.config.jsに大体の定義を済ませていたが無くなった?共通オプションを定義したインスタンスを生成してpluginsとして使いまわす方式とした。

before

(globalThis as unknown as Window).$nuxt.$axios.post('/api/xxxx', data)

after
plugins/fetch.ts

export default defineNuxtPlugin((nuxtApp) => {
  const defaultUrl = 'http://127.0.0.1:3001/';

  const headers = new Headers()
  headers.append('content-type', 'application/json')
  headers.append('X-Requested-With', 'XMLHttpRequest')
  headers.append('Authorization', 'token')

  const ins = $fetch.create({
    // baseURL: defaultUrl,
    headers,
    credentials: 'include',
    responseType: 'json',
  });
  return {
    provide: {
      fetchIns: ins,
    },
  };
});

呼び出し

// ofetchは型が必要ならインストールしておく
import { HTTPMethod } from 'h3'
import { NitroFetchOptions, NitroFetchRequest } from 'nitropack'
import { FetchError } from 'ofetch'

const execute = async (
  method: HTTPMethod,
  endpoint: string,
  body: Record<string, any> | undefined,
): Promise<{
  data: unknown;
}> => {
  const fetchOptions :NitroFetchOptions<NitroFetchRequest> = {
    method,
    body,
  }

  try {
    // 通常は$fetchIns()の方がいいかも
    const raw = await useNuxtApp().$fetchIns.raw<unknown>(endpoint, fetchOptions);
    const responseData = raw._data
    return {data: responseData}
  } catch (e) {
    if (e instanceof FetchError) {
      const fetchError = e as FetchError
      const responseData = fetchError.data
    }
    throw e;
  }
};

const response = await execute('POST','/api/xxxx', {aaa: 'aaa'})

APIのURLが異なる場合、BaseURLを$fetch.create()時に指定してもいいけど、従来のProxy相当の設定も可能。

nuxt.config.ts

  vite: {
    server: {
      proxy: {
        '/api/': {
          target: 'process.env.BACK_URL',
          changeOrigin: true,
          rewrite: path => path.replace(/^\/api/, ''),
        },
      },

Vetur -> Volar

VSCodeのVue開発用エクステンションはもともとVeturであったがVue3からVolarに変わった。

「Vue Language Features (Volar)」(vue.volar)をMarketplaceからインストール。TypeScriptを利用する場合は「TypeScript Vue Plugin (Volar)」(vue.vscode-typescript-vue-plugin)もインストール。

@builtin typescript」エクステンションと競合してパフォーマンスに影響するのでビルトインを無効化する「Takeover Mode」が推奨されている。

Veturの時にはeslintとの競合を避けたりなどで微妙にsettings.jsonの設定変更が必要だったがVolarに関しては今のところTakeover Modeの変更のみで使えている。但し、なぜかちょいちょいビルトインが復活するので気づいたら競合していたなんてことが。Remote Development環境だからかコマンドでは無効化できないしdevcontainer.jsonにもそんなオプション無いので自動化できていない。

以前はワンピースのパクリみたいなアイコンだったが今はまともなアイコンに変わっている。

デバッグ

VSCode

Nuxt2の頃は node --inspectnode_modules/.bin/nuxt を実行することによりVSCode上でBreakpointを置いてデバッグできていたがNuxt3でのまともなやり方がわかっていない。Nuxt3としては特別デバッグのやり方があるわけではなくVue3に準じるとのことだがVSCode上でできない。ブラウザでは問題ないので深掘りしていないがもう少し調査が必要。

ブラウザ

普通にyarn devで起動してlaunch.jsonでブラウザを起動し、開発者ツールでブレークポイントをおいてデバッグする。nuxt2の頃はデバッガ上に表れるディレクトリ構造が謎だったりソースコードが1ファイルにつき何個も表示されてどれが正解かわからなかったりしたが、nuxt3では2つ表示されるだけでそして対象のファイルがすぐわかるように。神。

launch.json

    {
      "type": "firefox",
      "request": "launch",
      "name": "nuxt3 browser",
      "url": "http://127.0.0.1:3000/",
      "webRoot": "${workspaceFolder}",
    },

tasks.jsonを使うことによりcode-serverの起動を自動化することもできたがここでは割愛。

Vue DevTools

ChromeエクステンションのVue開発者ツール「Vue Devtools」は使えるので引き続きこれを使うことになる。

@nuxt/devtools


2023-07-12 なぜか急にcode-serverがエラーで起動しなくなった。これではうまくいかないかも。

$ code-server 
/usr/local/bin/code-server: 行 1: 予期しないトークン `newline' 周辺に構文エラーがあります
/usr/local/bin/code-server: 行 1: `<!DOCTYPE html>'

Nuxt用の開発者ツールが追加されていた。ブラウザで開発したアプリケーションを開くと画面下にアイコンが追加されて、開くとNuxtの様々な状態が閲覧できる。

導入方法

npx nuxi@latest devtools enable

で、nuxt.config.tsに設定が追加される。

次にvscode-serverをインストールする。ブラウザからソースコードを閲覧できる。(使うかはともかく)

wget -O- https://aka.ms/install-vscode-server/setup.sh | sudo sh

起動確認

code-server

Nuxt起動時に自動起動するようにする。nuxt.config.tsのdevtoolsセクションを編集。

  devtools: {
    // Enable devtools (default: true)
    enabled: true,
    // VS Code Server options
    vscode: {
      enabled: true,
      startOnBoot: true,
      reuseExistingServer: true,
    },
  },

この状態でNuxtを実行してブラウザを開くとdevtoolsのアイコンが下に表示される。それを開いてメニューからVSCodeのアイコンを開く。認証求められるので指定のURLを開く。

devtoolsはものすごく高機能ぽいのでなんか重そう。普段使わなさそうのでOFFにしても良いかも。

git

ガイドを参考にNuxt3の変更点を追加。

# Nuxt dev/build outputs
.output
.nuxt

vuex 3

nuxt3からサポートが無くなり、公式には代わりにpiniaが採用された。今回は単純移植のためvuexをnuxt3に組み込む。メンテナンスがいつ止まってもおかしくないので今後はpiniaに置き換えていきたいが。

とりあえずパッケージを追加する。これまでのvuex3からvuex4へアップグレード可能。

yarn add --dev vuex@^4.1.0

pluginsとしてstoreを呼び出せるようにする。vueのpluginとしてのuse()は無くても動くようだが無くて良い確証が無いのでuse()しておく。

plugins/vuex.ts

import store from '@/store'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(store);
  return {
    provide: {
      store,
    },
  }
})

storeファイルはこれまでnuxt側で上手いことやってくれてた構造化を自力で行う必要がある。しかし元のコードはほぼそのまま使える。

store/parent.ts

import { ActionContext, Module } from 'vuex';
import { RootState } from './index'

interface ParentState {
  numberValue: number;
}
export const state = (): ParentState => ({
  numberValue: 0,
});

export const mutations = {
  increment (state: ParentState, value: number): void {
    state.numberValue += value;
  },
};

export const actions = {
  increment (
    context: ActionContext<ParentState, unknown>,
    value: number
  ): void {
    context.commit('increment', value)
  },
};

export const getters = {
  numberValue: (state: ParentState) => (): number => {
    return state.numberValue;
  },
};

export const module: Module<ParentState, RootState> = {
  namespaced: true,
  state,
  getters,
  mutations,
  actions,
  modules: { },
};

子モジュールがあった場合。構成は同じでOK。

store/parent/child.ts

import { ActionContext, Module } from 'vuex';
import { RootState } from '../index'

interface ChildState {
  messages: string[];
}

export const state = (): ChildState => ({
  messages: [],
});

export const mutations = {
  push (
    state: ChildState,
    message:string
  ): void {
    state.messages.push(message);
  },
};

export const actions = {
  push (
    context: ActionContext<ChildState, unknown>,
    message:string
  ): void {
    context.commit('push', message)
  },
};

export const getters = {
  messages: (state: ChildState) => (): string[] => {
    return state.messages;
  },
};

export const module: Module<ChildState, RootState> = {
  namespaced: true,
  state,
  getters,
  mutations,
  actions,
  modules: {},
};

indexでモジュールを階層化してstoreとして生成する。

store/index.ts

import { createStore } from 'vuex'
import * as parent from './parent'
import * as child from './parent/child'

export interface RootState {}

parent.module.modules = {
  child: child.module,
}

const store = createStore({
  modules: {
    parent: parent.module,
  },
})

export default store

これでこれまで通り呼び出しが可能になる。vue devtoolsで監視もできる。もちろん型補正は効かないので不便。

<script lang="ts" setup>
const store = useNuxtApp().$store

const messages = computed<string[]>(() => {
  return store.getters['parent/child/messages']()
})
const numberValue = computed<number>(() => {
  return store.getters['parent/numberValue']()
})

const doMutation = () => {
  useNuxtApp().$store.commit(
    'parent/increment', 1
  );
}
const doAction = () => {
  useNuxtApp().$store.dispatch(
    'parent/child/push', 'aaa'
  );
}
</script>

vuetify 3

今回の移植最大の山場。大量の破壊的変更しかない。インストールも自力でやる必要があり、正直何をしているのかさっぱりわからない。

参考: Nuxt 3 で Vuetify 3 を使う
参考: Vuetify3 Beta環境で、process.env.DEBUGにアクセスできない

yarn add -dev vuetify vite-plugin-vuetify @mdi/js eslint-plugin-vuetify

plugins/vuetify.ts

import { createVuetify } from 'vuetify'
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'

export default defineNuxtPlugin((nuxtApp) => {
  const vuetify = createVuetify({
    ssr: false,
    display: {
      mobileBreakpoint: 'sm',
    },
    icons: {
      defaultSet: 'mdi',
      aliases,
      sets: {
        mdi,
      },
    },
  })

  nuxtApp.vueApp.use(vuetify)
})

assets/main.scss

@use 'vuetify/styles';

nuxt.config.ts

import vuetify from 'vite-plugin-vuetify'

export default defineNuxtConfig({
  vite: {
    ssr: {
      noExternal: ['vuetify'],
    },
    define: {
      // 今はエラーでなくて要らないかも
      // 'process.env.DEBUG': false,
    },
  },
  css: ['@/assets/main.scss'],
  build: {
    transpile: ['vuetify'],
  },
  hooks: {
    'vite:extendConfig': (config) => {
      config.plugins!.push(vuetify());
    },
  },
});

eslintにplugin:vuetify/recommendedを追加する。(他にもある)

.eslint.js

      "extends": ["@nuxtjs/eslint-config", 'plugin:vuetify/recommended'],

これでvuetifyが利用可能となり、elisntによるチェックも効くようになる。vuetify3への移行もやってくれる部分がある。便利。但し各コンポーネントがそれぞれ大幅破壊的変更があるため、全部動作チェックが必要。以下、気づいた変更差分を挙げていく。実際は他にも勝手にeslintで変わった部分などある。

コンポーネント全体

vue3自体の変更に合わせてvalueなどの名称変更に対応が必要。

  • value->modelValue
  • input->update:modelValue
  • 従来inputではなかったイベントも一部update:modelValueに統一されている

update:modelValueについて、公式ドキュメントでは@update:modelValueとしてキャメルケースで使用されているが、スタイルガイド的にはv-onはケバブケース@update:model-valueになるべきなんじゃあと思った。が、とりあえず@update:modelValueとしておく。

<v-dialog :value="value" @input="$emit('input', $event)">
<v-dialog :modelValue="modelValue" @update:modelValue="emits('update:modelValue', $event)">

v-app

before

<v-app dark>
  <v-main>
    <v-container>
      <nuxt />
    </v-container>
  </v-main>
</v-app>

after
v-app->v-layout?

<v-layout>
  <v-main>
    <v-container>
        <slot />
    </v-container>
  </v-main>
</v-layout>

v-navigation-drawer/v-app-bar

変更点が多すぎて何が何やらわからなかった。完全再現は諦めてそれっぽく見えるように調整。

v-steppers


2023-11-14 その後labにstepperが追加された模様。それが使えるならそっちのほうがいい。


v-steppersなくなった、取り急ぎ簡易なものを自作、あとでちゃんと作る

before

  <v-stepper v-model="stepper.step">
    <v-stepper-header>
      <v-stepper-step :step="1"> 一覧 </v-stepper-step>
      <v-stepper-step :step="2"> 編集 </v-stepper-step>
    </v-stepper-header>

    <v-stepper-items>
      <v-stepper-content :step="1">
        <xxx />
      </v-stepper-content>
      <v-stepper-content :step="2">
        <xxx  />
      </v-stepper-content>
    </v-stepper-items>
  </v-stepper>

after
components/m-steppers.vue

<template>
  <div>
    <div v-for="n in steps">
      <slot :name="n" v-if="current === n"/>
    </div>
    <v-row
      justify="center"
    >
      <v-col
        v-for="step in steps" :key="step"
        class="text-center"
        cols="1"
      >
        <v-chip
          :class="{ 'bg-primary font-weight-bold': step == current }"
          rounded
        >{{ step }}</v-chip>
      </v-col>
    </v-row>
  </div>
</template>

<script lang="ts" setup>
  interface Props {
    steps: number,
    current: number
  }
  const props = withDefaults(
    defineProps<Props>(), { steps: 1, current: 1 }
  );
</script>

使う側

  <m-steppers :steps="stepper.steps" :current="stepper.step">
    <template #1>
      <xxx />
    </template>
    <template #2>
      <xxx />
    </template>
  </m-stepper>

v-btn

before

<v-btn nuxt to="/xxxx">XXXX</v-btn>

after
NuxtLinkを表すnuxtプロパティが無くなったみたい
(無くすだけでいいのかは未調査)

<v-btn to="/xxxx">XXXX</v-btn>

v-tooltip

before

        <v-tooltip bottom>
          <template #activator="{ on }">
            <span v-on="on">あああ</span>
          </template>
        </v-tooltip>

after
調べきれてないけどactivator周りが変わったかも。これで動いた。

        <v-tooltip location="bottom" activator="parent">
          <template #activator>
            <span>あああ</span>
          </template>
        </v-tooltip>

v-text-field

before

        <v-text-field
          :value="item"
          @change="handle(item, $event)"
        />

after
change->update:modelValue

        <v-text-field
          :model-value="item"
          @update:modelValue="handle(item, $event)"
        />

v-select

before

        <v-select
          :value="item"
          :items="selectableList"
          item-value="タイトル"
          @change="handle(item, $event)"
        />

after
ラベルはitem-title、change->update:modelValue

        <v-select
          :model-value="item"
          :items="selectableList"
          item-title="タイトル"
          @update:modelValue="handle(item, $event)"
        />

v-checkbox

before

        <v-checkbox
          :input-value="item"
          @change="handle(item, $event)"
        />

after
inputValue->modelValue、change->update:modelValue

        <v-checkbox
          :modelValue="item"
          @update:modelValue="handle(item, $event)"
        />

v-data-table

before

    <v-data-table
      dense
      :headers="sampleHeaderList"
      :items="sampleItemList"
      item-key="_id"
      show-select
    >
      <template
        v-for="(h, i) in sampleHeaderList"
        #[`header.${h.value}`]="{ header }"
      >
            <span>{{ header.text }}</span>
      </template>
      <template #[`item.name`]="{ item, index }">
        <p
          v-text="item.name"
        />
      </template>
      <template #no-data>
        <p>ありません</p>
      </template>
      <template #[`footer`]="{}">
      </template>
    </v-data-table>

略

import { DataTableHeader } from 'vuetify'

const sampleHeaderList: DataTableHeader[] = [
  {
    value: 'order',
    text: 'No.',
    align: 'start',
    sortable: false,
    width: '64px',
  },
  {
    value: 'name',
    text: '名前',
    align: 'start',
    sortable: false,
  },
]

after
ものすごく変わった上にドキュメントが無いに等しい。ギリギリ移植成功。実験的コンポーネント扱いみたい。


2023-11-14 その後アップデートでrawは不要になったかも。


    <v-data-table
      dense
      :headers="sampleHeaderList"
      :items="sampleItemList"
      item-key="_id"
      show-select
    >
      <!-- header->columnになりプロパティも変わった -->
      <template
        v-for="(h, i) in sampleHeaderList"
        #[`column.${h.value}`]="{ column }"
      >
            <span>{{ column.title }}</span>
      </template>
      <template #[`item.name`]="{ item, index }">
        <p
          v-text="item.raw.name"
        ></p>
      </template>
      <template #no-data>
        <p>ありません</p>
      </template>
      <!-- footerにprependがいる -->
      <template #[`footer.prepend`]>
      </template>
    </v-data-table>

略

import { VDataTable } from "vuetify/labs/components"
type DataTableHeaders = VDataTable["headers"];
// このままだと DataTableHeader | DataTableHeader[] みたいな定義になっちゃって扱いづらいので
type ExtractArray<T> = T extends readonly (infer U)[] ? U : never;
type ExtractNestedArray<T> = ExtractArray<ExtractArray<T>>;
type DataTableHeader = ExtractNestedArray<DataTableHeaders>;

// keyが増えtextがtilteになった
const sampleHeaderList: DataTableHeader[] = [
  {
    key: 'order',
    value: 'order',
    title: 'No.',
    align: 'start',
    sortable: false,
    width: 64,
  },
  {
    key: 'name',
    value: 'name',
    title: '名前',
    align: 'start',
    sortable: false,
  },
]

breakpoint

before

console.log((globalThis as unknown as Window).$nuxt.$vuetify.breakpoint.name)

after

import { useDisplay } from 'vuetify'
const { name } = useDisplay()
console.log(name.value)

v-item

before

          <v-item v-slot="{ active, toggle }" :value="key">

after
active->isSelected

          <v-item v-slot="{ isSelected, toggle }" :value="key">

v-radio-group

before

          <v-radio-group
            v-model="items[key]"
            row
            @change="handle()"
          >

after
row->inline?、change->update:modelValue

          <v-radio-group
            v-model="items[key]"
            inline
            @update:model-value="handle()"
          >

v-list/v-list-item-group

before


    <v-list dense>
      <v-list-item-group @change="handle($event)">

after
v-list-item-group無くなった?dense->lines="one"?、$event->$event.id?、change->click:select。他にもv-list-xxxxx系は変更が多い

      <v-list lines="one" @click:select="handle($event.id)">

v-btn-toggle

before

        <v-btn-toggle @change="handle($event)">

after
change->update:modelValue

        <v-btn-toggle @update:model-value="handle($event)">

v-file-input

before

Blob | null

after
v-modelの型がBlobからFile[]に変わった

File[] | undefined

@kevinmarrec/nuxt-pwa

@nuxtjs/pwaの代替パッケージ。

特殊な使い方をしていたのでコードは非記載。自分の使い方的には簡単に移植できた。

yarn add --dev @kevinmarrec/nuxt-pwa@^0.17.0

nuxt.config.ts

  modules: [
    '@kevinmarrec/nuxt-pwa',

略

  // 設定は完全に自分環境なので参考まで
  pwa: {
    workbox: {
      enabled: true,
      templatePath: '@/pwa/sw.js',
      autoRegister: false,
    },
    icon: false,
    manifest: {
      lang: 'ja',
      start_url: undefined,
    },
  },

@sidebase/nuxt-auth

@nuxtjs/auth-nextの代替パッケージ。困り要員。

まだβ版ということもあるが、バグだらけでまともに動かない上に旧パッケージの機能が網羅できていなくて今後サポートされるのかもわからない。

もともとJWT/AccessToken/RefreshTokenを利用したAPI/Websocket認証を実装していたが、@sidebase/nuxt-authでは(まだ?)RefreshTokenの実装が無さそう。他に良いパッケージが見当たらないため、一旦RefreshTokenを捨ててAccessTokenの扱いのみ移行した。最悪自前でリフレッシュの仕組みを実装・・・?

参考: withCredentials-like configuration functions

yarn add --dev @sidebase/nuxt-auth@^0.6.0-beta.7

nuxt.config.ts

  modules: [
    '@sidebase/nuxt-auth',

略

  // 設定は参考
  auth: {
    provider: {
      type: 'local',
      endpoints: {
        signIn: {
          path: '/api/auth/signin',
          method: 'post',
        },
        signOut: {
          path: '/api/auth/signout',
          method: 'post',
        },
        getSession: {
          path: '/api/auth/user',
          method: 'get',
        },
        // refresh: {
        //   url: '/api/auth/refresh',
        //   method: 'post',
        //   withCredentials: true,
        // },
      },
      pages: {
        login: '/',
      },
      token: {
        signInResponseTokenPointer: '/body/access_token',
        type: 'Bearer',
        headerName: 'Authorization',
        maxAgeInSeconds: 60 * 15,
      },
      sessionDataType: {
        code: 'number',
        success: 'boolean',
        detail: {
          user: {
            _id: 'string',
            email: 'string',
            name: 'string',
            user_roles: 'string[]',
          },
        },
      },
    },
    baseURL: 'http://127.0.0.1:3001/',
    globalAppMiddleware: {
      isEnabled: false,
    },
  },

まず最低限まともに動かすためにパッチを当てる。

patches/@sidebase+nuxt-auth+0.6.0-beta.7.patch

diff --git a/dist/runtime/composables/local/useAuth.mjs b/dist/runtime/composables/local/useAuth.mjs
index 7c3fcf0eee26ce23f4e82a8fba853bb9c2986025..cc6b6610456cdb585d54960b4e1ed0187b7ef5d8 100644
--- a/dist/runtime/composables/local/useAuth.mjs
+++ b/dist/runtime/composables/local/useAuth.mjs
@@ -13,9 +13,9 @@ const signIn = async (credentials, signInOptions, signInParams) => {
     method,
     body: {
       ...credentials,
-      ...signInOptions ?? {}
     },
-    params: signInParams ?? {}
+    params: signInParams ?? {},
+    credentials: signInOptions.credentials ?? "same-origin"
   });
   const extractedToken = jsonPointerGet(response, config.token.signInResponseTokenPointer);
   if (typeof extractedToken !== "string") {
diff --git a/dist/runtime/types.d.ts b/dist/runtime/types.d.ts
index 133acf8fa2e5db7c7733046ace63c1876423a418..bbbd0da5b48479b7a65fb7ee34503dbecd4383d1 100644
--- a/dist/runtime/types.d.ts
+++ b/dist/runtime/types.d.ts
@@ -323,6 +323,8 @@ export interface SecondarySignInOptions extends Record<string, unknown> {
      * @default true
      */
     redirect?: boolean;
+    /** Fetch API Credential Option https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch */
+    credentials?: RequestCredentials;
 }
 export interface SignOutOptions {
     callbackUrl?: string;
yarn add --dev patch-package

package.json書き換え

    "postinstall": "patch-package && nuxt prepare",
yarn install

before
@nuxtjs/auth-next

const $auth = (globalThis as unknown as Window).$nuxt.$auth

try {
  await $auth.loginWith('localRefresh', {
    data: loginForm,
  })
  console.log('loggedin')
  await router.push('/')
} catch(e) {
  console.log(e)
  console.log($auth.error)
  throw e
}

if ($auth.loggedIn) {
  console.log($auth.user)
  console.log($auth.strategies.localRefresh.token.get())
  console.log($auth.strategies.localRefresh.refreshToken.get())
  void $auth.logout()
}

after
@sidebase/nuxt-auth

const { data, status, signIn, signOut } = useAuth()

try{
  await signIn(loginForm, {callbackUrl: '/'})
  console.log('loggedin')
} catch(e) {
  console.log(e)
  throw e
}

if (status.value === 'authenticated') {
  const user = data.value!.xxxx
  console.log(data.value)
  console.log(user)
  console.log(token.value)
  console.log('RefreshTokenは未実装')
  void signOut({callbackUrl: '/'})
}

@nuxt/content 2

てっきりサポート切られるんだと思っていたcontent。なんとか生き残った。従来のコードだと動かなかったので書き換えたが仕様をちゃんと知らないのでもしかしたらここまで変えなくても動いたのかも。

yarn add @nuxt/content@^2.6.0

nuxt.config.ts

  modules: [
    '@nuxt/content',

略

  content: {
    api: {
      baseURL: '/content_api',
    },
  },

before
xxxx.vue(従来)

<template>
  <article>
    <h1>{{ page.title }}</h1>
    <nuxt-content :document="page" />
  </article>
</template>

<script>
export default {
  async asyncData({ $content }) {
    // content/xxxx.md読み込み
    const page = await $content('xxxx').fetch()

    return {
      page,
    }
  },
}
</script>

after
[...slug].vue(今回)

<template>
  <main>
    // content/contents/xxxx.mdを読み込み
    <ContentDoc />
  </main>
</template>

WSL

WSL2上で動作させる場合に必要らしい設定

nuxt.config.ts

vite: {
    server: {
      watch: {
        // Settings for WSL
        // https://ja.vitejs.dev/config/server-options.html
        usePolling: true,
      },

その他

困ったときは

  • ESLint: Restart ESLint Server
  • Volar: Restart Vue Server
1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?