nuxt-communityのexpress-templateを試してみる #nuxtjs #vuejs

概要

vue-cliから作れるNuxt.jsのテンプレートに、express-templateというのがある。

Nuxt.jsと同時にExpressを起動させて、バックエンドとして動かすことが可能。

Nuxt.js内でバックエンドのMockを作って動かせるので、フロントエンドとバックエンドの開発が切り分けるのが容易になる。

github.com

プロジェクト作成

vue-cliでプロジェクトを作成する。

$ vue init nuxt-community/express-template nuxt-express-sample
$ cd nuxt-express-sample
$ npm install # or yarn install

バックエンドのMockAPIを作成

下記のようなjsonを返すAPIを作成する。

[
  { name: 'とろ', price: 300 },
  { name: 'いか', price: 200 },
  { name: 'かっぱ巻き', price: 100 }
]

Routerの作成

server/api/sushi.js 名前でRouterを作成する。

import { Router } from 'express'

const router = Router()

// ダミーのレスポンスを定義
const sushi = [
  { name: 'とろ', price: 300 },
  { name: 'いか', price: 200 },
  { name: 'かっぱ巻き', price: 100 }
]

// `/api/sushi` のパスでアクセスできるようにする
router.get('/sushi', function(req, res, next) {
  // json形式で返却
  res.json(sushi)
})

export default router

Routerの登録

作成したRouterを server/api/index.js で登録する。

import { Router } from 'express'
// 作成したRouterのimport
import sushi from './sushi'

const router = Router()

// 作成したRouterの登録
router.use(sushi)

export default router

起動

起動して、アクセスしてみる。

http://localhost:3000/api/sushi

$ yarn dev

フロントエンドの作成

APIで取得したJSONを、フロントで一覧として表示してみる。

pages/sushi/index.vue を作成する。

<template>
<div id="sushi-list">
  <ul>
    <li v-for="(item) in sushiList" v-bind:key="item.name">
      {{item.name}}: {{item.price}}円</li>
  </ul>
</div>
</template>

<script>
// APIを叩くときにはaxiosが必要
import axios from '~/plugins/axios'

export default {
  data: function() {
    return {
      sushiList: []
    }
  },
  // asyncDataでレンダリング前にAPIを呼ぶ
  asyncData(context) {
    return axios.get('/api/sushi').then(res => {
      // asyncDataでreturnしたものはdataにマージされる
      return { sushiList: res.data }
    })
  }
}
</script>

起動

起動して、アクセスしてみる。

起動コマンドは変わらない(NuxtもExpressも一緒に立ち上がる)。

http://localhost:3000/sushi

$ yarn dev

asyncDataで取得したデータが表示される。

asyncDataだとSSR(サーバサイドレンダリング)するので、curlとかで取得してもAPIのレスポンスが反映されている。

またExpressも同時に起動しているので、APIにアクセスしても同様に確認できる。

まとめ

少なくとも開発時において、プロジェクト単体で簡単に動作できるというのは素敵。

Nuxt.jsのプロジェクトにCircleCIを適用する #nuxtjs #circleci

概要

Nuxt.jsのプロジェクトをCircleCIで動かしてみる

config.ymlの作成

Nodeプロジェクト用ののテンプレートを利用してconfig.ymlを作成する

プロジェクトルートから .circleci/config.yml を作る。

$ mkdir .circleci
$ touch .circleci/config.yml

config.ymlは下記の通り。

version: 2
jobs:
  build:
    docker:
      # nodeのバージョンを上げておく
      - image: circleci/node:8.9.4

    working_directory: ~/repo

    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "package.json" }}
          # fallback to using the latest cache if no exact match is found
          - v1-dependencies-

      - run: yarn install

      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}

      # run tests!
      - run: yarn test

Nodeのバージョンを上げておく

テンプレート通りだと、imageは circleci/node:7.10 になっていたのだが、これだとNodeのバージョンが低くてエラーになる。

circleci/node:8.9.4 を使用するようにしたら解決した。

CircleCIのDockerHub

error nuxt@1.2.1: The engine "node" is incompatible with this module. Expected version ">=8.0.0".
error Found incompatible module

参考

circleci.com qiita.com

Nuxt.jsでVuexを使ってみる #nuxtjs #vuejs

複数コンポーネントで状態管理をするのに便利らしいVuexを使ってみた。

