ポートフォリオを
Vue.js にしよう
Lesson 10 で TypeScript にしたポートフォリオを、Lesson 11 で学んだフレームワークの1つ、 Vue.js(ビュー・ジェイエス)で作り直します。 最後には、現場と同じ形のプロジェクトで動かすところまで進みます。
1. Vue.js の考え方
考え方①:データを変えると、画面が勝手に変わる
Lesson 9・10 では、画面を変えたいとき「HTML の中から探して(getElementById)、書き換える(textContent)」という2段階の作業をしていました。 Vue では、データ(箱の中身)を変えるだけで、そのデータを表示している画面が自動で書き換わります。これをリアクティブと呼びます。
const message = document.getElementById("message") as HTMLElement;
// 画面のどこを変えるか探してから、文字を書き換える
message.textContent = "スーの歌も好きだよ";const message = ref<string>("下のボタンを押してみてね。");
// データを変えるだけ。画面は Vue が書き換えてくれる
message.value = "スーの歌も好きだよ";
<!-- 画面(HTML)側には「ここに message を表示」と書いておく -->
<p>{{ message }}</p>たとえるなら、Lesson 10 は「掲示板の前まで歩いて行って、紙を貼り替える」やり方。Vue は「デジタルサイネージ」で、元のデータを変えれば、表示はすべて自動で切り替わります。
考え方②:画面を部品(コンポーネント)に分けて作る
Vue では、画面をコンポーネントという部品に分けて作ります。1つの部品は、1つの .vue ファイルに、
その部品の動き(script)と見た目(template)をセットで書きます。今回のポートフォリオは、こんな部品に分けます。
(あいさつ・カードを並べる)
(3回使い回す)
2. Vue のプロジェクトを作る
Vue のアプリは、Vite(ヴィート)という開発用ツールと組み合わせて作るのが標準です。Vite は、書いたコードをすぐにブラウザで確認できるようにしたり、 公開用のファイルにまとめたりしてくれます。準備には Lesson 10 で入れた Node.js を使います。
-
作業する場所でターミナルを開く
VS Code の「ファイル」→「フォルダーを開く」で、
portfolioフォルダがある場所(デスクトップなど)を開き、 「ターミナル」→「新しいターミナル」を選びます。新しいプロジェクトは、portfolioとは別のフォルダとして作ります。 -
プロジェクトを作るコマンドを実行する
ターミナルnpm create vue@latest「Need to install the following packages … Ok to proceed?」と聞かれたら、y を押して Enter。
-
質問に答える
いくつか英語で質問されるので、次のように答えます。矢印キーで選び、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 質問の内容や順番は、ツールのバージョンによって少し変わることがあります。
-
必要なツールをインストールして、起動する
画面に表示されるとおり、次の3つを順番に実行します。
ターミナルcd portfolio-vue npm install npm run devhttp://localhost:5173/のようなアドレスが表示されたら、Ctrl を押しながらクリックします。 ブラウザに「You did it!」と表示されれば、Vue のプロジェクトが動いています。 -
VS Code で portfolio-vue フォルダを開き直す
「ファイル」→「フォルダーを開く」で
portfolio-vueを開きます。右下に「推奨の拡張機能をインストールしますか?」と出たら 「インストール」を押してください(Vue (Official) という拡張機能で、.vueファイルを色分けして、間違いを教えてくれます)。
npm run dev を実行している間だけ、自分のパソコンの中に小さな Web サーバーが立ち上がり、
そこにブラウザでアクセスしています(Lesson 2 のクライアントとサーバーが、1台の中にいるイメージ)。
ターミナルで Ctrl + C を押すと止まります。
作られたプロジェクトのうち、今回さわるのは次のファイルです。
portfolio-vue/
├── index.html ← ページの入り口(ほぼそのまま)
├── package.json ← npm の設定(dev・build などのコマンド)
├── vite.config.ts ← Vite の設定
├── tsconfig.json など ← TypeScript の設定(さわらない)
├── node_modules/ ← ダウンロードしたツール(さわらない)
└── src/
├── main.ts ← アプリを起動するファイル
└── App.vue ← いちばん外側の部品(ページ全体)3. これまでのファイルを引っ越す
-
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 -
main.ts で CSS を読み込む
Lesson 8 では HTML の
<link>で CSS を読み込みましたが、Vue のプロジェクトではmain.tsに1行書いて読み込みます。src/main.tsimport { createApp } from 'vue' import App from './App.vue' import './style.css' createApp(App).mount('#app')createApp(App).mount('#app')は「App という部品を、index.html のid="app"の場所に表示してね」という意味です。 -
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 して使えるようになります。
// 0〜(length - 1) のどれかの番号を、ランダムに選ぶ
export function pickRandomIndex(length: number): number {
return Math.floor(Math.random() * length)
}Vue のプロジェクトでは、行の最後の ; を省略する書き方が一般的です(付けても動きます)。
部品1:カードの枠(components/ProfileCard.vue)
3つのカードに共通の「枠」を、使い回せる部品にします。
<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)
<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 が変わると、自動で回転も変わる |
person.style.transform = … と、画面を直接書き換えていました。Vue 版のタイマーは angle.value を5増やしているだけ。
画面の書き換えは Vue が引き受けてくれています。これが「1章の考え方①」のリアクティブです。
なお template の中では .value を付けずに angle と書けます。
部品3:コントローラー(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)
<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 を書き換えて、部品を組み立てます。あいさつ(①)もここに書きます。
<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>© 2026 Mayu Takahashi</p>
</footer>
</template>import ProfileCard from './components/ProfileCard.vue':作った部品を読み込む<ProfileCard title="…" text="…">:読み込んだ部品は、自分で作ったタグのように使える。title・textが props(注文票)<TetsuboAnimation />:中身のない部品は、/>で閉じて1行で書ける。カードの<slot>の場所に入る{{ greeting }}:あいさつの文字を表示する。HTML を探して書き換える必要はもうありません
5. 動かしてみる・公開用にまとめる
開発中:npm run dev
npm run dev を実行したままにしておくと、ファイルを保存した瞬間にブラウザの表示が自動で更新されます(いちいち F5 を押す必要がありません)。
下のように、Lesson 10 と同じ見た目・同じ動きになれば成功です。
↑ 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 で振り返る
portfolio/
├── index.html ← 画面の HTML を全部書く
├── style.css
├── script.ts ← ①〜④ の動きを全部ここに
├── script.js ← tsc が作る
├── tsconfig.json
├── package.json
└── images/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・footer | App.vue の template | HTML は App.vue と各部品の template に分かれた |
| index.html の section(カード3つ) | ProfileCard.vue | 同じ枠を3回書く代わりに、1つの部品を使い回す |
| script.ts ① あいさつ | App.vue | textContent で書き換えず、{{ greeting }} で表示 |
| script.ts ② 鉄棒 | TetsuboAnimation.vue | angle を変えるだけで回る(:style) |
| script.ts ③ コントローラー | GameController.vue | ボタン4つは v-for で作る。光らせるのは :class |
| script.ts ④ ひとこと | RandomMessage.vue | @click と {{ message }} で、探す処理が不要に |
| pickRandomIndex 関数 | utils/pickRandomIndex.ts | 別ファイルにして、必要な部品から import |
| style.css | src/style.css | 変更なし。CSS の知識はそのまま使える |
Claude Code にも聞いてみよう
GameController.vue の :class="[color, { pressed: index === pressedIndex }]" が
どう動いているのか、1つずつ分解して初心者向けに説明してください。「好きな食べ物」のカードを追加したいです。ProfileCard を使って、
どこに何を書けばいいか教えてください。コードはまだ書き換えないでください。確認クイズ
選択肢をクリックしてください。
Q1. Vue で、画面を分けて作る「部品」のことを何と呼ぶ?
Q2. Vue で画面の文字を変えたいとき、どうする?
Q3. リストの数だけ、同じタグをくり返し作るときに使う書き方は?
Q4. Vue の template で「クリックされたら」を書くときは?
Q5. 公開用のファイルを dist フォルダにまとめるコマンドは?
まとめ
- Vue.js はリアクティブ:
refの中身(データ)を変えるだけで、画面が自動で書き換わる。 - 画面はコンポーネント(
.vueファイル)に分けて作る。1つのファイルに script(動き)と template(見た目)をセットで書く。 - template では
{{ }}(表示)・@click(クリック)・:style/:class(スタイル・名札)・v-for(くり返し)・v-if(条件)を使う。 - 部品には props(注文票)でデータを渡し、slot で中身を差し込める。
- プロジェクトは
npm create vue@latestで作り、開発中はnpm run dev、公開するときはnpm run build。
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が残っていない
提出のしかた
-
提出用のフォルダを作る
デスクトップなどに、
Lesson12_氏名という名前のフォルダを作ります(例:Lesson12_YamadaTaro)。 -
プロジェクトのフォルダをコピーして入れる
Lesson 10 の
portfolioフォルダと、このレッスンのportfolio-vueフォルダの2つを、提出用のフォルダにコピーします。 -
node_modules フォルダを削除する
コピーした 提出用のフォルダの中の
portfolioとportfolio-vueから、node_modulesフォルダを削除します。node_modulesはとても大きい(数万ファイルになることも)うえに、npm installでいつでも作り直せるので、提出には含めません。元のフォルダの
node_modulesは消さないでください。消してしまった場合は、そのフォルダでnpm installを実行すれば元に戻ります。 -
zip ファイルにまとめる
提出用のフォルダを右クリック →「圧縮先」→「ZIP ファイル」を選びます(Windows 10 の場合は「送る」→「圧縮(zip 形式)フォルダー」)。 同じ場所に
Lesson12_氏名.zipができます。 -
共有する
できた zip ファイルを、講師・担当者から案内された共有先にアップロードして提出します。
提出する前に、zip ファイルをダブルクリックして中身を開き、必要なファイルがそろっているか確認しておくと安心です。
提出用のフォルダの中身は、次のようになります。
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 は入れない)