20200517のvue.jsに関する記事は12件です。

html要素のidにコロンを指定するのは注意!

そのidを検索する際の処理で躓く可能性があります、っていう話です。

はじめに

実績がない初心者なりに、アプリコンテストを目指したアプリの試作をしている今日此頃。

ところが、想定外していなかったミスで、数時間も食う事件が発生しました。

今回はその事件の原因について、自分への戒めも兼ねて記します。

事件背景

先述したアプリについて、ざっくりいうと、下のような構成です。

  • 外部API
  • フレームワーク
    • vuejs
  • レスポンシブデザイン向けライブラリ
    • bootstrap
  • 地図ライブラリ
    • leafletjs

要は外部APIからデータを取得し、そのデータと地図を連携させ、更にそれをbootstrapで良い感じに見せたい!というもの。

事件は、bootstrapのcollapse(折りたたみ)を使っている時に起こりました。

事件詳細

詳細なデータを受け取り、それをアコーディオン表示をさせようと、サンプルをほぼそのまま参考に、カード部分をコンポーネントにしました。vuejsが分かる人向けのイメージは以下。

Detail.vue
<template>
    <div id="detail">
        <div
        class="accordion"
        :id="cardParent"
        >
            <Card
            v-for="data in jsonData"
            :key="data['@id']"
            :cardId="data['@id']"
            :title="data['title']"
            :parentId="cardParent"
            />
        </div>
    </div>
</template>
Card.vue
<template>
    <div
    class="card"
    >
        <div
        class="card-header"
        :id="this.headerId"
        >
            <button
            class="btn btn-link btn-block text-left collapsed"
            type="button"
            data-toggle="collapse"
            :data-target="'#'+this.bodyId"
            aria-expanded="false"
            :aria-controls="this.bodyId"
            >
                {{this.title}}
            </button>
        </div>
        <div
        :id="this.bodyId"
        class="collapse card-body"
        :data-parent="'#'+this.parentId"
        >
            <p>テスト</p>
            <!--
                外部APIから取得したデータの
                詳細をここに記す
            -->
        </div>
    </div>
</template>
<script>

カードをどれだけ作るかは、外部APIのデータやユーザーの操作次第。なので、カードやその内部要素のidは、コンポーネント内では決められません。

そこで、外部APIのデータのidを元に、カードや内部要素のidを決めることに。

idがid00001なら、カードはid00001、内部要素はid00001_headerとするように、コードを記述しました。

(vuejsが分かる人向けに言うと、Card.vueではcardIdをプロパティに受け取り、それに基づき、算出プロパティで内部要素用idをセットしています)

サンプルほぼそのままなので、これで問題はないはず!と動かした所、最初は問題なく表示されているように見えました。

でも、何かがおかしい。よく見ると、アコーディオン表示が開けない。

しかし、サンプルコードを見比べても、動作しているhtmlを見ても、何も問題がない。

加えて、display : noneできちんとDOMは描画されている。

一体、何だ!?何が原因なんだ!?!?!?


犯人

結局、上記のミスに気づくのに数時間を費やしてしまいました。

まだ扱いに慣れていないbootstrapばかりに気を取られて、原因は全く関係のない所にあったことに気づくのが遅かった為です。

実は、外部APIのデータのidをもとに、カードのidとしたのが、犯人でした。

外部APIからのjsonデータ(のid)に、記号、コロンなどが含まれていました。

このidを、直接カードのidに指定していたのが、全ての問題の始まり。

別に、htmlの要素のidに、コロンを含む記号は、問題なく指定できます。

シンプルなgetElementById("変なid名")も、特に問題なく動きます。

しかし。ライブラリによっては、id名を元にする検索などで、思うように動いてくれない場合があります

今回は恐らく、bootstrapが依存するjQueryの問題ではないかと個人的に考えています。

