Vueのコア機能と基本ライブラリ

Edit

はじめに

この記事の対象

  • Vue.jsを使ったWebアプリケーションの開発環境を構築する方法がわからない
  • Vue.js触ってみたい
  • Vue 3とTypeScriptを組み合わせて使ってみたい

この記事の読み方

  • Vueの機能について勉強する
    • テンプレート構文・データバインディング
    • コンポーネント化
    • Router
    • Vuex
  • Todoリストを作ってみる
    • Vueの機能に最低限触れているので、Todoリストに編集・削除機能などはない
  • Linuxコマンドは先頭に$をつけて表示している

Vueのコア機能と基本ライブラリ一覧

以下のようなVueのコア機能とライブラリを使って簡易的なWebアプリケーションを作成する

テンプレート構文

  • 展開
    • Mustache構文
      • 「{{ 」と「}}」でVueインスタンスが扱っているデータを挟んで表示させる
  • ディレクティブ
    • 「v-」で始まる属性
ディレクティブ 機能
v-model Vueインスタンスで扱っている変数とフォームの値を連動させる
v-on 発火させてたいタイミングのイベントとVueインスタンス内の関数を指定する
v-bind 属性を動的に設定する
v-for Vueインスタンスで扱っている配列、オブジェクトの中の値を繰り返しレンダリングする
v-if Vueインスタンスで扱っている値に応じて条件分岐し、表示を切り替える

コンポーネント

名前付きの再利用可能なVueインスタンス

Vue Router

  • ページ遷移を可能にする

Vuex

  • 状態管理をする

環境構築

  • 使用する技術
    • Docker
    • Flask(Pythonのライブラリ)
      • APIサーバとして使う
  • ファイル構成
    • .
      ├── api
      │   ├── Dockerfile
      │   ├── requirements.txt
      │   └── scripts
      │       └── app.py
      ├── docker-compose.yml
      └── web
          └── Dockerfile
      
  • docker-compose.yml
    • version: '3'
      
      services:
        web:
          build: ./web
          container_name: web
          ports:
            - "8080:8080"
          volumes:
            - "./web:/projects"
          tty: true
        api:
          build: ./api
          container_name: api
          ports:
            - "5000:5000"
          volumes:
            - "./api/:/api"
          tty: true
      

api

  • Dockerfile
    • FROM python:3.10
      RUN mkdir api
      COPY ./requirements.txt /api/
      RUN pip install -r /api/requirements.txt
      WORKDIR /api/scripts/
      CMD ["python", "app.py"]
      
    • コンテナが立った時に自動でAPIサーバーが稼働するようにする
  • requirements.txt
    • flask
      
    • アプリケーションを稼働させるのに必要なPythonモジュール
  • app.py
    • from flask import Flask, jsonify
      
      app = Flask(__name__)
      app.config['JSON_AS_ASCII'] = False
      
      @app.route('/doing')
      def doing():
          message = "To-Do List!"
          return jsonify({'message': message})
      
      @app.route('/done')
      def done():
          message = "To-Do List?"
          return jsonify({'message': message})
      
      if __name__ == "__main__":
          app.run(host='0.0.0.0', debug=True)
      
    • 「<url>/doing」にhttpリクエストを投げると「To-Do List!」をmessageとして登録しているjsonオブジェクトを返す
    • 「<url>/done」にhttpリクエストを投げると「To-Do List?」をmessageとして登録しているjsonオブジェクトを返す

web

  • Dockerfile
    • FROM node:16
      RUN yarn global add @vue/cli
      WORKDIR /projects
      

