20210228のJavaScriptに関する記事は30件です。

【jQuery】好きな画像をボタンにする

好きな画像をボタンにする

Webサイトでボタンを作りたいとき、まず思い浮かべるのは「buttonタグ」や「inputタグ(type="button")」で作る方法だと思います。
ですが他にも、好きな画像をボタンのように扱うこともできるので、今回はその方法をご紹介します。
けっこう簡単です。

題材として、ボタンを押すとブドウの画像が出てくるサイトを作ります。
page.png

用意するもの

  • 押す前のボタン画像
  • 押した後のボタン画像
  • ブドウの画像
  • htmlファイル
  • cssファイル
  • jsファイル

ボタン画像は以下をダウンロードして使っていただいても構いません。

押す前ボタン
button_neutral.png
押した後ボタン
button_active.png

今回は以下のようなディレクトリ構成で作ります。
dir.png

ではさっそく作っていきましょう。
全部で3ステップ!

ステップ1 枠組みを作る

以下のようにhtmlファイルcssファイルを書いてみてください。
※ 枠組みは本題じゃないので適当でも大丈夫です。

index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>好きな画像をボタンにする</title>
    <link rel="stylesheet" href="css/style.css">
    <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script type="text/javascript" src="javascript/script.js"></script>
</head>

<body>
    <div id="main-div">
        <div id="h1-div" class="common lightgreen">
            <h1>ボタンを押すとブドウが出るよ</h1>
        </div>
        <div id="action-div" class="common lightgreen">
            <!-- ボタン画像を表示するdiv -->
            <div id="button-div" class="common whitesmoke">
            </div>
            <!-- ブドウ画像を表示するdiv -->
            <div id="grape-div" class="common whitesmoke">
            </div>
        </div>
    </div>
</body>

</html>
style.css
.common {
    margin: 10px;
    border-radius: 10px;
    text-align: center;
}

.lightgreen {
    background-color: lightgreen;
    width: 50%;
}

.whitesmoke {
    background-color: whitesmoke;
    width: 100%;
    height: 150px;
    display: flex;
    justify-content: center;
    align-items: center;
}

#action-div {
    display: flex;
}

この状態でindex.htmlを実行すると、以下のような枠組みが表示されると思います。
page2.png

ステップ2 画像でボタンを作る

まずindex.htmlid="button-div"のdiv内に、ボタン画像を埋め込むためのspanタグを書いてください。

<div id="button-div" class="common whitesmoke">
    <!-- ボタン画像を埋め込むためのspanタグ -->
    <span id="button"></span>
</div>

次にcssファイルに以下のルールセットを追加してください。
※ cssの各プロパティの意味は、是非調べてみてください。

/* ボタン画像のルール */
#button {
    background-image: url("../image/button_neutral.png");
    background-size: 100%;
    background-repeat: no-repeat;
    cursor: pointer;
    width: 200px;
    height: 100px;
}

/* クリック時は別の画像に置き換える */
#button:active {
    background-image: url("../image/button_active.png");
}

この状態でindex.htmlを実行すると、ボタン画像が表示され、押すとへこむと思います。
page1.png

ステップ3 ブドウを出す処理を作る

jsファイルに、以下のように処理を書いてください。

script.js
$(function () {
    // ブドウを表示するimgタグ
    var grape = '<img class="grape" src="./image/grape.png"></img>';

    // 上記「grape」をappendする関数
    var appendGrape = function () {
        $("#grape-div").append(grape);
    }

    // ボタンクリック時に上記「appendGrape」が呼ばれるように設定
    $("#button").click(appendGrape);
});

これで完成です!
page.png

ひとこと

なんでブドウか?
最近仮面ライダー龍玄にハマってるから。

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

社内でビブリオバトルを開催してみた ! (みんなでワイワイ本紹介をやるなら必見 !)

はじめに

こんにちは!
KDDI アジャイル開発センターの小板橋です.
本記事では, 先週お昼休みに開催したビブリオバトルについてとその内容, 魅力などなどお届けしたいと思います.

本記事は, こんな方におすすめ

  • チームやロールをこえて, 社内で「本」という共通の話題を作りたい方
  • 様々な本を知る機会が欲しい方
  • 新しいオンラインビデオ通話を試してみたい方
  • みんなでワイワイするイベントをやってみたい方

ビブリオバトルとは

さて, みなさんはビブリオバトルを知っていますでしょうか !
ビブリオバトルとは, 自分の好きな本をプレゼンし「一番読みたくなった本 = チャンプ本」を競う白熱のバトルです.

開催にあたって

さて, お次は開催方式とルールについて説明させていただきます.
公式のルールについては下記の公式ページがあるので,そちらをご覧ください !
公式ルール - 知的書評合戦ビブリオバトル

ただ, 今回は公式ルールにいくつかルール追加や, オリジナルのルールを設定し開催させていただきました !

開催方式

ビブリオバトルは, 下記のような流れで行います.

流れ.png

① 事前に参加者が読んで面白いと思った本を持って集まって頂きます.
② 順番に一人5分以内で本を紹介します.
③ それぞれの発表の後に参加者全員でその発表に関するディスカッションを2~3分行います.
④ 全ての発表が終わった後に「どの本が一番読みたくなったか?」を基準として, 投票を参加者全員一票で行い,最多票を集めたものを「チャンプ本」とします

ルール

  • 今回はオンラインビデオ通話スペース『 Gather.Town 』を利用し, オンラインでの開催
  • 紹介する本は, 技術書 / 情報系 / ビジネス書などエンジニアにオススメしたいと思うもの
  • プレゼンテーション資料を作成するかどうかは, 自由
  • 紹介する本は, 当日自分の発表順が来るまで内緒

使用したツール

  • オンラインビデオ通話スペース : Gather.Town => オススメです !!
  • 投票用ツール : Google form

開催

こんな感じでワイワイとやらせていただきました !
Gather.Townを使うと, 2ドットの世界の中で移動しながら話すことができます.
他のオンラインビデオサービスとは違い、実際に相手の顔を見て会話するだけでなく, 特定のスペースに集まりアバターをRPGのように移動できたり, スペースに椅子やテーブルなどを置けたりしながら人と話せるという現実っぽい感覚が味わえるのが面白いポイントです !

[ビブリオバトル中の風景]
スクリーンショット 2021-02-26 13.33.11.png

ご紹介していただいた本

今回のチャンプ本

ドゥルルル・・ジャーン!(ドラムロール)

:tada:今回のビブリオバトルでのチャンプ本は, ザ・ゴール コミック版となりました !:tada:
ご参加いただいた方々、本当にありがとうございました !

===================================================================
[投票結果]
スクリーンショット 2021-02-28 22.07.42.png

おわりに

  • 感想になってしまいますが, 普段なかなか話せない人もいる中で, 他のチームの方やロールをこえて「本」という共通の話題でワイワイ話ができたのはすごく楽しかったです!

参考情報

公式ルール - 知的書評合戦ビブリオバトル
classmethod: 本好きによる本好きのためのイベント!社内で「#ビブリオバトル」を開催してみた
classmethod: レトロRPG風デザインのオンラインビデオ通話スペース『 Gather.Town 』で”出社して仕事&気軽に雑談”を楽しく仮想体験!

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

一覧表示での進捗率ゲージの作成

はじめに

オリジナルアプリでクラウドファンディングサイトを作成した際に、
jQueryを使って進捗率ゲージを作成したので紹介します。
前回、詳細画面での作成方法を紹介しましたが
今回は一覧表示画面での作成方法です。

Image from Gyazo

詳細画面での作成はこちら→
https://qiita.com/takiguchiharuna1221/items/df517f2a9e4b74299328#comment-bd0d8efa2c7c3dc7651e
※こちらではlinear-gradientを使った別の方法も投稿していただいています。

初学者なのでより良い方法が他にもあるかもしれませんが、どなたかの参考になれば嬉しいです。

環境

・Rails 6.0.0
・Ruby 2.6.5
・jQuery

作成手順

【view】

index.html.haml
.Contents__projects
  - @projects.each do |project| 
  # 一部省略
    .Projects 
      = link_to display_project_path(project.id) , class: "Box3"do
        .Box1
          = image_tag project.image.url.to_s, class: "Box1__image"
        .Box2
          %p.Box2__title= project.title
          %p.Box2__percent__contents 進捗率
          - @percent = number_to_percentage(@total.to_f/project.target_amount*100,precision: 1)
          = @percent
          .contents-pacent__box2__graph1
            .contents-pacent__box2__graph2
              %input{name: "percent", type: "hidden", value:@percent, class: 'percent' }

①ゲージを挿入したい部分にクラスを設定します。
 →.contents-pacent_box2_graph2の部分

②inptを用いてDBのインスタンス変数の値を取得します。
 →inputのvalueに変数を設定することでjQueryで変数を使えるようにします。
  ※今回は変数の詳細は割愛します。
 参考記事:https://qiita.com/Kohei_Kishimoto0214/items/d919b00d75dec0699cf0

【CSS】

_project.scss
.contents-pacent__box2__graph2{
  background-color: #ea662d;
  height: 15px;
  border-radius: 6px;
  max-width: 100%;
}

③CSSは以上の通りです。
 進捗率が100%以上になっても突き抜けてしまわないように
 max-width: 100%;を設定しています。

【jQuery】

project.js
$(function() {
  $('.percent').each(function(i){
    $(this).attr('id', `percent_${i}`);
  });
  $('.contents-pacent__box2__graph2').each(function(i){
    $(this).attr('id', `graph_${i}`);
  });

  (window.onload = function() {
    var ele = document.getElementsByClassName("Projects");
    for(var i = 0; i < ele.length; i++){
      $("#graph_"+i).css({ 'width' : $( "#percent_"+ i).val() } );
    };
  });
});

④.percentと.contents-pacent_box2_graph2のそれぞれにidを振ります。
⑤リロードのタイミングでProjectsの数を数え、その回数だけ
”.contents-pacent
_box2_graph2のwidthの値を@percentからとる”
を繰り返すように①で付与したidを使ってfor文で設定します。

最後に

idの使い方やfor文の設定が自分では使ったことがなかったので苦労しました。
日々勉強します。

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

Svelteについて

Svelteとは

SvelteとはJSの最新フレームワークで、近年JSのfrontendフレームワークの中でも興味や満足度がNo.1のフレームワークです(詳しくはこちらをご覧ください)。Image from Gyazo

Svelteの何がすごいのか?

速い・簡単・軽い

この3つになります。このように言われている理由は公式サイトで述べられている以下の特徴によります。

1. コード記述量が少ない

この意味は以下の2通りの意味が有ります。
コード記述量が少ない
コンパイル結果が小さい
私は現在公式サイトのtutorialなどを触っているのですが、VueやReactでは必要だった記述が不要であったり、省略記法などもあるのでかなりコード記述量が少なく、開発者にとってはありがたいです。

実際に公式サイトでは、Reactで442文字、Vueで263文字、Svelte145文字で済んだという記事もあったくらいです。

このらへんは実際にtutorialなどで触っていただいて実感していただくのがベストだと思います。

また、最終的なファイルの大きさが小さいのでユーザーにも優しいです(SvelteはCSR。Sapperというフレームワークを使うとSSRにできる)。

このようにコード記述量が少ないため開発者に優しいですが、クライアントサイドに送られるファイルも小さいためユーザーにも優しいのがSvelteです。

2. 仮想DOMを使わない

ここがSvelteの一番の特徴だと思うのですが、従来のReactやVueと行ったフレームワークの仮想DOMレンダリングフローの無駄な部分をなくしたのがSvelteです。

まず仮想DOMが何なのかと言うと、こんな感じです。image.png画像にも書いてありますが、これってまだ差分検知っていう無駄なフローがあるんですよね。しかも差分が発生する際って、変更点はほんの一部なことが多いんですよね。
なのに毎回上から順にDOMツリー全体の差分を測っていくのは非効率とも言えます。

そこでSvelteは仮想DOMを使うのを辞めたというわけです。

とある海外のライターによると、Svelteはコンパイル時にDOMの変更の可能性がある状態(state)をトラックして、その状態の一部が変更されるたびに、createElementやsetAttributeなどのDOM APIを使用してDOMを更新してくれるそうです。

このように仮想DOMを使わずに差分のみのレンダリングを可能にしたこと、そして、無駄なフローを削った点においてSvelteはすごいと思います。

3. 真の'reactivity'

詳しい仕組みはまだわかっていませんが、ReactのようなフレームワークではsetState()などで反応性をトリガーする必要がある一方、Svelteではアプリ内の反応処理を全て自動的に行なってくれます。




まだまだわからないことが多いため、訂正部分がある場合は教えて下さいませ。

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

Alpine.jsでライフゲームを作ってみた

突然ですが、PETALスタックというのをご存知でしょうか?
少し前にElxir界隈ですこし話題になっていたのですが、

Phoenix
Elixir
Tailwind
Alpine.js
LiveView

の略みたいです。

この中でフロントエンド担当のAlpine.jsについては、 State of JavaScriptでも
話題にあがっており、気になっていたので少し触ってみました :sparkles:

Alpine.js

ベスト5の新顔はAlpine.jsで、これはLaravel LiveWireの作者によって作られたミニマルなリアクティブフレームワークです。
Vue.jsとAngularの両方から、カスタムHTMLディレクティブや双方向バインディングといったアイデアを拝借しています。
HTMLに古き良きscriptタグを追加するだけで簡単に使うことができ、ビルドプロセスも不要で、HTMLマークアップだけで全てを動かすことができます。
本格的なフレームワークを導入することが困難な既存のWebページをさくっと強化する目的については、最も適切なソリューションであるかもしれません。
引用元: JavaScript ベスト・オブ・ザ・イヤー 2020 by @rana_kualu

とてもミニマルなフレームワークみたいで、基本的にはx-hogehogeで指定できるHTMLディレクティブが14個と6個のMagic Propertyのみ覚えればいいみたいです。

作ったもの

alpine_game.gif

npmいらずでindex.htmlのみで150行ほどで実装することができました。

実装の中身は以下のような感じです。

    <div class="container" x-data="data()" x-init="init()">
        <div class="board">
            <template x-for="(row, index) in board">
                <div class="row">
                    <template x-for="(cell, idx) in row">
                        <div class="cell" x-on:click="toggle(index, idx)"
                        x-bind:class="{ 'live': !!cell, 'dead': !cell }"></div>
                    </template>
                </div>
            </template>
        </div>
        <div class="info">
            <button x-show="state == 'STOP'" x-on:click="start()">スタート</button>
            <button x-show="state == 'START'" x-on:click="stop()">ストップ</button>
            <button x-show="state == 'STOP'" x-on:click="randomBoard()">ランダム</button>
            <div>世代数: <span x-text="generation"></span> </div>
        </div>
    </div>
    <script>
        const data = () => ({
            w: 20, 
            h: 20,
            generation: 0,
            board: [],
            state: 'STOP', // START or STOP
            timerId: null,
            ...
            (ライフゲームの処理とか)
        })
    </script>

x-data="data()" をつけたdivで囲った範囲で、 data関数が返すオブジェクトの中身(プロパティ、変数)を参照できるようです。

