Composition API(Vue.js その3)

Vueコンポーネントは「 OptionsAPI」と「CompositionAPI」の2つの異なるAPIスタイルで作成できます
「OptionsAPI」は「Vue 2」と同じです(data, computed, methods, watchに分割して作成します)
「CompositionAPI」ではsetupオプション内にまとめてに定義します

英語版ですが、左サイドバーでOptionsAPIとCompositionAPIを切り替えることができます

HTMLベースのサイトにVue.jsを追加して利用(*ESモジュールを利用します)

<div id="container-1"></div>
<div id="container-2"></div>
<!-- type="module"が必要 -->
<script type="module">
//@nextは本番ではバージョン固定を推奨
import { createApp } from "https://unpkg.com/vue@next/dist/vue.esm-browser.prod.js";
const app1 = createApp({
  /* ... */
})
app1.mount('#container-1')

const app2 = createApp({
  /* ... */
})
app2.mount('#container-2')
</script>
目次
  1. Composition API
    1. setupオプション
    2. setupの引数
  2. Teleport
  3. 開発環境(Vite)について
  4. <script setup>
    1. クラスとスタイルのバインディングの実装例
  5. ProvideとInject
  6. 関数を切り出す(合成関数)
  7. nextTick
  8. setup関数内でvue-router

Composition API

setupオプション

「setupオプション」は「propsとcontext」を引数に取り、テンプレートで使う変数や関数を返す関数です
「creatded」より先に実行されます

setup関数でテンプレートで使う変数や関数returnで返す必要があります
setup関数内ではインスタンスを参照しないため、thisは使えません(アロー関数が使いやすい)

setup(props) {
    console.log(props) //Proxy
    console.log(this) // undefined
 }

reactiveとref

setup関数内で、リアクティブな変数の定義にはreactive()ref()を利用します
*プリミティブな値ではref()を利用し、それ以外はreactive()を利用するのが一般的なようです^^;

reactive()関数は、オブジェクトからリアクティブな状態を作るために利用します(プリミティブ型は保持できません)
オブジェクトのリアクティブなコピー(オブジェクトのプロキシ)を返します
*分割代入 const { count } = obj するとリアクティブ性が失われます
toRefs(オブジェクトに含まれるプロパティのrefを生成します)を使用します
const { count } = toRefs(obj)

//importが必要です
import { reactive, toRefs } from 'vue'

setup() {
 const obj = reactive({ count: 0 })
 console.log(obj.count) // 0
 return {
  //obj  //テンプレート内はobj.countで値にアクセス        
  //...obj リアクティブにならない
  //toRefsを利用するとリアクティブです
  ...toRefs(obj) //テンプレート内はcountで値にアクセス    
   }
}

ref関数は、値へのリアクティブな参照を作成します
任意の値への「参照」を作成し、リアクティブを失うことなく渡すことができます
ref関数の戻り値はvalueプロパティを持つrefオブジェクトです
script内で値へのアクセスは.value必要です
テンプレートでは.value不要です(入れ子になったrefは .value が必要です)

import { ref } from 'vue'

setup() {
 const count = ref(0)
 //valueプロパティを持つオブジェクト
 console.log(count) // RefImpl
 console.log(count.value) // 0
 return {
  count: count
 }
}

余談:ref(DOMを操作をする)

<div id="container-1">
 <button @click="handleClick">クリック</button>
 <p ref="text">HELLO</p>
</div>

<script type="module">
import { createApp, ref } from "https://unpkg.com/vue@next/dist/vue.esm-browser.prod.js";
const app1 = createApp({
 setup() {
   const text= ref(null)
   const handleClick = () => {
      text.value.style.color = 'red'
      text.value.textContent = 'Good bye'
    }
    return { text, handleClick }
 }
})
app1.mount('#container-1')
</script>

HELLO

setup関数内でcomputed
computedは、読み取り専用のリアクティブな参照を返します
computed(コールバック) *returnが必要です
新しく作成され算出された変数の値にアクセスするためには、refと同様に .value プロパティを使う必要があります

