- 投稿日:2020-06-28T22:52:28+09:00
Nuxt.js + Firebaseで早押しボタンを作ってみた
はじめに
昨年会社のイベントでクイズ大会を行いました。
参加者は100人弱で、人数分早押しボタンを用意するわけにもいかないのでWebアプリを構築したのですが、早押し判定でハマったので今更ながらサンプル版として残しておきたいと思います。
この記事ではFirebaseの基本的なセットアップや実装、Vue・Nuxtについての解説は行いません。環境
- Yarn v1.15.2
- Node v12.13.0
- Nuxt v2.13.2
- Firebase v7.15.5
完成品
サンプルをgithubにて公開しています。
quizBuzzer※UIライブラリにVuetify、効果音の実装にHowlerを使用(Web Audio API はめんどくさい!!)
早押しボタンの画面
進行役の画面
仕様
- クイズ参加者と進行役で別の画面を操作
- 参加者はそれぞれのデバイスの参加者用の画面で早押しボタンを押して解答権を獲得できる
- 進行役は専用の管理画面で事前に決められた問題の切り替え、参加者の解答に応じてピンポン・ブーを鳴らすことができる
- クイズの正誤判定は人力(笑)
実装の指針
- Realtime database(以下、RDB)には現在表示されている問題のIDと参加者のIDを保存する
- 参加者のIDは、手軽さを考慮しログイン機能を持たせたくないため、クライアントサイドで生成したランダムな文字列を使用
当初のコード
RDB上のデータ
quiz-state: { current-quiz: 1, respondent-id: "", }@/pages/index.vue<template> <div> <p class="text">{{ currentQuiz.text }}</p> <v-btn v-if="waitingClicked" :disabled="!!respondentId" type="button" x-large outlined color="indigo" @click="postQuizState" >PUSH!</v-btn > <p v-else-if="clickedAtFirst">解答権GET!</p> <p v-else>他の人に解答権があります</p> </div> </template> <script> import Vue from 'vue' import { Howl } from 'howler' import { postQuiz, respondentObserver, currentQuizObserver, } from '@/api/firebase' import quizData from '@/static/data/quiz' export default Vue.extend({ data: () => ({ quizData, currentQuizIndex: 0, userId: '', respondentId: '', sound: new Howl({ src: ['/audio/sound-btn.wav'], }), }), computed: { currentQuiz() { return this.quizData[this.currentQuizIndex] }, waitingClicked() { return !this.clickedAtFirst && !this.clickedAtSecondAndLater }, clickedAtFirst() { const result = this.respondentId && this.userId === this.respondentId return result }, clickedAtSecondAndLater() { return !!this.respondentId && this.userId !== this.respondentId }, }, watch: { clickedAtFirst(newVal) { if (newVal) { this.sound.play() } }, }, created() { currentQuizObserver((val) => { this.currentQuizIndex = val }) respondentObserver((val) => { this.respondentId = val }) }, mounted() { this.userId = this.$store.state.userId }, methods: { postQuizState() { if (this.respondentId) return postQuiz(this.userId) }, }, }) </script>@/api/firebase.jsimport { db } from '@/plugins/firebase' const respondentRef = db.ref('/quiz-state/respondent-id') export function postQuiz(val) { respondentRef // eslint-disable-next-line consistent-return .transaction((post) => { if (!post) return val }) } export function respondentObserver(callback) { respondentRef.on('value', (snapshot) => { callback(snapshot.val()) }) } export function resetBtn() { respondentRef.set('') } const currentQuizRef = db.ref('/quiz-state/current-quiz') export function changeQuiz(val) { currentQuizRef.set(val) } export function currentQuizObserver(callback) { currentQuizRef.on('value', (snapshot) => { callback(snapshot.val() === null ? 0 : snapshot.val()) }) }RDBの解答者のIDのsnapshotとユーザーIDを比較して最初にクリックしたか or 2番目以降にクリックしたかの判定を行なっています。
問題点
数百ミリ秒の誤差でほぼ同時にボタンが押された場合、押したのが2番目以降で解答権を獲得できなかったにも関わらず、一瞬だけ解答権獲得の判定がされる。
これは、最初に押したか or 2番目以降に押したかの判定中にもcomputedは随時更新され、さらにrespondentObserver
で監視しているRDBのクイズ解答者のIDのsnapshotは一時的にそのデバイスの更新が優先された値になることがあるためです。解決したコード
@/pages/index.vue<script> ... export default Vue.extend({ data: () => ({ ... isTransactioning: false, ... }), computed: { ... clickedAtFirst() { const result = !this.isTransactioning && this.respondentId && this.userId === this.respondentId return result }, ... }, ... methods: { postQuizState() { this.isTransactioning = true if (this.respondentId) return postQuiz(this.userId).then(() => { this.isTransactioning = false }) }, }, ... </script>@/api/firebase.jsexport function postQuiz(val) { return new Promise((resolve) => { respondentRef // eslint-disable-next-line consistent-return .transaction((post) => { if (!post) return val }) .then(() => { resolve() }) .catch(() => { resolve() }) }) }RDBのtransactionは引数のpostで現在のRDB上のデータを取得でき、返した値でRDBを更新できる非同期関数です。
なのでVueのインスタンスにtransactionが処理中かどうかのフラグを持たせ、
ボタンクリック時にフラグをtrueにし、その間は判定を待つ。
そしてtransactionの終了時にresolveし、thenでフラグをfalseにすることで最終的な判定が終わってから画面が更新されるようになりました。おわりに
実際にプロダクトで使用する際はユーザーIDのランダムな文字列をハッシュ値にする、はたまたログイン制にしてFirebaseのセキュリティルールもしっかり設定するなど課題はありますね。
あと今回の実装では、すでにRDBのデータがあればtransactionで値を返さない、というロジックにしたので
eslint-disable-next-line
に逃げてしまったのですが他に良い方法があれば是非ご教授ください?
- 投稿日:2020-06-28T22:45:10+09:00
JavaScriptの基本用語
プログラミング初心者が
JavaScriptの基本用語をアウトプット用にまとめてみました。JavaScriptの基本用語
1.JavaScript
ブラウザに搭載するための言語。
ユーザーが行う様々な操作に対応できる言語。
コンソールに直接書く。ScriptタグでJavaScriptのコードを呼び出す。2.window.alert( )
ブラウザでアラートを表示させるメソッド。3.console.log( )
ブラウザのコンソールにテキストを表示させるメソッド。
デバッグ用に使うこともある。4.変数宣言
letで変数を宣言する。後で書き換えることができる。
constで変数を宣言する。後で書き換えることができない。5.オブジェクト
名前と値をセット(プロパティ)で管理するデータのまとまりのこと。
「{ プロパティ名: 値 };」で作成や定義を行う。6.for文
JavaScriptにおける繰り返し処理を行うための文。7.function文
JavaScriptにおいて関数を定義するための文。8.return
JavaScriptではしっかり戻り値を明示しないといけない。9.関数式(無名関数)
JavaScriptにおける関数を定義するもう一つの方法。
関数式にすることで、なにかに代入しやすくなったり渡しやすくなったりする。
JavaScriptは関数宣言を先に読み込む。10.Bracket Pair Colorizer
VSCodeの拡張機能。
括弧が色分けされて読みやすくなる。11.DOM(Document Object Model)
HTMLを解析して、データを作成するAPI。
※HTMLファイルはただの文字情報。ブラウザによってCSSやJavaScriptによる修飾を行なって画面に映している。
※HTMLを解析しDOMに変換→CSSとJavaScriptを読み込み見た目を描画→ユーザーがページを閲覧。12.DOMツリー(ドキュメントツリー)
DOMによって解析されたHTMLは階層構造のあるデータになる。
このオブジェクトのツリー状の集合のこと。
JavaScriptで操作することができる。13.ノード
DOMツリーの中での、HTML1つ1つのタグのこと。14.document
開いているWebページのDOMツリーが入っているオブジェクト。
document.getElementById("id名");…DOMツリーから特定要素を取得するメソッド。
document.getElementsByClassName("class名");…同じクラスを持つ要素全てを取得するメソッド。
document.querySelector("セレクタ名")…セレクタに合致するもののうち一番最初に見つかった要素を1つ取得するメソッド。15.イベント
HTMLの要素に対して行われた処理要求のこと。16.イベント駆動
「イベント」が発生したら、それをきっかけにコードが実行される仕組み。17.イベントリスナ
一つのイベントと一つの関数を紐付ける仕組みのこと。
addEventListenerは、ノードオブジェクトにイベントが起きたときに関数を実行するメソッド。18.windowオブジェクト
元から用意されている、ブラウザの情報を持つオブジェクト。19.innerHTML
HTML要素の中身を書き換えることができるメソッド。20.classList.add
クラスを追加することができるメソッド。21.classList.remove
クラスを削除することができるメソッド。22.this
イベントの発生元となった要素を取得するメソッド。23.Array.prototype.slice.call( )
引数にとったオブジェクトを配列に変換するメソッド。24.forEach( )
配列によく使われる繰り返し処理。
引数には、コールバック関数を入れる。25.コールバック関数
配列の各でデータに対して行う処理。
第一引数はvalue(値)、第二引数はindex(要素番号)。26.indexOf( )
配列に対してだけ使い、DOMを引数にとって一致した要素番号を戻す。まとめ
JavaScriptの基本用語をまとめてみました。
- 投稿日:2020-06-28T19:17:04+09:00
【質問】URLのパラメータを判別して期間限定のページを出し分ける方法
表題の件について質問になります。
期間限定のページを運用で回していきたいと思っていて、
URL末尾に?enddate=200731 のようなパラメータをセットして、このパラメータを判別してページの表示期間を
制御させる方法(記述)をご教示頂けますと幸いです。この例で言うと20年8月1日になった時点でページが見れなくなる想定です。
それか、パラメータに有効期間をセットして、その有効期間を判別してページを出し分ける方法のどちらかの方法が知りたいです。。。
何卒よろしくお願いいたします。
- 投稿日:2020-06-28T18:44:39+09:00
【Rails】フリマアプリ商品編集機能について(プレビュー編集・DB更新)
はじめに
草野と申します。
今回の投稿は、プログラミングスクールでチーム開発にて行ったフリーマーケット系ECサイトのクローンアプリ商品編集機能についてです。自分用のメモのため、文章は拙いですが、少しでも初学者の助けになればと考えています。
内容は、表題にもあるとおり、プレビュー編集とDB更新についてです。未熟な点も多いと思います。不備等ありましたらご指摘ください。随時改善して行こうと思います。ちなみに私は、スクール自体は卒業しており、学習した内容の振り返りとして投稿させて頂いております。完成品
商品編集画面(プレビュー画像部)
商品編集画面(カテゴリー部)
商品編集画面(販売手数料・利益部)
更新成功時の遷移画面
実装手順
1.ルーティング編集
- update_doneのルート設定(更新成功時の遷移画面)
2.コントローラー編集
- editメソッド設定
- updateメソッド設定
- エラーハンドリング
- 画像削除
- update_daneメソッド設定
3.ビュー編集・作成
- プレビュー画像呼び出し
- カテゴリー呼び出しの調整
- 更新成功時の遷移画面
4.JS編集
- プレビュー画像及びinputタグの生成、削除
- 販売手数料・利益の表示
1.ルーティング編集
update_doneルートを生成します。
これは、更新成功時の遷移画面を表示するためのルーティングです。config/routes.rbresources :items do resources :comments, only: [:create, :destroy] resources :favorites, only: [:create, :destroy] collection do get 'get_category_children', defaults: { fomat: 'json'} get 'get_category_grandchildren', defaults: { fomat: 'json'} get 'search' get 'post_done' get 'delete_done' get 'detail_search' get 'update_done' # これを追加 end end2.コントローラー編集
今回編集したコントローラーの記述は下記の通りです。
app/controller/items_controller.rbclass ItemsController < ApplicationController before_action :category_parent_array, only: [:new, :create, :edit] before_action :set_item, only: [:show, :edit, :update, :destroy] before_action :show_all_instance, only: [:show, :edit, :destroy] # 中略 def edit grandchild = @item.category child = grandchild.parent if @category_id == 46 or @category_id == 74 or @category_id == 134 or @category_id == 142 or @category_id == 147 or @category_id == 150 or @category_id == 158 else @parent_array = [] @parent_array << @item.category.parent.parent.name @parent_array << @item.category.parent.parent.id end @category_children_array = Category.where(ancestry: child.ancestry) @child_array = [] @child_array << child.name @child_array << child.id @category_grandchildren_array = Category.where(ancestry: grandchild.ancestry) @grandchild_array = [] @grandchild_array << grandchild.name @grandchild_array << grandchild.id end def update if item_params[:images_attributes].nil? flash.now[:alert] = '更新できませんでした 【画像を1枚以上入れてください】' render :edit else exit_ids = [] item_params[:images_attributes].each do |a,b| exit_ids << item_params[:images_attributes].dig(:"#{a}",:id).to_i end ids = Image.where(item_id: params[:id]).map{|image| image.id } delete__db = ids - exit_ids Image.where(id:delete__db).destroy_all @item.touch if @item.update(item_params) redirect_to update_done_items_path else flash.now[:alert] = '更新できませんでした' render :edit end end end def update_done @item_update = Item.order("updated_at DESC").first end # 中略 private def item_params params.require(:item).permit(:name, :item_explanation, :category_id, :item_status, :auction_status, :delivery_fee, :shipping_origin, :exhibition_price,:brand_name, :days_until_shipping, images_attributes: [:image, :_destroy, :id]).merge(user_id: current_user.id) end def set_item @item = Item.find(params[:id]) end def category_parent_array @category_parent_array = Category.where(ancestry: nil).each do |parent| end end def show_all_instance @user = User.find(@item.user_id) @images = Image.where(item_id: params[:id]) @images_first = Image.where(item_id: params[:id]).first @category_id = @item.category_id @category_parent = Category.find(@category_id).parent.parent @category_child = Category.find(@category_id).parent @category_grandchild = Category.find(@category_id) end endまずeditメソッドを設定します。
他のメソッドで使用しているインスタンス変数を利用するのでリファクタリングのため、before_actionから呼び出しがあります。
使用しているインスタンス変数は下記の通りです。
② 親カテゴリーのnameとidが代入された配列
③ categoryモデル内の全ての子カテゴリー
④ 子カテゴリーのnameとidが代入された配列
⑤ categoryモデル内の全ての孫カテゴリー
⑥ 孫カテゴリーのnameとidが代入された配列
⑦ 該当商品情報
⑧ categoryモデル内の全ての親カテゴリー
⑨ 該当商品の画像
⑩ 該当商品のcategory_id(孫の数値)app/controller/items_controller.rbclass ItemsController < ApplicationController before_action :category_parent_array, only: [:new, :create, :edit] before_action :set_item, only: [:show, :edit, :update, :destroy] before_action :show_all_instance, only: [:show, :edit, :destroy] #中略 def edit # ▼ ①ここで該当商品の子・孫カテゴリーを変数へ代入 grandchild = @item.category child = grandchild.parent if @category_id == 46 or @category_id == 74 or @category_id == 134 or @category_id == 142 or @category_id == 147 or @category_id == 150 or @category_id == 158 else # ② ▼ 親カテゴリーのnameとidを配列代入 @parent_array = [] @parent_array << @item.category.parent.parent.name @parent_array << @item.category.parent.parent.id end # ③ ▼ 子カテゴリーを全てインスタンス変数へ代入 @category_children_array = Category.where(ancestry: child.ancestry) # ④ ▼ 子カテゴリーのnameとidを配列代入 @child_array = [] @child_array << child.name # ⑤で生成した変数を元にname・idを取得 @child_array << child.id # ⑤ ▼ 孫カテゴリーを全てインスタンス変数へ代入 @category_grandchildren_array = Category.where(ancestry: grandchild.ancestry) # ⑥ ▼ 孫カテゴリーのnameとidを配列代入 @grandchild_array = [] @grandchild_array << grandchild.name # ⑤で生成した変数を元にname・idを取得 @grandchild_array << grandchild.id end end #中略 private def item_params params.require(:item).permit(:name, :item_explanation, :category_id, :item_status, :auction_status, :delivery_fee, :shipping_origin, :exhibition_price,:brand_name, :days_until_shipping, images_attributes: [:image, :_destroy, :id]).merge(user_id: current_user.id) end def set_item @item = Item.find(params[:id]) # ⑦ 該当の商品情報をインスタンス変数へ代入 end def category_parent_array @category_parent_array = Category.where(ancestry: nil) # ⑧ 親カテゴリーを全てインスタンス変数へ代入 end def show_all_instance @user = User.find(@item.user_id) @images = Image.where(item_id: params[:id]) # ⑨ 該当商品の画像をインスタンス変数へ代入 @images_first = Image.where(item_id: params[:id]).first @category_id = @item.category_id # ⑩ 該当商品のレコードからカテゴリーidを取得し、インスタンス変数へ代入(この際に取得するidは孫カテゴリーidです。) @category_parent = Category.find(@category_id).parent.parent @category_child = Category.find(@category_id).parent @category_grandchild = Category.find(@category_id) endそれぞれを分類分けして並べ替えると下記の通りになります。
商品情報をinputタグに初期値として表示させるためのもの
⑦ 該当商品情報
商品画像をプレビューに初期値として表示させるためのもの
⑨ 該当商品の画像
カテゴリーをinputタグに初期値として表示させるためのもの
親・子・孫のname・idを取得し、ビュー側のcollection_selectで利用する情報
⑩ 該当商品のcategory_id(孫の数値)
② 親カテゴリーのnameとidが代入された配列
④ 子カテゴリーのnameとidが代入された配列
⑥ 孫カテゴリーのnameとidが代入された配列再入力時にビュー側のcollection_selectで利用する情報
⑧ categoryモデル内の全ての親カテゴリー
③ categoryモデル内の全ての子カテゴリー
⑤ categoryモデル内の全ての孫カテゴリー次にupdateメソッドの設定です。
editと同様に更新したい商品情報については、before_actionにて呼び出しを行っています。app/controller/items_controller.rbclass ItemsController < ApplicationController before_action :set_item, only: [:show, :edit, :update, :destroy] # 中略 def update # ① if item_params[:images_attributes].nil? flash.now[:alert] = '更新できませんでした 【画像を1枚以上入れてください】' render :edit else # ② exit_ids = [] item_params[:images_attributes].each do |a,b| exit_ids << item_params[:images_attributes].dig(:"#{a}",:id).to_i end ids = Image.where(item_id: params[:id]).map{|image| image.id } # ③ delete__db = ids - exit_ids Image.where(id:delete__db).destroy_all # ④ @item.touch if @item.update(item_params) redirect_to update_done_items_path else flash.now[:alert] = '更新できませんでした' render :edit end end end # 中略 private def item_params params.require(:item).permit(:name, :item_explanation, :category_id, :item_status, :auction_status, :delivery_fee, :shipping_origin, :exhibition_price,:brand_name, :days_until_shipping, images_attributes: [:image, :_destroy, :id]).merge(user_id: current_user.id) end def set_item @item = Item.find(params[:id]) end endまずこちらの記述は、画像が1枚もない時に更新できないようにif文でエラーハンドリングを記述しています。
item_params[:images_attributes].nil?の記述でparams内の画像が空か確かめています。
.nil?メソッドで空の場合はtureとなり、renderで編集画面に戻りflash.now[:alert]でエラーメッセージを表示します。①if item_params[:images_attributes].nil? flash.now[:alert] = '更新できませんでした 【画像を1枚以上入れてください】' render :edit else先ほどのif文でfalseとなった場合に②が動きます。
記述している内容は、exit_idsが更新ボタンを押した時点で入力されている画像のid、idsがDB内に保存されている更新前画像のidとなります。
exit_idsという配列を生成し、item_params[:images_attributes]という多次元配列内に含まれるidの値を取り出したいのでeach文でキーと値を順番に展開します。(|a,b| → a キーのこと b 値のこと)
そして多次元配列から値を取り出すために使用するのがdigメソッドです。
item_params[:images_attributes](多次元配列).dig(:"#{a(親キー)}",:id(子キー)).to_i(数値にする)という記述でidを取り出し、配列に代入します。
そしてidsにはDBから更新前の該当するレコードを取得し、mapメソッドにてidを抽出し代入します。②exit_ids = [] item_params[:images_attributes].each do |a,b| exit_ids << item_params[:images_attributes].dig(:"#{a}",:id).to_i end ids = Image.where(item_id: params[:id]).map{|image| image.id }ちなみにbinding.pryを使用してitem_params[:images_attributes]の中身を確認すると下記のように表示されます。画像1枚で更新ボタンをクリックしました。②のexit_idsに代入したい値は、子の配列内にある"322"という値です。
ターミナル(コンソール起動)[1] pry(#<ItemsController>)> item_params[:images_attributes] => <ActionController::Parameters {"0"=><ActionController::Parameters {"id"=>"322"} permitted: true>} permitted: true>③では、先ほどのexit_idsとidsを比較し、DBから初期値として編集画面に呼び出されていた画像を削除した場合にDB内の該当データを削除します。idsからexit_idsを引くことで削除されているidだけ残すことができます。それをdelete__dbに代入し、それを元にDBからレコードを検索し、destroy_allメソッドを使って削除します。
_allとしているのは複数レコードの場合も削除できるようにするためです。③delete__db = ids - exit_ids Image.where(id:delete__db).destroy_all④では、商品情報の更新を行っています。if文のエラーハンドリングにより更新できた場合には、update_dineルートを通り更新成功を伝える画面に遷移します。更新でなかった場合には編集画面に戻り、エラーメッセージを表示します。
一番最初の行に記述している@item.touchはitemsテーブルのupdate_atカラム(更新日時)も含めて更新するためのものです。
これを記述する理由は、後ほどご説明します。④@item.touch if @item.update(item_params) redirect_to update_done_items_path else flash.now[:alert] = '更新できませんでした' render :edit end次にupdate_doneメソッドの設定です。
更新成功を伝える画面には更新した商品詳細ページのリンクを設置しています。
先ほどのupdateメソッドの④で@item.touchを記述することによりitemsテーブルのupdate_atカラム(更新日時)を更新しました。orderメソッド、firstメソッドを使い、update_atカラム内を降順に一番目のものを@item_updateに代入します。app/controller/items_controller.rbdef update_done @item_update = Item.order("updated_at DESC").first end3.ビュー編集
全ての記述を載せると長くなってしまうのでここでは割愛して記述させて頂きます。
app/views/items/_form_edit.html.haml# ▼ 商品画像についての記載 .new__page__header = link_to image_tag("logo/logo.png", alt: "logo"), root_path = form_for @item do |f| = render 'layouts/error_messages', model: f.object .name__field#1 .form__label .lavel__name 出品画像 .lavel__Required [必須] #image-box-1{class:"#{@images.last.id}"} # ▼ ① プレビュー画像の表示 - @images.each do |img| .item-image{id:img.id} = image_tag(img.image.url,{width:"188",height:"180"}) .item-image__operetion .item-image__operetion--edit__delete__hidden 削除 %label.img-label{for: "img-file"} #image-box__container{class:"item-num-#{@images.length}"} #append-js-edit = f.fields_for :images do |image| .js-file_group{"data-index" => "#{image.index}"} = image.file_field :image, type: 'file', value:"#{image.object.id}",style: "", id:"img-file", class:'js-file-edit',name: "item[images_attributes][#{@item.images.count}][image]", data:{index:""} %i.fas.fa-camera # 中略 # ▼ カテゴリーについての記載 .append__category .category =f.collection_select :category_id, @category_children_array, :id, :name, {selected:@child_array}, {class:"serect_field"} - if @category_id == 46 or @category_id == 74 or @category_id == 134 or @category_id == 142 or @category_id == 147 or @category_id == 150 or @category_id == 158 .category__grandchild#children_wrapper =f.collection_select :category_id, @category_grandchildren_array, :id, :name, {},{selected:@grandchild_array, id:"child__category",class:"serect_field"} - else .category__child#children_wrapper =f.collection_select :category_id, @category_children_array, :id, :name, {},{selected:@child_array, id:"child__category", class:"serect_field"} .category__grandchild#grandchildren_wrapper =f.collection_select :category_id, @category_grandchildren_array, :id, :name, {selected:@grandchild_array}, {class:"serect_field"} # 省略下記の記述でプレビュー画像を表示しています。
.item-image_operetion--editdelete_hidden 削除 のhiddenという記述がポイントです。js編集の際にご説明します。①- @images.each do |img| .item-image{id:img.id} = image_tag(img.image.url,{width:"188",height:"180"}) .item-image__operetion .item-image__operetion--edit__delete__hidden 削除下記の記述でカテゴリーを表示しています。
if文で孫なしの場合と孫ありの場合で条件分岐させています。
collection_selectタグに中身の内{}を記述していますがこれはidを付与するにあたり、オプションを記述する際の引数の順番の関係で記述しています。カテゴリー.append__category .category =f.collection_select :category_id, @category_children_array, :id, :name, {selected:@child_array}, {class:"serect_field"} - if @category_id == 46 or @category_id == 74 or @category_id == 134 or @category_id == 142 or @category_id == 147 or @category_id == 150 or @category_id == 158 .category__grandchild#children_wrapper =f.collection_select :category_id, @category_grandchildren_array, :id, :name, {},{selected:@grandchild_array, id:"child__category",class:"serect_field"} - else .category__child#children_wrapper =f.collection_select :category_id, @category_children_array, :id, :name, {},{selected:@child_array, id:"child__category", class:"serect_field"} .category__grandchild#grandchildren_wrapper =f. :category_id, @category_grandchildren_array, :id, :name, {selected:@grandchild_array}, {class:"serect_field"}下記の記述は、更新成功時遷移画面のビューファイルになります。
app/views/items/_form_edit.html.haml= render "top/header" .done#fullsize .done__title 商品情報を更新しました .done__backlink = link_to 'トップページへ戻る', root_path, class: 'link' .done__backlink = link_to '更新した商品を確認する', item_path(@item_update), class: 'link' .done__backlink = link_to '出品中の商品一覧を見る', users_path, class: 'link' = render "top/lower-photo" = render "top/footer" = render "top/btn"4.JS編集
下記の通りjsファイルを編集します。
app/assets/javascript/edit_items.js$(function(){ var dataBox = new DataTransfer(); var file_field = document.getElementById('img-file') $('#append-js-edit').on('change','#img-file',function(){ $.each(this.files, function(i, file){ //FileReaderのreadAsDataURLで指定したFileオブジェクトを読み込む var fileReader = new FileReader(); //DataTransferオブジェクトに対して、fileを追加 dataBox.items.add(file) var num = $('.item-image').length + 1 + i var aaa = $('.item-image').length + i // ① var image_id = Number($('#image-box-1').attr('class')) var append_div_count = Number($('div[id=1]').length) var noreset_id = image_id + append_div_count fileReader.readAsDataURL(file); //画像が10枚になったら超えたらボックスを削除する if (num == 10){ $('#image-box__container').css('display', 'none') } //読み込みが完了すると、srcにfileのURLを格納 fileReader.onloadend = function() { var src = fileReader.result // ② var html= `<div class='item-image' data-image="${file.name}" data-index="${aaa}" id="${noreset_id-1}"> <div class=' item-image__content'> <div class='item-image__content--icon'> <img src=${src} width="188" height="180" > </div> </div> <div class='item-image__operetion'> <div class='item-image__operetion--edit__delete__file'>削除</div> </div> </div>` const buildFileField1 = (num)=> { // ③ const html = `<div class="js-file_group" data-index="${num}" id=1> <input class="js-file-edit" type="file" name="item[images_attributes][${append_div_count+9}][image]" id="img-file" data-index="${num}value="${noreset_id}" > </div>`; return html; } $('.js-file-edit').removeAttr('id'); //image_box__container要素の前にhtmlを差し込む $('.img-label').before(html); $('#append-js-edit').append(buildFileField1(num)); }; //image-box__containerのクラスを変更し、CSSでドロップボックスの大きさを変えてやる。 $('#image-box__container').attr('class', `item-num-${num}`) }); }); // ④ // 10枚登録されていた場合にボックスを消す $(document).ready(function(){ var image_num = $('.item-image').length if (image_num==10){ $('#image-box__container').css('display', 'none') } }); // ⑤ $(document).ready(function(){ $('.js-file-edit').removeAttr('id'); var num = $('.item-image').length - 1 var image_id = Number($('#image-box-1').attr('class')) var append_div_count = Number($('div[id=1]').length) var noreset_id = image_id + append_div_count const buildFileField = (num)=> { const html = `<div class="js-file_group" data-index="${num}" id=1> <input class="js-file-edit" type="file" name="item[images_attributes][100][image]" id="img-file" data-index="${num}" value="${noreset_id}" > </div>`; return html; } $('#append-js-edit').append(buildFileField(num)); }); // ⑥ $(document).on("click", '.item-image__operetion--edit__delete__hidden', function(){ //削除を押されたプレビュー要素を取得 var target_image = $(this).parent().parent(); //削除を押されたプレビューimageのfile名を取得 var target_id = $(target_image).attr('id'); var target_image_file = $('input[value="'+target_id+'"][type=hidden]'); //プレビューを削除 target_image.remove() target_image_file.remove() //image-box__containerクラスをもつdivタグのクラスを削除のたびに変更 var num = $('.item-image').length $('#image-box__container').show() $('#image-box__container').attr('class', `item-num-${num}`) }) // ⑦ $(document).on("click", '.item-image__operetion--edit__delete__file', function(){ //削除を押されたプレビュー要素を取得 var target_image = $(this).parent().parent(); var target_id = Number($(target_image).attr('id')); //削除を押されたプレビューimageのfile名を取得 var target_image_file = $('#append-js-edit').children('div').children('input[value="'+target_id+'"][type=file]'); //プレビューを削除 target_image.remove() target_image_file.remove() //image-box__containerクラスをもつdivタグのクラスを削除のたびに変更 var num = $('.item-image').length $('#image-box__container').show() $('#image-box__container').attr('class', `item-num-${num}`) })1行目の記述で一番最後に保存された画像idを取得しimage_id変数に代入します。2行目でビューファイル内のdivでid=1と付与されているタグの数を数えappend_div_count変数に代入します。3行目でそれを足し合わせ、
noreset_id変数に代入します。noreset_idは、画像を追加した際に新たに表示されるinputタグのvalueオプションにセットするためのものです。これを利用して削除の動作を行います。また、プレビュー画像の親となるdivタグにも同じ数値のものを狙って削除するため、idオプションにセットします。(②)①var image_id = Number($('#image-box-1').attr('class')) var append_div_count = Number($('div[id=1]').length) var noreset_id = image_id + append_div_count下記の記述は、画像データをinputタグに入力した際にイベント発火して生成されるプレビュー画像のHTMLです。
削除の記述でfileという記述がポイントです。ビュー編集時に言っていたhiddenの記述と見分け、新た生成されたinputタグなのか、最初から表示されているinputタグなのか判断します。②var html= `<div class='item-image' data-image="${file.name}" data-index="${aaa}" id="${noreset_id-1}"> <div class=' item-image__content'> <div class='item-image__content--icon'> <img src=${src} width="188" height="180" > </div> </div> <div class='item-image__operetion'> <div class='item-image__operetion--edit__delete__file'>削除</div> </div> </div>`下記の記述は、画像データをinputタグに入力した際にイベント発火して生成されるinputタグのHTMLです。
③const html = `<div class="js-file_group" data-index="${num}" id=1> <input class="js-file-edit" type="file" name="item[images_attributes][${append_div_count+9}][image]" id="img-file" data-index="${num}value="${noreset_id}" > </div>`;下記の記述は、readyメソッドにより画面がロード完了するとイベント発火し、プレビュー画像の数を数えて10枚だった場合に画像を入力するボックスを削除するという記述になります。
④// 10枚登録されていた場合にボックスを消す $(document).ready(function(){ var image_num = $('.item-image').length if (image_num==10){ $('#image-box__container').css('display', 'none') } });下記の記述は、readyメソッドにより画面がロード完了するとイベント発火し、inputタグが生成される記述になります。これを行わない場合、最初のinputタグへの入力が、既存の表示されているinputタグに入力されてしまいズレが生じてしまうため、画面ロード時に生成する必要があります。
⑤$(document).ready(function(){ $('.js-file-edit').removeAttr('id'); var num = $('.item-image').length - 1 var image_id = Number($('#image-box-1').attr('class')) var append_div_count = Number($('div[id=1]').length) var noreset_id = image_id + append_div_count const buildFileField = (num)=> { const html = `<div class="js-file_group" data-index="${num}" id=1> <input class="js-file-edit" type="file" name="item[images_attributes][100][image]" id="img-file" data-index="${num}" value="${noreset_id}" > </div>`; return html; } $('#append-js-edit').append(buildFileField(num)); });下記の記述は、editで呼び出した画像データが入力されているinputタグとプレビュー画像をプレビュー画像の左下に表示されている削除をクリックした際に削除するものです。
⑥$(document).on("click", '.item-image__operetion--edit__delete__hidden', function(){ //削除を押されたプレビュー要素を取得 var target_image = $(this).parent().parent(); //削除を押されたプレビューimageのfile名を取得 var target_id = $(target_image).attr('id'); var target_image_file = $('input[value="'+target_id+'"][type=hidden]'); //プレビューを削除 target_image.remove() target_image_file.remove() //image-box__containerクラスをもつdivタグのクラスを削除のたびに変更 var num = $('.item-image').length $('#image-box__container').show() $('#image-box__container').attr('class', `item-num-${num}`) })下記の記述は、新たに画像がinputタグに入力された際に生成呼び出した画像データが入力されているinputタグと新たに生成されたプレビュー画像をプレビュー画像の左下に表示されている削除をクリックした際に削除するものです。
⑦$(document).on("click", '.item-image__operetion--edit__delete__file', function(){ //削除を押されたプレビュー要素を取得 var target_image = $(this).parent().parent(); var target_id = Number($(target_image).attr('id')); //削除を押されたプレビューimageのfile名を取得 var target_image_file = $('#append-js-edit').children('div').children('input[value="'+target_id+'"][type=file]'); //プレビューを削除 target_image.remove() target_image_file.remove() //image-box__containerクラスをもつdivタグのクラスを削除のたびに変更 var num = $('.item-image').length $('#image-box__container').show() $('#image-box__container').attr('class', `item-num-${num}`) })下記の記述は、価格が入力された際に販売手数料、販売利益を計算して出力するものです。
編集した箇所は、2段目、4段目に記述してる内容で、readyメソッドにより画面ロード時に販売手数料、販売利益を計算し、表示させるというものです。app/assets/javascript/sales_commission.js$(function() { var input=$("#item_exhibition_price"),fee=1/10,feeIncluded=$("#sales_commission_price"); input.on("input",function(){ feeIncluded.text(Math.floor($(this).val() * fee).toLocaleString()); if($('sales_commission_price').present!=0){ sales_commission_price.append("円"); } }); $(document).ready(function(){ feeIncluded.text(Math.floor($("#item_exhibition_price").val() * fee).toLocaleString()); if($('sales_commission_price').present!=0){ sales_commission_price.append("円"); } }); }); $(function() { var input=$("#item_exhibition_price"),tax=9/10,salesProfit=$("#sales_profit_proce"); input.on("input",function(){ salesProfit.text(Math.ceil($(this).val() * tax).toLocaleString()); if($('sales_commission_price').present!=0){ sales_profit_proce.append("円"); } }); $(document).ready(function(){ salesProfit.text(Math.ceil($("#item_exhibition_price").val() * tax).toLocaleString()); if($('sales_commission_price').present!=0){ sales_profit_proce.append("円"); } }); });これで編集機能完成です。
ここまで読んでくださり、ありがとうございます。
- 投稿日:2020-06-28T18:40:52+09:00
もうわけからないぃぃ
webpackとかふつうに使っていると
一つのファイルにしてくれるけど
ローカルでの挙動とサーバーでの挙動が激的に変化するときがあって
そのときは、もう手におえない状態になるのだ。。なにせ、まとめたファイル(ここでは/dist/bundle.jsとでもしとこう)を
解析しようにも、そもそもごったまぜのスープになっており
どこのパッケージのどこのバージョンが。。とか基本は、省かれてる。なんとなくならわかるけど、じゃあどう対処するんだよーwってなる。。
そもそも、webpackとかって、いままで、ふつうにルートディレクトリーに
htmlファイルを置いとけば、ぜったい404とかでないのだが、
webpackの場合は、そうゆう形式でもないし。
基本react系ので、routerとかで、振り分けているのだし。。で、それっぽいキーワードで、ググっても、的を得ない回答しかでてこないしw
firebase系のjsもimportしてるのだが、最近はそれを疑っているありさま。w
もうここまでくるとカルト宗教化してるぜ
- 投稿日:2020-06-28T17:57:09+09:00
chat app.にてユーザー検索後、グループ追加を実装
はじめに・目的
- chat app.にてユーザー検索後、グループ追加を実装
- ユーザー検索をインクリメンタルサーチ
- サーチ後、追加ボタンでグループ追加。
- 削除ボタンでメンバー削除
- gif https://i.gyazo.com/00f3a7ce88036db231b951564a69c8a2.mp4
インクリメンタルサーチ実装
1.API側準備
- routing
routes.rbresources :users, only: [:index]
- users_controller.rbにindex定義
users_controllers.rbclass UsersController < ApplicationController def index respond_to do |format| format.html format.json end end
- app/views/usersディレクトリにindex.json.jbuilderファイルを作成
index.json.jbuilderjson.array! @users do |user| json.id user.id json.name user.name end2.テキストフィールド作成
_form.html.haml.chat-group-form__field .chat-group-form__field--left %label.chat-group-form__label{:for => "chat_group_チャットメンバーを追加"} チャットメンバーを追加 .chat-group-form__field--right .chat-group-form__search.clearfix %input#user-search-field.chat-group-form__input{:placeholder => "追加したいユーザー名を入力してください", :type => "text"}/ #user-search-result .chat-group-form__field.clearfix .chat-group-form__field--left %label.chat-group-form__label{:for => "chat_group_チャットメンバー"} チャットメンバー .chat-group-form__field--right #chat-group-users.js-add-user3.テキストフィールドに入力するたび、イベント発火
users.js$(function() { $("#user-search-field").on("keyup", function() { let input = $("#user-search-field").val(); console.log(input); }); });4.非同期通信ajax
users.js$("#user-search-field").on("keyup", function() { let input = $("#user-search-field").val(); $.ajax({ type: "GET", url: "/users", data: { keyword: input }, dataType: "json" }) }); });5.入力値を曖昧検索する
users_controller.rbdef index return nil if params[:keyword] == "" @users = User.where(['name LIKE ?', "%#{params[:keyword]}%"] ).where.not(id: current_user.id).limit(10) respond_to do |format| format.html format.json end endparams[:keyword]に値が入っていればそのまま処理は続けられ、空だった場合はそこで処理が終わります。
検索処理の内容は、whereメソッドを使用し、入力された値を含むかつ、ログインしているユーザーのidは除外するという条件で取得しています。6.非同期通信の結果を得て、HTMLを作成
users.js$(function() { function addUser(user) { let html = ` <div class="chat-group-user clearfix"> <p class="chat-group-user__name">${user.name}</p> <div class="user-search-add chat-group-user__btn chat-group-user__btn--add" data-user-id="${user.id}" data-user-name="${user.name}">追加</div> </div> `; $("#user-search-result").append(html); } function addNoUser() { let html = ` <div class="chat-group-user clearfix"> <p class="chat-group-user__name">ユーザーが見つかりません</p> </div> `; $("#user-search-result").append(html); } $("#user-search-field").on("keyup", function() { let input = $("#user-search-field").val(); $.ajax({ type: "GET", url: "/users", data: { keyword: input }, dataType: "json" }) .done(function(users) { $("#user-search-result").empty(); if (users.length !== 0) { users.forEach(function(user) { addUser(user); }); } else if (input.length == 0) { return false; } else { addNoUser(); } }) .fail(function() { alert("通信エラーです。ユーザーが表示できません。"); }); }); });メンバーの追加・削除機能の実装
1.追加ボタンが押された時にイベントが発火するようにする
users.js$(document).on("click", ".chat-group-user__btn--add", function() {
$(document).on
することで常に最新のHTMLの情報を取得する2.追加ボタンをクリックされたユーザーの名前を、チャットメンバーの部分に追加し、検索結果からは消す
まずは検索結果から名前を消す方法
users.js$(document).on("click", ".chat-group-user__btn--add", function() { console.log const userName = $(this).attr("data-user-name"); const userId = $(this).attr("data-user-id"); $(this) .parent() .remove();
- 「追加」ボタンがクリックされたユーザーが、検索結果一覧から消えるどのユーザーのhtmlかを特定するために
data-user-id
とdata-user-name
を取得するため、対象であるユーザー情報を定数へ代入。- イベントが発生した要素を取得し、その親要素を削除
- 今イベントが発生している追加ボタンのaタグを起点に、その親要素のchat-group-userを削除
次にメンバーをチャットメンバーに追加する
users.js$(document).on("click", ".chat-group-user__btn--add", function() { console.log const userName = $(this).attr("data-user-name"); const userId = $(this).attr("data-user-id"); $(this) .parent() .remove(); addDeleteUser(userName, userId); addMember(userId); });users.js(関数定義)function addDeleteUser(name, id) { //画面上の追加 let html = ` <div class="chat-group-user clearfix" id="${id}"> <p class="chat-group-user__name">${name}</p> <div class="user-search-remove chat-group-user__btn chat-group-user__btn--remove js-remove-btn" data-user-id="${id}" data-user-name="${name}">削除</div> </div>`; $(".js-add-user").append(html); } function addMember(userId) { //DB上への追加 let html = `<input value="${userId}" name="group[user_ids][]" type="hidden" id="group_user_ids_${userId}" />`; $(`#${userId}`).append(html); }
<input name='group[user_ids][]' type='hidden' value='ユーザーのid'>
この記述により、userがDBに保存される3.削除ボタンで、チャットメンバーから削除する機能を実装
users.js$(document).on("click", ".chat-group-user__btn--remove", function() { $(this) .parent() .remove(); });
.chat-group-user__btn--remove
がクリックされたら、その親要素を削除。4.ログイン中のユーザー(current_user)をチャットメンバーに表示し、他のメンバーを同じく表示する。
_form.html.haml#chat-group-users.js-add-user .chat-group-user.clearfix.js-chat-member %input{name: "group[user_ids][]", type: "hidden", value: current_user.id} %p.chat-group-user__name= current_user.name //current_user.idをgroup[user_ids]の配列に追加して、cuurent_user.nameを画面に表示 - group.users.each do |user| - if current_user.name != user.name .chat-group-user.clearfix.js-chat-member %input{name: "group[user_ids][]", type: "hidden", value: user.id} %p.chat-group-user__name = user.name %a.user-search-remove.chat-group-user__btn.chat-group-user__btn--remove.js-remove-btn 削除 //user.nameと削除ボタンを全て表示全体像
users.js$(function() { var search_list = $("#user-search-result"); function appendUser(user) { var html = ` <div class="chat-group-user clearfix"> <p class="chat-group-user__name">${user.name}</p> <div class="user-search-add chat-group-user__btn chat-group-user__btn-add" data-user-id="${user.id}" data-user-name="${user.name}">追加</div> </div> `; search_list.append(html); } function appendErrMsgToHTML() { var html = ` <div class="chat-group-user clearfix"> <p class="chat-group-user__name">ユーザーが見つかりません</p> </div>`; search_list.append(html); } function addDeleteUser(name, id) { let html = ` <div class="chat-group-user clearfix" id="${id}"> <p class="chat-group-user__name">${name}</p> <div class="user-search-remove chat-group-user__btn chat-group-user__btn--remove js-remove-btn" data-user-id="${id}" data-user-name="${name}">削除</div> </div> `; $(".js-add-user").append(html); } function addMember(userId) { let html = `<input value="${userId}" name="group[user_ids][]" type="hidden" id="group_user_ids_${userId}" />`; $(`#${userId}`).append(html); } $(".chat-group-form__input").on("keyup", function(){ var input = $(".chat-group-form__input").val(); $.ajax({ type: 'GET', url: '/users', data: { keyword: input }, dataType: 'json' }) .done(function(users) { search_list.empty(); if (users.length !== 0) { users.forEach(function(user) { appendUser(user); }); } else if (input.length == 0) { return false; } else { appendErrMsgToHTML(); } }) .fail(function() { alert('ユーザーが表示できません。'); }); }); $(document).on('click','.chat-group-user__btn-add',function(){ console.log const userName = $(this).attr("data-user-name"); const userId = $(this).attr("data-user-id"); $(this) .parent() .remove(); addDeleteUser(userName, userId); addMember(userId); }); $(document).on('click', '.chat-group-user__btn--remove', function(){ $(this) .parent() .remove(); }); });
- 投稿日:2020-06-28T17:45:41+09:00
【Vue.js】Scoped CSSのscoped具合を検証してみた
Vue.jsのScoped CSSは完全にはscopedじゃない、という話を聞いたので検証してみました。
結果、コンポーネント最上位の要素と、slotについては気をつける必要があることが分かりました。参考記事
親コンポーネントのstyleが子コンポーネントに影響を与える仕組みは、以下が詳しいです。
- https://qiita.com/wintyo/items/dfc232255ad45fdf376f
- https://www.dkrk-blog.net/javascript/vue_scopedcss
検証環境
- OS: macOS Catalina 10.15.5
- ブラウザ: Google Chrome 83.0.4103.116(Official Build)(64bit)
- Vue.js: 2.6.11
検証1: 素朴にstyleを当てた場合
親、子、孫コンポーネントにそれぞれ違った色を割り当てました。
すると、各コンポーネントの最上位の要素には親コンポーネントのスタイルが適用されてしまいました。
Parent.vue<template> <div class="root-div"> this is root div of parent <h1 class="my-title">this is h1 of parent</h1> <div class="h2-container"> <h2>this is h2 of parent</h2> </div> <Child /> </div> </template> <script lang="ts"> import Vue from "vue"; import Child from "@/components/Child.vue"; export default Vue.extend({ name: "Parent", components: { Child, }, }); </script> <style scoped lang="scss"> .root-div { color: red; } .my-title { color: red; } h2 { color: red; } </style>Child.vue<template> <div class="root-div"> this is root div of child <h1 class="my-title">this is h1 of child</h1> <div class="h2-container"> <h2>this is h2 of child</h2> </div> <GrandChild /> </div> </template> <script lang="ts"> import Vue from "vue"; import GrandChild from "@/components/GrandChild.vue"; export default Vue.extend({ name: "Child", components: { GrandChild, }, }); </script> <style scoped lang="scss"> .root-div { color: blue; // こいつが効いていない } .my-title { color: blue; } h2 { color: blue; } </style>GrandChild.vue<template> <div class="root-div"> this is root div of grandchild <h1 class="my-title">this is h1 of grandchild</h1> <div class="h2-container"> <h2>this is h2 of grandchild</h2> </div> </div> </template> <script lang="ts"> import Vue from "vue"; export default Vue.extend({ name: "GrandChild", }); </script> <style scoped lang="scss"> .root-div { color: green; } .my-title { color: green; } h2 { color: green; } </style>
検証2: 全称セレクタ(*)を使った場合
*
を使って乱暴にstyleを指定した場合どうなるかを見てみました。
結果、検証1と同じく、各コンポーネント最上位の要素だけは親コンポーネントのstyleの影響が出ました。
※検証1のコードから、Child.vueだけ変更しました。
Child.vue<template> <div class="root-div"> this is root div of child <h1 class="my-title">this is h1 of child</h1> <div class="h2-container"> <h2>this is h2 of child</h2> </div> <GrandChild /> </div> </template> <script lang="ts"> import Vue from "vue"; import GrandChild from "@/components/GrandChild.vue"; export default Vue.extend({ name: "Child", components: { GrandChild, }, }); </script> <style scoped lang="scss"> .root-div * { color: blue; } * { color: blue; } * * { color: blue; } </style>
検証3: globalのcssがある場合
当たり前ですが、ページ全体に適用されているcssがある場合、競合します。
例えば、ページ全体に* { color: black!important }
を当てたら、すべて黒になります。
App.vue<style lang="scss"> * { color: black!important; // プロダクトコードで書いたら殴られるやつ } </style>
もちろん、!important
を付けなければ、コンポーネント内のcssが優先的に適用されます。
(この辺はScoped CSSというよりはCSSそのものの話)
App.vue<style lang="scss"> * { color: black; } </style>
検証4: slotを使った場合
slotを使って、親から子へタグを渡した場合、渡したタグには親コンポーネントのstyleが適用されました。
Parent.vue<template> <div class="root-div"> this is root div of parent <h1 class="my-title">this is h1 of parent</h1> <div class="h2-container"> <h2>this is h2 of parent</h2> </div> <Child> <!-- 親からタグごとslotを挟むと、親のstyleが適用される --> <template #my-slot> <p>slot: this is p of parent</p> <div> <h2>slot: this is h2 of parent</h2> </div> </template> </Child> </div> </template> <script lang="ts"> import Vue from "vue"; import Child from "@/components/Child.vue"; export default Vue.extend({ name: "Parent", components: { Child, }, }); </script> <style scoped lang="scss"> .root-div { color: red; } .my-title { color: red; } h2 { color: red; } p { color: red; } </style>Child.vue<template> <div class="root-div"> this is root div of child <h1 class="my-title">this is h1 of child</h1> <div class="h2-container"> <h2>this is h2 of child</h2> </div> <slot name="my-slot"> <p>slot: this is p of child</p> <div> <h2>slot: this is h2 of child</h2> </div> </slot> <GrandChild /> </div> </template> <script lang="ts"> import Vue from "vue"; import GrandChild from "@/components/GrandChild.vue"; export default Vue.extend({ name: "Child", components: { GrandChild, }, }); </script> <style scoped lang="scss"> .root-div { color: blue; } .my-title { color: blue; } h2 { color: blue; } p { color: blue; } </style>
一方、親から子へ文字列だけを渡した場合、子のstyleが適用されました。
Parent.vue<template> <div class="root-div"> this is root div of parent <h1 class="my-title">this is h1 of parent</h1> <div class="h2-container"> <h2>this is h2 of parent</h2> </div> <Child> <!-- 親から文字列だけを渡すようにすれば、親のstyleは適用されない --> <template #my-slot-p>slot: this is p of parent</template> <template #my-slot-h2>slot: this is h2 of parent</template> </Child> </div> </template> <script lang="ts"> import Vue from "vue"; import Child from "@/components/Child.vue"; export default Vue.extend({ name: "Parent", components: { Child, }, }); </script> <style scoped lang="scss"> .root-div { color: red; } .my-title { color: red; } h2 { color: red; } p { color: red; } </style>Child.vue<template> <div class="root-div"> this is root div of child <h1 class="my-title">this is h1 of child</h1> <div class="h2-container"> <h2>this is h2 of child</h2> </div> <p><slot name="my-slot-p">slot: this is p of child</slot></p> <div> <h2><slot name="my-slot-h2">slot: this is h2 of child</slot></h2> </div> <GrandChild /> </div> </template> <script lang="ts"> import Vue from "vue"; import GrandChild from "@/components/GrandChild.vue"; export default Vue.extend({ name: "Child", components: { GrandChild, }, }); </script> <style scoped lang="scss"> .root-div { color: blue; } .my-title { color: blue; } h2 { color: blue; } p { color: blue; } </style>
で、結局何に気をつければいいのか
- コンポーネント最上位の要素になるべくstyleを当てない
- 当てざるを得ない場合、
.component-name-root { display: flex }
のように、重複しないクラス名を付けて、styleを指定する- 自前でslotを作る場合、極力DOM構造は子コンポーネント側で定義する
- デザインフレームワーク(vuetifyとか)の制約上slotにDOMを渡さざるを得ない場合は、予期せぬstyleが適用されないよう気をつける
所感
完全でないとはいえ、知らずに使っていてもそんなに気にならない程度にはscopedにできていると感じました。
個人的にはslotを使わないため、各コンポーネントの最上位のクラス名だけ気をつければ問題なし! という印象です。
Scoped CSS便利。
- 投稿日:2020-06-28T17:01:56+09:00
File , Blob で、ファイルのエクスポート/ インポート機能とIndexedDB登録, Vue CLI版
概要
前と同様、IndexedDB + Vue CLIで
Dexie.js ライブラリを使用した構成となり。
json ファイルのエクポート、インポートの調査してみました
File API , Blob等で実現できそうでした。・インポートした jsonファイルを IndexedDB
登録まで。可能でした構成
Chrome 83
Vue CLI
dexie : 3.0.1
vue: 2.6.11
vue-router参考
File
https://developer.mozilla.org/ja/docs/Web/API/FileBlob:
https://developer.mozilla.org/ja/docs/Web/API/Blobhttp://www.tohoho-web.com/html5/file_api.html
https://qiita.com/wadahiro/items/eb50ac6bbe2e18cf8813
package.json
https://github.com/kuc-arc-f/vue_spa3b_3files/blob/master/package.json
Vue components
・json生成、エクスポート
test.vue
https://github.com/kuc-arc-f/vue_spa3b_3files/blob/master/src/components/Files/test.vuevar arr = [ {id: 1 , name: "n1"}, {id: 2 , name: "n2"}, {id: 3 , name: "n3"}, {id: 4 , name: "n4"}, {id: 5 , name: "n5"}, ]; var content = JSON.stringify( arr ); var blob = new Blob([ content ], { "type" : "application/json" });・jsonアップロード, db登録
test2.vue
https://github.com/kuc-arc-f/vue_spa3b_3files/blob/master/src/components/Files/test2.vuechange_proc: function(){ console.log("#-change_proc") var self = this var files = window.document.getElementById('file1').files; for (var i = 0; i < files.length; i++) { var file = files[i]; console.log("i: " + i ); console.log("Name: " + file.name); console.log("Size: " + file.size); console.log("Type: " + file.type); console.log("Date: " + file.lastModified); console.log("Date: " + file.lastModifiedDate); var reader = new FileReader(); reader.onload = function(evt) { console.log("State: " + evt.target.readyState); console.log("Result: " + evt.target.result); var result =evt.target.result; var dat = JSON.parse(result || '[]') self.add_item(dat) self.items = dat console.log(dat) //document.getElementById("output1").innerHTML = evt.target.result; }; reader.onerror = function(evt) { console.log(evt.target.error.name); }; reader.readAsText(file, "utf-8"); } },
- 投稿日:2020-06-28T16:47:30+09:00
JavaScript ループ処理
JavaScriptのループ処理について備忘録としてまとめておきます。
Javascriptのループ処理
- for文
- for...in文
- for...of文
- while文
- do...while文
- 高階関数によるループ
for文
for (初期化式; 条件式; 加(減)算式)はじめに初期化式を書きます。その次にループを抜けるための条件を書き、最後に加(減)算をします。
const names = ["太郎", "次郎", "三郎"]; for (let i = 0; i < names.length; i++) { console.log(names[i]); } /* => "太郎" "次郎" "三郎" */ちなみに式を全て省略した書き方をすることもできます。この場合breakを書かないと無限ループになるので気をつけましょう。
const names = ["太郎", "次郎", "三郎"]; let i = 0; for (;;) { if (i >= names.length) break; // もしiがnamesの要素数以上だったら処理を抜ける。 console.log(names[i]); i++; } /* => "太郎" "次郎" "三郎" */for...in文
for...inはそれぞれの要素に対して反復処理を行います。
for (変数 in オブジェクト) { 処理 }配列
const names = ["太郎", "次郎", "三郎"]; for (let i in names) { // iにはインデックス番号が入る console.log(names[i]); } /* => "太郎" "次郎" "三郎" */オブジェクト
const obj = {name: "太郎", age: 20, gender: "male"}; for (let i in obj) { // iにはキーが入る console.log(obj[i]); } /* => 太郎 20 male */for...of文
for...ofは値に対して反復処理されます。
for (変数 of オブジェクト) { 処理 }配列
const names = ["太郎", "次郎", "三郎"]; for (let i of names) { // iにはそれぞれの値が入る console.log(i); } /* => "太郎" "次郎" "三郎" */オブジェクトの場合はObject.keys()で配列に変換にしてから処理をします。
const obj = {name: "太郎", age: 20, gender: "male"}; const keys = Object.keys(obj); // keysは["name", "age", "gender"] for (let i of keys) { // iにはそれぞれのキーが入る console.log(obj[i]); } /* => 太郎 20 male */while文
while文は形こそ違いますがやっていることはfor文と一緒です。
初期化式 while (条件式) { 加(減)算式 }条件式がtrueの場合反復処理を行うのでwhile文の中で条件をfalseにするための処理(今回はiをカウントアップ)しないと無限ループになるので気をつけましょう。
const names = ["太郎", "次郎", "三郎"]; let i = 0; while (i < names.length) { console.log(names[i]); i++; } /* => "太郎" "次郎" "三郎" */do...while文
do...while文は必ず最初の処理は実行され、その後は条件によって実行したい場合に使われます。
do { 処理 } while (条件式);doの間は実行されるので0番目の要素の太郎が出力されます。その後は条件式がfalseなので実行されません。
const names = ["太郎", "次郎", "三郎"]; let i = 0; do { console.log(names[i]); } while (i < 0); // => 太郎高階関数
高階関数とは、引数に関数を取るもの、もしくは関数を返り値として返すもののことを言います。
代表的な高階関数
- forEach
- map
- filter
- sort
- reduce
forEach
forEachは引数にとった関数を各要素に対して繰り返します。返り値はundefinedを返します。
const names = ["太郎", "次郎", "三郎"]; names.forEach(function(name) { console.log(name); }); /* => "太郎" "次郎" "三郎" */map
map()はforEach()と出力結果は同じですが返り値に関数を実行した結果を返します。返り値の配列を別に使いたい時に使用します。
const names = ["太郎", "次郎", "三郎"]; let newName = names.map(function(name) { console.log(name); return name + "さん"; }); /* => "太郎" "次郎" "三郎" */ console.log(newName); // => (3) ["太郎さん", "次郎さん", "三郎さん"] // forEachの場合はundefinedfilter
filter()は関数に与えられた条件に合致したものを新しい配列として返します。
const languages = ["JavaScript", "Ruby", "Go"]; let newName = languages.filter(function(lang) { return lang.length > 3; }); console.log(newName); // => (2) ["JavaScript", "Ruby"]sort
sort()は要素を昇順・降順に並べたりする際に使用します。
const nums = [4, 2, 5, 1, 3]; nums.sort(function (a, b) { return a - b; }); console.log(nums); // => (5) [1, 2, 3, 4, 5]reduce
reduce()は配列の各要素に対して関数を実行し、最終的に単一の値を返します。
const nums = [1, 2, 3, 4]; let newNums = nums.reduce(function(accumulator, currentValue) { return accumulator + currentValue; }); console.log(newNums); // => 10これは少しイメージしにくいので順を追って説明します。
まず最初の呼び出しで第1引数であるaccumulatorには0が、第2引数であるcurrentValueには最初の要素の値である1が入っています。
nums.reduce(function(0, 1) { return 0 + 1; });その返り値をaccumulatorが受け取ります。するとaccumulatorが1、currentValueは配列の次の値の2になります。
nums.reduce(function(1, 2) { return 1 + 2; });次はaccumulatorが3、currentValueも3になります。このように続けていくと最終的にaccumulatorが6、currentValueが4になりますので最終的に10が返されnewNumsの出力が10になります。
const nums = [1, 2, 3, 4]; let newNums = nums.reduce(function(accumulator, currentValue) { return accumulator + currentValue; // 6 + 4 }); console.log(newNums); // => 10最後に
JavaScriptのループ処理についてまとめてみました。
ループ処理は今どのような値が入っているのかを調べていくと理解が深まると思います。
MDN等を参照して手を動かしながら理解していきましょう。参考
- 投稿日:2020-06-28T16:47:30+09:00
JavaScript ループ処理を網羅的にまとめてみた。
JavaScriptのループ処理について備忘録としてまとめておきます。
Javascriptのループ処理
- for文
- for...in文
- for...of文
- while文
- do...while文
- 高階関数によるループ
for文
for (初期化式; 条件式; 加(減)算式)はじめに初期化式を書きます。その次にループを抜けるための条件を書き、最後に加(減)算をします。
const names = ["太郎", "次郎", "三郎"]; for (let i = 0; i < names.length; i++) { console.log(names[i]); } /* => "太郎" "次郎" "三郎" */ちなみに式を全て省略した書き方をすることもできます。この場合breakを書かないと無限ループになるので気をつけましょう。
const names = ["太郎", "次郎", "三郎"]; let i = 0; for (;;) { if (i >= names.length) break; // もしiがnamesの要素数以上だったら処理を抜ける。 console.log(names[i]); i++; } /* => "太郎" "次郎" "三郎" */for...in文
for...inはそれぞれの要素に対して反復処理を行います。
for (変数 in オブジェクト) { 処理 }配列
const names = ["太郎", "次郎", "三郎"]; for (let i in names) { // iにはインデックス番号が入る console.log(names[i]); } /* => "太郎" "次郎" "三郎" */オブジェクト
const obj = {name: "太郎", age: 20, gender: "male"}; for (let i in obj) { // iにはキーが入る console.log(obj[i]); } /* => 太郎 20 male */for...of文
for...ofは値に対して反復処理されます。
for (変数 of オブジェクト) { 処理 }配列
const names = ["太郎", "次郎", "三郎"]; for (let i of names) { // iにはそれぞれの値が入る console.log(i); } /* => "太郎" "次郎" "三郎" */オブジェクトの場合はObject.keys()で配列に変換にしてから処理をします。
const obj = {name: "太郎", age: 20, gender: "male"}; const keys = Object.keys(obj); // keysは["name", "age", "gender"] for (let i of keys) { // iにはそれぞれのキーが入る console.log(obj[i]); } /* => 太郎 20 male */while文
while文は形こそ違いますがやっていることはfor文と一緒です。
初期化式 while (条件式) { 加(減)算式 }条件式がtrueの場合反復処理を行うのでwhile文の中で条件をfalseにするための処理(今回はiをカウントアップ)しないと無限ループになるので気をつけましょう。
const names = ["太郎", "次郎", "三郎"]; let i = 0; while (i < names.length) { console.log(names[i]); i++; } /* => "太郎" "次郎" "三郎" */do...while文
do...while文は必ず最初の処理は実行され、その後は条件によって実行したい場合に使われます。
do { 処理 } while (条件式);doの間は実行されるので0番目の要素の太郎が出力されます。その後は条件式がfalseなので実行されません。
const names = ["太郎", "次郎", "三郎"]; let i = 0; do { console.log(names[i]); } while (i < 0); // => 太郎高階関数
高階関数とは、引数に関数を取るもの、もしくは関数を返り値として返すもののことを言います。
代表的な高階関数
- forEach
- map
- filter
- sort
- reduce
forEach
forEachは引数にとった関数を各要素に対して繰り返します。返り値はundefinedを返します。
const names = ["太郎", "次郎", "三郎"]; names.forEach(function(name) { console.log(name); }); /* => "太郎" "次郎" "三郎" */map
map()はforEach()と出力結果は同じですが返り値に関数を実行した結果を返します。返り値の配列を別に使いたい時に使用します。
const names = ["太郎", "次郎", "三郎"]; let newName = names.map(function(name) { console.log(name); return name + "さん"; }); /* => "太郎" "次郎" "三郎" */ console.log(newName); // => (3) ["太郎さん", "次郎さん", "三郎さん"] // forEachの場合はundefinedfilter
filter()は関数に与えられた条件に合致したものを新しい配列として返します。
const languages = ["JavaScript", "Ruby", "Go"]; let newName = languages.filter(function(lang) { return lang.length > 3; }); console.log(newName); // => (2) ["JavaScript", "Ruby"]sort
sort()は要素を昇順・降順に並べたりする際に使用します。
const nums = [4, 2, 5, 1, 3]; nums.sort(function (a, b) { return a - b; }); console.log(nums); // => (5) [1, 2, 3, 4, 5]reduce
reduce()は配列の各要素に対して関数を実行し、最終的に単一の値を返します。
const nums = [1, 2, 3, 4]; let newNums = nums.reduce(function(accumulator, currentValue) { return accumulator + currentValue; }); console.log(newNums); // => 10これは少しイメージしにくいので順を追って説明します。
まず最初の呼び出しで第1引数であるaccumulatorには0が、第2引数であるcurrentValueには最初の要素の値である1が入っています。
nums.reduce(function(0, 1) { return 0 + 1; });その返り値をaccumulatorが受け取ります。するとaccumulatorが1、currentValueは配列の次の値の2になります。
nums.reduce(function(1, 2) { return 1 + 2; });次はaccumulatorが3、currentValueも3になります。このように続けていくと最終的にaccumulatorが6、currentValueが4になりますので最終的に10が返されnewNumsの出力が10になります。
const nums = [1, 2, 3, 4]; let newNums = nums.reduce(function(accumulator, currentValue) { return accumulator + currentValue; // 6 + 4 }); console.log(newNums); // => 10最後に
JavaScriptのループ処理についてまとめてみました。
ループ処理は今どのような値が入っているのかを調べていくと理解が深まると思います。
MDN等を参照して手を動かしながら理解していきましょう。参考
- 投稿日:2020-06-28T16:32:39+09:00
【個人メモ】Express環境ハンズオン
完全に個人用メモです。
Node.jsとnpmがinstall済みであることを確認する
node -v, npm -vコマンド実行expressのインストール
作業ディレクトリでnpm initを実行してpackage.json作成
npm install express express-generator --saveexpressアプリケーションの雛形作成
./node_modules/.bin/express --view=pug sampleアプリ立ち上げ
cd sample
npm install
npm start
ブラウザからhttp://localhost:3000/につなぐと「Welcome to Express」が表示される。
- 投稿日:2020-06-28T16:28:05+09:00
three.js でlightmapを試す
概要
three.jsに限らず、3DCGのアプリケーションでは少しでも計算量を減らしてパフォーマンスを上げたいという要求があります。今回はそれに対する一つのアプローチであるlightmap(ライトマップ)を試したいと思います。
lightmapとは
lightmapを簡単に説明すると、物体の陰影をあらかじめ計算した結果の画像(テクスチャ)のことです。これを使用することで、光源の計算をする必要がなくなり、パフォーマンスが向上します。ただし、あらかじめ計算しておくという性質上、物体や光源が大きく動く場合には使用できません。
デモ
実際にthree.jsでlightmapを使用したデモを作成しました。
猿に陰影がありますが、シーンに光源は一切置いていません。
バージョンはr93
https://arihide.github.io/demos/lightmap/
ソースコードはこちら
https://github.com/Arihide/demos/tree/master/lightmap解説
ここではBlenderを用いてLightmapを作成し、three.jsで表示する方法を説明します。
lightmapの作成
まず始めに、Blenderでlightmap用のテクスチャとUVを作成する必要があります。例としてcyclesレンダラーで以下のような猿と光源の板を用意します。
次にdiffuseとは異なる、lightmap用のUVMapを用意します。
lightmapのUVを選択した状態で、猿を選択してEditモードに入り、「u」キーを押して「Lightmap Pack」を選択してUV展開します。
次に、Lightmap出力先のテクスチャを作成するために「UV ImageEditor」上で適当に画像を作成し…
その後、Node editor上でimage textureノードを作成し、右のBakeを実行しLightmapを作成します。
その結果、このようなテクスチャを得ることができればLightmap作成完了です!
three.jsでlightmapを使用する
最後に先ほど作成したlightmapをthree.jsで使用する方法を説明します。
といっても設定はとても簡単で、loader.load('./monkey.json', function (geo) { var lightmap = new THREE.TextureLoader().load('./lightmap.png'); var mat = new THREE.MeshBasicMaterial({ color: 0x0000ff, lightMap: lightmap }) var mesh = new THREE.Mesh(geo, mat); scene.add(mesh); render(); });という風に、3行目でlightmapを読み込んで5行目でマテリアルの引数にlightmapを与えるだけです。
- 投稿日:2020-06-28T15:35:37+09:00
three.js マウスクリックで一部の面の色を変える
概要
three.jsでクリックした面の色が変わるような物を作ります。
バージョンはr93デモ:https://arihide.github.io/demos/face_pick/
ソースコードはこちら
https://github.com/Arihide/demos/tree/master/face_pick解説
普通にオブジェクト追加するのと異なる部分に絞って説明します。
material作成時のvertexColorsの指定
以下のように、Materialを作成する際にvertexColorsを指定するようにします。
こうすることで、面ごとに色を指定できるようになります。var loader = new THREE.JSONLoader(); var mesh; loader.load('./rough_plane.json', function (geo) { var material = new THREE.MeshLambertMaterial({ color: 0xffffff, vertexColors: THREE.FaceColors }); mesh = new THREE.Mesh(geo, material); scene.add(mesh); renderer.render(scene, camera); });クリック時の処理
次に、クリックされた面の取得および、色変更について説明します。
以下がその処理部分です。window.addEventListener('click', function (e) { //クリックした面番号の取得 var square = getIntersectedIndex(e); if (square !== undefined && mesh) { mesh.geometry.colorsNeedUpdate = true; mesh.geometry.faces[square].color.set(0x00ff00); } renderer.render(scene, camera); }, false); var raycaster = new THREE.Raycaster(); function getIntersectedIndex(e) { var raymouse = new THREE.Vector2(); raymouse.x = (e.offsetX / width) * 2 - 1; raymouse.y = -(e.offsetY / height) * 2 + 1; raycaster.setFromCamera(raymouse, camera); var intersects = raycaster.intersectObjects(scene.children); if (intersects.length > 0) { var square = intersects[0].faceIndex; } return square; }ここでは大きく
1. getIntersectedIndexによる面番号の取得
2. 面番号から実際に色を変更
の二つで構成されています。1. getIntersectedIndexによる面番号の取得
そもそも、three.jsのメッシュには、faceIndexと呼ばれる面一つ一つに割り振られている番号があります。
THREE.RayCasterは交差判定処理をまとめたオブジェクトですが、「どの面番号の面と交差したか?」を調べることもできます。
getIntersectedIndexでは、これらを利用して、「クリックした面の面番号を返却する」処理になっています。2. 面番号から実際に色を変更
色を変更したい面番号がわかったら
mesh.geometry.faces[faceIndex].color.set(color);
で、色を変更することができます。ただし、rendererに色が変更されたことを知らせるために
mesh.geometry.colorsNeedUpdate = true;
も指定する必要があります。
- 投稿日:2020-06-28T15:34:57+09:00
Javascript: めっちゃ簡単なリスコフの置換原則(初級者向け)
最近、勉強会をする機会があったり、部下を育てるような話が出てきたのでSOLID原則についてまとめようと思いました。
これ、Javascriptばかり書いている人に伝えようと思っても、そもそもクラスつかってなかったり、インターフェースなんてないし、なんて人も多いので結構説明に困るんですよね。。。なので、Javascriptでの簡単な事例を通してSOLID原則を語るシリーズを書くことにしました。
今回は SOLID原則の 「L」 リスコフの置換原則(Liskov substitution principle)です。
ここにCMSで作成された記事データがあります。
const data = { { title: '記事A', body: '記事の中身', genre: '仮想通貨', option: { rate: '仮想通貨のレート情報'} }, { title: '記事B', body: '記事の中身', genre: '趣味', option: { affiliateLink: ['アフィリエイトリンク1', 'アフィリエイトリンク2']} }, { title: '記事C', body: '記事の中身', genre: 'VR', option: { appLink: 'アプリへのリンク' } }, // ... }データに応じてHTMLを組み立てる汎用メソッドを作ろうと考えます。が、困ったことにデータの genre によって固有のデータが option に割り振られていました。なので option の値に応じて処理を振り分ける処理も加えます。
const createArticle = article => { const title = `<h1>${article.title}</h1>` const body = `<div>${article.body}</div>` /* * NOTE: 各ジャンル固有のデータを取得する */ const rate = article.rate ? `<div>${article.option.rate}</div>` : null const affiliateLink = article.affiliateLink ? `<a href="${article.option.affiliateLink}">この商品のリンク</a>` : null const appLink = article.appLink ? `<a href="${article.option.appLink}">アプリのダウンロードはこちら</a>` : null // ... その他HTMLを組み立てる処理が続く }これで問題なく動作していましたが、しばらくして記事のジャンルに新しく「IT」が追加されました。
const data = { { title: '記事A', body: '記事の中身', genre: '仮想通貨', option: { rate: '仮想通貨のレート情報'} }, { title: '記事B', body: '記事の中身', genre: '趣味', option: { affiliateLink: ['アフィリエイトリンク1', 'アフィリエイトリンク2']} }, { title: '記事C', body: '記事の中身', genre: 'VR', option: { appLink: 'アプリへのリンク' } }, { title: '記事D', body: '記事の中身', genre: 'IT', option: { createExample: () => { /* 動作確認の例をiframeで埋め込むHTMLをつくる */ } } }, // <- これが追加された // ... }CMS担当者から次のような問い合わせが来るでしょう。
「ITの記事で実装例が表示されないんですけど...」
この場合、せっかく作った汎用メソッドを修正する必要があります。
const createArticle = article => { const title = `<h1>${article.title}</h1>` const body = `<div>${article.body}</div>` const rate = article.rate ? `<div>${article.option.rate}</div>` : null const affiliateLink = article.affiliateLink ? article.option.affiliateLink.reduce((current, link) => { current += `<a href="${link}">関連商品のリンク</a>` : null }, '') const appLink = article.appLink ? `<a href="${article.option.appLink}">アプリのダウンロードはこちら</a>` : null /* * 新しくITが追加されたので処理を追加する */ const createExample = article.createExample ? article.createExample() : null // ... }ここで気がつきます。
「またジャンル追加されたら修正しなきゃいけないのか。めんどくさい...」
オープン・クローズドの原則に基づいてデータ側の option を optionData という共通のプロパティに抽出します。
const data = { { title: '記事A', body: '記事の中身', genre: '仮想通貨', option: { rate: '仮想通貨のレート情報'} }, { title: '記事B', body: '記事の中身', genre: '趣味', option: { affiliateLink: ['アフィリエイトリンク1', 'アフィリエイトリンク2']} }, { title: '記事C', body: '記事の中身', genre: 'VR', option: { appLink: 'アプリへのリンク' } }, { title: '記事D', body: '記事の中身', genre: 'IT', option: { createExample: () => { /* 動作確認の例をiframeで埋め込むHTMLをつくる */ } } }, // ... }.map(item => { // 固有のプロパティを optionDataにまとめる const optionData = item.option[Object.keys(item.option)[0]] return { title: item.title, body: item.body, optionData, } })const createArticle = article => { const title = `<h1>${article.title}</h1>` const body = `<div>${article.body}</div>` /* * NOTE: 各ジャンル固有のデータは optionData で取得できるようになった */ const option = article.optionData // ほんとにこれで大丈夫? // ... }これで問題ない...でしょうか?よく考えると作成されるデータは次のようになっているでしょう
const data = { { title: '記事A', body: '記事の中身', genre: '仮想通貨', option: '仮想通貨のレート情報' }, { title: '記事B', body: '記事の中身', genre: '趣味', option: ['アフィリエイトリンク1', 'アフィリエイトリンク2'] }, { title: '記事C', body: '記事の中身', genre: 'VR', option: 'アプリへのリンク' }, { title: '記事D', body: '記事の中身', genre: 'IT', option: () => { /* 動作確認の例をiframeで埋め込む */ } }, // ... }「optionが文字列だったり、配列だったり、関数だったり。自由すぎる...」
こんなデータが飛んでくる時点でいろいろ問題があるのですが、この場合はCMS側の実装なのでどうにもなりません。そういうこともあるでしょう。
問題は、共通処理でこのデータが正しく処理されるかと言うことですが、当然エラーが発生するはずです。データの型が全然異なりますから。じゃあどうすればいいの?と言うことになりますが、諦めましょう。
正確には共通じゃない部分まで汎用メソッドに無理やり実装するのをやめましょう。/** * ここでは記事データの共通部分をHTMLに変換します * 共通部分は次の2つです * ・title: 記事タイトル * ・body: 記事本文 */ // const createArticle = article => { const createBasicArticle = article => { const title = `<h1>${article.title}</h1>` const body = `<div>${article.body}</div>` // ... } // ... option については別途考えるここで1点大事なのは、
基本の記事データを title と body を持ったオブジェクトだと定義したことです。
なので option について何かしようとしたときには基本の記事データではない処理を実装しているということになり、条件分岐をたくさん書かなければ無なくなるのでダメですよね、というふうに見ることができます。逆に言うと、title と body に 文字列 を持っているものならどんなデータだろうと動作に問題ないと言うことです。
まとめると
どのデータも置き換え可能です
どのデータも動作に違いはありません
この2つを守ってロジック部分を作らないとメンテしずらいコードになってしまうといってるわけですね。
- 投稿日:2020-06-28T15:24:05+09:00
JavaScript自分用まとめ
変数宣言
- const : 再代入不可
- let : 再代入可能
- var : なるべく使用しないほうが良い
const a; let b; var c;関数
function sample(a){ console.log(a); } const sample2 = function(a){ console.log(a); } const sample3 = a => console.log(a); //引数が1つ const sample4 = (a,b) => console.log(a+b); //引数が2つ const sample5 = (a,b) => { const c = a + b; console.log(c); }配列とforEach
・プログラム
const arry = [1,2,3]; arry.forEach(function(a,b,ary){ //(値、添え字、配列そのもの) console.log(a,b.arry); } )・コンソール
1 0 [1,2,3] 2 1 [1,2,3] 3 2 [1,2,3]reduceメソッド
・プログラム
const arrt = [1,2,3]; arry.reduce(fuction(a,b){ //1回目:(配列の最初の要素もしくは初期値,配列の2番目の要素) //2回目:(戻り値,配列の要素) console.log(a,b); return a + b; },0); //初期値は0に設定・コンソール
0,1 //初期値,配列の最初の要素 1,2 //0+1,配列の2番目の要素 3,3 //1+2,配列の3番目の要素DOM
- Document Object Model (DOM) は HTML や XML 文書のためのプログラミングインターフェイス
- JavaScriptでHTMLを操作することができる
例
- querySelector : 指定した最初の要素を取得
var el = document.querySelector(".myclass");
- add : 追加操作
- remove : 削除
- toggle : 要素を追加したり削除したりする
- trim : 空白を削除
- addEventListener : イベントの追加
function chageMyStyle(){ //処理 } btn.addEvenrListener('click',changeMyStyle);クラスとオブジェクト
- クラス : 設計図
- オブジェクト : 実際のモノ
- コンストラクター : 必ず必要なモノ
class Car{ constructor(color, company) { this.color = color; this.company = company; } run(){ //車を走らせる } stop(){ //車を止める } } const MyCar = new Car("red","nissen"); //設計図CarでMyCarというオブジェクトを作成するイメージ console.log(MyCar.color); //red console.log(MyCar.company); //nissen Mycar.run(); //走る
- 投稿日:2020-06-28T15:20:55+09:00
javascriptのアロー関数とthisの挙動の違い
アロー関数は、より短く、簡潔に書ける関数の構文です。
javascript
は得意ではありませんが、いろいろと動的に動かして遊びたいので学習メモとしてまとめます。関数をアロー関数に書き換える
例順を追って、関数のシンタックスを変換していきます。
例文は、引数
i
に2
をかける関数です。function timesTwo(i) { return i * 2; } const res = timesTwo(2); console.log(res);1.変数や定数に格納することで、下記のように書き直すことができます。
const timesTwo = function(i) { return i * 2; }2.
function
を消して=>
を付け足し、アロー関数に書き換えます。const timesTwo = (i) => { return i * 2; }3.引数がひとつの場合は、
( )
を省略できます。const timesTwo = i => { return i * 2; }4.関数の中身が1行しかない場合は、
{ }
とreturn
を省略できます。const timesTwo = i => i * 2;アロー関数を使うことで、スッキリと書くことができました。
無名関数とアロー関数のthisの挙動の違い
無名関数の扱う
this
と、アロー関数の扱うthis
には注意が必要です。let normalFn; normalFn = { id: 43, counter: function() { console.log(this.id); // この場合の無名関数のthis.idはwindowオブジェクトを指す window.setTimeout(function() { console.log(this) // undefined }, 1000); } }; normalFn.counter();上記の例を
console.log(this)
で確認すると、window
オブジェクトを参照していることが分かります。// thisをバインドする let normalFn; normalFn = { id: 43, counter: function() { console.log(this.id); // アロー関数はひとつ上のスコープのthisをバインドする window.setTimeout( () => { console.log(this) // {id: 43, counter: ƒ} }, 1000); } }; normalFn.counter();アロー関数に書き換えると、
this
はアロー関数内で定義されず、
ひとつ上のスコープにthis
を探しに行きます。
(スコープチェーンを辿って、ひとつずつ上のスコープでthis
を検索します。)参考
- 投稿日:2020-06-28T14:07:47+09:00
駆け出しエンジニア日記 #4
日記の目的
- 1週間通して得た知識や仕事に対する考え方のアウトプットの機会を作るため
- マークダウン記法に使いこなすようになるため
- 将来読み返した時に楽しむため
今後、エンジニアとして働く人たちの参考記事になるように目次
1.振り返り
2.学んだこと
3.まとめ
4.今週の参考記事振り返り
6/22~6/26
- 開発業務(使用言語:javaScript)
- プランごとのアプリの数の取得(これが本当に難しい)
- sequalizeのscopeメソッドの活用
- Array#map/filter/forEachの活用
- LPコーディング
- bootstrapの活用
- paddingとかmarginに手こずる
- 求人対応
- メール作成
- 日程調整
bootcamp(自分で進める社内研修みたいなもの)
- javaの勉強(というよりオブジェクト指向の理解度の向上)
サポート業務の増加
- サポート対応に追われ始めている
- 近頃アウトソースするっぽい
- リリース告知文作成
学んだこと
html/cssのmargin padding
- 最近は自社製品のLPコーディングをメインに行なっている
- 今週は業務に追われ学んだこと他にナシ
まとめ
- 開発しながら毎日勉強という日々
- わからないことが多すぎて
- 少し仕事の進捗が行き詰まりつつある(やばい)
- 学習してきたはずだがbootstrapをうまく使えなかったり
- cssのmargin/padding/displayに苦しめられたり
- 採用活動が活発化してきた
- メール作成
- 日程調整
- (そろそろ経験者も視野に入れて採用活動するフェーズに移行するべきか?)
今週の参考記事
- 投稿日:2020-06-28T12:29:11+09:00
Next.jsで環境変数を利用する
とりあえずやり方教えて!
.env
を用意します。.envTEST='HOGEEEEEE'
next.config.js
を編集します。ない場合はプロジェクトのルートディレクトリでvim next.config.js
して作ります。next.config.jsmodule.exports = { env: { TEST: process.env.TEST } }indexかなんかで表示してみます。
index.tsxexport const Index = () => { const fuga = process.env.TEST; return ( <p>{ fuga }</p> ) };あとは
yarn dev (OR npm run dev?)
をして..._人人人人人人_
> HOGEEEEEE <
 ̄Y^Y^Y^Y^Y^Y^ ̄概要
環境
- Fedora 31 Workstation
- nodejs 13.14.0
- yarn 1.22.4
- next 9.4.4 ※9.4以上じゃないとできません
- react 16.13.1
- reac-dom 16.13.1
解説
参考元は公式ドキュメントです: https://nextjs.org/docs/basic-features/environment-variables
さて、Next.js 9.4から
.env.local
系の環境変数ファイルを読み込む、またブラウザに直接変数を読み込ませる機能を使うことができるようになりました。
後者については本題とずれるので割愛させていただきますので、詳細はドキュメントをお読みください。これにより、例としてHeadコンポーネントを使用した、サイトタイトルの設定などがよくわからない設定を書かずに行えます。
また公式の文言によると、
Note:
.env
,.env.development
, and.env.production
files should be included in your repository as they define defaults..env*.local
should be added to.gitignore
, as those files are intended to be ignored..env.local
is where secrets can be stored.ようは、「
.env
、.env.development
、.env.production
はリポジトリに含まれなければならない、そして.env*.local
(末尾に.localがついているものですね)は.gitignore
によって除外されなければならない。」となっているので、基本的にはその運用になるかと思います。また、
In general only one .env.local file is needed. However, sometimes you might want to add some defaults for the development (next dev) or production (next start) environment.
「next dev(yarn dev)では
.env.development
(.localも)、next startでは.env.production
、が使われます」とあるので、こちらも有効活用しましょう。参考
https://nextjs.org/docs/basic-features/environment-variables
- 投稿日:2020-06-28T10:55:31+09:00
令和元年? JavaScript International Date フォーマットで和暦を表示させるには (再)
この記事は 平成最後の日、2019年4月31日に別のアカウントからポストしたものをこちらに移行した記事です。今更感があるかもしれないですが ECMA 402 について知るのは悪くないと思います〜。
令和元年おめでとうございます ?
さて、今回は新年号にふさわしく International Date format で和暦を表示する方法について書いていきたいと思います。
JavaScript
Intl.DateTimeFormat
オブジェクトを使えば、言語とローケルに特化した日付と時刻のフォーマットが可能になります。The ECMAScript Internationalization API(国際標準化 API) は、日付と時刻そして貨幣の表示のローカライズを JavaScript で可能にするために作られたもので、2010 年に初版が発行されて現在は第6版が最新です。この API は最新のブラウザではすでにサポート済です。(参照 Can I use)
Intl
オブジェクトのプロパティの一つ、DateTimeFormat
が言語に応じた日付と時刻のフォーマットを可能にします。ということは日本のローカルタイムを令和で表現できるということなのです。ただし、ブラウザがアップデートされていればの話ですが。
結論を言っておきますと、現時点で最新の Chrome ではまだ令和が和暦に追加されていませんので、この先のコードを正式にアウトプットするには、Chrome Canary 版が必要です。(この記事を急いで書いたので、しかも私のローケルでは時差でまだ平成ということもあり他のブラウザを試していませんが?♀️ Firefox では今のところまだのようです。Mozilla の Emma さん情報ありがとうございました。)
(現在はサポート済)? Using DateTimeFormat
ローケルと言語が設定されていない場合は
DateTimeFormat
はデフォルトの設定で表示されます。ですので、米国サンフランシスコにいる私が自分のマシン上のブラウザでこのコードを動かすとこのように今日の日付を en-US で表示します。
new Intl.DateTimeFormat().format(Date.now())(本当は私のタイムゾーンではまだ4月31日なのですが、日付が変わった程で?)
"5/1/2019"?? Specifying Locales
ここでlocales 引数を使い、ローケル識別子(言語コードと、国、地域コード)を設定することによって日付と時刻をローカライズしてみましょう。
例えば、ロシアの場合。
new Intl.DateTimeFormat('ru-RU').format(Date.now()) // "02.04.2019" と表示されるそして日本の場合。
new Intl.DateTimeFormat('ja-JP').format(Date.now()) // "2019/5/1"しかし、これではグレゴリオ暦のままです。完全に和暦で表示するにはどうしたらいいでしょうか。
ローケル識別子
ja-JP
だけでは足りませんので-u-ca-japanese
という拡張キーを追加し、ja-JP-u-ca-japanese
としましょう。なんだか長いのですが、
-u
拡張キーの識別は
-ca
カレンダー・タイプの
-japanese
日本のカレンダーを表示という意味になります。詳しくは Unicode Technical Standard #35 で。
(エラリー ジャンクリストフ さん、拡張キーについて詳しい情報のあるリンクを教えてくださって、ありがとうございました!)
new Intl.DateTimeFormat('ja-JP-u-ca-japanese').format(Date.now()) // 昨日までは、"31/4/30" // 今日は "1/5/1"さて和暦っぽい年号ではありますが、元号が表示されていません。どうしたら良いでしょうか。
元号を表示するには options 引数の、この場合
era
をlong
に設定しましょう。new Intl.DateTimeFormat('ja-JP-u-ca-japanese', {era:'long'}).format(Date.now()) // "令和1/5/1"さてこれで、"令和1/5/1" と表示されたら、お使いのブラウザが無事に令和をサポートしています?
"平成31年5月1日" と表示された方はブラウザをアップデートをする(もしくは出るまで待つ)。待てども待てども表示されないブラウザはバグを提出しましょう。
(注、令和にはいってから Chrome 上でのこの和暦の表記が「令和n年n月n日」から「令和n/n/nn」のように変更になったようです。いつ変更になったのかは私はわかりませんがたぶん元年の途中からだと思います。)
せっかくですのでタイのローケルでも試してみましょう。
new Intl.DateTimeFormat('th-TH-u-nu-thai', {era:'long'}).format(Date.now()) // "๑ ๕ พุทธศักราช ๒๕๖๒"なんか読めないけどイイ!?
タイはお釈迦様が入滅した翌年を仏滅紀元元年としており今年は 2562 年のようです。というわけで、もっと詳しく知りたい方は MDN Web Docs をどうぞ。
ちなみに、Is it Reiwa (令和) yet? というしょぼいテストサイトも作ってみました。
https://reiwa-yet.glitch.me/.それでは、今元号ともよろしくおねがいします。
元記事は、4月1日に元号が発表された直後に英語で書いたものです。今回、令和元年のはじまりを記念して日本語でも書きました。
https://girliemac.com/blog/2019/04/02/javascript-i18n-reiwa-era/
- 投稿日:2020-06-28T10:27:03+09:00
tusdとtus-js-client, uppyを使ったファイルアップロードをapache2.4のCentOS7で構築する(cookie編)
v2
- CORSは不要と気がつきましたので、
httpd.conf
の設定は不要だと思います(不具合が出たらまた見直すかもしれませんが)- 理由は前回記事をご覧ください。
- 変更点はv2としています。
目的
- tusdは巨大ファイルを分割して送信し、中断してもレジュームが聞くオープンソース(MITライセンス)のサービスです。
- これまでhttpと、httpsでのXHR通信でうまくいった記事を書きました。
- tusdとtus-js-client, uppyを使ったファイルアップロードをapache2.4のCentOS7で構築する(http編)
- tusdとtus-js-client, uppyを使ったファイルアップロードをapache2.4のCentOS7で構築する(https編)
- 今回はtusdを起動するときに、cookieを受け取ってユーザ毎やセッション毎に必要な処理の仕組みを試しました。
- その備忘録です。
環境
- CentOS7
- apache 2.4.41
- browser Firefox77 (MacOS)など
- tusd v1.0
- tus-js-client v2
仕組み
- tusでファイルアップロードする仕組みはクライアント(ブラウザ)から
https://www.your-site.net/files/
にfileをuploadすると宣言して、apacheではリバースプロキシで/files/
へのアクセスをhttp://localhost:1080/
に変換し、サーバのデーモンtusd
がこのポートを監視して、クライアントからのfileを/files/に格納する動きをするものでした。詳しくは前回の記事をご覧ください。- このとき
tusd
はファイルを生成するときとファイル処理の完了したときにコマンドを発行する、あるいはhttp
で処理することができます。これをhookと呼びます。git
と同じ仕組みだとか。といってもファイルフックはtusd
の処理タイミングで決まった名前のコマンド(シェルスクリプト)をキックするというだけです。- 取説はこちら:github.com/tusd/.../docs/hooks.md
- LATS(Linux, Apache, Textfile and Shell-script)では
http hook
でなく当然file hook
(シェルコマンド)を選択します。フックコマンド
- 日本語訳をgoogle翻訳ベースにポイントだけ載せておきます。
- pre-create:アップロード前にトリガーされ、ユーザー確認などができます。アップロードの拒否は終了コードを0以外にするだけです。
- post-create: アップロード作成後にトリガーされ、新しいアップロードを処理する必要があることをシステムの他の部分に通知します。
- pre-finish: アップロードが完全に終了した後、クライアントに応答が返される前にトリガーされ、これはブロッキングフックであり、アップロードされたファイルを検証または後処理するために使用できます。0以外の終了コードの場合400、HTTP 500エラーがクライアントに返されます。
- post-finish: アップロードが完全に終了した後にトリガーされ、すべてのチャンクが転送されストレージに保存されます。このファイルを使用してさらに処理するか、他のアプリケーションにこのアップロードの完了を通知できます。
- post-terminate: アップロードが終了した後にトリガーされ、アップロードが完全に停止し、関連チャンクがストレージから完全に削除されています。アップロードのコンテンツを取得できなくなり、このアップロードが再開または終了しないことを他のアプリケーションに通知することができます。
- post-receive: 実行中のアップロードごとにトリガーされ、現在の進行状況を示します。これは、サーバーがクライアントからより多くのデータを受信するたびに、最大で毎秒発行されます。オフセットプロパティは、その時点でサーバーに転送された合計バイト数に設定されます。
インプリ
apaheでCORSでcookieがやり取りできように設定[v2]
http.confにCORSでのcookieの設定(Access-Control-Allow-Credentials)を追加します。- この設定はクロスオリジンが発生していないので不要だと思います。
http.conf<Directory /var/www/files> Header always set Access-Control-Allow-Origin "https://www.your-site.net" Header always set Access-Control-Allow-Credentials "true" </Directory>
htmlでCORSでcookieをやり取りできるようにする[v2]
tus-js-clientのデモのindex.htmlでtus.jsのoptionにwithCredential
を設定します。75行目あたりにあります。- この設定はクロスオリジンが発生していないので不要だと思います。[v2]
tus-js-cilent/demos/browser/demo.jsvar options = { // // Cookie対応する withCredentials : true, ...クライアントのブラウザにクッキーを書き込む
- JavaScriptでクッキーを(例えばセッションID情報)を格納します。最終行に追加しておきます。
tus-js-cilent/demos/browser/demo.js// クッキーのセット document.cookie = 'ID_SESSION=c043fe31-cf1b-4c77-AAAA-7a49c49c14d5; path=/; secure;'tusdでファイルフックできるようにする
- 処理の段階で
tusd
でhook
できるようにtusd
の起動を次のようにしました。run_tusd.bash$ cat $(which run_tusd.bash) #!/bin/bash -vxeu # tusdをオプション込みで起動する # log file exec &> /var/www/log/$(basename $0).$(date '+%y%m%d_%H%M%S').$$.bash # # https用コマンドライン sudo -u apache /usr/local/bin/tusd -upload-dir /var/www/files/ -behind-proxy -hooks-dir /var/www/hooks -verbose &
- tusdのフックはオプション
-hooks-dir
でディレクトリを指定することでファイルフックが可能になります。このディレクトリにpre-create
とかの名前のshell scriptを書いておけばいいです。tusdのファイルアップロード開始時にクッキーを取り出す
- ファイルアップロード開始時は
pre-create
をキックするので、これにクッキー処理を書いておきます。下記の例は設定したキッキーのセッションIDを読み出します。- 実験用なのでどういった起動時の環境変数もモニターします。
$ cat pre-create #!/bin/bash -xveu exec 2> /var/www/log/$(basename $0).$(date '+%y%m%d_%H%M%S').$$.bash CMD_SESSION=$( basename $0 ) ___TUS___=$( cat - |jq ) ___ENV___=$( printenv | sort )
- コード解説します。
tusd
は-hooks-dir /var/www/hooks
で指定したディレクトリのpre-create
という名前のコマンドを起動します。- ここでは
bash
で起動します。- logを取っておきます
- 起動されたコマンド名を変数
CMD_SESSION
に設定されます。tusd
は起動するフックにstdin
として変数を格納します。そのため読み出しをcat -
で読み出し、jq
で可読性をあげ、変数___TUS___
に入れておきます- ついでにどんなSHELL環境変数だったのかを変数
___ENV___
に入れておきます。tusd
からキックされたpre-create
のクッキーがどうだったのか、logファイルを確認しましょう。確認(pre-createに渡された変数)
- それではlogを確認しましょう。全行のせます。セキュリティの観点で省略している部分やサイト名、IPは変更しています。 ログファイルです。
/var/www/log/pre-create.200628_084337.14802.bash$ cat pre-create.200628_084337.14802.bash CMD_SESSION=$( basename $0 ) ++ basename /var/www/hooks/pre-create + CMD_SESSION=pre-create ___TUS___=$( cat - |jq ) ++ cat - ++ jq + ___TUS___='{ "Upload": { "ID": "", "Size": 37091, "SizeIsDeferred": false, "Offset": 0, "MetaData": { "filename": "your-upload-music-file.mp3" }, "IsPartial": false, "IsFinal": false, "PartialUploads": null, "Storage": null }, "HTTPRequest": { "Method": "POST", "URI": "/files/", "RemoteAddr": "[::1]:57482", "Header": { "Accept": [ "*/*" ], "Accept-Encoding": [ "gzip, deflate, br" ], "Accept-Language": [ "ja,en-US;q=0.7,en;q=0.3" ], "Connection": [ "Keep-Alive" ], "Content-Length": [ "0" ], "Cookie": [ "ID_SESSION=c043fe31-cf1b-4c77-AAAA-7a49c49c14d5" ], "Origin": [ "https://www.your-site..net" ], "Referer": [ "https://www.your-site.net/vendors/node_modules/tus-js-client/demos/browser/index.html" ], "Tus-Resumable": [ "1.0.0" ], "Upload-Length": [ "37091" ], "Upload-Metadata": [ "filename My4xLjItaHR0cHPjgYvjgpnjgrvjgq3jg6Xjg6rjg4bjgqPjgabjgpnkv53orbfjgZXjgozjgZ/mjqXntprjgpLnorrnq4vjgabjgpnjgY3jgarjgYTjgajoqIDjgo/jgozjgZ/jgajjgY3jga7lr77lh6bms5UubWQ=,filetype" ], "User-Agent": [ "Mozilla/5.0 (省略) ], "X-Forwarded-For": [ "(省略)" ], "X-Forwarded-Host": [ "www.your-site.net" ], "X-Forwarded-Proto": [ "https" ], "X-Forwarded-Server": [ "www.your-site.net" ] } } }' ___env___=$( printenv | sort ) ++ printenv ++ sort + ___env___='HISTSIZE=1000 HOME=/usr/share/httpd HOSTNAME=(省略) LANG=ja_JP.UTF-8 LOGNAME=apache LS_COLORS=(省略) MAIL=(省略) PATH=(省略) PWD=/var/www/hooks SHELL=/sbin/nologin SHLVL=1 SUDO_COMMAND=/usr/local/bin/tusd -upload-dir /var/www/files/ -behind-proxy -hooks-dir /var/www/hooks -verbose SUDO_GID=0 SUDO_UID=0 SUDO_USER=root TERM=xterm-256color TUS_ID= TUS_OFFSET=0 TUS_SIZE=37091 USER=apache USERNAME=apache XDG_SESSION_ID=355 _=/bin/printenv'設定したcookie値を、きちんと
pre-create
で読むことができていますね。これはstdin
からの情報にあります。このコマンドを起動した環境変数には(もちろん)cookie情報がないことも確認できました。これでセッション情報に基づいたユーザ毎の処理をLATSでもできそうです。uppyにもCredentialの設定はできますよ。[v2]どなたかのお役に立てましたらとても嬉しいです。今回の記事は”動いたよ”という段階なので間違った設定や使い方があるかもしれません。その時はご指摘ください。
- 投稿日:2020-06-28T10:00:21+09:00
〔JavaScript〕indexOfの文字列、配列での使い方まとめ
はじめに
JavaScriptの
indexOf
で躓いてしまったので、備忘録として残しておきますindexOfとは
文字列に含まれる部分文字列を検索する為に、
indexOfメソッド
を利用します。indexOfメソッド
は、指定された部分文字列が最初に登場した位置を、文字列の先頭を0としたインデックス番号で返します。文字列が見つからなかった場合、戻り値は-1となります。使い方
indexOf
の第1引数には、検索したい文字を指定します。- そして、文字列の先頭(0番目)から順番に1文字ずつ検索をしていき、最初に一致した位置(index番号)を数値で返してくれます。※(大文字・小文字は区別します)
const str= 任意の文字列 str.indexOf( 検索したい文字, 検索開始位置 ); // ※検索開始位置は省略可能indexOfの文字列(String)検索方法
例で説明します
//①数列'123547'から7を取り出したい場合 const numbers = '123547'; const result = numbers.indexOf(7); console.log(result); //②文字列 'dog,shark,hamster,fox'からhamsterを取り出したい場合 const animals ='dog,shark,hamster,fox'; const result = animals.indexOf('hamster'); console.log(result);出力結果
//①の場合 5 //②の場合 10
- ①の場合はインデックス番号5番なので正しいことがわかりました!
②の場合は文字列の
hamster
のh
の位置がインデックス番号10番目なことを教えてくれています。文字列にすると
,
で一つ一つ区切られていると認識されている訳てはないので気を付けましょう。
(私はここで躓きました)検索開始位置を指定する
(例)
//9番から指定 const animals = 'dog,fox,shark,hamster,fox'; const result = animals.indexOf('fox', 9); console.log(result);出力結果
22lastIndexOfで後ろから検索する
lastIndexOf
での「後方検索」について学習しましょう!「後方検索」とは、任意の文字列を後ろから検索していくことで「
indexOf()
」と反対の検索方法になります。使い方説明します、
var str = 任意の文字列 str.lastIndexOf( 検索したい文字, 検索開始位置 );
- indexOf()構文は同じです。
- 違うところは、「
indexOf()
」が文字列の先頭から1文字ずつ順番に検索していたのに対して、この「lastIndexOf()
」は文字列の後ろから先頭に向かって検索していくという点です! (例)// 任意の文字列 const animals = 'dog,fox,shark,hamster,fox'; // 「apple」を末尾から検索する const result = animals.lastIndexOf( 'fox' ); console.log( result );出力結果
22
- 文字列内の後ろから一致するまで検索をしていきます。
先ほどの「
indexOf()
」の場合だと、文字列の先頭から検索するので5番目から始まる「fox」が一致しましたが、「lastIndexOf()
」は後ろから検索するので22番目が一致します。後ろから検索しても返ってくる「値」は「
indexOf()
」と同じく先頭から順番に数えた文字位置になるので注意してください。「indexOf」の配列(Array)検索方法
- これまでは文字列内のキーワードを検索してきましたが、実は配列に格納されているキーワードの検索も「
indexOf()
」を使うことが可能です。const animals =['dog','shark','hamster','fox']; const result = animals.indexOf('hamster'); console.log(result);出力結果
2文字列の場合と同様に、最初に一致したインデックスのみ返す点と、見つからなかった場合は「
-1
」が返り値になるという点を覚えておきましょう!まとめ
indexOfの文字列の初めから一文字ずつ数えて出力するとこで躓いたので
記事を書きました
参考にしていただけると嬉しいです!参考リンク
- 投稿日:2020-06-28T08:29:07+09:00
TypeScript の何が役に立つのか?
TypeScript を使ってみて何が役に立つか考えてみたことを初心者向けに解説する。
TypeScript ではプログラマーが型を記述する。JavaScript では必要なかった型を記述することによって手間がかかるが、ビルドやエディターが型情報を使ったらプログラマーにとっていろいろと役に立つ。
型情報を使うと何が役に立つのか?それは TypeScript を使ってみないと想像がつかないものだった。
TypeScriptのインストール方法
Node.jsをインストールするとnodeやnpmが使えるようになる。
グローバルで typescript をインストールし
%APPDATA%\npm
にパスを通す。> npm -g install typescriptまたは
npm init
で package.json を作成してから typescript をその場所にインストールする。.\node_modules\.bin
にパスを通す。> npm init -y > npm install typescripttsc が使えるようになる。
ビルドが型情報を使うと役に立つ
TypeScript のプログラムを JavaScript に翻訳することをビルドと呼ぶ。翻訳と言っても主に付けた型情報を削除するだけだが、TypeScript はビルドするとき型が合わないものをエラーで弾く。
たとえば TypeScript は次のプログラム test.ts をエラーで弾く。
test.tsfunction sum(a: number, b: number): number { return a + b; } sum(1); sum(1, 2, 3);数値を 2 つ受け取って数値を返すという関数の型を記述した。このプログラムは数値を 2 つ渡すべき所に数値を 1 つまたは 3 つ渡しているので、正しく動作しない。関数の型が合わないので型エラーになる。
次のようにしてビルドする。
> tsc --init message TS6071: Successfully created a tsconfig.json file. > tsc test.ts:5:1 - error TS2554: Expected 2 arguments, but got 1. 5 sum(1); test.ts:7:11 - error TS2554: Expected 2 arguments, but got 3. 7 sum(1, 2, 3);TypeScript のビルド方法は特殊だ。tsconfig.json でオプションを設定するが、tsc コマンド上でファイルを指定すると tsconfig.json を読み込まない。そこで tsconfig.json を作って tsc でビルドする。
JavaScript のプログラマーは目で見てわかるから型検査はいらないと思うかもしれない。わざわざ型定義を記述してビルドするのが手間だ。
短いプログラムのときは TypeScript の必要性を感じない。しかし、長いプログラムのとき型検査が役に立つことを感じる。
プログラムの一部を変更したとする。TypeScript がなければ、長いプログラムのどこに影響するかプログラマーが目で見て探さなければならない。
TypeScript は影響する箇所を型エラーとして報告する。プログラマーが探さなくていい。
エディターが型情報を使うと役に立つ
エディターはマイクロソフト製の Visual Studio Code を使う。マイクロソフト製の TypeScript と相性がいい。
この例は文字型の name プロパティと数値型の age プロパティを持つオブジェクトとして Person 型を記述した。
型情報がわかるので、プロパティの補完が正確に効く。
JavaScript のプログラムでもプロパティを補完するが、可能性のある候補を多めに挙げて正確ではない。
npmパッケージを使うときもパッケージの型定義が役に立つ。
パッケージのhttp-link-headerとは別に型定義ファイル
@types/http-link-header
をインストールするとhttp-link-headerをimportできるようになる。header.Link型にget2がないので型エラーが表示される。JavaScriptではエラーが表示されない。
header.Link.Reference型にrelとuriがあるので補完が正確に効く。JavaScriptも補完が効くが正確ではない。
http-link-headerの型定義はF12を押すと型定義ファイルの場所へジャンプして調べられる。
React と相性がいいらしい
次の型エラーのない React のプログラム test.tsx を考える。Visual Studio Code で編集する。
const MyButton の text_bb を書き間違えてみると間違えた所に型エラーが表示された。
ReactDOM.render の text_aa を書き間違えてみると間違えた所に型エラーが表示された。
test.tsx を test.js に変えて型情報を削除して同じ箇所を書き間違えてみたらエラーが表示されなかった。React は TypeScript がないとエラーを発見できない。
React を使うのなら TypeScript を使うべきだ。React の何が役に立つのかという疑問はあるが。
行の関係性に注目してみた
プログラムの不具合は行と行の関係性が原因で発生する。
行と行の関係性は行数の二乗の数だけある。それで長いプログラムほど不具合発生の関係性が増大する。
プログラムが長くなるとすべての関係性を調べる負担をプログラマーが負いきれなくなる。
TypeScript は型を調べることですべての関係性が合っているかを自動的に調べられる。それでプログラマーの負担を軽減できる。
まとめ
型情報があることでプログラミング検査の自動化が進む。
短いプログラムのときは型の記述とビルドに手間がかかるだけだが、型の記述は長いプログラムで真価を発揮する。
TypeScript の型検査はエラーの発生箇所を自動的に調べるので、プログラミングが楽になる。
- 投稿日:2020-06-28T03:42:35+09:00
Kindle蔵書一覧を取得する方法
Kindleの蔵書が1万冊を超えてきて、そろそろ蔵書管理したくなり、
蔵書一覧を取得する方法を調べたので、まとめておく。概要
蔵書一覧の取得方法としては大きく2つあり、
コンテンツと端末の管理ページからスクレイピングする方法と、
Kindle Cloud ReaderがWeb SQL Databaseを使っているので、クライアント側のDBからそのまま取得する方法がある。
後者のほうが簡単なため、ここでは後者の方法について記載する。
(前者の方法が知りたい方は、https://qiita.com/yshr1982/items/072e8b44d456f6d9358bなどを参考にしてください。)取得方法
- Kindle Cloud Readerにアクセス。本が大量にあると読み込みが終わるまで時間がかかるためしばらく待つ。
- F12を押して、デベロッパーツールを立ち上げる。
- Consoleタブに移動して、以下のコードを貼り付けて実行。Kindle.csvとして出力される。
getKindleCsv = function () { const FILE_NAME = 'Kindle.csv'; var db = openDatabase('K4W', '3', 'thedatabase', 1024 * 1024); // Set CSV header var csvData = "ASIN,Title,Authors,PurchaseDate\n"; db.transaction(function (tx) { tx.executeSql('SELECT * FROM bookdata order by title;', [], function (tx, results) { var len = results.rows.length; for (i = 0; i < len; i++) { var result = results.rows.item(i); var asin = result.asin; var title = result.title; var authors = JSON.parse(result.authors); var purchaseDate = new Date(result.purchaseDate).toLocaleDateString(); // Remove double quotes and CRLF from title title = title.replace(/"/g, ''); title = title.replace(/\n|\r\n|\r/g, ''); var authorList = authors[0]; // Write out CSV Style csvData += '"' + asin + '","' + title + '","' + authorList + '","' + purchaseDate + '"\n' } // Export CSV File var bom = new Uint8Array([0xEF, 0xBB, 0xBF]); var blob = new Blob([bom, csvData], { type: 'text/csv' }); var url = (window.URL || window.webkitURL).createObjectURL(blob); var link = document.createElement('a'); link.download = FILE_NAME; link.href = url; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); }); }; getKindleCsv();頻繁に利用するようであれば、以下のコードをブックマークレットとして登録してクリックひとつで利用することもできる。
javascript: (function () {const FILE_NAME = 'Kindle.csv';var db = openDatabase('K4W', '3', 'thedatabase', 1024 * 1024);var csvData = "ASIN,Title,Authors,PurchaseDate\n";db.transaction(function (tx) {tx.executeSql('SELECT * FROM bookdata order by title;', [], function (tx, results) {var len = results.rows.length;for (i = 0; i < len; i++) {var result = results.rows.item(i);var asin = result.asin;var title = result.title;var authors = JSON.parse(result.authors);var purchaseDate = new Date(result.purchaseDate).toLocaleDateString();title = title.replace(/"/g, '');title = title.replace(/\n|\r\n|\r/g, '');var authorList = authors[0];csvData += '"' + asin + '","' + title + '","' + authorList + '","' + purchaseDate + '"\n'}var bom = new Uint8Array([0xEF, 0xBB, 0xBF]);var blob = new Blob([bom, csvData], {type: 'text/csv'});var url = (window.URL || window.webkitURL).createObjectURL(blob);var link = document.createElement('a');link.download = FILE_NAME;link.href = url;document.body.appendChild(link);link.click();document.body.removeChild(link);});});})();参考
- 投稿日:2020-06-28T03:38:09+09:00
【Vue】オブジェクトを変更したい時どうするか?
Vueのオブジェクトの変更方法をまとめ
Vueの
data()
にあるオブジェクトについての話。取得は通常のJSのオブジェクトの操作と同様にキーを使えば問題なかったが、
追加と削除で嵌ったので備忘録として残しておく。以下、参考にしたページ
次のようなVueインスタンスの
data()
にcharObj
という空のオブジェクトがセットしてあるとする。export default { data () { return { charaObj: {} } } }プロパティを追加する
プロパティを追加したい時、通常のJSのオブジェクト同様
obj.anyKey = anyVal
とやってしまいたくなるがこれだとリアクティブにデータが反映されない(=データ更新してもテンプレート(画面)上に反映されない)ので、ダメということらしい。// リアクティブにデータが反映されないのでこの書き方はダメ this.charaObj.hero = '範馬刃牙'プロパティを追加したい時は
$set
を使う。
慣れない書き方だが、これでリアクティブにプロパティを追加できる。
第1引数にオブジェクトを、第2引数にキー、第3引数に値を渡す。// リアクティブになるやつ this.$set(this.charaObj, 'hero', '範馬刃牙')プロパティをまとめて追加したい
スプレッド演算子
...
を使って追加したいプロパティを含むオブジェクト内で展開すればいい。this.charObj = {...this.charObj, parent: '範馬勇次郎', friend: '烈海王'}または
Object.assign()
を使って新しいオブジェクトを作って代入する方法でもできるらしい。this.charaObj = Object.assign({}, this.charaObj, {parent: '範馬勇次郎', friend: '烈海王'})プロパティを削除する
追加の時と同じで、いつも通りに削除しようとするとリアクティブにならない。
// リアクティブにデータが反映されないのでこの書き方はダメ delete this.charaObj.heroプロパティを削除したい時は
$delete
を使う。第1引数に対象のオブジェクトを、第2引数に削除したいプロパティのキーを渡す。// リアクティブになるやつ this.$delete(this.charaObj, 'hero')全削除したい
最初、空のオブジェクトを再代入で初期化できないかと考えたが、うまくいかなかった。
// 再代入はうまくいかない this.obj = {}この場合は、キーの配列を
Object.keys()
で取得してループでdelete
を使って削除したところうまくいった。// プロパティを全削除 Object.keys(this.charObj).forEach(key => { this.$delete(this.charObj, key) })リアクティブにVueインスタンスのデータを削除するには先に書いた
$delete
を使う。プロパティを全て入れ替えたい
オブジェクトを一度空にしてからまとめて追加すればよい
オブジェクト自体を削除したい
あまり使うケースは無さそうだが、プロパティではなくオブジェクト自体(Vueインスタンスのトップレベルのプロパティ)を削除したい場合はどうするか?
$delete
を使いたくなるところだが、トップレベルのを削除する時は
$delete
を使おうとすると警告が出るので使っちゃダメっぽい。// トップレベルのプロパティを削除しようとエラーが出る this.$delete(this, 'charObj')この場合は
null
を入れればよいみたい。// charObjを削除する this.charObj = null
- 投稿日:2020-06-28T00:16:04+09:00
学習日記8日目(2020/6/27)
学習内容
- JavaScript基礎学習:Progateの学習コース全終了
JSの学習を終了してPHPやLaravelの学習に移っていきたいと思います。
その他
- タイピング練習
- paizaのスキルチェックに挑戦
参加しているサロンの方々がやっているのを目にし、気になったのでpaizaのスキルチェックやってみました。しかし、、標準入力ってなんやねん!という感じでそもそも入力値を変数等に代入できなくて積みました?
そんなこんなでfgets関数を使ったことがなかったので調べ、どうもfgets関数を繰り返し実行すれ入力を一行目から順に取り出せるらしいことがわかったので明日以降Cランクの問題にチャレンジしようと思います。今日はDランクの問題1問解いて終了です。
明日の予定
- UdemyでPHP&Laravelの学習
- 投稿日:2020-06-28T00:11:33+09:00
ぷよぷよプログラミング「上級コース」写経でどのくらいミスするのか
"無料でぷよぷよを通してプログラミング学習できる「ぷよぷよプログラミング」が登場したので体験してみた"
https://gigazine.net/news/20200626-puyopuyo-programing/
これの上級コース 全コード写経をしました。
所要時間は2時間41分43秒です。javascriptは殆ど書いたことがありません。
1015行をとくに見直しをしない状態で、どのような写経ミスがあるか調べました。比較はWindowsのTortoiseGitに付属しているTortoiseGitMergeで行いました。空白とセミコロンは無視しています。
もとのコードもセミコロンついてたりついてなかったりします。
結果は10件のミスがありました。
config.js (全33行)
コンフィグの値が違う
数値が違います。
// 私 Config.playerDownSpeed = 15; // プレイ中の下キー押下時の落下スピード // オリジナル Config.playerDownSpeed = 10; // プレイ中の下キー押下時の落下スピードgame.js (全121行)
スペルミス
スペルミスです。cal 'u' culateScore になっている。
// 私 Score.caluculateScore(combinationCount, eraseInfo.piece, eraseInfo.color); // オリジナル Score.calculateScore(combinationCount, eraseInfo.piece, eraseInfo.color);コメント写し忘れ
コメントを写し忘れました。ファイルの最後のほうなので気のゆるみがあります。
// 私 requestAnimationFrame(loop); // オリジナル requestAnimationFrame(loop); // 1/60秒後にもう一度呼び出すindex.html (全46行)
!がない
"してみよう!"の"!"がありません。単純な写経ミス。
<!-- 私 --> <title>ぷよぷよプログラミングを体験してみよう</title> <!-- オリジナル --> <title>ぷよぷよプログラミングを体験してみよう!</title>player.js (全428行)
大文字小文字違い
p と P が違います。単純なミスです。
// 私 if(isDownpressed) { // オリジナル if(isDownPressed) {xとyの違い
if(x と if(y が違います。これは動作が変わるので危険です。
// 私 if(x+1 >= Config.stageRows || Stage.board[y+1][x] || (y+dy+1 >= 0 && (y+dy+1 >= Config.stageRows || Stage.board[y+dy+1][x+dx]))){ // オリジナル if(y + 1 >= Config.stageRows || Stage.board[y + 1][x] || (y + dy + 1 >= 0 && (y + dy + 1 >= Config.stageRows || Stage.board[y + dy + 1][x + dx]))) {変数名まちがい
edがついていません。このコードの上のほうで定義している変数はisBlockedになっていました。コンパイルする言語ならコンパイルエラーになりそうです。
// 私 if(!isBlock){ // オリジナル if(!isBlocked) {謎の演算子
なぞの演算子が登場します。>= ではなく >~ になっています。
// 私 if(my < 0 || mx +cx < 0 || mx + cx >~ Config.stageCols || Stage.board[my][mx + cx]){ // オリジナル if(my < 0 || mx + cx < 0 || mx + cx >= Config.stageCols || Stage.board[my][mx + cx]) {スペルミス
スペルミスです。rotati 'o' ngになっています。
// 私 return 'rotationg'; // オリジナル return 'rotating';よけいなもの
ratio 'n' になっています。
// 私 const ration = Math.min(1, (frame - this.actionStartFrame) / Config.playerRotateFrame); // オリジナル const ratio = Math.min(1, (frame - this.actionStartFrame) / Config.playerRotateFrame);puyoimage.js (全41行)
相違なし
score.js (全59行)
相違なし
stage.js (全294行)
相違なし
感想
クラウドIDEが構文エラーには赤線引いたり、インテリセンスも効くので思ったより間違いは少なかったです。
もしWindowsのメモ帳でやっていたらもっと間違えていると思います。