Vue の基本的な使い方 (3) Vite と SFC 単一ファイルコンポーネント

関連ページ

作成日:2022年10月16日

Vite

Vite は Vue SFC をサポートをする Vue CLI に代わるビルドツールです(SFC ツール)。

現時点で、Vue CLI はメンテナンスモードになっていて、Vite の利用が推奨されています。

Vite でプロジェクトを作成するには、Node.js(14.18+、16+)がインストールされている必要があります(関連: Node.js を Mac にインストール)。

Node.js のバージョンは node -v コマンドで確認できます。

% node -v  return
v16.13.2

Vite でプロジェクトを生成するには npm コマンドや yarn, pnpm コマンドを利用することができます。以下及び以降では npm コマンドを使用します。

Vite を利用して Vue のプロジェクトを作成するには、以下のいずれかのコマンドを利用できます(コマンドを実行するとプロジェクトに必要な初期化が行われます)。

コマンド 説明
npm create vite@latest
create-vite を利用)
最新の Vite を利用して、指定したフレームワーク(Vue や React など)のプロジェクトを作成するためのベースを構成(初期化)。
npm init vite@latest 上記と同じこと
npm create vue@latest
create-vue を利用)
Vite を利用して最新版の Vue を使ったプロジェクトを作成するためのベースを構成(初期化)。npm create vue@3 や npm create vue@2 のように Vue のバージョンを指定することもできます。
npm init vue@latest 上記と同じこと

npm createnpm init のエイリアスで同じコマンドです(npm help create を実行すると npm help init と同じ内容が表示されます)。

上記のいずれのコマンドも、実行すると対話形式のプロジェクト生成ウィザードが起動して、プロジェクトを作成するためのベースを構成(Scaffolding)します(その後 npm install で必要なパッケージをインストールします)。

以下は npm create vite@latestcreate-vite)を実行して Vue のプロジェクトを作成する例です。

プロジェクト生成ウィザードの ? Select a variant で TypeScript を選択したり、Customize with create-vue を選択すれば、create-vue と同様、Vue Router や Pinia などを追加することができます。

% npm create vite@latest  return // create-vite でプロジェクトを初期化
// プロジェクト名を入力して return キーを押す
? Project name: › my-vite-project
//矢印キーで Vue を選択して return キーを押す
? Select a framework: › - Use arrow-keys. Return to submit.
    Vanilla
❯   Vue
    React
    Preact
    Lit
    Svelte

// JavaScript を選択して return キーを押す
? Select a variant: › - Use arrow-keys. Return to submit.
❯   JavaScript
    TypeScript
    Customize with create-vue  //create-vue でカスタマイズする場合
    Nuxt

Scaffolding project in /Applications/MAMP/htdocs/vue/my-vite-project...

Done. Now run:
  // 以下を順番に実行すると開発サーバーを起動します(後述)
  cd my-vite-project
  npm install
  npm run dev
  

また、必要なパッケージなどがあればメッセージが表示されるのでインストールします。

以下の例では create-vite@3.2.1 が必要というメッセージが表示されたので y を押してパッケージ(create-vite@3.2.1)をインストールしてから、ウィザードを続行しています。

不足しているパッケージがある場合の例
% npm create vite@latest  return // create-vite でプロジェクトを初期化
Need to install the following packages: //以下のパッケージが必要とのこと
  create-vite@3.2.1
Ok to proceed? (y) y  // y を押して上記パッケージをインストール

//ウィザードが開始される(選択肢は省略)
✔ Project name: … my-vite-project  //プロジェクト名を my-vite-project に指定
✔ Select a framework: › Vue  //フレームワークに Vue を指定
✔ Select a variant: › JavaScript  //言語に JavaScript を指定

Scaffolding project in /Applications/MAMP/htdocs/vue/my-vite-project...

Done. Now run:  //準備(Scaffolding)が完了したので以下を実行(後述)

  cd my-vite-project
  npm install
  npm run dev

プロジェクト名や使用するフレームワークは、コマンドラインオプションによって直接指定することもできます。以下は my-vue-app というプロジェクト名で Vue プロジェクトを作成する例です(npm 7+)。

//オプションでプロジェクト名(my-vue-app)とフレームワーク(vue)を指定する場合の例
% npm create vite@latest my-vue-app -- --template vue

npm create vue@latest の場合

以下は npm create vue@latestcreate-vue)を実行して Vue のプロジェクトを作成する例です。

この場合も、必要なパッケージなどがあればメッセージが表示されるのでインストールします。以下の例では create-vue@3.4.0 が必要と表示されたので y と入力して return キーを押してインストールしています。

% npm create vue@latest  return // create-vue でプロジェクトを作成
Need to install the following packages:
  create-vue@3.4.0
Ok to proceed? (y) y

Vue.js - The Progressive JavaScript Framework

✔ Project name: … my-vue-project // プロジェクト名を入力して return キーを押す
// 必要に応じてライブラリを選択して追加できる(この例では以下全てデフォルトの No)
✔ Add TypeScript? … No / Yes  //そのまま return キーを押せば No(インストールされない)
✔ 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 /Applications/MAMP/htdocs/vue/my-vue-project...

Done. Now run:  //準備(Scaffolding)が完了したので以下を実行

  cd my-vite-project
  npm install
  npm run dev

パッケージのインストール

上記のいずれかのコマンドを実行すると、指定したプロジェクト名のディレクトリにプロジェクトが作成されます。以下は npm create vite@latest を実行した場合の例です。

my-vite-project
├── README.md
├── index.html
├── package.json // パッケージ情報(npm の設定ファイル)
├── public
│   └── vite.svg
├── src
│   ├── App.vue
│   ├── assets
│   │   └── vue.svg
│   ├── components //create-vue の場合は、他に2つの .vue ファイルが追加される
│   │   └── HelloWorld.vue
│   ├── main.js
│   └── style.css
└── vite.config.js // Vite の設定ファイル

但し、この時点ではプロジェクトで利用するライブラリはまだインストールされていないので、cd コマンドでプロジェクトルート(生成されたディレクトリ)に移動して npm install コマンドを実行します。

npm install コマンドを実行することで Vue.js を動作させるために必要な JavaScript のパッケージがインストールされます。

% cd my-vite-project  return //プロジェクトルートに移動
% npm install  return //必要な JavaScript のパッケージ(ライブラリ)をインストール

added 33 packages, and audited 34 packages in 9s

4 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

以降の説明は、npm create vite@latest の場合のファイル構成になります(npm create vue@latest の場合とデフォルトで生成されるコンポーネントのファイル構成が少し異なります)。

node_modules というディレクトリが追加され、その中にインストールされたパッケージが入っています。また、インストールされたパッケージの依存関係を管理するファイル package.json も生成されます。

my-vite-project
├── README.md
├── index.html
├── node_modules  //インストールされたパッケージのディレクトリ
│   ├── @babel
│   ├── @esbuild
│   ├── @vitejs
│   ├── @vue
│   ├── csstype
│   ├── esbuild
│   ├── esbuild-darwin-64
│   ├── estree-walker
│   ├── fsevents
│   ├── function-bind
│   ├── has
│   ├── is-core-module
│   ├── magic-string
│   ├── nanoid
│   ├── path-parse
│   ├── picocolors
│   ├── postcss
│   ├── resolve
│   ├── rollup
│   ├── source-map
│   ├── source-map-js
│   ├── sourcemap-codec
│   ├── supports-preserve-symlinks-flag
│   ├── vite
│   └── vue
├── package-lock.json //インストールされたパッケージのバージョン情報(依存関係を管理)
├── package.json
├── public
│   └── vite.svg
├── src
│   ├── App.vue
│   ├── assets
│   │   └── vue.svg
│   ├── components
│   │   └── HelloWorld.vue
│   ├── main.js
│   └── style.css
└── vite.config.js //Vite の設定ファイル

