Laravel/Vue.js勉強会#3開催しました

こんにちは、SCOUTERの id:kotamat です。

2018/01/23、DMM.com Laboさんにて クリエイターズマッチさん、うるるさんご協力の元 Laravel,Vue.js勉強会第三回を開催いたしました。

laravue.connpass.com

f:id:kotamat:20180124123121j:plain
開始前。雰囲気があるとても広い会場です!

今回も今まで同様、定員を大きく超える方々にご応募頂きました!

今回はconnpassの参加者アンケートで、Vue.jsとLaravelを使用しているかどうかをとってみました。

f:id:kotamat:20180124105010p:plain
Vue.js割合

f:id:kotamat:20180124105213p:plain
Laravel割合

どちらも半数以上の方は業務or趣味で使用しているようですね。 両方業務で使われている方も多く、非常に興味深いアンケートでした!

発表内容

登壇

Vueコンポーネントを複数リポ で共通化するためにやったこと

speakerdeck.com

f:id:kotamat:20180124143420j:plain

弊社エンジニアの id:ryotakodaira の発表です。 複数リポジトリでVueコンポネントを使用するために専用のリポジトリを設置、挙動をstorybookを使って実装しており、そちらの発表でした。

Larvel Echo + Vue.js + axiosで簡単チャットアプリ開発

www.slideshare.net

f:id:kotamat:20180124143629j:plain @hukuzatsuさんに発表いただきました。

チャットアプリケーションをSPAで構築するための方法を細かくお話されており、非常にわかりやすい内容でした!

LT

s.kuriharaさん

f:id:kotamat:20180124143730j:plain

BladeとVue.jsを組み合わせたときのハマるポイントについてお話していただきました。 テンプレートの記法が似ているので、ハマるポイントは想像付いてましたが、予想以上に多い印象でした。

RyutaHamasakiさん

f:id:kotamat:20180124143815j:plain slides.com

LaravelとVueでスクラムのベロシティ管理ツールをつくった話をしていただきました。 1週間で構築したとのことですが、機能的にも非常に使いやすそうな印象でした!

mpywさん

f:id:kotamat:20180124143934j:plain qiita.com

Laravelのお役立ちネタということで6つ紹介していただきました。 それぞれLaravelの内部構造を把握し、ハックしているようなネタで、非常に興味深い内容でした!

k-kurikuriさん

f:id:kotamat:20180124143947j:plain speakerdeck.com

Laravel5.6のChangelogの話をしていただきました。 Laravelはさらに業務用ツールとしての機能拡充に踏み切っているなぁという印象でした!

たけなかさん

f:id:kotamat:20180124144002j:plain PHPとVue.jsでExcelの連携ツールを作ったお話をしていただきました。 ExcelまわりはレガシーなOSSがあったりして技術選定難しい印象ですが、フロントでスプレッドシートっぽいUI構築できるOSSの紹介等ためになるお話でした。

まとめ

皆さんLaravel、Vue.js双方掛け合わせというテーマにそった内容を発表していただきましたが、 それぞれのフレームワークだけの勉強会では聞けないような内容で非常に面白い回となりました!

会場をお貸しいただいたDMM.comLaboの皆さん、ご協力頂いたクリエイターズマッチの皆さん・うるるの皆さん、参加者の皆様ありがとうございました!

SwaggerでAPI仕様書に消耗しているなら.restを使うといい。特にLaravelなら

こんにちはSCOUTERの id:kotamat です。

背景

弊社では、LaravelとVue.jsを使ってSPA + APIサーバーの構成でアプリケーションを構築しています。 APIとクライアントを別で実装すると各々を並列に開発できるので、開発効率も上がるのですが、APIのインターフェースをドキュメント化するのにどうしても工数がかかってしまいます。

最近はWebのgRPCが出てきたり、そこまで行かなくてもProtocol Bufferのver3でjson生成してRESTAPIとする等の方法で互いの通信プロトコルを定義するプロジェクトが増えているので、そちらを使うという選択肢もあります。 しかし、Laravelに標準で入っているFormRequestだったり、Eloquentの便利なリレーションの機能を使おうとするとこれらの機能と競合するため、フロントでは純粋なjsonを使う必要が出てきます。

そこで当初はSwaggerを使ってAPIのインターフェースを定義していたのですが

  • 独自記法を覚える必要がある
  • どうしても記述量が多くなる
  • #definitions/スキーマをモデルごとに書く必要がある
  • 細かいカラムの表示制御(あるAPIではemailを含めるが、他のAPIでは情報保護のためにemailは表示しないなど)を表現しづらい
  • API仕様書自体の保守コストがかかり、実装と違う記述になる

