kurosame’s diary

フロントエンド中心です

Vue.js の data と methods と computed の使い分けについて

Vue.js で変数や関数を定義する時に data と methods と computed のどのオプションを使って書くのか
たまに迷うことがあるので、現状の理解の簡単なメモです


data と methods と computed の違い

  • data はコンポーネントの(編集可能な)ローカル変数
  • methods は関数
  • computed はプロパティ(値に変数も関数も定義できる)

基本的に computed を使うで良い

  • computed はデフォルトは getter 関数のみを提供しているため、Read-Only なプロパティを定義できる
    ※ ちなみに setter 関数も書けます

  • computed で定義したプロパティはキャッシュされる
    computed は Vue インスタンスを作成する際に、Watcher を生成している
    computed 内で data や props など を参照していた場合、data や props などの変更を Watcher に通知している
    ⇒ Vue インスタンス生成時にプロパティをキャッシュしておき、Watcher に通知があった時のみプロパティを再評価する仕組み

まとめると computed を使うと良い点は

  • 簡単に Read-Only なプロパティを定義できる
  • プロパティの中身で外部(data や props など)参照してても、キャッシュされる
    もちろん単純な定数的なプロパティもきちんとキャッシュされる
  • watch を使っている箇所を減らせるかも
    例えば props を watch して data の変数を更新する処理など computed のみで実装可能なパターンがけっこうあるかも

methods を使っているケース

  • 引数を必要とする
    data や computed などで定義されてない Vue.js 外の変数や関数を引数として渡す場合、computed ではキャッシュできないので、methods に書いている

  • 関数内でイベントを発火している
    キャッシュ時に emit とかでイベントが発火されると困る場合は、methods に書いている また、関数が呼ばれる度に常に実行したい処理があるとき

まとめると methods を使うケースは

  • Vue.js で管理していないパラメータを引数として渡すとき
  • キャッシュされると困る処理があるとき

data を使っているケース

  • 変更が多い変数
    キャッシュしても変更が多い変数はキャッシュする意味があまり無いので、data に書いてる
  • リアクティブな変数を宣言したい時
    変更をリアクティブに画面反映させたい時

まとめると data を使うケースは

  • 変更が多くキャッシュしても意味が無い変数を定義したいとき
  • リアクティブな変数を定義したいとき

E2E テストを Jest + Puppeteer から Cypress へ移行する

はじめに

Jest + Puppeteer で E2E テストを書いてて、使いやすかったのだけど、CircleCI 上で(コード変えてないのに)ちょいちょい落ちるので Cypress を試してみることにした
Cypress にした理由は、Twitter とかで最近名前をよく聞いて、評判も良さそうイメージだったから
Cypress は Chrome のみに対応しているテスト専用のツール
Selenium や Puppeteer とかは、ブラウザを操作して色々やるツールなので、テスト専用ってわけではない


導入

ここ見ながらやってみる docs.cypress.io

yarn add -D cypress
# package.json
"scripts": {
  "e2e": "cypress open"
}
yarn e2e

いくつかサンプルテストを追加してくれたらしい f:id:kurosame-th:20190201145301p:plain

全部のテストを実行する時はこっち f:id:kurosame-th:20190201145845p:plain

特定のテストを実行する時はこっち f:id:kurosame-th:20190201150321p:plain

使いやすい!


Cypress のディレクトリ構造

├── fixtures
│   ├── example.json
│   ├── profile.json
│   └── users.json
├── integration
│   └── examples
│       ├── actions.spec.js
│       ├── aliasing.spec.js
│       ├── assertions.spec.js
│       ├── connectors.spec.js
│       ├── cookies.spec.js
│       ├── cypress_api.spec.js
│       ├── files.spec.js
│       ├── local_storage.spec.js
│       ├── location.spec.js
│       ├── misc.spec.js
│       ├── navigation.spec.js
│       ├── network_requests.spec.js
│       ├── querying.spec.js
│       ├── spies_stubs_clocks.spec.js
│       ├── traversal.spec.js
│       ├── utilities.spec.js
│       ├── viewport.spec.js
│       ├── waiting.spec.js
│       └── window.spec.js
├── plugins
│   └── index.js
├── screenshots
│   └── All Specs
│       └── my-image.png
└── support
    ├── commands.js
    └── index.js
  • fixtures
    スタブ的な感じ
  • integration
    ここにディレクトリ作って、テスト書いていくっぽい
  • plugins
    Cypress 自体の拡張プラグインを設定できる
  • screenshots
    スクリーンショットが置かれる
  • support
    Cypress で定義している関数をオーバーライドしたり、新しく関数を定義できる
    使う時は cy からメソッドチェーンで実行できる
    これかなり便利そう