package.json

インストールされたパッケージの依存関係や npm スクリプトは package.json で確認できます。

npm create vite@latest を実行した場合の package.json の例
{
  "name": "my-vite-project",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.2.41"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^3.2.0",
    "vite": "^3.2.3"
  }
}
npm create vue@latest を実行した場合の package.json の例
{
  "name": "my-vue-project",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "vue": "^3.2.41"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^3.1.2",
    "vite": "^3.1.8"
  }
}

実際にインストールされているパッケージのバージョンは package-lock.jsonnpm view xxxx コマンドで確認できます。

vite.config.js

vite.config.js は Vite 固有の設定ファイルで、初期状態では以下のように Vue プラグインの組み込みが記述されています。

defineConfig に設定オプションを記述することができます(Vite の設定)。

vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()]
})

例えば、@ を使ってコンポーネントなど ./src 以下のファイルをインポートできるようにエイリアス(resolve.alias)を設定する場合は、以下のように記述できます。

vite.config.js
import { fileURLToPath, URL } from 'node:url'  //追加

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  // パスにエイリアスを設定する記述を追加
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})

上記を記述すると、例えば App.vue では、以下はいずれも同じインポートになります。

import HelloWorld from './components/HelloWorld.vue'
import HelloWorld from '../src/components/HelloWorld.vue'
import HelloWorld from '@/components/HelloWorld.vue'

開発サーバを起動

npm install で必要な JavaScript のパッケージがインストールされたら npm run dev コマンドでローカルの開発サーバを起動することができます。

% npm run dev  return //開発サーバを起動

> my-vite-project@0.0.0 dev
> vite


  VITE v3.1.4  ready in 1077 ms

  ➜  Local:   http://127.0.0.1:5173/   //開発サーバの URL
  ➜  Network: use --host to expose

npm run dev を実行すると開発サーバの URL が表示されるのでブラウザからアクセスします。ブラウザには以下のような画面が表示されます。

Hot Module Replacement

Vite は HMR 機能(Hot Module Replacement)により、ページの再読み込みをせずに即座に更新を反映します(特別な設定は必要ありません)。

上記の画面に「Edit components/HelloWorld.vue to test HMR」とあるように、HelloWorld.vue や App.vue のファイルを編集すると、その変更は即座に反映されます。

開発サーバを終了

開発サーバを終了するには、 control + c を押します。

コマンドラインインターフェイス

Vite がインストールされているプロジェクトでは npm スクリプトで vite バイナリを使用したり、npx vite で直接実行できます。生成された Vite プロジェクトのデフォルトの npm スクリプトは以下になります(package.json の scripts フィールド)。

package.json 抜粋
{
  "scripts": {
    "dev": "vite", // 開発サーバを起動。エイリアス: `vite dev`, `vite serve`
    "build": "vite build", // プロダクション用にビルド
    "preview": "vite preview" // プロダクション用ビルドをローカルでプレビュー
  }
}

プロジェクトをビルド

プロジェクト(アプリ)をビルドするには、npm run build を実行します。実行すると、本番環境に配置するためのファイル一式が作成されます。

% npm run build  return //プロジェクトをビルド

> my-vite-project@0.0.0 build
> vite build

vite v3.1.4 building for production...
✓ 16 modules transformed.
dist/assets/vue.5532db34.svg     0.48 KiB
dist/index.html                  0.44 KiB
dist/assets/index.43cf8108.css   1.26 KiB / gzip: 0.65 KiB
dist/assets/index.bd817b9d.js    52.75 KiB / gzip: 21.30 KiB

ビルドが成功すると、プロジェクトルートの直下に dist フォルダが生成されます(dist フォルダの配下のフォルダとファイル一式をサーバーのルートに配置すればアプリを実行できます)。

my-vite-project(プロジェクトルート)
├── dist  //本番環境に配置するためのファイル一式
│   ├── assets
│   │   ├── index.43cf8108.css
│   │   ├── index.bd817b9d.js
│   │   └── vue.5532db34.svg
│   ├── index.html
│   └── vite.svg
├── index.html
├── node_modules
├── package-lock.json
├── package.json
├── public
│   └── vite.svg
├── src
│   ├── App.vue
│   ├── assets
│   │   └── vue.svg
│   ├── components
│   │   └── HelloWorld.vue
│   ├── main.js
│   └── style.css
└── vite.config.js

プロダクション用ビルドをローカルでプレビューするには npm run preview コマンドを実行します。

npm run preview により実行される vite preview コマンドは、ローカルで静的なウェブサーバを起動し、dist のファイルを http://localhost:4173 で配信します。

% npm run preview  return //プロダクション用ビルドをプレビュー

> my-vite-project@0.0.0 preview
> vite preview

  ➜  Local:   http://127.0.0.1:4173/
  ➜  Network: use --host to expose

サーバのポートを設定するには、npm スクリプトの引数に --port フラグを指定します。

package.json 抜粋
"scripts": {
  "dev": "vite",
  "build": "vite build",
  "preview": "vite preview --port 8080"  //http://localhost:8080 で起動
},

または、npx コマンドを使って以下のように指定することもできます。

% npx vite preview --port 8080 return //ポート 8080 でプレビュー
  ➜  Local:   http://127.0.0.1:8080/
  ➜  Network: use --host to expose

静的サイトのデプロイ

サブディレクトリ用にビルド

サブディレクトリ(ネストしたパブリックパスの下)にプロジェクトをデプロイする場合は、vite.config.js で base 設定オプションを指定します。Public Base Path

例えば、プロジェクトを foo というサブディレクトリで公開したい場合は、以下のように base オプションを指定してビルドします。

vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  base: '/foo/',  // base オプションを追加
})

設定後ビルドすると、http://127.0.0.1:4173/foo/ でローカルでプレビューするとことができます。

% npm run build return //ビルド

> my-vite-project@0.0.0 build
> vite build

vite v3.1.4 building for production...
✓ 21 modules transformed.
dist/assets/vue.5532db34.svg     0.48 KiB
dist/index.html                  0.45 KiB
dist/assets/index.88d30a23.css   1.37 KiB / gzip: 0.70 KiB
dist/assets/index.f5b097f8.js    55.46 KiB / gzip: 22.33 KiB

% npm run preview return //プレビュー

> my-vite-project@0.0.0 preview
> vite preview

  ➜  Local:   http://127.0.0.1:4173/foo/
  ➜  Network: use --host to expose

本番環境用に生成された dist ディレクトリのファイルを、本番環境の foo ディレクトリにアップします。

ビルド先を変更

build.outDir に出力先のフォルダを指定することで、ビルド先をデフォルトの dist から変更することができます。

以下はビルド先を foo に変更する例です。以下の例では、サブディレクトリにデプロイする base オプションも foo に指定しているので、出力された foo をそのままコピーしてアップすることができます。

vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  base: '/foo/',
  //ビルド先を foo ディレクトリに
  build: {
    outDir: 'foo'
  }
})

本番環境用のビルド | ビルドオプション

Vue プロジェクトのファイル構成

npm create vite@latest で作成した Vue プロジェクトのファイル構成は以下のようになっています。

基本的には作成するコンポーネントを src/components に配置しますが、任意のフォルダを追加するなど変更することができます。

my-vite-project  //プロジェクトルート
├── index.html  //トップページ(表示用ファイル)
├── node_modules  //インストールされたパッケージ
├── package-lock.json
├── package.json
├── public  //public ディレクトリ
│   └── vite.svg
├── src  //ソースコード
│   ├── App.vue  //メインコンポーネント
│   ├── assets  //画像などのアセット
│   │   └── vue.svg
│   ├── components  //コンポーネント一式
│   │   └── HelloWorld.vue
│   ├── main.js  //エントリーポイント
│   └── style.css //スタイルシート
└── vite.config.js  