という短所があり、うまくワークしませんでした。 Swagger-PHPも検討しましたが、メソッドのコメントに記述する量が膨大で移行コスト対効果は認められないという判断となりました。

エディターベースのAPI定義: .rest

そういった中で、.rest.httpといったファイル形式でAPI仕様書を書くという方法に着目しました。 エディタごとに多少仕様はことなるのですが、RFC 7230ベースでAPI仕様を記述するだけで、実行可能なAPI仕様書を書くことができます。

今まではVimEmacsAtom等のエディタにプラグインを入れる形で上記の環境を整備出来ていましたが、PhpStormやIntelliJIDEAなどのJetBrains系IDEにも2017.3 EAPから公式導入され、よりプログラマにとって嬉しい環境が整ってきました。

この.rest, .http形式で記述することのメリットとしては、

  1. RFC 7230ベースでの記述となるので、見やすく、学習コストが非常に低く、自動出力の実装コストも低い。
  2. 依存関係がないのでファイルの分割粒度をリクエスト単位に分けても問題無い
  3. エディタのサポートがあることで、API仕様書用に環境を整備しなくても実行可能な環境が構築できる。
  4. ローカルファイルに記述するので、セキュリティー等考える必要がない。

があげられるかと思います。 特に1.の自動出力の実装コストが低いというところは、当初のニーズにマッチしていたため、この方法を採用しました。

Laravelのテストとの親和性

弊社ではLaravelというPHPフレームワークを使用しているのですが、LaravelにはPHPの標準UnitテストフレームワークPHPUnitをLaravel用に拡張したテスト用便利機能が実装されております。 特にAPIを叩き、レスポンスをテストする拡張が非常に使いやすく、下記のように記述するだけです。

// GET
$res = $this->getJson(
   "/api/user/" 
);
// POSTなどでのペイロードは第二引数に
$res = $this->postJson(
   "/api/user/",
   ["name" => "hoge"]
);

こちらは単なるクラスメソッドですので、オーバーロードをすることで、リクエスト、レスポンスに対して何かしらのフックを仕込むことが可能です。

ということで簡単なプラグインを作成

プラグインこちら ※プルリクお待ちしております! 使用方法は下記です。 (こちら https://github.com/kotamat/laravel-apispec-sample に実装サンプルを用意しました。diff)

まずcomposer requireでパッケージをインストールします

composer require --dev kotamat/laravel-apispec-generator

その後、上記APIレスポンスのテストケースを書くクラスにて、ApiSpec\ApiSpecTestCase を継承します。

<?php

namespace Tests\Feature;

use ApiSpec\ApiSpecTestCase;
use Tests\CreatesApplication;

class HttpTest extends ApiSpecTestCase
{
    use CreatesApplication;
    protected $isExportSpec = true;
}

ファイル出力するかどうかは$isExportSpecで切り替える事が可能です(デフォルトはfalse)。 envの値で真偽値を切り替えるといいかと思います。

テストケースで下記のように記述し、実行すると

public function testPost()
{
    $this->postJson("/api/test",['hoge'=>'huga']);
}

/storage/app/api/test/POST.http に当該のファイルが生成されます。(下部のコメントアウトされているところは、レスポンスの値となります。) Laravelにおいて/storage配下はGitに含まれないディレクトリとなりますので、生成後のファイルがGit汚染することはありません。

f:id:kotamat:20180123140339p:plain

ためしに、php artisan serve --port 9000http://0.0.0.0:9000にサーバーを立ち上げ

- POST /api/test
+ POST http://0.0.0.0:9000/api/test

としてあげると、

f:id:kotamat:20180123140404p:plain

このように実行結果が帰ってきます。

まとめ

Swaggerに変わるAPI定義書として、.rest.httpを紹介させていただきました。 今回紹介したようにテストケースとして記述しておけば、そのテストが通っている限り、APIの仕様が最新であることが保証でき、副産物としてテストの記述もれの防止やコードレビューでの補助ツールとして大きくプロジェクト品質に貢献するかと思います。

Nuxt.jsとFirebase/Firestoreで動的コンテンツをPWAする

Nuxt.jsとFirebase/Firestoreで動的コンテンツをPWAする

こんにちは、SCOUTERの id:kotamat です。

こちらは Firebase Advent Calendar 2017 - Qiita 23日目の記事です。

先日SafariがPreview版でServiceWorkerを実装したという話もあり、2018年はServiceWorker,それに伴うPWA関連の技術がより注目されそうですね。 私個人的にも注目の技術の一つとしてキャッチアップしておかないとと最近強く感じてます。

前回はNuxt.jsでPWAを設定する方法を紹介させてもらいました。 techblog.scouter.co.jp

今回はさらにDBを使って動的コンテンツをPWAする方法を検証してみたいと思います。 前回のプロジェクトをそのまま使用する前提で話を進めていきますので、プロジェクトの作成等は上記の記事を参考にしてください。

Nuxt.jsについてはこちらを参考に。

techblog.scouter.co.jp

完成イメージ

FireStoreに保存された画像のURLのリストを元に、Firebaseのホスティング上にレンダリング、さらにそのコンテンツをキャッシュさせてオフラインでも閲覧できるようにします。

画像のURLに関しては、外部サイトの画像のキャッシュを実現するため、tumblrの画像URLを使用してみます。

下記のようにオフラインでも画像が表示されれば完成です。

f:id:kotamat:20171223233906p:plain

Firebase用の環境を設定する

Firebaseのcliをインストールする

$ yarn global add firebase

次にFirebase cli上でログインします。

$ firebase login

対話的にログイン情報を求められるので、入力したらcliの設定は完了です。

モジュールインストール

まずは、Nuxt.jsとFirebaseの連携を簡単にするモジュールをインストールします。

$ yarn add nuxt-firebase

こちらのモジュールは、Nuxt.jsのpageコンポネント上でapp.$firebase(), this.$firebase()のような形でFirebaseのインスタンスを呼び出せるものとなります。

Firebaseには設定が必要ですので、webコンソールからアプリケーションを作成後、下記の設定を取得してください。

f:id:kotamat:20171223233915p:plain こちらのjsオブジェクトをnuxt.config.jsに貼り付けます。

+  env: {
+    apiKey: '',
+    authDomain: '',
+    databaseURL: '',
+    projectId: '',
+    storageBucket: '',
+    messagingSenderId: ''
+  },

Firebaseに必要なファイルの追加

今回はFirebaseの機能のうち

を使用したいと思うので、そこに必要な設定ファイルだけ追加します

firebase.json

{
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "hosting": {
    "public": "dist",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ]
  },
  "storage": {
    "rules": "storage.rules"
  }
}

