Blogブログ

nuxt3

vue

2023.01.31

神回|vue3.js nuxt3 typescriptどうやる?【ゼロから解説】

こんちは、FIELD山本です。
最近はwebエンジニアとしての開発よりもマーケティングや営業をやっています。
ですが、11月にvue3のnuxt3の安定版が出たということで
リハビリを兼ねてその解説をしていこうと思います。

それでは今から完全にゼロから初心者~中級者向けにvue3 nuxt3 typescript 糖衣構文、つまり全てを解説していきます。

(弊社はwebエンジニアの転職におすすめな転職サイトを紹介しています。)

(といいつつ割と他の言語が上級者としてできる人でも楽しめる内容になったかもしれません)

まずわたしはnodejs 16.11を使っていきます。

nodenv install 16.11.0

入ってるか確認しましょうか。

nodenv  versions

と実行すると..

14.15.4
  14.17.0
  16.11.0
* 16.16.0 (set by /Users/username/.anyenv/envs/nodenv/version)

こんな感じで表示されます

では、進めていきます。

弊社は来年あたり、
ボロボロの介護求人パークを2023/2くらいに、
弊社webエンジニアたちと一緒にnuxt3でリニューアルしようと思ってるので
kaigo-kyujin-parkfrontendみたいなフォルダでやろうかなと思います。
(めちゃめちゃライブ感ある感じで書いていますが、その通りです。)
ちなみに今の介護求人パークというサイトは誰が作ったのかは知らないですがかなりやばいですね。開発者は他に向いている別の仕事についた方がいいです^^
(契約含めて運営元に何いっても良いと言われています。)

$ npx nuxi init kaigo-kyujin-park-frontend
$ cd kaigo-kyujin-park-frontend
$ yarn 
$ yarn dev

はいこれで、http://localhost:3000でアクセスできるようになりましたね。

そうしたら次に、

yarn nuxi typecheck

を実行、これでtypescriptをインストールします。

rootディレクトリに
pagesというディレクトリを作り
その中にindex.vueとabout.vueを作ります。
その後、rootディレクトリのapp.vueを開き

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

に変えます。

この<NuxtPage />中にpagesのコンポーネントが入ってきます。

その後
index.vueを

<template>
    <h1>This is index Page</h1>
</template>

にしてから、

about.vueを

<template>
    <h1>This is about Page</h1>
</template>

にします。
そして、
http://localhost:3000/
http://localhost:3000/about/
これでページ移動ができることを確認できると思います。

ではnuxt3でcomponentを作ります。

pagesと同階層にcomponentsというフォルダを作りましょう。
そしてそのcomponentsの中にheader.vueを作ります。

<template>
    <div class="header">
        <h1>タイトル</h1>
    </div>
</template>

<style>
.header{
    background-color:blue;
    color: aliceblue;
    display:flex;
}
</style>

同じくfooter.vueを作り

<template>
  フッターです。
</template>

で保存しましょう

そして、ドキュメントルートのapp.vueを

<template>
  <div>
    <Header />
    <NuxtPage />
    <Footer />
  </div>
</template>

に変えましょう。
これで

これでlocalhost:3000にアクセスすると
headerとfooterも確認できるはずです。
コンポーネントを呼び出すときには先頭は大文字になることに注意

このようなページが出てくればOKです。

次に、
pages配下にjobsフォルダを作ります。
jobsの中にはindex.vueと[id].vueというファイルを作ります。

つまり、localhost:3000/jobsと localhost:3000/jobs/1等のURLを生成しています。
へ〜こうやってやるんだな〜という感じですよね。

同じく、pages配下にprefecturesフォルダを作りその下に[prefecture].vueを作ります。

そしてyarn devを再起動し、

http://localhost:3000/prefectures/pref

http://localhost:3000/prefectures/pref

にアクセスすると・・

見れますね。
/jobs/[id].vueのコードを

<template>
  あなたの名前は{{ name }}

  <input type="text" name="name" v-model="name">
</template>

<script>
import { ref } from 'vue'

export default {
  setup() {
    const name = ref('');

    return {
      name
    }
  }
}
</script>

<style>

</style>

vue3はrefというリアクティブ(状態管理)の値を定義するための関数を用意していて、

setupの中で
const name = ref(’’)のように定義できるようになります。
setup()の最後でreturnで変数nameを返すと、
上の<template></template>の中で使うことができるようになる
状態管理の対象としてnameを作っている感じになっている。
そしてv-modelでそのnameを指定すると、
<input type=”text” value=”XXX”>の値のXXXがリアルタイムにnameに渡され、
{{name}}としてリアルタイムに表示できる
refはオブジェクト以外の状態管理をすることができます。