public ディレクトリ

public ディレクトリに配置されたアセットは開発環境ではルートパス / で提供され、そのまま dist ディレクトリのルートにコピーされます。

public 内のアセットは、JavaScript からはインポートすることができません(public ディレクトリ)。

大まかな仕組み

表示用の index.html で、main.js を読み込んでいます。main.js ではメインコンポーネントの App.vue を読み込み、createApp() でアプリケーションのインスタンスを生成しています。App.vue では コンポーネントの HelloWorld.vue を読み込んでいます。

  • index.html :main.js をモジュールとして読み込む
  • src/main.js :App.vue を読み込みアプリケーションのインスタンスを生成
  • src/App.vue :ルートコンポーネント(他のコンポーネントを読み込む)
  • src/components/HelloWorld.vue :コンポーネント

言い換えると、App.vue でコンポーネントの HelloWorld.vue を読み込み、main.js で App.vue を読み込み、index.html で main.js を読み込んで表示します。

開発サーバを起動した状態にしておけば、ファイルを編集すると即座に反映されるようになっています。

以下は、それぞれのファイルの概要です。

index.html

index.html は開発サーバにアクセスすると表示されるファイルです。

Vite プロジェクトでは index.html は public 内ではなく、プロジェクトルート直下にあります。開発中、 index.html はアプリケーションのエントリポイントです。

以下では type="module" を指定した <script> 要素の src 属性に main.js を指定してモジュールとして読み込んでいます。

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Vue</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
main.js

以下は index.html の中の script タグで読み込まれている main.js です。

vue から import した createApp() にルートコンポーネントオブジェクト( App.vue から import した App)を渡して Vue.js のアプリケーションのインスタンスを作成し、mount() で index.html ファイルの id 属性が app の要素にマウントしています。

/src/main.js
import { createApp } from 'vue'  //vue から createApp 関数を import
import './style.css'  //style.css を import
import App from './App.vue'  //App.vue から App を import

//Vue.js のインスタンスを生成してマウント
createApp(App).mount('#app')
App.vue

以下は main.js で createApp() に渡しているルートコンポーネントの App.vue です。

拡張子 .vue がついたファイル (SFC) 内では script タグに JavaScript のコード(コンポーネントの定義)、template タグに HTML(テンプレート)、style タグに CSS を記述できます 。

コンポーネント定義は、script 要素に setup 属性を指定した <script setup> 構文を使用しています。

template タグの中では、img タグで public フォルダの vite.svg と assets フォルダの vue.svg を参照しています。vite.svg が Vite のロゴファイルで、vue.svg ファイルが Vue のロゴファイルです。

また、HelloWorld タグの msg カスタム属性に「Vite + Vue」を指定して、HelloWorld コンポーネントの props オプションを使って文字列「Vite + Vue」を表示しています。

/src/App.vue
<!-- コンポーネントの定義 -->
<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import HelloWorld from './components/HelloWorld.vue'
</script>

<!-- テンプレートの定義 -->
<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <HelloWorld msg="Vite + Vue" />
</template>

<!-- スタイルの定義  scoped 属性を指定 -->
<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>
HelloWorld.vue

以下は上記の App.vue の <script setup> ブロックで import されている HelloWorld コンポーネントのファイル HelloWorld.vue です。

拡張子が .vue なので、App.vue 同様、Vue コンポーネントを記述するカスタムファイル形式(SFC 単一ファイルコンポーネント )です。

<script setup> で defineProps を使って props を宣言し、ref メソッド でリアクティブな変数を宣言しています。

template タグの中では、button タグで v-on ディレクティブの省略形 @ を使ってクリックされたら count を増加(count++)するようになっています。

/src/components/HelloWorld.vue
<!-- コンポーネントの定義 -->
<script setup>
import { ref } from 'vue'

defineProps({
  msg: String
})

const count = ref(0)
</script>

<!-- テンプレートの定義 -->
<template>
  <h1>{{ msg }}</h1>

  <div class="card">
    <button type="button" @click="count++">count is {{ count }}</button>
    <p>
      Edit
      <code>components/HelloWorld.vue</code> to test HMR
    </p>
  </div>

  <p>
    Check out
    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
      >create-vue</a
    >, the official Vue + Vite starter
  </p>
  <p>
    Install
    <a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
    in your IDE for a better DX
  </p>
  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>

<!-- スタイルの定義  scoped 属性を指定 -->
<style scoped>
.read-the-docs {
  color: #888;
}
</style>

拡張子を省略したインポート

Vite ではデフォルトで以下の拡張子のファイルは、拡張子を省略してインポートできます。

  • .mjs
  • .js
  • .ts
  • .jsx
  • .tsx
  • .json

デフォルトでは、.vue の拡張子を省略してインポートするとエラーになります。

vite.config.js に以下を追加することもできますが、Vite のドキュメント resolve.extensions には、「カスタムインポートタイプ(.vue など)の拡張子を省略すると、IDE や型のサポートに支障をきたす可能性があるため、推奨されません。」とあるので、可能であれば省略しないほうが良いです。

vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  //デフォルトの拡張子に .vue の拡張子を追加(推奨されていない)
  resolve: {
    extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
  },
})

SFC 単一ファイルコンポーネント

単一ファイルコンポーネント(Single File Component または SFC)は、Vue コンポーネントのテンプレート、ロジック、そしてスタイルを1つのファイルにまとめることができる特別なファイル形式です。

拡張子は .vue を使い、Vite などを使ってビルド(標準的な JavaScript と CSS にプリコンパイル)する必要があります。Vite で生成したプロジェクトで開発サーバを起動した状態にしておけば自動的にコンパイルされるので何もする必要はありません(プロダクション用には別途ビルドします)。

以下は単一ファイルコンポーネント(SFC)の例です。

<script> ブロックでは、コンポーネント定義をデフォルトエクスポートします。

SFC(.vue ファイル)に含まれるコンポーネント定義は1つだけなので、default メンバー(モジュールの既定のメンバー)として定義します。名前は呼び出し側で付与するので、モジュール側では不要です。

<!-- コンポーネントの定義 -->
<script>
//コンポーネント定義をデフォルトエクスポート
export default {
  data() {
    return {
      greeting: 'Hello World!'
    }
  }
}
</script>

<!-- テンプレートの定義 -->
<template>
  <p class="greeting">{{ greeting }}</p>
</template>

<!-- スタイルの定義 -->
<style>
.greeting {
  color: red;
  font-weight: bold;
}
</style>

Vue SFC は HTML、CSS、JavaScript の拡張で以下の3種類のトップレベル言語ブロック(要素)で構成されています。

要素 概要
<script> コンポーネントを定義。スクリプトは ES モジュールとして実行され、コンポーネント定義をデフォルトエクスポートする必要があります。各 *.vue ファイルは1つの <script> ブロックを含むことができます(<script setup> を除く)。
<template> コンポーネントのテンプレートを定義(HTML)。template オプションx-template と同様、Mustache 構文ディレクティブなどの構文を利用できます。各 *.vue ファイルは1つのトップレベル <template> ブロックを含めることができます。
<style> コンポーネントに適用する CSS を定義(CSS)。<script> や <template> とは異なり、1つの .vue ファイルに複数の <style> を記述することができます。また、scoped 属性を利用して、現在のコンポーネントにのみ有効なスタイルを定義することができます。1 つの *.vue ファイルには複数の <style> タグを含めることができます。

SFC 構文の仕様

それぞれの要素(言語ブロック)では src 属性を利用することができます。例えば、テンプレートを別ファイルに切り出したい場合は、以下のように記述することができます。

但し、<script setup> ブロックは、src 属性と一緒に使うことはできません。

<template src="./foo.html"></template>

トップレベル言語ブロックの順序