firestore.indexes.json

{
  "indexes": []
}

firestore.rules

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write;
    }
  }
}

storage.rules

service firebase.storage {
  match /b/{bucket}/o {
    match /{allPaths=**} {
      allow read, write: if request.auth!=null;
    }
  }
}

設定は以上となります

動的コンテンツを生成する

FireStoreに画像のURLを保存していきます。 まずはDatabaseからFireStoreを選択し、使用可能にします。

f:id:kotamat:20171223233929p:plain 次に下記のようにwebコンソール上でデータを追加します。 Valueのところはtumblrから画像URLを取得し、入力します。

f:id:kotamat:20171223233922p:plain

Nuxt.jsのpage上でFirebaseのデータを取得できるようにする

FireStoreではRealtimeDatabaseと同様、オフラインでのキャッシュに対応しています。 app.$firebase.firestore().enablePersistence()という処理を実際の取得処理の前に記述しましょう。

collection('image').get()でデータを取得できるのですが、内部構造は下記のようになっているので、その形で<template>タグ内で画像をよびだします

images:
  docs:
    - data():
      - url: "画像URL"
    - data():
      - url: "画像URL"
    ...

<script>

 export default {
   components: {
     Logo
+  },
+  async asyncData ({app}) {
+    try {
+      await app.$firebase.firestore().enablePersistence()
+      const images = await app.$firebase.firestore().collection('images').limit(5).get()
+      return {
+        images: images.docs
+      }
+    } catch (err) {
+      console.log(err)
+    }
   }
 }
 </script>

<temlpate>

      <div>
        <img
          v-for="image in images"
          :src="image.data().url"
        />
      </div>

本番にデプロイする

nuxt build --spa/distディレクトリにデプロイ用のファイルを生成したのち、firebase deployコマンドでデプロイします。

デプロイが完了すると、https://<projectId>.firebaseapp.comにアプリケーションが反映されます。

デベロッパーツールでオフラインにしても同じように画像が表示されることが確認できるかと思います。

まとめ

Firebase, Nuxt.jsを使うことで、動的コンテンツのPWA化も簡単にできることが確認できました。

Trello+αでCSとのバグ対応を効率化する

f:id:yusuke_kuwa:20171212112715g:plain

株式会社SCOUTERの鍬(id:yusuke_kuwa) です。

今回は、Trelloを使用して、バグ対応の際のビジネスチームとのコミュニケーションを円滑にした方法を紹介します。

こちらはAtlassian(JIRA , Confuence, Trello, Bitbucket)のTips Advent Calendar 2017 - Qiita 12日目の記事となります。

背景

