LESSON 12課題あり!

ポートフォリオを
Vue.js にしよう

Lesson 10 で TypeScript にしたポートフォリオを、Lesson 11 で学んだフレームワークの1つ、 Vue.js(ビュー・ジェイエス)で作り直します。 最後には、現場と同じ形のプロジェクトで動かすところまで進みます。

Lesson 11 のおさらい: フレームワークは「作り方の型」と便利な機能のセット。決まった型の中に、自分のコードを書き込んでいきます。 Vue.js は、HTML に近い書き方で学びやすい、画面(フロントエンド)のフレームワークです。

1. Vue.js の考え方

考え方①:データを変えると、画面が勝手に変わる

Lesson 9・10 では、画面を変えたいとき「HTML の中から探して(getElementById)、書き換える(textContent)」という2段階の作業をしていました。 Vue では、データ(箱の中身)を変えるだけで、そのデータを表示している画面が自動で書き換わります。これをリアクティブと呼びます。

Lesson 10:探して、書き換える
const message = document.getElementById("message") as HTMLElement;

// 画面のどこを変えるか探してから、文字を書き換える
message.textContent = "スーの歌も好きだよ";
Lesson 12(Vue):データを変えるだけ
const message = ref<string>("下のボタンを押してみてね。");

// データを変えるだけ。画面は Vue が書き換えてくれる
message.value = "スーの歌も好きだよ";

<!-- 画面(HTML)側には「ここに message を表示」と書いておく -->
<p>{{ message }}</p>

たとえるなら、Lesson 10 は「掲示板の前まで歩いて行って、紙を貼り替える」やり方。Vue は「デジタルサイネージ」で、元のデータを変えれば、表示はすべて自動で切り替わります。

考え方②:画面を部品(コンポーネント)に分けて作る

Vue では、画面をコンポーネントという部品に分けて作ります。1つの部品は、1つの .vue ファイルに、 その部品の動き(script)と見た目(template)をセットで書きます。今回のポートフォリオは、こんな部品に分けます。

App.vueページ全体
(あいさつ・カードを並べる)
→
ProfileCard.vueカードの枠
(3回使い回す)
→
TetsuboAnimation.vue鉄棒アニメーション
GameController.vueコントローラー
RandomMessage.vueひとこと
レゴブロックのように、小さな部品を組み合わせてページを作ります。カードの枠(ProfileCard)は、中身を変えて3回使い回しています。

2. Vue のプロジェクトを作る

Vue のアプリは、Vite(ヴィート)という開発用ツールと組み合わせて作るのが標準です。Vite は、書いたコードをすぐにブラウザで確認できるようにしたり、 公開用のファイルにまとめたりしてくれます。準備には Lesson 10 で入れた Node.js を使います。

  1. 作業する場所でターミナルを開く

    VS Code の「ファイル」→「フォルダーを開く」で、portfolio フォルダがある場所(デスクトップなど)を開き、 「ターミナル」→「新しいターミナル」を選びます。新しいプロジェクトは、portfolio とは別のフォルダとして作ります。

  2. プロジェクトを作るコマンドを実行する

    ターミナル
    npm create vue@latest

    「Need to install the following packages … Ok to proceed?」と聞かれたら、y を押して Enter。

  3. 質問に答える

    いくつか英語で質問されるので、次のように答えます。矢印キーで選び、Enter で決定です。

    質問答え
    Project name(プロジェクト名)portfolio-vue と入力
    Use TypeScript?(TypeScript を使う?)Yes(Lesson 10 の続きなので)
    Select features to include in your project(追加する機能)何も選ばずに Enter
    Select experimental features to include in your project(実験的な機能)何も選ばずに Enter
    Skip all example code and start with a blank Vue project?(サンプルを省いて空のプロジェクトにする?)Yes

    質問の内容や順番は、ツールのバージョンによって少し変わることがあります。

  4. 必要なツールをインストールして、起動する

    画面に表示されるとおり、次の3つを順番に実行します。

    ターミナル
    cd portfolio-vue
    npm install
    npm run dev

    http://localhost:5173/ のようなアドレスが表示されたら、Ctrl を押しながらクリックします。 ブラウザに「You did it!」と表示されれば、Vue のプロジェクトが動いています。

  5. VS Code で portfolio-vue フォルダを開き直す

    「ファイル」→「フォルダーを開く」で portfolio-vue を開きます。右下に「推奨の拡張機能をインストールしますか?」と出たら 「インストール」を押してください(Vue (Official) という拡張機能で、.vue ファイルを色分けして、間違いを教えてくれます)。