import { ref, computed } from 'vue'
setup() {
 setup() {
  const count = 1
  const plusOne = computed(() => count + 1)
// スクリプト内は.valueが必要
  console.log(plusOne.value) // 2
  return {plusOne}
  }
 }

setup関数内で、watchを使う
状態の変化に応じて「副作用」を実行する場合(DOMを変更したり、非同期操作の結果に基づいて別の状態を変更したり)
watch(監視対象, 変更時に実行するコールバック)

import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newValue, oldValue) => {
console.log(count.value)
})

*注意:リアクティブオブジェクトのプロパティは監視できません

const obj = reactive({ count: 0 })
// obj.countは監視できません
watch(obj.count, (count) => {
  console.log(count)
})

setup関数内で、watchEffectを使う
watchEffectは監視対象を宣言しないので、簡略して書けます
watchは監視対象が変更されるまで、コールバックは呼び出されませんが
watchEffectはコールバック関数内で参照している変数が変化するたびに実行されます
watchEffect( 変更時に実行するコールバック)

import { ref, watchEffect } from 'vue'
setup() {
 const count = ref(0)
 watchEffect(() => {
      console.log(count.value)
  })
}

watchEffectのflushオプション
コンポーネントの変更前 にコールバック関数が実行されます
(デフォルトはflush: 'pre'
コンポーネントの更新後にウォッチャの作用を再実行する必要がある場合(例: テンプレート参照を使っている場合など)、追加でflush: 'post'を渡すことができます

// コンポーネントが更新された後に発火、更新されたDOMにアクセスできる
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'post'
  }
)

setup関数内でライスサイクルフックにアクセス(setup内の処理を遅らせて実行したいとき)
ライフサイクルフックの前に「on」をつけます
*「beforeCreate」と「created」はsetupはsetup関数内で実行されるため、フックは不要です

import { onMounted } from 'vue'
setup() {
 // mounted
 onMounted(() => {
      console.log('mounted')
  })
}

とりあえず「reactive・ref・computed・watchEffect」をつかってみる

<div id="container-1" style="padding:16px 16px 0; border:solid 1px #ccc">
   <h1>好きな果物ランキングTOP10</h1>
  <div v-show="!isActive">
    <input type="text" v-model="search.num" placeholder="半角1~10を入力">
    <p>{{announcement}}</p>
  </div>
  <button v-show="!isActive" @click="handleClick">一覧を表示する</button>
 <ul v-show="isActive">
    <li v-for="(list,index) in lists" :key="list">{{index + 1}}位:{{ list }}</li>
  </ul>
</div>

<script type="module">
import { createApp, computed, reactive, ref, watchEffect } from "https://unpkg.com/vue@next/dist/vue.esm-browser.prod.js";
const app1 = createApp({
setup() {
  let name = ref('')
 const isActive = ref(false);
  const search = reactive({num : null})
  const lists = ref(['いちご', 'マスカット', '桃', 'ぶどう', 'メロン','梨', 'みかん', 'マンゴー', 'バナナ', 'さくらんぼ'])
//computed:数字が入力された時に出力する
const announcement = computed(() => {
     if(search.num!==null && name.value!==undefined){return  `${search.num}位は${name.value}です`}     
    })
// watchEffect:入力される数字を監視して対象の果物をセットする
 watchEffect(() => {
      name.value = lists.value[search.num - 1]
    })
//一覧リスト表示の切り替え
 const handleClick = () => {
      isActive.value = true
  }
//テンプレートで使う変数、関数をreturnする
 return { lists, search, name, isActive, handleClick, announcement }
 }
})
app1.mount('#container-1')
</script>

好きな果物ランキングTOP10

位は?

{{announcement}}

   
  • {{index + 1}}位:{{ list }}

setupの引数

setup関数は2つの引数を取ります「setup(props, context)
props:setup関数内のpropsはリアクティブです(新しいpropsが渡されたら更新されます)
*分割するリアクティブで無くなるのでprops.xxxでアクセスします
*もしpropsを分割代入する場合は、setup関数内でtoRefsを、省略可能なプロパティの場合はtoRefを使う必要があります
context:contextはそのまま分割代入が利用できます
setup(props, { emit, attrs, slots })
propsが不要の場合setup(_, context)