今回想定している対応フローは以下の通りです。

  1. バグ発生

  2. ユーザが弊社CSチームに報告

  3. CSチームがエンジニアに報告

  4. エンジニアが対応

この中で今回は、エンジニアとビジネスチームの間でコミュニケーションコストが発生していた「2. CSチームがエンジニアに報告」の部分を取り上げます。

課題感

業態や取り扱うサービス内容によって様々だとは思いますが、弊社では

  • 報告内容が断片的でバグの対象ユーザを特定できない。

    • 例:「ユーザーからログインできないと報告がありました」
  • Slackで情報が流れる・交錯する

    • 例:大型のアップデートを行った直後で、複数箇所にバグがあった時にSlackが炎上
  • バグ対応の進捗確認のコミュニケーションコストが勿体無い

    • 例:「昨日の◯◯のバグ、どうなっていますか?」

などに課題がありました。

今回はこれらを2つの仕組みで解消していきます。

解決策1:Trelloを使おう

GithubのIssueなど、エンジニアのツールを押し付けてはダメです。

ビジネスサイドが使い慣れているツールでないと使ってくれません。報告が上がらなくなるか、Slackに何でも流れてしまいます。

ということで、Trelloに「バグ対応ボード」を作成し、Trello + IFTTT の運用をはじめました。

Trelloのリストには、

  • 報告リスト

  • 再度ユーザに問い合わせ中

  • 開発修正中

  • Won't Fix

  • Done

を用意し、これにより

  • Slackで情報が流れる・交錯する

  • バグ対応の進捗確認のコミュニケーションコストが勿体無い

を解決できました。

解決策2:欲しい情報を必ず入力してもらおう

残る課題は

  • 報告内容が断片的でバグの対象ユーザを特定できない。

ですね。

Trelloのラベルにユーザ種別や対象サイトURLを用意していたのですが、カードが汚くなるのと、使いにくさもあり浸透しませんでした。

また、テンプレートカードも用意してみましたが、テンプレートをコピペしてからカードを記入するのが煩雑で、同じく浸透しませんでした。

そこで、PowerUpを探しまわって(笑)、欲しい情報を必ず入力できる仕組みを作りました。

最終的に採用したPowerUpはこちらです。

※PowerUpについてはこちらをご覧ください。Get Connected with Trello Power-Ups

このCustomFieldは、"Trelloカードに任意のフィールドを追加できる" というだけのPowerUpです。

それだけの機能といえばそうなのですが、シンプル故に使いやすく、結局社内ではこれが浸透しました。

設定が完了すると、カードの作成画面が以下のようになります。

CustomFieldの入力内容がリストに表示される点も見やすいですね!

f:id:yusuke_kuwa:20171212112715g:plain

現在弊社では、

  • 発覚日時

  • ユーザID

  • 利用ブラウザ

  • 当該URL

  • IntercomURL(ユーザとの会話履歴を辿りたい為)

を入力してもらっています。

おまけ:社内発表してみた時の様子

ということで、最後になりましたが、CustomFieldを導入した後に、社内slackに投稿した時の様子がこちらです。

f:id:yusuke_kuwa:20171211160413j:plain

まとめ

今回はAtlassianのアドベントカレンダーということで、Trelloを活用したバグ報告フローの効率化について紹介しました。

最終的なバグ報告〜対応のフローは以下の通りです。

  1. Trelloを書く(エンジニアが入力して欲しい項目が抜け漏れなく入力される)

  2. Slackに自動で通知される

  3. Trelloのリストでバグ対応の進捗が分かる

かなりスムーズになりましたね!

CSとエンジニアが協力しつつ、お互いの職務に集中できる仕組みづくり、非常に大切です。

Trello等の便利なツールを活用してどんどん行っていきましょう!

Vuexを使って絞り込み機能を実装してみた

こちらはVue.js #4 Advent Calendar 2017 - Qiita 12日目の記事です。

株式会社SCOUTERの小平(id:ryotakodaira) です。

今回はVue.js用の状態管理パターン+ライブラリの「Vuex」を使って、一般的なサービスでよく見かける絞り込み機能を例に実際の実装方法を紹介していきます。

Vuexを導入することでアプリケーションの状態を集中的に管理でき、状態の変更を特定の場所からのみ許可することで予期しない状態変更が起きにくいなどのメリットがあると思います。

また、Vueコンポーネントを細分化していくとコンポーネント間の状態のやり取りにsync修飾子などを使って状態の受け渡しを行うようになるのですが、徐々に状態の受け渡しのコードが複雑になってしまいます。通常このような場合、状態管理パターンを導入して状態遷移を簡単にすることができます。

