【学習メモ】Vue.js入門 基礎から実践アプリケーション開発まで その5

  • 5.3 カスタムディレクティブ
  • 5.4 描画関数
  • 5.5 ミックスイン

はスキップ。もう少し基礎的な部分を理解したらもう一度読み直そうと思います。。

6 単一コンポーネントによる開発

6.2 単一コンポーネントとは

Vue.js では、上記のような単一ファイルコンポーネントのことを、単一コンポーネントの英語の呼び名の Single File Components の頭文字から、SFCsfc と略して呼ぶことがあります。また、Vueコンポーネント(Vue Components) と呼ぶことがあります。

は同じ。

6.5 単一コンポーネントの動作を体験する

Vue CLIネコ本のとき にやったようなプロジェクト単位の作成でないと使えないと思っていましたが、単一のファイルでも vue serve を使えば使用が可能。
ちょっと試してみたいときなどにはいいですね。

hello.vue ファイルの中身

<template>
  <p class="message">メッセージ: {{ msg }}</p>
</template>

<script>
  export default {
    data() {
      return {
        msg: "こんにちは"
      };
    }
  };
</script>

<style>
  .message {
    color: red;
  }
</style>

コマンドで、hello.vue ファイルの親フォルダに移動して、

$ vue serve hello.vue --open

addon が必要と怒られたので、

$ yarn global add @vue/cli-service-global

eslint がないと怒られたので、

Error: Cannot find module 'eslint-plugin-vue'
$ npm install --save eslint eslint-plugin-vue@nexeslint-config-vue

でもう一度、

$ vue serve hello.vue --open

とすると、http://localhost:8080/ の URL で hello.vue が展開される。

ここでは、Vue CLI から、webpack と Vue Loader を使ってバンドルしています。

6.6 単一ファイルコンポーネントの機能

6.6.1 外部ファイルのインポート

src 属性で以下のように外部ファイルの内容をインポートできます。 パスには、当該の単一ファイルコンポーネントからの相対パスを指定してください。

<template src="./template.html"></template>
<script src="./script.js"></script>
<style src="./style.css"></style>

6.6.2 スコープ付き CSS

<style> ブロックに scoped 属性を付与することで、その単一ファイルコンポーネント内の要素にのみ適用するカプセル化を実現します。Vue.js の単一ファイルコンポーネントでは、スコープ付き CSS(Scoped CSS)と呼んでいます。

<style scoped>
  .message {
    color: red;
  }
</style>

<!-- グローバルなCSSも同一ファイルに書ける -->
<style>
  .message {
    font-size: 30px;
  }
</style>

とスコープすると、下記のように出力される。(例)

...
<head>
  <style type="text/css">
    .message {
      font-size: 30px;
    }
    .message[data-v-3bcf9374] {
      color: red;
    }
  </style>
</head>
...

6.6.3 CSS モジュール

CSS モジュールでも名前空間衝突を防止できる。

.vueファイル

<template>
  <p :class="$style.message">メッセージ: {{ msg }}</p>
</template>

<script>
  export default {
    data() {
      return {
        msg: "こんにちは"
      };
    }
  };
</script>

<style module>
  .message {
    color: blue;
  }
</style>

出力結果

<!-- 省略 -->
...
<style type="text/css">
  .hello_message_1mO00 {
    color: blue;
  }
</style>
<!-- 省略 -->
<p class="hello_message_1mO00">メッセージ: こんにちは</p>
...
<!-- 省略 -->

スコープ付き CSS

接頭辞 data-v-ハッシュ値で構成されたカスタムデータ属性によりカプセル化

CSS モジュール

一意なスタイル識別子によってカプセル化
スタイル識別子オブジェクトは算出プロパティ $style としてアクセス可能。

【学習メモ】Vue.js入門 基礎から実践アプリケーション開発まで その4

5 Vue.js の高度な機能

5.1 トランジションアニメーション

5.1.1 transition ラッパーコンポーネント