x-init は最初に呼び出したい関数を指定できます(Vueでいうmountedみたいなもの)

Vueとかと違って変数を表示する場合は <span x-text="generation"></span> のようにx-textを使ってDOMのinnerTextとして、指定してあげるみたいです。

他にも
x-on:click
x-bind:class
x-show
など使っていますが、ほぼ直感的に書くことができて、とてもいい感じでした :sparkles:

ちなみにCSSフレームワークは同じく読み込むだけで使える sakura というCSSフレームワークを使っています

最後に

nodeランタイムなしで実装ができるので、ペライチなどでさらっと使う分にはとても良さそうに感じました!
フロントエンドフレームワークを入れるほどでもないけど、jQueryだけで頑張るにはつらい。。。。っていうときには採用を検討してみても良さそうです。

Tailwindとの組み合わせも非常に気になりますね :sparkles:
また機会があったら試してみようと思います!

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

ラズパイでラジコン作るぞ

書いてること

ラズパイでラジコンを作ったので、その方法をここに残します。
専用アプリ不要で手持ちのスマホがコントローラーになります。

完成品はこちら ↓

構成図

全体の構成図です。

image.png

  • ラズパイで4つのサーボモーターを制御
    • server.pyから制御信号を出す
    • ※Python3スクリプト
  • index.htmlserver.pyはSocket通信
  • http://{ラズパイ}:8000/index.htmlを開いたスマホがコントローラーになる
  • <制御の流れ概要>
    • index.htmlから5パターンの信号(前進/後進/左回り/右回り/停止)を発信
    • server.pyindex.htmlからの信号をもとにサーボモーターを制御

サーボモーターの配線はこちら

image.png

Raspberry Piの情報

$ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 10 (buster)
Release:        10
Codename:       buster

$ python3 -V
Python 3.7.3

スクリプト

スクリプトは全てラズパイの/home/pi/data/web配下に格納します。
ディレクトリ構成は以下のようになります。
ソケット通信については こちらのサイト を参考にさせて頂きました。

pi@raspi:~/data/web $ tree
.
├── images
│   ├── back.png
│   ├── go.png
│   ├── left.png
│   ├── right.png
│   └── stop.png
├── index.html
└── server.py

コントローラーになるindex.htmlのソースです。
index.htmlはSocket通信ではクライアント側になり、Socket通信のサーバー側となるserver.pyに対し5種類の信号(GO/BACK/LEFT/RIGHT/STOP)を送ります。

index.html
<!DOCTYPE html>
<html>/home/pi/data/web
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <title>コントローラー</title>
  </head>
<body>
  <img onclick="sendMessage('GO')"    src="images/go.png" />
  <img onclick="sendMessage('BACK')"  src="images/back.png" />
  <img onclick="sendMessage('LEFT')"  src="images/left.png" />
  <img onclick="sendMessage('RIGHT')" src="images/right.png" />
  <img onclick="sendMessage('STOP')"  src="images/stop.png" />
  <br />
  <!-- 出力 area -->
  <textarea id="messageTextArea" rows="10" cols="50"></textarea>
  <script type="text/javascript">
    var webSocket = new WebSocket("ws://192.168.3.19:9998"); // ウェブサーバを接続
    var messageTextArea = document.getElementById("messageTextArea"); // ウェブサーバから受信したデータを出力するオブジェクトを取得
    // ソケット接続すれば呼び出す関数。
    webSocket.onopen = function(message){
      messageTextArea.value += "Server connect...\n";
    };
    // ソケット接続が切ると呼び出す関数。
    webSocket.onclose = function(message){
      messageTextArea.value += "Server Disconnect...\n";
    };
    // ソケット通信中でエラーが発生すれば呼び出す関数。
    webSocket.onerror = function(message){
      messageTextArea.value += "error...\n";
    };
    // ソケットサーバからメッセージが受信すれば呼び出す関数。
    webSocket.onmessage = function(message){
      // 出力areaにメッセージを表示する。
      messageTextArea.value += "Recieve From Server => "+message.data+"\n";
    };

    function sendMessage(argVal){
      messageTextArea.value += "Send to Server => "+ argVal +"\n";
      webSocket.send(argVal); // WebSocketでtextMessageのオブジェクトの値をサーバに送信
    }

    // 通信を切断する。
    function disconnect(){
      webSocket.close();
    }
  </script>
</body>
</html>

サーバー側でサーボモーターを制御するserver.pyのソースです。
server.pyindex.htmlから受信した信号(GO/BACK/LEFT/RIGHT/STOP)に応じモーターを制御します。

server.py
import asyncio
import websockets
import RPi.GPIO as GPIO
import time

GPIO.setmode(GPIO.BCM)

wheelPin = {
    "FrontRight": "2",
    "FrontLeft" : "3",
    "RearRight" : "17",
    "RearLeft"  : "4"
  }

wheelPwm = {
    "FrontRight": "",
    "FrontLeft" : "",
    "RearRight" : "",
    "RearLeft"  : ""
}

## 角度をラジアンに変換
def exchDegreeToRadian(argDegree):
    return 2.5 + (12.0 - 2.5) / 180 * (argDegree + 90)

## サーボモータ制御
def wheelControl(argFlg, argTerm):
    global wheelPwm
    act01 = exchDegreeToRadian( -90 )
    act02 = exchDegreeToRadian(  90 )
    act03 = exchDegreeToRadian(   0 )
    if(argFlg == "GO"):
        wheelPwm["FrontRight"].ChangeDutyCycle( act01 )
        wheelPwm["FrontLeft" ].ChangeDutyCycle( act01 )
        wheelPwm["RearRight" ].ChangeDutyCycle( act01 )
        wheelPwm["RearLeft"  ].ChangeDutyCycle( act01 )
    elif(argFlg == "BACK"):
        wheelPwm["FrontRight"].ChangeDutyCycle( act02 )
        wheelPwm["FrontLeft" ].ChangeDutyCycle( act02 )
        wheelPwm["RearRight" ].ChangeDutyCycle( act02 )
        wheelPwm["RearLeft"  ].ChangeDutyCycle( act02 )
    elif(argFlg == "STOP"):
        wheelPwm["FrontRight"].ChangeDutyCycle( act03 )
        wheelPwm["FrontLeft" ].ChangeDutyCycle( act03 )
        wheelPwm["RearRight" ].ChangeDutyCycle( act03 )
        wheelPwm["RearLeft"  ].ChangeDutyCycle( act03 )
    elif(argFlg == "LEFT"):
        wheelPwm["FrontRight"].ChangeDutyCycle( act01 )
        wheelPwm["FrontLeft" ].ChangeDutyCycle( act02 )
        wheelPwm["RearRight" ].ChangeDutyCycle( act01 )
        wheelPwm["RearLeft"  ].ChangeDutyCycle( act02 )
    elif(argFlg == "RIGHT"):
        wheelPwm["FrontRight"].ChangeDutyCycle( act02 )
        wheelPwm["FrontLeft" ].ChangeDutyCycle( act01 )
        wheelPwm["RearRight" ].ChangeDutyCycle( act02 )
        wheelPwm["RearLeft"  ].ChangeDutyCycle( act01 )

    time.sleep(argTerm)

#####################################################
# ここから処理を開始

## サーボモータの初期設定
for wheel in wheelPin:
    print("wheel={0} , pin={1}".format( wheel, wheelPin[wheel] ) )
    PinNo = int(wheelPin[wheel]) # GPIO
    GPIO.setup(PinNo, GPIO.OUT)
    wheelPwm[wheel] = GPIO.PWM(PinNo, 50)
    wheelPwm[wheel].start(0.0)

##----------------------------------------##

# クライアント接続すると呼び出す
async def accept(websocket, path):
  while True:
    # クライアントからメッセージを待機
    data = await websocket.recv()
    print("receive : " + data)
    # クライアントでechoを付けて再送信
    #await websocket.send("echo : " + data)
    if data == "GO":
      wheelControl("GO", 0.1)
    elif data == "BACK":
      wheelControl("BACK", 0.1)
    elif data == "STOP":
      wheelControl("STOP", 0.1)
    elif data == "LEFT":
      wheelControl("LEFT", 0.1)
    elif data == "RIGHT":
      wheelControl("RIGHT", 0.1)

# WebSocketサーバー生成
start_server = websockets.serve(accept, "raspi203.local", 9998)

# 非同期でサーバを待機する。
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()

以上で全ての準備が完了しました。

実行方法

2つコンソールを起動し、コンソール①ではWebサーバーを起動し、コンソール②ではserver.pyを実行します。

コンソール①
$ cd /home/pi/data/web
$ python3 -m http.server 8000
コンソール②
$ cd /home/pi/data/web
$ python3 server.py

実際の実行画面はこんな感じ

image.png

これでサービスが起動状態になりました。スマホなどでhttp://{ラズパイIP}:8000/index.htmlを開くとそのデバイスがコントローラーになります。

おわりに

今回は、とりあえずラズパイをラジコンにするまでを紹介しました。今後は、コントローラのUIをもっとリッチにするとか、ラジコンにカメラを搭載しコントローラ側に表示するとか、機能を盛り込んでいきます。

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

【Chart.js × Laravel】Controllerで返されたデータをJavaScriptで操作するには

Controllerで返された値をJavascriptで操作する方法、Javascriptでchart.jsを使って複数のチャートで動的なデータを表示する方法があまり見当たらなかったのでこちらで紹介。

通常Controllerで返された値(配列)は@foreachを使ってbladeで表示できる。

test.blade.php
// as $key => $valueの場合、
@foreach($array_datas as $key => $value)
     <div class="box">
         <p>keyは{{ $key }}valueは{{ $value }}だよ</p>
     </div>
@endforeach

// as $dataの場合、
@foreach($array_datas as $data)
     <div class="box">
         <p>それぞれのデータは{{ $data }}として表示されたよ</p>
     </div>
@endforeach

bladeでそのままデータを表示する場合は、上記のようにすれば問題ない。
JSライブラリによっては、出力される属性をidを指定している。
そのため@foreachとの併用の場合少し工夫がいる。

chart.jsも同様で、出力される属性はidなのでそのまま@foreachを使うと最初だけchartが表示されて2回目以降は表示されない。これはidがユニークなものでなければならないため、重複したidは2度目以降読み込まれないから。
なので、idで設定しているものをforeachで回すときはHTML側で{{$key}}を設定し、それぞれを独立させる必要がある。また、Javascript側でも読み込めるようにforEachを使って読み込むidを指定する。

まず初めに、chart.jsをそれぞれで読み込めるようにblade側を以下のように変更する。

test.blade.php
@foreach($array_datas as $data)
     <div class="box">
         <canvas id="myBarChart{{ $key }}"></canvas>
     </div>
@endforeach

keyを追加することでユニークなidをid=myBarChart0id=myBarChart1id=myBarChart2、のように生成する。

javascript側はdocument.getElementById()でユニークなidを読み込むので、それぞれのidを読み込むように、forEachを使って要素ごとの実行を行う。
なお、$foreachで定義された$array_datas@JSONを使うことでjavascriptでも操作できる。

test.blade.php(chart.js以下にscriptを設ける)
<script>
const array_datas = @JSON($array_datas); //bladeの$array_datasをjavascriptで読み込む
const data_keys = Object.keys(array_datas); // それぞれのkeyを取得

data_keys.forEach(el =>{
  const chart_id = "myBarChart" + el; // elはdata_keysのそれぞれのkey
  var ctx = document.getElementById(chart_id);
  var myBarChart = new Chart(ctx, {
   // それぞれのidごとにchartが生成される。
  });
});
</script>

ここまでで@foreach の中にあるchartは2回目以降も表示される。
javascript側で常にkeyをみることで、bladeの$keyと連動させることができる。

とはいえ、これでは同じチャートの情報を複製しているにすぎない。

ここからはそれぞれのchartでkeyごとに別々のデータを表示する方法を紹介。
chart.jsでは表示されるデータの値を以下のようにdatasets以下のdata配列から参照する。

test.blade.php
var myBarChart = new Chart(ctx, {
    type: 'bar',
    data: {
      labels: ['8月1日', '8月2日', '8月3日', '8月4日', '8月5日', '8月6日', '8月7日'],
      datasets: [
        {
          label: 'A店 来客数',
          data: [62, 65, 93, 85, 51, 66, 47],
          backgroundColor: "rgba(219,39,91,0.5)"
        },{
          label: 'B店 来客数',
          data: [55, 45, 73, 75, 41, 45, 58],
          backgroundColor: "rgba(130,201,169,0.5)"
        },{
          label: 'C店 来客数',
          data: [33, 45, 62, 55, 31, 45, 38],
          backgroundColor: "rgba(255,183,76,0.5)"
        }
      ]
    },
.
.
.

例えば、Controllerで取得した配列データが連想配列で、そのデータがkey以下に複数の配列が存在する以下のような場合、
スクリーンショット 2021-02-28 19.20.54.png

それぞれのkey(0,1,2)ごとにデータ("Aサイト" => array3)を参照してchartで表示したい。
こんなときはJavascriptでいったんkeyごとに配列を生成して、それを参照するようにchart.jsに記述する。

const array_datas = @JSON($array_datas);
        const data_keys = Object.keys(array_datas);
        console.log(data_keys); // ["0", "1", "2"]
        data_keys.forEach(el =>{
            const month = [];
            const clicks = [];
            const imps = [];
            const chart_id = "myBarChart" + el;
            const array_data = Object.values(array_datas[el]);
            console.log(array_data);
            for(var i = 0; i < array_data[0].length; i++){
                month.push(array_data[0][i]['month']);
                clicks.push(array_data[0][i]['click']);
                imps.push(array_data[0][i]['imps']);
            }
            console.log(month); // ["2020-12", "2020-11", "2020-10"]
            console.log(clicks); // [3, 8, 1]
            console.log(imps); //[36, 52, 24]

            var ctx = document.getElementById(chart_id);
            var myBarChart = new Chart(ctx, {
                type: 'bar',
                data: {
                  labels: month,
                  datasets: [
                    {
                      label: 'クリック数',
                      data: clicks,
                      backgroundColor: "rgba(219,39,91,0.5)",
                    },{
                      label: '表示回数',
                      data: imps,
                      backgroundColor: "rgba(130,201,169,0.5)"
                    },
                  ]
                },
                options: {
                    scales: {
                      yAxes: [{ //y軸
                        ticks: {
                          suggestedMax: 80,
                          suggestedMin: 0,
                          stepSize: 10,
                        }
                      }],
                      xAxes: [{ //X軸
                        ticks: {
                          font: {
                            size: 3,
                          },
                          padding: 0
                        }
                      }]
                    },
                    layout: {
                        padding: {
                            left: 0,
                            right: 0,
                            top: 0,
                            bottom: 0
                        }
                    },
                    plugins: {
                        datalabels: { // 共通の設定はここ
                            font: {
                                size: 14,
                                color: 'rgba(200,60,60,1)',
                            },
                            anchor: 'end', 
                            align: 'end', 
                        }
                    },
                  }
                });

        });

bladeも若干調整する。

test.blade.php
@foreach($array_datas as $key => $value)
     <div class="chart-box">
         <div>keyの番号{{$key}}番目のチャート情報</div>
         @foreach ($value as $index_name => $item)
         <div>~~ {{$index_name}}の結果 ~~</div>
         @endforeach
         <canvas id="myBarChart{{ $key }}"></canvas>
     </div>
@endforeach

<style>
        .chart-box {
            display: inline-block;
            text-align: center;
            width: 300px;
            margin: 30px;
            background: #f7f7f7;
        }
</style>





 <script src="{{ asset('js/Chart.bundle.js') }}"></script>
 <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@0.7.0"></script>

結果、以下のようにそれぞれの値をchartで表示することができる。
スクリーンショット 2021-02-28 19.42.41.png

おまけ

chart.jsのy軸のラベルはデフォルトでは数値を記述することで,Max,Min,Stepを設定できる。できれば、配列のデータごとにlabelも可変されるとよりみやすくなる。その場合は、下記のように関数を設定して動的にlabelを設定するといい感じのチャートが完成する。

yAxes: [{ //y軸
         ticks: {
           suggestedMax: 80, // ←ここ
           suggestedMin: 0, 
           stepSize: 10, // ←ここ
         }
       }],

// ********* 以下のように変更 ********* //
yAxes: [{ //y軸
         ticks: {
           suggestedMax: adjastSuggestedMax(month,clicks,imps)
           suggestedMin: 0, 
           stepSize: adjastSuggestedStep(month,clicks,imps),
         }
       }],
.
.
.
});
// 以下2つの関数を追加
function adjastSuggestedMax(sc_keyword_month,sc_keyword_clicks,sc_keyword_imps){
            max_clicks = Math.max.apply(null, sc_keyword_clicks);
            max_imps = Math.max.apply(null, sc_keyword_imps);
            all_max_num = [];
            all_max_num.push(max_clicks,max_imps);

            max_num = Math.max.apply(null, all_max_num);
            if(max_num < 10){
                var suggestedMax = 10;
            } else if(10 <= max_num){
                var suggestedMax = max_num + 20;
            }
            return suggestedMax;
        };