今回はサンプルとしてQiitaのAPIを使用して簡単に記事のページングや絞り込みを行うことのできるページを作成しました。

完成例

f:id:ryotakodaira:20171211234935g:plain

 

検証実施環境

  • vue: ^2.5.2

  • vuex: ^3.0.0

  • QiitaAPI v2

初期設定

今回はvue-cliを使ってサクッとプロジェクトを立ち上げました。(vue-cliのインストールはこちらを参照)

vue init webpack qiita-time-line

追加で入れたパッケージは以下です。

{
    "axios": "^0.16.1",
    "element-ui": "^2.0.7",
    "lodash-es": "^4.17.4",
    "moment": "^2.19.4",
    "vuex": "^3.0.0"
}

package.json の変更が完了したら、 yarn install を実行しましょう。

コンポーネント

コンポーネントは以下の3つに分割しました。

  • QiitaTimeLine.vue

  • PageSearchOption.vue

    • ページを移動するためのフォームを実装
  • FilterSearchOption.vue

    • 記事をフィルタリングするためのフォームを実装

f:id:ryotakodaira:20171211234239p:plain

Store

itemという名前をモジュールを作成しています。 そこまでコード量が多くなかったため、1つのファイルに action, getter, mutaion を記述しています。 (mutationなどの細かい実装はこちらを御覧ください)

作成したactionは3種類です。

  • setQueryParams

    • APIに送信する検索条件を更新する
  • execGetItems

    • 検索条件と共にAPIにリクエストを送信する
  • setItems

    • 返ってきたデータをstateに格納する
import {cloneDeep} from 'lodash-es'

import types from '../mutation-types'
import initialState from '../initialState/itemList'
import qiitaApi from '../../api/qiitaApi'

const namespaced = true

const state = cloneDeep(initialState)

const getters = {
  state: state => state,
  queryParams: state => state.queryParams
}

const actions = {
  setQueryParams ({commit, state}, queryParams) {
    commit(types.SET_ITEM_LIST_SORT_DATA, {queryParams})
  },

  execGetItems ({commit, state}, queryParams) {
    commit(types.SEND_ITEM_LIST_REQUEST)

    return qiitaApi.getItems(queryParams)
  },

  setItems ({commit, state}, payload) {
    commit(types.RECEIVE_ITEM_LIST_RESPONSE, payload)
  }
}

const mutations = {
  [types.SET_ITEM_LIST_SORT_DATA] (state, {queryParams}) {
    Object.assign(state.queryParams, queryParams)
  },

  [types.SEND_ITEM_LIST_REQUEST] (state) {
    state.isLoading = true
  },

  [types.RECEIVE_ITEM_LIST_RESPONSE] (state, payload) {
    state.isLoading = false
    Object.assign(state, {items: payload})
  }
}

export default {
  namespaced,
  state,
  getters,
  actions,
  mutations
}

検索フォームの実装

まずはJS部分。 mapActions, mapGetters を使って、先程、storeで定義したactions, gettersを読み込みます。

次にフィルタリングを条件の変更を検知するためのwatcherを定義します。 ここでは、 computed で定義されている this.queryParams を監視の対象にしています。

この時に deep: true をつけるのを忘れないようにしましょう。これによりネストしたオブジェクトの中身の変更まで検知してくれるため、 this.queryParams の中身を一つずつwatchする必要がなくなります。 (watcherはこちらを参照)

後はAPIにリクエストを送信するためのメソッドと検索条件を更新するためのメソッドを用意します。

  • load ()

    • APIにリクエストを送信して記事の一覧を更新する
  • handleSelectFilterCondition ()

    • FilterSearchOptionコンポーネントに渡してフォームが変更されるたびに状態を更新する
  • handlePageChange ()

    • PageSearchOptionコンポーネントに渡してフォームが変更されるたびに状態を更新する
import {mapGetters, mapActions} from 'vuex'
import PageSearchOption from './PageSearchOption'
import FilterSearchOption from './FilterSearchOption'

export default {
  name: 'QiitaTimeLine',

  components: {
    PageSearchOption,
    FilterSearchOption
  },

  data () {
    return {}
  },

  mounted () {
    this.load()
  },

  computed: {
    ...mapGetters({
      state: 'item/state',
      queryParams: 'item/queryParams'
    }),
  },

  watch: {
    queryParams: {
      handler: function () {
        this.load()
      },
      deep: true
    }
  },

  methods: {
    ...mapActions({
      execGetItems: 'item/execGetItems',
      setQueryParams: 'item/setQueryParams',
      setItems: 'item/setItems'
    }),

    load () {
      this.execGetItems(this.queryParams).then(res => {
        this.setItems(res.data)
      }).catch(err => {
        console.log(err)
      })
    },

    handleSelectFilterCondition (val) {
      this.setQueryParams({query: val})
    },

    handlePageChange (num) {
      this.setQueryParams({page: num})
    }

  },
}