ここまで理解したら
今の[id].vueを

<template>

  <input type="text" name="name" v-model="name"><br><br><br>
  <button type="button" @click="addSan">登録</button> <br><br>

  あなたの名前は<strong>{{ nameWithSan }}</strong> <br><br>
</template>

<script language="ts">
import { ref } from 'vue'

export default {
  setup() {
    const name = ref('')
    const nameWithSan = ref('')


    const addSan = function () {
      nameWithSan.value = name.value + 'さん'
    };

    return {
      name,
      nameWithSan,
      addSan
    }
  }
}
</script>

<style>

</style>

こちらに変えてください。

変数と関数を定義してreturnで返してることがわかると思います。
addSanが押されたら、nameWithSanの中の値を書き換えるのですが
vue3では.valueと書くことでその値にアクセスできます。

ところで、
これからvue3.js nuxt3 tsをやろうというなら下記のような書き方はしません。

    const addSan = function () {
      nameWithSan.value = name.value + 'さん'
    };

このような書き方はしません
これは、このように↓書きます

const addSan =  () => {
      nameWithSan.value = name.value + 'さん'
};

function()が()⇒になった感じです。
このような記法をアロー関数といいます。
一応、vue3 nuxt3を勉強する前提としてアロー関数の例を書いておきます。

const plus = function (one, two) {
      return one + two;
}

const plus = (one, two) => {
      return one + two;
}

こう変わる感じです。
わかりやすいですね。

では、今の[id].vueを下記に書き換えてください

<template>

  <input type="text" name="name" v-model="person.name"><br><br><br>
  <button type="button" @click="addSan">登録</button> <br><br>

  あなたの名前は<strong>{{ person.name }}</strong> <br><br>
</template>

<script language="ts">
import { ref, reactive } from 'vue'

export default {
  setup() {
    const name = ref('')
    const nameWithSan = ref('')

    const person = reactive({
      id: 1,
      name: 'sato',
      age: 20
    });


    const addSan =  () => {
      nameWithSan.value = name.value + 'さん'
    };

    return {
      name,
      nameWithSan,
      person,
      addSan
    }
  }
}
</script>

<style>

</style>

refはデフォルトだとオブジェクトの初期化はできないので
reactiveというvueの機能をインポートすると
オブジェクトも状態管理できるようになります。
personオブジェクトが状態管理できるようになったので
v-model=”person.name”が書けるようになっています。

ではもう一度これに書き換えましょう

<template>

  <input type="text" name="name" v-model="person.name"><br><br><br>
  <button type="button" @click="addSan">登録</button> <br><br>

  あなたの名前は<strong>{{ person.name }}</strong> <br><br>

  <button type="button" @click="showProfile">プロフィールを表示</button> <br><br>
  <p v-if="isShowProfile">id: {{ person.id }}</p>
  <p v-if="isShowProfile">name: {{ person.name }}</p>
  <p v-if="isShowProfile">age: {{ person.age }}</p>
</template>

<script language="ts">
import { ref, reactive } from 'vue'

export default {
  setup() {
    const name = ref('')
    const nameWithSan = ref('')

    const isShowProfile = ref(false)

    const person = reactive({
      id: 1,
      name: 'sato',
      age: 20
    });


    const addSan =  () => {
      nameWithSan.value = name.value + 'さん'
    };

    const showProfile = () => {
      isShowProfile.value = !isShowProfile.value
    }

    return {
      name,
      nameWithSan,
      person,
      isShowProfile,
      addSan,
      showProfile,
    }
  }
}
</script>

<style>

</style>

refでboolianのケースと呼び出すたびにtrueをfalseに変える変数を作り
それをclickで読んでいます。
そしてv-ifのif文でtrue falseに応じてタグの中身を出すようにしています。

配列もわかっておいたほうがいいので
記載しておきます

<template>

  <input type="text" name="name" v-model="person.name"><br><br><br>
  <button type="button" @click="addSan">登録</button> <br><br>

  あなたの名前は<strong>{{ person.name }}</strong> <br><br>

  <button type="button" @click="showProfile">プロフィールを表示</button> <br><br>
  <p v-if="isShowProfile">id: {{ person.id }}</p>
  <p v-if="isShowProfile">name: {{ person.name }}</p>
  <p v-if="isShowProfile">age: {{ person.age }}</p>
  ------------------------------------------------------------------<br><br>
  <ul>
    <li v-for="job in jobs" :key="job.id">
      <p :class="{active:job.id == 2,active2:job.id==3}">求人ID: {{ job.id }}</p>
      <p>求人タイトル: {{ job.name }}</p>
      <p>求人内容: {{ job.content }}</p>
    </li>
  </ul>