function adjastSuggestedStep(sc_keyword_month,sc_keyword_clicks,sc_keyword_imps){
            max_clicks = Math.max.apply(null, sc_keyword_clicks);
            max_imps = Math.max.apply(null, sc_keyword_imps);
            all_max_num = [];
            all_max_num.push(max_clicks,max_imps);

            max_num = Math.max.apply(null, all_max_num);
            if(max_num < 10){
                var suggestedStep = 10;
            } else if(10 <= max_num){
                var suggestedStep = 30;
            }
            return suggestedStep;
        };

こうすることで配列の数字の中から最大値を検知してそれに合わせてラベルやステップを表示することができる。if条件をさらに細かくすれば自分好みの可変もできる。

まとめ

今回はbladeのデータをJavascriptで扱う方法と個別チャートの表示方法を紹介しました。
chart.jsに限らず多くのライブラリで同じような方法で個別に生成したりできるので、他のライブラリでも挑戦してみてください。

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

【JavaScript】配列を非破壊的に扱うために必要だった知識

TL;DR

文字列・数値・真偽値の配列ならarr.slice()でコピーして操作
プロパティがネストしないオブジェクトならarr.map(e => ({...e}))でコピーして操作
(二次元配列ならarr.map(e => [...e]))
プロパティがネストするオブジェクトならライブラリを探す

何故非破壊的に扱いたいのか

JavaScriptのArrayのメソッドには破壊的メソッドと非破壊的メソッドが混在している。
reverseだとか、shiftだとかの破壊的なメソッドが関数に紛れ込んでいると副作用を生じさせてしまう。

const fn = arr => arr.reverse().map(e => e + 1)

const arr = [1, 2, 3]
const arr2 = fn(arr)

console.log(arr2) //=> [4, 3, 2]
console.log(arr) //=> [3, 2, 1] 見かけ上arrには何もしていないはずなのに!

全部非破壊的に扱わせてくれと思った。

arr.slice()を試す

調べてすぐ出てくるのはarr.slice()またはarr.concat()
一旦配列のコピーを作ってもとの配列への作用を回避する手法だ。

const fn = arr => arr.slice().reverse().map(e => e + 1)

const arr = [1, 2, 3]
const arr2 = fn(arr)

console.log(arr2) //=> [4, 3, 2]
console.log(arr) //=> [1, 2, 3]

この場合はarr.slice()で問題ないが、MDNのsliceのページでは下記のように解説されている。

slice は元の配列を変更せず、元の配列から要素をシャローコピー (1 段階の深さのコピー) した新しい配列を返します。元の配列の要素は以下のように返される配列にコピーされます。

(実際のオブジェクトではない) オブジェクトの参照については、slice はオブジェクトの参照を新しい配列にコピーします。元の配列も新しい配列も同じオブジェクトを参照します。参照されたオブジェクトが修正された場合、その変更は新しい配列と元の配列の両方に現れます。

(String, Number, Boolean オブジェクトではなく) 文字列、数値、真偽値では、slice は値を新しい配列にコピーします。

一方の配列の文字列や数値に変更を加えても、他の配列に影響はしません。

つまり、オブジェクトの配列に対してarr.slice()でコピーしても中身がオブジェクトの参照なので、中身を操作するともとの配列(の中身)にも影響してしまう。

const arr = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
]

const fn = arr => arr.slice().reverse().map(e => e.shift())
console.log(fn(arr)) // [ 7, 4, 1 ]

// reverseに関しては影響していないが、shiftに関しては影響してしまっている
console.log(arr) // [ [ 2, 3 ], [ 5, 6 ], [ 8, 9 ] ]

こんな真似を実際するかは不明だが、arr.slice()はオブジェクトに関しては別のオブジェクトとしてコピーしているわけではないということが実例できた。
これも非破壊的に扱いてえ。

Spread syntaxを試す

{...e}でオブジェクトのコピーを、[...e]で配列のコピーを作成できる。
arr.map(e => [...e])arr.map(e => ({...e}))で内部の要素をコピーした新しい配列を生成すれば、内部要素ごと新しい配列として、元の配列(とその内部要素)に対し影響なく扱える。

const arr = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
]

const fn = arr => arr.map(e => [...e]).reverse().map(e => e.shift())
console.log(fn(arr)) // [ 7, 4, 1 ]
console.log(arr) // [ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ] ]

ただし、上記の方法も完璧ではなく、MDNのSpread syntaxの解説によれば、

メモ: コピーは 1 段階の深さで行われます。そのため、次の例のような多次元配列のようなオブジェクトをコピーする場合には適さないでしょう。(Object.assign() についても同じことが言えます。)

上記は多次元配列についての解説だが、オブジェクトがネストしたプロパティを持つオブジェクトに関しても同様のことが起こる。

type Contact = {
  email: string,
  tel: string,
}

type User = {
  name: string,
  contact: Contact,
}

上記のUser型のオブジェクトみたいなやつ。
User.contactみたいなネストしたプロパティが出る度にそのプロパティを新しくコピーするような関数をmapして……という対応を取らなければいけない。
arr.map(e => ({...e}))のようなワンライナーで済むならばコストは低いが、プロパティがネストしてそのオブジェクトのプロパティもネストして……となると辛い。

そうなったら食い下がるのはやめて、ライブラリを使おうと思いました。(いかがでしたかブログ的な終わり)

補足

arr.map(e => ({...e}))は、

arr.map(e => {
  return {...e}
})

をワンライナーで書くためにSpread syntaxを()で括ってあげてるものです。

参考

Array.prototype.slice() - JavaScript | MDN
スプレッド構文 - JavaScript | MDN

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

[CSS+Javascript] CMYの減法混色をmix-blend-modeを使ってシミュレートするためのベースロジック

はじめに

以前に書いた記事の続きに当たります。あるWEBアプリケーションのデモ開発を行った後、UIの一部、表題内容の処理部分をパーツとして分解し、JSFiddleCodePenで公開しましたので、本記事はその詳細という位置づけとなります。

目的

C、M、Y、それぞれのカラーブロックを重ね、実際に混色した際の色をシミュレートします。ベースロジックのデモのため、それぞれの色成分初期値は濃度100%とし、濃度の調整はスライダーにより、3色同比率で増減させます。

つまり、結果的には単に見た目が黒から白への濃度変化するものになるのですが、その表現方法の構築が本記事でのメインとなります。1

以下が今回の完成形です。


See the Pen
stCMYblend
by STSHISHO (@STSHISHO)
on CodePen.


CSS設定

1-カラーブロックの定義

<div class="container">
  <div class="box box-1"></div>
  <div class="box box-2"></div>
  <div class="box box-3"></div>
</div>
.container {
  width: 100%;
}
.box {
  width: 300px;
  height: 300px;
}
.box-1 {
  /* cyan */
  background: rgba(0, 255, 255, 1);
}
.box-2 {
  /* magenta */
  background: rgba(255, 0, 255, 1);
}
.box-3 {
  /* yellow */
  background: rgba(255, 255, 0, 1);
}

上記コードの結果、以下のようになります。
cmy-01.png

2-センター揃えで横に並べる

.container {
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center; /* if you need */
  height: 400px;
  position: relative; /* 中央揃えの場合は指定しなくても意図通りに表示 */
}
.box {
  width: 300px;
  height: 300px;
}

.containerの修正。以前ならfloatを使用していましたが、display: flex;で横並びにします。justify-content: center;で中央揃えになります。必要に応じてalign-itemsを指定します。今回は上下左右中央揃えにしています。
cmy-02-flex.png

3-カラーブロックを重ねる

これは単純です。.boxposition: absolute;を追加するだけです。一番最後に記述された要素が最前面になりますので、デモではYellowのカラーブロックが前面に表示されます。
cmy-03-abslt.png

4-mix-blend-modeを使う

CSSでPhotoshopのような乗算効果は再現可能か調べていたところ、こちらの記事を見つけ、MDNで仕様を確認しました。.boxmix-blend-mode: multiply;を追加して、設定完了。
cmy-04-mltply.png

スライダーを設置し、イベントリスナーを設定

<div class="slider">
  <span class="rowHead">濃度</span><input type="range" id="slider_all" min="0" max="100" step="1" value="100"><span class="rowTail">100%</span>
</div>

カラーブロックの直下にスライダーを設置し、イベントリスナーを設定します。

window.onload = () => {
  const t = document.querySelector('.container') //container全体を取得
  const s = document.querySelector('#slider_all')
  //スライダーにイベントリスナー設定
  s.addEventListener('input', e => t.style.opacity = e.target.value * 0.01, false)
}

inputで、リアルタイムに濃度が変わります。スライダーの値が0から100までなので、0.01を乗算することでそのままopacity値となり、.container全体のopacityを変更します。

最後に

上記を元に、スライダーをカラーごとに設置し、ターゲットをカラーブロックに変更することで、個別の濃度調整が可能になります。

なお、元になったWEBアプリケーションの詳細についてはいずれまとめる予定でいます。

[追記]

今回はあくまでも実際のケースに沿った表現方法の一例として、mix-blend-modeを使った方法を紹介しています。そのため、タイトルも少し修正しました。シンプルにRGB→CMY変換、減法混色をしたい場合には、il9437さんのコメントおよびコードの方がロジックとして王道ですし、参考になると思います。

[参考]


  1. 当該のアプリケーションではC、M、Y、個別の濃度調整スライダーを実装しています。また、それぞれの色成分初期値が、例えば、シアンの初期値(濃度100%)はRGB(0, 255, 255)ではなく、RGB(65,225,210)という具合にクライアントによって変更できるようになっています。 

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

[CSS+Javascript] CMYの減法混色をシミュレートするためのベースロジック

はじめに

以前に書いた記事の続きに当たります。あるWEBアプリケーションのデモ開発を行った後、UIの一部、表題内容の処理部分をパーツとして分解し、JSFiddleCodePenで公開しましたので、本記事はその詳細という位置づけとなります。

目的

C、M、Y、それぞれのカラーブロックを重ね、実際に混色した際の色をシミュレートします。ベースロジックのデモのため、それぞれの色成分初期値は濃度100%とし、濃度の調整はスライダーにより、3色同比率で増減させます。

つまり、結果的には単に見た目が黒から白への濃度変化するものになるのですが、その表現方法の構築が本記事でのメインとなります。1

以下が今回の完成形です。


See the Pen
stCMYblend
by STSHISHO (@STSHISHO)
on CodePen.


CSS設定

1-カラーブロックの定義

<div class="container">
  <div class="box box-1"></div>
  <div class="box box-2"></div>
  <div class="box box-3"></div>
</div>
.container {
  width: 100%;
}
.box {
  width: 300px;
  height: 300px;
}
.box-1 {
  /* cyan */
  background: rgba(0, 255, 255, 1);
}
.box-2 {
  /* magenta */
  background: rgba(255, 0, 255, 1);
}
.box-3 {
  /* yellow */
  background: rgba(255, 255, 0, 1);
}

上記コードの結果、以下のようになります。
cmy-01.png

2-センター揃えで横に並べる

.container {
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center; /* if you need */
  height: 400px;
  position: relative; /* 中央揃えの場合は指定しなくても意図通りに表示 */
}
.box {
  width: 300px;
  height: 300px;
}

.containerの修正。以前ならfloatを使用していましたが、display: flex;で横並びにします。justify-content: center;で中央揃えになります。必要に応じてalign-itemsを指定します。今回は上下左右中央揃えにしています。
cmy-02-flex.png

3-カラーブロックを重ねる

これは単純です。.boxposition: absolute;を追加するだけです。一番最後に記述された要素が最前面になりますので、デモではYellowのカラーブロックが前面に表示されます。
cmy-03-abslt.png

4-mix-blend-modeを使う

CSSでPhotoshopのような乗算効果は再現可能か調べていたところ、こちらの記事を見つけ、MDNで仕様を確認しました。.boxmix-blend-mode: multiply;を追加して、設定完了。
cmy-04-mltply.png

スライダーを設置し、イベントリスナーを設定

<div class="slider">
  <span class="rowHead">濃度</span><input type="range" id="slider_all" min="0" max="100" step="1" value="100"><span class="rowTail">100%</span>
</div>

カラーブロックの直下にスライダーを設置し、イベントリスナーを設定します。

window.onload = () => {
  const t = document.querySelector('.container') //container全体を取得
  const s = document.querySelector('#slider_all')
  //スライダーにイベントリスナー設定
  s.addEventListener('input', e => t.style.opacity = e.target.value * 0.01, false)
}

inputで、リアルタイムに濃度が変わります。スライダーの値が0から100までなので、0.01を乗算することでそのままopacity値となり、.container全体のopacityを変更します。

最後に

上記を元に、スライダーをカラーごとに設置し、ターゲットをカラーブロックに変更することで、個別の濃度調整が可能になります。

なお、元になったWEBアプリケーションの詳細についてはいずれまとめる予定でいます。

[参考]


  1. 当該のアプリケーションではC、M、Y、個別の濃度調整スライダーを実装しています。また、それぞれの色成分初期値が、例えば、シアンの初期値(濃度100%)はRGB(0, 255, 255)ではなく、RGB(65,225,210)という具合にクライアントによって変更できるようになっています。 

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

配列っぽいデータであれば必ず使えるforEach関数をつくろう!

あらすじ

