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

Nuxt.jsでWebSocketを使ってみる。

バックエンドはexpress-templateを使ってExpressで用意する。(Nuxt.jsはWebSocketのクライアントとして使う)

su-kun1899.hatenablog.com

サーバ側

nuxt-expressテンプレートを使っていることを前提とする。

ExpressでWebSocketを使えるように、 express-ws を追加する。

$ npm install --save express-ws

WebSocket用のエンドポイントを用意する。

server/index.js を修正。

import express from 'express'
import { Nuxt, Builder } from 'nuxt'
// expressWsを使う
import expressWs from 'express-ws'

const app = express()
const host = process.env.HOST || '127.0.0.1'
const port = process.env.PORT || 3000

// WebSocket用のエンドポイントを追加
expressWs(app)
app.ws('/ws', function(ws, req) {
  ws.on('message', function(msg) {
    console.log('from server: ' + msg)
  })
})

// Nuxtのビルド周りは省略

// 起動
app.listen(port, host)
console.log('Server listening on ' + host + ':' + port)

クライアント側

クライアント側はWebSocket-Nodeを使ってみた。

$ npm install --save websocket

実際にやりとりするpageとして pages/ws/index.vue を作成。

画面を表示するとWebSocket接続、ボタン押下でメッセージを送信するようにしている。

<template>
  <div>
    <textarea v-model="message"></textarea>
    <br/>
    <button v-on:click="send">送信</button>
    <p>You recieved message: <b>{{answer}}</b></p>
  </div>
</template>

<script>
import { w3cwebsocket } from 'websocket'
const W3CWebSocket = w3cwebsocket

export default {
  data: function() {
    return {
      // WebSocketクライアントの生成
      socket: new W3CWebSocket('ws://localhost:3000/ws'),
      // クライアントから送る値(textarea)
      message: '',
      // サーバから受け取る値
      answer: ''
    }
  },
  // createdライフサイクルで、Vueインスタンスが作成されたら
  // イベントリスナーを登録しておく
  created: function() {
    // イベントリスナーからVueコンポーネントに値を渡すために一度selfで変数化しておく
    const self = this
    self.socket.onmessage = function(e) {
      if (typeof e.data === 'string') {
        self.answer = e.data
      }
    }
  },
  methods: {
    send: function() {
      // ボタン押下でサーバに値を送る
      this.socket.send(this.message)
    }
  }
}
</script>

参考

github.com github.com github.com qiita.com qiita.com

jestでVuexのmapMutationsがエラーになる #vuejs #nuxtjs #jest

jestでVuexのmapMutationが Unexpected token on mapMutations なエラーになってしまう。

...mapMutations... がbabelによってエラーになってしまっているようだ。

... はスプレッド演算子(object rest spread operator)というらしい。

解決方法

スプレッド演算子をbabelがテスト時もトランスパイルできるように babel-plugin-transform-object-rest-spread を追加する。

$ npm install --save-dev babel-plugin-transform-object-rest-spread

.babelrc に設定を追加する。

{
  //...
  "env": {
    //...
    "test": {
      "plugins": ["transform-object-rest-spread"]
    }
  }
  //...
}

参考

jestでNuxt.jsの依存コンポーネントのパスがエラー #nuxtjs #vuejs #jest

Nuxt.jsはsrcディレクトリやrootディレクトリにエイリアスがついており、importなどではそれを使用する。

こんな感じで。

import child from '@/components/Child'

しかしjestでテストを書こうとした場合に、パスが解決出来なくてエラーになってしまう。

Cannot find module '../pages/index.vue' from 'index.test.js'

解決策

package.jsonで、jestの設定に moduleNameMapper を追加する。

これでパスを意図したとおりに解決してくれる様になる。

"moduleNameMapper": {
  "^@/(.*)$": "<rootDir>/$1",
  "^~/(.*)$": "<rootDir>/$1"
}

shallowを使う

UnitTestであればそもそもサブコンポーネントの依存を切り離してテストをした方がよい。

vue-test-utilsを使っているのであれば、shallowを使うとサブコンポーネントはスタビングされる。

import { shallow } from 'vue-test-utils'
import index from '../pages/index.vue'

describe('shallow sample', () => {
  test('shallowを使ってテスト', () => {
    // shallowの場合はサブコンポーネントがレンダリングされない
    const wrapper = shallow(index)

    expect(wrapper.vm.message).toEqual('Hello, shallow!')
  })
})

参考

期間の範囲を示すパラメータ名について考えてみた #programming

概要

APIのパラメータとして期間の範囲を表す命名について、職場でいい議論がなされたので自分なりにまとめておく。

期間の範囲というのは「開始日時」「終了日時」のようなものを想定。

start-end

扱いやすそうなのはこれ。

日時の場合は、終了側が排他的であった方が扱いやすいことが多い。 ※一ヶ月分取得しようと考えた際に、末日が月によって変わってしまうので、1/1-2/1 と検索可能なほうがうれしい

endは排他の命名としてリーダブルコードでも紹介されている。

ただしリーダブルコードでは begin-end で紹介している。

startとbeginは基本的には置き換え可能らしいが、 startの対義はstopであるのが正しいようだ。

しかし開始時間は start time のように表現することが多いので悩ましいところ。

Kotlinの TimeRanges が使っている。

startInclusive-endExclusive

開始が包含で、終了が排他であることが一目瞭然。

分かりやすさが重視されており、なかなかよい。

endは排他なのは前述の通りなのだが、英語が母語でない人に配慮したのかなと想像している。

しかし「長い/冗長」という意見もあった。(Docにちょっと書いておくくらいでいいのかも、という声も)

Javaで時間の範囲を扱うクラスの引数は上記のような命名になっている。

since-until

ほぼ間違いなく時間を指すだろうことを連想できる。

unitlも排他になる。

しかしsinceはどうやら未来のことを表現するのに相性がよくないようなので、性質をよく考慮したほうがいいかもしれない。
(since には「ご存知のように」的なニュアンスが含まれるらしい)

Goの time.Duration や、 RailsActiveSupportDuration はsince-untilのようだ。

from-to

from-toの場合は、時間軸には向いていないかもしれない。

from Sender to Reciever のように、時間以外の表現の場合は有力な選択肢になりうる。

ちなみにGoogleカレンダーの予定の入力欄を英語版で見たところ from-until でした。

offset-limit

limitは包含のニュアンスになるので、日時を扱う場合は微妙になる。

ただSQL等で使われる馴染みのキーワードであるので、包含させたい場合は有用。

first-last

こちらもlastは包含される。

min-max

maxは包含される。

あまり時間には使わなそう。

RubyTimeComparable のbetweenを使ってるっぽいので、 min-maxってことになるのかな。

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>

参考