- 投稿日:2021-06-23T22:09:46+09:00
AWS AmplifyとAmplify UI VueとVue.jsでログイン機能を構築してみた
AWS AmplifyとAmplify UI VueとVue.jsでログイン機能を構築してみました 2年くらい前に書いた「AWS AmplifyとVue.jsでログイン機能を構築してみた」の記事では、Amplify UI Componentsは「aws-amplify-vue」を利用していましたが、最新だとVue v2「ui-vue」やVue v3「ui-components」が推奨のため今回ログイン機能を最新にしてみました 事前準備 AWS Amplify CLIのインストールと設定 下記リンクがとてもわかりやすく説明されています。 AWS Amplify CLIの使い方〜インストールから初期セットアップまで〜 Vue.jsの環境準備 今回はVue.jsとVuetifyの構成にしました。詳細はpackage.jsonをご確認いただければと思います。 全体の構成はできるだけシンプルに。 バックエンド - ユーザー管理 Amazon Cognito フロントエンド - ログイン機能 AWS Amplify Vue.js v2 Vuetify バックエンド まずは、バックエンドを構築していきます。 今までは、Amazon Cognito等のサービスも自分で設定する必要があり設定だけでも苦労したのですが、AWS Amplify CLIが登場したことにより、コマンドのみでほぼ設定が完了することが可能なりました。 認証機能のみであれば2コマンドで実装可能です! amplify add auth amplify push これだけでバックエンドの構築は完了になります フロントエンド 次に、フロントエンドを構築していきます。 実行環境 node v16.3.0 npm v7.15.1 まず、プロジェクトに必要なパッケージを2つインストールします。 npm install aws-amplify npm install @aws-amplify/ui-vue 最後に、実際にログイン機能のコードを記述していきます。 基本的には、routerに認証部分のコードを記述し、UIは既にあるコンポーネントを指定することでそのまま表示することが可能です。 全体構成 package.json { "name": "amplify_prj", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "@aws-amplify/ui-vue": "^1.0.12", "aws-amplify": "^4.1.1", "core-js": "^3.6.5", "vue": "^2.6.11", "vue-router": "^3.2.0", "vuetify": "^2.4.0", "vuex": "^3.4.0" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0", "@vue/cli-plugin-router": "~4.5.0", "@vue/cli-plugin-vuex": "~4.5.0", "@vue/cli-service": "~4.5.0", "babel-eslint": "^10.1.0", "eslint": "^6.7.2", "eslint-plugin-vue": "^6.2.2", "sass": "~1.32.0", "sass-loader": "^10.0.0", "vue-cli-plugin-vuetify": "~2.4.1", "vue-template-compiler": "^2.6.11", "vuetify-loader": "^1.7.0" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/essential", "eslint:recommended" ], "parserOptions": { "parser": "babel-eslint" }, "rules": {} }, "browserslist": [ "> 1%", "last 2 versions", "not dead" ] } /src main.js import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import vuetify from './plugins/vuetify' // Amplify読み込み import '@aws-amplify/ui-vue' import Amplify from 'aws-amplify' import awsconfig from './aws-exports' Amplify.configure(awsconfig) Vue.config.productionTip = false new Vue({ router, store, vuetify, render: h => h(App) }).$mount('#app') /src/store index.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { user: null }, mutations: { setUser(state, user) { state.user = user }, }, getters: { isSignIn: (state) => { return state.user !== null }, }, actions: {}, modules: {} }); export default store /src/router index.js import Vue from 'vue' import Router from 'vue-router' import Home from '../views/Home.vue' import Login from '../components/Login.vue' import store from '../store/index.js' // Amplify読み込み import { Hub } from "@aws-amplify/core" import Auth from "@aws-amplify/auth" Vue.use(Router) let user; // ユーザー管理 getUser().then((user) => { if (user) { router.push({path: '/'}); } }); function getUser() { return Auth.currentAuthenticatedUser().then((data) => { if (data && data.signInUserSession) { store.commit('setUser', data); return data; } }).catch(() => { store.commit('setUser', null); return null; }); } // ログイン状態管理 Hub.listen("auth", async (data) => { if (data.payload.event === 'signOut'){ user = null; store.commit('setUser', null); router.push({path: '/login'}); } else if (data.payload.event === 'signIn') { user = await getUser(); router.push({path: '/'}); } }); // ルーティング設定 const router = new Router({ mode: 'history', routes: [ { // ログインページ path: '/login', name: 'login', component: Login }, { // トップページ path: '/', name: 'home', component: Home, meta: { requiresAuth: true} } ] }); // リダイレクト設定 router.beforeResolve(async (to, from, next) => { if (to.matched.some(record => record.meta.requiresAuth)) { user = await getUser(); if (!user) { return next({ path: '/login' }); } return next() } return next() }); export default router src/views Home.vue <template> <div class="home"> <v-container> <v-row> <v-col> <h1>ログイン済</h1> </v-col> </v-row> <v-row> <v-col> <!--ログアウトコンポーネント--> <amplify-sign-out></amplify-sign-out> </v-col> </v-row> </v-container> </div> </template> <script> export default { name: 'home', components: { } } </script> <style> .home { padding-top: 100px; } </style> /src/components Login.vue <template> <div class="login"> <!--ログインコンポーネント--> <amplify-authenticator></amplify-authenticator> </div> </template> <script> export default { name: 'login' } </script> 簡易ローカルサーバーで確認してみます。 npm run serve ローカルサーバーを立ち上げて、ログイン画面でユーザーを登録しログインしてみます AWSのコンソールを確認するとユーザーも正常に登録さています。 AWS AmplifyとAmplify UI VueとVue.jsを利用することで、手軽にログイン機能の構築ができました AWS Amplify CLIが登場したことにより、Amplifyを利用する場合に以前よりサーバーレス設定周りがめちゃくちゃ便利になったのでおすすめです。Amplify UI Componentsについては、前回の「aws-amplify-vue」から「ui-vue」に変更したことで一部コードの変更があったので参考にして頂ければと思います Vue.jsについて、他にも記事を書いています。よろしければぜひ tags - Vue.js やってみたシリーズ tags - Try AWS AmplifyとVue.jsでログイン機能を構築してみた
- 投稿日:2021-06-23T21:11:44+09:00
【Vue.js】ミックスインの使い方と注意
はじめに 仕事で使う事になったので1からVue.jsについて学んだ。ちゃんと覚えておかないとまずそうな事を備忘録として1つ1つ残しておく。 ミックスインの使い方と注意 ミックスインとは Vue.jsのインスタンスのオプション1を共通化させて再利用できるようにする仕組み。 以下の例では、import { tokyoNumber } from "@/tokyoNumber";としている部分でミックスインtokyoNumberを読み込んでいる。 CountNumber.vue <template> <div> <h2>{{ title | upperCase }}</h2> <h4>{{ subTitle | lowerCase }}</h4> <p>{{ number }}</p> <button type="button" class="btn btn-primary" @click="number++">+1</button> </div> </template> <script> import { tokyoNumber } from "@/tokyoNumber"; export default { mixins: [tokyoNumber], /** ミックスインにより以下は宣言する必要がなくなる */ // data() { // return { // title: "Welcome to Tokyo", // subTitle: "Tokyo is a great city", // number: 0, // }; // }, // filters: { // upperCase(value) { // return value.toUpperCase(); // }, // lowerCase(value) { // return value.toLowerCase(); // }, // }, }; </script> tokyoNumber.js export const tokyoNumber = { data() { return { title: "Welcome to Tokyo", subTitle: "Tokyo is a great city", number: 0, }; }, filters: { upperCase(value) { return value.toUpperCase(); }, lowerCase(value) { return value.toLowerCase(); }, }, created() { console.log('created in Mixin'); }, } ソースコード全体は以下。 ミックスインで定義しているオプションと同じオプションがコンポーネントにある場合、コンポーネントのオプションで上書きされる 以下の例では、dataのtitle: "Welcome to Los",がミックスインのtitle: "Welcome to Tokyo",と被っているが、この場合はコンポーネントの方のオプション(title: "Welcome to Los",)が適用されるので、画面は以下のようになる。 画像のソースコードは以下(tokyoNumber.jsは上記と同じ。) CountNumber.vue <template> <div> <h2>{{ title | upperCase }}</h2> <h4>{{ subTitle | lowerCase }}</h4> <p>{{ number }}</p> <button type="button" class="btn btn-primary" @click="number++">+1</button> </div> </template> <script> import { tokyoNumber } from "@/tokyoNumber"; export default { mixins: [tokyoNumber], data() { return { title: "Welcome to Los", }; }, }; </script> vue.App.vue <template> <div> <!-- 省略 --> <h2>{{ title | upperCase }}</h2> <h4>{{ subTitle | lowerCase }}</h4> <p>{{ number }}</p> <button type="button" class="btn btn-primary" @click="number++">+1</button> <hr /> <CountNumber></CountNumber> </div> </template> <script> import CountNumber from "./CountNumber.vue"; import { tokyoNumber } from "@/tokyoNumber"; export default { mixins: [tokyoNumber], components: { CountNumber, }, // 省略 }; </script> ※ただし、ライフサイクルフック関数2では、ミックスインのオプション・コンポーネントのオプションの両方が適用され、ミックスインのオプション→コンポーネントのオプションの順番で実行される。 ソースコード全体は以下。 グローバルミックスインとその注意点 グローバルミックスインとは グローバルミックスインは、Single File Componentのグローバル登録・カスタムディレクティブのグローバル登録などと同じでグローバルにミックインを登録する事。 グローバルミックスインのオプション、ローカルミックスイン(コンポーネントのmixins:に宣言)のオプション、コンポーネントのオプションの3つのVueインスタンスのオプションが存在するが、それぞれ以下の順番で適用される。同じオプションがあればコンポーネントのそれで上書きされる。 グローバルミックスインのオプションに宣言された内容 ローカルミックスイン(コンポーネントのmixins:に宣言)のオプションに宣言された内容 コンポーネントのオプションに宣言された内容 注意点 ただ、ミックスインのグローバル登録を行うと、自動的に全てコンポーネントに対してミックスインが登録される事になり、以下の画像で分かるように複雑になり分かりにくくなる。なので、基本はグローバルミックスインは使わない。 ライフサイクルフックのcreated(){ console.log("global mixin"); }オプションを持つグローバルミックスインを定義した時の、consoleの状態が以下の図。 ※グローバルミックスインを使う場面は、プラグインなどで全てのコンポーネントに適用させたい何かを定義した時などに限定するのが得策。 ソースコード全体は以下。 Vue.jsの勉強メモ一覧記事へのリンク Vue.jsについて勉強した際に書いた勉強メモ記事のリンクを集約した記事。 https://qiita.com/yuta-katayama-23/items/dabefb59d16a83f1a1d4 data(){}, filters(){}, など ↩ https://qiita.com/yuta-katayama-23/items/578bb0cecf165fd850db#vue%E3%82%A4%E3%83%B3%E3%82%B9%E3%82%BF%E3%83%B3%E3%82%B9%E3%81%AE%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB ↩
- 投稿日:2021-06-23T17:08:13+09:00
Vue.jsのslotあれこれ
Vue.jsのslot機能について勉強したので,備忘録的に書いてきます. はじめに そもそもslotとは何か. Vue.jsにおけるslotは,親コンポーネントから小コンポーネントにテンプレートを差し込むことができる機能です. 名前付きslot 例えば,タイトル,本文,ボタンなどを要素として持つ以下のようなarticleというコンポーネントをApp.vueから呼び出す場合を考えてみます. article.vue <template> <div style="width=500px;"> <h1>タイトル</h1> <p>本文</p> <button>ボタン</button> </div> </template> <script> export default { name: 'Article', } </script> App.vue <template> <Article /> </template> <script> import Article from './components/article.vue'; export default { name: 'App', components: { Article, } } </script> これらのファイルは,現状で以下のように動作します. しかし,article.vueを繰り返し使って複数の記事を作成したい場合,呼び出す際にタイトルや本文,ボタンの文字を指定できたら便利ですよね. それを可能にするのがslotです.上記のファイルを以下のように書き換えてみましょう. article.vue <template> <div style="width=500px;"> <h1> <slot name="title"><!-- ここにタイトルが差し込まれます --></slot> </h1> <p> <slot name="body"><!-- ここに本文が差し込まれます --></slot> </p> <button> <slot name="buttonText"><!-- ここにボタンテキストが差し込まれます --></slot> </button> </div> </template> <script> export default { name: 'Article', } </script> App.vue <template> <Article> <template v-slot:title> スロットで差し込んだタイトルです </template> <template v-slot:body> スロットで差し込んだ本文です </template> <template #buttonText> <!-- v-slotは省略記法として#で置き換えることができます --> スロットで差し込んだボタンテキストです </template> </Article> </template> <script> import Article from './components/article.vue'; export default { name: 'App', components: { Article, } } </script> 実行結果はこんな感じ このように,子コンポーネントで親からテンプレートを受け取りたい部分に<slot name="hogehoge">と書き,親コンポーネントで<template v-slot:hogehoge>と差し込みたいスロットの名前を指定してあげることで可読性も高く,汎用性の高いコンポーネントを作ることができます. v-slotは,v-bindにおける:,v-onにおける@のように省略記法を持っており,#を使って省略することができます. <template #buttonText> <!-- v-slotは省略記法として#で置き換えることができます --> スロットで差し込んだボタンテキストです </template> このように,差し込みたい箇所(slot)に名前をつけ,親コンポーネントからslotを指定されるslotを名前付きslotと言います. デフォルトスロット 名前付きslotに対して,名前のないslotも作ることができます.名前のないslotはdefault slotとも呼ばれます.先程の例では,テンプレートを差し込みたい箇所が複数ありましたが,一つで良い場合などにdefault slotを使います. 以下のような<a>タグをラップしたようなコンポーネント,navigation-link.vueを用意します. navigation-link.vue <template> <a :href="url"> <slot><!-- slotが一つだけなのでname属性がいらない --></slot> </a> </template> <script> export default { name: 'NavigationLink', props: { url: String, }, } </script> App.vue <template> <navigation-link url="https://www.google.com/">googleへのリンク</navigation-link> </template> <script> import NavigationLink from './components/navigation-link.vue'; export default { name: 'App', components: { NavigationLink, }, } </script> 実行結果はこんな感じ navigation-link.vueのように,スロットが一つだけの場合,name属性をつけないことでdefault slotになります.また,name属性に明示的にdefaultを指定することでも同様に,default slotにすることができます.親コンポーネント側でもslotの名前を指定する必要はありません. また,名前付きslotがある場合にも,名前なしslotを利用することができます. navigation-link.vue <template> <div style="width=500px;"> <h1> <slot name="title"></slot> </h1> <h2> <slot name="subTitle"></slot> </h2> <button> <slot name="buttonText"></slot> </button> <p style="color: red;"> <slot><!-- デフォルトスロット --></slot><!-- slot name="default"でも同様 --> </p> </div> </template> slotのデフォルトコンテンツ(フォールバックコンテンツ) 子コンポーネントに用意されたスロットに,何も渡されなかった場合に表示されるコンテンツを,子コンポーネントであらかじめ記述することができます(公式ドキュメントではこのようなコンテンツのことをフォールバックコンテンツと呼んでいます).記述する方法は簡単で,小コンポーネントの<slot></slot>タグの間にあらかじめコンテンツを書いておくだけで良いです. <slot name="hogehoge">デフォルトコンテンツ</slot> こうすることでhogehogeスロットにコンテンツが渡されなかった場合には”デフォルトコンテンツ”という文字が表示され,親コンポーネントで何かしらのコンテンツをhogehogeスロットに渡すとそのコンテンツでslot内を書き換えます. スコープ付きスロット 親コンポーネントで小コンポーネントへ渡すテンプレートを記述する際に,子コンポーネントが持っているデータを用いて記述したい場合もあると思います.article.vueを以下のように書き換えてみます. article.vue <template> <div style="width=500px;"> <h1> <slot name="title"></slot> </h1> <h2> <slot name="subTitle"></slot> </h2> <button> <slot name="buttonText"></slot> </button> <p> <slot name="updatedAt"> {{ updatedAt.year }} </slot> </p> </div> </template> <script> export default { name: 'Article', data() { return { updatedAt: { year: '2021', month: '06', date: '22', }, } } } </script> 更新日時を差し込むスロットupdatedAtを用意しました.また,データとして子コンポーネントであるarticle.vueに更新日時のデータを持たせています.updatedAtでは,デフォルトコンテンツとして{{ updatedAt.year }}と記述しているので,親コンポーネントであるApp.vueからupdatedAtスロットにコンテンツを指定しなければ2021という数字が表示されます. App.vue <template> <Article> <template v-slot:title> タイトルです </template> <template v-slot:body> サブタイトルです </template> <template #buttonText> 次へ </template> </Article> </template> <script> import Article from './components/article.vue'; export default { name: 'App', components: { Article, }, } </script> ここで,更新日時のうち,年ではなく,月を表示させたい場合はどのようにすれば良いのでしょうか.実は,スコープ付きスロットを用いることでこれを実現できます.article.vueを以下のように書き換えてみます. article.vue <template> <div style="width=500px;"> <h1> <slot name="title"></slot> </h1> <h2> <slot name="subTitle"></slot> </h2> <button> <slot name="buttonText"></slot> </button> <p> <slot name="updatedAt" v-bind:slotProps="updatedAt"><!-- スコープ付きスロット --> {{ updatedAt.year }} </slot> </p> </div> </template> <script> export default { name: 'Article', data() { return { updatedAt: { year: '2021', month: '06', date: '22', }, } } } </script> このように,子コンポーネントでデータを親コンポーネントに渡したい時にv-bindで変数をバインドしてあげます.ここではupdatedAtというオブジェクトをslotPropsという変数でバインドしています(slotPropsは任意の変数名で可). App.vueは次のように書き換えます. App.vue <template> <Article> <template v-slot:title> タイトルです </template> <template v-slot:body> サブタイトルです </template> <template #buttonText> 次へ </template> <template v-slot:updatedAt="{ slotProps }"><!-- スコープ付きスロットから変数(オブジェクト)を取得 --> {{ slotProps.month }}<!-- デフォルトコンテンツをmonthを表示するように上書き --> </template> </Article> </template> <script> import Article from './components/article.vue'; export default { name: 'App', components: { Article, }, } </script> このように,呼び出し側でv-slotの値として変数を受け取ることでそのスコープ内で値を使ってコンテンツを記述することができます.当然ですが,他のスロットなどではこの変数は機能しません. 実行結果はこんな感じ. おわりに Vue.jsのslotについて解説しました.今回紹介した方法以外にも,記述のバリエーションがいくつかあるので,この記事と少し状況が違うなという方は公式のドキュメントを参照してみてください. 参考 Vue.js公式ドキュメント - Slot:https://jp.vuejs.org/v2/guide/components-slots.html
- 投稿日:2021-06-23T12:38:16+09:00
【Nuxt】Layoutのsassが子Pageに適用されなくて困ったときのメモ
TL;DL nuxtでcomponentを利用したlayoutを組んでpageを作成したら、スタイルが適用されなくて困った styleをscopedのまま子ページまで効かせたい deep selector を利用することで解決した 環境 nuxt ^2.15.3 sass-loader ^10.1.0 スタイルが効かない nuxtでWebサービスを作成しているとき、レイアウトに記述したスタイルがページに適用されていないことに気づきました。 一時的に<style>のscopedを外すことで対応していたのですが、毛色の違う別のレイアウトを用意することになり、本格的に対応が必要になりました。 また、自分の開発形態が特殊で実際のレイアウトはcomponentsに用意してlayoutsの方から呼び出す形式をとっていました。 以下に簡単なサンプルを用意しています。 components ∟TestLayout.vue layouts ∟test.vue pages ∟testPage.vue // TestLayout.vue <template> <div> <h1>テスト</h1> <nuxt-child /> </div> </template> <style lang="scss" scoped> .hoge { color: aqua; } </style> // test.vue <template> <test-layout> <Nuxt /> </test-layout> </template> <script> import TestLayout from '../components/TestLayout' export default { components: { TestLayout, }, } </script> // testPage.vue <template> <div> <p class="hoge">コンテンツ!</p> </div> </template> <script> export default { layout: 'test', } </script> TestLayout.vueで定義した.hogeクラスをtestPage.vueに適用したいのですが、このままではダメです。 Deep Selector 詳細は分かりませんが、scopedのスタイルを子コンポーネントに及ぼしたい場合に利用できるものみたいです。 書き方としては、>>>、/deep/、::v-deepなどがあるみたいですが今回は::v-deepを採用しました。 scssなら下記のように囲ってあげるだけで大丈夫です。 // TestLayout.vue <template> <div> <h1>テスト</h1> <nuxt-child /> </div> </template> <style lang="scss" scoped> ::v-deep { .hoge { color: red; } } </style> 参考 スコープ付き CSS#ディープセレクタ Vue.jsでのdeep selectorの書き方