transition コンポーネントは、自身が囲んでいるコンポーネントあるいは要素が出入り(enter/leave)する際にトランジションを追加します。出入りは、以下の場合に起きます。

5.1.2 トランジションクラス

<transition name="fade"> と指定すれば、v-enterではなく、fade-enterというプレフィクスが付与される。

トランジションクラスの一覧

v-enter

要素が挿入される前に付与され、アニメーション開始時に削除されるクラスです。挿入のアニメーションの初期スタイルを適用するために使用します。

v-enter-to

挿入のアニメーションの開始時に付与され、アニメーション終了後に削除されるクラスです。挿入のアニメーションの終了時のスタイルを適用するために使用します。

v-enter-active

要素の挿入前からアニメーション終了まで付与されるクラスです。トランジションの設定を書くために使用します。

v-leave

削除のアニメーションの開始前に付与され、アニメーション開始時に削除されるクラスです。削除時のアニメーションの初期スタイルを適用するために使用します。

v-leave-to

削除のアニメーションの開始前に付与され、アニメーション終了時に削除されるクラスです。削除時のアニメーションの終了時のスタイルをあてるために使用します。

v-leave-active

削除のアニメーションの開始前から終了後まで付与されるクラスです。トランジションの設定を書くために使用します。

5.1.3 fade トランジションの実装

<div id="app">
  <button v-on:click="isShown = !isShown">表示の切り替え</button>
  <transition> <p v-show="isShown">Hello, world!</p> </transition>
</div>
.v-enter-active,
.v-leave-active {
  transition: opacity 500ms ease-out;
}

.v-enter {
  opacity: 0;
}
.v-enter-to {
  opacity: 1;
}

.v-leave {
  opacity: 1;
}
.v-leave-to {
  opacity: 0;
}
// Vue のマウント
var app = new Vue({
  el: "#app",
  data: function() {
    return {
      isShown: false
    };
  }
});

5.2 スロット

5.2.1 単一スロット

<my-button> 内にコンテンツがない場合は、<slot> 内の OK が表示される。

<div id="app">
  <!-- コンテンツありでコンポーネントを設置する -->
  <my-button>送信する</my-button>
  <!-- コンテンツ無しでコンポーネントを設置する -->
  <my-button></my-button>
</div>
var MyButton = {
  template: `
    <button>
      <!-- 親コンポーネントで渡されたコンテンツに差し替えられる -->
      <slot>OK</slot>
    </button>
  `
};

// Vue のマウント
var app = new Vue({
  el: "#app",
  components: {
    MyButton: MyButton
  }
});

5.2.2 名前付きスロット

スロットには slot 要素の name属性で名前を指定できます。これによって名前で指定した特定の箇所にスロットを挿入することが可能です。

<div id="app">
  <my-page>
    <!-- name 属性値が header の <slot> と置き換わるコンテンツ -->
    <h1 slot="header">This is my page</h1>
    <!-- 単一スロットと置き換わるコンテンツ -->
    <p>ここにテキストが入ります。</p>
    <!-- name 属性値が footer の <slot> と置き換わるコンテンツ -->
    <p slot="footer">This is footer</p>
  </my-page>
</div>
var MyPage = {
  template: `
    <div>
      <header>
        <!-- ヘッダーのスロット(名前付きスロット) -->
        <slot name="header"></slot>
      </header>
      <main>
        <!-- ボディのスロット(単一スロット) -->
        <slot></slot>
      </main>
      <footer>
        <!-- フッターのスロット(名前付きスロット) -->
        <slot name="footer"></slot>
      </footer>
    </div>
  `
};

// Vue のマウント
var app = new Vue({
  el: "#app",
  components: {
    MyPage: MyPage
  }
});

【学習メモ】Vue.js入門 基礎から実践アプリケーション開発まで その3

4 Vue Router を活用したアプリケーション開発

4.2 ルーティングの基礎

4.2.2 ルーティング設計

ルートルーターコンストラク を用います。

<div id="app">
  <router-link to="/top">トップページ</router-link>
  <router-link to="/users">ユーザー一覧ページ</router-link>
  <router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2.5.17"></script>
