本文へジャンプ

第一回 Vue.jsでWebアプリをつくろう!

Posted by MONSTER DIVE

近頃、Vue.jsのトリコになって抜け出せずにいるOhkiです。

Vue.jsとは表示部分の view層 に焦点を当てたフレームワークです。
記述が簡単なので、同じ様なフレームワークである、ReactやAngularに比べて 学習コストが低い のが良い所です。

基本的な使い方は公式サイトの日本語ドキュメントが豊富で困ることはないと思います。
ただ、Vue.jsについて検索すると初心者向けの記事はたくさん見つかるのですが、具体的なアプリの作り方はあまり無かったので、ご紹介したいと思います。

一回目ということで簡単に作れる時計アプリを一緒につくりましょう!

開発環境構築

Vue.jsには vue-cli という開発環境を簡単に構築出来るものが用意されています。

Node.jsを使用するのでインストールしていない方はインストールしてください。
nodist(Windows)、nodebrew(Mac)を使用してバージョン管理が出来るようにしておくと、色々な環境に合わせてバージョンを切り替えられるので便利です。
使用するバージョンは最新の LTS(長期サポート版)をお勧めします。(2018年6月現在 v8.11.2)

コマンドプロンプト(ターミナル)を開いて

npm install -g vue-cli

と入力するとvue-cliがインストールされます。(Macユーザーの方は sudo が必要かもしれません)

続いて、適当な場所にアプリ用のディレクトリを作り、そこにコマンドプロンプトで移動します。
そこで、

vue init webpack

と入力すると対話形式で設定できるようになっています。

Generate project in current directory?

現在のディレクトリにプロジェクトを作成するか聞かれるので、y を入力して Enter を押します。

Project name
Project description
Author

プロジェクトの名前/プロジェクトの概要/作者は適当に入力してください。

Vue build

コンパイラの有無を選択します。
「Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific HTML) are ONLY allowed in .vue files - r ender functions are required elsewhere」を選択して Enter を押します。
vue-loaderを使用している場合は、ビルド時に事前コンパイルされるようなのでランタイムのみにします。 (参考)

Install vue-router?

vue-routerを使用するか選択します。
今回は使用しないので、n を入力して Enter を押します。

Use ESLint to lint your code?

Linterを使用するか選択します。
今回は使用しないので、n を入力して Enter を押します。
複数人で開発する場合は入れた方が良いです。

Set up unit tests

ユニットテストを行うか選択します。
今回は行わないので、n を入力して Enter を押します。
大規模な開発では入れた方が良いです。

Setup e2e tests with Nightwatch?

E2Eテストを行うか選択します。
今回は行わないので、n を入力して Enter を押します。
こちらも、大規模な開発では入れた方が良いです。

Should we run `npm install` for you after the project has been created? (recommended)

Node.jsのモジュールをインストールするか選択します。
「Yes, use NPM」を選択して Enter を押します。
Yarnを使用してる方は「Yes, use Yarn」を選択しても問題ありません。

これで必要なものがインストールされるので

npm run dev

または

npm start

を実行するとローカルサーバが起動します。
http://localhost:8080
にアクセスし

↓の画像と同じ画面が表示されれば成功です!

初期画面

※表示できない場合はポートを変更してみてください。

  • config/index.js

の17行目

port: 8080,

これを、使われていないポート番号に変更してください。

おすすめエディタ

開発にはどんなエディタでも問題ないのですが、Visual Studio Code(Windows/macOS/Linux)が軽くて機能も豊富なのでおすすめです。
さらに、拡張機能でVeturをインストールするとシンタックスハイライト/コード補完/Emmetなどが使用出来るようになります。

開発開始

開発環境が出来たので、早速アプリ開発を開始しましょう!

まずは、ファイルの整理から行います。

  • src/App.vue

を開いて

<template>


</template>


<script>


</script>


<style>


</style>

一旦、空にします。

続いて

  • src/assets/logo.png
  • src/components/HelloWorld.vue