localhost とは: 「自分のパソコン」という意味の特別な名前です。npm run dev を実行している間だけ、自分のパソコンの中に小さな Web サーバーが立ち上がり、 そこにブラウザでアクセスしています(Lesson 2 のクライアントとサーバーが、1台の中にいるイメージ)。 ターミナルで Ctrl + C を押すと止まります。

作られたプロジェクトのうち、今回さわるのは次のファイルです。

portfolio-vue のフォルダの構成(主なもの)
portfolio-vue/
├── index.html        ← ページの入り口(ほぼそのまま)
├── package.json      ← npm の設定(dev・build などのコマンド)
├── vite.config.ts    ← Vite の設定
├── tsconfig.json など ← TypeScript の設定(さわらない)
├── node_modules/     ← ダウンロードしたツール(さわらない)
└── src/
    ├── main.ts       ← アプリを起動するファイル
    └── App.vue       ← いちばん外側の部品(ページ全体)

3. これまでのファイルを引っ越す

  1. CSS と画像をコピーする

    Lesson 10 の portfolio フォルダから、style.css を portfolio-vue/src/ に、 images フォルダの中身を portfolio-vue/src/assets/images/ にコピーします(assets/images フォルダは自分で作ります)。 CSS は1文字も変えずにそのまま使えます。

    フォルダの構成
    portfolio-vue/
    └── src/
        ├── style.css            ← portfolio からコピー
        └── assets/
            └── images/          ← portfolio の images の中身をコピー
                ├── tetsubo-bar.svg
                ├── person.svg
                └── game-base.svg
  2. main.ts で CSS を読み込む

    Lesson 8 では HTML の <link> で CSS を読み込みましたが、Vue のプロジェクトでは main.ts に1行書いて読み込みます。

    src/main.ts
    import { createApp } from 'vue'
    import App from './App.vue'
    import './style.css'
    
    createApp(App).mount('#app')

    createApp(App).mount('#app') は「App という部品を、index.html の id="app" の場所に表示してね」という意味です。

  3. index.html のタイトルを直す

    index.html の lang="" を lang="ja" に、<title> を自分のサイト名に書き換えます。中身の <div id="app"></div> は空のままでOK。ここに Vue が画面を描きます。

    index.html
    <!DOCTYPE html>
    <html lang="ja">
      <head>
        <meta charset="UTF-8">
        <link rel="icon" href="/favicon.ico">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Mayu Takahashi Portfolio</title>
      </head>
      <body>
        <div id="app"></div>
        <script type="module" src="/src/main.ts"></script>
      </body>
    </html>

4. 部品(コンポーネント)に分けて書く

.vue ファイルは、上が <script setup lang="ts">(動き・TypeScript)、 下が <template>(見た目・HTML)という2つのパートでできています。1つずつ作っていきましょう。 src の中に components フォルダと utils フォルダを作っておきます。

部品0:ランダムに番号を選ぶ関数(utils/pickRandomIndex.ts)

Lesson 10 で作った関数を、別のファイルに切り出します。export を付けると、他のファイルから import して使えるようになります。

src/utils/pickRandomIndex.ts
// 0〜(length - 1) のどれかの番号を、ランダムに選ぶ
export function pickRandomIndex(length: number): number {
  return Math.floor(Math.random() * length)
}

Vue のプロジェクトでは、行の最後の ; を省略する書き方が一般的です(付けても動きます)。

部品1:カードの枠(components/ProfileCard.vue)

3つのカードに共通の「枠」を、使い回せる部品にします。

src/components/ProfileCard.vue
<script setup lang="ts">
// 親(App.vue)から受け取るデータ
defineProps<{
  title: string
  text?: string
}>()
</script>

<template>
  <section class="card">
    <h2>{{ title }}</h2>
    <p v-if="text">{{ text }}</p>
    <slot></slot>
  </section>
</template>
書き方意味
defineProps<{ title: string; text?: string }>()この部品を使う側から受け取るデータ(props)。title は文字で必須、text は ? が付いているので省略してもよい
{{ title }}受け取った title を、ここに表示する
v-if="text"text があるときだけ、この <p> を表示する
<slot></slot>使う側が中に入れたもの(鉄棒やコントローラーの部品)を、ここに差し込む

props は「注文票」、slot は「お弁当箱の空いたスペース」のイメージです。枠は同じでも、注文票と中身を変えれば別のお弁当になります。