JavaScriptのforEach関数にはいくつか使いにくい部分があると思います!

  1. HTMLCollectionのような「配列のようだけど配列じゃない」データにたいして使えない。
  2. keyvalueどちらも参照したい場合、第一引数がvalue、第二引数がkeyとなるように指定しなければいけなくて、微妙に直感的じゃない。
  3. PHPのforeachと比べると、書き順が違うので、どちらも使う職場では混乱する。

※ PHPのforeach: foreach( $配列 as $キー => $値 ){ ...
※ JavaScriptのforEach: 配列.forEach( (値, キー) => { ...

上記のような問題点を解決するため、オリジナルのforeach関数を作ってみました!

参考記事: JavaScriptのループはどれが一番高速なのか

参考記事: 文字列を数値に変換する方法と処理速度

コード

for...inを使います!

function foreach( roopTarget, process ){
  for( const i in roopTarget ){
    switch( process.length ){ // process が関数だった場合、 length で引数の数が判別できる。
      case 0: process(); break;
      case 1: process( roopTarget[i] ); break;
      case 2: process( Number(i), roopTarget[i] ); break; // i が文字列型になっているので、数値型に変換しておく。 // 文字列→数値変換は Number() が早いらしい。
      default: console.error('引数が多すぎます!'); return;
    }
  }
}

ちなみに、ループしたいオブジェクトが NodeListHTMLCollection だった場合、
数値のキーと文字列のキーが混在しています。
中身の要素だけ処理したい場合は、switch文の前に「キーが文字列だった場合は処理しない」というコードを挟むといいと思います!
if( isNaN(i) ) continue;

おわり

当初の問題を解決したforeach関数ができあがりました!
これを使えば foreach( 配列, (キー,値)=>{ ... という書き方で、
どんなデータでもforeachが使えるようになります!

「ここまでする必要あるのか?」
多分、まったく必要ないです!!

普通に for...infor...of でぶん回すのが個人的には好きです! ご拝読ありがとうございました!!

謝罪
すみません、投稿時点では lengthプロパティを使ってfor文でループするパターンの書き方も掲載していたのですが、
キーが文字列になっているオブジェクトに対して利用できないことに気づいて、削除しました。

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

TypeGraphQLでN+1問題を解決した話

はじめに

GraphQLをサービスで使い始めて、N+1問題にぶち当たったのでその解決策を紹介する。

プロジェクト構成

  • Node.js
  • TypeScript
  • Express
  • GraphQL(Apollo, TypeGraphQL)

実際何が起こったか

DBにはとあるレコードが入っており、それぞれにuseridを保持している。
useridからユーザー名やメアドなどのユーザー情報を取り出すには、別の内部APIに問い合わせる必要がある。

GraphQLのスキーマはこのように定義している。

schema.gql
type Query {
  record(id: Int!): Record
  records(name: String): [Record!]!
}

type Record {
  id: Int!
  name: String!
  user: User
  userid: Int!
}

type User {
  userid: Int!
  username: String!
}

レコードはこのように取得しているとする。

RootResolver.ts
@Resolver()
export class RootResolver {
    @Query(returns => [Record])
    async records(): Promise<Record[]> {
        const records = await conn.query(`
            SELECT
                id,
                name,
                userid
            FROM
                xxx
        `);

        return records;
    }

    @Query(returns => Record, { nullable: true })
    async record(
        @Arg('id', type => Int) id: number
    ): Promise<Record | undefined> {
        const records = await conn.query(`
            SELECT
                id,
                name,
                userid
            FROM
                xxx
            WHERE
                id = ?
        `, [id]);

        return records[0];
    }
}

このとき、N+1問題を気にせずにuserリゾルバを書くことこのようになる。
fetchUsersは内部APIにリクエストを送ってユーザー情報を返す関数とする。

@Resolver(of => Record)
class RecordResolver {
    @FieldResolver()
    user(@Root() record: Record) {
        return fetchUsers([record.userid])[0];
    }
}

例えばRecordを1件だけ取得する場合は、内部APIへのリクエストは1回で済むが、
一覧画面などで100件取得する場合はfetchUsersがほぼ同時に100回呼ばれることとなり、内部APIサーバーやDBの負荷が上がってしまう。

10件取得した場合のログ

fetchUsers(0)
fetchUsers(1)
fetchUsers(2)
fetchUsers(3)
fetchUsers(4)
fetchUsers(5)
fetchUsers(6)
fetchUsers(7)
fetchUsers(8)
fetchUsers(9)

改善方法

リゾルバで即座にリクエストを送るのではなく、問い合わせたいIDを溜めて、バッチ処理で一つのリクエストに複数IDを載せて送ることでリクエストの量を削減させる。
※この場合、内部APIの方を複数IDに対応させる必要がある。

DataLoaderTypeGraphQL-DataLoaderを使うことでこれを簡単に実現できる。
DataLoaderは遅延読み込みをするためのFacebook製のライブラリで、TypeGraphQL-DataLoaderはDataLoaderをTypeGraphQLに適用させたライブラリである。

組み込み方法

ライブラリをインストールする。

npm i -S dataloader type-graphql-dataloader

プラグインを読み込む。

const server = new ApolloServer({
    schema: await makeSchema(),
    validationRules: [depthLimit(7)],
    plugins: [
        // これを追加
        ApolloServerLoaderPlugin({}),
    ]
});

userリゾルバをこのように修正する。

@Resolver(of => Record)
class RecordResolver {
    @FieldResolver()
    @Loader<number, User | undefined>(async (ids) => {
        const users = await fetchUsers([...ids]);
        return ids.map((id) => users.find((user) => user.userid === id));
    })
    user(@Root() record: Record) {
        return async (dataloader: DataLoader<number, User | undefined>) => {
            const user = await dataloader.load(record.userid);
            return user;
        };
    }
}

10件取得するとこのようなログになる。

fetchUsers(0,1,2,3,4,5,6,7,8,9)

おわりに

N+1問題は気づかずにDBや他のサーバーに負荷をかけてしまう可能性があるので注意して設計してほしい。
DataLoaderを使えば、意外と簡単に改善できるのでこれからも活用していきたい。

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

Chart.jsのレーダーチャートにpointlabelにonClickイベントをつける

さっくり知りたい人のための要約

  • Chart.jsにはpointLabelへのonClickはデフォルトで用意されていない
  • 自作する場合は、<canvas></canvas>にonClickイベントをつけて座標計算する必要がある
    • マウスのクリックイベントから座標を取得して、canvas内のpointLabelの座標内か判断する必要がある
    • pointLabelの座標も自力で計算する必要がある
  • 実行結果 | ソース | 私なりの解説

はじめに

  • pointLabelってどこのこと?
    →レーダーチャートのチャート外に表示されてる文字の部分(ここ:point_down_tone5:のこと)
    スクリーンショット 2020-07-11 13.15.38.png

  • Chart.js用意されてないの?
    →ない...はず....(v2.9.3現在)
    後で詳細に説明するけれど、canvasのどこをクリックして、それがラベルのある座標なのかを計算してむりくり実装するしかなさそう。
    有志が計算部分省略するためにchartObjectにpointLabelの座標を入れてくれ〜という提案をしているのでこのissue盛り上げて採用されよう:muscle_tone2::muscle_tone2:

  • あなたが考案したソース?
    →いいえ、こちらのStackOverflowの回答者simoncogginsから引用したものであり、彼の実装を元にコメントを日本語訳したくらいのものを紹介します。
    前述のissue書いてる人も同一人物です。感謝...:pray::pray::pray:

いざ実装:point_up_tone3:

今回の記事では v2.9.3 の Chart.js を利用します。(この記事を下書きに放置している間に2.9.4が出てるっぽい)
また、jQueryとかもなしのピュアなjavascriptで記載します。 jQueryとか他使っている人は適宜読み替えてくださいませ。

完成図

(実はGithubも用意してあるけれどまあCodePenあればいらないカモ)


See the Pen
MWKXQLN
by 中村@ガウチャ (@_nakashimamura)
on CodePen.


実装手順

  1. レーダーチャートを描画
  2. クリックイベントをレーダーチャートを描画するcanvasに設定
  3. クリックされたらクリックされた箇所がpointLabelかを座標計算して判定
  4. ラベルだったときは必要な処理をする! 終わり!:hugging:

*今回はcallbackは特別なことせずに、ただalertでイベントから受け取った情報を出すようにします

いざいざ実装

本題は 3. からなのでそこだけ読みたい人は

1. レーダーチャートを描画

HTML側はChart.jsとjsの読み込み、それとcanvasの設定。#chartCanbasにレーダーチャートを表示させる。
(Chart.jsをnpmでインストールしてjs側でimportしても良いけど今回はちゃちゃっとCDNから読み込んじゃう)

index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.js"></script>
<script src="./local.js"></script>

<!-- レーダーチャートを表示させるcanvas -->
<canvas id="chartCanvas"></canvas>
local.js
window.onload = () => {
  const chartArea = document.getElementById("chartCanvas"); // canvasのDom
  const radar = new Chart(new Chart(target.getContext("2d"), {type: "radar" .../* 省略 */...});
};

レーダーチャートの表示部分は今回の記事では紹介しません
公式ドキュメントとか、日本語翻訳してくれてる有志のサイトを参考に作ると良いかと!

2. クリックイベントをレーダーチャートを描画するcanvasに設定

特別なこともなく、いつものようにcanvasにonclickイベントを登録するだけ。
忘れずeventオブジェクトを引数で受け取ってください。

window.onload = () => {
  const chartArea = document.getElementById("chartCanvas");
  const radar = new Chart(new Chart(target.getContext("2d"), {type: "radar" .../* 省略 */...});

  // 以下canvasにクリックイベントを登録
  chartArea.onclick = (e) => {
    //
  }
};

3. クリックされたらクリックされた箇所がpointLabelかを座標計算して判定

さて、ここからが本記事の本題!:upside_down:
コードは全てchartArea.onclick = (e) => {内にかかれるものとして読んでください。

座標計算の流れとしては今回は
1. クリック位置を取得
2. ラベルの数だけループを回し、
3. ラベルがどこに描画されているかを計算 (これが大変)
4. クリック位置がラベルの範囲か計算・判定
5. ラベルをクリックしたのなら目的の処理を行う
6. ループを抜ける
となっています。

1. クリック位置を取得

// canvas内のクリックしたx,y座標
const mouseX = e.offsetX;
const mouseY = e.offsetY;

2. ラベルがどこに描画されているかを計算
new Chart()した際の戻り値であるchartObjectにラベルの座標はなので、
marginやpadding、fontSizeの設定値やラベルの文字数から計算する他ないです。
しかもラベルは1つじゃないのでループを回して全てを計算する必要があるという分けです。

:star: 前準備
まず目盛り文字の高さ一番外側の目盛り文字のレーダーチャート中心からの距離が必要ですので取得しておきます。
ラベルはこの目盛りより外に表示されますからね。
スクリーンショット 2021-02-28 16.15.23.png

const helpers = Chart.helpers; // 
const scale = radar.scale; // 軸
const opts = scale.options; // チャート全体の設定
const tickOpts = opts.ticks; // 目盛り

// 目盛りの高さ
// 基本はfontsize + 5px
const tickBackdropHeight =
  tickOpts.display && opts.display // chartの設定で optionで目盛りが表示されているか
    ? helpers.valueOrDefault(
        tickOpts.fontSize,
        Chart.defaults.global.defaultFontSize
      ) + 5
    : 0;

// 目盛りの中央からの距離
const outerDistance = scale.getDistanceFromCenterForValue(
  opts.ticks.reverse ? scale.min : scale.max
);

Chart.helpersについてはバージョン違いだが、こちらを参照。
scaleが持っているmethodは同じくバージョン違いだがこちらが参考になるっぽい。

:star: ラベルの位置を計算
ではループを回してラベル類の位置を特定していきましょう。

// ポイントラベル分ループ
for (var i = 0; i < scale.pointLabels.length; i++) {
  // 軸ラベルによってチャート上部に余分な空白があるので削除
  const extra = i === 0 ? tickBackdropHeight / 2 : 0;
  const pointLabelPosition = scale.getPointPosition(
    i,
    outerDistance + extra + 5
  );

  // ラベルサイズ情報を取得
  const plSize = scale._pointLabelSizes[i];

  // ラベルのtextAlignを取得する(ラベルの描画位置が変わるので)
  const angleRadians = scale.getIndexAngle(i);
  const angle = helpers.toDegrees(angleRadians);
  let textAlign = "right";
  if (angle == 0 || angle == 180) {
    textAlign = "center";
  } else if (angle < 180) {
    textAlign = "left";
  }

  // ラベルの垂直オフセット位置を取得
  // drawPointLabels()から取得し計算
  let verticalTextOffset = 0;
  if (angle === 90 || angle === 270) {
    verticalTextOffset = plSize.h / 2;
  } else if (angle > 270 || angle < 90) {
    verticalTextOffset = plSize.h;
  }

  // 対象のラベルの範囲の位置を計算(padding含み)
  const labelTop = pointLabelPosition.y - verticalTextOffset - labelPadding;
  const labelHeight = 2 * labelPadding + plSize.h;
  const labelBottom = labelTop + labelHeight;

  const labelWidth = plSize.w + 2 * labelPadding;
  let labelLeft;
  switch (textAlign) {
    case "center":
      labelLeft = pointLabelPosition.x - labelWidth / 2;
      break;
    case "left":
      labelLeft = pointLabelPosition.x - labelPadding;
      break;
    case "right":
      labelLeft = pointLabelPosition.x - labelWidth + labelPadding;
      break;
    default:
      console.warn("WARNING: unknown textAlign " + textAlign);
  }
  let labelRight = labelLeft + labelWidth;

  // ...以降クリックされたか判定などなど...

} // end of for

まんま貼り付けただけですが、textAlignを自力で取得したりするところがみそですね。
CSS的なのtextAlignではなく、レーダーチャートのの外周、左右上下のどこに描画されているかを計算しています。

5. ラベルをクリックしたのなら目的の処理を行う
6. ループを抜ける
for文の最後に以下で判定して完成でっす! :tada:

  // クリックされた範囲がラベルか判定
  if (
    mouseX >= labelLeft &&
    mouseX <= labelRight &&
    mouseY <= labelBottom &&
    mouseY >= labelTop
  ) {
    // したい処理をここに記載!!
    // なにか値が必要な場合もここまでで取得できていそうなのでなんでもできるかと!
    break; //ラベルクリックがわかったので処理を終了
  }

以上で目的は達成かと思います。

最後に

本当に以下の回答に Thanks! :bow:
https://stackoverflow.com/a/58296237

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

Chart.jsのレーダーチャートのpointlabelにonClickイベントをつける

さっくり知りたい人のための要約

  • Chart.jsにはpointLabelへのonClickはデフォルトで用意されていない
  • 自作する場合は、<canvas></canvas>にonClickイベントをつけて座標計算する必要がある
    • マウスのクリックイベントから座標を取得して、canvas内のpointLabelの座標内か判断する必要がある
    • pointLabelの座標も自力で計算する必要がある
  • 実行結果 | ソース | 私なりの解説

はじめに

  • pointLabelってどこのこと?
    →レーダーチャートのチャート外に表示されてる文字の部分(ここ:point_down_tone5:のこと)
    スクリーンショット 2020-07-11 13.15.38.png

  • Chart.js用意されてないの?
    →ない...はず....(v2.9.3現在)
    後で詳細に説明するけれど、canvasのどこをクリックして、それがラベルのある座標なのかを計算してむりくり実装するしかなさそう。
    有志が計算部分省略するためにchartObjectにpointLabelの座標を入れてくれ〜という提案をしているのでこのissue盛り上げて採用されよう:muscle_tone2::muscle_tone2:

  • あなたが考案したソース?
    →いいえ、こちらのStackOverflowの回答者simoncogginsから引用したものであり、彼の実装を元にコメントを日本語訳したくらいのものを紹介します。
    前述のissue書いてる人も同一人物です。感謝...:pray::pray::pray:

いざ実装:point_up_tone3:

今回の記事では v2.9.3 の Chart.js を利用します。(この記事を下書きに放置している間に2.9.4が出てるっぽい)
また、jQueryとかもなしのピュアなjavascriptで記載します。 jQueryとか他使っている人は適宜読み替えてくださいませ。

完成図

(実はGithubも用意してあるけれどまあCodePenあればいらないカモ)


See the Pen
MWKXQLN
by 中村@ガウチャ (@_nakashimamura)
on CodePen.


実装手順

  1. レーダーチャートを描画
  2. クリックイベントをレーダーチャートを描画するcanvasに設定
  3. クリックされたらクリックされた箇所がpointLabelかを座標計算して判定
  4. ラベルだったときは必要な処理をする! 終わり!:hugging:

*今回はcallbackは特別なことせずに、ただalertでイベントから受け取った情報を出すようにします

いざいざ実装

本題は 3. からなのでそこだけ読みたい人は

1. レーダーチャートを描画

HTML側はChart.jsとjsの読み込み、それとcanvasの設定。#chartCanbasにレーダーチャートを表示させる。
(Chart.jsをnpmでインストールしてjs側でimportしても良いけど今回はちゃちゃっとCDNから読み込んじゃう)

index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.js"></script>
<script src="./local.js"></script>

<!-- レーダーチャートを表示させるcanvas -->
<canvas id="chartCanvas"></canvas>
local.js
window.onload = () => {
  const chartArea = document.getElementById("chartCanvas"); // canvasのDom
  const radar = new Chart(new Chart(target.getContext("2d"), {type: "radar" .../* 省略 */...});
};

レーダーチャートの表示部分は今回の記事では紹介しません
公式ドキュメントとか、日本語翻訳してくれてる有志のサイトを参考に作ると良いかと!

2. クリックイベントをレーダーチャートを描画するcanvasに設定

特別なこともなく、いつものようにcanvasにonclickイベントを登録するだけ。
忘れずeventオブジェクトを引数で受け取ってください。

window.onload = () => {
  const chartArea = document.getElementById("chartCanvas");
  const radar = new Chart(new Chart(target.getContext("2d"), {type: "radar" .../* 省略 */...});

  // 以下canvasにクリックイベントを登録
  chartArea.onclick = (e) => {
    //
  }
};

3. クリックされたらクリックされた箇所がpointLabelかを座標計算して判定

さて、ここからが本記事の本題!:upside_down:
コードは全てchartArea.onclick = (e) => {内にかかれるものとして読んでください。

座標計算の流れとしては今回は
1. クリック位置を取得
2. ラベルの数だけループを回し、
3. ラベルがどこに描画されているかを計算 (これが大変)
4. クリック位置がラベルの範囲か計算・判定
5. ラベルをクリックしたのなら目的の処理を行う
6. ループを抜ける
となっています。

1. クリック位置を取得

// canvas内のクリックしたx,y座標
const mouseX = e.offsetX;
const mouseY = e.offsetY;

2. ラベルがどこに描画されているかを計算
new Chart()した際の戻り値であるchartObjectにラベルの座標はなので、
marginやpadding、fontSizeの設定値やラベルの文字数から計算する他ないです。
しかもラベルは1つじゃないのでループを回して全てを計算する必要があるという分けです。

:star: 前準備
まず目盛り文字の高さ一番外側の目盛り文字のレーダーチャート中心からの距離が必要ですので取得しておきます。
ラベルはこの目盛りより外に表示されますからね。
スクリーンショット 2021-02-28 16.15.23.png

const helpers = Chart.helpers; // 
const scale = radar.scale; // 軸
const opts = scale.options; // チャート全体の設定
const tickOpts = opts.ticks; // 目盛り

// 目盛りの高さ
// 基本はfontsize + 5px
const tickBackdropHeight =
  tickOpts.display && opts.display // chartの設定で optionで目盛りが表示されているか
    ? helpers.valueOrDefault(
        tickOpts.fontSize,
        Chart.defaults.global.defaultFontSize
      ) + 5
    : 0;

// 目盛りの中央からの距離
const outerDistance = scale.getDistanceFromCenterForValue(
  opts.ticks.reverse ? scale.min : scale.max
);

Chart.helpersについてはバージョン違いだが、こちらを参照。
scaleが持っているmethodは同じくバージョン違いだがこちらが参考になるっぽい。

:star: ラベルの位置を計算
ではループを回してラベル類の位置を特定していきましょう。

// ポイントラベル分ループ
for (var i = 0; i < scale.pointLabels.length; i++) {
  // 軸ラベルによってチャート上部に余分な空白があるので削除
  const extra = i === 0 ? tickBackdropHeight / 2 : 0;
  const pointLabelPosition = scale.getPointPosition(
    i,
    outerDistance + extra + 5
  );

  // ラベルサイズ情報を取得
  const plSize = scale._pointLabelSizes[i];

  // ラベルのtextAlignを取得する(ラベルの描画位置が変わるので)
  const angleRadians = scale.getIndexAngle(i);
  const angle = helpers.toDegrees(angleRadians);
  let textAlign = "right";
  if (angle == 0 || angle == 180) {
    textAlign = "center";
  } else if (angle < 180) {
    textAlign = "left";
  }

  // ラベルの垂直オフセット位置を取得
  // drawPointLabels()から取得し計算
  let verticalTextOffset = 0;
  if (angle === 90 || angle === 270) {
    verticalTextOffset = plSize.h / 2;
  } else if (angle > 270 || angle < 90) {
    verticalTextOffset = plSize.h;
  }

  // 対象のラベルの範囲の位置を計算(padding含み)
  const labelTop = pointLabelPosition.y - verticalTextOffset - labelPadding;
  const labelHeight = 2 * labelPadding + plSize.h;
  const labelBottom = labelTop + labelHeight;

  const labelWidth = plSize.w + 2 * labelPadding;
  let labelLeft;
  switch (textAlign) {
    case "center":
      labelLeft = pointLabelPosition.x - labelWidth / 2;
      break;
    case "left":
      labelLeft = pointLabelPosition.x - labelPadding;
      break;
    case "right":
      labelLeft = pointLabelPosition.x - labelWidth + labelPadding;
      break;
    default:
      console.warn("WARNING: unknown textAlign " + textAlign);
  }
  let labelRight = labelLeft + labelWidth;

  // ...以降クリックされたか判定などなど...

} // end of for

まんま貼り付けただけですが、textAlignを自力で取得したりするところがみそですね。
CSS的なのtextAlignではなく、レーダーチャートのの外周、左右上下のどこに描画されているかを計算しています。

5. ラベルをクリックしたのなら目的の処理を行う
6. ループを抜ける
for文の最後に以下で判定して完成でっす! :tada:

  // クリックされた範囲がラベルか判定
  if (
    mouseX >= labelLeft &&
    mouseX <= labelRight &&
    mouseY <= labelBottom &&
    mouseY >= labelTop
  ) {
    // したい処理をここに記載!!
    // なにか値が必要な場合もここまでで取得できていそうなのでなんでもできるかと!
    break; //ラベルクリックがわかったので処理を終了
  }

以上で目的は達成かと思います。

最後に

本当に以下の回答に Thanks! :bow:
https://stackoverflow.com/a/58296237

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

[HTML5+JavaScript]入れやすい住所入力欄を、欄を作りながら考える

本記事は、自分サイトのこの記事と同一内容です。

はじめに

様々なサイトで見かける住所入力欄。
今後フロントエンド系の仕事をすれば一度は関わることがあるのではと思いますので、入力しやすい住所入力欄を実際に実装しながら考えてみます。
なお今回は、ライブラリとしてBootstrap v5.0.0 を使用します。
ソースコード:HagiAyato.github.io/practice/address/
動作確認できるページ:入れやすい住所入力欄を、欄を作りながら考える

1.一番シンプルなフォーム

住所をテキストボックスに入れるだけ。
開発初心者が作りそうなフォームで、色々と住所を入れにくいです。
image.png

問題点:

  • 入力欄・入力欄に入れる文字が長くなる
  • 住所は住所でも、何を入れればいいかが分からない
  • 住所を空欄にしたまま"決定"される可能性がある

2.入力欄を分けたフォーム

住所入力欄を細分化し、何を入れればいいかを分かるようにしました。
しかしこれだけでは入力を間違えてもなにも警告が出ず、間違えたまま"決定"処理を行えます。
これでは入れやすいか以前の問題。
image.png

解決した問題:

  • 入力欄・入力欄に入れる文字が長くなる
  • 住所は住所でも、何を入れればいいかが分からない

新たな問題・未解決の問題:

  • 住所を空欄にしたまま"決定"される可能性がある
  • 住所各項目の入力チェックをしていない(郵便番号の桁数・ハイフン、都道府県名・市区町村名)

3.入力チェック付きフォーム(HTML5)

HTML5のpattern属性による、入力チェックを実装しました。
また郵便番号については、"2."の段階でハイフン有り無しの記載が説明になかったので、追記を行いました。
誤り入力をなくす意味ではこれで十分ですが、今回は入れやすい住所入力欄を作るのが目的です。
できれば他所の入力ミスを自動修正したい…
《注意》HTML5非対応環境を考慮する場合、およびエラーメッセージをカスタマイズしたい場合は、JavaScript等でこれ以外の実装方法を採用する必要あり。
image.png

解決した問題:

  • 住所を空欄にしたまま"決定"される可能性がある
  • 住所各項目の入力チェックをしていない(郵便番号の桁数・ハイフン、都道府県名・市区町村名)

新たな問題・未解決の問題:

  • 郵便番号からの住所自動入力機能が欲しい
  • 郵便番号の数字入力を、自動で半角・ハイフン抜きにしたい
  • 郵便番号の英数字・片仮名入力を、自動で全角にしたい

4.入力補助付きフォーム

AjaxとAPIにより、郵便番号による住所自動入力を実装しました。
また、JavaScriptで、入力補助を実装し、英数字・片仮名の半角⇔全角揃えを実装しました。
ここまでくれば、よくサイトにある入力しやすい住所入力欄と同じ機能と言ったところでしょうか。
image.png

解決した問題:

  • 郵便番号からの住所自動入力機能が欲しい
  • 郵便番号の数字入力を、自動で半角・ハイフン抜きにしたい
  • 郵便番号の英数字・片仮名入力を、自動で全角にしたい

実装の結果

結論:入れやすい住所入力欄は、単に住所を入力するだけでなくその誤りチェック・自動入力・誤り修正を行えるものである。

ただここまでくると、1つの入力欄だけでも実装に必要なコード行数が多くなります。
(今回はHTML:63行+JavaScript:107行=170行)
一つ一つ住所入力欄を実装する際にこれだけのコードを書くのは手間がかかるので、
コードをライブラリのように保存して、使いまわしができるようにすべきだと感じました。

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

WebでQRコードリーダー

目次

1.はじめに
2.アプリの構造
3.ソース解説
4.あとがき

1.はじめに

…主に自分が見直すための記録です…

Web で QR コードリーダーを実装したので、その時のメモ。

ライブラリ

jsQR というライブラリを使いました。
GitHubから jsQR をクローンするか、npm でインストールします。

node
npm install jsqr --save
QRreader.js
import jsQR from "jsqr";

// CommonJS require
const jsQR = require("jsqr");

jsQR(...);

QR を読み込むためにデバイスのカメラにアクセスします。そのため、https もしくは localhost 環境下でのみ稼働します。
node.js とか使って localhost サーバを立ててもいいのですが、たぶん一番簡単なのは Chrome の拡張機能の Web Server for Chorme です。
Web Server for Chorme

今回のアプリも Web Server for Chorme で動かす想定で作っています。

2.アプリの構造

ファイル 簡単な解説
app.css css
index.html メインで表示されるページ。
index.js 画面関連を担当する JS ファイル
jsQR.min.js jsQR ライブラリを minfy したやつ
QRreader.js QR リーダーを動かす JS ファイル

3.ソース解説

今回はボタンを押したらリーダーが始動するようにします。
アクセスと同時に起動させることもできますが、ポンコツPCだと常にビデオを起動させることに不安があったのでw
そのため、リーダーを停止させる stopボタンも用意しておきます。

起動ボタンは、一度押下すると再読み込みボタンになります。

index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>QR Reader test</title>
        <link rel="stylesheet" href="app.css">
        <script src="jsQR.min.js"></script>
    </head>
    <body>
        <h1>QR Reader test</h1>
        <p>URL情報を取得して表示します</p>
        <button id="btn" class="start">
            <span id="startBtn">start QRreader</span>
            <span id="restartBtn">再読み込み</span>
        </button>
        <button id="stopBtn">stop</button>
        <div id='loadingMessage'> starボタンを押すとReaderが起動します</div>
        <video id='video' hidden></video>
        <canvas id="canvas" hidden></canvas>
        <div id="output" hidden>
            <div id="outputMessage">No QR code detected.</div>
            <div hidden>
                <b>URL:</b>
                <span id="outputData"></span>
            </div>
        </div>
        <script src="QRreader.js"></script>
        <script src="index.js"></script>
    </body>
</html>
app.css
body {
    font-family: "Ropa Sans", sans-serif;
    color: #333;
    max-width: 640px;
    margin: 0 auto;
    position: relative;
}

h1 {
    margin: 10px 0;
    font-size: 40px;
}

#loadingMessage {
    text-align: center;
    padding: 40px;
    background-color: #eee;
}

#canvas,
#video {
    width: 100%;
}

#output {
    margin-top: 20px;
    background: #eee;
    padding: 10px;
    padding-bottom: 0;
}

#output div {
    padding-bottom: 10px;
    word-wrap: break-word;
}

#startBtn {
    display: block;
}

#restartBtn {
    display: none;
}

続いて QRreader.js です。
QR 読み取りに成功すると video から Canvas に切り替わるようにしました。

QRreader.js
const video = document.createElement('video');
const canvasElement = document.getElementById('canvas');
const canvas = canvasElement.getContext('2d');
const loadingMessage = document.getElementById('loadingMessage');
const outputContainer = document.getElementById('output');
const outputMessage = document.getElementById('outputMessage');
const outputData = document.getElementById('outputData');



const startQR = () => {
    navigator.mediaDevices.getUserMedia({
        video: {
            audio: false,
            facingMode: 'environment'//'user' でインカメを使う
        }
    }).then((stream) => {
        video.srcObject = stream;
        video.setAttribute('playsinline', true);
        video.play();
        requestAnimationFrame(tick);
    }).catch((err) => {
        alert(err.message)
    })
};


//QRの解析
function tick() {
    loadingMessage.innerHTML = 'Loading video...';
    if (video.readyState === video.HAVE_ENOUGH_DATA) {
        loadingMessage.hidden = true;
        canvasElement.hidden = false;
        outputContainer.hidden = false;

        canvasElement.height = video.videoHeight;
        canvasElement.width = video.videoWidth;
        canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height);
        const imageData = canvas.getImageData(0, 0, canvasElement.width, canvasElement.height);
        const code = jsQR(imageData.data, imageData.width, imageData.height, {
            inversionAttempts: 'dontInvert',
        });

        //QRが読み込めた時の挙動
        if (code) {
            drawLine(code.location.topLeftCorner, code.location.topRightCorner, "#FF3B58");
            drawLine(code.location.topRightCorner, code.location.bottomRightCorner, "#FF3B58");
            drawLine(code.location.bottomRightCorner, code.location.bottomLeftCorner, "#FF3B58");
            drawLine(code.location.bottomLeftCorner, code.location.topLeftCorner, "#FF3B58");
            outputMessage.hidden = true;
            outputData.parentElement.hidden = false;
            //読み取ったURLをリンクにして表示
            outputData.innerHTML = `<a href=${code.data}>${code.data}</a>`;
            //videoをcanvasに
            video.style.display = 'none';
            video.pause();
        } else {
            outputMessage.hidden = false;
            outputData.parentElement.hidden = true;
        }
    }
    requestAnimationFrame(tick);
};


//QRを囲むライン
const drawLine = (begin, end, color) => {
    canvas.beginPath();
    canvas.moveTo(begin.x, begin.y);
    canvas.lineTo(end.x, end.y);
    canvas.lineWidth = 4;
    canvas.strokeStyle = color;
    canvas.stroke();
};

//off
const videoOff = () => {
    video.pause();
    video.src = "";
    video.srcObject.getTracks()[0].stop();
};

最後に、 index.js を記述します。
index.js は主にボタン押下時の動きを担当します。

index.js
const btn = document.getElementById('btn');
const stopBtn = document.getElementById('stopBtn');


const changeBtn = () => {
    const btnStart = document.getElementById('startBtn');
    const btnRestart = document.getElementById('restartBtn');
    const toggleBtn = document.getElementById('btn');
    const toggleBtnClass = toggleBtn.getAttribute('class');
    //startボタンと再読み込みボタンを切り替える
    if (toggleBtnClass === 'start') {
        toggleBtn.classList.remove('start');
        toggleBtn.classList.add('restart');
        btnStart.style.display = 'none';
        btnRestart.style.display = 'block';
        startQR();
    } else if (toggleBtnClass === 'restart') {
        startQR();
        outputData.innerText = 'No QR code detected.';
    }
};

btn.addEventListener('click', () => {
    changeBtn();
});

stopBtn.addEventListener('click', () => {
    videoOff();
});

QR.png

4.あとがき

最近はWebで動くビデオ会議システムなんかも出てきて、ああいうのはネイティブアプリの領分だと思っていたのに…と戦慄してます。
JSのプログラマーの需要が増えるのはうれしい反面、学ぶべきことがどんどん増えていきますね…。

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

【JavaScript】特定のvalueが合致するオブジェクト同士で配列

備忘録。
目的:場所ごとにデータを分けて使用したい。

前提

勉強に付随するデータを見える化するアプリを作っているため、

  • 日にち
  • 場所
  • 集中度
  • 勉強内容
  • 勉強時間

入力した回数分オブジェクトを複数生成。 posts = [] に格納。

※一例 (axiosを利用しているため型の宣言付)

const obj = {
nowTime: {stringValue: "2021年2月26日19時47分"},
studyArea: {stringValue: "マクドナルド"},
studyDensity: {stringValue: "普通"},
studyContent: {stringValue: "JavaScript"},
studyTime: {integerValue: "3"} //hour
}

const posts = [{obj}, {obj}, {obj}...];

場所を被りなしで抽出して配列にしている。

const areas = ['マック', 'イオン', 'タリーズ', 'サンマルク'];

解答

const separateArea = [];

const separate = function() {
      let sortObj = [];
      for (let i = 0; i < areas.length; i++) {
        posts.forEach(post => {
          // index→0
          if (post.studyArea.stringValue.indexOf(areas[i]) !== -1) {
            sortObj.push(post);
          }
        });
        separateArea.push(sortObj);
        sortObj = [];
      }
    }

解説

  1. 分けたオブジェクトを最終的に格納する separateArea を定義。
  2. 同値のstudyAreaをもつオブジェクトを配列にするため sortObj を定義。
  3. postsの要素(post)にareasのi番目と同じ名前のstudyAreaがあればsortObjにそのオブジェクトをpush.これをpostsの要素数分行う。(forEach, indexOf)
  4. studyAreaが一致するオブジェクトを洗い出したらsortObjをseparateAreaにpush.
  5. sortObjを空に。
  6. areasの要素数分ループ。

ポイント

indexOf

indexOf() メソッドは、呼び出す String オブジェクト中で、 fromIndex から検索を始め、指定された値が最初に現れたインデックスを返します。値が見つからない場合は -1 を返します。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf

特定のvalueが合致するオブジェクト同士で配列を作りたかったためvalue同士を比べるために使用。
もっといい方法があれば教えてください。

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

モダンJavascript基礎 スプレッド構文の世界で一番分かり易い解説

はじめに

こんにちは! プログラミング学習中のタイソンです!
今回はES6で追加されたスプレッド構文について学んだことを、分かり易さ重視で解説していこうと思います。わかりにくいところ、間違っているところなどがございましたら、お気軽にご指摘ください!

スプレッド構文とは

「...」
このようなドットが3つ並んだコードを皆さんは見たことがあるでしょうか。この3つのドットをスプレッド構文と言います。このスプレッド構文を一言で表すと

順番に処理して展開する

というものです。

この言葉だけではあんまりピンと来ないですよね。
大丈夫です!コードでわかりやすく解説していきます!

配列の展開

const arr1 = [1,2];
console.log(arr1);

このような配列があるとします。この配列arr1の中身をconsole.log()で見てみましょう。
arr1の中身
もちろんこのような結果になります。次はスプレッド構文を使って配列の中身を見てみましょう。

const arr1 = [1,2];
//スプレッド構文で配列の中身をみる↓
console.log(...arr1);

arr2の中身
先ほどはconsole.logの結果が配列だったのに対して、今回は実際の1と2という値になっていることがわかります。

このようにドットを3つ書いてその後に配列を指定すると、配列の中身を順番に処理して展開します。

ちょっと複雑でわかりにくいですよね。もう少し分かり易い例で解説していきます。

//配列の作成↓
const arr1 = [1,2];
//アロー関数の作成↓
const sumFunc = (num1, num2) => console.log(num1 + num2);
//実引数に配列の値を指定↓
sumFunc(arr1[0], arr1[1]);

このような配列とアロー関数を作成して、コンソールで結果をみてみましょう。

コンソールの結果
このような結果になりました。
次は同じ式にスプレッド構文を使って引数を渡していこうと思います。

const arr1 = [1,2];
const sumFunc = (num1, num2) => console.log(num1 + num2);
sumFunc(...arr1);

結果はこのようになります。
コンソールの結果
先ほどと同じ結果になりましたね。先ほどの記述と、このスプレッド関数を使った例は同じ動きをしているということになります。

配列に対してドットが3つあったら、
順番に処理して展開
と頭の中で変換してください。

配列をひとつにまとめる

前の章はスプレッド構文の配列の展開についてでしたが、次は配列をひとつにまとめるというスプレッド構文の使い方について解説しようかと思います。

//配列を用意↓
const arr2 = [1, 2, 3, 4, 5];
//配列をスプレッド構文を使って分割代入↓
const [num1, num2, ...arr3] = arr2;

このような配列を用意し、分割代入していきます。
console.log()でnum1とnum2の値の中身を見てみましょう。

分割代入が何かわからない方はこちらの記事を読んでください。↓

const arr2 = [1, 2, 3, 4, 5];
const [num1, num2, ...arr3] = arr2;

console.log(num1);
console.log(num2);

コンソール2.png
このような結果になりました。次にarr3の中身をconsole.logで見てみましょう。

コンソール2.png

1と2以外の数字が配列としてまとめられていました。このようにスプレッド構文は配列をまとめることもできるのです。

配列のコピーや結合

次は配列のコピーや結合について解説していきます。とても大事なところなのでしっかり勉強していきましょう。

const arr4 = [10, 20];

const arr5 = [30, 40];

まずこのような2つの配列を用意します。そして新しくarr6という配列をスプレッド構文で用意します。

const arr4 = [10, 20];

const arr5 = [30, 40];

const arr6 = [...arr4];

console.log(arr6)で、配列arr6の中身を見てみましょう。
コンソール2.png
このようにarr4の中身がコピーされました。このようにスプレッド構文は配列のコピーにも使うことができます。

次にこれの応用として、スプレッド構文の結合について見ていこうと思います。先ほどのコードを以下のように変更します。

const arr4 = [10,20];

const arr5 = [30,40];

const arr6 = [...arr4, ...arr5];
console.log(arr6);

この配列arr6の中身はどうなっているのでしょうか。console.logで見てみましょう。
コンソール2.png
配列arr4とarr5の中身が結合されましたね。
このようにスプレッド構文を使うと配列の結合も容易に行うことができます。

参照渡しについて

しかし配列のコピーをするだけなら、以下のように

「配列を=でそのままコピーすればいいじゃん!」

と思う人がいるかもしれません。

const arr4 = [10,20];
//arr4をコピーしたarr8を定義
const arr8 = arr4;

console.log(arr8)の中身を見てみましょう。
コンソール2.png
このようにコピーができていますね。

しかしこれには問題があります。

参照が引き継がれてしまっているのです。
以下の例を見てみましょう。

const arr4 = [10,20];

const arr8 = arr4;
//arr4をコピーしたarr8の最初の値を変更する↓
arr8[0] = 100;

console.log(arr8);
console.log(arr4);

console.log()でarr8とarr4の中身を見て見ましょう。
コンソール2.png
arr8の値だけを100に変えたつもりなのに、元のarr4の値も100に変わってしまっています。

このようなことが起こってしまうので、配列のコピーをする際はスプレッド構文を使いましょう。スプレッド構文で同じことをやってみましょう。

const arr4 = [10,20];

const arr8 = [...arr4];

arr8[0] = 100;

console.log(arr8);
console.log(arr4);

console.logでarr8とarr4の中身を見てみましょう。
コンソール2.png
arr4の値を変えずに、arr8の値だけを変えることができました。

なぜこのようなことが起こってしまうのかは以下の記事を参考にしてください。

最後に

いかがだったでしょうか。スプレッド構文はjavascriptだけでなく、Reactなどでも使っていくのでしっかり理解を深めておきましょう。見ていただきありがとうございました。

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

JavaScriptでPDFのファイルサイズを取得して表示させる方法

実務で使う機会があったので、忘れないように投稿。

html
<a href="" class="js-pdfSize" target="_blank" data-filepath=""><p>PDFダウンロード<span class="file_size">0.0MB</span></p></a>
js
jQuery(function () {

jQuery.each(jQuery(".js-pdfSize"), function () {
  b(jQuery(this))
});

function b(d) {
  var h = "-";
  var g = d.data("filepath");
  if (!g) {
    d.find(".file_size").html(h);
    return
  }
  var e = g.split(".");
  if (e[e.length - 1].toLowerCase() !== "pdf") {
    d.find(".file_size").html(h);
    return
  }
  var f = $.ajax({
    type: "HEAD",
    url: g,
    dataType: "text"
  }).done(function () {
    var i = f.getResponseHeader("Content-Length");
    d.find(".file_size").html(c(i))
  }).fail(function () {
    d.find(".file_size").html(h)
  })
}

function c(d) {
  if (d < 1024) {
    return d + "B"
  } else {
    d = a(d)
  }
  if (d < 1024) {
    return d + "KB"
  } else {
    d = a(d)
  }
  if (d < 1024) {
    return d + "MB"
  } else {
    return a(d) + "GB"
  }
}

function a(d) {
  return (Math.ceil((d * 10) / 1024)) / 10
}
});

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

useRefの使い方よくわからないから学んでみる

概要

実務にてReactHooksを使うことが多いので、
「今こそhooksについて学びを深めるべきだ!!」
ということでuseRefについて学習した内容を
共有すべく記事にまとめます〜!

useRefとは

useRef は元々あった createRef の hooks 版です。その名の通り、DOMに対する参照を持つために使われるのが主な目的です。

とのことです。
ここで僕が気になったのは「createRefって?」

createRefとは、v16.3.0以上で利用可能なメソッドです。
ref属性に渡して、対象のDOMを参照できる、みたいなことです。
⬇︎例

// classコンポーネント内
  constructor(props) {
    this.ref = React.createRef();
  }
  render() {
    <div
      ref={this.ref}
    >
      <span>text</span>
      <span>text</span>
    </div>
  }
  componentDidMount() {
    console.log(this.ref.current.clildNodes);
    // output: NodeList(2) [span, span]
  }

ここにおけるReact.createRef()→useRef()ってことです。
つまり、書き換えると以下のようになります。
⬇︎例

// 関数コンポーネント内
  const ref = useRef();
  useEffect(() => {
    console.log(ref.current.childNodes);
    // output: NodeList(2) [span, span]
  });

  return (
    <div
      ref={ref}
    >
      <span>text</span>
      <span>text</span>
    </div>
  );

useEffectの部分は、componentDidMountに対応しており、マウント時だと思っていただければOKです!

詳細

useRefは、コンポーネントがマウントされる瞬間からアンマウントされるまで存在し続けます。
つまり、レンダーが起こってもアンマウントされてなければ前の値が入ったままになる、ということです。

const list1 = [];
const list2 = [];
// 関数コンポーネント内
  const [count, setCount] = useState(0);
  const onClick = () => {
    setCount(c => c + 1);
  };

  const obj1 = {};
  list1.push(obj1);
  if (list1.length >= 2) {
    console.log(list1[list1.length - 2] === list1[list1.length - 1]); 
    // これは、現在のobj1と1つ前のレンダーのobj1を比べています。  output: false
  }

  const obj2 = useRef();
  list2.push(obj2);
  if (list2.length >= 2) {
    console.log(list2[list2.length - 2] === list2[list2.length - 1]); 
    // これは、現在のobj2と1つ前のレンダーのobj2を比べています。  output: true
  }

  return (
    <div
      ref={ref}
    >
      <button type="button" onClick={onClick}>
        count up
      </button>
      {count}
    </div>
  );

この例のように、Ref以外の一般的なオブジェクトはレンダーごとに「新しいオブジェクトを生成している」ことがわかります。

逆に、useRefはレンダーされても「同じオブジェクト」を使い続けています。

つまりは、useRefは「レンダーと値が一対一になっていない」ということです。refの値が書き変わってもレンダーは起きないし、レンダーが起こってもrefの値は書き変わらない。
ここがuseRefの特徴でしょう。

しかし、この特徴はReactのコンセプトから外れてしまってるので非推奨というか多用すべきではないようです。

とはいえ使っていい例もあるようで、
それは、

  • テキストの選択、フォームへのフォーカスもしくはメディア(動画、音声)の再生の制御
  • アニメーションを発火させる場合
  • サードパーティのDOMコンポーネントを組み込む場合

と公式ドキュメントにも記載されてますね。
だからこそ、しっかりと理解して使うことができれば便利に開発できるようになるでしょう!

まとめ

今回はuseRefについて学んでみました。
僕からしたらかなり便利だなと感じます。

しかし、そういったものこそしっかりと理解し、特徴を踏まえた上で使用することが大切だなと。

基本的にはこの記事を書く上で理解できたのでどんどん使って慣れようかなと思います!
ガンガン成長していきましょう!!

参考資料

https://ja.reactjs.org/docs/hooks-reference.html#useref
https://qiita.com/seya/items/6bbfa3f9d489809ccb2c
https://numb86-tech.hatenablog.com/entry/2019/12/05/111342

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

【webpack】file-loaderで画像などの出力先を条件分岐させる方法

webpackでfile-loaderとhtml-loaderを使用しており、バンドルする際に画像と動画でoutputPathpublickPathを分岐させたかった。
それを実現するためにやったことのまとめ。

※ webpackやローダーのインストール方法や説明は特にしていません。

実行環境

  • macOS Catalina v10.15.7
  • node.js v12.18.1
  • npm v6.14.5

webpackなどのバージョン

  • webpack v4.43.0
  • webpack-cli v3.3.11
  • file-loader v6.0.0
  • html-loader v1.1.0
  • html-webpack-plugin v4.5.1

やりたいこと

file-uploaderとhtml-loaderを使用して、htmlファイルで使用する画像もwebpackでバンドルしている。
バンドル前は画像と動画でディレクトリを分けていたが、バンドル後には一つのディレクトリにまとめて出力されていた。
これを、バンドル前のディレクトリ構成と同じ状態で出力したい。

これを
webpack-img01.jpeg

こうしたい
webpack-img02.jpeg

ディレクトリ構成

以下のように出力したい

.
├── package.json
├── public // 出力先
│   ├── index.html
│   ├── js
│   │   └── bundle.js
│   ├──images
│   │   └── 画像ファイル
│   └──videos
│        └─ 動画ファイル
│
├── src // 編集ファイルのディレクトリ
│   ├── index.html
│   ├── js
│   │   └── app.js
│   ├──images
│   │   └── 画像ファイル
│   └──videos
│        └─ 動画ファイル
└── webpack.config.js

解決法

1.当初のwebpack設定ファイル

webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // プラグイン読み込み

module.exports = {
    mode: 'development',
    entry: './src/js/app.js', // エントリーポイント
    output: { // 出力設定
        path: path.resolve(__dirname, 'public'), // 絶対パス
        filename: 'js/[name].[contenthash].bundle.js', // 出力するファイル名
    },
    module: { // ローダー設定
        rules: [
            {
                test: /\.(jpe?g|gif|png|svg|mp4)$/, // 処理対象とするファイルの指定
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[name].[hash].[ext]',
                            outputPath: 'images', // 出力先
                            publicPath: './images' // htmlから読み込まれる際のパス
                        },
                    },
                    'image-webpack-loader',
                ]
            },
            {
                test: /\.html$/, // 処理対象とするファイルの指定
                loader: 'html-loader',
            },
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html', // テンプレートとなるHTMLを指定(画像バンドルのため)
        }),
    ]
}

