Open10

Nuxt3をさわるメモ

Takashi TenjoTakashi Tenjo

超久しぶりにNuxtをさわるのでメモを残す。ただしVue自体久しぶりなのでVue全般のことも含まれる。

ドキュメントを読みながら必要そうなところをまとめる。

Takashi TenjoTakashi Tenjo

Linting

.eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:vue/vue3-recommended",
    "prettier"
  ]
}

eslint-plugin-vueがまだFlat Configに対応してなさそうなので今まで通りに(issue)。

Nuxtを使っているとpagesディレクトリでComponent name "index" should always be multi-word. eslint (vue/multi-word-component-names)と怒られるけどどうしたらいいのだろうか。eslint-plugin-nuxtは一年前から更新がなさそうだし。

Takashi TenjoTakashi Tenjo

Auto-importを利用するとeslint/no-undefで怒らるので、Nuxt ComposablesNuxt Utils配下にある関数と、Vue API Referenceにある関数をglobalsに設定した。

あと、@typescript-eslintも導入した。

.eslintrc.json
{
  "root": true,
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:vue/vue3-recommended",
    "prettier"
  ],
  "parser": "vue-eslint-parser",
  "parserOptions": {
    "parser": "@typescript-eslint/parser",
    "sourceType": "module"
  },
  "plugins": ["@typescript-eslint"],
  "globals": {
    "useAppConfig": "readonly",
    "useAsyncData": "readonly",
    "useCookie": "readonly",
    "useError": "readonly",
    "useFetch": "readonly",
    "useHeadSafe": "readonly",
    "useHead": "readonly",
    "useHydration": "readonly",
    "useLazyAsyncData": "readonly",
    "useLazyFetch": "readonly",
    "useLoadingIndicator": "readonly",
    "useNuxtApp": "readonly",
    "useNuxtData": "readonly",
    "useRequestEvent": "readonly",
    "useRequestHeader": "readonly",
    "useRequestHeaders": "readonly",
    "useRequestURL": "readonly",
    "useRoute": "readonly",
    "useRouter": "readonly",
    "useRuntimeConfig": "readonly",
    "useSeoMeta": "readonly",
    "useServerSeoMeta": "readonly",
    "useState": "readonly",
    "$fetch": "readonly",
    "abortNavigation": "readonly",
    "addRouteMiddleware": "readonly",
    "callOnce": "readonly",
    "clearError": "readonly",
    "clearNuxtData": "readonly",
    "clearNuxtState": "readonly",
    "createError": "readonly",
    "defineNuxtComponent": "readonly",
    "defineNuxtRouteMiddleware": "readonly",
    "definePageMeta": "readonly",
    "defineRouteRules": "readonly",
    "navigateTo": "readonly",
    "onBeforeRouteLeave": "readonly",
    "onBeforeRouteUpdate": "readonly",
    "onNuxtReady": "readonly",
    "prefetchComponents": "readonly",
    "preloadComponents": "readonly",
    "preloadRouteComponents": "readonly",
    "prerenderRoutes": "readonly",
    "refreshNuxtData": "readonly",
    "reloadNuxtApp": "readonly",
    "setPageLayout": "readonly",
    "setResponseStatus": "readonly",
    "showError": "readonly",
    "updateAppConfig": "readonly",
    "nextTick": "readonly",
    "defineComponent": "readonly",
    "defineAsyncComponent": "readonly",
    "defineCustomElement": "readonly",
    "ref": "readonly",
    "computed": "readonly",
    "reactive": "readonly",
    "readonly": "readonly",
    "watchEffect": "readonly",
    "watchPostEffect": "readonly",
    "watchSyncEffect": "readonly",
    "watch": "readonly",
    "isRef": "readonly",
    "unref": "readonly",
    "toRef": "readonly",
    "toValue": "readonly",
    "toRefs": "readonly",
    "isProxy": "readonly",
    "isReactive": "readonly",
    "isReadonly": "readonly",
    "shallowRef": "readonly",
    "triggerRef": "readonly",
    "customRef": "readonly",
    "shallowReactive": "readonly",
    "shallowReadonly": "readonly",
    "toRaw": "readonly",
    "markRaw": "readonly",
    "effectScope": "readonly",
    "getCurrentScope": "readonly",
    "onScopeDispose": "readonly",
    "onMounted": "readonly",
    "onUpdated": "readonly",
    "onUnmounted": "readonly",
    "onBeforeMount": "readonly",
    "onBeforeUpdate": "readonly",
    "onBeforeUnmount": "readonly",
    "onErrorCaptured": "readonly",
    "onRenderTracked": "readonly",
    "onRenderTriggered": "readonly",
    "onActivated": "readonly",
    "onDeactivated": "readonly",
    "onServerPrefetch": "readonly",
    "provide": "readonly",
    "inject": "readonly",
    "hasInjectionContext": "readonly"
  }
}
Takashi TenjoTakashi Tenjo