部品2:鉄棒アニメーション(components/TetsuboAnimation.vue)

src/components/TetsuboAnimation.vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

// ② 鉄棒の人が、くるくる回り続ける
const angle = ref<number>(0)
const moving = ref<boolean>(true)
let timer: number = 0

onMounted(function (): void {
  timer = window.setInterval(function (): void {
    if (moving.value) {
      angle.value = angle.value + 5
    }
  }, 30)
})

onUnmounted(function (): void {
  window.clearInterval(timer)
})

function toggle(): void {
  moving.value = !moving.value
}
</script>

<template>
  <div class="stage" id="tetsubo" @click="toggle">
    <img src="../assets/images/tetsubo-bar.svg" alt="鉄棒のイラスト">
    <img
      src="../assets/images/person.svg"
      alt=""
      class="person"
      :style="{ transform: 'rotate(' + angle + 'deg)' }"
    >
  </div>
  <p class="hint">イラストをクリックすると、止まったり動いたりします。</p>
</template>
書き方意味
import { ref, onMounted, onUnmounted } from 'vue'Vue の機能のうち、使うものを読み込む
const angle = ref<number>(0)画面と連動する箱を作る(中身は数字で、最初は 0)。script の中では angle.value で中身を読み書きする
onMounted( … )この部品が画面に表示されたときに実行する。ここでアニメーションを始める
onUnmounted( … )この部品が画面から消えるときに実行する。ここでアニメーションを止めて後片付けをする
@click="toggle"クリックされたら toggle 関数を実行する(Lesson 9 の addEventListener("click", …) と同じ)
:style="{ transform: 'rotate(' + angle + 'deg)' }"angle の値を使って、回転のスタイルを付ける。angle が変わると、自動で回転も変わる
ポイント: Lesson 10 では person.style.transform = … と、画面を直接書き換えていました。Vue 版のタイマーは angle.value を5増やしているだけ。 画面の書き換えは Vue が引き受けてくれています。これが「1章の考え方①」のリアクティブです。 なお template の中では .value を付けずに angle と書けます。

部品3:コントローラー(components/GameController.vue)

src/components/GameController.vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { pickRandomIndex } from '../utils/pickRandomIndex'

// ③ コントローラーのボタンが、ポチポチ光る
const colors: string[] = ['red', 'yellow', 'green', 'blue']
const pressedIndex = ref<number>(-1) // 光っているボタンの番号(-1 はどれも光っていない)
let timer: number = 0

onMounted(function (): void {
  timer = window.setInterval(function (): void {
    pressedIndex.value = pickRandomIndex(colors.length)
    window.setTimeout(function (): void {
      pressedIndex.value = -1
    }, 200)
  }, 350)
})

onUnmounted(function (): void {
  window.clearInterval(timer)
})
</script>

<template>
  <div class="stage">
    <img src="../assets/images/game-base.svg" alt="ゲームのコントローラーのイラスト">
    <span
      v-for="(color, index) in colors"
      :key="color"
      class="pad-button"
      :class="[color, { pressed: index === pressedIndex }]"
    ></span>
  </div>
</template>
書き方意味
const pressedIndex = ref<number>(-1)今光っているボタンの番号を入れる箱。-1 は「どれも光っていない」
v-for="(color, index) in colors"colors のリストの数(4つ)だけ、<span> をくり返し作る。HTML にボタンを4回書かなくてよい
:key="color"くり返し作る1つ1つに付ける、見分けるための名前(v-for とセットで書くお約束)
:class="[color, { pressed: index === pressedIndex }]"色の名札(red など)を付け、さらに「自分の番号が pressedIndex と同じなら pressed の名札も付ける」

Lesson 10 では classList.add("pressed") → classList.remove("pressed") と、名札を手で付け外ししていました。 Vue 版は pressedIndex の番号を変えるだけで、どのボタンに名札を付けるかは Vue が判断してくれます。

部品4:ひとこと(components/RandomMessage.vue)

src/components/RandomMessage.vue
<script setup lang="ts">
import { ref } from 'vue'
import { pickRandomIndex } from '../utils/pickRandomIndex'

// ④ ボタンを押すと、ひとことがランダムに変わる
const messages: string[] = [
  '最近はエアロピクルスも好きだよ',
  'スーの歌も好きだよ',
  'ねずみなんだけど丑年なんでスー'
]
const message = ref<string>('下のボタンを押してみてね。')

function showMessage(): void {
  const picked = messages[pickRandomIndex(messages.length)]
  if (picked) {
    message.value = picked
  }
}
</script>