<script src="https://unpkg.com/vue-router@3.0.1"></script>
<script src="./assets/js/main.js"></script>
// ルーターコンストラクタ
// ルートオプションを渡してルーターインスタンスを生成
var router = new VueRouter({
  // ルート定義
  routes: [
    {
      path: "/top",
      component: {
        template: `<div>トップページです。</div>`
      }
    },
    {
      path: "/users",
      component: {
        template: `<div>ユーザー一覧ページです。</div>`
      }
    }
  ]
});

// Vue のマウント
// ルーターのインスタンスをrootとなるVueインスタンスに渡す
var app = new Vue({
  router: router,
  el: "#app"
});

4.4 サンプルアプリケーションの実装

<div id="app">
  <nav v-cloak>
    <!-- `router-link` によるナビゲーション定義 -->
    <router-link to="/top">トップページ</router-link>
    <router-link to="/users">ユーザー一覧ページ</router-link>
    <router-link to="/users/new?redirect=ture">新規ユーザー登録</router-link>
    <router-link to="/login" v-show="!Auth.loggedIn()">ログイン</router-link>
    <router-link to="/logout" v-show="Auth.loggedIn()">ログアウト</router-link>
  </nav>
  <router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2.5.17"></script>
<script src="https://unpkg.com/vue-router@3.0.1"></script>

<!-- ユーザー一覧ページのテンプレート -->
<script type="text/x-template" id="user-list">
  <div>
    <div class="loading" v-if="loading">読み込み中</div>
    <div v-if="error" class="error">
      {{ error }}
    </div>
    <!-- usersがロードされたら各ユーザーの名前を表示する -->
    <div v-for="user in users" :key="user.id">
      <h2>{{ user.name }}</h2>
    </div>
  </div>
</script>

<!-- ユーザー詳細ページのテンプレート -->
<script type="text/x-template" id="user-detail">
  <div>
    <div class="loading" v-if="loading">読み込み中</div>
    <div v-if="error" class="error">
      {{ error }}
    </div>
    <!-- users がロードされたら各ユーザの名前を表示する -->
    <div v-if="user">
      <h2>{{ user.name }}</h2>
      <p>{{ user.description }}</p>
    </div>
  </div>
</script>

<!-- ユーザー作成ページのテンプレート -->
<script type="text/x-template" id="user-create">
  <div>
    <div class="sending" v-if="sending">Sending</div>
    <div>
      <h2>新規ユーザー作成</h2>
      <div>
        <label>名前: </label>
        <input type="text" v-model="user.name">
      </div>
      <div>
        <label>説明文: </label>
        <textarea v-model="user.description"></textarea>
      </div>
      <div v-if="error" class="error">
        {{ error }}
      </div>
      <div>
        <input type="button" @click="createUser" value="送信">
      </div>
    </div>
  </div>
</script>

<!-- ログインページのテンプレート -->
<script type="text/x-template" id="login">
  <div>
    <h2>Login</h2>
    <p v-if="$route.query.redirect">
      ログインしてください
    </p>
    <form @submit.prevent="login">
      <label><input v-model="email" placeholder="email"></label>
      <label><input v-model="pass" placeholder="password" type="password"></label>
      <br>
      <button type="submit">ログイン</button>
      <p v-if="error" class="error">ログインに失敗しました</p>
    </form>
  </div>
</script>

<script src="./assets/js/main.js"></script>
//////////////////////////
// ユーザー一覧
//////////////////////////
var getUsers = function(callback) {
  setTimeout(function() {
    callback(null, [
      {
        id: 1,
        name: "Takuya Tejima"
      },
      {
        id: 2,
        name: "Yohei Noda"
      }
    ]);
  }, 1000);
};

