【vue.js】<script setup>の基本

# はじめに
※この記事は「[LTSグループAdvent Calendar 2023](https://qiita.com/advent-calendar/2023/ltsgroup)」に参加しています

はじめまして！[株式会社エル・ティー・エス](https://lt-s.jp/)でエンジニアとして働いているc-tyan410と申します。
Vue.jsをプロジェクトで使って、早一年が経ちました。
Vue.js 3.2からVue.jsを使い始めたので、最初から`<script setup>`構文を使っていました。
本記事では、`<script setup>`構文の書き方をまとめていきます。

# `<script setup>`構文とは
`<script setup>`は単一ファイルコンポーネント（SFC）内でCompositionAPIを使用するコンパイル時の糖衣構文です。公式もおすすめしています。

下記のようなメリットがあります。
* ボイラープレートが少なくて、より簡潔なコードになります
* propsとemitを定義する際に純粋なTypeScriptの構文が使えます
* ランタイムのパフォーマンスが向上します
* IDEのパフォーマンスが向上します

# 基本の構文
この構文を導入するには、setup属性を`<script>`ブロックに追加します。
```vuejs: app.vue
<script setup>
    console.log('Hello vue')
</script>
```

`<script setup>`構文はCompositionAPIにおけるsetup()関数内部を`<script>`直下に直接記述することができるという構文です。通常の`<script>`とは違って、コンポーネントが最初にインポートされたときに一度だけ実行されるのではなく、`<script setup>`内のコードはコンポーネントのインスタンスが作成されるたびに実行されます。

### 変数、関数宣言、インポート
`<script setup>` を使用する場合、`<script setup>`内で宣言された変数、関数宣言、インポート等はテンプレートで直接使用できます。

* Option API 
```vuejs: Option API
<script>
export default {
    data() {
        return {
        msg: 'Hello!'
        };
    },
    methods: {
        log() {
        console.log(this.msg);
        }
    }
}
</script>

<template>
    <button @click="log">{{ msg }}</button>
</template>
```

* `<script setup>`
```vuejs:script setup
<script setup>
// 変数
const msg = 'Hello!'

// 関数
const log = () => {
    console.log(msg)
}
</script>

<template>
    <button @click="log">{{ msg }}</button>
</template>
```

インポートも同様に、インポートされたヘルパー関数をmethodsオプションで公開することなくテンプレート内の式で直接使用できます。

* Option API 
```vuejs: Option API
<script>
import { capitalize } from './helpers';

export default {
    methods: {
        capitalize
    }
}
</script>

<template>
    <div>{{ capitalize('hello') }}</div>
</template>
```

* `<script setup>`
```vuejs:script setup
<script setup>
import { capitalize } from './helpers'
</script>

<template>
    <div>{{ capitalize('hello') }}</div>
</template>
```

# リアクティビティー　
リアクティブな状態はリアクティビティーAPIを使って明示的に作成する必要があります。setup()関数から返された値と同じように、テンプレート内で参照されるときに自動的にアンラップされます。

* Option API 
```vuejs: Option API
<template>
    <button @click="incrementCount"> {{ count }} </button>
    <p>{{ item.orange }}</p>
</template>

<script>
export default {
    data() {
        return {
            count: 0,
        item: {
            apple: 1,
            orange: 2,
            banana: '1本'
            }
        };
    },
    methods: {
        incrementCount() {
            this.count++;
        }
    }
};
</script>
```

* `<script setup>`
```vuejs:script setup
<script setup>
import { ref, reactive } from 'vue'

//refオブジェクト
const count = ref(0)
// 単一プロパティーである.valueを持っています
console.log(count.value)

//reactiveオブジェクト
const item = reactive({
    apple: 1,
    orange: 2,
    banana: '1本'
})
// 1
console.log(item.apple)

</script>

<template>
  <button @click="count++">{{ count }}</button>
  <p>{{ item.orange }}</p>
</template>
```

# v-bind, v-on, v-model, v-for, v-if
v-for,v-on,v-model,v-for,v-ifはtemplate内で使います。
`<script setup>`構文を使う場合も使わない場合もtemplateの内容は変わらないので、
v-for,v-on,v-model,v-for,v-ifは従来通りに使えます。

※v-onの省略記法は`@`、v-bindの省略記法は`:`です

* `<script setup>`
```vuejs:script setup
<script setup>
import { ref, reactive, computed } from 'vue'

// リアクティブなデータプロパティ
const items = reactive(['Apple', 'Banana', 'Orange']) // 表示するアイテムのリスト
const isVisible = ref(false) // アイテムリストの表示状態を制御する

// アイテムリストの表示/非表示を切り替える関数
const toggleVisibility = () => {
    isVisible.value = !isVisible.value
}
</script>

<template>
<!-- v-modelディレクティブを使用して、input要素とisVisibleリアクティブ変数を双方向バインド -->
<input v-model="isVisible" type="checkbox" />

<!-- @clickイベントリスナーでボタンクリック時の処理を定義 -->
<button @click="toggleVisibility">
    {{ isVisible ? 'Hide' : 'Show' }} Items
</button>

<!-- v-ifディレクティブを使用して、isVisibleがtrueの場合にのみ段落を表示 -->
<p v-if="isVisible">
    {{ fruits }}
</p>

<!-- v-forディレクティブでitems配列の各要素に対してli要素を生成 -->
<ul>
    <li v-for="(item, index) in items" :key="index">
        {{ item }}
    </li>
</ul>
</template>

```

# computed()
関数の中身が変化した時に、呼び出されるような関数を作りたい場合に使います。
リアクティビィーと同様に（こちらもリアクティビィーAPIですが）明示的に書く必要があります。

例：下記は2倍の数字を算出するコードです
* Option API 
```vuejs: Option API
<script>
export default {
    data() {
        return {
        counter: 0
        };
    },
    computed: {
    // computedを使用してカウンターの2倍の値を計算
        doubledCounter() {
            return this.counter * 2;
        }
    },
    methods: {
    // カウンターを増加させるメソッド
        increment() {
            this.counter++;
        }
    }
};
</script>

<template>
    <div>
        <h1>カウンター: {{ counter }}</h1>
        <h2>2倍の値: {{ doubledCounter }}</h2>
        <button @click="increment">増加</button>
    </div>
</template>
```

* `<script setup>`
```vuejs:script setup
<script setup>
import { ref, computed } from 'vue';

// refを使用してリアクティブなカウンター変数を作成
const counter = ref(0);

// computedを使用してカウンターの2倍の値を計算
const doubledCounter = computed(() => counter.value * 2);

// カウンターを増加させるメソッド
const increment = () => {
    counter.value++;
};
</script>

<template>
    <div>
        <h1>カウンター: {{ counter }}</h1>
        <h2>2倍の値: {{ doubledCounter }}</h2>
        <button @click="increment">増加</button>
    </div>
</template>
```
# watch, watchEffect
リアクティブな状態の一部が変更されるたびにコールバックを実行する関数です。
watchとwatchEffectの違いは、初期表示の際に実行されるか否かです。

watch：初期表示の際に実行されません（{ immediate: true }をつければ、初期表示でも実行されます。）
watchEffct：初期表示の際に実行されます。

例：以下のコードは、名前を変更した回数をカウントするコードです。
* Option API 
```vuejs: Option API
<script>
import { ref, watch, watchEffect } from 'vue';

export default {
    data() {
        return {
            name: '',
            nameChangeCount: 0
        };
    },
    watch: {
    // watchオプションを使用してnameの変更を監視
        name(newName, oldName) {
        console.log(`名前が ${oldName} から ${newName} に変わりました。`);
        this.nameChangeCount++;
        }
    },
    mounted() {
    // watchEffectを使用してリアクティブな状態の変更を監視
        watchEffect(() => {
            console.log(`現在の名前は ${this.name} です。`);
        });
    }
};
</script>

<template>
    <div>
        <input v-model="name" placeholder="名前を入力">
        <p>名前: {{ name }}</p>
        <p>名前の変更回数: {{ nameChangeCount }}</p>
    </div>
</template>
```

* `<script setup>`
```vuejs:script setup
<script setup>
import { ref, watch, watchEffect } from 'vue';

const name = ref('');
const nameChangeCount = ref(0);

//watchを使用してnameの変更を監視
watch(name, (newValue, oldValue) => {
    console.log(`名前が ${oldValue} から ${newValue} に変わりました。`);
    nameChangeCount.value++;
});

//watchEffectを使用してリアクティブな状態の変更を監視
watchEffect(() => {
    console.log(`現在の名前は ${name.value} です。`);
});
</script>

<template>
    <div>
        <input v-model="name" placeholder="名前を入力">
        <p>名前: {{ name }}</p>
        <p>名前の変更回数: {{ nameChangeCount }}</p>
    </div>
</template>
```
# コンポーネント
今までcomponentsにimportしたコンポーネントの一覧を登録しなければいけなかったのですが、`<script setup>`のスコープ内の値は、カスタムコンポーネントのタグ名としても直接使用できるようになっています。

* Option API 
```vuejs: Option API
<script>
import MyComponent from './MyComponent.vue';

export default {
    components: {
        MyComponent
    }
};
</script>

<template>
    <MyComponent />
</template>
```

* `<script setup>`
```vuejs:script setup
<script setup>
import ChildComponent from './ChildComponent.vue'
</script>

<template>
    <ChildComponent />
</template>
```
ケバブケースの`<my-component>`も同じようにテンプレートで動作しますが、公式は一貫性を持たせるためにパスカルケースを強く推奨しています。


# defineProps() & defineEmits()
完全な型推論のサポートつきでpropsとemitsのようなオプションを宣言するために、definePropsとdefineEmitsのAPIを使用できます。
これらは`<script setup>`の中で自動的に利用できるようになっています。
template内で、propsを使う場合は頭にpropsをつけなくても直説使うことができます。

### 親→子(defineProps())
* `<script setup>`
```vuejs:親コンポーネント
<script setup>
import ChildComponent from './ChildComponent.vue'
</script>

<template>
    <ChildComponent message="こんにちは"/>
</template>
```


```vuejs:子コンポーネント(ChildComponent.vue)
<script setup>
const props = defineProps({
    message {
        type: String,
        required: true,
    }
});

// script内で使う時には、propsを頭につける
console.log(props.message)
</script>


<template>
    <!-- template内で使う時には、propsを頭につけなくても良い -->
    <div>
        <p>受け取ったメッセージ: {{ message }}</p>
    </div>
</template>
```

### 子→親(defineEmits())
* `<script setup>`
```vuejs:子コンポーネント(ChildComponent.vue)
<script setup>
// 親コンポーネントではdefineEmitsで定義するイベントをキャメルケースで書く
const emit = defineEmits(['eventFromChild']);
// イベントを送出する関数
const sendEvent = () => {
    emit('eventFromChild', '子コンポーネントからのデータ');
};
</script>

<template>
    <button @click="sendEvent">イベントを送出</button>
</template>
```
```vuejs:親コンポーネント
<script setup>
import ChildComponent from './ChildComponent.vue'

// 子コンポーネントから受けっと値が引数で入ってくる
const handleEvent = (data) => {
    console.log(data)
}
</script>

<template>
    <ChildComponent @event-from-child="handleEvent"/>
</template>
```

* defineProps() & defineEmits()はインポートする必要はなく`<script setup>`が処理されるときにコンパイルされます。
* definePropsとdefineEmitsは、渡されたオプションに基づいて、適切な型の推論を行います。
* definePropsのバリデーション等の詳細は[こちら](https://ja.vuejs.org/guide/components/props)

# defineExpose()
`<script setup>`を使用したコンポーネントは、デフォルトで閉じられています。なので、`<script setup>`内で宣言されたバインディングを公開しません。

`<script setup>`コンポーネントのプロパティを明示的に公開するには、defineExpose コンパイラーマクロを使用します。

defineExpose()を使えば、子供のコンポーネントで宣言されたデータや関数を親コンポーネントに公開できるようになるため、子→親のデータの受け渡しなどに便利です。個人的にはdefineEmits()よりもdefineExpose()を使用することが多いです。

### 子→親(defineExpose())
* `<script setup>`
```vuejs:子コンポーネント(ChildComponent.vue)
<script setup>
import { ref } from 'vue';

const count = ref(0);
const increment = () => {
    count.value++;
};

// defineExposeを使用して、外部からアクセス可能なプロパティとメソッドを定義
defineExpose({
    count,
    increment
});
</script>
```
```vuejs:親コンポーネント
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue'

const childComponent = ref()
// valueをつけて呼び出す。
console.log(childComponent.value.count)

const incrementChild = () => {
    //子コンポーネントのincrement関数を親コンポーネントから使用する
    childComponent.value.increment()
}
</script>

<template>
    <!-- defineExposeで公開したプロパティをrefで受け取る -->
    <ChildComponent ref="childComponent"/>
    <button @click="incrementChild">子コンポーネントのカウントを増やす</button>
</template>
```

# まとめ
この記事では、Vue.jsの`<script setup>`構文について詳しく解説しました。`<script setup>`構文は、Vue.js 3.2以降で利用可能な、単一ファイルコンポーネント（SFC）内でCompositionAPIをより効率的に使用するためのシンタックスシュガーです。この構文を利用することで、ボイラープレートの削減、より簡潔なコードの記述、ランタイムとIDEのパフォーマンス向上などのメリットがあります。ぜひ使ってみてください！！


# 参考・引用

https://qiita.com/kskinaba/items/3e8887d45b11f9132012

https://ja.vuejs.org/api/sfc-script-setup

https://zenn.dev/azukiazusa/articles/676d88675e4e74