template部分も実装していきます。 PageSearchOption, FilterSearchOption コンポネントに検索条件を更新するための action(methodsで定義されているhandleSelectFilterCondition, handlePageChange)と現在選択している項目を選択済みにするために queryParams から該当する値を active-item-key として渡してあげます。

PageSearchOption, FilterSearchOption コンポーネントの詳細な実装はこちらを御覧ください!

<template>
  <div>
    <h2>Qiita Time Line</h2>
    <div class="filter">
      <PageSearchOption
        :active-item-key="this.queryParams.page"
        :action="handlePageChange"
      />
      <FilterSearchOption
        :active-item-key="this.queryParams.query"
        :action="handleSelectFilterCondition"
      />
    </div>
    <ul>
      <template v-if="this.state.isLoading === false">
        <li v-for="(item, key) in this.state.items">
          <a :href="item.url" target="_blank">
            <p class="trim green" style="font-weight: bold">{{item.title}}</p>
            <p>公開日時:{{item.created_at | formatJaTime }}</p>
            <p>いいね:{{item.likes_count}}</p>
            <p>タグ:<span class="tag" v-for="(tag, key) in item.tags">{{tag.name}}</span></p>
          </a>
        </li>
      </template>
      <li v-else>
        <p><i class="el-icon-loading"></i> Loading...</p>
      </li>
    </ul>
  </div>
</template>

まとめ

f:id:ryotakodaira:20171211234935g:plain

QiitaTimeLineコンポーネント では queryParamswatch しているため、検索条件が変更されるたびにloadメソッドが呼ばれて、記事の一覧を自動で更新することができます。 そのおかげで、毎回のように明示的にloadメソッドを呼び出して一覧を取得し直す必要がなくなりました!検索条件を新しく追加したいときも queryParams を更新するメソッドを作って適切に呼び出してやるだけでOKです。 この様に状態の変更をフックにすることで、いちいち検索ボタンをクリックしなくても簡単に検索条件を反映することができます。

尚、今回実装したサンプルの完全版はGitHubに公開していますので良かったら触ってみてください!

github.com

Vue.jsでドロップダウンを作り込む

f:id:yusuke_kuwa:20171208145625p:plainこちらはVue.js #4 Advent Calendar 2017 - Qiita 8日目の記事です。

株式会社SCOUTERの鍬(id:yusuke_kuwa) です。

今回は、Vue.jsらしい美しいコードだなと思ったドロップダウンのコンポーネント作成に焦点を当てて、作っていく工程を振り返ってみました。

検証実施環境

  • Vue.js ^2.5.0

  • Element ^2.0.0

  • lodash-es ^4.17.4

  • yarn ^1.0.0

コンポーネントの要件

今回は、以下の要件で使用されるコンポーネントとして作成しています。

  • テーブルや検索結果の件数指定・並び替えなどに使用できる

  • 今なにを選択しているのか分かる

  • 選択した直後にリクエストが送信される

完成イメージはgithubのDropDownです。

f:id:yusuke_kuwa:20171208102852j:plain
github.com-PullRequests

変数とpropsの設計

まずは、コンポーネントとして受け取る値を決めていきます。 上の要件を満たすように実装していきます。

props: {
        label: {
            type: String,
            required: true,
        },
        listItems: {
            type: Object,
            required: true,
        },
        activeItemKey: {
            type: [String, Number],
            required: false,
        },
        action: {
            type: Function,
            required: true,
        },
    },

data() {
        return {
            isActive: false,
        };
    },
  • labelgithubの図でいう "sort"の部分

  • listItemsは選択肢をkey:valueで受け取るObject

  • activeItemKeyは選択中のlistItemのkeyの値

  • actionはドロップダウンの中身のlistItemを選択した時の実行関数

となっています。

今回作成するドロップダウンはあくまでもパーツなので、実行関数は親コンポーネントに持たせます。

コンポーネント側のactionの役割は、

  • リクエストを送る

  • activeItemKeyの値を更新する

などを想定しています。

実装

コンポーネントから渡ってくる値が決まったので、ガシガシと作っていきます。

templateタグ周り

