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

JavaScript学習要点(随時更新予定)

  • JavaScriptは外部ファイルに書くのが一般的である。

  • console.log() は丸括弧 () 内に入力された文字をコンソールに出力する。文字列は、シングルクォーテーション( ' )かダブルクォーテーション( " )で囲む必要がある。

  • 文の最後はセミコロン(;)で終わる。セミコロンはなくても正常に動作する場合が多いが、思わぬ不具合が生じる可能性があるので忘れずにつける。

  • 文頭に「//」を書くと、その行はコメントとみなされる。

  • 変数は「let 変数名 = 値」として定義する。変数はクォーテーションで囲まない。「console.log("name");」のように変数名をクォーテーションで囲むと、nameが変数ではなく「文字列」として認識され、「name」とそのまま出力されてしまうので注意する。

  • 変数は、一度代入した値を変更することもできる。定義する時と違って「let」は必要なく、「変数名 = 新しい値」と書けば値が変更される。

  • 定数はletの代わりにconstを用いて定義する。変数は1度代入した値を更新することができるが、定数は値を更新することはできない。
    ※constはconstantの略

  • 文字列や定数の連結には、「+」記号を用いるが。ES6では、それ以外の方法として「テンプレートリテラル」という連結方法がある。文字列の中で「${定数}」とすることで、文字列の中に定数や変数を含めることができる。この時、文字列全体をバッククォーテーション(`)で囲む必要がある。

  • if文を書くときは、必ずインデントをするようにする。

  • 比較演算子には、左と右の値が等しいかを調べるものもある。「a === b」はaとbが等しければtrue、等しくなければfalseになる。「a !== b」はその逆になる。

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

Chrome Extentions "Krafty" 公開

Chrome Extention "Krafty"を公開しました。

開いているWebページのmeta情報やaltの確認、HTMLのネストの検証、要素のアウトラインの確認などができます。
マークアップエンジニアの方や、ディレクターの方には便利につかっていただけると思います。

名前は、New Orderのアルバムからいただきました。

ストア公開の申請時に、この拡張機能の使用にはホスト権限が必要なので、審査にかなり時間がかかると脅かされましたが、2日ほどで公開されました。
Googleさんありがとうございます。

Chrome Webstore
GitHub

機能紹介

Kraftyの4つの機能をご紹介します。

Head Checker

meta情報などを、ソースを開かずに確認できる機能です。
お客さんや一部ディレクターやなどソースを見るのに抵抗がある方や、コーディング時の漏れチェックのための機能です。

head.jpg

Nest Checker

タグのネストで、NGの部分を赤く表示して教えてくれる機能です。
技術的には、CSSのセレクタで、「このタグの直下にこのタグはNG」というのをひたすら指定しています。
問題なければ表示上何も変わりません。
構造上タグの組み合わせが膨大になってしまうので、漏れや間違いなどがないか、しばらく使って気付いたものを潰していこうと思います。

使用上の注意

許可するタグをnotで指定しているため、仕様上未知のタグはNG判定されてしまいます。
例:Google検索で使われているg-section
(キーワードがアレなのが多い気がしますが、たまたまです)

g-section2.jpg

g-section.jpg

テスト

試しに、Qiitaの記事ページで使用してみると・・・・あれ・・・・。

nest1.jpg

早速誤検出か・・・・・。

nest_selecter.jpg

でもdiv直下のdiv許可してるしな・・・・・。

nest_tag.jpg

あれ?閉じタグにもクラス名が?
あーなるほど、タグ名とクラス名の間が全角になっているので、<div>ではなく、未知のタグ<div class="logly-lift-ad-content">と認識されてNG判定されているようです。
タグのタイポも発見できる場合があることがわかりました。

Outline Checker

ページ内の要素のアウトラインを表示する機能です。
マークアップエンジニアの方が、要素がどのようにレイアウトされているか確認したり、スマホのCSSを書いているときに、謎の横スクロールの犯人の要素がなかなか見つからないときなどに使えると思います。

outline.jpg

Alt Checker

ページ内のimg要素のaltを表示する機能です。
お客さんや一部ディレクターやなどソースを見るのに抵抗がある方や、コーディング時の漏れチェックのための機能です。

alt.jpg

左上に画像があったんですね。気付きませんでした。

まとめ

最近無職となったので、ご興味持たれた会社様ご連絡お待ちしております。

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

canvasで画像比較したい

canvasで画像比較したい

最近ffmpegを使って音をいじいじする機会がありました。
いろんなパターンで音を編集してみたのですが、音の変化を耳で感じるのに限界を感じ、視覚的に見る方法ないかなぁと思っていました。
そしたらffmpegには音の波形を画像で出す機能があるようで。

https://nico-lab.net/create_samples_waves_image_with_ffmpeg/

波形の画像を生成したは良いが、微妙な違いを確認するためには結局目を凝らすしかないという結果に。。
Rubyにはrmagickがありますが、javascriptでなんかできないかなぁと思いcanvasを使ってやってみることにしました。

どうやったか

概要

wave_1.pngwave_1.pngを用意し、canvasに順番にレンダリング。
レンダリングした情報を変数に格納。
画像を比較して差分がある箇所だけ色を付ける。

index.html
<body>
  <p>image1</p>
  <img id="image_1" src="wave_1.png" />
  <p>image2</p>
  <img id="image_2" src="wave_2.png" />
  <p>diff</p>
  <canvas id="my-canvas" width="480" height="270"></canvas>
  <script type="application/javascript" src="canvas.js"></script>
</body>

class WaveDiffChecker {
  load() {
    this.image1 = document.getElementById("image_1");
    this.image2 = document.getElementById("image_2");
    this.c1 = document.getElementById("my-canvas");
    this.ctx1 = this.c1.getContext("2d");

    this.width = this.image1.clientWidth;
    this.height = this.image1.clientHeight;

    this.checkWave();
  }

  checkWave() {
    this.ctx1.drawImage(this.image1, 0, 0, this.width, this.height);
    const frame1 = this.ctx1.getImageData(0, 0, this.width, this.height);
    this.ctx1.drawImage(this.image2, 0, 0, this.width, this.height);
    const frame2 = this.ctx1.getImageData(0, 0, this.width, this.height);

    const pixels = frame1.data.length / 4; // 同じ画像サイズなのでdotsは1つでOK

    for (let i = 0; i < pixels; i++) {
      const index = i * 4;
      const isSame = [0, 1, 2].every(value => {
        return frame1.data[index + value] === frame2.data[index + value];
      });
      frame1.data[index] = isSame ? 0 : 100; // Red値
      frame1.data[index + 3] = isSame ? 0 : 100; // Alpha値
    }
    this.ctx1.putImageData(frame1, 0, 0);
  }
}

const diffChecker = new WaveDiffChecker();
diffChecker.load();


スクリーンショット 2020-07-10 22.09.23.png

ポイント

this.ctx1.getImageData(0, 0, this.width, this.height)
getImageDataの返り値はimageData
であり、imageData.dataでUint8ClampedArray
にアクセスできます。
このArrayはRGBAの順で0から255のデータを持っています。

//1ピクセルの構成
frame1.data[0] //=> 0(Red)
frame1.data[1] //=> 255(Green)
frame2.data[2] //=> 100(Blue)
frame3.data[3] //=> 150(Alpha)

各ピクセルの要素を順に確認していき、RGBAのどこかに差分がある場合のみ色をつけています。

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

【画面操作】ワンクリックでWebページからプレゼント応募しまくる

はじめに

基本的には業務時間外に開発とか絶対やらない。
と思っている私があくまで話のネタとして作ってみたものなので、これがベストなツールかベストな方法かはわかりません。

こんなのあるんだなーと興味をもってくれたら嬉しいです。
Googleの拡張機能、Tampermonkeyを使ってブラウザを操ってみましょう。

きっかけ

某化粧品口コミサイトで毎月プレゼント企画があって、応募しまくるとたまに当たるのでよくやっていました。
present.PNG

アカウントがあれば誰でも、ぽちぽちクリックするだけで応募できます。そうクリックするだけで。

1回につきプレゼントの種類はだいたい15くらい。内容は毎月4回更新。月に約60個も応募できる!
presents.PNG

一覧から応募したいものを選んで。

詳細画面に遷移したら応募ボタンを。

アカウント情報が表示されたら確定ボタン。

メルマガ希望のチェックを外すのを忘れずに。

240クリックはちょっぴり大変。

当たらなかったら悔しいから自動化しよう。

つくるもの

サイトに表示されているプレゼントすべてをワンクリックで応募完了させよう

Tampermonkeyを使ってみる

「画面操作を自動で行う」みたいな方法を調べてみた。
ブラウザ上で自分の書いたJavaScriptを実行できる、Tampermonkeyというものを発見。

環境構築なしで、Chromeの拡張機能をつくれるらしい。簡単そう!( ^∀^)

まずはここからインストール

Chromeの右上のアイコンをクリックするとメニューが出てきます。ダッシュボードへ。
tamper_icon.png

+マークのタブを開くと新規スクリプトのエディタが。ここになんか書けばいいのね(`・ω・´)
editor.png

実行するJavaScriptを用意する

ページを4つ遷移する×プレゼントの数なので、プレゼントの数だけタブを開いてそれぞれで処理していく感じにします。
JavaScriptは非同期処理なので1こ1こ順番に進むようにasyncしときます

こんな感じのソースになりました。これをさっきのエディタに貼ります。

@cosme_present_apply.js
// JavaScript source code
// @ts-check
// ==UserScript==
// @name         @cosme present apply
// @version      1.0
// @description  @cosme present apply
// @author       kitayama_y
// @match        https://www.cosme.net/*
// @grant        none
// ==/UserScript==

(async() => {
    const href = location.href;
    //プレゼント一覧画面
    if (href == "https://www.cosme.net/present") {
        var apply_urls = document.querySelectorAll("#list-member dd .apply > a")
        var window_count = 0;
        apply_urls.forEach( function( value ) {
            window.open(value);
        });
    }
    // プレゼント詳細画面
    if (href.includes("https://www.cosme.net/present/detail/present_id/")) {
        var button = /** @type {HTMLElement} */ (document.querySelector("#section .apply input"));
        if (button != null) {
            button.click();
        } else {
            window.close();
        }
        button.click();
    // 応募確定画面
    } else if (href.includes("https://www.cosme.net/present/confirm/")) {
        var button_confirm = /** @type {HTMLElement} */ (document.querySelector(".present-conf .present-apl-btn >input"));
        var addbrand_check = /** @type {HTMLElement} */ (document.querySelector("#addbrand"));
        addbrand_check.checked = false;
        button_confirm.click();
    }
})();

 
保存すると「インストール済み」のタブに現れます。ここで有効になっていればスクリプトが実行されるんですね。
上記のjsの場合はサイトのURLを条件にしているので、サイトに移動すれば応募しまくるスクリプトの発動です。
scripts.png

いざ、実行

スクリプトができたので、実行してみよう。最初のURLにポチっと移動すると…

お。Chromeがめっちゃ働いていますね( ^ω^)
execute.PNG

応募が終わったらタブをクローズするようにしているので、後片付けもバッチリです。

実行した後に、プレゼント詳細画面を見てみると…
applied.PNG

応募できてるー\(^o^)/

実行時に、ボタンが「応募済み」状態だった場合にもタブをクローズするようにしておけば、もう応募済みなのに誤爆してタブが開いて散らかった~ということもない。

おまけ

Tampermonkeyの語源(公式ソースではない)

Tamper:(許可なく、勝手に)変更する、改竄(かいざん)する、(勝手に)いじりまわす、勝手に開封する
なのでjsでブラウザをいじり回すサル。monkeyはなかなかキャッチー

と思ったらFirefoxにもともとGreasemonkeyという似たやつがあったらしい。
なんと、Greasemonkeyという
(自動車・飛行機の)修理工、整備士
って意味の単語があるらしいです。

突然文系の時間が始まってしまいましたが、monkeyとかfoxとかMozillaって動物が好きなん?と思ってこれもググっていたら、firefoxってキツネじゃなくてレッサーパンダらしい(驚)

おわり

感想

やったことはjsを書いただけで面倒なことがなかったので私でも簡単に使えました。
月に4回思い出して押してます。
当たれ~。

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

手動でABテストするためのコンテンツの出し分けのサンプル(Javascript+Cookie)

はじめに

実現したい事はWebサイトで
特定の画像/文章/デザインなどの部品を2パターン用意し、
ランダムで表示してその結果を後で
GoogleAnalyticsなどで検証するというものです。
※コードの例ではGAのイベントトラッキングを活用します。
rabel.png
オプティマイズや別の専用ツールなどもあるのですが、
ツールの縛りがある、使い辛い、まずは気軽に試したい、
という時に不便なので作りました。(個人サイトの場合など含め)

(利用例)
画像A / 画像B どちらを表示したほうがCVRが良いか?
※GA等の解析ツールにデータを送信し、後で分析をかけます。

補足:
コードは汚いので仕組みそのものや部品とりとしてご活用ください。

仕様

概要:
特定のIDを持つ要素に対してA/Bパターン2種類の中身(HTML)を用意し、
それをおよそ1/2の確率でランダムで表示します。
結果はCookieに記録し、次回以降該当のCookieが存在する場合、
同じ結果を表示します。
※Cookieを消したらまた1/2の確率でどちらかが表示されます。

これらの結果を表示する度に、GAにイベントとして
データを送信するというサンプルです。
※GAじゃなくてもコードをJSやHTMLで連携する類のものなら何でも可

使う言語:javascript+html+css
補足:cookieを活用します

Javascriptコード

※デバッグ用と記載のある関数/コードは使用しなくとも大丈夫です。

<script>

window.onload = function() {

const thiscookiename = 'abtest';
//Cookieの名前

const btag = "<p style='background-color:#ffffee;width:200px;'>私がオススメするのは<br>クレープです。<br>";
//Bパターンのタグをここに記載

const yosoid1 = 'abtest';
//変更対象の要素のID

const yukokigen = 7;
//Cookieの有効期限(日)

const abeventname = 'testcase2020';
//GAイベント名称

const abeventcategory = 'ABTEST';
//GAイベントカテゴリー

//初期設定
//END


//0か1の乱数生成 Math.random()で0~1の間の乱数、2倍にしたものを整数にする
let abtestran = Math.floor( Math.random() * 2 );

const expire = new Date();
expire.setTime( expire.getTime() +yukokigen *24*60*60*1000 );
//Cookie用日時と有効期限の設定

//Cookieの有無を確認
if(GetCookieABtestOnly(thiscookiename) == null){
    document.cookie = thiscookiename + '=' + abtestran + '; expires=' + expire.toUTCString();
    //存在しなかったらCookie書込み

}
else{
    abtestran = GetCookieABtestOnly(thiscookiename);
    //存在する場合はテスト判定用数値を設定する
}


if(abtestran==1){

    //Bの時の処理

    document.getElementById(yosoid1).style.display = 'inline-block';
    //要素を表示するスタイルに変更

    document.getElementById(yosoid1).innerHTML = btag;
    //Bパターンのタグを代入

    document.getElementById('kekka').innerHTML = abtestran + "が値です";
    //デバッグ用


//GAイベント送信
gtag('event',abeventname, {
  'event_category' :abeventcategory,
  'event_label' : 'B_PT'
});
//endGA


}

//Aの時(イベント送信以外何もしない)
else{
    document.getElementById(yosoid1).style.display = 'inline-block';
    //要素を表示するスタイルに変更

    document.getElementById('kekka').innerHTML = abtestran + "が値です";
    //デバッグ用

//GAイベント送信
gtag('event',abeventname, {
  'event_category' :abeventcategory,
  'event_label' : 'A_PT'
});
//endGA

}


}
//onload終了



//Cookie取得用の関数
function GetCookieABtestOnly( name )
{

let result = null;
const cookieName = name + '=';
const allcookies = document.cookie;

const position = allcookies.indexOf( cookieName );

if( position != -1 )
    {
        startIndex = position + cookieName.length;

        endIndex = allcookies.indexOf( ';', startIndex );
        if( endIndex == -1 )
        {
            endIndex = allcookies.length;
        }

        result = decodeURIComponent(
            allcookies.substring( startIndex, endIndex ) );
    }

return result;
}



//デバッグ用Cookie削除
function deleteCookie(cockname) {
date = new Date();
date.setTime( date.getTime() - 1 );
document.cookie = cockname + '=; expires=' + date.toUTCString();

document.getElementById('kekka').innerHTML = "削除しました";

}


</script>

HTMLのコード

※デバッグ用と記載のある部分は使用せずとも大丈夫です。

▼変更対象の要素:<br>
<!-- Aパターンの記載 -->
<div id="abtest" style="display:none;">
<p style="width:200px;background-color:#ffeeee;">
私がオススメするのは<br>
【タピオカ】です。<br>
</p>
</div>

<br><br>

Cookieの値を表示:デバッグ用<br>
<div id="kekka">
</div>

<br>

<div>
Cookieの削除:デバッグ用<br>
<input type="button" value="Cookie削除" onclick="deleteCookie('abtest');" style="cursor:pointer;">
</div>

解説(重要そうな部分)

ランダム表示用の変数を作る:
let abtestran = Math.floor( Math.random() * 2 );
ランダム表示用に0か1の乱数を生成します。
Math.random()で0~1の間の乱数を、2倍にしたものを整数にする事で、
0か1だけになります。

判定用(cookie有無):

別関数としてcookieの有無を確認するものを用意し、
nullだった場合はcookieを書き込みします。
存在していた場合は、Cookieの値をAB判定用の変数に代入します。

if(GetCookieABtestOnly(thiscookiename) == null){
document.cookie = thiscookiename + '=' + abtestran + '; expires=' + expire.toUTCString();
}
else{
abtestran = GetCookieABtestOnly(thiscookiename);
}

判定用:ABのどちらか
if(abtestran==1){}else{}
単純に1か0でAとBを判定します

コンテンツを表示する部分

HTML側:
<div id="abtest" style="display:none;">
Bを表示する時に一瞬、Aの画像が表示されるのを防ぐため、
HTML側はスタイルdisplay:noneで要素を隠しておきます。

JS側:
document.getElementById(yosoid1).style.display = 'inline-block';
Javascript側でスタイルを変えて、表示します。

document.getElementById(yosoid1).innerHTML = btag;
特定のIDを持つ要素に、JS内で記述しておいたBパターンの
タグを挿入します。

後述

お読みいただきありがとうございます!

ポイントは1/2ランダムで出し分けと、Cookieで記録しておくという
部分だと思います。※毎回完全ランダムだとユーザーが困るので…

また、今回AパターンはHTMLに記述しておいて、BパターンはJS内に記載しましたが、
両方display:noneにしておいて、display:blockにするやり方もありますね。

応用すればもっと便利に出来るはずですので、
参考という形にしていただければと思います。

動作サンプルも置いておきます。
http://allcountry.sakura.ne.jp/biz/code/javascript/20200709/cookieabtest.html

pta.png

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

Kinx アルゴリズム - 騎士巡回問題

Kinx アルゴリズム - 騎士巡回問題

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。「プログラム=アルゴリズム+データ構造」。アルゴリズムの実装例をご紹介。

元ネタは「C言語による(30年経っても)最新アルゴリズム事典」。今回は騎士巡回問題です。

最新アルゴリズム事典にはこういうのも結構載ってる。パズル的な。

このアルゴリズムも Kinx 初期の頃に書いてテストに使った。やはり二重配列の制御と再帰処理あたりで。お世話になりました。

騎士巡回問題

Wikipedia より

ナイト・ツアー(Knight's Tour)は、チェスを使った数学的パズルの一種。「騎士の巡歴(じゅんれき)」「桂馬拾い」[1]とも呼ばれ、チェスをモチーフにしたパズルの中でも昔からよく知られている。チェスボード上のナイトを移動させ、64マス全てを一回ずつ通過させる。

ソースコード

const N = 5;    // N x N

var board = [],
    dx = [ 2, 1,-1,-2,-2,-1, 1, 2 ],
    dy = [ 1, 2, 2, 1,-1,-2,-2,-1 ];
var count = 0;
var solution = 0;

function printboard() {
    System.print("\nSolution %d\n" % ++solution);
    for (var i = 2; i <= N + 1; i++) {
        for (var j = 2; j <= N + 1; j++) System.print("%4d" % board[i][j]);
        System.print("\n");
    }
}

function test(x, y) {
    if (board[x][y] != 0) return;
    board[x][y] = ++count;
    if (count == N * N) printboard();
    else for (var i = 0; i < 8; i++) test(x + dx[i], y + dy[i]);
    board[x][y] = 0;  count--;
}

function knight() {
    for (var i = 0; i <= N + 3; i++)
        for (var j = 0; j <= N + 3; j++) board[i][j] = 1;
    for (var i = 2; i <= N + 1; i++)
        for (var j = 2; j <= N + 1; j++) board[i][j] = 0;
    test(2, 2);
}

knight();

結果

Solution 1
   1   6  15  10  21
  14   9  20   5  16
  19   2   7  22  11
   8  13  24  17   4
  25  18   3  12  23

Solution 2
   1   6  11  18  21
  12  17  20   5  10
   7   2  15  22  19
  16  13  24   9   4
  25   8   3  14  23

Solution 3
   1   6  11  16  21
  12  15  20   5  10
   7   2  13  22  17
  14  19  24   9   4
  25   8   3  18  23

...(省略)

Solution 304
   1  10   5  16  25
   4  17   2  11   6
   9  20  13  24  15
  18   3  22   7  12
  21   8  19  14  23

おわりに

これはほとんど C 言語版と一緒の形で書けている。

こうしてみると、最初の頃(半年ほど前)は、この手のソースコードを使ってテストしてたんだなー。こういうのがちゃんと動くのを見られると嬉しいよね。

ではまた、次回。

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

【Nuxt.js】Nuxt文法編:@submit

? この記事はWP専用です
https://wp.me/pc9NHC-p9

前置き

今回はイベントハンドラ の一種、
@submitについて?

基本的な記法や
イベントの種類、
イベント修飾子などは
こちらで既に解説済みです??‍?
https://wp.me/pc9NHC-ht

@submitの使用例を
主に解説していきます?

@submit

イベントの種類

MDN
submitイベントは


送信された時に発生します?
formタグ自身につくもので、
form内のinputやbuttonでは発生しません?
consoleでエベントの種類を
確認してみましょう✅ ?

JavaScriptイベントハンドラ
--| submitイベント/
----| @submit

submitEvent.png

SubmitEventの中の
typeがsumit(@submit)とありますね?

コード

.preventについては後述します。

index.vue
<template>
 <div class="page">
   <form @submit.prevent="submit">
     <input
       type="text"
       placeholder="テキストを入力"
     >
     <button type="submit">送信</button>
   </form>
 </div>
</template>

<script>
export default {
 methods: {
   submit () {
     console.log(event)
   },
 },
}
</script>

使用例

こちらの記事が分かりやすいかと思います?
Step3: ログイン機能の実装
https://wp.me/pc9NHC-ik

firebaseAuthを使用して、
emailとpasswordを
submitしています??

コード

index.vue
<template>
<div class="page">
  <form
    class="form"
    @submit.prevent="login"
  >
    <label class="label">
      <span class="label">
        email
      </span>
      <input
        class="input"
        type="text"
        v-model="email"
      >
    </label>
    <label class="label">
      <span class="label">
        password
      </span>
      <input
        class="input"
        type="password"
        v-model="password"
      >
    </label>
    <button
      class="button"
      type="submit"
    >
      Login
    </button>
  </form>
</div>
</template>

<script>
import firebase from '~/plugins/firebase'

export default {
data() {
  return {
    email: '',
    password: ''
  }
},
methods : {
  login() {
    firebase.auth().signInWithEmailAndPassword(this.email, this.password)
    .then(user => {
      console.log('成功!')// eslint-disable-line
    }).catch((error) => {
      alert(error)
    });
  },
}
}
</script>

.prevent修飾子

? 続きはWPでご覧ください?
https://wp.me/pc9NHC-p9

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

JavaScript 基礎 サイコロ

サイコロ

HTML

<form>
  <input type="button" value="サイコロ" onClick="altRan2()">
  <span id="sai">-</span>
</form> 

JavaScript

function altRan2() {
  var r = Math.floor(Math.random() * 6) + 1;

  document.getElementById('sai').innerHTML = r;
}

Math.floor()

引数として与えた数以下の最大の整数を返します。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/floor

Math.random()

0–1(0以上、1未満)の範囲で浮動小数点の擬似乱数を返す

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/random

element.innerHTML

Element オブジェクトの innerHTML プロパティは、要素内の HTML または XML のマークアップを取得したり設定したりする

https://developer.mozilla.org/ja/docs/Web/API/Element/innerHTML

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

Javascriptで日付の連動更新をする

命題

JavaScriptを使って、二つ(以上)ある<input type="date">の一方が更新されたとき、もう片方を同じ値にしたい。
ブラウザはChrome 83を使用。

想定される使用例

検索期間を2組の<input type="date">で表し、例えば検索終了日を変更した際に、検索開始日より過去の日付だったら開始日を終了日と同じにする。

HTMLサンプル
<div>
  検索期間: <input id="DateSince" name="DateSince" type="date" value="2020-07-10"><input id="DateUntil" name="DateUntil" type="date" value="2020-07-10">←こちらを変更すると左側の日付も変わります。
</div>

具体的には上記のコードで、DateUntilを2020-7-09に変更した場合、DateSinceも2020-7-09に書き換わるようにする。

実際の実装では逆の処理(DateSinceをDateUntilより未来にした場合、DateUntilをDateSinceと同じ値にする)も必要だが、説明簡略化のために省く。

具体的な方法

イベント自体の作成は簡単。

DateUntil「変更」時に発生させるJSイベント
function dateUntilChanged(e) {
  const dateSince = document.getElementById("DateSince");
  const value = e.target.value;
  if (dateSince.value < value) {
    dateSince.value = value;
  }
}

問題はこのイベントをどのタイミングで発火させるかである。

ダメな例: changeイベントを使う

変更されたのなら素直にchangeイベントを使えばいいじゃん、とついつい考えてしまいがちだが、それは残念ながら間違い。

changeイベントを使用したJS
document.getElementById("DateUntil").addEventListener("change", dateUntilChanged);

こうすると(少なくともChromeでは)DateUntilが打鍵されるたびにイベントが発火するので、「2020」とキーボード入力する際に2→0→2→0と入力するためにDateSinceが0002年になってしまう(当然、日付の変更条件は満たされなくなるので、0002年のまま変更されない)。

この動作は正直言って好ましくない。イベントをいちいち発火するコストも問題だ。

よりよい例: blur(フォーカスアウト)イベントを使う

入力されるたびにイベントを発生させるのではなく、Dateの入力が完了した後、Tabキーや<input type="date">の外部をクリックすると起こるようなイベントがあればいい。
そこで登場するのがblurイベントだ。

blurといきなり言われても分かりづらいので(個人的にはブリットポップのバンドを思い出す)説明すると、ある要素からフォーカスが外れた際に(別のところをクリックするなど)発生するイベントのこと。
要はblur(ぼやけるの意)がfocusの対義語だから、フォーカスアウトするイベントをそう呼んでいるらしい。

blurイベントを使用した例
// 前掲例のchangeをblurに変えただけ
document.getElementById("DateUntil").addEventListener("blur", dateUntilChanged);

Chromeでは、デートピッカーを抜け出しただけではblurイベントが発生しないので注意!
(<input type="date">の年の部分が選択されているため)
ほかの箇所をクリックした時点でblurが発火するので、実用上はそこまで問題ではない。
私が使った場面は<form>で検索期間を指定して「検索」ボタン押下というシナリオだったので、この問題は無視できた。

おさらい(CodePen)

See the Pen Javascriptで日付の連動更新をする by Nagayama Toshiaki (@NagayamaToshiaki) on CodePen.

参考文献

holstetoさんの回答(英語)
javascript - HTML5 <input type=“date”> - onChange Event - Stack Overflow

<input type="date">が変更された際に発生するイベントについてまとめられています。
この回答に出会わなければこの記事を書けませんでした。

追記

Firefox 78でも同様の挙動が確認できたので、一部取り消し線を入れました。

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

ポートフォリオ

ポートフォリオ1

こちらのポートフォリオは環境構築だけで開発は行っていません。
(aws,nginx,ruby,docker,mysql)を使って環境構築を行いました。

https://github.com/tmk616window/docker-ruby2

ポートフォリオ2 

ポートフォリオ投稿サイトを作りました。

URLはこちらになります

https://github.com/tmk616window/review00
https://review0.herokuapp.com/

経緯

まずなぜこれを作ろうと思ったのか説明させていただきます。
自分がインターンの採用のためにポートフォリオを作ろうと思ったときに「何を作ればいいかわからない。どこまで作りこめばいいかわからない。」という悩みがありました。
ツイッターやyoutubeのコメント欄でもこういった悩みを持った方々をよく見かけました。
それだったら何を作ればいいのかまたどれくらい作りこめばいいのかの指標になるようなものをポートフォリオに落とし込んでみようと思いました。

コンプト

エンジニア採用が決まった方が記事を投稿し、それを未経験の方が参考にしてポートフォリオを作る指標を定めることを目標にしています。

機能

では実際に機能の説明をさせていただきます。

トップページ

2020-07-10.png

こちらがトップページになります。
採用された方が自分のポートフォリオを6項目で評価し、数値化してわかりやすくしています。

詳細ページ

2020-07-10 (7).png

興味がある投稿をクリックすると詳細ページに移動します。
詳細ページにはポートフォリオの情報がより詳しく記載されています。
いいねボタンはajaxで実装しました。
いいね数の数字もリアルタイムで変わるように実装しました。

相談

2020-07-10 (2).png

詳細ページの「相談する」のボタンを押すと詳細ページに移動します
相談ページではslack形式でリアルタイムに相談できるようなものを作りました。
こちらでは詳細ページで気になったことなどを質問できるようにするために作りました。
入力したメッセージは相手のマイページに通知が行くようになっています
actioncableを使いました。

マイページ

2020-07-10 (3).png

上のチャットメッセージのところに先ほど他のユーザーが相談ページで送ったメッセージの通知が来るようになっています。
マイページの画像はawsのs3に接続していましたが料金がかかりすぎていたので接続を切りました。
コード自体は残っているのでそちらを参照お願いします。
ほかのユーザーのマイページに入ると画像の下にフォローボタンが出てきてフォローすることができます。

イベント一覧

2020-07-10 (5).png

こちらは時間があったのでサブで機能追加したものです。
画像はawsのs3に接続していましたが料金がかかりすぎていたので接続を切りました。
コード自体は残っているのでそちらを参照お願いします。
気になるイベントをクリックすると詳細ページに飛びます。

イベント詳細ページ

2020-07-10 (6).png

詳細ページに飛びます。
下のイベント申し込みボタンをクリックすると申し込みページに飛んで申し込むことができます。
申し込みが完了すると相手のマイページのイベントメッセージのところに通知が行くようになっています。

以上でポートフォリオの説明を終わります。

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

React ハンズオン

今日作成するアプリ

chapter0.gif

1.Reactの簡単な説明

  • データの変更を検知したら、関連する部分だけを効率的に更新、描画する
  • 仮想 DOM(インメモリに保持されたUI表現)による高速な描画
  • JSXを使う(JavaScriptのソースコードにHTML的なものを埋め込む)
  • 単一のWebページでアプリケーション(Single Page Application)を作れる

Reactの採用事例

主な採用事例

  • Netflix
  • Slack
  • Uber
  • Airbnb
  • Paypal

ES2015(ES6)について

  • ECMASCriptの6th Editionのこと
  • letとconstで変数を宣言できる
  • アロー関数 : console.log(materials.map(material => material.length));
  • Class構文
  • extendsでクラスの継承
  • 全てのブラウザで対応しているわけではないため、Babelというトランスパイラを利用する
  • ReactはES6かES7で書く場合が多い

2.新しいシングルページアプリケーションを作成する

create-react-appという新規のReactプロジェクトを作るCLIツールを使います。
まずは、create-react-appをインストールします。
(Node >= 8.10 及び npm >= 5.6 の環境が必要です)

npm install -g create-react-app

todoプロジェクトを作成します。

npx create-react-app todo

3.ディレクトリ構成についての説明

.
├── README.md
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html Reactアプリケーションがのるページ
├── src
│   ├── App.css App.jsで使用されるcss
│   ├── App.js index.jsから呼ばれるReactコンポーネント
│   ├── App.test.js
│   ├── index.css index.jsで使われるcss
│   ├── index.js Reactアプリケーションで最初に走るスクリプト(ルート DOM ノードにレンダリングする処理が書かれている)
│   └── logo.svg
└── yarn.lock

4.実行する

次のコマンドを実行します。

npm start

ブラウザに次のように表示されたら成功です。(http://localhost:3000のURLでブラウザが起動します)

chapter4.png

5.ソースを読んでみる

src/index.jsを開きます

以下の処理はpublic/index.htmlの<div id="root"></div>にAppコンポーネントをレンダリングしています。

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

src/App.jsを開きます

AppクラスがReact.Componentを継承しReact コンポーネントになっています。React.Component サブクラスで必ず定義しなければならない唯一のメソッドは render() です。render() メソッドは変更が起こるたびに呼び出されます。

returnで返しているのはJSXです。classはclassNameと書かないといけないことに注意してください。

export default AppでこのファイルのデフォルトとしてAppをexportしています。

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <div className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h2>Welcome to React</h2>
        </div>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

6.コンポーネントを作る

目的:コンポーネントの作り方と使い方

1) コンポーネントを入れるためのディレクトリを作ります。

mkdir -p src/components

2) componentsディレクトリにList.jsを作ります。

List.js

import React, { Component } from 'react';

class List extends Component {
  render() {
    return (
      <table>
        <tbody>
          <tr><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>掃除する</td></tr>
          <tr><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>買い物</td></tr>
          <tr><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>洗濯する</td></tr>
        </tbody>
      </table>
    );
  }
}

export default List;

3) App.jsに組み込み