</template>

<script>
import { ref, reactive } from 'vue'

export default {
  setup() {
    const name = ref('')
    const nameWithSan = ref('')

    const isShowProfile = ref(false)

    const jobs = ref([]);
    jobs.value = [
      { id: 1, title: '求人1', content: '求人1の仕事内容です。' },
      { id: 2, title: '求人2', content: '求人2の仕事内容です。' },
      { id: 3, title: '求人3', content: '求人3の仕事内容です。' },
      { id: 4, title: '求人4', content: '求人4の仕事内容です。' },
    ];

    const person = reactive({
      id: 1,
      name: 'sato',
      age: 20
    });


    const addSan =  () => {
      nameWithSan.value = name.value + 'さん'
    };

    const showProfile = () => {
      isShowProfile.value = !isShowProfile.value
    }

    return {
      name,
      nameWithSan,
      person,
      isShowProfile,
      jobs,
      addSan,
      showProfile,
    }
  }
}
</script>

<style>

</style>

配列の定義とv-forの仕様で、
vue3 nuxt3をこれから学ぼうという人なら
なんとなくはわかると思います。

 <p :class="{active:job.id == 2,active2:job.id==3}">求人ID: {{ job.id }}</p>

これは右辺の条件を満たすならactiveをclassに出しますよという書き方でvueの記法です

ここで、
[id].vueなので
idを取得していきます。
nuxtが用意している$routeという変数を使いidの中身を取得していきます。
{{$route.params.id}}という形で取得することができます
この詳細を作る前に、
先に JOB INDEXを作ります。
jobs/index.vueをこれにしてください

<template>

  <h1>求人一覧</h1>

  <ul>
    <li v-for="job in jobs" :key="job.id">
      <p>求人ID: {{ job.id }}</p>
      <p>求人タイトル: {{ job.title }}</p>
      <p>求人内容: {{ job.description }}</p>
      <p>画像: <img :src="job.cover" alt=""></p>
    </li>
  </ul>
</template>

<script lang="ts">
import { ref, reactive } from 'vue'

export default {
  async setup() {
    const jobs = ref([])
    const { data } = await useFetch(`https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs`)

    jobs.value = data._rawValue
    console.log(data._rawValue);

    //const { data: jobs } =  await useFetch(`https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs`)

    return {

      jobs,
    }
  }
}
</script>

<style>
</style>

APIからのデータを取得しています。

const { data } = await useFetch(` https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs`)

この、` `の中では変数が使えます。

ちなみにこの左辺の
const {data}は、

//こういうオブジェクトがあるときに
const person = {
   name: 'developer',
   age: 25,
   pre: 'field'
};

//これはpersonの中のageだけを返している(キーは一致していないといけない)
const { age } = person
//これはpersonの中のキーをそれぞれの変数にして返している
const {name, age, pre } = person

という例を見ると何をしているかわかると思います。
つまり、APIの中のdataというキーを変数に入れてる感じです。
これhはjsの記法です。

const { data } = await useFetch(`https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs`)

awaitは、
基本jsは処理の完了を待たずにして下にどんどん実行されていきますが、
awaitとかくとここで処理が終わるまでに待ってもらえます。
ちなみにawaitを使う場合はその関数の先頭にasyncをつけないといけないので
setupの前にasyncをつけています。

ここまで対応していますが、
実際の開発では、
const jobs = ref([])がなくとも、

const { data: jobs } = await useFetch(https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs)

という書き方をすれば、
jobsのリアクティブ化も行い、
それと同時にjobsの中にAPIのデータを入れてくれます。

つまりコードはこのように修正されます。


<template>

  <h1>求人一覧</h1>

  <ul>
    <li v-for="job in jobs" :key="job.id">
      <p>求人ID: {{ job.id }}</p>
      <p>求人タイトル: {{ job.title }}</p>
      <p>求人内容: {{ job.description }}</p>
      <p>画像: <img :src="job.cover" alt=""></p>
    </li>
  </ul>
</template>

<script lang="ts">
import { ref, reactive } from 'vue'

export default {
  async setup() {
    // const jobs = ref([])
    const { data: jobs } =  await useFetch(`https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs`)
    return {
      jobs,
    }
  }
}
</script>

<style>

</style>

では、[id].vueの詳細ページも下記に書き換えましょう