コマンド実行

  • ターミナル
    • $ docker-compose up -d
      • 直下ディレクトリの中にあるdocker-compose.ymlを基に、サービスを立てる
    • $ docker-compose exec web bash
      • webというサービスを稼働させているコンテナにログイン
  • Dockerコンテナ内
    • $ vue create todo_list
      • todo_listという名前のWebアプリケーションを作成する
      • 選択するオプション
        •  ? Please pick a preset: Manually select features
           ? Check the features needed for your project: Choose Vue version, Babel, TS, Router, Vuex, Linter
           ? Choose a version of Vue.js that you want to start the project with 3.x
           ? Use class-style component syntax? No
           ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
           ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
           ? Pick a linter / formatter config: Airbnb
           ? Pick additional lint features: Lint on save
           ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
           ? Save this as a preset for future projects? No
           ? Pick the package manager to use when installing dependencies: Yarn
          
      • 上記のオプションを選択することで、TypeScriptに対応している、Vue Router, Vuerを使ったWebアプリケーションを作ることができる
        • Vue Router
          • ページ遷移ができる
        • Vuex
          • 状態管理をしてくれる
          • API通信などの非同期処理をする時には必須
      • Linterを入れる(Airbnb)
        • コード規則に違反していることを指摘してくれるもの
    • $ cd todo_list
    • $ yarn add axios
      • APIサーバとやり取りをするためのパッケージを追加する
    • $ vue add tailwind
      • tailwind.config.jsの生成オプションではminimalを選択する
    • $ yarn serve
      • http://localhost:8080/にアクセスすると、以下のようなWebページが表示される 0.png
      • Webサーバを立ち上げたままにして、ローカルのファイルを編集して開発する