を削除します。

WEBフォントを使用したいので、

  • index.html

を開いて

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>vue-clock</title>
    <link href="https://fonts.googleapis.com/css?family=Roboto+Mono:700|Teko:600" rel="stylesheet">
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

WEBフォントを読み込ませ、手動でリロードしてください。

そして今回つくるアプリの主役である

  • src/components/Clock.vue

を作成します。(ファイル名はパスカルケースかケバブケースにする必要があります)

まずは、script部分をつくっていきます。

<script>
const zeroPadding = (num, digit) => {
  return (Array(digit).join("0") + num).slice(-digit)
}


export default {
  data() {
    return {
      date: new Date(),
    }
  },
  computed: {
    year() {
      return this.date.getFullYear()
    },
    month() {
      return zeroPadding(this.date.getMonth() + 1, 2)
    },
    day() {
      return zeroPadding(this.date.getDate(), 2)
    },
    hours() {
      return zeroPadding(this.date.getHours(), 2)
    },
    minutes() {
      return zeroPadding(this.date.getMinutes(), 2)
    },
    seconds() {
      return zeroPadding(this.date.getSeconds(), 2)
    },
  },
  mounted() {
    this.setDate()
    setInterval(() => this.setDate(), 1000)
  },
  methods: {
    setDate() {
      this.date = new Date()
    },
  },
}
</script>

ポイントはdataには date しか持たせず、hoursやminutesなどを computed を使用して取得するようにしているところです。
computedの特徴は値をキャッシュする点です。
依存するデータが更新されるまで呼び出されるときキャッシュした値を返します。
今回の場合、1秒に1回dateを更新しているのであまり意味がないのですが...

次は画面に表示するtemplate部分をつくります。

<template>
  <div>
    <div class="container">
      <p class="date">{{ year }}/{{ month }}/{{ day }}</p>
      <div class="time">
        <p class="time-item hours">{{ hours }}</p>
        <p class="time-item minutes">{{ minutes }}</p>
        <p class="time-item seconds">{{ seconds }}</p>
      </div>
    </div>
  </div>
</template>

テキスト部分にMustache構文(二重中括弧)を使用するとそのなかで単一の式が書けます。
通常は変数を指定して表示させることが多いです。
また、templateの中で「this」は必要ありません。

最後に装飾を行うstyle部分をつくります。
Vue.jsにはstyleにscopedという機能があり、そのコンポーネントのみにスタイルを当てることが出来ます。
それ以外は通常のCSSと同じなのでお好きに変更して構いません。
コツはコンポーネント自身には大きさや位置を指定せず、親コンポーネントが大きさや位置を指定するようにすると使い回しが楽に出来るようになります。

<style scoped>
.container {
  background-color: #3a4a5e;
  padding: 2%;
}


.date {
  text-align: right;
  color: #fff;
  font-family: 'Teko', sans-serif;
  font-size: 4rem;
  letter-spacing: .1em;
  margin: .0em 0;
  line-height: 1;
}


.time {
  display: flex;
}


.time-item {
  display: flex;
  justify-content: center;
  align-items: center;
  flex: 1 1;
  height: 100px;
  position: relative;
  z-index: 1;
  padding: 0.5em;
  margin: 3px;
  color: #fff;
  font-family: 'Roboto Mono', monospace;
  font-size: 3rem;
  line-height: 1;
  background-color: #48b883;
  box-sizing: border-box;
}


.time-item:before {
  position: absolute;
  right: 5px;
  bottom: 1px;
  z-index: 1;
  color: #3a4a5e;
  font-family: 'Teko', sans-serif;
  font-size: 1.4rem;
  letter-spacing: .05em;
}


.hours:before {
  content: "Hours";
}


.minutes:before {
  content: "Minutes";
}


.seconds:before {
  content: "Seconds";
}
</style>

注意点 は、scopedを使用しても子コンポーネントのルート要素には親コンポーネントのスタイルも当たるようになっているので、クラス名が同じだと干渉 してしまい意図しない結果になってしまうことがあります。
例えば、