<template>
    <div>
        <div class="all-wrapper">
            <div class="dropdown-wrapper" @click="isActive = !isActive">
                <div class="dropdown-text">
                    {{label}}
                </div>
                <i class="el-icon-caret-bottom"></i>
            </div>
            <transition>
                <div class="list-items" v-if="isActive">
                    <template v-if="existsListItems">
                        <template v-for="(value, key) in listItems">
                            <div class="list-item"
                                 :class="[key == activeItemKey ? 'active' : '' ]"
                                 @click="handleClickItem(key)"
                            >
                                {{value.name}}
                            </div>
                        </template>
                    </template>
                    <template v-else>
                        <div class="list-item">
                            選択肢がありません
                        </div>
                    </template>
                </div>
            </transition>
        </div>
        <div class="dropdown-bg" @click="isActive = false" v-if="isActive"></div>
    </div>
</template>

propsから受け取る値の表示・制御はtemplateタグの中で済んでしまいます。

選択中の要素かどうかのactive判定も、propsで受け取ったactiveItemKeyを使ってテンプレート内で処理してしまいます。

最終的にscriptタグに残る記述は、

computed: {
    existsListItems() {
        return !isEmpty(this.listItems);
    },
},

methods: {
    handleClickItem(key) {
        if (key == this.activeItemKey) {
            return;
        }

        this.isActive = false;
        this.action(key);
    },
},

たったこれだけになりました。良いですね!

疑似背景

また、背景押下時にドロップダウンを閉じる挙動を

<div class="dropdown-bg" @click="isActive = false" v-if="isActive"></div>

が扱っており、CSSで疑似背景を全画面に展開しています。

.dropdown-bg {
    width: 100vw;
    height: 100vh;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 2;
}

スタイリング

ではいよいよ、list-itemにスタイルを当てていきます。

.all-wrapper {
    position: relative;

    .dropdown-wrapper {
        color: #666666;

        display: flex;
        align-items: center;

        &:hover {
            cursor: pointer;
        }

        .dropdown-text {
            font-size: 14px;
        }

        i {
            font-size: 10px;
            margin-left: 6px;
        }
    }

    .list-items {
        width: 260px;
        max-height: 300px;
        background-color: #fff;
        border-radius: 2px;
        border: 1px solid #B9BFC9;
        box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.12), 0 0 6px 0 rgba(0, 0, 0, 0.04);
        position: absolute;
        right: 0;
        overflow-y: scroll;
        z-index: 3;
        padding: 0.5rem 0;

        .list-item {
            color: #333;
            font-size: 14px;
            line-height: 16px;
            padding: 0.75rem 1rem;

            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;

            position: relative;

            &:not(.active):hover {
                background-color: #F3F4F6;
                cursor: pointer;
            }

            &.active {
                color: #fff;
                background-color: #182A4B;
            }
        }
    }
}

背景の上にlits-itemが乗るので、こちらはz-index:3となります。

仕上げのtransition

最後にtransition に、ヌルっと表れるcssを当てて仕上がりです。

Vue.jsのtransactionコンポーネントを使用しているので、クラスの制御はVue.jsに任せて、CSSを書くだけで実装できます。

参考: Enter/Leave とトランジション一覧 — Vue.js

.v-enter-active, .v-leave-active {
    transition: all 0.3s
}

.v-enter {
    transform: translateY(-10px);
}

.v-enter, .v-leave-to {
    opacity: 0
}

完成品

f:id:yusuke_kuwa:20171208114531g:plain

まとめ

今回はVue.jsの習熟度的な観点で難しいことは特にしていませんが、props,template,cssの役割をうまく分散させる設計が肝でしたね。

jQuery等はもちろんですが、Reactでもここまで綺麗に書けないのではないかなと思います。笑

よきVue.jsライフを!  

PWAをNuxt.jsで簡単に体験する

こちらはVue.js #1 Advent Calendar 2017 - Qiita 7日目の記事です。

こんにちは、 id:kotamat です。 最近盛り上がっているPWAを試す際に、Nuxt.jsを使うと非常に簡単に実現できそうだったので、その方法を紹介しようと思います。

はじめに

PWAとは

Progressive Web Appの略で、Webの技術を使って、スマホアプリっぽい挙動を実現できる技術です。 厳密に言うと複数のWeb技術の集合体なのですが、導入としては下記がよく取り扱われます

  • オフライン対応
  • Web通知(ブラウザに通知)
  • ホーム画面への追加とスプラッシュ画像

上記は今までネイティブアプリやハイブリッドアプリ、側ネイティブ等iOSAndroidのプラットフォームに載せるアプリケーションとして開発し、展開していました。 これが、Web上でできることによって、オーガニック流入からシームレスにアプリケーションを起動、その後のリテンションまでWebの技術だけで実現できるようになります。

ネイティブアプリ、Webアプリそれぞれのいいとこ取りをしている技術ということで、いくつかの技術は前から実装されてきていましたが、モバイル系のブラウザが対応してきたり、Safariが実装段階に入ったりと、環境としても整い始め注目度が増しています。

