20200628のJavaScriptに関する記事は26件です。

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 はめんどくさい!!)

早押しボタンの画面

スクリーンショット 2020-06-28 20.33.15.png

進行役の画面

スクリーンショット 2020-06-28 20.32.15.png

仕様

  • クイズ参加者と進行役で別の画面を操作
  • 参加者はそれぞれのデバイスの参加者用の画面で早押しボタンを押して解答権を獲得できる
  • 進行役は専用の管理画面で事前に決められた問題の切り替え、参加者の解答に応じてピンポン・ブーを鳴らすことができる
  • クイズの正誤判定は人力(笑)

実装の指針

  • 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.js
import { 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.js
export 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に逃げてしまったのですが他に良い方法があれば是非ご教授ください?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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の基本用語をまとめてみました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【質問】URLのパラメータを判別して期間限定のページを出し分ける方法

表題の件について質問になります。

期間限定のページを運用で回していきたいと思っていて、
URL末尾に?enddate=200731 のようなパラメータをセットして、このパラメータを判別してページの表示期間を
制御させる方法(記述)をご教示頂けますと幸いです。

この例で言うと20年8月1日になった時点でページが見れなくなる想定です。

それか、パラメータに有効期間をセットして、その有効期間を判別してページを出し分ける方法のどちらかの方法が知りたいです。。。

何卒よろしくお願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】フリマアプリ商品編集機能について(プレビュー編集・DB更新)

はじめに

草野と申します。
今回の投稿は、プログラミングスクールでチーム開発にて行ったフリーマーケット系ECサイトのクローンアプリ商品編集機能についてです。自分用のメモのため、文章は拙いですが、少しでも初学者の助けになればと考えています。
内容は、表題にもあるとおり、プレビュー編集とDB更新についてです。未熟な点も多いと思います。不備等ありましたらご指摘ください。随時改善して行こうと思います。ちなみに私は、スクール自体は卒業しており、学習した内容の振り返りとして投稿させて頂いております。

完成品

商品編集画面(プレビュー画像部)

Image from Gyazo

商品編集画面(カテゴリー部)

Image from Gyazo

商品編集画面(販売手数料・利益部)

Image from Gyazo

更新成功時の遷移画面

Image from Gyazo

実装手順

1.ルーティング編集

  • update_doneのルート設定(更新成功時の遷移画面)

2.コントローラー編集

  • editメソッド設定
  • updateメソッド設定
    • エラーハンドリング
    • 画像削除
  • update_daneメソッド設定

3.ビュー編集・作成

  • プレビュー画像呼び出し
  • カテゴリー呼び出しの調整
  • 更新成功時の遷移画面

4.JS編集

  • プレビュー画像及びinputタグの生成、削除
  • 販売手数料・利益の表示

1.ルーティング編集

update_doneルートを生成します。
これは、更新成功時の遷移画面を表示するためのルーティングです。

config/routes.rb 
  resources :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
  end

2.コントローラー編集

今回編集したコントローラーの記述は下記の通りです。

app/controller/items_controller.rb 
class 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.rb 
class 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.rb 
class 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.rb 
  def update_done
    @item_update = Item.order("updated_at DESC").first
  end

3.ビュー編集

全ての記述を載せると長くなってしまうのでここでは割愛して記述させて頂きます。

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("");
    }
  });
});

これで編集機能完成です。
ここまで読んでくださり、ありがとうございます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

もうわけからないぃぃ

webpackとかふつうに使っていると
一つのファイルにしてくれるけど
ローカルでの挙動とサーバーでの挙動が激的に変化するときがあって
そのときは、もう手におえない状態になるのだ。。

なにせ、まとめたファイル(ここでは/dist/bundle.jsとでもしとこう)を
解析しようにも、そもそもごったまぜのスープになっており
どこのパッケージのどこのバージョンが。。とか基本は、省かれてる。

なんとなくならわかるけど、じゃあどう対処するんだよーwってなる。。

そもそも、webpackとかって、いままで、ふつうにルートディレクトリーに
htmlファイルを置いとけば、ぜったい404とかでないのだが、
webpackの場合は、そうゆう形式でもないし。
基本react系ので、routerとかで、振り分けているのだし。。

で、それっぽいキーワードで、ググっても、的を得ない回答しかでてこないしw

firebase系のjsもimportしてるのだが、最近はそれを疑っているありさま。w

もうここまでくるとカルト宗教化してるぜ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