Vuexストアに状態管理を任せて、各コンポーネントはストア経由で状態変更することで、状態変更を一元管理できるっぽい。

ja.nuxtjs.org

ストアの作成

store/sample.js を作成する。

store配下にjsファイルを作ると、Nuxt.jsがVuexストアにしてくれる。

  • state
    • 状態管理する要素の管理
  • mutations
    • 状態の操作
  • actions
    • 状態の操作する処理を呼び出し、反映(commit)する
    • commitしない限り、反映されない
    • コンポーネントからは、actionで定義した処理が呼ばれる。
// 状態管理したい要素に名前をつけて、stateとしてexportする
export const state = () => ({
  // 'hogeFromStore' という名前の状態を管理する
  hogeFromStore: 'Hello, Vuex!'
})

// 状態を変更する処理は mutationとしてexportする
export const mutations = {
  // ここでは hogeFromStore の状態(値)を変更する処理を定義
  setHogeFromStore(state, value) {
    state.hogeFromStore = value
  }
}

// 実際に各コンポーネントから呼び出す処理をactionとしてexportする
export const actions = {
  writeHoge(context, value) {
    // コミットすることで状態変更が反映される
    context.commit('setHogeFromStore', value)
  }
}

コンポーネントからの呼び出し

コンポーネントからは、 $store 経由で参照できるようになる

<template>
  <div>
    <!-- $store.state経由で状態の取得 -->
    <p>store: {{this.$store.state.sample.hogeFromStore}}</p>
    <!-- $store.dispatch経由でactionを呼び出す -->
    <button v-on:click="$store.dispatch('sample/writeHoge', '値を書き換えます')">Test</button>
    <!-- コンポーネントのScript経由で利用することも可能 -->
    <button v-on:click="testMethod()">Test2</button>
  </div>
</template>

<script>
export default {
  methods: {
    // Scriptからも参照可能
    testMethod: function() {
      console.log(this.$store.state.sample.hogeFromStore)
      this.$store.dispatch('sample/writeHoge', 'メソッドからの書き換え')
      console.log(this.$store.state.sample.hogeFromStore)
    }
  }
}
</script>

mountedからの利用

mountedからも利用できる。

なので、data変更時の処理として使える。

fetchと組み合わせて、レンダリング前にコンポーネントのデータをセットするといったことも可能。

※ただしmountedはSSRされないので注意。SSRしたいときはpageコンポーネントでasyncDataを使うなどする。

mounted

<template>
  <div>
    <p>store: {{this.$store.state.sample.hogeFromStore}}</p>
  </div>
</template>

<script>
export default {
  mounted: function() {
    this.$store.dispatch('sample/writeHoge', 'mountedからも利用できる')
  }
}
</script>

参考

Requestパラメータを画面に表示する #nuxtjs #vuejs

概要

http://localhost:3000/sample?hoge=abc123

のようにRequestされた場合に、 abc123 を画面に表示してみる

asyncDataを使う

pageコンポーネントの場合、asyncDataというメソッドを使うと、Context経由でリクエストの情報を取得できる。

asyncDataはレンダリング前に呼びだれ、結果はdataとマージされる。

Queryパラメータの場合は context.query で取得できる

pages/sample.vue はこんな感じになる。

<template>
  <div>
    <p>hoge: {{hoge}}</p>
  </div>
</template>

<script>
export default {
  data: function() {
    return {
      // asyncDataで上書きされる
      hoge: 'default'
    }
  },
  asyncData(context) {
    return {
      // asyncDataでreturnすると、dataにマージされる
      hoge: context.query['hoge']
    }
  }
}
</script>

http://localhost:3000/sample だと「default」が表示されるが、 ?hoge=abc123 をつけると、「abc123」が表示されるようになる。

なお、下記のようにも書ける。

// 最初からqueryを使っちゃう
asyncData({ query }) {
  return {
    hoge: query['hoge']
  }
}

propsでサブコンポーネントに値を受け渡す

asyncDataは pageコンポーネントでしか使えないみたい。

propsを使えばサブコンポーネントに値を受け渡すことができる。

サブコンポーネントの作成

サブコンポーネントcomponents/Sample.vue として作成する。

propsを宣言しておくと値を外から受け取れるようになり、dataと同じように扱える。