//親から渡ってきたprops
props: {
  title: String
},
setup(props, context) {
    // refsのオブジェクトに変換
    const { title } = toRefs(props)
    // プロパティをrefに変換
    const title = toRef(props, 'title')
    //値の参照
    console.log(title.value)

 //emitを使う場合
  const emitTest = () => {
    context.emit('custom-event', '子の値')
   }
}

「propsとcontext」を使い、コンポーネント間でデータの受け渡しをします

<div id="container-1">
    <hello-component 
      v-bind:title="parentTitle"
      @parent-event="parentMethod"></hello-component>
</div>

<script type="module">
import { createApp, ref, computed } from "https://unpkg.com/vue@next/dist/vue.esm-browser.prod.js";
//コンポーネント
const helloComponent = {
    template: `<p>{{ title }}</p><p>{{ childTitle }}</p>
    <button @click="childEmit">子の値に変更<button>`,
    props: ['title'],
    setup(props, context) {
    //props
    const childTitle = computed(() => {
       return `${props.title}:from child`
    })
   //emitでイベントを登録
   const childEmit = () => {
    context.emit('parent-event', '子の値でーす!!')
   }
    return { childTitle, childEmit }
  }
}
//親のスコープ
const app1 = createApp({
 setup() {
   const parentTitle = ref('親のタイトル')
//カスタムイベントで親の値を子の値に変更
   const parentMethod = (e)=> {parentTitle.value = e}
   return { parentTitle, parentMethod }
 },
 components: {
    'hello-component': helloComponent
  }
})
app1.mount('#container-1')
</script>

Teleport

仮想DOM上の親子関係を飛び越えてることができます(どの親の下でレンダリングするかを制御します)
(例:モーダルのコンテンツをbodyタグの子としてレンダリングされるなど)
position: absolute は、親を参照するので、<teleport> を使って 「bodyタグ」に 「Teleport 」できます^^;

<div id="container">
  <button v-if="!modalOpen" @click="modalOpen = true">Open</button>
 <!-- to="HTMLElementまたは有効なクエリセレクター"-->
    <teleport to="body">
      <div v-if="modalOpen" class="modal">
        <div>
          <button @click="modalOpen = false">Close</button>
        </div>
      </div>
    </teleport>
</div>

<script type="module">
import { createApp, ref} from "https://unpkg.com/vue@next/dist/vue.esm-browser.prod.js";
const app = createApp({
   setup() {
    const modalOpen = ref(false);
    return { modalOpen };
  },
})
app.mount('#container')
</script>
<style>
.modal {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  background-color: rgba(0,0,0,.5);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}
.modal div {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: white;
  width: 150px;
  height: 150px;
  padding: 5px;
}
</style>

開発環境(Vite)について

ページ全体に単一のVueアプリケーションインスタンスをマウントする場合

Vueのプロジェクトを作成するにはVue CLIをグローバルインストールして、vueコマンドを使用することが一般的でしたが、「Vite(*Vue CLIのインストール不要)」でプロジェクト作成できます
Viteはフロントエンドのビルドツールです

Viteについて
*超高速なHMR(ファイルを変更してからブラウザで変更が反映されるまでが超高速)

  • モジュールを依存関係(開発中に頻繁に変更されることはありません)とソースコード(頻繁に編集されます)に分類します
    依存関係はesbuild(Goで記載されたバンドラー)で10〜100倍高速に事前にバンドルします
    ソースコードはESMを介するので基本的にブラウザがバンドラーの仕事をします(ブラウザの要求に応じて、ソースコードを変換して提供するだけ)
  • 本番のバンドルは、デフォルトではネイティブESMスクリプトタグとネイティブESM動的インポートをサポートするブラウザが対象です(レガシーブラウザは未対応)
    参考:https://vitejs.dev/guide/build.html#browser-compatibility
