- 投稿日:2020-03-30T23:58:09+09:00
Laravel7.xにVue-Routerを実装してみる
Vue-Routerのインストール
npm install vue-router
package.jsonに "vue-router"が追加されたことを確認
package.json"dependencies": { "bootstrap": "^4.4.1", "bootstrap-vue": "^2.7.0", "cross-env": "^7.0.2", "vue": "^2.6.11", "vue-router": "^3.1.6" }Vue-Routerを追加
・app.jsでVue-Routerをインポートして、VueRouterを定義する
・ラウトで表示する「Report / Formページ」も作成resources/js/app.jsrequire('./bootstrap'); // Bootstrap-vueの実装 import Vue from 'vue' import VueRouter from 'vue-router' import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' Vue.use(VueRouter) import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' window.Vue = require('vue'); import ReportPage from './pages/report.vue' import FormPage from './pages/form.vue' import NavVar from './components/navbar.vue' Vue.component('navigation-bar', NavVar) const router = new VueRouter({ mode: 'history', routes: [ { path: '/', name: 'report-page', component: ReportPage }, { path: 'form', name: 'form-page', component: FormPage } ] }); const app = new Vue({ el: '#app', router });ラウト先の内容を表示するコンポーネントを作成
router-viewの作成
resources/views/welcome.blade.php……省略…… <router-view></router-view> ……省略……VueRouterで定義したページを作成
resources/js/pages/report.vue<template> <div class="container"> <div class="row justify-content-center"> <div class="col-12"> <div class="card"> <div class="card-header">{{ greeting }}</div> <div class="card-body"> Report Component </div> </div> </div> </div> </div> </template> <script> module.exports = { data: function() { return { greeting: 'hello' } } } </script>resources/js/pages/form.vue<template> <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">{{ greeting }}</div> <div class="card-body"> Form Component </div> </div> </div> </div> </div> </template> <script> module.exports = { data: function() { return { greeting: 'hello' } } } </script>・各ページの入り口になるナビゲーションバーの作成
・<router-link>がVue-Routerでの<a>タグになるresources/js/components/navbar.vue<template> <nav class="navbar navbar-expand-lg fixed-top navbar-light" style="background-color: #e3f2fd;"> <a class="navbar-brand" href="#" style="font-weight:bold;">自己管理ツール</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav"> <li class="nav-item active"> <router-link class="nav-link" :to="{ name: 'report-page' }">レポート <span class="sr-only">(current)</span></router-link> </li> <li class="nav-item"> <router-link class="nav-link" :to="{ name: 'form-page' }">記録</router-link> </li> </ul> </div> </nav> </template>結果確認
→ Laravelアプリ内でのラウトではないため、404エラーが出力されるのが当然
今後対応してみよう!
- 投稿日:2020-03-30T23:58:09+09:00
Laravel7.xにVue-Routerを実装してSPAを実装してみる
Vue-Routerのインストール
npm install vue-router
package.jsonに "vue-router"が追加されたことを確認
package.json"dependencies": { "bootstrap": "^4.4.1", "bootstrap-vue": "^2.7.0", "cross-env": "^7.0.2", "vue": "^2.6.11", "vue-router": "^3.1.6" }Vue-Routerを追加
・app.jsでVue-Routerをインポートして、VueRouterを定義する
・ラウトで表示する「Report / Formページ」も作成resources/js/app.jsrequire('./bootstrap'); // Bootstrap-vueの実装 import Vue from 'vue' import VueRouter from 'vue-router' import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' Vue.use(VueRouter) import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' window.Vue = require('vue'); import ReportPage from './pages/report.vue' import FormPage from './pages/form.vue' import NavVar from './components/navbar.vue' Vue.component('navigation-bar', NavVar) const router = new VueRouter({ mode: 'history', routes: [ { path: '/', name: 'report-page', component: ReportPage }, { path: 'form', name: 'form-page', component: FormPage } ] }); const app = new Vue({ el: '#app', router });ラウト先の内容を表示するコンポーネントを作成
router-viewの作成
resources/views/welcome.blade.php……省略…… <router-view></router-view> ……省略……VueRouterで定義したページを作成
resources/js/pages/report.vue<template> <div class="container"> <div class="row justify-content-center"> <div class="col-12"> <div class="card"> <div class="card-header">{{ greeting }}</div> <div class="card-body"> Report Component </div> </div> </div> </div> </div> </template> <script> module.exports = { data: function() { return { greeting: 'hello' } } } </script>resources/js/pages/form.vue<template> <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">{{ greeting }}</div> <div class="card-body"> Form Component </div> </div> </div> </div> </div> </template> <script> module.exports = { data: function() { return { greeting: 'hello' } } } </script>・各ページの入り口になるナビゲーションバーの作成
・<router-link>がVue-Routerでの<a>タグになるresources/js/components/navbar.vue<template> <nav class="navbar navbar-expand-lg fixed-top navbar-light" style="background-color: #e3f2fd;"> <a class="navbar-brand" href="#" style="font-weight:bold;">自己管理ツール</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav"> <li class="nav-item active"> <router-link class="nav-link" :to="{ name: 'report-page' }">レポート <span class="sr-only">(current)</span></router-link> </li> <li class="nav-item"> <router-link class="nav-link" :to="{ name: 'form-page' }">記録</router-link> </li> </ul> </div> </nav> </template>結果確認
→ Laravelアプリ内でのラウトではないため、404エラーが出力されるのが当然
今後対応してみよう!
- 投稿日:2020-03-30T23:58:09+09:00
Laravel7.xにVue-Routerを実装してSPAを実装してみる (1)
LaravelアプリにVue-Routerを実装してみる
Vue-Routerのインストール
npm install vue-router
package.jsonに "vue-router"が追加されたことを確認
package.json"dependencies": { "bootstrap": "^4.4.1", "bootstrap-vue": "^2.7.0", "cross-env": "^7.0.2", "vue": "^2.6.11", "vue-router": "^3.1.6" }Vue-Routerを追加
・app.jsでVue-Routerをインポートして、VueRouterを定義する
・ラウトで表示する「Report / Formページ」も作成resources/js/app.jsrequire('./bootstrap'); // Bootstrap-vueの実装 import Vue from 'vue' import VueRouter from 'vue-router' import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' Vue.use(VueRouter) import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' window.Vue = require('vue'); import ReportPage from './pages/report.vue' import FormPage from './pages/form.vue' import NavVar from './components/navbar.vue' Vue.component('navigation-bar', NavVar) const router = new VueRouter({ mode: 'history', routes: [ { path: '/', name: 'report-page', component: ReportPage }, { path: 'form', name: 'form-page', component: FormPage } ] }); const app = new Vue({ el: '#app', router });ラウト先の内容を表示するコンポーネントを作成
router-viewの作成
resources/views/welcome.blade.php……省略…… <router-view></router-view> ……省略……VueRouterで定義したページを作成
resources/js/pages/report.vue<template> <div class="container"> <div class="row justify-content-center"> <div class="col-12"> <div class="card"> <div class="card-header">{{ greeting }}</div> <div class="card-body"> Report Component </div> </div> </div> </div> </div> </template> <script> module.exports = { data: function() { return { greeting: 'hello' } } } </script>resources/js/pages/form.vue<template> <div class="container"> <div class="row justify-content-center"> <div class="col-md-8"> <div class="card"> <div class="card-header">{{ greeting }}</div> <div class="card-body"> Form Component </div> </div> </div> </div> </div> </template> <script> module.exports = { data: function() { return { greeting: 'hello' } } } </script>・各ページの入り口になるナビゲーションバーの作成
・<router-link>がVue-Routerでの<a>タグになるresources/js/components/navbar.vue<template> <nav class="navbar navbar-expand-lg fixed-top navbar-light" style="background-color: #e3f2fd;"> <a class="navbar-brand" href="#" style="font-weight:bold;">自己管理ツール</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarNav"> <ul class="navbar-nav"> <li class="nav-item active"> <router-link class="nav-link" :to="{ name: 'report-page' }">レポート <span class="sr-only">(current)</span></router-link> </li> <li class="nav-item"> <router-link class="nav-link" :to="{ name: 'form-page' }">記録</router-link> </li> </ul> </div> </nav> </template>結果確認
→ Laravelアプリ内でのラウトではないため、404エラーが出力されるのが当然
今後対応してみよう!
- 投稿日:2020-03-30T23:23:36+09:00
mutationsについて学んだことの備忘録
mutationsについて
Vuexはグローバル変数なので、状態を変えると、stateを使っている箇所全てに影響する。
例えばこの以下の部分はデータの追跡や予測をしにくくしている
counter.jsthis.$store.state.count++これは、ぱっと見、データを変えているのか参照しているのかがわからない
mutationsを使わなくても、データを変えることはできけど
データの追跡や予測をしやすくするために、mutationsでしかstateを変えないようにした方が良いらしいです。mutationsの書き方
counter.jsexport default new Vuex.Store({ state:{ count:2 }, getters:{ doubleCount: state => state.count * 2, tripleCount: state => state.count * 3 }, //mutationsでしかstateを変えれないようにする mutations: { //第一引数はstate,第二引数は適当でいいけど今回はnumber increment(state,number){ state.count += number; }, decrement(state,number){ state.count -= number; } });double.countやtripleCountを使用している部分はどのように書くかというと
Header.vue<template> <div> <button @click="increment">*2</button> <button @click="decrement">-1</button> </div> </template> <script> export default { methods:{ increment(){ //mutationsを用いいない書き方だと、 //this.$store.state.count++; //引数(今回は2)もとることができる this.$store.commit('increment',2); }, decrement(){ this.$store.commit('decrement',2); } } }; </script>このように、stateを変える部分をこの場所一つに絞ることで、わかりやすくなります!
- 投稿日:2020-03-30T22:42:16+09:00
Docker環境でRailsアプリにvue.jsを導入する
目的
railsアプリにvue.jsを導入するために、練習もかねてサンプルのアプリにvueを導入してみたいと思う。
サンプルアプリのディレクトリ作成
任意のワークスペースでディレクトリを作成します。
ターミナルmkdir sample_app cd sample_appDockerの設定ファイルを作成
DockerfileFROM ruby:2.5.3 RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && apt-get update && \ apt-get install -y nodejs --no-install-recommends && rm -rf /var/lib/apt/lists/* RUN apt-get update -qq && apt-get install -y build-essential libpq-dev RUN apt-get update && apt-get install -y curl apt-transport-https wget && \ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ apt-get update && apt-get install -y yarn RUN curl -sL https://deb.nodesource.com/setup_8.x | bash - && \ apt-get install nodejs RUN yarn add node-sass RUN mkdir /app WORKDIR /app COPY Gemfile /app/Gemfile COPY Gemfile.lock /app/Gemfile.lock RUN bundle install COPY . /appdocker-compose.ymlversion: '3' services: web: build: . command: bundle exec rails s -p 3000 -b '0.0.0.0' volumes: - .:/app ports: - 3000:3000 depends_on: - db tty: true stdin_open: true db: image: mysql:5.7 volumes: - db-volume:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: password volumes: db-volume:Gemfilesource 'https://rubygems.org' gem 'rails', '5.2.3'あとは、Gemfile.lockの空ファイルを作成。
Railsプロジェクトを作成する
ターミナル$ docker-compose run web rails new . --force --database=mysql※rails newの . は現在いるディレクトリにそのままプロジェクトを作成するというもの)だそうです。
database.ymlを編集
default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password host: dbhostの部分をdbに変更する。
vue.jsの導入
Gemfilegem 'webpacker', github: 'rails/webpacker'その後、
$ docker-compose exec web bashでwebコンテナに入る。
コンテナ内でコマンドを以下のコマンドを実行
$ rails webpacker:install $ rails webpacker:install:vue $ bin/webpackRailsのviewファイルに以下を記述する
<%=javascript_pack_tag 'hello_vue'%>こんな画面が出てきたら成功です。
以上
- 投稿日:2020-03-30T22:39:18+09:00
Amplify + Vue.js で認証機能付きの Web サイトを公開する
はじめに
Amplify は、簡単に Web サイトに機能を追加して公開できるサービスです。Vue.js はフロントエンドアプリケーションを開発する際に用いられる JavaScript フレームワークです。本記事では Amplify を使って Vue.js アプリに認証機能を追加し、公開する方法を紹介します。
対象読者
- Amplify Framework の初心者の方
- Vue.js で作った Web サイトに Cognito の認証機能を付けたい方
- 【初心者向け】AWS Amplify Console + MkDocs で気軽に始める Web サイト公開(Qiita) を読まれた方
簡単用語説明
- AWS を使用したサーバーレスな Web およびモバイルアプリ開発のためのフレームワークです。
- CLI 、Framework 、Console の 3 つの要素で構成されています。( 詳細は下記 AWS 公式ブログをご参照ください。)
- CLI … コマンドラインから AWS のバックエンドを構築できるインターフェイスです。
- Framework … AWS バックエンドと連携しやすくするための各種ライブラリです。
- Console … 静的サイトのホスティング、CI/CD の運用を自動化するマネージドサービスです
- 参照:スタートアップが AWS Amplify を使うべき3つの理由
- ウェブアプリケーションやモバイルアプリケーションの認証、許可、ユーザー管理機能を提供する AWS サービスです。
- 同系統の他サービスとしては、Firebase Authentication や、Auth0 などが挙げられます。
- 同じく AWS サービスである Amplify と連携させやすいのが特徴です。
- 参照:はじめてのAWS Cognito!!
- Web サイトのユーザーインターフェイスを構築するための JavaScript フレームワークです。
- 同系統の他フレームワークとしては、 React や、 Angular 、Svelte などが挙げられます。
- 参照:Vue.jsをはじめてみる #1 ~公式ガイドを読む編~
Amplify Console を使ったWeb サイト公開の方法 2つ
1. AWS マネジメントコンソール ( 以下、AWS M/C と略 ) から使う方法
- 主として GUI ( AWS M/C ) を利用する方法です。Amplify 初心者向け。
- 詳細は、以前の記事で説明しています。
2. Amplify CLI から使う方法
- CUI ( Amplify CLI ) を利用する方法です。1. の方法で Amplify に慣れた方向け。
- 本記事では、こちらの方法を紹介します。
2. Amplify CLI から Amplify Console を使う方法
章の構成
- 2-1. Vue.js のセットアップ
- Vue.js をインストールして、ローカル PC で実行する方法を紹介します。
- 2-2. Amplify CLI のセットアップ
- Amplify CLI をインストールして、コマンド実行用 IAM ユーザーを設定する方法を紹介します。
- 2-3. Amplify アプリの新規作成
- Amplify アプリを新規作成する方法を紹介します。
- 2-4. Vue.js アプリをリリース
- 2-1. で作成した Vue.js アプリを Amplify Console にホスティングする方法を紹介します。
- Amplify Console へのホスティングに Amplify CLI を利用します。
- 2-5. バックエンド側で認証機能の追加
- AWS バックエンドにユーザー認証基盤を構築する方法を紹介します。
- 2-6. フロントエンド側に認証画面を追加
- Vue.js に Amplify Framework を組み込む方法を紹介します。
- 実際にサインイン画面からサインインできるところまで確認します。
- 2-7. ユーザーのサインアップの制限
- サインインできるユーザーを制限する方法を紹介します。
- 2-8. ブランチ自動検出設定を追加
- Amplify のブランチ自動検出設定を有効化する方法を紹介します。
- Git ブランチが新規作成される際、自動で新規バックエンドも構築されるようになります。
各節の構成
- 前提
- 後段の「操作」を実施するにあたっての前提知識や、用意 / 登録するべきサービスを紹介しています。
- 「操作」を実施する前に、ご一読されることをお勧めします。
- 操作
- 操作手順を、操作画面やコマンドプロンプトの実行コマンドを交えて紹介しています。
- 実行コマンドは、基本的に Windows 10 上の Git Bash で実行した結果を表示しています。
- 結果
- 「操作」を実施した後に想定される結果を紹介しています。
- 節の内容に応じて「結果」の後にオプションで項目を追加しています。
2-1. Vue.js のセットアップ
前提
- Vue.js の開発には、Vue CLI を使うと便利です。
- 今回の Vue CLI インストール手順は、下記の記事を参照しました。
- 参照:Vue.js を vue-cli を使ってシンプルにはじめてみる
- Vue CLI は Node.js 上で動きますので、Node.js をあらかじめインストールしておいてください。
- Windows 版インストール :Node.js / npmをインストールする(for Windows)
- Mac 版インストール:MacにNode.jsをインストール
操作
- Vue CLI をインストールしてください。
-g
を付けてインストールするため、次回アプリ作成時はこの操作は不要です。$ node -v // Node.js がインストールされていることと、node のバージョンの確認 v12.13.0 $ npm -v // npm のバージョンの確認 6.12.0 $ npm install -g @vue/cli // Vue CLI のインストール $ vue --version // 正常にインストールされていることの確認 @vue/cli 4.2.2
- Vue CLI でアプリのプロジェクトを作成してください。
$ cd root-dir // 下記コマンドを打つ際は、任意のディレクトリ配下で実施してください。 $ vue create tsproject // 新規プロジェクトの作成 > dev (babel, typescript, pwa, router, vuex, eslint) // この項目を選択してください。 default (babel, eslint) Manually select features $ cd tsproject // 作成されたディレクトリに移動してください。 $ npm run serve // ローカル PC 内で見れるようになります。結果
http://localhost:8080/
に Web ブラウザでアクセスすると、Vue.js のサンプルページが表示されます。- Vue Router の機能により、「About」を選択すると、
/about
ページへ遷移します。後続作業 ( Git リポジトリのセットアップ )
- 下記の記事の 1-2. と同様の操作を実施してください。
- 参照:【初心者向け】AWS Amplify Console + MkDocs で気軽に始める Web サイト公開
- リポジトリ名は
amplifytsproject
で作成しました。- Vue CLI で新規アプリを作成すると、自動で最初の
git commit
まで実施されているので、後はgit remote add
とgit push -u origin master
だけコマンド実行しました。2-2. Amplify CLI のセットアップ
前提
- 2-1. の「後続作業」まで完了した状態にしてください。
- Amplify CLI も、Node.js 上で動きます。インストール手順は下記記事を参照しました。
- AWS アカウントを持っていない方は、下記を参照して用意してください。
- Amplify CLI の初期セットアップ操作は、強い権限を持つ IAM ユーザー もしくは ルートユーザーで実施してください。
- 初期セットアップの結果、作成される IAM ユーザーはデフォルトで
AdministratorAccess
の権限が付与されます。
- AWS アカウント内でのほとんど全ての操作が可能な強い権限を持つ IAM ユーザーが作成されますので、運用される際はお気を付けください。
操作
- Amplify CLI をインストールしてください。既に実施されている方は飛ばして問題ありません。
$ npm install -g @aws-amplify/cli // Amplify CLI のインストール $ amplify -v // Amplify CLI がインストールされていることを確認 4.16.1
- Amplify CLI を初期セットアップしてください。既に実施されている方は飛ばして問題ありません。
$ amplify configure // このコマンドを実行すると Web ブラウザが立ち上がり、AWS ログインページが表示されます。 Follow 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 // 「AdministratorAccess」などの強い権限を持っている IAM ユーザーで AWS ログイン後にまたこのコンソール画面に戻ってきて Enter を押します。 // AWS ログイン後、Enter 押下 Specify the AWS Region ? region: ap-northeast-1 // ① 東京リージョンを選択しました。お好みのリージョンを選択してください。 Specify the username of the new IAM user: ? user name: amplify-TVofWdlfkj // ここで入力した値が IAM ユーザー名に使用されます。 Complete the user creation using the AWS console https://console.aws.amazon.com/iam/home?region=undefined#/users$new?step=final&accessKey&userNames=amplify-TVofWdlfkj&permissionType=policies&policies=arn:aws:iam::aws:policy%2FAdministratorAccess Press Enter to continue // 再び Web 画面に移り、IAM ユーザーを作成するように促されます。基本的には全てデフォルトで「次のステップ」を選択すれば大丈夫です。 // 作成された IAM ユーザーの「アクセスキー ID 」、「シークレットアクセスキー」はこの後すぐに使いますのでコピーしておいてください。 // IAM ユーザー作成後、Enter 押下 Enter the access key of the newly created user: ? accessKeyId: ********** // ② 作成した IAM ユーザーの「アクセスキー ID 」を入力してください。 ? secretAccessKey: ******************** // ③ 作成した IAM ユーザーの「シークレットアクセスキー」を入力してください。 This would update/create the AWS Profile in your local machine ? Profile Name: tsproject // ④ 任意のプロファイル名を指定してください。 Successfully set up the new user. // この表示が最後に表示されれば初期セットアップクリアです。結果
%UserProfile%\.aws
配下のconfig
ファイルとcredentials
ファイルにプロファイル設定が追記されます。( もしこれらのファイルが存在しなければ、新規作成されます。)// %UserProfile%\.aws\config の内容 [default] … ( 略 ) … [profile tsproject] // 操作の中の④で指定したプロファイル名が指定されている。 region=ap-northeast-1 // 操作の中の①で指定したリージョンが入力されている。 // %UserProfile%\.aws\credentials の内容 [default] … ( 略 ) … [tsproject] // 操作の中の④で指定したプロファイル名が指定されている。 aws_access_key_id=「アクセスキー ID 」 // 操作の中の②で指定したアクセスキー ID が指定されている。 aws_secret_access_key=「シークレットアクセスキー」 // 操作の中の③で指定したシークレットアクセスキーが指定されている。2-3. Amplify アプリの新規作成
前提
- 2-2. まで完了した状態にしてください。
操作
amplify init
で、アプリを初期化してください。$ cd tsproject // 2-2. と同一のディレクトリ配下で、`amplify init`を実行してください。 $ amplify init Note: It is recommended to run this command from the root of your app directory ? Enter a name for the project tsproject // ①アプリのプロジェクト名を指定 ? Enter a name for the environment prod // ②バックエンドの環境名を指定 ? Choose your default editor: Visual Studio Code // デフォルト > Visual Studio Code Atom Editor Sublime Text IntelliJ IDEA Vim (via Terminal, Mac OS only) Emacs (via Terminal, Mac OS only) None ? Choose the type of app that you're building javascript // デフォルト android ios > javascript Please tell us about your project ? What javascript framework are you using vue // デフォルト angular ember ionic react react-native > vue none ? Source Directory Path: src // デフォルト ? Distribution Directory Path: dist // デフォルト ? Build Command: npm.cmd run-script build // デフォルト ? Start Command: npm.cmd run-script serve // デフォルト Using default provider awscloudformation // CloudFormationが裏で実行されています。 … ( 中略 ) … Your project has been successfully initialized and connected to the cloud! // CloudFormation の実行が成功しました。 … ( 中略 ) …結果
- プロジェクトフォルダ内に
amplify
フォルダが作成されます。$ tree -a amplify/ tsproject/amplify/ |-- #current-cloud-backend // ③ S3にアップロードされるディレクトリ | `-- amplify-meta.json // ④ S3にアップロードされるファイル |-- .config | |-- local-aws-info.json | |-- local-env-info.json | `-- project-config.json |-- backend | |-- amplify-meta.json | `-- backend-config.json `-- team-provider-info.json
2-4. Vue.js アプリをリリース
前提
- 2-3. まで完了した状態にしてください。
操作
amplify hosting add
で、アプリをホスティングしてください。$ amplify hosting add ? Select the plugin module to execute (Use arrow keys) > Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment) Amazon CloudFront and S3 ? Choose a type > Continuous deployment (Git-based deployments) Manual deployment Learn more ? Continuous deployment is configured in the Amplify Console. Please hit enter once you connect your repository // Web ブラウザで Amplify サービス画面が表示される。? Continuous deployment is configured in the Amplify Console. Please hit enter once you connect your repository // Enter を押下する Amplify hosting urls: ┌──────────────┬──────────────────────────────────────────────┐ │ FrontEnd Env │ Domain │ ├──────────────┼──────────────────────────────────────────────┤ │ master │ https://master.<App ID>.amplifyapp.com │ └──────────────┴──────────────────────────────────────────────┘結果
https://master.<App ID>.amplifyapp.com
でアクセスすると、2-1. の結果と同じ画面 ( Vue.js のサンプルページ ) が表示されます。
amplify status
で現在のバックエンドのデプロイ状況を確認できます。$ amplify status Current Environment: prod | Category | Resource name | Operation | Provider plugin | | -------- | -------------- | --------- | --------------- | | Hosting | amplifyhosting | No Change | | // Hosting が追加されています。 Amplify hosting urls: ┌──────────────┬──────────────────────────────────────────────┐ │ FrontEnd Env │ Domain │ ├──────────────┼──────────────────────────────────────────────┤ │ master │ https://master.<App ID>.amplifyapp.com │ // 公開されている URL が表示されています。 └──────────────┴──────────────────────────────────────────────┘参考
- 2-4. の結果の時の CloudFormation の状態は、2-3. の結果の時と変わりません。
- 2-4. では 新規の CloudFront ディストリビューションも、新規の S3 バケットも作成されません。
- 私見ですが、ビルドされたコンテンツは AWS が管理する CloudFront、S3 から公開されているようです。
- CloudFront、S3 の細かい設定を変更する要件がある場合は、
amplify hosting add
を実行する際Hosting with Amplify Console
ではなくAmazon CloudFront and S3
を選択するべきと思われます。2-5. バックエンド側で認証機能の追加
前提
- 2-4. まで完了した状態にしてください。
操作
amplify add auth
で、バックエンドに認証機能を追加してください。$ amplify add auth > Default configuration // デフォルト Default configuration with Social Provider (Federation) Manual configuration I want to learn more. Warning: you will not be able to edit these selections. How do you want users to be able to sign in? Username > Email // Email を選択 Phone Number Email and Phone Number I want to learn more. Do you want to configure advanced settings? (Use arrow keys) > No, I am done. // デフォルト Yes, I want to make some additional changes.
- ローカルの設定を
amplify push
でクラウドに反映させてください。$ amplify push √ Successfully pulled backend environment prod from the cloud. Current Environment: prod | Category | Resource name | Operation | Provider plugin | | -------- | ----------------- | --------- | ----------------- | | Auth | tsproject1cc0f2f5 | Create | awscloudformation | | Hosting | amplifyhosting | No Change | | ? Are you sure you want to continue? Yes / Updating resources in the cloud. This may take a few minutes... … ( 中略 ) … √ All resources are updated in the cloud結果
2-6. フロントエンド側に認証画面を追加
バックエンドの認証基盤をフロントエンドから利用できるように修正します。
前提
- 2-5. まで完了した状態にしてください。
- TypeScript の設定を追加した Vue.js プロジェクトでは
aws-amplify
をインストールして実行するとエラーが出力されるため、下記を参照してエラーを修正しました。操作
aws-amplify
とaws-amplify-vue
をインストールしてください。npm install aws-amplify aws-amplify-vue
main.ts
を下記のように修正してください。typescript(main.ts)import Vue from 'vue' import App from './App.vue' import './registerServiceWorker' import router from './router' import store from './store' // ---------------↓ここから追記↓--------------- import Amplify, * as AmplifyModules from 'aws-amplify' // @ts-ignore import { AmplifyPlugin } from 'aws-amplify-vue' // @ts-ignore import aws_exports from './aws-exports' Amplify.configure(aws_exports) Vue.use(AmplifyPlugin, AmplifyModules) // ---------------↑ここまで追記↑--------------- Vue.config.productionTip = false new Vue({ router, store, render: h => h(App) }).$mount('#app')
tsconfig.json
を下記のように修正してください。json(tsconfig.json)// … ( 以上略 ) … "types": [ "webpack-env", "node" // 追記 ], // … ( 以下略 ) …
- モジュールが足りない旨のエラーが出る場合、下記を実行してモジュールを追加してください。
- 私は
zen-observable
が足りていない旨のエラーが出力されたため 下記コマンドでインストールしました。types
というユーザーが公開している、型定義済みファイルのパッケージという意味のようです。- 参照:npmでTypeScriptの型定義を管理できるtypesパッケージについて
npm install @types/zen-observable
@ts-ignore
や、camelcase
関連のエラーが出力された場合は、.eslintrc.js
に下記を追加してください。rules: { … ( 中略 ) … "@typescript-eslint/ban-ts-ignore": "off", // 追記 "@typescript-eslint/camelcase": "off", // 追記 "@typescript-eslint/no-explicit-any": "off", // 追記 "@typescript-eslint/no-empty-function": "off", // 追記 }
- それでも Amplify ビルド時に下記のようなエラーが出る場合は、
"skipLibCheck": true
をtsconfig.json
に追記してください。( ライブラリチェックをスキップするようになるため、おすすめしません。)// Amplify ビルド時エラー 2020-03-29T10:50:07.776Z [INFO]: error in /codebuild/output/src263382100/src/amplifytsproject/node_modules/@aws-amplify/api/lib-esm/types/index.d.ts ERROR in /codebuild/output/src263382100/src/amplifytsproject/node_modules/@aws-amplify/api/lib-esm/types/index.d.ts(1,30): 1:30 Could not find a declaration file for module 'graphql/language/ast'. '/codebuild/output/src263382100/src/amplifytsproject/node_modules/graphql/language/ast.js' implicitly has an 'any' type. Try `npm install @types/graphql` if it exists or add a new declaration (.d.ts) file containing `declare module 'graphql/language/ast';` > 1 | import { DocumentNode } from 'graphql/language/ast'; | ^ 2 | /** 3 | * RestClient instance options 4 | */ 2020-03-29T10:50:07.776Z [WARNING]: ERROR Build failed with errors.json(tsconfig.json){ "compilerOptions": { "skipLibCheck": true, // 追記 "target": "esnext", "module": "esnext", … ( 以下略 ) …
src\store\index.ts
を下記のように修正してください。import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { user: null // 追記 }, mutations: { setUser(state, user) { // 追記 state.user = user // 追記 } // 追記 }, actions: { }, modules: { } })
src\router\index.ts
を下記のように修正してください。
- 下記の記事を参考に設定しました。
- 参照:Try #024 – AWS AmplifyとVue.jsでログイン機能を構築してみた
- tslint に3、4回ほど指摘を受けたため、追記部 ① ~ ③ を書く順番は大事です。
import Vue from 'vue' import VueRouter from 'vue-router' import Home from '../views/Home.vue' // ---------------↓↓追記部①ここから↓↓--------------- // @ts-ignore import store from '../store' // @ts-ignore import { AmplifyEventBus, AmplifyPlugin, components } from 'aws-amplify-vue' import * as AmplifyModules from 'aws-amplify' Vue.use(AmplifyPlugin, AmplifyModules) // ---------------↑↑追記部①ここまで↑↑--------------- Vue.use(VueRouter) // ---------------↓↓追記部②ここから↓↓--------------- let user: any; function getUser() { return Vue.prototype.$Amplify.Auth.currentAuthenticatedUser() .then((data: any) => { if (data && data.signInUserSession) { store.commit('setUser', data); return data; } }).catch(() => { store.commit('setUser', null); return null; }); } // ---------------↑↑追記部②ここまで↑↑--------------- const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'Home', component: Home, meta: { requiresAuth: true } // 追記 }, { path: '/auth', name: 'auth', component: components.Authenticator }, { 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'), meta: { requiresAuth: true } // 追記 } ] }) // ---------------↓↓追記部③ここから↓↓--------------- // ユーザー管理 getUser().then((user: any) => { if (user) { router.push({ path: '/' }, () => { }, () => { }); } }); // ログイン状態管理 AmplifyEventBus.$on('authState', async (state: any) => { if (state === 'signedOut') { user = null; store.commit('setUser', null); router.push({ path: '/auth' }, () => { }, () => { }); } else if (state === 'signedIn') { user = await getUser(); router.push({ path: '/' }, () => { }, () => { }); } }); router.beforeResolve(async (to, from, next) => { if (to.matched.some(record => record.meta.requiresAuth)) { user = await getUser(); if (!user) { return next({ path: "/auth", query: { redirect: to.fullPath } }); } return next(); } return next(); }); // ---------------↑↑追記部③ここまで↑↑--------------- export default router
- 最後に下記コマンドで、GitHub へ push してください。Amplify Console が更新されます。
git add . git commit -m "完成!検証大変だった!( もっとちゃんとしたコメントを付けてください。)" git push結果
https://master.<App ID>.amplifyapp.com/
でアクセスすると、認証画面にリダイレクトされます。サインアップできることを確認してください。- サインインして Vue.js のサンプルページが表示されることを確認してください。
2-7. ユーザーのサインアップの制限
- 今の設定のままでは、誰でもサインアップしてユーザー登録できてしまいます。
- 管理者のみユーザー登録ができるように設定を追加します。
前提
- 2-6. まで完了した状態にしてください。
操作
結果
2-8. ブランチ自動検出設定を追加
- 新規の Git ブランチを作成したときにバックエンドが自動で構築されるように設定します。
前提
- 2-6. まで完了した状態にしてください。( 2-7. は飛ばしても問題ありません。)
- Amplify 用サービスロールを作成しない場合、この節の操作は実施できませんのでご注意ください。
- Amplify 用サービスロールを作成していない場合は、2-4. の操作画面ショットを参照して作成してください。
- Amplify 用サービスロールを作成する際に IAM Role に
AdministratorAccess
ポリシーの付与が必要です。- サービスロールの信頼されたエンティティには Amplify のみ設定されます。 EC2 など他サービスにサービスロールが割り当てられることが無いため まだ安心かと思われます。( それでも世の IAM 担当者が聞いたら卒倒する気がしますが。。笑 )
操作
$ git branch * master $ git checkout -b feature/fix-test Switched to a new branch 'feature/fix-test' $ git branch * feature/fix-test master $ git push origin feature/fix-test -u結果
feature ブランチにアクセスすると、下記のように Basic 認証と Cognito 認証の両方が追加されていることが確認できます。( この後サインアップとサインインすると Vue.js のページが表示されました。)
オプション操作
- 下記のようにコマンドを実行してローカルに Amplify の環境設定を反映させてください。
$ amplify env list // まだ featurefix 環境はローカルに反映されていません。 | Environments | | ------------ | | *prod | $ amplify pull --appId <App ID> --envName featurefix // 2-8. の結果の時にコピーしたコマンドを実行します。 For more information on AWS Profiles, see: https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html ? Do you want to use an AWS profile? Yes ? Please choose the profile you want to use tsproject Amplify AppID found: <App ID>. Amplify App name is: tsproject Backend environment featurefix found in Amplify Console app: tsproject ? Choose your default editor: Visual Studio Code ? 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.cmd run-script build ? Start Command: npm.cmd run-script serve ? Do you plan on modifying this backend? Yes Successfully pulled backend environment featurefix from the cloud. Run 'amplify pull' to sync upstream changes. $ amplify env list // featurefix 環境がローカルに反映されました。 | Environments | | ------------ | | *featurefix | // 追加されています。 | prod | $ amplify pull Pre-pull status: Current Environment: featurefix | Category | Resource name | Operation | Provider plugin | | -------- | ----------------- | --------- | ----------------- | | Hosting | amplifyhosting | No Change | | | Auth | tsproject1cc0f2f5 | No Change | awscloudformation | √ Successfully pulled backend environment featurefix from the cloud. … ( 以下略 ) …
- この操作により、Amplify CLI を使って featurefix 環境に対して設定変更 ( API やデータストアの追加など ) をしていくことができるようになります。
操作 & 結果は以上です。
まとめ
- Amplify ( Console + CLI + Framework ) + Vue.js で認証機能付きの Web サイトを作成することができました。
- Amplify に用意されている認証用ページをそのまま使うことができるため、よりコンテンツの中身に集中できるようになっています。
amplify api add
等、他のカテゴリの機能を追加する足掛かりができました。最後に
- TypeScript で作られた Vue.js プロジェクトに Amplify Framework を組み込むのはあまり事例が無いようで、とりあえず「落とし穴がたくさんある」ことは分かって良かったです。
- AppSync 、DynamoDB の通信状況を X-Ray でグラフィカルに表示できるようになったようなので 次は
amplify api add
の検証をしてみたいと考えています。- この記事内でお気づきになった点や修正が必要な記載がありましたら、ご指摘いただけますと幸いです。
- 最後になりましたが参照記事を執筆された方々に感謝を申し上げます。ありがとうございました。
- 投稿日:2020-03-30T22:11:59+09:00
vue-chartjsでデータの数に応じてグラフサイズを変える
やりたいこと
- Horizontal Barグラフでデータ数が増えたときに、ラベルが間引かれるのを防ぎたい。
- さらに、Barが小さくなるのではなく、グラフの方が大きくなって欲しい。
1.に対する解決策
autoSkip: false
を設定する。yAxes: [{ ticks: { autoSkip: false, //ラベルを間引かない fontSize: 10, }, }]2.に対する解決策
基本的に
responsive: true, maintainAspectRatio: false
の2つを設定すれば実現可能。しかし今回は開閉するパネル内にグラフを配置しており、パネルの開閉時にグラフの高さが0から増えないという現象に悩まされたので、明示的に高さを指定するようにする。
data () { labels: this.label, datasets: [{ data: this.value, }], return { options: { responsive: true, //レスポンシブ maintainAspectRatio: true //縦横は固定 }, } } mounted () { this.$refs.canvas.height = this.value.length*17.5; //データ数に応じて高さ指定 this.renderChart(this.data, this.options) },
- 投稿日:2020-03-30T22:09:30+09:00
Dockerのコマンドについて詳しくなろう
build
buildコマンドはimageを構築するのみ。コンテナ作成はしない。
$ docker-compose buildup
upコマンドでは、キャッシュがある場合はそれを使って一発でイメージの構築から、コンテナの構築・起動までします。
$ docker-compose upstart
startコマンドは既存のコンテナを起動します。
$ cocker-compose startrun
runコマンドはimageの構築から、コンテナの構築・起動までする。引数でサービスを指定する必要あり。
$ docker-compose run webexec
コンテナの中に移動できる
$ docker-compose exec web bashbash ashの違いは、ashの方が軽量なコマンドらしい。
新たなrailsプロジェクトを立ち上げるコマンド
ターミナルdocker-compose run web rails new . --force --database=mysqlrails newの . は現在いるディレクトリにそのままプロジェクトを作成するというもの)だそうです。
以上
- 投稿日:2020-03-30T21:04:02+09:00
【vue/vuex】 Vuexでエラーハンドリングして、vueコンポーネントで評価する
はじめに
VueコンポーネントからVuexのstore内部にAPI非同期通信するためにごにょごにょやっていました。
そこで、store内部でエラーハンドリングしつつ、vueコンポーネントでその結果を受けてアラートを出したり、finally処理をしたいと思ったときに自分用のメモした内容です。
※雰囲気覚えておくためのモノなのでミスしている部分あるかもしれません。ES2015とかES2017の知識ないと無理でした。
まさかの過去の自分の投稿に救われました。https://qiita.com/LemonmanNo39/items/53b42b2aa562bbf57209
内容
Vueコンポーネントで、Vuex内部のstoreを通して、axiosライブラリを用いてAPI通信します。
VueやVuexの共有内容も兼ねてなるべく丁寧にお伝えします。
実際の構成と異なり、単純化しています。
また、vue-cliを使用していないので、構成が異なる部分があります。
Nuxt.jsとかから入った人も困惑すると思いますが、基礎部分は同じです。VueやVuexのおさらい
VueやVuex何て知ってるから早くしろという人は本題まで飛ばしてください。
ファイル構成や流れ、実際のファイルを見ていただくほうが理解できると思うので、記述します。【ファイル構成】
- public - index.html - src - components - App.vue - store - api.js - main.js【流れ】
ざっくり以下の流れになります。
public(index.html) -> src(main.js) -> components(App.vue) -> store(api.js)【ファイル内容】
ファイル内部を記述します。
細かな部分は端折っています。public(index.html)<!DOCTYPE html> <html lang="ja"> <head> <script src="../src/main.js"></script> </head> <body> <div id="app"></div> </body> </html>「el」要素で「public(index.html)」の「app」に対してrenderする書き方もできます。
src(main.js)import Vue from 'vue'; import store from './store'; import App from './components/App.vue'; new Vue({ store, render: h => h(App) }).$mount('#app');components(App.vue)<template> <div> <button @click="getApi"></button> <ul> <li v-for="list in getApiData"> {{ list }} </li> </ul> </div> </template> <script> import App from './components' import { mapGetters } from 'vuex' export default ({ methods: { getApi(){ this.$store.dispatch('getApiInfoAction', { url: 'hogehoge.com', params: 'test' }) } }, computed: { ...mapGetters({ getApiData: "getApiInfo" }) } }) </script>store(api.js)import axios from 'axios' import Vuex from 'vuex' Vue.use(Vuex); const state = { message: "", } const getters = { getApiInfo(state) { return state.message } } const mutations = { getApiInfo(state, payload) { state.message = payload.message } } const actions = { async getApiInfoAction(context, payload) { const payload_data = { message: "", } await axios.get(payload.url, { params: payload.params }) .then(response => { payload_data.message = response.data }).catch(error => { payload_data.message = error }).finally(() => { context.commit('getApiInfo', payload_data) }) }) } } const store = new Vuex.Store({ state, getters, mutations, actions }) export default store本題
APIで処理が失敗したときにもっとキレイにかけないかな、components(App.vue)側でうまくやりたい、またうまくエラーハンドリングしたいという時に役立ったのが、Promiseでした。
自分の過去記事に助けられました。https://qiita.com/LemonmanNo39/items/53b42b2aa562bbf57209
結論
components(App.vue)methods: { getApi(){ this.$store.dispatch('getApiInfoAction', { url: 'hogehoge.com', params: 'test' }).then(response => { alert("ok beforeCreate: ", response) }, error => { alert("no beforeCreate: ", error) }).finally(() => { alert("yeah") }) } }store(api.js)const actions = { async getApiInfoAction(context, payload) { const payload_data = { message: "", } await new Promise((resolve, reject) => { axios.get(payload.url, { params: payload.params }) .then(response => { payload_data.message = response.data resolve(response) }).catch(error => { payload_data.message = error reject(error) }).finally(() => { context.commit('getApiInfo', payload_data) }) }) } }こうすることで、
APIに成功すれば「resolve」を返し
失敗すれば、「reject」を返してくれるようになり
vueコンポーネント側で良いようにできるようになった。まとめ
Promiseもasync/await、めっちゃ便利。
- 投稿日:2020-03-30T20:48:49+09:00
COVID-19 対策サイトのグラフの追加
この記事はなに
僕は三重県版コロナウイルス対策サイトの立ち上げメンバーのうちの一人で現在高校2年生です。現在の陽性患者数のグラフを実装することができたので、メモをしておきます。
他のグラフの追加方法についても少し書いてあります。
(※僕がVue.js,nuxt.jsにふれるのはこれが初めてなので、もっと良い書き方などがあれば教えてください。もし間違いがあればコメントまでお願いします。また三重県版サイトをもとに記事を書いています)まだ環境ができていない方はこちらの記事を先に読むことをお勧めします。
https://qiita.com/FPC_COMMUNITY/items/b9cc072813dc2231b2b2作成手順
1,グラフそのものを作成する
まずは、既存のグラフを1つコピーして、グラフを1つ増やしましょう。
index.vue<v-col cols="12" md="6" class="DataCard"> <time-bar-chart title="陽性患者が確認された件数" :title-id="'number-of-confirmed-cases'" :chart-id="'time-bar-chart-patients'" :chart-data="patientsGraph" :date="Data.patients.date" :unit="'件'" :url=" 'https://www.pref.mie.lg.jp/YAKUMUS/HP/m0068000066.htm#002' " /> </v-col>三重県版サイトでは、陽性患者が確認された件数のグラフが存在していたのでこれをコピーして、その下に張り付けてみます。
そして実行をすると、同じグラフが隣に並ぶことになります。
確認出来たら、title,title-id,chart-idを任意のものに変更しておきましょう。2,data.jsonにデータを追記
三重県では、現在の陽性患者数のデータそのものはまだ存在しませんが、陽性患者が確認された件数と陽性患者の属性より日別の陽性患者数のデータを作成することができたので、それを書いていきます。
data.json
data.json"nowinfectedperson": { "date": "2020/03/27 14:30", "data": [ { "日付": "2020-01-30T18:00:00+09:00", "小計": 1 }, { "日付": "2020-01-31T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-01T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-02T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-03T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-04T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-05T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-06T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-07T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-08T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-09T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-10T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-11T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-12T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-13T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-14T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-15T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-16T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-17T18:00:00+09:00", "小計": 1 }, { "日付": "2020-02-18T18:00:00+09:00", "小計": 0 }, { "日付": "2020-02-19T18:00:00+09:00", "小計": 0 }, { "日付": "2020-02-20T18:00:00+09:00", "小計": 0 }, { "日付": "2020-02-21T18:00:00+09:00", "小計": 0 }, { "日付": "2020-02-22T18:00:00+09:00", "小計": 0 }, { "日付": "2020-02-23T18:00:00+09:00", "小計": 0 }, { "日付": "2020-02-24T18:00:00+09:00", "小計": 0 }, { "日付": "2020-02-25T18:00:00+09:00", "小計": 0 }, { "日付": "2020-02-26T18:00:00+09:00", "小計": 0 }, { "日付": "2020-02-27T18:00:00+09:00", "小計": 0 }, { "日付": "2020-02-28T18:00:00+09:00", "小計": 0 }, { "日付": "2020-02-29T18:00:00+09:00", "小計": 0 }, { "日付": "2020-03-01T18:00:00+09:00", "小計": 0 }, { "日付": "2020-03-02T18:00:00+09:00", "小計": 0 }, { "日付": "2020-03-03T18:00:00+09:00", "小計": 0 }, { "日付": "2020-03-04T18:00:00+09:00", "小計": 1 }, { "日付": "2020-03-05T18:00:00+09:00", "小計": 1 }, { "日付": "2020-03-06T18:00:00+09:00", "小計": 1 }, { "日付": "2020-03-07T18:00:00+09:00", "小計": 1 }, { "日付": "2020-03-08T18:00:00+09:00", "小計": 1 }, { "日付": "2020-03-09T18:00:00+09:00", "小計": 1 }, { "日付": "2020-03-10T18:00:00+09:00", "小計": 2 }, { "日付": "2020-03-11T18:00:00+09:00", "小計": 7 }, { "日付": "2020-03-12T18:00:00+09:00", "小計": 7 }, { "日付": "2020-03-13T18:00:00+09:00", "小計": 7 }, { "日付": "2020-03-14T18:00:00+09:00", "小計": 8 }, { "日付": "2020-03-15T18:00:00+09:00", "小計": 8 }, { "日付": "2020-03-16T18:00:00+09:00", "小計": 8 }, { "日付": "2020-03-17T18:00:00+09:00", "小計": 8 }, { "日付": "2020-03-18T18:00:00+09:00", "小計": 9 }, { "日付": "2020-03-19T18:00:00+09:00", "小計": 8 }, { "日付": "2020-03-20T18:00:00+09:00", "小計": 8 }, { "日付": "2020-03-21T18:00:00+09:00", "小計": 8 }, { "日付": "2020-03-22T18:00:00+09:00", "小計": 8 }, { "日付": "2020-03-23T18:00:00+09:00", "小計": 8 }, { "日付": "2020-03-24T18:00:00+09:00", "小計": 8 }, { "日付": "2020-03-25T18:00:00+09:00", "小計": 8 } ] }3,データを置き換える
index.vueを編集します。今のままだと読み込んでいるデータが違うのでデータを変更していきます。
export default内のdata()の中に追記していきます。index.vueconst nowpatientsGraph = formatGraph(Data.nowinfectedperson.data) const nowsumInfoOfPatients = { lText: nowpatientsGraph[ nowpatientsGraph.length - 1 ].cumulative.toLocaleString(), sText: nowpatientsGraph[nowpatientsGraph.length - 1].label + 'までの累計', unit: '人' }const dataの中に
nowpatientsGraph
nowsumInfoOfPatients
を追加します。そして、新しく作成しているグラフの
chart-data
の値をnowpatientsGraph
に変更します
date
の値もそれに伴って変更しておきましょう。
こうすることで、データが変更されたと思います。日別・累計のボタンがそのまま必要であればここでグラフは完成です。お疲れさまでした。
4,日別・累計の表示を削除する&単位の変更
日別・累計の表示を削除する
データを変更することはできましたが、現在の陽性患者数のグラフには累計の項目が必要ないと思うので、そこを消していきます。
v-if="show"
この部分を追加していくと下のコードになります。TimeBarChart.vue<data-view :title="title" :title-id="titleId" :date="date" :url="url" > <template v-if="show" v-slot:button> <data-selector v-model="dataKind" /> </template>そして、下にある
props:
の中に下のコードを追加しましょうshow: { type: Boolean, required: false, default: true }このコードも追加したら、最後にindex.vueの現在の陽性患者数のところに下のコードを追加します。
:show="false"
default
をtrue
にしておくことで、日別・累計のボタンがいらないところだけfalse
を書けば消えるようになります。単位の変更
また今回作成するグラフは現在の陽性患者数のグラフなので
index.vue
の作成したグラフの:unit
を人
に変更しておきましょう。
またTimeBarChart.vue
のunit:
のdefault
を件
に変更しましょう。これで実行してみると、現在の陽性患者数の部分だけ日別・累計の切り替えボタンがなくなって、単位が
人
になっていると思います
そして累計のデータは必要なくなったのでnowsumInfoOfPatients
のコードは消しておきましょう。これでグラフは完成です!お疲れさまでした!
最後に
最後まで読んでいただきありがとうございました!
間違えている箇所がありましたら、コメントまでよろしくお願いします。
各都道府県でどんどんサイトはできていますが、グラフはまだまだ少ないところがあるので、Vue.js,nuxt.jsが初めての方でもぜひチャレンジしてみてください!
- 投稿日:2020-03-30T19:37:38+09:00
リンクを投稿できるだけのサイトを作ってみた
aws amplify の勉強を兼ねてリンクを投稿できるだけのサイトを作ってみました。
https://techlink.d2iaia9h9jeav.amplifyapp.com/
ページ下のフォームから登録できる。
タグをクリックすると、タグ検索ができる。
- 投稿日:2020-03-30T16:30:36+09:00
Vueitfy でフォームバリデーション失敗時、最初のコンポーネントにスクロールさせる
概要
- vuetify はフォームのバリデーション機能がある
- 縦にスクロールするフォームを作成して送信ボタンが一番下にある場合、上部のフィールドでエラーが発生していても気づきにくい
- 最初にエラーが発生したコンポーネントまでスクロールさせたい
サンプル
対応方法
- フォームから inputs で入力フィールドの一覧を走査
- 最初に見つかったフィールドに対して、vuetify の API でスクロールさせる
post(): void { const form = this.$refs.form; if (!form.validate()) { const firstError = form.inputs.find((input: any) => input.hasError); this.$vuetify.goTo(firstError, { offset: 100 }); return; } alert('(仮) POST します'); }こだわりポイント
- そのまま goTo させると、対象コンポーネントがピタッとくっついてしまう
- マウスホイールでちょっとスクロールした。くらいの位置を実現するため
offset: 100
を指定しているソースコード
https://github.com/Yoshihiro-Hirose/nuxt-vuetify-sample/blob/master/pages/samples/form-scroll.vue
- 投稿日:2020-03-30T16:24:05+09:00
Vuexを初めから丁寧に(2)~Vuexの構成要素と使い方~
はじめに
前の記事ではVuexを理解するための前提知識(状態管理やデータフローについて)を見てきました。
本記事ではいよいよ、Vuex本体について入っていきます。この記事を読むと
- Vuexの構成要素やその使い方が分かる
- Vuexを用いた本格的なアプリケーション開発に向けた準備が整う
想定読者
- Vue.js や Nuxt.js の初級〜中級者
- Vuex を何となく雰囲気で使っている
前提知識
- JavaScript 及び Vue についての基本知識 (Vue の基本知識がない方はこちらが入門書として最も最適です。) 『Vue.js 超入門』(掌田津耶乃/秀和システム)
- 状態管理やデータフローについての基礎知識(一つ目の記事をご覧ください)
Vuex による状態管理
いよいよ Vuex に入って行きます。
Vuex は Vue アプリケーション向けの状態管理ライブラリです。
Vuexは単なるライブラリとして機能を提供するだけではなく、公式に Vuex を使う際の実装のルールも示しており、それを含めてVuexです。
よって、例えば状態の更新はミューテーションでのみ行われるため、更新処理を探したいときはミューテーションを探せば良いです。
これには複数人で開発する際も既存のルールに従うだけで良いため設計やコミュニケーションの手間も省けます。ストア
ストアは主にアプリの状態を保持する役割を担います。
その他にも状態管理に関する機能を盛り込んでおり、vuex の根幹となります。// ストアの作成と代入 cosnt store = new Vuex.Store({オプション})Vuexは信頼できる唯一の情報源であることを前提に設計されています。
アプリケーション内で常にただ一つのストアのみが存在するようにします。※
※一つのディレクトリという意味であり、必ずしもファイスが一つとは限りません。例えばNuxt.jsのモジュールモードにおいてはストア内に複数のJSファイルが存在します。詳しくはこの記事の後半、および次の記事で扱います。ストアの構成要素
ストアの構成要素として、以下の4つの概念が存在します。
- アプリのステート(State)
- ステートの一部や、ステートから計算された値を返すゲッター(Getter)
- ステートを更新するミューテーション(Mutation)
- 主にAjaxリクエストのような非同期処理や、LocalStorageへの読み書きのような外部APIとのやり取りを行うアクション(Action)
ステートは状態、ミューテーションは更新処理に対応します。
規模の大きいアプリを作る際には、上記4つの構成要素をモジュール(Module)という単位で分割して見通しを良くします。
アプリの状態を全て一つの場所に置いてしまうと逆に管理が大変になるのではないかと感じるかもしれませんが、モジュール※を使うことで、信頼できる唯一の情報源を守りながら状態やそれに関わる更新、取得のロジックを複数の単位に分割をし、管理をシンプルに行えます。ステート
ステートの概要
Vuexのステートはアプリ全体の状態を保持するオブジェクトです。
全てのステートは一つの木構造として表現されます。
アプリケーションの全ての状態を一つの木としてステートに保持することで、「信頼できる唯一の情報源」として機能します。
しかし、アプリケーションの全ての状態を必ずしもVuexで管理するべきではありません。Vuexのステートで管理するべきデータと、そうでないデータの例は下記の通りです。ステートに適したデータ
- ログイン中のユーザーの情報など、アプリ全体で使用されるデータ
- ecサイトにおける商品の情報など、アプリの複数の場所で使用される可能性のあるデータコンポーネント側で持つべきデータ
- マウスポインタがある要素の上に存在するかどうかを、表すフラグ
- ドラッグ中の要素の座標
- 入力中のフォームの値import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // ストアの定義 const store = new Vuex.Store({ state: { count: 10 }ステートはコンポーネントのdataオプションに渡された値と同じように変更が追跡されます。
ステートに対して何らかの更新を行うとその変更は自動的にコンポーネントの算出プロパティやテンプレートへと反映されます。
これはVuexが内部的にVueのリアクティブシステムを活用して実装されているためです。
また、ステート内の依存関係がリアクティブシステムによって計算されるため、ステート更新時のuiの再描画が必要最小限になるというメリットもあります。ゲッター
ゲッターの概要
ゲッターはステートから別の値を算出するために用いられます。例えばユーザーの操作によって商品のリストを絞り込みたい時にはゲッターで絞り込んだ商品のリストを算出します。
ゲッターを使用することでコンポーネント上で表示のためにステートを計算することが避けられ、異なるコンポーネント間でロジックを再利用できるようになります。ゲッターの定義方法
gettersオプションに関数をもつオブジェクトを指定することでゲッターを定義します。
コンポーネントの算出プロパティとよく似た機能ですが、引数にステートと他のゲッターが渡され、それらを使った値を返す点が異なります。ゲッターの使い方
ゲッターはstore.gettersから参照できます。
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // ストアの定義 const store = new Vuex.Store({ state: { count: 10 }, // gettersオプションでgettersを定義する getters: { // ステートから別の値を算出する squared: (state) => state.count* getters.squared } })ゲッターはコンポーネントのcomputedオプションと同様に評価された値がキャッシュされます。
キャッシュされた値はそのゲッターが依存しているステートが更新されない限り再評価されません。したがって、よく使用するステートの算出ロジックはゲッターにすることでパフォーマンスの向上が期待できます。一方でゲッターを参照したときに定義した関数が常に実行されるわけではありません。
例えば依存するステートが存在しない時にサーバーから値を取得するというような処理はゲッターの中には書かず、続けて解説するミューテーション、アクションを使って取得とステートへの反映を行います。ミューテーション
ミューテーションの概要
ミューテーションはステートを更新するために用いられます。
Vuexでは規約としてミューテーション以外がステートの更新を行うことを禁止しています。
ステートの更新をミューテーションのみが行えば、ステートの変更がいつどこで発生したのかを追跡しやすくなります。ミューテーションの定義方法
mutationsオプションにミューテーション名をキーに持ち、ハンドラー関数を値に持つオブジェクトを指定することでミューテーションを定義できます。
ハンドラー内では第一引数に渡されたステートを更新します。ミューテーションの使い方
ミューテーションは直接は呼び出せません。store.commitにミューテーション名を与えて呼び出します。
これはイベントの発生と監視によく似ています。
incrementというイベントが発生したときに、その名前で登録したミューテーションハンドラーが実行されると考えると分かりやすいです。import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // ストアの定義 const store = new Vuex.Store({ state: { count: 10 }, // gettersオプションでgettersを定義する getters: { // ステートから別の値を算出する squared: (state) => state.count* getters.squared }, mutations: { // 'incremation'ミューテーションを定義 increment(state) { state.count = state.count + 1 } } })store.commitの第二引数になんらかの値を与えるとそれがハンドラーの第二引数に渡されます。この値のことをペイロード(payload)※と呼びます。ペイロードを使用することで、同じミューテーションでも渡す値によって異なるステートに更新できます。
※payloadの意味は?
ミューテーション内で行う処理は非同期を用いると意図しない動作を引き起こす可能性があるため、全て同期的にする必要があります。
非同期処理を行う必要があるときは次に紹介するアクションを代わりに使用します。アクション
アクションの概要
アクションは非同期処理や外部APIとの通信を行い、最終的にミューテーションを呼びだすために用いられます。
アクションの定義方法
actionsオプションにアクション名をキーに持ち、ハンドラー関数を値に持つオブジェクトを指定することでアクションを定義します。
アクションの使い方
アクションはミューテーションと同様に直接呼び出すことはできません。store.dispatchにアクション名を渡して呼び出します。
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // ストアの定義 const store = new Vuex.Store({ state: { count: 10 }, // gettersオプションでgettersを定義する getters: { // ステートから別の値を算出する squared: (state) => state.count* getters.squared }, mutations: { // 'incremation'ミューテーションを定義 increment(state) { state.count = state.count + 1 } }, // acitionsオプションでアクションを定義する actions: { incrementAction(ctx) { // incrementミューテーションを実行する ctx.commit('increment') } })アクションの定義はミューテーションとよく似ています。ただし、ハンドラーの第一引数にステートではなくコンテキスト(context)と呼ばれる特別なオブジェクトが渡される点で異なります。
コンテキストには以下が含まれます。
- state: 現在のステート
- getters: 定義されているゲッター
- dispatch: 他のアクションを実行するメソッド
- commit: ミューテーションを実行するメソッド
stateやgettersは、例えばデータのロード中にはアクションの処理を行わないというような現在の状態に応じてアクションの処理を切り替えるときに使います。
dispatchを使うことで、すでに定義してある他のアクションを呼びだせます。※
これによって共通の処理を一つのアクションにまとめることができますが、使い過ぎるとどのアクションから呼ばれているのか分かりづらくなるので気をつけましょう。
アクションはミューテーションを実行するのに用いられるため、commitが使われることが最も多いでしょう。以下はAjaxでデータを取得し、そのデータをペイロードに含めたミューテーションを呼び出すアクションを定義している例です。第一引数のコンテキストを分割代入({ commit })することで短い記法で書かれています。また、ミューテーションと同様にアクションも第二引数にペイロードを受け取ります。
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) // ストアの定義 const store = new Vuex.Store({ state: { count: 10 }, // gettersオプションでgettersを定義する getters: { // ステートから別の値を算出する squared: (state) => state.count* getters.squared }, mutations: { // 'incremation'ミューテーションを定義 increment(state) { state.count = state.count + 1 } }, // acitionsオプションでアクションを定義する actions: { incrementAction(ctx) { // incrementミューテーションを実行する ctx.commit('increment') } })おわりに
如何だったでしょうか。簡単に復習してみましょう。
- Vuexはステート、ゲッター、ミューテーション、アクションの4つの要素で構成される
- それぞれの要素の定義方法と使い方を覚えるこの内容がバッチリでしたら、Vuexの基本的な使い方が頭に入ったことになります。
次の記事では、さらにより実践的な応用知識について見ていきます。例えばNuxt.jsにおけるVuexの扱いについてです。参考文献
『Vue.js入門 基礎から実践アプリケーション開発まで』(川口和也, 喜多啓介, 野田陽平, 手島拓也, 片山真也/技術評論社)
Vue.jsについての書籍は増えてきていますが、問題なのはその殆どがVuexについての説明を省略していることです。Vue.jsやNuxt.jsを用いた実際の開発においてVuexによる状態管理は必須ですが、学習の障壁になるとして避けてしまっているのでしょう。私が読んだ中で唯一、Vuexについて丁寧に説明していたのが本書です。Vuex以外の内容も素晴らしいの一言。本書はVue.js・Nuxt.jsの開発に関わるエンジニアや組織にとって必携です。保存用・実用用・観賞用に3冊購入しましょう。あるいは、あなたが経営者の場合はぜひエンジニアに対して一人一冊ずつ買い与えてください。
ただし、全くVueについて未経験という方への第一歩としては内容が本格的すぎるかもしれません。その場合は『Vue.js 超入門』がおすすめです。『Vue.js 超入門』(掌田津耶乃/秀和システム)
とにかく分かりやすく、まず概要を把握するために最適の一冊です。「なんとなくで良いので概要を把握する」⇨「より詳細で厳密な理解する」という流れで学ぶとスムーズです。『初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発』(Ethan Brown, 武舎広幸,武舎るみ/オライリージャパン) )
JavaScriptの根本的な理解ができる、革命的な良書です。分厚いので手強そうに見えますが、実際はとても親切で分かりやすい作りです。本書も一人一冊は欲しいところです。
- 投稿日:2020-03-30T15:39:14+09:00
【Vue + vue-mq】レスポンシブ対応でのVue実装について
目次
・レスポンシブ対応での課題
・レスポンシブ対応での課題解決方法
・vue-mqとは
・vue-mqの実装方法
・vue-mqでのデメリット
・vue-mqのseo要件が満たされない可能性とは
・vue-mq実装時の動きレスポンシブ対応での課題
一般的な実装方法としては、PCやスマホなど各デバイス毎に各HTMLファイルを複数用意し最適化する手法を取っています。
レスポンシブ対応では、1つのHTMLファイルを、CSS3(Media Queries)で制御し、異なる画面サイズに応じてページのレイアウト・デザインを調整します。
その際、ここで問題となるのが「1つのHTMLファイル」。
一般的な実装方法を先ほど記載しましたが、基本PC・SPでHTMLが分かれている。
つまり、PC用のデザイン、SPの用のデザインとなっているため、それに合わせてHTML構造も異なります。レスポンシブ対応する際は、デザイナーがPC・SPでの表示を考慮してデザインしたものを対応する場合はそこまで困らないですが、
今まであったものをそのままレスポンシブで対応するとなると、
デザイン的に考慮されていなかった表示をレスポンシブで対応しなければいけません。そのため、「1つのHTMLファイル」とCSS3(Media Queries)での制御では実装的に苦しい部分が出てきます。
今回は、まさにこの内容で、
「今まであったものをそのままレスポンシブで対応」という要件があり、
HTML構造がPC、SPを「1つのHTMLファイル」で対応するとCSS側がごちゃごちゃするので、
こういった部分にだけ、vue側もレスポンシブ対応させようと思いました。レスポンシブ対応での課題解決方法
上記、課題解決の方法として、
vue実装であれば、「vue-mq」plugin を使用することで、課題解決ができそうだったので、
「vue-mq」plugin を試してみることにしました。vue-mqとは
vue-mqは、Vueを使用してレスポンシブのブレークポイントを定義できるライブラリです。
セマンティックおよび宣言的にモバイルファーストの構築が行えます。簡単にお伝えするとCSSのMedia QueriesのHTML版と考えてもらえるとわかりやすいかと思います。
vue-mqの実装方法
基本的にはドキュメントを参考に実施してもらえれば問題なくできるかと思います。
ドキュメント:https://www.npmjs.com/package/vue-mq
一応、こちらにも実施手順を記載します。
1.Installation
Using NPM
npm install vue-mqUsing Yarn
yarn add vue-mq2.ファイル作成
touch src/plugins/vue-mq.js3.vue-mq.jsにブレイクポイントを記載
import Vue from 'vue' import VueMq from 'vue-mq' Vue.use(VueMq, { breakpoints: { sp: 959, pc: 960 }, defaultBreakpoint: 'sp' })4.nuxt.config.ts(nuxt.config.js)にのplugins内にvue-mqを登録
plugins: ['~/plugins/vue-mq'],or
module.exports = { plugins: [ '~/plugins/vue-mq', ... ], }5.vue側での実装
<template> <div> <div>{{ $mq }}です</div> <div v-if="$mq === 'sp'"> SPのtemplateです。 </div> <div v-if="$mq === 'pc'"> PCのtemplateです。 </div> </div> </template>これにより、レスポンシブの対応で実装したMedia Queries同様に
vue側も表示領域に合わせたDOMに切り替わるため、
vueファイルは1つのままで、表示を切り替えることが可能となります。vue-mqでのデメリット
基本的にレスポンシブ対応には重宝すると思われる「vue-mq」ですが、いくつかデメリットも考えられます。
1、レスポンシブ対応と言いつつ、PC・SPでvue実装を分けているため、負債が多少残る可能性がある。(テンプレートを分けているのと似ているため)
2、SEO要件が満たされない可能性がゼロではない。(ここは最後に少しお話しします。)大きなデメリットは無いと思われるものの、
よく考えてみると見えていなかったデメリットも見えてくるので実施に使用するか否かの判断は
慎重に決める必要があるかなと思います。vue-mqのSEO要件が満たされない可能性とは
vue-mqは実際のHTMLにはソースとして入っていないため、googleBotが正確に認識できているかという点が挙げられます。
よくSPAはSEO上はよくないとか、そういう話が昔あったと思います。
vue-mqの特徴として「画面サイズに応じてクライアントサイドで再レンダリング」されます。
そのためSSRとはまた違った動きになります。注目すべき点は1つで、
「クライアントサイドで再レンダリングされる」、これがSEO的に問題ないか?という点になります。私もここは気になったので少し調べてみたところ、そちらの内容を検証されている方の記事がありました。
https://logmi.jp/tech/articles/321993内容的には以下になります。
・2019年5月にGooglebotが急にアップデートを発表
・アップデートにより「Webブラウザでユーザーが表示するのとまったく同様に」Googlebotがクローリングしてくれるようになった
・検証結果としてPC、SPでの表示内容をGooglebotが認識している上記内容から、
クライアントレンダリングがSEO的に問題と思われる節がありましたが、
そのような内容が解決してきている状況と見えそうですね。vue-mq実装時の動き
vue-mqを実際に実装した際の動きをcodepenに入れてみました。
下記のようにサイズが変化することで表示される内容が切り替わります。See the Pen vue-mq sample by miyawash (@miyawash) on CodePen.
- 投稿日:2020-03-30T12:55:20+09:00
テスト駆動開発でbuttonを追加
Vue.jsで作成した空白のwebページに、Jestを利用したテスト駆動開発でbuttonを追加をする。
- button(id=sampleButton)の存在をテスト
- buttonのラベル(サンプルのボタン)をテスト
- buttonのラベルが変数にバインドされていない場合
- buttonのラベルが変数にバインドされている場合
- buttonのラベルのテスト
- buttonのラベルがバインドされた変数のテスト
- buttonがクリックされて呼び出されるメソッド(onClick)のテスト
完成したソースコードはGitHubのリポジトリにある。
buttonを追加する前のディレクトリ構成とファイル
buttonを追加する前のディレクトリ構成
├ index.html ├ public │ └ index.html └ src ├ App.vue ├ components │ └ SampleButton.vue └ main.jspublic/index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <noscript> <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"></div> <!-- built files will be auto injected --> </body> </html>src/main.jsimport Vue from "vue"; import App from "./App.vue"; Vue.config.productionTip = false; new Vue({ render: h => h(App) }).$mount("#app");src/App.vue<template> <div id="app"> <SampleButton /> </div> </template> <script> import SampleButton from "./components/SampleButton.vue"; export default { name: "App", components: { SampleButton } }; </script> <style></style>src/components/SampleButton.vue<template> <div></div> </template> <script> export default { name: "SampleButton" }; </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped></style>buttonの存在を確認するテストを追加
buttonを追加するために、buttonの存在を確認するテストを追加する。
tests/unit/button.spec.jsimport { shallowMount } from '@vue/test-utils' import SampleButton from '@/components/SampleButton.vue' describe('追加するbuttonのidをsampleButtonとする。', () => { let wrapper beforeEach(() => { wrapper = shallowMount(SampleButton) }) it('存在する。', () => { expect(wrapper.find('#sampleButton').exists()).toBeTruthy() }) })まだbuttonは追加していないので、以下のようにエラーになる。
FAIL tests/unit/button.spec.js 追加するbuttonのidをsampleButtonとする。 ✕ 存在する。 (39ms) ● 追加するbuttonのidをsampleButtonとする。 › 存在する。 expect(received).toBeTruthy() Received: false 9 | 10 | it('存在する。', () => { > 11 | expect(wrapper.find('#sampleButton').exists()).toBeTruthy() | ^ 12 | }) 13 | }) 14 |src/components/SampleButton.vueにbuttonを追加することで、テストは成功する。
src/components/SampleButton.vue(抜粋)<template> <div> <button id='sampleButton'></button> </div> </template>buttonのラベルのテストを追加
buttonのラベルを確認するテストを追加する。
tests/unit/button.spec.js(抜粋)it('ラベル(サンプルのラベル)が正しい。', () => { expect(wrapper.find('#sampleButton').text()).toBe('サンプルのラベル') })ラベルを設定していないので、以下のようにエラーになる。
FAIL tests/unit/button.spec.js 追加するbuttonのidをsampleButtonとする。 ✓ 存在する。 (23ms) ✕ ラベル(サンプルのラベル)が正しい。 (6ms) ● 追加するbuttonのidをsampleButtonとする。 › ラベル(サンプルのラベル)が正しい。 expect(received).toBe(expected) // Object.is equality Expected: "サンプルのラベル" Received: "" 13 | 14 | it('ラベル(サンプルのラベル)が正しい。', () => { > 15 | expect(wrapper.find('#sampleButton').text()).toBe('サンプルのラベル') | ^ 16 | }) 17 | }) 18 |buttonのラベルをtemplateのHTMLに書いてしまう方法と、ラベルをバインドした変数にJavaScriptで設定する方法がある。
buttonのラベルを変数にバインドしない場合
ますは、templateのHTMLに書いてしまう場合には、src/components/SampleButton.vueのtemplateを修正することでテストは成功する。
src/components/SampleButton.vue(抜粋)<template> <div> <button id='sampleButton'>サンプルのラベル</button> </div> </template>buttonのラベルを変数にバインドする場合
buttonのラベルのテストを追加
ラベルをバインドした変数にJavaScriptで設定する場合は、src/components/SampleButton.vueのtemplateとscriptを修正する。
src/components/SampleButton.vue(抜粋)<template> <div> <button id="sampleButton">{{ sampleLabel }}</button> </div> </template> <script> export default { name: "SampleButton", data() { return { sampleLabel: "サンプルのラベル" }; }, methods: { onClick() { return; } } }; </script>buttonのラベルがバインドされた変数のテストを追加
buttonのラベルがバインドされた変数(sampleLabel)のテストを追加する。
tests/unit/button.spec.jsit('buttonにラベルにバインドされた変数が正しい。', () => { expect(wrapper.vm.sampleLabel).toBe('サンプルのラベル') })既に正しく変数にバインドされているため、テストは成功する。
buttonがクリックされて呼び出されるメソッドのテスト
buttonがクリックされるとonClickが呼び出されることを確認するテストを追加する。
tests/unit/button.spec.jsit('クリックするとonClickが呼び出される。', () => { const onClick = jest.fn() wrapper.setMethods({ onClick }) wrapper.find('#sampleButton').trigger('click') expect(onclick).toHaveBeenCalledTimes(1) })onClickを呼び出すように設定していないため、以下のようにエラーになる。
FAIL tests/unit/button.spec.js 追加するbuttonのidをsampleButtonとする。 ✓ 存在する。 (22ms) ✓ ラベル(サンプルのラベル)が正しい。 (2ms) ✕ クリックするとonClickが呼び出される。 (7ms) ● 追加するbuttonのidをsampleButtonとする。 › クリックするとonClickが呼び出される。 TypeError: Cannot set property 'onClick' of undefined 18 | it('クリックするとonClickが呼び出される。', () => { 19 | const onClick = jest.fn() > 20 | wrapper.setMethods({ onClick }) | ^ 21 | wrapper.find('#sampleButton').trigger('click') 22 | expect(onclick).toHaveBeenCalledTimes(1) 23 | })src/components/SampleButton.vueを以下のように修正して、クリックされたときにonCkickを呼び出すように設定することでテストに成功する。
src/components/SampleButton.vue(抜粋)<template> <div> <button id='sampleButton' @click="onClick">サンプルのラベル</button> </div> </template> <script> export default { name: "SampleButton", methods: { onClick() { return } } }; </script>以上で、Vue.jsで作成するwebページにテスト駆動でbuttonを追加した。
- 投稿日:2020-03-30T07:31:13+09:00
【SpecTest GUI - 2】MonacoEditor + Vue.js/Electron
SpecTest GUI ヘの道(2)
- 前回記事「SpecTest GUI ヘの道(1)」 の続きです。
- 今回は、同期スクロール とエディタの コマンド・ショートカット(ツールアイコン) を追加します。
誰向け?
- VSCode で使われている Monaco Editor に興味ある人
- Monaco Editor で Markdown Editor を Electron ベースで 作りたい人
- SpecTest を 応援してくれる 人
尚、今回の結果も以下にコミットしてあります。また、前回のには tag v0.1.0 をつけてあります。
はじめに
SpecTest は私が欲しいと思っていた BDD を実現するための汎用フレームワーク。
SpecTest GUI への道 と題して GUI 作っていきます。Kinx と両方並行して進めます。GUI 作りはサイド・プロジェクト。
前回の訂正
前回 の fontawesome の登録部分が足りてなかったので修正します。すみません。
main.jsimport { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { library } from '@fortawesome/fontawesome-svg-core' import { fas } from '@fortawesome/free-solid-svg-icons' import { fab } from '@fortawesome/free-brands-svg-icons' import { far } from '@fortawesome/free-regular-svg-icons' library.add(fas, far, fab) Vue.component('font-awesome-icon', FontAwesomeIcon)
FontAwesomeIcon
の import とVue.component('font-awesome-icon', FontAwesomeIcon)
がなかった...同期スクロール
基本、スクロールを検知したら相手側もスクロールさせる、ということを実装する。ただし、自動的にスクロールさせた結果も反対側で「スクロールした!」と反応して戻ってくるので何もしないと ループしてしまう。これを抑止しなければならない。
スクロール・ハンドラの追加
エディタ側は Monaco Editor の機能を使う。Monaco Editor には
onDidScrollChange
というインタフェースがあるので、そこにハンドラを登録する。Editor.vuemounted () { var editor = this.$refs.editor.getEditor() editor.onDidScrollChange(this.handleScroll) },ビューワ側は DOM オブジェクトなので、リスナに登録する。ここでは
id
属性をつけておいて DOM オブジェクトを取得するようにしている。Viewer.vuemounted () { document.getElementById("viewer").addEventListener('scroll', this.handleScroll); }, beforeDestroy () { document.getElementById("viewer").removeEventListener('scroll', this.handleScroll); },スクロール通知
スクロールの通知は親コンポーネントである
MarkdownPane
を介して行う。それぞれからイベントを受け取り、相手側に値を転送させる。尚、今回は 簡易的な 位置合わせベースのスクロール連動です。というのも、レイアウトがエディタとビューワで変わるので精密に連動させるのは結構手間がかかる。今回は、全体に対する位置を互いに転送しあってその位置まで動く、というだけのものになる。これでも大体のケースにおいてまあまあうまく機能するし、そもそも世の中の メジャーな Markdown Editor もそんな動きしかしていない ので良いでしょう。
MarkdownPane.vue
は以下のような感じ。@onScrollUpdatedViewer
と@onScrollUpdatedEditor
でそれぞれ相手側に位置情報を転送します。誤差があったので、1 以下に丸めている。MarkdonPane.vue<template> <splitpanes horizontal :style="{ height: height, overflow: 'hidden' }" @resized="resizedPane($event)"> <pane> <splitpanes :style="{ overflow: 'hidden' }" @resized="resizedPane($event)"> <pane class="pane-editor" ref="epane" size="55"> <MarkdownEditor ref="editor" @onScrollUpdatedViewer="onScrollUpdatedViewer" /> </pane> <pane class="pane-view" ref="vpane"> <MarkdownViewer ref="viewer" @onScrollUpdatedEditor="onScrollUpdatedEditor" /> </pane> </splitpanes> </pane> </splitpanes> </template> <script> ... methods () { ... onScrollUpdatedEditor (value) { this.$refs.editor.setScrollTop(value > 1 ? 1 : value); }, onScrollUpdatedViewer (value) { this.$refs.viewer.setScrollTop(value > 1 ? 1 : value); }, }, ... </script>設定するインタフェースは
setScrollTop
メソッドをそれぞれ用意しておく。次のループ抑制の中で一緒に説明する。ループ抑制
ループの抑制だが、単にフラグではうまくいかない。グリっとスクロールさせるといっぺんにいくつものスクロールイベントが発生するのでフラグの設定・解除漏れが発生する。ここでは相手からの通知があったら一定期間反対側からのスクロール・イベントはキャンセルするようにさせた。最後にイベントを受け取ってから 200 ミリに指定してある。
エディタ側は Monaco Editor の
setScrollTop()
メソッドを使う。this.clientHeight
を引いているのは、scrollTop
の値は画面の上部になるので、一画面分上になるからです。ビューワ側も同様。Editor.vuedata: () => { isScrollReceived: false, ... }, methods: { ... setTimeout (clearOnly) { if (this.timeoutId) { clearTimeout(this.timeoutId) this.timeoutId = null } if (!clearOnly) { this.timeoutId = setTimeout(() => { this.isScrollReceived = false this.timeoutId = null }, 200) } }, setScrollTop (v) { this.isScrollReceived = true this.setTimeout(false) var el = this.$refs.editor; var editor = el.getEditor() var topEnd = editor.getScrollHeight() - this.clientHeight this.$nextTick(() => { editor.setScrollTop(topEnd * v); }) }, handleScroll () { if (this.isScrollReceived) { return } var editor = this.$refs.editor.getEditor() var scrollTop = editor.getScrollTop(); var topEnd = editor.getScrollHeight() - this.clientHeight if (topEnd > 0) { this.$nextTick(() => { this.$emit('onScrollUpdatedViewer', scrollTop / topEnd) }) } }, },ビューワ側は DOM のプロパティにセットするだけ。
Viewer.vuedata: () => { isScrollReceived: false, ... }, methods: { ... setTimeout (clearOnly) { if (this.timeoutId) { clearTimeout(this.timeoutId) this.timeoutId = null } if (!clearOnly) { this.timeoutId = setTimeout(() => { this.isScrollReceived = false this.timeoutId = null }, 200) } }, setScrollTop (v) { this.isScrollReceived = true this.setTimeout(false) var el = this.$refs.viewer; var topEnd = el.scrollHeight - el.clientHeight this.$nextTick(() => { el.scrollTop = topEnd * v; }) }, handleScroll (e) { if (this.isScrollReceived) { return } var el = e.target if (el && el.clientHeight && el.scrollHeight) { var topEnd = el.scrollHeight - el.clientHeight if (topEnd > 0) { this.$nextTick(() => { this.$emit('onScrollUpdatedEditor', el.scrollTop / topEnd) }) } } }, },やってみる。
静止画ではわからないとは思うが、無事、スクロールが同期した。
一番上に戻る
長い文章になると一番上に戻りたくなるよね。付けましょう。よくあるフローティング・ボタンを右下につけて、一番上まで戻る機能を。
フローティング・ボタンは全体で一番右下なので、
App.vue
に追加。以下のように一番下に追加しておく。App.vue<template> <v-app> <v-app-bar app ref="appbar" height="56"> <v-app-bar-nav-icon></v-app-bar-nav-icon> <v-toolbar-title>SpecTest GUI</v-toolbar-title> </v-app-bar> <v-content> <MarkdownPane ref="pane" /> </v-content> <v-btn color="red" dark fixed right bottom fab @click="gotoTop"><font-awesome-icon icon="chevron-up" /></v-btn> </v-app> </template>
MarkdownPane
に ref を付けておき、ボタンが押されたらgotoTop
を通知。App.vuemethods: { gotoTop () { this.$refs.pane.gotoTop() }, },
MarkdownPane.vue
では、gotoTop
を受け取ったら ビューワのほうにだけ 通知。ビューワのほうが簡単なので。el.scrollTo
がいい具合にスムーズにアニメーションしてくれる。また、ビューワ側でスクロールするとさっきの同期処理が働いて自動的にエディタ側もスクロールしてくれる!素晴らしい。
MarkdownPane.vue
の実装はmethods
にこれを追加するだけ。とりあえず、null
を渡すのをサインにした。MarkdownPane.vuegotoTop () { this.$refs.viewer.setScrollTop(null); },ビューワ側の実装はこんな感じ。
setScrollTop
の先頭にnull
の場合を追加。Vuewer.vuesetScrollTop (v) { if (v == null) { this.$refs.viewer.scrollTo({ top: 0, behavior: "smooth" }) return } ...実行。
さあ、ボタンを押してみよう。
(静止画ではわかりづらいが)いい感じにスムーズ・スクロールした! ...そして違和感なくスムーズ・スクロールのまま同期するのも気分がいい。ビューン。
編集機能
さて、ついでに Monaco Editor の編集機能を充実させます。基本キーボードで操作するのが楽なのだが、念のためツールバーもつけておきます。でもテキストエディタでツールバーって実は使いづらいよね。
ショートカットキー
まずはショートカットキーから。Monaco Editor の場合、アンドゥ機能を有効にするためには実質メソッドはこれしかない。
editor.getModel().pushEditOperations(...)これで全てのことを実現する。
replace
とかinsert
とか気の利いた名前のメソッドは全くない。今回は、選択した文字列を特定の文字列で括る(例えばaaa
→**aaa**
とか)とヘッダを付ける機能を用意する。ここで@editorWillMount
イベントで受け取ったmonaco
オブジェクトが必要になる。文字列ラッピング・コマンド
まずは、汎用的なコマンド関数の生成関数(ジェネレータ)を作っておく。大体やること一緒なので。
ラッピング・コマンドの生成関数は次の通り。コマンド実行のクロージャ―を返す。Editor.vuefunction generateWrapperCommand(monaco, editor, startText, endText) { var len = startText.length + endText.length return () => { var sels = editor.getSelections() if (sels == null) { return } var ranges = [] sels.forEach(selection => { ranges.push(new monaco.Selection(selection.startLineNumber, selection.startColumn, selection.endLineNumber, selection.endColumn+len)) editor.getModel().pushEditOperations([], [ { range: { startLineNumber: selection.startLineNumber, startColumn: selection.startColumn, endLineNumber: selection.startLineNumber, endColumn: selection.startColumn }, text: startText }, { range: { startLineNumber: selection.endLineNumber, startColumn: selection.endColumn, endLineNumber: selection.endLineNumber, endColumn: selection.endColumn }, text: endText } ]) }) editor.setSelections(ranges) return null } }
pushEditOperations
の第二引数にオペレーションを登録していくが、start と end が一緒ならその位置に挿入、異なっているならその範囲を置換、と思えばよい。最後に挿入後の新しい範囲を指定するようにしておく。これを使ったショートカット・コマンドを登録する汎用関数を作っておく。ちなみに、
actionCommand[label]
に登録しているのは、後でツールバーからコマンド名で実行できるようにするため。Editor.vuevar actionCommand = {}, cmdid = 0; function addWrapperCommand(monaco, editor, context, label, keybindings, startText, endText) { actionCommand[label] = generateWrapperCommand(monaco, editor, startText, endText) editor.addAction({ id: 'markdwon-'+(cmdid++), label: label, keybindings: keybindings, contextMenuGroupId: context && 'navigation', contextMenuOrder: context && 1.5, run: actionCommand[label] }) }追加は
editor
のaddAction
を使う。context...
を指定しておくことで、label
に指定した名前でコンテキストメニューにも追加されて一石二鳥だ。では、これでいくつかコマンドを登録してみよう。キーボードバインディングがこれで使いやすいかは別として、登録の仕方はわかると思う。Editor.vuefunction setupShortcutKeys(monaco, editor) { addWrapperCommand(monaco, editor, true , "Bold", [ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_B ], "**", "**") addWrapperCommand(monaco, editor, true , "Italic", [ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_I ], "*", "*") addWrapperCommand(monaco, editor, true , "Underline", [ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_U ], "<u>", "</u>") addWrapperCommand(monaco, editor, true , "Strikethrough", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_S ], "~~", "~~") addWrapperCommand(monaco, editor, true , "Code", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_M ], "`", "`") addWrapperCommand(monaco, editor, false, "Link-1", [ monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_L ], "[", "]()") addWrapperCommand(monaco, editor, false, "Link-2", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.KEY_L ], "[](", ")") }この
setupShortcutKeys
はmounted
のときに呼び出す。一応、this.monaco
はmouted
の前に設定されることになっている(実際はここでチェックする必要はない)。Editor.vuemounted () { var editor = this.$refs.editor.getEditor() editor.onDidScrollChange(this.handleScroll) if (this.monaco) { setupShortcutKeys(this.monaco, editor) } },では、ヘッダを追加するショートカット・コマンドも追加してみる。今度は行頭に
##
等をつけるというもの。元々ある場合は差し替える動作が自然だろう。同じように登録用関数を定義する。Editor.vuefunction generateHeaderCommand(monaco, editor, startText) { return () => { var sels = editor.getSelections() if (sels == null) { sels = [editor.getSelection()] } sels.forEach(selection => { var m = editor.getModel().findNextMatch("^(#+ )", { lineNumber: selection.startLineNumber, column: 1 }, true, false, null, true) if (m != null) { if (m.range.startLineNumber == selection.startLineNumber && m.range.startColumn == 1) { if (m.matches[1] == startText) { return } editor.getModel().pushEditOperations([], [ { range: { startLineNumber: selection.startLineNumber, startColumn: 1, endLineNumber: selection.startLineNumber, endColumn: m.matches[1].length + 1 }, text: '' } ]) } } editor.getModel().pushEditOperations([], [ { range: { startLineNumber: selection.startLineNumber, startColumn: 1, endLineNumber: selection.startLineNumber, endColumn: 1 }, text: startText } ]) }) return null } } function addHeaderCommand(monaco, editor, context, label, keybindings, startText) { actionCommand[label] = generateHeaderCommand(monaco, editor, startText) editor.addAction({ id: 'markdwon-'+(cmdid++), label: label, keybindings: keybindings, contextMenuGroupId: context && 'navigation', contextMenuOrder: context && 1.5, run: actionCommand[label] }) }同じ文字列があるかどうかは
editor.getModel().findNextMatch(...)
を使う。正規表現も使えるので便利。見つからなかった場合、null
が返る。ただし、スタート位置を指定できるが最後まで行くと先頭に戻る動作をする模様。そこで、見つかった場合は同じ行の先頭かどうかを確認する。同じ行の先頭だった場合は一旦削除する。今と同じなら何もしない。これをショートカット・コマンドとして登録しよう。
Editor.vuefunction setupShortcutKeys(monaco, editor) { ... addHeaderCommand (monaco, editor, false, "Header 1", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.KEY_1 ], "# ") addHeaderCommand (monaco, editor, false, "Header 2", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.KEY_2 ], "## ") addHeaderCommand (monaco, editor, false, "Header 3", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.KEY_3 ], "### ") addHeaderCommand (monaco, editor, false, "Header 4", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.KEY_4 ], "#### ") addHeaderCommand (monaco, editor, false, "Header 5", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.KEY_5 ], "##### ") addHeaderCommand (monaco, editor, false, "Header 6", [ monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.KEY_6 ], "###### ") }ツールアイコン
アイコン
さて、せっかくなのでツールバーを作ってみる。イマイチ使いづらい気もするが、まぁ使わなければ後で消す。まずはアイコンから。長くないので全部載せる。以下のような修正。
- アイコン用の領域を作るが、高さを計算できるようにするため、高さを補正するコードを追加。
- アイコンが押されたときに、エディタに指示。エディタは
action
というメソッドを用意しておく。MarkdownPane.vue<template> <splitpanes horizontal :style="{ height: height, overflow: 'hidden' }" @resized="resizedPane($event)"> <pane :size="toobarSize"> <div style="margin-left: 8px"> <v-btn :style="{ marginTop: btnMargin }" class="btn-item" color="primary" fab x-small @click="click('Bold')"><font-awesome-icon icon="bold" /></v-btn> <v-btn :style="{ marginTop: btnMargin }" class="btn-item" color="primary" fab x-small @click="click('Italic')"><font-awesome-icon icon="italic" /></v-btn> <v-btn :style="{ marginTop: btnMargin }" class="btn-item" color="primary" fab x-small @click="click('Underline')"><font-awesome-icon icon="underline" /></v-btn> <v-btn :style="{ marginTop: btnMargin }" class="btn-item" color="primary" fab x-small @click="click('Strikethrough')"><font-awesome-icon icon="strikethrough" /></v-btn> <v-btn :style="{ marginTop: btnMargin }" class="btn-item" color="primary" fab x-small @click="click('Code')"><font-awesome-icon icon="code" /></v-btn> </div> </pane> <pane :size="editorSize"> <splitpanes :style="{ overflow: 'hidden' }" @resized="resizedPane($event)"> <pane class="pane-editor" ref="epane" size="55"> <MarkdownEditor ref="editor" @onScrollUpdatedViewer="onScrollUpdatedViewer" /> </pane> <pane class="pane-view" ref="vpane"> <MarkdownViewer ref="viewer" @onScrollUpdatedEditor="onScrollUpdatedEditor" /> </pane> </splitpanes> </pane> </splitpanes> </template> <script> import MarkdownEditor from './markdown/Editor' import MarkdownViewer from './markdown/Viewer' import { Splitpanes, Pane } from 'splitpanes' import 'splitpanes/dist/splitpanes.css' export default { name: 'MarkdownPane', components: { MarkdownEditor, MarkdownViewer, Splitpanes, Pane, }, data: () => ({ toolbarPx: 60, }), methods: { click (name) { this.$refs.editor.action(name); }, resizedPane () { this.$nextTick(() => { this.$refs.editor.resize(this.$refs.epane.$el, this.$store.state.windowSize.height - this.toolbarPx) this.$refs.viewer.resize(this.$refs.vpane.$el, this.$store.state.windowSize.height - this.toolbarPx) }) }, gotoTop () { this.$refs.viewer.setScrollTop(null); }, onScrollUpdatedEditor (value) { this.$refs.editor.setScrollTop(value > 1 ? 1 : value); }, onScrollUpdatedViewer (value) { this.$refs.viewer.setScrollTop(value > 1 ? 1 : value); }, }, computed: { height () { return (this.$store.state.windowSize.height - 1) + "px" }, btnMargin () { var top = ((this.toolbarPx - 4 - 32) / 2 + 4) + "px" return top }, toobarSize () { return this.toolbarPx * 100 / this.$store.state.windowSize.height }, editorSize () { return (this.$store.state.windowSize.height - this.toolbarPx) * 100 / this.$store.state.windowSize.height }, } }; </script> <style scoped> .btn-item { margin-left: 2px; margin-right: 2px; } </style>エディタの
action
は以下の通り。Editor.vueaction (name) { if (actionCommand[name] != null) { actionCommand[name]() } },これだけ。さっき
actionCommand
に関数登録しておいたおかげでそのまま使える。クロージャ―になっているので、monaco
とかeditor
とかも内部でちゃんと使えて問題ない。画面上はこんな感じになる。うん、マテリアル。
コマンド追加は同じ方法で可能なので、今後必要に応じて追加する。
おわりに
さて、Markdown Editor もいい感じにできた。というか、これベースにオリジナルで使いやすい Markdown Editor 作るのもアリじゃないか?というくらい個人的には出来がいいな。
次からは本当の意味で SpecTest に対応していこう。ただ、諸事情あってちょっとペースが落ちるかも。。。
ここまでの結果は、以下にコミットしてあります。v0.2.2 としてタグも打ってあります。SpecTest そのものに関しては以下を参照してください。
ではまた次回。
- 投稿日:2020-03-30T02:49:43+09:00
Vue.js + FlaskでWebアプリケーション制作 - herokuにデプロイするまで -
はじめに
フロントエンドをVue.js,バックエンドをFlaskでWebアプリ(SPA)を作成したので,その際の手順をまとめておきました.作成したWebアプリのherokuでの公開方法までまとめます.
個人的に,フロントとバックのデータ連携の方法や,herokuへのデプロイ方法などで,ハマりポイントを量産したので,そのあたりを詳しくまとめたいと思います.当記事の作成にあたり,以下の記事を参考にさせていただきました.
- https://qiita.com/y-tsutsu/items/67f71fc8430a199a3efd
- https://www.sukerou.com/2018/11/flask-restful-rest-apipython-tips.html
Vue.jsとは
WebアプリのUI作成用のjavascriptのフレームワーク.今回はVueのコマンドラインインターフェース(Vue CLI)を使って,開発を行っていきます.
せっかくなので,レスポンシブなWebデザイン1にしたいので,VuetifyというUIライブラリも使っていきます.
- https://jp.vuejs.org/v2/guide/
- https://cli.vuejs.org/
- https://vuetifyjs.com/ja/introduction/why-vuetify/
Flaskとは
Flaskは,プログラミング言語Python用の軽量なウェブアプリケーションフレームワーク.最小限の機能を標準で提供していて,導入も簡単.今回はflask restfulという追加パッケージも利用して,REST APIも実装していきます.
Herokuとは
アプリケーションの開発から実行,運用までのすべてをクラウドで完結できるプラットフォーム.無料プランでも結構色々なことができます.デプロイも割と簡単にできます.
事前準備
予め必要なもの
私の場合,以下のような環境で開発を進めていきます.必要に応じて,以下のものが使える状態から,開発を始めます.
- python : 3.7.5
- pipenv : version 2018.11.26
- Node.js : v13.8.0
- npm : 6.13.7
ディレクトリ構成
まず,以下のようにディレクトリを作成し,
myspa └ backend
myspa
下でgit init
します.$ cd myspa $ git init(注)ちなみに,最終的に作成するディレクトリの構成は次のようになります.herokuにデプロイすることも考慮して,ディレクトリの構成を考えています.
myspa (ルートディレクトリ) ├ frontend (Vue.jsの諸々) ├ backend (Flaskの諸々) ├ dist (フロントエンド側でbuildして作成されるファイル) ├ Pipfile (pipenvのバージョン管理情報) ├ Pipfile.lock (pipenvのバージョン管理情報) ├ Procfile (herokuのデプロイに必要なファイル) ├ requirements.txt └ runtime.txt (herokuのデプロイに必要なファイル)フロントエンドの準備
Veu CLIを使って,フロントエンドの準備をします.
Vue CLIのインストール
npmを使って,Vue CLIをグローバルにインストールします.2
$ npm install -g @vue/cli $ vue --version @vue/cli 4.2.3Vueプロジェクトの作成
Vue CLIを使って新規にvueのプロジェクトを作成します.最終的なディレクトリ構造を踏まえて,frontendという名前で作成しておきます.
vue create プロジェクト名
を実行すると,対話形式で細かな設定を確認されます.
後々使うことになるので,2番目の質問Check the features needed for your project
でBabel,Router,Vuex,Linterを追加してあります.myspa/$ vue create frontend Vue CLI v4.2.3 ? Please pick a preset: Manually select features ? Check the features needed for your project: Babel, Router, Vuex, Linter ? Use history mode for router? (Requires proper server setup for index fallback in production) Yes ? Pick a linter / formatter config: Standard ? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? Noここまで実行すると,
myspa
下にfrontend
というディレクトリが作成されているはずです.Vuetifyの追加
Vueコマンド使って,Vuetifyを加えます.frontendディレクトリ下に移動して,以下のコマンドを実行します.インストール途中でプリセットを聞かれるのですが,Defaultを選択しておけば大丈夫です.
myspa/frontend/vue add vuetify ? Choose a preset: Default (recommended)configなどの設定
ここまで実行すると,frontendディレクトリは以下のようになっているはずです.
frontend ├ node_modules ├ public ├ package.json ├ vue.config.js ├ .gitignore └ その他諸々のファイルここで,
package.json
とvue.config.js
,.gitignore
を以下のように編集します.package.jsonの編集
元々の
package.json
のscripts
部分を以下のように変更します.package.json"scripts": { "serve": "vue-cli-service serve", - "build": "vue-cli-service build", + "build": "vue-cli-service build --dest ../dist", "lint": "vue-cli-service lint" },これにより,
npm run build
(後で出てくる)を実行した際に,dist
ディレクトリ(バックエンド側が読み込むファイル類が格納されるフォルダ)がmyspa
の直下に生成されるようになります.vue.config.jsの編集
元々の
vue.config.js
を以下のように,変更します.vue.config.jsmodule.exports = { transpileDependencies: [ 'vuetify' ], + assetsDir: 'static', + devServer: { + port: 8080, + host: '127.0.0.1' + } }
devServer
の設定を変更したことで,npm run serve
を実行した際に http://127.0.0.1:8080/ に簡易サーバが立つようになります..gitignoreの編集
元々の
.gitignore
から,/dist
の行を消します..gitignore.DS_Store node_modules - /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log*今回使用するherokuは,gitを使ってリモートレポジトリにプッシュします.なので,
/dist
が.gitignore
に登録されていると,herokuにデプロイするときに,エラーを吐くようになってしまいます.簡易サーバーの起動
ここまで,出来たら開発用の簡易サーバーが立ち上がるようになっています.frontend下で以下のコードを実行します.
myspa/frontend/$ npm run serve
http://127.0.0.1:8080/ にアクセスすると,以下のような画面が立ち上がっているはずです.
ちなみに,この画面はVuetifyをインストールしたときに作成される初期画面です.ひとまず,ここでフロントエンドの準備は区切ります.バックエンドの準備
pipenvで新規プロジェクトの初期化
myspa
下で以下のコマンドを実行して,pipenvのプロジェクトの初期化を行います.myspa/$ pipenv --python 3.7.5これを実行すると,
myspa
下にPipfile
が生成されます.flaskのインストール
flaskと追加パッケージであるflask restfulをインストール.
myspa
下で以下を実行します.myspa/$ pipenv install flask $ pipenv install flask-restfulflaskアプリの作成
myspa
下にbackend
というディレクトリを作成します.現在のディレクトリ構成myspa ├ frontend ├ backend (今作成したディレクトリ) ├ Pipfile └ Pipfile.lock
backend
に移動し,main.py
を作成します.中身は,公式ドキュメント( https://a2c.bitbucket.io/flask/quickstart.html#id2 )に詳しく書いてあります.日本語でも書かれてるので,かなりわかりやすいと思います.
static_folder=...
とtemplate_folder=...
のところは,npm run build
を実行して作成されるdist
ディレクトリに合わせます.フロントエンドの準備をする際に,dist
はmyspa
直下に生成されるように設定したので,今回はtemplate_folder='../dist'
としてあります.backend/main.pyfrom flask import Flask, render_template import os app = Flask(__name__, static_folder='../dist/static', template_folder='../dist') @app.route('/', defaults={'path': ''}) @app.route('/<path:path>') def index(path): return render_template('index.html') if __name__ == '__main__': app.run()ここで,バックエンドの準備も一旦区切ります.
フロントエンドとバックエンドを繋げる
frontend/
内のディレクトリの構成を少し変更します.現在は,今のディレクトリ構成frontend/ ├ public │ ├ favicon.ico │ └ index.html └ その他諸々となっていますが,
変更後のディレクトリ構成frontend/ ├ public │ ├ static │ │ └ favicon.ico │ └ index.html └ その他諸々に変更します.これに伴って,
public
内のindex.html
の参照箇所も変更しておきます.frontend/public/index.html<meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> - <link rel="icon" href="<%= BASE_URL %>favicon.ico"> + <link rel="icon" href="<%= BASE_URL %>static/favicon.ico"> <title><%= htmlWebpackPlugin.options.title %></title> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css"> </head>フロントエンドのコードをBuildする
frontend
に移動し,以下を実行.ビルドに成功すると,myspa
直下にdist
ディレクトリが生成されます.frontend/$ npm run build
FlaskのWebサーバを起動
次にバックエンド側でWebサーバを立てて,フロントエンドで作成したWebページが読み込みます.まず,
Pipfile
にサーバ起動用のスクリプトを登録しておきます.myspa/Pipfile+ [scripts] + start = "python backend/main.py"
これをしておくと,いちいち
pipenv run python backend/main.py
を打たずに,pipenv run start
を実行するだけで,Flaskサーバが立ち上がります.
myspa
下でサーバを立ち上げると,vueで作成したWebページが見れるようになります.myspa/$ pipenv run start
http://127.0.0.1:5000/ にアクセスすると,先程フロントエンドで読み込んだページと同じものが表示されています.ひとまず,これでフロントエンドとバックエンドが連携した開発環境の構築が出来ました.
Webアプリの改造
せっかくローカルで色々といじれる環境が出来たので,少しアプリに手を加えます.あまり複雑なものを作成するのもアレなので,ユーザーがブラウザ上で入力した文字列をカウントするアプリにしようと思います.
フロントエンドとバックエンドのデータ連携もしたいので,
- 入力された文字列をbackendに送る
- backendで文字列の長さをカウント
- frontendに結果を送る
というような構造にしたいと思います.(こんなのわざわざbackendで処理しなくてもいいんですけどね...笑)
ただ,ここは本筋じゃないので軽めの説明にします.フロントエンドの変更
今回はfrontendとbackendのデータ連携をREST APIで行います.なので,HTTP通信を実装するためにaxiosをローカルにインストールします.
frontend/$ npm install -D axios次に,
frontend/src/
下のApp.vue
を以下のように変更する.Vue.jsでは,この.vue拡張子のファイルに手を加えて,見た目を変えていきます.frontend/src/App.vue<template> <v-app> <v-container fluid> <v-row align="start" justify="center"> <v-col cols="10"> <v-textarea outlined name="input-7-4" label="テキストを入力してください" v-model="InputText" ></v-textarea> </v-col> <v-col cols="2"> <v-btn outlined @click="SendData"> 文字数をカウント </v-btn> </v-col> </v-row> <v-row align="start" justify="center"> <v-col cols="6"> <v-card max-width="450" class="mx-auto" > <v-toolbar dark > <v-toolbar-title>Result</v-toolbar-title> </v-toolbar> <v-list three-line> <template v-for="(item, index) in items"> <v-list-item :key="item.title" > <v-list-item-content> <v-list-item-title >{{ item.count }}文字です</v-list-item-title> <v-list-item-subtitle> {{ item.text }} </v-list-item-subtitle> </v-list-item-content> </v-list-item> <v-divider :key="index" :inset="item.inset" ></v-divider> </template> </v-list> </v-card> </v-col> </v-row> </v-container> </v-app> </template> <script> import axios from 'axios' export default { name: 'App', data () { return { // 入力データ InputText: '', TextLength: null, items: [] } }, methods: { SendData: function () { var data = { text: this.InputText } axios .post('/api/post', data) .then(response => { this.items.push(response.data) }) .catch(err => { alert('APIサーバと接続できません') err = null }) } } } </script>
frontend
下でnpm run build
をして,dist
ディレクトリを更新.バックエンド側の変更
まず,以下の
api.py
を新たに作成します.backend/api.pyfrom flask import Blueprint, jsonify, request, session from flask_restful import Api, Resource import json # postされたテキストをカウントするapi(POSTメソッド) text_count_bp = Blueprint('text_count', __name__, url_prefix='/api/post') class TextCount(Resource): def post(self): # postされたデータを読み込み input_data = request.json # 入力文字列の文字数をカウント result_data = {'text':input_data['text'], 'count':len(input_data['text'])} return jsonify(result_data) text_count = Api(text_count_bp) text_count.add_resource(TextCount, '')次に,
main.py
を次のように変更します.backend/main.pyfrom flask import Flask, render_template from api import text_count_bp app = Flask(__name__, static_folder='../dist/static', template_folder='../dist') app.register_blueprint(text_count_bp) @app.route('/', defaults={'path': ''}) @app.route('/<path:path>') def index(path): return render_template('index.html') if __name__ == '__main__': app.run()ここで,
myspa
下でpipenv run start
を実行して, http://127.0.0.1:5000/ にアクセスすると,ページが変更されているはずです.
Herokuへのデプロイ
最後に,ここまでで完成したアプリをHerokuにデプロイします.アプリのデプロイの手順は大まかに以下のようになります.
- Herokuのアカウントを取得
- gunicornのインストール
- デプロイに必要な設定ファイル(Procfile,runtime.txt,requirements.txt)の作成
- heroku cliをインストール
- herokuアプリの作成
- herokuのリモートにプッシュ
herokuのアカウント取得
herokuの公式ページ( https://jp.heroku.com/ )に行き,アカウントを作成します.クレジットカードの情報などは入力せずに作成することができます.
gunicornのインストール
gunicornはWSGIサーバ3の1つです.これを利用することで,heroku上でも作成したアプリケーションを動かせるようになります.
flaskと同様に,gunicornもpipenvでインストールします.
myspa/pipenv install gunicorn
設定ファイルの作成
myspa
下にProcfile
を作成します.今回は,backend下のmain.py
にflaskアプリを記述しているので,backend.main:app
としてます.ここで,--pythonpath
はmain.py
が置かれているディレクトリに必ず設定してください.このオプションが無いと,api.py
などの自作モジュールを読み込んでくれなくなります.myspa/Procfileweb: gunicorn -b 0.0.0.0:$PORT --pythonpath backend backend.main:app次に,runtime.txtを作成します.ここには,使用しているpythonのバージョンを記述しておきます.
myspa/runtime.txtpython-3.7.5最後に,requirements.txtを作成します.ここには,インストールしたpythonパッケージのバージョン情報を記載します.手書きしてもいいですが,以下のコマンドで作成してしまったほうが楽です.
myspa/$ pipenv run pip freeze > requirements.txt実行すると,以下のようなファイルが作成されます.
myspa/requirements.txtaniso8601==8.0.0 click==7.1.1 Flask==1.1.1 Flask-RESTful==0.3.8 gunicorn==20.0.4 itsdangerous==1.1.0 Jinja2==2.11.1 MarkupSafe==1.1.1 pytz==2019.3 six==1.14.0 Werkzeug==1.0.0heroku cliのインストール
heroku cliはコマンドラインからheroku関連の操作をすることができるツールです.brewコマンドが使える方は,以下のコマンドインストールしてください.
$ brew tap heroku/brew && brew install herokubrewコマンドが使えない方は,公式のダウンロードページ( https://devcenter.heroku.com/articles/heroku-cli#download-and-install )からインストールしてください.
herokuアプリの作成
コマンドライン上でherokuにログインします.
$ heroku login
次に,herokuアプリを作成します.ここで付けた名前は,アプリのURLにも載ります.
$ heroku create 好きな名前
私の場合は,
test-app-nonta
という名前でアプリを作成したので,$ heroku create test-app-nonta Creating ⬢ test-app-nonta... done https://test-app-nonta.herokuapp.com/ | https://git.heroku.com/test-app-nonta.gitのように,アプリがデプロイされるURLとgitのリモートレポジトリを返してくれます.
最後に,herokuが返してくれたリモートレポジトリの設定をgitの設定に加えておきます.$ git remote add heroku https://git.heroku.com/アプリ名.git
これで,デプロイの準備は終了です.
Herokuへプッシュ
最後にremoteにプッシュして,デプロイ完了です.まず,コミットします.
myspa/$ git add . $ git commit -m "コミットメッセージ"herokuリモートにプッシュ
myspa/$ git push heroku master
デプロイ先のURLをブラウザに打ち込むか,
$ heroku open
を実行することで,デプロイしたアプリが開きます.ちゃんと動いてることが確認できますね.
おわりに
割と簡単に,Vue.js + Flask + HerokuでWebアプリを作れたのではないかと思います.今回は簡単な動作をするアプリケーションしか実装しませんでしたが,少しいじるだけで色々な機能を追加で実装することができると思います.
今後は,Vue+flaskアプリにgoogle firebaseのAuthenticationを使ったログイン機能や,leaflet.jsでの地図描画機能を追加する方法なんかもまとめていこうと思います.個人的にハマった・詰まったポイント
最後に私自身がハマって,時間を取られたポイントをまとめておこうと思います.
1. distディレクトリの位置
herokuは,gitの機能を使ってデプロイを行います.vue cliでプロジェクト作成すると,自動で
.gitignore
も作成され,その中に/dist
も含まれてしまっています.このまま何もしないと,herokuのリモートレポジトリにdistディレクトリはプッシュされないので,.gitignore
から/dist
を外すことと,dist
をルートディレクトリに生成するように設定を変更することは忘れないようにしてください.2. Procfileのpythonpath
ここには,かなりハマりました.Heroku(Procfile)の仕様というより,gunicornの仕様によるものです.今回のように複数ファイルに分けてを記述してる場合,
--pythonpath
の設定をしないと自作モジュールを正しく読み込んでくれなくなります.
一応,herokuにデプロイする前に,ローカルでgunicornサーバを立てて,動作するか確認しておくといいかもしれないですね.以下のコマンドでgunicornのサーバをローカルでて立てれます.( http://127.0.0.1:8000 にサーバが立ってるようですね)myspa/$ pipenv run gunicorn --pythonpath backend backend.main:app [2020-03-30 02:42:23 +0900] [93950] [INFO] Starting gunicorn 20.0.4 [2020-03-30 02:42:23 +0900] [93950] [INFO] Listening at: http://127.0.0.1:8000 (93950)
端末の画面サイズを判別することで,PCからアクセスしても,スマホやタブレットからアクセスしても,綺麗にUIを表示させることができるデザイン. ↩
最初はグローバルではなくローカルにVue CLIをインストールしようとしたのですが,脆弱性の警告が出てきてしまい上手くいきませんでした.なので,今回はグローバルにインストールしておきます. ↩
WSGI(ウィズギー)とは,Web Server Gateway Interfaceという「アプリケーションとWebサーバをつなぐインタフェースをPythonで定義したもの」です.WSGIサーバは,このWSGIの仕様に則って作成されたアプリケーションを動かすことのできるサーバを指します.WSGIやWSGIサーバについては, https://www.youtube.com/watch?v=S-InxJA5NOg&t=1578s で非常にわかりやすく説明しています. ↩