<template>
  <h2>求人詳細</h2>

  <section v-if="job">
    <h3>{{ job.title }}</h3>
    <div>
      <h4>概要</h4>
      <p>{{ job.description }}</p>
      <h4>カバー</h4>
      <p><img :src="job.cover" alt=""></p>
      <h4>作成日</h4>
      <p>{{ job.createdAt }}</p>
    </div>
  </section>
</template>

<script lang="ts">
import { ref, reactive } from 'vue'

export default {
  async setup() {
    const route = useRoute();
    const id = route.params.id
    const { data: job } = await useFetch(`https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs/${id}`)

    return {
      job
    }
  }
}
</script>

<style>

</style>

ではURLを見てみましょう。
http://localhost:3000/jobs/2

URLのgetパラメータの数字を取ろうと思ったら
const route = useRoute();
は必要です。
route.params.id
このidは[id].vueのidです。
これをconst idに入れて${id}でAPIのURIの生成に使っています。

ちなみに上の例ではAPIの値のdataの中身をjobとして受け取っています。

ではvue3 nuxt3のコンポーネントの解説をしていきます

/components/jobs/Item.vue を作りましょう。
ファイルの先頭は大文字という文化です。

<template>
  <h3>Job/Itemです</h3>
</template>

次に
jobs/index.vueの中身のHTMLにこれを追加します。

<ul>
    <JobsItem/>
</ul>

そうすると
http://localhost:3000/jobs
にこの内容が表示されるのがわかります。
これが確認できたら、
今のコードを

<JobsItem
        v-for="job in jobs"
        :key="job.id"
      />

に変えてみましょう。
そうすると、job一覧が表示されます。
componentに対してv-forを適応させてる感じです。
コードを全部貼ると…

<template>

  <h1>求人一覧</h1>

  <JobsItem
        v-for="job in jobs"
        :key="job.id"
  />
</template>

<script lang="ts">
import { ref, reactive } from 'vue'

export default {
  async setup() {
    // const jobs = ref([])
    const { data: jobs } =  await useFetch(`https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs`)
    return {
      jobs,
    }
  }
}
</script>

<style>

</style>

これができたら、
では、求人データを表示させてみましょう
Item.vueの中身を下記に変えてください

<script setup lang="ts">
const props = defineProps({
  job: {
    type: Object
  }
})

</script>

<template>
  <li>
    <p>求人ID: {{ props.job.id }}</p>
    <p>求人タイトル: {{ props.job.title }}</p>
    <p>求人内容: {{ props.job.description }}</p>
    <p>画像: <img :src="props.job.cover" alt=""></p>
    <p>
      <NuxtLink :to="`/jobs/${job.id}`">
        求人詳細へ
      </NuxtLink>
    </p>
  </li>
</template>

<style>

</style>

そして

index側のjobItemの表記を

<JobsItem
        v-for="job in jobs"
        :key="job.id"
        :job="job"
      />

に変更すると

job一覧ページに求人が出たと思います。

ここで、item.vueは糖衣構文で書いています
(vue3の公式推奨の書き方です)

<script setup lang="ts">
const props = defineProps({
  job: {
    type: Object
  }
})

</script>

糖衣構文にするとscriptは上に書きます
コンポーネント側でpropsこのような構文で書き、
jobの型を指定すると
job/indexのようなコンポーネントを呼び出す側で
jobを渡せるようになると思ってください。
これをやることでコンポーネントのファイルで。

{{ props.job.id }}

の形でjobを使うことができるようになります。

では次にvue3 nuxt3の解説としてlayoutをやります

ルートディレクトリにlayoutsというフォルダを作ります。

そしてその中身にdefault.vueを作り中身をこれにします

<script setup lang="ts">

</script>

<template>
  <div>
    <h2>デフォルトレイアウト</h2>
    <slot></slot>
  </div>
</template>


<style>

</style>

と、します。

その後に
app.vueの中身を

<template>
  <NuxtLayout>
    <Header />
    <NuxtPage />
    <Footer />
  </NuxtLayout>
</template>

にします。

こう書くことで、
default.vueのslotの中身に

  <NuxtLayout>
    <Header />
    <NuxtPage />
    <Footer />
  </NuxtLayout>

が入ってくるという形になります。

custom.vueのレイアウトも作ることでよりわかりやすく解説します

layoutsのフォルダにcustom.vueを作ってください

そして中身を

<script setup lang="ts">

</script>

<template>
  <div>
    <h2>カスタム</h2>
    <slot></slot>
  </div>
</template>