import React, { Component } from 'react';
import List from './components/List';

class App extends Component {
  render() {
    return (
      <List />
    );
  }
}

export default App;

次のように表示されます。
chapter6.png

7.stateを使う

目的:stateの使い方

stateはコンポーネントの内部で制御されるオブジェクトです。更新はsetStateにより非同期に更新されます。stateが更新されると再描画されます。

List.jsを次のように書き換えます。

import React, { Component } from 'react';

class List extends Component {

  constructor(props) {
      super(props);
      this.state = {
        todos: [
          '掃除する',
          '買い物',
          '洗濯する'
        ]
      };
  }

  onClickAdd() {
    const newTodo = window.prompt("やることを入力してください", "");
    const todos = this.state.todos;
    todos.push(newTodo);
    this.setState({todos});
  }

  render() {
    return (
      <div>
        <div><button onClick={() => this.onClickAdd()}>追加</button></div>
        <table>
          <tbody>
          {this.state.todos.map((todo) => (
            <tr key={todo}><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>{todo}</td></tr>
          ))}
          </tbody>
        </table>
      </div>

    );
  }
}

export default List;

追加でリストが増えると成功です。

8.propsを使う

目的:propsの使い方

propsはコンポーネントに属性として設定し値を渡すことができます。値だけでなく関数も渡すことができます。