基本的には、新しくテスト書き始める時は、integration の中にファイルかディレクトリ作って、examples ディレクトリにあるサンプル見ながらテスト書けば良さそう

さいごに

こちらの Vue.js で書かれたリポジトリの E2E テストを Cypress に移行しました github.com

今回は簡単なテストしか作ってませんが、規模が大きくなっても使っていけそうな感じはあります
CircleCI などのCI上で動かしたい時は Cypress の Docker イメージがいくつか用意してあるので、それを使うと楽できます
https://github.com/cypress-io/cypress-docker-images

ローカルで開発中は watch モードがほしいので、cypress openを使い、それ以外の CI で動かす時などはcypress runで良さそうです

実際にテスト書いてみた感想として

  • GUI とコマンドのどちらからクローズしても、GUI やプロセスがちゃんと kill される
    (当たり前のことかもしれませんが、Puppeteer 使ってる時はプロセスが残ってることが多かったので)
  • キャッシュが効いてるので、(変更した箇所以外の)2 回目以降のテストが早い
  • テストコードが少なく、1 つ 1 つのケースが分かりやすい
  • スクリーンショットが多機能だと思った

暇な時に React を使ってるプロジェクトにも導入してみようと思います

CircleCI で Lighthouse を定期的に実行して Slack に通知する

計測ツールの選定

調べると以下の 3 つが良さそうだった

  • WebPageTest
  • PageSpeed Insights
  • Lighthouse

今回は、「CI 上で定期的に計測ツールの API を叩いて、結果を Slack に通知する」ということがやりたい

WebPageTest は非同期的に計測が行われるため、テスト中のステータスをチェックする testStatusAPI のポーリング処理を実装するのが面倒なためパス
PageSpeed Insights は結果が JSON 形式で、Slack に通知する時に加工する必要がありそうなのでパス(HTML で見れると良い)
↑ でもちゃんと調べてないので、いいやり方あるかもしれない

今回は、Chrome DevTools で良く使っており、CircleCI 上で簡単に実行できそうな Lighthouse を使うことにした

ただ、将来的には WebPageTest を使ってがっつり計測するのがいいのかなと思う
そもそも Lighthouse は WebPageTest に組み込まれているので


CircleCI の定期実行の設定

## .circleci/config.yml
---
workflows:
  version: 2
  nightly:
    triggers:
      - schedule:
          cron: '00 0 * * 1'
          filters:
            branches:
              only:
                - master
    jobs:
      - lighthouse

時間は UTC なので、上記だと午前 9 時に実行される


CircleCI の Lighthouse 実行の設定

CI 上でヘッドレスブラウザで Lighthouse を動かす想定なので、必要なプラグインを用意してあげる必要がある

以下を参考に設定 github.com

yarn add -D lighthouse
## .circleci/config.yml
version: 2
jobs:
  lighthouse:
  ...
      - run:
          name: Lighthouse
          command: ./node_modules/.bin/lighthouse --chrome-flags="--headless" --output-path=./lighthouse-results.html https://github.com
  ...

当然ブラウザも必要なので、Chromium をインストールする
CI でフロントエンドのツールを動かす時は Node.js がインストールされた Docker イメージを使って実行しているので、以下のように Dockerfile にコマンドを付け足す

## Dockerfile
RUN apt-get install chromium

Lighthouse が動かず苦戦。。

たぶん上記の設定で動く人は動くのだと思うが、私の場合は全然動かなったので、動かすまでにやったことを備忘録として書いておく

謎エラー

method <= browser ERR:error Browser.getVersion  +1ms

Chromium 起因で起きてるのは、間違いないのだが調べても原因不明

以下を参考にヘッドレスではなく、フルブラウザモードで試してみる github.com

chromium-browser 入らない問題

こんな感じで入れてみる

## Dockerfile
RUN apt-get install chromium-browser xvfb

docker buildすると以下のエラー

E: Package 'chromium-browser' has no installation candidate

