公式のチュートリアルをやっていきます
https://ja.vuejs.org/guide/introduction.html
1. 書き方の話
書き方にいくつか種類があるようです
1-1. Options APIとComposition API
Options APIという書き方では、classを作って値をdata、処理をmethodとして書くようです
<script>
export default {
// data() で返すプロパティはリアクティブな状態になり、
// `this` 経由でアクセスすることができます。
data() {
return {
count: 0
}
},
// メソッドの中身は、状態を変化させ、更新をトリガーさせる関数です。
// 各メソッドは、テンプレート内のイベントハンドラーにバインドすることができます。
methods: {
increment() {
this.count++
}
},
// ライフサイクルフックは、コンポーネントのライフサイクルの
// 特定のステージで呼び出されます。
// 以下の関数は、コンポーネントが「マウント」されたときに呼び出されます。
mounted() {
console.log(`The initial count is ${this.count}.`)
}
}
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
これだとリアクティブな値とViewと分離ができない、共通処理を各コンポーネントに重複記述しなければならなくなる、などの問題があったので。それを解消する為に分けて書けるようにVue3から登場したのがComposition APIだそうです
https://ja.vuejs.org/guide/introduction.html#api-styles
<script setup>
import { ref, onMounted } from 'vue'
// リアクティブな状態
const count = ref(0)
// 状態を変更し、更新をトリガーする関数。
function increment() {
count.value++
}
// ライフサイクルフック
onMounted(() => {
console.log(`The initial count is ${count.value}.`)
})
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
上記のように書ければ、classのメソッドとして書いてあるのと違って処理だけどこかに書いてあるものを使うような書き方が出来ます。ではすべてComposition APIで書くべきなのかというとそうではなくて、Options APIをなくす予定はないし、小~中規模の開発ではOptions APIの方が適しているという認識なので、好きな方で書けば良いようです
1-2. SFC (Single File Component)
ボタンなどのコンポーネントの情報すべてを1つのファイルにまとめて書くSFCという書き方ができるようになっています
https://ja.vuejs.org/guide/scaling-up/sfc.html
<script setup>
defineProps({
msg: {
type: String,
required: true
}
})
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
You’ve successfully created a project with
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>
使うときはタグのようにして呼んでこれます
<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<HelloWorld msg="You did it!" />
</template>
npm init vueで生成されるサンプルプロジェクトもSFCを使って書かれていますので推奨の書き方であるようです。初見のプロジェクトでも見やすい良い書き方だと思います
2. ビルド
この辺の話です
https://ja.vuejs.org/guide/quick-start.html
2-1. ビルドなしで書く
既存のプロジェクトを部分的に置き換えるような場合、本当に小規模にちょこっとだけVueを使う場合は、CDNからVueを読んできて使う方が手軽だと思います
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
2-2. ビルドする為の環境を作って書く
SPA作る場合や新規プロジェクトを開発する場合はこっちが良いと思います
2-2-1. Node.jsのインストール
Node.js環境が要るようなので、まだ入ってない人は公式ページからダウンロードしてきてインストールしてください
https://nodejs.org/ja/download
入っていればnode -vでバージョン情報が出る筈です
> node -v
v18.16.1
2-2-2. プロジェクトを作る
適当なフォルダを作ったらそこに移動してnpm init vueすると環境を作ってくれます
> mkdir hoge
> cd hoge
> npm init vue@latest
Vue.js - The Progressive JavaScript Framework
√ Project name: ... fuga
√ Add TypeScript? ... No / Yes
√ Add JSX Support? ... No / Yes
√ Add Vue Router for Single Page Application development? ... No / Yes
√ Add Pinia for state management? ... No / Yes
√ Add Vitest for Unit Testing? ... No / Yes
√ Add an End-to-End Testing Solution? » No
√ Add ESLint for code quality? ... No / Yes
Scaffolding project in C:\Users\jjaka\マイドライブ\html_javascript\20230623_EXISS_UI\vue_tutorial\3\exiss_01...
Done. Now run:
cd fuga
npm install
npm run dev
要求されたプロジェクト名と同名のフォルダ (上記だとfuga) が自動で作成されて、その中にVue.jsプロジェクトに必要なファイル一式が生成されます
上手く出来たら指示通りにcd fugaしてnpm installすると必要なパッケージがインストールされて、npm run devで開発用サーバーがlocal hostに待機します
local hostを開くとこんな感じで今どきのウェブフレームワークっぽい画面が見られます
これで一式揃ったので、後は書き換えたり書き足したりしていくだけです
2-2-3. フォルダ構成
npm init vueで以下のようなファイルが書き出されます。現代的なウェブフレームワークらしく、要素ごとにフォルダ分けされていて分かりやすいです
index.htmlを開くとid="app"のdivタグとsrc/main.jsを読み込むscriptタグが書かれていて、ここをvueで書き換えてやるんだなと分かります
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
main.jsでは/src/App.vueを読み込んで#appを対象にcreateApp()しています。createApp()は新しいVueインスタンスを作るメソッドですので、ここで先程のid="app"であるdiv要素の場所にVueインスタンスを置くという指定がされているわけですね
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
/src/App.vueはSFCで書かれていて、script, template, styleを順に記載されています。vueファイルの中で別のvueファイルを参照することができるので、要素ごとにファイルを分けて書くことで使いまわしがやりやすくなっててめちゃめちゃ良いです
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import TheWelcome from './components/TheWelcome.vue'
</script>
<template>
<header>
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />
<div class="wrapper">
<HelloWorld msg="You did it!" />
</div>
</header>
<main>
<TheWelcome />
</main>
</template>
<style scoped>
header {
line-height: 1.5;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
}
</style>
SFCで書く以外に、render関数で書くやり方も出来るようです
https://ja.vuejs.org/guide/extras/render-function.html
3. SFCの書き方
なんだかいろいろ書き方があるようなんですが、ここではscript setupで書いていきたいと思います
https://ja.vuejs.org/api/sfc-script-setup.html#typescript-%E3%81%AE%E3%81%BF%E3%81%AE%E6%A9%9F%E8%83%BD
3-1. templateのみ
/src/App.vueを書き換えてみましょう。とりあえずtemplateだけ書いてあれば表示はできます
<template>
<div id="fuga">
hoge
</div>
</template>
3-2. リアクティブな値を扱う
リアクティブというのは値を監視していて変更があったら検知される状態の事だそうです。ref()に普通のプリミティブな値を渡すとリスポンシブな値のインスタンスを作ってくれます。それをtemplate内で表示しておくと値が更新された時に表示も更新してくれます
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => {
count.value++;
}
</script>
<template>
<div>
<div>
COUNT: {{ count }}
</div>
<button @click="increment">ボタン</button>
</div>
</template>
<style scoped>
div {
font-size: 1.2rem;
text-align: center;
}
</style>
ボタンを押した回数を表示するコンポーネントが表示される筈です
3-3. 子コンポーネントに値を渡す
親コンポーネントから子コンポーネントに値を渡すことができます
3-3-1. 親コンポーネント側
以下でv-bind:に続けて書いているinitialCount="7"の部分が子コンポーネントに渡す引数になります。数値を渡す場合も文字列として書くことになりますが、受け取る側で指定すれば数値やBooleanとして受け取ることも出来ます「v-bind:initialCount="7"」を省略して「:initialCount="7"」と書く事もできます。また、変数名を省略して「v-bind:="7"」や「:="7"」と書くとデフォルト変数名であるmodelValueとして渡します
<script setup>
import Count from './components/Count.vue'
</script>
<template>
<main>
<div>
<Count v-bind:initialCount="7"/>
</div>
</main>
</template>
3-3-2. 子コンポーネント側
子コンポーネント側ではdefineProps()によりどんな値を親コンポーネントから受け取るか指定します。以下の場合はinitialCountという引数をNumberとして受け取って初期値として使っています。親コンポーネント側はと文字列で渡していますが、受け取り側で数値として解釈しているわけです
<template>
<p>
<div>
COUNT: {{ count }}
</div>
<button @click="increment">クリック回数をカウントします</button>
</p>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
initialCount: Number,
})
const count = ref(props.initialCount);
const increment = () => {
count.value++;
}
</script>
<style scoped>
p {
font-size: 1.1rem;
text-align: center;
}
</style>
3-4-3. 単方向データバインディングの挙動
親コンポーネント側でリアクティブな変数になっている変数をv-bindで渡すと、親コンポーネント側で変数の値が変わった時に子コンポーネント側の値も変わる単方向データバインディングになります
例えば以下のようにすると親の値を子が表示するようになります
<script setup>
import { ref } from 'vue';
import CountChild from './CountChild.vue'
const count = ref(0);
const increment = () => {
count.value++;
}
</script>
<template>
<p>
<div>
CountParent: COUNT: {{ count }}
</div>
<CountChild :count="count" />
<button @click="increment">クリック回数をカウントします</button>
</p>
</template>
<style scoped>
p {
font-size: 1.1rem;
text-align: center;
}
</style>
<script setup>
const props = defineProps({
count: Number,
})
</script>
<template>
<div>
CountChild: COUNT: {{ count }}
</div>
</template>
CountChildの側が子コンポーネントですが、親の値を変えるだけで子の側も同じ値が表示されます
では逆に子側を書き換えるとどうなるのかというとそっちは出来ません。v-bindは書くと親から子への単方向でのみ伝えるようになるので、子側を変更しようとしてもエラー停止するようになって単方向データバインディングが強制されます
3-5. 双方向データバインディングする
v-bindで渡すと親コンポーネントから子コンポーネントに値を伝える単方向データバインディングになりましたが、v-modelを使うと逆に子コンポーネントの値の変更を親コンポーネント側に反映する双方向データバインディングも出来ます
親から子に伝えるpropsを定義するにはdefinePropsを使いましたが、子から親に伝えるemitsを定義するにはdefineEmitsを使います。emitsを使ってボタン押下などのイベントを親コンポーネント側に伝えることも出来ますので親コンポーネント側に何か伝えたい時にemitsを使えば良いという理解で良さげです
3-5-1. 親コンポーネント側
親コンポーネント側は先程v-bindで書いていた部分をv-modelに変更するだけです。以下の場合「v-model="text"」と引数名を省略しているので、v-model:modelValue="text"を渡しているのと同様になります
<template>
<p>
<p>parent</p>
<input v-model="text" />
<!-- 子コンポーネント -->
<Child v-model="text" />
</p>
</template>
<script setup lang="ts">
import { ref } from "vue";
import Child from "./Child.vue";
// 双方向バインディングする変数
const text = ref('');
</script>
3-5-2. 子コンポーネント側
受け取る子コンポーネント側には受け取り設定をdefinePropsに書き、戻し側の設定をdefineEmitsに書き、値の変更イベントがあったときの処理を書き、と3つ書く必要があります
<template>
<p>child</p>
<input v-model="text" />
</template>
<script setup lang="ts">
import { computed, defineProps, withDefaults, defineEmits } from "vue";
const props = withDefaults(
defineProps<{
modelValue: string;
}>(),
{
modelValue: '' // デフォルト値を指定
}
);
const emit = defineEmits<{
(e: 'update:modelValue', text: string): void
}>();
const text = computed({
get: () => props.modelValue,
set: (value) => { // 値に変更があると呼ばれるsetter
emit('update:modelValue', value);
},
});
</script>
これで、どちらの値を変更しても両方に反映されるようになりました
3-6. 双方向データバインディングその2
computed()した変数はリアクティブになっているのでボタン操作などから呼び出すこともできます
3-6-1. 親コンポーネント側
ボタンを押した回数をcountとして、これを子コンポーネントに渡しています
<template>
<p>
<div>
Parent COUNT: {{ count }}
</div>
<button @click="increment">クリック回数をカウントします</button>
<!-- 子コンポーネント -->
<Child2 v-model="count" />
</p>
</template>
<script setup lang="ts">
import { ref } from "vue";
import Child2 from "./Child2.vue";
// 双方向バインディングする変数
const count = ref(0);
const increment = () => {
count.value++;
}
</script>
3-6-2. 子コンポーネント側
双方向データバインディングの処理は同様のままで、computed()したcountを変更するだけで双方向データバインディングになります
<template>
<div>
Child COUNT: {{ count }}
</div>
<button @click="increment">クリック回数をカウントします</button>
</template>
<script setup lang="ts">
import { computed, defineProps, withDefaults, defineEmits } from "vue";
const props = withDefaults(
defineProps<{
modelValue: number;
}>(),
{
modelValue: 0 // デフォルト値を指定
}
);
const emit = defineEmits<{
(e: 'update:modelValue', text: number): void
}>();
const count = computed({
get: () => props.modelValue,
set: (value) => { // 値に変更があると呼ばれるsetter
emit('update:modelValue', value);
},
});
const increment = () => {
count.value++;
}
</script>
3-7. 子コンポーネントのイベントを親に伝える
ボタンクリックなどのイベントを親コンポーネントに伝えるだけならもっと簡単に書くことができます
3-7-1. 子コンポーネント
以下のようにするとbuttonクリックでclickIncrement()が呼ばれ、emits('emitIncrement')が実行されるようになります
<script setup lang="ts">
const emits = defineEmits(['emitIncrement'])
const clickIncrement = () => emits('emitIncrement')
</script>
<template>
<div>
<button @click="clickIncrement">子コンポーネントのボタンです</button>
</div>
</template>
3-7-2. 親コンポーネント側
とすることで、子コンポーネントのemits('emitIncrement')が来たらincrement()を実行するように出来ます
<template>
<p>
<div>
Parent COUNT: {{ count }}
</div>
<div>
<button @click="increment">親コンポーネントのボタンです</button>
</div>
<!-- 子コンポーネント -->
<Child @emitIncrement="increment"/>
</p>
</template>
<script setup lang="ts">
import { ref } from "vue";
import Child from "./Child.vue";
// 双方向バインディングする変数
const count = ref(0);
const increment = () => {
count.value++;
}
</script>
子コンポーネントのボタンで親コンポーネントの関数を動かせるようになりました
なお、ここでは書き方の確認の為に関数名とemits名にすべて違うものを使いましたが、全部同じでも問題ないので通常は揃えてしまった方が良いと思います
3-8. コンポーネントの表示非表示
v-ifやv-showに条件を渡すと条件を満たしたときだけ表示してくれます。実現する表示は同じですが、v-showがすべて書いておいてCSSで表示/非表示を切り替えるのに対してv-ifでは逐次書き換えていくので、切り替え頻度が高い場合はv-show、頻度が低い場合はv-ifが適するようです
3-8-1. v-showを使う
<h1 v-show="count>=3">3回以上クリックしました</h1>
3-8-2. v-ifを使う
連ねて書くことで3つ以上の表示選択肢をつくる事もできます
<h1 v-if="count>3">3回より多くクリックしました</h1>
<h1 v-else-if="count==3">3回クリックしました</h1>
<h1 v-else>3回クリックしてください</h1>
3-9. リストレンダリングする
v-forを使えば配列に収めた条件の数だけ要素を書いていくことが出来ます
<script setup>
const items = [
{ message: 'Foo' },
{ message: 'Bar' }
]
</script>
<template>
<ul id="example-1">
<li v-for="item in items" :key="item.message">
{{ item.message }}
</li>
</ul>
</template>
4. 雑感
emitsの書き方にかなり戸惑いましたが、SFCによりコンポーネント単位で整理できるのがすごく良いと思います。双方向データバインディングがキツイという話も聞いていたんですが、意識的に書かなければ双方向にならないので、思ったよりReactとの差は感じませんでした。仕事で書く予定なので印象が変わる可能性はありますがまずは入門できてよかったです
参考にしたサイト
https://tomosta.jp/2022/06/vue3-begin/
https://www.tohoho-web.com/ex/vuejs.html
https://qiita.com/Yametaro/items/5905f020acaf1f748d07
https://qiita.com/yasushi-jp/items/b5226a5379b3539da81b
https://qiita.com/karamage/items/7721c8cac149d60d4c4a
https://qiita.com/tsukasaI/items/334beb6d2443df3b42bd
https://zenn.dev/gamin/articles/47b93300f7d3ef
https://zenn.dev/rio_dev/articles/bc9dd92bfebe95
https://tekrog.com/v-model-in-vue3#v-model
https://zenn.dev/itouuuuuuuuu/articles/82071a897ac5c3