[Vue.js][ES2015][webpack]サンプルソースを利用した学習（HackerNews clone with Vue.js）


##TL;DR
これからVue.jsを利用してES2015の記法、Webpackなどを利用したモダンな開発を目指す方が対象です。Vue.jsは非常に学習コストが低く手軽にSPAが開発できるため初心者にはおすすめです。

##0.サンプルソースのインストール
以下のURLからソースをローカルに設置してください。
https://github.com/vuejs/vue-hackernews

設置後は対象ディレクトリで以下のコマンドを実施しパッケージをインストールしてください。

```
npm install
```

開発時は以下のコマンドを実施することでhttp://localhost:8080/ で確認できます。

```
npm run dev
```

ビルドは以下のコマンドを実施することで各ビルドファイルが作成されます。※詳細は後述

```
npm run build
```



[実際のデモページ](http://vuejs.github.io/vue-hackernews/ "HackerNews clone with Vue.js")


##1.処理の流れ
基本的にindex.htmlの\<div id="app">\</div>をイベントによって書き換えていく形となります。初期表示ではsrc/main.jsが処理されNewsページを表示します。（正確にはルートアクセス時にNewsページにリダイレクトしている）

・ニュース表示
index.html > main.js > App.vue > NewsView.vue > Item.vue 
・ユーザ表示
index.html > main.js > App.vue > UserView.vue 
・コメント表示
index.html > main.js > App.vue > ItemView.vue > Item.vue > Comment.vue

一度ページを表示すれば"index.html > main.js > App.vue >"は再読込されません。
実際にはページ起動時にすべてのファイルが読み込まれます。上記はコンポーネントの作成順序となります。
※vue-routerのkeep-aliveオプションがあるため一度レンダリングしたページは再読込されません。（後述を参照）
※インスタンス化の確認はexport内にcreatedプロパティ（コンストラクタ）を用意することで可能です。

HackerNewsのデータはstore/index.js経由でfirebaseから取得しておりNewsView.vue、ItemView.vueコンポーネント内から利用しています。


以降で各ファイルの説明を記載します。

##2.package.json

```Javascript:package.json
{
  "name": "vue-hackernews",
  "version": "1.0.0",
  "description": "HN clone with Vue.js using HN API",
  "scripts": {
    "dev": "webpack-dev-server --inline --hot --no-info",
    "build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/yyx990803/vue-hackernews.git"
  },
  "author": "Evan You",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/yyx990803/vue-hackernews/issues"
  },
  "homepage": "https://github.com/yyx990803/vue-hackernews",
  "devDependencies": {
    "babel-core": "^6.0.0",
    "babel-loader": "^6.0.0",
    "babel-plugin-transform-runtime": "^6.0.0",
    "babel-preset-es2015": "^6.0.0",
    "babel-runtime": "^6.0.0",
    "cross-env": "^1.0.5",
    "css-loader": "^0.23.1",
    "stylus": "^0.54.5",
    "stylus-loader": "^2.1.1",
    "vue-hot-reload-api": "^1.2.0",
    "vue-html-loader": "^1.0.0",
    "vue-loader": "^8.0.0",
    "vue-style-loader": "^1.0.0",
    "webpack": "^1.12.2",
    "webpack-dev-server": "^1.12.0"
  },
  "dependencies": {
    "es6-promise": "^3.0.2",
    "firebase": "^2.3.1",
    "vue": "^1.0.26",
    "vue-router": "^0.7.13"
  }
}

```
package.jsonはプロジェクトのタイトルや説明、利用するパッケージなどを記述します。
このファイルが存在するディレクトリで`npm install`を行うことで各パッケージのインストールなどが行なわれプロジェクトが作成されます。（[こちら](http://liberty-technology.biz/PublicItems/npm/package.json.html "npm package.json 取扱説明書")を参考にしました）

特化した部分を以下に記載します。


###scripts
devはwebpackを利用して開発コマンドを実施した場合、webpack-dev-serverを起動します。（本来はnpm scriptsコマンドなどで起動するが裏側でやってくれいている）
"--inline --hot --no-info"オプションはプログラムを変更した際にリアルタイムでページがリロードされ、その際に余計な情報を出力しない設定となります。オプションの詳細は[こちら](https://webpack.github.io/docs/webpack-dev-server.html#additional-configuration-options)

buildは環境変数NODE_ENVにproductionをセットしてwebpackを実行します。"--progress --hide-modules"オプションはビルド中の進捗を表示して、作成したモジュール名を表示しない設定となります。
オプションの詳細は[こちら](https://webpack.github.io/docs/cli.html#display-options)

###devDependencies
| パッケージ名 | 概要 | 
|:-----------|:------------|
| babel-core <br>babel-loader <br>babel-plugin-transform-runtime <br>babel-preset-es2015 <br>babel-runtime | babelはES6（ES2015）をES5にトランスパイルするために利用 |
| cross-env | 環境変数を異なる環境でも同じように設定するためのパッケージ |
| css-loader | 各コンポーネント単位でCSSを準備できるようにする（CSSモジュールの考え方は[こちら](http://postd.cc/css-modules/)） |
| stylus<br>stylus-loader | stylusはSASSと同じようなCSSメタ言語 |
| vue-hot-reload-api | 開発時にVue.jsのcomponentsのプログラムを変更した際にリアルタイムに更新内容をページに反映する |
| vue-html-loader | Vue.jsのhtmlをjavascript上で扱えるようにするために利用（html-loaderのfork） |
| vue-loader | Vue.jsのcomponentsの形式を扱えるようにするために利用 |
| vue-style-loader | Vue.jsのcssをjavascript上で扱えるようにするために利用（style-loaderのfork） |
| webpack | [こちら](https://ics.media/entry/12140)を参照 |
| webpack-dev-server | sriptsの説明を参照 |

###dependencies

| モジュール名 | 概要 | 
|:-----------|:------------|
| es6-promise | ES6記法でpromiseを利用するために利用（promiseの詳細は[こちら](http://azu.github.io/promises-book/) |
| firebase | Google製のmBaaS（Hackernewsの情報はここから取得する） |
| vue <br >vue-router| 割愛 |

##3.webpack(webpack.config.js)
babelや各種loaderはwebpackを利用して実行します。
webpackはコマンドラインからの利用も可能ですが、webpack.config.jsに各種設定を記述し`npm run`で実行します。
###webpack.config.js
```Javascript:webpack.config.js
var webpack = require('webpack')

module.exports = {
  entry: './src/main.js',
  output: {
    path: './static',
    publicPath: '/static/',
    filename: 'build.js'
  },
  module: {
    // avoid webpack trying to shim process
    noParse: /es6-promise\.js$/,
    loaders: [
      {
        test: /\.vue$/,
        loader: 'vue'
      },
      {
        test: /\.js$/,
        // excluding some local linked packages.
        // for normal use cases only node_modules is needed.
        exclude: /node_modules|vue\/dist|vue-router\/|vue-loader\/|vue-hot-reload-api\//,
        loader: 'babel'
      }
    ]
  },
  babel: {
    presets: ['es2015'],
    plugins: ['transform-runtime']
  }
}

if (process.env.NODE_ENV === 'production') {
  module.exports.plugins = [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: '"production"'
      }
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    }),
    new webpack.optimize.OccurenceOrderPlugin()
  ]
} else {
  module.exports.devtool = '#source-map'
}

```

| 階層１ | 階層２ | 階層３ | 概要 | 
|:------|:------|:------|:------------|
|entry  |       |       |ビルドの起点となるファイルのパス|
|output |path      |       |ビルドしたファイルの出力先パス|
|       |publicPath|       |URL参照となるパス|
|       |filename  |       |ビルドしたファイルの出力ファイル名|
|module |noParse   |       |パースを無効にするファイルを指定|
|       |loaders   |test   |変換するファイルを指定（正規表現も可能）|
|       |          |exclude|除外するファイルを指定（含める場合はinclude）|
|       |          |loader |使用するloaderの名前を指定|
|babel  |presets   |       |babel関連の設定でプリセットを指定([公式](http://babeljs.io/docs/plugins/))|
|       |plugins   |       |babel関連の設定で使用するプラグインを指定|
説明は[こちら](http://dackdive.hateblo.jp/entry/2016/04/13/123000)を参考にさせていただきました。

以下はビルド時のみ適用

| プラグイン名 | 概要 |
|:------|:------------|
|webpack.DefinePlugin  |環境変数の設定、この値によりロジックを切り分けている|
|webpack.optimize.UglifyJsPlugin  |ビルドしたJSをminifyする|
|webpack.optimize.OccurenceOrderPlugin  |利用順にプラグインを読み込む|

開発時のみmodule.exports.devtool = '#source-map'となり、source-mapを作成します。


##4.main.js
```Javascript:src/main.js
import Vue from 'vue'
import Router from 'vue-router'
import { domain, fromNow } from './filters'
import App from './components/App.vue'
import NewsView from './components/NewsView.vue'
import ItemView from './components/ItemView.vue'
import UserView from './components/UserView.vue'

// install router
Vue.use(Router)

// register filters globally
Vue.filter('fromNow', fromNow)
Vue.filter('domain', domain)

// routing
var router = new Router()

router.map({
  '/news/:page': {
    component: NewsView
  },
  '/user/:id': {
    component: UserView
  },
  '/item/:id': {
    component: ItemView
  }
})

router.beforeEach(function () {
  window.scrollTo(0, 0)
})

router.redirect({
  '*': '/news/1'
})

router.start(App, '#app')
```

importでは利用するモジュールを読み込みます。
>import 文 は、外部モジュールや他のスクリプトなどからエクスポートされた関数、オブジェクト、プリミティブをインポートするために使用します。

[import - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/import)

Vue.use() グローバルメソッドでvue-routerプラグインを使用します。
Vue.filter()でカスタムフィルタを登録します。（後述の12.Filterで説明）
以降はルーティングの設定で詳しくは[こちら](http://router.vuejs.org/ja/basic.html)を参照してください。（router.beforeEachは[こちら](http://router.vuejs.org/ja/api/before-each.html)）


##5.App.vue
```Html:src/components/App.vue
<template>
  <div id="wrapper">
    <!-- header -->
    <div id="header">
      <a id="yc" href="http://www.ycombinator.com">
        <img src="https://news.ycombinator.com/y18.gif">
      </a>
      <h1><a href="#/">Hacker News</a></h1>
      <span class="source">
        Built with <a href="http://vuejs.org" target="_blank">Vue.js</a> |
        <a href="https://github.com/vuejs/vue-hackernews" target="_blank">Source</a>
      </span>
    </div>
    <!-- main view -->
    <router-view
      class="view"
      keep-alive
      transition
      transition-mode="out-in">
    </router-view>
  </div>
</template>

<style lang="stylus">
@import "../variables.styl"

html, body
  font-family Verdana
  font-size 13px
  height 100%

ul
  list-style-type none
  padding 0
  margin 0

a
  color #000
  cursor pointer
  text-decoration none
  
#wrapper
  background-color $bg
  position relative
  width 85%
  min-height 80px
  margin 0 auto

#header
  background-color #f60
  height 24px
  position relative
  h1
    font-weight bold
    font-size 13px
    display inline-block
    vertical-align middle
    margin 0
  .source
    color #fff
    font-size 11px
    position absolute
    top 4px
    right 4px
    a
      color #fff
      &:hover
        text-decoration underline

#yc
  border 1px solid #fff
  margin 2px
  display inline-block
  vertical-align middle
  img
    vertical-align middle

.view
  position absolute
  background-color $bg
  width 100%
  transition opacity .2s ease
  box-sizing border-box
  padding 8px 20px
  &.v-enter, &.v-leave
    opacity 0

@media screen and (max-width: 700px)
  html, body
    margin 0
  #wrapper
    width 100%
</style>

```
Vue.jsのコンポーネントとは
>コンポーネントは Vue.js の最も強力な機能の1つです。基本的な >HTML 要素を拡張して再利用可能なコードのカプセル化を助けます。
>高度なレベルでは、コンポーネントは Vue.js のコンパイラが指定された振舞いをアタッチするカスタム要素です。
>場合によっては、特別な is 属性で拡張されたネイティブな HTML 要素の姿をとることもあります。

[コンポーネントとは何か？](https://jp.vuejs.org/guide/components.html)

App.vueはサンプルプロジェクトの根幹となるコンポーネントで、最初に記載しましたが、index.htmlの\<div id="app">\</div>にApp.vueの\<template>内のHTMLが追加されます。
アクセスするURLまたは操作イベントにより\<router-view>タグ内が該当するコンポーネントに置き換えられます。
class="view"は置き換わるラッパータグに付加されます。\<router-view>ではVue.jsコンポーネントのオプションが一部利用可能となっています。

・keep-alive
>状態を保持したり再レンダリングを避けたりするために、もし切り替えで取り除かれたコンポーネントを生きた状態で保持したい場合は、ディレクティブのパラメータ keep-alive を追加することができます

・transition
>transition-mode パラメータ属性は、2つの動的コンポーネント間でのトランジションがどう実行されるかを指定できます。
>
>デフォルトでは、入ってくるコンポーネントと出て行くコンポーネントのトランジションが同時に起こります。この属性によって、もう2つのモードを設定することができます:
>
>・in-out: 新しいコンポーネントのトランジションが初めに起こり、そのトランジションが完了した後に現在のコンポーネントの出て行くトランジションが開始します。
>
>・out-in: 現在のコンポーネントが出て行くトランジションが初めに起こり、そのトランジションが完了した後に新しいコンポーネントのトランジションが開始します。

\<router-view>とオプションの説明は[こちら](http://router.vuejs.org/ja/view.html)や[こちら](https://jp.vuejs.org/guide/components.html#動的コンポーネント)

template、script、styleタグにlang属性を設定することでそれぞれメタ言語を利用することができます。
（jade,coffeeなどなど)
CSS部分ではlang="stylus"とすることでstylus形式で記述することを宣言しています。



##6.NewsView.vue
```Html:src/components/NewsView.vue

<template>
  <div class="news-view" :class="{ loading: !items.length }">
    <!-- item list -->
    <item
      v-for="item in items"
      :item="item"
      :index="$index | formatItemIndex"
      track-by="id">
    </item>
    <!-- navigation -->
    <div class="nav" v-show="items.length > 0">
      <a v-if="page > 1" :href="'#/news/' + (page - 1)">&lt; prev</a>
      <a v-if="page < 4" :href="'#/news/' + (page + 1)">more...</a>
    </div>
  </div>
</template>

<script>
import store from '../store'
import Item from './Item.vue'

export default {

  name: 'NewsView',

  components: {
    Item
  },

  data () {
    return {
      page: 1,
      items: []
    }
  },

  route: {
    data ({ to }) {
      // This is the route data hook. It gets called every time the route
      // changes while this component is active.
      // 
      // What we are doing:
      // 
      // 1. Get the `to` route using ES2015 argument destructuring;
      // 2. Get the `page` param and cast it to a Number;
      // 3. Fetch the items from the store, which returns a Promise containing
      //    the fetched items;
      // 4. Chain the Promise and return the final data for the component.
      //    Note we are waiting until the items are resolved before resolving
      //    the entire object, because we don't want to update the page before
      //    the items are fetched.
      const page = +to.params.page
      document.title = 'Vue.js HN Clone'
      return store.fetchItemsByPage(page).then(items => ({
        page,
        items
      }))
    }
  },

  created () {
    store.on('topstories-updated', this.update)
  },

  destroyed () {
    store.removeListener('topstories-updated', this.update)
  },

  methods: {
    update () {
      store.fetchItemsByPage(this.page).then(items => {
        this.items = items
      })
    }
  },

  filters: {
    formatItemIndex (index) {
      return (this.page - 1) * store.storiesPerPage + index + 1
    }
  }
}
</script>

<style lang="stylus">
.news-view
  padding-left 5px
  padding-right 15px
  &.loading:before
    content "Loading..."
    position absolute
    top 16px
    left 20px
  .nav
    padding 10px 10px 10px 40px
    margin-top 10px
    border-top 2px solid #f60
    a
      margin-right 10px
      &:hover
        text-decoration underline
</style>
```

初期表示でも利用しているニュース一覧コンポーネントです。
storeのニュースデータ件数分のItemコンポーネントを作成し\<Item>内に表示します。
:class="{ loading: !items.length }"でstoreからdataのitemにデータがセットされるまでローディングを表示します。(ローディングはCSSで表現)
Vue.jsでは親コンポーネントのデータを子コンポーネントが利用する場合、Propsという機能を利用します。ここではItemコンポーネントにitem,indexを渡しています。
[#Props-によるデータ伝達](https://jp.vuejs.org/guide/components.html#Props-によるデータ伝達)

v-forディレクティブは[こちら](https://jp.vuejs.org/guide/list.html#v-for)を参照してください。track-byディレクティブは利用することでDOM要素を再利用することができます。そのためデータ内の一意となるIDをセットします。
[#track-by](https://jp.vuejs.org/guide/list.html#track-by)

\<script>タグ内はES2015で記述しています。データ取得用のstoreモジュールとItemコンポーネントを読み込みます。export default内はImportコマンドにて読みこまれた際にレスポンスとなる値で、Vue.jsのコンポーネント形式で記述します。 利用している各属性の説明は以下となります。

| 属性 | 概要 | 
|:-----------|:------------|
| name |コンポーネントの名称です。テンプレートで自分自身を再帰的に呼びだす際は必須です。|
| components | 子コンポーネントを指定します。ここに指定することで当コンポーネントのみ利用可能なコンポーネントが作成可能です。 |
| data |コンポーネントが利用するデータオブジェクトを指定します。dataの詳細は[こちら](http://jp.vuejs.org/api/#data)で、関数形式で指定する理由は[こちら](https://jp.vuejs.org/guide/components.html#コンポーネントオプションの注意事項) を確認してください。|
| route |vue-routerで利用します。dataはURL（route）の変更やコンポーネントが再利用で呼び出されコンポーネントのdataを更新します。ここではstore.js経由でfirebaseからpageに応じたデータを取得しdataオブジェクトを更新しています。 詳細は[こちら](http://router.vuejs.org/ja/pipeline/data.html)|
| created | コンポーネント作成時にコールされます。ここではstore.jsのイベントを監視します。詳細はstore.jsに記述。createdの詳細は[こちら](http://jp.vuejs.org/api/#created)|
| destroyed | コンポーネントが破棄された後にコールされます。ここではcreated時に登録したイベント監視を削除します。destroyedの詳細は[こちら](http://jp.vuejs.org/api/#destroyed)|
| methods | Vue インスタンスに組み込まれるメソッドで、メソッド内から直接アクセス可能となり、ディレクティブの式で使用することもできます。methodsの詳細は[こちら](http://jp.vuejs.org/api/#methods)|
| filter | ここではURL（route）で受け取ったpageを元にItemsコンポーネントに渡す値を算出しています。カスタムフィルタの詳細は[こちら](http://jp.vuejs.org/guide/custom-filter.html)|


各記法についてはNewsView.vueを変換したbuild.js（以下）と比較すると分かりやすいかと思います。

```Html:build.js（抜粋）
	'use strict';
	
	Object.defineProperty(exports, "__esModule", {
	  value: true
	});
	
	var _store = __webpack_require__(92);
	
	var _store2 = _interopRequireDefault(_store);
	
	var _Item = __webpack_require__(132);
	
	var _Item2 = _interopRequireDefault(_Item);
	
	function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
	
	// <template>
	//   <div class="news-view" :class="{ loading: !items.length }">
	//     <!-- item list -->
	//     <item
	//       v-for="item in items"
	//       :item="item"
	//       :index="$index | formatItemIndex"
	//       track-by="id">
	//     </item>
	//     <!-- navigation -->
	//     <div class="nav" v-show="items.length > 0">
	//       <a v-if="page > 1" :href="'#/news/' + (page - 1)">&lt; prev</a>
	//       <a v-if="page < 4" :href="'#/news/' + (page + 1)">more...</a>
	//     </div>
	//   </div>
	// </template>
	//
	// <script>
	exports.default = {
	
	  name: 'NewsView',
	
	  components: {
	    Item: _Item2.default
	  },
	
	  data: function data() {
	    return {
	      page: 1,
	      items: []
	    };
	  },
	
	
	  route: {
	    data: function data(_ref) {
	      var to = _ref.to;
	
	      // This is the route data hook. It gets called every time the route
	      // changes while this component is active.
	      // 
	      // What we are doing:
	      // 
	      // 1. Get the `to` route using ES2015 argument destructuring;
	      // 2. Get the `page` param and cast it to a Number;
	      // 3. Fetch the items from the store, which returns a Promise containing
	      //    the fetched items;
	      // 4. Chain the Promise and return the final data for the component.
	      //    Note we are waiting until the items are resolved before resolving
	      //    the entire object, because we don't want to update the page before
	      //    the items are fetched.
	      var page = +to.params.page;
	      document.title = 'Vue.js HN Clone';
	      return _store2.default.fetchItemsByPage(page).then(function (items) {
	        return {
	          page: page,
	          items: items
	        };
	      });
	    }
	  },
	
	  created: function created() {
	    _store2.default.on('topstories-updated', this.update);
	  },
	  destroyed: function destroyed() {
	    _store2.default.removeListener('topstories-updated', this.update);
	  },
	
	
	  methods: {
	    update: function update() {
	      var _this = this;
	
	      _store2.default.fetchItemsByPage(this.page).then(function (items) {
	        _this.items = items;
	      });
	    }
	  },
	
	  filters: {
	    formatItemIndex: function formatItemIndex(index) {
	      return (this.page - 1) * _store2.default.storiesPerPage + index + 1;
	    }
	  }
	};
```


##7.Store
```Html:src/store/index.js
import Firebase from 'firebase'
import { EventEmitter } from 'events'
import { Promise } from 'es6-promise'

const api = new Firebase('https://hacker-news.firebaseio.com/v0')
const itemsCache = Object.create(null)
const store = new EventEmitter()
const storiesPerPage = store.storiesPerPage = 30

let topStoryIds = []

export default store

/**
 * Subscribe to real time updates of the top 100 stories,
 * and cache the IDs locally.
 */

api.child('topstories').on('value', snapshot => {
  topStoryIds = snapshot.val()
  store.emit('topstories-updated')
})

/**
 * Fetch an item data with given id.
 *
 * @param {Number} id
 * @return {Promise}
 */

store.fetchItem = id => {
  return new Promise((resolve, reject) => {
    if (itemsCache[id]) {
      resolve(itemsCache[id])
    } else {
      api.child('item/' + id).once('value', snapshot => {
        const story = itemsCache[id] = snapshot.val()
        resolve(story)
      }, reject)
    }
  })
}

/**
 * Fetch the given list of items.
 *
 * @param {Array<Number>} ids
 * @return {Promise}
 */

store.fetchItems = ids => {
  if (!ids || !ids.length) {
    return Promise.resolve([])
  } else {
    return Promise.all(ids.map(id => store.fetchItem(id)))
  }
}

/**
 * Fetch items for the given page.
 *
 * @param {Number} page
 * @return {Promise}
 */

store.fetchItemsByPage = page => {
  const start = (page - 1) * storiesPerPage
  const end = page * storiesPerPage
  const ids = topStoryIds.slice(start, end)
  return store.fetchItems(ids)
}

/**
 * Fetch a user data with given id.
 *
 * @param {Number} id
 * @return {Promise}
 */

store.fetchUser = id => {
  return new Promise((resolve, reject) => {
    api.child('user/' + id).once('value', snapshot => {
      resolve(snapshot.val())
    }, reject)
  })
}
```

前述のとおりHackerNewsのデータはfirebaseから取得しています。
[Hacker News API](https://github.com/HackerNews/API)
firebaseの基本的な利用方法は[こちら](https://firebase.google.com/docs/)を参照してください。
※[旧ドキュメント](https://www.firebase.com/docs/web/)
Importにより利用するモジュールの概要は以下となります。

| モジュール | 概要 | 
|:-----------|:------------|
| firebase |firebaseがオフィシャルに用意しているパッケージ |
| events | Node.js上でイベントを扱うのに便利なパッケージ |
| es6-promise |ES6でpromiseオブジェクト（非同期処理を簡単にするもの）を扱うためのパッケージ<br>promiseについては[こちら](http://qiita.com/koki_cheese/items/c559da338a3d307c9d88)が分かりやすいかと|


次のconst、letは各値の宣言、初期化を行っています。Firebaseクラスは利用するオブジェクトのURL（RDBでいうところのデータベース）を指定します。[Object.create](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/create)ではitemsCacheをオブジェクト型として初期化しています。storiesPerPageは1ページに表示するニュース件数を定義しています。

export default storeはstoreモジュールをImportした際に利用可能となるオブジェクトを指定しています。export defaultではクラス、オブジェクト、文字列など任意の値が指定可能です。ここではEventEmitter型のオブジェクトを戻しています。

api.child('topstories').on('value',〜はtopstoriesオブジェクトから1回だけデータを取得する操作となります。firebaseはソケット通信を利用してデータの追加・変更時などのアクション登録も可能ですが、本プロジェクトでは利用毎に基本的に1回だけ取得する形となります。第2引数にはcallback関数を指定しており、レスポンスとなるニュースのID配列（コメントには100件とあるが500件入ってる）を保持して、'topstories-updated'イベントを発行します。少し前後しますがNewsView.vueではこのイベントを監視しており、NewsView内のupdateメソッド経由でstoreのデータ取得を呼び出します。

store.fetchItem = id => {〜は引数のidから対象のNewsデータを[Promiseオブジェクト](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise)の形式で戻します。既に取得済みの場合はキャッシュから、データがない場合はfirebaseから取得します。Promiseオブジェクトに渡すcallbackの引数はresolve, rejectとなり成功時はresolve、失敗時はrejectにレスポンスしたい値を渡します。

store.fetchItems = ids => {〜は上述のラッパーとなり、複数のidを受け取り紐づくデータをレスポンスします。


store.fetchItemsByPage = page => {〜はページに紐づくidを最初に保持したID配列から抽出し紐づくデータをレスポンスします。


store.fetchUser = id => {〜はユーザに関する情報をfirebaseのuserデータから取得しレスポンスします。



##8.Item.vue
```Html:src/components/Item.vue
<template>
  <div class="item">
    <span class="index">{{index}}.</span>
    <p>
      <a class="title" :href="href" target="_blank">{{{item.title}}}</a>
      <span class="domain" v-show="showDomain">
        ({{item.url | domain}})
      </span>
    </p>
    <p class="subtext">
      <span v-show="showInfo">
        {{item.score}} points by
        <a :href="'#/user/' + item.by">{{item.by}}</a>
      </span>
      {{item.time | fromNow}} ago
      <span class="comments-link" v-show="showInfo">
        | <a :href="'#/item/' + item.id">{{item.descendants}} {{item.descendants | pluralize 'comment'}}</a>
      </span>
    </p>
  </div>
</template>

<script>
export default {

  name: 'Item',

  props: {
    item: Object,
    index: Number
  },

  computed: {
    href () {
      return this.item.url || ('#/item/' + this.item.id)
    },
    showInfo () {
      return this.item.type === 'story' || this.item.type === 'poll'
    },
    showDomain () {
      return this.item.type === 'story'
    }
  }
}
</script>

<style lang="stylus">
@import "../variables.styl"

.item
  padding 2px 0 2px 40px
  position relative
  transition background-color .2s ease
  p
    margin 2px 0
  .title:visited
      color $gray
  .index
    color $gray
    position absolute
    width 30px
    text-align right
    left 0
    top 4px
  .domain, .subtext
    font-size 11px
    color $gray
    a
      color $gray
  .subtext a:hover
    text-decoration underline
</style>
```

ニュース一覧の明細行となるコンポーネントです。firebaseから取得するitemsは[こちら](https://github.com/HackerNews/API#items)を参照してください。
親コンポーネントからPropsでデータを受け取る場合は伝達を想定する props を明示的に宣言する必要があります。
computedではテンプレート内で利用する[算出プロパティ](https://jp.vuejs.org/guide/computed.html)を記述することができます。
基本的にはitemsのデータが外部サイトかHackerNews内のコンテンツか識別して出力内容を制御しています。

##9.UserView.vue
```Html:src/components/UserView.vue
<template>
  <div class="user-view" v-show="user">
    <ul>
      <li><span class="label">user:</span> {{user.id}}</li>
      <li><span class="label">created:</span> {{user.created | fromNow}} ago</li>
      <li><span class="label">karma:</span> {{user.karma}}</li>
      <li>
        <span class="label">about:</span>
        <div class="about">
          {{{user.about}}}
        </div>
      </li>
    </ul>
    <p class="links">
      <a :href="'https://news.ycombinator.com/submitted?id=' + user.id">submissions</a><br>
      <a :href="'https://news.ycombinator.com/threads?id=' + user.id">comments</a>
    </p>
  </div>
</template>

<script>
import store from '../store'

export default {

  name: 'UserView',

  data () {
    return {
      user: {}
    }
  },

  route: {
    data ({ to }) {
      // Promise sugar syntax: return an object that contains Promise fields.
      // http://router.vuejs.org/en/pipeline/data.html#promise-sugar
      document.title = 'Profile: ' + to.params.id + ' | Vue.js HN Clone'
      return {
        user: store.fetchUser(to.params.id)
      }
    }
  }
}
</script>

<style lang="stylus">
@import "../variables.styl"

.user-view
  color $gray
  li
    margin 5px 0
  .label
    display inline-block
    min-width 60px
  .about
    margin-top 1em
  .links a
    text-decoration underline
</style>
```
ニュース一覧のユーザ名クリックまたは直接URLをアクセスした際にユーザ情報を表示します。
userオブジェクトについては[こちら](https://github.com/HackerNews/API#users)を参照してください。
submissionsおよびcommentsのリンク先はcloneプロジェクトに用意はなく本家サイトとなっています。存在しないユーザ情報の場合はv-show="user"の制御でユーザ情報部分が非表示となります。


##10.ItemView.vue
```Html:src/components/ItemView.vue
<template>
  <div class="item-view" v-show="item">
    <item :item="item"></item>
    <p class="itemtext" v-if="hasText" v-html="item.text"></p>
    <ul class="poll-options" v-if="pollOptions">
      <li v-for="option in pollOptions">
        <p>{{option.text}}</p>
        <p class="subtext">{{option.score}} points</p>
      </li>
    </ul>
    <ul class="comments" v-if="comments">
      <comment
        v-for="comment in comments"
        :comment="comment">
      </comment>
    </ul>
    <p v-show="!comments.length && !isJob">No comments yet.</p>
  </div>
</template>

<script>
import store from '../store'
import Item from './Item.vue'
import Comment from './Comment.vue'

export default {

  name: 'ItemView',

  components: {
    Item,
    Comment
  },

  data () {
    return {
      item: {},
      comments: [],
      pollOptions: null
    }
  },

  route: {
    data ({ to }) {
      return store.fetchItem(to.params.id).then(item => {
        document.title = item.title + ' | Vue.js HN Clone'
        return {
          item,
          // the final resolved data can further contain Promises
          comments: store.fetchItems(item.kids),
          pollOptions: item.type === 'poll'
            ? store.fetchItems(item.parts)
            : null
        }
      })
    }
  },

  computed: {
    isJob () {
      return this.item.type === 'job'
    },

    hasText () {
      return this.item.hasOwnProperty('text')
    }
  }
}
</script>

<style lang="stylus">
@import "../variables.styl"

.item-view
  .item
    padding-left 0
    margin-bottom 30px
    .index
      display none
  .poll-options
    margin-left 30px
    margin-bottom 40px
    li
      margin 12px 0
    p
      margin 8px 0
    .subtext
      color $gray
      font-size 11px
  .itemtext
    color $gray
    margin-top 0
    margin-bottom 30px
  .itemtext p
    margin 10px 0
</style>
```

HackerNews内のコンテンツ、ニュース一覧のコメントクリックまたは直接URLをアクセスした際にニュース情報を表示します。typeのjobはhiring用のページでpollはアンケートページとなります。
URLパラメータのItemのidからItemの情報を取得して、コメント、アンケートの項目など紐づく情報を取得しdataにセットします。


##11.Comment.vue
```Html:src/components/Comment.vue
<template>
  <li v-show="comment.text">
    <div class="comhead">
      <a class="toggle" @click="open = !open">{{open ? '[-]' : '[+]'}}</a>
      <a :href="'#/user/' + comment.by">{{comment.by}}</a>
      {{comment.time | fromNow}} ago
    </div>
    <p class="comment-content" v-show="open">
      {{{comment.text}}}
    </p>
    <ul class="child-comments" v-if="comment.kids" v-show="open">
      <comment v-for="comment in childComments" :comment="comment"></comment>
    </ul>
  </li>
</template>

<script>
import store from '../store'

export default {

  name: 'Comment',

  props: {
    comment: Object
  },

  data () {
    return {
      childComments: [],
      open: true
    }
  },

  created () {
    if (this.comment.kids) {
      store.fetchItems(this.comment.kids).then(comments => {
        this.childComments = comments
      })
    }
  }
}
</script>

<style lang="stylus">
@import "../variables.styl"

.comhead
  color $gray
  font-size 11px
  margin-bottom 8px
  a
    color $gray
    &:hover
      text-decoration underline
  .toggle
    margin-right 4px

.comment-content
  margin 0 0 16px 24px
  word-wrap break-word
  code
    white-space pre-wrap

.child-comments
  margin 8px 0 8px 22px
</style>
```
ニュースに紐づくコメントを表示します。コメントに対するコメントを再帰処理にて取得・表示を行っています。


##12.filter
```Html:src/filters/index.js
const urlParser = document.createElement('a')

export function domain (url) {
  urlParser.href = url
  return urlParser.hostname
}

export function fromNow (time) {
  const between = Date.now() / 1000 - Number(time)
  if (between < 3600) {
    return pluralize(~~(between / 60), ' minute')
  } else if (between < 86400) {
    return pluralize(~~(between / 3600), ' hour')
  } else {
    return pluralize(~~(between / 86400), ' day')
  }
}

function pluralize(time, label) {
    if (time === 1) {
        return time + label
    }

    return time + label + 's';
}

```

カスタムフィルタはテンプレート内でdataの値を整形して表示をする時などに利用します。（詳細は[こちら](https://jp.vuejs.org/guide/custom-filter.html)）
このパッケージはmain.jsでImportされfilterに登録されており、表示用にURLからドメインを抽出して戻すdomainと、unixタイムを表示用の文字列に変換する2つを用意しています。
ドメイン抽出ではaタグのhostnameを利用しており、時間表示では単位にsを付加する識別を行っています。

##おわりに
雑に記載してしまったので不適切な表記、同じ意味で異なるワードの統一などは順次更新していきます。間違い等があれば、ご指摘ください。