Nuxt.jsとは

こちらの記事でも紹介させていただきましたが、Vue.jsで動くUniversalアプリケーションフレームワークです。 今回はNuxt.jsの特徴であるModuleを使い、PWAを触っていこうと思います。

導入する

Nuxt.jsはVue cliを使うとかんたんに導入できるので、今回はコチラを使おうかと思います。

$ yarn global install vue-cli
$ vue init nuxt-community/starter-template pwatest

とすると下記のようなファイルがpwatestに生成されます

pwatest/
├── README.md
├── assets
│   └── README.md
├── components
│   ├── Logo.vue
│   └── README.md
├── layouts
│   ├── README.md
│   └── default.vue
├── middleware
│   └── README.md
├── nuxt.config.js
├── package.json
├── pages
│   ├── README.md
│   └── index.vue
├── plugins
│   └── README.md
├── static
│   ├── README.md
│   └── favicon.ico
└── store
    └── README.md

次に、PWAモジュールをインストールします。

yarn add '@nuxtjs/pwa'

そして、nuxt.config.jsを下記のように設定します。

...
  build: {
    /*
    ** Run ESLint on save
    */
    extend (config, ctx) {
      if (ctx.dev && ctx.isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
-  }
+  },
+  modules: [
+    '@nuxtjs/pwa'
+  ],
+  workbox: {
+    dev: true, //開発環境でもPWAできるように
+  }
}

あとはビルドして、サーバー立ち上げれば終わりです

nuxt build
nuxt start

f:id:kotamat:20171207015007j:plain

デベロッパーツールを見ると、ServiceWorkerが動いていることが確認できます。

このままオフラインにしても、正常に表示されます。

f:id:kotamat:20171207015021p:plain

なぜなにも設定せずにこのようにキャッシュがされるかというと PWAモジュールはデフォルトでNuxt.jsに最適化された下記のようなキャッシュ戦略を持っているためです。

  • nuxt buildで生成されるjsファイル: precache
  • /_nuxt/*パスのファイル: cacheFirstでキャッシュ
  • /*パスのファイル(staticにある静的ファイル等): networkFirstでキャッシュ

もちろんこれ以外に設定することも可能で、たとえば、tumblr.comの画像をcacheFirstでキャッシュしたい場合は下記の用にnuxt.config.jsに設定します。

+  workbox: {
...
+    runtimeCaching: [
+      {
+        urlPattern: 'https://*.media.tumblr.com/*',
+        handler: 'cacheFirst',
+        method: 'GET',
+      },
+    ],

webプッシュの導入

PWAモジュールにはwebプッシュももちろん導入されています。 webプッシュサーバーを自前で用意してもいいですが、お手軽導入ということでOneSignalを使って導入してみましょう。

OneSignal上でアプリケーションを登録後、

yarn add '@nuxtjs/onesignal'

をし、nuxt.config.jsを下記のように設定します。

  },
  modules: [
+    '@nuxtjs/onesignal', // pwaの前に定義
    '@nuxtjs/pwa'
  ],
  
+  oneSignal: {
+    init: {
+      appId: '設定したappId',
+      allowLocalhostAsSecureOrigin: true,
+      welcomeNotification: {
+        disable: true
+      }
+    }

再度ビルド、サーバー立ち上げを行います。 OneSignalのコンソールからメッセージを送信すると下記のようにwebプッシュが送信されます

f:id:kotamat:20171207015150p:plain

manifestの設定

Android等のモバイル端末で、ホーム画面に追加すると、アプリっぽい挙動になるmanifestの実装も下記のようにnuxt.config.jsに設定するだけです。

+  manifest: {
+    name: 'PWATest',
+    short_name: 'PWA',
+    title: 'PWATest',
+    'og:title': 'PWATest',
+    description: 'PWATest',
+    'og:description': 'PWATest',
+    lang: 'ja',
+    theme_color: '#ffffff',
+    background_color: '#ffffff'
+  },

ホーム画面に表示するアイコン画像はデフォルトだと/static/logo.pngを読みに行くので、正方形の画像を用意して置いておきましょう。

そうすると、下記のように、ホーム画面に設置すると、アイコンが表示され、タップするとスプラッシュ画面とともにアプリケーションが表示されます

f:id:kotamat:20171207015654j:plain f:id:kotamat:20171207015701j:plain f:id:kotamat:20171207015710j:plain

まとめ

今回はPWAの基本となる3点の機能を紹介させていただきました。 他にも機能はあるので、興味のある方は GoogleWebFundamentals等を見るといいかと思います。