JavaScript
RxJS
RxJSDay 14

[RxJS] iPhone × ブラウザでバーコードリーダーの入力を扱う

More than 3 years have passed since last update.


はじめに

業務システムを作っているとバーコードを使いたいことがよくあるんですが、

iphone × ブラウザでバーコードを読み取るには以下の様な方法があります。


  1. 画像の写真をとってサーバーサイドで解析する

  2. 画像の写真をとってjsで解析する

  3. getUserMediaを利用して画像を読み取り、jsで解析する

  4. ネイティブアプリを呼び出す

  5. (アプリではなく)バーコードリーダーを繋いで使う(本題)

これらについて少しづつ補足しつつ、本題としてはバーコードリーダーの読み取りの、RxJSを用いた実装を簡単に解説します。


1. 画像の写真をとってサーバーサイドで処理する

そのままですね。。


2. 画像の写真をとってjsで解析する

こんなライブラリがあるので利用しましょう。

http://serratus.github.io/quaggaJS/examples/

ただ、単に写真をとって解析させるだけなのであまり使い勝手がよいとは言えません。


3. getUserMediaを利用する

カメラをlive-streamで処理します。上のquaggaJSでできるようで、静的な写真をとるより良いのですがiOSだと使えません。


4. ネイティブアプリを呼び出す

バーコードリーダーのアプリをインストールして、URIスキームで呼び出します。

幾つか対応しているアプリはあると思いますが、無料で簡単に使えそうなのだとPic2Shopがあります。


function openBarcodeReader(){
const callback = encodeURIComponent(window.location.href + '?q=EAN' );
document.location="pic2shop://scan?callback=" + callback;
}

http://www.pic2shop.com/demo/scan.html

ただこの方法だと、バーコードを読み取った後は当然ですが画面の再読み込みが行われるので、SPAでさくさくというわけには行かない。。

自分でバーコードリーダーアプリを開発してWebViewで表示してしまえば、読み取り結果をjsで処理することもできます。


5. (アプリではなく)バーコードリーダーを繋いで使う(本題)

と、いうことで本題ですが、業務で利用するのであればやはりちゃんとしたバーコードリーダーには勝てません。最近は、blootooth対応のバーコードリーダーも良い物がいろいろあります。

ということで、(アプリではなく、)バーコードリーダーを利用する方法について説明します。

といっても単純な話で、バーコードリーダーのインターフェースはキーボードと同じため、バーコードを読み取った時に行われていることは、キーボードを高速にタイプしているのと変わりません。

なので、入力フォームにフォーカスして読み取りましょう。

でもいいんですが、、フォーカスが当たっていない時にも利用したいですよね。

キーボードと同じなのでonKeyPress,onKeyDownなどが使えます。

さて、ここで問題になるのはどうやってキーボードの入力と区別させるか。

バーコードリーダーは通常バーコードのプリフィクス、サフィックスとして出力するコードを設定することができます。

通常は終端キーとしてEnterが入力されるように設定されている事が多いでしょう。

これを利用して、バーコードリーダーかどうかを判定するケースもあるようですが、当然キーボードで同じ入力しても同じなので、誤認識することも多いと思います。

ということで、RxJSを利用してもう少しインテリジェントにバーコードリーダーからの入力を検知してみましょう。


バーコードリーダーの読み取りとRxJSの親和性が高いという話

ということで実装です。

まずやりたい事は大きく


  1. keypressイベントを監視したい

  2. イベントの発生がinputタグの場合を除きたい

  3. 一定時間内で入力された文字を取り出す

の3つ。1. 2. は通常のイベントリスナを利用した実装でなんの問題なく実現できますが、

キーボードの入力はそれぞれ単発のイベントにすぎないので、3. は少し面倒です。それをいとも簡単に実装できてしまうのがこのRxJS。


keyPressイベントを監視したい

基本的なイベントの監視です。まずは普通に実装すると、

document.addEventListener('keypress', (event) => {

console.log(event);
})

RxJSでは、


Rx.Observable
.fromEvent(document, 'keypress')
.subscribe((text) => {
console.log(text)
});

あまり変わらないですね。