え?無いの?

エラーを調べると同じエラーに遭遇している人を発見 stackoverflow.com

wgetChromedeb パッケージをダウンロードして、インストールするようにした

## Dockerfile
RUN apt-get install -yq --no-install-recommends \
    gconf-service libasound2 libatk1.0-0 libcairo2 libcups2 libfontconfig1 libgdk-pixbuf2.0-0 \
    libgtk-3-0 libnspr4 libpango-1.0-0 libxss1 fonts-liberation libappindicator1 libnss3 \
    lsb-release xdg-utils
RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
RUN dpkg -i google-chrome-stable_current_amd64.deb; apt-get -fy install

ヘッドレスモードじゃないと動かない

ヘッドレスモードを解除すると以下エラーになる

ChromeLauncher:error [0127/120527.937238:ERROR:nacl_helper_linux.cc(310)] NaCl helper process running without a sandbox!
ChromeLauncher:error Most likely you need to configure your SUID sandbox correctly

やっぱりヘッドレスモードにする(--chrome-flags="--headless"

sandbox のエラー

Running as root without --no-sandbox is not supported. See https://crbug.com/638180.

--chrome-flags="--no-sandbox"を付けることにする

ようやく動いた

今回使用した Dockerfile です github.com For Puppeteer って所は、今回関係ないので無視してください

CircleCI で Lighthouse を実行している部分です

## .circleci/config.yml
version: 2
jobs:
  lighthouse:
  ...
      - run:
          name: Lighthouse
          command: ./node_modules/.bin/lighthouse --chrome-flags="--headless --no-sandbox" --output-path=./lighthouse-results.html https://github.com
  ...

Slack へ通知

## .circleci/config.yml
version: 2
jobs:
---
- store_artifacts:
    path: ./lighthouse-results.html
- run:
    name: Send to Slack
    command: |
      PAYLOAD=`cat << EOF
      {
        "attachments": [
          {
            "pretext": "Lighthouse result",
            "text": "https://$CIRCLE_BUILD_NUM-XXXXXXXXX-gh.circle-artifacts.com/0/root/project/lighthouse-results.html",
          }
        ]
      }
      EOF`
      curl -X POST -d "payload=$PAYLOAD" $SLACK_WEBHOOK_URL

store_artifacts で Lighthouse の結果を CircleCI 上に保存し、Artifacts というタブから Web 上で見れるようにする

後は、https://$CIRCLE_BUILD_NUM-XXXXXXXXX-gh.circle-artifacts.com/0/root/project/lighthouse-results.htmlのように Artifacts の URL を作って Slack に送信する
CIRCLE_BUILD_NUM は、CircleCI にデフォルトで定義してある現在のビルド番号を持つ環境変数
XXXXXXXXX の部分は、CircleCI のリポジトリごとに振られる番号が入るっぽいので、1 回 Artifacts を作って確かめてみてください


動作確認

CircleCI 全体のコードはこちらにあります github.com

CircleCI の定期ジョブで lighthouse ジョブが実行されると

CircleCI の Artifacts タブに Lighthouse の実行結果 HTML が保存され f:id:kurosame-th:20190129154028p:plain

もちろん結果も見れて f:id:kurosame-th:20190129154140p:plain

実行結果 HTML が Slack に通知されました f:id:kurosame-th:20190129161021p:plain

React と Vue.js と Angular の比較

はじめに

なんとなく React と Vue.js と Angular の比較メモを残したいと思った
(今年は新規プロジェクトの立ち上げが多く、会社でフロントエンドのフレームワークをどれ使ったらいいか聞かれることが多かったため)

私は React 歴半年、Vue.js 歴 1 年半、Angular 歴無しと経験が浅いため、
自分の考えは極力書かず、ネット上や周りのフロントエンドエンジニアに聞いた情報を中心に書こうと思う

※ この記事は随時更新する予定
新しくフロントエンドフレームワークの比較記事が出てきたら、それを読んでこの記事に反映するぐらいの温度感でいます


機能比較

機能 React Vue.js Angular
ベース構文 JS HTML HTML
データバインディング 単方向のデータバインディング(Data ⇒ View) 基本的には単方向のデータバインディング(Data ⇒ View)で構築するが、双方向のデータバインディング(Data ⇔ View)もいける(v-model) 双方向のデータバインディング(Data ⇔ View)
ルーティング react-router(バージョンアップの破壊的変更がキツい) vue-router(これ一択) @angular/router(Angular に標準で付いてる)
状態管理 Flux(最近は Redux 一択かな) Flux(Vuex 一択)   Observable(RxJS や ngrx)
SSR Next.js
SSR 周りのエコシステムが弱いらしい
Nuxt.js を使うと楽にできる Angular Universal
CSS styled-components が良い 「style scoped」が良い
グローバルスコープな CSS の各セレクタにビルド時にハッシュ値を付与して、名前を被らないようにして、ローカルスコープを実現する方法
これを Vue.js は簡単に導入できる
Native アプリのサポート React Native Weex Ionic
PWA のサポート create-react-app でサポート vue-cli でサポート ng add @angular/pwa
マイグレーション - - Ver.6 から機能でng updateというコマンドを使えば、バージョンアップ時のマイグレーションを自動化できる
言語のサポート体制 - - 半年に 1 回のメジャーバージョンリリースとメジャーバージョンごとに 18 ヶ月のサポートが決まっている

サイズ

以下のサイズが重い順
Angular(143k) -> React(43k) -> Vue.js(23k)
括弧内のサイズは環境やバージョンアップ等で変わっていくので、大体の目安と思ってください

学習曲線

React や Angular と比べると Vue.js は初学者に優しいと言われている

  • JSX などの新しい構文を覚える必要がない
  • HTML, CSS, JS の知識があれば書ける

モジュールバンドラー

私は、webpack, Browserify, Parcel, FuseBox を使ってきたが、webpack 一択
Parcel がちゃんと動けば最強だが、凝ったことをやり始めると結局 webpack に戻る
FuseBox は webpack より設定が少ないが、Parcel みたいに設定無しでいけないので、中途半端なイメージ
Browserify とか Gulp は今はもう使ってない


Flow はけっこう落ち目
TypeScript 一択になってきた


よく質問されること

コミュニティの規模やバックアップ企業についてなど

この質問の意図には、将来生き残るであろう言語を選んで使いたいという気持ちがあるのだと思うが、、

React は Facebook、Angular は Google と大企業のバックアップがあるのと比べると Vue.js は元々 Google の AngularJS のチームだった Evan 氏個人で作られたので、
たまに「どうなの?」って気になる人がいるが、今やコミュニティ規模もかなり大きくなったので、心配する必要は無いと思う

ドキュメントの充実さ

ドキュメントの充実さでいうと Vue.js の公式ドキュメントの日本語翻訳は最初はかなり助かる
ただ開発に慣れてきたら、どの言語を選んでもハマると英語を翻訳しながら進めることになると思う

Digdagのワークフロー実行時に見るテーブル

今やっていること

Digdagのキューを効率良く処理させたいため、digdag serverを動かすEC2のAuto Scaling Groupを構築し、その上でDigdagを動かしてみた

この時にキューの処理状況を見たいので、データベースを見たのだが、けっこうテーブルが多い。。 でもたぶん以下の2テーブルだけ見ておけば、大丈夫そうな気がしたので、簡単にメモっとく

queued_tasks

このテーブルで今どのくらいキューが溜まっているか見れる
タスク単位のキューが見れる
タスクが完了すると、そのキューは削除される

tasks

DigdagUI上のTasksとリンクしており、ワークフロー内のタスクのステータスを見ることができる
タスクを処理していくとstateカラムが随時更新されていく
AttemptIDごとに全てのタスクを消化するとテーブルからデータが削除される

ステータスの一例
7: Success
5: Planned
4: Running
0: Blocked
など

React Hooks について [ Basic Hooks編 ]

reactjs.org こちら公式サイトで Basic Hooks と呼ばれてる useState と useEffect と useContext のみ実装してみます
Additional Hooks については、また今度にしますが、useReducer など Redux の reducer のような機能もあって面白そうです

まだ Hooks は v16.7 の α 版の機能なので、以下を実行

yarn add react@16.7.0-alpha.0 react-dom@16.7.0-alpha.0 -D

今回作成したコードは以下のリポジトリにあります github.com ベースに使用したボイラープレートが TSX でしたが、今回の Hooks は TypeScript で書いてないです

useState

Function Component で State を扱えるようになる
useState に値を渡すとそれを初期値にしたStateそのStateを更新する関数を配列で返す

import React, { useState } from 'react'

const UseState = () => {
  const [count, setCount] = useState(1)
  return (
    <div>
      <p>########## UseState ##########</p>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <p>########## UseState ##########</p>
    </div>
  )
}

export default UseState

useEffect

React のライフサイクル関数の代わりとなるもの
useEffect の第 1 引数に実行する関数、第 2 引数に変更を watch する要素を配列で追加する
第 2 引数に指定した配列内の要素のいずれかが更新されたら、第 1 引数の関数を実行する

import React, { useEffect, useState } from 'react'

const UseEffect = () => {
  const [effect1, setEffect1] = useState(0)
  const [effect2, setEffect2] = useState(0)
  const [effect3, setEffect3] = useState(0)
  const [count, setCount] = useState(0)

  useEffect(() => setEffect1(effect1 + 1))
  useEffect(() => setEffect2(effect2 + 1), [count])
  useEffect(() => setEffect3(effect3 + 1), [])

  return (
    <div>
      <p>########## UseEffect ##########</p>
      <p>Effect1: {effect1}</p>
      <p>Effect2: {effect2}</p>
      <p>Effect3: {effect3}</p>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <p>########## UseEffect ##########</p>
    </div>
  )
}

export default UseEffect

effect1 は、useEffect で第 2 引数を渡してないので、常に第 1 引数の関数が呼ばれ続ける
⇒ このような使い方は通常やらないと思う

effect2 は、useEffect で第 2 引数に count を渡しているので、count が変更された時のみ第 1 引数の関数を呼び出す
⇒ componentDidMount + componentDidUpdate の呼び出しのように使えそう

effect3 は、useEffect で第 2 引数に空配列を渡しており、最初のマウント時のみ第 1 引数の関数を呼び出す
⇒ componentDidMount の呼び出しのように使えそう

useContext

Context を利用するための Hook

ContextAPI の Provider にセットした State を useContext で受け取ってみます

import React, { useContext } from 'react'

const Context = React.createContext()
const { Provider } = Context

// Function Component での Provider の作り方の正解がよく分からないので、一旦 class を使っている
class SetProvider extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 2
    }
  }

  render() {
    return (
      <Provider value={{ state: this.state }}>
        <UseContext />
      </Provider>
    )
  }
}