<template>
  <p id="message">{{ message }}</p>
  <button id="message-button" @click="showMessage">ひとことを見る</button>
</template>

if (picked) { … } は、Lesson 10 で出てきた「見つからないかもしれない」への備えです。 Vue のプロジェクトは TypeScript の設定が少し厳しめで、「リストから取り出したものは、ないかもしれないよ(undefined)」と教えてくれるので、 あるときだけ表示を変えるようにしています。

部品を組み立てる(App.vue)

最後に、いちばん外側の App.vue を書き換えて、部品を組み立てます。あいさつ(①)もここに書きます。

src/App.vue
<script setup lang="ts">
import ProfileCard from './components/ProfileCard.vue'
import TetsuboAnimation from './components/TetsuboAnimation.vue'
import GameController from './components/GameController.vue'
import RandomMessage from './components/RandomMessage.vue'

// ① 時間帯に合わせて、あいさつを変える
function getGreeting(hour: number): string {
  if (hour < 10) {
    return 'おはようございます!'
  } else if (hour < 18) {
    return 'こんにちは!'
  } else {
    return 'こんばんは!'
  }
}

const greeting: string = getGreeting(new Date().getHours())
</script>

<template>
  <header>
    <p id="greeting">{{ greeting }}</p>
    <h1>Mayu Takahashi Portfolio</h1>
    <p>はじめまして! 私の好きなものを紹介します。</p>
  </header>

  <main>
    <ProfileCard title="特技:鉄棒" text="子どものころから鉄棒が大好きです。">
      <TetsuboAnimation />
    </ProfileCard>

    <ProfileCard
      title="今いちばんハマっているもの:Valorant"
      text="5人対5人で戦う、チーム対戦型のシューティングゲームです。"
    >
      <GameController />
    </ProfileCard>

    <ProfileCard title="ひとこと">
      <RandomMessage />
    </ProfileCard>
  </main>

  <footer>
    <p>&copy; 2026 Mayu Takahashi</p>
  </footer>
</template>

5. 動かしてみる・公開用にまとめる

開発中:npm run dev

npm run dev を実行したままにしておくと、ファイルを保存した瞬間にブラウザの表示が自動で更新されます(いちいち F5 を押す必要がありません)。 下のように、Lesson 10 と同じ見た目・同じ動きになれば成功です。

localhost:5173(Vue.js 版)

↑ Vue.js 版の完成見本です。見た目も動きも Lesson 10 と同じですが、中身はまったく別の作りになっています。

公開するとき:npm run build

Web サイトとして公開するときは、次のコマンドで公開用のファイルにまとめます。

ターミナル
npm run build

型のチェック(Lesson 10 の tsc の役割)をしたうえで、dist フォルダに、ブラウザが読める HTML・CSS・JavaScript がまとめて作られます。 この dist フォルダの中身をサーバーに置けば公開完了です。 npm run preview を実行すると、まとめたファイルを公開前に確認できます。

このサイトの見本では、vite.config.ts に base: './' の1行を足して、どのフォルダに置いても動くようにしています。 なお、dist/index.html をダブルクリックで開いても表示されません。npm run preview や Web サーバーを通して開く必要があります。

6. As-Is / To-Be で振り返る

As-Is(Lesson 10)
portfolio/
├── index.html      ← 画面の HTML を全部書く
├── style.css
├── script.ts       ← ①〜④ の動きを全部ここに
├── script.js       ← tsc が作る
├── tsconfig.json
├── package.json
└── images/
To-Be(Lesson 12:Vue.js)
portfolio-vue/
├── index.html      ← 入り口だけ(中身は空)
├── package.json
├── vite.config.ts
└── src/
    ├── main.ts
    ├── style.css   ← そのままコピー
    ├── App.vue     ← ページ全体・① あいさつ
    ├── components/
    │   ├── ProfileCard.vue
    │   ├── TetsuboAnimation.vue  ← ②
    │   ├── GameController.vue    ← ③
    │   └── RandomMessage.vue     ← ④
    ├── utils/pickRandomIndex.ts
    └── assets/images/

1つのファイルにまとめて書いていた動きが、部品ごとのファイルに分かれました。