単一ファイルコンポーネント では、<script>、<template>、<style> タグを一貫した順序にし、 <style> は順序を最後にするのが推奨されています。

スタイルガイド:単一ファイルコンポーネントのトップレベルの属性の順序

単一ファイルコンポーネントのファイル名とコンポーネント名

単一ファイルコンポーネントのファイル名は、すべてパスカルケース (PascalCase) にするか、すべてケバブケース (kebab-case) にすることが強く推奨されています(単一ファイルコンポーネントのファイル名の形式)。

また、単一ファイルコンポーネントのテンプレート内では、コンポーネント名は常にパスカルケース(PascalCase)にすることが推奨されています。但し、DOM テンプレートの中ではケバブケース(kebab-case)です(テンプレート内でのコンポーネント名の形式 )。

SFC への書き換え例

以下は Vue を CDN 経由で読み込んで、Composition API で記述したカウンターのコンポーネントです。

関連項目:Composables コンポーザブル関数

my_counter.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>My Counter</title>
</head>
<body>
  <div id="app">
    <my-counter v-bind:initial-count="100"></my-counter>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@3.2.29/dist/vue.global.js"></script>
  <script src="js/use_counter.js"></script>
  <script src="js/my_counter.js"></script>
</body>
</html>
js/use_counter.js(コンポーザブル関数)
//カウンターのためのロジック
const useCounter = (initialCount) => {
  //カウンターで使用する変数
  const count = Vue.ref(initialCount);
  //カウンターで使用するメソッド
  const countUp = () => {
    count.value ++;
  };
  //カウンターで使用するメソッド
  const countDown = () => {
    count.value --;
  }
  //カウンターで使用するオブジェクト(変数とメソッド)を返す
  return {
    count,
    countUp,
    countDown
  }
}
js/my_counter.js(コンポーネント)
const app = Vue.createApp({});

//useCounter(コンポーザブル関数)を利用するコンポーネント
app.component('my-counter', {
  // props オプション
  props: {
    initialCount : {
      type: Number,
      default: 0
    }
  },
  // template オプション
  template: `<div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
  `,
  // setup メソッド
  setup(props, context) {
    //コンポーザブル関数(useCounter)を呼び出し、その結果を変数に代入
    const {count, countUp, countDown } = useCounter(props.initialCount);
    //コンポーザブル関数の結果を setup の戻り値に
    return {
      count,
      countUp,
      countDown
    }
  }
});
app.mount('#app');

以下は、上記を Vite で作成したプロジェクトで、単一ファイルコンポーネントで書き換えたものです。

my-vite-project  //プロジェクトルート
├── index.html //一部変更
├── src
│   ├── App.vue //変更
│   ├── components
│   │   ├── HelloWorld.vue //未使用(削除可能)
│   │   └── MyCounter.vue //追加
│   ├── composables //追加
│   │   └── useCounter.js //追加
│   ├── main.js //一部変更
│   └── style.css //未使用(削除可能 main.js でのインポートも削除)
└── vite.config.js

Vue を CDN 経由で読み込んで使用する場合とは異なり、コードをコンポーネントなどの機能単位でモジュールとして分割管理し、モジュール構文を使用しています。

.vue ファイルに含まれるコンポーネント定義は1つだけなので、default メンバーとして定義して export します。コンポーネントを利用するには、import して任意の名前を付けて使用することができますが、モジュール名(フィル名)をそのまま使うのが一般的です。

以下は my_counter.js に対応する MyCounter.vue(単一ファイルコンポーネント)です。

template オプションの内容(テンプレート)を <template> ブロックに記述し、<script> ブロックでは関数 useCounter をインポートして、コンポーネント定義をデフォルトエクスポートしています。

import を使って関数をインポートしているのと、export default でコンポーネント定義をデフォルトエクスポートしている以外は同じです。

name オプションはコンポーネントがテンプレートの中で自分自身を再帰的に呼び出すことを許可したり、デバッグの際に識別するための値で、単一ファイルコンポーネント(SFC)の場合は省略可能です(自動 name 推論)。

src/components/MyCounter.vue
<script>
//useCounter をインポート(拡張子 .js は省略可能)
import useCounter from '../composables/useCounter';

//コンポーネント定義をデフォルトエクスポート
export default {
  // name オプション(省略可能)
  name: 'MyCounter',
  // props オプション
  props: {
    initialCount : {
      type: Number,
      default: 0
    }
  },
  // setup メソッド
  setup(props, context) {
    //コンポーザブル関数(useCounter)を呼び出し、その結果を変数に代入
    const {count, countUp, countDown } = useCounter(props.initialCount);
    //コンポーザブル関数の結果を setup の戻り値に
    return {
      count,
      countUp,
      countDown
    }
  }
}
</script>

<template>
  <div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
</template>

以下は use_counter.js(コンポーザブル関数)をモジュール化した useCounter.js です。

CDN 経由で Vue を利用する場合は、ref などの関数はクラスメソッドの形式(Vue.ref など)で呼び出すことができますが、モジュールの場合(vue モジュールの関数なので)、vue からインポートする必要があります。

別のファイル(モジュール)から定義した関数を利用できるように、export default で定義した関数をエクスポートします(関数定義の内容は同じです)。

src/composables/useCounter.js
//ref を vue からインポート
import { ref } from 'vue'

export default function(initialCount) {
  //カウンターで使用する変数
  const count = ref(initialCount);
  //カウンターで使用するメソッド
  const countUp = () => {
    count.value ++;
  };
  //カウンターで使用するメソッド
  const countDown = () => {
    count.value --;
  }
  //カウンターで使用するオブジェクト(変数とメソッド)を返す
  return {
    count,
    countUp,
    countDown
  }
}

以下はメインコンポーネントの App.vue です。

components オプションにインポートした MyCounter をローカル登録しています。

src/App.vue
<script >
import MyCounter from './components/MyCounter.vue'

export default {
  name: 'App',  //省略可能
  components: {
    MyCounter  //MyCounter: MyCounter と同じ
  }
}
</script>

<template>
  <MyCounter :initial-count="100"></MyCounter>
</template>

<style>
#app {
  text-align: center;
  color: green;
  margin-top: 50px;
}
</style>

エントリポイントの main.js は Vite で生成したものとほぼ同じです(スタイルのインポートは不要なのでコメントアウトしています)。

ref 関数同様、createApp も vue モジュールの関数なので、vue からインポートします。

createApp の引数には、ルートコンポーネントのオプションオブジェクトを渡しますが、App コンポーネントは既に定義済みなので、コンポーネントオブジェクトをそのまま渡すことができます。

createApp() にコンポーネントオブジェクトを渡して Vue のインスタンスを作成し、mount() で index.html ファイルの id 属性が app の要素にマウントしています。

src/main.js
import { createApp } from 'vue'
//import './style.css'
import App from './App.vue'

createApp(App).mount('#app')

index.html では type="module" を指定した <script> 要素の src 属性に main.js を指定してモジュールとして読み込んでいます。

index.html も Vite で生成したものとほぼ同じです(title 要素のタイトルを変更し、rel="icon" の link 要素を削除しています)。

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>カウンターサンプル</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
Render 関数の使用

以下のコンポーネントは <template> ブロックにテンプレートを記述していますが、Render 関数を使用することもできます。

CountButton.vue
<script>
// ref を vue からインポート
import { ref } from 'vue';

//コンポーネント定義をデフォルトエクスポート
export default {
  // setup メソッド
  setup() {
    // データオブジェクトを宣言
    const count = ref(0);
    // イベントリスナー(関数)を定義
    const increment = () => ++count.value;
    // テンプレートに公開
    return {
      count,
      increment
    };
  }
}
</script>

<template>
  <button type="button" class="countup" v-on:click="increment">
    Count: {{ count }}
  </button>
</template>