const UseContext = () => {
  const context = useContext(Context)

  return (
    <div>
      <p>########## UseContext ##########</p>
      <p>Context.count: {context.state.count}</p>
      <p>########## UseContext ##########</p>
    </div>
  )
}

export default SetProvider

React の Context API について

ContextAPI

  • v16.3 から追加された機能
  • React のみでいい感じの State 管理ができる
  • Redux とは別の選択肢ができただけと思って良い認識

従来の State 受け渡し

class Parent extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 1
    }
  }

  render() {
    return (
      <div>
        親:
        {this.state.count}
        <Child count={this.state.count} />
      </div>
    )
  }
}

const Child = props => (
  <div>
    子:
    {props.count}
    <Grandchild count={props.count} />
  </div>
)

const Grandchild = props => (
  <div>
    孫:
    {props.count}
  </div>
)

ReactDOM.render(<Parent />, document.querySelector('#app'))

結果

f:id:kurosame-th:20181105172314p:plain

ContextAPI を使った State 受け渡し

const { Provider, Consumer } = React.createContext()

class Parent extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 2
    }
  }

  render() {
    return (
      <Provider value={{ state: this.state }}>
        <div>
          親:
          {this.state.count}
          <Child />
        </div>
      </Provider>
    )
  }
}

const Child = () => (
  <div>
    <Grandchild />
  </div>
)

const Grandchild = () => (
  <Consumer>
    {({ state }) => (
      <div>
        孫:
        {state.count}
      </div>
    )}
  </Consumer>
)

ReactDOM.render(<Parent />, document.querySelector('#app'))

結果

f:id:kurosame-th:20181105173941p:plain

今回は State のみだったが、Action も同じように渡せる

考察

  • Redux でやってたことは普通に実現できそう
  • よく言われているが、小規模であれば Context API はありだが、大規模であれば Redux 等を使った方が良さそう
    • Provider が肥大化すると、メンテナンス性が低そう。ネストもするので、可読性も悪い