(参考 : https://www.buildinsider.net/web/jqueryref/036 )

終わりに

教訓はこの2つ。以後気をつけたい。

  • idに記号を使う場合は要注意
    • ハイフンなど以外、入れないのが非常に好ましい
  • 外部データの値をidに使う場合は要注意
    • 意図せず記号が入ってしまう場合がある

余談

該当のコードは、v-forの要素数オプションも使って、下のようにid名を工夫した。

Detail.vue
<template>
    <div id="detail">
        <div
        class="accordion"
        :id="cardParent"
        >
            <Card
            v-for="(data,i) in jsonData"
            :key="data['@id']"
            :cardId="'card'+i"
            :title="data['title']"
            :parentId="cardParent"
            />
        </div>
    </div>
</template>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

はじめてのVue.js

概要

  • 公式のGET STARTEDを参考に、やりたいことベースでどうコードに落とし込むかまとめてみました
  • 自分の興味や必要になったものを随時追加していきます
  • 実際の動作を確認されたい場合はこちらをご覧ください?‍♂️

1. 基本の基

<div id="app_1">
    {{ message }}
</div>

<script>
    var app_1 = new Vue({
        el: '#app_1',
        data: {
            message: 'Hello Vue!'
        },
        methods: {
            hello: function() {
                console.log('Hello Vue!');
            }
        }
    });
</script>
  • Vueオブジェクトを利用する
  • elフィールドでレンダリングの対象DOMを指定
  • dataフィールドでレンダリングするデータをkey: valueで定義する
  • DOMの内部に{{key}}の形式で記述をするとdataないのkeyに該当するvalueがレンダリングされる
  • 後述のイベント等で使用できる関数をmethodsフィールドで定義できる
  • dataやmethodsの参照方法は以下の通り
    • 自分自身のdata: this.message
    • 自分自身のmethods: this.hello()
    • 外部からdata: app_1.message
    • 外部からmethods: app_1.hello()

2. 制御構文

2-1. 条件分岐

2-1-1. 単一エレメントの出し分け

<div id="app_2_1_1">
    <p v-if="condition">Condition: true</p>
    <p v-else="condition">Condition: false</p>
</div>

<script>
    var app_2_1_1 = new Vue({
        el: '#app_2_1_1',
        data: {
            condition: true
        }
    });
</script>
  • v-ifを用いる
  • 属性には真偽値になるような値や条件式を設定する
  • v-elseを用いるとelse構文と同じ働きをしてくれる

2-1-2. 複数エレメントの出し分け

<div id="app_2_1_2">
    <template v-if="condition">
        <p>multiple element</p>
        <p>multiple element</p>
    </template>
</div>

<script>
    var app_2_1_2 = new Vue({
        el: '#app_2_1_2',
        data: {
            condition: true
        }
    });
</script>
  • 単位エレメントとの違いは、出し分けるエレメントを<template>タグで囲うこと

2-1-3. 文字列比較

<div id="app_2_1_3">
    <p v-if="word === 'A'">The word is A</p>
    <p v-else>The word is not A</p>
</div>

<script>
    var app_2_1_3 = new Vue({
        el: '#app_2_1_3',
        data: {
            word: 'A'
        }
    });
</script>
  • v-ifの中身は真偽値になれば良いので文字列比較もできる
  • 文字列は'で囲うことを忘れずに

2-2. ループ

2-2-1. 配列

<div id="app_2_2_1">
    <ul>
        <li v-for="item in items">{{item}}</li>
    </ul>

    <ul>
        <li v-for="(item, index) in items">{{index}} - {{value}}</li>
    </ul>
</div>

<script>
    var app_2_2_1 = new Vue({
        el: '#app_2_2_1',
        data: {
            items: ['A', 'B', 'C']
        }
    });
</script>
  • v-forを使用
  • item in itemsitemsから要素をひとつずつ取り出しitemに格納
  • {{item}}と記述することでレンダリングされる
  • 要素番号も取得したい場合は(item, index) in itemsと記述
    • itemが先なので注意

2-2-2. Map(連想配列)

<div id="app_2_2_2">
    <ul>
        <li v-for="value in items">{{value}}</li>
    </ul>

    <ul>
        <li v-for="(value, key) of items">{{key}} : {{value}}</li>
    </ul>
</div>

<script>
    var app_2_2_2 = new Vue({
        el: '#app_2_2_2',
        data: {
            items: {
                'A': 'Apple',
                'B': 'Banana',
                'C': 'Cat'
            }
        }
    });
</script>
  • 配列と同じ使い方をした場合はkey: valueのうちvalueのみが取り出される
  • keyも取得したい場合は(value, key) in itemsと記述
    • valueが先なので注意
  • inofはどちらでも良いっぽい
    • 公式ドキュメントでも基本inだけどJavascriptの構文に合わせるためにofも使えるよみたいな描かれ方をしている

2-2-3. 複数要素のループ

<div id="app_2_2_3">
    <table>
        <template v-for="(item, index) in items">
            <tr>
                <td>{{index}}</td>
                <td>{{item}}</td>
            </tr>
        </template>
    </table>
</div>

<script>
    var app_2_2_3 = new Vue({
        el: '#app_2_2_3',
        data: {
            items: ['A', 'B', 'C']
        }
    });
</script>
  • ループさせるエレメントが複数になる場合は、v-if同様に<template>タグで囲う

3. ユーザー入力

3-1. 入力値をレンダリング

<div id="app_3_1">
    <input type="text" v-model="message">
    <p>The input message is {{message}}</p>
</div>

<script>
    var app_3_1 = new Vue({
        el: '#app_3_1',
        data: {
            message: "default"
        }
    });
</script>

4. イベント

4-1. click

<div id="app_4_1">
    <button v-on:click="method1">ボタン</button>
</div>

<script>
    var app_4_1 = new Vue({
        el: '#app_4_1',
        methods: {
            method1: function(event) {
                alert('Button was clicked');
            }
        }
    });
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxtでショッピングカートに商品情報を追加する

目的

個人開発してるサービスでカート機能を実装したい
今回はカートに入れるボタンを押下したらカートに商品情報が入るようにしたい

参考サイト

Nuxt.jsのStoreによるデータ保存 [vuex-persistedstate][js-cookie]

Vuex でショッピングカートを実装できる無料コース「Vuex for Everyone」を受講した

仕様

カートに入れる情報は下記を想定

  • イベントID
  • 商品ID
  • 個数
  • 区分(すぐ買う/後から配送)

同じ商品情報が存在した場合は、個数をインクリメントする
1週間ほどはカート情報は保持しておきたい

実装

カート情報を入れるVuexの実装

仕様でも書いたとおりカートには下記の情報を保持するようにする

  • イベントID
  • 商品ID
  • 個数
  • 区分(すぐ買う/後から配送)

イベントIDと商品IDと区分の同一なデータが有ればINCREMENT_QUANTITYが無ければPUSH_PRODUCT_TO_CARTが実行されるようにする

store/cart.js
export const getters = {
  getCartProducts(state) {
    return state.products
  }
}

export const state = () => ({
  products: []
})

export const mutations = {
  PUSH_PRODUCT_TO_CART(state, payload) {
    state.products.push({
      eventId: payload.rootState.event_info.eventSelected.id,
      id: payload.product.id,
      quantity: 1,
      // カート追加時にイベントが開催中であれば、配送の区分を0(今すぐ受取る)にする
      // 終了後であれば配送区分を1(自宅へ配送)に設定する
      delivery:
        new Date(
          payload.rootState.event_info.eventSelected.end_date
        ).getTime() > new Date().getTime()
          ? 0
          : 1
    })
  },
  INCREMENT_QUANTITY(state, payload) {
    payload.cartItem.quantity++
  }
}

export const actions = {
  addProductToCart({ commit, rootState, state }, product) {
    const cartItem = state.products.find(
      item =>
        item.id === product.id &&
        item.eventId === rootState.event_info.eventSelected.id &&
        (item.delivery ===
        new Date(rootState.event_info.eventSelected.end_date).getTime() >
          new Date().getTime()
          ? 0
          : 1)
    )

    if (!cartItem) {
      commit('PUSH_PRODUCT_TO_CART', { rootState, product })
    } else {
      commit('INCREMENT_QUANTITY', { rootState, cartItem })
    }
  }
}

カート追加処理を実装

呼び出し部分

<div class="field-button">
      <input
        type="submit"
        class="button is-blue is-large is-fullwidth"
        value="カートに入れる"
        @click="addProductToCart(product)"
      />
</div>

カート追加するVuexのAction呼び出し

addProductToCart(product) {
    this.$store.dispatch('cart/addProductToCart', product)
}

カート商品情報の永続化設定

Vuexは、ブラウザを更新したらデータが消えてしまうため
永続化を行うために下記を行う

モジュールをインストール

vuex-persistedstate
vuexを永続化させるためのモジュール
https://github.com/robinvdvleuten/vuex-persistedstate

js-cookie
cookieをnuxtで使用するためのモジュール
https://github.com/js-cookie/js-cookie

npm install vuex-persistedstate
npm install js-cookie

永続化の設定

plugins/cookie-storage.js
import createPersistedState from 'vuex-persistedstate'
import * as Cookies from 'js-cookie'

const cookieStorage = {
  getItem: function(key) {
    return Cookies.getJSON(key)
  },
  setItem: function(key, value) {
    return Cookies.set(key, value, { expires: 7, secure: true })
  },
  removeItem: function(key) {
    return Cookies.remove(key)
  }
}

export default context => {
  createPersistedState({
    key: 'products-cart',
    paths: ['cart.products'],
    storage: cookieStorage,
    getState: cookieStorage.getItem,
    setState: cookieStorage.setItem
  })(context.store)
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js で ag-grid を使ったときの使い方備忘

参考資料

  1. 公式サイト: https://www.ag-grid.com/vuejs-grid/
  2. ag-gridの使い方

準備

  • どの画面にグリッドを作るのか考える
  • どういうグリッドを作りたいか考える(何行何列で、各列には何を表示するのか、横幅など)
  • ag-grid-community, ag-grid-vue をインストールしておく
  • package.json はこうなってました(2020.5時点)
"dependencies": {
    "ag-grid-community": "^21.2.2",
    "ag-grid-vue": "^21.2.2",
 ...
    "vue": "^2.6.10",
 ...

.vue ファイルでやること

  • scriptでag-grid-vue を importする
 import {AgGridVue} from "ag-grid-vue";
  • コンポーネントに登録
export default {
  components: {
    AgGridVue,
  }
  • template 内にそのグリッドのコンポーネント名でタグを作成する
<template>
  <div>
      <AgGridVue id="grid" class="ag-theme-blue" 
          :rowData="rowData"
          :gridOptions="gridOptions">
      </AgGridVue>
  </div>
</template>
  • "rowData" や "gridOptions" はこの .vue の data() や props やcomputed にこれらの値があるということです。今回の例ではこんなふうになっています。
  data() {
    ...
    return {
      gridData : [],
      gridOptions: { ... }
    }
  },
  computed: {
    rowData () {
      return this.gridData;
    }
  },
  • ここでrowData が算出プロパティなのは深い意味はないです。直接データにある gridData をグリッドに渡してもいいと思います。

  • gridOptions というデータに設定する中身がグリッドの形や動作を決める設定になります。

  • 今回の gridOptions はこんな感じです

      gridOptions: {
        // 列の定義
        columnDefs:colDefs,

        // 1行選択モード
        rowSelection: 'single', 

        // 1行の高さ(タグ属性でも指定できるが、今回はオプションにした)
        rowHeight : 32, 

        // ページング機能を有効にするかの設定。タグ属性では指定できない模様
        pagination: true, 
        paginationAutoPageSize : true,

        // EVENTS - add event callback handlers
        // 行が選択されたイベント
        onRowSelected(params) { 
          params.api.redrawRows(); // 選択されたとき行の色を変える
        },
        // 行のスタイルを返すコールバック
        getRowStyle(params) {
          // 選択行のスタイル
          if (params.node.selected) {
            return { background: 'white' };
          } else { // 通常行のスタイル
            return { background: '#f1e8e8' };
          }
        },
        // グリッドの準備完了イベント
        onGridReady(params) {
          this.gridApi = params.api;
          this.columnApi = params.columnApi;

          // カラム幅を全体幅に合わせる.
          // これを呼ばないとcolumnDefが反映されません。
          this.gridApi.sizeColumnsToFit(); 

          console.log('gridReady is called.'); 
        },
      },
  • 列の定義として colDefs というのを渡していますが、こんな感じです。
    const colDefs = [{
        headerName: '#', 
        field: 'order', 
        maxWidth:50, 
        cellStyle: {textAlign: 'center'}
      },{
        headerName: '商品番号', 
        field: 'productCd', 
        maxWidth: 85,
        cellStyle: {textAlign: 'left'}
      },{
        headerName: '商品名称', 
        field: 'name', 
        cellStyle: {textAlign: 'left'}
      }];
  • ここでは列ヘッダとして #、 商品番号、 商品名称の3列を表示しています。
  • field というのは rowData に渡したオブジェクト配列の各要素にあるプロパティの名前と合わせる必要があります。つまり rowData[i].order とか rowData[i].productCd とかで保持している値がグリッドの各行の対応する列に表示されるわけです。
  • maxWidth というのは、その列に入れた値が長い文字列だったときなどに最大どのくらいまで列が拡がるかです。1列目の # だったら50pixel分まで広がるわけです。同様に minWidth というプロパティも設定できます。
  • 列の数はcolumnDefs の設定で決まりますが、行の数は何で決まるかというと、rowData に渡したオブジェクト配列の要素数で決まります。

実際の表示

こんな感じです。
無題.png

注意点:ヘッダ部分の text-align を center にしてもヘッダのテキストがセンタリングされない問題

  .ag-header-cell-text {
    flex: 1; /* ヘッダのテキストの位置を変更するにはこれが超重要らしい */
  }

その他メモ

  • ag-gridのサンプルなどでタグ属性値の先頭にコロン(:)を付けているのはVue.jsの v-bind の省略記法です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【備忘】Vue.js で ag-grid を使う

参考資料

  1. 公式サイト: https://www.ag-grid.com/vuejs-grid/
  2. ag-gridの使い方

準備

  • どの画面にグリッドを作るのか考える
  • どういうグリッドを作りたいか考える(何行何列で、各列には何を表示するのか、横幅など)
  • ag-grid-community, ag-grid-vue をインストールしておく
  • package.json はこうなってました(2020.5時点)
"dependencies": {
    "ag-grid-community": "^21.2.2",
    "ag-grid-vue": "^21.2.2",
 ...
    "vue": "^2.6.10",
 ...

.vue ファイルでやること

  • scriptでag-grid-vue を importする
 import {AgGridVue} from "ag-grid-vue";
  • コンポーネントに登録
export default {
  components: {
    AgGridVue,
  }
  • template 内にそのグリッドのコンポーネント名でタグを作成する
<template>
  <div>
      <AgGridVue id="grid" class="ag-theme-blue" 
          :rowData="rowData"
          :gridOptions="gridOptions">
      </AgGridVue>
  </div>
</template>
  • タグ属性値の先頭にコロン(:)を付けているのはVue.jsの v-bind ディレクティブの省略記法です。つまりこう書いたのと同じ意味です。
<template>
  <div>
      <AgGridVue id="grid" class="ag-theme-blue" 
          v-bind:rowData="rowData"
          v-bind:gridOptions="gridOptions">
      </AgGridVue>
  </div>
</template>
  • "rowData" や "gridOptions" はこの .vue の data() や props やcomputed にこれらの値があるということです。今回の例ではこんなふうになっています。
  data() {
    ...
    return {
      gridData : [],
      gridOptions: { ... }
    }
  },
  computed: {
    rowData () {
      return this.gridData;
    }
  },
  • ここでrowData が算出プロパティなのは深い意味はないです。直接データにある gridData をグリッドに渡してもいいと思います。

  • gridOptions というデータに設定する中身がグリッドの形や動作を決める設定になります。

  • 今回の gridOptions はこんな感じです

      gridOptions: {
        // 列の定義
        columnDefs:colDefs,

        // 1行選択モード
        rowSelection: 'single', 

        // 1行の高さ(タグ属性でも指定できるが、今回はオプションにした)
        rowHeight : 32, 

        // ページング機能を有効にするかの設定。タグ属性では指定できない模様
        pagination: true, 
        paginationAutoPageSize : true,

        // EVENTS - add event callback handlers
        // 行が選択されたイベント
        onRowSelected(params) { 
          params.api.redrawRows(); // 選択されたとき行の色を変える
        },
        // 行のスタイルを返すコールバック
        getRowStyle(params) {
          // 選択行のスタイル
          if (params.node.selected) {
            return { background: 'white' };
          } else { // 通常行のスタイル
            return { background: '#f1e8e8' };
          }
        },
        // グリッドの準備完了イベント
        onGridReady(params) {
          this.gridApi = params.api;
          this.columnApi = params.columnApi;

          // カラム幅を全体幅に合わせる.
          // これを呼ばないとcolumnDefが反映されません。
          this.gridApi.sizeColumnsToFit(); 

          console.log('gridReady is called.'); 
        },
      },
  • 列の定義として colDefs というのを渡していますが、こんな感じです。
    const colDefs = [{
        headerName: '#', 
        field: 'order', 
        maxWidth:50, 
        cellStyle: {textAlign: 'center'}
      },{
        headerName: '商品番号', 
        field: 'productCd', 
        maxWidth: 85,
        cellStyle: {textAlign: 'left'}
      },{
        headerName: '商品名称', 
        field: 'name', 
        cellStyle: {textAlign: 'left'}
      }];
  • ここでは列ヘッダとして #、 商品番号、 商品名称の3列を表示しています。
  • field というのは rowData に渡したオブジェクト配列の各要素にあるプロパティの名前と合わせる必要があります。つまり rowData[i].order とか rowData[i].productCd とかで保持している値がグリッドの各行の対応する列に表示されるわけです。
  • maxWidth というのは、その列に入れた値が長い文字列だったときなどに最大どのくらいまで列が拡がるかです。1列目の # だったら50pixel分まで広がるわけです。同様に minWidth というプロパティも設定できます。
  • 列の数はcolumnDefs の設定で決まりますが、行の数は何で決まるかというと、rowData に渡したオブジェクト配列の要素数で決まります。

実際の表示

こんな感じです。
無題.png

注意点:ヘッダ部分の text-align を center にしてもヘッダのテキストがセンタリングされない問題

  .ag-header-cell-text {
    flex: 1; /* ヘッダのテキストの位置を変更するにはこれが超重要らしい */
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vueで改行に合わせて伸縮するテキストフォーム

実装

textareaを利用することを想定。rowsの値を変更してformを伸び縮みさせている。

function fitToForm(target: HTMLFormElement, text: string, defaultRows = 1): void {
    const lines = text.match(/\n/g)?.length || 1;
    if (defaultRows <= lines) {
      target.rows = lines + 1;
    }
    if (defaultRows > lines) {
      target.rows = defaultRows;
    }
}

利用イメージ

v-model渡しているstateをfitToFormの第二引数に渡しています。
第三引数にtextareaのデフォルトのrowsを設定すると、それ以上小さくなりません。

<textarea
    v-model="comment"
    placeholder="コメントを入力"
    rows="4"
    type="text"
    @input="fitToForm($event.target, state.comment, 4)"
/>

完成gif

May-17-2020 14-58-11.gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js 3.0】Composition API なんかと同じ空間にいられるか!俺は 2.x 系に戻るぞ!

Vue.js 2.x から 3.0 へ

Vue.jsの開発ロードマップ によると、2020 年 Q2 での公式リリースが予定されています。
β版もリリースされ、だんだんメジャーアップデートに近づいてきた感じがしますね。

Vue.js 3.0 への アップデートに関する記事をチェックしていると、かなり記述方法が変更されそうだな、、という印象を受けました。
かつ記述方法が JS 寄りになった印象もあり、ライトに Vue.js を使用していた方は 3.0 へのアップデートに抵抗がある方も少なくないのではないでしょうか?
自分はそこで抵抗感を持ってしまい、このタイトルの思考に行き着きました。

ただこのセリフを吐いた者がこの後どうなってしまうかは、おそらく皆さんご存知のことかと思います。
悲しい結末を迎えないためにも、しっかりアップデート内容を確認し、追従していきたいところです。

アップデートでの変更点は?

  • Composition API
  • フラグメント / ポータル
  • TypeScripts のフルサポート

などが発表されています。
これらに関しては、Vue.js のコミッターである kazupon さんの解説資料

で一望することができます。

本記事では、この中でもアップデートの中心となりそうな Composition API について確認していきます。

そもそも Composition ってどういう意味?

構成(すること)、組み立て(ること)、合成、混成、組み立てられたもの、構成物、合成物、混合物、(合成)成分、作文1

単純に辞書からの引用ですが、 文字のみから推察するに Vue.js の構成、組み立て方(つまりコードの記述方法自体ですかね。。)に変更を与える機能実装のように思えます。

2.x 系の記述方法はどうなる?

前述しましたが、自分は Composition API の導入により、かなり記述方法が変更されそうだな、、という印象を受けました。

今後 Vue.js を使っていくにあたって、2.x の data や methods などを使用した書き方(Options API ベースの書き方と言うようです)は使えなくなるのでは?、という不安をもたれた方も少なくないのではないでしょうか。

まずこの点について言及しておくと、Vue.js 3.0 では、既存の API と併用 が可能なようです。(削除予定、非推奨となっている物は除きます。)

ただしアップデートによって追加される Composition API は従来の記法である Options API より先に解決されるため、併用する場合は Options API で定義されたプロパティには Composition API からはアクセスできないようです。

Usage Alongside Existing API

The Composition API can be used alongside the existing options-based API.

・ The Composition API is resolved before 2.x options (data, computed & methods) and will have no access to the properties defined by those options.
・ Properties returned from setup() will be exposed on this and will be accessible inside 2.x options.

少し話が逸れますが、Vue.js の開発ロードマップには以下のように記述されています。

Q: As a new user, should I start with Vue 2 now or wait for 3.0?

・ If you are just starting to learn the framework, you should start with Vue 2 now, since Vue 3 does not involve dramatic re-designs and the vast majority of your Vue 2 knowledge will still apply for Vue 3. There's no point in waiting if you are just learning.

新規に Vue.js を使用する場合 2.x 系を使用するか 3.0 を待つ方が良いのか、という話ですが、2.x 系の知識の大部分は 3.0 でも変わらず適用可能なので、新たに Vue.js を学習するユーザーは 2.x 系の記法から始めるべき、ということのようです。

Composition API 導入のモチベーション

Composition API の RFC ページ には、下のような比較図がみられます。アップデートに興味がある方であれば、一度は見たことがあるのではないでしょうか。

Options API が従来の記述方法、Composition API が Vue.js 3.0 で新たに導入される記述方法ですね。

62783026-810e6180-ba89-11e9-8774-e7771c8095d6.png

ぱっと見て思いました。
「文字小さくてよく分からないけど、何をどう色分けしてるの? Composition API ではなんとなく色が統一されて、綺麗にはなってるみたいだけど。。」

Options API は、data や methods ごとにまとめられた記述ですので、ロジックを中心にコードを辿った時、断片的に散らばったものになります。
これがもし複数の機能を持つ、粒度の大きなコンポーネントだとどうでしょうか?

「この methods の機能で使われている変数は data の中にあって、computed の方にも記述されていて、、」 と、一つの機能を編集・追加するのにもかなりコード間のジャンプが必要になってしまいます。
また、どのデータがどこで利用されているのか、コンポーネントがどんな機能をもっているのかなど、見通しが悪くなってきそうです。
色の散らばった Options API の図が、この状態ですね。

これに対して Composition API は、コンポーネントの機能ごとに関数を定義し、その中でデータの定義から機能実装までをまとめて記述することができます。
結果、色の整理された図のようにロジックごとにまとまったコードでの記述となり、機能の追加や改修などが進めやすくなる、ということのようです。

Composition API への書き換え

サンプルを辿りつつ、実際に記述方法を確認していきます。

Vue 2.6.11 の環境を Vue CLI で構築し、 Composition API を使用するためのプラグイン@vue/composition-api を依存関係に追加します。
また、main.js にも VueCompositionAPI を使用するための記述を追加します。

$ vue create __projectname__
$ yarn add @vue/composition-api
main.js
import Vue from 'vue'
import App from './App.vue'
import VueCompositionAPI from '@vue/composition-api' // 追加

Vue.use(VueCompositionAPI) // 追加
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

書き換えていくコードがこちら。
ボタンクリックで count をインクリメントしていく、よくあるやつですね。
computed の記述も確認したいので、count の二倍を返す doubleCount を記述しています。

App.vue
<template>
  <div id="app">
    <h1>Vue 3.0 Composition API</h1>
    <button @click="increment">Click</button>
    <p>count: {{ this.count }}</p>
    <p>doubleCount: {{ doubleCount }}</p>
  </div>
</template>

<script>
export default {
  data: function() {
    return {
      count: 0
    }
  },
  methods: {
    increment: function() {
      this.count++
    }
  },
  computed: {
    doubleCount: function () {
      return this.count * 2
    }
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
  border: 3px solid #2c3e50;
}
</style>

スクリーンショット 2020-05-10 1.57.55(2).png

では書き換えていきます。

Composition API では、setup という関数の中で Options API での基本的な記述を行うことができ、以下のように書き換えることが可能です。

App.vue
<template>
  <div id="app">
    <h1>Vue 3.0 Composition API</h1>
    <button @click="increment">Click</button>
    <p>count: {{ count }}</p>
    <p>doubleCount: {{ doubleCount }}</p>
  </div>
</template>

<script>
import { ref, computed } from '@vue/composition-api'

export default {
  setup() {
    // data での定義
    let count = ref(0)

    // methods での定義
    const increment = () => {
      count.value += 1
    }

    // computed での定義
    const doubleCount = computed(() => {
      return count.value * 2
    })

    return {
      count,
      increment,
      doubleCount
    }
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
  border: 3px solid #2c3e50;
}
</style>

setup 関数を使用した記法

Composition API での記法は、新たに追加された setup 関数 が中心となります。この記法には、Options API と比較して大きな変更点が二つあります。

  • 使用する Vue.js の機能を import する必要がある

上記のサンプルでは、setup内の記述で ref および computed の機能を使用しています。(ref については後述します)
Composition API では computed など Vue.js 独自の記法は import で読み込みが必要になります。
また、こちらも後述しますが、ライフサイクルメソッドに関しても import して記述する必要あります。

  • template で 使用する値、関数は return する必要がある

Composition API では、template 内で使用したいものデータや関数は setup 関数内から return しておく必要があるようです。

data はどうなる?

// data での定義
let count = ref(0)

Composition API ではリアクティブなデータを reactive()、またはref() で定義します。
ざっくり分けておくと、オブジェクトをリアクティブにしたい場合は reactive 、プリミティブなデータの場合は ref を使用した定義が可能なようです。どちらを利用するかはケースバイケースのようで、設計と併せて今後も議論されていくことかと思います。

こちらに関しては以下の記事が個人的にわかりやすく、とても参考になりました。

Vue.jsのcomposition-apiにおける"ref"と"reactive"って何が違うの?

ref で定義したデータは RefImpl というオブジェクトでラップされ、定義したデータは RefImpl.value に格納されます。
そのため、ref で定義したデータを操作する場合は .value を付与して操作する必要があります。

methods はどうなる?

// methods での定義
const increment = () => {
  count.value += 1
}

increment は単純な関数として定義され、setup 関数内で methods でラップせずに記述することができます。
count 変数は ref で定義されていますので、前述したように .value を付与して変更を行う必要があります。

computed はどうなる?

// computed での定義
const doubleCount = computed(() => {
  return count.value * 2
})

computed で定義していたものは、computed() の中に関数を記述していきます。
computed の import と return を忘れずに記述しておけば、問題なく使用できそうです。

ライフサイクルフックはどうなる?

上記サンプルでは使用していませんが、ライフサイクルフックを使用した記述も setup 関数内で記述することができます。
setup 関数内でライフサイクルフックを記述する場合、以下のように変更されるようです。(beforeCreate, created は setup 関数 に統合されるようです。)

Vue 2.x での記法 Vue 3.0 での記法
beforeCreate setup
created setup
beforeMount onBeforeMount
mounted onMounted
updated onUpdated
beforeDestroy onBeforeUnmount
destroyed onUnmounted
errorCaptured onErrorCaptured

こちらも <script> タグ内のみサンプルを記述してみます。

App.vue
<script>
import { ref, computed, onMounted } from '@vue/composition-api'

export default {
  setup() {
    // data での定義
    let count = ref(0)

    // methods での定義
    const increment = () => {
      count.value += 1
    }

    // computed での定義
    const doubleCount = computed(() => {
      return count.value * 2
    })

    // マウント時のライフサイクルフック
    onMounted(() => {
      console.log('mounted!')
    })

    return {
      count,
      increment,
      doubleCount
    }
  }
};
</script>

import で使用するライフサイクルメソッドを読み込み、setup 関数内で記述することで使用できます。

とりあえず書き換えてみたものの、正直あまり利点が分からない

正直なところ、computed やライフサイクルフックが setup 関数内に移動することで import などの記述量も増えたように感じ、その上でメンテナブルになったかというとそうでもない気がします。
この記述方法だと、機能が増えていくと結局 setup 関数内が肥大化していきそうな感じもしますし、あまりメリットが感じられないようにも思えます。

なので、もう一段階記述方法を変更していきます。
インクリメンタルの機能を setup 関数外に記述し、setup 関数内から呼び出す形に変更してみます。

function を setup 関数外に記述する

useIncrementNumber を export default の外側で記述し、setup 関数内から呼び出す形に変更してみます。

App.vue
<script>
import { ref, computed } from "@vue/composition-api";

export default {
  setup() {
    const { count, increment, doubleCount } = useIncrementNumber();

    return {
      count,
      increment,
      doubleCount
    }
  },
};

const useIncrementNumber = () => {
  // data での定義
  let count = ref(0);

  // methods での定義
  const increment = () => {
    count.value += 1;
  };

  // computed での定義
  const doubleCount = computed(() => {
    return count.value * 2;
  });

  return {
    count,
    increment,
    doubleCount,
  };
}
</script>

定義した関数内および setup 内で return する必要があるものの、機能ごとの切り分けはこの方法で行うことができそうです。
今回は一つしか関数を記述していませんが、粒度が大きく複数の機能を持つコンポーネントになると、template 内で使用されている値が setup 関数の return 内にまとまりますし、function の追加や変更時の影響範囲も見やすくなりそうです。

まとめ

  • Vue.js 3.0 でも基本的に従来の記述方法は使用可能
  • Composition API は新たに導入される setup 関数を使用した記述方法
  • 機能ごとにまとめられるので、粒度の大きいコンポーネントだと見通しがよくなりそう

個人的には、小規模のアプリケーション開発であれば Composition API 使用した記述方法は少し冗長な書き方では、という気もします。Options API での記述によるデータや関数ごとのまとまりにも良さがありますし、プロジェクトの大きさによってどう使用していくかを考えた上で使用していくのが良いかもしれないですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【nuxt.js】layoutsからpagesのmethodsを叩き、戻り値をlayoutsで表示する

かなり時間取られたので、記事のネタにして精神の安定を図ります。ちなみに私は「返り値」派ですが、「戻り値」の方が若干多数派っぽい?ですね。

やりたかった事

「layouts/header.vue」の?ボタンを押すと、「pages/hoge.vue」や「pages/poyo.vue」のmethods: generateHelp()を叩いて、その戻り値を「layouts/header.vue」に渡してダイアログに表示する、みたいなことをしたかった。methods: generateHelp()の無い「pages/huga.vue」については、何も起きないなりundefinedが返ってくるなり適当でいいやと。

図で表すと↓こんな感じ。
qiita.png

そもそもこの設計自体が間違ってる気がしないでもないです。何か良い案ございましたらご教示ください。

実現した方法

  1. ?ボタンを押すと'requireHelp'イベントを発火させる
  2. 「pages/hoge.vue」は'requireHelp'イベントを検知して、methods: generateHelp()を叩く
  3. methods: generateHelp()は処理の最後に、自身の戻り値を引数として'returnHelp'イベントを発火させる
  4. 「layouts/header.vue」は'returnHelp'イベントを検知して、引数をダイアログに表示させる
layouts/header.vue
<template>
  <h1>たいとる</h1>
  <button @click="clickHelpButton"></button>
  <nuxt />
</template>

<script>
export default {
  // 'returnHelp'イベントの検知(4の前半)
  created () {
    this.$nuxt.$on('returnHelp', (help) => {
      this.showDialog(help)
    })
  },

  // ページを離れる際に検知を終了
  // (書かないとどういった問題が起きるのか、誰か教えてください><)
  beforeDestroy () {
    this.$nuxt.$off('returnHelp')
  },

  methods: {
    // ボタンクリック時に各pagesへ向けて'requireHelp'イベント発火(1)
    clickHelpButton () {
      this.$nuxt.$emit('requireHelp')
    },
    // ダイアログを表示(4の後半)
    showDialog (help) {
      /* ダイアログを表示する処理 */
    }
  }
</script>
pages/hoge.vue
<script>
export default {
  // 'requireHelp'イベントの検知(2の前半)
  created () {
    this.$nuxt.$on('requireHelp', () => {
      this.generateHelp()
    })
  },

  // ページを離れる際に検知を終了
  beforeDestroy () {
    this.$nuxt.$off('requireHelp')
  },

  methods: {
    // ?ボタンが叩きたかったお目当ての関数(2の後半)
    generateHelp () {
      // なんかヘルプ内容を生成
      const help = 'ggrks'

      // layouts/header.vueに向けて'returnHelp'イベントを発火(3)
      this.$nuxt.$emit('returnHelp', help)
      return help
    }
  }
</script>

流れを図示すると↓こんな感じ。
2020-05-17_113259.png

うまくいかなかった方法

$refsを使う

「親コンポーネントから子コンポーネントのmethodsを触る方法」みたいな記事を参考にして、<nuxt ref="page" />としてthis.$refs.page.generateHelp()ってすれば直接叩けないかと思ったけど、駄目。this.$refs.pageが参照するのはnuxt自体(?)で、各pagesではない???(どう表現するのが正解なのか分かってない)

Vuex storeを経由する

ボタンを押したときにstoreを更新させようと思ったら、結局イベント発火が必要なのに気づいてやめた。layoutsからpages内の子コンポーネントのmethodsを……ってなったらstore経由した方が楽かもしれないが、そもそもその設計を見直した方が良さそう。

参考文献

[Nuxt.js] EventBusを使ってコンポーネント間で通信する
【Vue.js】親コンポーネントから子コンポーネントのメソッドを叩く

繰り返しになりますが、もっとこうしたらスマートだよ!って方法あれば教えてください。切に。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

サクッとfilterでデータの加工(Vue.js)

はじめに

データの加工をする時にVue.jsで独自のfilterを実装して、使い回すことがあるので、今後のメモとして残します。自分用のメモなので、簡易的に書きます。ご了承くださいませ。

filterの作成(テンプレ)

filter.js
export function filterA (value) {
  return value; // 独自のfilter処理
}
export function filterB (value) {
  return value; // 独自のfilter処理
}
export function filterC (value) {
  return value; // 独自のfilter処理
}

filterの作成(自分が今後使いそうなもの?)

filter.js
export function filterA (value) {
  return Math.floor(value); // 小数点以下を切り捨てる
}
export function filterB (value) {
  return value.toLocaleString(); // 数字を3桁カンマ区切りにする
}
export function filterC (value) {
  return Number(value).toLocaleString(); // String型の数字を3桁カンマ区切りにする
}

モジュールをインポート

main.js
import * as filters from './filter' // 相対または絶対パス名で指定

Object.keys(filters).forEach(key => {
  Vue.filter(key, filters[key]);
});

filter.jsのファイルに記述してあるモジュールのコンテンツすべてをインポートする。
次に、filter.jsで作成した関数をforEachメソッドを使用して、グローバルなVue.filterを使って、1つずつ登録してあげる。

基本構文(例)

Hoge.vue
{{ number | filterA }}

最後に、上記のように式の終わりに任意のフィルタを追加し、パイプ(‘|’)シンボルを使って記述してあげればコンポーネントの中で使用することができます。以上になります。

また、下記のようにパイプシンボルで繋げてあげれば、複数のフィルタを使用することもできます。

Hoge.vue
{{ number | filterA | filterB }}

おわりに

今回の記事の内容に誤り等ありましたら、お手数ですがご指摘いただけるとありがたいです。よろしくお願いいたします。

参考資料

Vue.js 公式
JavaScript MDN
Vue.js 独自のfilterを実装する

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

有料のWebサービスをリリースするまでに取り組んだこと・知見をまとめました【個人開発】

1. 作ったサービス

twikeshi-ogp.png

ツイ消し職人
https://twikeshi.net/

ツイ消し職人は大量のツイートを消したい方向けのツールです。
3,200件を超えるツイートを一括削除できます。
無料のツールなどでうまく削除できなかった方は是非ご利用ください。
既存のフォロワーをそのままに、Twitterをやり直すことができます。

2. 自己紹介

こんにちは、ひろと申します。
今年の3月に会社を辞め、現在はフリーランスエンジニアとして活動しています。

メガバンクのシステムエンジニア →
広告代理店(東証一部上場企業)のWebエンジニア →
フリーランスエンジニアという経歴です。

仕事でコードを書き始めたのは前職からで、プログラミングの経験年数は2年と9ヶ月くらいです。

3. なぜ作ったのか

私は今年の3月にフリーランスとして独立しました。
それに伴い、学生時代から使っていたTwitterアカウントの運用を変えようと思い、今までのツイートを削除してやり直すことにしました。
アカウントを作り直す選択肢もあったのですが、フォロワーを減らしたくなかったため、ツイ消しの道を選びました。
調べてみるとツイートの一括削除ツールがいくつか見つかったため、それを使ってツイ消しをすることにしました。

しかし、既存のツールではツイートの削除ができませんでした。
私の今までのツイート数は19万件で、Twitterアーカイブをダウンロードしたところzipファイルのサイズはなんと31GB。スマホの7GBプランなら4.5ヶ月分の通信量が必要になってしまう、とんでもない容量です。
そう、私がツイ廃だったのが全ての原因です。

普通のツイ消しサービスは、API制限の関係で3,200ツイートが削除の上限となってしまいます。私の19万ツイートに対してはあまりに無力すぎました。

もちろん、API制限を回避するためにTwitterアーカイブをアップロードして削除を行うサービスもあります。
しかし、31GBのzipファイルを送りつけると必ず500エラーが返ってきてしまい、私の試した範囲では、まともに動くものはありませんでした。海外の有料サービスでさえダメでした。(具体的なサービス名は出しませんが、日本円で1,600円払いました。手痛い出費です)

そこで私は、「ツイ廃でもツイートを削除できるサービス」が必要だと思い、ツイ消し職人の開発を始めました。

4. リリースするまでに取り組んだこと

取り組んだ全てのことを記載しています。

一. サービスの命名

最初は「ツイートクリーナー」という名前にしていました。
開発中盤に「ツイ消し職人」という名前を思いつき、変更しました。
ランサーズなどで募集するのも良いと思います。

二. ドメインの取得

ムームードメインでtwikeshi.netを取得しました。
欲しいドメインが埋まっている場合は、twikeshi-app.netのように工夫するのも良いと思います。

三. 商標権の取得

今回はお金がなかったので保留しています(いつでも取れるように商標調査は終えています)。
今はToreruなどの便利なサービスがあり、ものすごく簡単に出願できます。
48,000円で5年間有効になります。
商標権の取る取らないを選択するのは自由ですが、後から商標を第三者に取得されて商標権の侵害警告を受けた場合、サービス名やドメインを変える必要があるリスクは認識しておく必要があります。

四. プライシング

海外の同じようなサービスを参考に値付けを行いました。
現在は700円(税込)で提供しています。

その後、プライシングに関する本を3冊読んで(この本この本この本)考え方が変わったので、そのうち値上げするかもしれません。

どんなに高くても、その価格で買いたい人がいます。私自身も、このサービスを他の人が作っていたとしたら、喜んで利用していました。

間違っても、本来ターゲットでない人を取り込むために値下げするのはやめてください
例えば、私は友人達に「ツイ消し職人の適正価格はいくらだと思う?」と質問をすると、2人が「100円」と答えました。
しかし、断言しますが彼らは100円でも絶対に利用しません。何故ならば、彼らはこのツールの価値を理解していないからです。ツイ消しをしようと思ったことがない人に相場感を聞いても意味がありません。
逆に、本っっ当にツイ消しをしたくて困っている人からすれば、このツールが例え1万円でも喜んでお金を払うはずです。

五. 技術選定

1. バックエンド

バックエンドはLaravelで開発しました。
私は前職でSpring Bootを使っていましたが、このフレームワークではTwitterログインを実装するのに苦労しそうだったため、一からLaravelを学ぶことにしました。
Railsと悩みましたが、後述する理由によりレンタルサーバーで運用したかったので、Railsは諦めました。
Laravelはコードもドキュメントも読みやすいため、使っていて楽しいですね。
ドットインストールのLaravel入門がとても分かりやすかったのでオススメです。

役割 技術
PHPフレームワーク Laravel
データベース MySQL
Twitterログイン Laravel Socialite
Twitter APIライブラリ TwitterOAuth
メール送信 SendGrid

2. フロントエンド

CSSフレームワークにはMaterializeを採用しました。これも初めて使ったのですが、ドキュメントが分かりやすく情報量も多いのでオススメです。
今回はフロントで処理をする必要が無かったので、基本的にJavaScriptは使っていません。ファイルアップロードの画面は、アニメーションを付けるためにVue.jsを使いました。Vue.jsは以前から使っていたので、特に困ることはありませんでした。
次はNuxt.jsに挑戦するために勉強中です。

役割 技術
CSSフレームワーク Materialize
JavaScriptフレームワーク Vue.js
決済 Stripe Checkout

3. インフラ

バックエンドの項目で触れましたが、サーバーにはレンタルサーバーを採用しています。

役割 技術
レンタルサーバー エックスサーバー

Heroku / VPS / AWS EC2 / GCP App Engineなどの選択肢もありましたが、主にコストと運用の観点から除外しました。個人開発は自分でインフラを選べるのが良いですね。
今後もどんどんサービスを作っていく予定なので、サーバー費がかさむのはイヤだし、サービスを作る度に環境を構築するのも避けたかったのです。

もちろん要件によってはレンタルサーバーが使えない場合もあります(ミドルウェアの設定変更や追加インストールが必要な場合など)。
rootユーザーが使えないと困る場合は、状況に応じて各サービスを比較検討しましょう。
サーバーはHerokuだけどストレージにはAWS S3を使って、DBにはGCP Cloud SQLを使うといったトリッキーなこともできます。柔軟な発想で最適な構成を作りましょう。

参考に、私の考える主なインフラサービスのメリット・デメリットをまとめておきます。
※App Engineは詳しくないので簡易的な記載になってます

インフラサービス メリット デメリット
VPS(IaaS) ・安い
・root使える
・借りる度にお金がかさむ
・環境構築や設定が必要
EC2(IaaS) ・root使える
・マイクロサービス沢山ある
・高い
・借りる度にお金がかさむ
・環境構築や設定が必要
App Engine(PaaS) ・環境構築不要 ・高い
・借りる度にお金がかさむ
Heroku(PaaS) ・安い
・環境構築不要
・借りる度にお金がかさむ
・30秒タイムアウト辛い
レンタルサーバー(ほぼPaaS) ・安い
・1台でアプリ沢山動かせる
・環境構築ほぼ不要
・root使えない

六. 設計

小規模なサービスなので、ここにはほぼ時間をかけていません。
ワイヤーフレームなどは作らず、実際に画面をコーディングしてレイアウトを決めました。
DB設計もパパッと考えて終わり。
開発の中で必要になったときに都度、テーブルやカラム・画面を増やしていきました。
サービスによっては色々な機能を思いつくと思いますが、まずはスモールスタートでリリースすることをゴールにしましょう。YAGNIは正義。

七. 開発

一番時間をかけたのはこの工程です。
他の仕事が並行していたため正確ではありませんが、全体で2〜3週間はかかったと思います。
伝えたい情報がある場合はコメントを書いています。
有料サービスのみ必要になる項目には「☆」を付けています。

1. サービスの機能開発

一. Twitterログイン

ツイ消し職人では、決済完了時とツイート削除完了時に確認メールを送信しています。
そのため、Twitter AppのAdditional permissionsとして、Request email addressにチェックを入れています。

二. ☆決済

Stripe Checkoutは神。
JavaScriptをちょろっと書くだけで決済を提供できます。返金もボタンポチるだけです。
ツイ消し職人はクレジットカード、Google Pay、Apple Payに対応しています。

三. アーカイブアップロード

Twitterが生成したデータを読み取らないといけないので、アーカイブのどのファイルに何の情報があるのかを全て自分で調べました。
そして、ツイートの削除に本当に必要なファイルだけをアップロードさせることで、ファイルサイズを31GB→200MBまで減らすことができました。

アーカイブからはツイートの削除に必要な情報を正規表現で抽出する必要があります。
最初は、JavaScriptを使いフロント側で情報を抽出し、サーバーには最低限のデータだけ送るようにする予定だったのですが、少し時間がかかりそうだったので諦めました。

FileReader.readAsText()に100MBのファイルを食わせるとクラッシュしてしまうことが判明し、ファイルをチャンクして処理する必要が出てきたためです。
コンソールにエラーは出力されず、サイレントでクラッシュするので問題の特定に時間がかかりました。マジでやめてほしい。
サーバ側で抽出処理をやっても特に問題はないので、サーバ側で処理するようにしました。

四. ツイート削除

ノーコメント

五. 非同期処理

アップロードされてそのままツイートの削除を行うと、画面がタイムアウトしてしまいます。
そのため削除処理はLaravelのキューを使って非同期にしています。
失敗時の再実行もできるようになるので便利ですね。

六. メール送信

必ずユーザーに到達するようにSendGridを使っています。
返信や問い合わせを受けるためにはメールサーバーの設定が必要なので注意してください。
サーバーが用意できない場合は、G Suiteなどのホスティングサービスを利用しましょう。

七. ログ出力 + Slack通知

本番での例外発生時にはSlackにスタックトレースを飛ばすようにしています。
他にも、ツイート削除処理成功時など、正常系でも重要なものはSlackに通知を飛ばしてます。

ログは、出せる項目をなるべく出すようにしています。
Laravelのログ出力について記事書いてるので興味あったら読んでください↓
【Laravel】ログのフォーマットを変更してIPアドレスやユーザー名などを出力する

2. リリース準備

一. LP(トップページ)作成

ユーザーに効果的に訴求できる文言を考える必要があります。
デザイナーの人は腕の見せどころだと思います。
文字や画像・アニメーションを使っていい感じのレイアウトにしましょう。

ペライチなどのツールを使っても良いと思います。
コンバージョンに直結するので、一番力を入れるべき部分です。外注も考えましょう。

二. 利用規約・プライバシーポリシーの制定

この本が大変参考になりました。コピペできるひな形データも付いてくるのでオススメです。

三. ☆特定商取引法に基づく表示の作成

同上。
有料サービスの場合は必須です。
本名や住所、電話番号を晒さないといけないので、ここが一番の難関ではないでしょうか。
私の場合、IP電話アプリ(SMARTalk)を使い050から始まる電話番号を載せています。

四. Googleアナリティクス・Search Console設定

ノーコメント

五. meta description設定

Googleの検索結果でタイトルとともに出るやつです。
meta keywordは不要です。

六. ファビコン設定

GIMPで作りました。
サービスのロゴがある場合はファビコンにも活かせます。

七. OGP設定

GIMPで作りました。
OGP画像を動的に生成するサービスでは、トップページ用の画像を同じ方法で作るのも良いかも。
CTRに直結するので、ここも外注を検討しましょう。

八. サイトマップ設定

Search Consoleで送信します。
sitemap.xml Editorを使うと簡単に作成できます。
サイトが新しく、外部からのリンクが少ない場合はあったほうが良いみたいです。

九. SNSシェアボタンの設置

ツイ消し職人 - twikeshi.net.png
↑こういうのです。
ユーザーに拡散してもらえる仕組みを作っておくことは重要です。
ちなみにツイ消し職人は全く拡散されていません。悲しい。

十. お問い合わせフォームの設置

自分で作るのがめんどくさい場合は、Googleフォームformrunなどを活用しましょう。
お問い合わせフォームの代わりに、チャットサポートツールを入れるのもオススメです。

3. リリース

一. デプロイ

Laravel + レンタルサーバーの場合はgit pullすればほぼ終わりです。
あとは.env書いてマイグレーションしてキャッシュ系のコマンドを叩くだけです。
もちろん、GitHub ActionsなどのCIを設定するのも良いと思います。

私の場合は、以下のようなデプロイスクリプトを用意しています。

deploy.sh
#!/bin/sh

git pull

composer install --optimize-autoloader --no-dev

php artisan config:cache

php artisan view:cache

php artisan route:cache

二. テスト

本番環境で全ての機能がうまく動くことを確認しました。
中規模〜大規模サービスの場合は、検証環境の用意とテスト自動化がされていないと運用がしんどくなります。

八. リリース後

1. 知り合い・友人への拡散

LINE, Twitter, Facebookなどで拡散して使ってもらいましょう。

2. プレスリリースを出す

お金があればPR TIMESなどの有名サイトに出すのがオススメです。
(もしくは、法人ならスタートアップチャレンジの条件を満たすと無料になります)

私はお金がなかったので、valuepressのフリープランで配信しました。
3,200件を超えるツイートを一括削除できるツイ消しサービス「ツイ消し職人」を提供開始

3. アプリ紹介サイトに登録

私の場合、AnyMakemakepostEggineerApplishowを活用しています。

4. 新聞の広告枠に出稿する

リリース直後にスポーツ新聞の方から電話があり、新聞とサイトに広告を載せないかと打診がありました。
条件が合わなかったためお断りしましたが、人によっては選択肢になり得ると思います。

5. アフィリエイト広告に出稿する

現在検討中です。
お金がある場合は、A8.netなどの大手ASPを使うのが良いと思います。
もしもアフィリエイトマネートラックならば初期費用0円・月額費用0円で始められるようです。

6. SNS広告に出稿する

現在検討中です。
私の場合はTwitterユーザーをターゲットにしたサービスなので、Twitter広告と相性が良いです。
とりあえず試してみて、どれくらい成果が出るかチェックしてみようと思います。

7. 保守開発

本番環境でのエラーを監視し、新しく発現したバグがあれば修正しましょう。
手元で再現しないエラーは...ユーザー問い合わせを待つしか無い。。

もちろん、機能追加などサービス改善のための開発は怠らないようにしましょう。
大幅リニューアルや作り直しなどの選択肢もあります。

8. ブログなどでの発信

この記事のことですね。
Qiita, Crieit, Note, ブログなどの選択肢があります。
サービスを知ってもらうだけでなく、転職活動などでも役に立ちます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsを勉強するモチベーション

概要

流行や学習コストの低さなどでチヤホヤされる存在となったVue.jsですが、みなさんはVue.jsの学習捗ってますか?
業務で使うから仕方なく覚えるのではあれば、やるでしょう!
でも自分の可能性を広げるための勉強ならどうでしょう?
流行だからと飛び付いたもののやる気がでない…
他にモチベーションの繋がるものはないか?

アニメは好きかい?

Vue.jsの開発者エヴァン・ヨーさんはどうやら日本アニメが好きらしい
彼はアニメヲタクなのだ!

ヲタクならわかるだろ?

ersion 開発コードネーム 日本語名称 アニメ初出年
v2.6 Macross 超時空要塞マクロス 1982年
v2.5 Level E レベルE 2011年
v2.4 Kill la Kill キルラキル 2013年
v2.3 JoJo's Bizarre Adventure ジョジョの奇妙な冒険 1993年
v2.2 Initial D 頭文字D 1998年
v2.1 Hunter X Hunter HUNTER✕HUNTER 1999年
v2.0 Ghost in the Shell GHOST IN THE SHELL / 攻殻機動隊 1995年
v1.0 Evangelion 新世紀エヴァンゲリオン 1995年
v0.12.0 Dragon Ball ドラゴンボール 1986年
v0.11.0 Cowboy Bebop カウボーイビバップ 1998年
v0.12.0 Blade Runner - 1982年
v0.12.0 Animatrix アニマトリックス 2003年

日本のアニメの名前がコードネームに使われた技術に愛着がわかないわけがないだろう!

あとがき

今回の記事は技術的観点からの記事ではないけど、何かをはじめる時には動機や目標などが明確でないと長続きしないわけです。
そこでアニメが好きな人にはVue.jsを愛でる理由になりうるであろう開発コードネームを紹介する事にしました。
この記事が今にも勉強のモチベーションが消えかけているVue.jsを勉強しているアニメヲタクに届くとよいないと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ドメイン駆動設計 with Vue/Nuxt(Composition API)でリアルタイム・バリデーション

問題意識

ドメイン駆動設計に従うVue/Nuxtアプリケーションを作っていて、
名前フォームに文字列を打ち込んで8文字より多く書かれたとき、
そのフォームの一つ上に赤い文字で「名前は8文字以内です」というエラーをリアルタイムで表示してほしい、
……という要件があるとしましょう。

このときVue側のコードで、インプットフォームから受け取った変数に8文字という制限をすることも可能なのですが、
「それって 利口なUI という奴なのでは?」と考えていました。
一般に利口なUIはアンチパターンとして知られていて、(議論はありますが)たしかに、ドメイン知識がUI層に流出しています。
どのみちエンジニア的にも同じ内容を2度書くような気がして、利口なUIは利口じゃないコードになりかねません。

そういうわけで自分なりにその解決をしてみたいと思います。

完成品

devinoue/realtime-validation-ddd

環境

Nuxt2.12.2
Composition API
TypeScript

ドメインを書く

今回はNameクラスをTypeScriptで書きますが、必要そうな所だけです。
ひとまずdomainというディレクトリに以下のような値オブジェクトとしてName.tsを入れておきます。

domain/Name.ts
export default class Name {
  constructor(private _name: string) {
    Name.validation(this._name)
  }

  static validation(name: string): never | void {
    if (name === '') {
      throw new Error('名前を入力してください')
    }
    if (typeof name !== 'string') {
      throw new TypeError('名前は文字列にしてください')
    }
    if (name.length > 8) {
      throw new Error('名前は8文字以内にしてください')
    }
  }
}

ついでに、Eメールアドレス用値オブジェクトも作っておきます。
コード的にはほぼ同じになってしまうので、こちらから御覧ください。

ハンドラを書く

Composition APIで作るので、ハンドラも切り分けておきます。
別にそういうvue界の慣習があるわけではありませんし、切り分けなくてもいいのですが、
この方が見晴らしがいいという理由で分けています

handler/InputHandler.ts
import { ref, watch } from '@vue/composition-api'
import Name from '~/domain/Name'
import EmailAddress from '~/domain/EmailAddress'

interface IForm {
  name: string
  email: string
}

export default function() {
  const defaultInput: IForm = { name: '', email: '' }
  const errors = ref(defaultInput)

  const forms = {
    name: ref(''),
    email: ref('')
  }
  watch(
    forms.name,
    () => {
      errors.value.name = ''
      try {
        Name.validation(forms.name.value)
      } catch (e) {
        errors.value.name = e.message
      }
    },
    { lazy: true }
  )
  watch(
    forms.email,
    () => {
      errors.value.email = ''
      try {
        EmailAddress.validation(forms.email.value)
      } catch (e) {
        errors.value.email = e.message
      }
    },
    { lazy: true }
  )

  return { ...forms, errors }
}

ここ、無駄が多い気がしますが、watch関数を使っている手前まとめにくい、、、、

vueファイルを完成させる

さて、残りはpages以下のindex.vueで、さきほど作ったファイルを読み込むだけです。

pages/index.vue
<template>
  <div class="container">
    <span v-show="errors.name" class="error">{{ errors.name }}</span>
    <span>名前 : <input v-model="name" type="text"/></span>
    <br />
    <span v-show="errors.email" class="error">{{ errors.email }}</span>
    <span>メールアドレス : <input v-model="email" type="text"/></span>
    <br />
  </div>
</template>

<script lang="ts">
import useInputHandler from '~/handler/InputHandler'
export default {
  setup() {
    return { ...useInputHandler() }
  }
}
</script>
// スタイル省略

御覧ください!! ほとんど空っぽ! なんとスッキリしているのでしょう!!!?
今までのVueファイルがウソのようにキレイにまとめられました!

動作イメージ

何か書き込むたびにwatchメソッドが変数を監視して、エラーを報告してくれます。
これで「利口なUI」とならずに済みます?
コメント 2020-05-16 231755.png

終わりに

Vue3素晴らしい……!

GitHub:
devinoue/realtime-validation-ddd

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む