var UserList = {
  template: "#user-list",
  data: function() {
    return {
      loading: false,
      users: function() {
        return []; // 初期値の空配列
      },
      error: null
    };
  },
  // 初期化時にデータを取得する
  created: function() {
    this.fetchData();
  },
  // $route の変更をwatchすることでルーティングが変更されたときに再度データを取得
  watch: {
    $route: "fetchData"
  },
  methods: {
    fetchData: function() {
      this.loading = true;
      // 取得したデータの結果をusersに格納する
      getUsers(
        function(err, users) {
          this.loading = false;
          if (err) {
            this.error = err.toString();
          } else {
            this.users = users;
          }
        }.bind(this)
      );
    }
  }
};

//////////////////////////
// ユーザー詳細
//////////////////////////
var userData = [
  {
    id: 1,
    name: "Takuya Tejima",
    description: "東南アジアで働くエンジニアです。"
  },
  {
    id: 2,
    name: "Yohei Noda",
    description: "アウトドア・フットサルが趣味のエンジニアです。"
  }
];

var getUser = function(userId, callback) {
  setTimeout(function() {
    var filteredUsers = userData.filter(function(user) {
      return user.id === parseInt(userId, 10);
    });
    callback(null, filteredUsers && filteredUsers[0]);
  }, 1000);
};

var UserDetail = {
  template: "#user-detail",
  data: function() {
    return {
      loading: false,
      user: null,
      error: null
    };
  },
  // 初期化時にデータを取得する
  created: function() {
    this.fetchData();
  },
  // $route の変更をwatchすることでルーティングが変更されたときに再度データを取得
  watch: {
    $route: "fetchData"
  },
  methods: {
    fetchData: function() {
      this.loading = true;
      // `this.$route.params.userId` に現在のURL上のパラメータに対応した userIdが格納される
      getUser(
        this.$route.params.userId,
        function(err, user) {
          this.loading = false;
          if (err) {
            this.error = err.toString();
          } else {
            this.user = user;
          }
        }.bind(this)
      );
    }
  }
};

//////////////////////////
// ユーザー作成
//////////////////////////
// 擬似的にAPI経由で情報を更新したようにする
// 実際のWebアプリケーションではサーバーへへPOSTリクエストを行う
var postUser = function(params, callback) {
  setTimeout(function() {
    params.id = userData.length + 1;
    userData.push(params);
    callback(null, params);
  }, 1000);
};

// 新規ユーザー作成コンポーネント
var UserCreate = {
  template: "#user-create",
  data: function() {
    return {
      sending: false,
      user: this.defaultUser(),
      error: null
    };
  },
  created: function() {},
  methods: {
    defaultUser: function() {
      return {
        name: "",
        description: ""
      };
    },
    createUser: function() {
      // 入力パラメーターのバリデーション
      if (this.user.name.trim() === "") {
        this.error = "Nameは必須です";
        return;
      }
      if (this.user.description.trim() === "") {
        this.error = "Descriptionは必須です";
        return;
      }
      postUser(
        this.user,
        function(err, user) {
          this.sending = false;
          if (err) {
            this.error = null;
          } else {
            // デフォルトでフォームをリセット
            this.user = this.defaultUser();
            alert("新規ユーザーが登録されました");
            // ユーザー一覧ページに戻る
            this.$router.push("/users");
          }
        }.bind(this)
      );
    }
  }
};

//////////////////////////
// ログイン・ログアウト
//////////////////////////
var Auth = {
  login: function(email, pass, cb) {
    // ダミーデータを使った疑似ログイン
    setTimeout(function() {
      if (email === "vue@example.com" && pass === "vue") {
        // ログイン成功時はローカルストレージに taken を保存する
        localStorage.token = Math.random()
          .toString(36)
          .substring(7);
        if (cb) {
          cb(true);
        }
      } else {
        if (cb) {
          cb(false);
        }
      }
    }, 0);
  },
  logout: function() {
    delete localStorage.token;
  },
  loggedIn: function() {
    // ローカルストレージにtokenがあればログイン状態とみなす
    return !!localStorage.token;
  }
};