chat app.にてユーザー検索後、グループ追加を実装

はじめに・目的

インクリメンタルサーチ実装

1.API側準備

  • routing
routes.rb
resources :users, only: [:index]
  • users_controller.rbにindex定義
users_controllers.rb
class UsersController < ApplicationController
  def index
    respond_to do |format|
      format.html
      format.json
    end
  end
  • app/views/usersディレクトリにindex.json.jbuilderファイルを作成
index.json.jbuilder
json.array! @users do |user|
  json.id user.id
  json.name user.name
end

2.テキストフィールド作成

_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-user

3.テキストフィールドに入力するたび、イベント発火

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.rb
def 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
end

params[: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-iddata-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();
  });
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】Scoped CSSのscoped具合を検証してみた

Vue.jsのScoped CSSは完全にはscopedじゃない、という話を聞いたので検証してみました。
結果、コンポーネント最上位の要素と、slotについては気をつける必要があることが分かりました。

参考記事

親コンポーネントのstyleが子コンポーネントに影響を与える仕組みは、以下が詳しいです。

検証環境

  • OS: macOS Catalina 10.15.5
  • ブラウザ: Google Chrome 83.0.4103.116(Official Build)(64bit)
  • Vue.js: 2.6.11


検証1: 素朴にstyleを当てた場合

親、子、孫コンポーネントにそれぞれ違った色を割り当てました。
すると、各コンポーネントの最上位の要素には親コンポーネントのスタイルが適用されてしまいました。
1.png

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だけ変更しました。
1.png

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 }を当てたら、すべて黒になります。
1.png

App.vue
<style lang="scss">
* {
  color: black!important; // プロダクトコードで書いたら殴られるやつ
}
</style>



もちろん、!importantを付けなければ、コンポーネント内のcssが優先的に適用されます。
(この辺はScoped CSSというよりはCSSそのものの話)
1.png

App.vue
<style lang="scss">
* {
  color: black;
}
</style>


検証4: slotを使った場合

slotを使って、親から子へタグを渡した場合、渡したタグには親コンポーネントのstyleが適用されました。
1.png

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が適用されました。
1.png

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便利。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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/File

Blob:
https://developer.mozilla.org/ja/docs/Web/API/Blob

http://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.vue

var 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.vue

change_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");             
    }            
},


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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の場合はundefined

filter

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等を参照して手を動かしながら理解していきましょう。

参考

MDN ループと反復処理

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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の場合はundefined

filter

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等を参照して手を動かしながら理解していきましょう。

参考

MDN ループと反復処理

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【個人メモ】Express環境ハンズオン

完全に個人用メモです。

  1. Node.jsとnpmがinstall済みであることを確認する
    node -v, npm -vコマンド実行

  2. expressのインストール
    作業ディレクトリでnpm initを実行してpackage.json作成
    npm install express express-generator --save

  3. expressアプリケーションの雛形作成
    ./node_modules/.bin/express --view=pug sample

  4. アプリ立ち上げ
    cd sample
    npm install
    npm start
    ブラウザからhttp://localhost:3000/につなぐと「Welcome to Express」が表示される。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

three.js でlightmapを試す

概要

three.jsに限らず、3DCGのアプリケーションでは少しでも計算量を減らしてパフォーマンスを上げたいという要求があります。今回はそれに対する一つのアプローチであるlightmap(ライトマップ)を試したいと思います。

lightmapとは

lightmapを簡単に説明すると、物体の陰影をあらかじめ計算した結果の画像(テクスチャ)のことです。これを使用することで、光源の計算をする必要がなくなり、パフォーマンスが向上します。ただし、あらかじめ計算しておくという性質上、物体や光源が大きく動く場合には使用できません。

デモ

実際にthree.jsでlightmapを使用したデモを作成しました。
猿に陰影がありますが、シーンに光源は一切置いていません。
バージョンはr93
https://arihide.github.io/demos/lightmap/
スクリーンショット 2020-06-28 15.52.42.png

ソースコードはこちら
https://github.com/Arihide/demos/tree/master/lightmap

解説

ここではBlenderを用いてLightmapを作成し、three.jsで表示する方法を説明します。

lightmapの作成

まず始めに、Blenderでlightmap用のテクスチャとUVを作成する必要があります。例としてcyclesレンダラーで以下のような猿と光源の板を用意します。
a1b7dd9274ac2565c89f76e37751333a-1024x640.png