1) componentsディレクトリにItem.jsを作ります。

import React, { Component } from 'react';

class Item extends Component {

  render() {
    const {todo, onClickItem} = this.props;
    return (
      <tr onClick={() => onClickItem(todo)}><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>{todo}</td></tr>
    );
  }
}

export default Item;

2) List.jsのrenderを次のように書き換えます。

Itemをimportしてください。

import Item from './Item';
  render() {
    return (
      <div>
        <div><button onClick={() => this.onClickAdd()}>追加</button></div>
        <table>
          <tbody>
          {this.state.todos.map((todo) => (
            <Item key={todo} todo={todo} onClickItem={(todo) => alert(todo)} />
          ))}
          </tbody>
        </table>
      </div>
    );
  }

9.詳細内容表示用のコンポーネントの追加

1) src/componentsディレクトリにContent.jsを追加します。

import React, { Component } from 'react';

class Content extends Component {

  render() {
    return (
      <div style={{marginLeft: 50}}>
        <span>掃除をする</span>
      </div>
    );
  }

}

export default Content;

2) App.jsを次のように書き換えます。

import React, { Component } from 'react';
import List from './components/List';
import Content from './components/Content';

class App extends Component {
  render() {
    return (
      <div style={{display:'flex'}}>
        <List />
        <Content />
      </div>
    );
  }
}

export default App;

Listコンポーネントで選択した内容をContentコンポーネントに表示させたいですが、このままではうまく行きません。

10.reduxとredux-sagaの導入

目的:redux・redux-sagaの導入方法と使い方

Reduxは、Reactのstate(状態)を管理をするためのフレームワークです。
redux-sagaとは

redux-saga は、アプリケーションの副作用(つまり、データフェッチのような非同期のものや、ブラウザキャッシュへのアクセスのような不純なも> > の)を管理しやすく、実行効率が高く、テストが簡単で、障害処理を改善することを目的としたライブラリです。

redux-sagaの構成
chapter10.png

1) ライブラリをインストールします

npm install redux react-redux redux-saga
npm install

2) actionの作成

mkdir -p src/actions

src/actions/index.js

/**
 * Redux Actions
 */
export * from './TodoAppActions';

src/actions/TodoAppActions.js

/**
 * Todo App Actions
 */
import {
    GET_TODOS,
    GET_TODOS_SUCCESS,
    GET_TODOS_FAILURE,
    ADD_TODO,
    ADD_TODO_SUCCESS,
    SELECT_TODO,
    SELECT_TODO_SUCCESS,
} from './types';

export const getTodos = () => ({
    type: GET_TODOS
});

export const getTodosSuccess = (response) => ({
    type: GET_TODOS_SUCCESS,
    payload: response
});

export const getTodosFailure = (error) => ({
    type: GET_TODOS_FAILURE,
    payload: error
});

export const addTodo = (todo) => ({
    type: ADD_TODO,
    payload: todo
});

export const addTodoSuccess = (todo) => ({
    type: ADD_TODO_SUCCESS,
    payload: todo
});

export const selectTodo = (todo) => ({
    type: SELECT_TODO,
    payload: todo,
});

export const selectTodoSuccess = (todo) => ({
    type: SELECT_TODO_SUCCESS,
    payload: todo,
});

src/actions/types.js

export const GET_TODOS = 'GET_TODOS';
export const GET_TODOS_SUCCESS = 'GET_TODOS_SUCCESS';
export const GET_TODOS_FAILURE = 'GET_TODOS_FAILURE';
export const ADD_TODO = 'ADD_TODO';
export const ADD_TODO_SUCCESS = 'ADD_TODO_SUCCESS';
export const SELECT_TODO = 'SELECT_TODO';
export const SELECT_TODO_SUCCESS = 'SELECT_TODO_SUCCESS';

3) sagasの作成

src/sagas/index.js

/**
 * Root Sagas
 */
import { all } from 'redux-saga/effects';

// sagas
import todoSagas from './Todo';

export default function* rootSaga(getState) {
    yield all([
        todoSagas(),
    ]);
}

src/sagas/Todo.js

import { all, call, fork, put, takeEvery } from 'redux-saga/effects';

import {
    GET_TODOS,
    SELECT_TODO,
    ADD_TODO,
} from '../actions/types';

import {
    getTodosSuccess,
    getTodosFailure,
    selectTodoSuccess,
    addTodoSuccess,
} from '../actions';

const getTodosRequest = () => new Promise((resolve, reject) => {
  const todos = ['部屋の掃除', '買い物', '洗濯'];
  resolve(todos);
});

function* getTodosFromServer() {
    try {
        const response = yield call(getTodosRequest);
        yield put(getTodosSuccess(response));
    } catch (error) {
        yield put(getTodosFailure(error));
    }
}

function* addTodoToServer(action) {
  yield put(addTodoSuccess(action.payload));
}

function* selectTodoFromServer(action) {
  yield put(selectTodoSuccess(action.payload));
}

export function* getTodos() {
    yield takeEvery(GET_TODOS, getTodosFromServer);
}

export function* selectTodo() {
    yield takeEvery(SELECT_TODO, selectTodoFromServer);
}

export function* addTodo() {
    yield takeEvery(ADD_TODO, addTodoToServer);
}

export default function* rootSaga() {
    yield all([
        fork(getTodos),
        fork(selectTodo),
        fork(addTodo),
    ]);
}

function*についてはこちら
yieldについてはこちら

takeEvery: Actionがdispatchされるたびに起動させたいタスクを指定します
put: Actionをdispatchします

4) reducersの作成

src/reducers/index.js

/**
 * App Reducers
 */
import { combineReducers } from 'redux';
import todoAppReducer from './TodoAppReducer';

const reducers = combineReducers({
   todoApp: todoAppReducer,
});

export default reducers;

src/reducers/TodoAppReducer.js

/**
 * Todo App Reducer
 */

// action types
import {
    GET_TODOS,
    GET_TODOS_SUCCESS,
    GET_TODOS_FAILURE,
    ADD_TODO_SUCCESS,
    SELECT_TODO_SUCCESS,
} from '../actions/types';

// initial state
const INIT_STATE = {
    todos: [],
    selectedTodo: '',
};

export default (state = INIT_STATE, action) => {
    switch (action.type) {

        case GET_TODOS:
            return { ...state, todos: [] };

        case GET_TODOS_SUCCESS:
            return { ...state, todos: action.payload };

        case GET_TODOS_FAILURE:
            return {}

        case ADD_TODO_SUCCESS:
            const newTodos = [];
            state.todos.forEach((todo) => newTodos.push(todo));
            newTodos.push(action.payload);
            return { ...state, todos: newTodos };

        case SELECT_TODO_SUCCESS:
            return { ...state, selectedTodo: action.payload };

        default: return { ...state };

    }
}

5) storeの追加

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';

import { Provider } from "react-redux";
import createSagaMiddleware from 'redux-saga';
import { createStore, applyMiddleware } from "redux";

import reducers from './reducers';
import RootSaga from "./sagas";
const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  reducers,
  applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(RootSaga);

ReactDOM.render(
  <Provider store={store}>
     <App />
  </Provider>
,
  document.getElementById('root')
);

6) コンポーネントの修正

src/components/Content.js

import React, { Component } from 'react';
import { connect } from 'react-redux';

class Content extends Component {

  render() {
    const {selectedTodo} = this.props;
    return (
      <div style={{marginLeft: 50}}>
        <span>{selectedTodo}</span>
      </div>
    );
  }

}

const mapStateToProps = ({ todoApp }) => {
    const { selectedTodo } = todoApp;
    return { selectedTodo };
}

export default connect(mapStateToProps, null)(Content);

src/components/Item.js

import React, { Component } from 'react';

import { connect } from 'react-redux';
import { selectTodo } from '../actions';

class Item extends Component {

  render() {
    const {todo, selectTodo} = this.props;
    return (
      <tr onClick={() => selectTodo(todo)}><td style={{border: 1, borderColor: 'gray', borderStyle: 'solid', cursor: 'pointer'}}>{todo}</td></tr>
    );
  }
}

export default connect(null, { selectTodo })(Item);

src/components/List.js

import React, { Component } from 'react';
import Item from './Item';

import { connect } from 'react-redux';
import { getTodos, addTodo } from '../actions';

class List extends Component {

  componentDidMount() {
    const { getTodos } = this.props;
    getTodos();
  }

  onClickAdd() {
    const { addTodo } = this.props;
    const newTodo = window.prompt("やることを入力してください", "");
    addTodo(newTodo);
  }

  render() {
    const {todos} = this.props;
    return (
      <div>
        <div><button onClick={() => this.onClickAdd()}>追加</button></div>
        <table>
          <tbody>
          {todos.map((todo) => (
            <Item key={todo} todo={todo} />
          ))}
          </tbody>
        </table>
      </div>

    );
  }
}

const mapStateToProps = ({ todoApp }) => {
    const { todos } = todoApp;
    return { todos };
}

export default connect(mapStateToProps, { getTodos, addTodo })(List);

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

iframe絡んだJavaScript操作

Html画面制作中、iframeをよく利用して、別の画面を取り込みます。
 JavaScriptでiframe間でイベント発生などがよくあります。うまくできない場合があります。
その解決策をメモしております。

やりたいこと

iframe1のボタンを押したら、iframe2の表に行を追加

問題点

下記のコードでiframe1のボタンをイベントでiframe2の項目を認識できない。

    $(".sendSelAdd").on("click", function() {
        // テーブルの最終行をクローンしてテーブルの最後に追加する
        $("#sendSelList tr:last-child").clone(true).appendTo("#sendSelList");

        // 複製後に表示させる
        $("#sendSelList tr:last-child").css("display", "table-row");
    });

解決方法

iframeを利用すると、当iframe以外の要素をアクセスするとき、直接アクセスできなくて、下記のコードのようにすればアクセスできます。


    $(".sendSelAdd").on("click", function() {
        // テーブルの最終行をクローンしてテーブルの最後に追加する
        parent.$("#ifSendMaster").contents().find("#sendSelList tr:last-child").clone(true).appendTo(parent.$("#ifSendMaster").contents().find("#sendSelList"));

        // 複製後に表示させる
        parent.$("#ifSendMaster").contents().find("#sendSelList tr:last-child").css("display", "table-row");
        //$.getScript("../js/style.js", function(){});
    });

解説

iframeの親の要素をアクセスするとき、parent.$("#item")

別のiframeの中の要素をアクセスするとき、$("#iframe2").contents().find("#要素")

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

SpotifyのようなタブバーをReact Native(React Navigation 5.x)でつくってみた

TL;DR

本稿では、SpotifyのようなアニメーションつきのタブバーをReact Navigation 5.xで実装する方法についてを紹介します。
こちらが完成形です。

途中でSpotify愛を語りながらグダグダと書きますので、生き急いでる方はgithubリポジトリをご覧ください。

自己紹介

僕は北の大地で統計学、経済学、経営学などを学んでいる文系大学院生(闇)です。

大学3年の秋頃に暇だったのでプログラミングを学び始めました。専門は統計学だったので、「ディープラーニングができたらいいなあ」と思って最初の言語はPythonにしました。僕はいまM1なので、プログラミング歴でいうと2年フラットとなります。

また、今年度に入ってなぜか(コロナで暇だったから)アプリ開発をしたいと思い、React Nativeをやりはじめました。なので、React Native歴で言うと3ヶ月ほどになります。

アプリ開発をするためにSwiftもKotlinもどっちも学ぶのはめんどくせえなと思ってクロスプラットフォームフレームワークを使うことにしました。その中でもReact NativeにしたのはJavaScriptで書けるってところが大きかったですね。あとユーザー数の多さ(大事)

環境

macOS Catalina
node.js 12.17.0
expo-cli 3.21.13

Windows機でも動かせたのでOS間の違いは特にないはずです。

Spotify、マジでカッケェ

僕はSpotifyがホントに好きです。

つい最近『Spotify』という本が発売されました。SpotifyのCEOのダニエル・エクが、Spotifyを世界一のストリーミングプラットフォームにするまでの経緯を描いたノンフィクションです。
読んだら「やっぱりSpotifyはカッケェなあ(小並感」ってなりました。

Spotifyは、他のストリーミングサービス(Apple Music, Amazon Musicなど)と違って、様々な角度から曲を切っていて、様々な角度から新しい曲と出会えるという点で素晴らしいサービスだともともと思っていたんです。
あとからSpotifyはSpotとidentifyからきた造語で、どちらも「見つける」という意味であるということを知って、会社のビジョンがサービスを通じてユーザーに伝わっているのもマジでカッケェなと思いました。

※書籍『Spotify』の中ではダニエル・エクの聞き間違いからSpotifyという名前が生まれたと書かれていました。
image.png

Spotify、タブバーすらカッケェ

まずAppleの「ミュージック」アプリのタブバーを見てください。タブを切り替えても色が変わるだけです。
AppleMusic.gif
次にSpotifyのタブバーを見てください。
Spotify.gif

キモチェエエエエエエエエエエエエ

このアイコンが「ぷにっ」とするのがマジで気持ちいい!!!!!
タブバーに「ぷにっ」としたアニメーションがあるかどうかでA/Bテストを行ったら、ユーザーのアクティブ率とかに有意な差が絶対出ると思います!!!!!

こうして、SpotifyのようなタブバーをReact Native(React Navigation 5.x)で実装したいと僕は考えたわけです。

ちなみに僕はしっかりSpotifyのプレミアム版には登録してるのですが、音楽の趣味がバレると恥ずかしいということで、この画面録画のためだけにもう一つ無料のアカウントを作りました。

React Navigation 5.xのセットアップ

React NavigationはReact Nativeで画面遷移を行うためのパッケージです。タブバーとかドロワーバーとかの実装が比較的簡単にできます。

React Navigationはメジャーアップデートのたびに書き方がガラリと変わるので大変です。。
依存関係にあるパッケージも多いので、最初はReact Navigationを動かすだけで一苦労でした。。

React Navigationを使うには、依存関係にあるパッケージをインストールしておく必要があります。Expo環境を想定しているので、以下のコマンドで依存パッケージは一括でインストールすることができます。

expo install @react-navigation/native react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view

また、今回はタブバーを作成したいので、上記の他に、@react-navigation/bottom-tabsもインストールします。

expo install @react-navigation/bottom-tabs

これでReact Navigation 5.xを使う準備は完了です。

基本となるタブバーを作る

画面中央にHome!と書いてあるだけのHomeScreenというコンポーネントと、同じく画面中央にSettings!と書いてあるだけのSettingsScreenというコンポーネントを定義しています。

下記の通りに記述すれば最も簡単なタブナビゲーションが実装できます。

import React from 'react';
import { Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

function HomeScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings!</Text>
    </View>
  );
}

const Tab = createBottomTabNavigator();

function MyTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name='Home' component={HomeScreen} />
      <Tab.Screen name='Settings' component={SettingsScreen} />
    </Tab.Navigator>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <MyTabs />
    </NavigationContainer>
  );
}

以下のようになっていれば成功です。
アイコンがないと物足りない感がすごいので、次にアイコンを追加しましょう。

アイコンを追加する

今回はMaterialCommunityIconというアイコンセットを用います。
react-native-vector-icons/MaterialCommunityIconsをインポートします。
expo installでインストールする必要はありません。(Expo環境であれば)

MaterialCommunityIconを見てみると、Spotifyのアイコンもありました。
せっかくなのでこれを入れてタブを3つに増やします。

iconsでスクリーン名とアイコン名をペアにした辞書データを保持しています。
Icon内では、これを用いて、スクリーン名(route.name)から、アイコン名を取得しています。

import React from 'react';
import { Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';

function HomeScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings!</Text>
    </View>
  );
}

function PremiumScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center'}}>
      <Text>Premium!</Text>
    </View>
  )
}

const Tab = createBottomTabNavigator();

function MyTabs() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ color, size }) => {
          const icons = {
            Home: 'home',
            Settings: 'settings',
            Premium: 'spotify'
          };

          return (
            <Icon
              name={icons[route.name]}
              color={color}
              size={size}
            />
          );
        },
      })}
    >
      <Tab.Screen name='Home' component={HomeScreen} />
      <Tab.Screen name='Settings' component={SettingsScreen} />
      <Tab.Screen name='Premium' component={PremiumScreen} />
    </Tab.Navigator>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <MyTabs />
    </NavigationContainer>
  );
}

Spotifyのアイコンが増えているはずです。
アイコンが追加されるとよく見るタブバーになりました。
ここからこだわりが出始めます。

タブの選択でアイコンを変える

SpotifyのタブバーのHomeタブをよく見ると、選択する前と後でアイコンが異なります。Homeを選択していない状態だとアウトラインだけのアイコンで、Homeを選択するとなかの塗りつぶしのあるアイコンに切り替わっています。

これを実装するために、tabBarIconのfocusedというプロパティを用います。これは「現在そのタブが選択されているか」の真偽値です。これを三項演算子の条件式に用いると、簡単にアイコンが切り替わるようにできます。

MaterialCommunityIconにはhome-outlineとsettings-outlineというアイコンはあるのですが、spotify-outlineというアイコンはないので、(focused | (route.name == 'Premium'))として、Premiumタブに関しては常にspotifyアイコンが表示されるようにしています。

import React from 'react';
import { Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';

function HomeScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings!</Text>
    </View>
  );
}

function PremiumScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center'}}>
      <Text>Premium!</Text>
    </View>
  )
}

const Tab = createBottomTabNavigator();

function MyTabs() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ color, size, focused }) => {
          const icons = {
            Home: 'home',
            Settings: 'settings',
            Premium: 'spotify'
          };

          return (
            <Icon
              name={(focused | (route.name == 'Premium')) ? icons[route.name] : icons[route.name] + '-outline'}
              color={color}
              size={size}
            />
          );
        },
      })}
    >
      <Tab.Screen name='Home' component={HomeScreen} />
      <Tab.Screen name='Settings' component={SettingsScreen} />
      <Tab.Screen name='Premium' component={PremiumScreen} />
    </Tab.Navigator>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <MyTabs />
    </NavigationContainer>
  );
}

settingsアイコンがアウトラインだけのアイコンになっているはずです。
Home以外のタブを選択すると、homeアイコンもアウトラインだけになります。

タブバーを自由に作る

もっと柔軟にタブバーを作成することもできます。タブバーを自分でゼロから作り、そこにタブナビゲーションの機能を追加するというイメージです。

React Navigation 5.xの全機能を知っていれば、様々なことができるのでしょうが、タブバーを自分で作ってしまったほうが楽ですし。

ここでせっかくなので、背景色とタブバーの色とアイコンの色をSpotifyのようにしてみようと思います。
テキストの色を白くしないと背景の黒と同化して見えなくなってしまうので、テキストの色も変更してます。
ステータスバーのテキストの色も同様です。StatusBarを用いて簡単に変更できます。

また、このあたりでStyleSheetを用いて、スタイルもまとめて管理しようと思います。

さらに、これまではiPhoneX以降のホームバーの分、画面のボトムのマージンをReact Navigationが自動調整してくれていましたが、自力で調整しなければいけなくなるため、react-native-iphone-x-helperを使います。
expo install react-native-iphone-x-helperでインストールします。
getBottomSpace()は画面サイズからiPhoneX以上かどうかを判定して、iPhoneX以上であれば、ホームバー分の高さを返してくれる関数です。

ちなみにnavigation.emit()は画面遷移を実行するメソッドです。

import React from 'react';
import { Text, View, StyleSheet, TouchableOpacity, StatusBar } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { getBottomSpace } from 'react-native-iphone-x-helper'

function HomeScreen() {
  return (
    <View style={styles.conteiner}>
      <Text style={{color: '#fefdff'}}>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={styles.conteiner}>
      <Text style={{color: '#fefdff'}}>Settings!</Text>
    </View>
  );
}

function PremiumScreen() {
  return (
    <View style={styles.conteiner}>
      <Text style={{color: '#fefdff'}}>Premium!</Text>
    </View>
  )
}

const Tab = createBottomTabNavigator();

function MyTabBar({state, navigation}) {
  return (
    <View style={styles.tabbar}>
      {state.routes.map((route, index) => {

        const icons = {
          Home: 'home',
          Settings: 'settings',
          Premium: 'spotify',
        }
        let isFocused = (state.index == index);

        const _onPress = () => {
          const event = navigation.emit({
            type: 'tabPress',
            target: route.key,
          });
          if (!isFocused && !event.defaultPrevented) {
            navigation.navigate(route.name);
          };
        };

        return (
          <View style={styles.tabButton} key={route.name}>
            <TouchableOpacity
              onPress={_onPress}
              style={{ width:50, height:50, alignItems: 'center', justifyContent: 'center' }}
            >
              <Icon
                name={(isFocused | (route.name == 'Premium')) ? icons[route.name] : icons[route.name] + '-outline'}
                size={28} color={isFocused ? '#fefdff' : '#b7b4b7'}
                style={styles.tabButtonIcon}
              />
              <Text style={[styles.tabLabel, {color: isFocused ? '#fefdff' : '#b7b4b7'}]}>{route.name}</Text>
            </TouchableOpacity>
          </View>
        )
      })}
    </View>
  )
};

function MyTabs() {
  return (
    <Tab.Navigator tabBar={props => <MyTabBar {...props} />}>
      <Tab.Screen name='Home' component={HomeScreen} />
      <Tab.Screen name='Settings' component={SettingsScreen} />
      <Tab.Screen name='Premium' component={PremiumScreen} />
    </Tab.Navigator>
  );
}

export default function App() {
  StatusBar.setBarStyle('light-content', true);
  return (
    <NavigationContainer>
      <MyTabs />
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  conteiner: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#151116',
  },
  tabbar: {
    height: 60 + getBottomSpace(),
    width: '100%',
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#2A272C',
    paddingBottom: getBottomSpace(),
  },
  tabButton: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  tabButtonIcon: {
    marginVertical: 4,
  },
  tabLabel : {
    fontSize: 12,
  }
})

だいぶ完成形に近づいてきました。
あとはアニメーションを追加するだけです。

アニメーションの導入

Spotifyのタブバーをよく観察すると、以下のような点に気が付きます。

  • タブを押しても透明度が上がらない。(TouchbleOpacityはデフォルトで、タップされたらopacityが0.2になるようになっています。)
  • アニメーションはタブを押したあと開始される。
  • アニメーションに緩急がある気がする。アイコンが小さくなるとき、はじめはゆっくり小さくなり、あとから縮小するスピードが上がる気がする。逆に、アイコンが大きくなるとき、はじめは素早く大きくなり、あとから拡大するスピードが下がる気がする

タブを押しても透明度が上がらないようにするには

TouchbleOpacityのactiveOpacityを1.0に指定する。(デフォルトは0.2)

アニメーションがタブを押したあとに開始するようにするには

Promiseを用いて、非同期処理を行う必要があります。
タップ->アイコン&テキスト縮小->アイコン&テキスト拡大->画面遷移という順序で処理が走るようにする必要があります。

TouchbleOpacityをタップすると、まず_onPress()を実行しますが、_onPress()buttonSizeUp()が実行されない限り実行されません。また、buttonSizeUp()buttonSizeDown()が実行されない限り実行されません。

buttonSizeDown()buttonSizeUp()は、animatedIconValue.timing(...).start()animatedTextValue.timing(...).start()setTimeOut()を同時に実行します。setTimeOut()もPromiseの中に記述することによって、アニメーションの値が変化しきる前に次の処理を行うことを阻止しています。

_onPress()内では、画面遷移が行われています。アニメーションの処理が完了する前に遷移してしまうと、アニメーションが途中で終了してしまうので、Promiseを使って、最後に実行されるようにしています。

アニメーションに緩急をつけるには

Animated.Value().interpolate()を用いることで、入力値と出力値をセットで管理できます。

アイコンのサイズに関しては、100フレームに分割したとき、28からスタートし、60フレーム目までに26になり、100フレーム目までに24になるようにしています。
テキストのサイズに関しては、100フレームに分割したとき、12からスタートし、60フレーム目までに11に、100フレーム目までに10になるようにしています。
緩急をつけていると言っても気持ちだけという感じです。

また、拡大するときは、100フレーム目からスタートし、0フレーム目に戻ってくるというイメージです。

import React from 'react';
import { Text, View, StyleSheet, TouchableOpacity, StatusBar, Animated } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { getBottomSpace } from 'react-native-iphone-x-helper'

function HomeScreen() {
  return (
    <View style={styles.conteiner}>
      <Text style={{color: '#fefdff'}}>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={styles.conteiner}>
      <Text style={{color: '#fefdff'}}>Settings!</Text>
    </View>
  );
}

function PremiumScreen() {
  return (
    <View style={styles.conteiner}>
      <Text style={{color: '#fefdff'}}>Premium!</Text>
    </View>
  )
}

const Tab = createBottomTabNavigator();

function MyTabBar({state, navigation}) {
  return (
    <View style={styles.tabbar}>
      {state.routes.map((route, index) => {

        const AnimatedIcon = Animated.createAnimatedComponent(Icon);
        const AnimatedText = Animated.createAnimatedComponent(Text);
        const animatedValue = new Animated.Value(0);
        const interPolateIconSize = animatedValue.interpolate({
          inputRange: [0, 60, 100],
          outputRange: [28, 26, 24],
        });
        const interPolateTextSize = animatedValue.interpolate({
          inputRange: [0, 60, 100],
          outputRange: [12, 11, 10],
        });

        const icons = {
          Home: 'home',
          Settings: 'settings',
          Premium: 'spotify',
        }
        let isFocused = (state.index == index);

        const buttonSizeDown = () => {
          return new Promise((resolve) => {
            Animated.timing(animatedValue, {
              toValue: 100,
              duration: 75,
              useNativeDriver: false,
            }).start();
            setTimeout(() => {
              resolve()
            }, 75)
          })
        }

        const buttonSizeUp = () => {
          return new Promise((resolve) => {
            buttonSizeDown().finally(() => {
              Animated.timing(animatedValue, {
                toValue: 0,
                duration: 75,
                useNativeDriver: false,
              }).start();
              setTimeout(() => {
                resolve()
              }, 75);
              })
            })
          }

          const _onPress = () => {
            buttonSizeUp().finally(() => {
              const event = navigation.emit({
                type: 'tabPress',
                target: route.key,
              });
              if (!isFocused && !event.defaultPrevented) {
                navigation.navigate(route.name);
              };
            })
          }

        return (
          <View style={styles.tabButton} key={route.name}>
            <TouchableOpacity
              onPress={_onPress}
              activeOpacity={1.0}
              style={{ width:50, height:50, alignItems: 'center', justifyContent: 'center' }}
            >
              <AnimatedIcon
                name={(isFocused | (route.name == 'Premium')) ? icons[route.name] : icons[route.name] + '-outline'}
                color={isFocused ? '#fefdff' : '#b7b4b7'}
                style={[styles.tabButtonIcon, {fontSize: interPolateIconSize}]}
              />
              <AnimatedText style={{color: isFocused ? '#fefdff' : '#b7b4b7', fontSize: interPolateTextSize}}>{route.name}</AnimatedText>
            </TouchableOpacity>
          </View>
        )
      })}
    </View>
  )
};

function MyTabs() {
  return (
    <Tab.Navigator tabBar={props => <MyTabBar {...props} />}>
      <Tab.Screen name='Home' component={HomeScreen} />
      <Tab.Screen name='Settings' component={SettingsScreen} />
      <Tab.Screen name='Premium' component={PremiumScreen} />
    </Tab.Navigator>
  );
}

export default function App() {
  StatusBar.setBarStyle('light-content', true);
  return (
    <NavigationContainer>
      <MyTabs />
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  conteiner: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#151116',
  },
  tabbar: {
    height: 60 + getBottomSpace(),
    width: '100%',
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#2A272C',
    paddingBottom: getBottomSpace(),
  },
  tabButton: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  tabButtonIcon: {
    marginVertical: 4,
  },
  tabLabel : {
    fontSize: 12,
  }
})

できた!!!!!!!

これだけのために何時間も調査して実装するのはクレイジーだと思うのですが、神は細部に宿るそうなので、粘り強く開発するのが大切なのではないかと思います。知らんけど。

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

SpotifyのようなタブバーをReact Native (React Navigation 5.x) でつくってみた

TL;DR

本稿では、SpotifyのようなアニメーションつきのタブバーをReact Navigation 5.xで実装する方法についてを紹介します。
こちらが完成形です。

途中でSpotify愛を語りながらグダグダと書きますので、生き急いでる方はgithubリポジトリをご覧ください。

自己紹介

僕は北の大地で統計学、経済学、経営学などを学んでいる文系大学院生(闇)です。

大学3年の秋頃に暇だったのでプログラミングを学び始めました。専門は統計学だったので、「ディープラーニングができたらいいなあ」と思って最初の言語はPythonにしました。僕はいまM1なので、プログラミング歴でいうと2年フラットとなります。

また、今年度に入ってなぜか(コロナで暇だったから)アプリ開発をしたいと思い、React Nativeをやりはじめました。なので、React Native歴で言うと3ヶ月ほどになります。

アプリ開発をするためにSwiftもKotlinもどっちも学ぶのはめんどくせえなと思ってクロスプラットフォームフレームワークを使うことにしました。その中でもReact NativeにしたのはJavaScriptで書けるってところが大きかったですね。あとユーザー数の多さ(大事)

環境

macOS Catalina
node.js 12.17.0
expo-cli 3.21.13

Windows機でも動かせたのでOS間の違いは特にないはずです。

Spotify、マジでカッケェ

僕はSpotifyがホントに好きです。

つい最近『Spotify』という本が発売されました。SpotifyのCEOのダニエル・エクが、Spotifyを世界一のストリーミングプラットフォームにするまでの経緯を描いたノンフィクションです。
読んだら「やっぱりSpotifyはカッケェなあ(小並感」ってなりました。

Spotifyは、他のストリーミングサービス(Apple Music, Amazon Musicなど)と違って、様々な角度から曲を切っていて、様々な角度から新しい曲と出会えるという点で素晴らしいサービスだともともと思っていたんです。
あとからSpotifyはSpotとidentifyからきた造語で、どちらも「見つける」という意味であるということを知って、会社のビジョンがサービスを通じてユーザーに伝わっているのもマジでカッケェなと思いました。

※書籍『Spotify』の中ではダニエル・エクの聞き間違いからSpotifyという名前が生まれたと書かれていました。
image.png

Spotify、タブバーすらカッケェ

まずAppleの「ミュージック」アプリのタブバーを見てください。タブを切り替えても色が変わるだけです。
AppleMusic.gif
次にSpotifyのタブバーを見てください。
Spotify.gif

キモチェエエエエエエエエエエエエ

このアイコンが「ぷにっ」とするのがマジで気持ちいい!!!!!
タブバーに「ぷにっ」としたアニメーションがあるかどうかでA/Bテストを行ったら、ユーザーのアクティブ率とかに有意な差が絶対出ると思います!!!!!

こうして、SpotifyのようなタブバーをReact Native(React Navigation 5.x)で実装したいと僕は考えたわけです。

ちなみに僕はしっかりSpotifyのプレミアム版には登録してるのですが、音楽の趣味がバレると恥ずかしいということで、この画面録画のためだけにもう一つ無料のアカウントを作りました。

React Navigation 5.xのセットアップ

React NavigationはReact Nativeで画面遷移を行うためのパッケージです。タブバーとかドロワーバーとかの実装が比較的簡単にできます。

React Navigationはメジャーアップデートのたびに書き方がガラリと変わるので大変です。。
依存関係にあるパッケージも多いので、最初はReact Navigationを動かすだけで一苦労でした。。

React Navigationを使うには、依存関係にあるパッケージをインストールしておく必要があります。Expo環境を想定しているので、以下のコマンドで依存パッケージは一括でインストールすることができます。

expo install @react-navigation/native react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view

また、今回はタブバーを作成したいので、上記の他に、@react-navigation/bottom-tabsもインストールします。

expo install @react-navigation/bottom-tabs

これでReact Navigation 5.xを使う準備は完了です。

基本となるタブバーを作る

画面中央にHome!と書いてあるだけのHomeScreenというコンポーネントと、同じく画面中央にSettings!と書いてあるだけのSettingsScreenというコンポーネントを定義しています。

下記の通りに記述すれば最も簡単なタブナビゲーションが実装できます。

import React from 'react';
import { Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';

function HomeScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings!</Text>
    </View>
  );
}