今回は hogeFromPage という名前で受け渡すようにする。

<template>
  <p>hoge from page: {{hogeFromPage}}</p>
</template>

<script>
export default {
  
  props: ['hogeFromPage']
}
</script>

Pageコンポーネントから値を受け渡す。

propsは <sample hogeFromPage='hello!' /> のような形式で渡せるようになるのだけど、VueComponentの値を渡すときは v-bind を使えばよさそう。

<template>
  <div>
    <!-- v-bindで値を渡す -->
    <!-- <sample hogeFromPage='hello!' /> -->
    <sample v-bind="{'hogeFromPage': hoge}" />
  </div>
</template>

<script>
// サブコンポーネントをimport
import sample from '~/components/Sample'

export default {
  data: function() {
    return {
      hoge: 'default'
    }
  },
  // サブコンポーネントの利用
  components: { sample },
  asyncData(context) {
    return {
      hoge: context.query['hoge']
    }
  }
}
</script>

これでhogeの値をサブコンポーネントでも使えるようになるはず。

注意点としては コンポーネントからサブコンポーネントへの依存が発生してしまう こと。

親が子のことを意識しなければいけなくなるので、利用は慎重にした方がよさそう。

参考

ja.nuxtjs.org

Nuxt.jsプロジェクトでjestにpower-assertを導入 #nuxtjs #powerassert

Nuxt.jsのテストをjestで書くというのを先日やってみたが、Assertionにpower-assertを使うようにする。

su-kun1899.hatenablog.com

power-assertの導入

babel連携が必要になるので、 power-assertに加えてbabel-preset-power-assertをインストールする。

github.com

github.com

$ npm install --save-dev babel-preset-power-assert power-assert

.babelrc のpresetsを追記する。

{
  "presets": [
    "power-assert"
  ]
}

Assertを書き換える

importして、assertを書き換えるだけ。

// assertをimportする
import assert from 'assert'
import { mount } from 'vue-test-utils'
import Mikan from '../../components/Mikan'

describe('Mikan', () => {

  test('name is みかん', () => {
    let wrapper = mount(Mikan)

    // Assertをpower-assertに置き換える
    // expect(wrapper.vm.name).toEqual('みかん')
    assert.equal(wrapper.vm.name, 'みかん')
  })
})

power-assertを書くと、失敗時の出力がとても分かりやすくなる。

assert(wrapper.vm.name === 'みかん')
        |       |  |    |
        |       |  |    false
        |       |  "りんご"

メソッドの時もいい感じ。

assert(wrapper.vm.calcAmount(count) === 108)
        |       |  |          |      |
        |       |  300        3      false

nuxt generateで生成した静的ファイルをhttp-serverでホスティングする #nuxtjs

Nuxt.jsには静的ファイルの生成がサポートされており、成果物を静的ファイルとしてホスティングできるようになる。

開発中など、この静的ファイルをNuxt.jsに依存させないで確認したいときに、http-serverを使うと便利。

静的ファイルの生成

vue-cliから作成したNuxtプロジェクトであれば、package.json にscriptが記載されていると思う。