<style scoped>
.countup {
  background-color:lightpink;
  font-weight: bold;
}
</style>

以下は、上記を Render 関数を使って書き換えた例です。setup は同じスコープで宣言されたリアクティブなステートを直接利用することができる Render 関数を返すこともできます。

CountButton.vue
<script>
// ref と h 関数を vue からインポート
import { ref , h } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const increment = () => ++count.value;
    // Render 関数を返す
    return () => h(
      'button',
      {
        type: "button",
        class: "countup",
        onClick: increment
      },
      'Count: ' + count.value
    );
  }
}
</script>

<style scoped>
.countup {
  background-color:lightpink;
  font-weight: bold;
}
</style>

子コンポーネントのメソッドを親コンポーネント内で呼び出す例

以下は上記のコンポーネントの increment メソッドを expose で公開して、親コンポーネントでテンプレート参照を使って increment メソッドにアクセスするように書き換えたものです。

expose は setup の第2引数で分割代入で取得しています。

CountButton.vue(style は省略)
<script>
  import { ref , h } from 'vue';

  export default {
    setup(props, { expose })  {
      const count = ref(0);
      const increment = () => ++count.value;

      // increment メソッドを公開
      expose({
        increment
      });

      // Render 関数を返す(onClick 属性は親コンポーネントで指定)
      return () => h(
        'button',
        {
          type: "button",
          class: "countup",
        },
        'Count: ' + count.value
      );
    }
  }
</script>

expose で公開されたプロパティは、親コンポーネントでテンプレート参照を使って利用できます。

App.vue(親コンポーネント <script setup> 構文)
<script setup>
// ref を vue からインポート
import { ref } from 'vue';
import CountButton from './components/CountButton.vue';

//子コンポーネント側のコンポーネント情報を受け取る(参照を保持する)ref を宣言
const childRef = ref(null);  //または ref()
// 子コンポーネント側のメソッドを発火させるメソッド
const handleClick = () => {
  // 公開されているメソッドを呼び出す
  childRef.value.increment();
};
</script>

<template>
  <!-- ref の childRef を指定 -->
  <count-button ref="childRef" v-on:click="handleClick"></count-button>
</template>

script setup 属性

単一ファイルコンポーネントで Composition API を使用している場合、<script setup> 構文が使えます(Vue.js 3.2 から)。

<script setup> 構文は、通常の <script> 構文と比べ、以下のようなメリットがあります。

  • より簡潔なコード
  • props と emit の定義に TypeScript の構文が使える
  • 実行時のパフォーマンスの向上
  • IDE でのパフォーマンス向上

SFC <script setup>

使い方は、単一ファイルコンポーネントで <script> ブロックに setup 属性を指定します。

<script setup> の配下は setup() メソッドの配下に記述されたものとみなされます。

<script setup>
//内部のコードは、コンポーネントの setup() 関数の内容としてコンパイルされます
</script>

通常の <script> とは異なり、コンポーネントが最初にインポートされたときに一度だけ実行されるのではなく、<script setup> 内のコードは コンポーネントのインスタンスが作成されるたびに実行されます。

通常の <script> 構文と比較をすると以下の点が異なっています。

  • setup() メソッドの内容としてコンパイルされるので setup() を省略
  • トップレベル(直下)で宣言された変数や関数、import されたメンバーはテンプレートに公開され、<template> 内で使用できる(return する必要はない→ return するとエラーになる)
  • import されたメンバーはテンプレートに公開されるので、import するだけでコンポーネントを直接使える(インポートしたコンポーネントを components オプションで登録する必要がない)
  • export default でコンポーネント定義をデフォルトエクスポートする必要がない
  • props と emits を宣言するには、defineProps と defineEmits を使用する必要がある
  • 親コンポーネントからテンプレート参照を使ってアクセスするには、defineExpose でコンポーネントのプロパティを明示的に公開する必要がある

以下は通常の <script> 構文の例です(前述の MyCounter.vue)。

src/components/MyCounter.vue
<script>
import useCounter from '../composables/useCounter';

export default {
  props: {
    initialCount : {
      type: Number,
      default: 0
    }
  },
  setup(props, context) {
    const {count, countUp, countDown } = useCounter(props.initialCount);
    return {
      count,
      countUp,
      countDown
    }
  }
}
</script>

<template>
  <div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
</template>

以下は、上記のコードを <script setup> 構文を使って書き換えた例で、export default や setup() とその return を省略できるので簡潔に記述できます。

props の宣言は、defineProps() を使います(props オプションと同じ値を受け取ります)。

トップレベル(直下)で宣言された変数や関数(この例では、props、count、countUp、countDown)は <template> 内で使用できます。

src/components/MyCounter.vue
<script setup>
import useCounter from '../composables/useCounter';

const props = defineProps({
  initialCount : {
    type: Number,
    default: 0
  }
})

const {count, countUp, countDown } = useCounter(props.initialCount);
</script>

<template>
  <div>Count: {{ count }}
    <div>
      <button type="button" v-on:click="countUp">Increase</button>
      <button type="button" v-on:click="countDown">Decrease</button>
    </div>
  </div>
</template>

以下は通常の <script> 構文の例です(前述の App.vue)。

src/App.vue
<script >
import MyCounter from './components/MyCounter.vue'

export default {
  components: {
    MyCounter
  }
}
</script>

<template>
  <MyCounter :initial-count="100"></MyCounter>
</template>

以下は、上記のコードを <script setup> 構文を使って書き換えたものです。

components オプションに import したコンポーネントを登録してデフォルトエクスポートしていたものが、import するだけで直接使えるようになります。

<script setup> のスコープ内の値は、カスタムコンポーネントのタグ名としても直接使用できます。ケバブケースの <my-counter> も動作しまが、パスカルケースのコンポーネントタグが推奨されています。

src/App.vue
<script setup>
import MyCounter from './components/MyCounter.vue'
</script>

<template>
  <MyCounter :initial-count="100"></MyCounter>
</template>
defineProps と defineEmits

<script setup> の中で props emits を宣言するには、defineProps と defineEmits を使用する必要があります。

defineProps と defineEmits は、<script setup> 内でのみ使用可能なコンパイラマクロで、インポートする必要はありません(defineProps と defineEmits)。

setup() メソッドでは、引数(props と context)を介して props と emits にアクセスしますが、<script setup> の中では、defineProps と defineEmits の戻り値を使います。

const props = defineProps({
  userId : {
    type: String,
  }
});

// または(型やデフォルト値を指定しない場合)
const props = defineProps( ['userId'] )

上記の場合、テンプレートでは {{ userId }} のようにアクセスでき、script では props.userId でアクセスできます。

以下は <script setup> の中で props と emits を利用する例です。

my-vite-project  //プロジェクトルート
├── index.html
├── src
│   ├── App.vue //変更
│   ├── components
│   │   └── CounterButton.vue //追加
│   ├── main.js
│   └── style.css //未使用
└── vite.config.js
src/components/CounterButton.vue
<script setup>
//emit を defineEmits() で定義
const emit = defineEmits(['clickcount']);

//メソッド
const onclick = () => {
  //emit の実行(defineEmits の戻り値から実行)
  emit('clickcount');
}

//props を defineProps() で定義
const props = defineProps({
  label : {
    type: String,
    default: 'Click!'
  }
});
</script>

<template>
  <button type="button" @click="onclick">{{ label }}</button>
</template>

リアクティブな状態は ref メソッドや reactive メソッドを使って明示的に作成する必要があります。

setup() 関数から返された値と同じように、テンプレート内で参照されるときに ref は自動的にアンラップされます(リアクティビティ)。

src/App.vue
<script setup>
//CounterButton をインポート
import CounterButton from './components/CounterButton.vue'

//ref メソッドをインポート
import { ref } from 'vue';

