- 投稿日:2021-03-14T22:56:01+09:00
rails&vueでherokuにデプロイする際に参考になったこと
はじめに
Rails 6.0.3.5
ruby 2.6.5
yarn 1.22.10
にてデプロイを実行した。アプリ概要
投稿サイト
・jwtによるログインログアウト機能
・投稿機能(ログイン後投稿可、投稿者のみ削除編集可)
・プロフィール編集機能エラー内容
デプロイは成功し、
ログイン機能は正常に動いたのだが、
POST機能、EDIT機能を行う際に、
検証ツールのconsoleにて、401 Unauthorizedエラーが出現、
なぜ、ログインしているにも関わらず401が出現するのか、
protect_from_forgery with: :null_sessionになっていることを確認し、
skip_before_action :verify_authenticity_tokenを追加。
すると、今度は、
500 (Internal Server Error)が出現、
$ heroku logsにてエラー内容を確認したところ、
undefined method `post' for nil:NilClassというエラー内容、
また、プロフィール編集時のエラーは、undefined method `update' for nil:NilClassとなりました、、、
原因検索1
ローカル環境で動いているため、
[post]や[update]が定義されていないということは考えにくい。def create post = current_user.posts.create!(post_params) #ここのcurrent_user render json: post, serializer: postSerializer endpostする前のcurrent_userが渡っていなと推測。
なぜcurrent_userが取れていないのか、
local環境とproduction環境の違いをチェック。config/enviroments/development.rbとproduction.rbの違いを確認した。
認証に関わりそうな部分はなく、
特にここが原因ではないみたい。原因検索2
jwtが送信できているかを、ブラウザ検証ツールのnetworkタブで確認する.
Authorization: Bearer にて、トークンが到達していることを確認。原因検索3
ローカル環境で、問題なく動いているため、
再度、herokuにて問題がないことを確認、
herokuのsettings/Reveal Config Varsにて、、、、、master_keyの設定をしていませんでした。。。
secret_key_baseを設定し安心し切っていましたが、
思わぬところに原因がありました。補足
rails & vueのデプロイ
(herokuにpushした後)につきましては、
ログアウト → ログインをしてから、挙動を確かめるようにしましょう。最後に
初心者のうちは思いもよらないところにミスがある可能性が高いので、
深読みしすぎず、やるべきことをしっかりやってあるかの確認を、
怠らないように気をつけたいと思います。ご視聴いただきありがとうございます。
誰かの役に立てば幸いです。
- 投稿日:2021-03-14T21:08:51+09:00
vue.jsに触れてみる
前書き
vue.jsをする機会ができたので軽く触れていこうと思います。
とりあえずプロジェクト作成から始めていきます。プロジェクト作成
vue-cliのインストールから始めます。
その前にvue-cliとはVue CLIとは CLIはCommnad Line Interfaceの略で、コマンドラインを使ってvue.jsで開発を行うための前準備を支援してくれるツールです。
らしいです。
早速書きコマンドでインストール
npm install -g vue-cli
エラーが発生しました。
どうやらC:\Users\Admin\AppData\Roaming\npm\node_modules\vue-cli\bin\vueってファイルが既にあるらしく怒られています。
一度対象ファイルを削除してもう一度インストールコマンド実行
でもまた同じエラー...npm install -g vue-cli --force上記コマンドで無理やりインストールできるそうなのでこちらでインストール
次にプロジェクトを下記コマンドで作成します。
vue init webpack [project名]
作成できたみたいです。
下記コマンドでVue.jsのWebサーバが起動する。その後http://localhost:8080を開きます。
npm run dev
上記画面が表示され無事動作確認ができました。
- 投稿日:2021-03-14T20:50:41+09:00
Google Books APIsでブックカバー検索
一年前に作ったブックレビューアプリ、検索の精度を上げてポートフォリオを強化しようと思ったら2021年現在あまりにも情報が溢れているためポートフォリオとしては価値が無いと思ったので途中だけど供養します。
Nuxt-bookreview
↑本のタイトルを検索し表紙を表示するところまで作ってます。
表示された画像はボタンになっていて、押すとその画像だけが表示されます。
特に意味はないのですが、これから色々と遊んでみようと思います。見た目
<template> <div class=""> <div class="text-center"> <input class="ex-form" v-model="bookTitle" /> <button class="ex-btn_default" @click="search">検索</button> </div> <img style="width:200px" class="m-auto mt-48 justify-center" v-if="buttonImage" v-bind:src="buttonImage" /> <div v-else> <div style="flex-wrap" class="flex-wrap justify-center"> <div class="m-5 inline-flex" v-for="(booksArray, key) in booksArray" :key="key"> <img class="inline-block test" style="width: 150px; min-width: 150px" @click="doAction(booksArray)" v-bind:src="booksArray" /> </div> </div> </div> </div> </template>スクリプト
<script> const axios = require("axios"); let url = "https://www.googleapis.com/books/v1/volumes?&maxResults=30&q="; export default { data: function() { return { json_data: {}, items: [], bookTitle: "", booksArray: [], buttonImage: "" }; }, methods: { doAction: function(booksArray) { this.buttonImage = booksArray; }, list: function() { let booksArray = []; let book = []; for (let i = 0; i < 30; i++) { if (!this.items[i].volumeInfo.imageLinks) { continue; } book = this.items[i].volumeInfo.imageLinks.thumbnail; booksArray[i] = book; this.booksArray.push(booksArray[i]); console.log(booksArray); } }, search: function() { this.booksArray = []; this.buttonImage = ""; axios .get(url + this.bookTitle) .then(response => ((this.items = response.data.items), this.list())); } } }; </script>
- 入力フォームにタイトルを入力
- 検索ボタンを押すとsearchメソッドが発火
- axios.getでjson取得
- レスポンスを変数itemsに格納
- listメソッドを発火
listメソッドについて
items以下に複数のvolumeinfoカラムがある。for文でそれぞれ展開し、配列booksArrayに格納する。
imageLinks以下を持たないカラムもあるので、その場合はスキップする。listメソッドの他の書き方
whileを使う。
list: function() { this.booksArray = []; let booksArray = []; let book = []; let i = -1; while (true) { i += 1; if (!this.items[i].volumeInfo.imageLinks) { continue; } book = this.items[i].volumeInfo.imageLinks.thumbnail; booksArray[i] = book; this.booksArray.push(booksArray[i]); if (i == 30) { break; } } },CSS
基本はTailwindcssだが、ボタンやフォームなどのパーツはcssで書く。
<style> .t-book-w { max-width: 150%; } .test { display: flex; flex-wrap: wrap; } /* 入力フォーム */ .ex-form { height: 36px; background-color: #efefed; border-radius: 5px; font-family: "Noto Sans JP", sans-serif; line-height: 1.5; letter-spacing: 0.05em; padding: 8px 16px; outline: none; font-size: 14pt; color: #444444 !important; margin-top: 50px; width: 50% !important; } .ex-form:focus { box-shadow: 0 0 0 2px #ebd800; background-color: #ffffff; } /* ボタン */ .ex-btn_default { border-radius: 5px; background-color: #98f0ff; min-height: 40px; min-width: 160px; width: 160px; font-family: "M PLUS Rounded 1c", sans-serif; font-weight: Bold; font-size: 18px; letter-spacing: 0.05em; filter: drop-shadow(1px 3px 5px rgba(68, 68, 68, 0.1)); outline: none; border-radius: 30px; color: white; padding: 1px 30px 1px 30px; } .ex-btn_default:hover { background-color: #77e6fa; filter: drop-shadow(1px 3px 5px rgba(68, 68, 68, 0.2)); } .ex-btn_default:focus { outline: none; } </style>感想
JavaScript書くの楽しい。
アニメーションとか入れて遊んでみよう、という気持ちになってきました。
- 投稿日:2021-03-14T20:50:41+09:00
Google Books APIsでブックカバー検索 / Nuxt
一年前に作ったブックレビューアプリ、検索の精度を上げてポートフォリオを強化しようと思ったら2021年現在あまりにも情報が溢れているためポートフォリオとしては価値が無いと思ったので途中だけど供養します。
Nuxt-bookreview
↑本のタイトルを検索し表紙を表示するところまで作ってます。
表示された画像はボタンになっていて、押すとその画像だけが表示されます。
特に意味はないのですが、これから色々と遊んでみようと思います。見た目
<template> <div class=""> <div class="text-center"> <input class="ex-form" v-model="bookTitle" /> <button class="ex-btn_default" @click="search">検索</button> </div> <img style="width:200px" class="m-auto mt-48 justify-center" v-if="buttonImage" v-bind:src="buttonImage" /> <div v-else> <div style="flex-wrap" class="flex-wrap justify-center"> <div class="m-5 inline-flex" v-for="(booksArray, key) in booksArray" :key="key"> <img class="inline-block test" style="width: 150px; min-width: 150px" @click="doAction(booksArray)" v-bind:src="booksArray" /> </div> </div> </div> </div> </template>スクリプト
<script> const axios = require("axios"); let url = "https://www.googleapis.com/books/v1/volumes?&maxResults=30&q="; export default { data: function() { return { json_data: {}, items: [], bookTitle: "", booksArray: [], buttonImage: "" }; }, methods: { doAction: function(booksArray) { this.buttonImage = booksArray; }, list: function() { let booksArray = []; let book = []; for (let i = 0; i < 30; i++) { if (!this.items[i].volumeInfo.imageLinks) { continue; } book = this.items[i].volumeInfo.imageLinks.thumbnail; booksArray[i] = book; this.booksArray.push(booksArray[i]); console.log(booksArray); } }, search: function() { this.booksArray = []; this.buttonImage = ""; axios .get(url + this.bookTitle) .then(response => ((this.items = response.data.items), this.list())); } } }; </script>
- 入力フォームにタイトルを入力
- 検索ボタンを押すとsearchメソッドが発火
- axios.getでjson取得
- レスポンスを変数itemsに格納
- listメソッドを発火
listメソッドについて
items以下に複数のvolumeinfoカラムがある。for文でそれぞれ展開し、配列booksArrayに格納する。
imageLinks以下を持たないカラムもあるので、その場合はスキップする。listメソッドの他の書き方
whileを使う。
list: function() { this.booksArray = []; let booksArray = []; let book = []; let i = -1; while (true) { i += 1; if (!this.items[i].volumeInfo.imageLinks) { continue; } book = this.items[i].volumeInfo.imageLinks.thumbnail; booksArray[i] = book; this.booksArray.push(booksArray[i]); if (i == 30) { break; } } },CSS
基本はTailwindcssだが、ボタンやフォームなどのパーツはcssで書く。
<style> .t-book-w { max-width: 150%; } .test { display: flex; flex-wrap: wrap; } /* 入力フォーム */ .ex-form { height: 36px; background-color: #efefed; border-radius: 5px; font-family: "Noto Sans JP", sans-serif; line-height: 1.5; letter-spacing: 0.05em; padding: 8px 16px; outline: none; font-size: 14pt; color: #444444 !important; margin-top: 50px; width: 50% !important; } .ex-form:focus { box-shadow: 0 0 0 2px #ebd800; background-color: #ffffff; } /* ボタン */ .ex-btn_default { border-radius: 5px; background-color: #98f0ff; min-height: 40px; min-width: 160px; width: 160px; font-family: "M PLUS Rounded 1c", sans-serif; font-weight: Bold; font-size: 18px; letter-spacing: 0.05em; filter: drop-shadow(1px 3px 5px rgba(68, 68, 68, 0.1)); outline: none; border-radius: 30px; color: white; padding: 1px 30px 1px 30px; } .ex-btn_default:hover { background-color: #77e6fa; filter: drop-shadow(1px 3px 5px rgba(68, 68, 68, 0.2)); } .ex-btn_default:focus { outline: none; } </style>感想
JavaScript書くの楽しい。
アニメーションとか入れて遊んでみよう、という気持ちになってきました。
- 投稿日:2021-03-14T20:50:41+09:00
Google Books APIsで本や漫画の表紙画像を検索 / Nuxt
一年前に作ったブックレビューアプリの検索の精度を上げてポートフォリオを強化しようと思ったら2021年現在あまりにも関連情報が溢れているため、完成させてもポートフォリオとしては価値が無いと思ったので途中だけど供養します。
Nuxt-bookreview
↑本のタイトルを検索し表紙を表示するところまで作ってます。
表示された画像はボタンになっていて、押すとその画像だけが表示されます。
特に意味はないのですが、これから色々と遊んでみようと思います。見た目
<template> <div class=""> <div class="text-center"> <input class="ex-form" v-model="bookTitle" /> <button class="ex-btn_default" @click="search">検索</button> </div> <img style="width:200px" class="m-auto mt-48 justify-center" v-if="buttonImage" v-bind:src="buttonImage" /> <div v-else> <div class="flex-wrap justify-center"> <div class="m-5 inline-flex" v-for="(booksArray, key) in booksArray" :key="key"> <img class="inline-block ex-test" style="width: 150px; min-width: 150px" @click="doAction(booksArray)" v-bind:src="booksArray" /> </div> </div> </div> </div> </template>スクリプト
<script> const axios = require("axios"); let url = "https://www.googleapis.com/books/v1/volumes?&maxResults=30&q="; export default { data: function() { return { json_data: {}, items: [], bookTitle: "", booksArray: [], buttonImage: "" }; }, methods: { doAction: function(booksArray) { this.buttonImage = booksArray; }, list: function() { let booksArray = []; let book = []; for (let i = 0; i < 30; i++) { if (!this.items[i].volumeInfo.imageLinks) { continue; } book = this.items[i].volumeInfo.imageLinks.thumbnail; booksArray[i] = book; this.booksArray.push(booksArray[i]); console.log(booksArray); } }, search: function() { this.booksArray = []; this.buttonImage = ""; axios .get(url + this.bookTitle) .then(response => ((this.items = response.data.items), this.list())); } } }; </script>
- 入力フォームにタイトルを入力
- 検索ボタンを押すとsearchメソッドが発火
- axios.getでjson取得
- レスポンスを変数itemsに格納
- listメソッドを発火
listメソッドについて
items以下に複数のvolumeinfoカラムがある。for文でそれぞれ展開し、配列booksArrayに格納する。
imageLinks以下を持たないカラムもあるので、その場合はスキップする。listメソッドの他の書き方
whileを使う。
list: function() { this.booksArray = []; let booksArray = []; let book = []; let i = -1; while (true) { i += 1; if (!this.items[i].volumeInfo.imageLinks) { continue; } book = this.items[i].volumeInfo.imageLinks.thumbnail; booksArray[i] = book; this.booksArray.push(booksArray[i]); if (i == 30) { break; } } },CSS
基本はTailwindcssだが、ボタンやフォームなどのパーツはcssで書く。
Tailwindcssと差別化するために自作のcssは名前に"ex-"をつける。<style> .t-book-w { max-width: 150%; } .ex-test { display: flex; flex-wrap: wrap; } /* 入力フォーム */ .ex-form { height: 36px; background-color: #efefed; border-radius: 5px; font-family: "Noto Sans JP", sans-serif; line-height: 1.5; letter-spacing: 0.05em; padding: 8px 16px; outline: none; font-size: 14pt; color: #444444 !important; margin-top: 50px; width: 50% !important; } .ex-form:focus { box-shadow: 0 0 0 2px #ebd800; background-color: #ffffff; } /* ボタン */ .ex-btn_default { border-radius: 5px; background-color: #98f0ff; min-height: 40px; min-width: 160px; width: 160px; font-family: "M PLUS Rounded 1c", sans-serif; font-weight: Bold; font-size: 18px; letter-spacing: 0.05em; filter: drop-shadow(1px 3px 5px rgba(68, 68, 68, 0.1)); outline: none; border-radius: 30px; color: white; padding: 1px 30px 1px 30px; } .ex-btn_default:hover { background-color: #77e6fa; filter: drop-shadow(1px 3px 5px rgba(68, 68, 68, 0.2)); } .ex-btn_default:focus { outline: none; } </style>感想
JavaScript書くの楽しい。
アニメーションとか入れて遊んでみよう、という気持ちになってきました。
- 投稿日:2021-03-14T19:33:18+09:00
[Vue]パスの変更を監視する
- 投稿日:2021-03-14T19:23:21+09:00
vue-cliでのVueインスタンスの記述について
vue-cliでのVueインスタンスの記述について
背景
Vue-cliを使うと、Vueと違ってVueインスタンス部分のdataやmethodsなど、
data:
→data()
のように書き方が少し違う(自分はググって出てきた記事等を参照して書いていた)が、こちらは公式のガイド等はないのだろうか
結論
同じ種類のコンポーネントの異なるインスタンス間で、独立した data を扱うために、data はオブジェクトではなく、オブジェクトを返す関数でなくてはならない
コンポーネントの全てのインスタンスが同じデータオブジェクトを参照しているので、1つのリストのタイトルを変えることは、他の全てのリストのタイトルを変えることになる。
よって、CLIの場合は関数にしていないとやはりだめなよう
参考
CDNを使用した際は、
data: {}
でもdata: function() { return {} }
のどちらでもエラーは出ない
この記事は上記の公式をもう少し噛み砕いてマス
- 投稿日:2021-03-14T19:23:21+09:00
【Vue.js】vue-cliでのVueインスタンスの記述について
vue-cliでのVueインスタンスの記述について
背景
Vue-cliを使うと、Vueと違ってVueインスタンス部分のdataやmethodsなど、
data:
→data()
のように書き方が少し違う(自分はググって出てきた記事等を参照して書いていた)が、こちらは公式のガイド等はないのだろうか
結論
同じ種類のコンポーネントの異なるインスタンス間で、独立した data を扱うために、data はオブジェクトではなく、オブジェクトを返す関数でなくてはならない
コンポーネントの全てのインスタンスが同じデータオブジェクトを参照しているので、1つのリストのタイトルを変えることは、他の全てのリストのタイトルを変えることになる。
よって、CLIの場合は関数にしていないとやはりだめなよう
また、CDNの場合もオブジェクトを返す関数にした方がよいよう参考
CDNを使用した際は、
data: {}
でもdata: function() { return {} }
のどちらでもエラーは出ない
この記事は上記の公式をもう少し噛み砕いてマス
- 投稿日:2021-03-14T19:12:18+09:00
.vueファイル
.vueファイル
HTMLのテンプレートとスクリプト・スタイルを一緒にかいておける
いわゆるコンポーネント化ができているということ
App.vue<template> <div id="app"> <img alt="Vue logo" src="./assets/logo.png"> <HelloWorld msg="Welcome to Your Vue.js App"/> </div> </template> <script> import HelloWorld from './components/HelloWorld.vue' export default { name: 'App', components: { HelloWorld } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
- 投稿日:2021-03-14T19:12:18+09:00
【Vue.js】.vueファイル
.vueファイル
HTMLのテンプレートとスクリプト・スタイルを一緒にかいておける
いわゆるコンポーネント化ができているということ
App.vue<template> <div id="app"> <img alt="Vue logo" src="./assets/logo.png"> <HelloWorld msg="Welcome to Your Vue.js App"/> </div> </template> <script> import HelloWorld from './components/HelloWorld.vue' export default { name: 'App', components: { HelloWorld } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
- 投稿日:2021-03-14T19:07:20+09:00
【Vuex】mapState使い方
mapState使い方
目的
Vuexで値を表示する際、
$store.state
を省略して記述したい実装
vuexからmapStateを抽出する
import { mapState } from 'vuex'
computedにmapStateを設定
computed: { ...mapState({ //他の算出プロパティと共有する(...)(表示を省略して記述するための設定) gender: 'gender', year: 'year', month: 'month', day: 'day', q1: 'q1', q2: 'q2', q3: 'q3', consultation: 'consultation', }) }全体
<template> <div> <div class="form"> <div class="header"> <p id="step">STEP4</p> <p id="inst">以下の内容をご確認ください</p> </div> <div class="body"> <div> <p class="genre">-性別-</p> <p class="answer">{{ gender }}</p> </div> <div> <p class="genre">-生年月日-</p> <p class="answer">{{ year }}{{ month }}{{ day }}</p> </div> <div> <p class="genre">-現在、生命保険に加入されていますか?-</p> <p class="answer">{{ q1 }}</p> </div> <div> <p class="genre">-現在入院中ですか。または、最近3ヶ月以内に医師の診断・検査の結果、入院・手術をすすめられたことはありますか?-</p> <p class="answer">{{ q2 }}</p> </div> <div> <p class="genre">-過去5年以内に、病気やけがで、手術を受けたことまたは継続して7日以上の入院をしたことはありますか?-</p> <p class="answer">{{ q3 }}</p> </div> <div> <p class="genre">-ご相談内容-</p> <p class="answer">{{ consultation }}</p> </div> </div> </div> <div class="button-group"> <router-link to="/consultation" class="button">前へ戻る ></router-link> <router-link to="/confirmation" class="button">送信 ></router-link> </div> </div> </template> <script> import { mapState } from 'vuex' //vuexからmapStateを抽出する export default { computed: { ...mapState({ //他の算出プロパティと共有する(...)(表示を省略して記述するための設定) gender: 'gender', year: 'year', month: 'month', day: 'day', q1: 'q1', q2: 'q2', q3: 'q3', consultation: 'consultation', }) } } </script> <style scoped lang="scss"> * { // outline: auto; margin:0; padding:0; /*全要素のマージン・パディングをリセット*/ max-width: 100%; } #step{ background: #1e90ff; color: #fff; font-size: 2px; margin: 0; max-width: 40px; padding: 2px; text-align: center; } #inst { font-size: 12px; color: #696969; text-align: center; padding-bottom: 10px; } .header { background: #afeeee; margin: 0%; padding: 0%; line-height: 10px; border-bottom: solid 1px #48d1cc; } .body { font-size: 9px; margin: 10px; padding-bottom: 10px; line-height: 30px; text-align: left; } .genre { color: #1e90ff; text-align: left; line-height:200% } .answer { margin: 0 0 0 10px; } textarea { border: 1px solid #dcdcdc; } .form{ max-width: 80%; margin-top: 80px; margin-left: auto; margin-right: auto; border: 1px solid #48d1cc; border-bottom: 0.5px solid #000; } .button-group{ text-align: center; } .button { background-color: #40e0d0; color: #fff; border: solid 1px #48d1cc; margin-top: 1rem; padding: 5px 10px; border-radius: 0.5rem 0.5rem; text-decoration: none; display:inline-block; margin: 10px; } </style>
- 投稿日:2021-03-14T18:48:31+09:00
Vuexを使って確認画面で値を表示させる
Vuexを使って確認画面で値を表示させる
背景
Vue-cliでアンケートフォーム作成中
Vuexを使って答えた解答を最後、確認画面で表示させたい実装
値を取得する
@change
でメソッド起動this.$store.commit('updateGender', e.target.value)
で値を取得して、mutationsを実行するBasicForm.vue<template> <div> <div class="form"> <div class="header"> <p id="step">STEP1</p> <p id="inst">お客様の情報を入力してください</p> </div> <div class="body"> <p class="genre">-性別-</p> <label><input type="radio" name="gender" value="男性" @change="updateGender">男性</label> <label><input type="radio" name="gender" value="女性" @change="updateGender">女性</label> <p class="genre">-生年月日-</p> <select name="year" id="id_year" @change="updateYear"> <option v-for="(year, key) in years" :key="key">{{ year }}</option> </select>年 <select name="month" id="id_month" @change="updateMonth"> <option v-for="(month, key) in months" :key="key">{{ month }}</option> </select>月 <select name="day" id="id_day" @change="updateDay"> <option v-for="(day, key) in days" :key="key">{{ day }}</option> </select>日 </div> </div> <div class="button-group"> <router-link to="/questionnaire" class="button">次へ進む ></router-link> </div> </div> </template> <script> //省略 methods: { updateGender (e) { this.$store.commit('updateGender', e.target.value) }, updateYear (e) { this.$store.commit('updateYear', e.target.value) }, updateMonth (e) { this.$store.commit('updateMonth', e.target.value) }, updateDay (e) { this.$store.commit('updateDay', e.target.value) }, } } </script>取得した値をVUexストアのstateに格納する
- mutationsが起動して、設定しいたstateのプロパティに値を格納する
index.js"use strict" import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) //どこからでも参照できる唯一の情報源store const store = new Vuex.Store({ state: { gender: '', year: '', month: '', day: '', q1: '', q2: '', q3: '', consultation: '', }, mutations: { // ページ1 updateGender (state, gender) { state.gender = gender }, updateYear (state, year) { state.year = year += '年' }, updateMonth (state, month) { state.month = month += '月' }, updateDay (state, day) { state.day = day += '日' }, // ページ2 updateQ1 (state, q1) { state.q1 = q1 }, updateQ2 (state, q2) { state.q2 = q2 }, updateQ3 (state, q3) { state.q3 = q3 }, // ページ3 updateConsultation (state, consultation) { state.consultation = consultation }, }, }); export default storestateに格納した値をVueコンポーネントで表示させる
- {{}}の中身にstateで設定したプロパティ名を記述する
- mapStateを使用して表示する際の記述を省略している
確認画面.vue<template> <div> <div class="form"> <div class="header"> <p id="step">STEP4</p> <p id="inst">以下の内容をご確認ください</p> </div> <div class="body"> <div> <p class="genre">-性別-</p> <p class="answer">{{ gender }}</p> </div> <div> <p class="genre">-生年月日-</p> <p class="answer">{{ year }}{{ month }}{{ day }}</p> </div> <div> <p class="genre">-現在、生命保険に加入されていますか?-</p> <p class="answer">{{ q1 }}</p> </div> <div> <p class="genre">-現在入院中ですか。または、最近3ヶ月以内に医師の診断・検査の結果、入院・手術をすすめられたことはありますか?-</p> <p class="answer">{{ q2 }}</p> </div> <div> <p class="genre">-過去5年以内に、病気やけがで、手術を受けたことまたは継続して7日以上の入院をしたことはありますか?-</p> <p class="answer">{{ q3 }}</p> </div> <div> <p class="genre">-ご相談内容-</p> <p class="answer">{{ consultation }}</p> </div> </div> </div> <div class="button-group"> <router-link to="/consultation" class="button">前へ戻る ></router-link> <router-link to="/confirmation" class="button">送信 ></router-link> </div> </div> </template> <script> import { mapState } from 'vuex' export default { computed: { ...mapState({ gender: 'gender', year: 'year', month: 'month', day: 'day', q1: 'q1', q2: 'q2', q3: 'q3', consultation: 'consultation', }) } } </script> <style scoped lang="scss"> * { // outline: auto; margin:0; padding:0; /*全要素のマージン・パディングをリセット*/ max-width: 100%; } #step{ background: #1e90ff; color: #fff; font-size: 2px; margin: 0; max-width: 40px; padding: 2px; text-align: center; } #inst { font-size: 12px; color: #696969; text-align: center; padding-bottom: 10px; } .header { background: #afeeee; margin: 0%; padding: 0%; line-height: 10px; border-bottom: solid 1px #48d1cc; } .body { font-size: 9px; margin: 10px; padding-bottom: 10px; line-height: 30px; text-align: left; } .genre { color: #1e90ff; text-align: left; line-height:200% } .answer { margin: 0 0 0 10px; } textarea { border: 1px solid #dcdcdc; } .form{ max-width: 80%; margin-top: 80px; margin-left: auto; margin-right: auto; border: 1px solid #48d1cc; border-bottom: 0.5px solid #000; } .button-group{ text-align: center; } .button { background-color: #40e0d0; color: #fff; border: solid 1px #48d1cc; margin-top: 1rem; padding: 5px 10px; border-radius: 0.5rem 0.5rem; text-decoration: none; display:inline-block; margin: 10px; } </style>おまけ
以下確認画面です。
- 投稿日:2021-03-14T17:58:31+09:00
vue-cliで外部JSファイルを読み込む方法
vue-cliで外部JSファイルを読み込む方法
背景
Vueコンポーネントの年月日のプルダウンを外部ファイルのjsで実装したい
理由
可読性やメンテナンス性が向上するため
大規模なプロジェクトになるほど、こういった構成にしたときのメリットを感じやすい準備
vueCLIのディレクトリのsrcフォルダ直下にutilesという名前のフォルダを作成
その中にdefinition.jsとういう外部JSファイルを作成実装
外部js
- 外部ファイル内に年月日の値を生成する(関数を作成、ループ関数を配列に格納する←どちらでも可能だが、definitionsの役割は値の生成なので、jsファイル側で格納する)
- 年月日それぞれの値をexport
definition.jsvar this_year, today; today = new Date(); this_year = today.getFullYear(); // 配列を変数に格納 const yearList = [] const monthList = [] const dayList = [] // 第一引数〜第二引数までの年月日を生成し、配列(list)に追加する関数を作成 const optionLoop = (start, end, list) => { for( let i = start; i <= end; i++) list.push(i) } optionLoop(1950, this_year, yearList); optionLoop(1, 12, monthList); optionLoop(1, 31, dayList); // 年月日の値のforループを配列に格納し、vueコンポーネントにexportする export {yearList, monthList, dayList}読み込む側のVueコンポーネント
import { yearList, monthList, dayList } from '@/utiles/definition';
で外部jsファイルを読み込む- dataプロパティ部分でv-forで回す際の配列(years, months, days)に値をセットする
※yearList, monthList, dayListはそれぞれ年月日の値が入っている配列である
BasicForm.vue<template> <div> <div class="form"> <div class="header"> <p id="step">STEP1</p> <p id="inst">お客様の情報を入力してください</p> </div> <div class="body"> <p class="genre">-性別-</p> <label><input type="radio" name="gender" value="男性" @change="updateGender">男性</label> <label><input type="radio" name="gender" value="女性" @change="updateGender">女性</label> <p class="genre">-生年月日-</p> <select name="year" id="id_year" @change="updateYear"> <option v-for="(year, key) in years" :key="key">{{ year }}</option> </select>年 <select name="month" id="id_month" @change="updateMonth"> <option v-for="(month, key) in months" :key="key">{{ month }}</option> </select>月 <select name="day" id="id_day" @change="updateDay"> <option v-for="(day, key) in days" :key="key">{{ day }}</option> </select>日 </div> </div> <div class="button-group"> <router-link to="/questionnaire" class="button">次へ進む ></router-link> </div> </div> </template> <script> import { yearList, monthList, dayList } from '@/utiles/definition'; export default { data() { return { years: yearList, months: monthList, days: dayList, } }, methods: { updateGender (e) { this.$store.commit('updateGender', e.target.value) }, updateYear (e) { this.$store.commit('updateYear', e.target.value) }, updateMonth (e) { this.$store.commit('updateMonth', e.target.value) }, updateDay (e) { this.$store.commit('updateDay', e.target.value) }, } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped lang="scss"> * { // outline: auto; margin:0; padding:0; /*全要素のマージン・パディングをリセット*/ max-width: 100%; } #step{ background: #1e90ff; color: #fff; font-size: 2px; margin: 0; max-width: 40px; padding: 2px; text-align: center; } #inst { font-size: 12px; color: #696969; text-align: center; padding-bottom: 10px; } .header { background: #afeeee; margin: 0%; padding: 0%; line-height: 10px; border-bottom: solid 1px #48d1cc; } .body { font-size: 9px; margin: 10px; padding-bottom: 10px; line-height: 30px; text-align: left; } .body input { vertical-align:middle; } .genre { color: #1e90ff; } select { padding: 5px 10px 5px 0px; font-size: 9px; border: 1px solid #dcdcdc; } .form{ max-width: 80%; margin-top: 80px; margin-left: auto; margin-right: auto; border: 1px solid #48d1cc; border-bottom: 0.5px solid #000; } .button-group{ text-align: center; } .button { background-color: #40e0d0; color: #fff; border: solid 1px #48d1cc; margin-top: 1rem; padding: 5px 10px; border-radius: 0.5rem 0.5rem; text-decoration: none; display:inline-block; margin: 10px; } </style>おまけ
プルダウンは、コンポーネント内だけで完結させる(例)こともできるが、可読性やメンテナンス性向上のため、値を生成するjsファイルを用意するという方が適切だと判断し、今回実装を行った。
- 投稿日:2021-03-14T17:58:31+09:00
【Vue.js】vue-cliで外部JSファイルを読み込む方法
vue-cliで外部JSファイルを読み込む方法
背景
Vueコンポーネントの年月日のプルダウンを外部ファイルのjsで実装したい
理由
可読性やメンテナンス性が向上するため
大規模なプロジェクトになるほど、こういった構成にしたときのメリットを感じやすい準備
vueCLIのディレクトリのsrcフォルダ直下にutilesという名前のフォルダを作成
その中にdefinition.jsとういう外部JSファイルを作成実装
外部js
- 外部ファイル内に年月日の値を生成する(関数を作成、ループ関数を配列に格納する←どちらでも可能だが、definitionsの役割は値の生成なので、jsファイル側で格納する)
- 年月日それぞれの値をexport
definition.jsvar this_year, today; today = new Date(); this_year = today.getFullYear(); // 配列を変数に格納 const yearList = [] const monthList = [] const dayList = [] // 第一引数〜第二引数までの年月日を生成し、配列(list)に追加する関数を作成 const optionLoop = (start, end, list) => { for( let i = start; i <= end; i++) list.push(i) } optionLoop(1950, this_year, yearList); optionLoop(1, 12, monthList); optionLoop(1, 31, dayList); // 年月日の値のforループを配列に格納し、vueコンポーネントにexportする export {yearList, monthList, dayList}読み込む側のVueコンポーネント
import { yearList, monthList, dayList } from '@/utiles/definition';
で外部jsファイルを読み込む- dataプロパティ部分でv-forで回す際の配列(years, months, days)に値をセットする
※yearList, monthList, dayListはそれぞれ年月日の値が入っている配列である
BasicForm.vue<template> <div> <div class="form"> <div class="header"> <p id="step">STEP1</p> <p id="inst">お客様の情報を入力してください</p> </div> <div class="body"> <p class="genre">-性別-</p> <label><input type="radio" name="gender" value="男性" @change="updateGender">男性</label> <label><input type="radio" name="gender" value="女性" @change="updateGender">女性</label> <p class="genre">-生年月日-</p> <select name="year" id="id_year" @change="updateYear"> <option v-for="(year, key) in years" :key="key">{{ year }}</option> </select>年 <select name="month" id="id_month" @change="updateMonth"> <option v-for="(month, key) in months" :key="key">{{ month }}</option> </select>月 <select name="day" id="id_day" @change="updateDay"> <option v-for="(day, key) in days" :key="key">{{ day }}</option> </select>日 </div> </div> <div class="button-group"> <router-link to="/questionnaire" class="button">次へ進む ></router-link> </div> </div> </template> <script> import { yearList, monthList, dayList } from '@/utiles/definition'; export default { data() { return { years: yearList, months: monthList, days: dayList, } }, methods: { updateGender (e) { this.$store.commit('updateGender', e.target.value) }, updateYear (e) { this.$store.commit('updateYear', e.target.value) }, updateMonth (e) { this.$store.commit('updateMonth', e.target.value) }, updateDay (e) { this.$store.commit('updateDay', e.target.value) }, } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped lang="scss"> * { // outline: auto; margin:0; padding:0; /*全要素のマージン・パディングをリセット*/ max-width: 100%; } #step{ background: #1e90ff; color: #fff; font-size: 2px; margin: 0; max-width: 40px; padding: 2px; text-align: center; } #inst { font-size: 12px; color: #696969; text-align: center; padding-bottom: 10px; } .header { background: #afeeee; margin: 0%; padding: 0%; line-height: 10px; border-bottom: solid 1px #48d1cc; } .body { font-size: 9px; margin: 10px; padding-bottom: 10px; line-height: 30px; text-align: left; } .body input { vertical-align:middle; } .genre { color: #1e90ff; } select { padding: 5px 10px 5px 0px; font-size: 9px; border: 1px solid #dcdcdc; } .form{ max-width: 80%; margin-top: 80px; margin-left: auto; margin-right: auto; border: 1px solid #48d1cc; border-bottom: 0.5px solid #000; } .button-group{ text-align: center; } .button { background-color: #40e0d0; color: #fff; border: solid 1px #48d1cc; margin-top: 1rem; padding: 5px 10px; border-radius: 0.5rem 0.5rem; text-decoration: none; display:inline-block; margin: 10px; } </style>おまけ
プルダウンは、コンポーネント内だけで完結させる(例)こともできるが、可読性やメンテナンス性向上のため、値を生成するjsファイルを用意するという方が適切だと判断し、今回実装を行った。
- 投稿日:2021-03-14T17:32:25+09:00
Vue.jsのSPAでVuetifyのパンくずリストを実装する
SPAで画面毎urlが変わる場合のパンくずリストをVuetifyのBreadcrumbsコンポーネントを使用して表示させます。
Vue.jsではパンくずを表示するプラグインでvue-breadcrumbs、vue-2-breadcrumbsなどがあるようです。今回はvue-2-breadcrumbsを選択します。
vue-2-breadcrumbsを選択した理由は、vue-routerで画面の親子関係を設定しやすかったからです。環境
- Vue.js 2.5.17
- Vuetify 2.4.3
- vue-router 3.5.1
- vue-2-breadcrumbs 0.7.12
実装方法
SPAでurlを変更させるためvue-routerを使用しますが、meta属性のbreadcrumbプロパティを設定します。
オブジェクトを設定した場合labelが表示名で、parentが親画面名です。parentには親のname属性を設定します。String型をセットした場合、labelに適用されます。
この設定で「ラビットハウス / 香風智乃」「ラビットハウス / 香風タカヒロ」という関係になります。router.jsimport Vue from 'vue'; import VueRouter from 'vue-router'; Vue.use(VueRouter); const router = { mode: 'history', routes: [ { path: '/', name: 'Rabbithouse', component: RabbithouseComponent, meta: { breadcrumb: 'ラビットハウス', }, }, { path: '/charactor/chino', name: 'chino', component: KafuChinoComponent, meta: { breadcrumb: { label: '香風智乃', parent: 'Rabbithouse' } }, }, { path: '/charactor/takahiro', name: 'takahiro', component: KafuTakahiroComponent, meta: { breadcrumb: { label: '香風タカヒロ', parent: 'Rabbithouse' } }, }, ] }; export default new VueRouter(router);私の場合、App.jsとファイルを分けています。分けない場合は、App.jsに記述してください。vue-2-breadcrumbsをuseする時に、templateとcomputedをオプションで追加します。
Vuetifyではv-breadcrumbタグのitemsに画面情報のオブジェクトの配列をセットします。パンくずのデータはthis.$breadcrumbs
に入っているのでv-breadcrumbタグの仕様に合わせた配列に作り直してセットします。breadcrumb.jsimport Vue from 'vue'; import VueBreadcrumbs from 'vue-2-breadcrumbs' Vue.use(VueBreadcrumbs, { template: `<div> <v-breadcrumbs class='breadcrumb-item active' :items="items" /> </div>`, computed: { items() { return this.$breadcrumbs.map((crumb, i) => { return { text: this.getBreadcrumb(crumb.meta.breadcrumb), disabled: this.$breadcrumbs.length - 1 === i, to: this.getPath(crumb) }; }); } } }); export default VueBreadcrumbs;App.jsimport breadcrumb from "./breadcrumb"; const app = new Vue({ el: '#app', router: router, vuetify: vuetify, breadcrumb: breadcrumb, });
- 投稿日:2021-03-14T17:00:22+09:00
Nuxt Composition API ドキュメント抄訳
ドキュメント
useContext
Composition API中のNuxtコンテキストにアクセスできます。
useContext
はNuxtのコンテキストを返します。これを使うことでNuxtコンテキストにより容易にアクセスできます。import { defineComponent, useContext } from '@nuxtjs/composition-api' export default defineComponent({ setup() { const { store } = useContext() store.dispatch('myAction') }, })
route
、query
、from
およびparams
はリアクティブなref(.value
でアクセスできる)ですが、それ以外のコンテキストは異なることに注意してください。Nuxt 3へのアップグレードをスムーズにするため、 it is recommended not to access
route
,query
,from
およびparams
にはuseContext
からアクセスせずにuseRoute
ヘルパー関数を使うことが推奨されます。useAsync
一度だけ走ってクライアントサイドでデータを維持する非同期関数を定義できます。
useAsync
を用いて非同期通信に依存するリアクティブな値を作成できます。サーバー上では、このヘルパーは非同期通信の結果をHTMLに埋め込み、またクライアントモードに自動的に注入します。
asyncData
とまったく同様に、クライアントサイドで非同期通信を再び走らせません。しかしながら、SSRで通信が実行されなければ(たとえば初期ロードの後でページを遷移した場合)、非同期通信が解決されたときにその結果を埋めるような
null
のrefを返します。import { defineComponent, useAsync, useContext } from '@nuxtjs/composition-api' export default defineComponent({ setup() { const { $http } = useContext() const posts = useAsync(() => $http.$get('/api/posts')) return { posts } }, })そのとき、
useAsync
は1回限りの用途にのみ適しており、その限りでユニークなキーを提供します。 詳しい情報.useFetch
Composition APIの中でNuxtのfetch()フックにアクセスできます。
v2.12よりも新しいNuxtのバージョンは、サーバーサイドとクライアントサイドの非同期データフェッチングが可能な
fetch
と呼ばれるカスタムフック をサポートしています。このパッケージでは以下のようにアクセスできます:
import { defineComponent, ref, useFetch } from '@nuxtjs/composition-api' import axios from 'axios' export default defineComponent({ setup() { const name = ref('') const { fetch, fetchState } = useFetch(async () => { name.value = await axios.get('https://myapi.com/name') }) // Manually trigger a refetch fetch() // Access fetch error, pending and timestamp fetchState return { name } }, })
useFetch
はsetup()
の中で同期的に呼ばれなければなりません。コンポーネントのdataに対して、つまり、setup()
から 返却される プロパティに対してなされるいかなる変更も、クライアントに送られ直接読み込まれます。useFetch
フックのその他の副作用は後に残りません。
$fetch
と$fetchState
はインスタンスにすでに定義されています。つまり、setupからfetch
やfetchState
を返却する必要はありません。
useFetch
はonGlobalSetup
の中で使えないことに注意してください。useStatic
サイト生成時に静的なJSONを作り出す非同期通信を定義できます。
useStatic
を使って処理コストの高い関数をあらかじめ走らせることができます。import { defineComponent, useContext, useStatic, computed, } from '@nuxtjs/composition-api' import axios from 'axios' export default defineComponent({ setup() { const { params } = useContext() const id = computed(() => params.value.id) const post = useStatic( id => axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`), id, 'post' ) return { post } }, })SSG
もしアプリ全体を生成する(あるいは単にいくつかのルートを
nuxt build && nuxt generate --no-build
でプリレンダリングする)なら、以下の挙動が解禁されます:
- 生成時、
useStatic
で呼んだ結果はJSONファイルに保存され、/dist
ディレクトリの中にコピーされます。- 生成されたページのハードリロード時、JSONはページにインラインで埋め込まれ、キャッシュされ、キャッシュされます。
- 生成されたページへのクライアント遷移時、このJSONはフェッチされます。一度フェッチされると、後続の遷移のためにキャッシュされます。ページが事前に生成 されていなかった 時など、いかなる理由でこのJSONが存在しないような場合も、オリジナルの生成関数はクライアントサイドで走ります。
もしアプリ内でいくつかのページを事前に生成するなら、
generate.interval
を増やす必要があることに注意してください。(setupの説明 を参照。)SSR
もしルートが事前に生成されていないなら(devモードの場合を含む):
- ハードリロード時、サーバーは生成関数を走らせ、
nuxtState
の結果をインラインで埋め込みます。クライアントがAPIリクエストを再び走らせることのないようにするためです。結果は次のリクエストまでの間キャッシュされます。- クライアント遷移時、クライアントは生成関数を走らせます。
どちらの場合も、
useStatic
が返す結果はnull
のrefで、生成関数やJSONフェッチが解決されたとき、その結果によって埋められます。onGlobalSetup
グローバルなNuxtのsetup()関数で関数(またはdata)を走らせます。
グローバルなsetup関数でコールバック関数を走らせます。
import { onGlobalSetup, provide } from '@nuxtjs/composition-api' export default () => { onGlobalSetup(() => { provide('globalKey', true) }) }componentコンテキストの中ではなくpluginの中で呼ばないといけません。
リポジトリ
- 投稿日:2021-03-14T15:11:05+09:00
JavaScriptでクリップボードにコピーする機能を作成
概要
JSでクリップボードにコピーする機能を作成する方法を調べた際、navigatorのwriteTextメソッドを使用すると可能とあったのですが、HTTP環境だと使用できないので別の方法を調べました。
またVue.jsで処理している値を取得したかったので、どちらも可能な方法を調べました。
そもそもクリップボードにコピーする機能はライブラリを導入する方法もあったのですが、今回はライブラリを導入しない方法を探しました。環境
PHP:v7.3.11
Vue:v2実装
- クリップボードにコピーするメソッド(ViewModelクラスに記載)
public copyClipBoard(str: string) { const targetSentence = str; let input = document.createElement('input'); input.readOnly = true; document.body.appendChild(input); input.value = targetSentence; input.select(); document.execCommand('copy'); document.body.removeChild(input); }
- 今回はボタンを押した時にコピーできるようにしたかったので、トリガー用のメソッドを記載
doCopy() { _viewModel.copyBoard("コピーしたいテキスト"); }
- HTMLの実装
<button class="copy-clipboard" @click="doCopy">クリップボードにコピー</button>まとめ
- 今回はJSでクリップボードにコピーする方法についてまとめました。
- 2行くらいでシンプルにかけるかと思っていたのですが、意外と量が多かったため記事にしました。
- 投稿日:2021-03-14T01:35:52+09:00
【フロントエンド】Angularを1年間触ってみてたどり着いたコンポーネント設計
個人的に思う最適なコンポーネント設計とその提案
前置き
Angularを始めたはいいものの、しばらくして躓くことがありました。それは文法や英語のドキュメントなどよりも、なにより適切なコンポーネントの分け方がわからないということでした。
そこでいろいろ検索をしてみたのですが、しっくりくる答えにたどり着かなかったため、自分で考えることにしました。今回はその現状報告のようなものです。(コンポーネント設計の話なので、ReactやVue.jsでも通ずる話題かと勝手に思っています)
結果、Page-Layout-Presenter構造というコンポーネント設計を考えるに至ったのですが、この記事ではそこに至るまでの経緯を紹介し、この設計については別記事で投稿することにしました。ご興味がございましたら併せてご覧ください。
⇒【Angular】最適なコンポーネント設計について考えてみた: Page-Layout-Presenter構造注意:飽くまで個人的に現状、最もうまくいくと考えている設計なので、プロジェクトや人によってもっと良い方法があるかもしれません!よりよいアイデアがあればコメントをいただければありがたいです。
これまでAngularを触っていて感じてきた課題
思ったよりコンポーネントを分割できない
コンポーネントの分け方に明確なルールを持ち合わせていないため、感覚で分けるか、そもそも分けないかのどちらかになりがちでした。感覚で分けた場合、複数人で開発していると認識を合わせるのが大変です。そして、開発とともにコンポーネントを分けるコストが高くなり、それによりさらにコンポーネントがfatになるという悪循環で、どんどん動けなくなります。
このことから、コンポーネントの分け方には明確なルールが必要だと感じました。サービスの扱いが雑になりがち
コンポーネントにビジネスロジックを書いてしまい、サービスとコンポーネントの境界があいまいになってしまうことがありました。また、どのコンポーネントがどのサービスを呼び出しているかを管理できず、親コンポーネントと子コンポーネントで同じサービスを繰り返し呼び出してしまうなどの無駄が発生することもありました。
このことから、コンポーネントにビジネスロジックを書かないように意識しやすい設計にする必要があると感じました。また、サービスを呼び出せるコンポーネント群を決める必要があると感じました。気軽にスタイルシートを変更できない
<div class="some-block__some-element">
のように、スタイルとテンプレートをclass
属性で紐づけることはごく一般的だと思いますが、デザインの修正を行うたびに、class
を参照して、スタイルシートに移動して、対象のスタイルを探して変更する、というのが私としては意外と手間だと感じました。そして、パーツの内容を考慮した上で、認識のずれが起こりづらいような、一意なクラス名を英語で考えるというのも結構疲れる作業だと思いました。そして、スタイルシートとテンプレートの分離がエンジニアさんとデザイナーさんとの壁になっているのではと思いました。
また、プロジェクトによってはスタイルがすべてstyles.scss
などグローバルな場所に集められている場合もあり、変更の影響範囲がすぐに把握できないこともありました。私の現職のプロジェクトは実際そうなっていて、専任のエンジニアさんしかスタイルシートを触れないという状態でした。(そしてその方はすでに退職済みという...)
このことから、設計を考えるうえでデザインないしデザイナーさんとのかかわり方を考えることは切っても切り離せないと感じました。コストが高すぎてテストコードを書けない
Angularの場合、コンポーネントやサービスなどを新規作成すると必ずついてくる
*.spec.ts
というテストファイルがあると思います。テスト駆動開発などではこのファイルにテストコードを書いていくことでテストを極力自動化させるのだと思っています。しかし、実際はやろうと思っても後回しにされることも多いのではないかと推測します。それは、JasmineやKarmaについて学ぶ必要があるという点もあるとは思いますが、なにより、先述のことが原因でコンポーネントがfatになっていて、テストコードの実装の難易度が上がりすぎているのではないかと考えられます。一つのボタンを押すだけでそのコンポーネントでテストすべき項目は一体いくつあるのでしょうか。
このことから、コンポーネントを適切に分割し、最小限の役割を持たせることで、テストの範囲も分担させ、最小限にすることができるのではないかと思いました。参考にしてきた設計思想
これら課題を解決すべく、よりよいコンポーネント設計について考えていくことにしました。まずは、そのために参考にした設計思想などを紹介します。
【Page-Container-Presentational構造】
知ったきっかけ: Angular Webアプリケーションの最新設計手法
紹介されているコンポーネントの分け方から、ここでは仮にPage-Container-Presentational構造と呼ばせていただくことにします。(下画像は上記リンクより引用)
参考にした点
関心の分離の観点から、コンポーネントをグルーピングして役割を与えるという考え方が非常に参考になりました。例えば
Container Component
は状態を扱い、Presentational Component
は見た目を扱うなど。課題
課題というか、おそらく私の理解不足が問題なのですが、状態管理以外の点で考えた場合は、
Container
とPresentational
の境界をどこで決めればよいかがあいまいに感じました。また、最小単位であるPresentational
がfatになることがあるので、共通処理などを抽出し、さらに分割したいと感じました。【Atomic Designとドメイン駆動開発(DDD)】
知ったきっかけ: WEB+DB PRESS Vol.112
この雑誌は発行が2019年と少し前ではありますが、最新設計手法としてAtomic DesignとDDDを両立させたコンポーネント設計について解説されていました。ちなみにAngularではなくReactとVue.jsのコードを例にして書かれていました。
参考にした点
コンポーネントを、ドメインを担うコンポーネントと、UIを担うコンポーネントに分ける考え方が参考になりました。これにより、特定のコンポーネントからしか参照しないドメイン依存なコンポーネントと、それに対し、不特定多数のコンポーネントから呼び出せるUIパーツなどの共通のコンポーネントを作れるため、再利用性が上がると考えられました。
また、デザインの修正をページ単位よりも細かいコンポーネント単位で行えるため、ページ単位で修正を行うのに比べ、エンジニアがデザイナーを待つ時間とデザイナーがエンジニアを待つ時間が短縮されると雑誌で紹介されていました。これにより開発のサイクルがより小さく早くなります。デザイナーさんとの関わりを考慮した設計を考えるきっかけになり大変参考になりました。課題
Atomic Designの考えを取り入れると計5層の構造になり、さらに雑誌の手法にある
Containers
層とLayout
層を加える場合は7層以上になります。1つのページを作るだけでもコンポーネントの数が6つ以上必要で作るのが大変です。ほぼ記述がないコンポーネントをいくつも生成することになるので、フロントエンドのコンポーネント設計においては少し冗長に感じました。
層 Atomic Design + DDD Atomic Design 1 Pages Pages(ページ) 2 Containers 3 Domain Objects Templates(テンプレート) 4 Domain Elements Organisms(有機体) 5 Gui Groups Molecules(分子) 6 Gui Parts Atoms(原子) - 必要な場所にLayout 【BEM記法】
HTMLやCSSについて勉強しているとよく登場します。SASS記法と合わせてよく紹介されている印象があります。ざっくり言うと、自由度が高いHTMLのタグの
class
属性の命名に、block__element--modifire
の命名ルールを与えることで運用しやすくするという方法です。参考にした点
class
属性値の命名方法にルールがあることで、他のエンジニアさんとの認識のずれを起こしづらくできることが非常にメリットと感じました。
また、先述のAtomic Design + DDDと併用することで、スタイルのスコープをグローバルではなく上の表でいうDomain Elements
単位で行えるようになりました。これにより、コンポーネントレベルでスタイルを表現できるようになり、さらに、スタイル修正のコストを減らすことにつながると考えられました。
そして、スタイルの構造はテンプレートの構造とは関係がない、という考え方は自分にとって重要な学びでした(HTMLにどれだけ階層構造があったとしても、BEMで書くクラスはBlockとElement(Modified Element)との2層構造であるというルール)。これにより、スタイルをclass
値単位で再利用可能にしたというわけです。課題
ただ、コンポーネント設計を考えるうえで、再利用性はコンポーネント側がすでに持っているので、もはやスタイル側に持たせる必要がありません。むしろ、異なるコンポーネントで見た目を少しだけ変えたいというような場合、再利用性があることが逆に修正を難しくする原因になりかねません。なので、スタイルの再利用はさせないほうがいいのではないかと考えています。
そもそもクラス名を介してスタイルを紐づけるというやり方がよくないと思いました。なぜなら、その場合テンプレートからスタイルを参照する際に必ずスタイルシートに移動してさらにclass
名を辿るという手順が発生するためです。これを毎度行うことになるので、やはり少々面倒です。また、class
名はプロジェクトによって異なるものであり、共通認識のものではないため、ほかのエンジニアさんやデザイナーさんにとっては可読性を下げることになりかねません。また、大規模な場合は認識合わせを行ったり、ドキュメントを作成したりする必要が出てくると思うので、コストが意外と大きいのではないかと思っています。代わりに共通認識の何かで代用できれば、認識のすり合わせもドキュメント作成も不要になるのではないかと考えました。→ tailwindcss
次に、実はコンポーネントを分割するという観点からいうと、BEMは相性がそこまでよくないという気がしています。Angularでは、親コンポーネントからは子コンポーネントのテンプレートを知ることはできません。なので、子コンポーネントのタグのこの点はclass
属性も記述することができないため、コンポーネントの分割がそもそも不可能になってしまうからです。angular-bem
というモジュールで解決できます。これについても一苦労あったのですがその話はまたの機会とします。
BEM記法はコンポーネントを分割しない場合には使いやすいですが、コンポーネント指向な開発にはあっていないと個人的に感じました。【Windowsフォームアプリケーション】
知るきっかけ: 現職での仕事
参考にした点
.NET FrameworkにWindowsフォームというものがあります。簡単に言えば一世代前の、Windows用アプリケーションを作るためのフレームワークです。Windowsフォームでアプリケーションを作成するときは、Visual StudioのToolBoxペインから、必要なコントローラ(UIパーツのようなもの)をドラッグ&ドロップすることで配置することができます。つまり、必要なUIのパーツはすでにフレームワークによって用意されていて、開発者はマウス操作一つで直感的にそれらを配置することができるわけです。
Webアプリケーション開発でも、このように、より直感的にUIパーツを配置できれば楽なのにな、と考えるようになりました。課題
Angularやnpmによって、予めすべてのUIパーツが準備されていれば楽ですが、現状はそうはなっていません。また、Windowsフォームアプリケーションはスタイルの多様性が低く、スタイルをカスタマイズする場合についても考える必要があります。
【tailwindcss】
知ったきっかけ: The State of CSS 2020から読み解くWebフロントのトレンド
参考にした考え方
BEM記法はAngularで実用するには無理があると感じていたところ、tailwindを知りました。テンプレートに直接スタイルを記述できるため、頻繁に変更されると考えられるレイアウトに関するスタイルなどを効率よく操作でき、さらに、
class
名の運用に関する他のエンジニアさんとの認識のずれをなくせると考えられました。あと、ダークモードやレスポンシブ対応など向けの実装も簡単そうなのでいいと思いました。課題
現状、tailwindcssが
class
として提供できていないスタイルに関してはstyle
属性か結局スタイルシートに書く必要があります。また、これはBEM記法でも同じ課題ですが、少しだけ見た目の違う場合に新しいコンポーネントとして分けるべきか悩ましく、まだ私の中で最善策が決まっていません。【ディレクトリ構造】
中規模AngularアプリにおけるNgModule構成とディレクトリ構造
参考にした点
アプリケーション全体で参照可能な共通のディレクトリと、同一
feature
内でのみ参照可能なfeatures
ディレクトリを作るという点が参考になりました。また、ディレクトリ名を何にするか悩んだ時にいくつか参考にしました。【その他参考にした記事など】
- Angular Architecture Patterns and Best Practices (that help to scale)
- angular-atomic-design-boilerplate - StackBlitz
- 中規模Angularアプリケーションの再設計
- bitbank LT Night #3 ~Angular~を開催しました
- Atomic Designを使ってReactコンポーネントを再設計した話
個人的に最適と思うコンポーネント設計
以上を踏まえ、できる限り課題を解決するような設計について私なりに考えてみました。
これについては別記事で投稿しましたので、よければ併せてご覧ください。
⇒【Angular】最適なコンポーネント設計について考えてみた: Page-Layout-Presenter構造おわりに
今後もフロントエンド開発に関して学びつつ、よりよい設計について考えていきたいと思います。
記事として未熟な点もあったかもしれませんが、一部でも参考になれば幸いです。