Lesson 10 のどこLesson 12 のどこへ変わったこと
index.html の header・main・footerApp.vue の templateHTML は App.vue と各部品の template に分かれた
index.html の section(カード3つ)ProfileCard.vue同じ枠を3回書く代わりに、1つの部品を使い回す
script.ts ① あいさつApp.vuetextContent で書き換えず、{{ greeting }} で表示
script.ts ② 鉄棒TetsuboAnimation.vueangle を変えるだけで回る(:style)
script.ts ③ コントローラーGameController.vueボタン4つは v-for で作る。光らせるのは :class
script.ts ④ ひとことRandomMessage.vue@click と {{ message }} で、探す処理が不要に
pickRandomIndex 関数utils/pickRandomIndex.ts別ファイルにして、必要な部品から import
style.csssrc/style.css変更なし。CSS の知識はそのまま使える

Claude Code にも聞いてみよう

Claude Code への質問の例
GameController.vue の :class="[color, { pressed: index === pressedIndex }]" が
どう動いているのか、1つずつ分解して初心者向けに説明してください。
Claude Code への質問の例
「好きな食べ物」のカードを追加したいです。ProfileCard を使って、
どこに何を書けばいいか教えてください。コードはまだ書き換えないでください。

確認クイズ

選択肢をクリックしてください。

Q1. Vue で、画面を分けて作る「部品」のことを何と呼ぶ?

Q2. Vue で画面の文字を変えたいとき、どうする?

Q3. リストの数だけ、同じタグをくり返し作るときに使う書き方は?

Q4. Vue の template で「クリックされたら」を書くときは?

Q5. 公開用のファイルを dist フォルダにまとめるコマンドは?

まとめ

HTML・CSS・JavaScript・TypeScript、そしてフレームワーク。Lesson 5 の「Hello World!」から、ついに現場と同じ道具立てまでたどり着きました。 ここまで来れば、実際のプロジェクトのコードを見ても「あ、これは部品だな」「ここでデータを変えているな」と読み解けるはずです。

課題:Vue.js 版のポートフォリオを提出しよう課題あり!

Lesson 10 の TypeScript 版と、このレッスンの Vue.js 版の2つのプロジェクトを、まとめて提出してください。

課題の内容

  • Lesson 9 の課題で作った自分の内容(名前・画像・メッセージなど)を、Vue.js 版にも反映する
  • ProfileCard を使って、新しいカード(例:「好きな食べ物」)を1枚以上追加する
  • npm run build が、エラーなく最後まで成功することを確認する
  • (チャレンジ)新しいカードの中身を、自分で作った部品(.vue ファイル)にしてみる

提出前のチェックリスト

  • npm run dev で開いたページが、正しく表示されて動く
  • npm run build がエラーなく成功する
  • 提出用のフォルダに portfolio(TypeScript 版)と portfolio-vue(Vue.js 版)の2つが入っている
  • 提出用のフォルダの中に node_modules が残っていない

提出のしかた

  1. 提出用のフォルダを作る

    デスクトップなどに、Lesson12_氏名 という名前のフォルダを作ります(例:Lesson12_YamadaTaro)。

  2. プロジェクトのフォルダをコピーして入れる

    Lesson 10 の portfolio フォルダと、このレッスンの portfolio-vue フォルダの2つを、提出用のフォルダにコピーします。

  3. node_modules フォルダを削除する

    コピーした 提出用のフォルダの中の portfolio と portfolio-vue から、node_modules フォルダを削除します。 node_modules はとても大きい(数万ファイルになることも)うえに、npm install でいつでも作り直せるので、提出には含めません。

    元のフォルダの node_modules は消さないでください。消してしまった場合は、そのフォルダで npm install を実行すれば元に戻ります。

  4. zip ファイルにまとめる

    提出用のフォルダを右クリック →「圧縮先」→「ZIP ファイル」を選びます(Windows 10 の場合は「送る」→「圧縮(zip 形式)フォルダー」)。 同じ場所に Lesson12_氏名.zip ができます。

  5. 共有する

    できた zip ファイルを、講師・担当者から案内された共有先にアップロードして提出します。

提出する前に、zip ファイルをダブルクリックして中身を開き、必要なファイルがそろっているか確認しておくと安心です。

提出用のフォルダの中身は、次のようになります。

Lesson12_氏名 の中身
Lesson12_氏名/
├── portfolio/          ← Lesson 10 の TypeScript 版
│   ├── index.html
│   ├── style.css
│   ├── script.ts
│   ├── script.js
│   ├── tsconfig.json
│   ├── package.json
│   └── images/
└── portfolio-vue/      ← Lesson 12 の Vue.js 版
    ├── index.html
    ├── package.json
    ├── vite.config.ts
    ├── tsconfig.json など
    └── src/
    (node_modules は入れない)