- 投稿日:2019-07-09T18:35:49+09:00
[Vue]子コンポーネントのメソッドを、親が実行させるサンプル 〜子を意のままに操るには〜
したいこと
親コンポーネントから、子コンポーネントのメソッドを起動させたい場合のサンプルを共有します。
いきなり結論コード
親コンポーネント<!-- 〜略〜 --> <child ref="child"></child> <!-- 子コンポーネント --> <!-- 〜略〜 --> <script> this.$refs.child.$emit('child_method') </script>子コンポーネント<!-- 〜略〜 --> <script> methods: { child_method() { // 何かの処理 } }, </script>解説
ポイントを箇条書きすると…
親コンポーネント
- 使いたいメソッドのある子コンポーネントに
refを仕込み適当な名前をつける。- 呼び出す際は、
this.$refs.{refに仕込んだ名前}.$emit('{子コンポーネントにある使いたいメソッド名}')と書く。子コンポーネント
- 使いたいメソッドを仕込んでおく。
これだけです。
さいごに
端的な内容ですが、最後までありがとうございました。
関連記事
$refsとrefについての記事はこちらにもあります。
- 投稿日:2019-07-09T16:27:38+09:00
Vue.js+Firebase環境でreCAPTCHA v3を組み込む
はじめに
フロントエンドでVue.js(Vue CLI 3、Typescript)、バックエンドでFirebaseを使って個人?アプリを作成中ですが、reCAPTCHA v3を組み込むときに時間がかかったので、自身の忘備録として残したいと思います。
結論としては、まんまの↓を実装すれば良いです。
https://developers-jp.googleblog.com/2017/09/guard-your-web-content-from-abuse-with.html私の場合、要件に合わなかったので合う形に変えたサンプルをgithubにupしています。
secret keyなどは雑に扱ってます。実コードでは埋め込まないようにしましょう。。。
https://github.com/ryoutoku/vue-recaptcha-firebase想定読者
- Vue.jsを使う
- 注:TypeScript使っています
- reCAPTCHA v3を組み込む必要がある
- 注:バックエンドとしてFirebase(Cloud Functions)使っています
前提
- Firebaseでプロジェクトを作成しており、Vue.jsのコードのデプロイが可能な状態
書くこと
- サンプルコードなど
- 自身が理解した動作の内容
書かないこと
- VueとかFirebaseのプロジェクトの作成方法
- Firebaseのプロジェクトの設定(デプロイなど含む)
- reCAPTCHAのサイト登録などの方法
最終結果
- reCAPTCHAのスコアを取得(表示)する
reCAPTCHAとは
ざっくり言うと、ユーザがbotではないと判断するための以下のようなよくあるヤツです。
画像参照:https://developers.google.com/recaptcha/ちなみにv3はUIはありません。
前準備
組み込むためには、使用するための前準備が必要です
- googleアカウントを作成
- reCAPTCHAを使用するサイト等を設定
- 以下サイトからreCAPTCHA使うサイトの登録を行う必要があります
サイトキーとシークレットキーの発行が必要です
https://www.google.com/recaptcha/intro/v3.html処理イメージ
今回実装したもののデータの流れのイメージはこんな感じになりました。
ちなみに「reCAPTCHAサーバ」とか正確な名称ではありませんが、ニュアンスだけ感じて頂ければ。
- reCAPTCHAの設定で発行されたサイトキーを元にサーバにアクセス
- サーバからtokenが発行される
- 発行されたtokenを元にCloud Functionsにアクセス
- CORSの設定上、フロントから直接reCAPTCHAサーバにアクセスできないためCloud Functionsを経由する必要がある
- Cloud Functionsからtokenを元にreCAPTCHAサーバにアクセス
- 判定結果がCloud Functionsに戻る
- Cloud Functionsからロボットか否かの判定が帰ってくる
- 1~2は、以下の「Frontend integration」のコードに該当
https://developers.google.com/recaptcha/docs/v3- 3~5は、以下ページの記載に該当
https://developers.google.com/recaptcha/docs/verifyサンプルのUI
以下の様な簡単な動作をするものを作成しました。
1.reCAPTCHA実行するボタンを押下
2. 通信して戻ってきたtokenをtoken横に表示
3. そのtokenを使ってreCAPTCHAのチェックを実行、結果をresult横に表示
4. errorがあればerror横に表示する実装コード
フォルダ構成
作成したプロジェクトの構成は↓の様な感じです。
├── babel.config.js ├── firebase.json ├── functions │ ├── node_modules │ ├── package.json │ ├── package-lock.json │ ├── src │ │ └── index.ts // ← バックエンド側のメインロジック │ ├── tsconfig.json │ └── tslint.json ├── node_modules ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── README.md ├── src │ ├── App.vue │ ├── assets │ │ └── logo.png │ ├── components │ │ └── reCAPTCHAUI.vue // ← フロント側のメインロジック │ ├── main.ts │ ├── shims-tsx.d.ts │ └── shims-vue.d.ts ├── tsconfig.json ├── tslint.json └── yarn.lockVueにはreCAPTCHA v3用のVue reCAPTCHA-v3といったコンポーネントも公開されていますが、あまりメリットを感じられなかったのでreCAPTCHA-v3を使って実装しています。
cd project_root npm install recaptcha-v3 // or yarn add recaptcha-v3バックエンド側
処理コードのメインロジック
functions/src/index.tsimport * as functions from "firebase-functions"; import * as rp from "request-promise"; export const checkRecaptcha = functions.https.onCall(async (data, context) => { // ここに発行したsecret keyを設定 const secret = "secret"; let response = {}; await rp({ uri: "https://recaptcha.google.com/recaptcha/api/siteverify", method: "POST", formData: { secret, response: data.token, }, json: true, }) .then(async result => { console.log("recaptcha result", result); response = result; }) .catch(async reason => { console.log("Recaptcha request failure", reason); response = reason; }); return response; });簡易解説:
functions.https.onCallでデータを受ける
functions.https.onRequestを使っても可能だが、APIを外部に公開する訳ではない&外部公開する場合はHTTPメソッド(特にOPTIONS)に対応する必要があるためこちらを使う- reqestモジュールを使ってPOSTで
https://recaptcha.google.com/recaptcha/api/siteverifyにtokenを投げるフロントエンド側処理コード
処理コードのメインロジック抜き出し
src/components/reCAPTCHAUI.tsの一部interface IReCAPTCHAResult { success: boolean; challenge_ts: string; hostname: string; "error-codes": []; } @Component export default class reCAPTCHAUI extends Vue { private token: string = ""; private result: IReCAPTCHAResult = { success: false, challenge_ts: "", hostname: "", "error-codes": [] }; // ここに発行したsite keyを設定 private siteKey = "site-key"; // ここでreCAPTHCAのチェックレベルを設定 private action = "homepage"; private error = {}; private async click() { // モジュールを使ってtokenを取得する const recaptcha = await load(this.siteKey); this.token = await recaptcha.execute(this.action); // tokenを用いてCloud FunctionsのAPIを実行する const func = firebase.functions().httpsCallable("checkRecaptcha"); await func({ token: this.token }) .then(async response => { this.result = (await response.data) as IReCAPTCHAResult; }) .catch(error => { this.error = error; }); } }簡易解説:
- ボタン押下で
click()を呼び出すactionには所定の文字列を設定- 結果取得まで処理を待ちたいので
asyncとawaitを使用- Cloud Functionsは
functions.https.onCallを使用するので、それに準じfirebase.functions().httpsCallableを使用終わりに
Vue.js+FirebaseでreCAPTCHAのスコアを持ってくるところまでできました。
本サンプルでは結果をそのままフロントまで返していますが、バックエンド側でscoreに依る判定を組み込めばokです。参考
公式
https://www.google.com/recaptcha/intro/v3.html
https://developers-jp.googleblog.com/2017/09/guard-your-web-content-from-abuse-with.htmlその他参考
https://qiita.com/ritou/items/e92aad65c5d8c906edb3
https://github.com/AurityLab/vue-recaptcha-v3
https://github.com/AurityLab/recaptcha-v3
- 投稿日:2019-07-09T16:02:52+09:00
【Vue】メニュー周りの権限制御を行う
概要
VueなどでSPAを構築する場合、大体こんな感じのメニューがあると思います。
クリックされた項目でURLをルーティングして内容を変化させるやつですね。
権限制御を行う
このメニューで、以下のような権限制御を行う場合の例をご紹介します。
(※バックエンドについては今回省略します。)
- ユーザーのロールに設定された権限によりメニューの項目を出し分けたい
- 使用する権限を持っていない場合、項目は非表示とする
- URL直打ちでも開けないようにしたい
環境
- Vue 2.6
- VueRouter 3.0
- Vuex 3.0
- Vue Router Multiguard
- VueRouterのbeforeEnternに使用可能なヘルパー関数を用意してくれます
- URLごとにナビゲーションガードを行うために使用します
- https://github.com/atanas-angelov-dev/vue-router-multiguard#readme
テーブル
テーブルはこんな感じです
ユーザーはロールを1つ持ち、ロールは権限を複数持つ構成となっています。ユーザーテーブルのレコード例
ユーザーコード パスワード ロールコード admin ******* admin user1 ******* writer user2 ******* readonly ロール権限のレコード例
ロールコード 権限 admin menu1 admin menu2 admin menu3 writer menu1 writer menu2 readonly menu1 権限を取得
ストア
- ユーザーに紐付いた権限を保持します。
action-types.jsexport const GET_MENU_FLGS = "GET_MENU_FLGS"; export const ACTION = { GET_MENU_FLGS };mutation-types.jsexport const UPDATE = "UPDATE"; export const MUTATION = { UPDATE };menu-store.jsimport { MUTATION } from "./mutation-types"; import { ACTION } from "./action-types"; import axios from "axios"; const state = { //権限 menuFlgs: {} }; const getters = { menuFlgs: state => { return state.menuFlgs; } }; const actions = { //ログインユーザーのメニュー使用権限を取得します。 async [ACTION.GET_MENU_FLGS]({ commit }) { await axios .get("menus") .then(res => { commit(MUTATION.UPDATE, { menuFlgs: res.data }); }) .catch(err => { throw err; }); } }; const mutations = { [MUTATION.UPDATE](state, { menuFlgs }) { state.menuFlgs = menuFlgs; } }; export default { state: state, getters: getters, actions: actions, mutations: mutations };権限のJSONはこのようなイメージです。
menuFlgs: [ { menu1: true }, { menu2: true }, { menu3: true } ];VueRouter
- ①URLごとに権限を持っているかを判定します
- 権限がない場合は遷移をキャンセルします
- ②ストアに権限が保持されていない場合、取得します
router.jsimport Vue from "vue"; import Router from "vue-router"; import multiguard from "vue-router-multiguard"; import store from "/store/store.js"; import { GET_MENU_FLGS } from "/store/menu/action-types"; Vue.use(Router); /* * * ①ナビゲーションガード * */ const menu1Guard = (to, from, next) => { if (store.getters.menuFlgs.menu1) { next(); } else { next(false); } }; const menu2Guard = (to, from, next) => { if (store.getters.menuFlgs.menu2) { next(); } else { next(false); } }; const menu3Guard = (to, from, next) => { if (store.getters.menuFlgs.menu3) { next(); } else { next(false); } }; /* * * ルート定義 * */ const router = new Router({ routes: [ { path: "/menu1", //① beforeEnter: multiguard([menu1Guard]), component: () => import("@/components/pages/menu1/menu1Template") }, { path: "/menu2", //① beforeEnter: multiguard([menu2Guard]), component: () => import("@/components/pages/menu2/menu2Template") }, { path: "/menu3", //ナビゲーションガードを複数設定することもできます。 beforeEnter: multiguard([menu1Guard, menu2Guard, menu3Guard]), component: () => import("@/components/pages/menu3/menu3Template") } ] }); /* * * ②権限を取得します。 * */ router.beforeEach(async (to, from, next) => { if (!Object.keys(store.getters.menuFlgs).length) { await store.dispatch(GET_MENU_FLGS); } next(); }); export default router;メニュー表示
- ①メニューを配列で定義しておきます。
- ②ストアの権限から、使用可能なメニューのみ表示します
<template> <v-list> <v-list-tile v-for="menu in availableMenus" :key="menu.id"> <router-link :to="menu.url">{{menu.title}}</router-link> </v-list-tile> </v-list> </template> <script> export default { data() { return { //①メニューマスタ menuMaster: [ { id: 1, title: "メニュー1", url: "/menu1" }, { id: 2, title: "メニュー2", url: "/menu2" }, { id: 3, title: "メニュー3", url: "/menu3" } ] }; }, computed: { //②使用可能なメニューで絞り込みます availableMenus() { let available = []; for (const menu of this.menuMaster) { if (this.isAvailable(menu.url)) { available.push(menu); } } return available; } }, methods: { //ログインユーザーがメニューの使用権限を持っているかを判定します。 isAvailable(url) { const menuName = url.slice(1); return this.$store.getters.menuFlgs[menuName]; } } }; </script>まとめ
- Vue Router Multiguardを使うとナビゲーションガードが扱いやすくなりました
- 権限をどのタイミングで取得するかは賛否両論ありそうです
- 投稿日:2019-07-09T14:57:29+09:00
Vueをマウントできなくて奮闘
プログラミング歴3か月くらいの初心者です、自分のメモ用。
bladeファイル内になどを記載して、コンポーネントを挿入しても
app.js:38008 [Vue warn]: Failed to mount component: template or render function not defined. found in ---> <コンポーネント名> <Root>上記のエラーが発生する。
app.jsにもしっかり記載している。
Vue.component('my-component', require('./components/MyComponent.vue'));いろいろな記事を見て、webpack.config.jsに追記だの devコマンドを実行したかだのいろいろ対処法が
書かれていたがどれでも解決できないところ以下の記事を見つけた。どうやら app.jsで
Vue.component('my-component', require('./components/MyComponent.vue').default);最後に.defaultを追記するだけでした。(意味は知らない)
- 投稿日:2019-07-09T14:36:07+09:00
VuetifyのDatepickerの値が消えない
VuetifyのDatepicker、すごく便利なんだけど、
v-text-fieldと一緒に使った時によく使うclearableで値が消えないなーと思っていたメモ。何したのか
v-text-fieldに@click:clear="date = null"を追加した。コードは公式サンプルを利用
https://vuetifyjs.com/en/components/date-pickers#date-pickers-formatting-date-using-external-libs<template> <v-menu v-model="menu1" :close-on-content-click="false" full-width max-width="290" > <template v-slot:activator="{ on }"> <v-text-field :value="computedDateFormattedMomentjs" clearable label="Formatted with Moment.js" readonly v-on="on" @click:clear="date = null" // ←を追加 ></v-text-field> </template> <v-date-picker v-model="date" @change="menu1 = false" ></v-date-picker> </v-menu> </template>消えなかった理由
v-text-fieldを普通に使ってるだけならclearableオプションを追加するだけでいいけど、他の要素と組み合わせて使う時readonlyをつけてる。多分これが原因。
見た目部分だけは消せるけど、v-modelで紐付けられた値に変更後の値を返さないよーってなってるはず。以上!
- 投稿日:2019-07-09T12:51:38+09:00
Vue.js を勉強する Session�6
条件付きレンダリング
v-if
v-ifディレクティブは、要素を条件に応じて描画したい場合に使用する。
html部分<div id="example"> <p v-if="awesome"> 条件がtrueの時に表示される </p> </div>javascript部分let app = new Vue({ el: '#example', data: { awesome: false } })開発者モードのConsoleapp.awesome = true // trueブラウザの表示条件がtrueの時に表示されるv-else
通常のjavascript同様にv-elseでelseを記述することが可能です。
v-else要素は、v-ifまたはv-else-if要素の直後になければいけません。
それ以外の場合は認識されません。html部分<div id="example"> <p v-if="awesome"> 条件がtrueの時に表示される </p> <p v-else="awesome"> 条件がfalseの時に表示される </p> </div>javascript部分let app = new Vue({ el: '#example', data: { awesome: false } })開発者モードのConsoleapp.awesome = true // trueブラウザの表示条件がtrueの時に表示されるv-else-if
通常のjavascript同様にv-else-ifでelse ifを記述することが可能です。
v-else-if要素は、v-ifまたはv-else-if要素の直後になければいけません。
それ以外の場合は認識されません。html部分<div id="example"> <p v-if="awesome == 'A'"> 条件がAの時に表示される </p> <p v-else-if="awesome == 'B'"> 条件がBの時に表示される </p> <p v-else-if="awesome == 'C'"> 条件がCの時に表示される </p> <p v-else="awesome"> 条件に当てはまらなった時に表示される </p> </div>javascript部分let app = new Vue({ el: '#example', data: { awesome: '' } })ブラウザの表示条件に当てはまらなった時に表示される開発者モードのConsoleapp.awesome = 'A' // "A" // ブラウザの表示:条件がAの時に表示される app.awesome = 'B' // "B" // ブラウザの表示:条件がBの時に表示される app.awesome = 'C' // "C" // ブラウザの表示:条件がCの時に表示される app.awesome = 'D' // "D" // ブラウザの表示:条件に当てはまらなった時に表示されるテンプレートでのv-ifによる条件グループ
v-ifはディレクティブなので、1つの要素に付加する必要があります。
1要素よりも多くの要素を切り替えたい場合にHTML のコンテンツテンプレート(< template >)要素を使用します。html部分<div id="example"> <template v-if="boxflg"> <h1>Title</h1> <p>Paragraph 1</p> <p>Paragraph 2</p> </template> </div>javascript部分let app = new Vue({ el: '#example', data: { boxflg: false } })開発者モードのConsoleapp.boxflg = true // trueブラウザの表示Title Paragraph 1 Paragraph 2開発者モードのElement<div id="example"> <h1>Title</h1> <p>Paragraph 1</p> <p>Paragraph 2</p> </div>htmlのElementにコンテンツテンプレートが含まれることはありません。
key による再利用可能な要素の制御
Vue.jsは要素を可能な限り効率的に描画しようとしますが、1からレンダリングするかわりにそれら要素を再利用することがよくあります。
Vue.jsを非常に速くするのに役立つ以外にも、これにはいくつかの便利な利点があります。
たとえば、ユーザーが複数のログインタイプを切り替えることを許可する場合は、次のようにします。
- UserNameでログイン
- Emailでログイン
上記の2つを切り替える入力画面を作ります。
html部分<div id="example"> <template v-if="loginType === 'username'"> <label>Username</label> <input placeholder="Enter your username"> </template> <template v-else> <label>Email</label> <input placeholder="Enter your email address"> </template> <button @click="change">ログイン方法変更</button> </div>正直愚直なコードな気がして最善な書き方では無い気がしますが…
javascript部分let app = new Vue({ el: '#example', data: { loginType: 'username' }, methods: { change(){ if (!(this.loginType == "username")) { this.loginType = "username" }else{ this.loginType = "email" } } } })上記のコードでloginTypeを切り替えても、ユーザーが既に入力したものは消去されません。
両方のテンプレートが同じ要素を使用するので、< input > は置き換えられません。
placeholderが切り替わるだけです。
この結果は望ましい結果では無いかもしれません。
この2つの要素は完全に別個のもので、再利用したく無い場合があり、要素に一意であることを示すためにkey属性を追加します。html部分<div id="example"> <template v-if="loginType === 'username'"> <label>Username</label> <input placeholder="Enter your username" key="username-input"> </template> <template v-else> <label>Email</label> <input placeholder="Enter your email address" key="email-input"> </template> <button @click="change">ログイン方法変更</button> </div>javascriptのコードは上記の例と変更はありません。
これでログイン方法が変更ボタンが押されるたびにinputが再利用されなくなります。v-show
条件的に要素を表示するための別のオプションです。
使用方法はv-ifとほとんど同じです。html部分<div id="example"> <p v-show="awesome"> 条件がtrueの時に表示される </p> </div>javascript部分let app = new Vue({ el: '#example', data: { awesome: false } })開発者モードのElements<div id="example"> <p style="display: none;"> 条件がtrueの時に表示される </p> </div>開発者モードのConsoleapp.awesome = true // trueブラウザの表示条件がtrueの時に表示される開発者モードのElements<div id="example"> <p style> 条件がtrueの時に表示される </p> </div>v-ifとの違いは v-sowディレクティブでは要素自体は常に描画されてDOMに維持するということです。
v-showはシンプルに要素のdisplayCSSプロパティを切り替えているだけです。【注意】
v-showはコンテンツテンプレートをサポートしていません。
アンチパターン<div id="example"> <template v-show="boxflg"> <h1>Title</h1> <p>Paragraph 1</p> <p>Paragraph 2</p> </template> </div>これはコンテンツテンプレートがDOMのElementsに含まれないためです。
v-if vs v-show
v-if
イベントリスナと子コンポーネント内部の条件ブロックが適切に破棄され、切り替えられると再レンダリングされるため、"リアル"な条件レンダリングです。
v-ifは遅延描画(lazy)です。
初期表示においてfalseの場合レンダリングされず、条件がtrueになったときにレンダリングされます。v-show
要素は条件に関わらず常に描画されており、displayCSSプロパティを切り替えているだけです。
結論
シンプルかつ頻繁に切り替える必要があるのならばv-showを使用し
複雑な処理、条件が実行時から変更されることがほとんどない場合はv-ifを選ぶあとがき
v-ifとv-forの題目はv-ifとv-forを同時に利用する事は推奨されておらずバッドプラクティスですということです。
詳細はスタイルガイドを参照してください参考資料
- 投稿日:2019-07-09T12:39:46+09:00
【Vuex】ストアの4つの概念まとめ【唯一の情報源】
Vuexの根幹 【ストア】
【役割】
Vuexを使う上でアプリケーションの状態(情報)を保持する役割です。Vuexのコンセプトである『Vuexは信頼できる唯一の情報源である事』が前提にあるため、ストアはアプリケーションの中でただ一つだけの根幹となる存在です。
今回はその大事な役割であるストアの4つの概念をまとめます。4つの概念
・state→アプリケーションの状態(情報)
・getter→stateの一部やstateから返された値を保持する
・mutation→stateを更新(変化)させる
・action→非同期通信や外部APIとのやりとりを行うこの4つを一纏めにしたものをモジュールと言います。
モジュールでこの4つを守っているイメージです。
ストアの作成
Vue-cliでVuexを取り込んでおけば、srcフォルダにstore.jsが入っています。
Vuexも読み込まれていますので、そちらを使います。
まだ準備ができていない方はこちらの記事を参考にしてみてください。
【Vue.js】Vuexの入り口store.jsimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ //説明の為、代入しました //ここに実装を書きます }) export default store説明のため少しいじりました。この状態から始めます。
state
store.jsimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ //stateオプションで初期値を設定 state: { count: 10 } }) //store.stateで参照 console.log(store.state.count) //10 export default store【役割】
アプリケーション全体の状態を保持します。stateはdataオプションのような存在で、stateが変更されるとコンポーネントの算出プロパティやテンプレートへと反映されます。
これはVuexがVueのリアクティブシステムを活用して実装されている為です。
state更新時にUIの再描画が必要最小限に留まり、開発コストが下がります。【参照】
store.state.state名【注意点】
なんでもstateに情報を保持させるのではなく、コンポーネント内部でしか使わないようなものはこれまでと同様にdataオプションに、アプリケーション全体で管理するものはstore内で管理すると良いです。getter
store.jsimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { count: 10 }, //gettersオプションで定義 getters: { //stateから別の値を計算 squared: function(state) { return state.count * state.count }, //他のgettersの値を使うことも可能 (省略技法ももちろん使える) cubed: (state, getters) => state.count * getters.squared } }) console.log(store.state.count) //10 //store.gettersで参照 console.log(store.getters.cubed) //1000 export default store【役割】
stateから別の値を算出する為に使われます。gettersは算出プロパティcomputedと同様な働きをしてくれます。
値がキャシュされ、そのgettersが依存しているstateが更新されない限り再評価しません。
違う点は引数にstateと他のgettersを持つことで、それらを使って違う値を返します。【参照】
store.getters.getters名【引数】
state
他のgetters【注意点】
getterを参照した時に定義した関数が常に発火する訳ではありません。
computedと同様に、計算した値を返す以外の処理は行うべきではありません。mutation
store.jsimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { count: 10 }, //mutationsオプションで定義 mutations: { // 'increment'mutationを定義 increment: function(state, payload) { state.count += payload.amount } } }) console.log(store.state.count) //10 // store.commitでmutationを呼び出す store.commit('increment', { amount: 5 }) console.log(store.state.count) //15 export default store【役割】
stateの値を更新する為に使われます。【呼び出し】
store.commit('mutation名', payload)【引数】
mutation→ 第一引数:state(渡されたstateを更新)
commit→ 第二引数:payload
(store.commitの第二引数をpayloadと呼び、これを使用することで同じmutationで異なる値にstateを変更できます。)【注意点】
原則として、mutation以外でstateの更新を行うことを禁止しています。
stateの状態がいつどこで発生したのかを追跡しやすくする為です。
また、全て同期的な処理にする必要があります。
非同期処理を用いると特定のケースで意図しない動作を行ってしまう可能性がある為です。
非同期処理を行いたい時はactionsで定義します。action
store.jsimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { count: 10 }, mutations: { increment: function(state) { state.count += 1 } }, // actionオプションで定義 actions: { incrementAction: function(ctx) { // 'increment'mutationを実行 ctx.commit('increment') } } }) console.log(store.state.count) //10 // store.dispatchでactionを呼び出す store.dispatch('incrementAction') console.log(store.state.count) //11 export default store【役割】
非同期処理や外部APIとの通信を行い、最終的にmutationを呼び出す為に使われます。【呼び出し】
store.dispatch.action名【引数】
ctx(context)contextとは特別なオブジェクトで、次の内容が含まれています。
・state: 現在のstate
・getters: 定義されているgetter
・dispatch: 他のactionを実行するメソッド
・commit: 他のmutationを実行するメソッド分割代入
{ 変数名 }という記法があり、オブジェクトのプロパティを変数に代入する記述を省略できます。
(例)
分割代入不使用:let commit = ctx.commit
分割代入を使用:{ commit }【注意点】
mutation同様に、直接呼び出すことはできません。
また、dispatchを使うことによって共通の処理を一つのactionにまとめる事ができますが、使いすぎるとどのactionから呼ばれているのか分かりづらくなってしまいます。非同期処理の例
Ajaxでデータを取得し、そのデータをpayloadに含めたmutationを呼び出すactionを定義している例です。
store.jsimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // 例として非同期処理を行う関数 function getCountNum(type) { return new Promise(resolve => { // 1秒後にtypeに応じたデータを返す setTimeout(() => { let amount switch (type) { case 'one': amount = 1 break; case 'two': amount = 2 break; case 'ten': amount = 10 break; default: amount = 0 } resolve({ amount }) }, 1000); }) } const store = new Vuex.Store({ state: { count: 10 }, mutations: { increment: function(state, payload) { state.count += payload.amount } }, // actionオプションで定義 actions: { incrementAsync: function({ commit }, payload) { // 非同期にデータを取得 return getCountNum(payload.type) .then(data => { console.log(data) commit('increment', {amount: data.amount}) }) } } }) //後ほど下記に書き換えます store.dispatch('incrementAsync', {type: 'one'}) //後ほど下記に書き換えaction内でPromiseを返している場合、store.dispatchの戻り値のPromiseを使い、action内の非同期処理の完了を検知できます。
上記はaction内でPromiseを返しているので、下記のようにstore.dispatchの戻り値のPromiseに対してthenでコールバックしactionを完了を検知しています。store.js//上記に追記 console.log(store.state.count) //10 store.dispatch('incrementAsync', {type: 'one'}) .then(() => { //actionの処理が完了した後に実行される console.log(store.state.count) //11 }) export default store
今回は以上です。
また学習が進み次第、更新、掲載していきます。
ここまでで補足や訂正などありましたら是非ご教授いただけると嬉しいです。
最後まで読んでいただきありがとうございます。
- 投稿日:2019-07-09T11:00:41+09:00
Vue.jsのプロジェクト作成(Vuex、vue-router、Axios、ESLint、Prettier、Jest)
書いてあること
- テンプレートを利用しないVue.jsプロジェクト作成手順
- 各パッケージの設定方法、および簡単な動作確認
- Vuex
- vue-router
- ESLint
- Prettier
- Jest
環境
- CentOS Linux release 7.6.1810 (Core)
- Node.js v10.16.0
- Npm 6.10.0
- Vue 3.9.1
事前準備
CentOSへNode.jsをインストール
CentOSへVue.jsをインストール
CentOSへGitをインストールプロジェクト作成
プロジェクト作成
bash$ vue create vue-project Vue CLI v3.9.1 ? Please pick a preset: Manually select features ? Check the features needed for your project: Babel, Router, Vuex, Linter, Unit ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes ? Pick a linter / formatter config: Standard ? 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 ? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? No開発サーバーで起動確認
bash$ cd vue-project $ npm run serveAxios
パッケージをインストール
bashnpm install axios --saveプロトタイプ設定
この設定により、すべてのコンポーネントにおいて「this.$axios」でaxiosが利用可能となる。
main.jsimport Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' +import axios from 'axios' Vue.config.productionTip = false +Vue.prototype.$axios = axios new Vue({ router, store, render: h => h(App) }).$mount('#app')動作確認
About.vue(ビュー)
src/views/About.vue<template> <div class="about"> <h1>This is an about page</h1> <hr /> <ZipSearch /> </div> </template> <script> import ZipSearch from '@/components/ZipSearch.vue' export default { name: 'about', components: { ZipSearch, }, } </script>ZipSearch.vue(コンポーネント)
src/components/ZipSearch.vue<template> <div class="about"> <button @click="doSearch"> Search </button> <hr /> <p>status: {{ result.status }}</p> <p>zipcode: {{ result.zipcode }}</p> <p>prefcode: {{ result.prefcode }}</p> <p>address1: {{ result.address1 }}</p> <p>address2: {{ result.address2 }}</p> <p>address3: {{ result.address3 }}</p> </div> </template> <script> export default { name: 'ZipSearch', data: () => { return { result: { status: null, zipcode: '', prefcode: '', address1: '', address2: '', address3: '', }, } }, methods: { doSearch() { this.$axios .get('/zipcode.json') .then(res => { // console.log(res) const dt = res.data.results[0] // console.log(dt) this.result.status = res.data.status this.result.zipcode = dt.zipcode this.result.prefcode = dt.prefcode this.result.address1 = dt.address1 this.result.address2 = dt.address2 this.result.address3 = dt.address3 }) .catch(err => { console.log(err) }) }, }, } </script>zipcode.json(モックデータ)
public/zipcode.json{ "message": null, "results": [ { "address1": "神奈川県", "address2": "川崎市高津区", "address3": "久本", "kana1": "カナガワケン", "kana2": "カワサキシタカツク", "kana3": "ヒサモト", "prefcode": "14", "zipcode": "2130011" } ], "status": 200 }開発サーバーで起動確認
bash$ npm run servevue-router
動作確認
ルーティング設定
src/router.jsimport Vue from 'vue' import Router from 'vue-router' import Home from './views/Home.vue' +import About from './views/About.vue' +import Test from './views/Test.vue' Vue.use(Router) export default new Router({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'home', component: Home, }, { path: '/about', name: 'about', - // route level code-splitting - // this generates a separate chunk (about.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => import(/* webpackChunkName: "about" */ './views/About.vue'), + component: About, }, + { + path: '/test', + name: 'test', + component: Test, + }, ], })App.vue
src/App.vue<template> <div id="app"> <div id="nav"> <router-link to="/">Home</router-link> <router-link to="/about">About</router-link> + <router-link to="/test">Test</router-link> </div> <router-view /> </div> </template>Test.vue
src/views/Test.vue+<template> + <div class="test"> + <h1>This is a test page</h1> + </div> +</template>開発サーバーで起動確認
bash$ npm run serveVuex
動作確認
ストア設定
src/store.jsimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { + count: 0, }, getters: { + count(state) { + return state.count + }, }, mutations: { + increment(state, payload) { + state.count += payload.cnt * 1 + }, }, actions: { + increment({ commit }, payload) { + commit('increment', payload) + }, }, })TestComp.vue(コンポーネント)
src/components/TestComp.vue<template> <div class="test"> <h2>{{ msg }}</h2> <p>Count: {{ count }}</p> <hr /> <input type="number" v-model="num" /> <button @click="doAdd">追加</button> <hr /> <div> <router-link to="/">Home</router-link> </div> </div> </template> <script> import store from '@/store.js' export default { name: 'TestComp', props: { msg: String, }, data: function() { return { num: 0, } }, computed: { count() { return store.getters.count }, }, methods: { doAdd() { store.dispatch('increment', { cnt: this.num }) }, }, } </script>Test.vue(ビュー)
src/views/Test.vue<template> <div class="test"> <h1>This is a test page</h1> <TestComp msg="test" /> </div> </template> <script> // import store from '@/store.js' import TestComp from '@/components/TestComp.vue' // 直接ステートを取得 // console.log('store', store.state.count) // ミューテーションを実行 // store.commit('increment', { cnt: 3 }) // ゲッターでデータを取得 // console.log('getters', store.getters.count) // アクションを実行 // store.dispatch('increment', { cnt: 5 }) // ゲッターでデータを取得 // console.log('getters', store.getters.count) export default { name: 'test', components: { TestComp, }, } </script>開発サーバーで起動確認
bash$ npm run serveESLint
パッケージをインストール
Vue.jsプロジェクト作成時にeslint、eslint-plugin-vueの2つが既にインストールされていることを確認。
bashnpm install eslint-config-standard eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-standard --save-dev設定ファイルを修正
.eslintrc.jsmodule.exports = { root: true, env: { + browser: true, node: true }, 'extends': [ 'plugin:vue/essential', '@vue/standard', + 'standard' ], + plugins: [ + 'vue' + ], rules: { 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', + 'generator-star-spacing': 'off' }, parserOptions: { parser: 'babel-eslint' } }ESlint対象外のディレクトリ・ファイル設定
.eslintignore/dist/ /node_modules/ /public/ /*.js /package.json /package-lock.json動作確認
エラーが発生するようにソースを修正
src/router.jsimport Vue from 'vue' import Router from 'vue-router' import Home from './views/Home.vue' Vue.use(Router) export default new Router({ mode: 'history', base: process.env.BASE_URL, - routes: [ + routes:[ { - path: '/', + path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ './views/About.vue') } ] -}) +});ESLintを実行(自動修正なし)
bash$ npx eslint -c .eslintrc.js --color './src/**/*.{html,js,vue}' /root/vue-project/src/router.js 10:10 error Missing space before value for key 'routes' key-spacing 12:1 error Expected indentation of 6 spaces but found 8 indent 25:3 error Extra semicolon semi ✖ 3 problems (3 errors, 0 warnings) 3 errors and 0 warnings potentially fixable with the `--fix` option.ESLintを実行(自動修正あり)
bash$ npx eslint -c .eslintrc.js --color './src/**/*.{html,js,vue}' --fixスクリプトを変更
package.json{ "name": "vue-project", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", - "lint": "vue-cli-service lint" + "lint": "eslint -c .eslintrc.js --color './src/**/*.{html,js,vue}' --fix" }, } (略)Prettier
パッケージをインストール
bash$ npm install prettier prettier-eslint prettier-eslint-cli eslint-plugin-prettier eslint-config-prettier --save-devPrettier設定ファイルを作成
.prettierrc.jsmodule.exports = { trailingComma: 'es5', printWidth: 120, tabWidth: 2, singleQuote: true, semi: false, }Prettier対象外のディレクトリ・ファイル設定
.prettierignore/dist/ /node_modules/ /public/ /*.js /package.json /package-lock.jsonESLint設定ファイルを修正
ESLintと同時にPrettier(コード整形)を合わせて実行するように設定
.eslintrc.jsmodule.exports = { root: true, env: { browser: true, node: true }, 'extends': [ 'plugin:vue/essential', '@vue/standard', 'standard', + 'prettier' ], plugins: [ 'vue', + 'prettier' ], rules: { 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'generator-star-spacing': 'off', + 'prettier/prettier': 'error' }, parserOptions: { parser: 'babel-eslint' } }ESLintを実行(自動修正あり)
ESLintと合わせてPrettierが実行される(ESLint動作確認時のソース修正に加え、タブサイズを変えておくとPrettierが動いたことが確認可能)
bashnpm run lintJest
パッケージをインストール
Vue.jsプロジェクト作成時に@vue/test-utils、babel-jestの2つが既にインストールされていることを確認。
bashnpm install jest vue-jest @babel/core babel-preset-vue-app eslint-plugin-jest --save-dev設定ファイルを修正
jest.config.jsmodule.exports = { moduleFileExtensions: [ 'js', - 'jsx', 'json', 'vue' ], transform: { '^.+\\.vue$': 'vue-jest', '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub', - '^.+\\.jsx?$': 'babel-jest', + '^.+\\.js?$': 'babel-jest' }, transformIgnorePatterns: [ '/node_modules/' ], moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', + '^~/(.*)$': '<rootDir>/src/$1' }, snapshotSerializers: [ 'jest-serializer-vue' ], testMatch: [ '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' ], testURL: 'http://localhost/', watchPlugins: [ 'jest-watch-typeahead/filename', 'jest-watch-typeahead/testname' ] }
moduleFileExtensions:テスト対象のファイルの拡張子transform:変換を行うモジュールtransformIgnorePatterns:変換時に無視するファイルmoduleNameMapper:パスの省略記法を指定。Vue.jsにおける「@」「~」記法を利用する場合はこちらを指定snapshotSerializers:スナップショットに利用するシリアライザーのモジュールパスtestMatch:テストファイルを検出するためのglobパターン。testURL:JSDom環境のURLwatchPlugins:カスタムウォッチプラグインを利用ESLint設定ファイルを修正
テストファイルの
test、expectがESLintエラーとならないようにする設定bash$ npx eslint -c .eslintrc.js --color './src/**/*.{html,js,vue}' # ESLint設定変更前(test、expectがエラーとなる) /root/vue-project2/src/func/__tests__/sum.spec.js 3:1 error 'test' is not defined no-undef 4:3 error 'expect' is not defined no-undef ✖ 2 problems (2 errors, 0 warnings).eslintrc.jsmodule.exports = { root: true, env: { browser: true, node: true, + 'jest/globals': true }, 'extends': [ 'plugin:vue/essential', '@vue/standard', 'standard', 'prettier' ], plugins: [ 'vue', 'prettier', + 'jest' ], rules: { 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'generator-star-spacing': 'off', 'prettier/prettier': 'error' }, parserOptions: { parser: 'babel-eslint' } }スクリプトを修正・追加
package.json{ "scripts": { - "test:unit": "vue-cli-service test:unit", + "test": "vue-cli-service test:unit", + "test:coverage": "vue-cli-service test:unit --coverage" }, }動作確認
テスト対象のJavaScript、テストファイルを準備
src/func/sum.jsfunction sum(a, b) { return a + b } export default sumsrc/func/__tests__/sum.spec.jsimport sum from '@/func/sum' test('adds 1 + 2 to equal 3', () => { expect(sum(1, 2)).toBe(3) })src/store/__tests__/store.spec.jsimport Vuex from 'vuex' import store from '@/store.js' import { createLocalVue } from '@vue/test-utils' const localVue = createLocalVue() localVue.use(Vuex) describe('mutations', () => { test('commit increment mutations ⇒ count+1', () => { expect(store.getters['count']).toBe(0) store.commit('increment', { cnt: 1 }) expect(store.getters['count']).toBe(1) }) }) describe('actions', () => { test('dispatch increment actions ⇒ commit increment mutations', () => { expect(store.getters['count']).toBe(1) store.dispatch('increment', { cnt: 2 }) expect(store.getters['count']).toBe(3) }) })src/components/__test__/TestComp.spec.jsimport TestComp from '@/components/TestComp.vue' import { mount, createLocalVue, RouterLinkStub } from '@vue/test-utils' const localVue = createLocalVue() describe('TestComp.vue', () => { let wrapper beforeEach(() => { wrapper = null wrapper = mount(TestComp, { localVue, stubs: { RouterLink: RouterLinkStub, }, }) }) // デフォルト表示テスト test('Default Output Test', () => { expect(wrapper.find('p').text()).toBe('Count: 0') }) // ボタンクリック後表示テスト test('number input ⇒ Output Test', () => { wrapper.find('input').setValue('10') wrapper.find('button').trigger('click') expect(wrapper.find('p').text()).toBe('Count: 10') }) // RouterLinkテスト test('router link home', () => { expect(wrapper.find(RouterLinkStub).props().to).toBe('/') }) })テスト実行
bash$ npm run test > vue-project@0.1.0 test /root/vue-project2 > vue-cli-service test:unit PASS src/components/__tests__/TestComp.spec.js PASS src/func/__tests__/sum.spec.js PASS tests/unit/example.spec.js PASS src/store/__tests__/store.spec.js Test Suites: 4 passed, 4 total Tests: 7 passed, 7 total Snapshots: 0 total Time: 1.969s, estimated 2s Ran all test suites.テスト実行(カバレッジ取得)
bash$ npm run test:coverage > vue-project@0.1.0 test:coverage /root/vue-project2 > vue-cli-service test:unit --coverage PASS src/components/__tests__/TestComp.spec.js PASS src/store/__tests__/store.spec.js PASS src/func/__tests__/sum.spec.js PASS tests/unit/example.spec.js ----------------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | ----------------|----------|----------|----------|----------|-------------------| All files | 100 | 100 | 100 | 100 | | src | 100 | 100 | 100 | 100 | | store.js | 100 | 100 | 100 | 100 | | src/components | 100 | 100 | 100 | 100 | | TestComp.vue | 100 | 100 | 100 | 100 | | src/func | 100 | 100 | 100 | 100 | | sum.js | 100 | 100 | 100 | 100 | | ----------------|----------|----------|----------|----------|-------------------| Test Suites: 4 passed, 4 total Tests: 7 passed, 7 total Snapshots: 0 total Time: 1.929s, estimated 2s Ran all test suites.【補足】VSCode ESLint設定
拡張機能をインストール
設定(settings.json)に下記を追記
settings.json"eslint.validate": [ "javascript", { "autoFix": true, "language": "vue" } ]
- 投稿日:2019-07-09T00:07:36+09:00
[備忘録]GitHubの情報を取得する
GitHubの情報を取得する
準備
https://github.com/settings/tokensのPersonal access tokensでGenerate new tokenしてapiを用意簡易的な方法
https://api.github.com/users/[username]?access_token=[Personal access token key]ソースコード
axiosを使用します
npm install --save-dev axiosapp.vue
vue.js<template> <div id="app"> <GitData repoName="UserName" /> </div> </template> <script> import GitData from "./components/Git.vue"; export default { name: "app", components: { GitData } }; </script>Git.vue
vue.js<template> <div class="gitdata"> <h>public repo num : {{this.userDatas.public_repos}}</h> </div> </template> <script> import axios from "axios"; export default { name: "GitData", props: { repoName: String }, data() { return { userDatas: [] }; }, mounted() { const request = axios.create({ baseURL: "https://api.github.com" }); request .get( `/users/${this.repoName}?access_token=api_key` // api key は gitにあげない!.envなどで管理 ) .then(res => { this.userDatas = res.data; console.log(res); }); } }; </script> <style lang="scss"> </style>memo
apiの取得はどこですれば?
今回はDOMに描画するのでmountedでする。
https://qiita.com/kimullaa/items/2bf8948dffb8c52d2b6bcreated
インスタンスの初期化が済んで props や computed にアクセスできる
DOMにはアクセスできない
mounted
created + DOMにアクセスできる