//ref メソッドを使って変数をリアクティブに
const count = ref(0);

const countup = () => {
  //value プロパティを使って変数の値を操作(増加)
  count.value++;
}

const countdown = () => {
  count.value--;
}

const reset = () => {
  count.value = 0;
}

//ローカルのカスタムディレクティブの定義
const vTextColor = {
  mounted(el, binding) {
    el.style.color = binding.value;
  }
}
//カスタムディレクティブに渡す値
const resetColor ='red';
</script>

<template>
  <CounterButton @clickcount="countup" label="Count Up"></CounterButton>
  <CounterButton @clickcount="countdown" label="Count Down"></CounterButton>
  <CounterButton @clickcount="reset" label="Reset" v-text-color="resetColor"></CounterButton>
  <p>Count : {{ count }}</p>
</template>
カスタムディレクティブ

ローカルのカスタムディレクティブは、 directives オプションで明示的に登録する必要はなく、定義すればテンプレートで直接使用できます(カスタムディレクティブの使用)。

但し、vNameOfDirective という命名規則に従う必要があります(v から始まるキャメルケース)。

//ローカルのカスタムディレクティブの定義
const vTextColor = {
  mounted(el, binding) {
    el.style.color = binding.value;
  }
}

他の場所からディレクティブをインポートする場合、命名規則に合うようにリネームすることができます。

<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>
defineExpose

<script setup> を使用したコンポーネントは、デフォルトで閉じられています。テンプレート参照や $parent チェーンを介して取得されるコンポーネントのパブリックインスタンスは、<script setup> 内で宣言されたバインディングを公開しません。

そのため、<script setup> で定義したコンポーネントのプロパティを明示的に公開するには、defineExpose を使用する必要があります。defineExpose は、<script setup> 内でのみ使用可能なコンパイラマクロで、インポートする必要はありません。

以下は、子コンポーネント CountButton で定義した変数や関数を で公開して、親コンポーネント App でテンプレート参照でアクセスして利用する例です。

関連項目:子コンポーネントのメソッドを親コンポーネント内で呼び出す例

CountButton.vue
<script setup>
import { ref } from 'vue';

const count = ref(0);
const increment = () => ++count.value;
const message = 'This is CountButton';

// count と increment、message を公開
defineExpose({
  count,
  increment,
  message
});
</script>

<template>
  <button type="button" class="countup">
    Count: {{ count }}
  </button>
</template>

ref メソッドで宣言した childRef は ref オブジェクトなので value プロパティ(childRef.value)を使います。

App.vue
<script setup>
import { ref } from 'vue';
import CountButton from './components/CountButton.vue';

//子コンポーネント側のコンポーネント情報を受け取る(参照を保持する)ref を宣言
const childRef = ref(null);  //または ref()
// 子コンポーネント側のメソッドや変数を利用するメソッド
const handleClick = () => {
  // 公開されているメソッドを呼び出す
  childRef.value.increment();
  //公開されている変数を呼び出す
  console.log(childRef.value.message + ':' + childRef.value.count);
};
</script>

<template>
  <!--6行目で定義した childRef(ref) を ref 属性に指定 -->
  <count-button ref="childRef" v-on:click="handleClick"></count-button>
</template>
useSlots と useAttrs

<script setup> 内で slots や attrs を使用する場合は、それぞれ useSlots と useAttrs ヘルパーを使用します。

useSlots と useAttrs は、setupContext.slots と setupContext.attrs と同等のものを返す実際のランタイム関数で、通常の Composition API の関数内でも使用できます。

以下は useSlots と useAttrs を使って、単にコンポーネントのスロットプロパティでない属性の情報を出力するだけの例です。

App.vue
<script setup>
import MyComponent from './components/MyComponent.vue';
</script>

<template>
  <MyComponent class="bar">スロットのコンテンツ</MyComponent>
</template>

上記の MyComponent は以下のように記述したのと同じことです。

<MyComponent class="bar">
  <template v-slot:default>スロットのコンテンツ!</template>
</MyComponent>

slots.default() でデフォルトスロット(名前のないスロット)を取得し、その最初の要素は [0] でアクセスし、テキストを children プロパティでアクセスしています(関連項目:$slots)。

attrs にはコンポーネントの props や emits オプションで宣言されていないすべての属性が含まれているので、以下ではその中の class 属性を出力しています(関連項目:$attrs)。

MyComponent.vue
<script setup>

import { useSlots, useAttrs, onMounted } from 'vue';

const slots = useSlots();
const attrs = useAttrs();

onMounted(() => {
  //最初のデフォルトスロットの子要素(children)
  console.log(slots.default()[0].children);  //スロットのコンテンツ!
  //attrs の class
  console.log(attrs.class);  //bar(追加された class 属性)
});

</script>

<template>
  <div class="wrapper">
    <p class="foo">Hello, <slot>World.</slot></p>
  </div>
</template>
通常の script ブロックとの併用

<script setup> は、通常の <script> と一緒に使うことができます。

<script setup> で実行できない以下のようなコードが含まれている場合、<script setup> ブロックと通常の <script> ブロックを併用することになります。

  • inheritAttrs や、プラグインで有効になるカスタムオプションなど、<script setup> では表現できないオプションの宣言
  • 名前付きのエクスポートの宣言
  • 副作用を実行したり、モジュールスコープで一度しか実行しないコード(<script setup> 内のコードは コンポーネントのインスタンスが作成されるたびに実行されます)

通常の <script> との併用

Scoped CSS スコープ付きスタイル

<style> ブロックに scoped 属性を付与すると、その CSS は現在のコンポーネントの要素にだけ適用されるローカルなスタイルになります。これは Shadow DOM のスタイルのカプセル化に似ています(scoped 属性を指定しない <style> ブロックは、グローバルなスタイルになります)。

一般的には、グローバルなスタイルはコンポーネントとは別に定義し、それぞれのコンポーネントの <style> ブロックには scoped 属性を指定してローカルなスタイルとして定義することが多いようです。

例えば、Vite でデフォルトで生成されるプロジェクトでは、main.js で style.css(グローバルなスタイル)をインポートし、それぞれのコンポーネントの <style> ブロックには scoped 属性が指定され、ローカルなスタイルとして定義されています。

SFC スタイルの機能

以下は Vite でデフォルトで生成されるプロジェクトに HelloFoo.vue というコンポーネント(SFC)を追加して <style> ブロックに scoped 属性を指定して確認する例です。

※ スコープ付きスタイルでは、単に要素セレクタ(例えば p)にスタイルを指定するのではなく、クラスや ID を使ってスタイルを指定する方がパフォーマンスが良いです。

HelloFoo.vue
<script setup>
defineProps({
  foo: String
})
</script>

<template>
  <p class="hello">Hello {{ foo }} !</p>
</template>

<style scoped> /* style に scoped 属性を指定 */
.hello {
  display:inline-block;
  border: 1px solid #ccc;
  padding: 10px;
  background-color: yellow;
  font-weight: bold;
}
</style>

App.vue で HelloFoo.vue をインポートして、テンプレートに追加しています。

App.vue(ルートコンポーネント)
<script setup>
import HelloWorld from './components/HelloWorld.vue'
import HelloFoo from './components/HelloFoo.vue'  //追加
</script>

<template>
  <div>
    <a href="https://vitejs.dev" target="_blank">
      <img src="/vite.svg" class="logo" alt="Vite logo" />
    </a>
    <a href="https://vuejs.org/" target="_blank">
      <img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
    </a>
  </div>
  <HelloWorld msg="Vite + Vue" />
  <HelloFoo foo="Foo" /><!-- 追加 -->
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

以下は App.vue のテンプレートで使われている HelloWorld.vue です。

HelloWorld.vue
<script setup>
import { ref } from 'vue'

defineProps({
  msg: String
})

const count = ref(0)
</script>