var Login = {
  template: "#login",
  data: function() {
    return {
      email: "vue@example.com",
      pass: "",
      error: false
    };
  },
  methods: {
    login: function() {
      Auth.login(
        this.email,
        this.pass,
        function(loggedIn) {
          if (!loggedIn) {
            this.error = true;
          } else {
            // redirect パラメータがついている場合はそのパスに遷移
            this.$router.replace(this.$route.query.redirect || "/");
          }
        }.bind(this)
      );
    }
  }
};

//////////////////////////
// ルーター設定
//////////////////////////
// ルートオプションを渡してルーターインスタンスを生成
var router = new VueRouter({
  routes: [
    {
      path: "/top",
      component: {
        template: `<div>トップページです。</div>`
      }
    },
    {
      path: "/users",
      component: UserList
    },
    {
      // ルー定義を追加
      path: "/users/new",
      component: UserCreate,
      beforeEnter: function(to, from, next) {
        // 認証されていない状態でアクセスした時はloginページに遷移する
        if (!Auth.loggedIn()) {
          next({
            path: "/login",
            query: { redirect: to.fullPath }
          });
        } else {
          // 認証済みであればそのまま新規ユーザー作成ページに進む
          next();
        }
      }
    },
    {
      path: "/users/:userId",
      component: UserDetail
    },
    {
      path: "/login",
      component: Login
    },
    {
      path: "/logout",
      beforeEnter: function(to, from, next) {
        Auth.logout();
        next("/top");
      }
    },
    {
      // 定義されていないパスへの対応。トップページへリダイレクトする。
      path: "*",
      redirect: "/top"
    }
  ]
});

//////////////////////////
// Vue のマウント
//////////////////////////
var app = new Vue({
  data: {
    Auth: Auth
  },
  router: router,
  el: "#app"
});

2019年1月 振り返り

毎月のまとめとして月次の振り返りをしようと思います。

結果

ブログ

目標:月 12 回(週 3 回)更新
結果:月 22 回 更新

読書

目標:月 1 冊
結果:月 2 冊

反省点など

Vue.js は本を何冊かやっているが、実際の案件で用いるところまでは踏み込めてない印象。
AMP など、その他の技術に関しても少し自発的に調べたがあまりモノにはなっていないかも。
スクランナー周り、コマンドやビルドなど、フロントエンドで用いるツールに関しては、会社の業務でだいぶ億劫な気持ちはなくなった。

1 月から新しい会社だったので、そちらへの慣れも必要で体調管理は気をつけて生活習慣を維持できたと思う。
少し月末にかけてペースが失速気味になってしまったところが反省点

来月に向けて

Vue.js 入門本を一通り写経し終わったら、自分で手を動かして独自の機能を持ったサイトを書いてみる。
3 月までにはポートフォリオサイトを Vue で作れるようにする。

読書に関しては技術本、デザイン(UI/UX)の本、自己啓発本 の 3 冊読めるようにしたい。 学習やアウトプットのペースはできるときに一気にやることよりも、今は一定のペース守って癖をつけることを優先でやっていきたいと思う。

【学習メモ】Vue.js入門 基礎から実践アプリケーション開発まで その2

3 コンポーネントの基礎

3.2 Vue コンポーネントの定義

3.2.1 グローバルコンポーネントの定義

<!-- グローバルコンポーネント -->
<div id="app">
  <fruits-list-title></fruits-list-title>
  <fruits-list-description></fruits-list-description>
  <fruits-list-table></fruits-list-table>
  <fruits-list></fruits-list>
</div>
// グローバルコンポーネント
Vue.component("fruits-list-title", {
  template: `<h1>一覧</h1>`
});

Vue.component("fruits-list-description", {
  template: `<p>ここに説明が入ります</p>`
});

Vue.component("fruits-list-table", {
  template: `
  <table>
    <tr>
      <th>季節</th>
      <th>フルーツ</th>
    </tr>
    <tr>
      <td>春</td>
      <td>いちご</td>
    </tr>
    <tr>
      <td>夏</td>
      <td>スイカ</td>
    </tr>
  </table>
`
});

// 子コンポーネント
Vue.component("fruits-list", {
  template: `<div><fruits-list-table></fruits-list-table></div>`
});