const Tab = createBottomTabNavigator();

function MyTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name='Home' component={HomeScreen} />
      <Tab.Screen name='Settings' component={SettingsScreen} />
    </Tab.Navigator>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <MyTabs />
    </NavigationContainer>
  );
}

以下のようになっていれば成功です。
アイコンがないと物足りない感がすごいので、次にアイコンを追加しましょう。

アイコンを追加する

今回はMaterialCommunityIconというアイコンセットを用います。
react-native-vector-icons/MaterialCommunityIconsをインポートします。
expo installでインストールする必要はありません。(Expo環境であれば)

MaterialCommunityIconを見てみると、Spotifyのアイコンもありました。
せっかくなのでこれを入れてタブを3つに増やします。

iconsでスクリーン名とアイコン名をペアにした辞書データを保持しています。
Icon内では、これを用いて、スクリーン名(route.name)から、アイコン名を取得しています。

import React from 'react';
import { Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';

function HomeScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings!</Text>
    </View>
  );
}

function PremiumScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center'}}>
      <Text>Premium!</Text>
    </View>
  )
}

const Tab = createBottomTabNavigator();

function MyTabs() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ color, size }) => {
          const icons = {
            Home: 'home',
            Settings: 'settings',
            Premium: 'spotify'
          };

          return (
            <Icon
              name={icons[route.name]}
              color={color}
              size={size}
            />
          );
        },
      })}
    >
      <Tab.Screen name='Home' component={HomeScreen} />
      <Tab.Screen name='Settings' component={SettingsScreen} />
      <Tab.Screen name='Premium' component={PremiumScreen} />
    </Tab.Navigator>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <MyTabs />
    </NavigationContainer>
  );
}

Spotifyのアイコンが増えているはずです。
アイコンが追加されるとよく見るタブバーになりました。
ここからこだわりが出始めます。

タブの選択でアイコンを変える

SpotifyのタブバーのHomeタブをよく見ると、選択する前と後でアイコンが異なります。Homeを選択していない状態だとアウトラインだけのアイコンで、Homeを選択するとなかの塗りつぶしのあるアイコンに切り替わっています。

これを実装するために、tabBarIconのfocusedというプロパティを用います。これは「現在そのタブが選択されているか」の真偽値です。これを三項演算子の条件式に用いると、簡単にアイコンが切り替わるようにできます。

MaterialCommunityIconにはhome-outlineとsettings-outlineというアイコンはあるのですが、spotify-outlineというアイコンはないので、(focused | (route.name == 'Premium'))として、Premiumタブに関しては常にspotifyアイコンが表示されるようにしています。

import React from 'react';
import { Text, View } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';

function HomeScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
      <Text>Settings!</Text>
    </View>
  );
}

function PremiumScreen() {
  return (
    <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center'}}>
      <Text>Premium!</Text>
    </View>
  )
}

const Tab = createBottomTabNavigator();

function MyTabs() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ color, size, focused }) => {
          const icons = {
            Home: 'home',
            Settings: 'settings',
            Premium: 'spotify'
          };

          return (
            <Icon
              name={(focused | (route.name == 'Premium')) ? icons[route.name] : icons[route.name] + '-outline'}
              color={color}
              size={size}
            />
          );
        },
      })}
    >
      <Tab.Screen name='Home' component={HomeScreen} />
      <Tab.Screen name='Settings' component={SettingsScreen} />
      <Tab.Screen name='Premium' component={PremiumScreen} />
    </Tab.Navigator>
  );
}

export default function App() {
  return (
    <NavigationContainer>
      <MyTabs />
    </NavigationContainer>
  );
}

settingsアイコンがアウトラインだけのアイコンになっているはずです。
Home以外のタブを選択すると、homeアイコンもアウトラインだけになります。

タブバーを自由に作る

もっと柔軟にタブバーを作成することもできます。タブバーを自分でゼロから作り、そこにタブナビゲーションの機能を追加するというイメージです。

React Navigation 5.xの全機能を知っていれば、様々なことができるのでしょうが、タブバーを自分で作ってしまったほうが楽ですし。

ここでせっかくなので、背景色とタブバーの色とアイコンの色をSpotifyのようにしてみようと思います。
テキストの色を白くしないと背景の黒と同化して見えなくなってしまうので、テキストの色も変更してます。
ステータスバーのテキストの色も同様です。StatusBarを用いて簡単に変更できます。

また、このあたりでStyleSheetを用いて、スタイルもまとめて管理しようと思います。

さらに、これまではiPhoneX以降のホームバーの分、画面のボトムのマージンをReact Navigationが自動調整してくれていましたが、自力で調整しなければいけなくなるため、react-native-iphone-x-helperを使います。
expo install react-native-iphone-x-helperでインストールします。
getBottomSpace()は画面サイズからiPhoneX以上かどうかを判定して、iPhoneX以上であれば、ホームバー分の高さを返してくれる関数です。

ちなみにnavigation.emit()は画面遷移を実行するメソッドです。

import React from 'react';
import { Text, View, StyleSheet, TouchableOpacity, StatusBar } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { getBottomSpace } from 'react-native-iphone-x-helper'

function HomeScreen() {
  return (
    <View style={styles.conteiner}>
      <Text style={{color: '#fefdff'}}>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={styles.conteiner}>
      <Text style={{color: '#fefdff'}}>Settings!</Text>
    </View>
  );
}

function PremiumScreen() {
  return (
    <View style={styles.conteiner}>
      <Text style={{color: '#fefdff'}}>Premium!</Text>
    </View>
  )
}

const Tab = createBottomTabNavigator();

function MyTabBar({state, navigation}) {
  return (
    <View style={styles.tabbar}>
      {state.routes.map((route, index) => {

        const icons = {
          Home: 'home',
          Settings: 'settings',
          Premium: 'spotify',
        }
        let isFocused = (state.index == index);

        const _onPress = () => {
          const event = navigation.emit({
            type: 'tabPress',
            target: route.key,
          });
          if (!isFocused && !event.defaultPrevented) {
            navigation.navigate(route.name);
          };
        };

        return (
          <View style={styles.tabButton} key={route.name}>
            <TouchableOpacity
              onPress={_onPress}
              style={{ width:50, height:50, alignItems: 'center', justifyContent: 'center' }}
            >
              <Icon
                name={(isFocused | (route.name == 'Premium')) ? icons[route.name] : icons[route.name] + '-outline'}
                size={28} color={isFocused ? '#fefdff' : '#b7b4b7'}
                style={styles.tabButtonIcon}
              />
              <Text style={[styles.tabLabel, {color: isFocused ? '#fefdff' : '#b7b4b7'}]}>{route.name}</Text>
            </TouchableOpacity>
          </View>
        )
      })}
    </View>
  )
};

function MyTabs() {
  return (
    <Tab.Navigator tabBar={props => <MyTabBar {...props} />}>
      <Tab.Screen name='Home' component={HomeScreen} />
      <Tab.Screen name='Settings' component={SettingsScreen} />
      <Tab.Screen name='Premium' component={PremiumScreen} />
    </Tab.Navigator>
  );
}

export default function App() {
  StatusBar.setBarStyle('light-content', true);
  return (
    <NavigationContainer>
      <MyTabs />
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  conteiner: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#151116',
  },
  tabbar: {
    height: 60 + getBottomSpace(),
    width: '100%',
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#2A272C',
    paddingBottom: getBottomSpace(),
  },
  tabButton: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  tabButtonIcon: {
    marginVertical: 4,
  },
  tabLabel : {
    fontSize: 12,
  }
})

だいぶ完成形に近づいてきました。
あとはアニメーションを追加するだけです。

アニメーションの導入

Spotifyのタブバーをよく観察すると、以下のような点に気が付きます。

  • タブを押しても透明度が上がらない。(TouchbleOpacityはデフォルトで、タップされたらopacityが0.2になるようになっています。)
  • アニメーションはタブを押したあと開始される。
  • アニメーションに緩急がある気がする。アイコンが小さくなるとき、はじめはゆっくり小さくなり、あとから縮小するスピードが上がる気がする。逆に、アイコンが大きくなるとき、はじめは素早く大きくなり、あとから拡大するスピードが下がる気がする

タブを押しても透明度が上がらないようにするには

TouchbleOpacityのactiveOpacityを1.0に指定する。(デフォルトは0.2)

アニメーションがタブを押したあとに開始するようにするには

Promiseを用いて、非同期処理を行う必要があります。
タップ->アイコン&テキスト縮小->アイコン&テキスト拡大->画面遷移という順序で処理が走るようにする必要があります。

TouchbleOpacityをタップすると、まず_onPress()を実行しますが、_onPress()buttonSizeUp()が実行されない限り実行されません。また、buttonSizeUp()buttonSizeDown()が実行されない限り実行されません。

buttonSizeDown()buttonSizeUp()は、animatedIconValue.timing(...).start()animatedTextValue.timing(...).start()setTimeOut()を同時に実行します。setTimeOut()もPromiseの中に記述することによって、アニメーションの値が変化しきる前に次の処理を行うことを阻止しています。

_onPress()内では、画面遷移が行われています。アニメーションの処理が完了する前に遷移してしまうと、アニメーションが途中で終了してしまうので、Promiseを使って、最後に実行されるようにしています。

アニメーションに緩急をつけるには

Animated.Value().interpolate()を用いることで、入力値と出力値をセットで管理できます。

アイコンのサイズに関しては、100フレームに分割したとき、28からスタートし、60フレーム目までに26になり、100フレーム目までに24になるようにしています。
テキストのサイズに関しては、100フレームに分割したとき、12からスタートし、60フレーム目までに11に、100フレーム目までに10になるようにしています。
緩急をつけていると言っても気持ちだけという感じです。

また、拡大するときは、100フレーム目からスタートし、0フレーム目に戻ってくるというイメージです。

import React from 'react';
import { Text, View, StyleSheet, TouchableOpacity, StatusBar, Animated } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { getBottomSpace } from 'react-native-iphone-x-helper'

function HomeScreen() {
  return (
    <View style={styles.conteiner}>
      <Text style={{color: '#fefdff'}}>Home!</Text>
    </View>
  );
}

function SettingsScreen() {
  return (
    <View style={styles.conteiner}>
      <Text style={{color: '#fefdff'}}>Settings!</Text>
    </View>
  );
}

function PremiumScreen() {
  return (
    <View style={styles.conteiner}>
      <Text style={{color: '#fefdff'}}>Premium!</Text>
    </View>
  )
}

const Tab = createBottomTabNavigator();

function MyTabBar({state, navigation}) {
  return (
    <View style={styles.tabbar}>
      {state.routes.map((route, index) => {

        const AnimatedIcon = Animated.createAnimatedComponent(Icon);
        const AnimatedText = Animated.createAnimatedComponent(Text);
        const animatedValue = new Animated.Value(0);
        const interPolateIconSize = animatedValue.interpolate({
          inputRange: [0, 60, 100],
          outputRange: [28, 26, 24],
        });
        const interPolateTextSize = animatedValue.interpolate({
          inputRange: [0, 60, 100],
          outputRange: [12, 11, 10],
        });

        const icons = {
          Home: 'home',
          Settings: 'settings',
          Premium: 'spotify',
        }
        let isFocused = (state.index == index);

        const buttonSizeDown = () => {
          return new Promise((resolve) => {
            Animated.timing(animatedValue, {
              toValue: 100,
              duration: 75,
              useNativeDriver: false,
            }).start();
            setTimeout(() => {
              resolve()
            }, 75)
          })
        }

        const buttonSizeUp = () => {
          return new Promise((resolve) => {
            buttonSizeDown().finally(() => {
              Animated.timing(animatedValue, {
                toValue: 0,
                duration: 75,
                useNativeDriver: false,
              }).start();
              setTimeout(() => {
                resolve()
              }, 75);
              })
            })
          }

          const _onPress = () => {
            buttonSizeUp().finally(() => {
              const event = navigation.emit({
                type: 'tabPress',
                target: route.key,
              });
              if (!isFocused && !event.defaultPrevented) {
                navigation.navigate(route.name);
              };
            })
          }

        return (
          <View style={styles.tabButton} key={route.name}>
            <TouchableOpacity
              onPress={_onPress}
              activeOpacity={1.0}
              style={{ width:50, height:50, alignItems: 'center', justifyContent: 'center' }}
            >
              <AnimatedIcon
                name={(isFocused | (route.name == 'Premium')) ? icons[route.name] : icons[route.name] + '-outline'}
                color={isFocused ? '#fefdff' : '#b7b4b7'}
                style={[styles.tabButtonIcon, {fontSize: interPolateIconSize}]}
              />
              <AnimatedText style={{color: isFocused ? '#fefdff' : '#b7b4b7', fontSize: interPolateTextSize}}>{route.name}</AnimatedText>
            </TouchableOpacity>
          </View>
        )
      })}
    </View>
  )
};

function MyTabs() {
  return (
    <Tab.Navigator tabBar={props => <MyTabBar {...props} />}>
      <Tab.Screen name='Home' component={HomeScreen} />
      <Tab.Screen name='Settings' component={SettingsScreen} />
      <Tab.Screen name='Premium' component={PremiumScreen} />
    </Tab.Navigator>
  );
}

export default function App() {
  StatusBar.setBarStyle('light-content', true);
  return (
    <NavigationContainer>
      <MyTabs />
    </NavigationContainer>
  );
}

const styles = StyleSheet.create({
  conteiner: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#151116',
  },
  tabbar: {
    height: 60 + getBottomSpace(),
    width: '100%',
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: '#2A272C',
    paddingBottom: getBottomSpace(),
  },
  tabButton: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  tabButtonIcon: {
    marginVertical: 4,
  },
  tabLabel : {
    fontSize: 12,
  }
})

できた!!!!!!!

これだけのために何時間も調査して実装するのはクレイジーだと思うのですが、神は細部に宿るそうなので、粘り強く開発するのが大切なのではないかと思います。知らんけど。

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

"Kill sticky headers"の動作解説 ーWebサイト閲覧を快適にー

はじめに

はじめまして、@wa-cordと申します。

"Kill sticky headers"というブックマークレットをご存知でしょうか。Webサイト閲覧時、サイト上部に固定されているヘッダーなどを消してくれるものです。Alisdair McDiarmid氏が2013年に公開しました。

公式ページ:
https://alisdair.mcdiarmid.org/kill-sticky-headers/

私は愛用しています。非常に便利なものだと感じています。このブックマークレットのおかげでWebサイト閲覧が快適になりました。
Javascriptを学び、動作を理解することができたので、解説したいと思います。公式ページにも解説がありますが、初心者でも分かりやすいように丁寧に説明していきます。なお、Alisdair McDiarmid氏本人から紹介の許可を得ております。

使用方法

動作の解説の前に使用方法を説明します。簡単です。

  1. 公式ページの"Kill Sticky"という青いボタンを、ブックマークバーにドラッグ&ドロップする。(Google Chromeの場合)
  2. 追加されたブックマークレットをクリックする。

こうすると、公式ページの上部にあるヘッダーが消えます。ブックマークレットをクリックするだけで、他のWebページでも同様に動作します。消したヘッダーなどを元に戻したいときは、そのページをリロードします。

動作解説

ブックマークレットとは

ブックマークレットについて簡単に説明します。

ブックマークレット (Bookmarklet) とは、ユーザーがウェブブラウザのブックマークなどから起動し、なんらかの処理を行う簡易的なプログラムのことである。(Wikipediaより)

Wikipediaにある説明のとおりです。"Kill sticky headers"では、Javascriptで処理が記載されています。

"Kill sticky headers"の動作解説

"Kill sticky headers"のソースコードはこちらです。