Assets

public/

ルートでそのまま提供される。/path/to/fileのように/から参照する。

assets/

ビルドツール(Vite、Webpackなど)で処理するファイルを置く。~/assets/path/to/fileのように参照する。~はルートへのエイリアス。

Takashi TenjoTakashi Tenjo

Styling

<script>
  import "~/assets/css/main.css"
</script>
<style>
  @import url("~/assets/css/main.css");
</style>

cssのインポートは<script><style>のどちらからでもできる。どちらもインラインに展開される。nuxt.config.tsでもできるそう。

<style>の方を使用しようと思う。

nuxt.config.tsapp.headを使えlink要素を使ってcssを読み込める。これは静的に読み込む方法で、動的に読み込むにはコンポーネントでuseHeadを使う。

SFC

PostCSSはプラグインをnuxt.config.tsで設定して、コンポーネントでは<style lang="postcss">とする。

Scoped StylesとCSS Modulesの両方が使えるらしい。どっちがいいんだろうか。

Transitions

TODO

Takashi TenjoTakashi Tenjo
app.vue
<!-- before -->
<style scoped>
@import url("the-new-css-reset");
@import url("~/assets/global.css");
</style>
nuxt.config.ts
// after
export default defineNuxtConfig({
  css: ["the-new-css-reset", "~/assets/global.css"],
});

グローバルなcssをstyle要素で読み込もうとしたらScoped Stylesと相性が悪かったので、nuxt.config.tsの方で読み込むことにした。

Takashi TenjoTakashi Tenjo

Routing

Pages

ファイル名がそのままパスになる。例えば~/pages/foo/bar.vue/foo/barになる。

ダイナミックルートは[]を使う(users-[group][id].vue)。Optionalは二重括弧[[]]を使う。

pages/users-[group]/[id].vue
<script setup>
  const route = useRoute();

  console.log(route.params.group);
  console.log(route.params.id);
</script>
<template>
  <p>{{$route.params.group}} - {{$route.params.id}}</p>
</template>

値は<script setup>ではuseRouterを使い、テンプレート内では$routeから参照する。

Catch-allはファイル名を[...sulg]のようにする。Optionalはなさそう。

<NuxtLink to="path" />

リンクはNuxtLinkを使う。

Takashi TenjoTakashi Tenjo
/pages/article.vue
/pages/article/[id].vue

このようにルーティングを用意すると/article/...で常にarticle.vueの内容しか表示されない。

/pages/article/index.vue
/pages/article/[id].vue

このようにすると/articleではindex.vueが、/article/...では[id].vueが表示される。

なんでー?

Takashi TenjoTakashi Tenjo

SEO and Meta

全体の<head>を変更するにはnuxt.config.tsapp.headを使用する。

nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      charset: 'utf-8',
      viewport: 'width=device-width, initial-scale=1',
    }
  }
});

コンポーネント内で変更するにはuseHeadを使う。Unheadが使用されている。

ほかにもHeadコンポーネントが使えるらしいけど違いはなんだろうか。

Takashi TenjoTakashi Tenjo

Server

NuxtのサーバーはNitroが使われていて、Nitroではh3が使われている。なので、Nuxtで説明がないがNitroやh3のドキュメントで説明されていることがある。

export default defineEventHandler((event) => {
  // return json  
  return { hello: "world" };

  // or Promise
  return $fetch("http://example.com");

  // or event.node.res.end()
  event.node.res.end("foo");
});

イベントハンドラーを引数に渡したdefineEventHandlerをデフォルトエクスポートする。ハンドラーはasync関数も大丈夫(多分)。

サーバーの処理は~/server以下に書く。

Server Routes

~/server/api/article.ts -> /api/article
~/server/api/article/[id].ts -> /api/article/foo
~/server/api/article/[...slug].ts -> /api/article/foo/bar

~/server/apiに置かれたコードは/api以下でアクセスできる。もし/apiが嫌なら~/server/routeに書くと/以下から直接アクセスできる。[id][...slug]なども使える。

export default defineEventHandler((event) => {
  const id = getRouterParam(event, "id");
  const id = getRouterParams(event);
});

パラメターはgetRouterParamなどから使える。

export default defineEventHandler((event) => {
  const id = parseInt(event.context.params.id) as number;

  if (!Number.isInteger(id)) {
    throw createError({
      statusCode: 400,
      statusMessage: "ID should be an integer",
    });
  };
  return "All good";
});

エラーを返すにはcreateErrorをスローする。