file-loaderの設定でoutputPath: 'images'としているので、出力するとimagesディレクトリに画像ファイルも動画ファイルもまとまって出力される。これを画像はimagesディレクトリに、動画はvideosディレクトリに分岐させることが今回の目標。

また、publicPath: './images'としているので、htmlに挿入される際に、このままでは画像ファイルや動画ファイルへのパスが./images/ファイル名となってしまうので、仮にimagesvideosディレクトリに出力を分岐できたとしても、videos配下の動画ファイルを読み込目なくなるので、publicPathも調整する必要がある。

2.outputPathとpublicPathの分岐

webpackの公式ドキュメントのfile-loaderについてのページを見ると、outputPathpuclickPathのとり得る値は、Type: String|Functionとなっているので、関数を使用できることがわかる。
なので、outputPathpublicPathの値には、対象のファイルが置かれているディレクトリに応じて、出力先を分ける処理を追加すればよい。

イメージ

options: {
    name: '[name].[hash].[ext]',
    outputPath: function() {
        if (処理する画像がimagesディレクトリにある) {
       return 'images'
        } else {
            return 'videos'
        }
    },
    publicPath: function() {
        if (処理する画像がimagesディレクトリにある) {
       return './images'
        } else {
            return './videos'
        }
    },
},

2.修正後のwebpack設定ファイル

webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); // プラグイン読み込み

module.exports = {
    mode: 'development',
    entry: './src/js/app.js', // エントリーポイント
    output: { // 出力設定
        path: path.resolve(__dirname, 'public'), // 絶対パス
        filename: 'js/[name].[contenthash].bundle.js', // 出力するファイル名
    },
    module: { // ローダー設定
        rules: [
            {
                test: /\.(jpe?g|gif|png|svg|mp4)$/, // 処理対象とするファイルの指定
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[name].[hash].[ext]',
                            outputPath:(url, resourcePath) => {
                   // 処理するファイルの絶対パスに「images」にマッチする文字列があるか判定
                                if (/images/.test(resourcePath)) { 
                       return 'images/' + url;
                                } else {
                                    return 'videos/' + url;
                                }
                            },
                            publicPath:(url, resourcePath) => {
                   // 処理するファイルの絶対パスに「images」にマッチする文字列があるか判定
                                if (/images/.test(resourcePath)) {
                       return './images/' + url;
                                } else {
                                    return './videos/' + url;
                                }
                            },
                        },
                    },
                    'image-webpack-loader',
                ]
            },
            {
                test: /\.html$/, // 処理対象とするファイルの指定
                loader: 'html-loader',
            },
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/index.html', // テンプレートになるHTML
        }),
    ]
}

