- 投稿日:2020-09-07T23:12:21+09:00
Web 素人女子大生が Vue.js+Firebase+GAS で部活をハックする!!#1 環境構築
はじめに
Vue.js を愛してやまない友人から Vue 教の洗礼を受け、丸腰で Web の世界に足を突っ込んだ女子大生です。
素人が泥臭く開発しながらボチボチ書いていくので読みにくいと思いますが間違い等あったらご指摘お願いしますきっかけ
私の所属する部活の学生連盟(以後、学連)はひじょーーーにアナログで、毎回仕事が大変すぎました(泣)
そこで、部活の Web サイトを魔改造しよう!!とおもって開発をスタートしました。(これを書いている段階で少し開発を進めているため、最初のほうは記憶が曖昧です)こんなもの作りたいな
今までメールで行っていたこと+ローカルで行っていたことを搭載し、以下の機能を実装したいと思います。
また、随時 MVP としてメンバーに使ってもらい機能を追加していく予定です。
- 認証機能(メアド、パスワード)
- 選手登録
- 大会エントリー
- コート割り設定
- 点数計算
- 大会結果出力
また、機械慣れしていないユーザーを想定し、
- スマホ対応画面
- 直感的な UI
を意識していきたいと思います。
環境構築
やっと本題です。
私の既存の開発環境はこちら。
- VScode
- Node.js 14.5.0
- npm 6.14.8
- macOS catalina 10.15.6
Vue プロジェクト作成
$ npm install -g vue-cliこれでグローバルに vue-cli のインストールができるので、どこからでも vue コマンドが効くようになります。
確認してみました。$vue -V @vue/cli 4.5.4無事にインストールできました!
早速プロジェクト作成に入ります。
ここで大事なのは、Vue init はしないことです。← あとで環境変数の読み込みがめっっっちゃ大変になります。$vue create gkrn Default ([Vue 2] babel, eslint) Default (Vue 3 Preview) ([Vue 3] babel, eslint) Manually select featuresrouter を使いたかったので Manually select にしました。この時点では、Router、Vuex、Babel を選択しました。また、Vue のバージョンは 2.x にしました。あとは大体指示にしたがっていれば大丈夫でした。
確認です。$cd gkrn $npm run serve無事に Welcome されました!
ディレクトリはこんな感じです。gkrn/ ├node_modules | ├public │ └ index.html │ ├src | ├components | | └ Helloworld.vue | ├router | | └ index.js | ├app.vue | ├main.js | └assets | └package.json今回はサーバーレスの SPA を想定しているため、Firebase Javascript SDK を使います。Firebase にやってもらうことは以下の通りです。
- 認証(Firebase authenication)
- データベース(Firestore)
- デプロイ(Hosting)
まずは、firebase のサイト(https://console.firebase.google.com/)
から新しいプロジェクトを作成しました。初めて使う時はアカウント登録を求められますが、google アカウントさえあれば大丈夫です。GUI なので、画面の指示にしたがってポチポチしました。今回はwebアプリを設定します。
こんな感じでAPIkeyなどが発行されます。コピペして、/public/index.htmlの<body>タグの下部に貼り付けます。最後にVue.jsでFirebaseを呼び出すためのモジュールをインストールしました。$npm install -save firebaseを叩きます。--saveが入っているとpackage.jsonに記載されるので、デプロイしてもモジュールが使えます。
これでFirebase JavaScript SDKの導入は終わりです。今回は Hosting も使うので、firebase CLI の導入から行いました。
npm install -g firebase-tools
確認
$firebase -V 8.9.2成功しました。次に次に init して、firebase とローカルディレクトリを連結させます。
ターミナルにおめでたい感じで FIREBASE が表示されたのち、何を使うか聞かれます。◯ Database: Deploy Firebase Realtime Database Rules ◯ Firestore: Deploy rules and create indexes for Firestore ◯ Functions: Configure and deploy Cloud Functions ◯ Hosting: Configure and deploy Firebase Hosting sites ◯ Storage: Deploy Cloud Storage security rules ◯ Emulators: Set up local emulators for Firebase featuresこんな感じで何を使うか聞かれます。今回は Hosting を使いました。(後から追加できます)
? Please select an option: (Use arrow keys) ❯ Use an existing project Create a new project Add Firebase to an existing Google Cloud Platform project Don't set up a default project次にプロジェクトを聞かれます。さっき作ったやつを選択。
? What do you want to use as your public directory? public ? Configure as a single-page app (rewrite all urls to /index.html)? YesHosting の設定です。今回は SPA なので、SPA を選択。public は vue create して作られたやつです。無事に init できました。
次の目標
今回で環境設定を終えました。次から開発に入ります!!
- 投稿日:2020-09-07T23:12:21+09:00
Web 素人大学生が Vue.js+Firebase+GAS で部活をハックする!!#1 環境構築
はじめに
Vue.js を愛してやまない友人から Vue 教の洗礼を受け、丸腰で Web の世界に足を突っ込んだ女子大生です。
素人が泥臭く開発しながらボチボチ書いていくので読みにくいと思いますが間違い等あったらご指摘お願いしますきっかけ
私の所属する部活の学生連盟(以後、学連)はひじょーーーにアナログで、毎回仕事が大変すぎました(泣)
そこで、部活の Web サイトを魔改造しよう!!とおもって開発をスタートしました。(これを書いている段階で少し開発を進めているため、最初のほうは記憶が曖昧です)こんなもの作りたいな
今までメールで行っていたこと+ローカルで行っていたことを搭載し、以下の機能を実装したいと思います。
また、随時 MVP としてメンバーに使ってもらい機能を追加していく予定です。
- 認証機能(メアド、パスワード)
- 選手登録
- 大会エントリー
- コート割り設定
- 点数計算
- 大会結果出力
また、機械慣れしていないユーザーを想定し、
- スマホ対応画面
- 直感的な UI
を意識していきたいと思います。
環境構築
やっと本題です。
私の既存の開発環境はこちら。
- VScode
- Node.js 14.5.0
- npm 6.14.8
- macOS catalina 10.15.6
Vue プロジェクト作成
$ npm install -g vue-cliこれでグローバルに vue-cli のインストールができるので、どこからでも vue コマンドが効くようになります。
確認してみました。$vue -V @vue/cli 4.5.4無事にインストールできました!
早速プロジェクト作成に入ります。
ここで大事なのは、Vue init はしないことです。← あとで環境変数の読み込みがめっっっちゃ大変になります。$vue create gkrn Default ([Vue 2] babel, eslint) Default (Vue 3 Preview) ([Vue 3] babel, eslint) Manually select featuresrouter を使いたかったので Manually select にしました。この時点では、Router、Vuex、Babel を選択しました。また、Vue のバージョンは 2.x にしました。あとは大体指示にしたがっていれば大丈夫でした。
確認です。$cd gkrn $npm run serve無事に Welcome されました!
ディレクトリはこんな感じです。
gkrn/ ├node_modules | ├public │ └ index.html │ ├src | ├components | | └ Helloworld.vue | ├router | | └ index.js | ├app.vue | ├main.js | └assets | └package.jsonFirebase環境構築
今回はサーバーレスの SPA を想定しているため、Firebase Javascript SDK を使います。Firebase にやってもらうことは以下の通りです。
- 認証(Firebase authenication)
- データベース(Firestore)
- デプロイ(Hosting)
まずは、firebase のサイト(https://console.firebase.google.com/)
から新しいプロジェクトを作成しました。初めて使う時はアカウント登録を求められますが、google アカウントさえあれば大丈夫です。GUI なので、画面の指示にしたがってポチポチしました。今回はwebアプリを設定します。
こんな感じでAPIkeyなどが発行されます。コピペして、/public/index.htmlの<body>タグの下部に貼り付けます。最後にVue.jsでFirebaseを呼び出すためのモジュールをインストールしました。$npm install -save firebaseを叩きます。--saveが入っているとpackage.jsonに記載されるので、デプロイしてもモジュールが使えます。
これでFirebase JavaScript SDKの導入は終わりです。今回は Hosting も使うので、firebase CLI の導入から行いました。
npm install -g firebase-tools
確認
$firebase -V 8.9.2成功しました。次に次に init して、firebase とローカルディレクトリを連結させます。
ターミナルにおめでたい感じで FIREBASE が表示されたのち、何を使うか聞かれます。◯ Database: Deploy Firebase Realtime Database Rules ◯ Firestore: Deploy rules and create indexes for Firestore ◯ Functions: Configure and deploy Cloud Functions ◯ Hosting: Configure and deploy Firebase Hosting sites ◯ Storage: Deploy Cloud Storage security rules ◯ Emulators: Set up local emulators for Firebase featuresこんな感じで何を使うか聞かれます。今回は Hosting を使いました。(後から追加できます)
? Please select an option: (Use arrow keys) ❯ Use an existing project Create a new project Add Firebase to an existing Google Cloud Platform project Don't set up a default project次にプロジェクトを聞かれます。さっき作ったやつを選択。
? What do you want to use as your public directory? public ? Configure as a single-page app (rewrite all urls to /index.html)? YesHosting の設定です。今回は SPA なので、SPA を選択。public は vue create して作られたやつです。無事に init できました。
次の目標
今回で環境設定を終えました。次から開発に入ります!!
- 投稿日:2020-09-07T22:16:50+09:00
Vue.js + FireBaseで作成した食事管理のポートフォリオ
このポートフォリオを作成した背景
日々の食事を考える際に
これこの前食べたばっかりだよ〜、今日のご飯何にしようか?自炊?外食?といった
些細な迷いや悩みを解決する目的で開発しました。使用技術
フロントエンド
Vue.js(VueRouter,Vuex,Vuetify,axios)バックエンド
Firebase(Hosting,Authentication,CloudFirestore)主な機能一覧
・会員登録、ログイン、ログアウト
・食事内容CRUD機能
・楽天レシピAPIからレシピを取得しピックアップを一覧表示、またキーワード検索
・飲食店検索(ぐるなびAPI)使用方法
新規登録
アプリにアクセスすると登録画面が表示されるので新規登録ボタンを押下してフォームにemailとpasswordを登録します。ログイン
既に新規登録が完了している場合はログインボタンを押下して登録したemailとpasswordを入力してログインしますテスト用アカウント
GitHub: https://github.com/shiropt/recal.git
URL: https://recal-f1a64.web.app
email: recal@test.ne.jp
password: testpass
ユーザーの管理はfirebaseのAuthenticationを使いました1.最初にアクセスすると説明文が記載された画面が描画されます。
認証画面左側に新規登録とログインボタンを配置し、router-linkで選択したフォームを描画しています2.新規登録すると同時にログインユーザーの情報をfirestoreに保存しています。
3.ログインするとヘッダーに各機能のボタン一覧、右側に登録したメニューが表示されます。
登録したメニューはVueライフサイクルのcreatedでfirebaseからログインしているユーザーとfirebaseに登録されているユーザーのidが一致したデータを取得しstoreに格納、stateから配列に格納し、v-forで一覧表示しています。4.ログイン時のデフォルトではレシピを見るの画面が描画されます。
ヘッダーのボタン一覧はv-ifでユーザーがログインしていれば表示されるようにしています。
レシピを見る、外食する、ピックアップはそれぞれrouter-linkで遷移
記録するボタンとログアウトボタンはコンポーネントを作っています。投稿、編集、削除
投稿は1日1回
編集の場合はフォームに既存のデータを渡し、セットする
当日の削除するとまた投稿が出来るようになる
ピックアップ
- 楽天レシピAPIから取得したピックアップメニュー4件を一覧表示
- 作り方を見るボタンから楽天レシピのメニュー詳細へ飛ぶ
レシピを見る
- 検索フォームからキーワードを入力でインクリメンタルサーチで候補が表示される
- 検索候補は楽天レシピAPIから取得
- 検索候補クリックで外部サイト(楽天レシピの該当メニュー)へ飛ぶ
外食する
- 検索フォームはフリーワードとエリア検索が存在
- どちらかのフォームにも入力がない状態で検索ボタンを押すとエラーメッセージを表示
- 検索結果はぐるなびAPIから取得した飲食店情報を表示
- 予約するボタンクリックでぐるなびの店舗詳細ページへ飛ぶ(画面幅によってスマホ用とPC用のリンクを変えるよう実装)
レスポンシブ対応
スマートフォンようにレスポンシブデザインも用意
ヘッダーのボタンはハンバーガーメニューで対応
開発して学んだ事
・APIの使い方、非同期処理
API導入なんて難しそう、無理!と抵抗があったのですが、youtubeを参考に実装してみると、実装出来て面白かった。
※参考にした動画 https://www.youtube.com/watch?v=yxhNo1xTOlM
同時に扱う非同期処理についても理解が深まった・コンポーネント間のデータのやりとり、Vuex
propsやemitを使う機会が多かったのでデータや関数の渡し方の理解が深まった
Vuexを理解して使えるようになた。小規模なアプリなら不要かもしれませんが、Vuexにデータを保存したり、引っ張って来る事で実装が出来る事も多かった。・firebaseの使い方
認証、CRUD機能、デプロイといったバックエンドの実装がfirebaseを使って出来るようになった。
覚えてしまえばかなり楽。・ロジックを考える楽しさ
この機能はどうやって実現しようか〜と悩んで悩んで実装に移すのは楽しかったw反省点
・Gitのコミットが途中から荒くなったこと。
次回からの開発では実務でGitを使う事を見越してブランチを切り、プルリクも書くようにする・コンポーネントをもっと細かい単位で作成した方がいい
1ファイルのコードの記述量が多くなり、可読性が悪く、メンテナンスがしにくくなった・storeの分割
1つのstoreに全ての処理を記述した為、こちらも見にくい。・何を実装したのかをコメントに残しておく
後から見た際にこのメソッドは何をしていたっけ?という事がしばしばあったので、コメントは残しておいた方がいい総括すると、チームを意識した開発が出来ていなかったことが反省点。
可読性やメンテナンスを意識して次回の開発に繋げる。終わりに
最後までご覧いただきありがとうございました。
ソースコードに関しまして、ご指摘、ご意見などいただけると嬉しいです!
- 投稿日:2020-09-07T21:57:02+09:00
【Nuxt.js】v-forの一意なマーキングを使って削除ボタンを作る
v-forで出現したコンポーネントを削除するボタンを実装する
まずは、それぞれの要素にマーキングし、削除ボタンも一意化する
template<v-col cols="4" v-for="(feed, index) in feeds" :key="index"> <v-card> <v-card-actions> <v-btn @click="deleteFeed(index)" icon>RailsAPIに、削除リクエストを送る
scriptmethods: { async deleteFeed (index) { await this.$axios.$delete(`/api/v1/feeds/${this.feeds[index].id}`) } }index番号から特定した、配列feedsの要素idを用いてリクエストを送る
data内の配列feedsの要素を削除する
scriptdata () { return { feeds: [ { id: 1, title: "東京中央銀行" }, { id: 2, title: "東京セントラル証券" }, { id: 3, title: "電脳雑伎集団" }, { id: 4, title: "スパイラル株式会社" }, ], } }, methods: { async deleteFeed (index) { this.feeds.splice(index, 1) } }配列番号indexを用いて、特定の配列feedsの要素を削除している。
spliceメソッドについてはこちら最終的なソースコード
<template> <v-col cols="4" v-for="(feed, index) in feeds" :key="index"> <v-card> <v-card-actions> <v-btn @click="deleteFeed(index)" icon> <v-icon>mdi-close</v-icon> </v-btn> </v-card-actions> </v-card> </v-col> </template><script> export default { data () { return { feeds: [ { id: 1, title: "東京中央銀行" }, { id: 2, title: "東京セントラル証券" }, { id: 3, title: "電脳雑伎集団" }, { id: 4, title: "スパイラル株式会社" }, ], } }, methods: { async deleteFeed (index) { console.log(`${index}:${this.feeds[index].id}`); const res7 = await this.$axios.$delete(`/api/v1/feeds/${this.feeds[index].id}`) this.feeds.splice(index, 1) return console.log(res7.status); } } } </script>おまけ 一意なマーキングを要素のidにする
<v-col cols="4" v-for="feed in feeds" :key="feed.id"> <v-card> <v-card-actions> <v-btn @click="deleteFeed(feed.id)" icon> <v-icon>mdi-close</v-icon> </v-btn> </v-card-actions> </v-card> </v-col>もう一つv-forのマーキングで、idキーで付与する場合もあるが、、、複雑で冗長化するだろう。。。
- 投稿日:2020-09-07T20:37:28+09:00
Vuetify でフィルタリング機能付きセレクトボックスを実装する
Vuetifyのv-autocompleteを利用して絞り込み機能付きのセレクトボックスを実装します。
https://vuetifyjs.com/ja/components/autocompletes/
vueでの記述は下記のようになります
page.vue<v-autocomplete :items="users" item-value="id" item-text="name" placeholder="ユーザーを選択してください" v-model="user" autocomplete="off" > </v-autocomplete>methodは以下のようになります
page.vuedata() { return { users: [] }; }, methods: { userListLoad() { axios.get('https://example.com/users') .then( result => { Object.entries(result.users).forEach(user => this.users.push({ name: user["name"], id: user["id"] }) ); }, err => { console.log(err); } ); }, },かんたんですね。
- 投稿日:2020-09-07T19:54:36+09:00
Vuetify使うとmarginやpaddingの調整がめっちゃ楽な件
Vuetifyとは
VuetifyとはVue.jsやNuxt.jsのプロジェクトで使用することのできるUIライブラリです。
※Vuetify実際に私もよく使っていて、とても気に入っています。
Vuetifyの書き方は少し特殊なので慣れるまではちょっとややこしいかもしれませんが、慣れるとパパッとUIの実装ができるのでオススメです!本題
本題のmarginやpaddingの設定ですが、
実際どんな感じで書くのかというと、こんな感じ。index.vue<div class="my-5" />ん?....マイ? - 5?
って感じですよね。
実はこれは一文字一文字に意味があります。
property
感の鋭い方はもうお分かりかもしれませんが、
最初の「m」はmarginの「m」です。ということは...
paddingの場合は「p」と書きます。
それだけです。
direction
そして、次の「y」という文字。
これはy軸方向の「y」という意味です。
つまり上下を対象に適用させるということです。となると、「x」は左右を対象とします。
他にも、
・「t」 Top
・「b」 Bottom
・「l」 Left
・「r」 Right
・「a」 全方向
などがあります。
※「-(ハイフン)」に関してはただのハイフンです。
特に意味はありません。size
最後の「5」という数字、これはsizeを表すもので、
「1」が4pxを表しています。なので「5」の場合は5×4で20pxです。
つまり「my-5」は?
上下にマージンを20px適用するという意味になります!
ネガティブ margin・padding
そして個人的にめっちゃイイ!と思ったのが、
ネガティブ(マイナス)な値も設定できるという点です!これはとても便利!!
ネガティブを設定するには、
size(数字の部分)の前に、「n」を追加します。なので「my-n5」と書くと、
上下にネガティブマージンを20px適用するという意味になります。最後に
少しでもVuetifyに興味を持っていただけましたでしょうか?
Vuetifyを使うと直接タグ内に書けるので、
わざわざCSSをstyleに分けて書かなくて済みますし、
コード量も肥大化しにくく且つ、開発スピードも上がるのでメリットは大きいかと思います!また今回紹介したmarginとpadding以外にもたくさんの機能があるので、他の機能も是非見てみてください。
ある程度のことはVuetifyで再現できるかと思います。
※VuetifyただVuetifyも完璧ではないので、
私も基本Vuetifyで書いて、どうしてもVuetifyでは再現できないところに関してだけCSSを書いているという感じで開発しています。ご参考になれば幸いです!
最後までご覧下さり、ありがとうございました。
- 投稿日:2020-09-07T15:42:06+09:00
[Nuxt] emulatorでNuxt & Cloud functionsの開発を速くする
はじめに
cloud functionsを修正したり、ログ差し込むだけで、いちいちデプロイして動作確認するのは時間がかかって面倒!
なので、開発中は一切デプロイが不要になるエミュレータを使うことをおすすめします!前提
- Nuxt(Vue)とfirebase(Cloud functions)で既に開発していること
- この記事ではtypescriptで開発しています
- 最新版の
firebase-admin
(確か古いfirebase-adminでは動作しなかった気がする)// 簡単なディレクトリ構成 . ├── functions └── nuxt.runtimeconfig.json作成
functionsの環境変数をエミュレータで読み込むために
.runtimeconfig.json
を作成します$ cd functions $ touch .runtimeconfig.json
firebase functions:config:get
の内容をコピーしますfunctionsのscript
functions/package.json"scripts": { "emulate": "yarn build && firebase emulators:start --only functions --inspect-functions 9229", "watch": "./node_modules/.bin/tsc --watch" }
watch
は修正した内容をemulatorに反映させるため必要その他
functionsでemulator用に切り替えないといけないものがある場合は、
process.env.FUNCTIONS_EMULATOR
で判定して対応する
(CloudSQLはsocketPathが設定されてると動かないとかがある)Nuxt側
functionsのURLをエミュレータのものに差し替える
https://asia-northeast1-xxxxx.cloudfunctions.net ↓ http://localhost:5001/xxxxx/asia-northeast1xxxxxはfirebaseの
projectId
が入る
firebase.initializeApp
しているファイルがあると思うので、その下の行で以下の内容を記述functions.useFunctionsEmulator('http://localhost:5001');nuxtのscripts
EMULATEの環境変数を入れて、URLなどの切り替えをするといいと思います。
nuxt/package.json"scripts": { "dev:emulate": "EMULATE=TRUE nuxt-ts" }実行
それぞれ別のタブで実行する
$ cd functions $ yarn emulate$ cd functions $ yarn watch$ cd nuxt $ yarn dev:emulate
- 投稿日:2020-09-07T02:17:48+09:00
Vuetify でウィンドウの縦幅に合わせてコンテンツサイズを動的に配置・調整する。
はじめに
Vuetifyはグリッドシステムを備えており、これを活用すればウィンドウの横幅に対してコンテンツが動的に配置・調整されます。
Grid system
https://vuetifyjs.com/ja/components/grids/ただ、ウィンドウの縦幅に対する配置・調整が思い通りにならない場面があったため、調査・実施した結果を残します。
具体的に説明すること
次のような4つのメニューボタン(アイコンは適当)を表示する画面があったとして
縦幅の大きな余白が気になるので、次のように画面の縦幅が埋まるようにメニューボタンを表示できるようにしたいと思います。
※ iPhoneの場合はこうできます。画面サイズが変わっても、実際の画面に応じてメニューボタンのサイズが調整されます。
環境
- Vue.js 2.6.12
- Vuetify 2.3.10
結論
Home.vue<template> <div class="Home"> <v-container fluid > <v-row> <v-col cols="12"> <v-row v-resize="onResize" :style="style" > <v-col cols="6" v-for="menu in menus" v-bind:key="menu.key"> <v-btn color="accent" block height='100%' > <v-icon :size="iconSize">{{menu.icon}}</v-icon> </v-btn> </v-col> </v-row> </v-col> </v-row> </v-container> </div> </template> <script> export default { name: 'Home', data: () => ({ menus: [ { title: 'home', icon: 'mdi-home' }, { title: 'currency', icon: 'mdi-currency-cny' }, { title: 'gift', icon: 'mdi-gift' }, { title: 'kaji', icon: 'mdi-washing-machine'}, ], windowSize: { x: 0, y: 0, }, iconSize: 0 }), mounted () { this.onResize() }, computed: { style () { return 'height: ' + this.windowSize.y * 0.8 + 'px;' } }, methods: { onResize () { this.windowSize = { x: window.innerWidth, y: window.innerHeight } this.iconSize = window.innerHeight * 0.1 }, }, components: { }, } </script>詳細
基本的な方針としては、windowオブジェクトから取得したウィンドウサイズに応じてコンテンツのstyleを設定します。
また、ウィンドウサイズ変更時に呼び出されるコールバック関数を用意し、ウィンドウサイズに応じてstyleも更新されるようにします。
Resizing directive
https://vuetifyjs.com/ja/directives/resizing/このあたりの実装は以下が該当します。
mountedでもonResize()を呼び出すことで初回表示時にwindowSizeに値が格納されるようにします。
computedのstyle()はディレクティブに設定するstyleのとして、windowSizeの値に応じてheightのピクセルを返すようにしています。ウィンドウサイズの0.8倍としているのは決め打ちです。また、onResize()でアイコンサイズを動的に変更するようにしました。ウィンドウサイズ縦幅の0.1倍としているのは、これまた決め打ちです。
<script> export default { name: 'Home', data: () => ({ windowSize: { x: 0, y: 0, }, }), mounted () { this.onResize() }, computed: { style () { return 'height: ' + this.windowSize.y * 0.8 + 'px;' } }, methods: { onResize () { this.windowSize = { x: window.innerWidth, y: window.innerHeight } this.iconSize = window.innerHeight * 0.1 }, }, </script>onResize(コールバック関数)とstyleの設定は、コンテンツ()の親要素()で設定します。
ここで必要なのは、<v-btn>のheight='100%'
を設定しておくことで、ボタンがいい感じにウィンドウサイズ縦幅に合わせて調整されます。... <v-col cols="12"> <v-row v-resize="onResize" :style="style" > <v-col cols="6" v-for="menu in menus" v-bind:key="menu.key"> <v-btn color="accent" block height='100%' > <v-icon :size="iconSize">{{menu.icon}}</v-icon> </v-btn> </v-col> </v-row> </v-col> ...以上です。
- 投稿日:2020-09-07T02:10:36+09:00
Vue3のcomposition-apiで金魚を大量に泳がせたのでソースと解説
こんにちは。絵描き兼フロントの人の「ゆき」です。
先日Vue.jsのオンラインMeetupで「viteではじめるVue3 + TypeScript」という発表をしました。Vue3の新しい開発環境であるViteの解説として簡単な「金魚が泳ぐデモ」を作ったのですが、LTではこのデモをほとんど見せる時間がなかったので、今日はQiitaで解説して元を取ろうと思います
作ったもの: https://yuneco.github.io/vite-kingyo/
リポジトリ: https://github.com/yuneco/vite-kingyoリポジトリクローンして動かす時は
yarn; yarn dev
だけでOKです。この記事ではVue3の込み入った話やViteの話は書きませんが、Vue3のcomposition-apiでインタラクティブなアニメーションを作るためのポイントは適時挟んでいきます。composition-apiの動作する小さなサンプルアプリとして参考してもらえれば幸いです。
まずはCSSで金魚を作る
別に画像でも良いのですが、せっかくなのでちょっとCSS芸みたいなことをして
div
一つで金魚を作ってみたいと思います。See the Pen BaKJdYa by Yuki (@Yuneco) on CodePen.
クラス名が
Kingyo
のdivを作って、styleで色を指定すると良い感じに金魚を表示します。<div class="Kingyo" style="color:#f63;" ></div>CSSはちょっと長いですが、
border-width
を使って四角の一箇所だけに切り欠きを入れることで口や尾びれを表現している部分がわかればさほど難しくはないと思います。描画もさほど重くないはず。関数コンポーネントで金魚のコンポーネントを作る
この金魚divをVueで動かしてアニメーションさせます。
アニメーションと言ってもCSS Transformで座標変えたりするだけです。Vueコンポーネントとして特別な状態管理は不要なので、関数コンポーネントにしましょう。受け取ったpropsを使ってstyle
をセットしているだけです。/src/components/Fish.tsimport { h } from "vue"; type Props = { x: number; y: number; angle: number; color: string; scale: number; }; export const Fish = (props: Props) => { const scale = props.scale || 1; const style = `color: ${props.color};transform: translate(${props.x}px, ${ props.y }px) rotate(${props.angle}deg) scale(${scale}, ${ scale * 0.8 }) rotate(${-45}deg);`; return h("div", { class: "FishRoot", style }); };金魚をたくさん配置する
金魚のコンポーネントができたので、これをたくさん並べる
FishLayer
コンポーネントを作ります。/src/components/FishLayer.vue<template> <!-- 指定の匹数の金魚を表示するレイヤーです --> <div class="FishLayerRoot"> <Fish v-for="fishProps in stageState.fishList" :key="fishProps.id" class="FishElement" :x="fishProps.position.x" :y="fishProps.position.y" :angle="fishProps.angle" :color="fishProps.color" :scale="fishProps.scale" /> </div> </template>テンプレート部はシンプルですね。
stageState.fishList
というところに格納されている金魚の情報をもとにFish(金魚)コンポーネントを表示しているだけです。スクリプト部はちょっとややこしいので、⭐️印のポイントごとに分けて説明します。
/src/components/FishLayer.vue/** * 金魚レイヤーの状態を管理する型 */ type StageState = { fishList: FishModel[]; // ⭐️POINT1: 外部のFishModelクラスで金魚の状態を管理 }; export default defineComponent({ name: "FishLayer", components: { Fish }, props: { /** 最大金魚数:この数まで金魚が追加されます */ maxFish: { type: Number, default: 50 }, }, setup(props, ctx) { // state: レイヤーの状態 const stageState = reactive<StageState>({ fishList: [], }); // state: マウスの座標を状態として利用 const { mousePos: destination } = useMouse(); //⭐️POINT2: useMouseでポインター座標の管理を分離 // computed: 現在の金魚数 const fishCount = computed(() => stageState.fishList.length); // method: 金魚の位置や速度を更新するメソッド const updateFish = () => { const destPoint = new Point(destination.x, destination.y); stageState.fishList.forEach((fish) => fish.update(destPoint)); }; // method: 金魚を追加するメソッド const addFish = () => { stageState.fishList.push(new FishModel()); ctx.emit("count-changed", fishCount.value); }; // method: 金魚を削除するメソッド const removeFish = () => { stageState.fishList.shift(); ctx.emit("count-changed", fishCount.value); } // 描画フレームごとに呼ばれる処理。金魚の状態を更新する useAnimationFrame(() => { //⭐️POINT3: requestAnimationFrameの管理もuseで分離 updateFish(); if (fishCount.value < props.maxFish) { addFish(); } else if (fishCount.value > props.maxFish) { removeFish(); } // trueを返すとunmountまでの間繰り返し呼ばれる return true; }); // クリック時の処理。金魚が逃げるようカーソル方向と逆の力を与える useClick(() => { stageState.fishList.forEach((fish) => fish.setForce(-1 - Math.random() * 4) ); }); return { stageState, }; }, });⭐️POINT1: 外部のFishModelクラスで金魚の状態を管理
Vueに限らず、インタラクティブなアニメーションをwebで作ろうとすると、良い感じの動きを試行錯誤するうちにすぐコードが謎の定数まみれの数式で埋まっていきます。これ自体は遊びで作る分には問題ないとは思うのですが、Vueのロジックと絡み合うと正直辛いです
好き嫌いはあると思いますが、この手のプログラムでは、金魚の位置や動きといった金魚に属するデータと振る舞い(メソッド)はオブジェクト指向的な考え方で独立した「金魚クラス」に押し込めるほうがクリーンです。
今回は
FishModel
という金魚の状態と動きを定義したクラスをVueとは無関係な場所に作成して、この中で動きの試行錯誤を行うようにしています。FishModel.tsexport class FishModel { readonly id = instanseCount++; /** 金魚の位置 */ position = randomPoint(); /** 金魚の向き */ angle = Math.random() * 360; /** 金魚の速度ベクトル */ vector = new Point(); /** ゴールに向かう力の強さ */ force = DEFAULT_FORCE; /** 金魚の色 */ color = randomFishColor(); /** 金魚のサイズ */ scale = randomScale(); /** * 金魚の位置と速度を更新する * @pparam destPoint 金魚が目指す点 */ update(destPoint: Point) { // 位置・座標を計算して更新 // ... 略 ... } /** * 金魚が「目指す点」に向かう力を設定します。負値を指定すると反発して点から逃げます * @param value */ setForce(value: number) { this.force = value; } }
update
メソッドの中身等、実装はかなり汚いですが、逆にいろいろ試行錯誤したい部分をVueコンポーネントから切り離せていることがわかるかと思います。⭐️POINT2: useMouseでポインター座標の管理を分離
useMouse
はcomposition-apiの解説で必ずと言って良いほど出てくるサンプルですが、実はインタラクティブなイベント処理を書くときには非常に有効なcomposition-apiの使い方です。今回の例では、
useMouse()
を呼び出すだけで何も考えずにリアクティブにマウスの座標扱えるようになります。FishLayer.vue(抜粋)import { useMouse } from "../core/useMouse"; export default defineComponent({ setup(props, ctx) { // state: マウスの座標を状態として利用 const { mousePos: destination } = useMouse(); // → destination.x と destination.y で常に現在のカーソル座標が利用できる } }
useMouse
の中身もちょっとみてみましょう。結構泥臭いコードが並んでいることがわかると思います。useMouseimport { onMounted, reactive, onUnmounted } from "vue"; export const useMouse = (targetDom?: HTMLElement) => { const mousePos = reactive({ x: 0, y: 0, }); const onMove = (ev: PointerEvent): void => { mousePos.x = ev.clientX; mousePos.y = ev.clientY; }; const onMoveTouch = (ev: TouchEvent): void => { mousePos.x = ev.touches[0].clientX; mousePos.y = ev.touches[0].clientY; }; onMounted(() => { const target = targetDom ?? document.body; target.addEventListener("pointermove", onMove); target.addEventListener("touchmove", onMoveTouch); }); onUnmounted(() => { const target = targetDom ?? document.body; target.removeEventListener("pointermove", onMove); target.removeEventListener("touchmove", onMoveTouch); }); return { mousePos, }; };今回のアプリでは、「PCではカーソル移動で」「モバイルではタッチ&スワイプで」金魚を誘導します。
この動作を一元的に扱うために、useMouse.ts
ではPointerEvent
とTouchEvent
の両方をListenしています。1さらに、コンポーネントがunmountされたときにイベントリスナーを削除する処理も書いています。この手のイベントのListen&Removeは面倒な上にコンポーネントの可読性を下げるので、composition-apiで独立したファイルに追い出せるのはとても便利です。
⭐️POINT3: requestAnimationFrameの管理もuseで分離
インタラクティブなゲームやアニメーション表現をアニメーションライブラリなしで実装する場合、タイマーや
window.requestAnimation
を多用することになります。この手のコードもコンポーネントを長くして可読性を下げるのでcomposition-apiで外に出してしまうのが吉です。こんな感じで定義しておけば...
useAnimationFrameimport { onMounted, onBeforeUnmount } from "vue"; /** * requestAnimatinFrameの処理を登録します。 * @param onFire 処理。次のフレームでも継続して呼び出す場合、trueを返してください。 */ export const useAnimationFrame = (onFire: () => boolean) => { let isTerminated = false; onMounted(() => { const tick = () => { requestAnimationFrame(() => { if (isTerminated) { return; } const shouldContinue = onFire(); if (shouldContinue) { tick(); } }); }; tick(); }); // アンマウント後に動作しないように停止フラグを立てる onBeforeUnmount(() => { isTerminated = true; }); return {}; };使う側はコンポーネントのマウント状態を意識せずに意味のあるロジックを書くことに集中できます。
FishLayer.vue(抜粋)import { useAnimationFrame } from "../core/useAnimationFrame"; export default defineComponent({ setup(props, ctx) { useAnimationFrame(() => { updateFish(); // 金魚の位置や速度を更新 if (fishCount.value < props.maxFish) { addFish(); // 必要な金魚数が増えていたら追加投入 } else if (fishCount.value > props.maxFish) { removeFish(); // 減っていたら金魚を削除 } return true; }); } }波紋を表現するWaveコンポーネントを作る
金魚だけだと寂しいので他にもいくつかコンポーネントを作ります。
それぞれ複雑さやロジックは異なっていますが、基本の考え方は一緒です/src/components/Wave.vue
単一の波紋を表現するコンポーネントです。アニメーション完了後にend
イベントをemitする等、状態の管理が発生するので関数コンポーネントではない通常のコンポーネントにしています。/src/components/WaveLayer.vue
複数の波紋を管理・表示するコンポーネントです。useMouse
でカーソル位置を、useClick
でクリック/タップイベントを、useTicker
でタイマーによるランダムなイベント発火を管理することで、波紋の追加削除をシンプルに書くことができます。/src/core/WaveModel.ts
波紋の状態を管理するクラスです。モデル化するほど複雑なものではないのですが、FishModel
と同じ構造にするために独立したクラスにしています。全てを組み合わせる
最後に
Stage
コンポーネントを作って全てのレイヤーを合成します。Stage.vue<template> <!-- ステージ全体のコンポーネントです。背景・金魚・波紋を合成します --> <StageBg class="StageRoot"> <FishLayer :maxFish="maxFish" @count-changed="fishCountChanged" /> <WaveLayer /> </StageBg> </template>まとめ:Vue3のcomposition-apiはインタラクティブなアニメーション作りに(・∀・)イイ!!
この記事ではVue3のcomposition-apiを使って金魚を良い感じに泳がせるアプリを作ってみました。
Vue3のcomposition-apiは正直難しいと感じている方も多いと思います。
composition-apiの話はどうしても設計の良し悪しみたいな難しい議論になりやすいのですが、ゆるーく金魚を泳がせるアプリを作ってみることで「ちょっとイイかも...?」と思っていただけると嬉しいです。
本来は環境を判定してどちらか一方だけListenすべきです。両方無条件に扱っているのはただの手抜きです... ↩
- 投稿日:2020-09-07T00:21:41+09:00
vue props を再復習します
props ってそもそも何?
props
はコンポーネントに値を渡すためのものです。下図 ( 公式サイト引用 ) のように、UI パーツや構成ごとにコンポーネントを分割することはよくあることです。
このようなときにコンポーネント間で値 ( データ ) の受け渡しを行いたいことは多々あります。
props はそのようなときに利用される手段の 1 つです。記述方法
ブラウザの性質上、HTML はすべて小文字 ( LowerCase ) として認識されます。
そのため、
- HTML 内ではケバブケース
- JavaScript 内ではキャメルケース
で記述する必要があります。
コンポーネントを呼び出す側<!-- HTML 内ではケバブケース --> <custom-header main-title="Hello Vue.js" /> <!-- string 以外の型についてバインドすることが必須 --> <!-- これだと文字列の 0 として渡されてしまう --> <custom-header index="0" /> <!-- v-bind:index or :index とすることで number とみなされる --> <custom-header :index="0" />コンポーネント内// TypeScript 内ではキャメルケース props: { mainTitle: ... }props として渡せる型
props に渡せる型には ネイティブコンストラクタ と カスタムコンストラクタ が存在します。
よく使われているネイティブコンストラクタは以下の通りです。
TypeScript に存在する基本的な型は網羅されています。
( BigInt は比較的新しく登場した型なためコンストラクタは存在しないです。 )
型コンストラクタ String Number Boolean Array Object Date Function Symbol 渡しかた
props の宣言の仕方は 3 通りあります。
1 つ目は、配列として宣言してあげる方法です。
この書き方では値を受け取ることはできますが、「 props がどんな型で届いて、そもそも親コンポーネントから確実に受け取れているのか」などの情報を知ることができません。これではかなり不安が残ってしまいます。
そこで次の書き方です。配列として宣言// 配列として宣言することが可能 props: ['propsA', 'propsB']2 つ目は、型を定義してあげる方法です。
props の各プロパティに先ほど列挙した型コンストラクタをアノテートします。
こうすることで propsA には string でしか渡すことができなくなります。
もし、string 以外の値を渡すとコンソールにエラーが吐かれます。propsB は string か number を受け付ける交差型になります。
いい感じになりましたがまだ不十分です。次にいきましょう。
型を定義props: { propsA: String, // string or number propsB: [String, Number] }3 つ目は、型コンストラクタ以外の制約要素も加えて宣言する書き方です。
型コンストラクタを含めて 4 種類の制約要素があります。
これらを組み合わせることで強力な制約をかけることができます。
制約要素 必須 説明 type ○ 型コンストラクタ required × props の created 時の存在を定義 default × 値が渡されなかったときに設定される初期値 validator × 受け取るときにバリデーションを適用 複数の制約要素を設定するときは型コンストラクタを
type
として宣言します。
type 以外の制約要素も制約を満たさなかったときは同様エラーログが吐かれます。型コンストラクタ以外の制約要素props: { // string かつ必須 propsA: { type: String, required: true }, // number かつ 0 以上 // 値はなくてもよくて、無いときは 0 が代入される propsB: { type: Number, default: 0, validator: (value) => value >= 0 } }スタイルガイド
ここに関しては個人的な指標ですので参考程度に読み進めていただけると助かります。
規約的に type は必ず定義します。
値が必須のときはrequired: true
、不確定なときはdefault: ...
を定義します。
また、確定で値のレンジがわかっている場合はvalidator: () => {}
を定義します。default にプリミティブ以外 ( object ) を定義するときは
factory
を利用して定義します。
object はそのまま定義するとシャローコピーとなり、単方向のバインディングの流れを無視することになるためエラーが吐かれます。factoryを用いた初期値設定props: { propsA: { type: Object, default: () => { title: '' } } }幽霊プロパティ
幽霊プロパティとは対応するプロパティが定義されていないものを指します。
次のような状況で出現します。
- コンポーネントのカスタムタグに
props-c="JAPAN"
と記述- 一方でコンポーネントの props には
propsC
が無いこのようなとき、幽霊プロパティはコンポーネントのルート要素に追加されます。
つまり$attrs
となり、class, style
以外のプロパティは幽霊になります。定義されていないプロパティを渡す<!-- 親コンポーネント --> <custom-tag no-prop="定義されていないプロパティです!" /> <!-- custom-tag --> <!-- 「定義されていないプロパティです! or ...」 が画面に表示される --> <b>{{ noProp }}</b>Vue.js 3 からは未定義プロパティに関しては警告ログを出すようになっているのでそこまで注意する必要はありませんが、知っていることで余計なところで躓くことを防げます。
ネイティブコンストラクタは必要?
開発の中ではネイティブコンストラクタのみで事足りることがほとんどです。
しかし、採用するだけのメリットがあるからこそカスタムコンストラクタは存在します。カスタムコンストラクタは自作されたクラス・オブジェクトに対して型検査を発揮します。
具体的には値オブジェクトなどがその一例と言えるかと思います。
TypeScript での開発を進めるのであれば、型の制約を強めることができてより保守性の高い設計を行うことができます。と言いつつも、これまで一度も実務上利用したことがない props の型コンストラクタであるため、ネイティブについて詳しい方のコメントをいただければと思います。どの記事もネイティブコンストラクタには触れられていないので実はフロントエンド開発においてはあまり重要視されないものかも...。
属性の継承
属性の継承とはカスタムタグとコンポーネントの root タグで指定されているプロパティが同一の場合などに発生するものです。
よくあるパターンとしては、css クラスのスタイル被りによる値の上書きなどでしょうか。cssの継承<!-- 呼び出し元 --> <Main class="css-a" /> <!-- コンポーネント --> <div class="css-b" /> ... </div>このとき Main タグは
width: 1000px, height: 500px
となります。
つまり css-a と css-b がマージされ、衝突したスタイルに関しては呼び出し元の指定が優先され、衝突していないスタイルに関してはそのまま適用されます。ただし、vue.js 3 では「コンポーネントの root タグが 1 つのみ」の制限がなくなるため、カスタムタグに css クラスを指定してもスタイルには影響を与えません。ですので、同一 HTML タグ内で複数 css クラスを指定した時のマージは行われますが、上記のようなパターンは今後生じなくなる予定です。
.css-a { width: 1000px; } .css-b { width: 200px; height: 500px; }TypeScript での型推論
こちらは蛇足になりますが、vue.js 3 で強化された型推論についてです。
setup(props: Props)
の props に型アノテートできることにより TypeScript の型推論ができるようになります。これまではオブジェクトについては any として推論されるためなかなか開発で不便なことも多かったので助かります。
( required: false のときの:?
も推論してくれるのは嬉しいですね。 )