<style lang="scss">
  h2 {
    background-color: #F0F0F0;
  }
</style>

にします。
jobs/index.vueにこれを適応するには

definePageMeta({
  layout: 'custom',
});

を追記します。
先ほどのindex.vueはまだ糖衣構文ではいので、
糖衣構文に直すところも含めると
このようなコードになるはずです。

<script setup lang="ts">
  definePageMeta({
    layout: 'custom',
  });
  const { data: jobs } =  await useFetch(`https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs`)

</script>
<template>

  <h1>求人一覧</h1>
      <JobsItem
        v-for="job in jobs"
        :key="job.id"
        :job="job"
      />
</template>

<style>

</style>

styleについてもvue3 nuxt3ではどうなのか、解説していきます

styleタグがこれまで出てきましたが、
<style scoped>
</style>
にすると
そのスタイルはそのコンポーネントだけに適応されます。
default.vueの中にscopedでstyleを書くと、
default.vueで使われてタグだけにstyleが適応されることがわかります。

TitleやDescriptionの書き方もvue3 nuxt3でどう書くのか、解説していきます

nuxt.config.tsをこのファイルにしてください

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  app: {
    head: {
      title: 'myblog',
      htmlAttrs: {
        lang: 'en',
      },
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { hid: 'description', name: 'description', content: '' },
        { name: 'format-detection', content: 'telephone=no' },
      ],
      link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
      //script: [{ src: '/js/jquery-3.3.1.min.js' }, { src: '/js/slick/slick.min.js' }],
    }
  }
})

と、変えてください。

なんとなく見るとわかると思いますが
こういう感じでTDKを設定します。
ちなみにjqueryのslickを使いたい人向けにコメントアウトで記載もしておきました。
これだとデフォルトがすべてこのTDKになってしまうので、
各ページのTDKを設定したい場合は、
例えば、
jobのindexをこのように変えると
jobのindexだけのTDKを表示できます。

<script setup lang="ts">
  definePageMeta({
    layout: 'custom',
  });


  const { data: jobs } =  await useFetch(`https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs`)

  useHead({
    title: 'My App',
    meta: [
      { name: 'description', content: 'My amazing site.' }
    ],
    bodyAttrs: {
      class: 'test'
    },
    script: [ { children: 'console.log(\'Hello world\')' } ]
  })
</script>
<template>

  <h1>求人一覧</h1>
      <JobsItem
        v-for="job in jobs"
        :key="job.id"
        :job="job"
      />
</template>

<style>

</style>

ではさらにグローバルのcssの読み込みをしてみましょう。

ルートディレクトリに
assetsというフォルダとpublicというフォルダを作り
assetsの中にcssというフォルダを作りその中にreset.cssを作りましょう

コードはこちらです。

@charset "UTF-8";

html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code,
del, dfn, em, img, ins, kbd, q, samp,
small, strong, sub, sup, var,
b, i,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
  margin: 0;
  padding: 0;
  border: 0;
  outline: 0;
  font-size: 100%;
  vertical-align: baseline;
  background: transparent;
}

body {
  line-height: 1;
}

article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
  display: block;
}

nav ul {
  list-style: none;
}

blockquote, q {
  quotes: none;
}

blockquote:before, blockquote:after,
q:before, q:after {
  content: '';
  content: none;
}

a {
  margin: 0;
  padding: 0;
  font-size: 100%;
  vertical-align: baseline;
  background: transparent;
}

/ change colours to suit your needs /
ins {
  background-color: #ff9;
  color: #000;
  text-decoration: none;
}

/ change colours to suit your needs /
mark {
  background-color: #ff9;
  color: #000;
  font-style: italic;
  font-weight: bold;
}

del {
  text-decoration: line-through;
}

abbr[title], dfn[title] {
  border-bottom: 1px dotted;
  cursor: help;
}

table {
  border-collapse: collapse;
  border-spacing: 0;
}

/ change border colour to suit your needs /
hr {
  display: block;
  height: 1px;
  border: 0;
  border-top: 1px solid #cccccc;
  margin: 1em 0;
  padding: 0;
}

input, select {
  vertical-align: middle;
}

次にnuxt.config.tsを開きます。

// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
  app: {
    head: {
      title: 'myblog',
      htmlAttrs: {
        lang: 'en',
      },
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { hid: 'description', name: 'description', content: '' },
        { name: 'format-detection', content: 'telephone=no' },
      ],
      link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
      //script: [{ src: '/js/jquery-3.3.1.min.js' }, { src: '/js/slick/slick.min.js' }],
    }
  },
  css: ['@/assets/css/reset.css', '@/assets/css/style.css'],
})

