20191209のvue.jsに関する記事は20件です。

create-react-appを使わずにReactの環境構築をして周辺ツールとかを理解する

Reactに入門したのはいいもののcreate-react-appでプロジェクトを作成すると無駄なファイルが多かったり、環境構築を丸投げしてるのでちょっと色々まずいなと思い、
create-react-appを使わずに環境構築をしていこうと思います。ドキュメントを見ると学習用やちょっと試したいときに最適である的なことが書いてありますね。


以下はこの記事での手順をまとめたスクリプトです。npmが入っている前提ですが、実行ディレクトリにプロジェクトが生成されると思います。

最終的なスクリプト
PROJECT_NAME=myapp
echo $PROJECT_NAME
mkdir $PROJECT_NAME
cd $PROJECT_NAME

cat <<EOF > package.json
{
 "scripts": {
    "dev": "webpack-dev-server --open"
  }
}
EOF

npm i react react-dom webpack webpack-cli
npm i -D typescript ts-loader webpack-dev-server @types/{react,react-dom}


mkdir dist
mkdir src

cat <<EOF > dist/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title> my-app </title>
</head>
<body>
  <div id="root"></div>
  <script src="bundle.js"></script>
</body>
</html>
EOF

cat <<EOF > src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';

const App = () => <div> hello word </div>

ReactDOM.render(<App/>, document.getElementById("root"))
EOF

cat <<EOF > tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "allowSyntheticDefaultImports" :true
  }
}
EOF

cat <<EOF > webpack.config.js
const path = require('path');

module.exports = {
  mode: "development",
  entry: './src/index.tsx',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: [ '.tsx', '.ts', '.js' ],
  },
  devtool: 'inline-source-map',
  devServer: {
    contentBase: './dist',
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  }
};
EOF


実行後は以下のようになると思います。

├── dist
│   └── index.html
├── package-lock.json
├── package.json
├── src
│   └── index.tsx
├── tsconfig.json
└── webpack.config.js

必要なモジュール

  • React/ReactDOM
  • webpack
    • ソースコードを1つのファイルにまとめるツール。v4 から cli も必須になった。
  • webpack-dev-server
    • Hotリロード機能。
  • トランスパイラ
    • babel, ts-loaderとか
    • classとかJSXとかトランスパイルしてJavaScriptコードを生成する。

npmでいれる。

 npm i react react-dom webpack webpack-cli

typescriptを使う場合は型定義とかも

 npm i -D typescript ts-loader webpack-dev-server @types/{react,react-dom}

作成するファイル

  • index.html
  • index.js
  • package.json
  • ts-config.json
  • webpack.config.js

index.html

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>my-app</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="bundle.js"></script>
  </body>
</html>

htmlファイル。webpackでbundleしたファイルを読み込む。

index.ts

src/index.tsx

import React from "react"
import ReactDOM from "react-dom"

const App = () => <div> hello word </div>

ReactDOM.render(<App />, document.getElementById("root"))

エントリーファイル。Hello Worldだけ。

tsconfig.json

tsconfig.json

{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "allowSyntheticDefaultImports": true
  }
}

とりあえず公式のと同じです。適宜追加していきます。

webpack.config.js

webpackの設定ファイルです。初見ででかいconfigファイルを見るとビビってしまいますが、最低限必要なのはそこまで多くないです。

https://webpack.js.org/guides/typescript/
https://webpack.js.org/concepts/

webpack.config.js

const path = require("path")
module.exports = {
  mode: "development",
  entry: "./src/index.tsx",
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },
  devtool: "inline-source-map",
  devServer: {
    contentBase: "./dist",
  },
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
}

mode(dev or prod)と入出力パスとloaderとresolveなんで簡単ですね。

この部分はwebpack-dev-serverの設定です。

  devtool: "inline-source-map",
  devServer: {
    contentBase: "./dist",
  },

https://webpack.js.org/guides/development/#using-webpack-dev-server

package.json

npm init -yみたいにすると雛形が生成されますが、最低限ファイルがあればいいようです。

echo "{}" > package.json
npm i react, react-dom

あとオプションが微妙ですね。

--save-dev | -DでdevDependencyに追加
--save | -Sで両方とかですかね??

npxはローカルにインストールしたパッケージを実行できる(正確ではないかも)

npx webpack-dev-server --open

npm scriptに追記して

{
  "scripts": {
    "start": "webpack-dev-server --open"
  }
}
npm run start

で実行できるようになる。


あとは適当にテストとかlinterとかいれるだけ。

...用語とか言葉とかあやふやなところ適宜修正します。

その他

git

echo "node_modules" > .gitignore
git init
git add .
git commit -m "create project"

styled-components

npm i styled-components
npm i @types/styled-components
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[ Google Analytics ] eコマース / 拡張eコマース実装

この記事はチームラボエンジニアリングの13日目の記事です。

本記事ではGoogle AnalyticsのEコマース/拡張Eコマース導入の流れを記録として書きます。
また、今回Google Tag Managerを使用したため、その設定方法も合わせてまとめます。

最初に

本記事の大まかな流れとしては、 以下の通りです。

  • 全体の構成
  • [概要] Google Analytics(GA)、Google Tag Manager(GTM)、Eコマース、拡張Eコマース
  • [実装] Eコマース、拡張Eコマース
  • [設定] Google Tag Manager
  • [振り返り]全体を通して気をつけたこと
  • 参考URL・参考書

今回使用した技術

Vue.js
Nuxt.js
JavaScript
TypeScript

使用している図は今回の実装に合わせて独自で作成したものであるため、webサイトによって仕様が異なると考えます。また管理画面はテスト環境で使用していたものを使用しています。

全体の構成

概要

Google Analytics

Q: Google Analyticsとは?

Googleが無料で提供するwebサイトのアクセス解析サービス。
サイトコンテンツへのアクセス状況がわかるので、このデータを元にサイトの改善に繋げるなど、主に集客・収益の増加のために導入される。

Q: どのように活用するか?

GAを使用すればCV(コンバージョン)に繋がる流入経路、どのページがCVに貢献しているのかを視覚的に確認することができる。データ項目が多いので、改善のために「今どんなデータを見るべきか、どんなデータが必要か」という目的を明確にすることが大切。
*コンバージョン:webサイト運営の目的(ECサイトであれば商品の販売、見込み客の獲得など)を達成すること・ユーザーが行う特定のアクション(商品の注文や資料請求など)を指す。

改善目的を達成するためにPDCAサイクルに当てはめる。
PLAN:計画    
サイト訪問者の動向を把握し(人気ページや問題のあるページなど)、改善計画を立てる
DO:実行   
計画した方法をやってみる(少しづつ)
CHECK:評価   
計画に沿って実行できていたかを、比較分析して評価。有効かどうか判断する。
ACTION:改善 
サイトを改善する

ユーザー満足に繋げることで集客・収益増加に繋げる

Q: どのようにデータを取得しているか?

ウェブビーコンと呼ばれるトラッキング方法でデータを集計。
ウェブビーコン型:ウェブビーコンと呼ばれるトラッキングコードをwebサイトのソースに埋め込み、ユーザーがページを開いたときにこのウェブビーコンが反応してトラッキング情報をアクセス解析用のデータベースに送信することでデータ集計を行う方法。

GAの画面の操作に関しては、Google公式の無料教材がわかりやすかったです。操作の仕方やレポートの作成方法などはデモを使って実際に手を動かし、目で動きを追いかけながら確認できます。今回GAの設定・使用方法については省略させていただくので是非使ってみてください。参考:https://support.google.com/analytics/answer/4553001?hl=ja 

Google Tag Manager

Q: Google Tag Managerとは?

Googleが提供している無料のタグマネジメントツール。
前述の通り、GAはウェブビーコンという方法でデータを取得している。そのため、計測したい項目用のイベント発火タグや、ECサイトであればEコマース用のタグ、他にも広告タグなどを利用するためにそれぞれ専用のタグをwebサイトの対象ページのHTMLに埋め込む必要がある。このタグの追加・削除のたびにコードを編集するのは手間がかかるため、GTMのオンライン管理画面上からタグを埋め込むページや、タグの種類を管理できるようにしたもの。

Q: どのように活用するか?

設定(タグ、トリガー):
GTMの管理画面上で「タグ」「トリガー」を設定し、イベントの発火タイミングを決定する。(GTMでは「どのタグ」を「いつ」起動させるかといった情報が設定をする上で必要。この「いつ」を操るのが「トリガー」。)

公開前のテスト:
タグはGTMの「プレビューモード」(デバッグモード)という機能を使って本番環境にタグを公開する前に動作確認をする。(GTMにログインしているユーザーのみブラウザ上で表示される。)
登録したタグが「発火している/発火していない」という状況を確認できる。

バージョン管理機能:
GTMで設定をする場合、ワークスペースの現在の状態を1つのバージョンとしていつでも保存できるため、必要に応じてワークスペースを元のバージョンに戻すことができる。ミスがあった場合などにワークスペースを以前のバージョンに戻して公開することができる。

Q: どのようにGAとwebサイト上の各イベントを繋げているのか?

*以下図でまとめてみました。

①GAにはトラッキングID(GAに登録する際、webサイト一つに対して必ず一つのプロパティが作成される。このプロパティの識別IDのこと)と呼ばれる「UA-000000-2」のような文字列がある。(GA管理画面>プロパティ設定>トラッキングIDで確認できる)
②GTMでタグを作成する際に、このトラッキングIDを入力する箇所がある(GTM上で変数化しておくことによって、トラッキングIDを一元管理することもできる。*タグを作成するたびに入力する手間が省ける)
③ユーザーが該当ページを読み込んだ際、あらかじめ設定していたトリーガーの箇所でタグが発火、紐付くトラッキングIDを元に、GA上にデータが反映される。

Eコマース

Q: Eコマースとは?

Eコマース:「購入完了時のデータ」購入に至った流入元、商品名、商品単価、売り上げなどの情報を追跡、分析することができる

Q: どのように活用するか?

Eコマースを設定することで、取得したデータを活かして以下のようにwebサイト改善の手がかりを見つけることができる。

どの商品が売れているかを推測できる
広告キャンペーンの効果(売上、数量など)を確かめられる
ユーザーが商品購入に至るまでにサイトを訪れた回数、かかった日数が分かる
収益予測を立てることができる
ECサイトの改善(購入手続きの改良、他社との比較機能の導入など)に役立つ

拡張Eコマース

Q: 拡張eコマースとは?
拡張Eコマース:「ページの閲覧やカート投入なども網羅したデータ」標準のeコマース機能に追加して、商品詳細ページの表示回数や、カート落ち等の購入前行動を追跡、分析することが可能。

Q: どのように活用するか?

拡張Eコマースを設定することで、Eコマース機能より詳細に取得したデータを活かして以下のようにwebサイト改善の手がかりを見つけることができる。
購入プロセスの可視化によるボトルネックの把握(購入プロセスの改善は売上に直結しやすい)
商品軸で詳細な販売状況がわかる(商品ごとのクーポンコードの利用や返品も計測)
商品別の解析レポートの拡張でブランドとバリエーションもカテゴリに追加で比較しやすくなった(Eコマースでは商品情報を、「商品名」「SKU(最小管理単位)」「カテゴリ」の3つのフィールドで計測)
商品の露出状況とパフォーマンスの把握が可能になった(商品が表示される箇所を商品リストとして登録し、どの切り口でユーザーが購入に至ったのかを商品リストごとに比較することができる)
サイト内プロモーションと特定の商品を紐付けた評価ができる(今回未設定)
返金が発生した場合に返金リクエストを送信でき、より正確なデータが取得可能(webサイト上に返金処理を行う実装がないため今回未設定)

ここからは実際に設定と実装の説明に入ります。

Google Analytics設定

①アカウントの作成  https://marketingplatform.google.com/intl/ja/about/analytics/
②トラッキングコードを確認(トラッキングIDをGTM設定の際に使用するので控えておく)
③管理画面>目的のビュー>eコマースの設定>eコマースの有効化>オン → 保存

Google Tag Manager設定

①アカウントの作成
②GTMのライブラリをインポート (今回nuxt.jsを使用したSPAの実装のため、ページビューをこのライブラリでよしなにGAに送信してもらいます。)
③nuxt.congig.tsに設定ファイルを記述

Eコマース設定 - Google Tag Manager

管理画面で作成していきます。

作成するのは以下のタグ2つ、変数3つ、トリガー2つ
*名称はそれぞれのwebサイトに合わせて決めてください

【タグ】

(1)SPAタグ: ユーザーが読み込んだSPAのwebサイト全てのページビューをGAに送る

タグ名: PageView_SPA_Tagなど
タグの種類: Google アナリティクス:ユニバーサルアナリティクス
トラッキングタイプ: ページビュー
Googleアナリティクス設定: {{GoogleAnalytics_setting}} *以下で設定したトラッキングIDの変数

*詳細設定>設定するフィールド>
フィールド名:page、値:{{SPA_pageUrl}} *以下で設定したpageUrlの変数
フィールド名:title、値:{{SPA_routeName}} *以下で設定したrouteNameの変数

トリガー:SPA表示トランザクション

(2)Eコマースコンバージョンタグ: Eコマースのデータで必要となるユーザーの注文完了データをGAに送る

タグ名: Setting_eコマース_Tagなど
タグの種類: Google アナリティクス:ユニバーサルアナリティクス
トラッキングタイプ: トランザクション
Googleアナリティクス設定: {{GoogleAnalytics_setting}}

トリガー:Eコマーストランザクション

【変数】

(1)トラッキングID
タグで一回一回設定するのは手間が省ける&ミスが起こる可能性があるため、変数で扱う。

変数名:GoogleAnalytics_settingなど
変数のタイプ:Google アナリティクス設定
トラッキングID: GAのトラッキングID
Cookieドメイン: auto

(2)SPA_pageUrl

変数のタイプ:データレイヤーの変数
データレイヤーの変数名: GAのトラッキングID
Cookieドメイン: auto

(3)SPA_routeName

変数のタイプ:データレイヤーの変数
データレイヤーの変数名: GAのトラッキングID
Cookieドメイン: auto

【トリガー】

(1)SPA表示トランザクション

トリガーのタイプ:カスタムイベント
イベント名:nuxtRoute
このトリガーの発生場所:すべてのカスタムイベント

(2)Eコマーストランザクション

トリガーのタイプ:カスタムイベント
イベント名:sendOrderInfo
このトリガーの発生場所:すべてのカスタムイベント

Eコマース実装

注文完了画面処理が走るタイミングでEコマースのデータを収集するトラッキングコードをソースに設置。

①今回の実装では注文商品データはlistで持っているため、mapで回して以下の形式で変数productsに一つひとつ商品情報を入れる。(このキーの種類と内容についてはGA公式Eコマースドキュメントに詳しく記載されています。拡張Eコマースとは実装内容が違います。)

      sku: 'SKUまたはアイテムコード', 
      name: '商品名', 
      price: '商品価格', 
      category: '商品カテゴリー', 
      quantity: '数量'

②transactionProductsにこのproductsを渡して、GAに一会計分のデータを送る。

this.$gtm.pushEvent({ 
       event: 'イベント名', 
       transactionId: 'トランザクションNo', 
       transactionAffiliation: 'ブランド名', 
       transactionTotal:  '合計金額',
       transactionShipping: '送料',
       transactionTax: '消費税',
       transactionProducts: products 
     }); 

拡張Eコマース設定 - Google Tag Manager

*Eコマースと異なり、拡張Eコマースは取得する項目が多いため、その分タグの数も多くなります。
まずは拡張Eコマースで以下のデータを取得するように設計。

  • 商品インプレッションの測定
  • 商品クリックを測定
  • 商品詳細の表示を測定
  • カートに商品追加
  • カートから商品削除
  • 決済を測定する(決済ステップを測定する)
  • トランザクション(購入を測定する)

<取得するデータの概要>
1.商品インプレッションの測定:そのページ一覧で表示されるどんな商品を閲覧したかを計測
(例:ユーザーが検索結果のリスト上で初めて対象商品を目にしたとして、その商品がなんだったのかを一覧で情報取得するイメージ)
2.商品クリックを測定:ユーザーが商品に興味を示して商品リストをクリックするイメージ
3.商品詳細の表示を測定:ユーザーが商品に興味を示して商品リストをクリックした後、商品詳細画面を開くイメージ
4.カートに商品追加
5.カートから商品削除
6.決済を測定する(決済ステップを測定する):拡張Eコマースでは決済行動というデータを見れるため、決済行動をどこから開始とみなしてどこで終えたとするかをそれぞれのwebサイトの仕様に合わせて考える
7.トランザクション(購入を測定する):注文完了画面に遷移時に計測(Eコマースと同じ発火場所のイメージ)

<設計・実装の流れ> *今回私が行った方法

①取得が必要な項目とそのデータが取れる画面・ボタンなどをリストにして洗い出す
②それぞれイベント名を決める(デバッグ用に)*テスト環境で実装した時にタグの発火をリアルタイムで確認するため
③タグの発火タイミングを決める
④上記の<取得するデータの概要>のまとまりごとに実装
⑤イベントの発火を一つずつ確認しながら全てdataLayerの形式で実装する

タグ数が多かったので、画像のように項目ごとにタグとトリガーをセットにしてフォルダ分けしてみました。

今回はタグとトリガーを1セットとして、該当項目のイベントの数だけ以下と同じ形式で作成する。

【タグ】

タグ名:わかりやすいようにimpなどの項目名を入れた
タグの種類: Google アナリティクス:ユニバーサルアナリティクス
トラッキングタイプ: イベント
カテゴリ: この項目はデバッグ用に指定(カート削除など)*リアルタイムで見たときに判別できるように
アクション: この項目はデバッグ用に指定(カート一覧など)*リアルタイムで見たときに判別できるように
Googleアナリティクス設定: {{GoogleAnalytics_setting}}*ここはEコマースの設定と同様
eコマース>拡張eコマース機能を有効にする:真
データレイヤーを使用する:チェックマーク

トリガー:該当するトリガー

【トリガー】

トリガー名:タグと合わせた名前にすることで見分けやすくした
トリガーのタイプ:カスタムイベント
イベント名:ソースの**event**と同じもの

<デバッグの方法>

1.ソース→GTM発火確認:(GTMのプレビューモードを使用)
2.GTM→GAの反映確認:(GAのデバッグプラグインを使用してデータが期待通りに送られているか。GA上にリアルタイムで反映されているか)
*GA>リアルタイム>イベント で確認
*GAのデバッグプラグインで見た時にundefinedでデータが送られているときは実装に不備がないか確認

拡張Eコマース実装

それぞれデータを取得する箇所に以下の形式で拡張Eコマース用タグを埋め込む。(Nuxt.js Google Tag Managerのプラグインを使用するため、その形式に則った書き方をしています。)

ショッピング行動についての計測

Eコマースと同様、商品情報をそれぞれproductに入れ、eventをトリガーにしてpushEventでGAにデータを送る。

[1.ショッピング行動] 商品インプレッション
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   impressions: { product }   ※複数商品データ
  }
});
[2.ショッピング行動] 商品クリック
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   click: {
      actionField: { list : '商品一覧ページ名' }, 
      products: product  ※単体商品データ
   }
  }
});
[3.ショッピング行動] 商品詳細表示
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   detail: {
      actionField: { list : '商品一覧ページ名' }, 
      products: product ※単体商品データ
   }
  }
});
[4.ショッピング行動] 商品カート追加
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   add: {
      products: product ※単体商品データ
   }
  }
});
[4.ショッピング行動] 商品カート削除
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   remove: {
      products: product ※単体商品データ
   }
  }
});

決済行動についての計測

[決済行動] step1 ~ step3 (stepはそれぞれのwebサイトの仕様に合わせて)
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   checkout: { 
      actionField: { step:1 },
      products: product ※単体商品データ
   }
  }
});

下記のようにoptionをつけるとクレジットカード情報を送ることもできる

this.$gtm.pushEvent({ 
 event: 'イベント名', 
  ecommerce: { 
   checkout: { 
    actionField: { step: 2, option: ' visa ' }, 
    producs: product ※単体商品データ
   }
  }
});

ステップ設定時に気をつけること:
ソースのstepにはnumberを渡し、
GA上では管理>ビュー>eコマースの設定>目標到達のプロセスで設定したいnumberの順に合わせて表示するタイトルをつける。何もつけないとデフォルトで画像のようにステップ1,ステップ2のような表記になる。

最後にショッピング行動についての計測(再) ※これでラストです!

[5.ショッピング行動] 商品を購入
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   purchase: {
      Id: '商品ID',
      affiliation: 'ブランド名',
      revenue: '合計金額',
      shipping: '配送料',
      tax: '消費税(内税)',
      coupon: 'クーポン名',
      products: '商品データ'
   }
  }
});

振り返り