1  (function () { 
2    var i, elements = document.querySelectorAll('body *');
3  
4    for (i = 0; i < elements.length; i++) {
5      if (getComputedStyle(elements[i]).position === 'fixed') {
6        elements[i].parentNode.removeChild(elements[i]);
7      }
8    }
9  })();

2行目で、elementsという変数を定義しています。querySelectorAllメソッドによって、HTMLファイルのbody要素の中にあるすべての要素を取得し、配列の形でelementsに代入しています。(作成者は、このquerySelectorAllをawesomeと表現しています。私も同意です。)
続いて、4行目から8行目にかけてです。すべてのelementsに対して、「Styleのpositionがfixed」となっているものに6行目の処理をさせています。6行目は、対象となったelments要素をremoveChildメソッドにより削除するという処理です。

おわりに

"Kill sticky headers"というBookmarkletを紹介し、その動作を解説しました。短いコードでこんなにも便利なものが作れるということに感動します。作成者のAlisdair McDiarmid氏を尊敬します。最後までお読みいただき、ありがとうございます。

謝辞

紹介を快諾していただいたAlisdair McDiarmid氏に感謝申し上げます。

参考

公式ページ:https://alisdair.mcdiarmid.org/kill-sticky-headers/
querySelectorAll:https://www.w3.org/TR/selectors-api/#examples
https://gigazine.net/news/20170629-kill-sticky-headers/

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

Reactのレンダリング視覚的なガイド

Reactはどのタイミングで、なぜ再レンダリングするかを説明する視覚的なガイド。
もともとの記事は文字が多く、例も編集できないため、わかりやすいバージョンを作った。

https://will-it-render.vercel.app/

もともとの記事:https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/
翻訳:https://qiita.com/hellokenta/items/6b795501a0a8921bb6b5

中身

ガイドにはほとんど以下のような画像と編集できるCodesandboxの例がいくつか入っている。

u95az2s88db5h5uhnae5.png

一つの独立したページとして作っているので、上記でリンクをシェア。

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

Reactのレンダリング:視覚的なガイド

Reactはどのタイミングで、なぜ再レンダリングするかを説明する視覚的なガイド。
もともとの記事は文字が多く、例も編集できないため、わかりやすいバージョンを作った。

https://will-it-render.vercel.app/

もともとの記事:https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/
翻訳:https://qiita.com/hellokenta/items/6b795501a0a8921bb6b5

中身

ガイドにはほとんど以下のような画像と編集できるCodesandboxの例がいくつか入っている。

u95az2s88db5h5uhnae5.png

一つの独立したページとして作っているので、上記でリンクをシェア。

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

Payjp v2ざっくり実装

最近Payjpのv2なるものがリリースされたらしく、Payjp側もセキュリティの向上のためv2の使用を推奨している?らしい。
少し前に質問があり、気になったので実装してみました。忘れないようにメモとして記事作りました。
細かい説明は省きます。リファレンス見れば誰でも簡単に実装できます!!

v1との違いは?

v1ではトークン化APIのみを提供しており、カード情報入力フォームは加盟店側で用意する必要がありました。

しかしより安全なクレジットカード商取引のために、最新のPCI-DSSでは、カード情報入力フォームを決済代行業者側で用意することが求められています。
で、
payjp.js v2では、これらのフォームを弊社ドメインのiframe内で用意し、かつ独自スタイルの適用・イベント監視などv1で実現できた機能を提供いたします。 加えて、カード番号入力時の自動フォーマットやレスポンシブデザインなど、ニーズの高かった機能をデフォルトで提供致します。

との事です。Payjp側で用意された入力フォームを使えるので、わざわざ作らなくて良い感じらしいですね。
ところでPCI-DSSってセキュリティの基準か何かですね?金融業界出身の方いたら、教えて欲しいです。

ざっくりと実装

- application.html.hamlにv2のcdnを記載
%head
  %script{src: "https://js.pay.jp/v2/pay.js", type: "text/javascript"}
  .....

payjp.jsコード

$(function(){
  if(document.location.pathname !== "/cards/new") return false;
  // カード登録ページじゃい時は処理を実行しない
  const payjp = Payjp('pk_test_7370ce03239ee60f10ca694c') //公開鍵を読み込む。
  // Payjp.setPublicKey('pk_...................')にしなくて良いらしい
  const elements = payjp.elements();
  // payjpのインスタンス生成
  const cardElement = elements.create('card', {style: {base: {color: 'black'}}})
  // ここでformを生成してる。createの第一引数には、「card」「cardNumber」「cardExpiry」「cardCvc」とかのタイプを選んで作れる。
  // cardだとカード番号、有効期限、cvcの3つをまとめて横並びにしたフォームを生成する
  // フォームを分けたい人は「cardNumber」「cardExpiry」「cardCvc」を引数にして作ると良いです。
  cardElement.mount('#card-element');
  // 任意のセレクタ(#card-element)に対してiframe(入力フォームを付与する)
  const submit_btn = $("#info_submit")
  // いつものリファクタ


  submit_btn.click(function(e) {
    e.preventDefault();
    // submitしないように止める
    payjp.createToken(cardElement).then(function(response) { 
      if (response.error) {
        // Payjp側からの返ってくるオブジェクトがerrorオブジェクトを持ってた場合
        alert(response.error.message)
        // どの情報に対して不備があるのか教えてくれる。
     // 下記に記載しているが該当箇所のエラーを知らせてくれる
        regist_card.prop('disabled', false);
        // いつものやつ
        return ;
      }
      else {
        alert("カード登録が完了しました")
        $("#card_token").append(
            `<input type="hidden" name="payjp_token" value=${response.id}>
             <input type="hidden" name="card_token" value=${response.card.id}>`
            //  これもいつものやつ
         );
      cardElement.clear()
    // 入力情報を消す
      $('#card_form')[0].submit()
      }

    })
  });

});

ビュー

.mypage.horizontal-padding-15
  = render 'shared/side_bar'
  .mypage-main
    .block.horizontal-padding-25
      .block__menu
        支払い方法
    .block.horizontal-padding-25
      #card-element
    -# ↑これ追加しただけ。この#card-elemntがマウントされるセレクタ(名前は何でも良い)
      #card-icons
        = image_tag 'icon_visa.png'
        = image_tag 'icon_master.jpg'
        = image_tag 'icon_saison.png'
        = image_tag 'icon_jcb.png'
        = image_tag 'icon_american.svg'
        = image_tag 'icon_diners.png'
        = image_tag 'icon_discover.png'
      = form_with model: @card, id: "card_form" do |f|
        #card_token
        = f.submit "次へ進む", class:"button back-red font-white", id: "info_submit"

実際のカード情報入力ページ

スクリーンショット 2020-07-10 11.11.03.png
スクリーンショット 2020-07-10 11.14.49.png

入力に不備があると該当箇所に対してアラートを出すこともできる。(これはv1でもできるのか?)

スクリーンショット 2020-07-10 11.17.32.png

使ってみた所感

入力フォームはpayjpに任せて実装してもいいかなと。jsで完結するし
最終的にrailsで@cardに情報入れたいのでformなどは使わないといけないですが・・・。

好きな方使えば良いって感じです。

今回は特にリファクタなど一切してませんのでお許しを!
間違えた実装してたらすみません!

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

Google タグマネージャーで子孫要素のクリックイベントもきっちり検出する

Google タグ マネージャー (以下 GTM と表記)にはクリックイベントを検出するためのトリガータイプとして「すべての要素」と「リンクのみ」の二種類が用意されています。「すべての要素」を利用するとリンクだけでなくボタンやフォームなどの要素もターゲットに指定することができますが、ターゲットの子孫要素のイベントは検出してくれません。

この仕様は Google Analytics (以下 GA と表記)などの外部サービスにクリックのイベントデータを送出するといったユースケースで問題となる可能性があります。例えば以下のような HTML において <button> をイベントターゲットに指定した場合、子要素の <i> がクリックされてもトリガーはそのイベントを検出しません。

<button>  <!-- button 要素のクリックは検出される -->
  <i class="material-icons">close</i>  <!-- icon 要素のクリックは検出されない! -->
</button>

この記事では、子孫要素のクリックイベントも含めてターゲットのイベントとして扱う方法について考えてみます。

TL;DR

  • トリガーの発生条件に CSS セレクタでターゲット要素とその子孫要素の両方を指定する。
  • カスタム JavaScript の変数として、クリックされた要素に最も近いターゲット要素(の属性値など)を返す関数を作成する。

シチュエーション

GTM を経由して、クリックイベントをトリガーに GA へイベントデータを送出するシチュエーションについて考えます。例えば以下のような HTML について考えます。

<body>
  <a href="https://example.com/products/999"
     target="_blank"
     data-tracking-category="product-list"
     data-tracking-action="click-product"
     data-tracking-label="thumbnail">
    <div class="thumbnail-wrapper">
      <p class="product-name"></p>
      <img class="thumbnail" src="thumbnail.jpg">
      <div class="logo-wrapper">
        <img class="logo" src="logo.jpg">
      </div>
    </div>
  </a>
</body>

ここで GTM のコンテナスニペットは省略しています。

ターゲット要素に data-tracking-category といった属性を仕込んでおき、この値によってどの要素がクリックされたか識別できるようにしています。 GA のイベント要素については公式のヘルプページを参照してください。

Google Tag Manager の設定

トリガー

まず data-tracking-category を属性に持つ要素、およびその子孫要素のクリックイベントを検出するトリガーを作成します。今回は Click - Data Tracking という名前にして、以下のように設定します。

  • トリガーのタイプ: 「クリック - すべての要素」
  • このトリガーの発生場所: 「一部のクリック」
  • イベント発生時にこれらすべての条件が true の場合にこのトリガーを配信します:
    • 「Click Element」
    • 「CSS セレクタに一致する」
    • [data-tracking-category], [data-tracking-category] *

[data-tracking-category] でターゲット要素を、 [data-tracking-category] * でその子孫要素をそれぞれ取得することができます。

変数

次にクリックされた箇所の data-tracking-category などの値を格納する変数を作成します。通常、トリガーによって検出されたイベントの発生した要素(ここではクリックされた要素)の情報は「自動イベント変数」を使って取得することができます。ただし今回の場合、子孫要素がクリックされた場合に親の data-tracking-category を取得する必要があり、「自動イベント変数」ではこれを実現できません。

この問題の解決方法について Simo Ahava さんのブログ記事を参考にさせてもらいました。まず utility 関数として Find Closest という変数を作成します。

Find Closest
function() {
  return function(target, selector) {
    while (!target.matches(selector) && !target.matches('body')) {
      target = target.parentElement;
    }
    return target.matches(selector) ? target : undefined;
  }
}

この変数では引数 selector に渡された CSS セレクタに一致する要素が見つかるまで target から再帰的に親要素を辿ります。この引数に [data-tracking-category] を指定することで、 data-tracking-category 属性を持つ親要素が取得できるという寸法です。

実際に data-tracking-category の値を取得する変数を Closest Data Tracking Category という名前で以下のように作成します。

Closest Data Tracking Category
function() {
  var el = {{Find Closest}}({{Click Element}}, '[data-tracking-category]');
  return typeof el !== 'undefined' ? el.getAttribute('data-tracking-category') : undefined;
}

{{Click Element}} は GTM の組み込み変数で、名前の通りクリックされた要素が格納されます。
これと同様に Closest Data Tracking ActionClosest Data Tracking Label も作成しておきます。

タグ

最後に GA に情報を送るためのタグを作成します。

  • タグの設定
    • タグの種類: 「Google アナリティクス: ユニバーサル アナリティクス」
    • トラッキング タイプ: 「イベント」
    • カテゴリ: {{Closest Data Tracking Category}}
    • アクション: {{Closest Data Tracking Action}}
    • ラベル: {{Closest Data Tracking Label}}
  • トリガー
    • Click - Data Tracking

その他 GA の設定などは環境に合わせて適宜行ってください。

動作確認

最後にプレビューモードで動作確認します。トラッキングしたい要素およびその子孫要素をクリックし、イベントが発火すること、変数に目的の値が設定されていることが確認できればオーケーです!

!

!

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

GitHub Pagesで公開するときWebpackのパス関係で少し詰まった話

GitHub Pagesはとても手軽に公開ができ、
自分のようなフロントエンドエンジニアにはとても重宝しています。

ですが、Webpackを用いたページ作成の際、躓き、
これといった解決方法がWeb上に見つからず、自己解決したので、解決方法を残しておきます

どんな問題か?

GitHub Pagesは通常、

https://userName.github.io/repository/

というURLがルートとなる構成となっています。(userNameとrepositoryは任意)

その際、ルートディレクトリが1階層ずれてしまい、その影響で

fetch('../data/hoge.json')

といったファイルを読み込もうとしたときパスがずれてしまい404エラーとなってしまいました。

問題発生時の環境

WebpackにはJSとSASS(Scss)だけを通して、JSとCSSで出力していました。

長くなってしまったので各詳細はまとめてあります。


ディレクトリ構成
├ src/                <- 開発用ディレクトリ
│ ├ script/
│ │ ├ entry.js
│ │ ├ fetchJson.js
│ │ ├ ...
│ │
│ ├ data/
│ │ └ hoge.json
│ │
│ ├ style/
│ │ └ fuga.scss
│ │
│ └ index.html
│
├ dist/               <- コンパイル先ディレクトリ
│ ├ script/
│ │ └ main.js
│ │
│ ├ data/
│ │ └ hoge.json
│ │
│ ├ style/
│ │ └ style.css
│ │
│ └ index.html
│
├ docs/               <- 掲載時ディレクトリ(公開用ディレクトリ)
│ ├ script/
│ │ └ main.js
│ │
│ ├ data/
│ │ └ hoge.json
│ │
│ ├ style/
│ │ └ style.css
│ │
│ └ index.html
│
├ package.json
├ webpack.common.js
├ webpack.dev.js
├ webpack.prod.js
...

無駄なファイル、要素などは省いて書いたつもりですが、それでも長くなっちゃいますね。



Webpackの設定

common, dev, prodで3つに分けています。

webpack.common.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
var webpack = require('webpack');

module.exports = {
  entry: path.resolve('src', 'script', 'entry.ts'),
  output: {
    filename: 'main.js',
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '../style/style.css',
    }),
  ]
};

webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const sourceMap = true;
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = merge(common, {
  mode: 'development',
  watch: true,
  devtool: 'source-map',
  output: {
    path: path.resolve(__dirname, 'dist', 'script'),
  },
  module: {
    rules: [
      {
        test: /\.(sc|c|sa)ss$/,
        use: [
          { loader: MiniCssExtractPlugin.loader, },
          {
            loader: 'css-loader',
            options: { url: false, sourceMap, importLoaders: 2, },
          },
          {
            loader: 'sass-loader',
            options: { sourceMap, },
          },
        ],
      },
    ],
  },
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    port: 3000,
    watchContentBase: true,
    hot: true,
    open: true,
  },
});
webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = merge(common, {
  mode: 'production',
  output: {
    path: path.resolve(__dirname, 'docs', 'script'),
  },
  module: {
    rules: [
      {
        test: /\.(sc|c|sa)ss$/,
        use: [
          { loader: MiniCssExtractPlugin.loader, },
          {
            loader: 'css-loader',
            options: { url: false, importLoaders: 2, },
          },
          {
            loader: 'sass-loader',
          },
        ],
      },
    ],
  },
});


これで、jsonファイルを読みにいこうすると

async () => {
  await ('../data/hoge.json')
};

開発環境ではOKですが、

GitHub上ではエラーになります

GET https://userName.github.io/data/hoge.json 404 (Not Found)

読みに行こうとしているパスが1つがずれています。

正しくは

https://userName.github.io/repository/data/hoge.json

にファイルを取りに行きたいのです。

ですが、webpackの設定をいろいろ調整してみましたが、上手くいきません...。
(1日以上無駄にした)

解決策

「webpackの設定が悪いんだ!」と自分に暗示をかけていたのが悪かったです。

1歩後ろに下がって観察してみると簡単に解決策が見つかりました。
(簡単に解決策が見つかりました...。)

srcディレクトリ構成を変える

これが僕の答えです。

srcのすぐ下で開発しないといけないという
謎の固定概念にとらわれていました。

├ src
│ └ repository
│   └ index.html

という形でsrcと開発ファイルの間に1つディレクトリをはさみ、

コンパイル時に

├ dist
│ └ repository
│   └ index.html
└ docs
  └ index.html

というようにディレクトリの階層を変えてあげることで
開発サーバー上でもGitHub Pagesでもエラーが起きず表示することができました!

解決時のディレクトリ構成・webpack設定