{
  "scripts": {
    "generate": "nuxt generate",
  },

なので、これを実行するだけ。

$ npm run generate

そうすると dist/ 配下に静的ファイルが生成される。

しかしこのままだとHTMLから参照しているJSファイルのパス等が /_nuxt/pages/index.5a196feafb5f24fea7d5.js のようになっており気軽には確認できない。
(ブラウザ等でそのままファイルとして開いてもJSが404になってしまったり)

http-serverの導入

http-serverを使うと、簡単にホスティングできそう。

www.npmjs.com

npmで開発用にインストール

$ npm install http-server --save-dev

scriptを package.json に追記

{
  "scripts": {
    "start-static": "http-server ./dist"
  },

起動する。

$ npm run start-static

http://127.0.0.1:8080ホスティングされる。

ポート等はオプションで差し替えたりできるみたい。

おまけ

プロジェクト内(npm)で完結したかったのでhttp-serverを使ったけど、ただ静的ファイルをホスティングするだけならphpコマンドが簡単だと教わった。

$ php -S localhost:8000 -t dist/

参考

ja.nuxtjs.org qiita.com qiita.com

jestでNuxt.jsのテストを書く #jest #nuxtjs #vuejs

概要

Nuxt.jsで、jestというテスティングフレームワークを使って、コンポーネントのテストを書いてみました。

facebook.github.io

準備

vue-cli でNuxt.jsのプロジェクトは作成済とします。

jest-vue-preprocessorをインストールします。

たぶんVueとjestを連携するためのもの。。

$ npm i -D jest jest-vue-preprocessor babel-jest

vue-test-utilsをインストールします。

きっとテストのUtilでしょう。。

npm i -D vue-test-utils

package.json に、jest用の設定を追記します。

"jest": {
  "moduleNameMapper": {
    "^vue$": "vue/dist/vue.common.js"
  },
  "moduleFileExtensions": [
    "js",
    "vue"
  ],
  "transform": {
    "^.+\\.js$": "<rootDir>/node_modules/babel-jest",
    ".*\\.(vue)$": "<rootDir>/node_modules/jest-vue-preprocessor"
  }
}

テストがjestで実行されるように、scriptsも package.json に追記します。

"scripts": {
  "test": "jest",
  ...
},

プロジェクトルートに、 .babelrc を作成します。

babel用の設定だと思うんですが、babelは知識不足でイマイチよく分かってないです。。
ただしこれがないとテストはエラーになって動きませんでした。

{
  "presets": ["env"]
}

これで、テストがjestで実行できるようになります。

$ npm test

テスト対象のコンポーネント

下記のようなコンポーネントcomponents/Mikan.vue として作成します。

<template>
  <p>{{ name }}{{ priceWithTax }}円</p>
</template>

<script>
export default {
  data: function() {
    return {
      name: 'みかん',
      price: 100
    }
  },
  computed: {
    priceWithTax: function() {
      return this.price * 1.08
    }
  },
  methods: {
    calcAmount: function(count) {
      return this.price * count
    },

    calcAmountWithTax: function(count) {
      return this.calcAmount(count) * 1.08
    }
  }
}
</script>

プロパティのテスト

名前や値段のテストをします。

テスト用のファイルは test/components/Mikan.test.js として作成します。

import { mount } from 'vue-test-utils'
import Mikan from '../../components/Mikan'

// 直接test()を書くこともできるが、
// テストの大まかな単位でdescribeして囲っておくとよさげ
describe('Mikan', () => {
  let wrapper

  // beforeEachは各test毎の実行前に呼び出される
  beforeEach(() => {
    // mountすると、コンポーネントをテストで扱いやすくしたようなラッパーオブジェクトが作られる
    // プロパティやメソッドにアクセス可能になる
    // またDOMにも仮想DOMとしてアクセスできるようになる模様
    wrapper = mount(Mikan)
  })

  test('name is みかん', () => {
    // wrapper.vm でコンポーネントのプロパティにアクセスできる
    expect(wrapper.vm.name).toEqual('みかん')
  })

  test('price is 100', () => {
    expect(wrapper.vm.price).toEqual(100)
  })
})

computedのテスト

プロパティと同じようにテストできます。

  test('priceWithTax is 108', () => {
    expect(wrapper.vm.priceWithTax).toEqual(108)
  })

メソッドのテスト

メソッドもvm経由で呼び出せばOKです。

describe('3 Mikans', () => {
  const count = 3
  let wrapper

  beforeEach(() => {
    wrapper = mount(Mikan)
  })

  test('calcAmount is 300', () => {
    expect(wrapper.vm.calcAmount(count)).toEqual(300)
  })

  test('calcAmountWithTax is 300', () => {
    expect(wrapper.vm.calcAmountWithTax(count)).toEqual(324)
  })
})

テスト結果

npm test すると下記のように結果が表示されます。

  Mikan
    ✓ name is みかん (12ms)
    ✓ price is 100 (1ms)
    ✓ priceWithTax is 108 (1ms)
  3 Mikans
    ✓ calcAmount is 300 (1ms)
    ✓ calcAmountWithTax is 300 (1ms)

scriptを "test": "jest --coverage" に書き換えると、カバレッジも見られるようになります。

-----------|----------|----------|----------|----------|----------------|
File       |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
-----------|----------|----------|----------|----------|----------------|
All files  |      100 |       50 |      100 |      100 |                |
 Mikan.vue |      100 |       50 |      100 |      100 |          29,36 |
-----------|----------|----------|----------|----------|----------------|

参考