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

正規表現の基礎

概要

irbを使って正規表現について見ていきます。


irb

irb(main):001:0> name = "taro"
=> "taro"
irb(main):002:0> name.sub(/taro/,"kotaro")
=> "kotaro"

subメソッドを使ってtaroという文字をkotaroに置き換えています。
第1引数に置き換えたい文字列を指定し、第2引数に変換後の文字列を記述します。


irb(main):004:0> name.match(/taro/)
=> #<MatchData "taro">
irb(main):005:0> name.match(/bob/)
=> nil

matchメソッドを使って引数に指定した文字列が含まれているかをチェックしています。
含まれていた場合は指定した文字列がMatchDataオブジェクトとして返り値で返ってきます。
含まれていなかった場合はnilが返ってきます。

irb(main):006:0> array = name.match(/taro/)
=> #<MatchData "taro">
irb(main):007:0> array[0]
=> "taro"

MatchDataオブジェクトは配列なので、上記のようにすると値を取得することができます。

irb(main):008:0> phoneNumber = "080-1234-5678"
=> "080-1234-5678"
irb(main):009:0> phoneNumber.gsub(/-/,"")
=> "08012345678"

電話番号のハイフンを取り除きたい場合ですが、subメソッドを使ってしまうと最初のハイフンしか置換されないので、gsubメソッドを使いましょう。gとはグローバルマッチで、指定した文字列が複数含まれている場合全て置換してくれます。

irb(main):010:0> myPassword = "Taro0123"
=> "Taro0123"
irb(main):012:0> myPassword.match(/[a-z\d]{8,10}/i)
=> #<MatchData "Taro0123">
irb(main):013:0> myPassword.match(/[a-c\d]/i)
=> #<MatchData "a">
irb(main):014:0> myPassword.match(/[a-c\d]{8,}/i)
=> nil

・[a-z]は、a~zの文字がいずれか1個にマッチ
・\dは、数字にマッチ
・{8,10}は、直前の文字列が少なくとも8回、多くても10回出現するものにマッチ
・iは、大文字と小文字を区別しないで検索する
という意味です。

irb(main):015:0> myAddress = "yey@gmail.jp"
=> "yey@gmail.jp"
irb(main):016:0> myAddress.match(/@.+/)
=> #<MatchData "@gmail.jp">
irb(main):017:0> 

メールアドレスのドメインを取得する場合
.は、どの1文字にもマッチ
+は、直前の文字の1回以上の繰り返しにマッチ
という意味を持ちます。
ですので@.+ とすると
@から始まり、全ての文字にマッチ、それを繰り返す。ということになるので
@以降のドメインが取得できます。

この記事を読んでいただきありがとうございました。

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

JavaScriptの様々なメソッド

forEach文

配列に対してよく使われます。繰り返し処理のことです。

配列.forEach( コールバック関数 )

※コールバック関数
これは配列の中にある各要素に対して行う処理のことです

配列.forEach( function(value, index) {
} )

第二引数のindexを省略した場合
配列の各要素(value)だけ取り出すということになります。

Array.prototype.slice.call()

これは単純です。引数の中にあるオブジェクトを配列に変換してくれます。

line = Array.prototype.slice.call(obj)

これは引数にあるobjをまず配列に変えています。
配列に変えた上で、その配列を変数lineに代入しています。

indexOf()

これも配列に対して使うもので引数の中に書かれているものが南蛮めに書かれているのか知りたいときに使います。

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

googleに先駆け、js無効化に完璧に対応してみる

googleは未だ、js無効化に全く対応できていない。

 Microsoftを凌駕し、ITの王様となって久しいgoogle.comは、Javascript無効ブラウザに全く対応していないwebサイトの一つだ。
 例えば、検索窓に「ヤマト 集荷」と入力し、一番上に表示されたリンクをクリックすると、画面が真っ白になる。
 これを「google社の問題」とするかどうかは、閲覧者によって評価が分かれるかもしれないが、少なくとも貧乳教徒の私は、「ヤマト運輸の問題でもあり、google社の問題でもある」と思っている。
 そうではないという意見をお持ちの方には、(理論の基になるものが社会学か生物学か、はたまた憲法学か新興宗教かは分からないが、)その知見をご披露頂きたい。

js無効ブラウザに対する完璧な対応とは何か

 js無効ブラウザで自サイトを訪れた閲覧者に、自サイトを最も不自由なく使ってもらうにはどうすれば良いだろうか。何が最も親切な『神対応』だろうか。
 どんな対策を施せば、叶恭子をして「ファビュラス」と言わしめる事ができるだろうか。

Firefoxに直リンクを貼った。

 まさか、「(お使いのブラウザで)javascriptを有効にしてください」ではないだろう。こんなに傲慢で、不合理で、無配慮で、不見識でみっともない対応は他にあるまい。こんなに馬鹿ばかしい文言を堂々と掲載できるのは、かの有名なy●h●● japanぐらいだ。
(ニュースのコメント欄が"ロ●アのスパイから好評を得たいT●Y●T●従業員"で溢れかえっていて気持ち悪いので、私はy●h●● japanを閲覧するのを1年半前にやめた。)

 対して、「趣味でwebサイトをやっている者だ」な私が素人なりに、捻くれた頭を更に捻って導き出した最初の企みが、以下のコードになる。

<!doctype html>
<html lang=ja>
<meta charset="UTF-8">
<style id=i9999>
#i9998{-webkit-text-size-adjust:100%;
box-sizing:border-box;position:fixed;
width:100%;height:100%;z-index:100;
margin:0;padding:180px 0 0;
background:#fff}
#i9998>li{width:300px;margin-left:calc(50% - 150px)}
#i9998>li>a{/*display:inline;width:auto;*/background:#ffc}</style>
<script>
window.addEventListener('DOMContentLoaded', (event) => {document.getElementById('i9999').innerHTML='#i9998{display:none !important}'});
</script>
<style>
/*自由なスタイル*/
</style>
<meta name=viewport content='width=device-width,initial-scale=1,maximum-scale=1'>
<body>
<!--自由なHTML-->
<script>
//自由なスクリプト
</script>
<ul id=i9998><li>
Javascriptを有効化しても問題のない、<a href='//mozilla.org/ja/firefox/new'>安全なブラウザ</a>で example.com をご利用ください。
</ul>
</body>
</html>

やっていること

1. 無効対策専用style(id=i9999)でul(id=i9998)を縦横100%、最前面(z-index:100)のfixedとする。
2. window.onloadに先んじて発火するDOMContentLoadedにより、無効対策専用styleの内容をul#i9998{display:none}に書き換える。
3. ul#i9998の中身はjsが発火しない場合には「最前面に全表示」、発火したら「真っ先に非表示」になるので、そこにFirefoxのDLを促す文言を書き、リンクを貼る。

なぜこうしたか。

 js有効無効の判定をDOMContentLoaded、それ以外の通常のDHTMLプログラムをwindow.onloadと棲み分ける事で、互いに干渉しない。仮にwindow.onload以後のjsコードで何らかのエラーが発生したとしても、js無効ブラウザ向けのul(z-index:100)は既に非表示にされている。
 上記functionが実行できないような環境(js無効設定だけではなく、古いブラウザも当てはまるかもしれない)では自サイトを満足に使ってもらえないので、js有効の場合10に対する7の機能性を実現しようと悪戦苦闘するよりも、「使えるブラウザなんて1つじゃないんだから、さっさと2個目をDLして存分に使い倒してくださいよ。」と唐突に表明するのが最良であると判断した。

 今の時代、googleやamazonでさえ、「jsなしでも何とかなる」に固執する必然性は皆無だ。そんなものはユニバーサルデザインの必要条件ではない。それを言い出したら、3年前のガラホでも2000円の中華スマートウォッチでもYoutubeが見られなければおかしい。
 そんな悠長なタスク(あるいは強迫観念)に時間を割く暇があるなら、モバイルファーストを4分の1歩でも前進させてみろという話になるのではないか。

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

Node.jsでjsの非同期処理でつまづいたところ

概要

以前の記事で、イベントループ方式の理解に苦しんだことについて記事にあげましたが、今回は、非同期処理についてつまづいたところを忘れないように記事にしてみようと思います。

つまづいた経緯としては、Node.jsでローカルサーバーを構築し、リクエストを受け付けられるようになったところで、「リクエストごとに処理を変えたいなぁ」、と思ったことが全ての始まりでした。

環境

自分が実際につまづいた時の環境は、以下となります。

  • OS:Windows10
  • ブラウザ:Chrome

やりたかったこと

  • データを取得して、それをページに表示する。

最初はMVCモデルに基づいてプログラムを組もうと考えていましたが、少しめんどくさくなりそうだったので、簡単にデータを取得・反映させるページを作ろうと思いました。

つまづいたところ

最初に記述したコードになります。

sample_node.js
// モジュールの取り込み
const http = require('http');
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');
const mime = require('mime-types');
const qs = require('querystring');
const setting = require('./setting');
const db = require('./db');

// 本ファイルの親フォルダを取得(htmlファイル等を取得するため)
const parentDir = path.dirname(__dirname);

// webサーバーの作成
const server = http.createServer();
// リクエストの受付を検知する
server.on('request', AppController);
server.listen(setting.port, setting.host);
console.log('server listeneing...');

// URLのマッピング処理を振り分ける
async function AppController(req, res) {

    let filePath = req.url;

    if(filePath === '/favicon.ico'){
        res.end();
        return;
    }

    let parseData;

    if(req.method === "POST"){
        req.data = "";
        req.on("data", function(chunk){
            req.data += chunk;
        });
        req.on("end", function(){
            parseData = qs.parse(req.data);
            try{
                const resultData = selectAllItem();
                // 
                // 結果を取得して色々処理...
                // 
                buildPage(res, "/public/index.html", resultData);
            }catch(e){
                console.log(e.stack);
                buildPage(res, "err.html", "");
            }
        });

    }else if(req.method === "GET"){
            try{
                buildPage(res, "/public/index.html", "");
            }catch(e){
                console.log(e.stack);
                buildPage(res, "err.html", "");
            }
    }

};

// ページ遷移処理
const buildPage = function (res, filePath, postData){
    try{
        res.writeHead(200, {"Content-Type": mime.lookup(path.basename(filePath)) });
        const content = fs.readFileSync(parentDir + filePath, 'utf-8');
        const data = ejs.render(content,{form:postData});
        res.write(data);
        res.end();

    }catch(e){
        console.log(e.stack);
        throw e;
    }
}
// DBに接続して、データを取得する
const selectAllItem = async function(){

    let item1 = {};
    let item2 = {};
    let item3 = {};

    try{
        item1 = db.selectItem1("*");
        item2 = db.selectItem2("*");
        item3 = db.selectItem3("*");

    }catch(e){
        console.log(e.stack);
        throw e;
    }

    return {"item1":item1, "item2":item2, "item3":item3};
}



この状態で実行すると、selectAllItem()メソッド実行後の、resultDataに何も入っていませんでした。
これは、処理が非同期処理であるため、selectの実行をする前に、ページ遷移が先に実行されてしまったようです。
これを解決するためには、
 selectの実行 → データの編集処理 → ページ遷移
という順番を守ってもらわなければなりません。

解決策(Promise / async / await)

Node.jsでは、基本的にイベントループ方式のため、関数の終了を待ってはくれないみたいです。
そこで必要な知識となるのが、Promiseオブジェクトとasync/awaitの記述でした。
Promiseとasync/awaitは、書き方が違うだけで、実装できる内容はほぼほぼ同じです。
詳しい違いについては、以下の記事を参考にしてみてください。
https://qiita.com/h1guchi/items/0434f1295226cdd19a53

今回の解決策には、async / awaitを使用しました。
まずメイン処理から修正していきます。

メイン処理の修正

async_await_sample.js
// URLのマッピング処理を振り分ける
async function AppController(req, res) {

    ...//省略
    if(req.method === "POST"){
        req.data = "";
        req.on("data", function(chunk){
            req.data += chunk;
        });
        req.on("end", async function(){
            parseData = qs.parse(req.data);
            try{
                // 1・・・
                const resultData = await selectAllItem();
                // 2・・・
                // 結果を取得して色々処理...
                // 
                // 3・・・
                buildPage(res, "/public/index.html", resultData);
            }catch(e){
                console.log(e.stack);
                buildPage(res, "err.html", "");
            }
        });

    }else if(req.method === "GET"){
            try{
                buildPage(res, "/public/index.html", "");
            }catch(e){
                console.log(e.stack);
                buildPage(res, "err.html", "");
            }
    }

};

メイン処理で修正することは、下記の流れを守ってもらうことです。
 1. データを取得する
 2. データを編集する
 3. ページ遷移処理を実行する
非同期処理の場合は、この1・2・3が同時に実行されてしまったために問題が起きてしまいましたが、これを解決するために、awaitを使用します。awaitを実行したいasyncな処理の前に記述することで、その処理の終了を待つことができます。
ただし、このawaitを使用する場合は、その使用しているメソッドをasyncにする必要があるみたいです。(このために下層の関数はほとんどasyncをつけなくてはいけなくなりました。。。)

今回のメイン処理では、selectAllItem()メソッドの前に、awaitを記載しました。

次は、selectAllItemメソッド内の処理を修正していきます。

データ取得処理の修正

async_await_sample.js
const selectAllItem = async function(){

    let item1;
    let item2;
    let item3;

    return await Promise.all([
        db.selectItem1("*"),
        db.selectItem2("*"),
        db.selectItem3("*")
    ])
    .then((values) => {
        item1 = values[0];
        item2 = values[1];
        item3 = values[2];
        return {"item1":item1, "item2":item2, "item3":item3};
    })
    .catch((err) => { console.log(err.stack);throw err; });
}

ここでの処理は、Promise.all()という処理を使用しています。
最初修正した際は、1つ1つのselect処理にawaitをかけていたのですが、それぞれのselectの順番は気にしなくてもよいため、Promise.allとしています。
Promise.all()の中で定義されたasyncな関数は、全て非同期で実行されますが、その全ての関数の終了を待ってから次に進むため、一括でデータを取得したい時とかには向いているかもしれません。

まとめ

普段非同期処理などを意識することがあまりないため、かなり戸惑いましたが、とても勉強になりました。
普段はバックエンド側なので、javascriptを仕事で触ることは稀にしかないのですが、最近調べてみるとjavascriptでARの開発やサーバーの構築やフレームワーク使って簡単にリッチなWEBページを作ったり。。。すごいですねぇ
これから勉強して、またここにアウトプットしていこうと思います。

補足(参考リンク)

Promiseとasync/await
https://qiita.com/suin/items/97041d3e0691c12f4974
https://qiita.com/toshihirock/items/e49b66f8685a8510bd76#comments

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

Node.jsの非同期処理でつまづいたところ

概要

以前の記事で、イベントループ方式の理解に苦しんだことについて記事にあげましたが、今回は、非同期処理についてつまづいたところを忘れないように記事にしてみようと思います。

つまづいた経緯としては、Node.jsでローカルサーバーを構築し、リクエストを受け付けられるようになったところで、「リクエストごとに処理を変えたいなぁ」、と思ったことが全ての始まりでした。

環境

自分が実際につまづいた時の環境は、以下となります。

  • OS:Windows10
  • ブラウザ:Chrome

やりたかったこと

  • データを取得して、それをページに表示する。

最初はMVCモデルに基づいてプログラムを組もうと考えていましたが、少しめんどくさくなりそうだったので、簡単にデータを取得・反映させるページを作ろうと思いました。

つまづいたところ

最初に記述したコードになります。

sample_node.js
// モジュールの取り込み
const http = require('http');
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');
const mime = require('mime-types');
const qs = require('querystring');
const setting = require('./setting');
const db = require('./db');

// 本ファイルの親フォルダを取得(htmlファイル等を取得するため)
const parentDir = path.dirname(__dirname);

// webサーバーの作成
const server = http.createServer();
// リクエストの受付を検知する
server.on('request', AppController);
server.listen(setting.port, setting.host);
console.log('server listeneing...');

// URLのマッピング処理を振り分ける
async function AppController(req, res) {

    let filePath = req.url;

    if(filePath === '/favicon.ico'){
        res.end();
        return;
    }

    let parseData;

    if(req.method === "POST"){
        req.data = "";
        req.on("data", function(chunk){
            req.data += chunk;
        });
        req.on("end", function(){
            parseData = qs.parse(req.data);
            try{
                const resultData = selectAllItem();
                // 
                // 結果を取得して色々処理...
                // 
                buildPage(res, "/public/index.html", resultData);
            }catch(e){
                console.log(e.stack);
                buildPage(res, "err.html", "");
            }
        });

    }else if(req.method === "GET"){
            try{
                buildPage(res, "/public/index.html", "");
            }catch(e){
                console.log(e.stack);
                buildPage(res, "err.html", "");
            }
    }

};

// ページ遷移処理
const buildPage = function (res, filePath, postData){
    try{
        res.writeHead(200, {"Content-Type": mime.lookup(path.basename(filePath)) });
        const content = fs.readFileSync(parentDir + filePath, 'utf-8');
        const data = ejs.render(content,{form:postData});
        res.write(data);
        res.end();

    }catch(e){
        console.log(e.stack);
        throw e;
    }
}
// DBに接続して、データを取得する
const selectAllItem = async function(){

    let item1 = {};
    let item2 = {};
    let item3 = {};

    try{
        item1 = db.selectItem1("*");
        item2 = db.selectItem2("*");
        item3 = db.selectItem3("*");

    }catch(e){
        console.log(e.stack);
        throw e;
    }

    return {"item1":item1, "item2":item2, "item3":item3};
}



この状態で実行すると、selectAllItem()メソッド実行後の、resultDataに何も入っていませんでした。
これは、処理が非同期処理であるため、selectの実行をする前に、ページ遷移が先に実行されてしまったようです。
これを解決するためには、
 selectの実行 → データの編集処理 → ページ遷移
という順番を守ってもらわなければなりません。

解決策(Promise / async / await)

Node.jsでは、基本的にイベントループ方式のため、関数の終了を待ってはくれないみたいです。
そこで必要な知識となるのが、Promiseオブジェクトとasync/awaitの記述でした。
Promiseとasync/awaitは、書き方が違うだけで、実装できる内容はほぼほぼ同じです。
詳しい違いについては、以下の記事を参考にしてみてください。
https://qiita.com/h1guchi/items/0434f1295226cdd19a53

今回の解決策には、async / awaitを使用しました。
まずメイン処理から修正していきます。

メイン処理の修正