<template>
  <h1>{{ msg }}</h1>

  <div class="card">
    <button type="button" @click="count++">count is {{ count }}</button>
    <p>
      Edit
      <code>components/HelloWorld.vue</code> to test HMR
    </p>
  </div>

  <p>
    Check out
    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
      >create-vue</a
    >, the official Vue + Vite starter
  </p>
  <p>
    Install
    <a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
    in your IDE for a better DX
  </p>
  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>

<style scoped>
.read-the-docs {
  color: #888;
}
</style>

ブラウザで確認すると、以下のように HelloFoo.vue のコンポーネントの p 要素にのみ scoped 属性を指定した CSS が適用されています(HelloWorld.vue の要素には影響していません)。

子コンポーネントのルート要素

scoped を使うと、親コンポーネントのスタイルは子コンポーネントに適用されません。

但し、子コンポーネントのルート要素は、親のスコープ付き CSS と子のスコープ付き CSS の両方の影響を受けます。これは親コンポーネントがレイアウトの目的で子コンポーネントのルート要素をスタイルできるようにするためです。

上記の例をブラウザのインスペクターで確認すると以下のようになっています。

scoped 属性を指定した HelloFoo のスタイルには、セレクターに [data-v-34595007] のような属性が追加され、テンプレートの要素にも対応する data-v-34595007 属性が付与されています。

この例の場合、HelloFoo コンポーネントは1つのルート要素(p 要素)で構成されているので、親コンポーネント(App)の属性 data-v-7a7a37b1 も付与されています。

HelloFoo のテンプレートを以下のように div 要素をルート要素とすると、

<template>
  <div>
    <p class="hello">Hello {{ foo }} !</p>
  </div>
</template>

出力は以下のように、ルート要素の div 要素に親コンポーネント(App)の属性 data-v-7a7a37b1 が付与されます。

<div data-v-34595007="" data-v-7a7a37b1="">
  <p class="hello" data-v-34595007="">Hello Foo !</p>
</div>

また、HelloFoo のテンプレートを以下のように複数のルート要素にすると、

<template>
  <p class="hello">Hello {{ foo }} !</p>
  <p class="hello">Hello {{ foo }} !</p>
</template>

親コンポーネント(App)の属性は付与されません。

<p class="hello" data-v-34595007="">Hello Foo !</p>
<p class="hello" data-v-34595007="">Hello Foo !</p>

スコープ付きスタイルでもクラスが不要になるわけではない

p { color: red } はスコープ付きの場合(属性セレクタと合わせた場合)に何倍も遅くなります。.example { color: red } のようにクラスや ID を代わりに使えば、このパフォーマンスへの影響をほぼ解消することができます。

:deep 擬似クラス

スコープ付きスタイルの適用先は現在のコンポーネントで、配下に入れ子になったコンポーネントがあってもそれらに影響が及びません。

以下は、前述の例の HelloFoo.vue に子コンポーネント HelloFooChild を追加して入れ子にしたものです。

HelloFoo.vue
<script setup>
import HelloFooChild from './HelloFooChild.vue'

defineProps({
  foo: String
})
</script>

<template>
  <div>
    <p class="hello">Hello {{ foo }} !</p>
    <HelloFooChild />
  </div>
</template>

<style scoped>
.hello {
  display:inline-block;
  border: 1px solid #ccc;
  padding: 10px;
  background-color: yellow;
  font-weight: bold;
}
</style>
HelloFooChild.vue
<script setup>
</script>

<template>
  <div>
    <p class="hello">Hello from Child !</p>
  </div>
</template>

<style scoped>
</style>

上記の場合、子コンポーネントのテンプレートの .hello の p 要素には、親コンポーネントのスタイルは適用されません。

生成される CSS
.hello[data-v-34595007] {
  display:inline-block;
  border: 1px solid #ccc;
  padding: 10px;
  background-color: yellow;
  font-weight: bold;
}

:deep() 擬似クラスを利用すると、子コンポーネントまで波及するスタイルを適用することができます。

HelloFoo.vue の style ブロック抜粋
<style scoped>
/* :deep() 擬似クラス */
:deep(.hello) {
  display:inline-block;
  border: 1px solid #ccc;
  padding: 10px;
  background-color: yellow;
  font-weight: bold;
}
</style>

:deep() 擬似クラスを使うと、生成される CSS は以下のように属性の子孫セレクタになります。

生成される CSS
[data-v-34595007] .hello {
  display:inline-block;
  border: 1px solid #ccc;
  padding: 10px;
  background-color: yellow;
  font-weight: bold;
}

このため、出力される HTML は以下のように同じですが、子コンポーネントにスタイルが適用されるようになります。

<div data-v-34595007="" data-v-7a7a37b1="">
  <p class="hello" data-v-34595007="">Hello Foo !</p>
  <div data-v-34595007="">
    <p class="hello">Hello from Child !</p>
  </div>
</div>

v-html

v-html で作られた DOM コンテンツは、スコープ付きスタイルの影響を受けませんが、:deep() 擬似クラスを使ってスタイルすることができます。

:slotted 擬似クラス

スロットはスコープ付きスタイルでは、親コンポーネントが所有しているコンテンツとみなして、デフォルトではレンダリングされたコンテンツに適用されません。

スロットにスタイルを適用するには :slotted 擬似クラスを使用します。

HelloFoo.vue
<script setup>
</script>

<template>
  <div>
    <p class="hello">Hello <slot>Foo</slot> !</p>
  </div>
</template>

<style scoped>
/* :slotted 擬似クラスを使用 */
:slotted(.foo) {
  color: red;
}
</style>
App.vue(ルートコンポーネント)
<script setup>
import HelloFoo from './components/HelloFoo.vue'
</script>

<template>
  <!-- span 要素がスロットコンテンツ -->
  <HelloFoo> <span class="foo">Bar</span> </HelloFoo>
</template>

<style scoped>
</style>

以下のように出力され、

<div id="app" data-v-app="">
  <div data-v-34595007="">
    <p class="hello" data-v-34595007="">
      Hello <span class="foo" data-v-34595007-s="">Bar</span> !
    </p>
  </div>
</div>

以下のようなスタイルが生成されます。

<style type="text/css">
.foo[data-v-34595007-s] {
  color: red;
}
</style>
:global 擬似クラス

スコープ付きスタイルの中で、例外的にグローバルにルールを適用したい場合、別の <style> を作るのではなく、:global 擬似クラスを使用することができます。

<style scoped>
/* グローバルのスタイル */
:global(.foo) {
  color: red;
}
</style>

ローカルとグローバルのスタイルの併用

.vue ファイルでは、<style> を複数設置することができ、スコープ付き(ローカル)とスコープなし(グローバル)のスタイルの両方を同じコンポーネントに含めることもできます。

基本的にはグローバルなスタイルは <style> に記述し、スコープ付きスタイルの中で例外的にグローバルにしたいもの(その方が管理しやすいもの)に :global 擬似クラスを使用します。

<style scoped>
/* ローカルのスタイル */
</style>

<style>
/* グローバルのスタイル */
.foo {
  color: red;
}
</style>

CSS Modules / CSS のモジュール化

CSS のグローバルスコープ汚染を回避するために名前空間を分離する仕組みとして CSS Modules があり、CSS Modules を利用するには、<style> 要素に module 属性を指定します。

module 属性を指定した <style module> タグは CSS Modules としてコンパイルされ、結果として得られる CSS クラスを $style というキーの下にオブジェクトとしてコンポーネントに公開するので、テンプレートから $style 経由でアクセスできるようになります。

<style module>

以下は、<style module> で定義した CSS クラスを v-bind:class で $style.hello として指定しています。

HelloFoo.vue
<script setup>
</script>

