- 投稿日:2019-07-04T23:45:02+09:00
Vue.jsでQiita簡易クライアントを作成
こんにちは
普段はバックエンドがメインなのですが、フロントエンドの勉強がてらVue.jsをやってみます開発環境
- Vue CLI 3
- Vuex
- ESLint + Prettier
- BootstrapVue
- Qiita API v2
画面遷移しないのでVue Routerは使用しません
成果物
Qiita APIを利用してタグ検索できます
ソースコード
src/main.jsimport Vue from "vue"; import App from "./App.vue"; import store from "./store"; import BootstrapVue from "bootstrap-vue"; import "bootstrap/dist/css/bootstrap.css"; import "bootstrap-vue/dist/bootstrap-vue.css"; Vue.use(BootstrapVue); Vue.config.productionTip = false; new Vue({ store, render: h => h(App) }).$mount("#app");src/App.vue<template> <div id="app"> <qiita-form /> <qiita-content /> </div> </template> <script> import Form from "./components/Form.vue"; import Content from "./components/Content.vue"; export default { name: "app", components: { "qiita-form": Form, "qiita-content": Content } }; </script>ストア
src/store.jsimport Vue from "vue"; import Vuex from "vuex"; Vue.use(Vuex); const fetchItmes = async value => { const response = await fetch( `https://qiita.com/api/v2/tags/${value}/items?page=1&per_page=20`, { mode: "cors" } ); return response.json(); }; export default new Vuex.Store({ state: { selected: null, items: [] }, getters: { selected: state => state.selected, items: state => state.items }, mutations: { selected(state, selected) { state.selected = selected; }, items(state, items) { state.items = items; } }, actions: { onChange({ commit }, value) { commit("selected", value); }, async search({ commit, state }) { commit("items", []); const items = await fetchItmes(state.selected); commit("items", items); } } });コンポーネント
src/components/Form.vue<template> <div class="sticky-top"> <b-card> <b-form-group label="Qiitaくらいあんと: タグを付けた日時の降順で20件取得します" > <b-form-select :options="options" v-model="selected"> <template slot="first"> <option :value="null" disabled>-- 選択してください --</option> </template> </b-form-select> </b-form-group> <b-button @click="search" :disabled="isDisabled" variant="info" block >検索</b-button > </b-card> </div> </template> <script> export default { name: "Form", computed: { selected: { get() { return this.$store.getters.selected; }, set(value) { this.$store.dispatch("onChange", value); } }, isDisabled() { return this.$store.getters.selected === null; }, options: () => [ { value: "javascript", text: "JavaScript" }, { value: "typescript", text: "TypeScript" }, { value: "elm", text: "Elm" } ] }, methods: { search() { this.$store.dispatch("search"); } } }; </script>src/components/Content.vue<template> <div> <b-list-group v-for="item in items" :key="item.id"> <qiita-item :item="item" /> </b-list-group> </div> </template> <script> import Item from "./Item.vue"; export default { name: "Content", components: { "qiita-item": Item }, computed: { items() { return this.$store.getters.items.filter(item => !item.private); } } }; </script>src/components/Item.vue<template> <b-list-group-item> <div> <b-link :href="item.url" target="_blank">{{ item.title }}</b-link> <small>{{ updateAt }}</small> </div> <div> <span v-for="tag in item.tags" :key="tag.name"> <b-badge variant="info" pill>{{ tag.name }}</b-badge> </span> </div> <div> <small :id="userName">{{ `by ${userName}` }}</small> </div> </b-list-group-item> </template> <script> export default { name: "Item", props: { item: { url: String, title: String, updated_at: String, tags: Array, user: { name: String, id: String } } }, computed: { updateAt() { const date = new Date(this.item.updated_at); return ` (最終更新日: ${date.toLocaleDateString()})`; }, userName() { return this.item.user.name || `@${this.item.user.id}`; } } }; </script>
- 投稿日:2019-07-04T17:32:38+09:00
[Vue+TypeScript] Vue.extend で Vue らしさを保ちつつ TypeScript で書くときの型宣言についてまとめた
はじめに
Vue + TypeScript の組み合わせでVueを書くときに、
vue-property-decoratorを利用して書いていくことが多いと思います。
ただvue-property-decoratorを利用すると、どうしてもVueらしさがなくなるというか、よりTypeScriptにらしい書き方になると感じています。
せっかくJavaScriptでVue書けるようになったのに、全然書き方が違うじゃないか…と挫折しかけることもあるんじゃないでしょうか?
ちなみに私はvue-property-decoratorで書くほうが慣れているので好きですが、Vue入門者には厳しいところがあると思うので、Vue.extendベースでTypeScriptを書いていくという方法を紹介するのと、その際の型宣言についてもまとめていこうと思います。VueをTypeScriptで書きたいけど、
vue-property-decoratorは使いたくない…って人の参考になればいいなーと思います。Vue.extend ??
TypeScript内でVueモジュールをimport/extendして書く方法です。
JavaaScriptでのVueコンポーネントの記述に近い書き方でTypeScriptを書くことができます。<script lang="ts"> import Vue from "vue" export default Vue.extend({ name: "component", data() { return { value: "hoge" } } }) </script>環境構築
VueCLIで簡単に構築することができます。
TypeScriptを選択して、class-styleコンポーネントシンタックスを利用しないと選択すればいいです。? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection) ◯ Babel ❯◉ TypeScript ◯ Progressive Web App (PWA) Support ◯ Router ◯ Vuex ◯ CSS Pre-processors ◯ Linter / Formatter ◯ Unit Testing ◯ E2E Testing? Use class-style component syntax? (Y/n) nこれで
Vue.extendの環境が構築されます。簡単ですね。Vue.extend における型
Vue.extend ベースでTypeScriptで書いていく際の型宣言を紹介していこうと思います。
props
Propについてはネイティブコンストラクターを付与することで、内部で型推論されます。
またArrayやObjectの詳細な型宣言についてはPropTypeを利用することで宣言することができます。<script lang="ts"> import Vue, { PropType } from "vue" export type PropObjType = { id: string index: number } export default Vue.extend({ props: { val: String, obj: Object as PropType<PropObjType> } }) </script>ここで注意が必要なのが、ビルド時のコンパイルエラーを得ることができないという点です。ただ実行時のエラーを得ることはできます。
また用意されているネイティブコンストラクターは以下の通りです。
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
data
data()関数に向けて型を定義し、型アノテーションを付与します。
<script lang="ts"> import Vue from "vue" export type DataType = { value: string enable: boolean count: number } export default Vue.extend({ data(): DataType { return { value: "hoge", enable: true, count: 0 } } }) </script>lifecycle hooks
lifecycle hooksで着火するイベントは基本的に
void型の関数<script lang="ts"> import Vue from "vue" export default Vue.extend({ created(): void { console.log("Created!!!") } }) </script>computed
computedで呼び出される関数が返す値に対する型を宣言する
<script lang="ts"> import Vue from "vue" export type DataType = { value: string enable: boolean count: number } export default Vue.extend({ data(): DataType { return { value: "hoge", enable: true, count: 0 } }, computed: { isEnabled(): boolean { return this.enable }, getCount(): number { return this.count } }, }) </script>methods
methodsで呼び出される関数が返す値に対する型を宣言する
<script lang="ts"> import Vue from "vue" export type DataType = { value: string enable: boolean count: number } export default Vue.extend({ data(): DataType { return { value: "hoge", enable: true, count: 0 } }, methods: { countUp(): void { this.count += 1 }, getValue(): string { return this.value } } }) </script>まとめ
- Vueらしさを保ちつつTypeScriptで書きたいって人 → Vue.extend
- VueらしさよりTypeScriptらしく書きたいって人 → vue-property-decorator
という感じかなって思います。自分に合った方法でより楽しくVueを書いていきましょう!
ではまた!!!
- 投稿日:2019-07-04T17:19:45+09:00
v-forを使う時、配列に初期値があると初めから画面に表示されてしまう
vueでtodoリストを作る際に、v-forを使い配列の中身を順に表示させようとしたのですが、ボタン要素を作る為にhtmlに
<button>を記述したところ、初期の状態からボタン要素が見えてしまっている不備があった為、少し躓いたので解決策を記載します。html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <link rel="stylesheet" href="style.css"> </head> <body> <div id="app"> <h1>ToDoList</h1> <table> <thead> <tr> <th>コメント</th> <th>状態</th> </tr> </thead> <tbody> <tr v-for="todo in todos" :key="todo.value"> <td>{{ todo.item }}</td> <td><button @click="">{{ todo.state }}</button></td> </tr> </tbody> </table> <input type="text" v-model="newItem"> <button @click="addItem">追加</button> </div> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="script.js"></script> </body> </html>ここは問題なしです。
javascript(vue) 不備があったコード。
let vm = new Vue({ el: '#app', data: { newItem: '', todos: [{ item: this.newItem, state: '' }] }, methods: { //タスクを追加する関数 addItem: function () { this.todos.push({item: this.newItem, state: '作業中'}) } } });このコードだと、htmlに初めからボタン要素が見えてしまいます。
じゃあhtmlの<button>を消して、javascriptの関数内でボタン要素を作り、それを配列todosにpushしよう!
と思ったのですが、vueには生のjavascriptでお世話になったようなので、少し躓いてしまいました。document.createElementに相当するものは無い
(誤解を招く表現でしたので訂正します。わざわざ使う必要がない、という認識です。)難しく考えていましたが解決策は非常にシンプルでした。
解決したコード
let vm = new Vue({ el: '#app', data: { newItem: '', todos: [] }, methods: { //タスクを追加する関数 addItem: function () { this.todos.push({item: this.newItem, state: '作業中'}) } } });【解決策】
配列todosを空にするだけです。【原因】
htmlに初めからボタン要素が表示されていた原因は、配列todosのボタン要素になるプロパティstateに初期値を与えてしまったせいです。
ですので配列todosを空にし、関数内でpushする時にプロパティを作ってあげれば解決です。
(プロパティitemは今回のボタン要素の表示には関係ありませんが、同じ関数内で一緒にpushしてあげた方がスマートで保守性と可読性も上がると思います。)
以上です。
よく考えれば当たり前なのに理解不足もあり少し時間を取られたので記載しておきました。
補足や訂正などありましたら、ぜひご教授いただければ嬉しいです。
最後まで見ていただきありがとうございます。
- 投稿日:2019-07-04T15:48:57+09:00
AWS Amplifyでサーバーレスなログイン機能をスマートに実装
はじめに
AWS Amplify、使ったことありますか?フロントエンドの人でも楽しく使えるAWS、それがAmplifyです。AWS自体、最近勉強し始めたのですが、難しいことせずに素早くAWSサービス群が使えちゃうのでこれからもっと使っていきたいです。
本記事はサインアップ/サインインの認証機能を一から作っていくチュートリアルです。GitHubはこちらから。
目次
- AWS Amplifyの私の理解度
- サインアップなどのフォームとVue Routerの実装
- AWS Amplifyを設定
- AWS Amplifyの機能を実装
NOTE: 本記事ではAWS Amplifyチームが提供しているVue.jsのUIコンポーネントについては説明しません。
AWS Amplifyの私の理解度
AWS Amplifyでググってみたらいいんですが、大体以下のサービスの集まりだと認識するとググりやすいかもしれません。
- Amplify CLI: 開発時にお世話になるCLI。
- Amplify.js: JSコンポーネント。今回はWebの話しかしませんが、Naitive Mobile Appにも同等のものが提供されています。
- Amplify Console: AWS Consoleからアクセスできるクラウドサービス。デプロイ時のCIを提供。まだ触ったことはありません。。
サインアップなどのフォームとVue Routerの実装
ここで作るもの:
フォーム
- サインアップ
- メールアドレス確認
- サインイン
キレイにスタイリングした後のスクリーンショットはこちらのページにアップロードしています。
ルータ
サインインした時にはサインアップフォーム等を表示させたくないですよね。同時に、サインインしていない時にはサインアウトボタンのあるページを表示させたくないので、ルータを使ってアクセス制御します。
コーディングタイム!
インストール
まずはVue CLIでパッケージをインストールします。ここではVuetifyとVue Routerをインストールします。
$ # Install Vue CLI $ npm install @vue/cli -g $ # Create the project $ vue create my-app ? Please pick a preset: default (babel, eslint) $ cd $_ # Install Vuetify $ vue add vuetify ? Choose a preset: Default (recommended) # Install Vue Router $ vue add router ? Use history mode for router? (Requires proper server setup for index fallback in production) Yesサインアップフォーム
サインアップフォームを
src/views/signUp.vueに作ります。注意したいのは、メールアドレスの項目にusernameを使用しています。これは、AWS Amplifyではusernameが必須入力項目になっているためです。ここで変数名をusernameにする必要はないのですが便宜的に。ここでやっていることはVuetifyを使ったことのある方だったら流し読み程度で大丈夫です。フォームを設置し、メールアドレスとパスワードのバリデーションを設定しています。バリデーションを全部パスしたら「Submit」ボタンが押せるようになり、
Console.logに入力した値が表示されることを確認してください。// src/views/SignUp.vue <template> <div class="sign-up"> <h1>Sign Up</h1> <v-form v-model="valid" ref="form" lazy-validation> <v-text-field v-model="username" :rules="emailRules" label="Email Address" required/> <v-text-field v-model="password" :append-icon="passwordVisible ? 'visibility' : 'visibility_off'" :rules="[passwordRules.required, passwordRules.min]" :type="passwordVisible ? 'text' : 'password'" name="password" label="Password" hint="At least 8 characters" counter @click:append="passwordVisible = !passwordVisible" required/> <v-btn :disabled="!valid" @click="submit">Submit</v-btn> </v-form> </div> </template> <script> export default { name: "SignUp", data() { return { valid: false, username: '', password: '', passwordVisible: false, } }, computed: { emailRules() { return [ v => !!v || 'E-mail is required', v => /.+@.+/.test(v) || 'E-mail must be valid' ] }, passwordRules() { return { required: value => !!value || 'Required.', min: v => v.length >= 8 || 'Min 8 characters', emailMatch: () => ('The email and password you entered don\'t match'), } }, }, methods: { submit() { if (this.$refs.form.validate()) { console.log(`SIGN UP username: ${this.username}, password: ${this.password}, email: ${this.username}`); } }, }, } </script>サインアップ確認フォーム
サインアップが完了したら、AWSが対象メールアドレス宛にメールを送ってくれます。そのメールの中には確認コードがあるので、メールアドレスと確認コードを入力することで登録されたメールアドレスが正しいかを確認します。
サインアップフォームとやっていることは同じです。今の段階ではサインアップと同様に
console.logの出力しか行いません。src/views/SignUpConfirm.vueに作ります。// src/views/SignUpConfirm.vue <template> <div class="confirm"> <h1>Confirm</h1> <v-form v-model="valid" ref="form" lazy-validation> <v-text-field v-model="username" :rules="emailRules" label="Email Address" required/> <v-text-field v-model="code" :rules="codeRules" label="Code" required/> <v-btn :disabled="!valid" @click="submit">Submit</v-btn> </v-form> <v-btn @click="resend">Resend Code</v-btn> </div> </template> <script> export default { name: "SignUpConfirm", data() { return { valid: false, username: '', code: '', } }, computed: { emailRules() { return [ v => !!v || 'E-mail is required', v => /.+@.+/.test(v) || 'E-mail must be valid' ] }, codeRules() { return [ v => !!v || 'Code is required', v => (v && v.length === 6) || 'Code must be 6 digits' ] }, }, methods: { submit() { if (this.$refs.form.validate()) { console.log(`CONFIRM username: ${this.username}, code: ${this.code}`); } }, resend() { console.log(`RESEND username: ${this.username}`); } }, } </script>サインインフォーム
通常のサインインフォームです。サインアップフォームととても似ているので説明は特にしません。
src/views/SignIn.vueに作ります。// src/views/SignIn.vue <template> <div class="sign-in"> <h1>Sign In</h1> <v-form v-model="valid" ref="form" lazy-validation> <v-text-field v-model="username" :rules="emailRules" label="Email Address" required/> <v-text-field v-model="password" :append-icon="passwordVisible ? 'visibility' : 'visibility_off'" :rules="[passwordRules.required, passwordRules.min]" :type="passwordVisible ? 'text' : 'password'" name="password" label="Password" hint="At least 8 characters" counter @click:append="passwordVisible = !passwordVisible" required/> <v-btn :disabled="!valid" @click="submit">Submit</v-btn> </v-form> </div> </template> <script> export default { name: "SignIn", data() { return { valid: false, username: '', password: '', passwordVisible: false, } }, computed: { emailRules() { return [ v => !!v || 'E-mail is required', v => /.+@.+/.test(v) || 'E-mail must be valid' ] }, passwordRules() { return { required: value => !!value || 'Required.', min: v => v.length >= 8 || 'Min 8 characters', emailMatch: () => ('The email and password you entered don\'t match'), } }, }, methods: { submit() { if (this.$refs.form.validate()) { console.log(`SIGN IN username: ${this.username}, password: ${this.password}`); } }, }, } </script>サインイン後のページ
ここでサインインした後のページを作ってもいいのですが、面倒なので、Vue Routerをインストールした時に生成された
src/views/Home.vueを使い回しましょう。Vue Routerにフォームページを追加していく
Vue Routerをインストールした時、
src/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> </div> <router-view/> </div> </template>これを参考に、フォームページを追加していきます。
// 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="/signUp">Sign Up</router-link> | <router-link to="/signUpConfirm">Confirm</router-link> | <router-link to="/signIn">Sign In</router-link> </div> <router-view/> </div> </template> <style> #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #2c3e50; margin-top: 60px; } </style>同様に、Vue Routerをインストールした時に
src/router.jsというファイルが新しく追加されています。フォームページを追加していきましょう。// src/router.js import 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: [ { 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') }, { path: '/signUp', name: 'signUp', component: () => import(/* webpackChunkName: "signup" */ './views/SignUp.vue') }, { path: '/signUpConfirm', name: 'signUpConfirm', component: () => import(/* webpackChunkName: "confirm" */ './views/SignUpConfirm.vue') }, { path: '/signIn', name: 'signIn', component: () => import(/* webpackChunkName: "signin" */ './views/SignIn.vue') }, ] })この段階で、ブラウザ上にナビゲーションアイテムが追加されているのが確認できるはずです。それぞれをクリックして、対応したフォームが表示されているか確認してみましょう。
AWS Amplifyを設定
フロントエンドの方にとってなんだか敷居の高いAWSをこれから使っていきます。Get Startedのページに従って、AWSのアカウント作成、Amplify CLIのインストールを実行してみてください。インストールが終わったら早速CLIを使っていきます。
$ amplify configureざっくり言うとこのコマンドはお使いのコンピューターからAWSにアクセスすることを知らせます。いくつか選択肢があったりしますので、読み進めて任意に設定してください。私が選択した結果は以下の通りです。参考までに。
these steps to set up access to your AWS account: Sign in to your AWS administrator account: https://console.aws.amazon.com/ Press Enter to continue Specify the AWS Region ? region: us-west-2 Specify the username of the new IAM user: ? user name: amplify-cognito-vuejs-example Complete the user creation using the AWS console https://console.aws.amazon.com/iam/home?region=undefined#/users$new?step=final&accessKey&userNames=amplify-cognito-vuejs-example&permissionType=policies&policies=arn:aws:iam::aws:policy%2FAdministratorAccess Press Enter to continue Enter the access key of the newly created user: ? accessKeyId: AKIA2BSHMB********** ? secretAccessKey: 4IgyKbOh9EJiufb4prtd******************** This would update/create the AWS Profile in your local machine ? Profile Name: default Successfully set up the new user.次に、同じCLIを使ってプロジェクトを初期化します。
$ amplify initここも任意設定です。ご自身の環境に合わせて設定してください。私が選択した結果は以下の通りです。ほぼデフォルトですね。
Note: It is recommended to run this command from the root of your app directory ? Enter a name for the project my-app ? Enter a name for the environment dev ? Choose your default editor: Vim (via Terminal, Mac OS only) ? Choose the type of app that you're building javascript Please tell us about your project ? What javascript framework are you using vue ? Source Directory Path: src ? Distribution Directory Path: dist ? Build Command: npm run-script build ? Start Command: npm run-script serveそして、認証機能である
authを追加します。以下のコマンドだけで追加できちゃいます。amplify add authここでも選択肢をいくつか選択します。以下、私の選択した結果です。
Using service: Cognito, provided by: awscloudformation The current configured provider is Amazon Cognito. Do you want to use the default authentication and security configuration? Default configuration Warning: you will not be able to edit these selections. How do you want users to be able to sign in when using your Cognito User Pool? Email Warning: you will not be able to edit these selections. What attributes are required for signing up? Successfully added resource cognitoexample77f073c1 locally何をやっているのかさっぱりかと思いますが、ここではAWS Cognitoという認証機能の設定をしています。AWS Amplifyを通して、Cognitoというサービスを使っているんですね。
最後に、設定ファイルをアップロードします。この設定ファイルは今まで選択してきた結果をもとに勝手に作成されています。語彙が足りませんが、すごいです。
$ amplify pushCurrent Environment: dev | Category | Resource name | Operation | Provider plugin | | -------- | ---------------------- | --------- | ----------------- | | Auth | cognitoexampled26e7f7d | Create | awscloudformation | ? Are you sure you want to continue? YesAWS Amplifyの機能を実装
コーディングタイム!
Amplifyの機能をこれまでに作ったアプリに実装していきます。
インストール
$ npm install aws-amplify aws-amplify-vue
aws-amplify-vueというのをインストールしたのはAmplifyEventBusというものを使うためだけです。イベントを登録して他のところで実行するだけのためのものなのでReact.jsユーザーの方は代替するものがあるはずです。
main.js
src/main.jsにて、amplifyコマンドによって生成された設定ファイルsrc/aws-exports.jsをインポートします。// src/main.js import Amplify from 'aws-amplify' import awsconfig from './aws-exports' Amplify.configure(awsconfig) Vue.use(Auth)
auth.jsAWS Amplifyの提供されている機能を実際に使うモジュールを作ってみましょう。Vue.jsのファイル内に直接書いてもいいのですが、個人的に
*.vueファイルは描画に関連したこと以外は書きたくないので、こちらのファイルを作成しています。// src/utils/auth.js import { Auth } from 'aws-amplify' import { AmplifyEventBus } from 'aws-amplify-vue' function getUser() { return Auth.currentAuthenticatedUser().then((user) => { if (user && user.signInUserSession) { return user } else { return null } }).catch(err => { console.log(err); return null; }); } function signUp(username, password) { return Auth.signUp({ username, password, attributes: { email: username, }, }) .then(data => { AmplifyEventBus.$emit('localUser', data.user); if (data.userConfirmed === false) { AmplifyEventBus.$emit('authState', 'confirmSignUp'); } else { AmplifyEventBus.$emit('authState', 'signIn'); } return data; }) .catch(err => { console.log(err); }); } function confirmSignUp(username, code) { return Auth.confirmSignUp(username, code).then(data => { AmplifyEventBus.$emit('authState', 'signIn') return data // 'SUCCESS' }) .catch(err => { console.log(err); throw err; }); } function resendSignUp(username) { return Auth.resendSignUp(username).then(() => { return 'SUCCESS'; }).catch(err => { console.log(err); return err; }); } async function signIn(username, password) { try { const user = await Auth.signIn(username, password); if (user) { AmplifyEventBus.$emit('authState', 'signedIn'); } } catch (err) { if (err.code === 'UserNotConfirmedException') { // The error happens if the user didn't finish the confirmation step when signing up // In this case you need to resend the code and confirm the user // About how to resend the code and confirm the user, please check the signUp part } else if (err.code === 'PasswordResetRequiredException') { // The error happens when the password is reset in the Cognito console // In this case you need to call forgotPassword to reset the password // Please check the Forgot Password part. } else if (err.code === 'NotAuthorizedException') { // The error happens when the incorrect password is provided } else if (err.code === 'UserNotFoundException') { // The error happens when the supplied username/email does not exist in the Cognito user pool } else { console.log(err); } } } function signOut() { return Auth.signOut() .then(data => { AmplifyEventBus.$emit('authState', 'signedOut'); return data; }) .catch(err => { console.log(err); return err; }); } export {getUser, signUp, confirmSignUp, resendSignUp, signIn, signOut};
auth.jsを使っていきましょうこれまでに作成したページで
auth.jsをimportしていきましょう。// src/views/SignUp.vue <script> import {signUp} from '@/utils/auth.js' // Adding this line export default { name: "SignUp", ... methods: { submit() { if (this.$refs.form.validate()) { console.log(`SIGN UP username: ${this.username}, password: ${this.password}, email: ${this.username}`); signUp(this.username, this.password); // Adding this line as well } }, }, } </script>// src/views/SignUpConfirm.vue <script> import {confirmSignUp, resendSignUp} from '@/utils/auth.js' // Adding this line export default { name: "SignUpConfirm", ... methods: { submit() { if (this.$refs.form.validate()) { console.log(`CONFIRM username: ${this.username}, code: ${this.code}`); confirmSignUp(this.username, this.code); // Adding this line as well } }, resend() { console.log(`RESEND username: ${this.username}`); resendSignUp(this.username); // Adding this line as well } }, } </script>// src/views/SignIn.vue <script> import {signIn} from '@/utils/auth.js' // Adding this line export default { name: "SignIn", ... methods: { submit() { if (this.$refs.form.validate()) { console.log(`SIGN IN username: ${this.username}, password: ${this.password}`); signIn(this.username, this.password); // Adding this line as well } }, }, } </script>// src/views/Home.vue <template> <div class="home"> <v-btn @click="signOut">Sign Out</v-btn> <img alt="Vue logo" src="../assets/logo.png"> <HelloWorld msg="Welcome to Your Vue.js App"/> </div> </template> <script> // @ is an alias to /src import HelloWorld from '@/components/HelloWorld.vue' import {signOut} from '@/utils/auth.js' export default { name: 'home', components: { HelloWorld }, methods: { signOut() { signOut().then((data) => console.log('DONE', data)).catch((err) => console.log('SIGN OUT ERR', err)); } } } </script>
router.js先ほど少し書きましたが、ユーザーのログイン状態に応じてページ遷移を制御したいです。
http://localhost:8080/signUpをログイン済みのユーザーには表示させたくありませんし、ログインしていないユーザーにはHome.vueの内容を表示させたくはありませんよね。// src/router.js import Vue from 'vue' import Router from 'vue-router' import Home from './views/Home.vue' import { AmplifyEventBus } from 'aws-amplify-vue' import {getUser} from '@/utils/auth.js' Vue.use(Router) const router = new Router({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'home', component: Home, meta: { requiresAuth: true }, }, { 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') }, { path: '/signUp', name: 'signUp', component: () => import(/* webpackChunkName: "signup" */ './views/SignUp.vue'), meta: { requiresAuth: false }, }, { path: '/signUpConfirm', name: 'signUpConfirm', component: () => import(/* webpackChunkName: "confirm" */ './views/SignUpConfirm.vue'), meta: { requiresAuth: false }, }, { path: '/signIn', name: 'signIn', component: () => import(/* webpackChunkName: "signin" */ './views/SignIn.vue'), meta: { requiresAuth: false }, }, ] }) getUser().then((user) => { if (user) { router.push({path: '/'}) } }) AmplifyEventBus.$on('authState', async (state) => { const pushPathes = { signedOut: () => { router.push({path: '/signIn'}) }, signUp: () => { router.push({path: '/signUp'}) }, confirmSignUp: () => { router.push({path: '/signUpConfirm'}) }, signIn: () => { router.push({path: '/signIn'}) }, signedIn: () => { router.push({path: '/'}) } } if (typeof pushPathes[state] === 'function') { pushPathes[state]() } }) router.beforeResolve(async (to, from, next) => { const user = await getUser() if (!user) { if (to.matched.some((record) => record.meta.requiresAuth)) { return next({ path: '/signIn', }) } } else { if (to.matched.some((record) => typeof(record.meta.requiresAuth) === "boolean" && !record.meta.requiresAuth)) { return next({ path: '/', }) } } return next() }) export default router以上です!これだけで動くのか眉唾モノですね。
動かしてみましょう!
http://localhost:8080にアクセスしてみてください。もしhttp://localhost:8080/signInに勝手に遷移されたら、router.jsへの変更が無事反映されていますので安心してください。
"Sign Up"メニューから、ご自身のメールアドレスを使ってサインインしてみてください。確認メールが届くはずです。
メールアドレスと確認メールのコードを"Confirm"ページに入力後、ログインフォームにメールアドレスとパスワードを入力するとhttp://localhost:8080に遷移できるはずです。NOTE: 2019年7月1日現在、以下のエラーがコンソールに出てくるかもしれません。
No credentials, applicationId or region
これはレポート済みのエラーです。とりあえずアプリは動くはずですので無視してください。
おわりに
AWS Consoleをあまり使うことなく、簡単にログイン機能を作ることができました。Amplifyすごい。
作成されたユーザーはAWS Console > Cognito > Manage User Pools > Users and groupsで無効にしたり削除したりできるので、ユーザー作成に失敗したらAWS Consoleで削除してください。Firebaseを使ったことある方はそちらの方が簡単かもしれませんが、AWS Amplifyでも簡単にサーバーレスアプリが作れそうです。これから当分AWS Amplifyで遊んでいきたいと思っています。
お読み頂きありがとうございました。
- 投稿日:2019-07-04T15:10:19+09:00
Vuex内でAPIを叩きVue.jsのcreated内でstateを取得する
はじめに
初めての投稿です。
今回Vuex内でaxiosを使いAPIからデータを取得します。
サイトにアクセスした際にVue.jsのコンポーネントへデータを渡し画面にデータを表示させます。準備
バージョン 必須 Laravel 5.8.26 no Vue.js 2.5.17 yes Vuex 3.1.1 yes axios 0.18 yes 自分はLaravelを使いAPIもそちらで用意しました。
APIさえ使えればどのような状態でも大丈夫です。処理の流れ
①Vue.jsのcreated内でVuexのactionを呼び出す
②actionがAPIを叩く
③actionがAPIから取得したデータを引数としてmutationを呼び出す
④mutationでstateを更新する
⑤その後getterでstateを取得するVuexのコード
article.jsexport default { namespaced: true, state: { articles: [], }, getters: { getAll: state => { return state.articles; }, getOne: state => id => { return state.articles.find(list => list.id === id); }, }, mutations: { setArticles: (state, payload) => { state.articles = payload.data; }, }, actions: { async setArticles({ commit }) { const payload = { data: '', }; await axios.get('/api/articles') .then(response => { payload.data = response.data['data']; commit('setArticles', payload); }) .catch(error => { console.log(error); }); }, }, }APIから送られてくるデータはjsonです。
{ "data": [ { "id": 1, "title": "non", "body": "Sed non accusantium rem ad totam necessitatibus." }, { "id": 2, "title": "totam", "body": "Ab et veritatis veniam et expedita voluptatem ipsam." }, { "id": 3, "title": "magnam", "body": "Error enim laboriosam saepe delectus est." }, { "id": 4, "title": "assumenda", "body": "Delectus tempore omnis occaecati quibusdam nisi." }, { "id": 5, "title": "error", "body": "Quia exercitationem delectus vitae nulla corrupti eos." } ] }Vue.jsのコード
Article.vue<template> <div> <p>{{ article_list }}</p> </div> </template> <script> import { mapActions, mapGetters } from 'vuex'; export default { data() { return { article_list: [], } }, created() { //this.setArticles; 非同期で処理されるためか遅れて完了する //this.article_list = this.getAll; stateが更新されるよりも先に実行される this.setArticles().then( () => this.article_list = this.getAll ); //この書き方でうまく動いた }, computed: { ...mapGetters('articles', [ 'getAll', ]), }, methods: { ...mapActions('articles', [ 'setArticles', ]), }, } </script>コメントアウトしている箇所で苦しみました。
出力結果
最後に
JavaScriptを勉強したことがないためこのような書き方でいいのか正直わかっていません。
コメントのほうでご教授してもらえたら幸いです。
- 投稿日:2019-07-04T14:40:34+09:00
VueでTwitter認証するときにCROSエラーが発生したときの対処法
vueでいつものように
axiosを使ってサーバーと通信していました。
が、Twitterと連携させるときに以下のようなエラーが発生。Cross-Origin Read Blocking (CORB) blocked cross-origin response https://api.twitter.com/oauth/authenticate?oauth_token=xxxxxxxxxxxxxx with MIME type text/html. See https://www.chromestatus.com/feature/5629709824032768 for more details.どうやら
CORB(Cross Origin Request Blocking)によってブロックされてしまったらしい。
正直意味がわからなかったので色々調べてみたらSome-Origin-Policyが関わっていることが分かりました。そもそもSome-Origin-Policyとは何なのか
Some-Origin-Policy(同一オリジンポリシー)とは、同じドメイン同士でしかやり取りできませんよ!というウェブセキュリティにおけるルールのことです。
あるオリジン(スキーム + ホスト + ポート)にアクセスして、そのリソースから異なるオリジンにAjax通信できないよう制限する仕組みで、スキーム、ホスト、ポート等が一つでも違えばアクセスできません。
サーバーが二つあるとしたらこんな感じです。
- クライアントがAサーバーにアクセス
- Aサーバーはクライアントにレスポンスを返す
- クライアントがBサーバーにAjax通信を送る
- Bサーバーから違うオリジンからアクセスするな!と怒られる。
https:://example.comからhttps::/test.com にAjax通信はできません。
なぜならホスト名が違うから。(ポートやスキームが違ってもだめ)では今回のTwitter認証のエラーに当てはめてみましょう。
localhostにアクセスlocalhostはレスポンスを返すlocalhostからTwitterAPIにAjax通信を送る- 違うオリジン(URL)からアクセスするな!と怒られる。
Twitter側のAPIサーバーで、私はこのURLからのアクセスは許可しないよ!と拒否されてたということですね。
そこで、今回のように
some-origin-policyに弾かれずに異なるドメイン同士で安全にリソースを共有するための仕組みがCORS(Cross Origin Resource Sharing)というもの。アクセスするには、通信される側のサーバーで特定のドメインからのアクセスを許可してあげる必要があります。
詳しくは調べていただければと思うのですが、
例えばサーバー側で、以下のようにヘッダーで許可するドメインを設定しなければなりません。Access-Control-Allow-Origin: 'https://example.com'もちろん僕たちがTwitterのAPIサーバーをいじることはできないので、諦めることに。
解決法 Ajax通信をやめる
長々と調べごとをしてましたが、そもそもAjax通信する必要性がないことに気づいたので、XMLHttpRequestではなく、単純にHTTPRequestを送るように変更することで解決。
axios.get("users/auth/twitter")↓
document.location.pathname = "users/auth/twitter"フロントとバックエンドを分けると認証周りでつまづくことも多いですが、何かセキュリティ的に問題等ありましたらコメントいただけると幸いです。
- 投稿日:2019-07-04T12:00:39+09:00
今更ながらWebアプリを作ってFirebaseで公開してみた話
はじめに
制作物をアウトプットするのは重要である.ということで,僕も挑戦してみる.
この記事は僕が現実逃避の暇つぶしで勉強がてら制作したWebアプリをFirebaseで公開してみたという内容です.制作したWebアプリ:Vue-Lottery
メンバーリストから指定した人数をランダムに抽選するアプリです.
Vue.js + TypeScriptで制作しました.選ばれやすさの重み付け機能がついてます.
複数人を抽選する機能でグループ分けもできます.Firebaseでホスティング
アカウント作る部分は割愛して,プロジェクトのセットアップから始めていきます.
Firebaseのセットアップ
Firebase用のディレクトリを作ってセットアップしていきます.
途中,設定をいろいろ聞かれるので上下キーとスペース・エンターでよしなに選びます.
今回はHostingとベーシック認証に使うFunctionの項目をチェックしておきます.
その後もいろいろ聞かれますが,基本的にはエンター連打で大丈夫です.$ mkdir firebase $ cd firebase $ firebase init ######## #### ######## ######## ######## ### ###### ######## ## ## ## ## ## ## ## ## ## ## ## ###### ## ######## ###### ######## ######### ###### ###### ## ## ## ## ## ## ## ## ## ## ## ## #### ## ## ######## ######## ## ## ###### ######## You're about to initialize a Firebase project in this directory: vue_lottery/firebase ? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choice s. ◯ 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 ... .略. ...Basic認証の設定
この記事を大いに参考にさせていただきました.
具体的な編集内容はそちらを御覧ください.
firebase/firebase.jsonの編集firebase/functions/index.jsの編集firebase/public以下の不要ファイルの削除firebase/functions/static/以下にビルドしたファイルをコピーexpress,basic-auth-connectのインストール
を行います.最後にデプロイします
$ firebase deploy === Deploying to 'vue-lottery-sample'... i deploying functions, hosting i functions: ensuring necessary APIs are enabled... ✔ functions: all necessary APIs are enabled i functions: preparing functions directory for uploading... i hosting[vue-lottery-sample]: beginning deploy... i hosting[vue-lottery-sample]: found 0 files in public ✔ hosting[vue-lottery-sample]: file upload complete i hosting[vue-lottery-sample]: finalizing version... ✔ hosting[vue-lottery-sample]: version finalized i hosting[vue-lottery-sample]: releasing new version... ✔ hosting[vue-lottery-sample]: release complete ✔ Deploy complete! Please note that it can take up to 30 seconds for your updated functions to propagate. Project Console: https://console.firebase.google.com/project/vue-lottery-sample/overview Hosting URL: https://vue-lottery-sample.firebaseapp.comHosting URLにアクセスし,ログイン情報を入力するとアプリに飛べます.
まとめ
今更ながらVue.jsとTypeScriptを使ってWebアプリを制作しFirebaseで公開してみた話でした.
作成したコードはGitHubに置いてあるので参考にしたい方は御覧ください.(整理していないため不要なコードが含まれています)
メンバーリストはyamlファイルから読み込んでいますが,Firebaseのデータベースから読み込むように修正したいです.が,それはまたの機会に下のリンクはサンプルです.
user: test,pass: passwordで認証できます.
https://vue-lottery-sample.firebaseapp.com/
- 投稿日:2019-07-04T10:57:55+09:00
Vue.js・Nuxt.js ハマりポイントまとめ
Vue.js や Nuxt.js で開発していてハマったポイントを雑多にまとめています。(随時追加予定)
props を子コンポーネントにそのまま流したい
具体的な例をあげると、UI コンポーネントをラップしたコンポーネントを作りたい場合などです。
こういうときは、
$attrsを使うとよいです。
https://isoppp.com/note/2018-12-16/what-is-vue-attrs/computed は型アノテーションが必須
明らかに推論できるような場合でも、型アノテーションが必須です。
記述していない場合はエディタがエラーを表示します。computed: { fullName(): string { // 明らかに string を返すが、アノテーションは必須 return `${firstName} ${lastName}`; } }正しく Vue コンポーネントを記述しているのに何かエラーになる場合、これが原因のことが多いです。
リロードした時だけ動かない
具体的な例をあげると、「リロードした時だけ動かない。他のページから遷移してきた場合は動く。」のようなケースです。
これは、SSR と CSR に起因しているケースが多いです。
サーバーサイド(クライアントサイド)でしか実行してはいけない関数を実行してしまっていないか確認しましょう。例えば、サーバーサイドで window オブジェクトにアクセスしているとかです。テーブル周りでエラーになる
theadやtbodyを書けば直ることがあります。
Vue では、自動的にこれらを補完するため、SSR と CSR で DOM が一致せず、エラーになります。
- 投稿日:2019-07-04T08:42:30+09:00
Firebase + Nuxt.js + RaspberryPiで作る猫監視システム
かねてから作りたかった動体検知と画像判定を用いた猫監視アプリを作成することができたので、ご紹介です。
完成したもの
FirebaseにホスティングされたPWA対応のWebアプリです。
ラズパイのカメラが猫を検知すると、画像がアップロードされ、飼い主にPush通知が飛びます。
![]()
![]()
![]()
ソースコード
ソースコードは下記のリポジトリで公開しています。
機能一覧
- 動体検知による画像取得
- 画像判定
- Google認証
- 画像一覧
- PWA
- Push通知
モチベーション
以下のモチベーションから、本システムを開発しました。
- 家に不在の時、飼い猫が何をしているのか気になった
- WebアプリでFirebaseを使い倒してみたかった
開発工数
ざっと3人日ほどです。
会社の開発合宿を利用して開発しました。
開発合宿ってどんなものか気になる方はこちら。
採用技術
infra(Firebase)
- Cloud Firestore
- リアルタイムNoSQL。ユーザー情報、画像情報の保存に利用。
- Cloud Functions
- イベント駆動関数。Storageへの画像登録やFirestoreへのデータ登録をフックして起動。
- Cloud Storage
- ストレージ。画像を保存するのに利用。
- Firebase Authentication
- 認証。今回はGoogle認証のみとした、
- Firebase Cloud Messaging
- Push通知機能。トピック購読者に対する配信を利用。
- Firebase Hosting
- 静的サイトホスティング機能。SPAとしてフロントを構築し、ここにデプロイしている。
client
- Vue.js
- 言わずと知れたjsフレームワーク。
- Vuetify.js
- Vue.js用のマテリアルUIライブラリ。最近は必ず使ってる。
- Nuxt.js
- PWA、Flux、SPAなどを簡単に実現できるVue.js製フレームワーク。
hardware
- RaspberryPi ZERO WH + カメラモジュール
- モバイルバッテリーによる運用を意識して低消費電力のZeroモデルにした。
システム構成
システム構成全体は下記のようになっています。
細かい処理フローや実装はおって説明していきます。
処理フローと実装
ユーザー認証時
ユーザーがサイトにアクセスし、Google認証連携によりサインインしたときに、データをFirestoreに保存します。後述の通知用のトークンを保存するためにユーザーデータをFirestoreに保持しています。
functions.auth.user().onCreate()でユーザーの初回認証時に動作する関数をFunctionsで定義します。functions/index.jsconst functions = require('firebase-functions') const admin = require('firebase-admin') admin.initializeApp() exports.createUserData = functions.auth.user().onCreate(user => { const data = { name: user.email.split('@')[0], displayName: user.displayName, email: user.email, photoURL: user.photoURL, uid: user.uid, createdAt: admin.firestore.FieldValue.serverTimestamp() } const db = admin.firestore() const ref = db.collection('users').doc(user.uid) ref.set(data) return 0 })通知許可時
サイトの右上に通知をONにするボタンを設置しています。
ユーザーがブラウザやPWAで通知を許可した場合、発行されるトークンをFirestore上のユーザーデータの属性に追加します。
追加後、下記のFunctionsがフックされ、通知用のトピックを購読させます。Functions上でsubscribeを実施しているのはセキュリティ上の都合です。
詳細は公式ドキュメントを参照してください。functions/index.jsexports.subscribeTopic = functions.firestore .document('/users/{userId}') .onUpdate(async (change, context) => { const token = change.after.data().messagingToken console.log(token) const res = await admin.messaging().subscribeToTopic(token, '/topics/cat') console.log(res) return null })動体検知〜画像アップロード
ラズパイ上で稼働している
motionコマンドがカメラモジュールを経由して、動体を検知します。
動体が検知されると、その画像を生成するようにmotionコマンドを設定しています。
motionコマンドとは別に、画像の生成を検知し、画像をアップロードする下記のスクリプトを稼働させています。
inotifywatchコマンドでmotionが生成する画像を置くディレクトリを監視しています。
画像ファイルが生成されたらgsutilコマンドを使ってCloud Storageへ画像をアップロードする実装になっています。roles/cat/files/upload.sh#!/bin/bash TARGET_DIR="/tmp/motion" mkdir -p ${TARGET_DIR} while inotifywait -e CREATE ${TARGET_DIR}; do file=$(ls -rt ${TARGET_DIR} | tail -n 1) gsutil -o 'Credentials:gs_service_key_file=/root/.credentials.json' cp "${TARGET_DIR}/${file}" gs://cat-watcher.appspot.com/ done猫判定〜画像メタデータ保存
画像がStorageにアップロードされると、Functionsの
createImageData関数が起動します。
createImageData関数ではGoogle Cloud Vision APIを呼び出し、画像にラベル付けを行います。
ラベル付けの結果、Catラベルが含まれていない場合は、画像を削除し、処理は終了します。
Catラベルが含まれている場合は、画像の公開URLを発行し、Firestoreの画像一覧データにメタ情報を追加します。
Firestoreにメタ情報を保存しているのは、UIでリアルタイム同期で画像一覧を表示する際に利用するためです。functions/index.jsexports.createImageData = functions.storage .object() .onFinalize(async object => { // 画像ファイル以外は何もしない if (!object.contentType.startsWith('image/')) { console.error('This is not an image.') return null } // 更新時は何もしない if (object.metageneration !== '1') { console.info('updated.') return null } // Vision APIを利用してラベル判定 const client = new vision.ImageAnnotatorClient() const [result] = await client.labelDetection( `gs://${object.bucket}/${object.name}` ) console.log(result.labelAnnotations) const cat = result.labelAnnotations.filter( annotation => annotation.description === 'Cat' ) const file = admin .storage() .bucket(object.bucket) .file(object.name) if (cat.length > 0) { // Catラベルが付いていれば、公開URLを作成しFirestoreにメタデータ登録 const [downloadUrl] = await file.getSignedUrl({ action: 'read', expires: '01-01-2050' }) const ref = admin .firestore() .collection('images') .doc() const data = { id: ref.id, name: object.name, url: downloadUrl, createdAt: admin.firestore.FieldValue.serverTimestamp() } await ref.set(data) } else { // Catラベルが付いていなければ、画像を削除する await file.delete() console.log('deleted') } return null })Push通知
上記のラベル判定用のFunctions関数中で画像一覧データにデータが追加されと、Functionsの
sendMessage関数がフックされます。
sendMessage関数では、Cloud Messagingのトピックへ画像データが追加された旨を配信します。
この処理により、トピックを購読している=通知を許可したユーザーの端末へPush通知が送信されます。functions/index.jsexports.sendMessage = functions.firestore .document('/images/{imageId}') .onCreate(async (snap, context) => { const message = { notification: { title: NOTIFICATION_TITLE, body: '新しい画像が追加されました', icon: 'https://cat-watcher.firebaseapp.com/android-chrome-512x512.png', click_action: 'https://cat-watcher.firebaseapp.com/' } } await admin.messaging().sendToTopic('/topics/cat', message) return null })クライアントでの表示
自宅で運用するため、家族のみアクセスできるようにFirestoreとStorageのルールで制御しています。
サイトにアクセスされたら、vuexfireを使いFirestore上の画像メタ情報をvuexのstoreにロードしています。
画像メタ情報中の公開URLで画像をUIに表示しています。
また、認証用のUIはfirebaseui-webで簡単に作成することができます。まとめ
FirebaseとRaspberryPiを使ってさくっと猫監視システムを作ってみました。
やっぱりうちの猫はかわいいなあ。
- 投稿日:2019-07-04T02:15:49+09:00
【Vue】filtersで検索キーワードをハイライト!
Vue.jsでこの「検索キーワードのハイライト」がしたかった
Vue.jsのfiltersを使えばカンタンだね
1. まず、用意
vue-cliで、プロジェクト作るまでは割愛するよ〜
プロジェクトが出来たら、App.vue, components/Media.vueに、サンプルテキストを準備する
/App.vue<template> <div id="app"> <div class="container"> <div class="col"> <div class="row"> <input class="form-control col-6" type="text" v-model="search" // 検索キーワード placeholder="検索" aria-label="Search"> </div> <media v-for="(media, index) in medias" :key="index" :heading="media.heading" :body="media.body" class="mt-3"/> </div> </div> </div> </template> <script> import Media from './components/Media' export default { name: 'app', components: { Media }, data () { return { search: '', // 検索キーワード medias: [ { heading: 'その1', body: 'それも直接あにその始末心というののところをしたた。\n' }, { heading: 'その2', body: 'ひょろひょろ始めに建設人はたといこの発展たですまでを行かて行かますをは盲従出さでだから、もともとにはできないたたた。' }, { heading: 'その3', body: '個人をすむたい事もむしろ偶然に同じくたただ。\n' }, { heading: 'その4', body: 'やはり槙さんへ想像がたどう安心に繰り返しです嚢この人皆か関係にってご返事ですますなかっですから、そうした今は私か国家学校があって、木下さんのものの学生のあなたにいよいよご演説と勧めてあなた主義で大発展に移れように何しろご講演と得んないて、はなはだもっとも発展が構わないて来るん方にかけるなりない。それならもっともご権力を聞いものはぴたり正直と直さですて、どんな頭にはすれないばといった中からしが来なない。' }, { heading: 'その5', body: 'その時国の時同じ釣はあなた中をありでしょかと岡田さんに云ったう、新のたくさんですという小立脚なだませので、自己のところが他がほかかもの仲間を今込み入って行かで、少々の今をしのでその時にああ使いこなすでたといだつもりまして、忌まわしいらしいたて少しお作物知らで事んなた。' } ] } } } </script>/components/Media.vue<template> <div class="media"> <img class="mr-3" src="https://via.placeholder.com/150" alt="Generic placeholder image"> <div class="media-body"> <h5 class="mt-0">{{ heading }}</h5> <p>{{ body }}</p> </div> </div> </template> <script> export default { props: ['heading', 'body'] } </script>2. filtersを追加
公式のフィルターの項目を参考に、searchHighlightを追加してみよう
App.vue<template> <div id="app"> <div class="container"> <div class="col"> <div class="row"> <input v-model="search" class="form-control col-6" type="text" placeholder="検索" aria-label="Search"> </div> <media v-for="(media, index) in medias" :key="index" :heading="media.heading" :body="media.body" :search="search" // ここを追加 フィルターに使う検索キーワードを渡す class="mt-3"/> </div> </div> </div> </template>/components/Media.vue<template> <div class="media"> <img class="mr-3" src="https://via.placeholder.com/150" alt="Generic placeholder image"> <div class="media-body"> <!-- | を使って、フィルターした値を表示できるぜ --> <h5 class="mt-0">{{ heading | searchHighlight(search) }}</h5> <p>{{ body | searchHighlight(search) }}</p> </div> </div> </template> <script> export default { props: ['heading', 'body', 'search'], // propsにsearchを追加 filters: { searchHighlight (value, search) { // 検索キーワードが入力されているとき if (search) { // 検索キーワードに一致する部分を、spanタグに置換すればいいね return value.replace(search, `<span class="bg-warning">${search}</span>`) } // 検索キーワードが入力されていない場合は、ハイライトしない return value } } } </script>おや?エスケープされているぞ。
3. v-htmlを使いなさい
公式の見解によると、
v-htmlディレクティブを使えとさ。v-htmlで、フィルターを使うには、Google先生に聞いたところ
<h5 class="mt-0" v-html="$options.filters.searchHighlight(heading, search)"></h5>こう書くらしい。(optionsの中身を知りたい方は、mounted()メソッド内で、console.log(this.$options)をしてみよう)
この書き方をすると、
/components/Media.vue<template> <div class="media"> <img class="mr-3" src="https://via.placeholder.com/150" alt="Generic placeholder image"> <div class="media-body"> <!-- v-htmlを使用 --> <h5 class="mt-0" v-html="$options.filters.searchHighlight(heading, search)"></h5> <p v-html="$options.filters.searchHighlight(body, search)"></p> </div> </div> </template> <script> export default { props: ['heading', 'body', 'search'], filters: { searchHighlight (value, search) { if (search) { return value.replace(search, `<span class="bg-warning">${search}</span>`) } return value } } } </script>出来、、、、てない!
各文の最初の「た」 しか、ハイライトされていない!!
でも正規表現を使えば解決だ
4. 正規表現を使う
searchHighlight (value, search) { if (search) { // 世紀表現で、一致する文字全てをハイライトする const searchRegExp = new RegExp(search, 'ig') return value.replace(searchRegExp, (match) => { return `<span class="bg-warning">${match}</span>` }) } return value }出来た!!
- 投稿日:2019-07-04T00:08:20+09:00
お勉強を兼ねて、鳥貴族 注文ガチャ を作った(nuxt.js + rails + heroku + python + selenium)
背景
モダンな技術を勉強したかったので、それらを使ってwebアプリを何か作ろうというところから始まりました。作っていく中で勉強するのが一番手っ取り早いという考えもありました。
下記の「サイゼ1000円ガチャ」を見てメニューの注文ガチャいいなあと思ったので、居酒屋でも同じようなものがあったら面白そうと思い、今回の鳥貴族ガチャを作ることにしました。
そんな感じで、興味のある技術を使って開発をしてみることが一番の目的でした。
成果物:鳥貴族 注文ガチャ
概要
- 食べ物と飲み物の数を入力する
- [ガチャを回すボタン]を押下
- 入力した個数分のメニューがそれぞれランダムで出力される実物
鳥貴族 注文ガチャ - Heroku
https://ak-toriki-nuxt-frontend.herokuapp.com/フロントエンドプロジェクト(Nuxt.js) - GitHub
https://github.com/lelouch99v/toriki-nuxt-frontendバックエンドプロジェクト(Ruby on Rails) - GitHub
https://github.com/lelouch99v/toriki-backend制作のポイント
使った技術
以下の技術を使いました。
- Python
- selenium webdriver
- vue.js
- nuxt.js
- Ruby on Rails
- postgreSQL
- heroku
DBに入れるメニューをスクレイピングで取得
Pythonでのスクレイピングにハマっていたのでやってみました。
焼鳥、逸品料理、スピードメニュー、ドリンクの4つのカテゴリからそれぞれメニューを取得していきます。結果はcsvで出力します。
このcsvを使ってrailsのmigrateデータとしてメニューデータをDBに入れました。
トリキ スクレイピング - GitHub
https://github.com/lelouch99v/toriki-scraping/tree/master(今回の開発で一番楽しかったのがここです)
React → Vue.js に変更
前提としてSPA + API の構成としたかったです。
業務でAngularは使っているので、ReactかVueを学びたいと思っていました。
当初Reactを選択したのですが、学習に時間がかかり出来上がるのが先になってしまいそうなのと、Nuxt.jsが気になっていたのであっさりとVueに変えました。
すごく入りやすくて学習していて楽しいです。今後の課題
ボタン押下時のインタラクション
ガチャ回すボタン押すとメニューがランダム表示されますが、とても味気ないです。派手なものにする必要はないですが、最低限以下は実現したいと思っています。
- ボタンを押した感出す
- メニューが表示されるまでにワンクッション置く(ワクワク感が足りないため)各メニューのイメージ画像を見れるようにする
メニューの名称だけの表示ではなく、イメージ画像も見れるようにするといいと思いました。
そのまま画像を表示するのはスペースの問題などありそうなので、リンククリックでモーダル表示など一工夫は必要かもしれません。あとはメニュー表示の見た目が質素なので、もう少し改善が必要ですね。
スマホで数字入力をドロップダウンリストで可能にする
スマホではキーボード入力よりも、以下のような選択式のリストで入力のほうがやりやすいと思います。
最大値は99まであれば十分かなと。。。(もっと少なくてもいいですね)
てかすでにあった
鳥貴族ガチャでぐぐったらすでに作っていた人がいました。しかもクオリティ高い。
完全なリサーチ不足です。今回の目的は技術の勉強なので、もももんだいないんですけどね!




