長くなってしまいましたが、SPAのEコマース/拡張Eコマース実装でした!
必要なデータがフロントで取れてるか、どこでイベントを発火させるのかなど、設計の段階が大切だと思いました。タグの数が多い分、一つひとつデバッグしながら確実に進めるのが良いと感じました。今回は大まかな流れについて書きましたが、細かい部分についてまとめたいのでまた記事作成します。

参考URL 参考書

参考URL

成果を上げるPDCAサイクル(1)Googleアナリティクス編 (PDCAサイクルなどわかりやすい)
https://webbu.jp/pdca-ga-4004
Googleタグマネージャの基礎知識~導入からGoogleアナリティクスとの連携まで~
https://nandemo-nobiru.com/ad-5758
Googleアナリティクス登録・設定手順【これさえ読めばOK!】
https://wacul-ai.com/blog/access-analysis/google-analytics-setting/ga-register/
Googleアナリティクスのeコマース機能を利用する方法
https://wacul-ai.com/blog/access-analysis/google-analytics-setting/e-commerce/
Google アナリティクスで強化された「拡張eコマース」機能がサクッとわかる6つの特徴
参考URL: https://webtan.impress.co.jp/e/2014/08/08/17970
Nuxt.js GoogleTagManagerライブラリ
https://github.com/nuxt-community/modules/tree/master/packages/google-tag-manager
GTM公式サイト
https://tagmanager.google.com/
拡張eコマース概要
https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce
拡張eコマース実装ドキュメント(デベロッパー向け)
https://developers.google.com/tag-manager/enhanced-ecommerce?hl=ja#product-clicks
拡張eコマース実装参考URL (※デバッグツール開くと、操作時にGAに送られているデータの形式を確認できる)
https://shop.googlemerchandisestore.com/

参考書

徹底活用 Googleアナリティクス
Googleアナリティクスの優しい教科書。
わかばちゃんと学ぶGoogleアナリティクス
ウェブ解析士2019認定試験公式テキスト

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

Vue.jsでパーティクルを自動生成して雪を降らせる

はじめに

この記事は自動生成した要素をアニメーションさせてから自動消滅させるまでの流れを把握することを目的にしています。
初心者向けの内容ですが、2 ファイル合計 120 行程度のコードなので気軽にご覧ください。
※ガチのパーティクルを簡単に扱いたい場合には Vue Particles が便利です。

出来上がるもの

ゆきが降っています!(断言)
yuki.gif

チートシート的なもの(初心者向け)

vuejsのsnow.jpg

コード全文

App.vue
<template>
  <div id="app">
    <snow v-for="(particle,index) in perticles" v-bind:key="particle.id"
      :x="particle.x"
      :y="particle.y"
      :limit_y="windowHeight-30"
      :dr="particle.dr"
      @thaw="thaw_snow(index)"
    >
    </snow>
  </div>
</template>

<script>
import Snow from './components/snow.vue'

export default {
  name: 'app',
  components: {
    Snow
  },
  data () {
    return {
      perticles: [],
      windowWidth: window.innerWidth,
      windowHeight: window.innerHeight,
      lastSpawnTime: 0,
    }
  },
  mounted() {
    window.addEventListener('resize', this.get_window_size)
    this.spawn_loop(0)
  },
  methods: {
    random_x(){
      return Math.floor(Math.random() * this.windowWidth)
    },
    next_id(){
      const usedids = this.perticles.reduce((accumulator, element) => {
          accumulator[element.id] = true
          return accumulator
      }, []);
      const nextid = usedids.findIndex((exists) => !exists)
      return nextid < 0 ? usedids.length : nextid
    },
    spawn_snow(){
      const id = this.next_id()
      const particle = { id, x: this.random_x(), y:0, dr: 1300}
      this.$data.perticles.push(particle)
    },
    thaw_snow(index){
      this.perticles.splice(index, 1)
    },
    get_window_size: function() {
      this.windowWidth = window.innerWidth
      this.windowHeight = window.innerHeight
    },
    spawn_loop: function(timestamp) {
      if(timestamp - this.lastSpawnTime > 60){
        this.spawn_snow()
        this.lastSpawnTime = timestamp
      }
      window.requestAnimationFrame(this.spawn_loop)
    }
  }
}
</script>
snow.vue
<template>
  <div class="snow-container"
  :style="{
     transform: `translate(${x}px, ${y+dy}px)`,
     transition: `transform ${dr}ms linear`
   }"
   >
    <span>ゆき</span>
  </div>
</template>