のように

css: [‘@/assets/css/reset.css’, ‘@/assets/css/style.css’],
を追加
(ちなみに@はルートディレクトリパスです。)

次にvue3 nuxt3でpublicに画像を置いて呼び出す解説をします

画像は基本的にpublicに置きます。
assetsに置く人がいますがいずれ滅びます
コンパイル時に使われないものはpublicに置くというセオリーから
画像はpublicに置きます。

ではpublicの中にimagesというフォルダを作りましょう。
logo01.svgを保存しました。
ここでpages/jobs/index.vueで、
<img src=”@/images/logo01.svg” >
とかいたら呼び出せます。
publicが省略されますね。 assetsがついていないときは基本@とやるとpublicになってます。

SCSSでも使ってみますか。。

まずjob/index.vueのstyleタグを

<style scoped lang="scss">
  img {
   width:100px; height:auto;
  }

</style>

のようにlang=”scss”と書いてください
おそらくscssがないよというエラーが起きますのでこれをインストールしておきます。
インストールするために まずはルートに移動して

yarn add -D sass

を実行してください。
そうするとpackeage.jsonにscssが追加されているのがわかります。

{
  "private": true,
  "scripts": {
    "build": "nuxt build",
    "dev": "nuxt dev",
    "generate": "nuxt generate",
    "preview": "nuxt preview",
    "postinstall": "nuxt prepare"
  },
  "devDependencies": {
    "nuxt": "3.0.0-rc.11",
    "sass": "^1.55.0"
  }
}

これらをインストルしていきます。

yarn installをするとpackage.jsonの中身がインストールされます。
こうすることでscssが入ったのでエラーは消えたと思います。

ちなみにvue3 nuxt3 でscssを使うことを学ぶ場合は
Nuxt 3でSCSSを使う方法(Nuxt 3 RC4)
こちらの解説もわかりやすいです。

lodashでも入れてみますか。。

yarn add -D nuxt-lodash
を実行してyarn installを実行します。
これをインストールしたらどこでもladashを呼び出せるように
nuxt.config.tsのmoduleに追加します。

export default defineNuxtConfig({
  app: {
    head: {
      title: 'myblog',
      htmlAttrs: {
        lang: 'en',
      },
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        { hid: 'description', name: 'description', content: '' },
        { name: 'format-detection', content: 'telephone=no' },
      ],
      link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
      //script: [{ src: '/js/jquery-3.3.1.min.js' }, { src: '/js/slick/slick.min.js' }],
    }
  },
  css: ['@/assets/css/reset.css', '@/assets/css/style.css'],
  modules: ['nuxt-lodash'],
})

そうするとどこでも _ (ローダッシュ)が用意してる関数を使えます。

例えば…

const text = useToUpper('it works!');

userToUpperとかが使えるようになるという感じです。

↓こんな感じで使えます。

<script setup lang="ts">
  definePageMeta({
    layout: 'custom',
  });


  const { data: jobs } =  await useFetch(`https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs`)

  useHead({
    title: 'My App',
    meta: [
      { name: 'description', content: 'My amazing site.' }
    ],
    bodyAttrs: {
      class: 'test'
    },
    script: [ { children: 'console.log(\'Hello world\')' } ]
  })
  const text = useToUpper('it works!');
</script>
<template>

  <h1>求人一覧{{text}}</h1>

      <img src="@/images/logo01.svg" >
      <JobsItem
        v-for="job in jobs"
        :key="job.id"
        :job="job"
      />
</template>

<style scoped lang="scss">
  img {
   width:100px; height:auto;
  }

</style>

ちなみに

https://lodash.com/docs/4.17.15

このサイトの左側の関数等を使うことができて、

使うときは
use + 頭文字大文字の関数名
になります。
だからtoUpperという関数を使う場合は
userToUpper()になってます。これはNuxtの仕様です。

vue3 nuxt3 lodashは
[Nuxt.js]lodashを入れてみた
こちらの解説も勉強になります。

vue3 Nuxt3でのstateの解説をします。

ルートディレクトリにcomposableというフォルダを作ります

その中にjob.tsを作ります。

export const setJob = (job) => {
  return (newJob: {}) => (job.value = newJob)
}

export const useJob = () => {
  const job = useState('job', () => ({}))

  return {
    job: readonly(job),
    setJob: setJob(job)
  }
}

userJobの関数の中でuseStateがありますが、
これはstoreに対してデータを用意するという宣言となります。
jobというキーをstateの中に確保するという定義をしています。