async_await_sample.js
// URLのマッピング処理を振り分ける
async function AppController(req, res) {

    ...//省略
    if(req.method === "POST"){
        req.data = "";
        req.on("data", function(chunk){
            req.data += chunk;
        });
        req.on("end", async function(){
            parseData = qs.parse(req.data);
            try{
                // 1・・・
                const resultData = await selectAllItem();
                // 2・・・
                // 結果を取得して色々処理...
                // 
                // 3・・・
                buildPage(res, "/public/index.html", resultData);
            }catch(e){
                console.log(e.stack);
                buildPage(res, "err.html", "");
            }
        });

    }else if(req.method === "GET"){
            try{
                buildPage(res, "/public/index.html", "");
            }catch(e){
                console.log(e.stack);
                buildPage(res, "err.html", "");
            }
    }

};

メイン処理で修正することは、下記の流れを守ってもらうことです。
 1. データを取得する
 2. データを編集する
 3. ページ遷移処理を実行する
非同期処理の場合は、この1・2・3が同時に実行されてしまったために問題が起きてしまいましたが、これを解決するために、awaitを使用します。awaitを実行したいasyncな処理の前に記述することで、その処理の終了を待つことができます。
ただし、このawaitを使用する場合は、その使用しているメソッドをasyncにする必要があるみたいです。(このために下層の関数はほとんどasyncをつけなくてはいけなくなりました。。。)

今回のメイン処理では、selectAllItem()メソッドの前に、awaitを記載しました。

次は、selectAllItemメソッド内の処理を修正していきます。

データ取得処理の修正

async_await_sample.js
const selectAllItem = async function(){

    let item1;
    let item2;
    let item3;

    return await Promise.all([
        db.selectItem1("*"),
        db.selectItem2("*"),
        db.selectItem3("*")
    ])
    .then((values) => {
        item1 = values[0];
        item2 = values[1];
        item3 = values[2];
        return {"item1":item1, "item2":item2, "item3":item3};
    })
    .catch((err) => { console.log(err.stack);throw err; });
}

ここでの処理は、Promise.all()という処理を使用しています。
最初修正した際は、1つ1つのselect処理にawaitをかけていたのですが、それぞれのselectの順番は気にしなくてもよいため、Promise.allとしています。
Promise.all()の中で定義されたasyncな関数は、全て非同期で実行されますが、その全ての関数の終了を待ってから次に進むため、一括でデータを取得したい時とかには向いているかもしれません。

まとめ

普段非同期処理などを意識することがあまりないため、かなり戸惑いましたが、とても勉強になりました。
普段はバックエンド側なので、javascriptを仕事で触ることは稀にしかないのですが、最近調べてみるとjavascriptでARの開発やサーバーの構築やフレームワーク使って簡単にリッチなWEBページを作ったり。。。すごいですねぇ
これから勉強して、またここにアウトプットしていこうと思います。

補足(参考リンク)

Promiseとasync/await
https://qiita.com/suin/items/97041d3e0691c12f4974
https://qiita.com/toshihirock/items/e49b66f8685a8510bd76#comments

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

【JavaScript】アロー関数の使い方(functionとの違い)

【JavaScript】アロー関数の使い方(functionとの違い)

JavaScriptでよく使われるアロー関数の理解。

書き方

const 関数名 = (引数)=>{処理;};
 └ const:定数を定義。関数名になる
 └ 引数:省略可

functionと異なりアロー関数自体は関数名を持たないため、代入した定数(変数)名が関数名になる

*constはvarやletなど変数でも可(自由度が異なる)。誤った書き換えや重複防止のためにconst推奨。

アロー関数の呼び出し

関数名();

定数(変数)名に()をつける。
functionで定義した関数の呼び出しと同じ。

functionとの比較
//アロー関数の場合
const hello1 = ()=>{
    console.log('こんにちは');
}


//functionの場合
function hello2(){
    console.log('ハロー');
}

hello1();
hello2();


//出力
こんにちは
ハロー

functionを定数(変数)に代入することもできる
定数に代入
const hello3 = function hello2 (){
    console.log('ハロー');
}

hello3();
hello2();

//出力
ハロー
hello2 is not defined

※hello2()は使えなくなる。

関数名は省略できる
const hello3 = function(){
    console.log('ハロー');
}

hello3();

//出力
ハロー


引数を使う

const 関数名 = (引数名1, 引数名2,,,,)=>{処理}
 └ 引数名は任意(小文字アルファベット)
 └ 大文字、数値はエラー
 └ 引数の数と

引数名は内容を端的に示すものをつける。

const hello2 = (name, prefecture)=>{
    console.log(`${name}さんこんにちは。出身地は${prefecture}ですね`);
}

hello2('TODOROKI','長野')

//出力
//TODOROKIさんこんにちは。出身地は長野ですね