【解決】ディレクトリ構成
├ src/           <- 開発用ディレクトリ
│ └ repository/      <- 1個ディレクトリを追加することで解決!
│   ├ script/
│   │ ├ entry.js
│   │ ├ fetchJson.js
│   │ ├ ...
│   │
│   ├ data/
│   │ └ hoge.json
│   │
│   ├ style/
│   │ └ fuga.scss
│   │
│   └ index.html
│
├ dist/          <- コンパイル先ディレクトリ
│ └ repository/      <- 1個ディレクトリをはさみで解決!
│   ├ script/
│   │ └ main.js
│   │
│   ├ data/
│   │ └ hoge.json
│   │
│   ├ style/
│   │ └ style.css
│   │
│   └ index.html
│
├ docs/           <- 掲載時ディレクトリ(公開用ディレクトリ)
│ │                   <- ここはディレクトリをはさめない!
│ ├ script/
│ │ └ main.js
│ │
│ ├ data/
│ │ └ hoge.json
│ │
│ ├ style/
│ │ └ style.css
│ │
│ └ index.html
│
├ package.json
├ webpack.common.js
├ webpack.dev.js
├ webpack.prod.js
...



【解決】Webpackの設定

common, dev, prodで3つに分けています。

webpack.common.js
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
var webpack = require('webpack');

module.exports = {
  // repositoryフォルダを追加
  entry: path.resolve('src', 'repository', 'script', 'entry.ts'),
  output: {
    filename: 'main.js',
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '../style/style.css',
    }),
  ]
};

webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const sourceMap = true;
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = merge(common, {
  mode: 'development',
  watch: true,
  devtool: 'source-map',
  output: {
    // repositoryフォルダを追加
    path: path.resolve(__dirname, 'dist', 'repository', 'script'),
  },
  module: {
    rules: [
      {
        test: /\.(sc|c|sa)ss$/,
        use: [
          { loader: MiniCssExtractPlugin.loader, },
          {
            loader: 'css-loader',
            options: { url: false, sourceMap, importLoaders: 2, },
          },
          {
            loader: 'sass-loader',
            options: { sourceMap, },
          },
        ],
      },
    ],
  },
  devServer: {
    contentBase: path.join(__dirname, 'dist', 'repository'),
    port: 3000,
    watchContentBase: true,
    hot: true,
    open: true,
  },
});
webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = merge(common, {
  mode: 'production',
  output: {
    // ここはそのまま!
    path: path.resolve(__dirname, 'docs', 'script'),
  },
  module: {
    rules: [
      {
        test: /\.(sc|c|sa)ss$/,
        use: [
          { loader: MiniCssExtractPlugin.loader, },
          {
            loader: 'css-loader',
            options: { url: false, importLoaders: 2, },
          },
          {
            loader: 'sass-loader',
          },
        ],
      },
    ],
  },
});


所感

  • Webpackというものはとても便利という反面、
    どうゆう動きをしているのか把握するのは難しい。
  • ここが悪いという決めつけが問題解決を遅らせてしまうというのは
    コーディング時によくあるはなし。細かいところを見る視点を持ちながら俯瞰な視点も忘れずに開発したい。

本音はこれがベストプラクティスとは思えないというところです。
Webpackの知見がある方、是非、もっとよい解決策を教えてください!

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

【自分用メモ】JavaScript

Node.js

npmとYarnの違い

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

[JavaScript]お手軽に文字列を(ほぼ)衝突させないで生成する方法

例えば、ユーザ登録用のダミー値を複数かつ衝突させないで用意したい場合がありますよね。
そんな時にちょっとした工夫で、文字列を生成できる方法を紹介します。

実装

new Date().getTime().toString(36); // => kcexh9g6

解説

ミリ秒をX進数に変換するだけで。お手軽に文字列を生成することができます。
toString()に2-36の間の数を与えるとその進数へ変換できます。
また、Date().getTime()は1970年1月1日00:00:00から経過したミリ秒を返すので、
この二つを利用すれば桁数も自由に変えることも可能です。

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

初心者向け!JavaScriptの演算子について解説! byウェブカツ

JavaScriptに限らず、どのプログラミング言語でも共通する基礎には
「演算子」「条件文」「繰り返し」「変数」「関数」
などがあり、まずはこの基礎中の基礎の理解が必要となる。

今回はその中から「JavaScript」での「演算子」について解説します。

「演算子」とは?

演算子とは主に「データの代入・計算・比較」を担当する様々な記号のこと。

演算子の種類と解説

初心者は最初から全部を網羅する必要はないので、基本的な内容をピックアップしました。

代入演算子

「代入演算子」とは?

簡単に言うと、データを変数に代入するためのものです。
最もよく使用されるのが「=」記号。

学生時代にやった算数や数学では、「=」は「等しい」という意味で使っていたので、慣れるまで違和感があると思うが、プログラミングでは「等しい」ではなく「代入する」という意味で使われます。

代入:(=)記号

さきほども説明したように、代入演算子は「代入」をします。
「=」の右側と左側にわけて見ていただきたいのですが、「右側を左側に代入」します。

x=5;  //xに5を代入します。 この時点でxは5です。
y=x;  //yにxの値である5を代入します。 この時点でyは5です。
y=y+8; //(左側の)yには(右側の)yに8を加えた値を代入します。 この時点で(左側の)yは13となります。 

//2行目の時点でyは5でした。
//3行目の右辺「y+8」では、中身が5だったyに、さらに8を加えました。
//それを左辺のyに代入しなおしています。
//yの中身を上書きしているというイメージがわかりやすいでしょう。
//なので、3行目で(左側の)yは13になるのです。

繰り返しますが、「等しい」ではなく「右側から左側への代入」です。

加算代入:(+=)記号

加算代入では、左側の変数に右側の値を加えた値で上書きします。

先ほどの例で

y=y+8;

と出てきましたが、これを省略する書き方ができるのが加算代入です。

y=5;  //yに5を代入します。この時点でyは5です。 
y+=8; //y=y+8と同じことを意味します。 この時点でyは13となります。

プログラミングをする際には「元々の変数の値に何かの値を足して上書きしたい」ということが頻繁にでてきます。
その場合に少しでも簡潔にコードをかけるように作られたのがこの代入演算子ですね。
少しややこしいかもしれませんが、これが理解できると、この後に紹介する減算代入・乗算代入・除算代入も同じ理屈ですのですぐ理解できます。

減算代入:(-=)記号

減算代入では、左側の変数に右側の値を引いた値で上書きします。

x=5;  //xに5を代入します。 この時点でxは5です。
x-=3; //x=x-3と同じことを意味します。 この時点でxは2となります。

乗算代入:(*=)記号

乗算代入では、左側の変数に右側の値をかけ算した値で上書きします。
(後ほどご紹介しますが「*」記号は「かける」を意味します。)

x=8;  //xに8を代入します。 この時点でxは8です。
x*=6; //x=x*6と同じことを意味します。 この時点でxは48となります。

除算代入:(/=)記号

乗算代入では、左側の変数に右側の値をかけ算した値で上書きします。
(後ほどご紹介しますが「/」記号は「割る」を意味します。)

x=20;  //xに20を代入します。 この時点でxは20です。
x/=5; //x=x/5と同じことを意味します。 この時点でxは4となります。

算術演算子

算術演算子は数値を計算する際に使用します。

加算:(+)記号

足し算です。

x=y+z;  //「y+z」は「yにzを加える」という意味です。

//例
y=17;
x=y+8; //xは25となります。

減算:(-)記号

引き算です。

x=y-z;  //「y-z」は「yからzを引く」という意味です。

//例
y=15;
x=y-8; //xは7となります。

乗算:(*)記号

かけ算です。

x=y*z;  //「y*z」は「yにzをかける」という意味です。

//例
y=7;
x=y*8; //xは56となります。

除算:(/)記号

割り算です。

x=y/z;  //「y/z」は「yをzで割る」という意味です。

//例
y=48;
x=y/6; //xは8となります。

剰余:(%)記号

割った際の余りです。

x=y%z;  //「y%z」は「yをzで割った際の余り」という意味です。

//例
y=7;
x=y%3; //xは1となります。

インクリメント演算子:(++)記号

変数の値を1増やします。

//例
y=7;
y++; //yは8となります。

デクリメント演算子:(--)記号

変数の値を1減らします。

//例
x=6;
x--; //xは5となります。

比較演算子

比較演算子は2つの値が比較して、true(真)もしくはfalse(偽)を返します。
通常これらはif分の条件判定で使います。

厳密等価演算子:3つのイコール記号(===)

記号の左側と右側の値が「等しい、かつ、同じ型かどうか」を判定します。

判定の結果、
等しい、かつ、同じ型であれば「true」を返します。
等しい、かつ、同じ型でなければ「false」を返します。

//例
x=6;
y=6;
z="6";

result = x === y; //変数resultの中身は「true」
result = x === z; //変数resultの中身は「false」

//2行目、3行目の6は「数値型」として扱います。
//4行目の"6"は「""」で囲んでいるので、「文字列型」として扱います。
//7行目では、同じ6ですが型が異なると判定されます。
//変数の値には「型」があることを覚えておきましょう。

厳密不等価演算子:2つの等号の前にエクスクラメーション記号(!==)

記号の左側と右側の値が「等しい、かつ、同じ型ではないどうか」を判定します。

判定の結果、
等しい、かつ、同じ型でなければ「true」を返します。
等しい、かつ、同じ型であれば「false」を返します。

//例
x=6;
y=6;
z="6";

result = x !== y; //変数resultの中身は「false」
result = x !== z; //変数resultの中身は「true」

//「厳密等価演算子(===)」とは真逆ですね。

等価演算子:2つのイコール記号(==)

記号の左側と右側の値が「等しいかどうか」を判定します。型は判定しません。

判定の結果、
等しければ「true」を返します。
等しくなければ「false」を返します。

//例
x=6;
y=6;
z="6";

result = x == y; //変数resultの中身は「true」
result = x == z; //変数resultの中身は「true」

//「厳密等価演算子」とほぼ同じ例ですが、こちらは型が異なっても等しいと判定されます。

型が異なっても「等しい」と判定してしまうのは思わぬバグの原因となることがあります。ですので、特別な理由がない限り「等価演算子」ではなく「厳密等価演算子」を使用するようにするべきですね。

不等価演算子:等号の前にエクスクラメーション記号(!=)

記号の左側と右側の値が「等しいかどうか」を判定します。

判定の結果、
等しければ「false」を返します。
等しくなければ「true」を返します。

//例
x=6;
y=8;
z="6";

result = x != y; //変数resultの中身は「true」
result = x != z; //変数resultの中身は「false」

//「等価演算子(==)」と逆ですね。

こちらも「等価演算子」と同じ理由ですが、特別な理由がない限り「厳密不等価演算子」を使用するようにしましょう。

小なり演算子(より小さい):小なり記号(<)

記号の左側が右側の値よりも「小さいどうか」を判定します。
判定の結果、
小さければ「true」を返します。
そうでなければ「false」を返します。

小なりイコール演算子(より小さいまたは等しい):小なりイコール記号(<=)

記号の左側が右側の値よりも「小さい、もしくは等しいかどうか」を判定します。

判定の結果、
小さい、もしくは等しければ「true」を返します。
そうでなければ「false」を返します。

大なり演算子(より大きい):大なり記号(>)

記号の左側が右側の値よりも「大きいどうか」を判定します。

判定の結果、
大きければ「true」を返します。
そうでなければ「false」を返します。

大なりイコール演算子(より大きいまたは等しい):大なりイコール記号(>=)

記号の左側が右側の値よりも「大きい、もしくは等しいかどうか」を判定します。

判定の結果、
大きい、もしくは等しければ「true」を返します。
そうでなければ「false」を返します。

まとめ

JavaScriptの学習を始めたばかりの初心者であれば、まずはこの演算子をマスターしてください。
他にもいろいろありますが、それは後から少しずつ覚えればOKです。


かずきち

プログラミング学習サイト「ウェブカツ!!」の顧問。
不動産、保険の営業マンから、エンジニアへ転身。
「HTMLって何?」という状態から3ヶ月の独学のみでエンジニアへ転職し、1年で年収1千万を稼ぐエンジニアへ。
独学時代のプログラミング学習の分かりにくさや、「技術しか出来ずに稼げていないエンジニア」の現状を変えるため「ウェブカツ」を立ち上げ運営している。

【ウェブカツ公式WEBサイト】
https://webukatu.com/

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

Visual Studio Code で Nuxt.js を使った Docker コンテナ内での開発

はじめに

以前、環境を汚さず(選ばず)Nuxtプロジェクトを作成しGitHub Pagesで公開するまでの一部始終 という記事を書きました。
その後、より簡単にNuxtの開発環境を作れないかと何回かやっているうちに、
Visual Studio Code をうまく使えばよりシンプルにできることがわかってきたので、再度まとめました。
Nuxtのバージョンが上がってプロジェクト作成時の設定が若干変わっています。


事前準備

Docker, Visual Studio Codeをインストールしておきます。

これからインストールする方は以下を参考にしてください。

Windows 10 Home に Docker for Desktop をインストールする手順

Visual Studio Code のインストール手順(Windows)

※今回は Windows 10 Home 環境で作業していますが、Docker, Visual Studio Code が入っていればどのOSでも同じようにできるはずです。

今回の Nuxt.js のバージョンは 2.13.3 です。


Visual Studio Code の起動

Visual Studio Code を起動します。

image.png


プロジェクトフォルダの作成

メニュー File / Open Foder... を選択し、プロジェクト用フォルダを作成して開きます。

image.png

今回は C:\dev\nuxt_in_docker というフォルダを作成しました。
image.png

Welcome ページは不要ですので × ボタンで消します。
image.png

image.png


Dockerファイルの作成

ファイル作成ボタンimage.pngを押し、 Dockerfile を作成します。

image.png


Dockerfile に以下の内容を入力して保存します。

Dockerfile
FROM node:alpine
ENV CHOKIDAR_USEPOLLING=true NUXT_TELEMETRY_DISABLED=1
RUN apk update && apk add git

1行目では、apline OS上に node がインストールされた公式の Docker イメージを使うよう指定しています。
2行目では、Nuxt で開発する際の自動リロードと、統計情報参加の応答を環境変数で指定しています。
3行目では、Docker コンテナ内で Git を使えるようインストールしています。

image.png


Remote Development 拡張機能をインストール

Extensions image.pngに切り替えて、Remote Development を検索してインストールします。

(インストール済みなら何もしなくてOKです。)

image.png


コンテナ開発モードに切り替える

左下の Open a remote window ボタンをimage.png押して Remote-Containers: Reopen in Container を選択します。

image.png

つづけて、From 'Dockerfile' を選択します。

image.png

しばらく待つと左下の表示が image.png に変わります。
これで、Docker コンテナ内で開発する準備が整いました。

Vue開発用拡張機能をインストール

devcontainer.json が表示されるので、extentions に Vuter を追加し上書き保存します。

※Vuter は vue ファイルや js ファイル、スタイルシートのコードハイライトや、自動補完、構文チェックを行ってくれる拡張機能です。

devcontainer.json(変更前)
    "extensions": []
devcontainer.json(変更後)
    "extensions": ["octref.vetur"]

image.png


変更を反映させるために、左下の Open a remote window ボタンをimage.png押して Remote-Containers: Rebuild Container を選択します。

image.png


拡張機能を見ると、vuter がインストールされているのが確認できます。
image.png

Nuxtプロジェクト作成

メニュー Terminal / New Terminal を選択します。

image.png

Docker コンテナ内でターミナルを開いたことになるので、コマンドプロンプトではなくシェルになります。
image.png


ターミナル内で、Nuxtプロジェクトを作成コマンドを実行します。

TERMINAL
yarn create nuxt-app

image.png

実行途中で、プロジェクト作成時の各種設定を入力していきます。


プロジェクト名入力
? Project name: nuxt_in_docker

プロジェクト名を聞いてきますが、デフォルトでフォルダ名と同じに設定されていてそのままでいいので Enter を押します。


開発言語選択
? Programming language: JavaScript

JavaScript か TypeScript を選択できます。今回は JavaScript にしますので、そのまま Enter を押します。


パッケージ管理選択
? Package manager: Yarn

yarn か npm を選択できます。今回は yarn にしますので、そのまま Enter を押します。


sh:UI フレームワーク選択
? UI framework: (Use arrow keys)

下記11種類から選択できますが、今回は使用しない(None)ので、そのまま Enter を押します。

  1. Ant Design Vue
  2. Bootstrap Vue
  3. Buefy
  4. Bulma
  5. Element
  6. Framevuerk
  7. iView
  8. Tachyons
  9. Tailwind CSS
  10. Vuesax
  11. Vuetify.js

導入モジュール選択
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)

下記3つを選択できますが、後からでも追加できるので、そのまま Enter を押します。

  1. ◯ Axios
  2. ◯ Progressive Web App (PWA)
  3. ◯ Content

