- 投稿日:2020-11-13T19:34:26+09:00
[Vue.js] ルーティング による画面遷移
はじめに
Vue.js には URL の移動を伴う画面遷移の手段として ルーティング という仕組みが用意されている。
本記事では簡単な例を元に以下の 2点について見ていく。前提
- Vue CLI でプロジェクトを作成していること
- 単一ファイルコンポーネントであること( 1 )
- 動作確認は
$ npm run serve
で起動した環境で行なっている環境
Version 備考 Vue 2.6.11 公式はこちら Vue CLI 4.1.1 公式はこちら Vue Router 3.0.7 Vue.js の公式ルータ, 公式はこちら Buefy 0.9.2 Vue.js 用の UI コンポーネント, MIT ライセンス, 公式はこちら 事前準備
vue-router の追加
package.json に
vue-router
を追加してnpm install
でモジュールを追加する。
今回の記事で扱ったバージョンは3.0.7
。package.json// 省略 { "dependencies": { "vue-router": "3.0.7", // コレを追加 } } // 省略vue-routerを追加$ npm install added 1 package from 1 contributor and audited 1299 packages in 6.541s基本
この項目で扱う内容は次の 3点。
- ルーティングで扱いたいページの追加
- ルーティングを管理するファイルの追加
- Vue.js 上でルーティングを扱うための 2つ のファイル修正
で、表にまとめると以下のようになる。
ファイル 新規 / 修正 備考 *.vue 新規 ルーティングで扱いたいページ router.js 新規 ルーティングを管理する main.js 修正 ルーティングを扱うために修正 App.vue 修正 同上 では上から順に見ていく。
ページ(
*.vue
)の追加ルーティングで扱いたいページを実装する。
とは言っても、ここで扱う内容に特別なものはない。単純に 単一コンポーネントでページを実装しただけである。追加したページ<template> <div :class="$style.parent"> <div :class="$style.child"> <h1>Routing Test Page</h1> </div> </div> </template> <script> export default { name: 'Routing' } </script> // スタイルは割愛router.js ファイルを新規作成
こちらはルーティングを管理するための JavaScript ファイル。
router.jsimport Vue from 'vue' import Router from 'vue-router' import Home from './views/Home.vue' import Routing from './views/Routing.vue' // // 他のコンポーネントは省略 // Vue.use(Router) export default new Router({ // デフォルトの挙動では URL に `#` が含まれる. // URL から hash を取り除くには `mode:history` を指定する mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'home', component: Home }, // // 省略 // { path: '/routing', name: 'routing', component: Routing } ] })短いコードではあるがポイントがいくつかある。以下、順に見ていく。
vue-router
のimport
- 事前準備 で導入した
vue-router
をimport
している- ここは特筆すべきことはなく、単純に
vue-router
を使うための宣言である
vue-router
の登録
import
したvue-router
を Vue で使えるようにVue.use()
で指定する- これにより Vue アプリ上で
vue-router
を使ったルーティングが可能になる
vue-router
のインスタンス生成とexport
vue-router
のインスタンスを生成し、それをexport
している- これにより 各コンポーネントでは 本 JS ファイル(
router.js
) をimport
することなくthis.$router
で参照 することができるルーティングの設定
routes
プロパティを配列で定義し、その中に以下を設定することで path に応じたコンポーネントが呼び出される- ここの内容でルーティングのためのルールを設定することになる。 公式の Vue Router を充分に理解しておきたい
項目 説明 備考 path URL の path <router-link>
で指定する文字列を間違えないように注意することname <router-link>
やrouter.push
で指定する名前両者を使わないのであれば設定不要。だが設定しておいた方が良いと思う component 呼び出されるコンポーネント <router-view />
が配置された箇所でレンダリングされるmain.js の修正
このファイルの修正は単純で、 アプリケーション上でルーティングを行うために、前項で作成した
router.js
のimport
- Vue インスタンス生成時に
router.js
のインスタンスのセットの 2点 を行っている。
main.jsimport Vue from 'vue' import App from './App.vue' // ルーティングのために追加 import router from './router' Vue.config.productionTip = false new Vue({ router, // ルーティングのために追加 render: h => h(App), }).$mount('#app')App.vue の修正
こちらは前項で挙げた
main.js
のmain.jsnew Vue({ router, // ルーティングのために追加 render: h => h(App), }).$mount('#app')の部分で読み込まれていて、アプリケーションの起点となるファイルである。
このApp.vue
では、テンプレート部分でrouter.js
で定義したルーティングルールとの紐付けを行っている。App.vue<template> <div id="app"> <div id="nav" class="tab-area-base"> <ul class="tab-menu-base"> <!-- `router.js` で定義したルーティングルールとの紐付けを行っている --> <li><router-link to="/">Home</router-link></li> <!-- *** --> <!-- 省略 --> <!-- *** --> <li><router-link to="/routing">Rounting Test</router-link></li> </ul> </div> <router-view /> </div> </template> // ▼ ここから削除 ( ルーティングによって不要となるため ) //<script> //import HelloWorld from './components/HelloWorld.vue' // //export default { // name: 'App', // components: { // HelloWorld // } //} //</script> // ▲ ここまで削除 // スタイルは割愛このファイルもいくつかポイントがあるので順に見ていく。
<template>
部分
<router-link>
- 遷移先の設定を行っている
- ここで
to=
で設定しているのが router.js で設定 したパスとなる- 指定する文字列を間違うと リンクをクリックしても遷移しない うえに、コンソール上にエラーも出ない ため、意外と不具合の原因を探しづらいので注意されたい
<router-view />
- ここの部分に
<router-link>
からrouter.js
を経由して呼び出されたコンポーネントの内容が描画される- これが無いと コンポーネントを呼び出しているの描画されない ということになるので注意
<script>
部分
- コード中のコメントにもあるとおり、ルーティングを使うことで本ファイルでのコンポーネントの呼び出しは不要になった
動作確認
ここまでルーティングの基本について見てきた。
では実際にどう動くのかをキャプチャをもとに見ていく。ルーティング前
App.vue の修正前 のコードにあるように、
HelloWorld.vue
がそのまま表示されている。ルーティング後
初期表示 or Home タブを選択
ルーティングの実装後。
画面にはタブが表示され、Home
タブの URL であるhttp://localhost:8080
には初期画面として ルーティング前 と同じHelloWorld.vue
の画面が表示されている。
Routing Test タブを選択
Routing Test
タブの URL であるhttp://localhost:8080/routing
では、新たに実装したページであるRouting.vue
の画面が表示されている。
以上、ルーティングの基礎について見てきたが、決められたルールに沿うことで、少ない手間で URL を指定したページ遷移が実現できることがわかった。
次の項目では ルーティング時にパラメータの受け渡しを行う 方法について見ていく。進んだ使い方
vue-router
を使ったルーティングでは 任意の値をパラメータとして受け渡す ために
- URL のパターンマッチングによるページ遷移
router.push
を使ったページ遷移といった方法が用意されている。
以下、それぞれの方法について見ていく。
パターンマッチングによるページ遷移
URL のパターンマッチングによってパラメータの受け渡しを行うケース。ここで挙げる例は以下のとおり。
- ユーザ情報の詳細を持つコンポーネント
UserDetail.vue
がある- そのコンポーネントに対してユーザIDを URL で指定する
- 指定したユーザ情報の詳細を表示するページに遷移する
基本 の項目でルーティングのための準備はできているので、本項では
router.js
への追記とユーザ情報の詳細を持つコンポーネントUserDetail.vue
について扱う。router.js への追記
パターンマッチングでのページ遷移の実現にあたり、まずは
router.js
で URL でのパラメータの受け渡し のための設定を記述する。具体的には以下のとおり。
router.jsimport Router from 'vue-router' import UserDetail from './views/UserDetail.vue' Vue.use(Router) export default new Router({ { path: '/users/:id', name: 'user-detail', component: UserDetail } ] })ポイントは
path: '/users/:id'
の部分。path
内の URL に:
を使用する ことでパターンマッチングを実現する。
コンポーネントでは$route.params
から、ここで指定したパラメータ名と 同じ名前でアクセス することで値を取得できる。UserDetail.vue
UserDetail.vue
はrouter.js
で URL によるパターンマッチングでの遷移を行う際に指定されたコンポーネント。前述の説明のとおり、下記のコードでは
$route.params.id
とすることで'/users/:id'
で指定したid
の値を取得 している。UserDetail.vue<template> <div :class="$style.component"> <h1>This page is user detail.</h1> <div :class="$style.userinfo"> <table> <th :class="$style.item"> ITEM </th> <th :class="$style.value"> VALUE </th> <!-- users のリストにアクセスする際、インデックスは 0 からなので受け取った id の値から `-1` する --> <tr v-for="(value, name) in users[$route.params.id - 1]" :key="name"> <td :class="$style.item"> {{ name }} </td> <td :class="$style.value"> {{ value }} </td> </tr> </table> </div> </div> </template> <script> export default { name: 'UserDetail', data: function () { // 返却するオブジェクト users は本コンポーネントで表示するユーザ情報 // 本来ならば DB 等で保持するのだが、今回は記事用のサンプルコードということでリストで持たせている return { users: [ { id: 1, name: 'hogehoge', live: 'Japan Tokyo', phone: 'NNN-XXXX-HHHH', gender: 'male', mail: 'hogehoge@mail.com' }, { id: 2, name: 'barbar', live: 'Japan Kanagawa', phone: 'NNN-XXXX-BBBB', gender: 'male', mail: 'barbar@mail.com' }, { id: 3, name: 'piypiyo', live: 'Japan Kanagawa', phone: 'NNN-XXXX-PPPP', gender: 'female', mail: 'piypiyo@mail.com' }, { id: 4, name: 'fugafuga', live: 'Japan Chiba', phone: 'NNN-XXXX-FFFF', gender: 'male', mail: 'fugafuga@mail.com' }, { id: 5, name: 'varvar', live: 'Japan Saitama', phone: 'NNN-XXXX-VVVV', gender: 'female', mail: 'varvar@mail.com' } ], } } } </script> // スタイルは割愛動作確認
動作確認の結果が以下のキャプチャ。
URL に
http://localhost:8080/users/1
を指定することでid: 1
のユーザ情報詳細http://localhost:8080/users/4
を指定することでid: 4
のユーザ情報詳細が、ぞれぞれ表示されることが確認できた。
ボタンアクションによるページ遷移
前掲の パターンマッチング で URL上でパラメータがセットされたケース の例について触れた。
ここではボタンアクションによる パラメータの受け渡しを伴うページ遷移 について扱う。ここで挙げる例は以下のとおり。
- ユーザのリストを表示するコンポーネント
UserList.vue
と、その親コンポーネントUsers.vue
がある- コンポーネント
UserList.vue
でテーブルから任意のレコードを選択してShow more seleted...
ボタンをクリックすることで- 指定したユーザ情報の詳細を表示するコンポーネント
UserDetail.vue
を呼び出す( 遷移する )コードを見る前に
上記 3 つのコンポーネントの概要を表にすると以下のとおり。
コンポーネント 概要 子コンポーネント 備考 Users.vue User 情報を持つ UserList.vue UserLive.vue User 情報をテーブルで表示する なし UserDetails 指定された User 情報の詳細を表示する なし 前項 で扱っているのでここではコードを扱わない このうち
Users.vue
とUserDetail.vue
は同じデータオブジェクトをそれぞれのコンポーネント内で定義している。
本来ならばこれらのデータはDB
なりlocalStorage
で持つなり、なんらかの手段でデータの共有化を図るべきなのだけれども、今回は ルーティングが主題 であるためデータの持ち方については考慮外とした。Users.vue
Users.vue
はユーザの一覧をオブジェクトとして持ち、画面描画時に子コンポーネントであるUserList.vue
にユーザ情報を渡すだけの単純なもの。
- コンポーネントの親子関係
- 親コンポーネント → 子コンポーネントへのデータ授受
についても少し見てみたいと思い試してみた。
( コンポーネント間のデータのやりとりについては、別途記事を設けて見ていきたい )Users.vue<template> <div :class="$style.component"> <UserList :properties="properties" /> </div> </template> <script> // @ を指定することで `/src` の代替となる import UserList from '@/components/UserList.vue' export default { name: 'Users', components: { UserList }, data: function () { // ここで返却するデータは子コンポーネント `UserList.vue` で表示するユーザ情報 // 本来ならば DB 等で保持するのだが、今回は記事用のサンプルコードということでリストで持たせている return { properties: { users: [ { id: 1, name: 'hogehoge', live: 'Japan Tokyo', phone: 'NNN-XXXX-HHHH', gender: 'male', mail: 'hogehoge@mail.com' }, { id: 2, name: 'barbar', live: 'Japan Kanagawa', phone: 'NNN-XXXX-BBBB', gender: 'male', mail: 'barbar@mail.com' }, { id: 3, name: 'piypiyo', live: 'Japan Kanagawa', phone: 'NNN-XXXX-PPPP', gender: 'female', mail: 'piypiyo@mail.com' }, { id: 4, name: 'fugafuga', live: 'Japan Chiba', phone: 'NNN-XXXX-FFFF', gender: 'male', mail: 'fugafuga@mail.com' }, { id: 5, name: 'varvar', live: 'Japan Saitama', phone: 'NNN-XXXX-VVVV', gender: 'female', mail: 'varvar@mail.com' } ] } } } } </script> // スタイルは割愛UserList.vue
UserList.vue
は親コンポーネントであるUsers.vue
から受け取ったデータをテーブルで表示するコンポーネント。テーブルの実現には Buefy の Table を使用している。
UserList.vue<template> <div> <h1>This page is user list.</h1> <div :class="$style.userlist"> <!-- Buefy のテーブルを使って実現 --> <!-- https://buefy.org/documentation/table/ --> <b-table :data="properties.users" :columns="columns" :striped="true" :hoverable="true" :selected.sync="selected" /> </div> <div :class="$style.showmore"> <b-button type="is-info" @click="showMoreInformation"> Show more selected... </b-button> </div> </div> </template> <script> export default { name: 'UserList', props: { properties: { type: Object, 'default': () => { return null }, } }, data: function() { return { // `selected`, `columns` は Buefy のテーブルを使用する際に必要なパラメータ // https://buefy.org/documentation/table/ selected: null, columns: [ { field: 'id', label: 'ID', width: '50', numeric: true }, { field: 'name', label: 'NAME', width: '400', centered: true }, { field: 'mail', label: 'MAIL', width: '400', centered: true }, ] } }, methods: { showMoreInformation: function() { // アロー関数で定義すると `this` で `selected` が参照できない。 // 詳細は https://qiita.com/_Keitaro_/items/d48733a19c10889e2365 を参照のこと。 if (!this.selected) { alert('No data selected...') return false } const selected = this.selected this.$router.push({ name: 'user-detail', params: { id: selected['id'] } }) } } } </script> // スタイルは割愛本コンポーネントのポイントはボタンクリック時に実行されるメソッドである
showMoreInformation
。
レコードを選択した状態でshowMoreInformation
が実行されるとUserDetail.vue
コンポーネントが表示され、選択されたユーザ情報の詳細が表示される。その動きを実現しているのが下記の部分。
const selected = this.selected this.$router.push({ name: 'user-detail', params: { id: selected['id'] } })
this.$router.push
の引数にname
とparams
プロパティをセットすることでルーティングを実現させている。ここで
name
とparams.id
は パターンマッチング の router.js で設定した{ path: '/users/:id', name: 'user-detail', component: UserDetail }の部分とリンクしている。
それぞれの相関を表にすると次のとおり。
UserList.vue のコード router.js のコード name name: 'user-detail'
name: 'user-detail'
id params: { id: selected['id'] }
path: '/users/:id'
こうした
name
プロパティを利用したルーティングを 名前付きルート というらしい。動作確認
動作確認の結果が以下のキャプチャ。
User List
タブで表示されたテーブルからid: 3
のレコードを選択してShow more selected...
ボタンをクリックUserDetail
タブに遷移してid: 3
のユーザ情報詳細が表示されることが確認できた。
まとめ
基本
- ルーティングには
vue-router
を使うと簡単に実現できる- ルーティングを管理するために
router.js
を実装してmain.js
でimport
することで利用するApp.vue
では以下を行ってうことで画面遷移を実現する
router-link
で遷移先の指定router-view
で遷移先のページの表示進んだ使い方
- ルーティング時に任意の値をパラメータとして渡すことができる
- そのための手段として以下を用いる
- URL パラメータとして指定する
router.push
時にパラメータとして指定するrouter.push
時に遷移先としてname
を指定することを 名前付きルート というソースコード
今回の記事で動作確認に使用したコードは下記にアップしております。
ご参考まで。( 以下は ブランチのリンクですが、 master にもマージ済みです )参考
公式
Vue.js のコンポーネントを単独のファイルとして作成する機能
拡張子「.vue」のファイルのことで<template>
,<script>
,<style>
のブロックで構成されている。 ↩
- 投稿日:2020-11-13T18:11:31+09:00
Nuxt.js+Firebaseで2回目の新しいプロジェクトを作る
Nuxt.js+Firebaseを勉強しています。
プロジェクトを作成し、Webサービスをデプロイすることができました。本記事は、もう一つプロジェクトを作成してSNS認証を実装するところまでのメモです。
環境
- Firebase 8.0.1
- Vue 2.6.11
- Nuxt 2.10.2
- 開発環境 WindowsPC
実施手順
Firebaseコンソール で新規プロジェクトを作成
- 「このプロジェクトで Google アナリティクスを有効にする」を有効
- 「Google アナリティクスの構成」はデフォルトを選択
作成が完了し、プロジェクトのコンソールを見れるようになった。
Firebaseコンソールでウェブアプリを作成
プロジェクトのコンソールで開始するアプリ(ウェブ)を追加する。
- 「アプリのニックネーム」は自由に決める。
- Firebase Hostingの設定は後からでもできるとのことなので、このタイミングではしない。
「アプリを登録」ボタンを押下する。
Nuxtでプロジェクトを作成する
Nuxtでプロジェクトを作成するコマンド
npx create-nuxt-app (プロジェクト名)
コマンドを実行したディレクトリに指定したプロジェクト名のディレクトリが作られる。
Vuetifyで簡単に作りたかったので、設定はこんな感じ。
- ? Project name: (プロジェクト名) - デフォルトでいいので未入力で Enter - ? Programming language: (Use arrow keys) - > JavaScript - TypeScript - ? Package manager: (Use arrow keys) - Yarn - > Npm - ? UI framework: (Use arrow keys) - None - Ant Design Vue - Bootstrap Vue - Buefy - Bulma - Chakra UI - Element - Framevuerk - iView - Tachyons - Tailwind CSS - Vuesax - > Vuetify.js - ? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection) - >( ) Axios - ( ) Progressive Web App (PWA) - ( ) Content - ※ 選択無しでEnter - ? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection) - >( ) ESLint - ( ) Prettier - ( ) Lint staged files - ( ) StyleLint - ( ) Commitlint - ※ 選択無しでEnter - ? Testing framework: (Use arrow keys) - > None - Jest - AVA - WebdriverIO - ? Rendering mode: (Use arrow keys) - Universal (SSR / SSG) - > Single Page App - ? Deployment target: (Use arrow keys) - > Server (Node.js hosting) - Static (Static/JAMStack hosting) - ? Development tools: (Press <space> to select, <a> to toggle all, <i> to invertselection) - >( ) jsconfig.json (Recommended for VS Code if you're not using typescript) - ( ) Semantic Pull Requests - ( ) Dependabot (For auto-updating dependencies, GitHub only) - ※ 選択無しでEnter - ? What is your GitHub username? - GitHubのユーザー名を入力する。 - ? Version control system: (Use arrow keys) - Git - > Noneビルド実行
>npm run build (中略) ? Are you interested in participation? (Y/n)
n
にした。Firebase SDKのインストールとプロジェクトへの紐付け
作成したプロジェクトにFirebase SDKをインストールする。
今回はnpmで--saveでインストールする。(-gではなく)cd (プロジェクト名のフォルダ) npm install firebase --saveインストールしたfirebaseのバージョンを確認する。
>npm list firebase `-- firebase@8.0.1テスト環境で動かしてみよう
実行コマンドは次の通り。
npm run dev
http://localhost:3000/
にアクセスすると、Vuetifyの画面が出る。プロジェクトをFirebase初期設定する
1.Firebaseコンソールでリージョンを設定
Firebaseコンソール にログインし、当該プロジェクトのSettingsを開く。
[全般] 中程の リソース ロケーション を任意のロケーションに指定する。
今回は東京リージョン asia-northeast1 を選択する。2.Cloud Firestore の作成
Cloud Firestoreのページで、「データベースの作成」ボタンを押下する。
3.
firebase init
を実行NuxtでFirebaseを使いたいので、create-nuxt-appで作成したNuxtプロジェクトディレクトリのトップでコマンド
firebase init
を実行する。to select features, then Enter to confirm your choices. (Press <space> to select, <a> to toggle all, <i> to invert selection) >( ) 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? 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 projectFirestore Rulesについてはデフォルトのまま Enter
? What file should be used for Firestore Rules? (firestore.rules)Firestore indexesについてもデフォルトのまま Enter
? What file should be used for Firestore indexes? (firestore.indexes.json)? What do you want to use as your public directory? (public)SPAについての設定も y で。
? Configure as a single-page app (rewrite all urls to /index.html)? (y/N)firebase-tools のアップデート
╭───────────────────────────────────────────╮ │ │ │ Update available 8.4.3 → 8.15.1 │ │ Run npm i -g firebase-tools to update │ │ │ ╰───────────────────────────────────────────╯>npm i -g firebase-toolsfirebase.config の作成
firebase.configファイルをプロジェクトのディレクトリに作成して、
Firebase SDK snippet
を書き込む。
Firebase SDK snippet
は、Firebaseコンソールの「設定」-「全般」を参照する。export default { apiKey: "", authDomain: "", databaseURL: "", projectId: "", storageBucket: "", messagingSenderId: "", appId: "", measurementId: "" }firebase.configファイルは、gitignoreに設定する。
# others firebase.configvuex(ビューエックス)をインストール
vuexを利用するので、インストールする。
npm install vuex --savefirebaseuiをインストール
npm install firebaseui --saveFirebaseUIのcssを読み込ませる
FirebaseUIのcssは
nuxt.config.js
で読み込んでおく。~/nuxt.config.jscss: [ "firebaseui/dist/firebaseui.css", ],Firebaseの初期化処理のコーディング
Firebaseの初期化処理をpluginsに書く。firebase.configの読み込み、SNS認証の初期設定、フィルターの読み込み。
pluginsフォルダで実装したjsは、「root Vue.jsアプリケーションがマウント」される前に実行される。フィルターは、金額をカンマ区切りで表示したい場合等に利用する。
~/filter/filters.js
にファイルを作成し、pluginsで読み込ませることで、どのページでも利用できるようになる。~/plugins/firebase.jsimport firebase from 'firebase/app'; import 'firebase/auth'; import 'firebase/firestore'; import config from '~/firebase.config'; import Vue from 'vue'; import * as filters from '~/filter/filters'; if (!firebase.apps.length) { firebase.initializeApp(config); firebase.analytics(); } // initialize authorization export const authProviders = { // 使うものだけ定義しておきましょう Email: firebase.auth.EmailAuthProvider.PROVIDER_ID, Google: firebase.auth.GoogleAuthProvider.PROVIDER_ID, Facebook: firebase.auth.FacebookAuthProvider.PROVIDER_ID, Twitter: firebase.auth.TwitterAuthProvider.PROVIDER_ID, Github: firebase.auth.GithubAuthProvider.PROVIDER_ID }; export const auth = firebase.auth(); // initialize Firestore const db = firebase.firestore(); export { db }; // import filters Object.keys(filters).forEach(key => { Vue.filter(key, filters[key]); });~/filter/filters.js// 数字の3桁ごとにカンマ区切りをつけるフィルタ export function number_format (value) { if (value === 0) { return 0; } if (value === "0") { return 0; } if (! value) { return 0; } return value.toString().replace( /([0-9]+?)(?=(?:[0-9]{3})+$)/g , '$1,' ); } // yyyymm文字列を"yyyy年mm月"文字列にするフィルタ export function yyyymm_format (value) { if (! value) { return false; } return value.substr( 0, 4 ) + "年" + value.substr(4) + "月"; }プラグインをNuxt.jsで使えるように設定ファイル
nuxt.config.js
を編集する。~/nuxt.config.jsplugins: [ '~/plugins/firebase', ],SNS認証のログイン/ログアウトのステータスを保存する
~/store/auth.jsimport Vue from 'vue'; import { auth } from '~/plugins/firebase'; export const state = () => ({ user: {}, status: "" }); export const mutations = { setUser(state, user) { state.status = "loggedIn"; state.user = user; }, logout(state) { state.status = "loggedOut"; state.user = {}; } }; export const getters = { isLoggedIn: (state) => { return state.status === "loggedIn"; }, getUsername: (state) => state.user.displayName, getUid: (state) => state.user.uid }; export const actions = { gotUser({ commit }, user) { commit("setUser", user); }, logout({ commit }) { auth.signOut().then(() => { commit("logout"); }) }, };ログイン/ログアウトの認証情報による処理の追加
middlewareはページがレンダリングされる前に実行される。
(すべての画面遷移の前に実行される)middlewareに、ログイン/ログアウトなどの認証情報の変化で発火するonAuthStateChangedを設定しておく。
さらに、
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.SESSION);
を追加する。
これによって、認証情報をローカルストレージに保存することができるようになる。~/middleware/authenticated.jsimport firebase from 'firebase'; import { auth } from '~/plugins/firebase'; export default function ({ route, store, redirect }) { auth.setPersistence(firebase.auth.Auth.Persistence.SESSION); auth.onAuthStateChanged((user) => { if (user) { store.dispatch("auth/gotUser", user); } else { // ログアウトせずにタブを消して、ブラウザを閉じずにまたURLから画面を開いたとき、 // firebase.authのuserがnullなのに、ローカルストレージの情報が残っていてstoreのauthはログイン状態になっている。 // そこで、ここで一度ログアウトを実行しておく。 store.dispatch("auth/logout"); // もしログインしていなかったらログインページにリダイレクト if(route.name !== "login") redirect("/login"); } }); }~/middleware/notAuthenticated.jsexport default function ({ store, redirect }) { if (store.getters["auth/isLoggedIn"]) { return redirect('/'); } }ログインボタンを表示するUI部分を作成
下記はPUGを利用したHTML記述方法になっている。
firebaseuiのimportはエラーが出たため、requireを利用することにした。~/components/FirebaseAuth.vue<template> <div id="firebaseui-auth-container"></div> </template> <script> import { auth, authProviders } from '~/plugins/firebase'; export default { name: 'FirebaseAuth', mounted() { auth.onAuthStateChanged(user => { if (!user) { const firebaseui = require('firebaseui'); const ui = firebaseui.auth.AuthUI.getInstance() || new firebaseui.auth.AuthUI(auth); const config = { signInOptions: [ // authProviders.Email, authProviders.Google, // authProviders.Facebook, ], callbacks: { }, signInSuccessUrl: '/', signInFlow: 'popup', // ログインフロー設定。Nuxtのローカルサーバーで起こるCORSエラーがあるのでpopupがオススメです。 }; ui.start('#firebaseui-auth-container', config); } }); } } </script>signInFlowで設定しているログインフロー設定とは、サインアップフローをポップアップにするか、リダイレクトにするかなどを設定するためのものです。
ログインページの作成
pagesディレクトリに、login.vueを作る。とりあえず中身はない。
~/pages/login.vue <template> <div> </div> </template> <script> import { mapState, mapGetters, mapActions } from "vuex"; export default { middleware: 'notAuthenticated', name: 'Login', } </script>ヘッダーの作成
ログイン/ログアウトをヘッダーに実装する。
~/components/Header.vue <template> <v-app-bar :clipped-left="clipped" fixed app > <v-app-bar-nav-icon v-if="isLoggedIn" @click.stop="drawer = !drawer" /> <v-btn icon v-if="isLoggedIn" @click.stop="miniVariant = !miniVariant" > <v-icon>mdi-{{ `chevron-${miniVariant ? 'right' : 'left'}` }}</v-icon> </v-btn> <v-toolbar-title></v-toolbar-title> <v-spacer /> <v-btn text='' v-if="isLoggedIn" @click="logout"> Logout </v-btn> <FirebaseAuth/> </v-app-bar> </template> <script> import FirebaseAuth from '@/components/FirebaseAuth.vue'; import { mapGetters, mapActions } from "vuex"; export default { components: { FirebaseAuth }, computed: { ...mapGetters("auth", [ "isLoggedIn", "getUsername", "getUid" ]), }, methods: { ...mapActions("auth", ["logout"]) } } </script>フッターの作成
~/components/Footer.vue <template> <v-footer> <div class="flex-grow-1"></div> <div v-if="getUid"><p class="grey--text" style="font-size: small;"> UID: {{getUid}} </p></div> </v-footer> </template> <script> import { mapGetters } from "vuex"; export default { computed: { ...mapGetters("auth", [ "getUid" ]), }, } </script>FirebaseコンソールでSNS認証の利用を開始
- Firebaseコンソール の「Authentication」で「始める」ボタンを押下する。
- 「Sign-in method」で
Firebase Hostingの初期設定
以下のコマンドを実行する。
firebase init hostingpublicディレクトリは
dist
を指定する。? What do you want to use as your public directory? dist? Configure as a single-page app (rewrite all urls to /index.html)? Yes? Set up automatic builds and deploys with GitHub? No? File dist/index.html already exists. Overwrite? Noローカル環境でテスト
npm run build
firebase serve --only hosting
- http://localhost:5000/ ※ローカルテスト環境
これでSNS認証によるログインができるようになった。。はず
- 投稿日:2020-11-13T17:09:35+09:00
Vue.js 3 : 複数項目のInputで、最後のInputが入力されたら自動的に次のInputを追加する
フォームを作っていると、次のように、入力された数が増えた時に、入力欄を自動で追加してほしいことがあります。
これを Vue.js でやる場合のコードを紹介します。
コード
<form action='/send_email' method='post'> <p> メールを送ります。 送信先のメールアドレスを入力してください。 </p> <div id="email_list"> <div class="row" v-for="(email, index) in emails"> {{index + 1}}: <input type="email" name="email[]" v-model="email.value"> </div> </div> <button type="submit">Submit</button> </form> <script> Vue.createApp({ data() { return { emails: [{value: ''}] } }, beforeUpdate() { const emails = this.$data.emails if (emails[emails.length - 1].value.trim()) emails.push({value:''}) } }).mount('#email_list') </script>
beforeUpdate
を使って入力値の状態をチェックして入力欄を増やしています。
beforeUpdate
は、 データが変更されるとき、DOM が適用される前に呼ばれます。環境
- Vue.js 3.0.2
- 投稿日:2020-11-13T16:56:17+09:00
axiosでエラー情報を取得
概要
axiosでAPI通信を行い、APIの方でエラーが発生した場合のエラー情報の取得方法についてまとめていきたい。
catchで取得したエラーの中身をみていく
axiosaxios.get('https://example.com', params) .then(function(response) { //正常時にAPIから返却されるレスポンスデータ return response; }) .catch(function(error) { //エラー時にAPIから返却されるレスポンスデータ error.response.data(); });キャッチしたHTTPステータスコードをフロント側で処理することも可能そうです。
- 投稿日:2020-11-13T15:52:51+09:00
vue component の 配列データを更新したのに値がviewに反映されない。
LTするまでもないが、簡単なのにVue ですげーハマった話。
問題
- vue component の Array data に対して、代入更新を行ったのに、template に反映されない。
解決
直接代入を行っても、vue側のレンダリングトリガーを引くことができないため、規定のレンダリングトリガーに対応したmethod を call する必要がある。
push, splice, shift… など。
これが起きた時には、 this.$nexttik は有効に働かないので、上記のトリガーを引かせる。
直接代入はやめようぜって話だった。参考になったlink
- https://aqua-engineer.com/vue-js-reactve/
- https://jp.vuejs.org/2016/02/06/common-gotchas/
- 投稿日:2020-11-13T13:09:09+09:00
VSCodeで使えるNuxt ×TypeScript × CompositionAPIの便利スニペッツ
VSCode上でNuxt,TypeScript,CompositionAPIで開発するときに毎回同じ内容を書くのが面倒だなと思ったのでスニペットを作ってみました。誰かの参考になれば嬉しいです:)
vue.json{ "nuxt3+ts template": { "prefix": "nuxt3+ts template", "body": [ "<template>", "", "</template>", "", "<script lang=\"ts\">", "import { defineComponent } from '@nuxtjs/composition-api'", "export default defineComponent({", " name: '$1',", " setup(){}", "})", "</script>", "", "<style scoped></style>" ] } }
- 投稿日:2020-11-13T11:57:17+09:00
【Vue】メソッドを外部のjsファイルに移動する方法。外部ファイルの処理結果をvueに戻す。
メソッドを外部のjsファイルに移動する方法について。
変数を外部ファイル(.js)に切り出してexport defaultでインポートする方法はこちら
今回は応用編として、.vue内のメソッドを外部ファイルに切り出す方法。
流れと考え方
操作の流れは、vueのデータをjsに渡して、処理後に戻す作業になる。
(1).vue -> (2).js -> (3).vue
(1) .vueの処理
・vueのメソッドを実行
・メソッド内で変数を定義し、jsファイルにプロパティ名でデータを渡す。(2) .jsの処理
・vueのデータを引数としてプロパティで受け取り処理を実行後、戻り値で返す。(3) .vueの処理
.jsから戻ってきた値を、vueの値(this.xxx)に代入する。
▼考え方
Inputする(渡す)データとOutputする(戻す)データの受け渡しを意識することが大切。.vueの処理(概念)methods:{ vueのメソッド名(){ const {受け取るデータ} = jsファイルのコマンド名({ 渡すデータ }) vueのデータ = 受け取るデータ } }.jsの処理(概念)const メソッド名 = ({受け取るデータ}) => { 処理 return { 渡すデータ } } export default 渡すメソッドjsファイルに引数としてデータを渡し、処理後にreturnでvue側の変数名と一致するデータを返す。
最後に、受け取ったデータをvueのデータに入れて受け取り完了となる。
実例
実例で見るとよりわかりやすい。
.vue(scriptの中身)//外部ファイル(transformationJs.js)からメソッドをjsMultiとしてインポート import jsMulti from "./transformationJs" export default { data(){ return{ A: 1, B: 2, x: 100, y: 200, } }, //ここが重要!jsファイルのメソッドとデータをやりとりする記述 methods:{ btnClicked(){ //処理結果をいれるプロパティを用意 const {A, B} = jsMulti({ //引数内でプロパティ名を指定してデータを渡す A: this.A, B: this.B, x: this.x, y: this.y }) //vueのデータにjsファイルの処理結果を代入 this.A = A this.B = B } } }読み込んだのはテンプレートではなくメソッドなので、
components:{}
は不要.js//メソッドを定義 const jsMulti = ({A, B, x, y}) => { //処理結果を代入する変数を定義 const _A = A * x const _B = B * y return { //ここが重要! //vue側で設定したプロパティにデータを入れる A: _A, B: _B } } //外部ファイルに渡すメソッド export default jsMulti・.js側で処理済みの値はわかりやすいように
_
を付けたが、これは別に何でもいい。(Aのままでもいいし、他の新しい変数名でも可)・.jsのメソッドは最後にreturnをプロパティとして返す。
・
A = _A
はエラーになる。(※プロパティの形になっていない)・
A
のみで値を入れない場合は、空のデータが返る。 (※AはA:Aの省略表記となるため、変数Aを定義していない場合は空のプロパティを返したことになる。)・A:Aの省略表記Aをshort propertyと呼ぶ
処理結果事例とフルコード
上記の実例は、ボタンをクリックする毎に、A × 100、B × 200をする処理となる。
↓ クリック
↓ クリック
▼フルコード
.vue<template> <div> <!-- クリックイベントでbtnClickedを発火 --> <button @click="btnClicked" >【Click Me】</button> <p>・A: {{A}}</p> <p>・B: {{B}}</p> </div> </template> <script> import jsMulti from "./transformationJs" export default { data(){ return{ A: 1, B: 2, x: 100, y: 200, } }, methods:{ btnClicked(){ const {A, B} = jsMulti({ A: this.A, B: this.B, x: this.x, y: this.y }) this.A = A this.B = B } } } </script>transformationJs.jsconst jsMulti = ({A, B, x, y}) => { const _A = A * x const _B = B * y return { A: _A, B: _B } } export default jsMulti
配列の中のコマンドを実行する例
v-forで表示した配列として格納されているボタンに対し、外部ファイルの処理を実行する場合の例。(*流れのみ)
(1)ボタンタグをクリックするとメソッドが発火する。
このとき、引数でクリックされたボタンの要素を渡す。
@click="onClickButton(command)
(2)ボタンクリックで発火させる処理
command.onClick
でJSファイル内のプロパティonClickを実行している。.vue<script> import cellCommands from "./commands" export default { methods:{ onClickButton(command){ const { //js内で処理後の値を格納する変数を用意(output用) rows, selectedCells, } = command.onClick({ //jsファイルに渡すデータ(input用)。プロパティ名: 値 。returnで返す。 selectedCells: this.selectedCells, rows: this.innerValue, maxColNum: this.maxColNum, thPosition: this.thPosition, }) //jsの出力をvueに渡す(output) this.innerValue = rows this.selectedCells = selectedCells }, } } </script>各要素のプロパティonClickの中に記述された処理が実行される。
commands.jsconst commands = [ { cmd: 'clearSelects', text: "選択解除", icon: 'mdi-cancel', isActive: anySelectedCells, onClick: ({ rows, }) => { return { selectedCells:[], rows, } } }, { cmd: 'addColumnBefore', text:"左に列追加", icon: 'mdi-table-column-plus-before', isActive: (params) => notThColandSelectedFirstCol(params) && singleSelectedCell(params), onClick: ({ selectedCells, rows, thPosition }) => { if(selectedCells.length != 1) { return { selectedCells, rows, } } //変換後の出力の値は別の変数に代入。一時的として、_変数名とする。 const _rows = rows.map((tr, index) => { let newCell if(thPosition == 'row' && index === 0){ newCell = initTh() }else{ newCell = initTd() } tr.article_items = [ ...tr.article_items.slice(0, selectedCells[0].cellIndex), ...[newCell], ...tr.article_items.slice(selectedCells[0].cellIndex), ] return tr }) const _selectedCells = selectedCells.map((cell)=>{ const {cellIndex: cCellIndex} = cell if(cCellIndex >= selectedCells[0].cellIndex){ return { ...cell, cellIndex: cCellIndex + 1 } } else { return cell } }) //vueに戻すoutput用 return { selectedCells: _selectedCells, rows: _rows, } } }, { cmd: 'addColumnAfter', text:"右に列追加", icon: 'mdi-table-column-plus-after', isActive: singleSelectedCell, onClick: ({ selectedCells, rows, thPosition, }) => { if(selectedCells.length != 1) { return { selectedCells, rows, } } const _rows = rows.map((tr, index) => { let newCell if (thPosition == 'row' && index === 0) { newCell = initTh() } else { newCell = initTd() } return { ...tr, article_items: [ ...tr.article_items.slice(0, selectedCells[0].cellIndex + 1), ...[newCell], ...tr.article_items.slice(selectedCells[0].cellIndex + 1), ] } }) const _selectedCells = selectedCells.map((cell)=>{ const {cellIndex: cCellIndex} = cell if(cCellIndex > selectedCells[0].cellIndex){ return { ...cell, cellIndex: cCellIndex + 1 } }else { return cell } }) return { rows: _rows, selectedCells: _selectedCells } } }, { cmd: 'removeColumn', text:"列削除", icon: 'mdi-table-column-remove', color: 'error', isActive: (params) => anySelectedCells(params) && notAllColumnsSelected(params), onClick: ({ selectedCells, rows, maxColNum, }) => { const cellIndicesToRemove = new Set( selectedCells.map((cell) => cell.cellIndex) ) if(cellIndicesToRemove.size == maxColNum) { return } const _rows = rows.map((row) => { return { ...row, article_items: row.article_items.filter((cells, cellIndex)=> !cellIndicesToRemove.has(cellIndex) ) } }) return { rows: _rows, selectedCells: [] } }, }, { cmd: 'addRowAfter', text:"下に行追加", icon: 'mdi-table-row-plus-after', isActive: singleSelectedCell, onClick: ({ selectedCells, rows, maxColNum, thPosition, }) => { if(selectedCells.length != 1) { return { selectedCells, rows, } } const _rows = [ ...rows.slice(0, selectedCells[0].rowIndex + 1), ...[getTr( {maxColNum, thPosition} )], ...rows.slice(selectedCells[0].rowIndex + 1), ] const _selectedCells = selectedCells.map((cell) => { const { rowIndex: cRowIndex } = cell if (cRowIndex > selectedCells[0].rowIndex){ return { ...cell, rowIndex: cRowIndex + 1, } } else { return cell } }) return { rows: _rows, selectedCells: _selectedCells } }, }, { cmd: 'addRowBefore', text:"上に行追加", icon: 'mdi-table-row-plus-before', isActive: (params) => singleSelectedCell(params) && notThColandSelectedFirstRow(params), onClick: ({ selectedCells, rows, maxColNum, thPosition, }) => { if(selectedCells.length != 1) { return { selectedCells, rows, } } const _rows = [ ...rows.slice(0, selectedCells[0].rowIndex), ...[getTr( {maxColNum, thPosition} )], ...rows.slice(selectedCells[0].rowIndex), ] const _selectedCells = selectedCells.map((cell) => { const { rowIndex: cRowIndex } = cell if (cRowIndex >= selectedCells[0].rowIndex){ return { ...cell, rowIndex: cRowIndex + 1, } } else { return cell } }) return { rows: _rows, selectedCells: _selectedCells } } }, { cmd: 'removeRow', text:"行削除", icon: 'mdi-table-row-remove', color: 'error', isActive: (params) => anySelectedCells(params) && notAllRowsSelected(params), onClick: ({ selectedCells, rows, }) => { const rowIndicesToRemove = new Set( selectedCells.map((cell)=> cell.rowIndex) ) if(rowIndicesToRemove.size == rows.length) return const _rows = rows.filter((row, rowIndex)=> !rowIndicesToRemove.has(rowIndex) ) return { rows: _rows, selectedCells: [] } }, } ] export default commandscommands.jsの中身はかなり長いので、vueファイルから切り出すことでコードを見やすくすることができる。
参考リンク
>export defaultとimport fromの使い方
>複数の変数やメソッドを渡す方法
>MDN公式 export default
- 投稿日:2020-11-13T11:53:32+09:00
【Vue.js】コンポーネントを使うとうまく表示されない時の解決策
はじめに
Vue.jsを学習している最中にコンポーネントの使い方について躓いたところがあったので、
記録かつ同じ所で躓いた方のためになればと思い、投稿します。コンポーネントとは?
公式サイト引用
Vue においては、「コンポーネント」は本質的にはあらかじめ定義されたオプションを持つ Vue インスタンスです。
簡単に言い換えると独自のHTMLタグを再利用したいときに使用するものですかね。
使ってみる
jsファイルに"user-item"というコンポーネントを作成。
sample.jsVue.component('user-item', { props: ['user'] template: '<li>{{ user.name }}</li>' }) new Vue({ el: "app-user" data: { users: [ {id: 001, name: "Sato"}, {id: 002, name: "Tanaka"}, {id: 003, name: "Suzuki"}, ] }HTML側で"user-item"タグを使用する。
sample.html<div id="app-user"> <ol> <user-item v-for="user in users" v-bind:user="user" v-bind:key="user.id" ></user-item> </ol> </div>画面での表示はこうなります。
1. Sato
2. Tanaka
3. Suzuki躓いたポイント
上記のようにV-forを使ってテーブルでも同じことがしたいと思い試したところ、うまくいきませんでした。
試したコードtable.jsVue.component('user-table', { props: ['user'], template: '\ <tr>\ <td>{{ user.id }}</td>\ <td>{{ user.name }}</td>\ </tr>\ ' }) new Vue({ el: "#app-table", data: { users: [ {id: 001, name: "Sato"}, {id: 002, name: "Tanaka"}, {id: 003, name: "Suzuki"}, ] } });table.html<div id="app-table"> <table> <thead> <tr> <th>ID</th> <th>NAME</th> </tr> </thead> <tbody> <user-table v-for="user in users" v-bind:user="user" v-bind:key="user.id" ></user-table> </tbody> </table> </div>画面での表示
1Sato
2Tanaka
3Suzuki
ID NAMEヘッダーが一番最後にきてしまっています。
解決策
HTMLにはタグの入れ子にルールがあり、なんでも入れれるわけではありません。
tbodyタグに関してMDN調べると、許可されている内容 : 0 個以上の tr 要素
つまり、今回のuser-tableタグはHTMLのルールに引っかかっていることが分かりました。
そんな場合にVueではis属性というものが提供されています。
is属性を使用したHTMLコードtable.html(修正後)<div id="app-table"> <table> <thead> <tr> <th>ID</th> <th>NAME</th> </tr> </thead> <tbody> <tr is="user-table" v-for="user in users" v-bind:user="user" v-bind:key="user.id" ></tr> </tbody> </table> </div>うまく表示できました。
まとめ
HTMLの入れ子タグにはルールがあるので、is属性を使って回避しました。
- 投稿日:2020-11-13T11:41:11+09:00
Vue.jsの$refsがundefinedになってしまう問題
console.log(this.$refs.hoge)としても、undefinedが返ってしまってくる場合について、ハマったのでまとめておきます。
二つの原因
- createdでthis.$refsの処理をしている
- v-showではなく、v-ifを使っている
createdでthis.$refsの処理をしている
index.vue<template> <div ref="hoge"> ~ </div> </template> <script> export default{ created(){ const hoge = this.$refs.hoge // 何らかの処理 } } </script>createdではなく、mountedを使うとうまくいきました。
基本的に、createdはDOM処理未完了、mountedはDOM処理完了の状態ですが、refを使う場合は該当のDOMが読み込まれていないといけません。
ページ描写時にrefの処理をするときは、mountedに記載しておいたほうがよさそうです。
createdでもうまくいくことがありましたが、ページの重さによるようです。v-showではなく、v-ifを使っている
前述したとおりDOMが生成されていないとrefの処理ができませんが、v-ifを使うとDOMがそもそも生成されません。
v-showならば、DOMを生成しつつdisplay:none;にしてくれるので、refの処理が通ります。通らない例
index.vue<template> <div ref="hoge" v-if="isHogeExist"> ~ </div> <p v-else>hogeはないよ</p> <button @click="isHogeExist = !isHogeExist"></button> </template> <script> export default{ data(){ return{ isHogeExist:false } } mounted(){ const hoge = this.$refs.hoge // 何らかの処理 } } </script>通る例
index.vue<template> <div ref="hoge" v-show="isHogeExist"> ~ </div> <p v-show="!isHogeExist">Hogeはないよ</p> <button @click="isHogeExist = !isHogeExist"></button> </template> <script> export default{ data(){ return{ isHogeExist:false } } mounted(){ const hoge = this.$refs.hoge // 何らかの処理 } } </script>以上です。
- 投稿日:2020-11-13T09:12:56+09:00
【1分解決】Vue.jsで小技を用いてフォームを作ってみる
はじめに
本記事へのアクセスありがとうございます。
投稿主はプログラミング初心者であり、この方法が「最適解」かは分かりません。
しかし、動作は検証済みです。こんなやり方もできるんだ〜程度に見てもらえれば幸いです。
さっそくスタート
この記事では下記の環境のフォームを作成します。
・インプットタグでenterキーを押すと、次のインプットタグへ遷移する
・2つ目のタグ内には半角数字のみを出力する<div> <input type="text" v-model="formOne" @keypress.enter="focusWord" autofocus /> <input type="text" ref="focusWord" v-model="formTwo" /> </div>data() { return { formOne: "", formTwo: "", }; }, watch: { formTwo: function (v) { this.formTwo = v.replace(/[A-Za-z0-9]/g, function (s) { return String.fromCharCode(s.charCodeAt(0) - 65248); }); this.formTwo = v .replace(/[^0-90-9]/g, "") .replace(/[0-9]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 0xfee0) ); }, }, methods: { firstWord() { this.$refs.focusWord.focus(); }, },おわり
結論として、watchを利用してリアルタイムに文字判断をしてfocusメソッドを用いて遷移させましょう。
少しでも役に立ったと思う方がいましたらLGTMをお願いします?♂️