次にdiffuseとは異なる、lightmap用のUVMapを用意します。
81987d68ca302d7fecb4c50113de9c21.png

lightmapのUVを選択した状態で、猿を選択してEditモードに入り、「u」キーを押して「Lightmap Pack」を選択してUV展開します。
3f23249d984c621e45124a7230f209ea.png

次に、Lightmap出力先のテクスチャを作成するために「UV ImageEditor」上で適当に画像を作成し…
898265887ec7c983fec763896340e9f2.png

その後、Node editor上でimage textureノードを作成し、右のBakeを実行しLightmapを作成します。
fbfc61c5e21a4c6a7ad5bf7f91104a33-1024x640.png

その結果、このようなテクスチャを得ることができればLightmap作成完了です!
4ef22436f4c0a07727851f25589c2614.png

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を与えるだけです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

three.js マウスクリックで一部の面の色を変える

概要

three.jsでクリックした面の色が変わるような物を作ります。
バージョンはr93

デモ:https://arihide.github.io/demos/face_pick/
スクリーンショット 2020-06-28 14.42.12.png

ソースコードはこちら
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;
も指定する必要があります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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つを守ってロジック部分を作らないとメンテしずらいコードになってしまうといってるわけですね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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(); //走る

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascriptのアロー関数とthisの挙動の違い

アロー関数は、より短く、簡潔に書ける関数の構文です。
javascriptは得意ではありませんが、いろいろと動的に動かして遊びたいので学習メモとしてまとめます。

関数をアロー関数に書き換える

例順を追って、関数のシンタックスを変換していきます。

例文は、引数i2をかける関数です。

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の出力結果:
Screen Shot 0032-06-28 at 15.07.12.png

上記の例を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を検索します。)

参考

アロー関数/MDN
Udemy/Javascript中級者への道

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

駆け出しエンジニア日記 #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に苦しめられたり
  • 採用活動が活発化してきた
    • メール作成
    • 日程調整
    • (そろそろ経験者も視野に入れて採用活動するフェーズに移行するべきか?)

今週の参考記事

https://codelearn.jp/articles/damn-good-advice

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsで環境変数を利用する

とりあえずやり方教えて!

.envを用意します。

.env
TEST='HOGEEEEEE'

next.config.jsを編集します。ない場合はプロジェクトのルートディレクトリでvim next.config.jsして作ります。

next.config.js
module.exports = {
  env: {
    TEST: process.env.TEST
  }
}

indexかなんかで表示してみます。

index.tsx
export 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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

令和元年? 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 引数の、この場合 eralong に設定しましょう。

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/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

tusdとtus-js-client, uppyを使ったファイルアップロードをapache2.4のCentOS7で構築する(cookie編)

v2

  • CORSは不要と気がつきましたので、httpd.confの設定は不要だと思います(不具合が出たらまた見直すかもしれませんが)
  • 理由は前回記事をご覧ください。
  • 変更点はv2としています。

目的

環境

  • 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.js
var 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でファイルフックできるようにする

  • 処理の段階でtusdhookできるように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]

どなたかのお役に立てましたらとても嬉しいです。今回の記事は”動いたよ”という段階なので間違った設定や使い方があるかもしれません。その時はご指摘ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

〔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番なので正しいことがわかりました!
  • ②の場合は文字列のhamsterhの位置がインデックス番号10番目なことを教えてくれています。

  • 文字列にすると,で一つ一つ区切られていると認識されている訳てはないので気を付けましょう。
    (私はここで躓きました)

検索開始位置を指定する

(例)

//9番から指定
const animals = 'dog,fox,shark,hamster,fox';
const result = animals.indexOf('fox', 9);

console.log(result);

出力結果

22

lastIndexOfで後ろから検索する

  • 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の文字列の初めから一文字ずつ数えて出力するとこで躓いたので
記事を書きました
参考にしていただけると嬉しいです!

参考リンク

JavaScript入門】文字列の検索まとめ(indexOf/search/match/test)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScript の何が役に立つのか?

TypeScript を使ってみて何が役に立つか考えてみたことを初心者向けに解説する。

TypeScript ではプログラマーが型を記述する。JavaScript では必要なかった型を記述することによって手間がかかるが、ビルドやエディターが型情報を使ったらプログラマーにとっていろいろと役に立つ

型情報を使うと何が役に立つのか?それは TypeScript を使ってみないと想像がつかないものだった。

TypeScriptのインストール方法