ここで利用しているのが Observableと呼ばれるもの。

RxJSでは全ての事象を時間軸で捉えますが、ある事象を観測できるもの、つまりObserveされるものということで、 Observableと表現します。これは分かりやすくストリームという言い方で説明することもあります。


イベントの発生がinputタグの場合を除きたい

同じく普通に実装すると

document.addEventListener('keypress', (event) => {

if (event.target.tagName !== 'INPUT') {
console.log(event);
}
});

RxJSでは、

Rx.Observable.fromEvent(document, 'keypress')

.fromEvent(document, 'keypress')
.filter((e) => e.target.tagName !== 'INPUT')
.subscribe((text) => {
console.log(text)
});

RxJSで扱う Observableは時間軸で捉えると記載しましたが、時間軸でとらえた Observableは関数型でよくされるように、collection系の操作で処理することができます。

filter、map、reduce、などなど、、宣言的に記述できるため、見通しがよく実装できるのが嬉しい。


一定時間内で入力された文字を取り出す

RxJSを使わなければ、


let start;
let eventList;
document.addEventListener('keypress', (event) => {
if (event.target.tagName !== 'INPUT') {
const now = Date.now();
if ( now - start > 10) {
start = now;
eventList = []
}
eventList.push(event);
}
});

function monitor() {
const now = Date.now();
if (now - start > 10) {
console.log(eventList);
}
setTimeout(monitor, 10);
}
monitor();

という感じでしょうか。なんとなくで書いたので多分動きませんしもっと良い書き方はあると思いますが、本筋ではないので目をつぶって下さい。

RxJSだと、

Rx.Observable.fromEvent(document, 'keypress')

.filter((e) => e.target.tagName !== 'INPUT')
.buffer(() => {
return Rx.Observable.fromEvent(document, 'keypress')
.debounce(10);
})
.subscribe((events) => {
console.log(events);
});

一定時間内で入力された文字を取り出す、という処理を、


  • 一定時間で発生した最後のイベントだけを流すストリームを作る

  • 特定のタイミングでイベントをまとめる(そしてそのタイミングに、上記のストリームを利用する)

として実現しています。利用したのは、


debounce

イベント発生後、一定時間他のイベントが発生しなかったもののみを通すオペレータ。

http://reactivex.io/documentation/operators/debounce.html

https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/debounce.md

keypressのイベントストリームにつなげる事で、一定時間入力が無い最後のkeypressイベントを取得しています。


buffer

流れてくるイベントを指定された方法に従ってまとめ、配列に変換する。

http://reactivex.io/documentation/operators/buffer.html

https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/buffer.md


全貌

と、言うことで、他にも幾つかの処理をくみあわせるとこういう感じでしょうか。


const ENTER = {};

// keypressイベントを監視
Rx.Observable.fromEvent(document, 'keypress')

// INPUTタグ内で発生したイベントは除外
.filter((e) => e.target.tagName !== 'INPUT')

// ASCIIとEnterのみ。なくても良いが。
.filter((e) => e.charCode || e.keyCode === 13 )

// イベントをcharに変換する
.map((e) => {
if (e.keyCode === 13){
return ENTER;
} else {
return String.fromCharCode(e.charCode);
}
})

// 連続した入力を一つの配列にまとめる
.buffer(() => {

// 10msec以内に連続して発生したkeypressイベントを開始し、最後のイベントを取得する
return Rx.Observable.fromEvent(document, 'keypress')
.debounce(10);
})

// 最後の入力がEnterの場合のみに絞る
.filter((chars) => chars && chars.length > 1 && ( chars[chars.length - 1] === ENTER ))

// 最後のENTERを覗いて文字列を結合する
.map((chars) => {
return chars.slice(0, -1).join('');
})
.subscribe((text) => {
console.log(text)
});


最後に

RxJSは様々なものを Observableで扱うことができるので応用範囲は広いと思いますが、

いわゆるブラウザのイベント処理に適用するだけでも結構便利に使えます。今回のように時間軸が絡む処理は最適だと思いますし、宣言的に記述できるというだけでも、複雑な条件がある場合には見通しがよくなるため結構な利点ではないでしょうか。