変数展開:「`${変数名}`」を使うと、文字列と変数をつなげて書ける。
※`:バッククオート(shift+@)



▼引数名は任意の例

const hello3 = (aaa, bbb)=>{
    console.log(`${aaa}さんこんにちは。出身地は${bbb}ですね`);
}

hello3('TODOROKI','長野')

//出力
//TODOROKIさんこんにちは。出身地は長野ですね


補足

▼仮引数の数だけ渡さなくてもエラーにならない。
 └ undefinedになる
 └ phpやpythonはエラー

undefinedの例
const hello1 = (name, prefecture)=>{
    console.log(`${name}さんこんにちは。出身地は${prefecture}ですね`);
}

hello1()

//出力
//undefinedさんこんにちは。出身地はundefinedですね
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】コールバック関数とは。実例で使い方を解説(forEach, find, mapメソッド)

【JavaScript】コールバック関数とは。実例で使い方を解説

JavaScriptでよく出てくるコールバック関数について、forEach, find, mapメソッドの使い方も含める。

コールバック関数自体はすごく簡単。コールバック関数を使った処理が複雑(に見える)。

  1. コールバック関数とは
  2. 関数を別々に定義する方法
  3. 引数の中に直接関数を書く方法
  4. メソッドの引数に関数を使う
    1. forEachメソッド
    2. findメソッド
    3. mapメソッド


コールバック関数とは

引数のカッコ内に入った関数の呼び名。



▼よく見るコールバック関数の使い方

①関数を別々に定義する方法
関数名1(関数名2);
 └ 関数名1: 関数を引数として受け取れる関数
 └ 関数名2:別で定義した関数。(コールバック関数になる)
 └ 関数は変数で定義。(変数に関数自身を格納)

②引数の中に直接関数を書く方法
関数名(関数)
 └ 関数名: 関数を引数として受け取れる関数
 └ 関数:引数の中で直接定義(コールバック関数になる)

③メソッドの引数として呼び出す
オブジェクト.メソッド(()=>{});
 └ メソッド:forEachやfindなど
 └ アロー関数がコールバック関数になる

②の応用。デフォルトで定義されている関数を使う。

コールバック関数の実例

①関数を別々に定義する方法

関数名1(関数名2);

<手順>
1. 引数で関数を呼び出せる関数を定義。(引数の値は任意)
2. 後ほどコールバック関数になる関数を定義。(1,2は逆でもOK)
3. 2の関数名の引数に1を入れて実行。

//関数1(引数に関数を受け取れる)
const birthday = (aaa)=>{
    aaa();
    console.log('happy birthday');
}


//関数2(後ほどコールバック関数になる)
const hello = ()=>{
    console.log('hello');
}

//関数呼び出し(関数helloがコールバック関数になる)
birthday(hello);


//▼出力
//hello
//happy birthday

コールバック関数を使うメリットは、別で定義した処理を足し合わせて使えること。(1行の処理だとありがたみが感じられないが、、)

▼メリットを感じてみる

//関数2(後ほどコールバック関数になる)
const hello = ()=>{
    console.log('hello');
    console.log('こんにちは');
    console.log('アニョハセヨ');
    console.log('ナマステ');
    console.log('ボンジョルノ');
    console.log('ニーハオ');
    console.log('ボンジュール');
    console.log('スラマシアン');
}


//関数1(引数に関数を受け取れる)
const birthday = (aaa)=>{
    aaa();
    console.log('★happy birthday');
}

//関数1(引数に関数を受け取れる)
const seeYou = (bbb)=>{
    bbb();
    console.log('★See You!')
}


//関数呼び出し(関数helloがコールバック関数になる)
birthday(hello);
seeYou(hello);

処理結果(長いので)
hello
こんにちは
アニョハセヨ
ナマステ
ボンジョルノ
ニーハオ
ボンジュール
スラマシアン
★happy birthday
hello
こんにちは
アニョハセヨ
ナマステ
ボンジョルノ
ニーハオ
ボンジュール
スラマシアン
★See You!

<メリット>
・長い処理が書かれた関数を1行で呼び出せる。
・別の関数でも呼び出せる。


アロー関数を使わない場合(function)

アロー関数をfunctionを使った書き方に変更。やってることは同じ。

関数名、変数名、引数名は同じでも違ってもOK。
どれがどの処理化わかりやすくするために固有してます。

//関数1(引数に関数を受け取れる)
const birthday = function omedeto(aaa){
    aaa();
    console.log('happy birthday');
}

//関数2(後ほどコールバック関数になる)
const hello = function konchiwa(){
    console.log('hello');
}

//関数呼び出し(関数helloがコールバック関数になる)
birthday(hello);


//▼出力
//hello
//happy birthday

変数に代入しない場合

関数名でも実行できる。

//関数1(引数に関数を受け取れる)
function omedeto(aaa){
    aaa();
    console.log('happy birthday');
}

//関数2(後ほどコールバック関数になる)
function konchiwa(){
    console.log('hello');
}

//関数呼び出し(関数helloがコールバック関数になる)
omedeto(konchiwa);


//▼出力
//hello
//happy birthday


②引数の中に直接関数を書く方法

関数名(関数)

<手順>
1. 引数に関数をとれる関数を定義
2. 1の関数名の引数に関数を記述

//関数(引数に関数を受け取れる)
const birthday = (aaa)=>{
    aaa();
    console.log('happy birthday');
}

//birthday()の引数内に関数を直接記述
birthday(()=>{
    console.log('hello');
});

①よりも記述が少なくなる。


③メソッドの引数に関数を使う

オブジェクト.メソッド(()=>{});

メソッド(関数)の引数として(アロー)関数を呼び出す。
アロー関数=コールバック関数。

1.forEachメソッド

オブジェクト.forEach((変数名)=>{処理;});
 └ オブジェクト:配列
 └ 変数名:配列から一つづつ取り出した要素の変数名

配列の要素を一つづつ取り出す。

const letters=['a','b','c','d'];

letters.forEach((letter)=>{
    console.log(letter);
});

//出力
a
b
c
d


2.findメソッド

オブジェクト.find((変数名)=>{return 条件式;});
 └ オブジェクト:配列
 └ 変数名:配列から一つづつ取り出した要素の変数名

条件に一致する最初の要素を取り出す。
・console.logでfindメソッド自体を出力する。
 └ findメソッドがreturnで数値と置き換わるため。

const numbers=[1,10,100,1000];

finder = numbers.find((number)=>{
    return number > 99;
});

console.log(finder);


//出力
//100


3.mapメソッド

オブジェクト.map((変数名)=>{return 処理;});
 └ オブジェクト:配列
 └ 変数名:配列から一つづつ取り出した要素の変数名

・要素一つづつに指定した処理を行う。
・処理の例:数値を2倍するなど。
・console.logなどで処理自体を呼び出す
 └ 関数はreturnで数値に置き換わる。

const numbers=[1,10,100,1000];

const triple = numbers.map((number)=>{
    return number*3;
});

console.log(triple);


//出力
[ 3, 30, 300, 3000 ]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Meetでもバーチャル背景を使いたい

はじめに

最近色々なWeb会議システムを使うことが増えました。未経験だったZoomや、久しぶりにSkypeも使ってます。
Zoomのバーチャル背景が人気ですが、確かに便利です。が、残念ながら私の仕事で標準のGoogle Meetでは、まだ使うことはできません。

Snap Cameraや、OBSなどOSで仮想カメラとして認識されるアプリを使えば可能ですが、そこはWebシステムなのでJavaScriptでなんとかしたいですよね。そこで無理やりやってみた、という話です。(Chrome限定です)

meet_fuji.png

使える材料

tensorflow.js + body-pix

以前の私のの記事で紹介したように、tensorflow.js + body-pix を使えば人体検出+マスク処理ができます。これを使えば自分で作ったアプリなら、バーチャル背景はできそうです。

実際にこちらのかたの記事では、色々組み合わせてバーチャル背景を実現しています。(BodyPix.toMask()の解析は、とても参考になりました)

Chrome Extension

上記の処理を、Google MeetのようにすでにあるWebアプリに反映する方法がわからず途方に暮れていたところ、次の記事を発見しました。

これだ! ということで早速真似させていただきました。

実装

ソースコードはGitHubで公開しています。それを抜粋して紹介します。

画像を背景に合成

Body-pixを使った人体検出は、以前の記事を参照ください(前編後編)。今回は画像を背景に合成する部分のコードの概要を記載します。

// canvas ... 合成に使うcanvas
// segmentation ... Body-pixで抽出したセグメンテーデョン
// frontElement ... 人物が映った映像(video要素)
// backElement ... 背景にする画像(img要素)
function _drawFrontBackToCanvas(canvas, segmentation, frontElement, backElement) {
  const ctx = canvas.getContext("2d");
  const width = canvas.width;
  const height = canvas.height;

  // 先に人物が映った映像(前景)を描画し、イメージとして取っておく
  ctx.drawImage(frontElement, 0, 0);
  const front_img = ctx.getImageData(0, 0, width, height);

  // 背景になる画像を描画し、イメージを取り出す
  const srcWidth = backElement.naturalWidth;
  const srcHeight = backElement.naturalHeight;
  ctx.drawImage(backElement, 0, 0, srcWidth, srcHeight,
    0, 0, width, height
  );
  let imageData = ctx.getImageData(0, 0, width, height);

  // セグメントを走査し、人体の部分だったら前景の画像の値を背景の画像に合成する
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let base = (y * width + x) * 4;
      let segbase = y * width + x;
      if (segmentation.data[segbase] == 1) { // is fg
        // --- 前景 ---
        pixels[base + 0] = front_img.data[base + 0]; // R
        pixels[base + 1] = front_img.data[base + 1]; // G
        pixels[base + 2] = front_img.data[base + 2]; // B
        pixels[base + 3] = front_img.data[base + 3]; // α
      }
    }
  }

  // 合成した画像を、改めてキャンバスに描画する
  ctx.putImageData(imageData, 0, 0);
}

mediaDevices.getUserMedia()のフック

今回は画像との合成を自分の作ったアプリを対象とするのではなく、Google Meetのような既存のWebアプリに対して行うのが目的です。

そこで、次のように mediaDevices.getUserMedia()をフックして、こちらで用意した処理に差し替えます。

hook_getusermedia.png

実際に差し替える処理は次のようになります。

  function _replaceGetUserMedia() {
    if (navigator.mediaDevices._getUserMedia) {
      // すでに置き換え済みなら、何もしない
      return;
    }

    // 元の処理を取っておく
    navigator.mediaDevices._getUserMedia = navigator.mediaDevices.getUserMedia

    // 自分で用意した関数に置き換える
    navigator.mediaDevices.getUserMedia = _modifiedGetUserMedia;
  }

getUserMedia()を置き換える、Body-pixを使った処理はこちらです。(上記の_modifiedGetUserMedia()から、呼び出しています)

  function _startBodyPixStream(withVideo, withAudio, constraints) {
    return new Promise((resolve, reject) => {
      // まずはデバイスの映像を取得する(指定されていれば音声も)
      navigator.mediaDevices._getUserMedia(constraints).
        then(async (stream) => {
          // 映像が取得できたら、非表示のvideo要素でsaisei
          video.srcObject = stream;
          await video.play().catch(err => console.error('local play ERROR:', err));
          video.volume = 0.0;

          // Canvasを更新する処理を、requestAnimationFrame()で呼び出す
          requestAnimationFrame(_updateCanvasWithMask);

          // Canvasから映像ストリームを取り出す
          const canvasStream = canvas.captureStream(10);
          if (!canvasStream) {
            reject('canvas Capture ERROR');
          }
          keepAnimation = true;

          // 定期的にbody-pixによる人体セグメンテーション検出を呼び出す
          _bodypix_updateSegment();

          // 利用側で映像の停止が呼び出されたら、元のデバイスの映像も停止させるように処理を追加
          const videoTrack = canvasStream.getVideoTracks()[0];
          if (videoTrack) {
            videoTrack._stop = videoTrack.stop;
            videoTrack.stop = function () {
              keepAnimation = false;
              videoTrack._stop();
              stream.getTracks().forEach(track => {
                track.stop();
              });
            };
          }

          // --- 音声が指定されていたら、デバイスからの音声をCanvasの映像に追加する
          if (withAudio) {
            const audioTrack = stream.getAudioTracks()[0];
            if (audioTrack) {
              canvasStream.addTrack(audioTrack);
            }
          }

          resolve(canvasStream);
        })
        .catch(err => {
          reject(err);
        });
    });
  }

ここでは説明を省きますが、_updateCanvasWithMask()の内部で先に説明した_drawFrontBackToCanvas()を呼び出して、背景の合成を行います。

Chrome Extension の Contents Script の利用

上記のコードを使って既存のWebアプリをフックするのは、Chrome Extension を利用します。対象となるページにJavaScriptを差し込んだり、DOM要素をいじったいるすることができます。

manifest.json

Chrome Extension を使うには、manifest.json を用意します。今回は次のような感じです。

manifest.json
{
  "manifest_version": 2,
  "content_scripts": [
    {
      "matches": [
        "http://localhost:*/*",
        "https://meet.google.com/*",
      ],
      "js": [
        "loader.js"
      ],
      "run_at": "document_start"
    }
  ],
  "permissions": [
    "https://localhost:*/",
    "https://meet.google.com/",
  ],
  "web_accessible_resources": [
    "cs.js"
  ]
}

  • content_scripts
    • maches: 対象とするサイトのURL(ここでは、localhostと、Google Meet)
    • js: 実行するJavaScript(用しておいたjsファイルを、対象サイトに差し込む処理を担う)
    • run_at: 上記jsを実行するタイミング。この例では、元のサイトに元々含まれるJavaScriptよりも先に実行する
  • permissions ... このExtensionが操作できる対象サイトを指定(ここでは、localhostと、Google Meet)
  • web_accessible_resources ... Extensionに埋め込んで配布するリソース
    • ここでは、サイトに差し込むJavaScriptファイル。実際の人体検出、背景合成処理を行う

コンテンツを差し込む、loader.js

対象サイト読み込み時に先立って実行される loader.js の処理は、次のようになっています。

loader.js
async function load() {
  // cs.jsの内容を読み込み、scriptタグを作ってdocumentに差し込む
  const res = await fetch(chrome.runtime.getURL('cs.js'), { method: 'GET' })
  const js = await res.text()
  const script = document.createElement('script')
  script.textContent = js
  document.body.insertBefore(script, document.body.firstChild)

  // 外部の tfjsの内容を読み込み、scriptタグを作ってdocumentに差し込む
  // --- tfjs ---
  const res_tf = await fetch('https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@1.2', { method: 'GET' })
  const js_tf = await res_tf.text();
  const script_tf = document.createElement('script');
  script_tf.textContent = js_tf;

  // body-pixの処理を読み込むscriptタグを作って、documentに差し込む(後で読み込ませる)
  // --- bodypix ---
  const script_bp = document.createElement('script');
  script_bp.src = 'https://cdn.jsdelivr.net/npm/@tensorflow-models/body-pix@2.0';

  document.body.insertBefore(script_bp, document.body.firstChild);
  document.body.insertBefore(script_tf, document.body.firstChild);
}

// window.onload()イベントで実行する
window.addEventListener('load', async (evt) => {
  _loaderlog('event load'); // 元のindex.html の中の処理より後に呼ばれる
  await load();
}, true); // use capture

tfjsとbody-pixを読み込むタイミングを調整するために読み込み方法を変えています。将来的にはタイミングが変わってきてしまうかも知れません。

差し込まれるコンテンツ cs.js

差し込まれる cs.js は、色々な処理を行っていますが、主要な役割は次の3つです。

  • navigator.meidaDevices.getUserMedia()をフックして、自前の処理を呼ぶように置き換える
  • tfjs + body-pixを用いたバーチャル背景の合成を行う
    • ※実際には他にも様々な仮想的カメラ映像を作れるように処理を用意しています
  • 仮想映像の種類を選んだり、背景に使う画像を選択するUIを提供する

最初の2つはすでに概要を示しています。最後のUIを提供するのは、次のようにHTMLにDOM要素を差し込んでいます。

  // --- ファイル選択GUIを挿入 ---
  function _insertPanel(node) {
    try {
      // 最小化された状態のパネルを、左上に表示する
      const html1 =
        `<div id="gum_panel" style="border: 1px solid blue; position: absolute; left:2px; top:2px;  z-index: 2001; background-color: rgba(192, 250, 192, 0.5);">
        <div><span id="gum_pannel_button">[+]</span><span id="gum_position_button">[_]</span></div>
        <table id="gum_control" style="display: none;">
          <tr>
            <td><label for="video_type">種類</label></td>
            <td>
              <select id="video_type" title="Google Meetではいったんカメラをオフ→オンしてください">
                <option value="camera" selected="1">デバイス</option>
                <option value="file">ファイル</option>
                <option value="clock">時計</option>
                <option value="screen">画面キャプチャー</option>
              </select>
            </td>
            <td colspan="2"><span id="message_span">message</span></td>
          </tr>
          <tr>
            <td><label for="video_file">動画</label></td>
            <td><input type="file" accept="video/mp4,video/webm" id="video_file"></td>
            <td><label for="image_file">背景</label></td>
            <td><input type="file" accept="image/*" id="image_file"></td>
          </tr>
        </table>
        </div>`;

      // 最後に差し込む
      node.insertAdjacentHTML('beforeend', html1);

      // イベントハンドラを追加する 
      node.querySelector('#video_file').addEventListener('change', (evt) => {
        _startVideoPlay();
      }, false);

    } catch (e) {
      console.error('_insertPanel() ERROR:', e);
    }
  }

しょぼいUIですが、[+][_] という簡易ボタンが並んだパネルを左上に表示しています。

  • [+]をクリックすると、パネルが広がって仮想カメラの種類や、背景画像が選択できます
  • [_]をクリックすると、パネルが左下に移動します

contents scriptが動くタイミング

  • loader.js ... run_at:"document_start" を指定しているため、元のページのJavaScriptよりも前に動く
  • cs.js ... 実際に差し込まれるのは window.onload()イベントの最初。元のページのbodyに直接書かれていたJavaScriptよりは後になる
    • そのため、なのでbodyで直接 getUserMedia()が呼び出されていると、フックする前に元々のgetUserMedia()が呼び出されてしまう

残念ながら、ユーザーの操作を待たずにカメラ映像を取得していまうサイトでは、今回のExtensionによる差し替えは通用しません。

バーチャル背景を使ってみる

Chrome Extension の読み込み

Chrome Web App には登録していないので、自分でChromeに読み込む必要があります。

  • コードを https://github.com/mganeko/chrome_virtual_camera からダウンロード
  • 使いたいサイトに合わせ、manifest.json を編集
    • content_scripts, permissions のセクションを編集し、使いたいサイトを追加する
    • 同様に、使いたくないサイトを除外する
  • 拡張機能の設定画面(chrome://extensions/)を開く
  • 右上の「デペロッパーモード」を有効に
  • 「パッケージ化されていない拡張機能を読み込む」からダウンロードしたリポジトリのフォルダーを選択し、読み込む
  • 拡張機能のページで、読み込んだ「Chrome Virtual Camera」が表示されれ、有効になっているのを確認

chrome_extension_page.png

対象サイトでの利用

  • Chrome で、対象のサイトにアクセスする
    • 左上に [+][_] ボタンを持つ小さなパネルがオーバーレイ表示される
  • ※こちらのシンプルなテストページでも利用できます

google_meet_small_panel.png

  • [+]ボタンをクリックしてパネルを拡大し、仮想カメラの種類を選ぶ。バーチャル背景以外もサポート
    • デバイス ... マシンのカメラ/マイクを利用(デフォルト)
    • ファイル ... 動画ファイルを選択し、その映像/音声を利用
    • 時計 ... Canvas/WebAudioを利用した、デジタル時計
    • 画面キャプチャー ... getDisplayMedia()を利用した、画面キャプチャーを利用
    • 背景を塗りつぶし ... 人物を検出、背景をグレーで塗りつぶす
    • 背景を画像で隠す ... いわゆるバーチャル背景。人物を検出、背景を画像ファイルで隠す
    • 人物を塗りつぶし ... 人物を検出、人物をグレーで塗りつぶす

google_meet_panel_select.png

  • 動画ファイルや背景の画像ファイルを選ぶ
    • 種類が「ファイル」の場合、動画ファイルを選択
    • 種類が「背景を画像で隠す(バーチャル背景)」の場合、背景の画像ファイルを選択
  • 対象サイトで、カメラ映像/マイク音声の取得を開始、通信を開始
    • mediaDevices.getUserMedia()が呼び出されると、フックした処理が動く  - 選択した種類の映像、音声が取得される

※ Google Meet の場合は、パネルで種類を選択する前にカメラ映像が取得されています。その場合、種類と必要なファイルを選択後に、改めてカメラをオフ --> オンすると、選択された種類の映像が取得されます。

仮想カメラの例

今回、いわゆるバーチャル背景以外の仮想カメラも用意しています。いくつか紹介します。

時計

シンプルなデジタル時計です。Audioが指定されている場合は、1秒ごとに音も鳴ります。開発中のテストで、自分の姿を見続けたくない場合に便利です。

meet_clock.png

Google Meetや多くのWeb会議システムでは、自分の映像は鏡のように左右反転して表示されるので、時計の文字も左右反転になっています。相手には反転なしで表示されます。

ファイル

動画ファイルを指定して、カメラの代わりに映像/音声を流します。Big Buck Bunnyのサンプル動画を使っている例がこちらです。

meet_bunny_video.png

背景を塗りつぶし、人物を塗りつぶし

virtual_camera_paint.png

画面キャプチャー

画面全体をキャプチャーしたり、特定のアプリのウィンドウをキャプチャーすることが可能です。

  • PCのカメラを使って顔を認識して、アバターを動かすアプリ
  • iPhone でアバターを動かすアプリ + macOS + QuickTimeで、ミラーリング

等を使うと、好きなアバターの映像をカメラの代わりに使ってGoogle Meetに参加できます。

おわりに

もちろんnSnap Cameraなどいろいろな仮想カメラソフトがあるので、それを使えばGoogle Meetでもバーチャル背景やARカメラなどが利用できます。が、今回のようにChrome Extensionで実現できれば、いろいろな映像や効果を自分で追加することができます。リモート会議を楽しくするためにも、お試しください。

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

TypeScriptで学ぶデザインパターン〜Proxy編〜

対象読者

  • デザインパターンを学習あるいは復習したい方
  • TypeScriptが既に読めるあるいは気合いで読める方
    • いずれかのオブジェクト指向言語を知っている方は気合いで読めると思います
  • UMLが既に読めるあるいは気合いで読める方

環境

  • OS: macOS Mojave
  • Node.js: v12.7.0
  • npm: 6.14.3
  • TypeScript: Version 3.8.3

本シリーズ記事一覧(随時更新)

Proxyパターンとは

あるクラス(本人)のインターフェースとして機能するクラス(代理人)を用意するためのパターンです。

代理人を用意して利用側は代理人にお仕事を依頼します。また、代理人では対応できないお仕事があったら、そのときはじめて代理人経由で本人にお仕事を依頼します。

サンプルコード

Proxyパターンで作られたクラス群がどんなものになるのか確認していきましょう。

今回は、題材として"生徒の情報を表示する機能"を想定します。GitHubにも公開しています。

modules/StudentInterface.ts

本人と代理人を同一視するためのインターフェースです。

StudentInterface.ts
export default interface StudentInterface {
  setName(name: string): void;
  getName(): string;
  calculateGrade(): void;
}

setNamegetNameはnameプロパティのセッターとゲッター、calculateGradeは成績を表示するためのメソッドです。

modules/Student.ts

生徒(本人)を表現するクラスです。

Student.ts
import StudentInterface from "./StudentInterface";

export default class Student implements StudentInterface{
  private name: string;

  constructor(name: string) {
    console.log('(重い処理・・・)');
    this.name = name;
  }

  setName(name: string): void {
    this.name = name;
  }

  getName(): string {
    return this.name;
  }

  calculateGrade(): void {
    console.log('成績は・・・');
  }
}

constructorでは、擬似的に重い処理を走らせることを想定して文字列を出力しています。これは、処理を走らせたときにいつ本クラスのインスタンスが生成されているのか見やすくするためです。
calculateGradeでは、成績を表示します。今回は特に何もせず単に文字列を表示させるだけに留めます。

modules/StudentProxy.ts

生徒(代理人)を表現するクラスです。

StudentProxy.ts
import StudentInterface from "./StudentInterface";
import Student from "./Student";

export default class StudentProxy implements StudentInterface{
  private name: string;
  private real: Student;

  constructor(name: string) {
    this.name = name;
  }

  setName(name: string): void {
    if (this.real) {
      this.real.setName(name);
    }
    this.name = name;
  }

  getName(): string {
    return this.name;
  }

  calculateGrade(): void {
    this.realize();
    this.real.calculateGrade();
  }

  private realize(): void {
    if (!this.real) {
      this.real = new Student(this.name);
    }
  }
}

nameプロパティの設定(setName)や取得(getName)は代理人が行います。
また、calculateGradeはここでは代理人で処理はせず(できないことを想定)本人(Studentインスタンス)に実施を依頼します。

modules/Main.ts

本デザインパターンで作成されたクラス群を利用している処理です。

Main.ts
import StudentInterface from "./modules/StudentInterface";
import StudentProxy from "./modules/StudentProxy";

const student: StudentInterface = new StudentProxy('A君');
console.log('生徒の名前は' + student.getName());
student.setName('B君');
console.log('生徒の名前は' + student.getName());
student.calculateGrade();

getNamesetNameを呼ぶだけではStudentインスタンスは作成されませんが、calculateGradeを呼ぶとStudentインスタンスが作成されます。

クラス図

ここまでProxyパターンで作られたクラス群を1つずつ確認してきました。次にクラス図を示します。Proxyパターンの全体像を整理するのにお役立てください。

Proxy.png

  • Subject: サンプルコードではStudentInterfaceインターフェースが対応
  • Proxy: サンプルコードではStudentProxyクラスクラスが対応
  • RealSubject: サンプルコードではStudentクラスが対応
  • Client: サンプルコードではMainが対応

LucidChartを使用して作成

解説

最後に、このデザインパターンの存在意義を考えます。

本人の初期化処理過程で重い処理が走ってしまう場合、代理人が処理を肩代わりできるようにすることで、処理スピードを上げることができます。今回のサンプルコードでは実感しにくいと思いますが、初期化処理に時間がかかるようなクラスの中のある一部分の処理をサクッと使いたい場合は効果を発揮するデザインパターンです。

補足

サンプルコードの実行方法はこちらと同様です。

参考

あとがたり

HTTPプロキシーのことを知っている人ならわりと理解しやすいデザインパターンかなと思った。

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

Javascriptの復習(2)

この記事について

Javascriptの復習(2)というタイトル通り2回目です。Javascriptの復習(1)でも書きましたが、初学者がアウトプットとして投稿した記事なので無視してください。

繰り返し処理

例えば、1~100までの数字を出力する場合、2,3行目を100回書けば出力できますが、はっきり言って非効率的です。

script.js
let number = 1;
//下の2行を繰り返せば
console.log(number);
number += 1;

繰り返し処理を用いることで短いコードで済ます事ができます。

script.js
let number = 1;
//やっている事自体は上のコードを100回繰り返すのと同じ。
while(number <= 100) {
  console.log(number);
  number += 1;
}

for文

while文以外にも繰り返し処理ができるコードの書き方があります。for文はより短く書く事ができ、括弧内で"変数の定義"、"繰り返しの条件"、"変数の更新"の全てを書きます。

script.js
let number = 1;
//左から順番に"変数の定義"、"繰り返しの条件"、"変数の更新" セミコロンで区切る
for(let number = 1; number <= 100; number += 1) {
  console.log(number);
}

条件分岐 (if文)

ある条件が成り立つ時にだけ処理を行ないたい時に使用します。

script.js
const number = 12;
//()内が条件式で{}内が条件を満たした時と行なわれる処理です。
if(number >= 10) {
  console.log("numberは10より大きい");
}

条件を満たさなかった場合の処理を書きたいときはelseを使用します。

script.js
const number = 7;
if(number >= 10) {
  console.log("numberは10より大きい");
} else {
//条件を満たしていない時に行なわれる処理です。
  console.log("numberは10より小さい");
}

さらに条件を追加したい時はelse ifを使用します。

script.js
const number = 7;
if(number >= 10) {
  console.log("numberは10より大きい");
} else if(number >= 5) {
//ifの条件を満たしていないがelse if内の条件を満たしている時に行なわれる。
  console.log("5より大きく10より小さい")
} else {
  console.log("numberは5より小さい");
}

switch文

if文と同様に条件分岐の処理が行なえる書き方ですが、定数の値によって処理を分けたい時に使用するとif分よりもコードが短くなります。

script.js
const animal = "";
//括弧内に変数や定数等の条件の値を記載
switch(animal){
  case""://ここはコロン
//条件の値がcaseに書いた値と一致していたら行なわれる処理
    console.log("ニャーと鳴く")
    break;//breakで処理の終わりをハッキリさせる。
}

条件の数だけcaseを追加してください。

script.js
const animal = "";
//括弧内に変数や定数等の条件の値を記載
switch(animal){
  case""://ここはコロン
    console.log("ニャーと鳴く")
    break;//breakで処理の終わりをハッキリさせる。
  case""://しつこいけどここはコロン
    console.log("ワンと鳴く")
    break;
  case"カラス"://コロン!!
    console.log("カァーと鳴く")
    break;
}

どの条件にも該当しない時の処理は、default以下に書きます。

script.js
const animal = "";
//括弧内に変数や定数等の条件の値を記載
switch(animal){
  case""://ここはコロン
    console.log("ニャーと鳴く")
    break;//breakで処理の終わりをハッキリさせる。
  case""://しつこいけどここはコロン
    console.log("ワンと鳴く")
    break;
  case"カラス"://コロン!!
    console.log("カァーと鳴く")
    break;
  default:
//if文のelseのようなもの
    console.log("そのような動物は存在しません。");
    break;
}

複数の条件の書き方(and と or)

括弧内に複数の条件を書く方法があります。

script.js
const number = 53;
//&&を挟むと両方の条件を満たした時に処理が行なわれる条件式になる(andの条件)。
//||を挟むと片方の条件を満たしていれば処理が行なわれる(orの条件)。
if(number >= 10 && number < 100) {
  console.log("numberは2桁です");
} else {
  console.log("numberは1桁です");
}

 感想

既にRubyを学習した後だったお陰かすんなりと頭の中に入った。
この勢いでJavaScriptの全容を把握できたらいいなと思います。

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

2. State 管理を Redux に移行する - Redux よくわからんので Todo つくる

はじめに

前回は、React のみでこのようななんでもない Todo アプリを作成しました
react-todo.gif

はい
なんでもないですね

今回は当初の目的である、State 管理の Redux への移行を行います

今回からいろいろと Rollup に惨敗して Webpack 使ってます
なんにも知らないくせに使うから...

前回のコードに変更はないので、それだけ

なぜ Redux を使うのか?

「なぜ State 管理を分離するのか?」と言ったほうが正しいかも

今回のような、小規模も小規模なアプリではほぼ無縁な問題ですが、アプリの規模が大きくなると、以下のような様々な問題が浮き彫りとなりやがります

  • State 分散されすぎ... いちいち別ファイル開くのが面倒、てかどこ? ?
    • (トップコンポーネントに State 集中しすぎることのほうが多そう
  • State 管理用のコードがコンポーネント内に増えまくり... お手上げ ?‍♂️
  • UI、State 処理のコードが混在して、コードが読みづらい... ?
  • Prop のバケツリレーつらい ?
  • etc...

しかし、Redux を使うと...?

  • State は一点管理✨ どこにあるかも一目瞭然!! ?
  • State 管理のコードはコンポーネント外部に追いやり! State 管理は Redux に任せとき~~ ?
  • コンポーネントは描画に集中! 内部には UI 処理のコードだけ!! ?
  • 必要なコンポーネントが直接 State 変更関数を受け取り!! ?
  • etc...

YEAH ?

React 単体でもある程度マシに出来ると思いますが、そもそも React は UI 構築ライブラリであることを意識しておいたほうが良いんじゃないかなあ~と思います
(結局バケツリレーはどうにもなりませんし)

あくまで React の言う State というのは "UI(表示) に関するもの" のことであって "表示されるデータ" では無い...的な(わかって)
そこをはっきりとさせることで、開発の効率は確実に上がると考えます
(バケツリレーも無くなりますし)

コーディングの基本は分割...ってね

ディレクトリ構造

src/
 ├ components/
 │  └ app/
 ├ actions/
 ├ store/
 │  └ reducer/
 └ index.ts - エントリ

まず、Redux 用のフォルダが追加されるため、前回 src/ 直下にあった App コンポーネントフォルダを components/ に移動しています

そして、Redux 用に actions/, store/, store/reducer/ を作成します

パッケージの追加

yarn add redux react-redux
yarn add -D @types/react-redux

Store の作成

まずは Store を作らないことには React との連携もへったくれも無いので Redux 単体の機能である Store を作成します

必要な要素

Action
コンポーネントが、Store に対して「何が起きたか」を説明する
store.dispatch() を使用して Store に送信する

Action は以下のような type プロパティを持つただのオブジェクト

{
  type: "Reducer が Action の種類を識別するための文字列",
  // あとは自由
  // State 変更に必要なデータを入れておく
}

Action Creator
コンポーネントからデータを受け取り、Action を作成する

Reducer
Store に送信された Action を受け取り、State がどのように変化するかを指定する
事実上実際 State の変更を担当する大事なトコ

Store
アプリケーションに一つだけ存在し、State を保持する

  • State へのアクセス手段 (store.getState())
  • State の更新手段 (store.dispatch())
  • 更新リスナの登録 (store.subscribe())

を提供する

Action Creator

誤字とかでエラーが出るのを防ぐために Action type を別に定義しておきます
Action Creator に渡した時に string 型になるのを防ぐために as const を付けています

src/actions/todo/index.ts
const ADD_TODO = "ADD_TODO" as const;
const TOGGLE_COMPLETED = "TOGGLE_COMPLETED" as const;
const DELETE_TODO = "DELETE_TODO" as const;
src/actions/filter/index.ts
const SET_FILTER = "SET_FILTER" as const;

あとは必要なデータを引数に受け取り、いい感じに加工して Action を作ります
若干 JSON API 意識で、必要なデータは全て data プロパティ内に入れています

src/actions/todo/index.ts
const addTodo = (text: string) => ({
  type: ADD_TODO,
  data: { id: Math.random(), text, complete: false },
});

const toggleCompleted = (id: number, isCompleted: boolean) => ({
  type: TOGGLE_COMPLETED,
  data: { id, isCompleted },
});

const deleteTodo = (id: number) => ({
  type: DELETE_TODO,
  data: id,
});
src/actions/filter/index.ts
const setFilter = (filter: FilterStateType) => ({
  type: SET_FILTER,
  data: filter,
});

型定義

Reducer で型チェックするため、ReturnType を使用して、Action の型を定義します

ReturnType<typeof addTodo>
// ⇓
{
  type: "ADD_TODO";
  data: {
    id: number;
    text: string;
    complete: boolean;
  };
}
src/actions/todo/types.ts
type AddTodoAction = ReturnType<typeof addTodo>;
type ToggleCompletedAction = ReturnType<typeof toggleCompleted>;
type DeleteTodoAction = ReturnType<typeof deleteTodo>;

type TodoActions =
  | AddTodoAction
  | ToggleCompletedAction
  | DeleteTodoAction;
src/actions/filter/types.ts
type SetFilterAction = ReturnType<typeof setFilter>;

Reducer

引数の state, action の型定義のため、Redux.Reducer<State, Action> を使用します

初期化時には stateundefined が渡されるため、初期値を設定し
Action type で識別し、新しい State を返します

default case では引数 action を never 型に割り当てることで、絞り込みの漏れが無いようにしています
参考: TypeScript 2.0のneverでTagged union typesの絞込を漏れ無くチェックする

State の生成コードは前回と変わりないですね

src/store/reducer/todo/index.ts
const todoReducer: Redux.Reducer<TodoStateType, TodoActions> = (
  state = new Map(),
  action
) => {
  switch (action.type) {
    case ADD_TODO:
      return new Map(state.set(action.data.id, action.data));

    case TOGGLE_COMPLETED:
      const todo = state.get(action.data.id);

      if (todo) {
        return new Map(
          state.set(todo.id, { ...todo, complete: action.data.isCompleted })
        );
      }
      return state;

    case DELETE_TODO:
      state.delete(action.data);
      return new Map(state);

    default:
      const __check: never = action;
      return state;
  }
};

Filter の Action は 1種類だけなので if で

src/store/reducer/filter/index.ts
const filterReducer: Redux.Reducer<FilterStateType, SetFilterAction> = (
  state = "ALL",
  action
) => {
  if (action.type === SET_FILTER) return action.data;

  return state;
};

分割された Reducer を combineReducers を使用して1つにまとめます

src/store/reducer/index.ts
combineReducers({ todoReducer, filterReducer });

Store

Reducer を createStore に渡せば Store の完成です

src/store/index.ts
createStore(reducer);

型定義

前回の State 型と
Store 全体の型を定義します

src/store/types.ts
type TodoType = { readonly id: number; text: string; complete: boolean };
type TodoStateType = Map<number, TodoType>;

type FilterStateType = "ALL" | "COMPLETED" | "ACTIVE";

type StoreType = {
  todo: TodoStateType;
  filter: FilterStateType;
};

React との連携

私が React 触り始めるより前の話でしたが、hooks に対応したため、面倒な stateToProps だとか dispatchToProps なんかは書かずに、呆れるほど簡単に連携可能になりました

ここが一番面倒で厄介でよくわからん意味不明な所だったのでもうこれは革命です
Redux 全然よくわからんくないです、タイトル詐欺です
(一回 connect を使って書いちゃった後に気づいたのはひみつ)

まずはトップコンポーネントを Provider でラップし、store を渡します
これで、ネストされたコンポーネントで Redux にアクセス出来るようになります

src/components/app/index.tsx
import { Provider } from "react-redux";
...
const App: React.FC = () => {
  return (
    <Provider store={store}>
      <AddTodo />
      <ToggleFilter />
      <TodoList />
    </Provider>
  );
};

そうすれば後は以下の hooks で実際に Redux にアクセスし、State の取得、Dispatch を行うだけです

useSelector()

stateToProps に当たる機能です

引数として、selector 関数を受け取り、store の値を返します
selector 関数は引数に store を受け取り、値を返す関数です

TodoList での例

src/components/app/todo-list/index.ts
import { useSelector } from "react-redux";
...
const TodoList: React.FC = () => {
  // Todo のリストを取得
  const todoList = useSelector((store: StoreType) => store.todo);
  // 現在のフィルタを取得
  const filter = useSelector((store: StoreType) => store.filter);
  ...

useDispatch()

dispatchToProps に当たる機能です

store.dispatch() を返します

Todo での例
特定の Action のみ受け付けるよう型で縛ってます

src/components/app/todo-list/todo/index.ts
import { Dispatch } from "redux";
import { useDispatch } from "react-redux";
...
const Todo: React.FC<Props> = ({ todo }) => {
  const dispatch = useDispatch<Dispatch<ToggleCompletedAction | DeleteTodoAction>>();
  ...

まとめ

私の言う「Redux よくわからん」は十中八九 connect 周りの事だったので、hooks が使えたことで、大体 Prop で受け取っていた所を、useSelector, useDispatch に変更した感じになっちゃいました
記事書く前は使えるとか知らなかったんだもん...

それはそれとして、ディレクトリ構造とか型定義の場所とかを自分的に整理することができたので良かったです

趣旨ズレですが、Redux を使う利点もよく分かったと思います
TodoList とかが特に分かりやすい: Only React / With Redux

今回作成したコードは こちら (canoypa/react-redux-test-todo-app) にあります

Prev: とりあえず React だけで Todo
React (State) -> Redux の流れを掴むため、という名目で記事稼ぎのため一旦 React のみで Todo アプリをつくってます

参考

Redux入門【ダイジェスト版】10分で理解するReduxの基礎 、及びもと記事
Redux Docs
React Redux Docs

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

SpeechRecognitionで繰り返しマイクを拾い続ける

話している言葉をずっとテキストに起こし続けたかった

そのためにはマイクをずっと動かして声を拾い続けないといけません。SpeechRecognitionは時間が一定経つと自動的に終わってしまいます。その回避策です。

そもそも何故そうしたかったのかというとリアルタイムに声をテキストとして表示する「Zimack」というサイトを作った というので詳細を書いているのですが動画配信用に字幕を出すものが欲しかったためです。それで手ごろだったのがVueでwebページを作ってOBSでキャプチャすることでした。

Zimack

リポジトリ

実装内容

今回の実装はApp.vueにメソッドとして書いています。それを切り抜いて以下のような流れになります。

  data: () => ({
    speechRecognition: window.SpeechRecognition || window.webkitSpeechRecognition,
    isFirstStarted: false,
-----省略------
  methods: {
    settingRecognition () {
      var recognition = new this.speechRecognition()
      recognition.lang = 'ja-JP'
      recognition.interimResults = true
      recognition.continuous = true
      // start時のコールバック、フラグを立てる
      recognition.addEventListener('start', () => {
        this.isFirstStarted = true
      })

   // テキスト起こしの途中経過が入ってくる
      recognition.addEventListener('result', event => {
        const stackText = Array.from(event.results).map(x => x[0]).map(x => x.transcript)
        this.currentText = stackText.join('')
      })

      // end時のコールバック、複数回呼ばれる可能性があるのでフラグが立っていれば再度テキスト起こしを始める
      recognition.addEventListener('end', () => {
        if (this.isFirstStarted) {
          recognition.start()
          this.isFirstStarted = false
        }
      })

      recognition.start()
    }
  },
-----省略------
  mounted () {
    // SpeechRecognitionが扱えるのであればundefinedにならない
    if (!this.speechRecognition) {
      alert('ChromeなどのSpeechRecognitionに対応したブラウザをお使いください。')
      return
    }
    this.settingRecognition()
-----省略------

endが複数回呼ばれたりして適切なタイミングで start() を行わないとエラーが出たりブラウザが重くなったりしていました。

まとめ

サクッと1日くらいで作ったものなので変数名や処理の流れの雑さは否めないのですが自分は躓いたのでメモ書きとして残します。
Youtubeライブとかの配信でも字幕を出せるようになるので意外と面白いです。すこし間違ったテキストになるのもまた好きなのでサクッと実装できるこのようなapiがあることに感謝ァ…

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

AR.jsとthree.jsでMarker Based AR

AR.jsでMarker Based ARを行う場合、3DライブラリとしてA-Frameとthree.jsのどちらかを選べます。この記事では、three.jsでAR.jsのMarker Based ARを試してみたいと思います。

Marker Based - AR.js Documentation


サンプルとして作成したのは、以下のようにhiroマーカー上に回転するキューブを表示するだけのシンプルなものです。
arjs-crop.gif

コード全文は以下のようになります。このコードはAR.jsのリポジトリ内にあるサンプルを参考にしています。このサンプルを実行するにはdataというディレクトリを作成して、その中にcamera_para.datpatt.hiroを配置する必要があります。

<!DOCTYPE html>
<html>
  <head>
    <title>Marker Based AR with AR.js and Three.js</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.js"></script>
    <script src="https://raw.githack.com/AR-js-org/AR.js/3.1.0/three.js/build/ar.js"></script>
  </head>
  <body>
    <script>
      const renderer = new THREE.WebGLRenderer({
        antialias: true,
        alpha: true
      });
      renderer.setClearColor(new THREE.Color(), 0);
      renderer.setSize(640, 480);
      renderer.domElement.style.position = 'absolute';
      renderer.domElement.style.top = '0px';
      renderer.domElement.style.left = '0px';
      document.body.appendChild(renderer.domElement);

      const scene = new THREE.Scene();
      scene.visible = false;
      const camera = new THREE.Camera();
      scene.add(camera);

      const arToolkitSource = new THREEx.ArToolkitSource({
        sourceType: 'webcam'
      });

      arToolkitSource.init(() => {
        setTimeout(() => {
          onResize();
        }, 2000);
      });

      addEventListener('resize', () => {
        onResize();
      });

      function onResize() {
        arToolkitSource.onResizeElement();
        arToolkitSource.copyElementSizeTo(renderer.domElement);
        if (arToolkitContext.arController !== null) {
          arToolkitSource.copyElementSizeTo(arToolkitContext.arController.canvas);
        }
      };

      const arToolkitContext = new THREEx.ArToolkitContext({
        cameraParametersUrl: 'data/camera_para.dat',
        detectionMode: 'mono'
      });

      arToolkitContext.init(() => {
        camera.projectionMatrix.copy(arToolkitContext.getProjectionMatrix());
      });

      const arMarkerControls = new THREEx.ArMarkerControls(arToolkitContext, camera, {
        type: 'pattern',
        patternUrl: 'data/patt.hiro',
        changeMatrixMode: 'cameraTransformMatrix'
      });

      const mesh = new THREE.Mesh(
        new THREE.CubeGeometry(1, 1, 1),
        new THREE.MeshNormalMaterial(),
      );
      mesh.position.y = 1.0;
      scene.add(mesh);

      const clock = new THREE.Clock();
      requestAnimationFrame(function animate(){
        requestAnimationFrame(animate);
        if (arToolkitSource.ready) {
          arToolkitContext.update(arToolkitSource.domElement);
          scene.visible = camera.visible;
        }
        const delta = clock.getDelta();
        mesh.rotation.x += delta * 1.0;
        mesh.rotation.y += delta * 1.5; 
        renderer.render(scene, camera);
      });
    </script>
  </body>
</html>

コードについて順に解説していきます。

まず、AR.jsとthree.jsをインポートします。AR.jsではDocumentationに書いてあるように、使用する機能(Marker BasedまたはImage Tracking)と3Dライブラリ(A-Frameまたはthree.js)ごとに異なるビルドが提供されているので注意してください。ここでは、2020年5月25日現在において最新バージョンであるAR.js 3.1.0と、CDNで取得できるうちの最新バージョンであるthree.js r110を使用しています。

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.js"></script>
<script src="https://raw.githack.com/AR-js-org/AR.js/3.1.0/three.js/build/ar.js"></script>

次にスクリプトを見ていきます。

最初にTHREE.WebGLRendererを作成します。AR.jsでは3D描画を行うcanvas要素の背面にカメラから取得した映像を描画するHTML要素を配置しています。3Dオブジェクトが存在しない箇所では背景のHTML要素が見えるようにするためにalpha: trueを設定して、setClearColorの第2引数を0にする必要があります(参考: javascript - Transparent background with three.js - Stack Overflow)。また、全画面表示することを想定しているのでstyleでマージンが生じないようにします。

const renderer = new THREE.WebGLRenderer({
  antialias: true,
  alpha: true
});
renderer.setClearColor(new THREE.Color(), 0);
renderer.setSize(640, 480);
renderer.domElement.style.position = 'absolute';
renderer.domElement.style.top = '0px';
renderer.domElement.style.left = '0px';
document.body.appendChild(renderer.domElement);

次にシーンとカメラを作成します。 マーカーを検出するまではシーン内のオブジェクトが表示される必要がないので、scene.visibleの初期値はfalseにしています。カメラのパラメータはデバイスのカメラのパラメータに合わせるために後から書き換えられるので、THREE.PerspectiveCameraなどではなく基底クラスのTHREE.Cameraを使用しています。

const scene = new THREE.Scene();
scene.visible = false;
const camera = new THREE.Camera();
scene.add(camera);

THREEx.ArToolkitSourceを作成して、解析対象とする画像ソースを取得するようにします。webcamを指定してデバイスのカメラを使うようにするのが一般的だと思いますが、静止画(image)や動画(video)を使用することも可能です。

const arToolkitSource = new THREEx.ArToolkitSource({
  sourceType: 'webcam'
});

画像ソースの準備が完了したら、3Dを描画するcanvas要素と背景のカメラ映像を表示するHTML要素が画面いっぱいに表示されるように、サイズ調整を行います。arToolkitSource.onResizeElementで、カメラ映像のHTML要素のサイズを調整し、arToolkitSource.copyElementSizeToでcanvasも同じ大きさになるようにしています。arToolkitSource.initonResizeの実行を2秒ほど遅らせている理由はよくわかりませんが、参考にしたサンプルでは意図的にこの処理を入れているようなので、ここでも入れておきました。

arToolkitSource.init(() => {
  setTimeout(() => {
    onResize();
  }, 2000);
});

addEventListener('resize', () => {
  onResize();
});

function onResize() {
  arToolkitSource.onResizeElement();
  arToolkitSource.copyElementSizeTo(renderer.domElement);
  if (arToolkitContext.arController !== null) {
    arToolkitSource.copyElementSizeTo(arToolkitContext.arController.canvas);
  }
};

THREEx.ARToolkitContextの作成を行います。cameraParametersUrlにカメラのキャリブレーションに必要なデータを格納するファイルのパスを指定します。ここで指定したファイルの情報をもとにカメラのプロジェクション行列を設定します。detectionModeについてはよくわかってないですが、Marker Based ARの場合はモノクローム(mono)でいいのかなという気がします。

const arToolkitContext = new THREEx.ArToolkitContext({
  cameraParametersUrl: 'data/camera_para.dat',
  detectionMode: 'mono'
});

arToolkitContext.init(() => {
  camera.projectionMatrix.copy(arToolkitContext.getProjectionMatrix());
});

THREEx.ArMarkerControlsが一つのマーカーに対応しています。THREEx.ArMakerControlsの第2引数はマーカーを発見したときに、そのマーカーと整合性をあわせるために位置や回転を操作するObject3Dを指定します。
今回はcameraを第2引数に指定しているので、カメラを動かすためにchangeMatrixModecameraTransformMatrixを指定します。changeMatrixModeにはmodelViewMatrixという値を設定することも可能です。 modelViewMatrixを使用する方法はこの記事の最後に書きました。

const arMarkerControls = new THREEx.ArMarkerControls(arToolkitContext, camera, {
  type: 'pattern',
  patternUrl: 'data/patt.hiro',
  changeMatrixMode: 'cameraTransformMatrix'
});

マーカーが発見されたときに表示するキューブを作成して、シーンに追加しておきます。

const mesh = new THREE.Mesh(
  new THREE.CubeGeometry(1, 1, 1),
  new THREE.MeshNormalMaterial(),
);
mesh.position.y = 1.0;
scene.add(mesh);

最後にレンダリングループを回します。マーカーが見つかったかどうかに応じてTHREEx.ArMakerControlsの第2引数で指定したオブジェクト(ここではcameraのこと)のvisibleが操作されます。その値をシーンのvisibleに反映することでマーカーが発見されたときにキューブが描画されるようにしています。

const clock = new THREE.Clock();
requestAnimationFrame(function animate(){
  requestAnimationFrame(animate);
  if (arToolkitSource.ready) {
    arToolkitContext.update(arToolkitSource.domElement);
    scene.visible = camera.visible;
  }
  const delta = clock.getDelta();
  mesh.rotation.x += delta * 1.0;
  mesh.rotation.y += delta * 1.5; 
  renderer.render(scene, camera);
});

ソースコードについて一通り解説しました。

最後にTHREEx.ArMarkerControlsで指定するchangeMatrixModeのもう一つの値であるmodelViewMatrixについても見ていきたいと思います。modelViewMatrixを指定する場合、ArMarkerControlsの第2引数は以下のようにカメラではなくなります。ここでは階層構造を表したいだけなのでTHREE.Groupを使用しています。一度に複数のマーカーを使用する場合には、個々のマーカーに対してvisibleのオンオフを変更できるので、こちらを使用したほうがいいと思います。

const marker = new THREE.Group();
scene.add(marker);
const arMarkerControls = new THREEx.ArMarkerControls(arToolkitContext, marker, {
  type: 'pattern',
  patternUrl: 'data/patt.hiro',
  changeMatrixMode: 'modelViewMatrix'
});

const mesh = new THREE.Mesh(
  new THREE.CubeGeometry(1, 1, 1),
  new THREE.MeshNormalMaterial(),
);
mesh.position.y = 1.0;
marker.add(mesh);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npmでインストールされたパッケージの存在を意識する

はじめに

npmインストールされたパッケージに攻撃コードが混入されているケースがなきにしもあらずなので認知はしといたほうがいいかも?という話

こちらこちらの記事に書いてあるように過去npmパッケージには攻撃コードが混入されていたこともあるようです。

いろいろパッケージをインストールしてみる

Reactで簡単なwebアプリを作成する想定で(具体的な流れは割愛。インストールされるnpmパッケージに着目していきます。)どのくらいのパッケージがインストールされるか確認してみる。

babel関連のインストール

まずは以下のようにbabel関連をインストール

npm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader
+ babel-loader@8.1.0
+ @babel/preset-react@7.9.4
+ @babel/core@7.9.6
+ @babel/preset-env@7.9.6
added 176 packages from 84 contributors and audited 176 packages in 50.712s

7 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

指定した4つのパッケージと依存関係にある176のパッケージがインストールされました。

webpack関連のインストール

次にwebpack関連のインストール

npm install --save-dev webpack webpack-cli
+ webpack-cli@3.3.11
+ webpack@4.43.0
added 389 packages from 224 contributors and audited 565 packages in 26.474s

11 packages are looking for funding
  run `npm fund` for details

指定した2つのパッケージと依存関係にある389のパッケージがインストールされました。

React関連のインストール

npm install --save react react-dom
+ react@16.13.1
+ react-dom@16.13.1
added 5 packages and audited 570 packages in 5.488s

11 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

指定した2つのパッケージと依存関係にある5のパッケージがインストールされました。

合計570のパッケージがインストールされました。
ここから本題

多数のインストールされたパッケージを意識する

冒頭で述べた通り、過去npmパッケージに悪意のある攻撃コードが混入されていたこともあるようです。

また、is-promiseというパッケージが原因でビルドエラーがおき不具合が起きたこともあるそうです。

参考
https://qiita.com/stm32p103/items/7b1e00d6d840bb20e1ba

npmはとても楽。

しかし、使用するだけでなく、その背景で依存関係を解消するために数多くのパッケージがインストールされており、それらが原因で不具合や悪意のあるユーザーの攻撃をうけるかもしれないということを意識する必要があると思う。

まとめ

npmは使うだけでなく、何をしていて、何をインストールしているのかざっくりでも理解しといたほうがいいかもしれないという話でした。

また、npmパッケージはGithubのセキュリティーアラートや、バージョンを更新するなど気をつける必要がありそう。

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

【Javascript】カートの中身を自動的にCSV出力するスクリプトで会計処理を楽にする

作るに至った経緯

会計処理の際に購入予定のもの情報を一つ一つExcelにコピーして貼り付ける作業が毎回手作業なので、どうにか時間短縮できないかなと思い、自動化するスクリプトを作ることにしました。NodeやSeleniumだと既存のブラウザにアタッチできないのでjavascriptを使いました。

今回やったこと

モノタロウの通販サイトにログインし、カートに入っている全ての商品の情報をCSVファイルに自動でまとめてくれるスクリプトを作りました。
ちなみに秋月電子は
https://tlt.gurigoro.net/entry/2018/01/13/200510 この拡張機能を使ってまとめるとよいです。
できるだけ有りものは利用しましょう。

環境

Windows10 Home
CPU: Intel Core i5 7200U
tampermonkey
javascript
Google Chrome

本題

前提

モノタロウの通販サイトで既に買いたい商品がカートに入っていて、ログインが済んでいる。カートのページにいる。

プログラムの流れ

モノタロウの商品カートから情報を取得
取得した情報の配列を作る
配列の整形
配列からCSVに変換する
CSVファイルをダウンロードする

ブラウザでCSVをダウンロードする方法

Google chrome上のjavascriptでCSVファイルをダウンロードする方法を学びました。まずはこちらのサイトを参考に情報を取得した前提でCSVをダウンロードしてみます。ちなみにこのCSVファイルはwindowsでないと文字化けします。詳しくはjavascript で作成したCSVファイルをエクセルで表示可能にするを参考にしてください。

tampermonkeyの導入

まずはchrome拡張 tampermonkeyを入れます。登録したページに行くとjavascriptを実行してくれる拡張機能です。
詳細はこちらの記事が参考になります。

カートの中身の情報をスクレイピングする

tampermonkeyという拡張機能を使って製品名などの情報を取得し、csvに出力します。残念ながら今の私の技能ではtampermonkeyとspredsheetを連携させることは無理でした。

要素の取得はCSSセレクタを手打ちで入力していきました。コーディングのポイントは、そのタグやクラスの名前が固有のものだったらそのまま書く、固有でなかったら子要素を利用して絞り込みます。item_contentなど、親要素の名前が同じ場合がありますが、子要素のクラス名が独特なものなので直接指定しています。入れ子にするかは状況をみて使い分けましょう。

tampermonkeyでは(function() {})(){}の中にコードを書いて実行します。
@matchは実行するページのURLを書きます。ここに書いたページに遷移すると実行されるので、カートに入るボタンを押したら実行されることになります。

MonotaroCart_to_csv.js
// ==UserScript==
// @name         カートの中身CSV出力くん
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @match        https://www.monotaro.com/monotaroMain.py?func=monotaro.basket.showListServlet.ShowListServlet&RtnPage=%2Fg%2F04470570%2F%3Fes%3D1
// @grant        none
// ==/UserScript==

(function() {
let product_name = [];
let oneset_num = [];
let price = [];
let buy_num = [];
let url = [];
let subtotal = [];
const Store = 'モノタロウ';
let heder_label = ['用途', '種別', '製品名', '購入場所', '内容量', '単価', '購入数量', 'URL', '小計', '合計'];

const ProductInfo = () => {
    let elm = document.querySelectorAll('.product_td');//商品ブロックの要素を取得
    let price_elm = document.querySelectorAll('td.price_td');//金額ブロックの要素を取得
    for (let i = 0; i < elm.length; i++) {
        product_name[i] = elm[i].querySelector('h4>a').innerText;//製品名を取得
        //oneset_num[i] = elm[i].querySelector('li>#text').innerText;//内容量を取得
        url[i] = elm[i].querySelector('h4>a').href;//urlを取得
    }
    for (let i = 0; i < price_elm.length; i++) {
        price[i] = price_elm[i].querySelector('td.item_content').innerText;//値段
        buy_num[i] = price_elm[i].querySelector('input.button_items_quantity.imemode_inactive').value;//購入数量
        subtotal[i] = price_elm[i].querySelector('em').innerText;//小計

    }
}



class CSV {
    constructor(data, keys = false) {
        this.ARRAY = Symbol('ARRAY');
        this.OBJECT = Symbol('OBJECT');

        this.data = data;

        if (CSV.isArray(data)) {
            if (0 == data.length) {
                this.dataType = this.ARRAY
            } else if (CSV.isObject(data[0])) {
                this.dataType = this.OBJECT
            } else if (CSV.isArray(data[0])) {
                this.dataType = this.ARRAY
            } else {
                throw Error('Error: 未対応のデータ型です')
            }
        } else {
            throw Error('Error: 未対応のデータ型です')
        }

        this.keys = keys
    }

    toString() {
        if (this.dataType === this.ARRAY) {
            return this.data.map((record) => (
                record.map((field) => (
                    CSV.prepare(field)
                )).join(',')
            )).join('\n')
        } else if (this.dataType === this.OBJECT) {
            const keys = this.keys || Array.from(this.extractKeys(this.data))

            const arrayData = this.data.map((record) => (
                keys.map((key) => record[key])
            ))

            console.log([].concat([keys], arrayData))

            return [].concat([keys], arrayData).map((record) => (
                record.map((field) => (
                    CSV.prepare(field)
                )).join(',')
            )).join('\n')
        }
    }

    save(filename = 'data.csv') {
        if (!filename.match(/\.csv$/i)) { filename = filename + '.csv' }

        console.info('filename:', filename);
        console.table(this.data);

        const csvStr = this.toString();

        const bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
        const blob = new Blob([bom, csvStr], { 'type': 'text/csv' });
        const url = window.URL || window.webkitURL;
        const blobURL = url.createObjectURL(blob);

        let a = document.createElement('a');
        a.download = decodeURI(filename);
        a.href = blobURL;
        a.type = 'text/csv';

        a.click();
    }

    extractKeys(data) {
        return new Set([].concat(...this.data.map((record) => Object.keys(record))));
    }

    static prepare(field) {
        return '"' + ('' + field).replace(/"/g, '""') + '"';
    }

    static isObject(obj) {
        return '[object Object]' === Object.prototype.toString.call(obj);
    }

    static isArray(obj) {
        return '[object Array]' === Object.prototype.toString.call(obj);
    }
}


const MakeList = () => {
    let csvfile = [heder_label,];
    for (let i = 0; i < product_name.length; i++) {
        csvfile.push([, , product_name[i], Store, oneset_num[i], price[i], buy_num[i], url[i], subtotal[i]]);
    }

    return csvfile;
}
//console.log(MakeList());
ProductInfo();
(new CSV(MakeList())).save('buylist.csv');//csvオブジェクトの作成 引数にはファイル名を指定
})();

新しいスクリプトをtampermonkeyに書いて有効にしたらモノタロウのカートに移動します。すると、以下のようなテーブルでcsvファイルが作成され、CSVファイルがダウンロードできます。
buylist.PNG

最後に

seleniumだと環境構築が面倒ですが、tampermonkeyは拡張機能のインストールだけで動くので非常に簡単でいいですね。
今回でjavascriptを使ったスクレイピングはできるようになったので、今後は別のECサイトの分もつくりたいです。

参考
https://qiita.com/sagami1991/items/794168cf14ed198d6372
https://blog.mudatobunka.org/entry/2017/04/23/135753

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

Ajax通信をrubyに導入した話

jQueryでAjax通信 × Railsでサーバー処理

jQueryのコードを使ってAjax通信でリクエストし、その処理をRailsで行ってjson形式のレスポンスで返したいと思います。
具体的には、「ボタンをクリックしたらリクエストが発動して、設定したデータをRailsに送信し、レスポンスをクラインアントが受け取っとら、アラートやコンソール上で送られてきたデータを表示する」ということをやってみたいと思います。

今回は、クライアント側とサーバー側を分かりやすくするため、Erbを使わずに処理を行いたいと思います。
したがって、今回利用するファイルの拡張子は以下になります。

クライアント側

  • html
  • js

サーバー側

  • rb

HTMLファイルで必要なコード

・jQueryのサーバーから、jQueryを使えるようにする機能を読み込むコード(いわゆるjQueryの導入)

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

・JavaScriptファイルを読み込むコード

<script src="sample.js"></script>

・サーバーへリクエストするボタン

<input type="button" id="btn1" value="ボタン">

これらを組み合わせたhtmlのソースコードは以下になります。

sample.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Ajaxの練習</title>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
  <script src="sample.js"></script>
</head>
<body>
  <input type="button" id="btn1" value="ボタン">
</body>
</html>

JavaScriptファイルに必要なコード

Ajax通信するコード

以下のような違いがあるため、今回は$.ajaxを使うことにします。

$.ajax 通信の成功時も失敗時の処理も書ける
$.post 通信の成功時の処理のみしか書けない

パラメータ

・クライアントからのリクエストの送信先を指定

url: "/test"

・HTTP通信の種類を設定
GETとPOSTがありますが、今回はPOSTで、データを送信します。

type: "POST"

・サーバーからのレスポンスを受け取るデータ形式を設定。

dataType: "json"

・サーバーへ送るデータの中身

data: { user: { name: "foo", age: 25 } }

メソッド

・成功時の処理

通信が成功したか分かりやすくするため、アラートでデータを表示されるようにしています。
GoogleChromeだと、alertだけでは「object」としか表示されないため、
特別なメソッド(JSON.stringify)を追加する必要がある。

done(function(human){
  alert(JSON.stringify(human));
});

・失敗時の処理(任意)

通信が失敗したら、送ったデータをコンソール上で表示するようにしています。responseTextメソッドは、Ajax通信途中のデータを取得できるメソッド。

fail(function(human){
  console.log(human.responseText);
});

・成功しても失敗しても行われる処理

always(function(){
  console.log(human);
});

これらのjQueryのコードをまとめると以下になります。

補足
id属性btn1のボタンをクリックしたら、以下のAjax通信を行う($.ajax)
・/testに、userというデータをPOSTで送る。
・サーバーからのレスポンス時にはJSON形式でデータを受け取る。
・通信成功時には、返ってきたデータ(変数human)をアラートでブラウザに表示する。(.done)
・通信失敗時には、レスポンスされたデータを文字列としてコンソール上に表示する。(.fail)
・通信成功時でも失敗時でも、レスポンスされたデータをコンソール上に表示する。(.always)

sample.js
$(function(){
  $('#btn1').click(function(){
    $.ajax({
      url: "/test",
      type: "POST",
      dataType: "json",
      data: { user: { name: "foo", age: 25 } }
    })
    .done((human) => {
      alert(JSON.stringify(human))
    })
    .fail((human) => {
      console.log(human.responseText)
    })
    .always((human) => {
      console.log(human)
    });
  });
});

Rails(サーバー側)で必要なコード

・ルーティング
jsファイルに記述した、リクエストの送信先(/test)を設定します。

route.rb
Rails.application.routes.draw do
  get "/test", to: 'statics#test'
  post "/test", to: 'statics#test'
end

・コントローラ

jsファイルに記述した、送信するデータを受け取り、json形式でクライアントにレスポンスで返します。
・送信されたデータ{ user: { name: "foo", age: 25 } }を、変数humanで受け取る
・変数humanをjson形式でレスポンスする。

statics_controller.rb
class StaticsController < ApplicationController
  def test
    human = params[:user]
    render :json => human
  end
end

最後に

うまくいけば以下のような表示になると思います。
スクリーンショット 2020-05-25 16.18.45.png

いかがだったでしょうか、非同期通信を行うことで身軽な通信ができて、大勢の人に知れ渡るアプリへの一歩になるんじゃないかと思います。

ただその分、初心者には複雑な仕組みになりがちなので、参考にしていただければと思います。

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

React vs Vue.js vs Angular.js 【データバインディング編】

この内容について

この内容は、私が運営しているサイトの一部抜粋です。よければそちらもご活用ください。
Reactチートシート | コレワカ
Vue.jsチートシート | コレワカ
AngularJSチートシート | コレワカ

それぞれの特徴

React Vue.js AngularJS
特徴   状態管理に特化したUI構築のためのライブラリ トランスコンパイル不要なUI構築のためのライブラリ 大抵の機能が全て揃うフルスタックなフレームワーク
開発規模 小規模〜大規模 小規模〜中規模 中規模〜大規模
組み合わせ   Redux・TypeScript・webpack・babelなど Vuex・Laravel・Firebaseなど TypeScript・AWSなど

データバインディングとは

データと描画を同期する仕組みのこと

それぞれのコード

React

See the Pen React_onChange by engineerhikaru (@engineerhikaru) on CodePen.

Vue.js

See the Pen Vue.js_v-model by engineerhikaru (@engineerhikaru) on CodePen.

AngularJS

See the Pen AngularJS_ng-model by engineerhikaru (@engineerhikaru) on CodePen.

簡単な解説

React

Reactは、単方向データバインディングなので、
setStateでデータを保管し、changeイベンド(onChange)で、View ⇆ Modelを実現しています。

Vue.js

Vue.jsは、双方向データバインディングなので、
Model関数(v-model)を使って、View ⇆ Modelを実現しています。

AngularJS

AngularJSは、双方向データバインディングなので、
Model関数(ng-model)を使って、View ⇆ Modelを実現しています。

おまけ

jQueryで書いた場合

キーが押された時に処理を実行するkeyup関数とテキスト出力をするためのtext関数を使って、
View ⇆ Modelを実現しています。


See the Pen
jQuery_databinding
by engineerhikaru (@engineerhikaru)
on CodePen.


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

PAY.JPでクレジットカードの登録・削除機能を実装する

はじめに

個人アプリにて、クレジットカード決済を行うため、PAY.JPを導入しました。
導入において、少しつまずいた部分もあったので、備忘録として記載しています。

前提条件

  • Rails 5.2.4.2
  • Ruby 2.5.1
  • devise使用
  • haml使用
  • VSCode使用

手順

  1. PAY.JPの登録
  2. アプリにPAY.JPを導入・下準備
  3. モデルの作成
  4. マイグレーションファイル
  5. コントローラーの作成
  6. gonの導入
  7. JSファイルの作成

PAY.JPの登録

以下のURLより登録します。
PAY.JP

41b709e395800ed89184beb6bbfee74f.png

登録が完了すると、上記のような画面に移ります。(使用したため、売上がたっております。)
最初はテストモードになっております。
実取引を行うためには,申請を行い、ライブモードに切り替える必要があります。
今回は個人アプリでの使用であり、商用目的ではないので、テストモードを使用しています。

メニューバーにある"API"の内容を使います。
996ed64b5c8b7f2c66a991720dc78f83.png

自身のアプリにはこのテスト秘密鍵とテスト公開鍵を使用していきます。

アプリにPAY.JPを導入・下準備

Gemfileに以下を追記

gem 'payjp'

ここから、カード情報の入力フォームを作成準備をしていくのですが、やり方としては2つあります。

カード情報のトークン化について

  1. チェックアウト
  2. カスタム

チェックアウトでは

<form action="/pay" method="post">
  <script src="https://checkout.pay.jp/" class="payjp-button" data-key=""></script>
</form>

上記を記述することでPAYJP側が用意したフォームを使用できます。

今回は自分はフォームを自身でカスタムしたかったので、以下のようにやっていきました。

application.html.hamlのhead部分に以下を追記します。

 %script{src: "https://js.pay.jp", type: "text/javascript"}

公式では上記に加えて、公開鍵の記述もしておりますが、自分は別で記載しました(後述)。

続いて、PAYJPの公開鍵と秘密鍵をcredentials.yml.encに記述していきます。

※Rails5.2系から導入されたEncrypted Credentialsという機能を利用します。これは、APIキーなどセキュリティ的に外部に公開してはいけない値の管理をシンプルに行うことができる機能です。

alt

当該ファイルを編集するには少し準備が必要です。
まずは、ターミナルからVSCodeを起動できるよう設定を行います。
VSCodeで、「Command + Shift + P」を同時に押してコマンドパレットを開きます。
続いて、「shell」と入力しましょう。
メニューに、「PATH内に'code'コマンドをインストールします」という項目が表示されるので、それをクリックします。
この操作を行うことで、ターミナルから「code」と打つことでVSCodeを起動できるようになりました。

続いて以下にてファイルの中身を編集していきます。

$ pwd
# 自身のアプリディレクトリにいることを確認
$ EDITOR='code --wait' rails credentials:edit

しばらくすると、以下のようにファイルが開きます。
alt

ここに以下のように記述していきます。
ネストしていることに注意してください。

payjp:
  PAYJP_SECRET_KEY: sk_test_...
  PAYJP_PUBLIC_KEY: pk_test_...

ご自身の鍵を記述して、保存してからファイルを閉じると
”New credentials encrypted and saved.”
とターミナルに出力されるので完了です。

なお、保存できているかどうかは以下コマンドで確認できます。

$ rails c
$ Rails.application.credentials[:payjp][:PAYJP_SECRET_KEY]
$ Rails.application.credentials[:payjp][:PAYJP_PUBLIC_KEY]

モデルの作成

今回はCardモデルと命名してやっていきます。
以下のように作成しました。

class Card < ApplicationRecord
  belongs_to :user
  has_one :order, dependent: :nullify

  require 'payjp'
  Payjp.api_key = Rails.application.credentials.dig(:payjp, :PAYJP_SECRET_KEY)

  def self.create_card_to_payjp(params)
    # トークンを作成 
    token = Payjp::Token.create({
      card: {
        number:     params['number'],
        cvc:        params['cvc'],
        exp_month:  params['valid_month'],
        exp_year:   params['valid_year']
      }},
      {'X-Payjp-Direct-Token-Generate': 'true'} 
    )
    # 上記で作成したトークンをもとに顧客情報を作成
    Payjp::Customer.create(card: token.id)
  end

個人アプリではECサイトを作成していたため、アソシエーションはUserとOrderとしています。

ここでカード情報のトークン化を行っております。

テーブルの作成

以下の内容でテーブルを作成しました。

class CreateCards < ActiveRecord::Migration[5.2]
  def change
    create_table :cards do |t|
      t.string :customer_id, null: false
      t.string :card_id, null: false
      t.references :user, null: false, foreign_key: true
      t.timestamps
    end
  end
end

自分は最初知らなかったですが、customer_idとcard_idはマストでカラムとして作成する必要があります。
テーブルには以下のようにレコードが登録されていきます。

コントローラーの作成

今回は4つのアクションで以下のようにコントローラーを構成しました。

class CardsController < ApplicationController
  before_action :set_card, only: [:new, :show, :destroy]
  before_action :set_payjpSecretKey, except: :new
  before_action :set_cart
  before_action :set_user

  require "payjp"

  def new
    redirect_to action: :show, id: current_user.id if @card.present?
    @card = Card.new 
    gon.payjpPublicKey = Rails.application.credentials[:payjp][:PAYJP_PUBLIC_KEY]
  end

  def create
    render action: :new if params['payjpToken'].blank?
    customer = Payjp::Customer.create(
      card: params['payjpToken']
    )
    @card = Card.new(
      card_id: customer.default_card,
      user_id: current_user.id,
      customer_id: customer.id
    )
    if @card.save
      flash[:notice] = 'クレジットカードの登録が完了しました'
      redirect_to action: :show, id: current_user.id
    else
      flash[:alert] = 'クレジットカード登録に失敗しました'
      redirect_to action: :new
    end
  end

   def show
    redirect_to action: :new if @card.blank?
    customer = Payjp::Customer.retrieve(@card.customer_id)
    default_card_information = customer.cards.retrieve(@card.card_id)
    @card_info = customer.cards.retrieve(@card.card_id)
    @exp_month = default_card_information.exp_month.to_s
    @exp_year = default_card_information.exp_year.to_s.slice(2,3)
    customer_card = customer.cards.retrieve(@card.card_id)
    @card_brand = customer_card.brand
    case @card_brand
    when "Visa"
      @card_src = "icon_visa.png"
    when "JCB"
      @card_src = "icon_jcb.png"
    when "MasterCard"
      @card_src = "icon_mastercard.png"
    when "American Express"
      @card_src = "icon_amex.png"
    when "Diners Club"
      @card_src = "icon_diners.png"
    when "Discover"
      @card_src = "icon_discover.png"
    end
  end

  def destroy
    customer = Payjp::Customer.retrieve(@card.customer_id)
    @card.destroy
    customer.delete
    flash[:notice] = 'クレジットカードが削除されました'
    redirect_to controller: :users, action: :show, id: current_user.id
  end

  private
  def set_card
    @card = Card.where(user_id: current_user.id).first
  end

  def set_payjpSecretKey
    Payjp.api_key = Rails.application.credentials[:payjp][:PAYJP_SECRET_KEY]
  end

  def set_cart
    @cart = current_cart
  end

  def set_user
    @user = current_user
  end
end

また、PAYJPの公開鍵を後述するJSファイルにベタ書きすることを避けるため、"gon"というGemを導入しました。
JSファイルでrubyの変数を使用したい時に使うという認識です。

gonの導入

Gemfileに以下を追記

gem 'gon'

application.html.hamlのhead部分に以下を追記します。

= include_gon(init: true)

これで準備はOKです。

JSファイルの作成

ここから、コントローラーのnewアクションで呼び出すnew.html.hamlと連動するJSファイルを作成します。
まずはnew.html.hamlを記載します。
※フォームに入力するカード番号等についてですが、以下の公式ページのものを使用してください。
APIで使用するテストカードはこちらです。

.cardNew
  .title 
    クレジットカード情報入力
  .cardForm
    = form_with model: @card, id: "form" do |f|
      .cardForm__number
        = f.label :カード番号, class: "cardForm__number__title"
        %span.must_check 必須
      .cardForm__field
        = f.text_field :card_number, id: "card_number", placeholder: "半角数字のみ", class: "form-group__input", maxlength: 16
      .cardForm__image
        = image_tag(image_path('cards/icon_visa.png'), class: 'visa', width: 58, height: 28)
        = image_tag(image_path('cards/icon_mastercard.png'), class: 'master', width: 47, height: 36)
        = image_tag(image_path('cards/icon_jcb.png'), class: 'jcb', width: 40, height: 30)
        = image_tag(image_path('cards/icon_amex.png'), class: 'amex', width: 40, height: 30)
        = image_tag(image_path('cards/icon_diners.png'), class: 'diners', width: 45, height: 32)
        = image_tag(image_path('cards/icon_discover.png'), class: 'discover', width: 47, height: 30)
      .cardForm__expirationdate
        .cardForm__expirationdate__details
          = f.label :有効期限
          %span.must_check 必須
          %br
        .cardForm__expirationdate__choice
          .cardForm__expirationdate__choice__month
            = f.select :expiration_month, options_for_select(["01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12"]), {}, {id: "exp_month", name: "exp_month", type: "text"}
            = f.label :, class: "cardForm__expirationdate__choice__month__label"
          .cardForm__expirationdate__choice__year
            = f.select :expiration_year, options_for_select((2020..2030)), {}, {id: "exp_year", name: "exp_year", type: "text"}
            = f.label :, class: "cardForm__expirationdate__choice__year__label"
      .cardForm__securitycode
        .cardForm__securitycode__details
          .cardForm__securitycode__details__title 
            = f.label :セキュリティコード, class: "label"
            %span.must_check 必須
          .cardForm__securitycode__details__field
            = f.text_field :cvc,  id: "cvc", class: "cvc", placeholder: "カード背面3~4桁の番号", maxlength: "4"
          .cardForm__securitycode__details__hatena
            = link_to "カード背面の番号とは?", "#", class: "cardForm__securitycode__details__hatena__link"
      #card_token
        = f.submit "登録する", id: "token_submit", url: cards_path, method: :post

続いて、上記viewファイルに対応するJSファイルを作成します。

$(document).on('turbolinks:load', function() {
  $(function() {
    Payjp.setPublicKey(gon.payjpPublicKey);
    $("#token_submit").on('click', function(e){
      e.preventDefault();
      let card = {
          number: $('#card_number').val(),
          cvc:$('#cvc').val(),
          exp_month: $('#exp_month').val(),
          exp_year: $('#exp_year').val()
      };

      Payjp.createToken(card, function(status, response) {
        if (response.error) {
          $("#token_submit").prop('disabled', false);
          alert("カード情報が正しくありません。");
        }
        else {
          $("#card_number").removeAttr("name");
          $("#cvc").removeAttr("name");
          $("#exp_month").removeAttr("name");
          $("#exp_year").removeAttr("name");

          let token = response.id;
          $("#form").append(`<input type="hidden" name="payjpToken" value=${token}>`);
          $("#form").get(0).submit();
          alert("登録が完了しました");
        }
      });
    });
  });
});

viewファイルで登録ボタン( = f.submit "登録する", id: "token_submit")を押すと、JSが発火します。
トークン作成は Payjp.createToken というメソッドで行います。

上記までで、カードの登録までは完了です。

最後に削除機能について簡単に記載しておきます。
自分はshowアクションで呼び出すshow.html.hamlに削除ボタンを以下のように設置しました。

.payment
  .payment__content
    .payment__content__title
      カード情報
    .payment__content__box
      .payment__content__box__cardImage
        = image_tag "cards/#{@card_src}", width: 40, height: 28
      .payment__content__box__details
        .payment__content__box__details__cardNumber
          = "カード番号:**** **** **** " + @card_info.last4
        .payment__content____boxdetails__cardMMYY
          = "有効期限:" + @exp_month + " / " + @exp_year
  .payment__cardDelete
    = button_to "削除する", card_path(@card.id), {method: :delete, id: 'charge-form', name: "inputForm", class: "payment__cardDelete__deleteBtn"}

次回は、登録できたカードでの決済機能を記載します。

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

初心者に捧げるヘッダーの作り方

初めに

初心者に捧げるハンバーガーメニューの作り方
初心者に捧げるシリーズ第二弾です。jQueryを使って、スクロールした時に変化のあるヘッダーを作ります!!
ソースコードをコメントで解説しながら記述していきます。

対象読者

・上部に固定したヘッダーを作りたい
・スクロールした時に変化するヘッダーを作りたい
・自分でオリジナルの見た目を作って応用したい人
・(ちなみに筆者の自己学習のためにも記事を書いているので間違っている箇所があれば是非とも教えて頂きたい)

目次

・完成イメージ
・HTMLの記述
・CSSの記述
・jQueryの記述
・まとめ

完成イメージ

ダウンロード.gif

HTMLの記述

<div class="container">
        <header>
            <ul>
                <!-- リンク先をつけるときはliの中に<a></a>を追加してその中に記述する -->
                <li>トップ</li>
                <li>ちくわの歴史</li>
                <li>世界のちくわ</li>
                <li>ちくわ豆知識</li>
                <li>よろちくわ</li>
            </ul>
        </header>

        <!-- スクロールするためのコンテンツ -->
        <div class="content">
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
          <p>aaa</p>
        </div>
    </div>

CSSの記述

/* HTMLに元からついているpaddingとmarginを消します */
        *{
            padding: 0;
            margin: 0;
        }
        header{
            /* 高さは好み */
            height: 64px;
            /* 上に固定するためにfixedとtop:0を書く */
            position: fixed;
            top: 0px;
            /* ボーダーは好み */
           border-bottom: 2px solid black;
            /* 幅は好み */
            width: 100%;
            /* 中央に揃える */
            text-align: center;
            /* メインのコンテンツより前面に表示 */
            z-index: 2;
        }

        ul {
            /* liについた点を消す */
            list-style:none;
            /* 上下のmarginの初期値は0なので:0 auto;じゃなくてもok */
            margin: auto;
            /* ヘッダーより前面に表示 */
            z-index: 3;
        }

        ul li {
            /* 横並びにする */
            display: inline-block;
            /* paddingは好み */
            padding: 26px 10px 20px 10px;
            /* 色は好み */
            color: black;   
        }
        /* スクロールを試すためなので自由 */
        .content {
            position: absolute;
            top: 100px;
        }

jQueryの記述

// $(function(){ HTMLの読み込みが完了した時に実行される
            $(function() {
                // windowがスクロールされた時
                $(window).scroll(function() {
                    // もし画面表示のtopが0より大きくなったら
                    if ($(this).scrollTop() > 0) {
                        // cssの記述は自由
                        $('header').css( 'background-color', 'black');
                        $('li').css( 'color', 'white');
                        $('header, li').css('transition', 'all 0.5s');
                    // 0より大きくなってない時
                    } else {
                        // cssは自由
                        $('header').css( 'background-color', 'white')
                        $('li').css( 'color', 'black');
                    }
                });
            });

まとめ

前回の初心者に捧げるハンバーガーメニューの作り方と合わせることもできるので是非とも見てみてください。
この記事をみてくださったあなたの成長を応援します!!!

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

Day24 jQueryについて

jQueryセレクタ

$("#idSelector") // idがidSelectorの要素を取得
$(".classSelector") // classがclassSelectorの要素をすべて取得
$("h1") // h1要素をすべて取得
$("input[ type='radio' ]");  // <input type="radio">のHTML要素を取得する

要するに、JavaScriptで書いていたDOM要素の取得を全て共通の$("セレクタ名") で書き換えることができます。

HTMLの読み込みをjQueryで

javascriptの場合
window.addEventListener("load", function() {
  // 処理
});
jQueryの場合
$(function() {
   // 処理
});

preventDefault()

デフォルトのイベントをキャンセル

$(function() {
  $('form').on('submit', function(e) {
    console.log('送信ボタンが押されました');
    e.preventDefault();
  });
});

イベント内の関数の第一引数に自動でイベントオブジェクトが渡されます。イベントオブジェクトには、イベントの発生元の要素や押されたキーの情報などが入っています。
ここで取得したイベントオブジェクトに対して、preventDefault()を使用することで要素のイベントをキャンセルすることができます

formに入力されたチェックボックスを出力

index.html
<body>
    <form>
      <h1>好きな果物を選んでね</h1>
      <div>
        <span>好きな数だけ選べます</span>
      </div>
      <label>
        <input type="checkbox" value="りんご">
        りんご
      </label>
      <label>
        <input type="checkbox" value="バナナ">
        バナナ
      </label>
      <label>
        <input type="checkbox" value="みかん">
        みかん
      </label>
      <div>
        <input type="submit" value="送信">
      </div>
    </form>
  </body>
script.js
$(function() {
  $('form').on('submit', function(e) {
    let output =''; //出力する値を文字列の変数(output)で定義//

    let checkboxes = $(this).find('input[type="checkbox"]');
    //チェックボックスを取得して変数checkboxesに代入//

    //each文を使い、checkboxesの中の要素を取り出す//
    checkboxes.each(function(i,checkbox) {
      checkbox = $(checkboxes[i]);
      if (checkbox.prop('checked')) {
        output += checkbox.attr('value') + '\n';
      }
    });
    //prop()を使って取り出した要素がチェックされているかどうかを確認。//
    //trueの場合、チェックされている要素の値だけをoutputに代入。//
    e.preventDefault();
    alert('あなたが選んだ果物:\n' + output);
    //outputをalertで出力//
  });
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Javascript]変数、定数

変数定義

let 変数名 = 値;

let name = "tanaka";

変数の出力

let name = "tanaka";

console.log(name);

変数の値の更新

letをつけずに定義する。

let name = "tanaka";
console.log(name);

name = "satou";
console.log(name);

定数定義

定数は値の上書きができない。
const 定数名 = 値;

const name = "tanaka"

まとめ

いわゆる変数を定義する時
値が変わる
let 変数名 = 値;

値が変わらない
const 定数名 = 値;

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

Javascript タグ

・HTML文字設定
innerHTML = 内容;
innerText = 内容;
textContent = 内容;

・HTML属性設定
setAttribute("属性名", 内容);

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

Day24 javascript メニュータブの切り替え実装

メニュータブの切り替え実装

htmlでmenu項目about, service, contactをlist表記

index.html
<div class="container">
  <ul class="menu">
    <li><a href="#" id="about" class="menu_item active">Rails</a></li>
    <li><a href="#" id="service" class="menu_item">Javascript</a></li>
    <li><a href="#" id="contact" class="menu_item">Ruby</a></li>
  </ul>
  <ul class="contents">
    <li class="content show">
      about about about about about about about about about
    </li>
    <li class="content">
      service service service service service service service
    </li>
    <li class="content">
      contact contact contact contact contact contact contact
    </li>
  </ul>
</div>
style.css
ul.menu li .active {
  background: #353d3e;
  color: #fff;
}

ul.contents li {
  list-style: none;
  font-size: 14px;
  padding: 7px 10px;
  line-height: 1.4;
  background: #353d3e;
  color: #fff;
  min-height: 150px;
  display: none;
}

ul.contents li.show {
  display: block;
}

activeクラスとshowクラスが付いているものが、cssでdisplay: block;で表示できるようにし、付いていないものはdisplay: none;で表示できないようにしています。
タブメニューをクリックした時に、対応するクラスにactiveクラスとshowクラスを追加することで実装することができます。

main.js
window.addEventListener("load", function() {
  // タブのDOM要素を取得し、変数で定義
  let tabs = document.getElementsByClassName("menu_item");
  // tabsを配列に変換する
  tabsAry = Array.prototype.slice.call(tabs);

  // クラスの切り替えをtabSwitch関数で定義
  function tabSwitch() {
    // 全てのactiveクラスのうち、最初の要素を削除("[0]は、最初の要素の意味")
    document.getElementsByClassName("active")[0].classList.remove("active");
    // クリックしたタブにactiveクラスを追加
    // ②`this.`の後に、classListを使用してactiveクラスを追加しよう
    this.classList.add("active");

    // コンテンツの全てのshowクラスのうち、最初の要素を削除
    // ③`document.getElementsByClassName('show')[0].`の後に、showクラスを削除しよう
    document.getElementsByClassName('show')[0].classList.remove("show");

    // 何番目の要素がクリックされたかを、配列tabsから要素番号を取得
    const index = tabsAry.indexOf(this);

    // クリックしたcoutentクラスにshowクラスを追加する
    // ④`document.getElementsByClassName("content")[index].`の後に、showクラスを追加しよう
    document.getElementsByClassName("content")[index].classList.add("show");
  }

  // タブメニューの中でクリックイベントが発生した場所を探し、下で定義したtabSwitch関数を呼び出す
  tabsAry.forEach(function(value) {
    // ①`value.`の後に、イベントリスナーでクリックイベントが発生した時に、tabSwitch関数を呼び出す処理を書きましょう。
    value.addEventListener("click", tabSwitch);
  });
});

Array.prototype.slice.call(引数); :引数にとったオブジェクトを配列に変換してくれる。
forEach() :配列に対してよく使われる繰り返し処理です。
indexOf() :inexOf()は配列に対してだけ使い、DOMを引数にとって一致した要素番号を戻します。

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

Day24 javascript, jQuery メニュータブの切り替え実装

メニュータブの切り替え実装(javascriptの場合)

htmlでmenu項目about, service, contactをlist表記

index.html
<div class="container">
  <ul class="menu">
    <li><a href="#" id="about" class="menu_item active">Rails</a></li>
    <li><a href="#" id="service" class="menu_item">Javascript</a></li>
    <li><a href="#" id="contact" class="menu_item">Ruby</a></li>
  </ul>
  <ul class="contents">
    <li class="content show">
      about about about about about about about about about
    </li>
    <li class="content">
      service service service service service service service
    </li>
    <li class="content">
      contact contact contact contact contact contact contact
    </li>
  </ul>
</div>
style.css
ul.menu li .active {
  background: #353d3e;
  color: #fff;
}

ul.contents li {
  list-style: none;
  font-size: 14px;
  padding: 7px 10px;
  line-height: 1.4;
  background: #353d3e;
  color: #fff;
  min-height: 150px;
  display: none;
}

ul.contents li.show {
  display: block;
}

activeクラスとshowクラスが付いているものが、cssでdisplay: block;で表示できるようにし、付いていないものはdisplay: none;で表示できないようにしています。
タブメニューをクリックした時に、対応するクラスにactiveクラスとshowクラスを追加することで実装することができます。

main.js
window.addEventListener("load", function() {
  // タブのDOM要素を取得し、変数で定義
  let tabs = document.getElementsByClassName("menu_item");
  // tabsを配列に変換する
  tabsAry = Array.prototype.slice.call(tabs);

  // クラスの切り替えをtabSwitch関数で定義
  function tabSwitch() {
    // 全てのactiveクラスのうち、最初の要素を削除("[0]は、最初の要素の意味")
    document.getElementsByClassName("active")[0].classList.remove("active");
    // クリックしたタブにactiveクラスを追加
    // ②`this.`の後に、classListを使用してactiveクラスを追加しよう
    this.classList.add("active");

    // コンテンツの全てのshowクラスのうち、最初の要素を削除
    // ③`document.getElementsByClassName('show')[0].`の後に、showクラスを削除しよう
    document.getElementsByClassName('show')[0].classList.remove("show");

    // 何番目の要素がクリックされたかを、配列tabsから要素番号を取得
    const index = tabsAry.indexOf(this);

    // クリックしたcoutentクラスにshowクラスを追加する
    // ④`document.getElementsByClassName("content")[index].`の後に、showクラスを追加しよう
    document.getElementsByClassName("content")[index].classList.add("show");
  }

  // タブメニューの中でクリックイベントが発生した場所を探し、下で定義したtabSwitch関数を呼び出す
  tabsAry.forEach(function(value) {
    // ①`value.`の後に、イベントリスナーでクリックイベントが発生した時に、tabSwitch関数を呼び出す処理を書きましょう。
    value.addEventListener("click", tabSwitch);
  });
});

Array.prototype.slice.call(引数); :引数にとったオブジェクトを配列に変換してくれる。
forEach() :配列に対してよく使われる繰り返し処理です。
indexOf() :inexOf()は配列に対してだけ使い、DOMを引数にとって一致した要素番号を戻します。

image.png

メニュータブの切り替え実装(jQueryの場合)

1.クリックしたタブのクラス要素を削除

removeClass()でクラス要素を削除

main.js
function tabSwitch() {
   $(".active").removeClass("active");
~~  ~~
   $('.show').removeClass("show");

2.クリックしたタブにクラス要素を追加

addClass()でクラス要素追加

main.js
function tabSwitch() {
   $(".active").removeClass("active");
   $(this).addClass("active");
   $('.show').removeClass("show");

3.クリックされた要素番号を戻す

クリックされた要素が何番目か、集合したDOM要素から引数に指定したDOMと同じ要素番号を戻す

main.js
const index = tabs.index(this);

index()は配列に戻す必要がない

4.リファクタリング

contentクラスの全てのshowクラスを削除した後、クリックされたタブの要素番号と一致するcontentクラスの要素だけ、showクラスを追加する

main.js
  function tabSwitch() {
    $(".active").removeClass("active");
    $(this).addClass("active");
    const index = tabs.index(this);
    $(".content").removeClass("show").eq(index).addClass("show");
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

華麗なるGatsby.jsの実践(認証機能をつけてみよう)

Gatsby.jsに認証機能などの動的な機能をつけるにはどうすればいいのだろう?
と思って、公式を参考にしつつ認証機能のサンプルコードを実装してみました。
なお、あくまでサンプルですので、パスやユーザーはgatsby.js内にハードコーディングされています。

リポジトリは以下になります。
https://github.com/takanokana/gatsby-practice

【参考公式ページ】
https://www.gatsbyjs.org/docs/react-hydration
https://www.gatsbyjs.org/docs/adding-app-and-website-functionality/
https://www.gatsbyjs.org/docs/client-only-routes-and-user-authentication/#implementing-client-only-routes

React Hydration

Gatsbyは、HTMLを静的に生成する静的サイトジェネレーターとしての機能、それに加えて、
生成したHTMLを、 React hydrationを通してクライアントサイドで拡張し、アプリのような振る舞いを持たせます。

上記の機能とGatsbyに付属している@reach/routerを使用し、client only routes,つまり静的ページとしては吐き出さないページを作ることができます。

認証機能においては、Gatsbyが生成した静的HTMLはファイルサーバ上にあるので、制御が不可能です。(ユーザーが直接URLを入力するとアクセスできてしまう)
なので、client only routesを使用することでユーザーをルーティングさせ、アクセスを制限することが必要となります。

src/pages/app.js
import React from "react"
import { Router } from "@reach/router"
import Auth from "../components/Auth"
const App = () => {
  return(
    <div>
      <Router basepath="/app">
        <Auth path="/" />
      </Router>
    </div>
  )
}

export default App
src/pages/Auth.js
import React from "react"

export default function Auth() {
  return (
    <div>認証ページ</div>
  )
}

以上のコーディングで、 localhost:8000/appにアクセスすると、認証ページ、と記述されたページを出すことができます。

またGatsbyではビルドがNode.jsで実行される関係でビルド時にlocalStoragewindowを使うことができません。しかし、外部認証サービスなどの中にはlocalStorageやwindowといったものにアクセスするものもあります。

なので、ビルド中に不具合を起こさないため、該当コードをラッピングする必要があります。

import app from "firebase/app"
if (typeof window !== 'undefined'){
  app.initializeApp(config)
 }

onCreatePage

gatsby-node.jsを編集して、/app/が制限された区画であることを定義して、必要に応じてページを作成するようにします。
onCreatePageは、全てのページが作成された後に呼ばれます。
matchPathで指定された部分は、build時に生成しないようになります。

gatsby-node.js
exports.onCreatePage = async({ page, actions }) => {
  const { createPage } = actions

  if(page.path.match(/^\/app/)){
    page.matchPath = "/app/*"
    createPage(page)
  }
}

実例

実際に、仮の認証システムをjs上で用意して、認証機能をつけてみます。
下記src/service/auth.jsで実装する機能は、本来ならばfirebaseなどが受け持ちます。

src/service/auth.js
export const isBrowser = () => typeof window !== "undefined"

export const getUser = () =>
 isBrowser() && window.localStorage.getItem("gatsbyUser")
 ? JSON.parse(window.localStorage.getItem('gatsbyUser'))
 : {}
const setUser = user =>
 window.localStorage.setItem("gatsbyUsr", JSON.stringify(user))

export const handleLogin = ({ username, password }) => {
 if (username === `join` && password === `pass`){
   return setUser({
         username: `join`,
         name: `Johnny`,
         email: `johnny@example.com`
    })
 }
 return false
}

export const isLoggedIn = () => {
  const user = getUser()

  return !!user.username
}
export const logout = callback => {
  setUser({})
  callback()
}

app.jsを下記のようにします。

app.js
import React from "react"
import { Router } from "@reach/router"
import Auth from "../components/Auth"
import PrivateRoute from "../components/PrivateRoute"
import Secret from "../components/Secret"


const App = () => {
  return(
    <div>
      <Router basepath="/app">
        <PrivateRoute path="/secret" component={Secret} />
        <Auth path="/login" />
      </Router>
    </div>
  )
}

export default App

PrivateRouteは下記のようなHOCとなっています。

src/components/PrivateRoute
import React from "react"
import { navigate } from "gatsby"
import { isLoggedIn } from "../service/auth"

const PrivateRoute = ({ component: Component, location, ...rest}) => {
  if (!isLoggedIn() && location.pathname !== `/app/login`) {
    navigate("/app/login")
    return null
  }

  return <Component {...rest} />
}

export default PrivateRoute

navigate (https://www.gatsbyjs.org/docs/gatsby-link/) ですが、
送信後、サンクスページに移動するといった用途に使用できます。stateを渡すことなども可能です。
PrivateRouteをかますことで、ログインしていなければ /app/loginへ、ログインしていれば該当ページへ飛ぶ、といった制限付きのルーティングが実現します。

ログインページは下記のように実装しました。

src/components/Auth.js
import React, { Component } from "react"
import { handleLogin, isLoggedIn} from "../service/auth"
import { navigate, Link } from "gatsby"

export default class Auth extends Component {
  state = {
    username: ``,
    password: ``
  }


## 実際のページ

  handleUpdate(event) {
    this.setState({
      [event.target.name]: event.target.value
    })
  }
  handleSubmit(event) {
    event.preventDefault()
    handleLogin(this.state)
    navigate(`/app/secret`)
  }
  render() {
    return (
      <div>
        認証ページ
        {isLoggedIn() ?
        <Link
          to="/app/secret"
        >認証後ページへ</Link>
        :
        <>
          <dl>
            <dt>名前</dt>
            <dd>
              <input
                name="username"
                onChange={e => this.handleUpdate(e)}
              ></input>
            </dd>
          </dl>
          <dl>
            <dt>パスワード</dt>
            <dd>
              <input
                name="password"
                onChange={e => this.handleUpdate(e)}
              />
            </dd>
          </dl>
          <button
          type="submit"
          onClick={e => this.handleSubmit(e)}
        >送信</button>
        </>
        }
      </div>
    )
  }
}

上記により、名前とパスワードが正しい状態でログインボタンを押すと、(ここではhandleLoginで判定されている、 john/pass)認証後ページであるSecret.jsに飛ぶことができます。

認証後ページは、下記のようにログアウト機能もいれました。

Secret.js
import React from "react"
import { logout } from "../service/auth"
import { navigate } from "gatsby"
export default function Auth() {
  const logoutHandler = () => {
    navigate('/')
    return
  }
  return (
    <div>認証後ページ ?

    <button
      type="button"
      onClick={e => logout(logoutHandler)}
    >ログアウトする</button>
    </div>
  )
}

実際の挙動

このようになります。
たとえ直接 /app/secret と打ち込んでも、ログインされていなければsecretは見ることができません。
ezgif-6-a9de48eb2daa.gif

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

Day24 javascript #DOM #ノード #イベント

DOMとは

Document Object Model(ドキュメントオブジェクトモデル)
HTMLを解析し、データを作成する仕組み
image.png
HTMLは階層構造になっていることが特徴です。
DOMによって解析されたHTMLは、階層構造のあるデータとなります。これを、DOMツリーやドキュメントツリーと呼びます。
image.png
JavaScriptを使うと、DOMツリーを操作することができます。
HTMLの要素名や、「id、class」といった属性の情報を元にDOMツリーの一部を取得し、CSSを変更したり、要素を増やしたり、消したりできます。
するとそれがブラウザに反映され、描画も変わります。DOMツリーの一部のことを、ノードオブジェクトと呼びます。

image.png

ノードの取得

document.getElementById("id名"); :id名での取得
document.getElementsByClassName("class名"); :class名での取得
document.querySelector("セレクタ名"); :セレクタ名での取得

addEventListener

main.js
let btn = document.querySelector("button");
// ボタンをノードオブジェクトとして取得し、変数btnに代入する

function printHello() {
  console.log("Hello world");
}
// printHello関数を定義

btn.addEventListener("click", printHello);
// ボタンのノードオブジェクトであるbtnに対して、
// clickイベントとprintHello関数を紐付ける仕組みであるイベントリスナを追加する

↑これだとhtml内のbuttonタグにたどり着く前にmain.jsが読み込まれるため、
"button"のノードオブジェクトを取得できない。

main.js
function printHelloWithButton() {
  let btn = document.querySelector("button");

  function printHello() {
    console.log("Hello world");
  }
  // 関数内で定義された関数は、関数の中でしか呼び出せない性質があるだけで、
  // 通常の関数同様に呼び出せる

  btn.addEventListener("click", printHello);
}
// 一連の処理をまとめた関数を作る

window.addEventListener("load", printHelloWithButton);

window.addEventListenerを追加することで、pageがloadされたら、
関数printHelloWithButtonが実行される。

↓今回、わざわざconsole.logを関数定義したのは、
window.addEventListenerの第二引数に関数を置く必要があるため。
そこで、function()と無名関数を定義してあげるとこのようになる。

main.js
window.addEventListener("load", function() {
  let btn = document.querySelector("button");

  btn.addEventListener("click", function() {
    console.log("Hello world");
  });
});

innerHTML

innerHTMLを使用するとHTML要素の中身を書き換えることができる

main.js
window.addEventListener("load", function() {
  let btn = document.querySelector("button");
  btn.addEventListener("click", function() {
    console.log("Hello world");
  });
  // テキストの要素を取得し、変数で定義
  let btn2 = document.querySelector("#Button2");
  let changeText = document.querySelector("p");
  // ボタン2をクリックしたらテキストが置換される
  btn2.addEventListener("click", function() {
    changeText.innerHTML = '変更されました';
  });
});

classList.add

「クラス追加」ボタンが押されたら、cssの背景色が赤のredクラスが追加されるようにclassList.addを利用します。

main.js
// Button3を取得して、変数で定義
let btn3 = document.querySelector("#Button3");

// クラス追加を押したらredクラスが追加される
btn3.addEventListener("click", function() {
  changeText.classList.add("red");
});

classList.remove

main.js
// Button4を取得して、変数で定義
let btn4 = document.querySelector("#Button4");

// div要素を取得して、変数で定義
let obj = document.querySelector("div");

// クラス削除を押したらblueクラスが削除される
btn4.addEventListener("click", function() {
  obj.classList.remove("blue");
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ESLint v7.1.0

v7.0.0 | 次 (2020-06-05 JST)

ESLint 7.1.0 がリリースされました。小さな機能追加とバグ修正が含まれています。

また、公式のサポートチャット (英語) が Gitter から Discord に移動しました。日本語の方も移動予定です。

質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。

? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット
? 本家リポジトリ
? 本家サポート チャット


[PR] ESLint は開発リソースを確保するための寄付を募っています。
応援してくださると嬉しいです。


✨ 本体への機能追加

特になし

? 新しいルール

no-loss-of-precision

? #12747

number 型 (IEEE 754: 64bit 浮動小数点数) で表現できない桁数の数値リテラルを警告するルールです。

/*eslint no-loss-of-precision: error */

//✘ BAD
const a = 9007199254740993
const b = 5123000000000000000000000000001
const c = 1230000000000000000000000.0
const d = .1230000000000000000000000
const e = 0X20000000000001

» Online Demo

? オプションが追加されたルール

特になし

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

javascriptで動画の明るさを調整できるchrome拡張機能を作ったから、作り方を1から解説してみる

初めに

adjusting the video bright
Youtubeやニコニコ動画の動画の明るさを調整できるchrome拡張機能です。

地味に便利な拡張機能と思います。

自分なりの解釈で書いているので、間違っているところがあるかもしれないので参考程度に見ておいてください。

必要なファイル

  • index.html (browser actionで開くときに必要)
  • background.js (backgroundで必要)
  • content.js (content_scriptで必要)
  • manifest.json (chrome拡張機能の名前や説明、使うchrome APIなどを記述するときに使う 必須なファイル) 。

まとめる

まず、コードを書く前にやるべきことをまとめます。

1,ポップアップ画面はどのような画面にするか
明るさを調整するだけなので、シンプルでいいです。

input type="range"で調整できるようにして、それを左に置き,
決定ボタンは右に置くようにします。
決定ボタンを押せば、動画が暗くなるようにします。

popup.png

汚くてすみません....
右上のiconをクリックすればポップアップが表示されて、rangeが左,ボタンが右 という感じです。
最低でもinputタグのrangeタイプとbuttonタグがあれば十分です。あとはお好みのデザインにしても構いません。

2.必要なjavascriptファイル。メッセージパッシング

まずyoutubeの画面を暗くするには、cssであれこれやるのですが、それをするためにはDOM操作が必要になります。
ポップアップに使うHtmlファイルの外部jsファイルでやればいいかと思うとできません。

DOM操作を行うためのJsファイルは決まってます。
manifest.jsonにあるcontents_scriptに指定したjsファイルです。

ポップアップのhtmlファイルにcontent_scriptsに指定したjsファイルを外部jsファイルとしてdom操作をしたら、出来るのかというと,これもできません。

ポップアップするhtmlファイルをこの記事では拡張機能側と呼んでおきます。
拡張機能側からdom操作をすることはできません。

1.png

じゃあどうすればいいのかというと、拡張機能側からcontent_scriptsのjsファイルに必要なデータを渡して、そのjsファイル(content.js)でdom操作を行うようにすればいいことです。

無題.png

htmlだけじゃcontent_scriptsにデータを送信できないので、外部jsファイルでcontent_scriptsに送信します。
またデータを送信するにはmanifest.jsonのbackgroundに指定されたbackground.jsがchromeAPIを使い、送信していきます。

流れをまとめると

1,拡張機能側で必要なinputの値を取って、決定ボタンを押す

2,外部jsファイル(background.js)がinputの値などのデータをcontent_scriptsに送信

3,content_scriptsはデータを受け取り、dom操作でページに反映する。

このような感じです。

この送信したり受け取ったりすることをメッセージパッシング(message passing)と言います。

コードを書いていく

1,DOM操作で、動画を暗くするやり方を知る。
まず,暗くするやり方を身につけましょう。

Youtubeの動画の画面をデベロッパーツールで見ます。すると、videoタグでclass属性はvideo-streamなどが記載されていることが分かります。
では、そこに filter: brigtness(0.5); とデベロッパーツールの下の方にある、cssを入力できる場所に入力してみると画面が暗くなります。

cssのfilterプロパティとは画像や動画を加工するために使われています。
特に画像に使われます。
MDNにbrightness以外のものもあるので見てください。

つまり、content_scriptsで指定したjsファイル(content.js)で、videoタグのclass属性を取得さえすれば出来るということです。

let movie = document.getElementsByClassName("video-stream");
movie[0].style.filter = "brightness(0.5)";

まず1行目でvideo-streamクラスを取得します。
2行目なのですが、なぜmovie[0]の[0]を付けたのかということを簡単に言いますと、特定するためです。
id属性は一つしか付与することはできないのに対し、class属性は複数のタグに同じクラス名を付与できます。

movieだけだと、どのタグについてるvideo-streamクラスなのかわかりません。
なので、ここは一番最初のvideo-streamクラスにfilterプロパティをつけたいので、movie[0]としています。

<div class="video-stream">0</div> //movie[0]
<div class="video-stream">1</div> //movie[1]

一つ目のタグを取りたいなら[0]をつけ、2つ目のタグをとりたいなら[1]をつけるようにする。
なお、id属性をとるのなら、[0]などについては必要ありません。

2,manifest.jsonを作る

まずサンプルをここに載せて置きます。

manifest.json
{
   "manifest_version": 2,
   "name": "",
   "description": "",
   "version": "1.0",
   "browser_action": {
     "default_icon" : "",
     "default_title": "",
     "default_popup": ""
   },
   "icons":{
     "16":"16x16....",
     "
   }
   "permissions" : [
   ],
   "content_scripts": [{
       "matches": ["<all_urls>"],
       "js": [""]
   }],
   "background":{
     "scripts": [""],
     "persistent": false
   }
 }

masnifest.jsonの書き方

manifest.jsonにはこういう感じで書いていきます。
まず、"manifest_version" : 2とありますが、manifestのバージョンで、現在バージョンが2であるためです。これは絶対にこのまま書いておきましょう

nameとは拡張機能の名前です。

descriptionとは拡張機能の説明です。どのような拡張機能か書きましょう。

versionとは拡張機能のバージョンを指します。なにか変えたりしてアップデートを行った場合、versionも変えましょう。作成した拡張機能をアップデートをして、再びウェブストアにアップロードするときversionを変えてなかったら、エラーガ起こります。

続いてbrowser_actionについて

default_icon拡張機能のアイコンです。
38x38 の大きさの画像を作成して、指定してあげてください。

default_titleは右上にある拡張機能のアイコンにマウスオーバー(マウスを乗せる)すると出てくる名前です。

default_popupに関しては,htmlファイルを指定します。
指定した場合、アイコンをクリックすると、ポップアップが出てきます。
指定しなかった場合,何も出てきません。

icons指定された画像はウェブストアの時に使います
16x16 48x48 128x128の大きさを指定してあげてください

permissionsではchrome APIを使いたいときに記述します。
permissionsに書かなくても使えるchromeAPIはあります。

content_scriptsdom操作をするときに使います。
machesには正規表現で書き、拡張機能を使う対象のサイトを書きます。
jsでは動作させるスクリプトを書きます。
content_scriptsで、今見ているタブ(サイト)をDOM操作で色々と書き換えることができます。

background: バックグランドページとイベントページがあります。
今はイベントページが推奨されています。
scriptsバックグランドで動作するjsファイルを指定します。
persistentはfalseにすると、イベントページとなり何も書かなかったらバックグランドページとなります。
バックグラウンドページは裏側ではずっと動いているのに対して、イベントページでは必要なときに動くのでpersistent: falseは書いておきましょう。

ポップアップ画面を作る

次にポップアップ画面をデザインしていきます。
これは好きなようにして構いませんが、最低でも

  <input type="range">
  <button></button>

この二つは必要です。

3,chrome APIをリファレンスを読みながら作っていく。
基本的に chrome api を読んでいきながら作ってみましょう。

まず,どんなapiを使うのかまとめると

chrome tabs api
chrome runtime api

これらを使っていきます。
tabs APIで送信して、runtime APIで受け取るようにします。
なおtabs APIbackgroundで動くjavascriptでないと動作しないので、ポップアップするHtmlファイルの外部jsにbackground.jsを使います。

つまり
1,htmlのinputタグの値をbackground.jsで取得し、tabsAPIを使って送信

2,content_scriptsのjsで,background.jsが送信したデータをruntime APIで受け取る

3,受け取ったデータを使いDOM操作をやる。

background.jsを書いていこう!

background.js
let btn = document.getElementById('btn'); //button を取得
let col = document.getElementById('elem'); // input type=range を取得

ボタンをクリックしたら、送信したいので

background.js
btn.addEventListener('click',function(){
    //ここにapiを記述
}

このように記述します。
まずchrome tabs apiに行きます。tabsAPIで使いたいメソッドは、

  • query()
  • sendMessage()

この二つのメソッドとなります。

まずqueryメソッドについて学んでいこうと思います。

chrome.tabs.query(object queryInfo, function callback)

このようなコードとなっています。何が何だかわからないと思いますが見ていきましょう。
queryメソッドの引数にはobject queryInfo, とあります。最後にコンマ( , )があるので、第一引数はここまでということが分かります。

まずobjectとありますが、これは型を示しています。
javascriptでオブジェクトを書くときは,

sample.js
let obj = {
  "***": ***
}

と書くように、このobject queryInfoも{}で囲んであげて、
その中に ○○○:○○○と書いていきます。

sample.js
chrome.tabs.query({}, function callback)

chrome.tabs.query({○○○:○○○, ○○○:○○○}, function callback)

このような感じになります。この{}の中にjsonと同じように書いていきます。
公式サイトを見ますが、
active pinned audibleとずらっと書いてあります。その左にbooleanと書いてありますが、これが何を示しているのかというと、簡単に言えば設定です。
booleanというとtrueやfalseということなので、
{active: true} また{active: false}このような感じで書けということです。

ここで使うオプションはactivecurrentWindowの二つのオプションです。
ちなみにどっちもbooleanです。

activeはタブがウィンドウでアクティブかどうか。とグーグル翻訳で出ています。
つまり今このタブを開いているかどうかです。
trueにしましょう。

currentWindowはタブが現在のウィンドウにあるかどうかです。
つまり、今見ているサイトかどうかです。これもtrueにしましょう。

この二つのオプションをtrueにすることで、開いているサイトで尚且つ、今見ているサイトの情報を取得することになります。

sample.js
chrome.tabs.query({active: true, currentWindow: true }, () => {
})

object queryInfoが第一引数に対して、第二引数はfunction callbackです。
引数の中に関数があるため、第二引数はコールバック関数であることは一目瞭然です。

コールバック関数の引数には,object queryInfoで指定されたタブ(サイト)の情報があり、それをsendMessageメソッドに渡します。

sample.js
chrome.tabs.sendMessage(integer tabId, any message, object options, funct
ion responseCallback)

第一引数には、見ているサイトのtabIdを書かないといけないので、queryメソッドのコールバック関数にサイトの情報 (tabId)があるので、使います。

sample.js
chrome.tabs.query({active: true, currentWindow: true},tab => {
  chrome.tabs.sendMessage(tab[0].id, any message, object options, fu
  nction responseCallback)
})

tab[0].idtabIdを取得できます。

第二引数には送信したいデータを指定します。
公式サイトには送信するメッセージ。このメッセージは、JSONで送信可能なオブジェクトである必要がありますと書いています。
つまりjson形式で書いてくださいということです。

また送信したいデータはinput type="range"の値です。

sample.js
let col = document.getElementById('elem'); // input tag

chrome.tabs.query({active: true, currentWindow: true},tab => {
  chrome.tabs.sendMessage(tab[0].id, { dark : Number(col.value)})
})

{dark : Number(col.value)}と第二引数に書きました。
Numberメソッドで、引数にあるinput range の値を数値に変えています。

第三引数からに関しては必要ないと思っています。
まず第一、このinput rangeの値さえcontent_scriptsに渡せばいいのですから、一方的なんですよね。

これをまとめたコードがこちらです。

background.js
let btn = document.getElementById('btn');
let col = document.getElementById('elem');

btn.addEventListener('click',function(){
 chrome.tabs.query({active: true, currentWindow: true},tab => {
   chrome.tabs.sendMessage(tab[0].id,{ dark : Number(col.value) })
 })  
})

これでボタンをクリックすればapiが実行され、content_scriptsに送信されます。

次はcontent.jsで送信されたデータを受け取り、dom操作で反映させます。

content.jsを書く

受け取る方法は簡単で,
runtime apichrome.runtime.onMessage.addListener()を使うだけです。

content.js
chrome.runtime.onMessage.addListener(function callback)

引数の中に関数functionがあるのでコールバック関数となります。
コールバック関数の引数には,

第一引数にmessage, 第二引数にsender, 第三引数にsendResponseがあります。

content.js
chrome.runtime.onMessage.addListener(
  function(message, sender, sendResponse){

  }
)

受け取ったデータをDOM操作するというシンプルなことですから、第一引数のmessageだけで、問題ないです。
messageには送信したデータが入っています。

あとは、最初に学んだDOM操作を関数の中に書いてあげます。

content.js
chrome.runtime.onMessage.addListener(
  function(message, sender, sendResponse){
    let movie = document.getElementsByClassName('video-stream');
    movie[0].style.filter = `brightness(${message.dark})`
  }
)

brightness(${message.dark})はテンプレートリテラルを使ってます。バッククォートで囲んであげてください。

作った拡張機能を読み込んでみよう

これで拡張機能は作れたので読み込んでみましょう。

1, まずブラウザを開き,右上の3つの点をクリック
2, その他のツールをクリック出てきた、拡張機能をクリック
3, パッケージ化されていない拡張機能を読み込むをクリックし、フォルダを選択
4,完了です。ページに戻り右上に見ると追加されているのが分かります。
エラーの場合は、manifest.jsonにちょっとおかしいところがあります。なおしましょう。

ウェブストアにアップロードしよう。

実際にアップロードするのは、この記事で作った拡張機能ではなく、オリジナルの拡張機能でお願いします。

1.chromeウェブストアに行く。
2,右上にある歯車マークを押し,デベロッパーダッシュモードを選択
3,1ドル(だいたい100円)を払う。
4,作ったフォルダを圧縮してzipにしてから、アップロード。
5,ダッシュボードに行き、英語で拡張機能の説明を書き、完了したら審査に出す。自分は英語が書けないので、DeepLを使い書きました。

これでアップ完了です。自分はアップしてから五日ほどで審査が終わり、ストアに出せるようになりました。最初は検索しても出てきませんでしたが、2日ほど待つと後ろの方ですが検索に出てくるようになりました。
またアップデートしたものを、ウェブストアに出すとき、審査されますが自分は1日で終わりました。

終わりに

初めて拡張機能を作ってみましたが、chromeAPIがちょっと難しかったなと思います。
adjusting the video brightという拡張機能をできれば試してほしいと思います。
UIが崩れてるかもしれないので、もし崩れてたら教えてほしいです。
お願いします!

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

AIRBNBのようにGOOGLEMAPをWEBに追加

追加背景

今自分で習いながらAIRBNBのような民泊シェアリングエコノミーサイトを作っているので、その中でもGOOGLEMAPの位置表示機能を追加する手順をシェアできればかと思います。今回は部屋の掲載ページに載せるので主にROOM関連のファイルで編集していきます。

GEM追加

Gemfile.
gem 'geocoder', '~> 1.4'

bundle install

DB migration

rails g migration AddFieldsToRoom latitude:float longitude: float (掲載した部屋のページに表示したいのでこの場合ROOMのDBに追加)
rails db: migrate

rooms table

Column Type Options
home_type string null: false
room_type string null: false
accommodate integer null: false
bed_room integer null: false
bath_room integer null: false
listing_name string null: false
summary text null: false
address text null: false
is_tv boolean null: false
is_kitchen boolean null: false
is_aircon boolean null: false
is_heating boolean null: false
is_internet boolean null: false
price integer null: false
active boolean null: false
latitude float null: false
longitude float null: false
user references foreign_key: true

Association

  • belongs_to: user
  • has_many: photos

Roomモデルファイル編集

room.rb
  geocoded_by :address
  after_validation :geocode, if: :address_changed?

ROOMのDBのADDRESSから位置付けをする。あとはアドレスがアップデートされる度にGEOCODEがVALIDATIONをかけ、位置経緯を自動的に決める。

Roomビューファイル編集

rooms/show.html.rb
    <!-- GOOGLE MAP -->
    <div class="row">

      <div id="map" style="width: 100%; height: 400px"></div>

      <script src="https://maps.googleapis.com/maps/api/js"></script>
      <script>
          function initialize() {
            var location = {lat: <%= @room.latitude %>, lng: <%= @room.longitude %>};
            var map = new google.maps.Map(document.getElementById('map'), {
              center: location,
              zoom: 14
            });

            var marker = new google.maps.Marker({
              position: location,
              map: map
            });

            var infoWindow = new google.maps.InfoWindow({
              content: '<div id="content"><%= image_tag @room.cover_photo(:medium) %></div>'
            });
            infoWindow.open(map, marker);
          }

          google.maps.event.addDomListener(window, 'load', initialize);
      </script>

最後の一行はGOOGLEMAP APIのマップ生成を行うためのJquery
https://techacademy.jp/magazine/5638#sec2 (DOMについて忘れたかけてるのでもう一度復習)

Markerはマップ上にピンマークされるように指定。
infowindowでは部屋のカバー写真をマップ上に表示させる

これでマップは表示されるはず。(下の図を参照、場所は適当にマレーシアにしてます笑)
image.png

DBではちゃんと経緯を生成されている!
image.png

そして。。。表示成功!勉強しながらやっていますが、レベルアップしたような達成感はあります。(ズームアウトすると訳のわからん熱帯雨林みたいなところにある、ちょうど写真には合うけど笑)
image.png

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

[JavaScript] 配列の存在チェック(空判定)は if (array.length) {...} でいいよって話

配列の存在チェック(空判定)について個人的に思うことです。
少しだけECMAにも触れます。

配列の存在チェックどうしてる?

チェックの方法って結構バラバラですよね。

配列の存在チェック例
// array => []
if (array.length === 0) {...}
if (array[0]) {...} // falsyな値の時は意図しない動きに。詳しくはコメント欄へ。
if (array[0] !== void 0) {...}
if (array.length > 0) {...}

チェックはもうこれで良いんじゃないかな

if (array.length) {...}
if (!array.length) {...}

理由

  1. そもそも、lengthって名前が0以上の数値ってことが自明だから
  2. JavaScriptでは、0がfalse、1以上はtrueとなるから

まとめ その1

早いですが、一旦まとめます。
別に大した話じゃなく、配列の存在チェックはarray.lengthでなんら問題ないということです。
array.length === 0とかが駄目という話ではないということは、念のために書いておきます。(可読性的な意味で)
伝えたいことはこれでほぼ全部なので、これ以降はおまけです。

根拠

配列の仕様について少しだけ触れます。

配列のインデックスについて

配列のインデックスについてはこのように定義されています。(一部抜粋)

An array index is an integer index whose numeric value i is in the range +0 ≤ i < 2^32 - 1. 1

配列インデックスの範囲は +0 ≤ i < 2^32 - 1 の範囲である。
つまり、マイナス値を取ることはないということです。

lengthプロパティについて

配列のlengthプロパティはこのように定義されています。(一部抜粋)

Every Array object has a non-configurable "length" property whose value is always a nonnegative integer less than 2^32 2

  • non-configurableなプロパティ3
  • 2^32以下の非負な整数値

さらに、lengthプロパティはWritableなので値を代入できます。

// array => ["a", "b"]
array.length // => 2
array.length = 3 // => 3
array.length = -1 // => Uncaught RangeError

注目していただきたいのは、マイナスを代入したときのエラーです。
なぜこうなるかは、やはり仕様を見ればわかります。

9.4.2.4 ArraySetLength ( A, Desc ) 4
The abstract operation ArraySetLength takes arguments A (an Array object) and Desc (a Property Descriptor). It performs the following steps when called:

A は配列オブジェクトのことです。Desc とはProperty Descriptorのことです。
Property Descriptorとは簡単にいうとオブジェクトプロパティへのメタ属性達のことであり、[[Value]]や[[Writable]]などが存在します。
配列の作成時には { [[Value]]: length, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false } が設定されています。

  1. If Desc.[[Value]] is absent, then
    a. Return OrdinaryDefineOwnProperty(A, "length", Desc).
  2. Let newLenDesc be a copy of Desc.
  3. Let newLen be ? ToUint32(Desc.[[Value]]).
  4. Let numberLen be ? ToNumber(Desc.[[Value]]).
  5. If newLen ≠ numberLen, throw a RangeError exception.
    ...(省略)

(訳)

  1. もし Desc.[[Value]] がなければ
    a. OrdinaryDefineOwnProperty(A, "length", Desc). を返す
  2. newLenDesc は Desc のコピーとする
  3. newLen は ? ToUint32(Desc.[[Value]]) とする
  4. numberLen は ? ToNumber(Desc.[[Value]]).
  5. もし newLen ≠ numberLen であれば, RangeError exception. をスローする

[[Value]]は属性名です。プロパティに対してgetで取得された値を指します。

array.length = -1をトレースするとこうなります。
3.で-1を渡すのでToUint32(-1) // => 42949672955となります。
4.で-1を渡すのでToNumber(-1) // => -1となります。
5.で4294967295 ≠ -1なので、RangeError exception. をスローします。

まとめ その2

根拠で確認した内容から絶対0未満にならないことがわかりました。
仕様という根拠があればこれでいい気がしてきませんでしょうか?

if (array.length) {...}

以上、配列の存在チェック(空判定)は if (array.length) {...} でいいよって話でした。

さらにおまけ

例えば、こんなお行儀の悪いことをしている人がいたとしたら length では検知できません。

const array = []
array["hoge"] = "hoge"

console.log(array) // => [hoge: "hoge"]
console.log(array[0]) // => undefind
console.log(array.length) // => 0

// pushなどをしてあげれば、lengthの値も変更される
array.push("fuga")

console.log(array) // => ["fuga", a: "a"]
console.log(array[0]) // => "fuga"
console.log(array.length) // => 1

これは、配列はオブジェクトであり、Objectを継承したものが配列であるからです。
key-valueの形式で値を保持することができるのですが、まあ気持ちが悪いですね。
因みに、-1もkeyに設定できます。

const array = []
array[-1] = "hoge" 
console.log(array) // => [-1: "hoge"]
console.log(array[0]) // => undefind
console.log(array.length) // => 0
console.log(array[-1]) // => "hoge"

配列のインデックスについてで述べたとおり、配列で指定できるインデックスの範囲外であるためkeyとして設定されます。

チェックする方法はあります。

const array = []
array["hoge"] = "hoge"
array[0] = "fuga"

Object.keys(array) // => ["0", "hoge"]
Object.keys(array).length // => 2

Object.keysを使用してkeyを列挙することでチェック可能です。

参考

ECMAScript® 2021 Language Specification


  1. ECMAScript® 2021 Language Specification#sec-object-type 

  2. ECMAScript® 2021 Language Specification#sec-array-exotic-objects 

  3. プロパティの削除が出来ないなどの意味を持ちます。 

  4. ECMAScript® 2021 Language Specification#sec-arraysetlength 

  5. ToUint32についてなにをやっているかは、仕様を見ていただくか、JavaScriptのビット演算の仕組みを理解する - 風と宇宙とプログラム こちらのブログが大変わかりやすいです。 

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