<template>
  <div>
    <!-- v-bind:class で $style 経由で CSS クラスにアクセス -->
    <p v-bind:class="$style.hello">Hello Foo !</p>
  </div>
</template>

<style module> /* module 属性を指定 */
.hello {
  display:inline-block;
  border: 1px solid #ccc;
  padding: 10px;
  background-color: yellow;
  font-weight: bold;
}
</style>

上記の場合、v-bind:class="$style.hello" を指定した p 要素は、例えば以下のように出力され、

<p class="_hello_1ez75_2">Hello Foo !</p>

以下のようなスタイルが生成されます。

<style type="text/css">
._hello_1ez75_2 {
  display:inline-block;
  border: 1px solid #ccc;
  padding: 10px;
  background-color: yellow;
  font-weight: bold;
}
</style>

結果として得られるクラス(例えば上記の場合は ._hello_1ez75_2)は、衝突を避けるためにクラスセレクターがユニークになるようにハッシュ化(変換)され、CSS を現在のコンポーネントだけにスコープするのと同じ効果を実現します(擬似的なローカルスコープを実現します)。

キー($style)のカスタマイズ

module 属性に値(プロパティキー名)を指定することで、クラスオブジェクトのプロパティキー($style)をカスタマイズできます。

以下は module 属性の値に foo を指定して、$style の代わりにカスタマイズしたキー(foo)で CSS クラスにアクセスしています。

HelloFoo.vue
<script setup>
</script>

<template>
  <div>
    <!-- カスタマイズしたキーで CSS クラスにアクセス -->
    <p v-bind:class="foo.hello">Hello Foo !</p>
  </div>
</template>

<style module="foo"> /* module 属性に値(キー名)を指定 */
.hello {
  display:inline-block;
  border: 1px solid #ccc;
  padding: 10px;
  background-color: yellow;
  font-weight: bold;
}
</style>
Composition API での使用

CSS Modules で注入されたクラスは setup() や <script setup> から useCssModule() を使ってアクセスできます。

useCssModule() はデフォルトでは <style module> を返します。

以下は setup() で useCssModule() を使って <style module> を取得して、変数 style に代入し、注入されたクラスに style.hello でアクセスしています。

HelloFoo.vue
<script>
import { useCssModule } from 'vue'

export default {
  setup() {
    // useCssModule() で CSS Modules で注入されたスタイルを取得
    const style = useCssModule();
    return {
      style
    }
  }
}
</script>

<template>
  <div>
    <p v-bind:class="style.hello">Hello Foo !</p>
  </div>
</template>

<style module>
.hello {
  display:inline-block;
  border: 1px solid #ccc;
  padding: 10px;
  background-color: yellow;
  font-weight: bold;
}
</style>

上記の <script> ブロックの部分は <script setup> を使うと以下のようにシンプルに記述できます。

<script setup>
import { useCssModule } from 'vue'
// useCssModule() で CSS Modules で注入されたスタイルを取得
const style = useCssModule();
</script>

以下は setup() で Render 関数を使って書き換えた例です。

HelloFoo.vue
<script>
import { h, useCssModule } from 'vue'

export default {
  setup() {
    // CSS Modules のスタイルを取得
    const style = useCssModule()

    // Render 関数を返す
    return () => h(
      'div',
      h(
        'p',
        {
          class: style.hello // 取得したスタイルからクラスを指定
        },
        'Hello Foo !'
      )
    )
  }
}
</script>

<style module>
.hello {
  display:inline-block;
  border: 1px solid #ccc;
  padding: 10px;
  background-color: yellow;
  font-weight: bold;
}
</style>

以下は上記を <script setup> を使って書き換えた例です。h() 関数で生成した要素を <template> ブロックで参照しています。

HelloFoo.vue
<script setup>
import { h, useCssModule } from 'vue'
// CSS Modules のスタイルを取得
const style = useCssModule()
// 要素(VNode)を生成
const foo = h(
  'div',
  h(
    'p',
    {
      class: style.hello // 取得したスタイルからクラスを指定
    },
    'Hello Foo !'
  )
)
</script>

<template>
  <foo />
</template>

<style module>
.hello {
  display:inline-block;
  border: 1px solid #ccc;
  padding: 10px;
  background-color: yellow;
  font-weight: bold;
}
</style>

名前付き <style module="名前">

名前付き <style module> では useCssModule() の引数に module 属性の値(名前)を指定します。

HelloFoo.vue
<script setup>
  import { useCssModule } from 'vue'
  //引数に module 属性の値(プロパティキー名)を指定
  const style = useCssModule('foo');
</script>

<template>
  <div>
    <p v-bind:class="style.hello">Hello Foo !</p>
  </div>
</template>

<style module="foo"> /* module 属性に値(キー名)を指定 */
.hello {
  display:inline-block;
  border: 1px solid #ccc;
  padding: 10px;
  background-color: yellow;
  font-weight: bold;
}
</style>

動的 CSS

単一ファイルコンポーネントの <style> タグでは v-bind CSS 関数を使って、CSS の値を動的コンポーネントの状態にリンクする(データオブジェクトの値に同期する)ことができます。

以下は CSS の値を v-bind CSS 関数を使ってデータオブジェクトの color の値に同期させる例です。初期状態では文字色は green ですが、ボタンをクリックすると red になります。

HelloFoo.vue
<script>
export default {
  data() {
    return {
      color: 'green'
    }
  },
  methods: {
    onclick() {
      this.color = 'red'
    }
  }
}
</script>

<template>
  <div>
    <p class="hello">Hello Foo !</p>
    <button type="button" v-on:click="onclick">Change Color</button>
  </div>
</template>

<style scoped>
.hello {
  /* v-bind CSS 関数で動的に値を変更 */
  color: v-bind(color);  /* または v-bind('color') */
}
</style>

<script setup> 構文の場合

以下は、前述の例を <script setup> 構文で書き換えたものです。

以下の場合、v-bind CSS 関数の引数に指定する JavaScript 式(theme.color)は引用符で囲む必要があります。

<script setup>
import { reactive } from 'vue';

// reactive メソッドでオブジェクトをリアクティブに
const theme = reactive({
  color: 'green'
});

const onclick = () => {
  theme.color = 'red'
}
</script>

<template>
  <div>
    <p class="hello">Hello Foo !</p>
    <button type="button" v-on:click="onclick">Change Color</button>
  </div>
</template>

<style scoped>
.hello {
  /* この場合、引数の JavaScript 式は引用符で囲む必要があります */
  color: v-bind('theme.color');
}
</style>

以下の場合、v-bind CSS 関数の引数に指定する color は引用符で囲んでも、囲まなくても大丈夫です。

<script setup>
import { ref } from 'vue';
// ref メソッドでリアクティブに
const color = ref('green');

const onclick = () => {
  // ref オブジェクトの value プロパティを操作
  color.value = 'red'
}
</script>

<template>
  <div>
    <p class="hello">Hello Foo !</p>
    <button type="button" v-on:click="onclick">Change Color</button>
  </div>
</template>

<style scoped>
.hello {
  color: v-bind(color);   /* または v-bind('color') */
}
</style>

setup メソッドの場合

以下は、<script> ブロックで setup メソッドを使って書き換えた例です。

<script>
import { reactive } from 'vue';
export default {
  // setup メソッド
  setup(props, context) {
    // reactive メソッドでオブジェクトをリアクティブに
    const theme = reactive({
      color: 'green'
    });
    const onclick = () => {
      theme.color = 'red'
    }
    // 定義したプロパティを返す
    return {
      theme,
      onclick
    }
  }
}
</script>

<template>
  <div>
    <p class="hello">Hello Foo !</p>
    <button type="button" v-on:click="onclick">Change Color</button>
  </div>
</template>

<style scoped>
.hello {
  /* v-bind CSS 関数で動的に値を変更 */
  color: v-bind('theme.color');
}
</style>