const job = useState(‘job’, () => ({}))とは

jobの値がない時は空のオブジェクトをセットし、
あるならその値をjobに入れたままにするという記述です。

setJobは
stateとしてjobを作りましたがその中に値を入れるための関数です
stateはこのようなセッターを通してでしか値を登録できません。

newJobというのはsetJobで受け渡される求人情報だと思ってください

job.valueは
refの時と同じvalueへの代入方法で
stateの変数に値を入れるときはvalueに入れますよという感じで覚えてください。

ではコンポーネントフォルダのjobフォルダの中にcover.vueを作ります。

<script setup lang="ts">

const { job: jobState } = useJob()

</script>

<template>
  <div>
    <p>Cover コンポーネント</p>
    <img :src="job.cover" alt="">
  </div>
</template>

この画面は求人詳細から呼び出されるコンポーネントです。

求人のAPIがcoverという情報を持っていて画像が入ってい他ので、
それに合わせて作成しました。
この子コンポーネントを親から呼ぶときにprops等で値を渡さずに呼びます。
つまり、
pages/jobs/[id].vue

<JobsCover />

だけを入れます。

全文のコードを一応書いておくと…

<template>
  <h2>求人詳細</h2>

  <section v-if="job">
    <h3>{{ job.title }}</h3>
    <div>
      <h4>概要</h4>
      <p>{{ job.description }}</p>
      <h4>カバー</h4>
      <p><img :src="job.cover" alt=""></p>
      <h4>作成日</h4>
      <p>{{ job.createdAt }}</p>
    </div>
  </section>
  <JobsCover />
</template>

<script lang="ts">
import { ref, reactive } from 'vue'

export default {
  async setup() {
    const route = useRoute();
    const id = route.params.id
    const { data: job } = await useFetch(`https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs/${id}`,    {initialCache:false} )

    const { job: jobState, setJob } = useJob()
    setJob(job.value);

    return {
      job
    }
  }
}
</script>

<style>

</style>

です。

useFetchでjob詳細を持ってきます、
ちなみに

await useFetch(https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs/${id}, {initialCache:false} )

の第二引数のオブジェクトはキャッシュさせないという設定で大体詳細系はこれをつけてください。 なぜ必要なのかは取ってみてしばらく運用するとすぐに気付きます。 一覧→詳細→一覧→詳細→一覧→詳細とやると、 詳細の値がキャッシュによりいつも変わらないということになってしまうので、この第二引数をつけます。

そしてuseFetchのdataというキーの値をjobというキーに入れてくれということで入れておき、
useJob()を実行しておきます。

useJob()の中身を見るとjobとsetJobが返り値なので、それをそのまま
const { job, setJob } = useJob()
と受けたいところですが、
上でもうjobを使ってしまってるので、
jobStateというキーに入れたいということで、

const { job: jobState, setJob } = useJob()

としています。

そしてsetJob(job.value);

これで、jobステートにuseFetchで取得したjobの値を入れます。

そしてコンポーネントの<jobsCover />が呼ばれています。
もう一度上のjobsCoverのコードを見ると
const { job: jobState } = useJob()
が呼ばれていて、
ここで先ほどセットしたjobの値を
jobStateで受けています。

なのでjobState.coverが使えてるということです。

コンポーネントがネストしたりいろいろなコンポーネントにjobの情報がまたがる時に
このようなstateは便利ですね。
SPAだと特に便利です。

ちなみにvue3 nuxt3 のstateの解説はこの記事も勉強になります。
Nuxt3入門(第8回) – Nuxt3のuseStateでコンポーネント間で状態を共有する

ではvue3 nuxt3をtypescriptで書く解説をします。

vue3はts推奨です。

そもそも最近はjavascriptではなくて
typescriptを書くことが推奨されているので もうjs書くのはやめましょう。
VSコードでTypeScript Vue Plugin (Volar)というプラグインを入れてから始めましょう。

Nuxt 3 を今すぐオススメしたい 15 のポイント
この記事も勉強になりますのでおすすめ

というわけで、
pages/job/index.vueをtsにしてみます。

<script setup lang="ts">
  definePageMeta({
    layout: 'custom',
  });

  const { data: jobs } =  await useFetch(`https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs`)

  useHead({
    title: 'My App',
    meta: [
      { name: 'description', content: 'My amazing site.' }
    ],
    bodyAttrs: {
      class: 'test'
    },
    script: [ { children: 'console.log(\'Hello world\')' } ]
  })
  const text = useToUpper('it works!');