npm init vite@latest <プロジェクト名> 
# vanilla・vue・react・preact・lit・svelteから選択
cd <プロジェクト名>
npm install
npm run dev

先にテンプレートオプションを指定する場合
vanilla・vanilla-ts・vue・vue-ts・react・react-ts・preac・tpreact-ts・lit・lit-ts・svelte・svelte-ts

# npm 6.x
npm create vite@latest <プロジェクト名>  --template vue

# npm 7+ は--が2つです
npm create vite@latest <プロジェクト名>  -- --template vue

<script setup>

<script setup>は、SFCCompositionAPIの両方を使用している場合に推奨される構文です(簡潔に書くことができます)
export defaultでのexportが不要
変数や関数をreturnすることなくテンプレートで直接使用できます
componentsオプションが不要
トップレベルでawaitが使えます
<script setup>は通常の<script>と一緒に使用できます
*<script setup>はコンポーネントが作成されるたびに実行されるので、1度だけ呼び出したい処理は<script>に書きます

<template>
  <button @click="increment">
    {{ state.count }}
  </button>
</template>

//通常の<script>
<script>
import { reactive } from 'vue'
export default {
  setup() {
    const state = reactive({ count: 0 })
    function increment() {
      state.count++
    }
    return {
      state,
      increment
    }
  }
}
</script>

//<script setup>を使う場合
<script setup>
import { reactive } from 'vue'
const state = reactive({ count: 0 })
function increment() {
  state.count++
}
</script>

propsとemitsの宣言は、「defineProps」 と 「defineEmits」を使用する必要があります

<script setup>
const props = defineProps({
  foo: String
})

const emit = defineEmits(['change'])

</script>

クラスとスタイルのバインディングの実装例

<template>
    <div :class="classObject"></div>
</template>
<script setup>
import { reactive } from 'vue'
const classObject = reactive({
    active: true,
    'text-danger': true
})
</script>

//結果
 <div class="active text-danger"></div>
<template>
// <div :class="{ active: isActive, 'text-danger': hasError }"></div>
</template>
<script setup>
import { ref } from 'vue'
const isActive = ref(true)
const hasError = ref(true)
</script>

//結果
 <div class="active text-danger"></div>
<template>
    <!-- オブジェクトを返すcomputedにバインドする -->
    <div :class="classObject"></div>
</template>
<script setup>
import { ref, computed } from 'vue'
const isActive = ref(true)
const error = ref(true)
//オブジェクトを返すcomputedにバインドする
const classObject = computed(() => ({
    active: isActive.value && !error.value,
    'text-danger': error.value
}))
</script>

//結果
 <div class="text-danger"></div>

インラインスタイルのバインド

<template>
    <div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
    <div :style="styleObject"></div>
</template>
<script setup>
import { ref, reactive } from 'vue'
//refで
const activeColor = ref('red')
const fontSize = ref(30)
//reactiveで
const styleObject = reactive({
    color: 'red',
    fontSize: '13px'
})
</script>

//結果
<div style="color: red; font-size: 13px;></div>
<div style="color: red; font-size: 13px;></div>

ProvideとInject

コンポーネント階層の深さに関係なく、全ての子コンポーネントでデータが共有できます

親は(例:Provide.vue)は、データを提供するための「provideオプション」を持つことができます
任意の値(右)をキー文字列(左)と一緒に保持できます

<script setup>
import { provide } from 'vue'
provide('myName', '親は花子')
</script>

受け取る側のコンポーネント(例:<Inject />)は、提供されたデータを利用するための「injectオプション」を持つことができます
injectをimportして任意の場所で使用します
injectに渡している文字列は、値をprovideに渡した時のキー文字列です

<script setup>
import { inject } from 'vue'
const parentName = inject('myName')
</script>

<template>
    <h1>{{ parentName }}</h1>
</template>

リアクティブにするには、値を提供する際にrefまたはreactiveを使います
どちらかのプロパティに変更があった場合、Injectコンポーネントも自動的で更新されます

