- 投稿日:2019-10-11T21:27:24+09:00
Vue + SVG で作るグラフィックアプリの骨格
はじめに
この記事はもう一つの記事、
Vue + Canvas で作るグラフィックアプリの骨格
の SVG 版です。Canvas や SVG のおかげでブラウザ上でもインタラクティブにグラフィックを簡単に扱えるようになりました。
おまけに Vue を使うとまるでパソコン上のアプリを作るかのように Web 上にグラフィックアプリを作ることができます。
この記事では Vue + SVG でマウスで四角を書いていくような 80 行程度の簡単なサンプルを作ることで、Web 上のグラフィックアプリの骨格を示したいと思います。
準備
- node, npm をダウンロードします
https://nodejs.org
この記事では node v12.4.0 で検証しています
- vue-cli をダウンロードして、とりあえず qvs という名前でアプリを作ります。
$ npm i @vue/cli -g $ vue create qvs $ cd qvs $ yarn serveこの記事では vue 3.12.0 で検証しています。yarn を使ってない人は最後の行は
$ npm run serveになると思います。
なお、以下のコードは ESLint の掟に従ってないので、
vue create qvs
の時に ESLint をはずさないと多分エラーになると思われます。
また、セパレータを前に書いたり、引数とか変数の名前が _ だったりするのは単なる趣味ですので、気に入らなくても怒らないでください。コード
src/App.vue を以下のようにしてみます。
App.vue<template> <div id=app style="overflow: scroll"> <div id=menu style="position: fixed; top: 0; left: 0"> <button @click="mode='select'" >select </button> <button @click="mode='rect'" >rect </button> </div> <svg style = "margin: 24px 4px 4px 4px; background: aliceblue" :class = "{ drawRect: mode == 'rect' }" :width = "extent[ 0 ]" :height = "extent[ 1 ]" @mousedown = "mouseDown" @mousemove = "mouseMove" @mouseup = "mouseUp" @keyup.esc = "keyUpESC" > <template v-for="_ in elements"> <rect v-bind="_" stroke=black fill=none /> </template> <rect v-bind=dragRect stroke=blue fill=none /> </svg> </div> </template> <script> export default { data : () => ( { b : null , c : null , mode : 'select' , elements : [] } ) , computed: { extent () { return [ 700, 500 ] } , svg () { return this.$el.getElementsByTagName( 'svg' )[ 0 ] } , dragRect() { return ( ! this.b || ! this.c ) ? null : { x : this.b.offsetX , y : this.b.offsetY , width : this.c.offsetX - this.b.offsetX , height : this.c.offsetY - this.b.offsetY } } } , methods : { mouseDown( _ ) { this.b = _ } , mouseMove( _ ) { this.c = _ } , mouseUp( _ ) { this.c = _ switch ( this.mode ) { case 'rect': this.elements.push( this.dragRect ) break } this.b = null } , keyUpESC( _ ) { this.mode = 'select' } } , mounted() { this.svg.setAttribute( 'tabindex', 0 ) } } </script> <style> .drawRect { cursor: crosshair } </style>勘所
キーイベントの取得
svg に tabindex 属性をつけると、キーイベントを取得できるようになります。
this.svg.setAttribute( 'tabindex', 0 )カーソルの切り替え
mode が 'select' と 'rect' の2つの状態が存在します。
'rect' の状態の時にクロスヘアカーソルを表示するために
クラスとスタイルのバインディングを動的に使っています。:class = "{ drawRect: mode == 'rect' }" <style> .drawRect { cursor: crosshair } </style>v-bind でプロパティを一括設定
プロパティ名と同じキーを持つ辞書を用意して
dragRect = { x : 100 , y : 200 , width : 300 , height : 400 }<rect v-bind=dragRect />とやると、
<rect x=100 y=200 width=300 height=400 />と展開されます。
最後に
この記事が何かの役にたてたら幸いです。わからないことがあればコメントください!
- 投稿日:2019-10-11T18:34:06+09:00
Vue + Canvas で作るグラフィックアプリの骨格
はじめに
この記事はもう一つの記事、
Vue + SVG で作るグラフィックアプリの骨格
の Canvas 版です。Canvas や SVG のおかげでブラウザ上でもインタラクティブにグラフィックを簡単に扱えるようになりました。
おまけに Vue を使うとまるでパソコン上のアプリを作るかのように Web 上にグラフィックアプリを作ることができます。
この記事では Vue + Canvas でマウスで四角を書いていくような 100 行程度の簡単なサンプルを作ることで、Web 上のグラフィックアプリの骨格を示したいと思います。
準備
- node, npm をダウンロードします
https://nodejs.org
この記事では node v12.4.0 で検証しています
- vue-cli をダウンロードして、とりあえず qvc という名前でアプリを作ります。
$ npm i @vue/cli -g $ vue create qvc $ cd qvc $ yarn serveこの記事では vue 3.12.0 で検証しています。yarn を使ってない人は最後の行は
$ npm run serveになると思います。
なお、以下のコードは ESLint の掟に従ってないので、
vue create qvc
の時に ESLint をはずさないと多分エラーになると思われます。
また、セパレータを前に書いたり、引数とか変数の名前が _ だったりするのは単なる趣味ですので、気に入らなくても怒らないでください。コード
src/App.vue を以下のようにしてみます。
App.vue<template> <div id=app style="overflow: scroll"> <div id=menu style="position: fixed; top: 0; left: 0"> <button @click="mode='select'" >select </button> <button @click="mode='rect'" >rect </button> </div> <canvas style = "margin: 24px 4px 4px 4px; background: aliceblue" :class = "{ drawRect: mode == 'rect' }" :width = "extent[ 0 ]" :height = "extent[ 1 ]" @mousedown = "mouseDown" @mousemove = "mouseMove" @mouseup = "mouseUp" @keyup.esc = "keyUpESC" /> </div> </template> <script> export default { data : () => ( { b : null , c : null , mode : 'select' , elements : [] } ) , computed: { extent () { return [ 700, 500 ] } , canvas () { return this.$el.getElementsByTagName( 'canvas' )[ 0 ] } , ctx () { return this.canvas.getContext( '2d' ) } , dragRect() { return ( ! this.b || ! this.c ) ? null : [ this.b.offsetX , this.b.offsetY , this.c.offsetX - this.b.offsetX , this.c.offsetY - this.b.offsetY ] } } , methods : { draw() { this.ctx.clearRect( 0, 0, ...this.extent ) for ( let _ of this.elements ) this.ctx.strokeRect( ..._ ) if ( ! this.dragRect ) return switch ( this.mode ) { case 'rect': this.ctx.setLineDash( [ 1 ] ) this.ctx.strokeRect( ...this.dragRect ) this.ctx.setLineDash( [] ) break } } , mouseDown( _ ) { this.b = _ this.draw() } , mouseMove( _ ) { this.c = _ this.draw() } , mouseUp( _ ) { this.c = _ switch ( this.mode ) { case 'rect': this.elements.push( this.dragRect ) break } this.b = null this.draw() } , keyUpESC( _ ) { this.mode = 'select' this.draw() } } , mounted() { this.canvas.setAttribute( 'tabindex', 0 ) this.draw() } } </script> <style> .drawRect { cursor: crosshair } </style>勘所
キーイベントの取得
canvas に tabindex 属性をつけると、キーイベントを取得できるようになります。
this.canvas.setAttribute( 'tabindex', 0 )カーソルの切り替え
mode が 'select' と 'rect' の2つの状態が存在します。
'rect' の状態の時にクロスヘアカーソルを表示するために
クラスとスタイルのバインディングを動的に使っています。:class = "{ drawRect: mode == 'rect' }" <style> .drawRect { cursor: crosshair } </style>最後に
この記事が何かの役にたてたら幸いです。わからないことがあればコメントください!
- 投稿日:2019-10-11T18:22:25+09:00
propsで受け取った値を、チェックしてdataに格納する。
親から子に値を渡す時に、子はpropsで受け取りますが、
受け取ったpropsの値をチェックしてからdataに格納したい。
けど、迷ったので忘備録です。props: { 'value': { 'type': [Array, Boolean, String], 'default': false } }, data: function(){ const me = this; let checkVal = false; if(me.value) { checkVal = true; } return { 'val': checkVal, }; },propsで受け取ったデータはmethodsとかcomputedで
ごにゃごにゃしてからdataに格納するのかと思ったら、
こんなシンプルな方法できました。このdata内のifでバリデーションを色々かけれそうですね。
ちなみにpropsの値を直接ゴニョゴニョ変更しようとすると怒られます。
簡単な記事だけど、とりあえず忘備録。
時間があるときにちゃんとした記事にします。
- 投稿日:2019-10-11T18:13:31+09:00
VuexでIndexedDBのコネクション管理
HTML5の勉強してたら出てきた"Indexed DataBase API"を使ってみたかったんです
store.jsimport Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { db: null, }, mutations: { DB_CONNECTION: (state, payload) => (state.db = payload), }, actions: { createDatabase({ commit }) { // ブラウザにより異なるらしい const indexedDB = window.indexedDB || window.mozIndexedDB || window.msIndexedDB if (indexedDB) { var dbOpen = indexedDB.open(`${YOUR_DATABASE_NAME}`, 1.0) // 第2引数はversion // データベースがない場合、既存のversionよりも大きい引数でopenした場合 dbOpen.onupgradeneeded = function(event) { const dbConnection = event.target.result // スキーマを作成する const store = dbConnection.createObjectStore(`${YOUR_STORE_NAME}`, { // store = テーブルのようなもの keyPath: `id`, // 主キー用 autoIncrement: true // オートインクリメントを有効にしておかないとレコード挿入時に指定必須 }) store.createIndex('contentIndex', `${YOUR_INDEX}`) // index = カラムのようなもの commit('DB_CONNECTION', dbConnection) } // 接続成功の場合 dbOpen.onsuccess = function(event) { commit('DB_CONNECTION', event.target.result) } } else { // IndexedDB使用不可 } }, }, })あとはVueのインスタンス作成した時にdispatchで呼んで、以後はstateのdbを参照すればOK
- 投稿日:2019-10-11T16:47:31+09:00
graphql-ruby×nuxt.js 画像アップロード
走り書きすみません。綺麗にします。
Rails
carrierwaveuploader/carrierwaveを設定した後のお話。
ModelとUploaderを設定してね。GraphqlController
jaydenseric/apollo-upload-clientコレを使うと、ActionController::Parametersに入ってくる値が変化する。(リクエストを変更している。)
~/app/controllers/graphql_controller.rbclass GraphqlController < ActionController::API def execute if params[:operations].present? param = JSON.parse(params[:operations]) query = param["query"] operation_name = param["operationName"] variables = { "file" => params["1"], } else # コレ要らないかも? variables = ensure_hash(params[:variables]) query = params[:query] operation_name = params[:operationName] end context = { session: session, current_user: current_user, } result = ApiSchema.execute(query, variables: variables, context: context, operation_name: operation_name) render json: result rescue JWT::ExpiredSignature, JWT::DecodeError, JWT::VerificationError => e render json: { error: { message: e.message } }, status: :unauthorized rescue => e raise e unless Rails.env.development? handle_error_in_development e end endImageType
~/app/graphql/scalar_types/image_type.rbmodule ScalarTypes class ImageType < Types::BaseScalar graphql_name 'ImageType' description 'ActionDispatch::Http::UploadedFile' def coerce_input(file, _context) ActionDispatch::Http::UploadedFile.new( filename: file.original_filename, type: file.content_type, head: file.headers, tempfile: file.tempfile ) end def coerce_result(value, _context) I18n.l(value, format: :default) end end end中身の確認
variables["file"].original_filename # => "hogehoge.jpg" variables["file"].content_type # => "image/jpeg" variables["file"].headers # => "Content-Disposition: form-data; name=\"1\"; filename=\"hogehoge.jpg\"\r\nContent-Type: image/jpeg\r\n" variables["file"].tempfile # => #<File:/tmp/RackMultipart20191011-1-vtsg19.jpg>Mutation
~/app/graphql/mutations/user_resource/update_user_profile_image.rbmodule Mutations module UserResource class UpdateUserProfileImage < Mutations::BaseMutation null false argument :profile_image, ScalarTypes::ImageType, required: true field :results, Boolean, null: true def resolve(profile_image:) ActiveRecord::Base.transaction do user = context[:current_user] user.remove_profile_image! if user.profile_image user.profile_image = profile_image user.save ? { results: true } : { results: false } end end end end end~/app/graphql/object_types/user_type.rbmodule ObjectTypes class UserType < Types::BaseObject field :id, ID, null: false field :profile_image, ScalarTypes::ImageType, null: true field :created_at, GraphQL::Types::ISO8601DateTime, null: true field :updated_at, GraphQL::Types::ISO8601DateTime, null: true end endNuxt
Apollo設定
以下のコードは使いません。
~/apollo/client-configs/default.jsimport { HttpLink } from 'apollo-link-http' export default () => { const httpLink = new HttpLink({ uri: 'http://localhost:3000/graphql' }) }このnode_moduleを使います。jaydenseric/apollo-upload-client
入力フォーム
vue-upload-componentインストールする。
~/apollo/client-configs/default.jsimport { ApolloLink } from 'apollo-link' import { InMemoryCache } from 'apollo-cache-inmemory' import { createUploadLink } from 'apollo-upload-client' export default () => { const uploadLink = new createUploadLink({ uri: 'http://localhost:3000/graphql' }) const current_user = JSON.parse(localStorage.getItem('current_user')) const middlewareLink = new ApolloLink((operation, forward) => { operation.setContext({ headers: { authorization: !current_user ? '' : current_user.token ? `Bearer ${current_user.token}` : '' } }) return forward(operation) }) const link = ApolloLink.from([ middlewareLink, uploadLink ]) return { link, cache: new InMemoryCache() } }~/pages/settings/profile.vue<template> <v-container> <label for="profile_image">Button</label> <file-upload extensions='gif,jpg,jpeg,png,webp' accept='image/png,image/gif,image/jpeg,image/webp' name='profile_image' v-model='profileImage' @input-filter='inputFilter' @input-file='inputFile' ref='upload'/> </v-container> </template> <script> import UpdateUserProfileImage from '~/apollo/gql/mutations/user_resource/updateUserProfileImage.gql' import FileUpload from 'vue-upload-component' export default { components: { FileUpload }, data: () => ({ profileImage: [] }), methods: { async storeProfileImage(file) { await this.$apollo.mutate({ mutation: UpdateUserProfileImage, variables: { file: file.file } }).then(res => { console.log(res) }).catch(err => { console.error(err) }) }, inputFile(newFile) { if (newFile) { this.$nextTick(function() { this.storeProfileImage(newFile) }) } }, inputFilter(newFile, oldFile, prevent) { if (newFile && !oldFile) { if (!/\.(gif|jpg|jpeg|png|webp)$/i.test(newFile.name)) { return prevent() } } if (newFile && (!oldFile || newFile.file !== oldFile.file)) { newFile.url = '' let URL = window.URL || window.webkitURL if (URL && URL.createObjectURL) { newFile.url = URL.createObjectURL(newFile.file) } } } } } </script>GQL
引数の名前が$fileでなければ、エラーが発生するっぽい。
jaydenseric/apollo-upload-client
Usage
Use FileList, File, Blob or ReactNativeFile instances anywhere within query or mutation variables to send a > GraphQL multipart request.
~/apollo/gql/mutations/user_resource/updateUserProfileImage.gqlmutation UpdateUserProfileImage($file: ImageType!) { updateUserProfileImage(profileImage: $file) { results } }残った疑問
- ↓調べる。
ActionController::Parameters
- 投稿日:2019-10-11T14:00:10+09:00
Vuexメモ
state
dataのようなもの
状態を表しているgetter
computedのようなもの
stateから別の値を算出するために用いられる。
ステートの値を書き換える力はない。
getterの中で他のgetterを使うことも可能mutation
唯一stateを更新する役割。
methodsのようなもの。
第一引数に渡されたstateを更新する。
呼ぶときは、store.commit(mutation)のように呼ぶ。
この時第二引数に何らかの値を渡すと、mutationの第二引数に渡される。
この値のことをペイロードという。
まあメソッドの引数みたい感じ。
mutationの中ではsettimeoutのような非同期の処理は用いらない。action
非同期処理や外部APIとの通信を行い、最終的にmutationを呼び出すために用いられる。
actionの定義はmutationに似ているが、第一引数にステートではなく、コンテキストと呼ばれる特別なオブジェクトが渡される。
コンテキストとは、state、getters、dispatch、commitが含まれる。
actionはmutationを実行するために用いられるため、commitが使われることが多い。
dispatchでactionを呼ぶ。
いろいろ複雑だが、流れ的には
1、store.dispatch(incrementAction)でincrementActionというactionを呼び出す。
2、incrementActionの中のcommit(incrementMutation)でincrementMutationというmutationを実行する。
という風なる。
dispatch→action→commit→mutationという感じにコードを追っていけば、実際に行われている処理にたどり着くであろう。
- 投稿日:2019-10-11T12:29:10+09:00
Nuxt.js - Firebaseを使ったToDoアプリの作成
はじめに
Nuxt.jsの学習でTodoアプリを作成したので、理解した内容で少し機能の追加をしてみました。
0から作成する手順をまとめたいと思います。初学者のため、コード等お見苦しい箇所が多くあるかもしれません。お許しください作成物
機能
以下が主な機能でnewが追加した部分になります。
- タスクが登録できる
- タスクに備考が登録できる new
- タスクに日付が登録できる new
- タスクを完了にできる
- タスクを削除できる
- UIフレームワークでVuetifyを使用する new
- 登録の際にダイアログを表示する new
- TodoとDoneは別々の表示領域を用意する new
前提
- Googleアカウントを所持していること
- Nuxtの開発環境が用意してあること
※開発環境の用意についてはこちらにまとめてみました!
Firebaseの設定
データの格納場所としてFirebaseの
CloudFirestore
を使用します。
Cloud Firestore
サイトへアクセス
以下のURLへアクセスします。
https://firebase.google.com/?hl=ja使ってみるを選択するとGoogleアカウントの認証画面が開くので認証情報を入力します。
プロプロジェクトの作成
Firebaseブロジェクトの「プロジェクトの追加」を選択します。
プロジェクト名を入力して「続行」を選択します。
Googleアナリティクスを有効にして「続行」を選択します。
アナリティクスを日本にして利用規約等を読み「プロジェクトを作成」を選択します。
Database(CloudFirestore)の作成
「開発」 -> 「Database」からCloudFirestoreを開きます。
「データベースを作成」を選択します。セキュリティ保護ルールの作成にて「テストモードで開始」を選択します。ロックモードでは認証ありのモードです。テストモードは認証がありませんので検証や学習の際に利用します。
テストモードの場合、プロジェクトIDのみで利用できてしまうので、外部にプロジェクトIDを出さないよう注意しましょう。検証が終わったらdatabaseは削除します
ロケーション(物理サーバーの配置場所)の設定では「asia-northeast1 (東京)」を選択して完了を選択します。
これでFirebase側の準備はOKです!
プロジェクトの作成
terminalnpx create-nuxt-app nuxt-todo作成時以下の内容だけ指定して、他は初期値にします。 // 利用するパッケージマネージャー ? Choose the package manager Npm - Npm // 利用するUIフレームワーク ? Choose UI framework None - Vuetify // 利用するコード検証ツール(コードのチェックツール) ? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection) - ESLint - Prettier // 利用するレンダリングモード ? Choose rendering mode Universal (SSR) - SPAFirebase関連のパッケージをインストール
firebaseを利用するための基本パッケージ(firebase)をインストールします。
terminalnpm install --save firebase@6.2.4CloudfireStoreを簡単に利用するためのパッケージ(vuexfire)をインストールします。
terminalnpm install --save vuexfire@3.0.1環境変数の設定
.envから環境変数を読み込むためのパッケージ(dotenv)をインストールします。
terminalnpm install --save @nuxtjs/dotenv@1.3.0.envを作成
.envFIREBASE_PROJECT_ID = 'project-id'プロジェクトIDはコンソール画面の歯車マークから「プロジェクトの設定」を選択し、開いた「Setting」の画面に表示されています。
Gitで管理する予定の場合は
.gitignore
に.envが含まれていることを確認します。(Gitの反映対象外とする).gitignore# dotenv environment variables file .envFirebaseとの連携
pluginsディレクトリの下に連携用のファイルを作成します。ファイル名は自由です。
firebase.jsimport firebase from 'firebase/app' import 'firebase/firestore' // .envからプロジェクトIDを取得して定数に設定 const config = { projectId: process.env.FIREBASE_PROJECT_ID } // firebaseの初期化処理 if (!firebase.apps.length) { firebase.initializeApp(config) } export default firebaseストアの作成
storeディレクトリの下にindex.jsファイルを作成します。
index.jsimport { vuexfireMutations } from 'vuexfire' export const mutations = { ...vuexfireMutations }storeディレクトリの下にtask.jsファイルを作成します。
task.jsimport { firestoreAction } from 'vuexfire' import firebase from '~/plugins/firebase' const db = firebase.firestore() const taskRef = db.collection('task') export const state = () => ({ tasks: [] }) export const actions = { // 初期化 init: firestoreAction(({ bindFirestoreRef }) => { bindFirestoreRef('tasks', taskRef) }), // 追加 add: firestoreAction((context, { title, detail, date }) => { if (title.trim()) { taskRef.add({ title, detail, date, status: false }) } }), // 削除 remove: firestoreAction((context, id) => { taskRef.doc(id).delete() }), // ステータス更新 toggle: firestoreAction((context, todo) => { taskRef.doc(todo.id).update({ status: !todo.status }) }) } // 日付の昇順でソート export const getters = { orderdTodos: (state) => { return _.orderBy(state.tasks, 'date', 'asc') } }一覧データを日付の昇順で表示するためにlodashというライブラリを使用しました。
使用するためにはnuxt.config.jsに以下の記載をします。nuxt.config.jsimport webpack from 'webpack' build: { /* ** You can extend webpack config here */ extend(config, ctx) {}, plugins: [ new webpack.ProvidePlugin({ _: 'lodash' }) ] }コンポーネントの作成
コンポーネントは全部で3つ作成しました。
もっと細かく分けたかったのですが、ざっくりと分けることにしました。タスクコンポーネント
まずはダイアログの部分です。
Vuetifyにはダイアログのコンポーネントがあるのでそれを利用しました。
datepickerのコンポーネントも色々表示のさせ方が豊富で便利ですね
登録を押した時にaddメソッドからtask.jsのaddが実行されてDBに登録されます。/components/TaskDetail.vue<template> <v-dialog v-model="dialog" persistent max-width="600px"> <template v-slot:activator="{ on }"> <v-btn color="#5963F8" dark class="font-weight-bold" v-on="on" ><v-icon small class="mr-2">mdi-plus-circle-outline </v-icon >新規タスクを追加</v-btn > </template> <v-card> <v-card-title> <span class="headline">新規タスクを追加</span> </v-card-title> <v-card-text> <v-container> <v-row> <v-col cols="12"> <v-text-field v-model="title" label="Title"></v-text-field> </v-col> <v-col cols="12"> <v-textarea v-model="detail" label="Detail"></v-textarea> </v-col> <v-col cols="12"> <v-dialog ref="dialog" v-model="modal" :return-value.sync="date" persistent width="290px" > <template v-slot:activator="{ on }"> <v-text-field v-model="date" label="日時" readonly v-on="on" ></v-text-field> </template> <v-date-picker v-model="date" scrollable> <div class="flex-grow-1"></div> <v-btn text color="primary" @click="modal = false" >Cancel</v-btn > <v-btn text color="primary" @click="$refs.dialog.save(date)" >OK</v-btn > </v-date-picker> </v-dialog> </v-col> </v-row> </v-container> </v-card-text> <v-card-actions> <div class="flex-grow-1"></div> <v-btn color="primary" text @click="dialog = false">キャンセル</v-btn> <v-btn color="primary" text @click="add">登録</v-btn> </v-card-actions> </v-card> </v-dialog> </template> <script> export default { data() { return { title: '', detail: '', dialog: false, date: new Date().toISOString().substr(0, 10), menu: false, modal: false } }, methods: { add() { this.$store.dispatch('task/add', { title: this.title, detail: this.detail, date: this.date }) this.dialog = false } } } </script>リストコンポーネント
次に一覧を表示するタスクの一覧の部分です。
TodoとDoneで利用するのでtitileとtasklistは親から値をもらうようにしています。
チェックボックスを押した時にtoggle、削除アイコンを押した時にremoveを実行します。/components/TaskList.vue<template> <v-row> <v-col cols="12" md="12"> <v-list color="#f4f5fc"> <v-subheader class="font-weight-bold">{{ title }}</v-subheader> <v-col v-for="task in tasklist" :key="task.id" cols="12" class="pt-0"> <v-card> <v-card-title class="headline pb-0"> <v-checkbox :checked="task.status" color="primary" class="ma-0" :label="task.title" @change="toggle(task)" ></v-checkbox> </v-card-title> <v-card-text class="pb-0">{{ task.detail }}</v-card-text> <v-card-actions class="pt-0"> <v-col cols="10" md="10" class="pl-0"> <v-btn text> <v-chip color="grey lighten-3" label> <v-avatar left> <v-icon small color="primary">mdi-calendar</v-icon> </v-avatar> {{ task.date }} </v-chip></v-btn > </v-col> <v-col cols="2" md="2"> <v-btn icon color="grey" text dark @click="remove(task.id)"> <v-icon>mdi-close-circle-outline</v-icon> </v-btn> </v-col> </v-card-actions> </v-card> </v-col> </v-list> </v-col> </v-row> </template> <script> export default { props: { title: { type: String, default: '' }, tasklist: { type: Array, default: null } }, methods: { remove(id) { this.$store.dispatch('task/remove', id) }, toggle(task) { this.$store.dispatch('task/toggle', task) } } } </script> <style> .theme--light.v-label { color: #000; } .v-input--selection-controls:not(.v-input--hide-details) .v-input__slot { margin-bottom: 0px; } .v-application--is-ltr .v-list-item__avatar:first-child { margin-right: 0; } </style>ボードコンポーネント
最後にpagesディレクトリにboard.vueを作成します。
上記で作成した、タスクコンポーネント、リストコンポーネントをimportします。
また新規登録用の処理とcreatedのタイミングでfirebaseの初期化処理を実行します。/pages/board.vue<template> <v-container class="todo"> <v-form ref="form"> <v-row> <v-col cols="12" md="12"> <task-detail></task-detail> </v-col> </v-row> </v-form> <task-list title="Todo" :tasklist="todolist"></task-list> <task-list title="Done" :tasklist="donelist"></task-list> </v-container> </template> <script> import TaskList from '../components/TaskList.vue' import TaskDetail from '../components/TaskDetail.vue' export default { components: { TaskList, TaskDetail }, computed: { todolist() { return this.$store.getters['task/orderdTodos'].filter(function(el) { return el.status === false }, this) }, donelist() { return this.$store.getters['task/orderdTodos'].filter(function(el) { return el.status === true }, this) } }, created() { this.$store.dispatch('task/init') } } </script> <style scoped> .status.done { text-decoration: line-through; } </style>UIの調整
外観の部分を調整するためレイアウトを少しだけ修正しました。
layout/default.vue<template> <v-app dark> <v-app-bar color="#5963F8" fixed app dark> <v-toolbar-title>{{ title }}</v-toolbar-title> </v-app-bar> <v-content> <v-container> <nuxt /> </v-container> </v-content> </v-app> </template> <script> export default { data() { return { title: 'TodoList' } } } </script> <style> .theme--light.v-application { background: #fff; } .v-toolbar__title { font-family: 'Damion', cursive; font-size: 2.5rem; width: 100%; text-align: center; } </style>おわりに
Vueの学習課題としてよく見かけるTodoアプリの作成を、自分が本当に理解できたのか確認するために今回の作業を行いました。編集できるようにしたり、ドラッグ&ドロップできるようにしたりなど意外にやれることは多くて面白い課題だなと感じました。どこかでTrelloやTodoListなどのアプリを参考にして機能を追加していけたらいいなと思っています。
ここまでお読みいただきありがとうございました!!
参考
- 投稿日:2019-10-11T11:39:02+09:00
30分で基礎がしっかりわかる【Vuex】-入門
はじめに
皆さん、こんにちは!Webシステム開発エンジニアの蘭です!
今日は【Vuex】について語りたいと思います。Vuexって何?
Vuexはすべてのコンポーネントでデータを一元管理するための仕組みです。
何故Vuex?
Vueで足りるんじゃない?
アプリを構築している中、最初はコンポーネント内だけでdataを操作してたが、コンポネントが多くなってくると、コンポーネントで共有して使うデータが現れます。その時思いついたのが、Vueの$emitやpropsを使うことでコンポーネント間のデータ共有問題を解決してました。しかしアプリが大きくなるに連れ、コンポーネントが更に多くなり、共有データが300以上に増加、もうこのコンポーネントのデータはどのコンポーネントから持ってきたのかが分からない!
まさに開発に連れ、地獄の道へと進んでしまってたのです。その時に、現れたのがVuexでした。
Vuexっていつ導入すべきなのか?
ここで想像してみましょう。
例えばコンポネントは一つの店だとします。
Aコンポーネントはバナナしか在庫がなく、Bコンポーネントはりんご、Cコンポーネントはスイカ等、それぞれ一種類の果物しか預かってません。それで各コンポーネントが
フルーツパフェを作りたい時に、コンポーネントAはBにりんごを買ってきたり、BはAからバナナを入荷してましたが
、なにかややこしいですよね。
それでVuexの考え方はコンポネントで皆使う共有の果物は全部Storeという多きな倉庫に入れといて
、他の果物が欲しいコンポネントは倉庫から入荷しましょうという改善方法が生まれました。
その倉庫がVuexでの「Store(ストア)」です
。
めでたし、めでたし・Vuexでのデータ管理のイメージ
Vuexってどんな時に使うといいの?
・アプリケーション全体で使用されるデータ→Vuexで管理する
・コンポーネントの内部のみで使用されるデータ→dataオプションで管理する実例:Vuexのシンプルなストア(倉庫)
・以下の例はVuexストア(倉庫)から変数countを取得します。
・コンポーネントからVuexストア共有変数countを
store.state.count
で取得。
・VuexではStoreの共有変数countを直接変更できない為、
・対策:ストア共有変数を変更する関数increment()
をストア内に準備し、コンポーネントではボタンを押した後、store.commit('increment')
経由で共有変数countを間接的に更新する。See the Pen Vuex_Simple_Store_Demo by Uramaya (@uramaya) on CodePen.
要するに:ストアと単純なグローバルオブジェクトの違い
・ストアの状態を直接変更することはできない。明示的にmutationsをコミットすることによってのみストアの状態を変更する。これによりすべての状態変更に追跡可能な記録を残すことが保証される。
Vuexをやってみよう!
Vuexのインストール
・通常開発ではnpmやyarnでインスタンスします。
※前提:Vueのインストール済みvuex_npm_install.npm install vuex --savevuex_yarn_install.yarn add vuex・npmやyarnでvuexをインストールの方、
以下のVuexを明示的に導入が必要。import_Vuex.import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex)・試しであれば、cdnでもOK
vuex_cdn.https://cdnjs.cloudflare.com/ajax/libs/vuex/2.0.0/vuex.jsVuexの概念
以下の内容はこちらを参照しています。
Vuexのストアは構成要素として5つの概念を持つ
【state】, 【mutations】, 【getters】, 【actions】, 【modules】
以下は上記の5つの概念について説明します。1.【state】
・ストアで管理する状態(共有変数、データ)。コンポーネントのdataみたいにデータを保存する場所。
・gettersから参照され、更新はmutationsで行うストアから状態を取り出す`一番シンプルな方法は、算出プロパティ (computed)で取得しまた返すことです。
・こちらは直接'store.state.count'でストア状態を持ってきてますが、
・開発で以下の欠点があります。
モジュールを使うとき、ストアの状態を使っている全てのコンポーネントをインポートしなければなりません。(ストア状態を共有してるモジュールが100個あった場合、地獄に落ちます。)
See the Pen Vuex_Store_Example by Uramaya (@uramaya) on CodePen.
・上記解決方法:ルートコンポーネントに store オプションを指定
これですべての子コンポーネントにストアを共有する事ができます。
index.vuenew Vue({ el: '#app', //ルートインスタンスに store オプションを渡す //★これをすることで、this.$store で各コンポーネントから参照することができます。 store, });See the Pen gOOpGxN by Uramaya (@uramaya) on CodePen.
・
mapState
ヘルパー:算出プロパティ(Computed)で宣伝方法の改善・以下を見るとわかりますが、算出プロパティを全て宣伝するのは冗長です。
Before:元の算出プロパティ(Computed)宣伝方法const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return this.$store.state.count } count2 () { return this.$store.state.count2 } count3 () { return this.$store.state.count3 } ... } }After:MapState使用後.computed: mapState({ count: 'count', // count: state => state.countと同義 count2: 'count2', // count2: state => state.count2と同義 count3: 'count3' // count3: state => state.count3と同義 })・npmやyarnでvuexをインストールの方、魔法の言葉を忘れずに
mapState.import {mapState} from 'vuex'2.【getters】
・state内の状態をもとに算出した値を返す関数が書かれる場所
・stateのデータを加工して表示
・state加工するので、最初の引数はstate
・状態をフィルタリング、カウントした値を返すSee the Pen Vuex_Store_Example_getters by Uramaya (@uramaya) on CodePen.
3.【mutations】
・stateを更新する関数が書かれる場所
・stateの更新はしない
・第一引数は必ずstate, それ以降の引数はpayload
・state状態を更新する際は必ずcommitを使用
See the Pen
Vuex_Store_Example_mutations by Uramaya (@uramaya)
on CodePen.
4.【actions】
・
非同期処理
や外部API通信
する場所
・actionで非同期処理を開始→
実際stateの更新はmutationsをcommitで実行する
⇛actionでstateの更新は行わない
⇛最終的にmutationsにデータをコミットする関数
⇛commitは同期でなければならないActions処理
以下の例でActions処理の過程を見てみよう。
全体の処理の流れとしては、コンポーネントからdispatchでアクションを呼び出し、アクション内で外部APIなどからの非同期処理を行った後、commitでミューテーションを使いステートを更新するという流れとなります。
上記はこちらから引用。・コンポーネントからストアの処理を呼び出すメモ:
MutationsはCommit / Actionsはdispatchコンポーネント内のmethodsにActionsをdispatchで実行.methods: { increment(plus) { //plusは引数 this.$store.dispatch('incrementAsync', plus); this.$store.dispatch('warningAsync'); } },その後、ストアのActionsでcommitを呼び出します.actions:{ incrementAsync({ commit }, plus) { setTimeout(()=>{ alert("これが非同期処理です。"); commit('increment', plus) }, 5000) //非同期で五秒遅らせる事ができる。 }, warningAsync({ commit }) { commit('warning') } },そして、commitでMutationsからステートを変更します.mutations: { //★ここでstate状態を変更する関数を用意 increment (state, plus) { state.total += plus state.warning_show = false }, warning (state) { state.warning = "5秒お待ち下さい。" state.warning_show = true } },それでは実装してみよう。
非同期処理
See the Pen Vuex_Store_Example_actions by Uramaya (@uramaya) on CodePen.
API通信
・今回非同期のAPI通信では、ライブラリ「axios」を使います。
・async関数を使ってみる(必須ではない):async関数:
async関数・メソッドはメソッドの冒頭に async をつけることで、 await が使え、awaitは必ずaxios.get等通信処理が終了し、responseもしくはerrorが返ってきた後に、awaitの処理を実行します。See the Pen ExxVjym by Uramaya (@uramaya) on CodePen.
【state】、【getters】、【mutations】、【actions】を使ってTodoListを作ってみよう!
ここまで読んでいただき、ありがとうございます!
ではモジュールを理解する前に、以下のコードで遊んでみてください。See the Pen Vue + Vuex Demo by Arpit Gupta (@arpitg) on CodePen.
5.【modules】
・上記4つのストア構成要素を分割したもの。
・アプリケーションの肥大化に伴い大きくなるストアに対し、見通しをよく保つためにモジュールに分割する。以下の記事を参考しました。
Vuex公式-モジュール開発に連れ、コードが膨大する中、全て何百個のstate, getters, mutations, actionsを一つのファイルにまとめるのは事実上管理不可能です。
ここで解決法が【モジュール】です。
モジュールとは元々凄く膨大なstateやgetters等を小さなモジュール単位に分割して、管理することです。
もちろん、これで保守、修正、管理が便利になります。モジュールの使い方を見てみよう!
・1.同一ファイルでモジュール分け
同一ファイルでモジュール分け.//モジュールA const moduleA = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } } //モジュールB const moduleB = { state: { ... }, mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ modules: { module_a: moduleA, module_b: moduleB } }) store.state.module_a // -> `moduleA` のステート store.state.module_b // -> `moduleB` のステート・2.違うファイルでモジュール分け(現場)
・モジュールの分け方について以下を参考しました。
Vuex の Modules 機能
vuexでmoduleを分ける方法と注意点・(1)モジュール:「superFunction」と「header」を
storeに登録
/store/index.jsimport Vue from "vue"; import Vuex from "vuex"; import superFunction from "./superFunction"; import header from "./header"; Vue.use(Vuex); const store = new Vuex.Store({ modules: { superFunction, header } }); export default store;・(2)モジュールの指定を明確にする為、
namespaced: true
にすることを忘れずに・namespacedについて以下を引用しました。
Vuexのストアをモジュールに分割する・Vuex名前空間の概念:【namespaced: true】 とは
namespaced:オプションをtrueにすることで、それぞれのモジュールに名前空間を与えて呼び出し方を管理することができる。
ただstateについては、namespaced: trueの有無は関係なく一律で$store.state.(モジュール名).data.messageのように呼び出す。
その他のmutation, action, getterはnamespaced: trueを与えなかった場合はモジュールを使用せずグローバルに登録したときと同じように呼び出せる。
もしnamespaced: trueを与えていない複数のモジュール内で名前が被った場合、mutationとactionはそれぞれが同時に実行される。
getterは以下のようなエラーが発生する。[vuex] duplicate getter key: greetingC.namespaced: trueを与えたmutation, action, getterは、名前の前にモジュール名/を付与して呼び出すことで、上記のエラーを避けられます。
console.log(this.$store.getters['moduleA/greeting'] console.log(this.$store.commit('moduleA/greeting') console.log(this.$store.dispatch('moduleA/greeting') // getterは[], mutationとactionは()で囲むので注意・モジュールを作ってみよう
/store/superFunction/index.jsconst state = { appNumber: 0 }; const getters = { appNumber(state) { return state.appNumber; } }; const actions = { changeNumber({ commit }, val) { commit("changeNumber", val); } }; const mutations = { changeNumber(state, value) { state.appNumber = state.appNumber + value; } }; const superFunction = { namespaced: true, // 忘れずに state, getters, actions, mutations }; export default superFunction; //モジュールの名前・(3)コンポネントでモジュール:superFunctionを使う
以下の注意点:
・getter と action の参照の仕方が変わる
・this.$store.getters["moduleName/getterName"]
・this.$store.dispatch("moduleName/actionName")/components/AppMain.vue<template> <main> {{appNumber}} <Controller :changeNumber="changeNumber"/> </main> </template> <script> import Controller from "./Controller"; export default { components: { Controller }, computed: { appNumber() { return this.$store.getters["superFunction/appNumber"]; } }, methods: { changeNumber(val) { this.$store.dispatch("superFunction/changeNumber", val); } } }; </script>・上記の実際のソースコードを見て試してみよう【Codesandbox】
https://codesandbox.io/embed/w67wklz0pk?fontsize=14おまけに
・npmやyarnでVuexをインストールした方は、必ずuseでVuexの参照をしてください。
/store/index.jsimport Vue from 'vue' import Vuex from 'vuex' import App from './App.vue' Vue.use(Vuex); //忘れずに const store = new Vuex.Store({ ...略...・Vuexで開発する際に、以下の記事も参考にできればと思います。
Vuexを用いた開発プロジェクト用にガイドラインを作成した話
メンテナンスしやすいVueComponentを設計するために気をつけていること・VueとVuexの練習
30minくらいで学ぶVue.jsとVuex
簡単なTODOアプリで Vue + Vuex を学んでみよう
簡単なTODOアプリソースコード(Github)・Vueの基本概念
10分で基礎がわからるVue.js-入門最後に
今回はVuexについて基本の使い方を紹介しました。
自分のニーズに沿って、是非Vuexを開発で使って見てください!:D
- 投稿日:2019-10-11T11:21:11+09:00
Heroku(Python3)+Auth0+GitHub Pages(Vue.js)で大学の関係者だけが見れるページをサクッと(?)作る。
前作の話
こちらは前に書き、なんとトレンドに乗っかってしまった記事です。
Firebase+Vue.jsで大学の関係者だけが見れるサイトをサクッと作る - Qiita
この投稿からなんと5ヶ月が経ちました。
いろいろ学びを深めたのでそのアウトプットがてら、この記事を持ちましてリベンジをさせて頂きたいと思います。
概要
学生に配布されるメールアドレスのドメインを見て、同じ大学のメールアドレスのドメインの場合だけ見れるようなサイト(ページ)を作りたい!
使う技術・サービス・言語
- github.io
- 静的サイトをホスティングしてくれるサービス。GitHubアカウントがあれば利用できます。
- Auth0
- 認証プラットフォーム、〇〇でログインとか簡単に実装できてすごく便利。無料でもそれなりに遊べます。
- 今回の記事はAuth0のRulesが便利すぎて凄いぞと思って生まれています。
- Heroku
- アプリを実行してくれるプラットフォーム、いわゆるPaaS。今回はAPIサーバ扱い
- Python3
- ここなら割とお馴染みプログラミング言語、機械学習が熱い(らしい)
- Vue.js (JavaScriptフレームワーク)
動作環境
- macOS Catalina 10.15 Beta
- Python3.7.3
- nodejs v10.15.3
- npmは v6.4.1
動作イメージ
- ログインの処理をAuth0にぶん投げます。
- アクセストークン の検証をHeroku(Backend,Python)で
適当にやって、正しい形式なら秘密の情報を返します。- フロントエンドは受け取った情報を良い感じにして表示します。(今回はGitHub Pagesにデプロイします。)
この手順をサクッと進めていこう! と言う企画。
かなり雑ですがこんな感じ
作成手順
この記事では以下の手順で実装していきます。
- Auth0側設定
- Herokuのアカウント作成、アプリ作成、Python3でのコード作成、デプロイ(すなわちバックエンドの構築)
- フロントエンドアプリの作成
- デプロイ
- 大学のメールドメインのバリデーション
Auth0側設定
ここにアクセスしてサインアップしましょ
Never Compromise on Identity. - Auth0サインアップをすると、テナントドメインを決めてくださいと言う旨のメッセージが出ます。
でこの辺はお好きに。
完了したらダッシュボードに移動して、
CREATE APPLICATOIONを選択して、作成画面に移行。
アプリ名とかお好きに、application typeはSingle Page Web Applications に設定してください。
こんな感じで作成します。
Auth0のテナント名はメモっておいてください。
<ここ>.auth0.com
です。次に、Googleログインを無効にします。(今回はメアド+パスワードのみの許可にしたいので)
出来たら、Applicationsから自分の作成したアプリを選択しましょう
そしてタブからConnectionsを選択
そしてこんな感じにgoogle-oauth2を無効にします。
ここまできたら完了です。お疲れ様です。
バックエンド環境の構築
Herokuアカウントの作成
ここにアクセスしてサインアップしましょう。
アプリの作成
アカウント作ったらこんな感じの画面になるはず
Create New Appを選んでApp nameやRegionを設定。この辺はお任せします。
今回はこんな感じにしました。
デプロイ準備
アプリ作ったらいろいろ出てきますが、まずはHeroku CLIだけインストールしましょう。(HerokuのデプロイとかをCLIからやってくれるツールです。)
$ brew tap heroku/brew && brew install herokuインストールできたら、
$ heroku login
でログインをしましょう。
コーディング
とりあえず、こんな感じの要件で作るとします。
・ アクセストークンが正しければ、JSON形式で{ "himitu": "ゴニョゴニョ"} ・ アクセストークンが変だったら403を返す。大体こんな感じの機能で作ります。
今回はAPIフレームワークとしてFastAPIを使います。
かなり新しめのフレームワークです。
理由としてはAPIサーバ作るなら一番シンプルだったり、今回は関係ないですがSwagger UIで自動的にdocumentを生成してくれたりで学習コストが軽量な割りに凄く便利で気に入ったからです。 ついでに僕は新しい物好きなので
で、Heroku+FastAPIってことをしているサンプルコード探していたのですが、ないので作りました。 今回はこれを弄くり回して使いましょう。
reud/fast-api-heroku-sample: Python-Server + Heroku Sample
forkしてcloneとかしてください。
で、gitリポジトリできたらとりあえず、プロジェクトフォルダ直下で
$ heroku git:remote -a アプリ名
でリポジトリとherokuアプリの結び付けを行います。結び付けが出来たら、githubとかにpushする感覚と同様にaddとか色々し終わってるのを確認してから、
git push heroku master
でdeployが完了します。(手順わからない場合は参考資料が凄く役に立つはずなのでそちらをご覧ください)
僕の場合はApp nameの関係で、
https://himitu-api-server.herokuapp.com/
にデプロイされます。他の人は
https://<App name>.herokuapp.com/
にデプロイされると思います。ブラウザでアクセスしてみてコード通りの動作している事を確認してください。
多分こんな感じでJSONがブラウザに表示されます。
で、ここからコードを弄りましょう。
その前に
今回はAuth0からアクセストークンを貰う形式ですが、何に対して発行するかで、やることが変わってきます。
今回はベストなやり方としては、APIサーバを作っている以上Auth0にカスタムAPIを作成して、「このカスタムAPIをください!」ってaudienceを使用して教えて上げる必要があります。(audienceはどのリソースサーバを使用するか決定するために指定します。)
すなわち、Auth0にURL (https://himitu-api-server.herokuapp.com/) でカスタムAPIを作成するのが一番正しいはずです。
しかし、カスタムAPIを作成すると手間がまぁ増えます。
参考: Validate an Access Token - Auth0
端的に言うと、カスタムAPIでアクセストークンを得た場合、トークン検証が必要になります。アクセストークンはJWTと言われる形式で渡されるのでそれをゴニョゴニョしてJWKSを照らし合わせて・・・ってやるのですが、それだけで1記事出来てしまうのと、まだ勉強不足なので今手を出すとこの記事が未完になりそうなため少し手抜きをします。
取得するAuth0のアクセストークンのaudienceをManagement APIにします。こうした場合はJWTの検証は不要になります。
Management APIはユーザの作成や削除などAuth0の全体(テナントって呼ばれています。)を管理するAuth0のAPIです。
Auth0のAPIを発行しているためAuth0がトークンの検証を担保するイメージですね。(これを利用して検証をサボります。)
そんな激ヤバなトークンを発行して良いのか、トークン抜き出されたら終わりではって思うかもしれませんが、その辺も大丈夫です。
さらにSPA側でManagement API アクセストークンを発行すると、勝手にscopeに制限がかかり、出来ることが大幅に減ります。
参考: Get Management API Tokens for Single-Page Applications -Auth0
上記サイトを見る限り、他のユーザに干渉出来るようなscopeは一つも貰えず、良い感じに渡しても良いscopeしか渡さなくなります。
今回は、scopeにread:current_userを入れて、バックエンドはSPAから受け取ったアクセストークン でManagement APIを叩いて、上手く行ったらOKということにしちゃいましょう。
コードを書く
とりあえず、今回はリクエストからAuthorizationヘッダーの値をぶっこ抜いて、得られた文字列をManagenement APIに投げて、上手く行ったらOKみたいな感じにしたいと思います。
まずは追加でパッケージのインストール、
pip install starlette pip install requestsコードを書きましょう。
main.py
にこんな感じのコードをつらつらと、import traceback import requests from fastapi import FastAPI, HTTPException from starlette.requests import Request from starlette.middleware.cors import CORSMiddleware import os app = FastAPI() app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, # 追記により追加 allow_methods=["*"], # 追記により追加 allow_headers=["*"] # 追記により追加 ) @app.get("/") def read_root(): return {"200": "Welcome To Heroku"} @app.get("/himitu/{user_id}") def read_item(user_id: str, request: Request): k = request.headers.get('Authorization') if not k: raise HTTPException(status_code=403, detail="forbidden") headers = {'Authorization': k} # 適宜読み替えてください try: r = requests.get(f'https://{ os.getenv("TENANT") }.auth0.com/api/v2/users/{user_id}', headers=headers) except: raise HTTPException(status_code=403, detail=traceback.print_exc()) j = r.json() try: # checker n = j['nickname'] return {'himitsu': os.getenv('HIMITSU')} except: raise HTTPException(status_code=403, detail='forbidden')で、
$ heroku config:set HIMITSU="<あなたの秘密のメッセージ>" TENANT="<あなたのAuth0 テナント名>"
をして、Herokuに環境変数を設定します。
Postmanやcurlなどのツールでリクエストを送ってみて正常に動作したらバックエンドの構築は完了!!お疲れ様です。
フロントエンドの環境構築
サクッと作るためにAuth0のサンプルを持ってくるとします。
これをforkしてcloneしちゃいましょう。
upgrade-sdkブランチに切り替え、ざっとこんな感じに直してください。(URLはよしなに・・・)
デプロイ先は
https://<GitHub ユーザ名>.github.io/auth0-vue-samples/
になるはず。update · reud/auth0-vue-samples@b73103a
diffにも出ちゃってますが、デプロイに
gh-pages
を使用するので、
npm install gh-pages --save-dev
でインストールしちゃいましょう。
色々編集し終えたら、
npm run build
からのgh-pages -d dist
でアップロードされます。アップロード後は
https://<GitHub ユーザ名>.github.io/auth0-vue-samples/
にアクセスして、動作するか確認してみましょう。 アクセス後、画面右上LOGINからユーザを作成して、ログインしてみます。ログインしてこんな感じの画面になればおkです。(Vue.jsのロゴは読み込まれていませんが、今回は直さないまま進めちゃいます。)
大丈夫そうなら、上記ナビゲーションバーから秘密のページに移動して、Call APIをクリックして、バックエンドとの疎通を試みます。
こんな感じでうまくいけば完了です!
大学の関係者のみ見れる様にする
ここまででログイン時のみ見れる特別なページを作る事が出来ました。
しかし今のままでは大学の関係者だとか関係なく、メールアドレスを持っていれば、ページがみれてしまいます。そこで、メールアドレスが大学のドメインでない場合は、ログインに失敗させる様にしましょう。
Auth0のダッシュボードにあるサイドバーからRulesを選択
大学のメールドメインでない場合は弾く様に設定
CREATE RULEを選ぶ。
テンプレート一覧が表示されるので、
Check if user email domain matches configured domain
を選択。そして中身をこんな感じに編集
function (user, context, callback) { const allowedDomains = new Set(['stu.hoge.ac.jp']); // Access allowed if domain is found const emailSplit = user.email.split('@'); const userEmailDomain = emailSplit[emailSplit.length - 1].toLowerCase(); if (allowedDomains.has(userEmailDomain)) return callback(null, user, context); return callback('Access denied you are'+userEmailDomain+" but, expected stu.hoge.ac.jp"); }これはログイン時のメールアドレスが、
stu.hoge.ac.jp
でない場合はログイン失敗させるというRuleです。stu.hoge.ac.jp
をいじって自分の大学のメールドメインに変更してください。メールの確認されていない場合は弾く様に設定
デフォルトでは、メールアドレスを確認しなくてもそのままログインできてしまいます。
すなわち、サインアップ時にドメインだけ正しければ、適当なメールアドレスを入れてしまえば秘密のページが見れてしまう事になります。
これは流石によくないので、メールアドレスの確認を強制させましょう。 これもAuth0のRuleで上手くやってくれます。
先ほどと同じ様にCREATE RULEを選んで、Force email verificationを選択、今回は何も編集せずにSAVE CHANGESで大丈夫です。
動作を確認する。
(ログイン失敗時などはつどCookieを削除してみてください)
今回は以下の部分において手抜きをしているため、正常に動作しません。
- ナビゲーションバーの推移先(URLの設定が上手くいっていないため)
- ログイン・サインアップ失敗時(URLみるとログイン・サインアップ失敗理由が書いてありますが、Vue側は何もしていないので何も出てきません。また、その状態でLOGINボタンを再度押してもログイン失敗してしまうため、Cookieを手動で削除する必要があります。)
また、以下の部分は甘いです。
- Heroku側のCORS設定(もっと絞り込む必要あり)
- カスタムAPIを使用していない(ユーザ毎に権限を振り分けたい時は)(このままだとあまり認可っぽくない)
- 今回はJWT検証サボりたいためこんな感じの実装にしていますが、ちゃんとしたサービス作るならカスタムAPIにすべきかと
もっと洗い出せば沢山出てきそうですが、とりあえずこの辺り。
所属大学の学生を対象としたサービスなどを作ろうと考えている際はこの辺りを気を付けつつこの記事を見れば役に立つのかもしれません。
以上です。ありがとうございました。
参考資料
- 認証プラットフォーム Auth0 とは? - Qiita
- 【Python】PythonプログラムをHerokuにデプロイする方法 - Qiita
- JWT Authentication with FastAPI and AWS Cognito - Data Driven Investor - Medium
- Why is it necessary to pass the 'audience' parameter to receive a JWT? - Auth0 Community
- Get Management API Tokens for Single-Page Applications
- FastAPIでCORSを回避 - Qiita
- 投稿日:2019-10-11T10:15:50+09:00
GoogleのOpenIDを使ったログインの実装
概要
OpenIDについて調べたので、実際にGoogleのOpenIDを利用してのログインの実装方法を調べました。
採用技術
- Vue.js
- Ruby on Rails
- GoogleSignIn
実装
クライアント
クライアントはVue.jsで作っていきます。Vue.jsについての説明は省略します。
今回はImplicit Flowでのログインを行いたいので、GoogleSignInを利用します。実装方法はこちらで説明されています。
ただ、今回Vue.jsを利用するので、そのまま利用はできませんでした。
概要は以下のようなものです。index.htmlの<div id="app"></div>
にApp.jsが描画されると思ってください。index.html
<!DOCTYPE html> <html lang="en"> <head> <script src="https://apis.google.com/js/platform.js"></script> <title>app</title> </head> <body> <div id="app"></div> </body> </html>App.js
<template> <div> <div v-if="!signedIn" id="google-signin-button"></div> <a href="#" @click="signOut" v-if="signedIn">Sign out</a> </div> </template> <script> export default { name: 'app', data() { return { signedIn: false } }, mounted() { this.renderSignInButton(); }, methods: { renderSignInButton() { gapi.load("auth2", (signin2) => { gapi.auth2.init({ client_id: 'YOUR_CLIENT_ID.apps.googleusercontent.com' scope: 'profile email', hosted_domain: 'YOUR_DOMAIN' // ドメインを限定したい場合 }); gapi.signin2.render('google-signin-button', { onsuccess: this.onSignIn, }) }); }, onSignIn(googleUser) { this.signedIn = true; }, } } </script>GoogleSignInの実装サンプルでは
class="g-signin2"
としているところにボタンを描画してくれるんだと思います。ただ、App.jsの中身が描画されるのが間に合わないらしく、そのままではGoogleSignInボタンを表示してくれませんでした。なのでここを参考にmountedでボタンを描画してます。API側実装
クライアントからAPIへリクエストするときはAuthorizationヘッダーでIDTokenを渡し、APIはそのIDTokenを検証することでリクエストの認証を行います。
以下では自分のプロフィール情報を取得するAPIを作成します。クライアントからのリクエストはこんな感じ(apiはlocalhost:3000で起動しているものとします。)
var auth2 = gapi.auth2.getAuthInstance(); var idToken = auth2.currentUser.get().getAuthResponse().id_token; fetch("http://localhost:3000/my/profile", { headers: { Authorization: `Bearer ${idToken}` } }).then((res) => { return res.json(); }).then((json)=>{ console.log(json); });API側ではとりあえずapplication_controllerに認証処理を記述します。
認証処理
class ApplicationController < ActionController::API before_action :verify_id_token def verify_id_token return false unless request.headers['Authorization'].present? # ①IDTokenを取り出してデコード id_token = request.headers['Authorization'].gsub(/Bearer /, '') decoded_token = JWT.decode id_token, nil, false # ②Googleから公開鍵情報を取得 res = Faraday.get('https://www.googleapis.com/oauth2/v3/certs') keys = JSON.parse(res.body)['keys'] key = keys.find { |item| item['kid'] == decoded_token[1]['kid'] } # ③公開鍵情報から公開鍵作成 exponential = OpenSSL::BN.new(Base64.urlsafe_decode64(key['e']), 2) modulus = OpenSSL::BN.new(Base64.urlsafe_decode64(key['n']), 2) public_key = OpenSSL::PKey::RSA.new.set_key(modulus, exponential, nil).public_key # ④ruby-jwtでIDTokenを検証 raise JWT::VerificationError if decoded_token[0]['hd'] != 'YOUR_DOMAIN' @id_token = JWT.decode id_token, public_key, true, aud: "YOUR_CLIENT_ID.apps.googleusercontent.com", iss: "accounts.google.com", verify_aud: true, verify_iss: true, algorithm: 'RS256' rescue JWT::DecodeError => exception # ログ出力などなど end def current_user return unless @id_token @current_user ||= User.find_or_create_by(google_user_id: @id_token[0]['sub']) end def authenticate! render status: :forbidden unless current_user.present? end end②Googleから公開鍵情報を取得
IDTokenを検証するための公開鍵を取得します。
公開鍵がどこにあるかというと、こちらで説明されています。以下のURLからOpenIDConnectの情報が取れるみたいです。https://accounts.google.com/.well-known/openid-configurationこの
/.well-known/openid-configuration
ですが、OpenIDConnectの仕様にも記載されているので、他のOpenIDプロバイダーを利用する際もこんなURLで公開されているんだと思います。この情報から、公開鍵は以下のURLにあるとわかります。
https://www.googleapis.com/oauth2/v3/certs④ruby-jwtでIDTokenを検証
基本的にはruby-jwtがいい感じで検証してくれます。
以下の3つは必ず検証するよう記載されていました。
- iss(accounts.google.com)
- aud(プロジェクトID)
- exp
expは特にコード上書いていませんが、ruby-jwtがチェックしてくれるみたいです。
また、ドメインを限定したい場合は、hd
にドメインが記載されているのでこちらもチェックすると良いと思います。アクションに認証をかける
あとは必要なコントローラーで使うだけです。
app/controllers/my/profiles_controller.rb
class My::ProfilesController < ApplicationController before_action :authenticate! def show render json: current_user end end終わりに
上記コードは認証の概要を理解するためにかなり簡略なもので終わらせています。
公開鍵をキャッシュしたり、検証済みのIDTokenをキャッシュしておいたりなど改善点はいっぱいあるとは思います。
ですがとりあえずかなり便利そうだし、わりと簡単に使えるということはわかりました。
- 投稿日:2019-10-11T01:28:29+09:00
Vueでよく書くユニットテストのパターン
Vueのコンポーネントのユニットテストでよく書くパターンを紹介します。
環境はvue-cliで生成したものをそのまま使っています。APIの詳細な説明はVue Test Utilsを参照してください。
スナップショットテスト
出力されるHTMLが予期せず変更されないようにする場合に使うテストです。
GachaMv.spec.tsit("itemsの値がhtmlに出力されているか?", () => { const items = [ new Item("アイテム", 3, "アイテム説明") ]; const wrapper = shallowMount(GachaMv, { propsData: { items } }); expect(wrapper.html()).toMatchSnapshot(); });上記のテストを追加し
npm run test:unit
oryarn test:unit
を実行するとテストファイルと同階層に__snapshots__/GachaMv.spec.ts.snap
が出力されます。GachaMv.spec.ts.snapexports[`GachaMv.vue itemsの値がhtmlに出力されているか? 1`] = ` <div class="gacha-mv"> <slick-stub options="[object Object]"> <div class="gacha-mv-list"> <p class="gacha-mv-list-name">アイテム</p> <p class="gacha-mv-list-rare">3</p> <p class="gacha-mv-list-description">アイテム説明</p> </div> </slick-stub> </div> `;shallowMountではなくmountを使用した場合は、以下のように子コンポーネントも展開した状態で出力されます。
GachaMv.spec.ts.snapexports[`GachaMv.vue itemsの値がhtmlに出力されているか? 1`] = ` <div class="gacha-mv"> <div class="slick-initialized slick-slider"> <div class="slick-list draggable"> <div class="slick-track" style="opacity: 1; width: 0px; transform: translate(0px, 0px);"> <div class="slick-slide slick-current slick-active" data-slick-index="0" aria-hidden="false" style="margin-left: 0px; width: 0px;"> <div> <div class="gacha-mv-list" style="width: 100%; display: inline-block;"> <p class="gacha-mv-list-name">アイテム名</p> <p class="gacha-mv-list-rare">3</p> <p class="gacha-mv-list-description">アイテム説明</p> </div> </div> </div> </div> </div> </div> </div> `;以下のようにshallowMountとstubsを組み合わせることで、一部のコンポーネントのみスタブすることも可能です。
GachaMv.spec.ts.snapconst wrapper = shallowMount(GachaMv, { stubs: { 'slick': Slick, }, }computedが期待した値を返しているか?
computedが正しい値を確認しているかのテストです。
GachaMv.spec.tsit("pickupItemが期待通りか?", async() => { const items = [ new Item("アイテム名3", 3, "アイテム説明3"), new Item("アイテム名4", 4, "アイテム説明4"), new Item("アイテム名5", 5, "アイテム説明5"), ]; const wrapper = shallowMount(GachaMv, { propsData: { items } }); expect((wrapper.vm as any).pickupItems).toEqual([new Item("アイテム名5", 5, "アイテム説明5")]); });クリックイベントが動作しているか?
要素がイベントを発火した時に、定義した関数が実行されているかのテストです。
スタブにsinonを使ってます。GachaPlay.spec.tsit(".gacha-playクリック時にonPlayGachaが実行されるか?", () => { const onPlayGachaStub = sinon.stub(); const wrapper = shallowMount(GachaPlay, { propsData: { number: 1, onPlayGacha: onPlayGachaStub } }); wrapper.find(".gacha-play").trigger('click'); expect(onPlayGachaStub.called).toBe(true); onPlayGachaStub.restore(); });sinon or jest.fn
先ほどのクリックイベントのテストでsinonを使いましたが、jest.fnでも同じことができます。
個人的は使い慣れているsinonを使用しています。unit testing - stubbing a function using jest - Stack Overflow
非同期のテスト
非同期テストでは、flush-promisesやVue.nextTickを使わないと期待した結果が得られません。
flush-promisesについて
保留中の解決済みの約束のハンドラをすべてフラッシュします。
やっていることはmicrotasks または macrotasksのプロミスを返しているだけです。(setImmediate関数がある場合はmicrotasksが選択されるので、これがDOMの更新を待たなかった原因かもしれません。)
microtasks、macrotasksについは以下の記事が分かりやすかったです。Tasks, microtasks, queues and schedules - JakeArchibald.com
Vue.nextTickについて
callbackを延期し、DOMの更新サイクル後に実行します。DOM更新を待ち受けるために、いくつかのデータを変更した直後に使用してください。
テスト記述例
VueのDOMの更新後の値をテストする場合はnextTick、非同期処理(Promiseをfulfilled)する場合はflushPromisesを使用します。
it('test', async() => { const wrapper = shallowMount(Foo); await flushPromises(); // マウント時などに非同期処理がある場合 expect(wrapper.vm.text).toBe('マウント時のテキスト'); // expect(wrapper.find('text').text()).toBe('マウント時のテキスト'); // DOMが更新される保証はないので、Errorになるかも wrapper.find('button').trigger('click'); // クリックイベントで非同期処理を実行 await wrapper.vm.$nextTick(); // DOMの更新を保証 expect(wrapper.find('text').text()).toBe('クリック後のテキスト'); })さいごに
簡単なコンポーネントのテストであれば、これらの組み合わせで十分かと思います。
今回のユニットテストではUIの崩れなど検知できないので、以下のリンク先で紹介されているようなstorybookやvisual regressionテストなどの導入を検討してみてください。Storybookとvue-i18nで多言語確認を容易にしよう - スタディスト開発ブログ - Medium
Storybookとreg-suitで気軽にはじめるVisual Regression Testing - wadackel.me
- 投稿日:2019-10-11T00:12:47+09:00
Vue.jsのv-if,v-show 要素の表示非表示
要素の表示非表示を切り替える
js
var app = new Vue({ el: '#app', data: { toggle: true } })html
<div id='app'> <p v-if=toggle> Hello </p> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16"></script>表示結果
jsのtoggleをfalseにすると非表示になる。
var app = new Vue({ el: '#app', data: { toggle: false } })v-showでfalseの場合はcssでdisplay:noneとなる。
v-ifの場合はdomレベルで削除となる。