<script>
export default {
  name: 'snow',
  props: {
    x: { type: [Number], default: 0 },
    y: { type: [Number], default: 0 },
    limit_y: { type: [Number], default: 0 },
    dr: { type: [Number], default: 0 },
  },
  data () {
    return {
      dy: 0
    }
  },
  mounted(){
    window.setTimeout(() => {
      this.fall()
    } , 100)
  },
  methods: {
    fall(){
      this.dy = this.limit_y - this.y
      window.setTimeout(() => {
        this.$emit('thaw')
      } , this.dr)
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
div.snow-container {
  position: absolute;
  animation: fadein 1s ease 0s 1 normal;
}

@keyframes fadein {
  0% {opacity: 0}
  20% {opacity: 1}
}

</style>

定期的な雪情報の生成

雪の生成と管理に使う情報は App.vue で保持しています。

App.vue
data () {
  return {
    perticles: [],
    windowWidth: window.innerWidth,
    windowHeight: window.innerHeight,
    lastSpawnTime: 0,
  }
}

雪の位置情報は perticles 配列に格納します。
画面の横幅/縦幅は windowWidth / windowHeight に格納します。
最後に雪を生成した時刻は lastSpawnTime に格納します。

では雪を生成(spawn)する関数を見ます。

App.vue
random_x(){
  return Math.floor(Math.random() * this.windowWidth)
},
next_id(){
  const usedids = this.perticles.reduce((accumulator, element) => {
      accumulator[element.id] = true
      return accumulator
  }, []);
  const nextid = usedids.findIndex((exists) => !exists)
  return nextid < 0 ? usedids.length : nextid
},
spawn_snow(){
  const id = this.next_id()
  const particle = { id, x: this.random_x(), y:0, dr: 1300}
  this.$data.perticles.push(particle)
},

関数random_x()は画面の横幅に収まるようにランダムな x 座標を取得します。
関数next_id() は perticles 内で未使用の id を探し出します。
@FumioNonaka 様の記事を参考にさせていただきました。

追加と削除が繰り返される配列要素のオブジェクトに一意のid番号を振る

関数spawn_snow() は新しい雪情報を perticles に追加します。
雪の y 座標は画面の最上部である 0 にしています。
dr は 雪の落下にかかる時間(ミリ秒)で、1.3 秒に固定値しています。

spawn_snow()を定期的に実行するために、マウント後に発生するイベントで関数spawn_loop()呼び出します。

App.vue
mounted() {
  window.addEventListener('resize', this.get_window_size)
  this.spawn_loop(0)
},
App.vue
spawn_loop: function(timestamp) {
  if(timestamp - this.lastSpawnTime > 60){
    this.spawn_snow()
    this.lastSpawnTime = timestamp
  }
  window.requestAnimationFrame(this.spawn_loop)
}

関数spawn_loop()は、内部で関数requestAnimationFrame()を使用して繰り返しspawn_loop()を呼び出します。
前回の生成時刻から 60 ミリ秒経過している場合に関数spawn_snow()を実行します。

雪情報の定義

雪情報は snow.vue の snow コンポーネントの props で定義しています。

snow.vue
export default {
  name: 'snow',
  props: {
    x: { type: [Number], default: 0 },
    y: { type: [Number], default: 0 },
    limit_y: { type: [Number], default: 0 },
    dr: { type: [Number], default: 0 },
  },

x,y は最初に雪を表示する画面上の x 座標と y 座標です。
limit_y は雪が降った後で消える地点の y 座標です。
dr は雪が現れてから降り終わるまでの時間(ミリ秒)です。

蛇足ですが、これらの情報は実際には App.vue が管理します。snow.vue の役割はこれらの値を App.vue から受け取って表示に反映するだけです。

すべての雪の描画

v-for ディレクティブを使用して perticles の要素数だけ snow コンポーネントを生成しています。

App.vue
<snow v-for="(particle,index) in perticles" v-bind:key="particle.id"
  :x="particle.x"
  :y="particle.y"
  :limit_y="windowHeight-30"
  :dr="particle.dr"
  @thaw="thaw_snow(index)"
>

ここでは、App.vue が持つ情報と snow コンポーネントの props を関連付けています。
後述しますが、snow コンポーネントが通知する @thawイベントのハンドラも定義しています。

雪の落下アニメーション

snow コンポーネントのスタイルを指定します。

snow.vue
div.snow-container {
  position: absolute;
  animation: fadein 1s ease 0s 1 normal;
}

@keyframes fadein {
  0% {opacity: 0}
  20% {opacity: 1}
}

要素の座標は absolute (絶対位置)にしています。
また、雪が現れた瞬間はうっすらと半透明にしたいので、キーフレームアニメーションでフェードインの効果を付けています。

雪が降る動きは CSS アニメーションを用います。
まず、雪コンポーネント唯一の data として dy を定義しています。

snow.vue
data () {
  return {
    dy: 0
  }
},

dy は y 座標の変位です。props の y との合計値が落下後の y 座標になります。

snow.vue
<div class="snow-container"
  :style="{
   transform: `translate(${x}px, ${y+dy}px)`,
   transition: `transform ${dr}ms linear`
 }"
 >
  <span>ゆき</span>
</div>

Vue.jsでの CSS ア二メーションは @yuneco 様の記事を参考にさせていただきました。
Vue.js+SVGで自由にCSSアニメーションしたい人のための完全解説(ソース付き)

雪のタイミング制御

雪の落下タイミングを調整するために、マウント後のイベント内で関数setTimeout()を用います。

snow.vue
mounted(){
  window.setTimeout(() => {
    this.fall()
  } , 100)
},

マウント後、つまり雪が画面上部(y 座標:0)の位置に現れてから 100 ミリ秒後に落下用の関数fall()を実行します。

snow.vue
fall(){
  this.dy = this.limit_y
    window.setTimeout(() => {
      this.$emit('thaw')
  } , this.dr)
}

fall()が dy を変更することで、落下アニメーションが発生します。
その後、アニメーション時間分待機してから、雪を消す通知を発行します。(後述)

雪を消す通知

雪を消すための通知には$emitを用います。

snow.vue
fall(){
  this.dy = this.limit_y
    window.setTimeout(() => {
      this.$emit('thaw')
  } , this.dr)
}

上記のコードでは dr ミリ秒後(アニメーション時間後)にthawという名前のイベントを発行します。

App.vue
<snow v-for="(particle,index) in perticles" v-bind:key="particle.id"
  :x="particle.x"
  :y="particle.y"
  :limit_y="windowHeight-30"
  :dr="particle.dr"
  @thaw="thaw_snow(index)"
>

App.vue は snow コンポーネントが発行する@thawイベントを検知するために、バインディングしています。
App.vue は@thawイベントを受け取ると、関数thaw_snow()を実行します。

雪情報配列からの削除

thaw_snow()はアニメーションの終了した雪を配列から削除します。

App.vue
thaw_snow(index){
  this.perticles.splice(index, 1)
},

引数で配列番号を受け取り、splice()で配列要素を削除します。

まとめ

Vue.js でコンポーネント化した要素の扱い方を学ぶために、自動生成やメッセージを試してみました。
しかしながら要素の削除においては、ICS Media の 池田 泰延 様が以下のような調査報告をなさっています。

要素を動的に追加、削除する際は注意が必要ですね。

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

Cloud Firestore を Vue CLI で動かしてみる

はじめに

最近、Vue CLI を使えば、Cloud Firestore を動かすことが簡単にできることに気づいたので、
それを共有します

対象

  • Firestore を試してみたいが、環境構築の方法がわからない方
  • Firestore をとりあえず触ってみたい方

ゴール

Vue CLI を使って、 Firestore へのデータの書き込み・読み出しを行うこと

環境

  • Mac OS
  • npm インストール済み

Vue CLI のインストール ~ テストページの起動

Vue CLI のインストール

$ npm install vue
$ npm install -g @vue/cli

プロジェクトの作成・テストページの表示

プロジェクトを作成します
途中で設定をどうするか聞かれるのでdefaultを選択します

$ vue create my-project

Vue CLI を使ってテストページを開きます

$ cd my-project
$ npm run serve

ブラウザで http://localhost:8080/ をアクセスするとテストページが表示されます

test-page.png

ここまでで、テストページの表示ができるようになりました

Firebase の設定 ~ Vueに記載する設定の取得

Firebase コンソールにアクセスし、Firebaseプロジェクトを作成していきます(Google アカウントが必要)

コンソール

https://console.firebase.google.com/

Firebase プロジェクトの作成

コンソール画面から、プロジェクトを作成を選択します
コメント 2019-12-09 213927.png

プロジェクト名を入力します
コメント 2019-12-09 213452.png

注意点を確認し、「続行」
コメント 2019-12-09 214309.png

そのままにして「続行」(off にしてもよいです)
コメント 2019-12-09 214424.png

「Default Account for Firebase」を選択します

コメント 2019-12-09 214611.png

Vueに記載する設定の取得

Vue で Firebase を利用するために必要な設定を取得します

Project Overview -> </> でWebアプリ設定画面へ

コメント 2019-12-09 215319.png

アプリ名を入力します
コメント 2019-12-09 215609.png

以下の値をコピーしておきます

  // Your web app's Firebase configuration
  var firebaseConfig = {
    apiKey: [表示された値],
    authDomain: [表示された値],
    databaseURL: [表示された値]",
    projectId: [表示された値],
    storageBucket: [表示された値],
    messagingSenderId: [表示された値],
    appId: [表示された値],
    measurementId: [表示された値]
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);

Cloud Firestore の作成

  1. メニューから Database を選択します
  2. 「データベースを作成」を選択します
    コメント 2019-12-09 220229.png

  3. 「テストモードで開始」を選択(公開する時には、別途ルールの設定を行ってください)
    コメント 2019-12-09 220455.png

  4. asia-northeast1(東京)を選択します
    コメント 2019-12-09 220939.png

以上で、Firebase 側の設定がおわります

Vue で Firestore を利用する

初期化を行う

firebase パッケージをインストールします

$ npm install --save firebase

my-project/src/main.js に、Cloud Firestore を初期化する処理を追加します

// main.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

import firebase from 'firebase'
var firebaseConfig = {
  apiKey: [表示された値],
  authDomain: [表示された値],
  databaseURL: [表示された値],
  projectId: [表示された値],
  storageBucket: [表示された値],
  messagingSenderId: [表示された値],
  appId: [表示された値],
  measurementId: [表示された値]
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

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

テストページをVue CLIで再度起動し、コンパイルエラーが発生していないことを確認します

$ cd [my-project へのパス]
$ npm run serve

以上で、Firestore を利用するための設定が終わりました

データを追加する

Vuejs で、Firestore のコレクションに、データを追加してみます

コーディング

以下のファイルを追加

  • src/components/todo/Add.vue
  • src/App.vue
// src/components/todo/Add.vue
<template>
  <div>
    <h1>New TODO</h1>
    <div>
      <ul>
        <li><input v-model="name"></li>
        <li><button v-on:click="addTodo">Add</button></li>
      </ul>
    </div>
  </div>
</template>

<script>
import firebase from 'firebase'
import 'firebase/firestore';

export default {
  name: 'todoAdd',
  data: function () {
    return {
      db: null,
      name: ''
    }
  },
  created: function() {
    this.db = firebase.firestore()
  },
  methods: {
    addTodo: function () {
      var _this = this

      // todos コレクションにドキュメントを追加
      this.db.collection('todos').add({
        name: _this.name
      })
      .then(function () {
        // 追加に成功したら、name を空にする
        _this.name = ''
      })
      .catch(function () {
        // エラー時の処理
      })
    }
  }
}
</script>


// src/App.vue
<template>
  <div id="app">
    <Add></Add>
  </div>
</template>

<script>
import Add from './components/todo/Add.vue'

export default {
  name: 'app',
  components: {
    Add
  }
}
</script>

動作確認

データが追加されることを確認するために、
Firebase Console の Database ページを開いておきます

コメント 2019-12-09 222431.png

npm run serve を実行し、http://localhost:8080/を開きます

コメント 2019-12-09 230338.png

追加ボタンを押すと、
コンソール画面で、 todosコレクションが新たに作成され、その中にドキュメントが作成されていることがわかります
コメント 2019-12-09 222431.png

データを取得・監視する

Firestore では、コレクションやドキュメントの取得だけでなく、リアルタイムアップデートを取得することもできます
ここでは、todos コレクションを監視し、変更を表示に反映させるようにします

コーディング

以下のファイルを追加・更新します

  • src/components/todo/Index.vue
  • src/App.vue
// src/components/todo/Index.vue
<template>
  <div>
    <h1>Index</h1>
    <table>
      <thead>
        <td>name</td>
      </thead>
      <tr v-for="todo in this.todos" :key="todo.id">
        <td>
          {{ todo.name }}
        </td>
      </tr>
    </table>
  </div>
</template>

<script>
import firebase from 'firebase'

export default {
  name: 'todoIndex',
  data: function () {
    return {
      db: null,
      todos: []
    }
  },
  created: function () {
    this.db = firebase.firestore()

    var _this = this
  // todos コレクションを監視する
    this.db.collection('todos').onSnapshot(function (querySnapshot) {
      _this.todos = []
      querySnapshot.forEach(function (doc) {
        var data = doc.data()
        data.id = doc.id
        _this.todos.push(data)
      })
    })
  }
}
</script>
// src/App.vue
<template>
  <div id="app">
    <Add></Add>
    <Index></Index>
  </div>
</template>

<script>
import Add from './components/todo/Add.vue'
import Index from './components/todo/Index.vue'

export default {
  name: 'app',
  components: {
    Add,
    Index
  }
}
</script>

動作確認

追加ボタンで、todosコレクションにドキュメントを追加し、それが表示に反映されるかを確認します

http://localhost:8080/ を開き、追加ボタンを押すと、

コメント 2019-12-09 234620.png

ページが動的に更新されることを確認できます

コメント 2019-12-09 234636.png

コード

最後に、ここまでで書いたコードをのせておきます

// main.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

import firebase from 'firebase'
var firebaseConfig = {
  apiKey: [表示された値],
  authDomain: [表示された値],
  databaseURL: [表示された値],
  projectId: [表示された値],
  storageBucket: [表示された値],
  messagingSenderId: [表示された値],
  appId: [表示された値],
  measurementId: [表示された値]
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

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

// src/components/todo/Add.vue
<template>
  <div>
    <h1>New TODO</h1>
    <div>
      <ul>
        <li><input v-model="name"></li>
        <li><button v-on:click="addTodo">Add</button></li>
      </ul>
    </div>
  </div>
</template>

<script>
import firebase from 'firebase'
import 'firebase/firestore';

export default {
  name: 'todoAdd',
  data: function () {
    return {
      db: null,
      name: ''
    }
  },
  created: function() {
    this.db = firebase.firestore()
  },
  methods: {
    addTodo: function () {
      var _this = this

      // todos コレクションにドキュメントを追加
      this.db.collection('todos').add({
        name: _this.name
      })
      .then(function () {
        // 追加に成功したら、name を空にする
        _this.name = ''
      })
      .catch(function () {
        // エラー時の処理
      })
    }
  }
}
</script>

```vuejs

// src/components/todo/Index.vue
<template>
  <div>
    <h1>Index</h1>
    <table>
      <thead>
        <td>name</td>
      </thead>
      <tr v-for="todo in this.todos" :key="todo.id">
        <td>
          {{ todo.name }}
        </td>
      </tr>
    </table>
  </div>
</template>

<script>
import firebase from 'firebase'

export default {
  name: 'todoIndex',
  data: function () {
    return {
      db: null,
      todos: []
    }
  },
  created: function () {
    this.db = firebase.firestore()

    var _this = this
  // todos コレクションを監視する
    this.db.collection('todos').onSnapshot(function (querySnapshot) {
      _this.todos = []
      querySnapshot.forEach(function (doc) {
        var data = doc.data()
        data.id = doc.id
        _this.todos.push(data)
      })
    })
  }
}
</script>
// src/App.vue
<template>
  <div id="app">
    <Add></Add>
    <Index></Index>
  </div>
</template>

<script>
import Add from './components/todo/Add.vue'
import Index from './components/todo/Index.vue'

export default {
  name: 'app',
  components: {
    Add,
    Index
  }
}
</script>

最後に

Vue CLIでFirebaseを動作させる方法を紹介させていただきました

参考

Vue CLI
https://cli.vuejs.org/

Vue.js を vue-cli を使ってシンプルにはじめてみる
https://qiita.com/567000/items/dde495d6a8ad1c25fa43

Cloud Firestore データベースを作成する(公式)
https://firebase.google.com/docs/firestore/quickstart?hl=ja

Cloud Firestore を使ってみる(公式)
https://firebase.google.com/docs/firestore/quickstart?hl=ja

Cloud Firestore にデータを追加する(公式)
https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja

Cloud Firestore でリアルタイム アップデートを入手する(公式)
https://firebase.google.com/docs/firestore/query-data/listen?hl=ja

Vue.js + FirebaseでTodoアプリを作る
https://qiita.com/magaya0403/items/e292cd250184ea3fe7b0

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

jQueryしか書けなかった自分がVueを使えるようになるまでの話と同じような環境の人たちへ

JavaScriptといえば、jQueryだった

Vueを触る前はHTML+CSS+jQuery+WordPressが主な使用言語でした。以前は小〜中規模なWebサイト制作がメインだったので、JavaScriptを操作する場面というのは多くありませんでした。

使ってもハンバーガーボタンの操作やjQuery プラグインのスライダー、ちょっとしたエフェクト程度です。少なくとも状態管理をするような案件はほとんどなく、DOM操作がメインなのでjQueryで十分でした。

1つだけ状態管理をする案件があり、案件としてはVueに最適でしたが、jQueryでごり押しました。過去の自分に教えてあげたい。

VueってWebアプリケーションやSPAのためのものでしょ?

Vueを学習する前は自分もそう思っていました。Webサイト制作がメインの自分にはあまりメリットがないのかと。確かに小規模でSPAでもないWebサイトであればVueで作るメリットは少ないです。中規模になってくると、共通パーツが出てきたり、さまざまなインタラクション処理が出てきます。そこでVueのコンポーネント機能や便利なAPIを活用できます。

クリックやホバーといったマウスアクションに対する処理やフォーム入力、キーボード操作などにもAPIが用意されているので、楽にその処理を書けます。Vue単体ではSPAになりますが、Vueを拡張したフレームワークNuxtを使えばSPAの利点を活かしつつ静的サイトも作れます。

jQueryしか分からない自分がVueを始めてぶち当たった壁

Vueを学習し始めて、いくつか壁にぶつかりました。

JavaScriptの知識が足りてない

jQueryしか触ってこなかった自分には素のJavaScriptの知識が不足していました。DOM操作はdocument.querySelecter()などで置き換えればよかったですが、配列の操作などはpushとshiftくらいしか分かりません。filterメソッドの書き方もよく分からなかったですし、コールバック関数もあまり理解していませんでした。さらに、API取得で重要な非同期処理についてはほとんど知識0の状態です。

そのためVueの課題に出てくるfetchメソッドや配列操作など、Vueとは別のところで苦戦しました。Vue学習と並行して、JavaScriptの勉強もしました。

state管理という概念

Vuexを用いたstate管理というのは新しい概念でした。それまでのjQuery案件ではそんなに状態管理をする場面がありませんでした。せいぜいメニュー、アコーディオンの開閉状態くらいです。それをstoreで集中管理するという概念は新しいものでした。

最初はそのありがたみはよく分かりませんでした。上記のような例であれば関数内に状態を持たせてしまえば良いですが、WebアプリケーションやSPAなどのページ遷移をせず、画面が切り替わる場合には面倒になってきます。

SPAなどでなくても、現代のWebサイトは単純に表示するだけでなく、画面内で処理を行う場面が増えてきました。メニューの開閉以外にも入力状態などを管理するときにstoreを用いた集中管理は便利です。

学習半ばでVue CLIに手を出した。

実際の現場では<script>タグで読み込むよりはnpm(yarn)で環境構築して開発する方が多いかと思います。教本のテンプレート機能の章を飛ばして、単一ファイルコンポーネントによる開発に手を出してしまいました。そのためエラーが起きてもテンプレートの構文ミスなのか、単純な処理のミスなのか問題の切り分けが大変で、自らの首を締める結果になりました。

同時にTypeScriptにも手を出した。

さらには全く触ったことのないTypeScriptにも手を出したので、より難易度は高まってしまいました。これは先程の問題と同様TypeScriptエラーなのかどうかも分からず、苦戦が続きました。(とりあえず、anyでつぶした

先回りや近道などせず、地道に教本の通りに進めていくのが良いです。

それでもVueは習得できた

そんな自分でもVue自体の習得は難しくなかったです。というのもVue自体はフレームワークであり、「書き方」さえ覚えれば使えるようになります。もちろん、mutation, actionによるstate変更手続きやdispatchの記法などや、Vuexによるstateの管理など新しい概念はありましたが、書き方自体はすごく難しいわけではありません。

大事なのはJavaScriptがきれいに書けること

結局、自分が一番時間をかけたのは中の処理の書き方、つまりJavaScriptを書けるようになることでした。VueはJavaScriptのフレームワークであるので、JavaScriptがきちんと書けることが大切です。基本的な配列の操作、非同期処理の知識はVueを使いこなす上で大切になってきます。現代的なJavaScriptの書き方(ES6仕様やアロー関数など)も同時に勉強しました。

自分の場合、Vueを使えるようになってもJavaScriptが稚拙だったので、初めてオリジナルで作ったVueプロジェクトは今見返すと、結構ひどい出来でした。(一応目的通り動きますが、全然スマートな書き方ではありませんでした。)

逆を言えば、Vue自体の学習コストはそこまで高くなく、導入は簡単です。教本の通りに進めていけば、TODOリストなりのSPAはすぐに作れるようになります。

さいごに

フレームワークは面倒なことを手軽に実装できます。jQueryは画面(DOM)操作を簡単にし、VueはJavaScriptによる動的な処理や状態管理の実装を簡単にしてくれます。VueとjQueryの目的はそれぞれ違うので、完全上位互換というわけでもありません。

最近のWebサイトは単純なDOM操作だけでなく、画面上の動的な処理を行う場面も増えてきていますので、Vueを活用できる機会もより増えてくると思います。もし、自分と同じようにjQueryしか書けず、Vueに挑戦したい人の助けになれば幸いです。

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

意外と間違えるVue.jsのバージョン確認方法

間違い

% vue -V
@vue/cli 4.1.1

一見合ってそうですが、このコマンドではVue.jsのプロジェクトを簡単に立ち上げるためのパッケージ「Vue CLI 」のバージョンが表示されてしまいます。

正解

プロジェクトごとにインストールしている場合

% npm list vue
vue-hoge@1.0.0 /Users/fuga/project/vue-hoge
└── vue@2.6.10 

グローバルにインストールしている場合

% npm list -g vue
/usr/local/lib
└── vue@2.6.10 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

カルネージハートとは関係ないVue Router内でVueAppにアクセスする

カルネージハート Advent Calendar 2019 8日目の記事です。

今回はカルネージハートとは全く関係ないVue Router内でVueAppにアクセスする話です。

route.jsの設定

import Vue from 'vue'
import Router from 'vue-router'
import Home from './components/page/Home'

Vue.use(Router)

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
    },
  ]
});

export default router

こんな感じでVue Routerを設定している場合、console.log(router)でアクセスできる当たり前だ

import Vue from 'vue'
import Router from 'vue-router'
import Home from './components/page/Home'

Vue.use(Router)

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
    },
  ]
});

console.log(router);

export default router

beforeEachで毎回API通信してなんかやりたいときも

import axios from "axios"



router.beforeEach((to, from, next) => {
  axios.get("/api/category").then(response => {
  });
})
export default router

のような感じで

または

router.beforeEach((to, from, next) => {
  router.app.$http.get("/api/category").then(response => {
    console.log(response.data);
  });



app.jsなどでVue.prototype.$http = axiosを設定ずみ

Axiosを使ってAPI通信も可。

最後に

事実上の最新作EXAが2010年に発売以降続編の情報が皆無ですが、一部の熱狂的ファンは大会を開催してゲームを続けています。ゲームを盛り上げることで続編も出るかもしれません。カルネジスト、ネジらーの皆様のご協力をお願いします!

カルネージハートファンのプログラミング知識を共有しましょう!

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

docker-compose + vue.js + typescript 環境構築

元々知っているvue.jsに
最近仕事で使うdocker + typescriptを合わせた環境構築を行ってみた

基本参照はここかなり見ました。

os ubuntu 18.04

docker-compose用のyml生成

cd your/dev/dic
touch docker-compose.yml 
docker-compose.yml
version: '3'
services:
  node:
    image: node:12.7.0-alpine
    volumes:
      - .:/vuejs

nodeのversion指定 + マウント先はvuejsに指定

コンテナ内に入る

入った後にcli起動して初期インストール

docker-compose run node sh
cd vuejs
yarn global add @vue/cli

cli終わったら
vueのプロジェクトを作成

vue create .

vue/cil経由で環境構築(注意点有)

対話型で処理を進めて行くのですが、please pick a presetのときに
Manually select features を選んでください
(そうじゃないとtypescript入らない)

Vue CLI v4.1.1
? Generate project in current directory? Yes


Vue CLI v4.1.1
? Please pick a preset: Manually select features
? Check the features needed for your project: 
 ◉ Babel
 ◉ TypeScript
 ◉ Progressive Web App (PWA) Support
 ◉ Router
 ◉ Vuex
 ◉ CSS Pre-processors
 ◉ Linter / Formatter
 ◉ Unit Testing
 ◉ E2E Testing

作る環境によって必要なmoduleが違うでしょうが
とりあえず今回は全部チェック入れ

後は細かい設定聞かれますがわからないやつは基本ググってほしい
たぶんtest toolを jestとかフォーマット設定まともにしておけば
大丈夫そう

Vue CLI v4.1.1
? Generate project in current directory? Yes
Vue CLI v4.1.1
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, PWA, Router, Vu
ex, CSS Pre-processors, Linter, Unit, E2E
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode
, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server set
up for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS 
Modules are supported by default): Sass/SCSS (with node-sa
ss)
? Pick a linter / formatter config: TSLint
? Pick additional lint features: (Press <space> to select,
 <a> to toggle all, <i> to invert selection)Lint on save
? Pick a unit testing solution: Jest
? Pick a E2E testing solution: Cypress
? Where do you prefer placing config for Babel, ESLint, et
c.? In dedicated config files
? Save this as a preset for future projects? No

コンテナに戻るとプロジェクトが生成されているはず

dockerで動かす

対象ディレクトリ直下にdockerfileを作成

dockerfile
FROM node:12.7.0-alpine

WORKDIR /myapp

COPY package.json ./
COPY yarn.lock ./

RUN yarn install
docker-compose.yml
version: '3'
services:
  view:
    build: .
    command: yarn run serve
    volumes:
      - .:/myapp
      - /myapp/node_modules
    ports:
      - "8000:8080"
docker-compose up -d

後はlocalhost:8000でサイトが開けるようになっているはずです!

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

【Vue.js】とあるテキストエディタのライブラリ選定の変遷

アドベントカレンダーでもなんでもないです。

とあるプロジェクトでテキストエディタの開発に1年以上に渡って携わりました。
その中でのライブラリ選定にあたり紆余曲折がありましたので記します。

要件

  • シンプルにテキスト入力ができる
  • データとしてプレーンテキストを取り出せる
  • コピー&ペーストなどができる
  • 文字装飾は一切不要
  • 画像を挿入できる

この画像挿入が鬼門となります。
画像挿入を満たすため、WYSIWYGエディタライブラリを模索することとなります。

version.1 tiptap

https://github.com/scrumpy/tiptap

以下の理由からtiptapを採用しました

  • 要件を満たせる機能郡
  • Vueライブラリである

生じた問題

実際にリリースしてみると、特定の環境や操作において以下のような問題が発生してしまいました。

  • ペースト時に連続する半角スペースが1つになる
  • ペースト時に全角スペースが半角スペースになる
  • ペースト時に改行が消える
  • 約1万文字を超えるとクラッシュする

主にiOSで多くの異常が見られました。
その他にも全角文字や改行コードの入力に関するハックが必要な事象が多く発見されました。

基本的に変換不要な英数字の基本的な入力しかサポートされてないように思われます。

version.2α

ライブラリの再選定

https://github.com/codex-team/editor.js
https://github.com/Microsoft/roosterjs
https://prosemirror.net/
https://draftjs.org/

version.2 quill

いくつかのライブラリを発見したものの、採用実績やフレームワークとの相性を加味した結果次のQuillを採用しました。

https://quilljs.com/

  • 試験実装において前回の不具合は発生しない
  • Vueライブラリではないものの、導入が容易

生じた問題

今回はversion.1における不具合を踏まえQuality Assuranceを通過したものの、新たな特定の環境や操作に依存した不具合が発生してしまいました。

  • HTML形式のデータをペーストすると一部不要なスタイルの適用や改行の増加が見られる(Wordなど)
  • 特定の環境・特定の位置でbackspaceするとクラッシュする
  • 見た目上編集できるがインスタンス内にはデータが存在しないことがある

特にIEで問題が頻発することとなります。
クリップボード上のHTMLの加工処理の複雑さからこちらの想定するもとのプレーンなテキストをペーストをするのが困難です。
改行に関しては特に<p>タグをコピーしていた場合、ブラウザによりテキスト+改行でとしてデータが取得されるために改行の増加が見られましいた。
また、内部の特殊なデータ構造の複雑さ・イベント処理の複雑さからオーバーライドしての処理なども困難です。

version.3α エディタ自前実装の検討

これまでの不具合を勘案し、エディタライブラリの自作という選択肢を考慮したものの以下の理由から断念することとなりました。

  • メンテナーがいなくなる懸念(少人数チームのため)
  • contenteditableのブラウザ差異を埋める実装の困難さ
  • WYSIWYGを作り込むのは機能過多かつ工数不足

version3. そして現在

以上を考慮し、contenteditableを捨てることとなりました。
結局いくついたのは<textarea>です。。。

抜粋.vue
<div v-for="(t, index) in texts" :key="index">
  <div v-if="t.image">
    <img :src="t.image.src"
         :alt="t.image.alt"
         @click="deleteImage(index)">
  </div>
  <div v-else>
    <textarea v-model="t"
              :index="index"
              :ref="`text${index}`">
  </div>
</div>

これに各種カーソル移動や変更のハンドラを追加したものが完成形となりました。

教訓

  • 過多な機能は誤動作を巻き起こす
  • ブラウザ差異の想定されるAPI(contenteditableのような)はライブラリでも解決されない場合がある
  • 全角入力は想定されてない場合がある
  • iOSとIEは魔境

Simple is best.

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

G Suite(Google apps script)というサーバレスな環境で、BootstrapとVue.jsを組み合わせたフォーム開発のフォームテンプレ一覧

はじめに

BootstrapとVue.jsを組み合わせて、サーバレスな環境であるG Suite(Google apps script)でフォームを作成する。

本ページでの掲載事項

フォームの基本的な項目のテンプレートを掲載。

前提

G SuiteのAPIを使ってHTMLページをフォームとして作成するため、
スプレッドシートからスクリプトエディタを起動させプロジェクトを新規作成しておく。

コード.gs

主な処理内容
- アプリケーションにアクセスした際にHTMLページを返却
- スプレッドシートに格納された値を取得して辞書型のオブジェクトにして返却

コード.gs
function doGet() {
  var html = HtmlService.createTemplateFromFile("index").evaluate().addMetaTag('viewport', 'width=device-width, initial-scale=1, shrink-to-fit=no');
  return html;
}

function getSS(spreadSheetID, sheetName){

  var res = SpreadsheetApp.openById(spreadSheetID)
    .getSheetByName(sheetName).getDataRange().getDisplayValues();

  var keys = res.splice(0, 1)[0];

  return value = res.map(function(row) {
    var obj = {}
    row.map(function(item, index) {
      obj[keys[index]] = item;
    });
    return obj;
  });
}

function getData() {

  var SSID = "--yourSpreadsheetID--";
  var SN = "--yourSheetName--";

  var options2 = getSS(SSID, SN);

  return options2; 
}

index.html

主な処理内容
- レイアウトは、Bootstrapのsampleを利用
- 「入力項目」「disabel状態の入力項目」「Vue.jsの変数の値」を表示の順に項目を設定

index.html
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <?!= HtmlService.createHtmlOutputFromFile('css').getContent(); ?>
    <?!= HtmlService.createHtmlOutputFromFile('customcss').getContent(); ?>

  </head>
  <body>

  <nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
    <a class="navbar-brand" href="#">Navbar</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>

  <div class="collapse navbar-collapse" id="navbarsExampleDefault">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="#">Link</a>
      </li>
      <li class="nav-item">
        <a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
      </li>
      <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" href="#" id="dropdown01" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Dropdown</a>
        <div class="dropdown-menu" aria-labelledby="dropdown01">
          <a class="dropdown-item" href="#">Action</a>
          <a class="dropdown-item" href="#">Another action</a>
          <a class="dropdown-item" href="#">Something else here</a>
        </div>
      </li>
    </ul>
    <form class="form-inline my-2 my-lg-0">
      <input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
      <button class="btn btn-secondary my-2 my-sm-0" type="submit">Search</button>
    </form>
  </div>
  </nav>

  <main role="main" class="container">

    <div class="starter-template">
      <h1>Bootstrap starter template</h1>
      <p class="lead">Use this document as a way to quickly start any new project.<br> All you get is this text and a mostly barebones HTML document.</p>
    </div><!-- /.starter-template -->

    <div id="app">
    <div class="alert alert-primary" role="alert">
      input
    </div>
    <div class="form-group">
    <p>
      <label for="input">Example input</label>
      <input class="form-control" type="text" id="input" placeholder="Example input" v-model.trim="input"/>
    </p>
    <p>
      <label for="input">Example input</label>
      <input class="form-control" type="text" id="input" placeholder="Example input" v-model.trim="input" disabled/>
    </p>
    <p>Output: {{  input  }}</p>
    </div>

    <div class="alert alert-primary" role="alert">
      textarea
    </div>
    <div class="form-group">
      <label for="textarea">Example textarea</label>
      <textarea class="form-control" id="textarea" rows="3" v-model.trim="textarea"></textarea>
    </div>
    <div class="form-group">
      <label for="textarea">Example textarea</label>
      <textarea class="form-control" id="textarea" rows="3" v-model.trim="textarea" disabled></textarea>
    </div>
    <p>Output: {{  textarea  }}</p>

    <div class="alert alert-primary" role="alert">
      radio
    </div>
    <div class="form-check"> 
      <input class="form-check-input" type="radio" name="optionsRadios" id="optionsRadios1" value="unnecessary" checked="" v-model="necessary"/>
      <label class="form-check-label" for="optionsRadios1">
      unnecessary
      </label>
    </div>
    <div class="form-check">
    <input class="form-check-input" type="radio" name="optionsRadios" id="optionsRadios2" value="necessary" v-model="necessary"/>
      <label class="form-check-label" for="optionsRadios2">        
      necessary
      </label>
    </div>    
    <div class="form-check"> 
      <input class="form-check-input" type="radio" name="optionsRadios" id="optionsRadios1" value="unnecessary" v-model="necessary" disabled/>
      <label class="form-check-label" for="optionsRadios1">
      unnecessary
      </label>
    </div>
    <div class="form-check">
    <input class="form-check-input" type="radio" name="optionsRadios" id="optionsRadios2" value="necessary" v-model="necessary" disabled/>
      <label class="form-check-label" for="optionsRadios2">        
      necessary
      </label>
    </div>
    <p>Output: {{  necessary  }}</p>

    <div class="alert alert-primary" role="alert">
      select from vue
    </div>
    <div class="form-check"> 
      <label for="select">Example select</label>
      <select id="select" v-model="select" class="form-control">
        <option v-for="option in options" v-bind:value="option.value">
          {{ option.text }}
        </option>
      </select>
    </div>
    <div class="form-check"> 
      <label for="select">Example select</label>
      <select id="select" v-model="select" class="form-control" disabled>
        <option v-for="option in options" v-bind:value="option.value">
          {{ option.text }}
        </option>
      </select>
    </div>
    <p>Output: {{  select  }}</p>

    <div class="alert alert-primary" role="alert">
      select from spreadsheet
    </div>
    <div class="form-check"> 
      <label for="select">Example select</label>
      <select id="select2" v-model="select2" class="form-control">
        <option v-for="option2 in options2" v-bind:value="option2.value">
          {{ option2.text }}
        </option>
      </select>
    </div>
    <div class="form-check"> 
      <label for="select">Example select</label>
      <select id="select2" v-model="select2" class="form-control" disabled>
        <option v-for="option2 in options2" v-bind:value="option2.value">
          {{ option2.text }}
        </option>
      </select>
    </div>
    <p>Output: {{  select2  }}</p>

    <div class="alert alert-primary" role="alert">
      select from spreadsheet by sort
    </div>
    <div class="form-check"> 
      <label for="select">Example select</label>
      <select id="select3" v-model="select3" class="form-control">
        <option v-for="option3 in options3" v-bind:value="option3.value">
          {{ option3.text }}
        </option>
      </select>
    </div>
    <div class="form-check"> 
      <label for="select">Example select</label>
      <select id="select3" v-model="select3" class="form-control" disabled>
        <option v-for="option3 in options3" v-bind:value="option3.value">
          {{ option3.text }}
        </option>
      </select>
    </div>
    <p>Output: {{  select3  }}</p>

    <div class="alert alert-primary" role="alert">
      checkbox
    </div>
    <div class="form-check">
      <input class="form-check-input" type="checkbox" value="" id="checkbox" v-model="checkbox"/>
      <label class="form-check-label" for="checkbox">
        Check this checkbox
      </label>
    </div>
    <div class="form-check">
      <input class="form-check-input" type="checkbox" value="" id="checkbox" v-model="checkbox" disabled/>
      <label class="form-check-label" for="checkbox">
        Check this checkbox
      </label>
    </div>
    <p>Output: {{  checkbox  }}</p>

    </div><!-- /.vue.el.app -->

  </main><!-- /.container -->

  <?!= HtmlService.createHtmlOutputFromFile('js').getContent(); ?>
  </body>
  <?!= HtmlService.createHtmlOutputFromFile('vue').getContent(); ?>
</html>

vue.html

主な処理内容
- vue.methods
- initOptions3…読み込んだデータのうち条件に一致する行データを取得し、option3に格納

vue.js
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>

<script>  
  var app = new Vue({
    el: '#app',
    data: {
      input:'',
      textarea:'',
      necessary:'',
      select:'',
      options: [
        { text: 'One', value: 'A' },
        { text: 'Two', value: 'B' },
        { text: 'Three', value: 'C' }
      ],
      select2:'',
      options2: [],
      select3:'',
      options3: [],
      checkbox:'',
    },
    methods:{
      initOptions2: function(options2){
        this.options2 = options2;
      },
      initOptions3: function(options3){
        this.options3 = options3.filter(function(el,index){
          if (el.Availability == 'available') return true;
        });
      },
    },
    created: function(){
      google.script.run
        .withSuccessHandler(this.initOptions2).getData();
      google.script.run
        .withSuccessHandler(this.initOptions3).getData();
    },
  })
</script>

js.html

主な処理内容
- BootstrapのJSファイルを読み込むためのおまじない。

js.html
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>

css.html

主な処理内容
- BootstrapのCSSファイルを読み込むためのおまじない。

css.html
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">

customcss.html

主な処理内容
- Bootstrapのsampleに記載された個別のCSSを抽出。

customcss.html
<style>
  body {
    padding-top: 5rem;
  }
  .starter-template {
    padding: 3rem 1.5rem;
    text-align: center;
  }     
  .bd-placeholder-img {
    font-size: 1.125rem;
    text-anchor: middle;
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
  }
  @media (min-width: 768px) {
    .bd-placeholder-img-lg {
    font-size: 3.5rem;
     }
  }
</style>

参考

Bootstrap > Getting-started
Bootstrap > content > forms
Vue.js > インストール
Vue.js > フォーム入力バインディング
Apps Script > リファレンス

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

[ Google Analytics ] eコマース / 拡張eコマース実装

この記事はチームラボエンジニアリングの13日目の記事です。

本記事ではGoogle AnalyticsのEコマース/拡張Eコマース導入の流れを記録として書きます。
また、今回Google Tag Managerを使用したため、その設定方法も合わせてまとめます。

最初に

本記事の大まかな流れとしては、 以下の通りです。

  • 全体の構成
  • [概要] Google Analytics(GA)、Google Tag Manager(GTM)、Eコマース、拡張Eコマース
  • [実装] Eコマース、拡張Eコマース
  • [設定] Google Tag Manager
  • [振り返り]全体を通して気をつけたこと
  • 参考URL・参考書

今回使用した技術

Vue.js
Nuxt.js
JavaScript
TypeScript

使用している図は今回の実装に合わせて独自で作成したものであるため、webサイトによって仕様が異なると考えます。また管理画面はテスト環境で使用していたものを使用しています。

全体の構成

概要

Google Analytics

Q: Google Analyticsとは?

Googleが無料で提供するwebサイトのアクセス解析サービス。
サイトコンテンツへのアクセス状況がわかるので、このデータを元にサイトの改善に繋げるなど、主に集客・収益の増加のために導入される。

Q: どのように活用するか?

GAを使用すればCV(コンバージョン)に繋がる流入経路、どのページがCVに貢献しているのかを視覚的に確認することができる。データ項目が多いので、改善のために「今どんなデータを見るべきか、どんなデータが必要か」という目的を明確にすることが大切。
*コンバージョン:webサイト運営の目的(ECサイトであれば商品の販売、見込み客の獲得など)を達成すること・ユーザーが行う特定のアクション(商品の注文や資料請求など)を指す。

改善目的を達成するためにPDCAサイクルに当てはめる。
PLAN:計画    
サイト訪問者の動向を把握し(人気ページや問題のあるページなど)、改善計画を立てる
DO:実行   
計画した方法をやってみる(少しづつ)
CHECK:評価   
計画に沿って実行できていたかを、比較分析して評価。有効かどうか判断する。
ACTION:改善 
サイトを改善する

ユーザー満足に繋げることで集客・収益増加に繋げる

Q: どのようにデータを取得しているか?

ウェブビーコンと呼ばれるトラッキング方法でデータを集計。
ウェブビーコン型:ウェブビーコンと呼ばれるトラッキングコードをwebサイトのソースに埋め込み、ユーザーがページを開いたときにこのウェブビーコンが反応してトラッキング情報をアクセス解析用のデータベースに送信することでデータ集計を行う方法。

GAの画面の操作に関しては、Google公式の無料教材がわかりやすかったです。操作の仕方やレポートの作成方法などはデモを使って実際に手を動かし、目で動きを追いかけながら確認できます。今回GAの設定・使用方法については省略させていただくので是非使ってみてください。参考:https://support.google.com/analytics/answer/4553001?hl=ja 

Google Tag Manager

Q: Google Tag Managerとは?

Googleが提供している無料のタグマネジメントツール。
前述の通り、GAはウェブビーコンという方法でデータを取得している。そのため、計測したい項目用のイベント発火タグや、ECサイトであればEコマース用のタグ、他にも広告タグなどを利用するためにそれぞれ専用のタグをwebサイトの対象ページのHTMLに埋め込む必要がある。このタグの追加・削除のたびにコードを編集するのは手間がかかるため、GTMのオンライン管理画面上からタグを埋め込むページや、タグの種類を管理できるようにしたもの。

Q: どのように活用するか?

設定(タグ、トリガー):
GTMの管理画面上で「タグ」「トリガー」を設定し、イベントの発火タイミングを決定する。(GTMでは「どのタグ」を「いつ」起動させるかといった情報が設定をする上で必要。この「いつ」を操るのが「トリガー」。)

公開前のテスト:
タグはGTMの「プレビューモード」(デバッグモード)という機能を使って本番環境にタグを公開する前に動作確認をする。(GTMにログインしているユーザーのみブラウザ上で表示される。)
登録したタグが「発火している/発火していない」という状況を確認できる。

バージョン管理機能:
GTMで設定をする場合、ワークスペースの現在の状態を1つのバージョンとしていつでも保存できるため、必要に応じてワークスペースを元のバージョンに戻すことができる。ミスがあった場合などにワークスペースを以前のバージョンに戻して公開することができる。

Q: どのようにGAとwebサイト上の各イベントを繋げているのか?

*以下図でまとめてみました。

①GAにはトラッキングID(GAに登録する際、webサイト一つに対して必ず一つのプロパティが作成される。このプロパティの識別IDのこと)と呼ばれる「UA-000000-2」のような文字列がある。(GA管理画面>プロパティ設定>トラッキングIDで確認できる)
②GTMでタグを作成する際に、このトラッキングIDを入力する箇所がある(GTM上で変数化しておくことによって、トラッキングIDを一元管理することもできる。*タグを作成するたびに入力する手間が省ける)
③ユーザーが該当ページを読み込んだ際、あらかじめ設定していたトリーガーの箇所でタグが発火、紐付くトラッキングIDを元に、GA上にデータが反映される。

Eコマース

Q: Eコマースとは?

Eコマース:「購入完了時のデータ」購入に至った流入元、商品名、商品単価、売り上げなどの情報を追跡、分析することができる

Q: どのように活用するか?

Eコマースを設定することで、取得したデータを活かして以下のようにwebサイト改善の手がかりを見つけることができる。

どの商品が売れているかを推測できる
広告キャンペーンの効果(売上、数量など)を確かめられる
ユーザーが商品購入に至るまでにサイトを訪れた回数、かかった日数が分かる
収益予測を立てることができる
ECサイトの改善(購入手続きの改良、他社との比較機能の導入など)に役立つ

拡張Eコマース

Q: 拡張eコマースとは?
拡張Eコマース:「ページの閲覧やカート投入なども網羅したデータ」標準のeコマース機能に追加して、商品詳細ページの表示回数や、カート落ち等の購入前行動を追跡、分析することが可能。

Q: どのように活用するか?

拡張Eコマースを設定することで、Eコマース機能より詳細に取得したデータを活かして以下のようにwebサイト改善の手がかりを見つけることができる。
購入プロセスの可視化によるボトルネックの把握(購入プロセスの改善は売上に直結しやすい)
商品軸で詳細な販売状況がわかる(商品ごとのクーポンコードの利用や返品も計測)
商品別の解析レポートの拡張でブランドとバリエーションもカテゴリに追加で比較しやすくなった(Eコマースでは商品情報を、「商品名」「SKU(最小管理単位)」「カテゴリ」の3つのフィールドで計測)
商品の露出状況とパフォーマンスの把握が可能になった(商品が表示される箇所を商品リストとして登録し、どの切り口でユーザーが購入に至ったのかを商品リストごとに比較することができる)
サイト内プロモーションと特定の商品を紐付けた評価ができる(今回未設定)
返金が発生した場合に返金リクエストを送信でき、より正確なデータが取得可能(webサイト上に返金処理を行う実装がないため今回未設定)

ここからは実際に設定と実装の説明に入ります。

Google Analytics設定

①アカウントの作成  https://marketingplatform.google.com/intl/ja/about/analytics/
②トラッキングコードを確認(トラッキングIDをGTM設定の際に使用するので控えておく)
③管理画面>目的のビュー>eコマースの設定>eコマースの有効化>オン → 保存

Google Tag Manager設定

①アカウントの作成
②GTMのライブラリをインポート (今回nuxt.jsを使用したSPAの実装のため、ページビューをこのライブラリでよしなにGAに送信してもらいます。)
③nuxt.congig.tsに設定ファイルを記述

Eコマース設定 - Google Tag Manager

管理画面で作成していきます。

作成するのは以下のタグ2つ、変数3つ、トリガー2つ
*名称はそれぞれのwebサイトに合わせて決めてください

【タグ】

(1)SPAタグ: ユーザーが読み込んだSPAのwebサイト全てのページビューをGAに送る

タグ名: PageView_SPA_Tagなど
タグの種類: Google アナリティクス:ユニバーサルアナリティクス
トラッキングタイプ: ページビュー
Googleアナリティクス設定: {{GoogleAnalytics_setting}} *以下で設定したトラッキングIDの変数

*詳細設定>設定するフィールド>
フィールド名:page、値:{{SPA_pageUrl}} *以下で設定したpageUrlの変数
フィールド名:title、値:{{SPA_routeName}} *以下で設定したrouteNameの変数

トリガー:SPA表示トランザクション

(2)Eコマースコンバージョンタグ: Eコマースのデータで必要となるユーザーの注文完了データをGAに送る

タグ名: Setting_eコマース_Tagなど
タグの種類: Google アナリティクス:ユニバーサルアナリティクス
トラッキングタイプ: トランザクション
Googleアナリティクス設定: {{GoogleAnalytics_setting}}

トリガー:Eコマーストランザクション

【変数】

(1)トラッキングID
タグで一回一回設定するのは手間が省ける&ミスが起こる可能性があるため、変数で扱う。

変数名:GoogleAnalytics_settingなど
変数のタイプ:Google アナリティクス設定
トラッキングID: GAのトラッキングID
Cookieドメイン: auto

(2)SPA_pageUrl

変数のタイプ:データレイヤーの変数
データレイヤーの変数名: GAのトラッキングID
Cookieドメイン: auto

(3)SPA_routeName

変数のタイプ:データレイヤーの変数
データレイヤーの変数名: GAのトラッキングID
Cookieドメイン: auto

【トリガー】

(1)SPA表示トランザクション

トリガーのタイプ:カスタムイベント
イベント名:nuxtRoute
このトリガーの発生場所:すべてのカスタムイベント

(2)Eコマーストランザクション

トリガーのタイプ:カスタムイベント
イベント名:sendOrderInfo
このトリガーの発生場所:すべてのカスタムイベント

Eコマース実装

注文完了画面処理が走るタイミングでEコマースのデータを収集するトラッキングコードをソースに設置。

①今回の実装では注文商品データはlistで持っているため、mapで回して以下の形式で変数productsに一つひとつ商品情報を入れる。(このキーの種類と内容についてはGA公式Eコマースドキュメントに詳しく記載されています。拡張Eコマースとは実装内容が違います。)

      sku: 'SKUまたはアイテムコード', 
      name: '商品名', 
      price: '商品価格', 
      category: '商品カテゴリー', 
      quantity: '数量'

②transactionProductsにこのproductsを渡して、GAに一会計分のデータを送る。

this.$gtm.pushEvent({ 
       event: 'イベント名', 
       transactionId: 'トランザクションNo', 
       transactionAffiliation: 'ブランド名', 
       transactionTotal:  '合計金額',
       transactionShipping: '送料',
       transactionTax: '消費税',
       transactionProducts: products 
     }); 

拡張Eコマース設定 - Google Tag Manager

*Eコマースと異なり、拡張Eコマースは取得する項目が多いため、その分タグの数も多くなります。
まずは拡張Eコマースで以下のデータを取得するように設計。

  • 商品インプレッションの測定
  • 商品クリックを測定
  • 商品詳細の表示を測定
  • カートに商品追加
  • カートから商品削除
  • 決済を測定する(決済ステップを測定する)
  • トランザクション(購入を測定する)

<取得するデータの概要>
1.商品インプレッションの測定:そのページ一覧で表示されるどんな商品を閲覧したかを計測
(例:ユーザーが検索結果のリスト上で初めて対象商品を目にしたとして、その商品がなんだったのかを一覧で情報取得するイメージ)
2.商品クリックを測定:ユーザーが商品に興味を示して商品リストをクリックするイメージ
3.商品詳細の表示を測定:ユーザーが商品に興味を示して商品リストをクリックした後、商品詳細画面を開くイメージ
4.カートに商品追加
5.カートから商品削除
6.決済を測定する(決済ステップを測定する):拡張Eコマースでは決済行動というデータを見れるため、決済行動をどこから開始とみなしてどこで終えたとするかをそれぞれのwebサイトの仕様に合わせて考える
7.トランザクション(購入を測定する):注文完了画面に遷移時に計測(Eコマースと同じ発火場所のイメージ)

<設計・実装の流れ> *今回私が行った方法

①取得が必要な項目とそのデータが取れる画面・ボタンなどをリストにして洗い出す
②それぞれイベント名を決める(デバッグ用に)*テスト環境で実装した時にタグの発火をリアルタイムで確認するため
③タグの発火タイミングを決める
④上記の<取得するデータの概要>のまとまりごとに実装
⑤イベントの発火を一つずつ確認しながら全てdataLayerの形式で実装する

タグ数が多かったので、画像のように項目ごとにタグとトリガーをセットにしてフォルダ分けしてみました。

今回はタグとトリガーを1セットとして、該当項目のイベントの数だけ以下と同じ形式で作成する。

【タグ】

タグ名:わかりやすいようにimpなどの項目名を入れた
タグの種類: Google アナリティクス:ユニバーサルアナリティクス
トラッキングタイプ: イベント
カテゴリ: この項目はデバッグ用に指定(カート削除など)*リアルタイムで見たときに判別できるように
アクション: この項目はデバッグ用に指定(カート一覧など)*リアルタイムで見たときに判別できるように
Googleアナリティクス設定: {{GoogleAnalytics_setting}}*ここはEコマースの設定と同様
eコマース>拡張eコマース機能を有効にする:真
データレイヤーを使用する:チェックマーク

トリガー:該当するトリガー

【トリガー】

トリガー名:タグと合わせた名前にすることで見分けやすくした
トリガーのタイプ:カスタムイベント
イベント名:ソースの**event**と同じもの

<デバッグの方法>

1.ソース→GTM発火確認:(GTMのプレビューモードを使用)
2.GTM→GAの反映確認:(GAのデバッグプラグインを使用してデータが期待通りに送られているか。GA上にリアルタイムで反映されているか)
*GA>リアルタイム>イベント で確認
*GAのデバッグプラグインで見た時にundefinedでデータが送られているときは実装に不備がないか確認

拡張Eコマース実装

それぞれデータを取得する箇所に以下の形式で拡張Eコマース用タグを埋め込む。(Nuxt.js Google Tag Managerのプラグインを使用するため、その形式に則った書き方をしています。)

ショッピング行動についての計測

Eコマースと同様、商品情報をそれぞれproductに入れ、eventをトリガーにしてpushEventでGAにデータを送る。

[1.ショッピング行動] 商品インプレッション
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   impressions: { product }   ※複数商品データ
  }
});
[2.ショッピング行動] 商品クリック
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   click: {
      actionField: { list : '商品一覧ページ名' }, 
      products: product  ※単体商品データ
   }
  }
});
[3.ショッピング行動] 商品詳細表示
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   detail: {
      actionField: { list : '商品一覧ページ名' }, 
      products: product ※単体商品データ
   }
  }
});
[4.ショッピング行動] 商品カート追加
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   add: {
      products: product ※単体商品データ
   }
  }
});
[4.ショッピング行動] 商品カート削除
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   remove: {
      products: product ※単体商品データ
   }
  }
});

決済行動についての計測

[決済行動] step1 ~ step3 (stepはそれぞれのwebサイトの仕様に合わせて)
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   checkout: { 
      actionField: { step:1 },
      products: product ※単体商品データ
   }
  }
});

下記のようにoptionをつけるとクレジットカード情報を送ることもできる

this.$gtm.pushEvent({ 
 event: 'イベント名', 
  ecommerce: { 
   checkout: { 
    actionField: { step: 2, option: ' visa ' }, 
    producs: product ※単体商品データ
   }
  }
});

ステップ設定時に気をつけること:
ソースのstepにはnumberを渡し、
GA上では管理>ビュー>eコマースの設定>目標到達のプロセスで設定したいnumberの順に合わせて表示するタイトルをつける。何もつけないとデフォルトで画像のようにステップ1,ステップ2のような表記になる。

最後にショッピング行動についての計測(再) ※これでラストです!

[5.ショッピング行動] 商品を購入
this.$gtm.pushEvent({ 
 event: 'イベント名',
  ecommerce: {
   purchase: {
      Id: '商品ID',
      affiliation: 'ブランド名',
      revenue: '合計金額',
      shipping: '配送料',
      tax: '消費税(内税)',
      coupon: 'クーポン名',
      products: '商品データ'
   }
  }
});

振り返り

長くなってしまいましたが、SPAのEコマース/拡張Eコマース実装でした!
必要なデータがフロントで取れてるか、どこでイベントを発火させるのかなど、設計の段階が大切だと思いました。タグの数が多い分、一つひとつデバッグしながら確実に進めるのが良いと感じました。今回は大まかな流れについて書きましたが、細かい部分についてまとめたいのでまた記事作成します。

参考URL 参考書

参考URL

成果を上げるPDCAサイクル(1)Googleアナリティクス編 (PDCAサイクルなどわかりやすい)
https://webbu.jp/pdca-ga-4004
Googleタグマネージャの基礎知識~導入からGoogleアナリティクスとの連携まで~
https://nandemo-nobiru.com/ad-5758
Googleアナリティクス登録・設定手順【これさえ読めばOK!】
https://wacul-ai.com/blog/access-analysis/google-analytics-setting/ga-register/
Googleアナリティクスのeコマース機能を利用する方法
https://wacul-ai.com/blog/access-analysis/google-analytics-setting/e-commerce/
Google アナリティクスで強化された「拡張eコマース」機能がサクッとわかる6つの特徴
参考URL: https://webtan.impress.co.jp/e/2014/08/08/17970
Nuxt.js GoogleTagManagerライブラリ
https://github.com/nuxt-community/modules/tree/master/packages/google-tag-manager
GTM公式サイト
https://tagmanager.google.com/
拡張eコマース概要
https://developers.google.com/analytics/devguides/collection/gtagjs/enhanced-ecommerce
拡張eコマース実装ドキュメント(デベロッパー向け)
https://developers.google.com/tag-manager/enhanced-ecommerce?hl=ja#product-clicks
拡張eコマース実装参考URL (※デバッグツール開くと、操作時にGAに送られているデータの形式を確認できる)
https://shop.googlemerchandisestore.com/

参考書

徹底活用 Googleアナリティクス
Googleアナリティクスの優しい教科書。
わかばちゃんと学ぶGoogleアナリティクス
ウェブ解析士2019認定試験公式テキスト

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

Vueを使ってサイトを1ヶ月で作った話。そして、それを、1ヶ月使ってリプレイスした話。

この記事はAteam Hikkoshi Samurai Inc. & Ateam Connect Inc.(エイチーム引越し侍、エイチームコネクト) Advent Calendar 2019 9日目の記事になります。

はじめに

「引越し侍 まるごとスイッチ」
https://hikkoshizamurai.jp/switch/

というサイトを12月2日にリリースしました。
引越しの際に必要な、電気やガスの切り替えが一括で行えるサイトで、
ゆくゆくは水道や金融機関、郵便など、引越しにまつわる、様々な手続きをサポートしていく予定です。

この記事は、このサイトをVueを使って1ヶ月で作った話。
そして、それを1ヶ月使ってリプレイスをした話をします。

簡単でテンポよく開発できるVue.jsNuxt.jsを設計せずに使うとどんなことが起こるのか、
どうすれば、Vue.jsNuxt.jsの良さを活かしつつ、開発ができるのか
是非見ていってください!

TL;DR

  • Vue.jsNuxt.jsは簡単に使える。
  • が、、故に、きちんとやらないと破綻する。
  • どんなに急いでいても、コンポーネント設計やVuexの設計、準備はきちんとしようねという話

背景

「引越し侍 まるごとスイッチ」
https://hikkoshizamurai.jp/switch/

実はこのサイト、
内閣官房が推進している引越しワンストップサービスというものの協力主体として、
弊社が参画し、作ったサービスになります。

制作に与えられた期限は約2ヶ月半。
(最初の半月はDB設計等をしていたので、フロントに与えられたのは実質2ヶ月)
限られたリソースの中で、仕様を詰めながら、
どんなサイトにするかを考え、作る。(もちろんバックエンドも、管理画面も)

その中で僕たち制作メンバーが選んだフロント技術はVue.jsNuxt.jsです。

Vue.jsの良さは、簡単で柔軟性の高いというところ
Nuxt.jsの良さは、開発スピードが出せるという点です。

ですが、その良さの反面、きちんとした設計をしないと、すぐに破綻する時がやって来ます。

この記事では、Vue.jsNuxt.jsを使って1ヶ月でβ版を作った話。
そして、それを1ヶ月使ってリプレイスした話をします。

最初の1ヶ月で何があって、残りの1ヶ月で何をしたのか、是非、見ていってください。

最初の1ヶ月

最初の1ヶ月、我々制作チームが奮闘したのは、とにかく早くリリースをするという点でした。
いわゆるβリリースのようなものが1ヶ月後にあったのです。

そうです。時間がなかったのです。。
みんなで決めて、とりあえず開発速度を意識した実装をすることにしました。
そして、(来ることは知ってたけど、思ったより早く)訪れたこと

  • コピペ地獄
  • どこか修正するとどこかがバグる
  • Vuexに入れていたはずのデータがどこかで書き換わってる
  • 書き方が統一されていない
  • 変わり続ける仕様に対応できない

開発しはじめて、1ヶ月しか経っていないにも関わらず、こんなことが起きていました。

なぜこんなことになったのか?

コンポーネント設計をしなかった

これが、最大のミスです。
弊社では、他のプロジェクトでもVue.jsNuxt.jsを使った経験があり、
その時はしていたはずのコンポーネント設計を怠りました。
これにより、コピペの嵐、何か仕様変更があると、全てを書き換える必要がありました。
当然、どこにバグを埋め込んでしまったのか検討もつかない状態になりました。

Vuexを乱用しすぎた

何でもかんでもVuexに入れているプロジェクトをよく見ますが、
Vuexの乱用は完全なアンチパターンだと思っています。
思っていたのにやったのです。。

すると、あるページで変更した内容が他のページに来た時にも反映されていたり、
値がなくて落ちたりするのです。。

Linterを無視した

途中で流石にやばいと思って、コマンドを打って見たんです。

yarn lint
...
3043 problems

おわた :angel:

yarn lint --fix

をする勇気もなく、したところで解消できる数ではなさそうなので、そっとしておきました
こうして、書き方がバラバラのプロジェクトができあがりました。

残りの1ヶ月

なんとか一時的にリリースを果たしましたが、、

このままでは、ダメだ

このまま行くと、必ず破綻する。
リリース後、誰も触りたくないプロジェクトになってしまう。

仮のCSSだったので、本リリースまでの間にCSSをフルチェンジする必要があった我々は、
このタイミングで、きちんと話をして、コンポーネント設計やVuexの設計をする時間を設けました。

以下は、残りの1ヶ月でやったことです。

コンポーネント設計をした

他のプロジェクト同様、Atomic Designを採用しました。
きちんとチケットにどの粒度でコンポーネントを切るか明記し、
迷った時は議論しながら進めました。

atoms/ molecules/ organisms/でディレクトリを切って、
どこに何のコンポーネントを入れるか話し合いながら決めました。

もしコンポーネントの切り方に興味のある方は、以下のURLで
簡単なタスクリストを作るアプリケーションを作って見たので、
実際に手を動かすとなった時に、参考にしていただけると幸いです。

https://codesandbox.io/embed/atomic-sample-137cy?fontsize=14&hidenavigation=1&theme=dark

Vuexを極力使わないようにした

改めて言います。
Vuexの乱用はアンチパターンです。
きちんとその性質を理解して使うようにしてください。

Vuexは便利な一方、どこからでも値の参照や格納ができてしまうグローバル変数です。
使い方を誤れば、どこかのページで格納したデータがどこかのページに来た時に反映されていたり、
書き換わってしまっていてバグが生まれます。

そして、我々のチームで、定めたルールが以下の通りです。

1. ページをまたいで、データを保持したい時以外は使わない
ページをまたがないデータ保持は、一番上の親にデータを持たせて、
v-modelv-bindv-onを使えば解決します

2. ページをまたいで必要な時も、値の格納は一回しかしない
一つのモジュールで値の格納があるのは一回で十分だと思います。

3. ページをまたいで必要な時も、<nuxt-child>で代用できないか考える
nuxt-childNuxt.jsのページ単位で親子の関係を作ることができます。
1とほぼ同様で、親にデータを持たせたいが、ページ間を超えてデータが必要になる際に使います。

4. Atomic Designの生体(organisms)以上の単位でしか参照や格納はしない
原子(atom)、分子(molecules)の単位でVuexにアクセスしてしまうと、
汎用性のないコンポーネントになってしまいます。

ESLintやPrettierの設定をした

書き方の統一のため、ESlintとPrettierを導入しました。
VSCodeで自動フォーマットし、人によるばらつきが出ないようにしました。

これにより生まれたこと

バグが少なく、スピードのある開発ができるようになった。

最初にコンポーネント設計や、Vuexの設計をしたことで、
この場合はどうする?、ここはこれで行こうと思うけど、いいかなーなど
今までになかったコミュニケーションが生まれました。
結果として、バグが入りにくく、スピードを加速させることができました。

変更や新しいデザインに強くなった

コンポーネント設計をし、slotを用いた柔軟性の高いコンポーネントが作れたことにより、
新しいデザインが来た時も、同じコンポーネントを使い回せるようになった。
結果として、変化し続けるサービスのパフォーマンスを最大にすることができた。

うまく、Vuexと付き合えるようになった。(サイトの表示速度も上がった)

あらゆるところから参照、値の格納をしていたVuexですが、
うまく、サイト全体で必要な情報だけ取得して来て、レンダリングすることで、
サイトの表示速度も上がりました。

レビューの速度が上がった

小さな単位でレビューをすることが容易になったこと、
複雑な処理が分割されたこと、
Linterなどの導入により、書き方が統一されたことにより、
レビュー速度も上がりました。

最後に2ヶ月を振り返って

最初「時間がないから、設計や設定をせずに行こう」という話をしましたが、
終わって見て振り返ると「時間がないからこそ、設計や設定を最初にきちんとしよう
というのが正解だと思いました。

実際、後半の1ヶ月の方が開発スピードは速く、バグにすぐさま対応できるようになっていたと思います。

そして、何より今回、サイトの書き換えをしたにも関わらず、スピードをあげて開発ができた背景には、
メンバー間のコミュニケーションがきちんと取れていて、
素直に改善をすることができるメンバーに恵まれたという点にあると思います。

皆さんもVue.jsNuxt.jsで何かを作る際は、設計やルール、設定をきちんとみんなで話し合い
スタートすることをお勧めします。

ありがとうございました!

お知らせ

エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひともエイチームグループ採用ページよりご応募ください!
Webエンジニア詳細ページ)よりお問い合わせ下さい。

フロントエンンドエンジニアも募集しております
https://www.a-tm.co.jp/recruit/requirements/career/lifestylesupport-frontenddesigner/

明日

いかがでしたでしょうか?
明日のエイチーム引越し侍、エイチーム コネクトアドベントカレンダーの担当は、
期待の新卒チーズ@cheez921です!

是非、明日も見て下さい!

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

Vue.jsで開発をはじめよう

選んだ教材はこれ

超Vue JS 2 入門 完全パック - もう他の教材は買わなくてOK! (Vue Router, Vuex含む)

https://www.udemy.com/course/vue-js-complete-guide/

選んだ理由としては、
・評価が高かったこと
・必要条件が自分に合っていたこと
・タイトル見て、この教材1つでいいんだ〜って思ったから
以上の3点です。

なぜVue.jsを学習しようと思ったのか

Railsの学習が一段落したところで、フロントエンドのフレームワークにも挑戦したいと感じていました。
フロントの開発は目に見えて反映されるので、僕自身やっていて楽しかったです。

その中で最近流行り?で比較的入りやすそうなVue.jsに決めました。
正直フレームワークに関しては、ReactでもAngularでも良かったと思います。
サクッと決断して、サクッと取り組み始めることが大事だと思っています。



Vue.jsでアプリ作って公開するでぇ〜!!
お楽しみに!!



ではまた!

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

【Vue.js+firebase】アプリ開発未経験者がTechTrain MISSIONに挑戦したので振り返る

こんな人に読んでほしい(検索で来た人向け)

・TechTrainのMISSIONやメンター制度について知りたい人

・アプリ開発初心者がVue.js+firebaseを勉強した流れについて知りたい人

・Progateやドットインストールなど、プログラミング未経験者向けの教材を一通りこなしたが、次に何をしたらいいか迷っている人

1. はじめに

こんにちは!辻野です。

今回はTechbowlさんが展開するオンラインインターンシップ「TechTrain」のうち、サイボウズさんが提供するMISSION『就活に便利!会社情報をみんなで共有しよう』に挑戦してみました。

現在まだ挑戦中ではありますが、一区切りとして本MISSIONへの取り組みを記事にしたいと思います(githubリポジトリはこちら)。

私自身について簡単に自己紹介をしますと、私は現在IT業界とは全く関係のない日系企業で正社員として働いている社会人です。

その傍ら、大学時代のプログラミング経験と、社会人以降の独学で得た知識をベースにして、副業として某ITベンチャーからデータ分析業務(言語はpython)に取り組んでいます。

ITエンジニアを自称していいのかどうか、すごく微妙な立ち位置にいる感じです(気になった方は是非、私のブログを読んでみてください)。

さて、今回私がTechTrainに参加しようと思った理由は、シンプルに「アプリ開発を経験してみたかったから」です。

そもそも私にとってはアプリ開発経験自体、本MISSIONが初めてでした。

参加時点では、Progateやドットインストール、Udemyで基本的なWeb系言語に触れたことはあるものの、なんらかの成果物を自力で作ったことはない、という段階にいました。

TechTrainは、

・大まかな成果物完成までの道筋を提示してくれる
・プロのエンジニアとコミュニケーションを取れるメンター制度がある
・学生に限らず、U30あれば社会人でも参加できる
オンラインで完結でき、かつ自分のペースで進められるため、本業を抱えている社会人でも気軽に利用できる
なのになぜか無料

などの理由から、自分にピッタリのサービスだと思い、参加させていただくことにしました

そんな私のMISSIONの振り返りが、誰かの役に立てばと思います(アドベントカレンダーという文化自体、今回初めて触れるのですが、こういう内容でいいのかな・・・?)

2.雛形を作りながら基礎学習

私が参加したサイボウズさんのMISSIONについて軽くおさらいすると、「就活生同士で会社情報を共有できるアプリを作成する」ことを目的としたものになります。

本MISSIONは、以下の9つのSTEPから構成されています。

STEP1. UIと画面遷移を設計しよう
STEP2. 対象プラットフォームや使用する言語を決めよう
STEP3. Firebaseを使えるようにしよう
STEP4. Cloud Firestoreに会社情報のサンプルデータを登録して画面に表示しよう
STEP5. 会社情報の一覧画面から詳細画面に遷移しよう
STEP6. Firebase Authenticationを用いてGoogleアカウントでサインアップ、ログイン、ログアウトしよう
STEP7. 会社情報を登録しよう
STEP8. 会社情報に対してコメントを投稿しよう
(STEP9. オプション機能にチャレンジしよう)

STEP8まで必須機能、STEP9は努力目標、といった感じです。

加えて、フロントエンドの言語(フレームワーク)選択は自由、サーバー側の処理はfirebaseを利用すること、となっておりました。

今回私は、Javascritは少し触れたことがあった経験から、Vue.jsをフロントエンドフレームワークとして選定しました。

もちろん、Javascritを少し触れたことがあるとはいってもほんの少しですし、何よりVue.jsに関しては完全に初心者です。そのため、まずは基礎の勉強から始める必要がありました。

ただ今回は、教科書的なものから一歩ずつ進めるやり方ではなく、「コピペでもなんでもいいから、とにかく動くもの作る」ことを優先させました

理由としては、後ほどメンター制度を利用することを考えた際「知識はそこそこ身につけましたが、まだ何も作っていません」という状態よりも「中身はよくわかってませんが、とりあえずこんなもの作ってみました」という状態の方が、目的物が具体化されている分、相談や質問がしやすくなるかな?と考えたからです。

(今振り返っても、この判断は正しかったように思います)

成果物の雛形作りにあたり、

・Vue+firebaseのアプリ開発について、初心者向けの解説がなされている
・完成品が本MISSIONの内容にかなり合致している

という理由から、こちらのnoteを参考にしました。

Vue.jsとFirebaseで、noteライクなSNSアプリを5時間で作ろう!

このnote記事は、

・Vue-cliによる開発環境構築とVue-cliの大まかな仕組み
・Firebaseの初期設定
・Googleアカウントでのサインアップ、ログイン、ログアウト機能の実装
・ユーザー投稿機能の実装
・Hostingの方法

など、今回のMISSIONで必要とされる知識を一通り学習できる教材でした。

後半部分の閲覧するためには¥500が必要ですが、「Vue.js+firebaseでとりあえず動くものを作りたい」という需要には答えてくれる記事でしたので、¥500分の価値はあったかなと思います。

コピペ箇所を間違えるなどの初歩的なミスにつまづきながらも、まずは無事に成果物の雛形が完成しました。この時点で ”成果物上は” 本MISSIONで示すところのSTEP7まで進めたことになります。

もちろん、かなりの部分をコピペで進めたため、中身を十分に理解できていたわけではありません。それでも、「Vueって機能が簡単に変えられて楽しいなー」「firebaseを使うとログイン機能がすごい簡単に実装できるなー」という、アプリ開発の全体像をざっくりと把握することはできました。

3.コメント機能実装でSTEP8までクリア

さて、STEP8のコメント機能さえ実装すれば必須機能は完成、という段階まで来れました。

しかし、Vueやfirebaseについての基礎知識が曖昧な状態では、先のnote記事には含まれない”新しい機能”の追加は、自力ではできそうにありませんでした(特にVue.js)

そのため、ここで初めてVue.jsの体系的な学習を始めることにしました。何らかの書籍を教科書として決めても良かったのですが、今回はこちらを使用しました。

Vue.js + Firebaseで作るシングルページアプリケーション
Vue JS 入門決定版!jQuery を使わない Web 開発 - 導入からアプリケーション開発まで体系的に動画で学ぶ

Udemyで評価の高かった、Vue.jsの講座です。

定価は1~2万円くらいする動画教材なのですが、私が買ったときは偶々セール中で90%OFF(すごい)で売られていました。試しに二つ購入してみましたが、どちらもとても分かりやすい内容だったため、Vue初学者の方はどちらを買ってもいいかと思います。

上記のUdemy教材で一からVueを学び、自身の成果物の仕組みについてそれなりに理解ができた時点で、真の意味でSTEP7までクリアできたかな、と感じました。

その後、コメント機能の実装には Firebase Cloud Firestore の「サブコレクション」の概念についても理解する必要がありましたが、その際には下記のqiitaの記事が参考になりました。同じところでつまづいている人は読んでみてください。

Firebase Cloud Firestoreの使い方

ここまでで、STEP8、すなわち「必須の機能」と定義しているものまでは完成させることができました。

4.メンター制度を利用

さて、ここからメンター制度の話です。最低限のMISSION課題はクリアしていましたが、よりアプリの完成度を高めるために、メンター制度を利用することにしました。

これまでは「○○の機能を追加するためにはどうしたらよいか」という、答えが明確に存在する課題について取り組んできた印象がありました。そのため、独学でも十分に進めることができました。

しかしながら、

・どんな機能が追加であったら便利か
・良いUI/UXデザインとは何か

といったような、必ずしも答えが一様に定まらないオープンなクエスチョンについては、最前線の現場で経験を積んできたプロのエンジニアの方に直接お聞きする方がいいかな、と思いました

正直、最初はzoomの使い方すら怪しい状態だったので、初利用は緊張しました笑。主にWebフロントを担当しているメンター何名かにお話を伺わせてもらったのですが、全体的な印象として、

「メンターみんな知識量ヤバすぎ!」

でした(もちろん良い意味で)

メンター制度は、1回30分の制限があります。限られた時間を有効に活用するためにも、「ざっくり見てもらって何か感想ください」というふわっとした相談ではなく「○○について困っているのですが、どうしたらいいですか」という、答えが明確に返ってきそうな質問を事前に用意しておくことを心がけていました

しかし、実際にメンターの方とお話をすると、こちらが用意した質問以上の情報や知見を得ることができました。1質問すると10返ってくるイメージです。

こちらのQに対してAを返すだけでなく「この周辺の知識が必要な時は、このサイトを参考にした方がいい」など、私が後々の勉強がしやすくなるようなサポートまでしていただきました。

それに加え、「実際の開発の現場ではこのような進め方をする」など、現役のエンジニアだからこそ語れる内容についても、お聞きすることができました。

魚を与えるのではなく、魚の釣り方を教える、という態度を徹底しているメンターが多いと感じました。

同時に「メンター制度、もっと早くから使っておけばよかったかも」とも思いました。

今回私にとって、本MISSIONが初めてのアプリ開発でした。「何もかも分からないので一から教えてください!」という態度は、さすがに指導を受ける側としては不適切だと思い「まずは一人できるところまではやってみよう」という思いで、MISSIONを進めてきました。

もちろん、その考え自体が間違っていたとは思いません。「メンターの方の仰っている言葉の意味すら分からない」という状態には陥らなかったのは、基礎の基礎を独学で固めたおかげだったとは思います。

ただ、もう少しフランクに利用してみてもよかったかもしれません。自分一人で教材片手に勉強するのとは比べものにならないスピードで知識を吸収できると感じました

どのメンターの皆さん気さくに相談に乗ってくださった上、こちらがうまく言葉にできない疑問についても、先回りして答えをくれる印象でした(多分、初心者がつまづく箇所はだいたい同じなので、こちらが悩んでいることを経験的に読めるのかもしれません)

なぜこのサービスが無料なの?と思えるくらいです。

失礼な言い方にはなりますが、「やっぱプロのエンジニアは違う」という、至極当たり前の事実を、改めて認識した次第です。

私のようなガチガチの初心者に限らず、「歴の浅い経験者」であっても、得るものは多いサービスかと思います。

5.最後に

以上、MISSION振り返りでした。

まだまだMISSIONの完成度アップは続けていく予定です。今後はメンター制度ももっと積極的に活用していく所存です。

この記事を機に、一人でもプログラミング初学者がTechTrainに参加してくれるのであれば、投稿者冥利につきます。

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

Vue-Routerの異なる呼び出し方のメモ

その①

vueのコンポーネントをimportしてからルーティングを指定する

import Home from './components/Home.vue';

export const routes = [
     {
         path: '/',
         component: Home,
     },
];

その②

component: 以降にrequireをつけ、componentのパスまで指定して書く

export const routes = [
    {
      path: '/home',
      component: require('./components/Home.vue'),
      name: 'home'
    }
];

SPAを構築するには、
シングルページとして登録するvueファイルを呼び出しroutesという変数に入れてexportする必要があるよということですね!

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

AWS S3更新時にLambdaでCloudFrontのInvalidationを自動実行

Vue.js で作ったウェブアプリは、静的サイトとして AWS S3 + CloudFront からホスティングできます。

非常にコスパのいいオススメの設計なのですが、S3 内の本番ファイルを更新した場合、CloudFront のエッジサーバ上に残っているキャッシュのせいで、その更新は即座に反映されません。

CloudFront のコンソールから「Invalidation」を手動で実行すればそのキャッシュを削除できます。

今回は、その Invalidation 手動実行の部分を AWS Lambda を使って自動化するやり方を整理します。

前提

静的サイト(特に Vue.js)を S3 + CloudFront でホストしているケースを前提としています。

Lambda を使った Invalidation の自動化手順

IAM ロールの作成

まず Lambda 関数に適用する IAM ロールの準備です。

IAM コンソールから新しいロールを作成します。ロールを使用する権限は「Lambda」、権限内容は以下の JSON で指定します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
        "Effect": "Allow",
        "Action": [
            "cloudfront:CreateInvalidation"
        ],
        "Resource": [
            "*"
        ]
    }
  ]
}

Automatic Cloudfront invalidation with Amazon Lambda」より引用

ロール名は「lambda_cloud_front_invalidation」など分かりやすい適当な名前にしましょう。

Lambda 関数の作成

続いて Lambda 関数の作成です。

関数の作成

Lambda コンソールから新しい関数を作成します。今回、ランタイムで使用する言語は「Python 2.7」、実行ロールは既存のロールから「先ほど作成した IAM ロール」を指定します。

トリガーの追加

関数のトリガーに静的サイトをホストしている S3 を追加します。イベントタイプは「すべてのオブジェクト作成イベント」を選択します。

関数コードの設定

デフォルトの設定されている関数コード lambda_function.py を、以下の内容で更新します。

lambda_function.py
from __future__ import print_function

import boto3
import time

def lambda_handler(event, context):
    path = "/dist"
    for items in event["Records"]:
        if path in items["s3"]["object"]["key"]:
            client = boto3.client('cloudfront')
            invalidation = client.create_invalidation(DistributionId='YOUR_DISTRIBUTION_ID',
                InvalidationBatch={
                    'Paths': {
                        'Quantity': 1,
                        'Items': [path + '/*']
                },
                'CallerReference': str(time.time())
            })
            break

コード内容は、S3 の /dist ディレクトリ内(Vue.js のビルドしたコード)の更新を検知して CloudFront の Invalidation を走らせるというものです。

YOUR_DISTRIBUTION_ID の部分は、ご自身の の Distribution ID に置き換えてください。

稼働テスト

Lambda 関数が意図しているように動くかテストします。

S3 の対象ディレクトリ(今回は /dist)を更新し、Lambda コンソールの「モニタリング> Error count and Success rate」で処理が成功したか確認できます。

スクリーンショット 2019-12-07 17.43.17.png

さらに新しくブラウザを開いて、変更内容が反映されていればOKです。

請求アラーム設定もお忘れなく

意図しない高額請求が発生しないように CloudWatch のコンソール にて、請求アラームを設定しておきましょう。

Reference

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

AWS Amplify の ServiceWorker で Web Push 対応を実装してみる - クライアント編

Web Push、送りたいことありますよね。

この記事は Web Push 通知を使うアプリを AWS Amplify で作ってみる話です。
特にクライアント側を AWS Amplify でどう実装するかを見てみます。
当初サーバー側もあわせて書こうとしたのですが、分量が多くなりそうだったので今回は一旦クライアント編として、サーバー編はまた Amplify の API カテゴリを絡めたものを後日書きます(たぶん)。

Web Push やりたいときにやらなきゃいけないこと

Web Push を運用しようとすると、けっこう色々やることがあります。

  1. Push を受け取るクライアント側
    1. Service Worker の実装
    2. 購読状態管理、Service Worker 登録等を行うフロントアプリケーションの実装
    3. 購読処理時に払い出されるエンドポイントをサーバーサイドに送信
  2. Push を送るサーバー側
    1. 秘密鍵・公開鍵の生成、管理
    2. クライアントから送られてきたエンドポイント情報をサービスのユーザーと紐付けて保存、管理
    3. Web Push 送信用ライブラリ を使って適切なタイミングで送信

等々です。Amplify の ServiceWorker クラスはこの中で、特に 1-2. 購読状態管理、Service Worker 登録等を行うフロントアプリケーションの実装 を助けてくれます。
また、その Amplify プロジェクトで Analytics カテゴリを有効にしている場合、ServiceWorker のライフサイクルイベントを自動で収集、可視化してくれる機能も Amplify が提供してくれます。

前提条件

仕様

今回は、

  • ブロック済でなければページを開いたときにプッシュ通知購読の許可を求める
  • 購読の停止、停止後の再購読を行う UI、機能は提供しない ※
  • 現在の購読状況(未購読、購読済み、ブロック済み)を表示する
  • Push 通知がサポートされていない環境(Safari 等)ではそう表示する
  • 購読済みの場合は、プッシュ通知に必要なサブスクリプション情報を表示する

というモノを実装することにします。

※ 実際のサービス運用時には、購読停止の際に Unsubscribe() の実行、サーバーサイドのデータベース更新等が必要になるでしょう

参考にした資料

この記事は以下2つのドキュメント・チュートリアルを参考にしています。

  1. Service Workers - Amplify JavaScript
  2. ウェブアプリへのプッシュ通知の追加 - developers.google.com

この記事内では AWS Amplify が関わる Web Push の登録・受信部分のみに触れますが、実際のアプリケーションではユーザーの体験を考える上でどんなときにどう購読 ON/OFF の UI を表示すべきかなど、もう少し考えることがあります。
上の 2. のチュートリアルはそういう面にも触れられているのでぜひご覧ください。

作業環境

今回は Vue project をベースにします。


$ node -v
v12.13.1
$ npm -v
6.9.0
$ npm install -g @vue/cli
...
$ vue --version
@vue/cli 4.1.1
$ npm install -g @aws-amplify/cli
...
$ amplify --version
4.5.0

使った環境、ブラウザは↓です。なお、Safari は現在 Push 通知に対応していません。
* macOS High Sierra 10.13.6
* Google Chrome ver.78
* Firefox ver.68.3

実装していき

Vue プロジェクト、Amplify プロジェクトの作成

$ vue create amplifywebpush
? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint)

...

$ cd amplifywebpush
$ npm install aws-amplify aws-amplify-vue
$ amplify init
? Enter a name for the project amplifywebpush
? Enter a name for the environment dev
? Choose your default editor: Vim (via Terminal, Mac OS only)
? Choose the type of app that you're building javascript
Please tell us about your project
? What javascript framework are you using vue
? Source Directory Path:  src
? Distribution Directory Path: dist
? Build Command:  npm run-script build
? Start Command: npm run-script serve
? Do you want to use an AWS profile? Yes
? Please choose the profile you want to use yourprofilename
...

$ tree -L 1
.
├── README.md
├── amplify
├── babel.config.js
├── node_modules
├── package-lock.json
├── package.json
├── public
├── src
└── yarn.lock

4 directories, 5 files

はい

必要な Amplify のモジュール、カテゴリの追加

まず、ServiceWorker クラスは @aws-amplify/core パッケージに、Analytics は aws-amplify/analytics に入っているのでインストールします。

$ npm install @aws-amplify/core
$ npm install @aws-amplify/analytics

次に、Service Worker のライフタイムイベントを集計するため、アプリケーションに Analytics カテゴリを追加します。途中の選択肢は全てデフォルトで Enter を押しています。

$ amplify add analytics
$ amplify push
✔ Successfully pulled backend environment dev from the cloud.

Current Environment: dev

| Category  | Resource name   | Operation | Provider plugin   |
| --------- | --------------- | --------- | ----------------- |
| Auth      | cognito57ed5053 | Create    | awscloudformation |
| Analytics | amplifywebpush  | Create    | awscloudformation |
? Are you sure you want to continue? (Y/n) Yes

...

$ amplify status

src/main.js で Amplify を初期化します。

import Vue from 'vue';
import App from './App.vue';
import awsconfig from './aws-exports';
import Amplify, * as AmplifyModules from 'aws-amplify';
import { AmplifyPlugin } from 'aws-amplify-vue';

Amplify.configure(awsconfig);
Vue.use(AmplifyPlugin, AmplifyModules);
Vue.config.productionTip = false;

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

この時点では何も UI をいじっていないので、デフォルトの Vue プロジェクトの画面が表示されます。

$ amplify run

...

  App running at:
  - Local:   http://localhost:8080/
  - Network: http://192.168.0.1:8080/

  Note that the development build is not optimized.
  To create a production build, run yarn build.

vue.png

はい

ServiceWorker の実装

まず、public/service-worker.js を次のように実装してみます。

/**
 * Push 通知の受信時に発火するイベント
 */
addEventListener('push', (event) => {
  console.log('[Service Worker] Push Received.');
  console.log(`[Service Worker] Push had this data: "${event.data.text()}"`);

  if (!(self.Notification && self.Notification.permission === 'granted'))
    return;

  let data = event.data ? event.data.json() : {};

  let title = data.title || "Web Push Notification";
  let message = data.message || "New Push Notification Received";
  let icon = "path/to/image";
  let badge = "path/to/image";
  let options = {
    body: message,
    icon: icon,
    badge: badge
  };
  event.waitUntil(self.registration.showNotification(title, options));
});

/**
 * 通知がクリックされたときに発火するイベント
 * 通知ごとに適切なクリック時の処理を記述。
 * 今回は Amplify JavaScript のドキュメントを開くという処理にする。
 */
addEventListener('notificationclick', (event) => {
  console.log('[Service Worker] Notification click: ', event);
  event.notification.close();
  event.waitUntil(
    clients.openWindow('https://aws-amplify.github.io/amplify-js')
  );
});

これはかなり最小限の実装ですが、色々な API やキャッシュ機能を使った例としては Example Service Worker - Amplify JavaScriptウェブアプリへのプッシュ通知の追加 - developers.google.com を参照するといいと思います。

ServiceWorker の登録

で、src/App.vue を次のように実装します。

<template>
  <div>
    <h2>Amplifyで作るWebPushアプリ</h2>
    <p>{{ state }}</p>
    <p>{{ endpointInfo }}</p>
  </div>
</template>

<script>
import { ServiceWorker } from 'aws-amplify';

const serviceWorker = new ServiceWorker();
const yourPublicKey = 'Paste your public key here';

export default {
  name: 'app',
  data(){
    return {
      registeredServiceWorker: null,
      state: '',
      endpointInfo: '',
    }
  },
  methods :{
    isPushSupported() {
      return ('serviceWorker' in navigator && 'PushManager' in window)
    },
    async updateUI() {
      if (!this.isPushSupported()) {
        this.state = 'Push 通知がサポートされていない環境';
        this.endpointInfo = '';
        return;
      }

      // 購読状況によって UI を変える
      if (Notification.permission == 'denied') {
        // すでに拒否されている
        this.state = 'ブロック済';
        this.endpointInfo = '';
      } else {
        var subscription = await this.registeredServiceWorker.pushManager.getSubscription();
        if (subscription) {
          // 購読済み
          this.state = '購読済';
          this.endpointInfo = JSON.stringify(subscription);
        } else {
          // 未購読
          this.state = '未購読';
          this.endpointInfo = '';
        }
      }
    },
  },
  async mounted(){
    this.registeredServiceWorker = await serviceWorker.register('/service-worker.js', '/');

    if ('permissions' in navigator) {
      let notificationPermission = await navigator.permissions.query({name:'notifications'});
      notificationPermission.onchange = () => {
        this.updateUI();
      };
    }

    if (Notification.permission !== 'denied') {
      await serviceWorker.enablePush(yourPublicKey);
    }
    this.updateUI();
  },
}
</script>

serviceWorker.enablePush(yourPublicKey) は Push 通知の購読をユーザーに確認し、許可されたら購読処理をすすめるメソッドです。サーバー側で生成する公開鍵を引数に渡しています。この値はあとで置き換えます。

これらの行は、割と Amplify が処理を隠蔽してくれているところで、例えば公開鍵は URL エンコードされた Base64 文字列を UInt8Array に変換してから使う必要がありますが、それは enablePush() メソッドが内包しています。
参考: 該当部分の実装 - github.com/aws-amplify/amplify-js

この状態で、amplify run してから Chrome や Firefox でページを開くと、おなじみ?の通知許可が求められます。 serviceWorker.enablePush(yourPublicKey) による動作です。

image.png

公開鍵の取得と模擬的なサーバーサイドの用意

冒頭で書いたとおり、今回はフロント側について書きたいので、バックエンドは手軽に鍵生成や Push 送信のテストができるコンパニオンサイト、Push Companionを使います。

Push Companionを開くと、Public KeyとPrivate Keyが表示されています。

image.png

Public Key をコピーして、src/App.vue に反映します。

[-]
const yourPublicKey = 'Paste your public key here';

[+]
const yourPublicKey = 'BDirXoCByCLittjLnybgMtlmAl1Oc52zE--QIgU378Z7ljkoiWDjy2F*****************************';

アプリケーションを起動して購読してみる

$ amplify run

...

  App running at:
  - Local:   http://localhost:8080/
  - Network: http://192.168.10.13:8080/

ブラウザで http://localhost:8080/ を開き、通知許可のダイアログを表示します。

image.png

ここで許可(Allow)すると、画面上に JSON 文字列が表示されます。

image.png

この JSON 文字列は Subscription 情報などと呼ばれ、サーバーサイドから Push を送信するときの宛先情報にあたるものです。

実際のサービスでは、この後 subscriptionInfo をサーバーサイドに送り、ユーザー情報と紐付けてデータベースに保存しておくなどして、必要なときに必要なユーザーに Push を送信することができるよう管理する必要があるでしょう。

Push を送信、受信してみる

では、いよいよ Push 通知を受信してみます。またコンパニオンサイトに頼ります。

"Subscription to Send To" に、先程の SubscriptionInfo 文字列をコピーして、"Text to Send" に好きなメッセージを書いて "SEND PUSH MESSAGE" をクリックします。単なる文字列を送ってもいいのですが、通常 JSON で送ることが一般的で、今回の Service Worker 側でもそれを期待しているので、次の内容を送ってみます。

{"title": "Hello Amplify!!!", "message": "Amplifyかわいい"}


image.png

うまく実装されていれば、次のような通知が表示されます。

image.png

Vue アプリケーションのタブを閉じていたり、別 Window を表示したりしていても通知が表示されるでしょうか?通知をクリックしたとき、ちゃんと service-worker.js で記述した処理が実行されているでしょうか?確認してみてください。

期待通りに動いていれば、これで Amplify を使ったクライアントサイドの Web Push 通知対応ができました。

Analytics の集計情報を見てみる

最初に、Service worker のイベントを集計するために Analytics カテゴリを追加してありました。ちゃんと動いているか見てみましょう。

$ amplify console analytics

Amazon Pinpoint のマネジメントコンソールが開いたら、 Analytics > Events を見てみます。
Filters を有効にすると、次のようなイベントが収集されています。

image.png

Amplify が自動的に Service worker のライフサイクルや状態変化、メッセージングなどのイベントを集計してくれています。

補足

開発中に購読状態を変更したいとき

開発中は、一度購読を許可した後にもう一度もとに戻したい、拒否を取り消したいときがたくさんあると思います。
Chrome のアドレスバーのアイコンをクリックすると、そのサイトに対する購読状態を変更することができます。

image.png

サーバーサイドの Push 送信実装について

今回はコンパニオンサイトに頼りましたが、実際にサーバーサイドからの Push 送信を自分で実装するときは Web Push 用のライブラリを使って実装することが一般的かと思います。

https://github.com/web-push-libs/

言語別のライブラリがあるので、必要のある方は見てみてください。

その辺の実装は、後日書くサーバー編で触れたいと思います。

その他考慮すること

前提条件にも書きましたが、今回の記事はクライアント側の Amplify に関わる部分にフォーカスしています。
サービスを運用する際は、これら以外に購読済みのユーザーが購読を停止するための UI や処理、ユーザーの Subscription 情報や状況を保存するバックエンドのデータベース等が必要になると思われます。
その辺も後日書くサーバー編に(ry

また、Appiterate による調べでは、不適切で不快な通知はユーザー離反の最も大きな要因になり得ます。ユーザー体験を十分に設計する必要があります。

image.png
[AWS Start-up ゼミ] よくある課題を一気に解説! 御社の技術レベルがアップする 2019 春期講習

まとめ

  • Amplify の ServiceWorker クラスは Web Push 購読処理を隠蔽して、ちょっと手軽にしてくれる
  • Amplify で Analytics カテゴリを有効にしていると、自動的に Service worker の挙動をトラッキングしてくれる

みんなも Amplify で Web Push 処理しましょう。

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

【Nuxt.js】create-nuxt-appのすすめ 〜create-nuxt-appの質問に負けたくないあなたへ〜

この記事はエイチーム引越し侍 / エイチームコネクト Advent Calendar 2019の10日目の記事です。

前日は、こぼさん(@anneau)の『Vueを使ってサイトを1ヶ月で作った話。そして、それを、1ヶ月使ってリプレイスした話。』でした!
私も身近で引越し侍 まるごとスイッチを開発している姿を見ていたのですが、膨大な仕様のサービスを、2ヶ月という限られた期間で、スピード感もって開発していることがひしひしと伝わりました。
尊敬する大先輩の記事です。ぜひ読んでみてください!

本日はぴかぴかの新卒1年生のちーずが担当いたします!
QiitaがPWAに対応していることに最近気づき、以前より記事を読むようにになりましたが、投稿は久しぶりになってしまいました:cry:
アイコンのQiitanのシルエットがかわいいので、スマホのホーム画面にQiitaを追加することをおすすめします:slight_smile:

はじめに

Nuxt.jsを勉強し始めたときに、いちばん最初にcreate-nuxt-appをしてみました。
すると、すごい勢いで大量な質問されて、:thinking: ってなっちゃいました。
そんな人が他にもいるかな?と思い、create-nuxt-appしたときに迷わないように選択の手がかりになるような記事にしてみました!

こんな記事です

  • Nuxt.js 初学者向けの記事です
  • create-nuxt-appのしくみとはじめかたがわかります。
  • 8つの質問に答えて、最適な環境づくりをしよう! (※2019年12月現在)

1. create-nuxt-appとは?

公式ドキュメントみると、Create Nuxt.js App in seconds.と書いてありますが、まさにその通りで、1つのコマンドだけでNuxt.jsのwebアプリをぱっとつくれちゃうツールです。

Nuxtでよく使われるフレームワークや、モードを選択して、環境が整った状態で始められるのが最大のメリットです。

どんな仕組みでできてるの?って思った人は、 公式ドキュメントをもくもくと読むとよくわかると思いますが、テンプレートとなるpackage.jsonをみるだけでも、なるほどね〜ってなります。

create-nuxt-app/template/_package.json

2. create-nuxt-appのはじめかた

create-nuxt-appは2通りの方法ではじめることができます。

npxではじめる

npm 5.2.0以上からはデフォルトで入っているnpxを用いてcreate-nuxt-appする場合は、下記コマンドでできます。

$ npx create-nuxt-app <project-name>

npxには、インストールしていないパッケージを一度だけ実行できる機能があります。
コマンドの実行が完了次第、グローバルから削除されるため、おすすめです。

npxに関しては、こちらの記事がわかりやすかったので、是非よんでみてください。

npm 5.2.0の新機能! 「npx」でローカルパッケージを手軽に実行しよう

yarnではじめる

yarnを用いてcreate-nuxt-appする場合は、下記コマンドでできます。

$ yarn create nuxt-app <project-name>

このコマンドは、下記コマンドと全く同じであるため、create-nuxt-appのパッケージをグローバルに追加し、create-nuxt-appを実行してることがわかります。

$ yarn global add create-nuxt-app
$ create-nuxt-app <project-name>

yarn createに関する詳しい情報はこちら。

yarn create | Yarn

3. 8つのQuestion

プロジェクト名や、筆者などありきたりなことを聞かれたあとに、質問責めが始まります。
初create-nuxt-appの方は少しびっくりしちゃうかもですが、この記事では一つずつ説明していきます。

(1) パッケージマネージャー

Nuxt.jsでアプリを作成する際に、Node.jsパッケージを管理するシステムであるパッケージマネージャーが必要です。
パッケージマネージャーには以下の二つの選択肢があります。

? Choose the package manager (Use arrow keys)
❯ Yarn
  Npm

正直、止むを得ない場合以外はYarn一択だと思います。
Yarnの方が早くて、出力が簡潔です。

パッケージマネージャーって?ってなった方には、私の過去の記事を紹介いたします。笑

パッケージ管理ツール(パッケージマネージャー)

(2) vue.jsのUIフレームワーク

UIフレームワークは、CSSのフレームワークをVue.jsに拡張したものがほとんどです。
現在(2019/12/10)では、下記フレームワークから選択することができます。

 Choose UI framework (Use arrow keys)
❯ None
  Ant Design Vue
  Bootstrap Vue
  Buefy
  Bulma
  Element
  Framevuerk
  iView
  Tachyons
  Tailwind CSS
  Vuetify.js

Ant Design Vue

Ant_Design_Vue.png

シンプル且つ、ユーザビリティが良さそうな印象のフレームワーク。
元は、ReactのUIフレームワークであり、APIもそこそこ充実しています。

github star 8.8k
コンポーネント、APIの充実度 ★★★☆☆
難易度
適切なデバイス PC

Bootstrap Vue

bootstrap.png
BootstrapをVueのUIコンポーネントとして拡張してるため、もろBootstrapな見た目のフレームワークです。
ちょっとドキュメント読みづらいかなと感じました。

github star 10.5k
コンポーネント、APIの充実度 ★★★★☆
難易度
適切なデバイス PC/SP

Buefy

Buefy.png

BulmaをVueのUIコンポーネントとして拡張してるため、もろBulmaな見た目のUIフレームワーク。
APIのドキュメントが読みやすく、シンプルで汎用性が高い印象。

github star 6.7k
コンポーネント、APIの充実度 ★★★★☆
難易度
適切なデバイス PC(SPもいけなくはない)

Bulma

bulma.png
シンプル且つ軽量なCSSフレームワーク。
機能は自分で実装したい!って人におすすめ。

github star 37.7k
コンポーネント、APIの充実度 ★★☆☆☆
難易度
適切なデバイス PC(SPもいけなくはない)

Element

Element.png

見た目はシンプルだが、高機能なコンポーネントが多い。
数も充実しており、使ってる人も多い印象。

github star 42.7k
コンポーネント、APIの充実度 ★★★★★
難易度
適切なデバイス PC

Framevuerk

Framevuerk.png
とてつもなくシンプルで最低限にコンポーネントが用意されているUIフレームワーク。
ただ、更新頻度も低く、汎用性も低いためおすすめはしません。

github star 194
コンポーネント、APIの充実度 ★☆☆☆☆
難易度
適切なデバイス PC...?

iView

iView.png
component数や機能面において過不足なくちょうどよい感じのUIフレームワーク。
ただ、日本人にユーザーが少なそう。。。

github star 900
コンポーネント、APIの充実度 ★★★☆☆
難易度
適切なデバイス PC

Tachyons

Tachyons.png

マルチクラスで元のcomponentをアレンジしていく感じ。
ドキュメントがギャラリーになっており、自分の実現したいデザインを探しやすいかも?

github star 9.4k
コンポーネント、APIの充実度 ★★★☆☆
難易度
適切なデバイス PC/SP

Tailwind CSS

Tailwind.png
元のcomponentにマルチクラスをあててアレンジしていくCSSフレームワーク。
拡張性が高く、レイアウトも自由が効き、デザイン製も美しい。

github star 17.4k
コンポーネント、APIの充実度 ★★★☆☆
難易度
適切なデバイス PC/SP

Vuetify.js

Vuetify.png
マテリアルデザインのUIフレームワークで、アニメーションが美しい。
高機能かつ充実したcomponent量だが、ドキュメントだけ読みづらい。

github star 22.8k
コンポーネント、APIの充実度 ★★★★★
難易度
適切なデバイス PC/SP

※ 難易度は、「どれくらい簡易に動く機能が実装できるか」という基準で評価しているため、CSSのみのフレームワークは「高」とさせていただきます。
※ star数は、12月10日現在の数です。

つくりたいものによって、使い分けるのがベストだとおもいますが、個人的におすすめなのが、BuefyVuetify.jsです。
どちらともcomponentのAPIの機能性が高く、拡張性があるため、使いやすいなと感じました。

ここで筆者がつまづいたtips

フレームワークのConfigurationなどから変えられず、やむなく上書きしたい時は、ディープセレクタである必要があります。

  • styleタグにscopedを付与
  • class名の前に >>>

することで上書きができます。

<style scoped>
>>> .theme--light.v-application {
  background-color: #f3f3f3;
}
</style>

(3) サーバーサイドのフレームワーク

SSRモードの際に用いられるサーバーサイドのフレームワークを指定することができます。
(基本的には、フレームワークを用いることは滅多にないと思われます。)
現在(2019/12/10)では、下記フレームワークから選択することができます。

? Choose custom server framework (Use arrow keys)
❯ None (Recommended)
  AdonisJs
  Express
  Fastify
  Feathers
  hapi
  Koa
  Micro

AdonisJs

Laravelっぽく書けるフレームワーク。
github star: 7.4k

adonis-framework - Github

Express

Node.js製のWebフレームワークといえば...ってくらい有名なフレームワーク。
REST APIの開発によく使われてるイメージです。

github star: 46.4k

express - Github

Fastify

名前の通り、最速を目指してるwebフレームワークです。
github star: 12.7k

fastify - Github

Feathers

リアルタイムアプリケーションとREST APIに特化したフレームワークです。
クライアントのみでも使えるみたい。
github star: 11.9k

feathers - Github

hapi

軽量で拡張性の高いNode.jsフレームワーク。
github star: 11.8k

hapi - Github

Koa

expressを開発したチームが、よりわかりやすく、軽量に作ったwebフレームワークです。
github star: 28k

koa - Github

Micro

高速かつわかりやすいフレームワーク。
下記記事がわかりやすかったです。
github star: 8.9k

Node.js でちょっとしたサーバーサイドやるなら、 Micro が良いかも

micro - Github


※ star数は、12月10日現在の数です。

Node.jsのフレームワークがこんなにもあるのに驚きました。
個人的には、REST APIを作成するにはFeathersが書きやすそうだなと感じました。
また、サーバーサイド言語での開発が多い人は、AdonisJsがディレクトリ構造や書き方など、馴染みやすい印象を受けました。

(4) Nuxt.jsが提供するモジュール

nuxt-communityが提供する、便利でよく使われるモジュールを選択できます。
上3つと違い、複数選択し導入することが可能です。

? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert sele
ction)
❯◯ Axios
 ◯ Progressive Web App (PWA) Support
 ◯ DotEnv

今回は関連性がないため、一つずつ説明していきます。

Axios

Axiosは、非同期通信を用いて簡単に外部APIを叩くことができるツールです。
jQueryでいう、$.ajaxのようなものです。
非同期処理を行うPromiseベースであり、非常に書きやすいです。
APIを叩くNuxtアプリを作成する際は、ほぼ必ず必要になるため、入れておくとよいでしょう。

Introduction - Axios Modul
GitHub - nuxt-community/axios-module

PWA

PWA(Progressive Web Apps)は、webアプリをモバイルアプリのように使うことのできる仕組みです。
ホームに追加すると、アドレスバーがなくなり、プッシュ通知やオフライン利用などができたりします。すごい。
そのため、今後モバイルでの展開を考えてるサービスを作る際には、この項目をチェックすると良いと思います。

今回インストールされるのは、nuxt pwaという、nuxtで簡単にPWAに対応したwebアプリを作れるパッケージです。

⚡ Nuxt PWA

PWAを理解するのは難しいですが、下記記事がわかりやすいと思います。

いまさら聞けないPWAとAMP

また、Googleが提供するチェックリストもあるため、参考になるかも...?

Progressive Web App Checklist | Google Developers

DotEnv

DotEnvは、環境変数を管理するモジュールです。
環境依存の設定やを管理する際に使えます。
(そのため、設定ファイルはgitignoreしましょう。)

GitHub - nuxt-community/dotenv-modul

他にも、nuxt-communityが提供するモジュールを見てみると、便利そうなものがたくさんあるので、網羅的に見てみることをおすすめします。

(5) ソースを美しく保つためのツール

Lint(linter)とは、ソースコードのルールをチェックしてくれるツールです。
共同開発する際に、ソースコードのルールがないと、長期的に見たソースの品質が悪くなる可能性があります。
それを監視してくれるのが、Lintです。
こちらも言語によってツールが違うため、複数指定することができます。

? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert select
ion)
❯◯ ESLint
 ◯ Prettier
 ◯ Lint staged files
 ◯ StyleLint

ESLint

ESLintは、JavaScriptのソースチェッカーです。
クォーテーションの種類やconsole.logを許すかなど、様々なルールを設定し、
そのルールにあってるかをチェックしてくれます。

ESLint - 公式サイト
ESLint - ルール一覧

Prettier

Prettierは、ルールに合ったコードに整形してくれるコードフォーマッタです。
Linterと親和性が高く、Linterに設定したルールに自動的に書き換えてくれたりします。

Prettier · Opinionated Code Formatter

また、ESLintとPrettierに関してはNuxt.jsの公式ドキュメントにも詳しく書いてあります。

また、下記記事がわかりやすかったため、参考にしてみてください。

Prettier 入門 ~ESLintとの違いを理解して併用する~

Lint staged files

この項目を選択すると、huskylint-stagedというモジュールが追加されます。
huskyは、gitコマンドをhookにして、指定したコマンドを叩くことができるモジュールで、
lint-stagedは、コミット前のファイルに強制的にlintingさせることができるモジュールです。
これらを組み合わせることで、lintの通ったソースコードのみしかcommitできない環境が出来上がります。

GitHub - typicode/husky
GitHub - okonet/lint-stagedy

StyleLint

StyleLintはその名の通り、スタイルシートのコードをチェックしてくれるツールです。
この項目をチェックすると、下記モジュールが入れられます。

GitHub - nuxt-community/stylelint-module

共同開発する場合は、なるべく上記のLintをチームでカスタマイズして使っていくことをおすすめします。

(6) テストフレームワーク

この項目では、Unitテストフレームワークを実行してくれるテストランナーを指定することができます。
自分はまだテストランナーを使ったことがないです...
本当に知見がないため、さらっと調査し、紹介します...
(大反省しております。。。)

? Choose test framework (Use arrow keys)
❯ None
  Jest
  AVA

Jest

Facebook製のユニットテストツール
github star: 28.6k

jest - Github

AVA

早い・軽い・簡単が売りのテストツール
github star: 17.1k

ava - Github

※ star数は、12月10日現在の数です。

(7) レンダリングモード

Nuxtの最大の特徴である、レンダリングモードの指定ができます。
(心のなかで、なぜ最後のほうに聞いてくるんだ?って思ってます。最初に聞いてほしい...)

? Choose rendering mode (Use arrow keys)
❯ Universal (SSR)
  Single Page App

Universal (SSR)は、初回リクエストやリロードの際にサーバーサイドでレンダリングを行い、
Single Page App(SPA)では、クライアント側でjavascriptをレタリングします。

どちらを選ぶべきかに関しては、下記記事がとってもわかりやすいです。

Nuxt.jsを使うときに、SPA・SSR・静的化のどれがいいか迷ったら

(8) 開発ツール

開発時に使うツールを選択することができます。

? Choose development tools
❯◯ jsconfig.json (Recommended for VS Code)

jsconfig.jsonを設定することで、エディタ(VS Code)にこのコードはES6で書かれてますよということを伝えることができ、正しくシンタックスエラーがでたりハイライトしたりします。

jsconfig.json Reference

以上でcreate-nuxt-app時の質問の解説をおわります。
この質問は日々アップデートされているため、項目が変化したりしますのでご注意を。。。

4. スクラッチからはじめることもできる

Nuxt.jsのアプリ開発に慣れてきたら、スクラッチからはじめることをおすすめします。
ご紹介したモジュールや設定等を自分で一から追加、設定していくと、より自分に合ったNuxtアプリを作ることができます。
ハッカソンや短期開発の時は、create-nuxt-appの方がおすすめです。

公式ドキュメント - スクラッチから始める

終わりに

ここまで書いた感想

正直なところ、物量が多くてすごく疲れました。笑
しかし、記事を書いていくうちに発見も多かったし、自分自身とても勉強になりました。
サーバーサイドのフレームワークなんて、express以外全く知らなかったため、調べるのが楽しかったです。
まだ使えてないものも多いので、この機会にcreate-nuxt-appしまくろうと思います!笑

また、私と同じように、create-nuxt-appの質問に圧倒され、 :thinking: ってなる人もいると思います。
そんな方にこの記事が届くといいなぁって思います。

めっちゃ参考にした文献

お知らせ

エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひともエイチームグループ採用ページWebエンジニア詳細ページ)よりお問い合わせ下さい。

フロントエンンドエンジニアも募集しております
https://www.a-tm.co.jp/recruit/requirements/career/lifestylesupport-frontenddesigner/

明日

明日のエイチーム引越し侍、エイチーム コネクトアドベントカレンダーの担当は、@Ingwardさんです!
どんな技術領域の記事なのか楽しみです...!

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

Vuetifyのdatepickerを使って【和暦】+【年度/月】pickerを作ってみた

vuetifyのv-date-pickerは、表面上のテキストをfuncitionで書き換えることができるので【和暦表示】と【年度/月】が選択できるピッカーを作ってみました。
0.5x倍にしてみると普通に見れると思います。

See the Pen WarekiDatePicker by BigFly3 (@bigfly3) on CodePen.

本来やらないような操作したときに稀にエラーになるかもしれませんが、公式のもおかしな感じになるのでそこは無視してください。

component化したもの

v-date-pickerをラッパー化て、フォーマット関連以外のプロパティは、ほぼそのまま使えるようにしてみました。v-date-pickerから置き換えても普通に動かせるかと思います。

jpv-date-picker
<template>
<v-date-picker
  :allowedDates='allowedDates'
  :color='color'
  :dark='dark'
  :dayFormat='jpvDayFormat'
  :disabled='disabled'
  :eventColor='eventColor'
  :events='events'
  :firstDayOfWeek='firstDayOfWeek'
  :fullWidth='fullWidth'
  :headerColor='headerColor'
  :headerDateFormat='str => jpvHeaderDateFormat(str)'
  :landscape='landscape'
  :light='light'
  locale='ja-jp'
  :max='max'
  :min='min'
  :monthFormat='str => jpvMonthFormat(str)'
  :multiple='multiple'
  :nextIcon='nextIcon'
  :noTitle='noTitle'
  :pickerDate='pickerDate'
  :prevIcon='prevIcon'
  :range='range'
  :reactive='reactive'
  :readonly='readonly'
  :scrollable='scrollable'
  :selectedItemsText='selectedItemsText'
  :showCurrent='pickerShowCurrent'
  :showWeek='showWeek'
  :titleDateFormat=jpvTitleDateFormat
  :type='jpvType'
  :value='jpvValue'
  :width='width'
  :yearFormat='str => jpvYearFormat(str)'
  :yearIcon='yearIcon'
  @change="changeValue"
  @click:month='clickMonth'
  @click:date='clickDate'
  @update:picker-date='updatePickerDate'

  ><slot></slot></v-date-picker>
</template>
<script>
export default {
  props:{
    allowedDates: Function,
    color: String,
    dark: Boolean,
    dayFormat: Function,
    disabled: Boolean,
    eventColor: [Array, Function, Object, String],
    events: [Array, Function, Object],
    firstDayOfWeek: [String, Number],
    fullWidth: Boolean,
    headerColor: String,
    landscape: Boolean,
    light: Boolean,
    max: String,
    min: String,
    multiple: Boolean,
    nextIcon: String,
    noTitle: Boolean,
    pickerDate: String,
    prevIcon: String,
    range: Boolean,
    reactive: Boolean,
    readonly: Boolean,
    scrollable: Boolean,
    showCurrent: [Boolean, String],
    showWeek: [Boolean],
    selectedItemsText: String,
    type: {
      type: String,
      default: 'date' // 'date' or 'month' or 'nendo'
    },
    value:[String, Array, Object],
    width:[Number, String],
    yearIcon: String
  },
  data: () => {
    const now = new Date();
    return {
      inputValue:'',
      now:now,
    }
  },
  created(){
    this.inputValue = this.value
  },
  methods:{
    real2nendoM(realM){ // 内部/月 → 年度/月
      return realM < 10 ? realM + 3 : realM - 9
    },
    real2nendoY(realY,realM){ // 内部/月 → 年度/年
      return realM < 10 ? realY : realY + 1
    },
    real2nendoYM(realYM){ // 内部/年月 → 年度/年月
      if(!realYM) return ''
      const y = parseInt(realYM.split('-')[0]);
      const m = parseInt(realYM.split('-')[1]);
      return this.real2nendoY(y,m) + '-' + this.strpad(this.real2nendoM(m))
    },
    nendo2realM(nendoM){ // 年度/月 → 内部/月
      return nendoM < 4 ? nendoM + 9 : nendoM - 3
    },
    nendo2realY(nendoY,nendoM){ // 年度/年月 → 内部/年
      return nendoM < 4 ? nendoY - 1 : nendoY
    },
    nendo2realYM(nendoYM){ // 年度/年月 → 内部/年月
      if(!nendoYM) return ''
      const y = parseInt(nendoYM.split('-')[0]);
      const m = parseInt(nendoYM.split('-')[1]);
      return this.nendo2realY(y,m) + '-' + this.strpad(this.nendo2realM(m))
    },
    year2Wareki(year){
      let wYear = ''
      let gen = ''
      if(year > 2018){
        wYear = year-2018
        gen = '令和'
      }else if(year > 1988){
        wYear= year-1988
        gen = '平成'
      }else if(year > 1925){
        wYear = year-1925
        gen = '昭和'
      }else if(year > 1911){
        wYear = year-1911
        gen = '大正'
      }else if(year > 1867){
        wYear = year-1867
        gen = '明治'
      }
      if(wYear === 1) wYear = ''
      return gen !== '' ? gen + wYear + '' : false
    },
    strpad(num){
      return ( '00' + num ).slice( -2 );
    },
    changeValue:function(str){
      if(!str) return false
      if(this.readonly) return false
      if(this.type === 'nendo'){
        str = this.real2nendoYM(str)
      }
      this.inputValue = str
      this.$emit('change',this.jpvDate)
    },
    clickMonth:function(str){
      if(this.readonly) return false
      if(this.type === 'nendo'){
        str = this.real2nendoYM(str)
      }
      if(this.type !== 'date'){
        this.updateValue(str)
      }
      this.$emit('input',this.inputValue)
    },
    clickDate:function(str){
      if(this.readonly) return false
      this.updateValue(str)
      this.$emit('input',this.inputValue)
    },
    updateValue(str){
      if(this.multiple){
        const index = this.inputValue.indexOf(str)
        if(index > -1){
          this.inputValue = this.inputValue.filter(n => n != str);
        }else{
          this.inputValue.push(str)
        }
      }else if(this.range){
        if(this.inputValue.length > 1){
          this.inputValue = []
        }
        this.inputValue.push(str)
      }
    },
    updatePickerDate(str){
      this.$emit('update:picker-date',str)
    },

    //ヘッダーの一番上にある年表示+年選択のフォーマット
    jpvYearFormat(str){
      if(!str) return ''
      const year = this.type==='date' ? str.split('-')[0]: str
      return this.year2Wareki(year) + '(' + year + ')'
    },
    //ヘッダーの選択データを表示する部分
    jpvTitleDateFormat(str){ // realYM → YYYY-MM
      if(!str) return ''
      return this.multiple || this.range ? String(this.inputValue.length) + ' selected' : this.inputValue
    },
    //月選択ボタン上部のYYYYを変換するfunction
    jpvHeaderDateFormat(str){
      if(!str) return ''
      const [year,month,day] = str.split('-')
      if(this.type === 'nendo'){
        return this.year2Wareki(year) + '度(' + year + ')'
      }else if(this.type === 'date' && month){ //dateの場合でも月選択するケースがある
        return this.year2Wareki(year)  + '(' + year + ')' + month + ''
      }else{
        return this.year2Wareki(year) + '(' + year + ')'
      }
      return str
    },
    //月選択ボタンを年度の見た目にするfunction
    jpvMonthFormat(str){ // realYM → YYYY-MM
      if(str === '') return str
      let m = parseInt(str.split('-')[1])
      if(this.type === 'nendo') m = this.real2nendoM(m)
      return m + ''
    },
    //月選択ボタンを年度の見た目にするfunction
    jpvDayFormat(str){ // realYMD → YYYY-MM-DD
      if(str === '') return str
      let d = parseInt(str.split('-')[2])
      return d
    },
  },
  computed:{
    pickerShowCurrent(){ //picker値は元のまま変わらないのでpickerの位置を3か月ずらす
      if(!this.showCurrent){ //カレント表示をなくす場合
        return false
      }
      let current = this.showCurrent
      if(this.type === 'nendo'){
        //初期状態でカレントがセットされていない場合、内部を現時刻から3か月前にセット
        if(current === true){
          current = this.now.getFullYear() + '-' +  parseInt(this.now.getMonth() + 1);
        }
        current = this.nendo2realYM(current)
      }
      return current
    },
    lastValue(){
      return this.multiple || this.range ? this.inputValue[this.inputValue.length - 1] : this.inputValue
    },
    jpvType(){
      return this.type==='nendo' ? 'month' : this.type
    },
    jpvDate(){
      if(typeof this.inputValue !== 'string' || !this.inputValue.match( /^(\d{4})/ )) return ""

      const d = {}
      d.value = this.inputValue
      let [year,month,day] = d.value.split('-')

      d.year = parseInt(year)
      d.month = month
      d.day = day ? day : ''
      d.wareki = this.year2Wareki(d.year)

      d.WM = d.wareki + d.month + ''
      d.WYM = d.wareki + '(' + d.year + '/' + d.month + ')'

      //表示用のデータ
      if(this.type==='date'){
        d.value = d.year + '-' + d.month + '-' + d.day
        d.WMD = d.wareki + d.month + '' + d.day + ''
        d.WYMD = d.wareki + '(' + d.year + '/' + d.month + '/' + d.day + ')'
      }else if(this.type==='nendo'){
        d.nendo = {}
        d.nendo.year = d.month < 4 ? d.year -1 : d.year
        d.nendo.month = d.month
        d.nendo.wareki = this.year2Wareki(d.nendo.year) + ''
        d.nendo.WM = d.nendo.wareki + d.nendo.month + ''
        d.nendo.WYM = d.nendo.wareki + '(' + d.nendo.year + '/' + d.nendo.month + ')'
      }
      return d
    },
    jpvValue(){
      if(!this.inputValue){
        return this.multiple || this.range ? []:"";
      }
      let returnValue = this.inputValue;
      if(this.type==='nendo'){
        if(this.multiple || this.range){
          returnValue = this.inputValue.map((str)=>{
            return str.match( /^(\d{4})-(\d{2})$/ ) ? this.nendo2realYM(str) : str
          })
        }else{
          returnValue = this.nendo2realYM(this.inputValue)
        }
      }
      return returnValue
    },
  },
  watch:{
    multiple:{
      handler(newVal){
        this.inputValue = newVal ? []: ''
        this.$emit('input',this.inputValue)
      }
    },
    range:{
      handler(newVal){
        this.inputValue = newVal ? []: ''
        this.$emit('input',this.inputValue)
      }
    },
    type:{
      handler(newVal){ //typeを切り替えるとv-date-pickerとの整合性が合わないので再設定する
        this.$children[0].tableDate = this.$children[0].inputDate
      }
    },
  }
}
</script>

データについて

v-modelでデフォルトのvalueをセットできます。

@change → 和暦のデータを含めたオブジェクトの取得
@input → valueの取得

フォーマット変更したい場合

jpv~Format()のメソッド部分をいじれば色々変えられるかと思います。

オブジェクトで返すデータを調整したい場合

jpvDate()の中で調整すると、自分好みのデータを返すことが出来ると思います。

最後に

あんまり自分で使ったわけではないので問題があるかわからないですが、vuetifyの公式サンプルが動くくらいにはなってると思います。よかったらお試しください。

:christmas_tree: FORK Advent Calendar 2019
:arrow_left: 08日目 Timelineをもう一度 @AsaToBan
:arrow_right: 10日目 @karaage7 さんよろしくお願いします。

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

TypeScriptでVue.set/deleteを型安全にするライブラリ作った

この記事はVue #2 Advent Calendar 2019の11日目です。

昨日は @nishihara_zari さんの jQueryしか書けなかった自分がVueを使えるようになるまでの話と同じような環境の人たちへ でした。

はじめに

以前、TypeScriptでVue.setを型安全にしたいという記事を書ました。
今回はその記事で紹介した、Vue.set(とVue.delete)を型安全にする実装をライブラリ化したので、実装内容についてご紹介します。

本文だけ読みたいという方は 実装について まで読み飛ばしてください。

ライブラリのご紹介

実装の紹介の前に、公開したライブラリについてご紹介させていただきます。

vtyped.png

@lollipop-onl/vue-typed-reactive - npm

@lollipop-onl/vue-typed-reactiveはVueのグローバルにVue.typedSetVue.typedDeleteを、インスタンスにvm.$typedSetvm.$typedDeleteを提供するプラグインです。

$ yarn add @lollipop-onl/vue-typed-reactive
# or
$ npm install -S @lollipop-onl/vue-typed-reactive

使い方

使い方は簡単で、Vueへの登録とtsconfig.jsonへの設定の追加のみです。

// Vueへの登録
import Vue from 'vue';
import TypedReactive from '@lollipop-onl/vue-typed-reactive';

Vue.use(TypedReactive);

Nuxtの場合

Nuxtの場合はプラグイン化して登録するのがおすすめです。

// @/plugins/libs/vue-typed-reactive.ts
import { Plugin } from '@nuxt/types';
import Vue from 'vue';
import TypedReactive from '@lollipop-onl/vue-typed-reactive';

const plugin: Plugin = () => {
  Vue.use(TypedReactive);
};

export default plugin;
import { Configuration } from '@nuxt/types';

const config: Configuration = {
  ...
  plugins: [
    '@/plugins/libs/vue-typed-reactive',
  ],
  ...
};


tsconfig.jsonでは、typesオプションにこのライブラリを追加してください。

// tsconfig.json
{
  "compilerOptions": {
    "types": [
      "@lollipop-onl/vue-typed-reactive"
    ]
  }
}

あとは、Vueコンポーネント内であればthis.$typedSetを、VuexのMutationの中などであればVue.typedSetを使用するようにするだけです。

interface IUser {
    name?: string;
    age: number;
}

// Vue Component
@Component
export default class SampleComponent extends Vue {
  profile: IUser = { age: 0 };

  onChangeName(name: string): void {
    this.$typedSet(this.profile, 'name', name);
  }

  clearProfileName(): void {
    this.$typedDelete(this.profile, 'name');
    this.$typedDelete(this.profile, 'age'); // TypeError!
  }
}
// Vuex Store Module
export const mutations = {
  setUserName(state: IState, name: string): void {
    Vue.typedSet(state.user, 'name', name);
  },
};

実装上は、Vue.typedSetVue.setの完全なエイリアスなので、全て置き換えても正常に動作すると思います(deleteも同様)。

機能

typedSettypedDeleteいずれも、プロパティと値の型安全以外にも 余計な 機能をつけました。

typedSetでは、readonlyなプロパティへの値の設定ができないようにし、typedDeleteでは、Optionalなプロパティのキーのみ削除できるようにしました。

interface IFoo {
  bar: number;
  baz?: string;
  readonly qux: string;
}

const foo: IFoo = { ... };

Vue.typedSet(foo, 'bar', 100); // ok.
Vue.typedSet(foo, 'qux', 'hello'); // Argument of type '"qux"' is not assignable to parameter of type 'never'.

Vue.typedDelete(foo, 'bar'); // Argument of type '"bar"' is not assignable to parameter of type 'never'.
Vue.typedDelete(foo, 'baz'); // ok.
Vue.typedDelete(foo, 'qux'); // Argument of type '"qux"' is not assignable to parameter of type 'never'.

また、もともとのVueの実装は残っているため、対応不可な型エラーはVue.setVue.deleteで回避することができます。

恩恵

実際に開発に組み込んでみて、VuexストアのMutationでとても活躍しています。
VuexのモジュールモードはモジュールがネストするとMutation内でのVue.setの使用が必須となります。

せっかくTypeScriptを使っているのにStateへの代入時の型不整合がチェックできないのはそこそこしんどいと思っていました。
また、プロパティ名の変更などもVue.setVue.deleteでは検知できないので手動リファクタが必要なってきます。

そういうのって面倒ですよね。

Vue.typedSetVue.typedDeleteを使ってよりリファクタリングのしやすいVueアプリにしませんか?

実装について

ここからは、vue-typed-reactiveの実装内容についてご紹介します。

以降はほぼほぼTypeScriptに関する内容になります。
Vueのアドベントカレンダーなのにすみません。。

Vueの拡張

Vue.typedSetVue.typedDeleteというメソッドはVueには存在しません。
それぞれVue.setVue.deleteのエイリアスとして機能するよう、Vueを拡張してます。

// グローバルメソッド Vue.typedSet
Vue.typedSet = Vue.set;
Vue.typedDelete = Vue.delete;

// インスタンスメソッド vm.$typedSet
Vue.prototype.$typedSet = Vue.set;
Vue.prototype.$typedDelete = Vue.delete;

これらをVue.useでVueに登録できるようにするために、Vueプラグイン形式のオブジェクトでラップしています。

import { PluginObject } = 'vue';

const TypedReactive: PluginObject<never> = {
  // Vue.useしたときに呼ばれるメソッド
  install: (Vue) => {
    // グローバルメソッド Vue.typedSet
    Vue.typedSet = Vue.set;
    Vue.typedDelete = Vue.delete;

    // インスタンスメソッド vm.$typedSet
    Vue.prototype.$typedSet = Vue.set;
    Vue.prototype.$typedDelete = Vue.delete;
  },
};

? 実際の実装箇所

PluginObjectはVueプラグインの型情報です。Genericにはプラグインオプションの型を指定します。
今回はオプションなしなのでneverを指定しています。

プラグイン — Vue.js

Vue.typedSetの型定義

Vue.typedSetの定義は以下のとおりです。

declare module 'vue/types/vue' {
  interface VueConstructor {
    typedSet<T extends Object, K extends WritableKeys<T>>(object: T, key: K, value: T[K]): T[K];
  }
}

? 実際の実装箇所

ちょっと長めですが、シンプルにすると以下のようになります。

typedSet<T extends Object, K extends keyof T>(object: T, key: K, value: T[K]): T[K];

言語化すると

objectで受け取った引数にあるプロパティのみをkeyで指定でき、keyで指定したプロパティの値と同じ型のみvalueで受け付ける

という実装になっています。

さて、シンプルにする前後で異なるのは、readonlyでないプロパティのみを取得する型 WritableKeys です。
詳しく見ていきましょう。

WritableKeys

WritableKeys型は以下のような定義になっています。

/** オブジェクトの値の型を取得する */
export type Values<T extends object> = T[keyof T];

/** 2つの型を比較し、一緒ならtrueを異なればfalseを返す */
export type IsEquals<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;

/** プロパティがreadonlyかどうかを判定する */
export type IsNotReadonly<T extends Object, P extends keyof T> = IsEquals<{ [K in P]: T[P] }, { -readonly [K in P]: T[P]}>;

/** readonlyでないプロパティのみ取得する */
export type WritableKeys<T extends Object> = 
  T extends any[] ? number : Values<{
    [P in keyof T]-?: IsNotReadonly<T, P> extends true ? P : never;
  }>;

ちょっと情報が多いですね。WritableKeys型のみ見てみましょう。

WritableKeys型は、Genericで渡された型が配列であればnumber型を、そうでなければreadonlyではないプロパティのキーを返します。

/** readonlyでないプロパティのキーを取得する */
export type WritableKeys<T extends Object> = 
  T extends any[] ? number : Values<{
    [P in keyof T]-?: IsNotReadonly<T, P> extends true ? P : never;
  }>;

? 実際の実装箇所

まず、型TにはObject型が渡されますが、配列である可能性もあります。
Tが配列である場合(T extends any[])はキーの型としてnumber型を返しています。
ここまでが以下の部分です。

export type WritableKeys<T extends Object> = 
  T extends any[] ? number : /*
    readonlyではないプロパティのキーを取得する
  */;

続いて、readonlyでないプロパティのみを取り出します。

ここでは、 IsNotReadonly を使って、readonlyでないプロパティであれば値がプロパティ名に、readonlyなプロパティであれば値がneverになるようなオブジェクト型に変換しています。

{
  // Optionalなプロパティは値にundefinedが残るので -? でOptional除去
  [P in keyof T]-?: IsNotReadonly<T, P> extends true ? P : never;
}

// 例
interface IFoo {
  bar: string;
  readonly baz: string;
  qux: number;
}
// 以下のような型に変換される
{
  bar: 'bar';
  baz: never;
  qux: 'qux';
}

その上で、 Values で値の型のみ取り出しています。

export type WritableKeys<T extends Object> = 
  /* 配列判定 */ : Values<{
    [P in keyof T]-?: IsNotReadonly<T, P> extends true ? P : never;
  }>;

// 例
interface IFoo {
  bar: string;
  readonly baz: string;
  qux: number;
}
// 以下のような型に変換される
type keys = WritableKeys<IFoo>; // 'bar' | 'qux'

プロパティのフィルタリング

このようにすこし回りくどい方法でプロパティのフィルタリングを行っているのは、TypeScriptではマッピングとともにプロパティを除去することができないからです。

TypeScriptでは{ [K in keyof T]: any }keyofinを使うことにより、元のオブジェクトと同じキーを持つ別の型を生成することができます。

このとき、マッピングに使えるのがkeyofだけで、キー自体をフィルタリングする方法は今の所ありません。

そこで、回りくどいですが、一度value側に値をセットし、そのvalueの型を取り出すことでキーをフィルタリングしているように見せています。

Vue.typedDeleteの型定義

Vue.typedDeleteの定義は以下のとおりです。

declare module 'vue/types/vue' {
  interface VueConstructor {
    typedDelete<T extends Object, K extends OptionalKeys<T>>(object: T, key: K): void;
  }
}

? 実際の実装箇所

Vue.typeSetと同じく、シンプルにするとobjectで受け取った引数にあるプロパティのみをkeyで受け取れるという実装になっています。

typedDelete<T extends Object, K extends keyof T>(object: T, key: K): void;

こちらでシンプルにする前後で異なるのはOptionalなプロパティのキーのみを取得する型OptionalKeysです。
詳しく見ていきましょう。

OptionalKeys

OptionalKeys方は以下のような定義になっています。

/** オブジェクトの値の型を取得する */
export type Values<T extends object> = T[keyof T];

/** 2つの型がいずれもtrueの場合のみtrueを返す */
export type And<X extends boolean, Y extends boolean> =
  X extends true
    ? Y extends true
      ? true
      : false
    : false;

/** 2つの型を比較し、一緒ならtrueを異なればfalseを返す */
export type IsEquals<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;

/** プロパティがOptionalかどうかを判定する */
export type IsOptional<T extends Object, P extends keyof T> = IsEquals<Pick<T, P>, { [K in P]?: T[P] }>;

/** プロパティがreadonlyかどうかを判定する */
export type IsNotReadonly<T extends Object, P extends keyof T> = IsEquals<{ [K in P]: T[P] }, { -readonly [K in P]: T[P]}>;

/** Optionalなプロパティのみ取得する */
export type OptionalKeys<T extends Object> = 
  T extends any[] ? number : Values<{
    [P in keyof T]-?: And<IsOptional<T, P>, IsNotReadonly<T, P>> extends true ? P : never;
  }>;

こちらも情報が多いですね。

OptionalKeys型は、Genericで渡された型が配列であればnumber型を、そうでなければOptionalかつreadonlyではないプロパティのキーを返します。

export type OptionalKeys<T extends Object> = 
  T extends any[] ? number : Values<{
    [P in keyof T]-?: And<IsOptional<T, P>, IsNotReadonly<T, P>> extends true ? P : never;
  }>;

? 実際の実装箇所

valueの条件以外はWritableKeys型と同じです。

条件もOptionalなプロパティ(IsOptional<T, P>)かつ、readonlyではないプロパティ(IsNotReadonly<T, P>)というシンプルなものです。

And<IsOptional<T, P>, IsNotReadonly<T, P>> extends true ? P : never;

vm.$typedSet/vm.$typedDeleteの型定義

最後に、インスタンスメソッドの型定義を行います。

メソッド名が異なるので、全く同じ定義を2度書かなければなりません。

declare module 'vue/types/vue' {
  interface Vue {
    /** Type-safe version of Vue.set */
    $typedSet<T extends Object, K extends WritableKeys<T>>(object: T, key: K, value: T[K]): T[K];
    /** Type-safe version of Vue.delete */
    $typedDelete<T extends Object, K extends OptionalKeys<T>>(object: T, key: K): void;
  }
}

? 実際の実装箇所

とくに説明することもないですね。

ユーティリティ型

本文中にちょいちょい出てくるユーティリティ型の解説をまとめています。適宜参照してください。

Values

export type Values<T extends object> = T[keyof T];

type foo = Values<{ foo: 0, bar: 1 }>; // 0 | 1
type bar = Values<{ foo: 0, bar: 'foo' }>; // 0 | 'foo'

? 実際の実装箇所

この型は、オブジェクトの値の型を取得するためのものです。

オブジェクトの型Tに対してkeyof Tの値を参照しています。
そのためすべてのプロパティ(keyof T)の値の型情報を取り出すことができます。

And

export type And<X extends boolean, Y extends boolean> =
  X extends true
    ? Y extends true
      ? true
      : false
    : false;

? 実際の実装箇所

この型は、Genericで渡された2つの型の両方がtrueの場合のみtrueを、それ以外の場合はfalseを返すものです。
JavaScriptでの&&と同等の型です。

IsEquals

export type IsEquals<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false;

? 実際の実装箇所

この型はGenericで指定した2つの型が同一かを判定するものです。

[Feature request]type level equal operator · Issue #27024 · microsoft/TypeScript

TypeScriptリポジトリのIssue内で提案された型ですが、正直ちゃんと理解できていません。

挙動を見るに、1つ目のConditional TypeでTの型が型Xとひも付き必ず1となり、2つ目のConditional Typeでは1つ目で確定した型Tと型Yを比較することでXと同じであれば1、異なれば2になる。みたいなことではないかなと思っています(挙動からの憶測です。。)。

詳しい方、この型の解説をコメントしていただきたいです...

IsNotReadonly

export type IsNotReadonly<T extends Object, P extends keyof T> = IsEquals<{ [K in P]: T[P] }, { -readonly [K in P]: T[P]}>;

? 実際の実装箇所

この型は、Genericで渡されたオブジェクトの型TとプロパティPについて、Preadonlyでない場合にtrueを返すものです。

前述のIsEqualsでもともとのプロパティ({ [K in P]: T[P] })とreadonlyを除去したプロパティ({ -readonly [K in P]: T[P]})とを比較しています。
この比較がtrueであるイコールもともとreadonlyがついていないという判断がつきます。

IsOptional

export type IsOptional<T extends Object, P extends keyof T> = IsEquals<{ [K in P]: T[P] }, { [K in P]?: T[P] }>;

? 実際の実装箇所

この型はGenericで渡されたオブジェクトの型TとプロパティPについてPがOptionalなプロパティかどうかを判定しています。

実装内容についてはIsNotReadonlyのOptional版なので詳しい説明は省略します。

型定義のテスト

一応、@lollipop-onl/vue-typed-reactiveでは型定義のテストを書いています。

? 実際の実装箇所

テストに使用しているのは conditional-type-checks という型ライブラリです。

dsherret/conditional-type-checks: Types for testing TypeScript types.

conditional-type-checksでは、型の比較などに便利な型定義が様々収録されています。
また、asset関数を使えば「型チェックにパスする or 失敗するかどうか」をテストすることができます。

@lollipop-onl/vue-typed-reactiveでの例

import { assert, IsExact, Has, NotHas } from 'conditional-type-checks';

// typedSet - key
/** typedSetで指定可能なキーを取得する */
type TypedSetKeys<T extends Object> = TypedSet<T> extends (object: T, key: infer K, value: any) => any ? K : never;

/** レコード型に対して正しくキーを指定できる */
assert<Has<TypedSetKeys<{ foo: string }>, 'foo'>>(true);
/** readonlyなプロパティに対してキーを指定できない */
assert<NotHas<TypedSetKeys<{ readonly foo: string }>, 'foo'>>(true);
/** 配列に対してnumber型をキーとして指定できる */
assert<IsExact<TypedSetKeys<[1, 2, 3]>, number>>(true);

// typedSet - value
/** typedSetで指定可能な値の型を取得する */
type TypedSetValue<T extends Object, K extends WritableKeys<T>> = TypedSet<T> extends (object: T, key: K, value: infer V) => any ? V : never;

/** Record型に対して値を設定できる */
assert<IsExact<TypedSetValue<{ foo: string }, 'foo'>, string>>(true);
/** Record型のOptionalなプロパティに対して値を設定できる */
assert<IsExact<TypedSetValue<{ foo?: string }, 'foo'>, string>>(true);

// typedDelete
/** typeDeleteで指定可能なキーを取得する */
type TypedDeleteKey<T extends Object> = TypedDelete<T> extends (object: T, key: infer K) => void ? K : never;

/** Optionalでないプロパティのキーを指定できない */
assert<NotHas<TypedDeleteKey<{ foo: string }>, 'foo'>>(true);
/** Optoinalなプロパティのキーを指定できる */
assert<Has<TypedDeleteKey<{ foo?: string }>, 'foo'>>(true);
/** readonlyでOptionalなプロパティのキーを指定できない */
assert<NotHas<TypedDeleteKey<{ readonly foo?: string }>, 'foo'>>(true);
/** 配列に対してnumber型のキーを指定できる */
assert<IsExact<TypedDeleteKey<[1, 2, 3]>, number>>(true);

関数の引数をテストしたい場合などは自前で型定義が必要だったりしますが、型定義にもテストを書けるのはリファクタリングなどがしやすいので助かります。

出典

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