<script setup>
import { provide, reactive, ref } from 'vue'
const name = ref('花子')
const profile = reactive({
    age: 20,
  hobby: '映画鑑賞'
})
provide('parentName', name)
provide('parentProfile', profile)
</script>
<script setup>
import { inject } from 'vue'
const parentName = inject('parentName')
const parentProfilee = inject('parentProfile')
</script>

<template>
    <h1>{{ parentName }}</h1>
    <p>{{ parentProfilee.age}}</p>
    <p>{{ parentProfilee.hobby}}</p>
</template>

リアクティブなプロパティに対しての変更はprovide側(提供する側)の内部に留めることが推奨されます
しかし、inject側(注入された側)コンポーネントでデータを更新する必要がある場合
provide側(提供する側)でリアクティブなプロパティを更新する責務を持つメソッドを提供することが推奨されています

//省略
const updateName = () => {
   Name.value = '太郎'
}
provide('updateName', updateName)

provideで渡したデータが、inject側(注入された側)のコンポーネント内で変更されないようするには、提供するプロパティにreadonlyをつけます

import { readonly, provide, ref } from 'vue'
const name = ref('山田花子')
provide('parentName', readonly(name))

関数を切り出す(合成関数)

関数や状態を合成関数として切り出すことができます
合成関数名は先頭に「use」をつけるのが慣例だそうです
*WebAPIからJSONデータを取得する関数

<div id="container" style="padding:16px; border:solid 1px #ccc">
    <div v-if="error">{{ error }}</div>
    <p>アドバイスをくれるAPI</p>
    <p v-if="jsonData.length!== 0">{{jsonData.slip.advice}}</p>
    <button @click="load()">別のアドバイスを取得</button>
</div>

<script type="module">
import { createApp, ref } from "https://unpkg.com/vue@next/dist/vue.esm-browser.prod.js";
//APIからJSONデータを取得する関数 Useを名前の先頭につける
const UseGetJson = () => {
  const jsonData = ref([])
  const error = ref(null)
  const load = async () => {
    try {
      let data = await fetch('https://api.adviceslip.com/advice')
      if(!data.ok) {
        throw Error('データがありません')
      }
      jsonData.value = await data.json()
    }
    catch(err) {
      error.value = err.message
    }
  }
//returnする
  return { jsonData, error, load }
}

const app = createApp({ 
  setup() { 
 //setup関数内でUseGetJsonを利用
    const { jsonData, error, load } = UseGetJson()
    load()
    return { jsonData, error, load }
  },
})
app.mount('#container')
</script>
{{ error }}

アドバイスをくれるAPIです!!

{{jsonData.slip.advice}}

モジュールにする

//モジュール側
export const useHoge = () => {
  
  return { hogeRef, hogeFn }
}

//利用する側
import { useHoge } from './useHoge'
 // useHogeの機能を利用
const { hogeRef, hogeFn } = useHoge()

nextTick

*Vueコンポーネントのデータへの変更はDOMにすぐには反映されません(DOMの更新は非同期です)

DOMが更新されたばかりの瞬間を捉えたい場合(コンポーネントのデータと同期した最新のDOMを保証する)は、nextTick()を状態変更の直後に使用します
nextTick()はDOMの更新が完了するのを待つことができます
コールバックを引数として渡すか、返されるPromiseを待ちます

<script setup>
import { ref, nextTick } from 'vue'
const count = ref(0)
async function increment() {
  count.value++
  // DOMはまだ更新されていません
  console.log(document.getElementById('counter').textContent) // 0
  await nextTick()
  // DOMが更新されています
  console.log(document.getElementById('counter').textContent) // 1
}
</script>

<template>
  <button id="counter" @click="increment">{{ count }}</button>
</template>

setup関数内でvue-router

テンプレート内では今までどおり、$router、$routeでアクセスできます
setup関数内では

import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const route = useRoute()

//ナビゲーションガード
import { onBeforeRouteLeave, onBeforeRouteUpdate, } from 'vue-router'
onBeforeRouteLeave((to, from) => {
      // 
 })
onBeforeRouteUpdate((to, from) => {
      // 
})