Node.jsをインストールするとnodenpmが使えるようになる。

グローバルで typescript をインストールし%APPDATA%\npmにパスを通す。

> npm -g install typescript

またはnpm initで package.json を作成してから typescript をその場所にインストールする。.\node_modules\.binにパスを通す。

> npm init -y
> npm install typescript

tsc が使えるようになる。

ビルドが型情報を使うと役に立つ

TypeScript のプログラムを JavaScript に翻訳することをビルドと呼ぶ。翻訳と言っても主に付けた型情報を削除するだけだが、TypeScript はビルドするとき型が合わないものをエラーで弾く。

たとえば TypeScript は次のプログラム test.ts をエラーで弾く。

test.ts
function 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 と相性がいい。

Visual Studio Code 1

この例は文字型の name プロパティと数値型の age プロパティを持つオブジェクトとして Person 型を記述した。

型情報がわかるので、プロパティの補完が正確に効く

JavaScript のプログラムでもプロパティを補完するが、可能性のある候補を多めに挙げて正確ではない。


npmパッケージを使うときもパッケージの型定義が役に立つ。

Visual Studio Code 2

パッケージの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 で編集する。

React 1

const MyButton の text_bb を書き間違えてみると間違えた所に型エラーが表示された。

React 2

ReactDOM.render の text_aa を書き間違えてみると間違えた所に型エラーが表示された。

React 3

test.tsx を test.js に変えて型情報を削除して同じ箇所を書き間違えてみたらエラーが表示されなかった。React は TypeScript がないとエラーを発見できない

React を使うのなら TypeScript を使うべきだ。React の何が役に立つのかという疑問はあるが。

行の関係性に注目してみた

プログラムの不具合は行と行の関係性が原因で発生する。

行と行の関係性は行数の二乗の数だけある。それで長いプログラムほど不具合発生の関係性が増大する。

プログラムが長くなるとすべての関係性を調べる負担をプログラマーが負いきれなくなる。

TypeScript は型を調べることですべての関係性が合っているかを自動的に調べられる。それでプログラマーの負担を軽減できる。

まとめ

型情報があることでプログラミング検査の自動化が進む。

短いプログラムのときは型の記述とビルドに手間がかかるだけだが、型の記述は長いプログラムで真価を発揮する。

TypeScript の型検査はエラーの発生箇所を自動的に調べるので、プログラミングが楽になる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kindle蔵書一覧を取得する方法

Kindleの蔵書が1万冊を超えてきて、そろそろ蔵書管理したくなり、
蔵書一覧を取得する方法を調べたので、まとめておく。

概要

蔵書一覧の取得方法としては大きく2つあり、
コンテンツと端末の管理ページからスクレイピングする方法と、
Kindle Cloud ReaderがWeb SQL Databaseを使っているので、クライアント側のDBからそのまま取得する方法がある。
後者のほうが簡単なため、ここでは後者の方法について記載する。
(前者の方法が知りたい方は、https://qiita.com/yshr1982/items/072e8b44d456f6d9358bなどを参考にしてください。)

取得方法

  1. Kindle Cloud Readerにアクセス。本が大量にあると読み込みが終わるまで時間がかかるためしばらく待つ。
  2. F12を押して、デベロッパーツールを立ち上げる。
  3. 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);});});})();

参考

Gist - Amazon Kindle Export

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

学習日記8日目(2020/6/27)

学習内容

  • JavaScript基礎学習:Progateの学習コース全終了

 JSの学習を終了してPHPやLaravelの学習に移っていきたいと思います。

その他

  • タイピング練習
  • paizaのスキルチェックに挑戦

 参加しているサロンの方々がやっているのを目にし、気になったのでpaizaのスキルチェックやってみました。しかし、、標準入力ってなんやねん!という感じでそもそも入力値を変数等に代入できなくて積みました?

 そんなこんなでfgets関数を使ったことがなかったので調べ、どうもfgets関数を繰り返し実行すれ入力を一行目から順に取り出せるらしいことがわかったので明日以降Cランクの問題にチャレンジしようと思います。今日はDランクの問題1問解いて終了です。

明日の予定

  • UdemyでPHP&Laravelの学習
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ぷよぷよプログラミング「上級コース」写経でどのくらいミスするのか

"無料でぷよぷよを通してプログラミング学習できる「ぷよぷよプログラミング」が登場したので体験してみた"

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のメモ帳でやっていたらもっと間違えていると思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む