ドキュメントに従って、outputPathpublickPathの値に関数を記述して、if文でoutputPathとpublicPathを分岐させることができた。
注意点としては、デフォルトでoutputPathやpabulicPathを指定する際には、ディレクトリ名のみを設定すればよいが、今回の場合は、ファイル名まで指定しないと正しく処理されなかった。

ここでの関数は、url, resourcePath, contextと3つの引数を取ることができる。

実際に引数を確認してみたところ

  • url => ファイル名
  • resourcePath => ファイルまでの絶対パス
  • context => プロジェクトのディレクトリまでの絶対パス

という内容が渡ってきていた。
この引数を駆使すれば、他にも目的に応じた処理をすることができそう。

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

何でJavaScriptはsingleThreadを選んだんだろう。

疑問を抱いた時点はtypeScriptを勉強したところ読んでしまったある文章。

 ジャヴァスクリプトは単一スレッド(single-thread)で動作するため、できる限りreadFileSyncのような同期APIは使用を控えましょう。

あれれ。それではJAVAのマルティスレッドとはどう違うのだろ。


まずはjavaScriptから呼出があった場合を流れを探ってみましょう。

console.log('a');
setTimeout(() => console.log('c'));
console.log('b');![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1070645/c74924d9-179d-ffa0-df6e-7be42ecd3f57.png)

結果はアルファベットから予想野通り "a, "b", "c"

この呼出を始まりに行われる環境を見てみよう。
image.png

  1. 初めにconsole.log('a'); → callstack に入ります。(FLOW)
  2. コード実行print('a')してcallstackから離れます(POP)
  3. setTimeout(() => console.log('c'));がcallstack に入ります。(FLOW)
  4. setTimeoutはJSにない処理のため実行を試したが実行失敗。
  5. callstackから離れ(POP)、該当処理を探すためWEB APIに入ります(FLOW)
  6. callstackが空になったのでconsole.log('b')が入ります(FLOW)
  7. コード実行print('b'してcallstackから離れます(POP)
  8. web api実行結果がcallback queueに格納されていたらevent loopはcallstackに移す。(FLOW)
  9. コード実行print('c')してcallstackから離れます(POP)
( *event loop: call stack, web api, callback queue を回しながらstackからweb apiに移したり、リターンをcallback queueからcall stackに移します)

それではどうしでsingle threadを使うのでしょう。まずはjavaのmulti threadを見てみましょう。

multi threadの強さはマルチ作業が可能にすること。例え音楽を聴きながら(作業①)、コーディングをしること(作業②)。各スレッドはメモリを割り当てられるのでエラー発生にも自分の領域以外を影響を及ぼさないです。
multi threadの困ることは同時性。各違うスレッドが同時に同じ変数の値を変更した場合などで意図してなかった処理が発生しかねません。そのため、スレッドのスケジューリングは気にしないといけなません。

javascriptのsingle threadは当たり前だがスレッドのスケジューリングを気にすることはなく、一スレッドでAYSNCなどの同期APIなどで内部スケジューリングだけコントロールすればより高い反応性、同時性を保つことができます。また内部スケジューリングでマルチ作業も可能にさせます。


参照資料
https://www.geeksforgeeks.org/why-javascript-is-a-single-thread-language-that-can-be-non-blocking/

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

Stripe Element でカード番号の入力フィールドを縦に並べたい

背景

  • Stripe.js を用いてカード番号を入力するフォームを生成する時、デフォルトではカード番号・有効期限・CVCの入力フィールドが横並びになってしまう。
  • デザイン上、フィールドを縦に並べたい時にどのようにフォームを生成したら良いのか調べた。

完成イメージ

Online_Salon.png

フォームの生成

  • まずフォームの生成時は stripe.elements() の返り値からカード番号・有効期限・CVCを入力する各フィールドを create する。
  • この時、 elements.create() を実行する時点で document 上に対象として selector で指定した element が存在している状態であることを確認すること。
    • 今回の場合は #card-number-input #card-expiry-input #card-cvc-input の3つを用意しておく。
    • 雑に <div id="card-number-input" /> のような div を置いておくだけで良い。
    • React 等の document を動的に操作するライブラリを使う場合は、レンダリングが完了してから実行すること。
import { loadStripe, StripeElementStyle } from '@stripe/stripe-js';

const style: StripeElementStyle = {
  base: {
    fontSize: '16px',
  },
};

const stripe = await loadStripe('pk_test_TYooMQauvdEDq54NiTphI7jx');
const elements = stripe.elements();

const cardNumberElement = elements.create('cardNumber', { style });
const cardExpiryElement = elements.create('cardExpiry', { style });
const cardCvcElement = elements.create('cardCvc', { style });

cardNumberElement.mount('#card-number-input');
cardExpiryElement.mount('#card-expiry-input');
cardCvcElement.mount('#card-cvc-input');

return cardNumberElement;

これで入力フィールドが生成される。

フォームの送信

  • ユーザーが入力を完了してカードトークンを取得したくなった時は、先ほどフォームの生成に使った cardNumberElementstripe.createToken() の引数に渡せばOKです。
await stripe.createToken(cardNumberElement);

これでトークンを取得できるので、煮るなり焼くなり好きにできます。

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

【忘備録】JavaScriptできちんと日付を取得する

【忘備録】JavaScriptできちんと日付を取得する

getDateに +1するだけでは 月をまたぐ際に、32日となったりする

【正】

let date = new Date();
date.setDate(date.getDate() + 1);

【使用例】

今日から1週間を取得

let date = new Date();

for(let i = 0; i<7; i++){
date.setDate(date.getDate() + 1);
console.log(date.getDate());
}

一昨日

let date = new Date();
date.setDate(date.getDate() - 2);  

明後日

let date = new Date();
date.setDate(date.getDate() + 2);  

1週間後

let date = new Date();
date.setDate(date.getDate() + 7);  
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Node.js】promiseの使い方

プログラミング勉強日記

2021年2月28日

基本的な書き方

 promise処理を作るには任意の関数の中でnew Promise()を返すのが基本となる。

return new Promise(resolve) {
    // 処理を記述する
}
具体例
function myFunction() {
  return new Promise(function (resolve) {
      resolve("Hello World");
  })
}

thenを使ったメソッドチェーン

 promiseによる非同期処理の結果を取得するにはthenを使ったメソッドチェーンを使用することができる。

// dataにpromiseの結果が格納されている
// resolve()に設定した文字列になる
myFunction().then(function(data) { console.log(data) })
実行結果
Hello World

promiseの並列処理

 allメソッドとraceメソッドを使った方法がある。
 allメソッドは、違うpromise処理が記述された関数をまとめて実行して、すべての結果が得られたタイミングでthenを実行できるようにする。なので、複数のpromise処理の結果をまとめて取得したい場合に向いてる。
 raceメソッドも複数のpromise処理を実行できるが、最初に結果が得られたpromiseの結果だけをthenメソッドで取得できる。

エラーハンドリング

 promiseの引数には結果を格納するresolveだけではなく、エラー情報を格納するrejectを利用できる。

function myFunction() {
  return new Promise(function (resolve, reject) {
      reject(new Error("エラーが発生しました"));
  })
}
myPromise()
  .then(function(data) {
    console.log(data);
  }, function(error) {
    console.log(error.message);
  })
実行結果
エラーが発生しました

参考文献

非同期処理:コールバック/Promise/Async Function
【node.js入門】Promiseによる非同期処理の使い方まとめ!

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

Next.jsとtailwindを使ってダークモードをサクッと実装する

初めに

最近、Next.jsTailwind CSSで作成した個人ブログにダークモード機能を追加したので、その備忘録として。

Tailwindのダークモードを有効化する

TailwindのV-2からダークモードが組み込まれており、ダークモード時に指定の色に切り替えてくれるようになりました。

ダークモードを有効化するのは簡単で、tailwind.config.jsdarkModeclassmediaを指定するだけです。mediaを指定した場合は、OSの設定に基づいて、自動でdarkモードに切り替えてくれますが、今回はユーザーがページ上でON/OFFの切り替えが出来るようにしたいので、classを指定して、ダークモードを有効化させます。

詳細はTailwindのドキュメントに記載されてます。

tailwind.config.js
module.exports = {
  purge: [],
  darkMode: 'class', //ダークモードを有効化する
  theme: {
    extend: {
      colors: {
        darkgrey: '#222831', //darkModeで使用したい色を拡張定義
      },
    },
  },
  variants: {
    extend: {
},
  },
  plugins: [],
};

next-themesをインストールする

次にボタンを押下した際に、htmlタグのclassに対してdarkをアクティブ/非アクティブにするための実装が必要になります。

htmlタグのclassにdarkを付与できるようにする必要がある
<html class="dark">

Tailwindのダークモードは、htmlタグのclassにdarkが付与されてる場合、以下のような記述で、ダークモード時の色を指定する事が出来ます。

dark
<div className="dark:bg-darkgrey dark:text-white">

ドキュメントには現在のモードをlocalStorageに保存する方法が紹介されていますが、ここでは、next-themesというライブラリを使って実装を進めます。

next-themes

next-themesをインストール
$ npm install next-themes
# or
$ yarn add next-themes

ライブラリをインストールしたら、_app.jsThemeProviderをimportします。
attributeにはclassを指定しましょう。(※Tailwindのダークモードを使用するには、この記述が必要です)

_app.tsx
import '../../styles/globals.css';
import '../../styles/common.css';
import { AppProps } from 'next/app';
import { ThemeProvider } from 'next-themes';

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider attribute="class">
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

export default MyApp;

ダークモードを切り替えるボタンを作成する

切り替えるボタンは以下のようにしました。
useTheme()フックを利用して、テーマの切り替えを制御します。

import { useTheme } from 'next-themes';

const { theme, setTheme } = useTheme();

// レンダー後かを判定
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);