</script>
<template>

  <h1>求人一覧{{text}}{{name}}</h1>

      <img src="@/images/logo01.svg" >
      <JobsItem
        v-for="job in jobs"
        :key="job.id"
        :job="job"
      />
</template>

<style scoped lang="scss">
  img {
   width:100px; height:auto;
  }

</style>

これのスクリプトの上に

type Job = {
    id: number,
    title: string,
    description: string,
    cover: string,
    createdAt: string,
  }

と追記し、

ここでjob型の型定義をしています。

useFetchの部分を

const { data: jobs } = await useFetch<Job[]>('https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs')

に変更します。
これは上で作った「job型の配列がくるよ」ということを表しています。

つまりこういうコードになります。

<script setup lang="ts">
  type Job = {
    id: number,
    title: string,
    description: string,
    cover: string,
    createdAt: string,
  }

  definePageMeta({
    layout: 'custom',
  });

  const { data: jobs } = await useFetch<Job[]>('https://62fe320941165d66bfbabe7e.mockapi.io/apt/v1/jobs')
  useHead({
    title: 'My App',
    meta: [
      { name: 'description', content: 'My amazing site.' }
    ],
    bodyAttrs: {
      class: 'test'
    },
    script: [ { children: 'console.log(\'Hello world\')' } ]
  })
  const text = useToUpper('it works!');
</script>
<template>

  <h1>求人一覧{{text}}{{name}}</h1>

      <img src="@/images/logo01.svg" >
      <JobsItem
        v-for="job in jobs"
        :key="job.id"
        :job="job"
      />
</template>

<style scoped lang="scss">
  img {
   width:100px; height:auto;
  }

</style>

次に、jobsItemのコンポーネントのところですが、

これも

item.vueのコードも変えます。

このように変えてください。

<script setup lang="ts">
type Job = {
  id: number,
  title: string,
  description: string,
  cover: string,
  createdAt: string,
}

type Props = {
  job: Job,
}
const props = withDefaults(defineProps<Props>(), {
  job: () => {
    return {
      id: 0,
      title: '',
      description: '',
      cover: '',
      createdAt: ''
    }
  }
})

</script>

<template>
  <li>
    <p>求人ID: {{ props.job.id }}</p>
    <p>求人タイトル: {{ props.job.title }}</p>
    <p>求人内容: {{ props.job.description }}</p>
    <p>画像: <img :src="props.job.cover" alt=""></p>
    <p>
      <NuxtLink :to="`/jobs/${job.id}`">
        求人詳細へ
      </NuxtLink>
    </p>
  </li>
</template>

<style>

</style>

コンポーネントの親のjobと同じものを定義して、
コンポーネントではprops型にそのjobの型を適応し、
そしてその中身をwithDefaults(defineProps<Props>()で定義しています。
これで、job.contentというような存在しないキーを使ったり、
string型のところに数字がきたりするとVSコードのエラーとして表示してくれるようになります

jobs/index.vueにもitem.vueにも
jobが型として書かれています。

これいつも書かないといけないのかというとそうではなく
実際は一つのファイルに型をまとめてそれを呼び出して使います。

ルートにtypesフォルダを作りそこにjob.tsを作ります。

そして、

export type Job = {
  id: number,
  title: string,
  description: string,
  cover: string,
  createdAt: string,
}

を作ります。

明らかにtype jobを作りexportしてるので外部からimportできるようになっています。

ではインポートします。

script直下に

import { Job } from '@/types/job'

を書いてインポートします。

このように書くと、VSコードでも型をマウスオーバーすると

というようにサジェストしてくれるようになります。

TypeScript + VSCodeで快適プログラミング
ちなみにこの記事も勉強になります。

型の指定は

export type Job = {
  id: number,
  title: ?string,
  description: ?string,
  cover: ?string,
  createdAt: string,
}

?をつけるとないこともあるよという宣言となり、
型付の関数の時は

const addSanToName = (name: string): string => {
  return `${name}さん`
}

const showPersonalities = (personalities: string[]): void => {
  personalities.forEach((personality: string) => {
    console.log(personality);
  })
}

console.log(addSanToName(name))
console.log(showPersonalities(personalities))

(name: string): string
引数のnameはstring、この関数の返り値もstring。
const showPersonalities = (personalities: string[]): void
この:voidはこの関数が返り値を返さない時のことです。

疲れたのでこの辺りで終わります。

どうでしたか、
vue3 nuxt3 typescript 糖衣構文の解説でした。

この記事を見た後に
こちらの記事を見ると結構わかりやすい気がします。

ちなみに僕のインスタこれです。