ファイル作成・編集

  • src/assetsの中にある「tailwind.css」を以下のように編集する
    • tailwind.css
      • @tailwind base;
        
        @tailwind components;
        
        @tailwind utilities;
        
        body {
          @apply bg-gray-900;
        }
        
      • 以下のような画面になる 1.png
  • $ mkdir src/types
  • src/typesの中に「ToDo.ts」というファイルを作成して以下のように編集する
    • ToDo.ts
      • interface ToDo {
          id: number,
          title: string,
          doing: boolean,
          done: boolean,
        }
        
        export default ToDo
        
    • ここで定義されている型はsrc/store/index.ts, src/components/todo/ToDoList.vueで使用される
  • src/storeの中にある「index.ts」というファイルを作成して以下のように編集する
    • index.ts
      • import axios from 'axios';
        import { createStore } from 'vuex';
        import ToDo from '@/types/ToDo';
        
        export type State = { toDoList: ToDo[], nextId: number, title: string }
        
        const state: State = { toDoList: [], nextId: 0, title: 'To-Do List' };
        
        export const store = createStore({
          state,
          mutations: {
            /* eslint no-shadow: ["error", { "allow": ["state"] }] */
            addToDo(state: State, toDo: ToDo) {
              state.toDoList.push(toDo);
              state.nextId += 1;
            },
            setMessage(state: State, title: string) {
              state.title = title;
            },
          },
          actions: {
            addToDo({ commit }, toDo) {
              commit('addToDo', toDo);
            },
            getTitle({ commit }, status) {
              /* eslint-disable-next-line */
              axios.get('http://localhost:5000/' + status).then((response) => {
                commit('setMessage', JSON.parse(JSON.stringify(response.data)).message);
              });
            },
          },
          getters: {
            /* eslint no-shadow: ["error", { "allow": ["state"] }] */
            toDoList(state: State) {
              return state.toDoList;
            },
            title(state: State) {
              return state.title;
            },
          },
        });
        
    • 状態管理をしてくれるVuexを使っている
      • 非同期処理はVuexに任せるのがいい
      • Vuexのガイド
  • $ mkdir src/components/todo
  • src/components/todoの中に「Title.vue」、「ToDoList.vue」というファイルを作成して以下のように編集する
    • Title.vue
      • <template>
          <div class="title">
            <h1>{{ store.getters.title }}</h1>
          </div>
        </template>
        
        <script lang="ts">
        import { defineComponent } from 'vue';
        import { useStore } from 'vuex';
        
        export default defineComponent({
          name: 'Title',
          setup() {
            const store = useStore();
            return { store };
          },
        });
        </script>
        
        <style scoped>
          .title {
            @apply font-bold text-center container mx-auto my-10 w-80 h-10 text-green-400 text-5xl;
          }
        </style>
        
    • ToDoList.vue
      • <template>
          <div class="to-do-list">
            <div class="add">
              <div class="add-column">
                <div class="attribute-name">
                  <p>Title</p>
                </div>
                <div class="small-column">
                  <input v-model="toDoTitle" class="to-do-title-input">
                </div>
              </div>
              <div class="add-column">
                <div class="attribute-name">
                  <p>Status</p>
                </div>
                <div class="small-column">
                  <button v-on:click='activateDoing' v-bind:class="{'selected': doing, 'unselected': done}">
                    Doing
                   </button>
                  <button v-on:click='activateDone' v-bind:class="{'selected': done, 'unselected': doing}">
                    Done
                  </button>
                </div>
              </div>
              <div class="add-column">
                <button v-on:click='addToDo' class="add-button">Add</button>
              </div>
            </div>
          </div>
          <div ref="list" class="list">
            <div v-for="todo in store.getters.toDoList" :key="todo.id">
              <div class="to-do">
                <div class="column">
                  <div class="to-do-title">
                    <p> {{ todo.title }} </p>
                  </div>
                  <div class="to-do-status">
                    <p v-if=todo.doing>Doing</p>
                    <p v-if=todo.done>Done</p>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </template>
        
        <script lang="ts">
        import {
          defineComponent, ref, onUpdated, nextTick,
        } from 'vue';
        import { useStore } from 'vuex';
        import ToDo from '@/types/ToDo';
        
        export default defineComponent({
          name: 'ToDoList',
          setup() {
            const store = useStore();
            const toDoTitle = ref<string>('');
            const doing = ref<boolean>(true);
            const done = ref<boolean>(false);
            const list = ref<HTMLImageElement>();
        
            const activateDoing = () => {
              doing.value = true;
              done.value = false;
            };
            const activateDone = () => {
              doing.value = false;
              done.value = true;
            };
            const addToDo = () => {
              const toDo: ToDo = {
                id: store.state.nextId,
                title: toDoTitle.value,
                doing: doing.value,
                done: done.value,
              };
              store.commit('addToDo', toDo);
              toDoTitle.value = '';
              if (doing.value) {
                store.dispatch('getTitle', 'doing');
              } else if (done.value) {
                store.dispatch('getTitle', 'done');
              }
            };
        
            onUpdated(() => {
              nextTick(() => {
                if (!list.value) return;
                list.value.scrollTop = list.value.scrollHeight;
              });
            });
        
            return {
              store, toDoTitle, doing, done, activateDoing, activateDone, addToDo, list,
            };
          },
        });
        </script>
        
        <style scoped>
          .add {
            @apply bg-gray-500 container mx-auto rounded-xl my-10 w-96 h-48;
          }
          .add-column{
            @apply flex items-center justify-around w-full h-16;
          }
          .column{
            @apply flex items-center justify-around w-full h-full;
          }
          .attribute-name{
            @apply font-light text-gray-100 text-center text-2xl bg-gray-900 rounded-xl w-24 h-8;
          }
          .small-column{
            @apply flex items-center justify-between w-48 h-8;
          }
          .to-do-title-input{
            @apply text-center bg-gray-100 rounded-xl w-full h-full;
          }
          .unselected{
            @apply font-light text-gray-300 bg-gray-100 rounded-xl w-20 h-full;
          }
          .selected{
            @apply font-light text-gray-900 bg-gray-100 rounded-xl w-20 h-full;
          }
          .add-button{
            @apply font-light text-gray-100 text-center text-2xl bg-gray-900 rounded-xl w-24 h-10;
          }
          .list {
            @apply overflow-auto container mx-auto rounded-xl my-10 w-96 h-96;
          }
          .to-do {
            @apply bg-gray-500 rounded-xl my-2 w-full h-16;
          }
          .to-do-title{
            @apply font-light text-gray-100 text-center leading-10
                   text-2xl bg-gray-900 rounded-xl w-48 h-10;
          }
          .to-do-status{
            @apply font-light text-gray-900 leading-8 bg-gray-100 rounded-xl w-20 h-8;
          }
        </style>
        
    • コンポーネントという機能を使っている
      • Title、ToDoListというコンポーネントを定義した
      • 利点
        • 違うページで共通のパーツを使う時に再利用可能である
        • ドキュメントの構成(html)、動きやロジック(JavaScript, TypeScript)、見た目(css)をコンポーネントに閉じることができる
      • ガイド
    • テンプレート構文を使っている
      • ディレクティブ(v-から始まる属性)
        • v-model
          • Vueインスタンスで扱っている変数とフォームの値を連動させる
          • ガイド
            • 英語を少しスクロールしたところにある
            • 日本語を少しスクロールしたところにある
        • v-on
          • 発火させてたいタイミングのイベントとVueインスタンス内の関数を指定する
          • ガイド
        • v-bind
        • v-for
          • Vueインスタンスで扱っている配列、オブジェクトの中の値を繰り返しレンダリングする
          • ガイド
        • v-if
          • Vueインスタンスで扱っている値に応じて条件分岐し、表示を切り替える
          • ガイド
      • マスタッシュ構文
        • Vueインスタンスが扱っているデータをページ上に展開する
        • バインドさせたい表示させたいデータを{{ }}で囲う
        • ガイド
  • src/viewsの中にある「Home.vue」を編集し、「ToDo.vue」というファイルを作成して以下のように編集する
    • src/views/Home.vue
      • <template>
          <div class="home">
            <div class="logo">
              <img alt="Vue logo" src="../assets/logo.png">
            </div>
            <div class="head">
              <h1>Vueのチュートリアル</h1>
            </div>
            <div class="text">
              <p>
                Vueのディレクティブ、マスタッシュ構文、Vue-Router、Vuexなどの様々な機能を使っています。
              </p>
              <p>
                TypeScriptに対応した書き方で、CSSフレームワークにはTailWind CSSを使用しています。
              </p>
            </div>
          </div>
        </template>
        
        <script>
        export default {
          name: 'Home',
        };
        </script>
        
        <style scoped>
          .logo{
            @apply flex justify-center w-full;
          }
          .head{
            @apply font-light text-center text-gray-100 text-4xl;
          }
          .text{
            @apply font-light text-center text-gray-100;
          }
        </style>
        
    • src/views/ToDo.vue
      • <template>
          <div class="todo">
            <Title />
            <ToDoList />
          </div>
        </template>
        
        <script>
        import Title from '@/components/todo/Title.vue';
        import ToDoList from '@/components/todo/ToDoList.vue';
        
        export default {
          name: 'ToDo',
          components: {
            Title, ToDoList,
          },
        };
        </script>
        
    • Vue Routerを使っている
      • ページ遷移ができるようになる
      • src/viewsというフォルダが生成される
        • そのフォルダで実際にユーザから見ることができるページコンポーネントを書く
    • Vue Routerのガイド
  • src/routerの中にある「index.ts」を以下のように編集する
    • index.ts
      • import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
        import Home from '../views/Home.vue';
        
        const routes: Array<RouteRecordRaw> = [
          {
            path: '/',
            name: 'Home',
            component: Home,
          },
          {
            path: '/todo',
            name: 'ToDo',
            // route level code-splitting
            // this generates a separate chunk (about.[hash].js) for this route
            // which is lazy-loaded when the route is visited.
            component: () => import(/* webpackChunkName: "about" */ '../views/ToDo.vue'),
          },
        ];
        
        const router = createRouter({
          history: createWebHistory(process.env.BASE_URL),
          routes,
        });
        
        export default router;
        
    • ここではページ遷移に必要な情報(URLのパス名、ページコンポーネントの名前)を定義している
  • srcの中にある「App.vue」を以下のように編集する
    • App.vue
      • <template>
          <div id="nav">
            <router-link to="/">Home</router-link> |
            <router-link to="/ToDo">To-Do List</router-link>
          </div>
          <router-view/>
        </template>
        
        <style>
        #app {
          font-family: Avenir, Helvetica, Arial, sans-serif;
          -webkit-font-smoothing: antialiased;
          -moz-osx-font-smoothing: grayscale;
          text-align: center;
          color: #2c3e50;
        }
        
        #nav {
          padding: 30px;
        }
        
        #nav a {
          font-weight: bold;
          color: #2c3e50;
        }
        
        #nav a.router-link-exact-active {
          color: #42b983;
        }
        </style>
        
  • srcの中にある「main.ts」を以下のように編集する
    • main.ts
      • import { createApp } from 'vue';
        import App from './App.vue';
        import router from './router';
        import { store } from './store/index';
        import './assets/tailwind.css';
        
        createApp(App).use(store).use(router).mount('#app');
        
  • キャッシュの影響でエラーが出力されている可能性もあるので、コンテナのWebサーバを「Ctrl+c」で止めて、もう一度$ yarn serve
    • 現時点で以下のようなページを見れるようになっている 2.png 3.png
comments powered by Disqus