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

コールバック地獄からの脱出

どうもこんにちは。うめきです。
これはアドベントカレンダー「厚木の民」の3日目の記事です。
本日はJavaScriptにおけるコールバック地獄の回避方法について書きます。JavaScriptを既にある程度理解している方向けの記事となっています。そもそもJavaScriptってなんぞやって人は直接会ったときに聞いてみてください。ちゃんと教えます笑

この記事を読んでわかること

  • JavaScriptの非同期処理って具体的にどんなもの?
  • コールバック地獄ってなに?
  • コールバック地獄を抜け出すためのPromiseってなに?
  • Promiseを使いやすくしてくれるasync/awaitってなに?

並列処理はできないが非同期処理はできるJavaScript

JavaScriptにおける同期・非同期処理を語るうえで重要な特性が2つあります。

1. JavaScriptはシングルスレッドで動いており、キューにたまった関数を1つずつ処理する
2. 関数がキューに登録される順番が同期的であったり非同期的であったりする

まず1についてです。以下のソースコードを例にすると、キューにはaの計算をせよという命令の次に、bの計算をせよという命令が入っていることになります。そして、aの計算が終わった後でbが計算され、aとbが同時に計算されることはありません。これは簡単ですね。

a = 1 + 2;
b = 3 + 4;

次に2についてです。以下のソースコードにおいてはどのようにキューに追加されていくでしょうか。まず、同期的にconsole.log(1)、setTimeout(...)、console.log(3)が順番にキューに追加されます。そして、setTimeout(...)が実行されたタイミングでconsole.log(2)がタイマーに渡されます。タイマーは関数によって指定された時間(ここでは0秒)が経過後にconsole.log(2)をキューに追加します。このタイミングでは先にconsole.log(3)がキューに追加されています。

console.log(1);
setTimeout(function(){console.log(2)}, 0);
consol.log(3);

したがって、このソースコードの実行結果は以下のようになるわけです。

1
3
2

0秒だから1,2,3と表示されるんじゃ?と思っていた方もいたのではないでしょうか?console.log(2)は、ソースコードに書かれた順番通りにキューに追加されていません。非同期的にキューに追加されています。

このように、JavaScriptのスレッド以外にタスクを投げたとき、関数が非同期的にキューに追加されることがあり、結果としてJavaScriptは非同期処理であるという言い方をするのです。具体的には、タイマー処理、HTTP通信、データベース処理などが非同期的なキューの追加を発生させます。

コールバック地獄との遭遇

前章から、JavaScriptにおいては同期処理や非同期処理に気を付けなければならないことがわかったと思います。実際に私も、JavaScriptを扱うようになってから同期処理と非同期処理を意識して使わなければならない場面が多く出てきました。例えば、複数のデータの読み込みが完了してから読み込んだデータに対して処理をかけたいという場合です。非同期処理の完了後に特定の処理を行いたい場合、コールバック関数に次の処理を書いていきます。テキストファイルを複数読み込んで、そのあとに処理をかける場合以下のように書けます。

const fs = require('fs');

fs.readFile('data1.txt', function(data1) {
     fs.readFile('data2.txt', function(data2) {
         fs.readFile('data3.txt', function(data3) {
             fs.readFile('data4.txt', function(data4) {
                 fs.readFile('data5.txt', function(data5) {
                     console.log(data1 + data2 + data3 + data4 + data5);
                 })
             })
         })
     })
 })

どうでしょう?このソースコードのキモさがわかるでしょうか?

  • 深いネストにより可読性が下がる → コードを書く領域がどんどん狭くなっていく…
  • エラーをまとめて受け取れない  → 記入漏れありそう…バグの温床では…?
  • 処理の追加/削除が大変     → メンテナンスが大変…     

これがいわゆるコールバック地獄というやつですね。JavaScriptを書くことはコールバック地獄との戦いであります。
ダメ押しですが、上記のソースコードにエラー処理を付けた場合どのようになるのか一応載せておきます。

const fs = require('fs');

fs.readFile('data1.txt', function(error, data1) {
    if(error) {
        console.log(error);
        return;
    }
    fs.readFile('data2.txt', function(error, data2) {
        if(error) {
            console.log(error);
            return;
        }
        fs.readFile('data3.txt', function(error, data3) {
            if(error) {
                console.log(error);
                return;
            }
            fs.readFile('data4.txt', function(error, data4) {
                if(error) {
                    console.log(error);
                    return;
                }
                fs.readFile('data5.txt', function(data5) {
                    if(error) {
                        console.log(error);
                        return;
                    }
                    console.log(data1 + data2 + data3 + data4 + data5);
                })
            })
        })
    })
})

もはや吐き気を催すレベルです。こんなコードを世の中に生み出してはいけません。。

Promise先生との出会い

コールバック地獄を解決してくれるのがPromiseです。Promiseは「非同期処理の最終的な完了もしくは失敗を表すオブジェクト」です。Promiseを使えば、非同期処理が終わるのを待ってから次の処理を始めてね的なことが簡単にできます。具体的なメリットは以下の通り。

  • 非同期処理を連結する際、コールバック地獄から解放される
  • エラー処理をうまく記述できる
  • 一連の非同期処理を関数化して再利用しやすくできる

ここからは少しややこしくなりますが頑張りましょう!

Promiseオブジェクトはnew Promise(fn)によってインスタンス化されます(関数fnには非同期処理が書かれます)。

let promise = new Promise(fn);

インスタンス化されたPromiseオブジェクトは、以下の3つを持っています。

  • Promiseオブジェクトの状態(Fullfilled、Rejected、Pending)
  • 非同期処理が成功したときに実行されるコールバック関数(onFullFilled)
  • 非同期処理が失敗したときに実行されるコールバック関数(onRejected)

上記のonFullFilled関数およびonRejected関数をPromiseオブジェクトに登録するためには以下のメソッドを使います。

promise.then(onFullFilled, onRejected);

インスタンス化直後は、Promiseオブジェクト内の非同期処理が成功も失敗もしていないPending状態になっており、非同期処理が成功(FullFilled状態)するとonFullFilled関数が実行され、非同期処理が失敗(Rejected状態)するとonRejected関数が実行されます。つまりこんな感じで非同期処理をつなげて書くことができます。

let promise = new Promise(fn);

promise
.then(onFullFilled_1, onRejected_1)
.then(onFullFilled_2, onRejected_2)
.then(onFullFilled_3, onRejected_3)
.then(onFullFilled_4, onRejected_4)
.then(onFullFilled_5, onRejected_5)

なんだか良さげにみえますね。もう少し詳しく見てみます。

実は、onFullFilled関数およびonRejected関数はPromiseオブジェクトから処理結果の値やエラーオブジェクトを受け取ることが可能です。Promiseオブジェクトのコールバック関数fnは引数にresolve関数とreject関数をもっています。fn内の非同期処理が正常に処理された場合はresolve関数が結果の値をonFullFilled関数に渡し、処理がエラー終了した場合はreject関数がエラーオブジェクトをonRejected関数に渡しています。

これだけではわからないと思いますので具体例を見てみましょう。
さきほどのテキストファイル読み込み処理をPromiseオブジェクトを返す関数にしてみました。

const fs = require('fs');

function readFile(file) {
    return new Promise(function(resolve, reject){   // new Promise()でPromiseオブジェクトを生成
        fs.readFile(file, function(error, data){
            if (error) reject(error);   // errorがあればreject関数を呼び出す(引数はエラーオブジェクト)
            resolve(data);              // errorがなければ成功とみなしresolve関数を呼び出す(引数は返却したい値)
        });
    });
}

上記のソースコードで、reject関数が呼び出された場合はエラーオブジェクトerrorがonRejected関数に渡されます。また、solve関数が呼び出された場合は処理結果の値dataがonFullFilled関数に渡されます。ここまで大丈夫そうでしょうか?あとちょっと話します。

エラー処理をまとめて書きたいときには、promise.then(undifined, onRejected)と等価な以下の記法が使えます。

promise.catch(onRejected)

なお、thenメソッドの引数はオプショナルであるので以下のように書くことが多いです。

promise
.then(onFullFilled)
.catch(onRejected)

なお、全体に共通したエラー処理もcatchメソッドで処理可能です。

promise
.then(onFullFilled_1)
.then(onFullFilled_2)
.then(onFullFilled_3)
.then(onFullFilled_4)
.then(onFullFilled_5)
.catch(onRejected)

ここまでくるとコールバック地獄を回避して最初のコードを書き換えられそうですね。実際に書き換えました。

let data1, data2, data3, data4, data5

readFile('data1.txt')
.then(function(_data1) {
    data1 = _data1;
    return readFile('data2.txt');
})
.then(function(_data2) {
    data2 = _data2;
    return readFile('data3.txt');
})
.then(function(_data3) {
    data3 = _data3;
    return readFile('data4.txt');
})
.then(function(_data4) {
    data4 = _data4;
    return readFile('data5.txt');
})
.then(function(_data5) {
    data5 = _data5;
    console.log(data1 + data2 + data3 + data4 + data5);
})
.catch(function(error) {
    console.log(error);
})

んんん!!!!なんか思ったよりきれいじゃない!
エラー処理は一か所にまとめることができていますがdata1 = _data1みたいな行はなんぞや?となってると思います。結局Promiseオブジェクトが返す値はsolve関数を介して次のコールバックに渡してあげないと参照できなくなってしまうからこんな残念な感じになっているんですね。。

いやいやでももうちょっときれいに書けるだろ!って批判が各方面から来そうなので書き直しました。

function subReader(file) {
    return function(previous) {
        return new Promise(function(resolve, reject){   
            fs.readFile(file, function(error, data){
                if (error) reject(error);   
                resolve(previous + data);            
            });
        });
    }
}

reader('data1.txt')
.then(subReader('data2.txt'))
.then(subReader('data3.txt'))
.then(subReader('data4.txt'))
.then(subReader('data5.txt'))
.then(function(data) {console.log(data)})
.catch(function(error) {console.log(error)})

あれ?意外といいかも!さすがPromise!
じゃあ今度はsubReader関数の処理だけじゃなくて、各ファイルで取得したデータに異なる処理をかけてみましょう!

あれ、、?ちょっと処理が違うだけでも全部Promiseを返す関数を書かなきゃいけないの?
メソッド間での変数の引き渡しも最初から考え直さなきゃいけない?めんどくさいよ。。。Promise先生。

関数を再帰的に呼び出せばいいじゃん!と思う方がいるかもしれません。上記のケースでは確かに再帰呼び出しによりソースコードをコンパクトにまとめることが可能です。しかしながら、処理A→処理B→処理C→処理Dのように異なる非同期処理を連続して実行する場合にはきれいにまとめて書くことができません。また、処理Aの結果を処理Dにおいて使いたい場合には、thenチェーンによって処理Aの結果を渡し続けるか、thenチェーンの外側に変数を退避させる必要があります。このようなプログラムを書いた場合、処理の追加や削除のたびにそこそこ面倒な修正が必要になってしまいます。

じゃあ、どのような書き方ができるのが理想でしょうか?こんな感じになるのではないかと思います。

// data1 -> data2 -> data3 -> data4 -> data5 の順番に取得
let data1 = readFile('data1.txt');
let data2 = readFile('data2.txt');
let data3 = readFile('data3.txt');
let data4 = readFile('data4.txt');
let data5 = readFile('data5.txt');
// 全データを取得後に実行
console.log(data1 + data2 + data3 + data4 + data5);

もちろんこのままでは正常に動作しません。しかし、仮に上のような書き方ができたなら、より簡潔に非同期処理を書くことができますし、thenチェーン間の変数の受け渡しという面倒な制約を考えなくてよいことになります。結果として、処理の追加や削除にも柔軟に対応できるプログラムになります。

ちなみにsubReader関数の引数に違和感を覚えた方はこちらを参照してください。

async/await大先生の登場

理想形はわかったけども結局どうすればいいのやら、、と思って探してみるとasync/awaitなるものがありました!
async/awaitの最大の魅力は、Promiseを利用した構文よりも簡潔に非同期処理が書けることです。async/awaitはPromiseを使いやすくしてくれます。以下、詳しく見てみましょう。

async functionにより、非同期関数を宣言することができます。async functionは暗黙的にPromiseオブジェクトを返します。

async function readFiles() {}

async functionの中では、await式を使うことができます。

async function readFiles() {
    let data1 = await readFile('data1.txt');
}

awaitはasync関数の処理を一時停止し、awaitの後ろに書かれた関数のPromiseが解決するまで(FullFilledかRejectedになるまで)待ちます。Promiseが解決後に、async関数の処理を再開し、解決された値を返します。

ではこれまで何度もみてきたソースコードはasync/awaitを使ってどのように生まれ変わるのでしょうか?

async function readFiles() {
    let data1 = await readFile('data1.txt');
    let data2 = await readFile('data2.txt');
    let data3 = await readFile('data3.txt');
    let data4 = await readFile('data4.txt');
    let data5 = await readFile('data5.txt');
    console.log(data1 + data2 + data3 + data4 + data5);
}

readFiles()
.catch(error) {console.log(error)}

やばい、神なのか!!これがずっと望んでいた最終形態ともいえる形なんじゃないでしょうか。さきほどの理想的なソースコードと比較してください。理想に限りなく近い書き方が可能になっていることがわかります。async/awaitを使えば、thenメソッド間のデータの処理を気にする必要もありませんし、非同期処理内容の変更にも柔軟に対応することができます。

もうちょっと汎用的な形にしましょう。

let files = ['data1.txt', 'data2.txt', 'data3.txt', 'data4.txt', 'data5.txt'];

async function readFiles(files) {
    let data = [];
    for(let i = 0; i < files.length; i++) {data.push(await readFile(files[i]))}
    console.log(data.reduce((x, y) => x + y));
}

readFiles(files)
.catch(function(error) {console.log(error)})

ついに非同期処理にfor文を使えるところまできました。とでも感慨深いですね。
ついでに、5というマジックナンバーを消して、配列の和を取る部分はreduceメソッドを使ってみました(蛇足ですが、ちょっとでも速度を上げたいのであれば、files.length の部分はfor文の外で変数に格納しておく方がよいと思います)。

コールバック地獄との長い戦いの末にasync/awaitが問題の大部分を解決してくれることが分かりました。
実は例外処理については気をつけなきゃいけないのですがこちらはまた今度にしましょう。

まとめ

  • 並列処理はできない、非同期処理はできる(JavaScriptはシングルスレッド)
  • コールバック地獄を解決するにはasync/awaitを使おう
  • async/awaitを使うにはPromiseの理解が必要
  • 例外処理には気を付けること

参考文献

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

年末まで毎日webサイトを作り続ける大学生 〜45日目 JavaScriptで位置情報を取得する〜

はじめに

こんにちは!@70days_jsです。

JavaScriptで位置情報を取得してみました。

今日は45日目。(2019/12/2)
よろしくお願いします。

サイトURL

https://sin2cos21.github.io/day45.html

やったこと

JavaScriptで位置情報を取得してみました。
取得した情報は
1. 緯度
2. 経度
3. 緯度/経度の精度
4. 高度
5. 方角
6. 速度
です。

実際にpcでやってみると、上3つだけ表示することができました。

スクリーンショット 2019-12-02 23.49.00.png

<body>
    <div id="test">緯度: </div>
    <div id="test2">経度: </div>
    <div id="test3">緯度/経度の精度: </div>
    <div id="test4">高度: </div>
    <div id="test5">方角: </div>
    <div id="test6">速度: </div>
</body>
let geolocation = window.navigator.geolocation;

let option = {
    enableHighAccuracy: true
};

function error() {
    alert('エラーです。');
}

function success(position) {
    test.innerHTML += position.coords.latitude;
    test2.innerHTML += position.coords.longitude;
    test3.innerHTML += position.coords.accuracy;
    test4.innerHTML += position.coords.altitude;
    test5.innerHTML += position.coords.heading;
    test6.innerHTML += position.coords.speed;
}

if (geolocation) {
    geolocation.getCurrentPosition(success, error, option);
}

getCurrentPositionメソッドは引数を3つ用意してます。
第一引数は取得時の関数
第二引数は取得失敗時の関数
第三引数はオプションです。

感想

位置情報はモバイルアプリを作るときには便利かもしれませんね。
いずれJavaScriptで作りたいと思っているので、そのときに利用しようと思います。

最後までお読みいただきありがとうございます。明日も投稿しますのでよろしくお願い致します。

