- 投稿日:2021-12-03T20:33:50+09:00
【Vue.js】フォームバリデーションで使うgetter/setter
Tips1 公式リファレンス 入力文字列が20文字以上だった場合にリアルタイムでerrorの表示をさせたい。 算出プロパティのget/setを使用して入力を監視する。 vue <script> let app = new Vue({ el:'#app', data(){ return { contact:{ yourName:'', //input要素を定義する。 }, hasError:{ nameError: false //error判定用の初期値 } } }, computed:{ yourName:{ get(){ //yourNameで入力された取得。 return this.contact.yourName }, set(value){ //yourNameで入力された値を引数で受け取る。 if(value.length <= 20){ this.hasError.nameError = false //20文字以内だったらfalseをセット } else { this.hasError.nameError = true //20文字以上だったらtrueをセット } return this.contact.yourName = value //valueをyourNameに帰す } } } }) </script> html CSS:20文字以上だった場合のerrorクラスにcssをあてる。 文字数カウント:20文字以上だった場合に、errorクラスを付与させる。 v-show:20文字以上の時にメッセージを表示 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .error{ //20文字以上だった場合のerrorクラスにcssをあてる。 color:red; } </style> </head> <body> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> <div id="app"> <form> <label for="yourName">氏名</label> <input id="yourName" type="text" v-model="yourName"><br> <!-- computedで監視している値 --> <p :class="{error: hasError.nameError}">{{yourName.length}}/20</p> //20文字以上だった場合に、errorクラスを付与させる。 <p v-show="hasError.nameError" class="error">氏名は20文字以内</p> //v-show:20文字以上の時にメッセージを表示 {{contact.yourName}} </form> </div> 全体 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .error{ color:red; } </style> </head> <body> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> <div id="app"> <form> <label for="yourName">氏名</label> <input id="yourName" type="text" v-model="yourName"><br> <!-- computedで監視している値 --> <p :class="{error: hasError.nameError}">{{yourName.length}}/20</p> <p v-show="hasError.nameError" class="error">氏名は20文字以内</p> {{contact.yourName}} </form> </div> <script> let app = new Vue({ el:'#app', data(){ return { contact:{ yourName:'', }, hasError:{ nameError: false //error判定用の初期値 } } }, computed:{ yourName:{ get(){ return this.contact.yourName }, set(value){ //yourNameで入力された値を引数で受け取る。 if(value.length <= 20){ this.hasError.nameError = false } else { this.hasError.nameError = true } return this.contact.yourName = value //valueをyourNameに帰す } } } }) </script> </body> </html>
- 投稿日:2021-12-03T19:11:56+09:00
Cognito を使って認証機能付きのサーバーレスな API を構築する
はじめに この記事は、AWS Lambda と Serverless Advent Calendar 2021 3日目の投稿です! 「最近サーバーレスでこんなことやったぜっ!」と意気込んでサーバーレスのアドベントカレンダーにエントリーさせていただいたのですが、 いざ記事を書いてみると「これってサーバーレスじゃなくてもできるのでは」と気づきました。。 Serverless Framework 上で動かしているということは事実なので、お許しいただけると嬉しいです!! 構成 front / back 構成の Web アプリケーションを、以下のような組み合わせで構築しました。 front: Vue.js (Vue3) (S3でホスティングする予定) back: Serverless Framework を利用して Express on Lambda + API Gateway 認証: Cognito サンプルコードは TypeScript ですが、 JavaScript でも参考にできると思います。 そもそも Cognito とは Amazon Cognito は、ウェブおよびモバイルアプリの認証、承認、およびユーザー管理機能を提供します。ユーザーは、ユーザー名とパスワードを使用して直接サインインするか、Facebook、Amazon、Google、Apple などのサードパーティーを通じてサインインできます。 認証/認可を簡単にしてくれるやつ。いわゆる IDaaS。 Firebase Authentication や Auth0 の仲間、という理解です。 ユーザープール(認証)に加えてIDプール(認可)も使えば、 DynamoDB や S3 など、AWSリソースの認可が簡単にできるはず。 今回はユーザープールのみ試しました。 手順 0. APIを準備する Serverless Framework を利用して、 Lambda 上の Express を呼び出せる API Gateway のエンドポイントを作成しておきます。 参考情報がたくさんあるので割愛。 クラスメソッドさんの記事や、公式のサンプルリポジトリがわかりやすいです? 1. Cognito をセットアップする Cognito のユーザープールを作成し、アプリクライアントを追加しておきます。 こちらも細かい手順は割愛。 以下の記事を参考にしました。 Amazon Cognitoを使ったサインイン画面をつくってみる 2. ログイン画面を作る 今回のフロントは Vue.js。 サーバーレスのテーマから離れるのですが、ここが1番ハマりました? 静的なフロントで SDK を使うサンプルコードはたくさんあったのですが、 Vue.js は Amplify で実現している事例が多く、なかなかズバリな資料にたどり着けず。(Amplify は今回の要件に too much だったので見送りました) Cognito 関連のライブラリがいくつかあり色々試してみたところ、最終的には amazon-cognito-identity-js でうまくいきました。 本題からは離れますが、どなたかのお役に立つこともあるかもしれないのでソースコードを貼っておきます。 Signin.vue <template> <div id="signup"> <h1>Sign In</h1> <form name="form-signup"> <span>User ID(Email)</span> <input v-model="state.email" type="text" placeholder="Email Address" > <br> <span>Password</span> <input v-model="state.password" type="password" placeholder="Password" > <br><br> <input id="createAccount" type="button" value="サインイン" @click="onSigninButtonClick" > </form> </div> </template> <script lang="ts"> import { defineComponent, reactive, onMounted } from 'vue'; import { useRouter } from 'vue-router'; import * as AmazonCognitoIdentity from 'amazon-cognito-identity-js'; export default defineComponent({ name: 'SigninPage', setup() { const router = useRouter(); const state = reactive({ email: '', password: '', }); const onSigninButtonClick = () => { const authenticationDetails = new AmazonCognitoIdentity.AuthenticationDetails({ Username: state.email, Password: state.password, }); const userPool = new AmazonCognitoIdentity.CognitoUserPool({ UserPoolId: 'ユーザープールID', ClientId: 'アプリクライアントのID', }); const cognitoUser = new AmazonCognitoIdentity.CognitoUser({ Username: state.email, Pool: userPool, }); // 認証処理 cognitoUser.authenticateUser(authenticationDetails, { onSuccess(result) { // とりあえず idToken を console.log() しているけど、実際には storage に保存しよう console.log(`idToken: ${result.getIdToken().getJwtToken()}`); // サインイン成功の場合、次の画面へ遷移 router.push('/'); }, newPasswordRequired(userAttributes, requiredAttributes) { // パスワード再設定画面へ router.push('/new-password'); }, onFailure(err) { console.error(err); }, }); }; return { state, onSigninButtonClick, }; }, }); </script> あらかじめ Cognito のダッシュボードで登録しておいたユーザーでログイン→パスワード再設定→再度ログイン、で idToken を取得できました。 3. トークンを検証する ここからは API 側、Serverless Framework 側のプロジェクトを触ります。 まず、JWT トークンの検証用に、jsonwebtoken と jwks-rsa を追加しておきます。 npm install jsonwebtoken jwks-rsa 検証用のミドルウェアを作成します。 src/middlewares/verifytoken.ts import { Request, Response, NextFunction } from 'express'; import jwt, { JwtHeader, SigningKeyCallback } from 'jsonwebtoken'; import jwksClient from 'jwks-rsa'; // 作成したユーザープールの JWKS を指定 const client = jwksClient({ jwksUri: `https://cognito-idp.ap-northeast-1.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`, }); const getKey = (header: JwtHeader, callback: SigningKeyCallback) => { if (!header.kid) throw new Error('not found kid!'); client.getSigningKey(header.kid, (err, key) => { if (err) throw err; callback(null, key.getPublicKey()); }); } export default (req: Request, res: Response, next: NextFunction) => { // Authorization ヘッダーから idToken 部分を取り出す const authHeader = req.headers.authorization; if (authHeader == null) { res.status(401).json({ error: 'Missing authorization header.' }); return; } const [bearerText, idToken] = authHeader.split(' '); if (bearerText !== 'Bearer') { res.status(401).json({ error: 'Illegal authorization header format.' }); return; } // JWT を検証して、OKだったら Express の Request オブジェクトに格納しておく jwt.verify(idToken, getKey, (err, decoded) => { if (err) { res.status(401).json({ error: err.message }); return; } req.auth = decoded; next(); }); }; req.auth = decoded; の箇所で TypeScript の型エラーが発生してしまったので、以下のように Request インターフェースを拡張しておきました。 src/types/main.d.ts import { JwtPayload } from 'jsonwebtoken'; declare global { namespace Express { export interface Request { auth?: JwtPayload | undefined; } } } Router 側で、上記で作成したミドルウェアを指定します。 src/routers/authTest.router.ts import express, { Request, Response } from 'express'; import verifyToken from '../../middlewares/verifyToken'; const router = express(); router.get('/', verifyToken, (req: Request, res: Response) => { if (!req.auth) { res.json({ message: 'Something went wrong...' }); return; } // verifyToken ミドルウェアを通過しているので、 `req.auth` に情報がセットされているはず res.json({ message: `Hello! Your sub: ${req.auth.sub}` }); }); export default router; req.auth.sub にはユーザープール上で一意なユーザーIDがセットされています。 この値を DB に保管しておけば、認可の機能を持たせることもできます。 (e.g, ログインユーザーが保存した写真のみを表示する) また、 Cognito 自身にも IDプール という認可の仕組みがあるので、構成やユースケースに応じて使い分けたいですね。 4. フロントから呼び出してみる とりあえず curl から呼んでみましょう。 まずは serverless offline でAPIを起動。 serverless offline 2.で作成した画面で Cognito にログインし、 console.log() された idToken をコピーします。 curl http://localhost:3000/auth-test -H "Authorization:Bearer コピーしたidToken すると {"message":"Hello! Your sub:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"} ちゃんとミドルウェアを通過して、値が返ってきました。 試しに idToken を適当に変更してみると {"error":"invalid signature"} ちゃんとエラーになりました? あとは Vue.js 側でログイン時に idToken を保存して、各種 API 呼び出し時のヘッダーにセットすれば、認証機能付きアプリケーションの完成です。 よかったよかった。 おわりに Cognito を使うと、「いつも同じような実装してるけど、いつもそれなりに気を遣うな〜」という認証機能をおまかせできちゃうので、開発の負担が減りますね! IDプール(認可の機能)を使うとより本領を発揮する気がするので、もっといろいろ試してみたいな〜。
- 投稿日:2021-12-03T17:06:32+09:00
[vue]methods
methodの使い方 (クリックなどの)イベントをすると発火/実行する 基本的な書き方 <p>{{ counter }}</p> <!-- v-on:〇〇="メソッド名" --> <button @click="counterMethod"> *1 </button> <script> data: { counter: 0, }, methods: { // ↓ counterMethod () { と省略して書ける counterMethod: function() { this.counter += 1 } } </script> methodsの追記 htmlに書くときはどちらの書き方でも大丈夫 引数が必要な場合は()必須 ok <button @click="counterMethod"> *1 </button> ok <button @click="counterMethod()"> *1 </button>
- 投稿日:2021-12-03T15:51:33+09:00
Vue+compositionAPIでiframe内のスクロールを検知する
はじめに webアプリでよく見る利用規約を下までスクロールするとボタンが活性化し、同意できる機能を実装したい コード全体 <template> <div class="container"> <div class="content"> <div class="term-of-use-container"> <iframe class="term-of-use" ref="termOfUseRef" src="/term_of_use.html" frameborder="0" /> </div> <div class="button-container"> <button class="button color-orange" :class="{ 'color-gray': isDisabled }" :disabled="isDisabled" @click="onClickButton" > 同意する </button> </div> </div> </div> </template> <script lang="ts"> import { defineComponent, ref, onMounted } from "vue"; export default defineComponent({ setup() { const termOfUseRef = ref<HTMLIFrameElement>(); const isDisabled = ref<boolean>(true); const onClickButton = () => alert("同意しました!"); onMounted(() => { termOfUseRef.value?.contentWindow?.addEventListener("scroll", () => { if (!termOfUseRef.value) return; if (!termOfUseRef.value.contentWindow) return; const scrollAdjustmentValue = 50; // テキスト全体の高さ const scrollHeight = termOfUseRef.value.contentDocument?.documentElement.scrollHeight || 0; // スクロール量 const scrollAmount = termOfUseRef.value.contentWindow.scrollY || 0; // ウィンドウ(表示される枠)の高さ const windowHeight = termOfUseRef.value.contentWindow.innerHeight || 0; if ( scrollAmount + windowHeight + scrollAdjustmentValue > scrollHeight ) { isDisabled.value = false; } }); }); return { termOfUseRef, isDisabled, onClickButton, }; }, }); </script> <style scoped> .container { display: flex; justify-content: center; width: 100vw; height: 100vh; } .content { margin-top: 50px; margin-bottom: 50px; } .term-of-use { width: 500px; height: 500px; } .button-container { display: flex; justify-content: center; margin-top: 20px; } .button { width: 200px; color: #fff; border-radius: 100vh; } .color-orange { background-color: orange; } .color-gray { background-color: gray; } </style> 解説 <iframe class="term-of-use" ref="termOfUseRef" src="/term_of_use.html" frameborder="0" /> vueテンプレート内にref="termOfUseRef"のようにテンプレート参照を付与します。 const termOfUseRef = ref<HTMLIFrameElement>(); setup関数内で参照の変数を定義します。このとき変数名はテンプレート内に記述したref="termOfUseRef"と同じ名前にします。 onMounted(() => { termOfUseRef.value?.contentWindow?.addEventListener("scroll", () => { // 省略 }); }); onMounted内でイベントリスナを呼び出しスクロールイベントを検知します。 ポイントはcontentWindowの部分で、これによりiframe内のDOMにアクセスすることができます。そこでイベントリスナを呼び出すことでiframe内のスクロールを検知することができます。 const scrollAdjustmentValue = 50; // テキスト全体の高さ const scrollHeight = termOfUseRef.value.contentDocument?.documentElement.scrollHeight || 0; // スクロール量 const scrollAmount = termOfUseRef.value.contentWindow.scrollY || 0; // ウィンドウ(表示される枠)の高さ const windowHeight = termOfUseRef.value.contentWindow.innerHeight || 0; if ( scrollAmount + windowHeight + scrollAdjustmentValue > scrollHeight ) { isDisabled.value = false; } 最後にこちらのコードでスクロール量を計算し、下までスクロールした時にボタン活性化のフラグを更新します。 参考にさせていただきました
- 投稿日:2021-12-03T15:45:00+09:00
【Vue.js】v-modelを使用したフォームの作成
Tips1 基本的に公式リファレンスに丁寧に記述されているので細かいところはリファレンスを見てもらうのがいいと思う。 公式リファレンス そのなかで気になった点だけ、記述する。 selectbox optionにdisabledを使用しないとiosに上手く表示されないらしい <option disabled value="">年齢を選択してください。</option> v-model.オプションを選択できる。 .lazy 全ての入力が完了されないとバインディングされない。 .number 文字列ではなく整数で値がセットされる。 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> <div id="app"> <form action=""> 氏名 <input type="text" v-model="contact.yourName" /> <br /> 電話番号 <input type="tel" v-model="contact.tel" /> <br /> メールアドレス <input type="email" v-model.lazy="contact.email" /> <br /> 性別 <input type="radio" value="male" v-model="contact.gender" />男性 <input type="radio" value="female" v-model="contact.gender" />女性 <input type="radio" value="other" v-model="contact.gender" />その他 <br /> 年齢 <select type="text" v-model="contact.age"> <option disabled value="">年齢を選択してください。</option> <option>10代</option> <option>20代</option> <option>30代</option> <option>40代〜</option> </select> <br /> メッセージ <textarea type="text" v-model="contact.message"></textarea> <br /> このサイトを知った理由 <input type="checkbox" value="webサイト" v-model="contact.attracts" />webサイト <input type="checkbox" value="チラシ" v-model="contact.attracts" />チラシ <input type="checkbox" value="その他" v-model="contact.attracts" />その他 <br /> 注意事項に同意する <input type="checkbox" v-model="contact.caution" /> </form> </div> <script> let app = new Vue({ el: "#app", data() { return { contact: { yourName: "", tel: "", email: "", gender: "", age: "", message: "", attracts: [], caution: false, }, }; }, }); </script> </body> </html> Tips2 formにバリデーションをつけたい場合は、@submit.preventを使う。 送信ボタンを押してもrequestが飛ばない。 <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <style> .error{ color:red; } </style> </head> <body> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> <div id="app"> <p v-if="errors.length"> <ul> <li class="error" v-for="error in errors">{{error}}</li> </ul> </p> <form @submit.prevent='validate'> //この部分 氏名 <input type="text" v-model="contact.yourName" /> <br /> 電話番号 <input type="tel" v-model="contact.tel" /> <br /> メールアドレス <input type="email" v-model.lazy="contact.email" /> <br /> 性別 <input type="radio" value="male" v-model="contact.gender" />男性 <input type="radio" value="female" v-model="contact.gender" />女性 <input type="radio" value="other" v-model="contact.gender" />その他 <br /> 年齢 <select type="text" v-model="contact.age"> <option disabled value="">年齢を選択してください。</option> <option>10代</option> <option>20代</option> <option>30代</option> <option>40代〜</option> </select> <br /> メッセージ <textarea type="text" v-model="contact.message"></textarea> <br /> このサイトを知った理由 <input type="checkbox" value="webサイト" v-model="contact.attracts" />webサイト <input type="checkbox" value="チラシ" v-model="contact.attracts" />チラシ <input type="checkbox" value="その他" v-model="contact.attracts" />その他 <br /> 注意事項に同意する <input type="checkbox" v-model="contact.caution" /> <button type="submit" value="送信">送信</button> </form> </div> <script> let app = new Vue({ el: "#app", data() { return { contact: { yourName: "", tel: "", email: "", gender: "", age: "", message: "", attracts: [], caution: false, }, errors:[] }; }, methods:{ validate(){ this.errors = [] //初期化 if(!this.contact.caution){ this.errors.push('注意事項にチェックを入れてください。') } } } }); </script> </body> </html>
- 投稿日:2021-12-03T15:05:57+09:00
【Vue.js】v-bindによる双方向バインディングの仕組み
case v-bindで双方向のバインディングを行う場合, 入力された値が格納されている$event.target.valueへvue側が参照している状態になる。 これをシンプルに記述できるようにしているのがv-modelディレクティブ <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11/dist/vue.js"></script> <div id="app"> <input :value="test" @input="test = $event.target.value"> //この部分 </div> <script> let app = new Vue({ el:'#app', data(){ return { test:'aaa' } } }) </script> </body> </html>
- 投稿日:2021-12-03T12:25:00+09:00
【Jest】Component内に特定の要素が表示されないことをテストする【Vue】
この記事で説明すること 表題の通りです。 例えば、「v-if で制御している箇所がelseの場合に表示されないこと」をテストしたい時に使える方法です。 findAll と filter を使います。 テスト対象のコンポーネント Sample.vue <template> <div>あああ</div> <div>いいい</div> <div>ううう</div> <div>えええ</div> <div>おおお</div> </template> ※適当ですみませんw テストを書いてみる 例えば、「かかか」という要素が表示されないことをテストしたい時は、以下のように書きます。 sample.spec.ts // .. const wrapper = mount(Sample) expect(wrapper.findAll('div').filter((w) => w.text() === 'かかか').length).toBe(0); // .. ちょっと解説 findAll('div') でdiv要素を全て返す filter((w) => w.text() === 'かかか') でtextにかかかを持つ要素を返す まとめると、上記で空のWrapperArrayを返してくれるので、「lengthが0」であるテストを書いてます。 findAll('div').at(6).isExist()などでも書くことができますが、atで指定してしまうと、div要素の数が変わった時にテストも修正しなければいけなくなってしまう(+書き方がイケてない)ので、上記の書き方の方が良いかなぁと思います! 参考 公式 - filterについて
- 投稿日:2021-12-03T09:36:33+09:00
Vue.jsについて振り返る
はじめに 今回は、普段開発する際に書いているコードは、ちゃんと中身を理解してかけているかという点を振り返るために記事を投稿します。使用言語は"Vue.js"です。 ここで振り返りを行う目的は、 ・なんとなく書いていたコードをきちんと理解すること ・今後エラーが発生した際に、自分の力で対応できるようになること になります。 ※実際のコードで説明します と記載がある部分は後ほど記事を修正する予定です。 目次 本記事の目次になります 1.Vueのライフサイクル 2.コンポーネントの構図・仕組 3.propsと$emit 4.グリッドkeyの役割 5.DOM直接参照($refs) 6.v-slot 7.Vuex 1. Vueのライフサイクル 各 Vue インスタンスは、生成時に一連の初期化を行います。例えば、データの監視のセットアップやテンプレートのコンパイル、DOM へのインスタンスのマウント、データが変化したときの DOM の更新などがあります。その初期化の過程で、特定の段階でユーザー自身のコードを追加する、いくつかの ライフサイクルフック(lifecycle hooks) と呼ばれる関数を実行します。 それぞれの関数の流れを示した図と一覧は以下の通り。 -Vue.js公式ドキュメントから引用 ライフサイクル タイミング 備考 beforeCreate インスタンスは生成されたがデータが初期化される前 - created インスタンスが生成され、且つデータが初期化された後 - beforeMount インスタンスが DOM 要素にマウントされる前 - mounted インスタンスが DOM 要素にマウントされた後 コンポーネントがHTML要素の一員として画面に描画されている状態。DOM にアクセス可能ということ beforeUpdate データは更新されたが DOM に適用される前 - updated データが更新され、且つ DOM に適用された後 値の変更タイミングで DOM を自動的に更新し、再描画する beforeDestroy Vue インスタンスが破壊される前 v-if,v-forなどのインスタンスが表示されなくなるタイミングで呼ばれる destroyed Vue インスタンスが破壊された後 - 以下参考 2. コンポーネントの構図・仕組 コンポーネントは、Vue.js の強力な機能の一つである。機能を持つUI部品ごとにテンプレートとJavaScriptを一つのセットにして、他の部品とは切り離した開発及び管理ができるようにする仕組み・機能。 自身が使ってみて感じたメリットを記載します。 自身が感じたメリット ... ①. 単一ファイルコンポーネント※で開発できるため、管理し易い このファイルは何をするものかがわかりやすい ※単一ファイルコンポーネント ... HTML, CSS, JavaScriptを1ファイルにまとめて書くこと ②. 記述量が多くなった場合でもに複雑に感じにくい どこでイベントを呼ぶ、メソッドを書く、といったことを記述する場所がはっきりわかれていから 以下はアプリケーションがネストされたコンポーネントのツリーイメージ図 -Vue.js 公式ドキュメントから引用 3. propsと$emit 以下の定義で整理する。また整理した図も引用する。 属性 定義 props 親から子へのデータの受け渡し $emit 子から親へのイベントの通知 ※実際のコードで説明します -こちらの記事から引用(https://www.hypertextcandy.com/vuejs-components-introduction-communication-between-components) [補足]プロパティを使用した子コンポーネントへのデータの受け渡し -Vue.js 公式ドキュメントから引用 """ 表示する特定のコンテンツなどのデータをコンポーネントに渡すことができない限り、そのコンポーネントは役に立たないということです。プロパティはここで役立ちます。 プロパティはコンポーネントに登録できるカスタム属性です。値がプロパティ属性に渡されると、そのコンポーネントインスタンスのプロパティになります。ブログ投稿コンポーネントにタイトルを渡すには、props オプションを使用して、このコンポーネントが受け入れるプロパティのリストにそれを含めることができます: """ Vue.component('blog-post', { props: ['title'], template: '<h3>{{ title }}</h3>' }) 4. グリッドのkeyの役割 グリッドの Key の役割とは... 要素の識別と効率的な描画処理を可能とするもの Keyを指定する場合としない場合で、グリッドに対して操作した場合以下のような違いが起きる。 Key ある場合 Key がない場合 消滅した key の DOM が削除されるだけ 要素の文字が変更されたと解釈し、順番の変わった要素を全て更新してしまう ※実際のコードで説明します 5. DOM直接参照($refs) そもそも ref 属性とは... 分散されたネーム・レゾリューションを容易に行ったり、複数のサーバーに渡って検索を行ったりするために使用される属性を指す。 参照するサーバー内で指定する項目に出現します。ref 属性の値は、参照されるサーバー内で保持されている項目を指します。 言い換えると ... 取得したいコンポーネントのタグ内に、ref属性を付与することでマーキングし、実際にref 属性でマーキングしたコンポーネントを取得することができる。 以下参考 6. v-slot v-slotとは ... 親となるコンポーネント側から、子のコンポーネントのテンプレートの一部を差し込む機能 ※実際のコードで説明します 以下参考 7. Vuex ※Vue.js 公式ドキュメントより概要をおさらいし、実際のコードで説明 Vuex ... Vue.js アプリケーションのための 状態管理パターン + ライブラリ 以下はVuexの全体像を表した図 -Vue.js 公式ドキュメントから引用 ※実際のコードで説明します まとめ なんとなくイメージしているコードを、ちゃんと理解することは、エラーが出た際にも何が原因かを自力で追えるようになると思います。今回記事にできなかった分も追加していく予定です。
- 投稿日:2021-12-03T01:06:50+09:00
Vue3のComposition APIでロジックをいい感じに切り分ける!カスタムフックのすゝめ
Vue3系から導入されたComposition APIを使用することで、VueでもReactのようにカスタムフックを用いたロジックの切り分けができるようになりました。 今回はVue3系のComposition APIで使えるカスタムフックを利用したロジックの切り分け方について、解説してみました。 Composition APIについて簡単に解説 カスタムフックの説明に入る前にComposition APIについて、従来の書き方であるOptions APIと比較しながら簡単に解説していきます。 Options APIでの書き方 今回はサンプルコードとして、ボタンをクリックした回数を表示するコンポーネントを用意しました。 Options APIで実装すると、以下のようになります。 Button.vue <template> <button @click="increment">Counter</button> <p>{{ count }}</p> </template> <script> export default { data() { return { count: 0, } }, methods: { increment () { this.count++; } }, } </script> Options APIではdata()の中でstateの定義を行い、methodsの中に関数を記述することで、ロジックの実装を行います。 Composition APIでの書き方 Optioons APIで実装したボタンコンポーネントを今度はComposition APIで実装してみます。 Vue3系から使用可能なComposition APIで実装を行うと以下のようになります。 Button.vue <template> <button @click="increment">Counter</button> <p>{{ count }}</p> </template> <script> import { defineComponent, ref } from 'vue'; export default defineComponent({ setup() { const count = ref(0); const increment = () => count.value++; return { count, increment, }; }, }); </script> Composition APIではsetup関数の中に、stateの定義とロジックをまとめて処理を記述することが可能です。 関数の中に状態の保持とロジックをまとめて処理を記述できるので、stateの定義や関数の外出しが簡単にできるという特徴があります。 状態管理を方法に関してもComposition APIではrefやreactiveという関数を利用することでstate管理を行うことができます。 少々本題から脱線しますが、自分はrefとreactiveは以下のように使い分けています。 ref・・・単一の値の状態管理を行いたいときに使用。 reactive・・・オブジェクトを用いて複数の値の状態を管理したいときに使用。 じゃあ、カスタムフックって一体何なのかって話 では本題です。 カスタムフックとは一体何なのでしょうか? カスタムフックとは、stateの定義や操作に関する処理を別のファイルに切り出して定義した関数のことです。 カスタムフックはReactでビュー(ユーザーの目に見える部分)とロジックを切り出すため使用されるテクニックなのですが、Composition APIの登場によって、ロジックの切り分けが簡単にできるようになったVue3系でもカスタムフックが使えるようになりました。 カスタムフックに関して、Reactの公式ドキュメントでは以下のように説明されています。 自分独自のフックを作成することで、コンポーネントからロジックを抽出して再利用可能な関数を作ることが可能です。 参考サイト: 独自フックの作成 トーストコンポーネントを作りながらカスタムフックを理解する 実際にどうやって処理を書いていくのか文章だけだとイメージが湧きづらいかと思うので、今回はカスタムフックを用いてシンプルなトーストコンポーネントを作ってみました。 サンプルコードはGitHubに掲載していますので、ご覧ください。 トーストのロジックを別ファイルに切り出す まずトーストのstate操作を行うロジックを別ファイルに切り出します。 フックであると一目で分かるように、use-toast.tsというファイルを作成して、関数の定義を行います。 use-toast.ts import { ref } from 'vue'; export const useToast = () => { const isToastActive = ref(false); const handleClick = () => { isToastActive.value = !isToastActive.value; }; const closeToast = () => { isToastActive.value = false; }; return { isToastActive, handleClick, closeToast, }; }; フックの命名規則ですが、Reactだと慣例としてuse〇〇という名前でつける決まりがあるので、それに習ってuseToastと命名しました。 コンポーネント側でstateや関数を分割代入で呼び出せるようにするために、定数やロジックをオブジェクトとして返却しています。 今回は不要なので指定していませんが、引数を設定する必要があればフックには引数を渡せるようにしても構いません。 APIのリクエスト結果に対して型をつけたい時はジェネリクスを用いて、レスポンス結果のオブジェクトを渡せるようにして、汎用性を持たせる場合もあると思います。 実装したカスタムフックをコンポーネントから呼び出して使用する App.vue <template> <button @click="handleClick">トーストを表示</button> <Toast :is-toast-active="isToastActive" @close-toast="closeToast"> <h2>Toastのサンプル</h2> <p>トースト?</p> </Toast> </template> <script lang="ts"> import { defineComponent } from 'vue'; import { useToast }from './lib/use-toast'; import Toast from './components/Toast.vue'; export default defineComponent({ components: { Toast, }, setup() { const { isToastActive, handleClick, closeToast } = useToast(); return { isToastActive, handleClick, closeToast, } } }); </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } h1,h2 { padding: 0; margin: 0; } </style> フックが完成したら後はコンポーネント側で呼び出すだけです。 コンポーネントではuseToastというフックをインポートしています。 フックからstateやロジックを呼び出す処理しか書いていないので、全体的に処理がすっきりコードが読みやすくなりました。 ちなみに、トーストコンポーネントはこんな感じで実装してみました。 Toast.vue <template> <transition name="bottom"> <div v-if="isToastActive" class="toast"> <button @click="$emit('closeToast')" class="toast-close-btn">×</button> <div class="toast-container"> <slot /> </div> </div> </transition> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ props: { isToastActive: { type: Boolean, required: true, } }, emits: ['closeToast'], }); </script> <style scoped> .toast { position: fixed; bottom: 30px; right: 30px; transition: all 0.6s; border: 1px solid rgba(0,0,0,.1); width: 100%; max-width: 420px; box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 15%); } .toast-close-btn { position: absolute; top: 0; right: 0; background: none; border: none; cursor: pointer; color: red; font-size: 30px; } .toast-container { padding: 8px; } /* アニメーション */ .bottom-enter-active, .bottom-leave-active { transition: transform 225ms cubic-bezier(0, 0, 0.2, 1) 0ms; } .bottom-enter-from { transform: translateY(100vh); } .bottom-enter-from, .bottom-leave-to { opacity: 0; } .bottom-leave-active { transition: all 0.4s; transform: translateY(300px); } </style> カスタムフックを利用するメリット シンプルなトーストコンポーネントを例にカスタムフックについて解説をしましたが、コンポーネントにロジックを直書きするのではなくカスタムフックとして切り出すとどのようなメリットがあるのでしょうか? カスタムフックを利用するメリットとしては、以下のようなメリットが挙げられます。 複数のコンポーネントで使用する可能性がある処理をまとめられる ロジックをコンポーネントから切り離すことで、コードの可読性が上がる ビューとロジックを別ファイルに切り出すことで、コンポーネントの可読性を高められるんだなー程度に考えておけば間違いはないと思います。 カスタムフックとして扱ってはいけないケース 簡単にコンポーネントからロジックを切り出すことができるカスタムフックですが、カスタムフックとして扱ってはいけないケースがあります。 扱ってはいけないケースですが、グローバルステートを扱うときです。 グローバルステートをカスタムフックとして扱ってはいけない理由ですが、カスタムフックで管理するstateをコンポーネント同士が共有することはないからです。 stateをコンポーネント同士が共有することがないとはどういうことか説明するための例を用意してみました。 Hoge.vue <template> <button @click="handleClick">トーストを表示(Hoge.vue)</button> <Toast :is-toast-active="isToastActive" @close-toast="closeToast"> <h2>Toastのサンプル Hoge.vue</h2> <p>トースト(Hoge)?</p> </Toast> </template> <script lang="ts"> import { defineComponent } from 'vue'; import { useToast }from '../lib/use-toast'; import Toast from './Toast.vue'; export default defineComponent({ components: { Toast, }, setup() { const { isToastActive, handleClick, closeToast } = useToast(); return { isToastActive, handleClick, closeToast, } } }); </script> Fuga.vue <template> <button @click="handleClick">トーストを表示(Fuga.vue)</button> <Toast :is-toast-active="isToastActive" @close-toast="closeToast"> <h2>Toastのサンプル Fuga.vue</h2> <p>トースト(Fuga)?</p> </Toast> </template> <script lang="ts"> import { defineComponent } from 'vue'; import { useToast } from '../lib/use-toast'; import Toast from './Toast.vue'; export default defineComponent({ components: { Toast, }, setup() { const { isToastActive, handleClick, closeToast } = useToast(); return { isToastActive, handleClick, closeToast, } } }); </script> App.vue <template> <Hoge /> <Fuga /> </template> <script lang="ts"> import { defineComponent } from 'vue'; import Hoge from './components/Hoge.vue'; import Fuga from './components/Fuga.vue'; export default defineComponent({ components: { Hoge, Fuga, }, }); </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } h1,h2 { padding: 0; margin: 0; } </style> 先程のトーストコンポーネントをHoge.vueとFuga.vue2つのコンポーネントに分けて、App.vueで読み込ませてみました。 トーストの挙動を確認すると、以下のような挙動になります。 トーストを表示(Hoge.vue)をクリック・・・Toastのサンプル Hoge.vueが表示される トーストを表示(Fuga.vue)をクリック・・・Toastのサンプル Fuga.vueが表示される 上記の挙動から、Hoge.vueとFuga.vueは別々のstate呼び出して、別々のstateを切り替える処理を行なっていることが分かります。 このことから、同じフックを使ってるコンポーネント同士がstateを共有することはないということが言えます。 グローバルステートは原則として、アプリケーション内のどのコンポーネントからもstateの参照、操作ができないといけないので、フックとは違うことが分かるかと思います。 Vue3系のComposition APIでグローバルステートを扱う手段としては、Options APIの頃からあったVuexに加えProvide・injectパターンなども使用できますが、これらを用いて状態管理・操作を行うときは、カスタムフックではなくstoreとして扱った方がいいと思います。 まとめ カスタムフックは、コンポーネントからロジックを切り離すときに使用する フックは慣例として、use〇〇という名前で命名する必要がある カスタムフックで管理するステートをコンポーネント同士が共有することはない グローバルステートの保持・操作を行うときはカスタムフックとして扱ってはいけない 今回の内容で記事を執筆するにあたって、Vueのカスタムフックについて解説している記事がなかなか見つからなかったので、Reactのカスタムフックについて解説した記事も参考にさせていただきました。 https://ja.reactjs.org/docs/hooks-custom.html https://zenn.dev/luvmini511/articles/df410f137d1e21 https://qiita.com/cheez921/items/af5878b0c6db376dbaf0 読んでいただきまして、ありがとうございました。