子コンポーネント

<template>
  <div class="root">
    <p>Child</p>
  </div>
</template>


<style scoped>
.root {
  width: 500px;
}
</style>

親コンポーネント

<template>
  <div class="root">
    <Child/>
  </div>
</template>


<style scoped>
.root {
  padding: 10px;
}
</style>

この場合、子コンポーネントにもpaddingが効いてしまいます。

なので、それを防ぐためにコンポーネントのルート要素には自身でスタイルを当てず親コンポーネントからスタイルを当てる事をオススメします。
基本的には大きさや位置を指定するのがいいですね。

子コンポーネント

<template>
  <div>
    <p>Child</p>
  </div>
</template>

親コンポーネント

<template>
  <div>
    <div class="container">
      <Child class="child"/>
    </div>
  </div>
</template>


<style scoped>
.container {
  padding: 10px;
}


.child {
  width: 500px;
}
</style>

こんな感じにすると上手くいきます。
あくまで個人的な考え方なので合わせる必要はありません。

続いて

  • src/App.vue

を開いてClockコンポーネントを表示させます。

まずはscript部分をつくります。

<script>
import Clock from "@/components/Clock"


export default {
  components: {
    Clock,
  },
}
</script>

AppコンポーネントにimportでClockコンポーネントを読み込み、componentsに登録して使用出来るようにします。

import Clock from "@/components/Clock"

これの@は何かというとwebpackにある resolveのaliasという機能です。
通常は相対パスで書かなければいけないところですが、こうする事でどんな階層にあるファイルからでも、簡単に指定する事が出来ます。

  • build/webpack.base.conf.js

上記のファイルでsrcディレクトリが指定されています。

続いてtemplateです。

<template>
  <div>
    <Clock class="clock"/>
  </div>
</template>

はい、非常にシンプルですね。
AppコンポーネントからClockコンポーネントの大きさを指定するためclassを付けてあります。

最後にstyleです。

<style scoped>
.clock {
  width: 80%;
  max-width: 500px;
  margin: 30px auto;
}
</style>


<style>
html {
  font-size: 62.5%;
}


body {
  margin: 0;
}


p {
  margin: 0;
}
</style>

styleにscopedを付けないと全体に適用されます。これは通常のCSSと同じということですね。
ですので、そこにリセット系のスタイルを適用させましょう。

これで↓のような画面が表示されるはずです。

仮完成画面

これで完成!と言いたいところですが、これだけだと物足りないので、日本以外の時間も表示できるようにしてみましょう!

  • src/components/Clock.vue

のscriptにpropsの追加とソースの調整を行います。

<script>
const zeroPadding = (num, digit) => {
  return (Array(digit).join("0") + num).slice(-digit)
}


export default {
  props: ["location", "diff"], // ←追加
  data() {
    return {
      date: new Date(),
    }
  },
  computed: {
    year() {
      return this.date.getFullYear()
    },
    month() {
      return zeroPadding(this.date.getMonth() + 1, 2)
    },
    day() {
      return zeroPadding(this.date.getDate(), 2)
    },
    hours() {
      return zeroPadding(this.date.getHours(), 2)
    },
    minutes() {
      return zeroPadding(this.date.getMinutes(), 2)
    },
    seconds() {
      return zeroPadding(this.date.getSeconds(), 2)
    },
  },
  mounted() {
    this.setDate()
    setInterval(() => this.setDate(), 1000)
  },
  methods: {
    setDate() {
      this.date = new Date()
      this.date.setHours(this.date.getHours() + this.diff) // ←追加
    },
  },
}
</script>

props とは親コンポーネントから値を受け取るものです。通常のプログラミングでいうと 引数 のようなものですね。

methodsのsetDateで親コンポーネントから渡されたdiffを使って時差を加算しています。
今回、サマータイムは考慮していないので、興味がある方は修正してみてください。

次にtemplateを調整します。