// Vue のマウント
var app = new Vue({
  el: "#app"
});

3.2.3 ローカルコンポーネントの定義

<!-- ローカルコンポーネント -->
<div id="app">
  <fruits-list-title></fruits-list-title>
  <fruits-list-description></fruits-list-description>
  <fruits-list-table></fruits-list-table>
  <fruits-list></fruits-list>
</div>
// ローカルコンポーネント
// Vue のマウント
var app = new Vue({
  el: "#app",
  components: {
    "fruits-list-title": {
      template: `<h1>一覧</h1>`
    },
    "fruits-list-description": {
      template: `<p>ここに説明が入ります</p>`
    },
    "fruits-list-table": {
      template: `
      <table>
        <tr>
          <th>季節</th>
          <th>フルーツ</th>
        </tr>
        <tr>
          <td>春</td>
          <td>いちご</td>
        </tr>
        <tr>
          <td>夏</td>
          <td>スイカ</td>
        </tr>
      </table>`
    }
  }
});

3.3 コンポーネント間の通信

コンポーネント → 子コンポーネント への通信: props
コンポーネント → 親コンポーネント への通信: event

3.3.1 親コンポーネントから子コンポーネントへデータ伝播

props の例

<!-- 親が `fruits-component` にマウントされたインスタンス -->
<div id="app">
  <ol>
    <!-- `v-for` で繰り返した各fruitを `props(fruits-item)に与えている` -->
    <fruits-item-name
      v-for="fruit in fruitsItems"
      :key="fruit.name"
      :fruits-item="fruit"
    ></fruits-item-name>
  </ol>
</div>
Vue.component("fruits-item-name", {
  props: {
    fruitsItem: {
      // テンプレート中ではケバブケース
      type: Object, // オブジェクトかどうか
      required: true // このコンポーネントには必須なので `true`
    }
  },
  template: "<li>{{fruitsItem.name}}</li>"
});

// Vue のマウント
var app = new Vue({
  el: "#app",
  data: {
    // 親では配列だが、`v-for` で `Object` として渡している
    fruitsItems: [{ name: "梨" }, { name: "いちご" }]
  }
});

3.3.2 子コンポーネントから親コンポーネントへデータ伝播

カスタムイベントを使用

<!-- カスタムイベントを `v-on` で補足 -->
<div id="app">
  <div v-for="fruit in fruits">
    {{ fruit.name }}:
    <counter-button v-on:increment="incrementCartStatus()"></counter-button>
  </div>
  <p>合計: {{ total }}</p>
</div>
var counterButton = Vue.extend({
  template: `<span>{{counter}}個<button v-on:click="addToCart">追加</button></span>`,
  data: function() {
    return {
      counter: 0
    };
  },
  methods: {
    addToCart: function() {
      this.counter += 1;
      this.$emit("increment"); // `increment`カスタムイベントの発火
    }
  }
});

// Vue のマウント
var app = new Vue({
  el: "#app",
  components: {
    "counter-button": counterButton
  },
  data: {
    total: 0,
    fruits: [{ name: "梨" }, { name: "いちご" }]
  },
  methods: {
    incrementCartStatus: function() {
      this.total += 1;
    }
  }
});

3.4 コンポーネント間の設計

3.3.4 ログインフォームコンポーネントの作成

<div id="login-example"><user-login></user-login></div>
// コンポーネントの定義
Vue.component("user-login", {
  template: `
  <div id="login-template">
    <div>
      <input type="text" placeholder="ログインID" v-model="userid">
    </div>
    <div>
      <input type="password" placeholder="パスワード" v-model="password">
    </div>
    <button @click="login()">ログイン</button>
  </div>
  `,
  data: function() {
    return {
      userid: "",
      password: ""
    };
  },
  methods: {
    login: function() {
      auth.login(this.userid, this.password);
    }
  }
});

var auth = {
  login: function(id, pass) {
    window.alert("userid:" + id + "\n" + "password:" + pass);
  }
};

// Vue のマウント
var app = new Vue({
  el: "#login-example"
});