構文チェックツール選択
? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)

下記4つを選択できますが、Vuter である程度カバーできるので今回はそのまま Enter を押します。

  1. ◯ ESLint
  2. ◯ Prettier
  3. ◯ Lint staged files
  4. ◯ StyleLint

テストフレームワーク選択
? Testing framework: Jest

下記3つから選択できます。後からも追加できますが、設定が若干面倒ですので、今回は Jest を入れておきます。
↓キーで Jest を選択して Enter キーを押します。

  1. Jest
  2. AVA
  3. WebdriverIO

レンダリング方式
? Rendering mode: Universal (SSR / SSG)

Universal (SSR / SSG) か Single Page App (SPA) を選択できます。

今回は最終的に静的な html, js, css を生成するので、 Universal を選びます。そのまま Enter を押します。
※後から設定を変更することも可能です。


デプロイ方式選択
? Deployment target: Static (Static/JAMStack hosting)

作成したものをどのようにに動作させるかを選択します。
Server (Node.js hosting) は、Node.js のサーバで動作させます。
Static (Static/JAMStack hosting) は、JAM(JavaScript, API, Markup)=通常のWebサーバで動作させます。
今回は Static を選択し、Enter を押します。


開発ツール選択
? Development tools: jsconfig.json (Recommended for VS Code)

下記2つを選択できます。 Visual Studio Code 用におすすめされているので、
jsconfig.json を選択し、Enter キーを押します。

  1. ◯ jsconfig.json (Recommended for VS Code)
  2. ◯ Semantic Pull Requests

作成完了になるまでしばらく待ちましょう。

作成完了
?  Successfully created project nuxt_in_docker

  To get started:

        yarn dev

  To build & start for production:

        yarn build
        yarn start

  To test:

        yarn test

Done in 186.42s.

Git管理開始

gitを初期化し、ユーザ名とメールアドレスを登録します。
※ユーザ名、メールアドレスは自分のものに置き換えて入力してください。

TERMINAL
git init
git config user.name ユーザ名
git config user.email メールアドレス

初回コミット

Source Control image.png に切り替えて、コミットします。

image.png
コミットコメント欄に first と入力し、コミットボタンimage.pngを押します。

ステージングに入れてないと確認が出ますが、全ファイルをコミットするのでYesを選択します。
image.png

以下の表示が出る場合は、ユーザ名、メールアドレスが正しく登録されていませんので、再度登録コマンドを実行してください。
image.png


開発サーバを起動

開発サーバを起動します。

Exploer image.png に切り替え、下のほうにある NPM SCRIPTS を開き、dev の右側の三角ボタンimage.pngを押します。

しばらく待つと、 Listening on: http://localhost:3000/ と表示されます。
これで開発サーバが起動しました。

image.png

ポート設定

ブラウザで表示できるようにするために、Docker 内のポートとローカルポートを接続します。

Remote Exploer image.png に切り替えます。

3000番ポートが表示されているので、右の追加ボタンimage.pngを押します。

image.png

以下の画面が出た場合は「アクセスを許可する」ボタンを押します。

image.png

これで Docker 内の 3000 番ポートと、ローカルの 3000 番ポートが接続され、
ブラウザで動作確認ができます。

image.png


ブラウザで画面を確認

Open in Brower ボタンimage.pngを押します。

トップ画面が表示されます。
image.png


タイトルを変更

pages/index.vue を開きます。

image.png

nuxt_in_docker の文字を Nuxtはじめました に書き換えて上書き保存します。

image.png

保存するとブラウザの表示が自動的に切り替わります。

image.png


テストを実行

NPM SCRIPTS 内の test の右の三角ボンタンを押します。
image.png

しばらく待つとテスト結果とカバレッジが表示されます。
image.png


テストを追加

index.vue のテストを追加してみます。

test/Logo.spec.js をCtrlドラッグでコピーします。
image.png

ファイル名を index.spec.js に変更します。
image.png

Logo の部分を index に書き換えて上書き保存します。
image.png

再度テストを実行します。

すると以下のように Logo コンポーネントが正しく登録されていないと出ます。
image.png


警告が出ないうように修正

警告を消すために、index.vue の <script>タグ内を記述します。
image.png

変更前
<script>
export default {}
</script>
変更後
<script>
import Logo from "@/components/Logo.vue";
export default {
  components: {
    Logo
  }
};
</script>

再度テストを実行します。

image.png

警告が消えてきれいに実行できました。

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

VisualStudioCodeでNuxt.jsを使ったDockerコンテナ内での開発

はじめに

以前、環境を汚さず(選ばず)Nuxtプロジェクトを作成しGitHub Pagesで公開するまでの一部始終 という記事を書きました。
その後、より簡単にNuxtの開発環境を作れないかと何回かやっているうちに、
VisualStudioCodeをうまく使えばよりシンプルにできることがわかってきたので、再度まとめました。
Nuxtのバージョンが上がってプロジェクト作成時の設定が若干変わっています。

事前準備

Docker, Visual Studio Codeをインストールしておきます。

これからインストールする方は以下を参考にしてください。

Windows 10 Home に Docker for Desktop をインストールする手順

VisualStudioCodeのインストール手順(Windows)

※今回は Windows 10 Home 環境で作業していますが、Docker, VisualStudioCode が入っていればどのOSでも同じようにできるはずです。

今回の Nuxt.js のバージョンは 2.13.3 です。


VisualStudioCodeを起動します。

image.png


メニュー File / Open Foder... を選択し、プロジェクト用フォルダを作成して開きます。

image.png

今回は C:\dev\nuxt_in_docker というフォルダを作成しました。
image.png

Welcome ページは不要ですので × ボタンで消します。
image.png

image.png


ファイル作成ボタンimage.pngを押し、 Dockerfile を作成します。

image.png


Dockerfile に以下の内容を入力して保存します。

FROM node:alpine
ENV CHOKIDAR_USEPOLLING=true NUXT_TELEMETRY_DISABLED=1
RUN apk update && apk add git

1行目では、apline OS上に node がインストールされた公式の Docker イメージを使うよう指定しています。
2行目では、Nuxt で開発する際の自動リロードと、統計情報参加の応答を環境変数で指定しています。
3行目では、Docker コンテナ内で Git を使えるようインストールしています。

image.png

Extensions image.pngに切り替えて、Remote Development を検索してインストールします。

(インストール済みなら何もしなくてOKです。)

image.png


左下の Open a remote window ボタンをimage.png押して Reopen in Container を選択します。

image.png

つづけて、From Dockerfile を選択します。

image.png

しばらく待つと左下の表示が image.png に変わります。
これで、Dockerコンテナ内で開発する準備が整いました。

devcontainer.json が表示されるので、extentions に vuter を追加し上書き保存します。

※Vuter は vue ファイルや js ファイル、スタイルシートのコードハイライトや、自動補完、構文チェックを行ってくれる拡張機能です。

変更前
    "extensions": []
変更後
    "extensions": ["octref.vetur"]

image.png

変更を反映させるために、左下の Open a remote window ボタンをimage.png押して Rebuild Container を選択します。

image.png

拡張機能を見ると、vuter がインストールされているのが確認できます。
image.png

メニュー Terminal / New Terminal を選択します。

image.png

Docker コンテナ内でターミナルを開いたことになるので、コマンドプロンプトではなくシェルになります。
image.png


ターミナル内で、Nuxtプロジェクトを作成コマンドを実行します。

yarn create nuxt-app

image.png

実行途中で、プロジェクト作成時の各種設定を入力していきます。


プロジェクト名入力
? Project name: nuxt_in_docker

プロジェクト名を聞いてきますが、デフォルトでフォルダ名と同じに設定されていてそのままでいいので Enter を押します。


開発言語選択
? Programming language: JavaScript

JavaScript か TypeScript を選択できます。今回は JavaScript にしますので、そのまま Enter を押します。


パッケージ管理選択
? Package manager: Yarn

yarn か npm を選択できます。今回は yarn にしますので、そのまま Enter を押します。


:UI フレームワーク選択
? UI framework: (Use arrow keys)

下記11種類から選択できますが、今回は使用しない(None)ので、そのまま Enter を押します。

  1. Ant Design Vue
  2. Bootstrap Vue
  3. Buefy
  4. Bulma
  5. Element
  6. Framevuerk
  7. iView
  8. Tachyons
  9. Tailwind CSS
  10. Vuesax
  11. Vuetify.js

導入モジュール選択
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)

下記3つを選択できますが、後からでも追加できるので、そのまま Enter を押します。

  1. ◯ Axios
  2. ◯ Progressive Web App (PWA)
  3. ◯ Content

構文チェックツール選択
? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)

下記4つを選択できますが、Vuter である程度カバーできるので今回はそのまま Enter を押します。

  1. ◯ ESLint
  2. ◯ Prettier
  3. ◯ Lint staged files
  4. ◯ StyleLint

テストフレームワーク選択
? Testing framework: Jest

下記3つから選択できます。後からも追加できますが、設定が若干面倒ですので、今回は Jest を入れておきます。
↓キーで Jest を選択して Enter キーを押します。

  1. Jest
  2. AVA
  3. WebdriverIO

レンダリング方式
? Rendering mode: Universal (SSR / SSG)

Universal (SSR / SSG) か Single Page App (SPA) を選択できます。

今回は最終的に静的な html, js, css を生成するので、 Universal を選びます。そのまま Enter を押します。
※後から設定を変更することも可能です。


デプロイ方式選択
? Deployment target: Static (Static/JAMStack hosting)

作成したものをどのようにに動作させるかを選択します。
Server (Node.js hosting) は、Node.js のサーバで動作させます。
Static (Static/JAMStack hosting) は、JAM(JavaScript, API, Markup)=通常のWebサーバで動作させます。
今回は Static を選択し、Enter を押します。


開発ツール選択
? Development tools: jsconfig.json (Recommended for VS Code)

下記2つを選択できます。 Visual Studio Code 用におすすめされているので、
jsconfig.json を選択し、Enter キーを押します。

  1. ◯ jsconfig.json (Recommended for VS Code)
  2. ◯ Semantic Pull Requests

作成完了になるまでしばらく待ちましょう。

作成完了
?  Successfully created project nuxt_in_docker

  To get started:

        yarn dev

  To build & start for production:

        yarn build
        yarn start

  To test:

        yarn test

Done in 186.42s.

gitを初期化し、ユーザ名とメールアドレスを登録します。

※ユーザ名、メールアドレスは自分のものに置き換えて入力してください。

git init
git config user.name ユーザ名
git config user.email メールアドレス

Source Control image.png に切り替えて、コミットします。

image.png
コミットコメント欄に first と入力し、コミットボタンimage.pngを押します。

ステージングに入れてないと確認が出ますが、全ファイルをコミットするのでYesを選択します。
image.png

以下の表示が出る場合は、ユーザ名、メールアドレスが正しく登録されていませんので、再度登録コマンドを実行してください。
image.png

開発サーバ起動を起動します。

Exploer image.png に切り替え、下のほうにある NPM SCRIPTS を開き、dev の右側の三角ボタンimage.pngを押します。

しばらく待つと、 Listening on: http://localhost:3000/ と表示されます。
これで開発サーバが起動しました。

image.png

Docker内のポートとローカルポートを接続します。

Remote Exploer image.png に切り替えます。

3000番ポートが表示されているので、右の追加ボタンimage.pngを押します。

image.png

以下の画面が出た場合は「アクセスを許可する」ボタンを押します。

image.png

これで Docker 内の 3000 番ポートと、ローカルの 3000 番ポートが接続され、
ブラウザで動作確認ができます。

image.png

Open in Brower ボタンimage.pngを押します。

トップ画面が表示されます。
image.png


タイトルを変更してみる

pages/index.vue を開きます。

image.png

nuxt_in_docker の文字を Nuxtはじめました に書き換えて上書き保存します。

image.png

保存するとブラウザの表示が自動的に切り替わります。

image.png


テストを実行する

NPM SCRIPTS 内の test の右の三角ボンタンを押します。
image.png

しばらく待つとテスト結果とカバレッジが表示されます。
image.png

テストを追加する

index.vue のテストを追加してみます。

test/Logo.spec.js をCtrlドラッグでコピーします。
image.png

ファイル名を index.spec.js に変更します。
image.png

Logo の部分を index に書き換えて上書き保存します。
image.png

再度テストを実行します。

すると以下のように Logo コンポーネントが正しく登録されていないと出ます。
image.png

警告を消すために、index.vue の <script>タグ内を記述します。
image.png

変更前
<script>
export default {}
</script>
変更後
<script>
import Logo from "@/components/Logo.vue";
export default {
  components: {
    Logo
  }
};
</script>

再度テストを実行します。

image.png

警告が消えてきれいに実行できました。

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

【javascript】ロックとキーのシミュレート

書籍のアウトプットとして

ロックとキーのシステムを構築する。
lock関数では鍵をかけて保管するデータが有る。
戻り地はロック解除のための一意のキー(key)。
unlock関数は正しいキーを使ってロックを解除する。
したがってAPIは{key,unlock}=lock(secret)となる。

function lock(secret) {
  const key = Symbol('key')
  return {
    key,unlock(keyUserd) {
      if (keyUserd == key) {
        return secret;
      } else {
        return '*'.repeat(secret.length || secret.toString().length)
      }
    }
  }
}

const {key,unlock}=lovk(42)
console.log('key')//**
console.log(42)//**
console.log(key)//42

lock関数の引数を渡すと、一意にキーが作成されkeyとunlock関数のオブジェクトを返す。
塩用されたキーが正しくない場合は文字列をマスクする処理をしている。

'*'.repeat(secret.length || secret.toString().length)

この容量で複数のロックと複数のキーを作成することができる。

ゲームを作成する

function choose(message, options, secondAttempt) {
  //プロンプトに表示する複数選択オプションを作成
  //各インデックスに1を足すことでオプションが0でなく1から始まるようにする
  const opts = options.map(function(option, index) {
    return `Type ${index+1} for: ${option}`;
  })

  //ユーザがプロンプトで入力した番号を取得
  //先の足した1を引く
  const resp = Number(prompt(`${message}\n\n${opts.join('\n')}`)) - 1
  if (options[resp]) {
    //プロンプトから適切なレスポンスが返された場合はそれを返す。
    return resp;
  } else if (!secondAttempt) {
    //有効なタイじゃなかったらもう一回オプションを表示
    return choose(`Last try\n${message}`, options, true)
  } else {
    //2回目もだめならエラー
    throw Error('No selection')
  }
}

const message='最高のスーパーヒーローは誰か'
const options=['スーパーマン','アイアンマン','バットマン','キャプテンアメリカ'];
const hero=choose(message,options)

ここではmapを使っているがこれは新しい配列を作成している。

Array.prototype.map()

構文
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
    // 新しい配列の要素を返す
}[, thisArg])

引数

callback
新しい配列の要素を生成するためのコールバック関数は3つの引数を取る。

callbackの引数 説明
currentValue 現在処理中の要素の値
index(optional) 現在処理中の要素の配列内におけるインデックス
array(optional) mapが実行されている配列

thisArg(optional)
callbacを実行するときにthisとして使う値

返り値

新しい配列

initでまとめるとこうなる

function lock(secret) {
  const key = Symbol('key')
  return {
    key,
    unlock(keyUserd) {
      if (keyUserd == key) {
        return secret;
      } else {
        return '*'.repeat(secret.length || secret.toString().length)
      }
    }
  }
}


function choose(message, options, secondAttempt) {
  //プロンプトに表示する複数選択オプションを作成
  //各インデックスに1を足すことでオプションが0でなく1から始まるようにする
  const opts = options.map(function(option, index) {
    return `Type ${index+1} for: ${option}`;
  })

  //ユーザがプロンプトで入力した番号を取得
  //先の足した1を引く
  const resp = Number(prompt(`${message}\n\n${opts.join('\n')}`)) - 1
  if (options[resp]) {
    //プロンプトから適切なレスポンスが返された場合はそれを返す。
    return resp;
  } else if (!secondAttempt) {
    //有効なタイじゃなかったらもう一回オプションを表示
    return choose(`Last try\n${message}`, options, true)
  } else {
    //2回目もだめならエラー
    throw Error('No selection')
  }
}
function init(){
  const {key:key1,unlock:door1}=lock('A new car')
  const {key:key2,unlock:door2}=lock('A trip to Hawaii')
  const {key:key3,unlock:door3}=lock('$100 Doloars')

  const keys=[key1,key2,key3]
  const doors=[door1,door2,door3]
  const key=keys[Math.floor(Math.random()*3)]

  const message=`you have been given a \u{1F511} plese choose a door.`

  const options=doors.map(function(door,index){
    return `Door #${index+1}: ${door()}`
  })

  const door=doors[choose(message,options)]
  alert(door(key))
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む