参考

  1. JavaScriptで位置情報を取得する方法(Geolocation API) (https://syncer.jp/how-to-use-geolocation-api)

とても分かりやすかったです。ありがとうございます!

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

Nuxt.jsで作っているサイトにKARTEのタグを入れてみるメモ

Nuxt.jsで作っているWebサイトKARTEを入れてみたときのメモです。

NuxtにGoogle アナリティクスを導入する手順とほぼ同じで出来た

公式のGoogle アナリティクスを使うには?が参考になります。

読んでみると

他のトラッキングサービスでも、同様の方法を使うことができます。

って書いてますね。

1. plugins/karte.jsを作成

こんな雰囲気でpluginsの中にkarte.jsを作成します。

スクリーンショット 2019-12-02 22.53.40.png

KARTEの管理画面で計測タグを探してスクリプトタグの内部を見つけましょう。

plugins/karte.js
export default ({ app }) => {
    /*
    ** クライアントサイドかつプロダクションモードでのみ実行
    */
    if (process.env.NODE_ENV !== 'production') return
    /*
    ** karteのスクリプトをインクルード
    */
    (function(){var t,e,n,r,a;for(t=function(){var t;return t=[],function(){var e,n,r,a;for(n=["init","start","stop","user","track","action","event","goal","chat","buy","page","view","admin","group","alias","ready","link","form","click","submit","cmd","emit","on","send","css","js","style","option","get","set","collection"],e=function(e){return function(){return t.push([e].concat(Array.prototype.slice.call(arguments,0)))}},r=0,a=[];r<n.length;)t[n[r]]=e(n[r]),a.push(r++);return a}(),t.init=function(e,n){var r,a;return t.api_key=e,t.options=n||{},a=document.createElement("script"),a.type="text/javascript",a.async=!0,a.charset="utf-8",a.src=t.options.tracker_url||"https://static.karte.io/libs/tracker.js",r=document.getElementsByTagName("script")[0],r.parentNode.insertBefore(a,r)},t},r=window.karte_tracker_names||["tracker"],e=0,n=r.length;n>e;e++)a=r[e],window[a]||(window[a]=t());tracker.init("xxxxxxxxxxxxxxxxxxxxxxxxxx")}).call(this);
   /* ↑計測タグの中身をコピペ*/ 

}

IDっぽいところをxxxx~~にしてますが、こんな感じで入れ込みます。

ここのタグは人によって権限っぽい指定の箇所が変わるかもしれないので、この記事からよりは、大元からのコピペが良いと思います。

2. nuxt.config.jsの設定

利用するにあたり、nuxt.config.jsにも以下の記載をします。

nuxt.config.js
省略

  plugins: [
    { src: '~plugins/karte.js', mode: 'client' }
  ],

省略

以上!

所感

割と簡単に出来ました。

実際に僕の場合はnuxt generateで静的サイトにしてホスティングしていますが、静的ホスティングじゃない場合でも同様に動くと思います。(たぶん)

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

javascript 基本その4 ライブラリ

今日は発展形であるライブラリについて書きます。

ライブラリ

Rubyにおいてgemにあたります。

汎用性の高い複数のプログラムを再利用可能な形でひとまとまりにしたもの
他のプログラムに何らかの機能を提供するコードの集合体です。

JavaScriptで開発を行うのを簡単にするツールです。

ライブラリの種類

色々種類があるので
それぞれ紹介していきます。

まず大元のjavascriptを例文として紹介します。

javascript
var btn = document.getElementById("title");
btn.addEventListener("click", function() {
  console.log("Hello world");
});

jQuery

DOM操作(DOM要素の取得や追加削除など)をもっと短く簡単に書くことができます。
Webサイト制作などをやりたい場合は、jQueryの知識が必要になります。

jquery
$(function() {
  $("#button").on("click", function() {
    console.log("Hello world");
  });
});

React

Facebookが開発をしたライブラリです。

仮想DOMの概念によって、より早い高速なアプリケーション実装が実現できます。
AndroidやiOSに対してもReact Nativeを使用すればReactを適用でき
柔軟性も高いことから、最近大きく人気が増しています。

書き方は、JavascriptとHTMLを合わせて書きます。

React
class Hoge extends React.Component{
    render(){
        return (<div onClick={() => console.log('Hello world') }>
        </div>)
    }
}

仮想DOM

jQueryの場合、データを更新したらDOMを操作してビューを更新します(クラスの追加や削除など)。

実は、DOMを直接操作するのは思っているよりも処理が遅いです。
そこで、構造化した仮のDOMを作成し、データ更新したら
仮想DOMの差分だけをDOMに反映させるのが基本的な考え方です。

これで処理を速くすることができます。

Vue.js

Reactと同様に仮想DOMの概念があり
記述を減らして、HTML,CSSを中心にしたWebアプリ開発が可能です。

Vue.js
new  Vue({
  el:  '#button',
  methods: {onClick:  function() {
    console.log('Hello world');
  }}
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Figmaプラグイン開発時の各ファイルの役割を把握しよう

はじめに

この記事はfigmaプラグイン開発の準備
の記事をやった方向けです!

プラグイン開発の準備ができたので、実際に開発をする上で把握しておきたい各ファイルの役割について解説したいと思います。

ファイル構成の確認

Figmaプラグインの雛形を作成すると、下記のようなファイルを含むフォルダが作成されます。

pj_folda/
 ├ code.ts(今回の主役)
 ├ code.js(code.tsと同義として扱うので説明しない)
 ├ ui.html(今回のヒロイン)
 ├ manifest.json(ボス)
 ├ figma.d.ts(説明しない)
 ├ tsconfig.json(説明しない)
 ├ README.md(説明しない)

これらのファイルの中から

  • 主役
  • ヒロイン
  • ボス

を切り出して、それぞれの役割を確認していきましょう!

code.ts

まずは主役の紹介から!Figmaプラグインを開発するうえで主役となるメインファイルです。

code.tsからはFigmaアプリ内のオブジェクトや情報にアクセスし、操作することができます。
例えば、図形を作成したり、テキストを変えたり、色を変えたりなど、基本Figmaを操作するうえでできることは大体できます。

簡単に触って理解してみましょう。

デフォルトはui.htmlと連携するコードになっており、少しだけ複雑なので、code.tsだけで完結するようにより簡単にしたサンプルコードに変えてみましょう。
レクタングルを1つ作成して選択状態にし、作成したレクタングルが見えるようにviewportを設定します。

code.ts
const nodes: SceneNode[] = [];
const rect = figma.createRectangle();
rect.x = 150;
rect.fills = [{type: 'SOLID', color: {r: 1, g: 0.5, b: 0}}];
figma.currentPage.appendChild(rect);
nodes.push(rect);
figma.currentPage.selection = nodes;
figma.viewport.scrollAndZoomIntoView(nodes);

実行すると下記のような図形ができると思います。

スクリーンショット 2019-12-02 13.43.26.png

このように、code.tsはFigmaの中の情報にアクセスするためのファイルとして使うことができます。

ui.html

続いてヒロインの登場です。

Figmaのオブジェクトにアクセスする処理のみの場合は、code.tsのみで良いのですが、ユーザーの入力を求めたり、外部のネットワークを使用してFigmaオブジェクトを操作したい場合は、ui.htmlを利用します。

ui.htmlはcode.tsなどのFigmaオブジェクトを操作できる空間とは別の空間として生成されます。
簡単にいうと、ui.htmlからはFigmaオブジェクトを操作することはできないということです。
具体的には、code.tsなどが存在するFigmaの空間の中にiframeが生成されて、その中にui.htmlが表示されます。

この「提供しているサービスを直接操作できない別の空間のHTMLファイルに記述する」という考え方は、拡張機能を開発するうえではよく出てきます。(Chrome extensionとかも同じ感じ)

なぜ、わざわざ分けるのかというと、セキュリティのためです。
外部ネットワークを使うと機能面での選択肢が広がり便利になりますが、同時にセキュリティのリスクも上がります。
Figmaオブジェクトが存在する空間とは別の空間で外部ネットワークに接続させることで、ある程度の制限ができ、セキュリティの強度をコントロールすることができるのです。

ui.html
<h2>Rectangle Creator</h2>
<p>Count: <input id="count" value="5"></p>
<button id="create">Create</button>
<button id="cancel">Cancel</button>
<script>

document.getElementById('create').onclick = () => {
  const textbox = document.getElementById('count');
  const count = parseInt(textbox.value, 10);
  parent.postMessage({ pluginMessage: { type: 'create-rectangles', count } }, '*')
}

document.getElementById('cancel').onclick = () => {
  parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*')
}

</script>

ui.htmlの中には、Figmaオブジェクトを操作するコードはありませんね。

postMessage & onmessage

ここで重要なコードは下記です。

parent.postMessage({ pluginMessage: { type: 'create-rectangles', count } }, '*')

また、code.tsのデフォルトのコードにも下記のような記述があります。

code.ts
figma.ui.onmessage = msg => {

このpostMessageとonmessageを抑えることができれば、プラグイン開発がぐっとらくになると思うので解説していきます。

先ほど述べたように、code.tsとui.htmlは別世界にあります。
しかし、javascriptが提供しているpostMessageとonmessageを利用することで2つの世界を連携をすることができるようになります。

postMessage

postMessageはメッセージの送信です。メッセージと一緒に値も送ることができます。
下記のコードでは2つのパターンのメッセージを送っています。

  • createがクリックされたら「レクタングルを作ってくれ、数はこれだ。」とメッセージを送る
  • cancelがクリックされたら「cancelしてくれ。」とメッセージを送る
ui.html
<script>
document.getElementById('create').onclick = () => {
  const textbox = document.getElementById('count');
  const count = parseInt(textbox.value, 10);
  parent.postMessage({ pluginMessage: { type: 'create-rectangles', count } }, '*')
}

document.getElementById('cancel').onclick = () => {
  parent.postMessage({ pluginMessage: { type: 'cancel' } }, '*')
}
</script>

このように、処理応じてメッセージを発信することができ、そのメッセージは別の世界に届けることができるのです。

onmessasge

次は受け取り方です。
onmessasgeを用意しておくとpostMessageが来た時に反応を起こせるようになります。
つまりは、イベントが発火できます。
onmessasgeはどのメッセージが来ても発火してしまうので、onmessasgeの中で条件分岐をさせます。

code.ts
figma.ui.onmessage = msg => {
  if (msg.type === 'cancel') {
    // 処理
  }
  if (msg.type === 'create-rectangles') {
    // 処理
  }
}

postMessage & onmessageでのやりとり

サンプルでは、ui.htmlからcode.tsにpostMessage を送り、code.ts側でonmessage発火するコードになっていますが、逆のパターンも可能です。

お互いにメッセージを送ったり受け取ったりして、絶の世界の住人とやり取りをするのです。(主役とヒロインという設定にしていたから若干ロマン感じる?)

manifest.json

では最後にボスの登場です。
ボスはこの世界の設定を定義しています。
もし、ここのファイル名やパスが間違っていたら、、、、

manifest.json
{
  "name": "plugin_test",
  "id": "111111111111111111",
  "api": "1.0.0",
  "main": "code.js",
  "ui": "ui.html"
}

まとめ

みんなでFigmaプラグインを作ろう!
雑な説明になってしまいましたが、少しでも誰かの助けになっていれば幸いです。

補足・訂正やもっとわかりやすい説明あれば歓迎です!

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

JavaScript 複数キーを対象とするソートの方法

概要

画面上テーブルで表示するようなデータを配列で持つ場合、以下のように オブジェクトを配列で持つようにする場合が結構あると思う。

[
    {
        "id": 1,
        "group": 1,
        "name": "tom"
    },
    {
        "id": 2,
        "group": 1,
        "name": "tim"
    }
]

このような形のデータを画面処理でソートする際、
さらには複数キーでのソートを行いたい場合に実装した内容のメモ書きになります。

詳細

まずはベースとなるソート処理の関数
やってることとしては、keyを使って比較するオブジェクト(aとb)の値を取得し、判定するロジック。
後ろの引数で昇順、降順、データがない場合の表示順を制御できるようにもしてる。

const defaultSortFunc = function(a, b, key, direction = 1, nullsFirst = 1) {
  if (a[key] == undefined && b[key] == undefined) return 0;
  if (a[key] == undefined) return nullsFirst * 1;
  if (b[key] == undefined) return nullsFirst * -1;
  if (a[key] > b[key]) return direction * 1;
  if (a[key] < b[key]) return direction * -1;
  return 0;
}

ソートキーが1つの場合は、sort処理内で関数を呼び出すだけでおしまい。

const data = [
  {"id":1,"group":1,"name":"tom"},
  {"id":2,"group":1,"name":"tim"},
  {"id":3,"group":3,"name":"tomas"},
  {"id":4,"group":3,"name":"tanaka"},
  {"id":5,"group":2,"name":"takahashi"},
  {"id":6,"group":2,"name":"takada"}
]
data.sort((a, b) => defaultSortFunc(a, b, 'name'))

ソートキーが複数の場合は、以下のような処理になる。
対象キーでソートの判断がつかない場合のみ、次のキーでのソートを行っていくようなイメージ。

const sortFunc = function(data, keys) {
  const _data = data.slice();
  _data.sort((a, b) => {
    let order = 0;
    keys.some(key => {
      order = defaultSortFunc(a, b, key);
      return !!order;
    });
    return order;
  });
  return _data;
}

上記の関数を呼び出してソートを行う場合は以下のような呼び出し方になる。
ソートのキーとなる項目を配列で渡してあげる。

const data = [
  {"id":1,"group":1,"name":"tom"},
  {"id":2,"group":1,"name":"tim"},
  {"id":3,"group":3,"name":"tomas"},
  {"id":4,"group":3,"name":"tanaka"},
  {"id":5,"group":2,"name":"takahashi"},
  {"id":6,"group":2,"name":"takada"}
]
sortFunc(data, ['group','id'])

まとめ

オブジェクトを配列で持つこの形はAPIのレスポンスなどでも結構多い形のイメージがあるので、
結構使いどころがある処理だと思います。

ソートの基本的な処理を関数にして呼び出しておく形にすると、
ソート処理で特殊な判断をしたい項目があったら関数を置き換えてソート処理をするなど、
応用がきくので個人的には便利だと思っています。

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

あまり知られてないWeb APIを使って鬼ごっこゲームを作る

テックタッチアドベントカレンダーの3日目を担当する @92thunder です。
2日目は @smith-30 による 初めての ECS でした。ちなみに自分は手元のDocker環境が動かなくなったのでバックエンドを触れなくなってしまいました。

できたもの

See the Pen Demon Play with IntersectionObserver , MutationObserver, elementFromPoint by Ryota Kunisada (@92thunder) on CodePen.

  • MutationObserver, IntersectionObserver, elementFromPointを使って鬼ごっこゲームを作りました
  • 素晴らしい名作ゲームが簡単に完成してしまいましたね
  • 所要時間2時間

Mutation Observer

DOMの変更を監視するためのAPI
オプションを設定することで子孫全部やstyleが変わった場合だけ反応するよう設定できる
https://developer.mozilla.org/ja/docs/Web/API/MutationObserver

subtreeオプションを有効にしてdocument.bodyなどに対して使うとページ読み込み時など動作が重くなってしまうので使いどころに気を付けたほうが良さそうです。
今回のゲームではPlayerとEnemyの要素が動くたびにスタイルが変わっていてその変更時に毎回elementFromPointによる衝突判定が行われているので敵を大量に増やすとパフォーマンスが残念なことになりそうです

// コールバックの設定とオブザーバインスタンスの設定
const observer = new MutationObserver(() => {
  console.log('DOMに変更がありました')
})

// document.bodyを対象に監視を開始する
observer.observe(document.body)

// 監視を止める
observe.disconnect()

Document.elementFromPoint

左上を起点として、座標位置にある要素を返す
iframe中の要素だった場合はiframeを返すため、iframeを考慮した使い方が必要
https://developer.mozilla.org/ja/docs/Web/API/Document/elementFromPoint

今回のゲームではPlayerの上にEnemyが重なっているかの判定に使っていて、重なったら負けになるよう実装しています
テックタッチでは、これを使うと座標の上に何の要素が表示されているのかがわかるので、要素が隠れているかどうかなどの判定に使っています

// 100, 100の位置にある要素を返す
const element = document.elementFromPoint(100, 100)

// Node.containsと合わせて使うことがよくあります
const app = getElementById('app')
if (app.contains(element)) {
  // appに含まれているので無視
  return
}

IntersectionObserver

対象の要素と祖先もしくは最上位のビューポートと交差したタイミングでコールバックを実行する
スクロールを使ったコンテンツの遅延読み込みなどに使う
オプションでrootを指定して交差判定に使う要素や交差の閾値を設定できる
IEはサポートされていない

IntersectionObserverを使って画面の範囲外に出たら負けになるよう実装しました
交差の閾値(要素が何%はみ出ているか)を設定できるので50%の0.5を設定しています
https://developer.mozilla.org/ja/docs/Web/API/IntersectionObserver

// コールバック設定
const observer = new IntersectionObserver(() => {
  console.log('交差しました')
})

// #targetの要素とビューポートの範囲外に出るor範囲内に入るたびにコールバック関数を実行
observer.observe(document.getElementById('target'))

// 監視を止める
observe.disconnect()

所感

  • 3つとも今年初めて知ったAPIで普通の開発ではあまり使わなさそうだがとても便利
  • テックタッチは外部サービスにembedして機能するサービスのため、このような普段の開発では使わないような機能をふんだんに使って開発をしています

4日目は @terunuma による「4Kモニタ環境で1年間Web開発してみた所感」です。

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

非同期処理(と、その周辺のこと)について調べたこと

Twitterにて、「エンジニアにとってAPIと非同期処理は要にして難関」というツイートをみたのでまず非同期処理について調べたことをど初心者ながらまとめたいと思います。

非同期処理について

  • JavaScriptはシングルスロットの実装しかできない
  • webブラウザから一部の情報をリクエストするのでそれ以外の部分は変わらない

同期処理

  • プログラムが記述された通りに実行されるもの
  • 1行のプログラムに時間がかかったら全体が重くなる
  • その処理の1つに時間のかかる処理があると、その実行が完了するまで、次の行には進まない

こちらのサイトを拝見していると、「Callback」という単語が出てきたので・・・


Callback関数とは

こちらのサイト(わかりやすかったです)によると、

  • 別の関数に呼び出してもらうための関数
  • 高階関数に渡すための関数
  • 他の関数に引数として渡す関数のこと
  • 「ある関数が呼び出された時、戻り値として、本来渡したい結果を返すのではなく、一度関数としては終了し(=呼び出し元に戻る)、あとで『本来渡したかった値』を返せる状態になった時に、呼び出し元にその値を通知する」仕組み

ということです。

コールバックの問題点

  • 複雑な処理になると、途端にわかりにくくなるということ

▶︎読みやすく書くためには、同期的に書く!

- ネストが入れこいにならない
- 上から下に読んで実行フローが把握できる
- 非同期処理の結果得られた値を次の処理に引き回せる。
- try/catchで素直に例外を捕捉できる

同期的に書く代表的な方法は
- Promiseを使う方法
- Generatorを使うほほう

がある


Promiseについて

こちらのサイトによると、Promiseは

  • ES2015(ES6)から追加仕様として加わった

  • 3つの状態(resolve, reject, done)を持つ

という特徴がある。

  • promiseの基本的な考え方は、非同期的に動作する関数は、本来返したい戻り値の代わりに、『プロミスオブジェクト』という特別なオブジェクトを返しておき、(本来返したい)値を渡せる状態になったら、そのプロミスオブジェクトを通して、呼び出し元に戻す」というもの

また追加で調べたことがあれば、追記していきます!

JavaScriptの「コールバック関数」とは一体なんなのか

JavaScriptの非同期処理Promise・async awaitを学んでみた

Promisen非同期処理について

非同期処理ってどういうこと?JavaScriptで一から学ぶ

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

SpresenseとiS110B(wifiモジュール)をつかってIoTことはじめをしてみる。

はじめに

Spresenseとネットを繋ごうとしたときにwifiモジュールをつけて
なにかしようと考えた際に、候補になるのはおそらく以下の2つだと思う。

今回は、パット見技術ブログが見当たらなかった後者のiS110Bについて話したいと思う。
偉そうなことを言っているけども、やることはサンプルを動かすだけ。

やること

  1. iS110Bを使える環境をセットアップする。
  2. サンプルのHTTPClient.inoをなにも考えずに動かす。

用意するもの

ソフトウェア的に用意するもの

下記に書いたバージョンは私が手を動かした際の環境をそのまま書いている。必ずこの環境でなければ動作しないという事ではないため注意すること。
ちなみに、私が動作確認した際のOSはmacOS Catalina 10.15.1である。

これらのソフトウェアはすでにインストール済みとして話をすすめる。

1. iS110Bを使える環境をセットアップする。

まず、iS110Bのサンプルプログラムが入っている下記のGithubページにアクセスする。
ちなみに、この"GS2200"という名称
は基盤上に実装されているWifiモジュール本体のTelit社製の"GS2200MIZ"によるもので、"iS110B"はボード全体の名称である。
https://github.com/jittermaster/GS2200-WiFi

スクリーンショット 2019-12-01 14.56.57.png

次に、開いたページを開いて"Clone or download"をクリックし、出てきたポップアップにある"Download ZIP"を再度クリックする。Zipファイルの保存先に関してはご自身の好きな場所でOK。

スクリーンショット 2019-12-01 14.49.02.png

スクリーンショット 2019-12-01 15.06.29.png

次に、Arduino IDEを起動し、メニューバーの"ライブラリをインクルード"→".zip形式のライブラリをインストール"をクリックする。その後、ダウンロードしたファイル(おそらくGS2200-WiFi-master.zip)を選択する。

その後、Arduinoのコンソールにゴニョゴニョと出力されるため、完了するまで待つ。完了したら一旦Arduino IDEを再起動し、メニューの”ファイル”→スケッチ例に"GS2200"という項目が入っていることを確認する。

スクリーンショット 2019-12-01 15.10.46.png

入っていれば本節は完了。

2. サンプルのHTTPClient.inoをなにも考えずに動かす。

先にSpresenseとiS110bをくっつけてしまおう。以下の写真を参考に、
SpresenseにiS110bを差し込む。ポイントとしては、Sonyのロゴに対して二本のピン側が向くようにして、Sonyロゴ側にピン穴が残らないように差し込む。

P_20191202_200605_vHDR_On.jpg

その後、Arduinoを閉じていたら再度起動して、、HTTPClientを開く。先程のスケッチ例から選択できる。
スクリーンショット 2019-12-01 15.16.46.png

ファイルが開いたら、メニューの"スケッチ"→"スケッチのフォルダを表示"をクリックし、サンプルコードの入ったフォルダを開く。

スクリーンショット 2019-12-01 15.19.51.png

このファイル3つの入ったフォルダが出てきたらOK。
スクリーンショット 2019-12-01 15.23.22.png

その後、フォルダ階層を2つ下がって"GS2200-WiFi-master"フォルダに下がり、"script"フォルダが見える状態にしておく。

スクリーンショット 2019-12-01 15.30.12.png

フォルダを右クリック→"サービス"→"フォルダに新規ターミナル"をクリックして、ターミナルを起動する。Windowsの人だったらコマンドプロンプト。

スクリーンショット 2019-12-02 19.39.15.png

ターミナルを開いたら、自分のIPアドレスを確認する。mac使いならおなじみのifconfigで確認する。

スクリーンショット 2019-12-02 19.51.20.png

スクリーンショット 2019-12-02 19.52.41.png

その後、"node HTTP_Server.js"を実行する。
スクリーンショット 2019-12-02 19.47.02.png

多分コンソールには何も表示されないはず。ここでArduino IDEに戻り、先程の"HTTPClient.ino"を表示する。そのあとに隣のタブにある"config.h"を選択し、ご自身の環境に揃える。

スクリーンショット 2019-12-02 20.28.56.png

終わったら、メニューのファイル→”名前をつけて保存”で好きな場所に保存しておく。

その後、Spresenseとシリアルポートをつなぎ、MainCoreに対してプログラムを書き込む。プログラムの書き込むときのオプションは以下の写真の通りにする。

スクリーンショット 2019-12-02 20.19.20.png

→矢印ボタンをクリックして書き込む。
スクリーンショット 2019-12-02 20.25.22.png

書き込みが完了したあとにメニューの"ツール"→"シリアルモニタ"を選ぶと、Spresenseと母艦のPCが互いに数字をカウントアップしながらGET-POSTで値を贈り合っている様が確認できるはず。

75540120_2756341821139708_3936563970344747008_o.jpg

とりあえず、事始めはこれで完了かな。

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

大きく表示するタイマーが欲しかったからdivのborderで数字を描いてみた

はじめに

divのborderで数字を描いてみた話です。

やりたかったこと

小学校の教室でタイマーを大きく表示したい。

背景

月に数回程度、小学校にお邪魔してK3Tunnelを使ったプログラミング授業をお届け しています。

普通の授業時間にお邪魔するので、時間厳守。
時間厳守しないと給食時間や休み時間が短くなってしまいます。
出張授業なので、45分×2コマの1本勝負。
続きはまた今度ね。は許されません。

私たちがやっているプログラミング授業は、シナリオが用意された体験モノです。そのため、ここまでは到達してほしい、ここの面白さを感じてほしいというポイントがあります。

なので、時には、作業途中の子どもたちの手を止めて「次」に移ってもらったり。
ゆっくりゆっくりやっている子のペースアップを促したりすることもあります。

が。こんな感じのこどもたちを目にしたら。
kids_doro_asobi_s.png

それがなかなか難しい。

言いにくいことはAIに言わせる。

これ。エンジニアの常識です。

時間が来たよと機械的に伝える。

終了時間を指定して時計を見てもらうだけでは弱い。
タイマーをセットして突然音を鳴らすのは心臓に悪い。
子どもたち自身のペースメイクを促したい。

タイマーを大きく誰もが見える場所に表示するのがベスト。

というのが私の結論でした。

対応方針

物理的な時計やタイマーアプリなどには目もくれず、つくる。いいから作る。
nichiyoudaiku_s.png

作成方針

実装に使うモノについて学習しない。
なぜならJavaScript/html/cssで何か作りたい熱が高まったのがトリガーで、「今日は時間が取れる」休日がぽっと生まれたときに作ることにしたから。

1日でぱぱっとつくれることが重要。
慣れない言語に手を出すなど論外。
初めてのライブラリやフレームワークに挑戦するとかもなし。

だがしかし。つくる。

つくったもの

できあがりはこちらで確認できます。
https://cocoakamen.github.io/cocoa-timer/

構想する

学習せずに作るといっても、特大フォントで時間表示するだけでは、いくらなんでも芸がない。
少しくらいはチャレンジ要素を入れたい。

と。

しばし考えて、思いついた。

ひょっとして。

divのborderを太くしたら、デジタル時計っぽくなるのでは?

とりあえず、このHTMLで、上下と左右で色を変えたdiv(縦横100pxで太さ20px)を表示してみた。

<html>
  <div style="border-color:black gray; height:100px; width:100px; border-style:solid; border-width:20px;"></div>
</html>

結果。
timer-try.jpg

デジタル数字ができそうな気がする見た目をしている。

作る

divを並べる

こんな感じにdivを並べることに。太枠のボックスが数字を描こうとしているdiv。
timer-div.jpg
各数字、二つのdivを使うイメージ。
箱の中に書いてあるのはid名。
id名が書いていないのは、スペース調整用だったりなんだり。
classがいっぱいあるのは、スタイル設定するための色々。
もしかしたら使っていないclassもあるかもしれない。。

数字を表示しているところのHTMLはこちら。
上下それぞれ横に並べるのが少しややこしい。

index.html
<div class="timer-container">
  <div class="number-row">
      <div class="digital-number number-left number-up" id="number-minute-left-up"></div>
      <div class="digital-blank-separater"></div>
      <div class="digital-number number-right number-up" id="number-minute-right-up"></div>
      <div class="digital-separater" id="separater-up"></div>
      <div class="digital-number number-left number-up" id="number-second-left-up"></div>
      <div class="digital-blank-separater"></div>
      <div class="digital-number number-right number-up" id="number-second-right-up"></div>    
  </div>
  <div class="number-row">
      <div class="digital-number number-left number-down" id="number-minute-left-down"></div>
      <div class="digital-blank-separater"></div>
      <div class="digital-number number-right number-down" id="number-minute-right-down"></div>
      <div class="digital-separater" id="separater-down"></div>  
      <div class="digital-number number-left number-down" id="number-second-left-down"></div>
      <div class="digital-blank-separater"></div>
      <div class="digital-number number-right number-down" id="number-second-right-down"></div>    
  </div>  
</div>

borderのスタイルを決めていく

文字の太さ

cssの:root 疑似クラスカスタムプロパティで定義。

timer.css
:root {
 --number-line-width: 30px;
}

digital-numberクラスにベースの設定をカキカキ。(数字を描くdiv要素は全部digital-number)

timer.css
main .digital-number{
  border: solid;
  width:200px; 
  height:170px;
  border-width: var(--number-line-width); /* :root要素で設定しているカスタムプロパティ参照 */
  box-sizing: border-box; /* パディングとボーダーを幅と高さに含める */
}
/* 全体的にmain要素で囲っているのでmainがついてます */

上と下が重なり合うところは、それぞれ幅を2分の1にします。
こういうとき、カスタムプロパティを定義しておくと便利です。

timer.css
main .number-up {
  border-bottom-width: calc( var(--number-line-width)/2);
}

main .number-down {
  border-top-width: calc( var(--number-line-width)/2);
}

文字の色

文字の色は、JavaScriptの中で指定。
コンストラクタ定義でベタ打ち定義。

timer.js
function CocoaTimer() {
  this.numberColor = 'black'; // 文字の色
  this.GRAY_OUT_COLOR = '#EFEFEF'; // 線がない部分の色
  this.remainingTime = 0;
  this.timer = {};
};

数字をかく

表示する数字ごとに色を指定します。
こんなイメージ。
number_color.jpg

あとは地道に定義していきました。引数は数字を描くdivの上と下の要素。

timer.js
CocoaTimer.prototype.drawNumber = function(upElement, downElement, num) {
  // 中略
  // 数字
  switch(num) {
    case 0:
      upElement.style.borderColor = this.numberColor;
      upElement.style.borderBottomColor = this.GRAY_OUT_COLOR;
      downElement.style.borderColor = this.numberColor;
      downElement.style.borderTopColor = this.GRAY_OUT_COLOR;
      break;
    case 1:
      upElement.style.borderColor = this.GRAY_OUT_COLOR;
      upElement.style.borderRightColor = this.numberColor;
      downElement.style.borderColor = this.GRAY_OUT_COLOR;
      downElement.style.borderRightColor = this.numberColor;
      break;
    // 中略 以下9まで地道に定義
      break;
    default:
      break;
  }
};

時間は普通に計算

時間はミリ秒で計算。
残り時間のミリ秒から、分、秒それぞれ1の位と10の位の数字を求める計算をsetInterval使って実行して再描画。詳細は省略。
時間になったら点滅。今のところ音はなりません。

ソースはこちらにも

https://github.com/cocoakamen/cocoa-timer

子どもたちの反応

授業で実際に使った時の反応はおおむね「やばい爆弾だ。爆弾。」
jigenbakudan_s.png
なんですが、残念ながら、爆弾イフェクトは実装されておりません。

将来ゲームクリエイターになりたいんだ♪という子に、あのタイマー、私がつくったんだよと自慢したところ、尊敬のまなざしを得られた気がします。

もちろん、狙い通り、子どもたち自身が残り時間を意識している様子も見受けられ、ほくほくです。

まとめ

タイマーを導入したら、子どもたちのフォローをするときに、時間を気にする脳ミソ使用量が減った気がします。ワークショップの進行がとてもラクになるのでお勧めです。

ちなみに、自分の子の小学校授業参観にいったら、物理タイマーを実物投影機で大きいディスプレイに映してました。

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

【WEB版 WEAR】position: sticky で似ているアイテム検索の使いやすい UI/UX を実現する

この記事はZOZOテクノロジーズ #2 Advent Calendar 2019 19日目の記事になります。
昨日は、@saitoryuji さんによる「新卒2年目のエンジニアが単体テストをやってみる」でした。

また、今年は全部で5つのAdvent Calendarが公開されています。

はじめに

最近、スマートフォンのWEB版 WEARで、コーデ画像をもとにAIがアイテムを検出し、さらにそのアイテムに似ているアイテムを検出して一覧で表示する機能をリリースしました:tada:
真似したいコーデの服が簡単に見つかる!

この機能をWEBで実装するにあたり、使いやすいUI/UXを目指した奮闘記を紹介します!

こんなのをやりたい

まず、大まかな要件は以下の通りです。

  1. コーデ画像の左下の検索アイコンをタッチすると検索スクリーンが下から上がってくる
  2. コーデ画像からアイテムが検出された箇所を「◯」で表示する
  3. スクリーン下部に、選択されたアイテムの「似ているアイテム」の一覧を表示する
  4. スクロールすると「似ているアイテム」の一覧が上がってくる
  5. 「似ているアイテム」の一覧はヘッダーが固定され、一覧内でスクロールできる

コーデ詳細.png → デフォルト.png → スクロール.png → 一覧表示.png

最初にこの要件を聞いたとき、経験上難しそうだなーと感じたのは、
windowのスクロールとは別に、一覧内もスクロールできるようにしないとけないところでした。
実装自体は可能なのですが、スクロールできる要素の中にスクロールできる要素を配置すると、スクロールしたい方じゃない方がスクロールされるなど、とても使いにくいイメージがありました。。。:sweat:

今回は上記の要件のうち、1、4、5の部分について説明します。
※以下に出てくるソースコードは、説明用のサンプルです。

ボツになった案

それでも、まずは思いついたままに、以下のような構成で実装してみた。
(最初に言っておきますが、これはボツです笑)

実装

HTML
<div id="searchScreen" style="position: relative; transform: translateY(0px);">
    <div id="image" style="position: absolute;">...</div>
    <div id="list">
        <div id="listHead"></div>
        <div id="listBody" style="height: 812px; overflow-y: scroll;">
            <ul>
                <li></li>
                :
            </ul>
        </div>
    </div>
</div>

See the Pen Sample1 by mitanih (@mitanih) on CodePen.

#searchScreen
コーデ画像の検索アイコンボタンのクリックイベントで、下から上がってくる検索スクリーン
下から上げる処理として、CSSアニメーションでtransformtranslateY(100%)からtranslateY(0px)に切り替えて表現する
※marginの切り替えでも表現できるが、残像ができたり動きがカクつくなどの問題があった

#image
検索スクリーン内上部のイメージエリア
windowがスクロールされても固定したいので、position: absoluteを指定して、常に画面の同じ位置にいるようJavaScriptで制御する
※親要素にtranslateを指定しているのでposition: fixedは使えない

#list
検索スクリーン内下部の一覧

#listHead
一覧のヘッダー

#listBody
一覧のボディ
一覧内をスクロールさせるためheightを設定し、overflow-y: scrollを指定

問題点

触ってみて、やっぱり使いにくかった。。。:sweat_smile:
iOSのSafariで動作確認した範囲ですが、windowをスクロールさせて一覧を上に持っていきたいのに、一覧内がスクロールされてしまったり、慣性スクロールの影響なのか、windowがスクロールして完全に止まらないと、一覧内がスクロールできないなどの現象も起きた。
スクロールしやすいように工夫して、一覧が一番上まで来ていなければ一覧内はスクロールできないようJavaScriptで制御も入れてみたが、スムーズな動きとはいかなかった。
あと、#imageの位置を固定するためスクロールの度にJavaScriptで位置を計算して設定したが、フラフラ動いてなんか変。
何より無理し過ぎな気がしました。。。:sweat:

採用した方法

いや、もっといい方法はあるはずだ。他のサービスでこんなぎこちないの見たことないぞ。。。:thinking:
と、いろいろ調べていて、position: stickyに辿り着きました!
Excelの行固定みたいな機能が簡単に実現できるあのスタイルです。

position: stickyの説明
https://developer.mozilla.org/ja/docs/Web/CSS/position

メジャーなiOS、Androidのブラウザ対応も問題なさそう!
https://caniuse.com/#search=sticky

実装

HTML
<div id="searchScreen" style="position: relative; transform: translateY(0px);">
    <div id="image" style="position: sticky;">...</div>
    <div id="list">
        <div id="listHead" style="position: sticky;"></div>
        <div id="listBody">
            <ul>
                <li></li>
                :
            </ul>
        </div>
    </div>
</div>

See the Pen Sample2 by mitanih (@mitanih) on CodePen.

細かい調整は省略しますが、要は#imageと#listHeadにposition: stickyを設定するだけで解決しました!
windowのスクロールで#listが上に来るまでは、#imageがstickyしてくれる。#listが上までくると#listHeadがstickyし、windowのスクロールでそのまま#listBodyが下まで見られる。
まさに理想的なスタイル!
無理していない実装なので動きもスムーズです!:blush:

まとめ

JavaScriptやCSSのプロパティを覚えると、自力でゴリゴリ書けちゃうんですが、実機で確認すると動きがぎこちなくてなんか無理してる感じがすることってあると思います。
そんなときは視点を変えてもっとシンプルな方法を探すと、よりよい解決策が見つかるということに気付かされます。
プログラミングって、そんなことのくり返しのような気がする。。。

明日は、@ikkou さんの記事です。
お楽しみに~!

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

【JavaScript】 {} === {} はfalseになるので、簡単なnpmパッケージを作りました。

結構前に、JavaScriptで遊んでいたところ、以下のような場面に遭遇しました。

console.log({} === {})

出力結果は、 true でしょうか。 false でしょうか。

こんなの余裕で true でしょ!そう思っていました。

結果
false

でした。。。。

なんでだろう???

trueでしょ!!そう思った方は、是非最後までご覧ください。

1. JavaScriptの比較・データ型について

等価演算子(==)と厳密等価演算子(===)

等価演算子は、2 つのオペランドが同じ型でないならばオペランドを変換して、それから厳密な比較を行います。
厳密等価演算子は、型変換なしでオペランド同士が等しければ真を返します。
参考: 比較演算子 |MDN

重要な点は、等価演算子(==)は、暗黙的に型の変換を行った上で比較を行うという点です。

データ型

プリミティブ型とオブジェクト型の2種類があり、
プリミティブ型には、以下7種類のデータ型があります。
Boolean
Null
Undefined
Number
BigInt
String
Symbol

プリミティブ型でないものは全てオブジェクト型になります。
配列もオブジェクト型です。

参考: JavaScript のデータ型とデータ構造

プリミティブとは

JavaScript において、プリミティブ (primitive、プリミティブ値、プリミティブデータ型) はオブジェクトでなく、メソッドを持たないデータのことです。 すべてのプリミティブ値は、イミュータブル、つまり変更できません。変数には新しい値を再割り当てすることができますが、既存の値については、オブジェクト、配列、関数が変更できるのに対して、プリミティブ値は変更することができません。

参考: プリミティブ |MDN

つまり、プリミティブな型は、値を貸すことはできるが、定義された値に対して新しい値を代入することができません。

具体的に例で示します。

プリミティブな型の場合。

const number = 10
numberChange(number)
console.log(number) // => 10

function numberChange(number){
    number += 10000
    console.log(number) // => 10010
}

値を渡すことはできますが、定義したconst number = 10の値自体が変更されることはありません。

オブジェクト型(プリミティブ型でない)場合。

const array = [1,2,3]
arrayChange(array)
console.log(array) // => [1,2,3,10000]

function arrayChange(array){
    array.push(10000)
    console.log(array) // => [1,2,3,10000]
}

値ではなく、アドレスを渡している(参照渡し)ため、const array = [1,2,3]の値が変更されます。

1. 上記疑問を解決する

ドキュメントに答えが書かれていました。

等価演算子は、2 つのオペランドが同じ型でないならばオペランドを変換して、それから厳密な比較を行います。両方のオペランドがオブジェクトならば、 JavaScript は内部参照を比較するので、オペランドがメモリ内の同じオブジェクトを参照するときに等しくなります。

具体的に例で示します。

object  = {name: "aoki"}
object2 = {name: "aoki"}
object3 = object

console.log(object === object2) // => false 参照が異なるため  
console.log(object === object3) // => true 参照が同じため

objectobject2は、プロパティとその値の組み合わせこそ同じなのですが、参照が異なるのでfalseとなります。
objectobject3は、参照が同じなのでtrueとなります。

以上のことから、

console.log({} === {}) // => false

となるわけです。なるほどっ!

でもやっぱり違和感を感じてしまいます。
直感的には、
{name: "aoki"}{name: "aoki"}は同じなんだから、trueを返してほしい。

そう思い、npmパッケージを作ることにしました。

2. npmパッケージを作って公開する

今回、作成するnpmパッケージが満たすべき要件は、以下のように設定しました。

  • 二つのオブジェクトを比較した際に、プロパティと値の組み合わせが同じなら、プロパティの順番が異なっていても、trueを返す

そこで、必要な機能をまとめました。

(1)それぞれのオブジェクトのプロパティをソートする
(2)それぞれソートされたオブジェクトをjson形式にして比較する

まず、(1)です。以下のように実装しました。

(1)の実装
function objectSort(object) {
  var newObject = {};
  var keyArray = [];
  for (key in object) {
    keyArray.push(key);
  }
  keyArray.sort()
  for (var i = 0; i < keyArray.length; i++) {
    newObject[keyArray[i]] = object[keyArray[i]];
  }
  return newObject;
}

新しいオブジェクト・配列を用意し、引数として与えられたオブジェクトのプロパティを配列に格納します。
格納された配列をsortメソッドを使ってソートし、ソートされた配列に対して、対応する値とともに新しいオブジェクトに格納します。
格納されたオブジェクトを返します。

参考: オブジェクトのキーでソートをしたい

次に、(2)です。
JSON.stringify()というメソッドが用意されているので、それを使います。

(1)と(2)を組み合わせて、関数を作ります。

index.js
function objectMatch(obj1,obj2){
  const obj1Sorted = objectSort(obj1)
  const obj2Sorted = objectSort(obj2)
  obj1Json = JSON.stringify(obj1Sorted)
  obj2Lson = JSON.stringify(obj2Sorted)
  if(obj1Json === obj2Lson){
    return true
  }else{
    return false
  }
}

function objectSort(object) {
  var newObject = {};
  var keyArray = [];
  for (key in object) {
    keyArray.push(key);
  }
  keyArray.sort()
  for (var i = 0; i < keyArray.length; i++) {
    newObject[keyArray[i]] = object[keyArray[i]];
  }
  return newObject;
}

できました!!
いくつかテストをしてみます。

index_test.js
const objectMatch = require('./index');

const object1 = {
  name: "aoki",gender: 1,height: 168.5
}

const object2 = {
  gender: 1,name: "aoki",height: 168.5
}

const object3 = {
  gender: 1,name: "aoki",height: 168.6
}

const object4 = {
  gender: 1,name: "aoki",profile: {hobby: {sports: ["soccer","baseball","basketball"]}}
}

const object5 = {
  gender: 1,profile: {hobby: {sports: ["soccer","baseball","basketball"]}},name: "aoki"
}

//プロパティとその値の組み合わせは同じだが、順番が異なる時、trueを返す。
test('object1 and object2 is equal', () => {
  expect(objectMatch(object1,object2)).toBe(true);
});

//プロパティに対して、その値が異なる時、falseを返す。
test('object1 and object3 is not equal', () => {
  expect(objectMatch(object1,object3)).toBe(false);
});

//プロパティに対する値が複雑でも、同じならtrueを返す。
test('object4 and object5 is equal', () => {
  expect(objectMatch(object4,object5)).toBe(true);
});
 PASS  ./index.test.js
  ✓ object1 and object2 is equal (2ms)
  ✓ object1 and object3 is not equal (1ms)
  ✓ object4 and object5 is equal (1ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        2.969s

通りました。

では、この関数をnpmパッケージにして公開します。
こちらの記事がとても分かりやすく、とても簡単に公開することができました!

公開したnpmパッケージについてはこちらから
githubについてはこちらから

スクリーンショット 2019-12-02 19.25.56.png

以上になります!

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

load関数の中身が動作しない時はこれを読んでほしい(JSの実行タイミングのはなし)

Ateam Lifestyle Advent Calendar 2019の17日目は、株式会社エイチームライフスタイルの @ryosuketter が担当します。

はじめに

本記事は、JavaScriptとjQueryを使用した場合のブラウザレンダリングの順序に関する内容を書いています。

「何を今さら、jQuery」と言うご意見もあるかもしれませんが、実際の現場では、jQueryを使うことで素早く実装できるシーンもあるため、現在も少なからず使われています。手軽に使えるjQueryではありますが、正しい理解していないとハマってしまうかもしれない内容でしたので、今回は備忘録として残しておきました。

問題

ある実装を完成させ、検証したら、Internet Explorer(IE)だけ、$(window).on (‘load’)の中に書いてあるスクリプトが読み込まれておらず、アタフタしてしまいました。

問題の1つは、自分が、jQueryの下のやつを無意識で使っていたことが原因でした。

$(function(){
  // ここにプログラムを記述
});

この記述は、jQueryでよく見かけるおまじない、ではなくて、ちゃんと意味があります。これは、「HTMLを読み込んでから処理を実行する」という意味で、画像などを除いて、HTML(DOM)の読み込みが終わったら、関数の中のスクリプトを実行するという意味です。

基本、jQueryはDOMを操作するための言語なので、それが全部読み込まれていないまま処理を実行すると正しく動作しない可能性があるので、大抵、上記の記述をまず書きます。自分は、無意識に書いていたので、ハマってしまいました。

ページ読み込み時のJavaScriptの実行タイミング

  • ページの読み込みが始まる
  • HTML(DOM)の読み込みが終わる
  • $(function(){ ここのプログラムが実行される }); ← いわゆるdocument-ready
  • 画像や動画など含めすべてのページにあるリソースが完全に読み込まれる
  • $(window).on('load', function(){ここのプログラムが実行される });

ハマった原因

自分はなんでハマったかとおいうと、下記のような記述をしてしまったからです。

$(function(){
  // htmlロード時の処理
  $(window).on('load', function(){
    // ページ全体のリソースが読み込まれた時の処理
  });
});

つまり、ready関数とload関数の処理の実行タイミング理解が曖昧だったのが原因ですね。

ready関数
$(function(){
  // htmlロード時の処理
});
load関数
$(window).on('load', function(){
  // ページ全体のリソースが読み込まれた時の処理
});

おそらく、HTML(DOM)の読み込みが終わって、中身を読み込む時に、画像などの読み込みが完了しておらず、ready関数($(function(){ ここのプログラムが実行される });)を出てしまい、実行されず終わってしまったのが原因かと思います。

ちなみに、これがjQuery2系だったら実行されていたかもしれませんが、自分は3系を使っていたので実行されませんでした。なぜなら、3系から、documentがreadyかどうかにかかわらず、処理の順序の一貫性を保証するようになったからです。

https://jquery.com/upgrade-guide/3.0/#breaking-change-document-ready-handlers-are-now-asynchronous

$(document).readyの中身

$(document).readyの中身をgithubにある、jQueryの中身(実態はJS)を見てみましょう。

https://github.com/jquery/jquery/blob/master/src/core/ready.js

一部、自分には難しい記述もありましたが、今回関係するのは、以下の記述ですね(下の方にあります)。

document.addEventListener( "DOMContentLoaded", completed );

これを見ると、DOMContentLoadedというイベントが発火したタイミングで、ready関数は実行されるようです。

DOMContentLoadedとは

MDNを見ると

The DOMContentLoaded event fires when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading.

Window: DOMContentLoaded event - Web APIs | MDN

これより、ready関数は、CSSや画像ファイルの読み込みを待たず実行していることが分かりますね。また、jQueryに限らず、新しいバージョンを使用する際は変更点をしっかり把握していないといけませんね。

参考

まとめ

Ateam Lifestyle Advent Calendar 2019の18日目は、 @seira さんがお送りします。

“挑戦”を大事にするエイチームグループでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
https://www.a-tm.co.jp/recruit/

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

javascriptについて質問です。

https://blog.codecamp.jp/javascript-geolocation
上記リンクにて位置情報取得について調べていたのですが、


getCurrentPosition()を呼び出すと、すぐに制御が返されます(次の文が実行される)。そして、位置情報の取得に成功すると、引数に渡した関数にPositionオブジェクトが渡されます。

という一文があるのですが、いまいち理解できません。。。。
どなたか解説お願いできないでしょうか。。。。

<!DOCTYPE html>





test


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

第一回 Backbone.jsのextendについて

本記事の目的

  • Backbone.jsのextendを理解する

Backbone.jsでは至る所で使用する

export default Backbone.Model.extend({
})
export default Backbone.Collection.extend({
})
export default Backbone.View.extend({
})

まずはModelのextendから

Backbone.Model を拡張し、独自のModelクラスを作成する機能です。
インスタンスプロパティとクラスプロパティをそれぞれ指定で引数可能です。

Backbone.Model.extend(properties, [classProperties])
// properties:インスタンスプロパティ
// classProperties: クラスプロパティ

参考:https://backbonejs.org/#Model-extend


extendの使用例

// Backbone.Modelを拡張したサブクラス(ExtendModel)
const ExtendModel = Backbone.Model.extend({
  idAttribute: 'id2',
  extendProp: 'Extend!!',
})

const model = new Backbone.Model()
const extendModel = new ExtendModel()

// ExtendModelはBackbone.Modelのプロパティが上書きされている
console.log(model.idAttribute)          // 'id'
console.log(extendModel.idAttribute)    // 'id2'

// モデルに存在しないプロパティはundefined
console.log(model.extendProp)           // undefined
console.log(extendModel.extendProp)     // 'Extend!!'

extendの使用例2

下記のようにサブクラスをさらに拡張することも可能です。

// Backbone.Modelを拡張したサブクラス(BaseModel)
const BaseModel = Backbone.Model.extend({
  baseProp: 'Base!!',
})
// BackModelを拡張したサブクラス(ExtendModel)
const ExtendModel = BaseModel.extend({
  baseProp: 'Base2',
  extendProp: 'Extend!!',
})

const baseModel = new BaseModel()
const extendModel = new ExtendModel()

// basePropは両方あるがExtendModelは上書きされている
console.log(baseModel.baseProp)      // 'Base!!'
console.log(extendModel.baseProp)    // 'Base2'

// extendPropはExtendModelのみ
console.log(baseModel.extendProp)    // undefined
console.log(extendModel.extendProp)  // 'Extend!!'

親オブジェクトの機能の呼び出し

下記の手順で親オブジェクトのメソッドを明示的に呼び出すこともできます。

const Note = Backbone.Model.extend({ 
  set: function (attributes, options) {
    Backbone.Model.prototype.set.apply(this, [attributes, options])
    ... 
  }
})

これにより、setやsaveなどのコア関数をオーバーライドし、拡張することも容易です。


extendとは

今回はModelのextendについて説明でしたが、基本的に他のモジュール(CollectionView)も同様です。
イメージとしては Java のクラスでいう継承にあたります。


まとめ

  • Backbone.Modelのextendとは
    Backbone.Model を拡張し、独自のModelクラスを作成する機能
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CircleCIを使ってテストを自動化する

はじめに

この記事は、North Detail Advent Calendar 2019 の10日目の記事です。

業務でテスト自動化が必要になったので調べてみたところ、
導入のハードルが思ったより低かったので備忘録として手順を書き残します:writing_hand:

今回は GitHubCircleCI を組み合わせてテストを自動化した環境を作ります。

CircleCI ?

性能、柔軟性、制御性を兼ね備えたプラットフォーム
https://circleci.com/ja/product/

CIツールの1つです:recycle:
実行OSやジョブの並列実行数など、機能に制限はありますが無料で利用することができます。

Freeプランで注意すべきはクレジット数でしょうか。
2,500クレジット/週の使用制限がかかっており、ジョブを実行することで消費されていきます。

クレジットの消費数はマシンによって異なりますが、
例えば Linux(Medium) のマシンを使用する場合は
ビルド時間1分あたり10クレジット消費となっているため、250分/週に制限されます。

準備

npm初期化 ~ Jestをインストール

自動化の前に、まずはテスト環境を構築します。
(ここではJestを使います)

$ npm init -y

質問をすっ飛ばして初期状態で作成します。

$ npm install jest

Jestをインストールし、package.jsonの "scripts.test" を以下のように変更します。

package.json
"scripts": {
  "test": "jest"
}

これで $ npm run test を叩いた際にJestが呼ばれるようになりました。

テスト対象のコードを作成

足し算するだけのjsファイルを作成してみます。

calc.js
function sum(x, y) {
  return x + y;
}

module.exports.sum = sum;

テストコードを作成

先ほど作成した足し算にテストコードを書きます。

calc.test.js
const calc = require('./calc.js');

test('Sum test', () => {
  expect(calc.sum(1, 2)).toBe(3);
});

テストを実行してみる

$ npm run test

 PASS  ./calc.test.js
  ✓ Sum test (4ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.527s
Ran all test suites.

無事に正常終了しました :ok_woman:

設定ファイルを作成

CircleCI用の設定ファイルを追加します。
今回は以下のような内容にしてみました。

  1. 該当ブランチをcheckout
  2. 必要なモジュールをインストール
  3. テスト実行
.circleci/config.yml
# CircleCIのバージョン
version: 2
jobs:
  build:
    # 使用するDockerイメージ
    docker:
      - image: circleci/node:8.12.0
    working_directory: ~/repo
    # 処理内容
    steps:
      - checkout
      - run:
          name: Setting npm
          command: npm install
      - run:
          name: Run test
          command: npm run test

GitHubへpush

画像のようなディレクトリ構成になっていればOK :ok_hand:
GitHubへpushして準備完了です。
1.png

いざCircleCI

CircleCIでプロジェクトを新規作成

先ほどpushしたリポジトリにアクセスできるユーザーでCircleCIへログインし、
サイドメニューから Add Projects をクリック。

リポジトリ一覧が表示されるので、処理したいリポジトリで「Set Up Project」をクリックします。
2.png

新規CircleCIプロジェクトの設定画面に遷移しました。

今回は以下のような設定にします。

  • Operating Systemを Linux
  • Languageを Node

3.png
設定ファイル(config.yml)の追加を求められますが、

すでに作成済みなので「Start building」をクリックしてビルドを開始します。
4.png

設定に問題が無ければ実行結果の画面に遷移します。
ローカルと同じ結果になっていますね。
5.png
これでCircleCIの設定は完了です。
今後このリポジトリに対して変更が入るたびに実行されます。

テストを失敗させてみる

今回はテストを正常終了させましたが、
失敗していた場合はどうなるでしょうか?

派生ブランチを作成し、テストコードを以下のように変更してみます。

calc.test.js
const calc = require('./calc.js');

test('Sum test', () => {
  expect(calc.sum(1,2)).toBe(300); // ここを変更
});

実行してみると、もちろん失敗します。

$ npm run test

 FAIL  ./calc.test.js
  ✕ Sum test (7ms)

  ● Sum test

    expect(received).toBe(expected) // Object.is equality

    Expected: 300
    Received: 3

      2 | 
      3 | test('Sum test', () => {
    > 4 |   expect(calc.sum(1,2)).toBe(300);
        |                         ^
      5 | });

      at Object.<anonymous> (calc.test.js:4:25)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        2.648s
Ran all test suites.

この状態でプルリクを投げてみましょう。
6.png
CircleCI と連携したことで GitHub のプルリク画面にジョブの実行結果が表示されています。
テストで失敗しているため、警告が表示されて「Merge pull request」ボタンが非活性(のような色)になっています。

Details をクリックしてエラー内容を確認してみましょう。
7.png
ローカルでの実行結果と同様にエラーが発生していました :ok_woman:

まとめ

GitHubCircleCI を組み合わせて、テストが自動化された環境を作りました。

自動化とか難しそう…と勝手なイメージから避けていたのですが、
実際にやってみると初心者でも簡単に導入できました :relaxed:

ユニットテストの他にも自動化できると便利そうなものはありますが、
実行時間が長くなると消費クレジットも増えていきます。
詰め込みすぎには注意です :warning:

明日は

「North Detail Advent Calendar」 11日目は @ooishi-hy さんです! :clap:

参考リンク

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

Prettierに関する小ネタ

はじめに

以下ではPrettierに関する小ネタをまとめる。

設定ファイルを楽に書く

Prettierは以下のいずれかの形式で設定する (Configuration File · Prettierより):

  • package.jsonファイル内の"prettier"キー。
  • JSONかYAMLで書かれた.prettierrcファイル。ファイル名に次の拡張子を付けることも可能: .json/.yaml/.yml
  • オブジェクトをエクスポートする.prettierrc.jsprettier.config.js
  • TOMLで書かれた.prettierrc.tomlファイル (.toml拡張子は必須) 。

Prettierのオプションに関しては以下にドキュメントが存在する:

Options · Prettier

それほど数は多くないためすべて目を通すことも大変ではないが、Prettierのウェブサイト内にはPlaygroundという、ビジュアルに設定ファイルを生成することができる機能があるので、手っ取り早く設定ファイルを作成したい場合は利用するといい。

Screenshot from 2019-12-02 02-45-00.png

エディタが左右に分割されており、左側のエディタがフォーマット前のコードを、右側のエディタがフォーマット後のコードをそれぞれ表わしている。タブのサイズやセミコロンの有無など、フォーマット時の設定を決めているのが一番左側にあるペインで、ここで各オプションを選択すると、すぐに右側のエディタにそのオプションを選択した場合のフォーマット結果が反映される。

フォーマット結果に満足できたら、最下部にある"Copy config JSON"をクリックすると、クリップボードに設定用のJSONがコピーされるので、あとは設定ファイルにペーストすればいい。

Playgroundを共有する機能などもあるため、チームで設定を議論するための場としても使うことができる。

eslint-config-prettierでESLintの不要なルールを取り除く

LinterとしてESLintを併用している人は多いと思うが、フォーマットに関するルールがPrettierの設定と衝突する。こうしたPrettierと競合するルールを無効化するためのライブラリに、eslint-config-prettierがある。

インストール方法:

$ npm install --save-dev eslint-config-prettier

ESLintの設定ファイルに以下を記述する:

{
  "extends": [
    "他の設定",
    "prettier"
  ]
}

これでESLintの不要なルールが無効化される。なお、他の設定をオーバーライドできるように、必ず配列の末尾に"prettier"を記入する必要がある。

Husky、lint-stagedとの連携

VS Codeなどで"Format On Save"を利用して保存時にフォーマットしている場合は別だが、そうした機能がないエディタを使用している場合に、フォーマットのコマンドを毎度入力することは面倒だ。他のメンバがフォーマットし忘れたコードをPRしてくる可能性もある。ところで、gitにはフック機能があり、これを利用して特定のgit操作時に任意のアクションを実行させることができるので、たとえば「リントがパスしない/フォーマットされていない」コードのコミットを弾くことなどができる。

フックを自前で書くことももちろん可能だが (なお、git init時に、フックのサンプルが自動的に.git/hooksに生成されるので、確認しておくといい)、Huskyを使えばフックがより簡単に設定できる。

Huskyをインストールすると、.git/hooksディレクトリにファイルが自動的に追加される:

$ ls .git/hooks      
applypatch-msg.sample  fsmonitor-watchman.sample  pre-applypatch.sample  prepare-commit-msg.sample  pre-rebase.sample   update.sample
commit-msg.sample      post-update.sample     pre-commit.sample  pre-push.sample        pre-receive.sample
$ npm install --save-dev husky
$ ls .git/hooks
applypatch-msg         fsmonitor-watchman.sample  post-merge    post-update.sample     pre-commit      prepare-commit-msg.sample  pre-rebase.sample   sendemail-validate
applypatch-msg.sample  post-applypatch        post-receive  pre-applypatch         pre-commit.sample   pre-push           pre-receive     update
commit-msg         post-checkout          post-rewrite  pre-applypatch.sample  pre-merge-commit    pre-push.sample        pre-receive.sample  update.sample
commit-msg.sample      post-commit        post-update   pre-auto-gc        prepare-commit-msg  pre-rebase             push-to-checkout

Huskyの設定は、package.jsonまたは.hukyrcにておこなう。たとえばコミット時にテストを実行したい場合は次のようにする:

// package.json
{
  "husky": {
    "hooks": {
      "pre-commit": "npm test",
    }
  }
}

あるいは

// .huskyrc
{
  "hooks": {
    "pre-commit": "npm test"
  }
}

これにより、コミット時にテストが実行され、テストが失敗した場合はコミットされないようになる。

Prettierの設定

ESLintやPrettierでチェックをおこなう場合は、package.jsonにあらかじめscriptsとしてコマンドを登録しておき、それをさらにhooksとして登録するのがいいだろう。たとえば:

// package.json
{
  "scripts": {
    "lint": "eslint .",
    "prettier": "prettier \"**/*.+(js|json)\"",
    "format": "npm run prettier -- --write",
    "check-format": "npm run prettier -- --list-different",
    "validate": "npm run lint && npm run check-format"
  }
  "husky": {
    "hooks": {
      "pre-commit": "npm run validate"
    }
  }
}

ここで、--list-differentはフォーマットされていないファイルがある場合、そのファイル名を出力してエラーコードを返す。これによりフックが止まってくれるため、フォーマットされていないファイルがある場合はコミットできないという仕組みになる。

なお、npm runの際に--を用いることで引数を指定することができるので、実際にフォーマットをおこなうformatと、チェックのみおこなうcheck-formatの重複部分をprettierとしてまとめている。

コミット時に自動フォーマットする

上の設定でフォーマットされていないコードを弾くことができるようになったが、上述したように、自動フォーマットされないエディタを使用している場合は毎度フォーマット用のコマンドを打ち込むことになり面倒だ。そこで、コミット時に自動的にコードをフォーマットすることを次に考える (これをしたいかどうかは人によって好みがあると思うが) 。

上のpre-commitの設定を素朴にnpm run formatに変更しても、フォーマット後のファイルがgit addされていないため期待通りの結果とならない。&&でコマンドをつなぐこともできるが、lint-stagedを使うとより簡単・効率的におこなうことができる。

lint-stagedは、変更された (git addされた) ファイルに対してのみリントをおこなうためのライブラリだ。これにより、リントの実行時間を短くし効率化できる。名前にはlintと書いてあるが、フォーマッタなど他のコマンドをパイプのようにつなぐこともできる。

インストール方法:

$ npm install --save-dev lint-staged

設定はpackage.jsonまたは.lintstagedrcにておこなう:

// package.json
{
  "lint-staged": {
    "*": "your-cmd"
  }
}

あるいは

// .lintstagedrc
{
  "*": "your-cmd"
}

のようにする。ここで、キーとなっている"*"は、コマンドの実行対象をglobで指定したものとなる。よって、たとえばJavaScriptとTypeScriptを対象とする場合は

"*.+(js|ts)": "your-cmd"

のようにする。stageされたファイルが"*.+(js|ts)"によってフィルタされ、その結果に対して"your-cmd"コマンドが実行されるという意味だ。"your-cmd"の部分は配列にして複数のコマンドを記述することもできる。

ESLintとPrettierを組み合わせる場合は次のように書く:

{
  "*.+(js)": [
    "eslint",
    "prettier --write",
    "git add"
  ]
}

また、設定内容をフックとして登録するためには、Huskyの設定ファイルを次のように変更する:

"hooks": {
  "pre-commit": "lint-staged"
}

これにより、まずフィルターされたstagedファイルに対してリントが実行され、次にフォーマットが実行され、最後にgit addされることとなる。ここまででエラーが発生しなければ、実際にコミットが実行される。

実行の順番をまとめると

commitを試みる -> pre-commit hookが呼ばれる -> lint-stagedが実行される -> 対象ファイルに対してリント、フォーマット、git addをおこなう -> 本commit

のようになる。

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

コピペで使うFirebase【Firestore ページング編】

公式ドキュメント

クエリカーソルを使用したデータのページ設定

概要

ページングのメモ

Firestoreを使用してページング実装する場合は、クエリーカーソルを使う。
orderByメソッドで指定されたフィールドで始まる新しいクエリを作成して返す。

メソッド一覧
メソッド 動作
startAt クエリの開始点(クエリーカーソルを含む)からのデータを返す
startAfter クエリの開始点(クエリーカーソルを含まない)からのデータを返す
endAt クエリの終着点(クエリーカーソルを含む)からのデータを返す
endBefore クエリの終着点(クエリーカーソルを含まない)からのデータを返す

サンプル

firestoreのコレクションは以下としいidを昇順で並べる

コレクション名: member
ドキュメント
{ id: 1, name: "taro" }
{ id: 2, name: "hana" }
{ id: 3, name: "aki" }
{ id: 4, name: "yuto" }
{ id: 5, name: "ryo" }

startAt

startAt
firebase
    .firestore()
    .collection('member')
    .orderBy('id', 'asc')
    .startAt(3)
    .get()
    .then(querySnapshot => {
        let memberList = []; 
        querySnapshot.forEach(doc => memberList.push(doc.data()));
        console.log(memberList);
        // [{ id: 3, name: "aki" }, { id: 4, name: "yuto" }, { id: 5, name: "ryo" }]
    })

startAfter

startAfter
firebase
    .firestore()
    .collection('mamber')
    .orderBy('id', 'asc')
    .startAfter(3)
    .get()
    .then(querySnapshot => {
        let memberList = []; 
        querySnapshot.forEach(doc => memberList.push(doc.data()));
        console.log(memberList);
        // [{ id: 4, name: "yuto" }, { id: 5, name: "ryo" }]
    })

endAt

endAt
firebase
    .firestore()
    .collection('mamber')
    .orderBy('id', 'asc')
    .endAt(3)
    .get()
    .then(querySnapshot => {
        let memberList = []; 
        querySnapshot.forEach(doc => memberList.push(doc.data()));
        console.log(memberList);
        // [{ id: 1, name: "taro" }, { id: 2, name: "hana" }, { id: 3, name: "aki" }]
    })

endBefore

endBefore
firebase
    .firestore()
    .collection('mamber')
    .orderBy('id', 'asc')
    .endBefore(3)
    .get()
    .then(querySnapshot => {
        let memberList = []; 
        querySnapshot.forEach(doc => memberList.push(doc.data()));
        console.log(memberList);
        // [{ id: 1, name: "taro" }, { id: 2, name: "hana" }, { id: 3, name: "aki" }]
    })

ドキュメント スナップショットを使用したサンプル

※ うまく動かない... ドキュメント見てる感じだと動くはず

firebase
    .firestore()
    .collection('member')
    .where('name', "==",'hana')
    .get()
    .then(documentSnapshot => {
        const cursor = documentSnapshot.docs[0].data();
        firebase
            .firestore()
            .collection('member')
            .orderBy('id', 'asc')
            .startAt(cursor)
            .get()
            .then(snapshot => {
                snapshot.forEach(doc => {
                console.log("doc", doc.data());
            })
        })
    });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.js のおかげで 10 年以上ぶりの Web 開発でも最先端に高速で到達した話

Nuxt.js Advent Calendar 20193 日目をお届けします。

言いたいこと

  • Nuxt.js いいよ!
  • フロントエンドあまり慣れていない人ほど使うと恩恵が大きい
  • みんなも触ってみよう:blush:

背景

なんやかんやありましてフロントエンドの開発を担当することになりました。
開発者の背景は以下な感じです。

  • 本格的な Web アプリケーション開発は 10 年以上ぶり
    • Struts + JSP、Ruby On Rails を少々という経験で時間が止まっている。
    • エディタは eclipse で HTML 要素をハイライトするプラグインを入れて書いていたような気がする。
  • スキップしてきたもの
    • HTML5 + CSS3
    • Ajax で非同期な API 呼び出し, JQuery
    • IE 対応
    • Babel? webpack?
  • 主に経験してきたこと
    • java による DataSpider の開発
      • C# も少々

フレームワークの検討

開発にあたってフレームワークの採用は必須だと考えていました。
御三家(Angular/React/Vue.js)の存在は知っていたので、そのあたりから何か選べればと調査していき、今回は Nuxt.js + Vuetify を採用しました。
Nuxt.js は Vue.js をベースにしたフレームワークです。

良かったところ

背景に対するカウンターとして Nuxt.js たちの良かったところをあげていきます

やるべきことが明確で最小限なので迷いが少ない

Struts + JSP、Ruby On Rails を少々という経験で時間が止まっている。

  • Nuxt.js のプロジェクトを作成すると、雛形として様々な設定が組み込まれたファイルツリーが構築されます。
  • Nuxt.js が用意してくれた流れに乗っていけば良いので、一番最初にどうすれば良いのか?と迷うことが少なかったです。
  • 画面を量産するにあたって pages 配下にディレクトリと .vue ファイルを配置していけば URL に対応するルーティングが自動で構成されるのも楽でした。
    • なので軌道に乗ってくると pages 以下のちょっとした修正で機能が追加可能です。 image.png

ソースコードの品質が保ちやすい

eclipse で HTML 要素をハイライトするプラグインを入れて書いていたような気がする。

  • 開発環境は Visual Studio Code が最高でした
  • プロジェクト作成時にソースコードの品質を保つための設定も組み込み済みなので、エディタの環境を整えれば正しい形に矯正してくれます。
  • 以下の拡張を使って、エディタ保存時に自動フォーマットする設定もしました。
    • vetur
    • EditorConfig for VS Code
    • Prettier
  • ついでにビルドツールは yarn を使っていて、npm よりも早くよかったです。
  • さらには husky を使ってコミット前に lint を実行していました。
package.json
  "husky": {
    "hooks": {
      "pre-commit": "yarn run lint"
    }
  },

Vuetify を使うことで UI 実装の大幅な時間短縮と一定の品質を確保

HTML5 + CSS3

  • UI 実装に採用した Vuetify がとても良かった
    • マテリアルデザインの登場と、その実装が使えることのありがたさ。
    • 大小コンポーネントが豊富で、今回はカレンダーが必要でサポートしていることも決め手になった。 image.png
  • レイアウト調整に関しては Grid system と Spacing で大体対応できた
  • CSS を書かなくてもある程度のことは実現できる

axios + async/await でバックエンドとの非同期通信が超簡単

Ajax で非同期な API 呼び出し, JQuery

  • HTTP クライアントに axios を使い async/await を使って非同期処理を書くことで、とても簡単に見通しよくバックエンドとの通信処理が書けました。
sample.vue
  // 新着イベント取得ページの例
  async asyncData({ $api }: Context): Promise<any> {
    // $api という plugin をインジェクションしている。関数の中で axios を使っている
    return {
      newEvents: await $api.getNewEvents()
    };
  },
  • 非同期処理でありがちなコールバック地獄も async/await 文法のおかげで解決
  • あとは取得した JSON をページテンプレートの v-for/v-if などで構築すれば DOM を複雑に操作する必要もなかった

IE 対応はあまり苦にならなかった

IE 対応

  • IE かどうかの if 文などは必要としませんでした
  • IE でサポートしていない JavaScript 文法を使った場合は?
    • ビルド時に Babel が変換(トランスパイル)してくれる
  • IE でサポートしていない関数を使った場合は?
    • polyfill.io で対応
nuxt.config.ts
  /*
   ** Headers of the page
   */
  head: {
    // ...
    script: [
      {
        src:
          'https://polyfill.io/v3/polyfill.min.js?features=es2017%2Ces2015%2CIntersectionObserver'
      }
    ]
  },
  • CSS の実装差異は?
    • そもそも CSS をほぼ書かなかった
    • Vuetify の v-menu コンポーネントで回避策が必要だったがそれぐらい

フロントエンドもビルドする時代だが既に整っていた

Babel? webpack?

  • Nuxt.js のプロジェクトを作った時点で既に設定済み
  • リリース時に yarn build でビルドすれば勝手にやってくれる
  • 今回デバッグ用に console.log を使っていたがビルド時の削除するなどのカスタマイズも可能
nuxt.config.ts
  /*
   ** Build configuration
   */
  build: {
    /*
     ** You can extend webpack config here
     */
    extend() {},
    terser: {
      terserOptions: {
        compress: {
          drop_console: process.env.NODE_ENV === 'production'
        }
      }
    }
  }
  • ゼロから設定していくのはとても大変だったと思う

まとめ

  • Nuxt.js の肩に乗っかることで最新のフロントエンド開発環境を得ることができました
  • 実例が少なく抽象的な内容になりがちでしたが Nuxt.js の良さが少しでも伝われば幸いです
    • 気になることあればコメントもお待ちしております
  • yarn create nuxt-app helloworld で簡単に体験できるのでとりあえず触ってみましょう!
  • しばらくは Nuxt.js/Vuetify 関連の情報を発信していくと思うので Qiita 等フォローをぜひ
  • なんやかんやの取り組みなど興味があれば個人としても組織としてもお気軽に連絡ください

明日 4 日目@ushironoko さんです!

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

Laravel+Vue.js+MySQLで入力内容の途中保存機能を実装してみた

グレンジ Advent Calendar 2019 4日目担当の soyo と申します。
グレンジでクライアントエンジニアをしております。
とはいえ、今年の記事もクライアントとはまったく関係ありません。

普段Googleフォームなどでアンケートを回答する際に、
「あれ、途中で保存することができないの?」って自分はたまに思います。

ユーザーが一項目ずつ入力したらサーバーに送信してデータベースに記録するから、
ページに再度アクセスしたら記録されている情報を自動的に反映するまで、
PHPを使って簡単に実装してみました。

目標

「ラジオボタンの選択内容」と「テキストの入力内容」を途中保存できるようにする
2.png

開発環境

  • macOS 10.14.6
  • PHP 7.3.8
  • Laravel 6.6.0
  • MySQL 8.0.18

フロントエンド

Vue.jsで入力内容の操作

今回の戦場はLaravelプロジェクトのwelcome画面にします。
まずはそこにVue.jsを導入して、ラジオボタン3つとテキストボックス1つを置きます。

resources/views/welcome.blade.php
...
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
...
<div id="app">
    ラジオボタン<br/>
    <input type="radio" value="1" v-model="radio">選択肢1<br/>
    <input type="radio" value="2" v-model="radio">選択肢2<br/>
    <input type="radio" value="3" v-model="radio">選択肢3<br/>
    <br/>
    テキスト入力<br/>
    <input type="text" v-model="text" placeholder="内容を入力">
    <br/>
</div>
...
public/js/main.js
const app = new Vue({
    el: '#app',
    data: {
        radio: '2',
        text: 'あいうえお'
    },
});

これでradiotextでラジオボタンとテキストボックスを操作することができます。
1.png

UUIDの作成と保存

javascriptで適当なUUIDを生成する方法がありまして、
生成したUUIDをJavaScript Cookieでローカルに保存するようにします。

resources/views/welcome.blade.php
...
<script src="https://cdn.jsdelivr.net/npm/js-cookie@beta/dist/js.cookie.min.js"></script>
...
public/js/main.js
const app = new Vue({
    el: '#app',
    data: {
        radio: '',
        text: '',
        uuid: ''
    },
    methods: {
        initUUID: function() {
            if (Cookies.get('uuid') !== undefined) {
                this.uuid = Cookies.get('uuid');
                return;
            }

            var d = new Date().getTime();
            var d2 = (performance && performance.now && (performance.now() * 1000)) || 0;
            this.uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                var r = Math.random() * 16;
                if (d > 0){
                    r = (d + r) % 16 | 0;
                    d = Math.floor(d / 16);
                } else {
                    r = (d2 + r) % 16 | 0;
                    d2 = Math.floor(d2 / 16);
                }
                return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
            });

            // とりあえず期限を10年にする
            Cookies.set('uuid', this.uuid, { expires: 3650 });
        }
    }
});

app.initUUID();

これで画面を開く度にcookieからuuidを取得し、存在しない場合は生成できるようになりました。

サーバーとの通信

サーバーとの通信はaxiosで行います。

resources/views/welcome.blade.php
...
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
...
public/js/main.js
const app = new Vue({
...
    methods: {
        saveData: function(key, value) {
            let postData = {
                'user_id': this.uuid,
                'key': key,
                'value': value
            };

            axios.post("/saveData", postData).then(response => {
                // 成功
            }).catch(error => {
                // 失敗
            });
        },

        loadData: function () {
            let postData = {
                'user_id': this.uuid
            };

            axios.post("/loadData", postData).then(response => {
                // 成功
            }).catch(error => {
                // 失敗
            });
        }
    }
});

送信する内容についてですが、
文字を入力する度に送信してしまうとサーバーに負荷をかける可能性がありますので、
今回は連続する入力を無視してくれるLodashdebounceで制御します。

resources/views/welcome.blade.php
...
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
...
<div id="app">
    ラジオボタン<br/>
    <input type="radio" value="1" v-model="radio" @click="isRadioSelecting = true">選択肢1<br/>
    <input type="radio" value="2" v-model="radio" @click="isRadioSelecting = true">選択肢2<br/>
    <input type="radio" value="3" v-model="radio" @click="isRadioSelecting = true">選択肢3<br/>
    <br/>
    テキスト入力<br/>
    <input type="text" v-model="text" @input="isTextTyping = true" placeholder="内容を入力">
    <br/>
</div>
...
public/js/main.js
const app = new Vue({
    el: '#app',
    data: {
        ...
        isTextTyping: false,
        isRadioSelecting: false,
        ...
    },
    watch: {
        radio: _.debounce(function() {
            this.isRadioSelecting = false;
        }, 1000),

        text: _.debounce(function() {
            this.isTextTyping = false;
        }, 2000),

        isRadioSelecting: function(selecting) {
            if (selecting) {
                return;
            }
            this.saveData('radio', this.radio);
        },

        isTextTyping: function(typing) {
            if (typing) {
                return;
            }
            this.saveData('text', this.text);
        },
    },
    ...
});

これでラジオボタンは選択停止後1秒、テキストボックスは入力停止後2秒からサーバーにデータを送るようになりました。

最後に、ステータスをわかるためにvue2-notifyを使ってプッシュ通知を表示させます。

resources/views/welcome.blade.php
...
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.4.0/index.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
...

使い方の例

this.$notify.info({
    title: '受信',
    message: '内容読み取り完了'
});

これで、フロントエンドの方は必要な機能を揃えました。
完成したコードはこの記事の最後にまとめております。

サーバーサイド

データベース構造

テストのため、すごくシンプルなテーブルを作ります。

+------------+
| database() |
+------------+
| vue_test   |
+------------+

+------------+
| TABLE_NAME |
+------------+
| user_input |
+------------+

+-------------+-----------+
| COLUMN_NAME | DATA_TYPE |
+-------------+-----------+
| id          | int       |
| user_id     | varchar   |
| radio       | int       |
| text        | varchar   |
+-------------+-----------+

リクエストデータ処理クラス

ユーザー入力内容をデータベースに書き込む・読み取り処理を行います。

app/Http/Controllers/UserInputController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class UserInputController extends Controller
{
    public function saveData(Request $request)
    {
        DB::table('user_input')->updateOrInsert(
            [
                'user_id' => $request->input('user_id')
            ],
            [
                $request->input('key') => $request->input('value')
            ]
        );
    }

    public function loadData(Request $request)
    {
        $user_id = $request->input('user_id');
        $data = [
            'result' => DB::table('user_input')->where('user_id', $user_id)->first()
        ];
        return $data;
    }
}

ルーティング

routes/web.php
...
Route::post('/saveData', 'UserInputController@saveData');
Route::post('/loadData', 'UserInputController@loadData');
...

コードまとめ

resources/views/welcome.blade.php
<!DOCTYPE html>
<html>
    <head>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/js-cookie@beta/dist/js.cookie.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.4.0/index.js"></script>
        <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
    </head>
    <body>
        <div id="app">
            ラジオボタン<br/>
            <input type="radio" value="1" v-model="radio" @click="isRadioSelecting = true">選択肢1<br/>
            <input type="radio" value="2" v-model="radio" @click="isRadioSelecting = true">選択肢2<br/>
            <input type="radio" value="3" v-model="radio" @click="isRadioSelecting = true">選択肢3<br/>
            <br/>
            テキスト入力<br/>
            <input type="text" v-model="text" @input="isTextTyping = true" placeholder="内容を入力">
            <br/>
        </div>
    </body>
    <script src="{{ asset('/js/main.js') }}"></script>
</html>

public/js/main.js
const app = new Vue({
    el: '#app',
    data: {
        radio: '',
        text: '',
        isTextTyping: false,
        isRadioSelecting: false,

        uuid: ''
    },
    watch: {
        radio: _.debounce(function() {
            this.isRadioSelecting = false;
        }, 1000),

        text: _.debounce(function() {
            this.isTextTyping = false;
        }, 2000),

        isRadioSelecting: function(selecting) {
            if (selecting) {
                return;
            }
            this.saveData('radio', this.radio, 'ラジオボタン');
        },

        isTextTyping: function(typing) {
            if (typing) {
                return;
            }
            this.saveData('text', this.text, 'テキスト入力');
        },
    },
    methods: {
        initUUID: function() {
            if (Cookies.get('uuid') !== undefined) {
                this.uuid = Cookies.get('uuid');
                return;
            }

            var d = new Date().getTime();
            var d2 = (performance && performance.now && (performance.now() * 1000)) || 0;
            this.uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                var r = Math.random() * 16;
                if (d > 0){
                    r = (d + r) % 16 | 0;
                    d = Math.floor(d / 16);
                } else {
                    r = (d2 + r) % 16 | 0;
                    d2 = Math.floor(d2 / 16);
                }
                return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
            });


            Cookies.set('uuid', this.uuid, { expires: 3650 });
        },

        saveData: function(key, value, description) {
            let postData = {
                'user_id': this.uuid,
                'key': key,
                'value': value
            };
            axios.post("/saveData", postData).then(response => {
                this.$notify.info({
                    title: '送信',
                    message: '内容保存済み:' + description
                });
            }).catch(error => {
                this.$notify.error({
                    title: '送信',
                    message: '送信に失敗しました'
                })
            });
        },

        loadData: function () {
            let postData = {
                'user_id': this.uuid
            };

            axios.post("/loadData", postData).then(response => {
                let data = response.data['result'];
                if (data == null) {
                    this.$notify.info({
                        title: '受信',
                        message: '新規ユーザー'
                    });
                    return;
                }

                this.radio = data['radio'];
                this.text = data['text'];
                this.$notify.info({
                    title: '受信',
                    message: '内容読み取り完了'
                });
            }).catch(error => {
                this.$notify.error({
                    title: '受信',
                    message: '受信に失敗しました'
                })
            });
        }
    }
});

app.initUUID();
app.loadData();
app/Http/Controllers/UserInputController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class UserInputController extends Controller
{
    public function saveData(Request $request)
    {
        DB::table('user_input')->updateOrInsert(
            [
                'user_id' => $request->input('user_id')
            ],
            [
                $request->input('key') => $request->input('value')
            ]
        );
    }

    public function loadData(Request $request)
    {
        $user_id = $request->input('user_id');
        $data = [
            'result' => DB::table('user_input')->where('user_id', $user_id)->first()
        ];
        return $data;
    }
}
routes/web.php
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Route::post('/saveData', 'UserInputController@saveData');
Route::post('/loadData', 'UserInputController@loadData');

最後に

Vue.jsが使いやすくて、サードパーティのライブラリもたくさんあって、
導入と実装がかなり楽でした(cocos2d-xとunityと比べるとねw)

また、項目を増やす度にテーブルにカラムを追加するのはさすがに面倒ですね。
その場合はテーブルのスキーマをユーザーID、項目ID、内容にして、
select文でユーザーIDと項目IDで検索して、その結果を処理して反映すればいいと思います。

ありがとうございました。

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

JavaScriptのテスト用ツールやライブラリの分類

いっぱいありすぎてわけわからなくなりがちなので整理のため

テストツールタイプ

タイプ 役割 ツール例
テストランナー Node.js(cli)やブラウザ上でテストを実行・ウォッチするためのツール Karma(ブラウザ), Mocha(cli), Jasmine(cli), Jest(cli), TestCafe(ブラウザ), Cypress(ブラウザ)
テストフレームワーク 構造的にテストを書くためのライブラリ Mocha, Jasmine, Jest, Cucumber, TestCafe, Cypress, Ava, tape
アサーション テストが返す結果が期待どおりかどうかを確認するためのライブラリ Chai, Jasmine, Jest, Unexpected, TestCafe, Cypress
モック、スパイ、スタブ いわゆるモック。例えばAPIの呼び出しなどの副作用がある部分を分離しそれらを置き換えるためのライブラリ Sinon, Jasmine, enzyme, Jest, testdouble
コードカバレッジ 書いたテストでどのくらいのコードを網羅できているかレポートするツール Istanbul, Jest, Blanket
ブラウザコントローラ シミュレータを利用して機能テストを行うためのツール Nightwatch, Nightmare, Phantom, Puppeteer, TestCafe, Cypress
ビジュアル回帰ツール 画像を使って前回のバージョンのものと比較して変更点を検知するためのツール Applitools、Percy、Wraith、WebdriverCSS

参考

https://medium.com/welldone-software/an-overview-of-javascript-testing-in-2019-264e19514d0a

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

頑張って書きます。

未定です

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

jsで現在時刻と指定時刻を比較して、要素の表示非表示したい

新着情報とか、日付が来たら表示させたい

例えばこんな html

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>新着一覧</title>
</head>
<body>
    <ul>
        <li id="__target" data-showDate="2019/12/2" style="display:none;">項目1</li>
        <li>項目2</li>
        <li>項目3</li>
    </ul>
</body>
</html>

li[id="__target"] に data-showDate="この日付け以降は表示させる" と style="display:none;" の属性値を付加。

こんなjs

//-----------------------------------
// 現在時刻をUNIXタイムで取得
//-----------------------------------
let _now  = new Date(),
    now_y = _now.getFullYear(),
    now_m = _now.getMonth() + 1,
    now_d = _now.getDate(),
    now_h = _now.getHours(),
    now_i = _now.getMinutes(),
    now_s = _now.getSeconds();
let now = now_y + '/' + now_m + '/' + now_d + ' ' + now_h + ':' + now_i + ':' + now_s;
// UNIXタイムスタンプで取得
let ux_now = Date.parse(now.replace( /-/g, '/')) / 1000;

//-----------------------------------
// 指定日付をUNIXタイムで取得
//-----------------------------------
let _showDate = document.getElementById('__target').getAttribute('data-showdate').split('/');
let showDate = _showDate[0] + '/' + _showDate[1] + '/' + _showDate[2] + ' 0:0:0';

// UNIXタイムスタンプで取得
let ux_showDate = Date.parse(showDate.replace( /-/g, '/')) / 1000;

// アクセス現在時刻が指定時刻を超えてたらSHOW
if ( ux_showDate < ux_now ) {
    document.getElementById('__target').style.display = '';
}  

応急処置的な時に使う。UNIXタイムスタンプ取得するとこ関数にしたほうが良いにきまってる。

当然 documentロード後じゃないとDOM取得できないので、__target読み込み後に実行。
data-showDate の日付を動かしてみて。

jsFiddle
https://jsfiddle.net/380mi/8rus3yv5/

Qiitaの使い方がこれでいいのかわからない

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

JSConfJP 2019 スライドまとめ(WIP)

行けなかったので行きたい気持ちの供養のためにまとめています。
リンクされていないスライドを見つけたらリクエストいただけると幸いです。

DAY 1 (2019/11/30)

Room A(体育館) Room B(B105) Room C(屋上)
12:00
-
13:00
Open
13:00
-
13:30
Opening talk
13:30
-
14:00
The State of JavaScript
by Raphaël Benitte and Sacha Greif
14:00
-
14:15
Break
14:15
-
14:45
「繋がり」の可視化
by Nadieh Bremer
WebAuthnで実現する安全・快適なログイン
by Eiji Kitamura / えーじ
JavaScript AST プログラミング: 入門とその1歩先へ
by Takuto Wada
14:45
-
15:15
「オープンソース」の定義
by Henry Zhu
覚醒するアクセシビリティ
by Lena Morita
4年分のプロシージャルなJS
by Andy Hall
15:15
-
15:30
Break
15:30
-
16:00
Building and Deploying for the Modern Web with JAMstack
by Guillermo Rauch
Wrap-up: Runtime-friendly JavaScript
by Sho Miyamoto
JS開発者のためのSEOテクニック
by Martin Splitt
16:00
-
16:30
Write What Not How
by Jorge Bucaran
How to Boost Your Code with WebAssembly
by FUJI Goro
Playing Pokémon Together with Node.js
by Samuel Agnew
16:30
-
16:45
Break
16:45
-
17:15
Deno - JavaScript の新たな道筋
by Kitson Kelly
Streams APIをちゃんと理解する
by 加藤 健志
You might also like...
by Maria Clara
17:15
-
17:45
Headers for Hackers
by Andrew Betts
Make it Declarative with React
by Toru Kobayashi
Web Accessibilityのすゝめ
by Nazanin Delam
17:45
-
18:15
Break
18:15
-
18:45
Sponsor talk (mediba, メルカリ, サイボウズ, CureApp)
18:45
-
18:55
予測的 Prefetching によるパフォーマンス改善
by Praveen Yedidi
18:55
-
19:05
Web Components era phase 2
by Yoshiki Shibukawa
19:05
-
19:15
Cache Me If You Can
by Maxi Ferreira
19:15
-
19:25
Node.js でつくる Node.js - WASM/WASI ミニミニコンパイラー
by がねこまさし
19:25
-
19:35
React アプリのライセンス違反について
by dynamis
19:35
-
21:00
Party

DAY 2 (2019/12/01)

Room A(体育館) Room B(B105) Room C(屋上)
10:00
-
11:00
Open
11:00
-
11:30
時間はただの幻想である… JavaScriptにおいては
by Jennifer Wong
InversifyJSを用いたレイヤードアーキテクチャの構築
by 奥野 賢太郎
Vue.js で D3.js を使ったインタラクティブなデータの可視化
by Shirley Wu
11:30
-
12:00
ストリームの人生
by Dominic Tarr
Build and scale multiple Voice application
by using TypeScript

by Hidetaka Okamoto
12:00
-
13:00
Break
11:00
-
13:00
Web の自重
by Jxck
正攻法はあるのか !? 泥臭く戦った Node.js バージョンアップ一部始終
by Masato Nishihara
パスワードは90年代の代物だ
by Sam Bellen
13:30
-
14:00
JavaScript, Rust and Wasm Walk into a Ramen Shop ...
by Irina Shestak
Migration from React Native to PWA
by ohbarye
npm i -g @next-and-beyond: Building the future of package management
by Claudia Hernández
14:00
-
14:15
Break
14:15
-
14:45
JSConf Panel Talks
by Jan Lehnardt and Lena Morita and Mariko Kosaka and Yosuke Furukawa
GraphQLを用いたECサイトにおけるパフォーマンス改善
by 澤井宣彦
Minimum Hands-on Node.js
by 栗山 太希
14:45
-
15:15
悪用された npm パッケージの分析
by Jarrod Overson
JavaScriptのままでTypeScriptを始める
by 高梨ギンペイ
15:15
-
15:30
Break
15:30
-
16:00
Browser APIs: 知られざるヒーロー達
by Rowdy Rabouw
Your benchmark may not guide a real application performance
by Tetsuharu OHZEKI
16:00
-
16:30
Anatomy of a Click
by Benjamin Gruenbaum
JavaScriptとSwift/JavaをつなげるCapacitorと、これからのWeb Frontned.
by 榊原昌彦
Recruit Speed Hackathon
by 新井 智士
16:30
-
16:45
Break
16:45
-
17:15
AIとJavaScript による生物認識
by Jonny Kalambay
大規模アプリケーション開発でのElm実践
by 海老原 圭吾
17:15
-
17:45
Pika: レジストリの再創造
by Fred K. Schott
最新のWeb技術でIoT開発をする
by 木戸 康平
17:45
-
18:00
Break
18:00
-
18:30
Sponsor talk Yahoo! Japan, リクルート, ドワンゴ, DMM, Twilio
18:30
-
19:00
points at random
by Mariko Kosaka
19:00
-
19:30
Closing

Gist

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

React で VRM モデルを表示する方法

本エントリは金沢工業大学の学生が書く KIT Developer Advent Calendar の3日目です。
English Version: How to display 3D humanoid avatar with React - dev.to

はじめに

3DCG や VR の技術は様々な場所で用いられ、私たちにとって身近なものになりました。そして Web ブラウザ上でも同じような現象が起きています。今回は VRM と React や @pixiv/three-vrm を用いてどのように VRM を表示するのか紹介します。

VRM とは?

VRM は VR アプリケーション向けの人型 3D アバター (3D モデル) データを扱うためのファイルフォーマットです。VRM に準拠したアバターを持っていれば、3D アバターが必要な様々なアプリケーションを楽しむことが出来ます。

@pixiv/three-vrm とは?

@pixiv/three-vrm は Three.js で VRM を使うための JavaScript ライブラリです。これを使えば VRoid Hub のように Web アプリケーションでも VRM を表示することが出来ます。

VRoid Hub

※ 本エントリで利用している VRM モデルは製作者から許可を得ています

VRM の準備

まずはじめに、VRoid Hub から VRM をダウンロードする必要があります。

  1. タグで VRM モデルを検索
  2. お気に入りのモデルを選択
  3. モデルのページに移動して「このモデルを利用する」をクリックしてダウンロード

プロジェクトのセットアップ

$ npx create-react-app three-vrm-sample
$ cd three-vrm-sample/
$ yarn add @pixiv/three-vrm three react-three-fiber
index.html
<!DOCTYPE html>
<html>
  <head>
    <title>@pixiv/three-vrm sample</title>
    <style>
      html,
      body {
        background-color: #000;
        color: #fff;
        margin: 0;
        width: 100vw;
        height: 100vh;
      }

      #root {
        width: 100%;
        height: 100%;
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
index.js
import React from 'react'
import ReactDOM from 'react-dom'

const App = () => null

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

VRM のローダーを追加する

VRM は GLTF と似たフォーマットなので、Three.js に組み込まれている GLTFLoader で読み込むことが出来ます。
この処理は <App /> コンポーネント内に直接記述しても良いのですが、関心を分離するために Custom Hook にしました。

余談ですが、個人的には「use○○○」と命名できそうなものは積極的に Custom Hook に切り分けるようにしています。コードがスッキリしたり、テストしやすくなったりするだけではなくて、何をしているのか明示的に表現できるのが好きです。

import { VRM } from '@pixiv/three-vrm'
import { useRef, useState } from 'react'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'

const useVrm = () => {
  const { current: loader } = useRef(new GLTFLoader())
  const [vrm, setVrm] = useState(null)

  const loadVrm = url => {
    loader.load(url, async gltf => {
      const vrm = await VRM.from(gltf)
      setVrm(vrm)
    })
  }

  return { vrm, loadVrm }
}

react-three-fiber で VRM を表示する

react-three-fiber は react-spring がメンテナンスしている React で Three.js をより簡単に扱うためのライブラリです。普通に Three.js を書いても良いのですが、これを使うことで Three.js を宣言的に扱うことが出来るというメリットがあります。今回は、以下の3つのエレメントを使用します。

  • <Canvas>: react-three-fiber のエレメントのラッパー
  • <spotLight>: オブジェクトを照らすライトのエレメント
  • <primitive>: 3D オブジェクトのエレメント

また、VRM ファイルを追加すると handleFileChange() がファイルの URL を生成して VRM を読み込むようになっています。

import React from 'react'
import { Canvas } from 'react-three-fiber'
import * as THREE from 'three'

const App = () => {
  const { vrm, loadVrm } = useVrm()

  const handleFileChange = event => {
    const url = URL.createObjectURL(event.target.files[0])
    loadVrm(url)
  }

  return (
    <>
      <input type="file" accept=".vrm" onChange={handleFileChange} />
      <Canvas>
        <spotLight position={[0, 0, 50]} />
        {vrm && <primitive object={vrm.scene} />}
      </Canvas>
    </>
  )
}

結果:

Result

見た目をいい感じにする

表示されている VRM モデルは小さくて向こう側を向いています。もっと拡大して、こっちを向いてもらいましょう。

1. 新しいカメラを追加する

Note:
useThree() を使うと glscenecameraclock のような Three.js の基本的なオブジェクト全てを利用できます。

import React, { useEffect, useRef } from 'react'
import { useThree, Canvas } from 'react-three-fiber'
import * as THREE from 'three'

const App = () => {
  const { aspect } = useThree()
  const { current: camera } = useRef(new THREE.PerspectiveCamera(30, aspect, 0.01, 20))
  const { vrm, loadVrm } = useVrm()

  const handleFileChange = event => {
    const url = URL.createObjectURL(event.target.files[0])
    loadVrm(url)
  }

  // Set camera position
  useEffect(() => {
    camera.position.set(0, 0.6, 4)
  }, [camera])

  return (
    <>
      <input type="file" accept=".vrm" onChange={handleFileChange} />
      <Canvas camera={camera}>
        <spotLight position={[0, 0, 50]} />
        {vrm && <primitive object={vrm.scene} />}
      </Canvas>
    </>
  )
}

2. モデルを回転させてカメラを見てもらう

cameravrm.lookAt に代入して、さらに vrm を180°回転させます。
ここで利用している camera は 1 で追加されたカメラと同じものです。

import { VRM } from '@pixiv/three-vrm'
import { useEffect, useRef, useState } from 'react'
import { useThree } from 'react-three-fiber'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'

const useVrm = () => {
  const { camera } = useThree()
  const { current: loader } = useRef(new GLTFLoader())
  const [vrm, setVrm] = useState(null)

  const loadVrm = url => {
    loader.load(url, async gltf => {
      const vrm = await VRM.from(gltf)
      vrm.scene.rotation.y = Math.PI
      setVrm(vrm)
    })
  }

  // Look at camera
  useEffect(() => {
    if (!vrm || !vrm.lookAt) return
    vrm.lookAt.target = camera
  }, [camera, vrm])

  return { vrm, loadVrm }
}

最終的なコード:

index.js
import { VRM } from '@pixiv/three-vrm'
import ReactDOM from 'react-dom'
import React, { useEffect, useRef, useState } from 'react'
import { useThree, Canvas } from 'react-three-fiber'
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'

const useVrm = () => {
  const { camera } = useThree()
  const { current: loader } = useRef(new GLTFLoader())
  const [vrm, setVrm] = useState(null)

  const loadVrm = url => {
    loader.load(url, async gltf => {
      const vrm = await VRM.from(gltf)
      vrm.scene.rotation.y = Math.PI
      setVrm(vrm)
    })
  }

  // Look at camera
  useEffect(() => {
    if (!vrm || !vrm.lookAt) return
    vrm.lookAt.target = camera
  }, [camera, vrm])

  return { vrm, loadVrm }
}

const App = () => {
  const { aspect } = useThree()
  const { current: camera } = useRef(new THREE.PerspectiveCamera(30, aspect, 0.01, 20))
  const { vrm, loadVrm } = useVrm()

  const handleFileChange = event => {
    const url = URL.createObjectURL(event.target.files[0])
    loadVrm(url)
  }

  // Set camera position
  useEffect(() => {
    camera.position.set(0, 0.6, 4)
  }, [camera])

  return (
    <>
      <input type="file" accept=".vrm" onChange={handleFileChange} />
      <Canvas camera={camera}>
        <spotLight position={[0, 0, 50]} />
        {vrm && <primitive object={vrm.scene} />}
      </Canvas>
    </>
  )
}

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

結果:

Result

いい感じですね。

おわりに

VRM は今後より広い場面で利用されることが予測されます。読者が React で VRM を扱う場面でこの記事が助けになれば嬉しいです。また @pixiv/three-vrm はもっと多くの機能を持っているので、もし興味があればドキュメントを読んで試してみてください。
最後になりますが、もし問題点や質問があればコメントや僕の Twitter アカウントで伝えていただけると幸いです。

Sample Repository: saitoeku3/three-vrm-sample

5日目は @arakappa さんが Expo の Camera と Firebase Storage について書かれるのでお楽しみに! (4日目は埋まらなかった…)

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

HTMLコーディングで良く使うwebサービス様

HTMLのコード整形ツール

はちゃめちゃ大混乱なHTMLを治す時。<button>とか<a>はインデントされないので、その辺は手動で調整。
https://lab.syncer.jp/Tool/HTML-PrettyPrint/

Base64エンコーダー

Emmetでもできるけど、プロジェクト内に画像を移動するのすら面倒な時にここでエンコードしてそのままcssにぶち込む。
https://lab.syncer.jp/Tool/Base64-encode/

beforeやafter疑似要素のcontentプロパティで日本語の文字化けを回避する方法

cssの content:""; 内で文字化け防ぐ用。
https://webllica.com/css-content-property-mojibake/

CSS三角形作成ツール

吹き出しパーツデザイン用。
http://apps.eky.hk/css-triangle-generator/ja

SVGデータをCSSのbackground-image向けのBase64コードにかんたん変換ツール

background-imageにSVG使いたいよ
https://blog.s0014.com/posts/2017-01-19-il-to-svg/

CSS3のtransition-timing-functionの値、cubic-bezier()に関して

jquery Easingとか使ってる時、CSS側でもアニメーション揃えたい時
http://www.knockknock.jp/archives/184

Placehold.jp

ダミー画像とりあえず入れたいよ
http://placehold.jp/

HTMLタグ除去

テキストだけ抜いてサイトの調査とかしたいよ
https://crocro.com/tools/item/del_html_tag.html

Geocoding - 住所から緯度経度を検索

場所の緯度経度確認したいとき
https://www.geocoding.jp/

正規表現

どれが何やったっけなるとき
http://shanabrian.com/web/regular_expression.php

You might not need jQuery

いい加減脱jqueryしたいとき
http://youmightnotneedjquery.com/

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

git add -pした後のpre-commit lint-stagedが失敗する

症状

git add -pなどして「partially staged」した状態からgit commit経由でlint-stagedをすると、エラーになった。

$ git commit
husky > pre-commit (node v11.13.0)
Stashing changes... [started]
Stashing changes... [completed]
Running tasks... [started]
Running tasks for *.js [started]
eslint --fix [started]
eslint --fix [completed]
git add [started]
git add [completed]
Running tasks for *.js [completed]
Running tasks... [completed]
Updating stash... [started]
Updating stash... [completed]
Restoring local changes... [started]
Restoring local changes... [failed]
→ Command failed with exit code 128 (Unknown system error -128): git checkout-index -a
Command failed with exit code 128 (Unknown system error -128): git checkout-index -af
husky > pre-commit hook failed (add --no-verify to bypass)

これが起こると、lintが異常終了していて以下の状態になる。

  • add -pして分け分けしたdiffを持つファイルが全部add状態になる
  • 一部のファイルがworkdirからなくなる (たまたま作業中のファイルに当たると悲惨)

解決策

lint-staged@9.5.0晴れて修正されているそうなので、それにアップデートする。

npm install --save lint-staged@9.5.0

参考

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

テンプレートリテラルを本当にテンプレートエンジンっぽく使うサンプル

テンプレートリテラルなるものがある

Perl、PHP、Rubyに代表されるスクリプト言語に比べると、初期のJavaSciptは以下の機能がなかったのが辛かった。

  • ヒアドキュメント
  • 変数展開

ES2015 / ES6 になってからは、テンプレートリテラルという構文ができたので、もう大丈夫。

実行結果

image.png

ソースコード

teml.html
<html>

<script type="text/javascript">

// 静的なサンプル
function tab1(){
return  `
    <table border=1>
    <tr>
    <td>ああ</td>
    </tr>
    </table>
 `;
}

// 文字列変数を展開する
function tab2(mes){
return  `
    <table border=1>
    <tr>
    <td>${mes}</td>
    </tr>
    </table>
 `;
}

//配列変数の中でメソッドを使ってみる(配列を文字列に)
function tab3(ary){
return  `
    <table border=1>
    <tr>
    <td>${ary.join('|')}</td>
    </tr>
    </table>
 `;
}

//変数展開の中のメソッドの中で変数展開をする
function tab4(ary){
return `
    <table border=1>
    <tr>
    ${
    ary.map( item => `<td>${item}</td>`).join('')
    }
    </tr>
    </table>
 `;
}
</script>

<div id="main"></div>

<script type="text/javascript">
let ele = document.querySelector("#main");
ele.insertAdjacentHTML('beforeend',tab1());
ele.insertAdjacentHTML('beforeend',tab2('hello'));
ele.insertAdjacentHTML('beforeend',tab3(['a','b','c']));
ele.insertAdjacentHTML('beforeend',tab4(['a','b','c']));
</script>
</html>

こころ残り

HTMLテンプレートとして使うなら、htmlエスケープもしたいところ。

  • h()という関数を作って都度エスケープする(←ダサい。忘れそう)
  • ${}自体を上書きしちゃう(← 変数の中にhtmlタグが登場することもある)
  • テンプレートに渡す前の変数や配列やハッシュの値を全部エスケープする
  • コメントで htsign さんからいただいた「テンプレートリテラルに関数をかませる」(← 読みやすい)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

05. n-gram

05. n-gram

リストなど)からn-gramを作る関数を作成せよ.この関数を用い,"I am an NLPer"という文から単語bi-gram,文字bi-gramを得よ.

Go

package main

import (
    "fmt"
    "strings"
)

//  文字単位の bi-gram
func ch_bi_gram(src string) []string {
    var res []string

    //  文字数ループ(1文字少なく)
    for i:=0 ; i<(len(src)-1);i++ {
        //  添字の文字と次の文字を連結
        res = append(res, string(src[i]) + string(src[i+1]))
    }

    return res
}

//  単語単位の bi-gram
func wd_bi_gram(src string) []string {
    var res []string

    //  単語単位で文字列を分割
    words := strings.Split(src, " ")

    //  単語数ループ(1配列少なく)
    for i:=0 ; i<(len(words)-1);i++ {
        //  空白で単語を区切ったため、空白を追加し単語を連結
        res = append(res, words[i] + " " + words[i+1])
    }

    return res
}

func main() {
    src := "I am an NLPer"

    //  文字単位の bi-gram
    res1 := ch_bi_gram(src)
    fmt.Printf("%q\n",res1)

    //  単語単位の bi-gram
    res2 := wd_bi_gram(src)
    fmt.Printf("%q\n",res2)
}

python

# -*- coding: utf-8 -*-

#   文字単位の bi-gram
def ch_bi_gram(src):
    res = list(range(0))

    #   文字数ループ(1文字少なく)
    for i in range(len(src)-1):
        #   添字の文字と次の文字を連結
        res.append(src[i] + src[i+1])

    return res

#   単語単位の bi-gram
def wd_bi_gram(src):
    res = list(range(0))

    #   単語単位で文字列を分割
    words = src.split(" ")

    #   単語数ループ(1配列少なく)
    for i in range(len(words)-1):
        #   空白で単語を区切ったため、空白を追加し単語を連結
        res.append(words[i] + " " + words[i+1])

    return res


src = "I am an NLPer"

#   文字単位の bi-gram
res = ch_bi_gram(src)
print res

#   単語単位の bi-gram
res = wd_bi_gram(src)
print res

Javascript

//  文字単位の bi-gram
function ch_bi_gram(src) {
    var res = [];

    //  文字数ループ(1文字少なく)
    for (var i = 0; i < (src.length-1); i++) {
        //  添字の文字と次の文字を連結
        res.push(src[i] + src[i+1]);
    }

    return res;
}

//  単語単位の bi-gram
function wd_bi_gram(src) {
    var res = [];

    //  単語単位で文字列を分割
    var words = src.split(' ');

    //  単語数ループ(1配列少なく)
    for (var i=0;i<(words.length-1);i++) {
        //  空白で単語を区切ったため、空白を追加し単語を連結
        res.push(words[i] + " " + words[i+1]);
    }

    return res;
}

var src = "I am an NLPer"

//  文字単位の bi-gram
var chr = ch_bi_gram(src);
console.log(chr);

//  単語単位の bi-gram
var wd = wd_bi_gram(src);
console.log(wd);

まとめ

次の「05. n-gram」 は他の方の解説を読むが難しい。
[n番煎じ] 言語処理100本ノック 2015 第1章 with Python に結果が載ってた。

文字列(入力): I am an NLPer
文字バイグラム: ['I ', ' a', 'am', 'm ', ' a', 'an', 'n ', ' N', 'NL', 'LP', 'Pe', 'er']
単語バイグラム: ['I am', 'am an', 'an NLPer']

解りやすい。文章解読能力が無いことを痛感。

気になってる点。文章を空白で分割したがその空白を後で結合している部分。
これでいいのかなぁ。なんかカッコよくない気がする。

トップ

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

Mattermostでリマインダーを作ってみたよ

この記事は、富士通システムズウェブテクノロジー Advent Calendarの3日目の記事です。
(お約束)本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。

はじめに

MattermostにはSlackと違い、リマインダーが標準で用意されていない。
社内でリマインダー機能が欲しいって意見があったので、「リマインダーくらい直ぐ作ってるよー」って意気込みで作ってみました。
※Slackと同じ使い勝手のリマインダーが一応プラグインとして提供されてるらしい。

用意するもの

  • Node-RED
  • SQLServer
    ※データベースを使ってますが、ファイルでも代用可能です。

処理について

登録処理:Mattermostのスラッシュコマンドからリマインダーを登録する。

  1. ユーザーはスラッシュコマンドを入力する。
    コマンド:/reminder
  2. BOTはリマインダー登録ダイアログを作成しJSON形式でMattermostに送信する。
  3. ユーザーはダイアログにリマインダーの登録したい情報を入力し、ボタンを押す
  4. BOTは入力内容を受け取り、データベースに登録し、登録結果をMattermostにJSON形式で送信する。

通知処理:指定した日時になったらMattermostに通知を投げる。

  1. BOTは毎分データベースにDELETE_FLGはOFFのリマインダー情報をSELECT文で受け取る。
  2. SELECT文実行結果を1行ずつ読み込み、ユーザーの指定した日時が現在日時と一致した場合、Mattermostにリマインダー情報をJSON形式で送信する。

登録処理

Mattermostに表示されるダイアログは以下のとおりである。ここにリマインダー情報を入力し、DBに登録される。

ダイアログ

image.png

テキストボックスに日時のサブタイプがない為、yyyyMMddhhmm形式でゴリ押しで作成した。
とりあえず、日付と時間のところは現在の日時を入れ、必要がある人だけが書き換えれば良いという風にした。

ソースコード
var dt = new Date();
var message_help_text = "メッセージは本ダイアログ呼出し時のチャンネルにリマインドされます。" +
                        "プライベートチャンネルにはリマインド出来ません。" +
                        "Markdown記法に対応しています。" +
                        "メンションをメッセージに含ませると、指定したメンション先に通知が来るようになります。";
msg.payload = {
    url:"http://**.**.**.**:8888/reminder2",
    channel_id:msg.payload.channel_id,
    trigger_id:msg.payload.trigger_id,
    dialog: {
        title: "開発用リマインダー設定(公開チャンネルのみ)",
        submit_label: "SET",
        state:msg.payload.response_url + "," + msg.payload.channel_name + "," + msg.payload.user_name,
        elements: [
            {
                display_name: "メッセージ",
                name: "MESSAGE",
                type: "textarea",
                default:msg.payload.text,
                placeholder: "担当者は顧客に提示報告メールを送信してください。",
                help_text:message_help_text
            },
            {
                display_name: "繰り返し",
                name: "INTERVAL",
                type: "select",
                options: [
                    {
                        text: "しない",
                        value: "None"
                    },
                    {
                        text: "平日",
                        value: "Weekdays"
                    },
                    {
                        text: "休日",
                        value: "Holiday"
                    },
                    {
                        text: "毎日",
                        value: "EveryDay"
                    },
                    {
                        text: "毎週",
                        value: "EveryWeek"
                    },
                    {
                        text: "毎月",
                        value: "Monthly"
                    },
                ]
            },
            {
                display_name: "開始日付(yyyyMMdd)",
                name: "TARGET_DATE",
                type: "text",
                max_length:8,
                min_length:8,
                default:createDate(),
                placeholder:"yyyyMMdd",
                help_text:"デフォルト値:ダイアログ表示時の日付"
            },
            {
                display_name: "指定時刻(hhmm)",
                name: "TARGET_TIME",
                type: "text",
                max_length:4,
                min_length:4,
                default:createTime(),
                placeholder:"hhmm",
                help_text:"デフォルト値:ダイアログ表示時の時刻"
            }
        ],
    }
}
return msg;

function createDate()
{
    var format = String(dt.getFullYear());
    if(String((dt.getMonth() + 1)).length === 1) {
        format += "0" +  String((dt.getMonth() + 1));
    } else {
        format += String((dt.getMonth() + 1));
    }

    if(String(dt.getDate()).length === 1) {
        format += "0" +  String(dt.getDate());
    } else {
        format += String(dt.getDate());
    }
    return format;
}

function createTime()
{
    var format = null;
    if(String(dt.getHours()).length === 1) {
        format = "0" + dt.getHours(); 
    } else {
        format = String(dt.getHours());
    }

    if(String(dt.getMinutes()).length === 1) {
        format += "0" +  String(dt.getMinutes());
    } else {
        format += String(dt.getMinutes());
    }
    return format;
}

ダイアログで必要事項を入力し「SET」ボタンを押すと、コマンドを実行したチャンネルに以下のような通知がBOTから来る。

BOTからのメッセージ

キャプチャ.PNG

通知処理

先ほど登録したリマインダーはこんな感じで通知が来ます。

通知内容

image.png

BOTは毎分データベースにDELETE_FLGはOFFのリマインダー情報をSELECT文で受け取り、条件と一致したリマインダーをMattermostに送信する。

ソースコード
var dt = new Date();
var WeekChars = [ "日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日" ];
var wDay = WeekChars[dt.getDay()];
var hh = ("0"+dt.getHours()).slice(-2);
var mm = ("0"+dt.getMinutes()).slice(-2);
var hhmm = hh + "" + mm;
var yyyy = dt.getFullYear();
var MM = (dt.getMonth() + 1);
var dd = dt.getDate();
var yyyyMMdd = String(yyyy) + String(MM) + String(dd); 

var year = null;
var month = null;
var date = null;
var dayOfWeek = null;
var hour = null;
var minute = null;
var thisInterval = "Weekdays";
if (wDay === "日曜日" || wDay === "土曜日" ) {
        thisInterval = "Holiday";
}
for(i=0;i<msg.database.length;i++)
{
    year = msg.database[i].TARGET_DATE.slice(0, 4);
    month = msg.database[i].TARGET_DATE.slice(4, 6);
    date = msg.database[i].TARGET_DATE.slice( -2 );
    var targetDate =  new Date(parseInt(year), (parseInt(month) -1), parseInt(date));
    dayOfWeek = targetDate.getDay();

    if(msg.database[i].TARGET_TIME === hhmm) {
        if (msg.database[i].INTERVAL === "EveryDay") {
            return createMsg(); //毎日
        } else if (msg.database[i].INTERVAL === "None" && msg.database[i].TARGET_DATE === yyyyMMdd) {
            return createMsg(); //繰り返しなし
        } else if (msg.database[i].INTERVAL === thisInterval) {
            return createMsg(); //平日or休日
        } else if (dayOfWeek === dt.getDay() && msg.database[i].INTERVAL === "EveryWeek") {
            return createMsg(); // 毎週
        } else if (date === date && msg.database[i].INTERVAL === "Monthly") {
            return createMsg(); // 毎月
        }
    }
}
return null;

function createMsg() {
    if (msg.database[i].INTERVAL === "None") {
        msg.payload = {
            userName: "いつもアシスト ふくまろ",
            channel:msg.database[i].CHANNEL_NAME,
            text:"リマインドします。",
            attachments: 
            [{
                author_name:"REMINDER_ID." + msg.database[i].REMINDER_ID,
                text:msg.database[i].MESSAGE,
            }]
        };
        msg.database = {
            "REMINDER_ID":msg.database[i].REMINDER_ID,
            "CHANNEL":msg.database[i].CHANNEL_NAME,
            "INTERVAL":msg.database[i].INTERVAL
        };
    } else {
        msg.payload = {
            userName: "いつもアシスト ふくまろ",
            channel:msg.database[i].CHANNEL_NAME,
            text:"リマインドします。",
            attachments: 
            [{
                author_name:"REMINDER_ID." + msg.database[i].REMINDER_ID,
                text:msg.database[i].MESSAGE,
                actions:[{
                    name:"このリマインダーを停止させる",
                    integration: {
                        url: "http://**.**.**.**:8888/stop/reminder",
                        context:{
                            "REMINDER_ID":msg.database[i].REMINDER_ID,
                            "CHANNEL":msg.database[i].CHANNEL_NAME,
                            "INTERVAL":msg.database[i].INTERVAL
                        }
                    }
                }]
            }]
        };
    }
    return msg;
}

ひとこと

Slackのコマンドより個人的には使いやすい自分好みのリマインダーを作ることが出来ました。
次回はプラグイン化してどこの環境でも使えるリマインダーに出来たらなぁって思ってます。ノシ

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