<button
  aria-label="DarkModeToggle"
  type="button"
  className="p-3 h-12 w-12 order-2 md:order-3 absolute left-2/4 transform -translate-x-2/4 md:relative md:left-0"
  onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
  {mounted && (
    <>
      {theme === 'dark' ? (
        <Moon height={'25'} width={'25'} />
      ) : (
        <Sun height={'25'} width={'25'} />
      )}
    </>
  )}
</button>

後は、Tailwindのいつもの書き方でダークモード時の色を指定してあげるだけです。

dark
<div className="relative mb-10 dark:bg-darkgrey">
...
</div>

以上、これでお手軽にダークモードを実装することが出来ます。

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

JavaScriptの基本文法

デイトラWeb制作コース中級編DAY1の学び

【この記事に書いてあること】

 【学び】

1 console.log
文字を出力する

maiin.js
 console.log("こんにちは")

「こんにちは」

*半角で入力しないと反映されない!

2 エラー表示
スクリーンショット 2021-02-25 5.38.17.png
このエターが出たら、文法が間違っている!
緑の部分は、エラーの説明(今回のは、「”」は使えないと書いている)

3 数字の計算
算数で習う数式と一緒

*あまりを出す計算のみ、「%」を使う

例:
スクリーンショット 2021-02-25 5.51.45.png

4 変数
データにラベルをつけるイメージ
「let 変数名=〇〇〇」で入力する

スクリーンショット 2021-02-25 6.30.55.png

・変数なので、値を変えることもできる
スクリーンショット 2021-02-25 6.33.20.png

・変数を使って定義もできる
スクリーンショット 2021-02-25 6.36.14.png

5 変数名のルール
・アルファベットと数字のみ
・小文字
・数字を先頭に置かない

6 定数
変数と違い、値を変更することができない
「const 変数名=〇〇〇」で入力する

7 変数を使った文字の埋め込み
スクリーンショット 2021-02-25 6.44.15.png

↓ 打ち込むのが面倒くさい、、、、

スクリーンショット 2021-02-25 6.44.58.png

${変数名}」を使えば簡単に文字を埋め込むことができる!

8 関数
特定の処理をまとめたもの

ルール:function 関数名(引数){//処理を書く}

重要な概念

・引数:関数に値を渡す役割

・返り値:関数の出力のこと。 例:計算した結果を関数の外に伝える
    *返り値を指定するときは、returnを入力する
スクリーンショット 2021-02-28 5.37.20.png

9 関数とスコープ
 スコープ=変数や定数がどこまで使えるか

・<関数の中で定義された定数や変数は実行することができない>
スクリーンショット 2021-02-28 5.56.18.png

・<関数(function)の外で定義された定数や変数は、関数(functuon)の中で実行できる>
スクリーンショット 2021-02-28 5.59.05.png

10 条件分岐 【if文】
 〇〇の場合□□する → if(条件){//処理}
 *条件にはBooleanを使う(true or false) true = 表示する  false = 表示しない
スクリーンショット 2021-02-28 6.27.24.png

11 特定の値を設定する
 ・=== 3連イコール → 同じ
 ・!== びっくりマーク+2連続イコール → 同じではない
スクリーンショット 2021-02-28 6.30.30.png

12 条件を複数指定する
 スコアが20以上でかつ90以下 → if (score > 20 && score <90)
 * &&をつかう!

 スコアが0または89 → if (score === 0 || score == 89)

13 条件分岐 【else文】
〇〇が満たされなかった場合□□する
スクリーンショット 2021-02-28 7.31.59.png

14 条件分岐 【else-if文】
スクリーンショット 2021-02-28 7.38.36.png

15 条件の中に条件を含めることができる
  入れ子構造が可能
スクリーンショット 2021-02-28 7.46.02.png

16 条件分岐 【switch文】
* breakで区切るのを忘れない!
* defaultはそれ以外という意味
スクリーンショット 2021-02-28 7.57.49.png

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

【JavaScript】 海外Teck系YouTuberを真似てみた!(partⅢ) Task Tracker App | React JS

1,はじめに

この記事は、海外Teck系YouTuberの動画を参考に、同じプロジェクトを作成してみたものになります!

簡単にですが、動画を通して学べた技術や知識をまとめました!

今回の完成品は、下記に載せています:bangbang:

2,学んだこと

今回はさらに発展して、Reactのモジュール(RouteLink)なども使ってページ遷移も行いました!
async await の技術も使っているので、内容が盛り沢山でした:star:

今回は、

① async await の使い方
② JSONサーバーをローカルの5000portに立ち上げて、そこにデータを保存する方法
③ フォーム画面から値を受け取る方法
④ onClickonToggleonDeleteonDoubleClickonChange

async await の考え方を理解していないと、終盤についていくのが大変でした:joy:

3,参考動画

IMAGE ALT TEXT HERE

4,完成品です:tada:

左がJSONサーバーの様子で、右がブラウザ表示になります!

2021-02-28_04h28_37.gif

5、最後に

今回は、かなり手ごたえがありました!
特にasync await の考え方をもう一度振り返る必要があると感じました!
次回も、Reactを使ったプロジェクトにトライしてみます。

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

[React]フォーム入力の度にフォーカスが外れるときに確認すべきこと2選

はじめに

Reactでフォーム入力を扱ってる時,こんな風に入力の度にフォーカスが外れて連続入力ができない現象に遭遇して少しハマったのでまとめました。(以下のGIFで入力のたびに青枠が外れてるのがわかるかと思います)

inputForm.gif

問題のコード と CodeSandbox

CodeSandbox:
https://codesandbox.io/s/billowing-cherry-fy3dx?file=/src/App.js

import React from "react";

function App() {
  const [inputValue, setInputValue] = React.useState("");

  const handleInput = (event) => {
    event.preventDefault();
    setInputValue(event.target.value);
  };

  const InputForm = () => {
    return (
      <label htmlFor="input2">
        <input
          id="input2"
          type="text"
          defaultValue={inputValue}
          onChange={handleInput}
        />
      </label>
    );
  };

  return (
    <div className="App">
      <InputForm />
    </div>
  );
}

export default App;

原因

子コンポーネントを親コンポーネントのブロックの中で定義していたことが原因でした。
image.png

親コンポーネント(App)が再描画される度に、小コンポーネント(InputForm)を再定義、再描画していたので、Reactがもともとフォーカスを当てていたエレメントが破棄され、新しいエレメントとしてのinputが作成されていたためにフォーカスが外れていたようです。

解決方法:

子コンポーネントの定義をコンポーネントの外にすればOKです!
CodeSandbox
https://codesandbox.io/s/input-fixed-9hphl?file=/src/App.js

import React from "react";

function App() {
  const [inputValue, setInputValue] = React.useState("");

  const handleInput = (event) => {
    event.preventDefault();
    setInputValue(event.target.value);
  };

  return (
    <div className="App">
      <InputForm value={inputValue} handler={handleInput} />
    </div>
  );
}

export default App;

//親コンポーネントの外側に定義 別ファイルでも可
const InputForm = (props) => {
  return (
    <label htmlFor="input2">
      <input
        id="input2"
        type="text"
        defaultValue={props.value}
        onChange={props.handler}
      />
    </label>
  );
};

別の原因の可能性

実は原因を調べる過程で、同じ現象が起きる状況があることがわかりました。
それは mapで複数コンポーネントを描画した際のkeyに描画ごとに変化しうる値を使っている場合です
こちらも一例目と同じように、Reactがフォーカスをあてていたinputのkeyが存在しなくなり、新規エレメントとして描画していることがフォーカスが外れる原因みたいです。

バグコードの例

このコードでは、keyをフォームごとにユニークにするために入力値を用いています。(これだとユニークになるとは限らないですが、一例です)
Codesandbox:
https://codesandbox.io/s/multiple-inputs-with-a-bug-qt7lu?file=/src/App.js

import React from "react";

function App() {
  const [inputValueArr, setInputValueArr] = React.useState([
    "input1",
    "input2",
    "input3"
  ]);

  const handleInput = (event, index) => {
    event.preventDefault();
    const newInputValueArr = [...inputValueArr];
    newInputValueArr[index] = event.target.value;
    setInputValueArr(newInputValueArr);
  };

  return (
    <div className="App">
      {inputValueArr.map((value, index) => (
        <label key={`hoge_${value}`} htmlFor="input1">
          keyにバリューを使っている時
          <input
            id={`input_${value}`}
            type="text"
            defaultValue={value}
            onChange={(e) => handleInput(e, index)}
          />
        </label>
      ))}
    </div>
  );
}

export default App;

修正後の例

上記コードではkeyが入力のたびに変化してしまうので、フォームごとに固定で固有なkeyをつけてあげます。ここではindexを使います。
Codesandbox:
https://codesandbox.io/s/multiple-inputs-fixed-8wcsi?file=/src/App.js

import React from "react";

function App() {
  const [inputValueArr, setInputValueArr] = React.useState([
    "input1",
    "input2",
    "input3"
  ]);

  const handleInput = (event, index) => {
    event.preventDefault();
    const newInputValueArr = [...inputValueArr];
    newInputValueArr[index] = event.target.value;
    setInputValueArr(newInputValueArr);
  };

  return (
    <div className="App">
      {inputValueArr.map((value, index) => (
        <label key={index} htmlFor="input1">
          keyにバリューを使っている時
          <input
            id={`input_${value}`}
            type="text"
            defaultValue={value}
            onChange={(e) => handleInput(e, index)}
          />
        </label>
      ))}
    </div>
  );
}

export default App;

おわりに

最初は浅いコピーやら深いコピーやら絡みのStateの更新の仕方の問題かと思いましたが、もっと初歩的な話でした。公式ドキュメント読んだら書いてあるのかもしれないですね。この記事がどなたかのお役に立てれば幸いです!

参考

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

[React]フォーム入力の度にフォーカスが外れてしまうときに確認すべきこと2選

はじめに

Reactでフォーム入力を扱ってる時,こんな風に入力の度にフォーカスが外れて連続入力ができない現象に遭遇して少しハマったのでまとめました。(以下のGIFで入力のたびに青枠が外れてるのがわかるかと思います)

inputForm.gif

問題のコード と CodeSandbox

CodeSandbox:
https://codesandbox.io/s/billowing-cherry-fy3dx?file=/src/App.js

import React from "react";

function App() {
  const [inputValue, setInputValue] = React.useState("");

  const handleInput = (event) => {
    event.preventDefault();
    setInputValue(event.target.value);
  };

  const InputForm = () => {
    return (
      <label htmlFor="input2">
        <input
          id="input2"
          type="text"
          defaultValue={inputValue}
          onChange={handleInput}
        />
      </label>
    );
  };

  return (
    <div className="App">
      <InputForm />
    </div>
  );
}

export default App;

原因

子コンポーネントを親コンポーネントのブロックの中で定義していたことが原因でした。
image.png

親コンポーネント(App)が再描画される度に、小コンポーネント(InputForm)を再定義、再描画していたので、Reactがもともとフォーカスを当てていたエレメントが破棄され、新しいエレメントとしてのinputが作成されていたためにフォーカスが外れていたようです。

解決方法:

子コンポーネントの定義をコンポーネントの外にすればOKです!
CodeSandbox
https://codesandbox.io/s/input-fixed-9hphl?file=/src/App.js

import React from "react";

function App() {
  const [inputValue, setInputValue] = React.useState("");

  const handleInput = (event) => {
    event.preventDefault();
    setInputValue(event.target.value);
  };

  return (
    <div className="App">
      <InputForm value={inputValue} handler={handleInput} />
    </div>
  );
}

export default App;

//親コンポーネントの外側に定義 別ファイルでも可
const InputForm = (props) => {
  return (
    <label htmlFor="input2">
      <input
        id="input2"
        type="text"
        defaultValue={props.value}
        onChange={props.handler}
      />
    </label>
  );
};

別の原因の可能性

実は原因を調べる過程で、同じ現象が起きる状況が他にもあることがわかりました。
それは mapで複数コンポーネントを描画した際のkeyに描画ごとに変化しうる値を使っている場合です
こちらも一例目と同じように、Reactがフォーカスをあてていたinputのkeyが存在しなくなり、新規エレメントとして描画していることがフォーカスが外れる原因みたいです。

バグコードの例

このコードでは、keyをフォームごとにユニークにするために入力値を用いています。(これだとユニークになるとは限らないですが、一例です)
Codesandbox:
https://codesandbox.io/s/multiple-inputs-with-a-bug-qt7lu?file=/src/App.js

import React from "react";

function App() {
  const [inputValueArr, setInputValueArr] = React.useState([
    "input1",
    "input2",
    "input3"
  ]);

  const handleInput = (event, index) => {
    event.preventDefault();
    const newInputValueArr = [...inputValueArr];
    newInputValueArr[index] = event.target.value;
    setInputValueArr(newInputValueArr);
  };

  return (
    <div className="App">
      {inputValueArr.map((value, index) => (
        <label key={`hoge_${value}`} htmlFor="input1">
          keyにバリューを使っている時
          <input
            id={`input_${value}`}
            type="text"
            defaultValue={value}
            onChange={(e) => handleInput(e, index)}
          />
        </label>
      ))}
    </div>
  );
}

export default App;

修正後の例

上記コードではkeyが入力のたびに変化してしまうので、フォームごとに固定で固有なkeyをつけてあげます。ここではindexを使います。
Codesandbox:
https://codesandbox.io/s/multiple-inputs-fixed-8wcsi?file=/src/App.js

import React from "react";

function App() {
  const [inputValueArr, setInputValueArr] = React.useState([
    "input1",
    "input2",
    "input3"
  ]);

  const handleInput = (event, index) => {
    event.preventDefault();
    const newInputValueArr = [...inputValueArr];
    newInputValueArr[index] = event.target.value;
    setInputValueArr(newInputValueArr);
  };

  return (
    <div className="App">
      {inputValueArr.map((value, index) => (
        <label key={index} htmlFor="input1">
          keyにバリューを使っている時
          <input
            id={`input_${value}`}
            type="text"
            defaultValue={value}
            onChange={(e) => handleInput(e, index)}
          />
        </label>
      ))}
    </div>
  );
}

export default App;

おわりに

最初は浅いコピーやら深いコピーやら絡みのStateの更新の仕方の問題かと思いましたが、もっと初歩的な話でした。公式ドキュメント読んだら書いてあるのかもしれないですね。この記事がどなたかのお役に立てれば幸いです!

参考

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