- 投稿日:2019-12-09T23:40:19+09:00
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-clitypescriptを使う場合は型定義とかも
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.jsonnpm 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
- 投稿日:2019-12-09T19:49:38+09:00
[ 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認定試験公式テキスト
- 投稿日:2019-12-09T19:33:51+09:00
Vue.jsでパーティクルを自動生成して雪を降らせる
はじめに
この記事は自動生成した要素をアニメーションさせてから自動消滅させるまでの流れを把握することを目的にしています。
初心者向けの内容ですが、2 ファイル合計 120 行程度のコードなので気軽にご覧ください。
※ガチのパーティクルを簡単に扱いたい場合には Vue Particles が便利です。出来上がるもの
チートシート的なもの(初心者向け)
- 定期的な雪情報の生成 → requestAnimationFrame
- 雪情報配列への格納 → push
- 雪情報の定義 → コンポーネント化
- 見た目の構成 → template
- すべての雪の描画 → v-for
- 雪の落下アニメーション → transform
- 雪のタイミング制御 → setTimeout
- 雪を消す通知 → emit
- 雪情報配列からの削除 → splice
コード全文
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.vuedata () { return { perticles: [], windowWidth: window.innerWidth, windowHeight: window.innerHeight, lastSpawnTime: 0, } }雪の位置情報は perticles 配列に格納します。
画面の横幅/縦幅は windowWidth / windowHeight に格納します。
最後に雪を生成した時刻は lastSpawnTime に格納します。では雪を生成(spawn)する関数を見ます。
App.vuerandom_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.vuemounted() { window.addEventListener('resize', this.get_window_size) this.spawn_loop(0) },App.vuespawn_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.vueexport 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.vuediv.snow-container { position: absolute; animation: fadein 1s ease 0s 1 normal; } @keyframes fadein { 0% {opacity: 0} 20% {opacity: 1} }要素の座標は
absolute
(絶対位置)にしています。
また、雪が現れた瞬間はうっすらと半透明にしたいので、キーフレームアニメーションでフェードインの効果を付けています。雪が降る動きは CSS アニメーションを用います。
まず、雪コンポーネント唯一の data として dy を定義しています。snow.vuedata () { 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.vuemounted(){ window.setTimeout(() => { this.fall() } , 100) },マウント後、つまり雪が画面上部(y 座標:0)の位置に現れてから 100 ミリ秒後に落下用の関数
fall()
を実行します。snow.vuefall(){ this.dy = this.limit_y window.setTimeout(() => { this.$emit('thaw') } , this.dr) }
fall()
が dy を変更することで、落下アニメーションが発生します。
その後、アニメーション時間分待機してから、雪を消す通知を発行します。(後述)雪を消す通知
雪を消すための通知には
$emit
を用います。snow.vuefall(){ 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.vuethaw_snow(index){ this.perticles.splice(index, 1) },引数で配列番号を受け取り、
splice()
で配列要素を削除します。まとめ
Vue.js でコンポーネント化した要素の扱い方を学ぶために、自動生成やメッセージを試してみました。
しかしながら要素の削除においては、ICS Media の 池田 泰延 様が以下のような調査報告をなさっています。Vue.jsの性能が優れないのはDevToolsを使うと理解できます。
— 池田 泰延 (@clockmaker) August 1, 2019
リストレンダリングにおいて、追加・更新時は問題ないものの、削除時には仮想DOMから実DOMへの反映がうまくいっていません。
(要素が全部作り直されている)
比較検証を実施することで、Vue.jsに苦手処理があることが判明するわけです。 pic.twitter.com/2eVu6ErQvq要素を動的に追加、削除する際は注意が必要ですね。
- 投稿日:2019-12-09T18:39:43+09:00
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-projectVue CLI を使ってテストページを開きます
$ cd my-project $ npm run serveブラウザで http://localhost:8080/ をアクセスするとテストページが表示されます
ここまでで、テストページの表示ができるようになりました
Firebase の設定 ~ Vueに記載する設定の取得
Firebase コンソールにアクセスし、Firebaseプロジェクトを作成していきます(Google アカウントが必要)
コンソール
https://console.firebase.google.com/
Firebase プロジェクトの作成
「Default Account for Firebase」を選択します
Vueに記載する設定の取得
Vue で Firebase を利用するために必要な設定を取得します
Project Overview
-></>
でWebアプリ設定画面へ以下の値をコピーしておきます
// Your web app's Firebase configuration var firebaseConfig = { apiKey: [表示された値], authDomain: [表示された値], databaseURL: [表示された値]", projectId: [表示された値], storageBucket: [表示された値], messagingSenderId: [表示された値], appId: [表示された値], measurementId: [表示された値] }; // Initialize Firebase firebase.initializeApp(firebaseConfig);Cloud Firestore の作成
- メニューから Database を選択します
以上で、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 ページを開いておきます
npm run serve
を実行し、http://localhost:8080/を開きます追加ボタンを押すと、
コンソール画面で、todos
コレクションが新たに作成され、その中にドキュメントが作成されていることがわかります
データを取得・監視する
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/ を開き、追加ボタンを押すと、
ページが動的に更新されることを確認できます
コード
最後に、ここまでで書いたコードをのせておきます
// 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/dde495d6a8ad1c25fa43Cloud Firestore データベースを作成する(公式)
https://firebase.google.com/docs/firestore/quickstart?hl=jaCloud Firestore を使ってみる(公式)
https://firebase.google.com/docs/firestore/quickstart?hl=jaCloud Firestore にデータを追加する(公式)
https://firebase.google.com/docs/firestore/manage-data/add-data?hl=jaCloud Firestore でリアルタイム アップデートを入手する(公式)
https://firebase.google.com/docs/firestore/query-data/listen?hl=jaVue.js + FirebaseでTodoアプリを作る
https://qiita.com/magaya0403/items/e292cd250184ea3fe7b0
- 投稿日:2019-12-09T18:33:56+09:00
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に挑戦したい人の助けになれば幸いです。
- 投稿日:2019-12-09T18:06:42+09:00
意外と間違えるVue.jsのバージョン確認方法
- 投稿日:2019-12-09T17:57:34+09:00
カルネージハートとは関係ない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 routerbeforeEachで毎回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年に発売以降続編の情報が皆無ですが、一部の熱狂的ファンは大会を開催してゲームを続けています。ゲームを盛り上げることで続編も出るかもしれません。カルネジスト、ネジらーの皆様のご協力をお願いします!
カルネージハートファンのプログラミング知識を共有しましょう!
- 投稿日:2019-12-09T15:23:17+09:00
docker-compose + vue.js + typescript 環境構築
元々知っているvue.jsに
最近仕事で使うdocker + typescriptを合わせた環境構築を行ってみた基本参照はここかなり見ました。
os ubuntu 18.04
docker-compose用のyml生成
cd your/dev/dic touch docker-compose.ymldocker-compose.ymlversion: '3' services: node: image: node:12.7.0-alpine volumes: - .:/vuejsnodeの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を作成
dockerfileFROM node:12.7.0-alpine WORKDIR /myapp COPY package.json ./ COPY yarn.lock ./ RUN yarn installdocker-compose.ymlversion: '3' services: view: build: . command: yarn run serve volumes: - .:/myapp - /myapp/node_modules ports: - "8000:8080"docker-compose up -d後は
localhost:8000
でサイトが開けるようになっているはずです!
- 投稿日:2019-12-09T14:36:10+09:00
【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を採用しました。
- 試験実装において前回の不具合は発生しない
- 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.
- 投稿日:2019-12-09T13:02:29+09:00
G Suite(Google apps script)というサーバレスな環境で、BootstrapとVue.jsを組み合わせたフォーム開発のフォームテンプレ一覧
はじめに
BootstrapとVue.jsを組み合わせて、サーバレスな環境であるG Suite(Google apps script)でフォームを作成する。
本ページでの掲載事項
フォームの基本的な項目のテンプレートを掲載。
前提
G SuiteのAPIを使ってHTMLページをフォームとして作成するため、
スプレッドシートからスクリプトエディタを起動させプロジェクトを新規作成しておく。コード.gs
主な処理内容
- アプリケーションにアクセスした際にHTMLページを返却
- スプレッドシートに格納された値を取得して辞書型のオブジェクトにして返却コード.gsfunction 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 > リファレンス
- 投稿日:2019-12-09T10:57:02+09:00
[ 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認定試験公式テキスト
- 投稿日:2019-12-09T09:48:54+09:00
Vueを使ってサイトを1ヶ月で作った話。そして、それを、1ヶ月使ってリプレイスした話。
この記事はAteam Hikkoshi Samurai Inc. & Ateam Connect Inc.(エイチーム引越し侍、エイチームコネクト) Advent Calendar 2019 9日目の記事になります。
はじめに
「引越し侍 まるごとスイッチ」
https://hikkoshizamurai.jp/switch/というサイトを12月2日にリリースしました。
引越しの際に必要な、電気やガスの切り替えが一括で行えるサイトで、
ゆくゆくは水道や金融機関、郵便など、引越しにまつわる、様々な手続きをサポートしていく予定です。この記事は、このサイトをVueを使って1ヶ月で作った話。
そして、それを1ヶ月使ってリプレイスをした話をします。簡単でテンポよく開発できる
Vue.js
、Nuxt.js
を設計せずに使うとどんなことが起こるのか、
どうすれば、Vue.js
、Nuxt.js
の良さを活かしつつ、開発ができるのか
是非見ていってください!TL;DR
Vue.js
、Nuxt.js
は簡単に使える。- が、、故に、きちんとやらないと破綻する。
- どんなに急いでいても、コンポーネント設計やVuexの設計、準備はきちんとしようねという話
背景
「引越し侍 まるごとスイッチ」
https://hikkoshizamurai.jp/switch/実はこのサイト、
内閣官房が推進している引越しワンストップサービスというものの協力主体として、
弊社が参画し、作ったサービスになります。制作に与えられた期限は約2ヶ月半。
(最初の半月はDB設計等をしていたので、フロントに与えられたのは実質2ヶ月)
限られたリソースの中で、仕様を詰めながら、
どんなサイトにするかを考え、作る。(もちろんバックエンドも、管理画面も)その中で僕たち制作メンバーが選んだフロント技術は
Vue.js
、Nuxt.js
です。
Vue.js
の良さは、簡単で柔軟性の高いというところ
Nuxt.js
の良さは、開発スピードが出せるという点です。ですが、その良さの反面、きちんとした設計をしないと、すぐに破綻する時がやって来ます。
この記事では、
Vue.js
、Nuxt.js
を使って1ヶ月でβ版を作った話。
そして、それを1ヶ月使ってリプレイスした話をします。最初の1ヶ月で何があって、残りの1ヶ月で何をしたのか、是非、見ていってください。
最初の1ヶ月
最初の1ヶ月、我々制作チームが奮闘したのは、とにかく早くリリースをするという点でした。
いわゆるβリリース
のようなものが1ヶ月後にあったのです。そうです。時間がなかったのです。。
みんなで決めて、とりあえず開発速度を意識した実装をすることにしました。
そして、(来ることは知ってたけど、思ったより早く)訪れたこと
- コピペ地獄
- どこか修正するとどこかがバグる
- Vuexに入れていたはずのデータがどこかで書き換わってる
- 書き方が統一されていない
- 変わり続ける仕様に対応できない
開発しはじめて、1ヶ月しか経っていないにも関わらず、こんなことが起きていました。
なぜこんなことになったのか?
コンポーネント設計をしなかった
これが、最大のミスです。
弊社では、他のプロジェクトでもVue.js
やNuxt.js
を使った経験があり、
その時はしていたはずのコンポーネント設計を怠りました。
これにより、コピペの嵐、何か仕様変更があると、全てを書き換える必要がありました。
当然、どこにバグを埋め込んでしまったのか検討もつかない状態になりました。Vuexを乱用しすぎた
何でもかんでもVuexに入れているプロジェクトをよく見ますが、
Vuexの乱用は完全なアンチパターンだと思っています。
思っていたのにやったのです。。すると、あるページで変更した内容が他のページに来た時にも反映されていたり、
値がなくて落ちたりするのです。。Linterを無視した
途中で流石にやばいと思って、コマンドを打って見たんです。
yarn lint ... 3043 problemsおわた
![]()
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-model
、v-bind
、v-on
を使えば解決します2. ページをまたいで必要な時も、値の格納は一回しかしない
一つのモジュールで値の格納があるのは一回で十分だと思います。3. ページをまたいで必要な時も、
<nuxt-child>
で代用できないか考える
nuxt-child
はNuxt.js
のページ単位で親子の関係を作ることができます。
1とほぼ同様で、親にデータを持たせたいが、ページ間を超えてデータが必要になる際に使います。4.
Atomic Design
の生体(organisms
)以上の単位でしか参照や格納はしない
原子(atom
)、分子(molecules
)の単位でVuex
にアクセスしてしまうと、
汎用性のないコンポーネントになってしまいます。ESLintやPrettierの設定をした
書き方の統一のため、ESlintとPrettierを導入しました。
VSCodeで自動フォーマットし、人によるばらつきが出ないようにしました。これにより生まれたこと
バグが少なく、スピードのある開発ができるようになった。
最初にコンポーネント設計や、Vuexの設計をしたことで、
この場合はどうする?、ここはこれで行こうと思うけど、いいかなーなど
今までになかったコミュニケーションが生まれました。
結果として、バグが入りにくく、スピードを加速させることができました。変更や新しいデザインに強くなった
コンポーネント設計をし、
slot
を用いた柔軟性の高いコンポーネントが作れたことにより、
新しいデザインが来た時も、同じコンポーネントを使い回せるようになった。
結果として、変化し続けるサービスのパフォーマンスを最大にすることができた。うまく、Vuexと付き合えるようになった。(サイトの表示速度も上がった)
あらゆるところから参照、値の格納をしていたVuexですが、
うまく、サイト全体で必要な情報だけ取得して来て、レンダリングすることで、
サイトの表示速度も上がりました。レビューの速度が上がった
小さな単位でレビューをすることが容易になったこと、
複雑な処理が分割されたこと、
Linterなどの導入により、書き方が統一されたことにより、
レビュー速度も上がりました。最後に2ヶ月を振り返って
最初「時間がないから、設計や設定をせずに行こう」という話をしましたが、
終わって見て振り返ると「時間がないからこそ、設計や設定を最初にきちんとしよう」
というのが正解だと思いました。実際、後半の1ヶ月の方が開発スピードは速く、バグにすぐさま対応できるようになっていたと思います。
そして、何より今回、サイトの書き換えをしたにも関わらず、スピードをあげて開発ができた背景には、
メンバー間のコミュニケーションがきちんと取れていて、
素直に改善をすることができるメンバーに恵まれたという点にあると思います。皆さんも
Vue.js
、Nuxt.js
で何かを作る際は、設計やルール、設定をきちんとみんなで話し合い、
スタートすることをお勧めします。ありがとうございました!
お知らせ
エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひともエイチームグループ採用ページよりご応募ください!
(Webエンジニア詳細ページ)よりお問い合わせ下さい。フロントエンンドエンジニアも募集しております
https://www.a-tm.co.jp/recruit/requirements/career/lifestylesupport-frontenddesigner/明日
いかがでしたでしょうか?
明日のエイチーム引越し侍、エイチーム コネクトアドベントカレンダーの担当は、
期待の新卒チーズ@cheez921です!是非、明日も見て下さい!
- 投稿日:2019-12-09T08:40:05+09:00
Vue.jsで開発をはじめよう
選んだ教材はこれ
超Vue JS 2 入門 完全パック - もう他の教材は買わなくてOK! (Vue Router, Vuex含む)
選んだ理由としては、
・評価が高かったこと
・必要条件が自分に合っていたこと
・タイトル見て、この教材1つでいいんだ〜って思ったから
以上の3点です。なぜVue.jsを学習しようと思ったのか
Railsの学習が一段落したところで、フロントエンドのフレームワークにも挑戦したいと感じていました。
フロントの開発は目に見えて反映されるので、僕自身やっていて楽しかったです。その中で最近流行り?で比較的入りやすそうなVue.jsに決めました。
正直フレームワークに関しては、ReactでもAngularでも良かったと思います。
サクッと決断して、サクッと取り組み始めることが大事だと思っています。
Vue.jsでアプリ作って公開するでぇ〜!!
お楽しみに!!
ではまた!
- 投稿日:2019-12-09T08:37:31+09:00
【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に参加してくれるのであれば、投稿者冥利につきます。
- 投稿日:2019-12-09T07:57:32+09:00
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する必要があるよということですね!
- 投稿日:2019-12-09T07:44:56+09:00
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.pyfrom __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」で処理が成功したか確認できます。さらに新しくブラウザを開いて、変更内容が反映されていればOKです。
請求アラーム設定もお忘れなく
意図しない高額請求が発生しないように CloudWatch のコンソール にて、請求アラームを設定しておきましょう。
Reference
- 投稿日:2019-12-09T07:13:13+09:00
AWS Amplify の ServiceWorker で Web Push 対応を実装してみる - クライアント編
Web Push、送りたいことありますよね。
この記事は Web Push 通知を使うアプリを AWS Amplify で作ってみる話です。
特にクライアント側を AWS Amplify でどう実装するかを見てみます。
当初サーバー側もあわせて書こうとしたのですが、分量が多くなりそうだったので今回は一旦クライアント編として、サーバー編はまた Amplify の API カテゴリを絡めたものを後日書きます(たぶん)。Web Push やりたいときにやらなきゃいけないこと
Web Push を運用しようとすると、けっこう色々やることがあります。
- Push を受け取るクライアント側
- Service Worker の実装
- 購読状態管理、Service Worker 登録等を行うフロントアプリケーションの実装
- 購読処理時に払い出されるエンドポイントをサーバーサイドに送信
- Push を送るサーバー側
- 秘密鍵・公開鍵の生成、管理
- クライアントから送られてきたエンドポイント情報をサービスのユーザーと紐付けて保存、管理
- Web Push 送信用ライブラリ を使って適切なタイミングで送信
等々です。Amplify の ServiceWorker クラスはこの中で、特に 1-2. 購読状態管理、Service Worker 登録等を行うフロントアプリケーションの実装 を助けてくれます。
また、その Amplify プロジェクトで Analytics カテゴリを有効にしている場合、ServiceWorker のライフサイクルイベントを自動で収集、可視化してくれる機能も Amplify が提供してくれます。前提条件
仕様
今回は、
- ブロック済でなければページを開いたときにプッシュ通知購読の許可を求める
- 購読の停止、停止後の再購読を行う UI、機能は提供しない ※
- 現在の購読状況(未購読、購読済み、ブロック済み)を表示する
- Push 通知がサポートされていない環境(Safari 等)ではそう表示する
- 購読済みの場合は、プッシュ通知に必要なサブスクリプション情報を表示する
というモノを実装することにします。
※ 実際のサービス運用時には、購読停止の際に Unsubscribe() の実行、サーバーサイドのデータベース更新等が必要になるでしょう
参考にした資料
この記事は以下2つのドキュメント・チュートリアルを参考にしています。
この記事内では 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.はい
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)
による動作です。公開鍵の取得と模擬的なサーバーサイドの用意
冒頭で書いたとおり、今回はフロント側について書きたいので、バックエンドは手軽に鍵生成や Push 送信のテストができるコンパニオンサイト、Push Companionを使います。
Push Companionを開くと、Public KeyとPrivate Keyが表示されています。
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/
を開き、通知許可のダイアログを表示します。ここで許可(Allow)すると、画面上に JSON 文字列が表示されます。
この 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かわいい"}
![]()
うまく実装されていれば、次のような通知が表示されます。
Vue アプリケーションのタブを閉じていたり、別 Window を表示したりしていても通知が表示されるでしょうか?通知をクリックしたとき、ちゃんと
service-worker.js
で記述した処理が実行されているでしょうか?確認してみてください。期待通りに動いていれば、これで Amplify を使ったクライアントサイドの Web Push 通知対応ができました。
Analytics の集計情報を見てみる
最初に、Service worker のイベントを集計するために Analytics カテゴリを追加してありました。ちゃんと動いているか見てみましょう。
$ amplify console analytics
Amazon Pinpoint のマネジメントコンソールが開いたら、 Analytics > Events を見てみます。
Filters を有効にすると、次のようなイベントが収集されています。Amplify が自動的に Service worker のライフサイクルや状態変化、メッセージングなどのイベントを集計してくれています。
補足
開発中に購読状態を変更したいとき
開発中は、一度購読を許可した後にもう一度もとに戻したい、拒否を取り消したいときがたくさんあると思います。
Chrome のアドレスバーのアイコンをクリックすると、そのサイトに対する購読状態を変更することができます。サーバーサイドの Push 送信実装について
今回はコンパニオンサイトに頼りましたが、実際にサーバーサイドからの Push 送信を自分で実装するときは Web Push 用のライブラリを使って実装することが一般的かと思います。
https://github.com/web-push-libs/
言語別のライブラリがあるので、必要のある方は見てみてください。
その辺の実装は、後日書くサーバー編で触れたいと思います。
その他考慮すること
前提条件にも書きましたが、今回の記事はクライアント側の Amplify に関わる部分にフォーカスしています。
サービスを運用する際は、これら以外に購読済みのユーザーが購読を停止するための UI や処理、ユーザーの Subscription 情報や状況を保存するバックエンドのデータベース等が必要になると思われます。
その辺も後日書くサーバー編に(ryまた、Appiterate による調べでは、不適切で不快な通知はユーザー離反の最も大きな要因になり得ます。ユーザー体験を十分に設計する必要があります。
[AWS Start-up ゼミ] よくある課題を一気に解説! 御社の技術レベルがアップする 2019 春期講習まとめ
- Amplify の
ServiceWorker
クラスは Web Push 購読処理を隠蔽して、ちょっと手軽にしてくれる- Amplify で
Analytics
カテゴリを有効にしていると、自動的に Service worker の挙動をトラッキングしてくれるみんなも Amplify で Web Push 処理しましょう。
- 投稿日:2019-12-09T04:16:15+09:00
【Nuxt.js】create-nuxt-appのすすめ 〜create-nuxt-appの質問に負けたくないあなたへ〜
この記事はエイチーム引越し侍 / エイチームコネクト Advent Calendar 2019の10日目の記事です。
前日は、こぼさん(@anneau)の『Vueを使ってサイトを1ヶ月で作った話。そして、それを、1ヶ月使ってリプレイスした話。』でした!
私も身近で引越し侍 まるごとスイッチを開発している姿を見ていたのですが、膨大な仕様のサービスを、2ヶ月という限られた期間で、スピード感もって開発していることがひしひしと伝わりました。
尊敬する大先輩の記事です。ぜひ読んでみてください!本日はぴかぴかの新卒1年生のちーずが担当いたします!
QiitaがPWAに対応していることに最近気づき、以前より記事を読むようにになりましたが、投稿は久しぶりになってしまいました
アイコンのQiitanのシルエットがかわいいので、スマホのホーム画面にQiitaを追加することをおすすめしますはじめに
Nuxt.jsを勉強し始めたときに、いちばん最初に
create-nuxt-app
をしてみました。
すると、すごい勢いで大量な質問されて、ってなっちゃいました。
そんな人が他にもいるかな?と思い、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
をみるだけでも、なるほどね〜ってなります。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に関しては、こちらの記事がわかりやすかったので、是非よんでみてください。
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
に関する詳しい情報はこちら。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.jsAnt Design Vue
シンプル且つ、ユーザビリティが良さそうな印象のフレームワーク。
元は、ReactのUIフレームワークであり、APIもそこそこ充実しています。
github star 8.8k コンポーネント、APIの充実度 ★★★☆☆ 難易度 低 適切なデバイス PC
Bootstrap Vue
![]()
BootstrapをVueのUIコンポーネントとして拡張してるため、もろBootstrapな見た目のフレームワークです。
ちょっとドキュメント読みづらいかなと感じました。
github star 10.5k コンポーネント、APIの充実度 ★★★★☆ 難易度 中 適切なデバイス PC/SP
Buefy
BulmaをVueのUIコンポーネントとして拡張してるため、もろBulmaな見た目のUIフレームワーク。
APIのドキュメントが読みやすく、シンプルで汎用性が高い印象。
github star 6.7k コンポーネント、APIの充実度 ★★★★☆ 難易度 低 適切なデバイス PC(SPもいけなくはない)
Bulma
シンプル且つ軽量なCSSフレームワーク。
機能は自分で実装したい!って人におすすめ。
github star 37.7k コンポーネント、APIの充実度 ★★☆☆☆ 難易度 高 適切なデバイス PC(SPもいけなくはない)
Element
見た目はシンプルだが、高機能なコンポーネントが多い。
数も充実しており、使ってる人も多い印象。
github star 42.7k コンポーネント、APIの充実度 ★★★★★ 難易度 低 適切なデバイス PC
Framevuerk
とてつもなくシンプルで最低限にコンポーネントが用意されているUIフレームワーク。
ただ、更新頻度も低く、汎用性も低いためおすすめはしません。
github star 194 コンポーネント、APIの充実度 ★☆☆☆☆ 難易度 中 適切なデバイス PC...?
iView
component数や機能面において過不足なくちょうどよい感じのUIフレームワーク。
ただ、日本人にユーザーが少なそう。。。
github star 900 コンポーネント、APIの充実度 ★★★☆☆ 難易度 中 適切なデバイス PC
Tachyons
マルチクラスで元のcomponentをアレンジしていく感じ。
ドキュメントがギャラリーになっており、自分の実現したいデザインを探しやすいかも?
github star 9.4k コンポーネント、APIの充実度 ★★★☆☆ 難易度 高 適切なデバイス PC/SP
Tailwind CSS
元のcomponentにマルチクラスをあててアレンジしていくCSSフレームワーク。
拡張性が高く、レイアウトも自由が効き、デザイン製も美しい。
github star 17.4k コンポーネント、APIの充実度 ★★★☆☆ 難易度 高 適切なデバイス PC/SP
Vuetify.js
マテリアルデザインのUIフレームワークで、アニメーションが美しい。
高機能かつ充実したcomponent量だが、ドキュメントだけ読みづらい。
github star 22.8k コンポーネント、APIの充実度 ★★★★★ 難易度 中 適切なデバイス PC/SP
※ 難易度は、「どれくらい簡易に動く機能が実装できるか」という基準で評価しているため、CSSのみのフレームワークは「高」とさせていただきます。
※ star数は、12月10日現在の数です。つくりたいものによって、使い分けるのがベストだとおもいますが、個人的におすすめなのが、BuefyとVuetify.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 MicroAdonisJs
Laravelっぽく書けるフレームワーク。
github star: 7.4k
Express
Node.js製のWebフレームワークといえば...ってくらい有名なフレームワーク。
REST APIの開発によく使われてるイメージです。
github star: 46.4k
Fastify
名前の通り、最速を目指してるwebフレームワークです。
github star: 12.7k
Feathers
リアルタイムアプリケーションとREST APIに特化したフレームワークです。
クライアントのみでも使えるみたい。
github star: 11.9k
hapi
軽量で拡張性の高いNode.jsフレームワーク。
github star: 11.8k
Koa
expressを開発したチームが、よりわかりやすく、軽量に作ったwebフレームワークです。
github star: 28k
Micro
高速かつわかりやすいフレームワーク。
下記記事がわかりやすかったです。
github star: 8.9k
Node.js でちょっとしたサーバーサイドやるなら、 Micro が良いかも
※ 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-modulePWA
PWA(Progressive Web Apps)は、webアプリをモバイルアプリのように使うことのできる仕組みです。
ホームに追加すると、アドレスバーがなくなり、プッシュ通知やオフライン利用などができたりします。すごい。
そのため、今後モバイルでの展開を考えてるサービスを作る際には、この項目をチェックすると良いと思います。今回インストールされるのは、
nuxt pwa
という、nuxtで簡単にPWAに対応したwebアプリを作れるパッケージです。PWAを理解するのは難しいですが、下記記事がわかりやすいと思います。
また、Googleが提供するチェックリストもあるため、参考になるかも...?
DotEnv
DotEnvは、環境変数を管理するモジュールです。
環境依存の設定やを管理する際に使えます。
(そのため、設定ファイルはgitignore
しましょう。)他にも、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 ◯ StyleLintESLint
ESLintは、JavaScriptのソースチェッカーです。
クォーテーションの種類やconsole.log
を許すかなど、様々なルールを設定し、
そのルールにあってるかをチェックしてくれます。Prettier
Prettierは、ルールに合ったコードに整形してくれるコードフォーマッタです。
Linterと親和性が高く、Linterに設定したルールに自動的に書き換えてくれたりします。また、ESLintとPrettierに関してはNuxt.jsの公式ドキュメントにも詳しく書いてあります。
また、下記記事がわかりやすかったため、参考にしてみてください。
Lint staged files
この項目を選択すると、
husky
とlint-staged
というモジュールが追加されます。
husky
は、gitコマンドをhookにして、指定したコマンドを叩くことができるモジュールで、
lint-staged
は、コミット前のファイルに強制的にlintingさせることができるモジュールです。
これらを組み合わせることで、lintの通ったソースコードのみしかcommitできない環境が出来上がります。StyleLint
StyleLintはその名の通り、スタイルシートのコードをチェックしてくれるツールです。
この項目をチェックすると、下記モジュールが入れられます。共同開発する場合は、なるべく上記のLintをチームでカスタマイズして使っていくことをおすすめします。
(6) テストフレームワーク
この項目では、Unitテストフレームワークを実行してくれるテストランナーを指定することができます。
自分はまだテストランナーを使ったことがないです...
本当に知見がないため、さらっと調査し、紹介します...
(大反省しております。。。)? Choose test framework (Use arrow keys) ❯ None Jest AVAJest
Facebook製のユニットテストツール
github star: 28.6k
AVA
早い・軽い・簡単が売りのテストツール
github star: 17.1k
※ star数は、12月10日現在の数です。
(7) レンダリングモード
Nuxtの最大の特徴である、レンダリングモードの指定ができます。
(心のなかで、なぜ最後のほうに聞いてくるんだ?って思ってます。最初に聞いてほしい...)? Choose rendering mode (Use arrow keys) ❯ Universal (SSR) Single Page AppUniversal (SSR)は、初回リクエストやリロードの際にサーバーサイドでレンダリングを行い、
Single Page App(SPA)では、クライアント側でjavascriptをレタリングします。どちらを選ぶべきかに関しては、下記記事がとってもわかりやすいです。
(8) 開発ツール
開発時に使うツールを選択することができます。
? Choose development tools ❯◯ jsconfig.json (Recommended for VS Code)jsconfig.jsonを設定することで、エディタ(VS Code)にこのコードはES6で書かれてますよということを伝えることができ、正しくシンタックスエラーがでたりハイライトしたりします。
以上で
create-nuxt-app
時の質問の解説をおわります。
この質問は日々アップデートされているため、項目が変化したりしますのでご注意を。。。4. スクラッチからはじめることもできる
Nuxt.jsのアプリ開発に慣れてきたら、スクラッチからはじめることをおすすめします。
ご紹介したモジュールや設定等を自分で一から追加、設定していくと、より自分に合ったNuxtアプリを作ることができます。
ハッカソンや短期開発の時は、create-nuxt-app
の方がおすすめです。終わりに
ここまで書いた感想
正直なところ、物量が多くてすごく疲れました。笑
しかし、記事を書いていくうちに発見も多かったし、自分自身とても勉強になりました。
サーバーサイドのフレームワークなんて、express以外全く知らなかったため、調べるのが楽しかったです。
まだ使えてないものも多いので、この機会にcreate-nuxt-app
しまくろうと思います!笑また、私と同じように、
create-nuxt-app
の質問に圧倒され、ってなる人もいると思います。
そんな方にこの記事が届くといいなぁって思います。めっちゃ参考にした文献
お知らせ
エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひともエイチームグループ採用ページ(Webエンジニア詳細ページ)よりお問い合わせ下さい。フロントエンンドエンジニアも募集しております
https://www.a-tm.co.jp/recruit/requirements/career/lifestylesupport-frontenddesigner/明日
明日のエイチーム引越し侍、エイチーム コネクトアドベントカレンダーの担当は、@Ingwardさんです!
どんな技術領域の記事なのか楽しみです...!
- 投稿日:2019-12-09T03:24:47+09:00
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の公式サンプルが動くくらいにはなってると思います。よかったらお試しください。
FORK Advent Calendar 2019
08日目 Timelineをもう一度 @AsaToBan
10日目 @karaage7 さんよろしくお願いします。
- 投稿日:2019-12-09T03:07:19+09:00
TypeScriptでVue.set/deleteを型安全にするライブラリ作った
この記事はVue #2 Advent Calendar 2019の11日目です。
昨日は @nishihara_zari さんの jQueryしか書けなかった自分がVueを使えるようになるまでの話と同じような環境の人たちへ でした。
はじめに
以前、TypeScriptでVue.setを型安全にしたいという記事を書ました。
今回はその記事で紹介した、Vue.set
(とVue.delete
)を型安全にする実装をライブラリ化したので、実装内容についてご紹介します。本文だけ読みたいという方は 実装について まで読み飛ばしてください。
ライブラリのご紹介
実装の紹介の前に、公開したライブラリについてご紹介させていただきます。
@lollipop-onl/vue-typed-reactive
はVueのグローバルにVue.typedSet
とVue.typedDelete
を、インスタンスにvm.$typedSet
とvm.$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.typedSet
はVue.set
の完全なエイリアスなので、全て置き換えても正常に動作すると思います(delete
も同様)。機能
typedSet
とtypedDelete
いずれも、プロパティと値の型安全以外にも余計な機能をつけました。
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.set
、Vue.delete
で回避することができます。恩恵
実際に開発に組み込んでみて、VuexストアのMutationでとても活躍しています。
VuexのモジュールモードはモジュールがネストするとMutation内でのVue.set
の使用が必須となります。せっかくTypeScriptを使っているのにStateへの代入時の型不整合がチェックできないのはそこそこしんどいと思っていました。
また、プロパティ名の変更などもVue.set
、Vue.delete
では検知できないので手動リファクタが必要なってきます。そういうのって面倒ですよね。
Vue.typedSet
とVue.typedDelete
を使ってよりリファクタリングのしやすいVueアプリにしませんか?実装について
ここからは、
vue-typed-reactive
の実装内容についてご紹介します。以降はほぼほぼTypeScriptに関する内容になります。
Vueのアドベントカレンダーなのにすみません。。Vueの拡張
Vue.typedSet
、Vue.typedDelete
というメソッドはVueには存在しません。
それぞれVue.set
、Vue.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.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 }
とkeyof
とin
を使うことにより、元のオブジェクトと同じキーを持つ別の型を生成することができます。このとき、マッピングに使えるのが
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
について、P
がreadonly
でない場合に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);関数の引数をテストしたい場合などは自前で型定義が必要だったりしますが、型定義にもテストを書けるのはリファクタリングなどがしやすいので助かります。
出典