<template>
  <div>
    <div class="container">
      <p class="location">{{ location }}</p> <!-- ←追加 -->
      <p class="date">{{ year }}/{{ month }}/{{ day }}</p>
      <div class="time">
        <p class="time-item hours">{{ hours }}</p>
        <p class="time-item minutes">{{ minutes }}</p>
        <p class="time-item seconds">{{ seconds }}</p>
      </div>
    </div>
  </div>
</template>

locationを表示する要素を追加しました。

次にstyleを調整します。

<style scoped>
.container {
  background-color: #3a4a5e;
  padding: 2%;
}


/* ↓追加 */
.location { 
  color: #48b883;
  font-family: 'Teko', sans-serif;
  font-size: 5rem;
  letter-spacing: .05em;
  line-height: 1;
}
/* ↑追加 */


.date {
  text-align: right;
  color: #fff;
  font-family: 'Teko', sans-serif;
  font-size: 4rem;
  letter-spacing: .1em;
  margin: .0em 0;
  line-height: 1;
}


.time {
  display: flex;
}


.time-item {
  display: flex;
  justify-content: center;
  align-items: center;
  flex: 1 1;
  height: 100px;
  position: relative;
  z-index: 1;
  padding: 0.5em;
  margin: 3px;
  color: #fff;
  font-family: 'Roboto Mono', monospace;
  font-size: 3rem;
  line-height: 1;
  background-color: #48b883;
  box-sizing: border-box;
}


.time-item:before {
  position: absolute;
  right: 5px;
  bottom: 1px;
  z-index: 1;
  color: #3a4a5e;
  font-family: 'Teko', sans-serif;
  font-size: 1.4rem;
  letter-spacing: .05em;
}


.hours:before {
  content: "Hours";
}


.minutes:before {
  content: "Minutes";
}


.seconds:before {
  content: "Seconds";
}
</style>

.locationのスタイルを追加しました。

最後に

  • src/App.vue

のtemplateを修正します。

<template>
  <div>
    <Clock class="clock" location="TOKYO" :diff="0"/>
    <Clock class="clock" location="NY" :diff="-14"/>
    <Clock class="clock" location="LA" :diff="-17"/>
    <Clock class="clock" location="UK" :diff="-9"/>
    <Clock class="clock" location="BRAZIL" :diff="-12"/>
    <Clock class="clock" location="SYDNEY" :diff="1"/>
    <Clock class="clock" location="DUBAI" :diff="-5"/>
    <Clock class="clock" location="JOHANNESBURG" :diff="-7"/>
  </div>
</template>

ここでのポイントはdiffの前に付いている「:」コロンです。
これはv-bindの省略記法です。

v-bindについて公式サイトの説明では

1つ以上の属性またはコンポーネントのプロパティと式を動的に束縛します。

となってます。

簡単に言うと親コンポーネントで値を変更した時、自動的 に子コンポーネントの値も変更されるということです。
他の使い方としては通常の属性では文字列しか渡せないので、数値など他のオブジェクトを渡したい場合にも使用します。
locationは文字列なので通常の属性で渡して、diffは数値なので : を付けて渡してあげています。

完成画面

これで↑のような画面が表示されていれば完成です!
お疲れ様でした!!

最後に、WEBサイトにアップするときの 注意点 として、アップする場所によって

  • config/index.js

の46行目にある「assetsPublicPath」を変更する必要があります。
例えば、
http://example.com/hoge/
にアップする場合は、

assetsPublicPath: '/hoge/',

この様に変更する必要があります。

そして、

npm run build

を実行すると dist ディレクトリが作成されるので、その中にあるファイル一式をアップしてください。

まとめ

一回目ということで簡単なアプリでしたが、次回はもう少し複雑なアプリをつくりたいと思います。
さらに、それ以降はルーティング機能を実装する vue-router や状態を管理する vuex の使い方も紹介していきたいと思っています。
Vue.js仲間がもっと増えると嬉しいです!

Recent Entries
MD EVENT REPORT
What's Hot?