20190927のJavaScriptに関する記事は16件です。

プログラミング勉強中2ヶ月目くらいの進捗

1ヶ月目
HTML、CSSは1ヶ月くらいプロゲートやってあんま動いてないホームページを模写できるくらいまでにはなりました。

2ヶ月目
プロゲートのJavaScriptは終わりました
次はpaizaが良いらしいのでそれを2周間くらいやっていますが…
これが難しすぎて進めません。
JavaScript入門編はプロゲートに比べて更に複雑な機能も解説されています。演習も沢山できて勉強になります。でも仕事しながらでも数日で終わってしまいます。

その後はもうオープンワールドに放り込まれると言うか、ノー教材、ノーヒントで問題集を解いていくことになります

まず始めたのが
スキルチェック。Dランクは128問あって、まずこれを全問制覇を目標に初めていくのですが、殆どの問題が標準入力の計算問題。
paizaラーニングではほんの少ししか解説していなくて、全然わからない。じゃあググって調べれば良いのかと言うとそんなことはない。
標準入力 JavaScript 計算 NodeJSとか検索してもそれほど記事や解説は出てこなく、ユーチューブにもpaizaの公式が標準入力の解説動画出してるけど、Dランクでも使えないくらいの内容しか載っていません。

諦めて他にも沢山コンテンツがあるので探してみると、どれも難しすぎてできない。
唯一コードガールだけ超簡単だったのでプレイすることにしました。
とにかく簡単。似たような問題が永遠と出てきて永遠とコード書きまくっていく内容です。絵面とストーリーは全然好みじゃないしガチャとか全然興味ないけど(プロゲートの方が絵面は好き)とても練習になります。これ良いやん!って3日くらい進めて行って

現在ステージ12

もうここから進めません。積みました。

・今年の西暦の下2桁を出力して下さい。

・if文を使って「うるう年」ならば「うるう年」それ以外は「それ以外」で出力してください

この二つがたまに出てきて毎回ここで積みます。paizaラーニングの動画が復習動画で出てきますが、もうそんなの見てわかるレベルじゃない。ググってももちろん出てきません。「JavaScript ググっても出てこない」って検索しても「じゃあ英語で検索しよう!」って記事があったから、問題の一部を英語に翻訳してスペースJavaScriptで検索したらそれっぽいのが出てきました。でも翻訳で書かれているし、まんまそれではないので理屈がわからないと、どの部分を参考にすれば良いのかもわからなく、結局先に進めない。

毎日勉強してはいますが、ググっても出てこなく何も進まないので日に日に勉強時間が減っていっています。
挫折したくないけど、挫折へのルートに進みつつあります。

いややー!

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

YYTypeScript#2「トランスパイル後のJSのパフォーマンス」「npmモジュールの信頼性、どう担保してる?」「コンパイルが遅い」「みんなのtslintの設定とかTipsを教えて」「オブジェクトの構築と実行について」

これは2019年9月27日に開催したTypeScriptイベントYYTypeScript#2のイベントレポートです。

YYTypeScriptは一言で「TypeScripterの部室」です。発表者の話を聞く「一方向的な勉強会」とは真逆で、TypeScriptについて、雑に・ゆるく・ワイワイ話しながらTypeScripter同士の交流を深める「双方向的な座談会」の形式になります。集まった人たちで「今日話たいこと」「聞きたいこと」をいくつか挙げていき、それをテーマに雑談していきます。

今回の配信動画

過去回の配信動画YouTubeプレイリスト「YYTypeScript」
前回YYTypeScript#1「JavaScriptを知らない人がTypeScriptを学ぶ方法を知りたい」「TypeScript初心者がどうやって勉強すると効率がいいか?」「なぜTSが選ばれるのか?」「PHPと比べて、サーバサイドをTSで書くメリットは?」「TypeScriptのバックエンドのオススメフレームワークって?」「定義をinterfaceとtypeどっちで書いてる?」「JSで書かれたプロダクトのTS化ってどうしてる? 」 - Qiita

雑談

トランスパイル後のJSのパフォーマンスが気になる (ましろ)

TypeScript実はこう書くとJSになったときパフォーマンスが落ちるなどあれば知りたい。

・・・

  • 気にして書いてるひといます?
    • あんまり気にしてないかな
  • async/awaitをES5にコンパイルすると、結構ごちゃごちゃしたコードが増えるのは気になる。
  • polyfill的なコードが増えると遅くなっちゃう気はする。
  • あんまりトランスパイルで遅くなったって話は聞いたことが無い気がします。
  • なるべく新し目のtargetにしてあげたほうが良さそう。
  • だいたいlodash使ってるんですけど、map/filterとかでchainする時、量が多そうだったらreduceでまとめる。ってのは考えてますが、jsの世界の話ですね。。
    • map/filter書く時にlodash使います? Array.prototype.map の方を好んで使ってます
    • _(arr|obj).map().filter().value() が好きで。。
      • ひゃー、知りませんでした・・・ こんな感じ ですかね
        • ですです
      • 個人的には、それでも [].filter(() => _).map(() => _) の方が趣味ですねー
      • obj側との行き来?とかで強い?ような気がしてるんですけど、もう足りてますかねー
  • コンパイル後のコードって見てます?
    • 変わったコードを書いたときに気になって見ることはあるが。
    • あんまり見ない。ふと気になって見る程度。
    • あまり見ない構文/ts独自の構文とかどうなってるかな?って見るけど普段見ない
      • interface 消えてるかな?とか
    • へー。ほとんど見たことないなぁ…
    • Enumとか見ると、ちょっと面白い。 Enum["hoge"]Enum[0] どちらでも参照できるトリック。
    • トランスパイル後のコード気になるときは、 公式のplayground が便利です。
  • 10++ TypeScript Pro tips/patterns with (or without) React
    • const Enumはいいの?よくないの?
  • 僕はES2015+互換性の低めな機能は避けてたりします
    • Babelでトランスパイル出来なかったりとかも…。
    • polyfillで解決できないパターンとかがあるのかな(polyfill使ってないので知らないのですが)
  • IE11で動かしたいという業務システムもあって、ES5を選びがちだったりします。
    • EdgeにIE11モードがついたらしい
    • IE11+Windows10は2029年まで……
    • Chromeになっていってほしい
    • そういう意味だとSafariが遅れ気味になってる感じもする
    • ブラウザ使わずElectronで業務システムってだめなのかなー

オブジェクトの構築と実行について (かきうち)

オブジェクトに構築と実行の段階があるという話を聞いたが、もっと詳しく聞きたい。

・・・

完全コンストラクタ 

// 構築フェーズ
const obj = new ClassFoo(
    new Foo(),
    new Hoge(new Test()),
)

// 実行フェーズ
obj.doSomething()

不完全コンストラクタ

// 構築フェーズ
const obj = new ClassFoo(
    new Foo(),
)

// 実行フェーズ
obj.setHoge(new Hoge(new Test())
obj.doSomething()
// 不完全コンストラクタなお家
class IncompleteHouse {
    private floor: Floor | null = null
    private bed: Bed | null = null

    setFloor(f: Floor) {
        this.floor = f
    }
    setBed(b: Bed) {
        this.bed = b
    }

    build() {
        return this.floor.set(this.bed)
    }
}

const incompHouse = new IncompleteHouse()

incompHouse.setBed(new Bed())
incompHouse.build() // this.floor.setでぬるぽ(床がないのにベッドを置くな)

// 完全コンストラクタなお家
class CompleteHouse {
    // 実際に作る時は、readonly付けておくの推奨です
    private bed: Bed | null = null

    constructor(private floor: Floor) { }

    setBed(bed: Bed) {
        this.bed = bed // 今回はmutableにしちゃいます(サクッと書くのを優先して・・・)
    }

    build() {
        return this.floor.set(this.bed) // Floor#setはnull許容に(ry
    }
}
const compHouse = new CompleteHouse(new Floor())

compHouse.build() // コンストラクタでfloorの存在が保証されている
  • クラスを使わない(過激派)

  • newするのを最初に作りきっちゃう。

  • 構造を全部作りきってから、実行すると見通しがいい。

  • 不完全コンストラクタはやめましょう、みたいなお話ですかね。

    • 不完全コンストラクタ「newした後、setHogeで値を設定してからdoHogeしないと死にます」
      • 構造の組み立てが、newとsetHogeの2段階に別れてる、的な
      • なるほど!
    • SOLID原則のI(インターフェース分離原則)的にあかんパターンですかね
    • 複雑な感じになってる感じがあるかなー(責務で、さらにクラスや関数を分離した方がいいパターンかも?)
  • 変動する値は実行時に決めるしかない

  • 不完全コンストラクタにならざるを得ない構造の場合、build で、いっそ、別オブジェクト(完全になったオブジェクト)を返すとかどうだろう?不完全コンストラクタは完全コンストラクタを作る、ビルダパターンに責務を限定しちゃって、実際の動きは完全コンストラクタに任せるみたいな二段構え作戦とか

npmモジュールの信頼性ってどう担保、チェックしてる? (すいん)

マルウェアが入ってたり、攻撃コードが混入されることへの予防策を知りたい。
過去にnpmモジュールを乗っ取り、ビットコインウォレットを狙う事件があった。2018/11/27に判明したnpmパッケージ乗っ取りについて - Qiita

最近サーバサイドでTSを使っているので怖い。

・・・

  • Auditing package dependencies for security vulnerabilities | npm Documentation
    • npm auditコマンドで脆弱性のチェックができるっぽい。
    • CIに組み込んだらいいと思う。
      • おー、良さそう
    • Securing your code | npm Documentation
    • 既知の脆弱性だと見つけられる、未知だと厳しい。
  • denoだと通信していいURLを制限できたりする。
    • そういう意味だとDockerとか、インフラで制限したほうがいい。
    • インフラ、ネットワークも含めて多方面での防護(多層防御)をした方がいい。
  • みんなどうやってパッケージ見極めている?
    • 直接依存しているパッケージは見るようにしているが、
    • 依存しているパッケージが依存しているパッケージまでには目が行かない。
  • npm | Enterprise
    • パッケージをチェックしてくれる機能があるらしい。
  • そういや最近GitHubにyarn.lockをcommitすると、(自分が直接依存してない)パッケージで古いやつが紛れてると、インセキュアじゃ!と言って勝手にPR出されたりするよね…
    • yarnの話とは違うけどgithubがsecurity-alertみたいなメール出してくれてますね
  • 依存が少ないpackage使うと良かったりします?
    • 多いと、巻き添え的に何かしら喰らいやすい気はしてます
  • npmのアカウントを2要素認証にしておくことで、npmアカウントの乗っ取りを防止する。
  • 開発者のPGPのシグネチャがついていないパッケージは使わないこと

babelでTS解析楽しい(erukiti) リモート

コンパイルが遅い。ts-nodeのオーバーヘッド。(interface の?) generics。promise経由のせいか若干推論がおかしかったこと。 (ゆた) リモート

コンパイルが遅い。

具体例が特にあるわけじゃないんですけど…自分のprojectがいま遅くなってきてて。
こういうプロジェクトは遅くなってきますよとかなにか知見があったら嬉しいです。

・・・

  • 何行くらいですか?

    • githubで見たら105ファイル、行数は測るツールが手元にない
  • ファイル数が多くなるとどうしようもないかも

  • 自分のMacだと5分くらい。

  • 2分くらい。

  • 初回が遅いだけで、--watchをつけると差分は早い。

  • --incrementalってどうなの?

    • TypeScript 3.4のincrementalフラグを試す - Qiita
      • incremental 確か試しました。出力をwebpackに食わせてたんですけど日時で検知してるのかwepack側がおそくなってしまった記憶です
        • webpack側でts-loaderにするとか色々分けるとか考える余地はあるのでそのうち試します。。
          • 構造を変えるパターンのお話聞こえました。その余地はあるかんじです。
      • 今は --watch で、 webpackに食わせてます。
  • コンパイルのボトルネックを分析するツールって無いんですか?

  • jestも、ts-jest使うと遅いですよねー。babel-jestに乗り換えたら一気に快適になった。babel-node使うのありかも??(型チェックはしなくなるけど)

ts-nodeのオーバーヘッド。

npm-run-scripts で task 管理してるんですけど、長いscriptはtsファイルに追いやって ts-node 経由で呼んでる。pre/post のチェーンで何度も ts-node が呼ばれることになるのでじゃっかん気になる。run-scripts の管理方法的な話とかも聞けたら嬉しい。

・・・

  • ts-nodeは最初実行前にコンパイルする時間は気になります。
  • ts-nodeが何回も呼ばれるのでそれを回避 > scriptのpre-post含めたシーケンスをすべてまとめてしまうって感じになりそう
  • 遅い原因は、 ts-node 自体の起動時間、tsのコンパイルが入ると思うので。って感じです
  • 現状何分とかの内容ではないですが、今後このままでよいのかな?という不安がある
  • 1つのtsファイルを起点にするようにpackage.jsonのscripts定義を書き直すと安心?
    • ↑の scriptのpre-post含めたシーケンスをすべてまとめてしまうって感じになりそう ですかね
    • ですかね!ts-node実行の最初のオーバーヘッドが問題だとして、それが1回になれば問題になるほどではなくなるはず
    • npm-run-scripts で小さく管理しよう。っていう話もあるような。

(interface の?) genericsでうまく書けなかった

やりたかったことはこんなの。
-> 推論がおかしかったのは別の話です(promiseの例は作れなかったです)

はい。genericsの書き方の話です。

interface Action<IN, OUT> {
  (i: IN): OUT 
}

interface DAction<OUT> extends Action<string, OUT> {
}

最終的にはこうした。

interface DAction {
  <OUT>(i:string): OUT;
}

// こんな感じで普通に使えそうな気がしたけれど、こういう事ではない?
interface Action<IN, OUT> {
  (i: IN): OUT 
}

interface DAction<OUT> extends Action<string, OUT> {
}

const a: DAction<number> = (i: string) => parseInt(i, 10)

console.log(a('123'))
// こうしたいのです
const b: number = a<number>("");
// 元のコードの例があまりよくないのかも
// あとさらにDActionをgenericsでラップしてるので
// そこら辺含めるとうまく伝えられていない感じがします。。
  • Promiseの型推論、通常のresolveの型はgenericsで指定できるけど、rejectする型が指定出来なくて悲しみ
    • わかりみ
    • catch で as Hoge みたいに assertion しないと型が付いてくれない…
      • そうしてますね

Linter と Formatter の設定Tips とか聞きたいです (tadashim) リモート

  • どんな設定してますか?

    • フレームワークによってガラッと変わる気がする
  • 最近、prettierだけ設定して、eslintはあまり真面目に考えてない…

    • React のときは、create-react-app で作られるヤツそのまんまで思考放棄
    • TypeScript 自体がある種の Linter だから型さえ真面目に書けばある程度linterサボれる気がしてる
  • とりあえず↓が入ってた
    json:tslint.json
    "extends": [
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    ],

  • Angularのtslint使っていて、prettierを合わせて使ってる。

    • たまに衝突して辛い。
      • 設定同士の衝突系は、VSCodeとも衝突するからそれぞれの設定が面倒になってしまうのはありますね
    • prettierとの衝突回避↓
tslint.json
  "extends": ["tslint:recommended", "tslint-plugin-prettier", "tslint-config-prettier"],

tslintの今後:

8月 コアルール追加の終了
11月 機能追加の終了
来年1月 セキュリティを除く全ての変更を停止
来年12月 すべての変更を停止

  • tslintからeslintへの以降は難しい?
    • わかりません

その他の雑談

技術書典7

JSふわっと覚えてる人におすすめ

  • 『JavaScript Primer: ECMAScript 2019時代のJavaScript入門書』 - すでにプログラミング経験がある人が読むとJavaScriptの文法や機能を中心に学ぶことができる本。TypeScriptを書くにもJavaScriptの知識が必要不可欠なので、雰囲気でJSを書いてきた人やちゃんとおさらいしたい人におすすめ。ブラウザで無償で読める。

TypeScript Meetup

Nestって?

サーバサイドのフレームワーク。Angular使ってる人は使いやすいらしい。

ngrxって?

Angularで使われている状態管理。

参加してよかったこと(参加者の感想)

  • いろんな知見が得られてよかったです
  • incrementalフラグを知ることができて良かったです。
  • オブジェクト指向の構築と実行について聞けたこと。
  • リモートだけど参加している感がある
  • セキュリティとか、あまり真面目に追いかけられてないので、これはやっておくべきだなーという気持ちに。
  • いろんな話題があって面白かった。

YYTypeScriptは毎週やってます

YYTypeScriptについてワイワイ話したい方は、YYTypeScriptのイベント情報をチェックしてみて下さい。

以上、YYTypeScriptのレポートでした。次回もワイワイやっていきたいと思います! では、また来週!

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

jsのビルドで「Module not found: Error: Can't resolve 'regenerator-runtime/runtime'」が出た時の対策

「Module not found: Error: Can't resolve 'regenerator-runtime/runtime'」が出た

async/awaitを使っているコードをビルド(babelをかけたら)したら
以下のようなエラーとなる場合がある。

Module not found: Error: Can't resolve 'regenerator-runtime/runtime' in xxx

対策案その1 core-jsとregenerator-runtimeを導入する

Babel 7.4.0以降を使っているなら、

2つのnpmモジュールcore-jsregenerator-runtimeをインストール

npm install core-js@3 regenerator-runtime

async/awaitを使っているソースコード(.js)でいまインストールしたモジュールをimportする

import "core-js/stable";
import "regenerator-runtime/runtime";
・・・以下略・・・

対策案その2 @babel/polyfill を導入する

@babel/polyfillをインストール

npm install --save-dev @babel/polyfill

async/awaitを使っているソースコード(.js)で import "@babel/polyfill" する

import "@babel/polyfill";
・・・以下略・・・

babel公式によると、@babel/polyfillBabel 7.4.0で deprecatedとなっているので注意

対策案その3 対象ブラウザを変更する

変更前>古いブラウザを対象にした@babel/preset-envの設定

※targetsに注目

webpack.config.js抜粋
{
 test: /\.js$/,
 exclude: /(node_modules|bower_components)/,
 use: [
  {
   loader: 'babel-loader',
   options: {
    presets: [
     [
      '@babel/preset-env',
      {
       'modules': 'false',//commonjs,amd,umd,systemjs,auto
       'useBuiltIns': 'usage',
       'targets': '> 0.25%, not dead',
       'corejs': 3
      }
     ]
    ]
   }
  }
 ],
}, 

変更後>新しいブラウザを対象にした@babel/preset-envの設定

※targetsに注目

webpack.config.js抜粋
{
 test: /\.js$/,
 exclude: /(node_modules|bower_components)/,
 use: [
  {
   loader: 'babel-loader',
   options: {
    presets: [
     [
      '@babel/preset-env',
      {
       'modules': 'false',//commonjs,amd,umd,systemjs,auto
       'useBuiltIns': 'usage',
       "targets": {
        "browsers": [
         "last 1 Chrome version",
         "last 1 Firefox version",
         "last 1 Edge version",
         "last 1 Safari version",
         "last 1 Opera version"
        ]
       },
       'corejs': 3
      }
     ]
    ]
   }
  }
 ],
},

(対策その2のほうは、babelの目的から考えると、本末転倒かな。)

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

【JavaScript】 変数について

この記事の概要

JavaScriptの変数についてまとめていきます。
動作確認はNode.jsを用いました。

変数の名前について

命名規則

大文字と小文字は区別

大文字と小文字は区別されます。

var name = 'hoge';
var Name = 'piyo';

予約語は使用不可

まず、他の言語と同じように予約語は使えません。
予約語の一覧

使える文字

場所 使える文字
一文字目 英字 ドル記号($) アンダースコア(_)
二文字目 さらに数字も使用可能

一般的には

種類 記法 具体例
変数 キャメルケース userName
関数 (同上) appendUser
定数 スネークケース ADULT_AGE
クラス パスカルケース SuperHero

それっぽい具体例があんまり思いつきませんでした。すいません。

記法について

キャメルケース

最初の単語は小文字スタート、以降の単語を大文字スタート。

大文字がラクダのコブのように見えるからcamelだそうです。
thisIsCamelCase
スネークケース

単語をアンダースコアで繋いでいきます。

へびのように見えるからsnakemだそうです。
this_is_snake_case
パスカルケース

大文字スタートのキャメルケースと一緒です。アッパーキャメルケースともいいます。

Pascal言語で使用されていたことが由来らしいです。
ThisIsPascalCase

var

使い方

varを使って変数を宣言
var [変数名];
varを使って変数を宣言し初期化
var [変数名] = [初期値];
変数を宣言し初期化、違う値を代入
var number = 5;
number = 10;
console.log(number);//10と出力される

変数の宣言だけでもエラーにならない

変数の宣言
var number;
console.log(number);//undefinedと出力される

例えばRubyなどだと上記のようなことはできません。

Ruby
number 
puts number # undefined local variable or method `number' for main:Object (NameError)

同じ名前の変数を宣言する

同名の変数の宣言
var number = 1;
var number = 2;
console.log(number);//2と出力される

let

ECMAScript 2015からは変数の宣言時にletの使用が可能になりました。
letを使いましょうみたいなことが言われています。

All of us have used “var” to define a variable in the past, we now have a “let” keyword and we will see in a bit why it is better.

引用元の記事

letを使用して変数を宣言する

let number;
console.log(number);//undefined

変数の宣言と初期化

変数を宣言し初期化、違う値を代入
let number = 5;
number = 10;
console.log(number);

varとletの違い

同名の変数の宣言をした時

letの場合
let number = 1;
let number = 2;
console.log(number);//Identifier 'number' has already been declared

letの場合はすでにnumberという変数は宣言済み(decleared)であるとのエラーが出ます。

varの場合(再掲)
var number = 1;
var number = 2;
console.log(number);//2と出力される

しかしながら、先ほども確認しましたように,varの場合はエラーが出ません。

スコープの違い

JavaScript公式ドキュメントより引用します。

var で宣言した変数は、ブロックスコープを持ちません。ブロック内で導入された変数は、それを含んでいる関数またはスクリプトがスコープとなり、変数を設定した効果は、そのブロック自体を超えて持続します

詳しく知りたい場合はこちらをご覧ください(引用元です)

ブロックというのは、{}の内に複数の文をまとめることができるもので、普通は下記のように単独で出てくるものではなく、何らかの制御フロー文を伴います。

ブロック
{var number = 1;}

letはどうなっているかというとletはブロックスコープを持ちます。
その二つの違いを具体的な動作で確認してみたいと思います。

ブロック内を参照する(varの場合)
function f(){
  {var number = 1;}
  console.log(number)
}

f(); //1と出力される

varを使用した場合、変数numberはブロックスコープを持ちません。今回の場合ですと関数スコープを持ちます。

ブロック内を参照する(letの場合)
function f(){
  {let number = 1;}
  console.log(number)
}

f(); //ReferenceError: number is not defined

こちらの場合はブロック外のconsole.log(number)はスコープ外なので、numberを参照できなくなります。

宣言されていない変数への代入みたいな書き方

注意:グローバル変数が作成される
number = 1;
console.log(number);//1と出力される

あまり推奨はされていないようですが、上記のような書き方もできます。この時、黙字的に宣言と初期化がなされるという説明が書籍ではなされていることが多いような気がしますが、グローバル変数が作成されるということにも注意が必要です。

グローバル変数とローカル変数を関数の外から参照することでスコープを比較してみます。

グローバル変数
function f(){
  var local = 5;
  global = 5;
}
f();//関数f()の実行 
console.log(global);//5と出力される
ローカル変数
function f(){
  var local = 5;
  global = 5;
}
f();//関数f()の実行 
console.log(local);//ReferenceError: local is not defined

厳格モード

"use strict"と記載することにより厳格モードとなります。厳格モードになるとより厳しい文法チェックがなされます。その一例として、先ほどの横着ができなくなります。

厳格モードではない
number = 1;
console.log(number);

非厳格モードでは、先ほど確認したようにエラーは出ません。

厳格モード
"use strict"
number = 1;
console.log(number);

実行しようとすると、厳格なので、下記のように怒られます。

ReferenceError: number is not defined

厳格モードでは、「黙字的に宣言と代入をしてくれる」という部分がなくなるので、宣言されていない変数numberに代入しようとしていることになり、「numberは定義されていませんよ」というエラーが生じます。

厳格に厳格モードを知りたい方はこちらへ

終わりに

今回は変数についてまとめました。間違いなどありましたら教えていただけますと幸いです。

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

JavaScriptでQRコードを生成・解析

ブラウザでQRコードが弄れるらしいので、試してみました。

Reader

入力デバイスを提供するMediaDevicesインターフェイスを使い、カメラから画像を読み取り、QR解析ライブラリを使用してデコードします。

HTML

reader.html
<script src="https://cdn.jsdelivr.net/npm/jsqr@latest/dist/jsQR.min.js"></script>
<video id="cap" width="320" height="240" autoplay></video>

<textarea id="res" readonly></textarea>

ここで指定するwidthheightは、表示領域の解像度です。
autoplayは無いとキャプチャはされていても映像ストリームが表示されません。

JavaScript

reader.js
function readQr(video){
    return navigator.mediaDevices.getUserMedia({
        audio: false,
        video: {
            width: {
                exact: 320
            },
            height: {
                exact: 480
            },
            facingMode: {
                exact: "environment"
            }
        }
    })
    .then((stream)=>{
        return new Promise((res)=>{
            video.addEventListener("loadedmetadata", ev => res(ev));
            video.srcObject = stream;
        });
    })
    .then(({target})=>{
        return new Promise((res)=>{
            const loop = setInterval(()=>{
                const canvas = document.createElement("canvas").getContext("2d");
                canvas.drawImage(target, 0, 0, target.videoWidth, target.videoHeight);
                const img = canvas.getImageData(0, 0, target.videoWidth, target.videoHeight);
                const data = jsQR(img.data, img.width, img.height);
                if(data){
                    clearInterval(loop);
                    target.srcObject.getTracks()[0].stop();
                    target.srcObject = null;
                    res(data);
                }
            }, 100);
        });
    });
}

ビデオキャプチャ初期化

まずnavigator.mediaDevices.getUserMedia()でカメラデバイスにアクセスします。

この時いくつかの初期化オプションを渡します。
まず解像度ですが、ここで指定する解像度はキャプチャ解像度になります。
私の環境では、これ以上高くすると読み取れませんでした。

facingModeは、スマートフォンなどの場合にリアカメラ・フロントカメラを指定できます。
リアカメラを起動したい場合は"environment"で、フロントカメラを起動したい場合は"user"を代入します。

いずれもexactプロパティに代入する事で、必ずその値で初期化されるよう指定します。

そしてブラウザからカメラ試用許可を要求されます。
許可すると、映像ストリームオブジェクトが返されるので<video>要素に紐付けます。

ここまでがビデオキャプチャの初期化で、次からがQRコードリーダーの中身になります。

QR解析

まずtargetは、映像ストリームのメタデータの読込が完了したときに解決されるEventTargetで、対象の<video>要素です。

実体はreadQr()引数videoと同一ですが、精神衛生上こちらを使いました?

QR解析にはImageData型の静止画が必要なので、映像ストリームから定期的に静止画を抽出する必要があります。

setInterval()で1秒間に10回、映像ストリームをcanvas仮想要素に静止画として流し込み、ピクセルデータをImageData型で出力し、ライブラリの解析メソッドjsQR()へ渡します。

解析結果が存在したら、繰り返し処理の解除と映像ストリームの終了を行います。

使う

HowToUse
readQr(document.getElementById("cap"))
.then(({data}) => document.getElementById("res").value = data);

Writer

めちゃめちゃ簡単です?
QR生成はライブラリを使います。

HTML

writer.html
<script src="https://cdn.jsdelivr.net/npm/qrcode@latest/build/qrcode.min.js"></script>
<canvas id="qr"></canvas>

特にこれといった事はしていません。

JavaScript

writer.js
function writeQr(canvas, data){
    return new Promise((res, rej)=>{
        QRCode.toCanvas(canvas, data, {
            margin: 2,
            scale: 2
        }, (err, tg) => !err ? res(tg) : rej(err));
    });
}

既存の<canvas>要素に紐付けて、そこに書き込みます。

QRバージョン、誤り訂正レベル、データモードなどは、入力されるデータによってライブラリが自動で決定してくれます。

ライブラリがコールバック仕様なので、利便性のためPromiseでラップしています。
解決されるtgEventTargetで対象の<canvas>要素ですが、これも実体はwriteQr()引数canvasと同一なので、特に使う必要はありません。

なお、メソッド内でcreateElement("canvas")をして<canvas>仮想要素へ出力する場合は、最終的にDOMへ挿入する場合などに使います。

使う

HowToUse
writeQr(document.getElementById("qr"), "ほげほげ");

サンプル

GitHub Gistに載せました。
https://gist.github.com/dojyorin/89ffa8fdb7cd14f47e9646ffc42adf6a

Gist Previewで試せます。
https://gistpreview.github.io/?89ffa8fdb7cd14f47e9646ffc42adf6a

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

【iOS13】新しくなったWebVRの使い方

【iOS13】新しくなったWebVRの使い方

iOS13でWebVRをやる方法が変わりました。今まで(iOS 12)は、Safariの設定から加速度の許可をする方法だったのですが、iOS13から許可の方法が変わり、ブラウザ上から許可を求めるようになりました。

iOS 12までのWebVRを使用するための権限を取得する方法

  1. iOSの設定を開く
  2. モーションと画面の向きのアクセスを許可する
  3. ブラウザ上でデバイスの動作と方向を取得できるようになる

iOS 13から変わった"動作と方向へのアクセス"

  1. ブラウザからアラートを出す
  2. 許可または拒否のどちらかが選択される
  3. 許可された場合ブラウザ上でデバイスの動作と方向を取得できるようになる

PlayCanvasでWebVRを使用する

  • JavaScriptでゲームを作れるエンジンのPlayCanvasを使用してiOS13に対応をさせます。

このプロジェクトのFork元のStarter Kit: VRを参考にして作成します。

1. PlayCanvasでWebVRのスターターキットを使用して作成をする

one

2. シーンの選択をする

two

Rootのweb-vr-ui.jsを書き換える

three

変更後のweb-vr-ui.js

web-vr-ui.js
/*jshint esversion: 6, asi: true, laxbreak: true*/
const WebVrUi = pc.createScript('webVrUi');

WebVrUi.attributes.add("camera", {type: "entity", title: "Camera"});
WebVrUi.attributes.add("enterVrWhite", {type: "asset", title: "Enter VR White Asset"});
WebVrUi.attributes.add("enterVrOrange", {type: "asset", title: "Enter VR Orange Asset"});
WebVrUi.attributes.add("infoBoxLifeSpan", {type: "number", default: 3, title: "Info Box Life Span",});

WebVrUi.prototype.initialize = function() {    
    if (this.app.vr && this.app.vr.display) {
        this.app.vr.display.on("presentchange", this.onVrPresentChange, this);
    }

    this.app.assets.load(this.enterVrWhite);
    this.app.assets.load(this.enterVrOrange);

    // HTML UI setup
    const css = '#vr-button {position: absolute;right: 0px;bottom: 0px;background-image: url("'+ this.enterVrWhite.getFileUrl() +'");width: 146px;height: 104px;display: block;'+
        'background-position: 0px 0px;background-size: 146px 104px; cursor: pointer;}' +        
        '#vr-button:hover {background-image: url("' + this.enterVrOrange.getFileUrl() + '");}' +
        '#info-box {position: absolute; right: 140px;bottom: 26px;display: block;background-color: rgba(0,0,0, 168);color: rgb(218, 218, 218);padding: 5px 10px 5px 10px;max-width: 220px;}' +
        '#info-box a, #info-box a:hover, #info-box a:visited, #info-box a:active {text-decoration: underline;color: rgb(218, 218, 218);}';

    const style = pc.createStyle(css);
    document.head.appendChild(style);

    this.vrButtonDiv = document.createElement("div");
    this.vrButtonDiv.id = "vr-button";
    this.vrButtonDiv.innerHTML = "&nbsp"; 

    document.body.appendChild(this.vrButtonDiv);

    this.infoBoxDiv = document.createElement("div");
    this.infoBoxDiv.id = "info-box";    

    this.infoBoxLifeTime = 0;
    this.infoBoxShowing = false;

    this.vrEntered = false;

    const self = this;

    const onEnterVrPressedEvent = function() {  
        // If WebVR is available and a VrDisplay is attached
        if (self.app.vr && self.app.vr.display) {
            if(self.vrEntered) {
                // Exit vr (needed for non-mobile)
                self.camera.camera.exitVr(function (err) {
                    if (err) {
                        console.warn(err);
                    } 
                });
            }
            else {
                // Enter vr
                self.camera.camera.enterVr(function (err) {
                    if (err) {
                        console.warn(err);
                    } 
                });
            }
        } 
        else {
            if (!self.infoBoxShowing) {
                if (self.app.vr.isSupported) {
                    self.infoBoxDiv.innerHTML = "No VR display or headset is detected.";
                }
                else {
                    self.infoBoxDiv.innerHTML = "Sorry, your browser does not support WebVR :(. Please go <a href='https://webvr.info/' target='_blank'>here</a> for more information.";
                }   

                self.infoBoxLifeTime = self.infoBoxLifeSpan;
                document.body.appendChild(self.infoBoxDiv);
                self.infoBoxShowing = true;
            }
        }
    };


    const onEnterVrPressedEventIOS13 = function(){

        if ( DeviceMotionEvent && DeviceMotionEvent.requestPermission && typeof DeviceMotionEvent.requestPermission === 'function') {
            DeviceMotionEvent.requestPermission();
          }

          if ( DeviceOrientationEvent && DeviceOrientationEvent.requestPermission && typeof DeviceOrientationEvent.requestPermission === 'function') {
            DeviceOrientationEvent.requestPermission().then((state) => {
                if(state === "granted"){
                    self.camera.camera.enterVr(function (err) {
                        if (err) {
                            console.warn(err);
                        } 
                });
                }else if(state === "denied"){
                    alert("Permission is denied !")
                }

            })
          }

    }

    if(window.navigator.userAgent.includes("OS 13")){
        this.vrButtonDiv.addEventListener('click', onEnterVrPressedEventIOS13, false);

    }else{
        this.vrButtonDiv.addEventListener('click', onEnterVrPressedEvent, false);
        onEnterVrPressedEvent();
    }

};

// update code called every frame
WebVrUi.prototype.update = function(dt) {
    if (this.infoBoxShowing) {
        this.infoBoxLifeTime -= dt;
        if (this.infoBoxLifeTime <= 0) {
            document.body.removeChild(this.infoBoxDiv);
            this.infoBoxShowing = false;
        }
    }
};

WebVrUi.prototype.onVrPresentChange = function(display) {
    if (display.presenting) {
        // Only remove the VR button if we are on mobile
        if (pc.isMobile()) {
            document.body.removeChild(this.vrButtonDiv);
        }
        this.vrEntered = true;
    }
    else {
        if (pc.isMobile()) {
            document.body.appendChild(this.vrButtonDiv);
        }
        this.vrEntered = false;
    }
};

WebVrUi.prototype.scaleInfoPanel = function(scale) {
};

変更点について

変更点はGistに差分があります。
既存のパソコンなどの動作はそのまま使用できるので、userAgentを見てiOS13のときのみに許可を求めるスクリプトを追加します。
https://gist.github.com/yushimatenjin/ce6e0dfe346510c94d65ff7e4afde14a#file-web-vr-ui-js-L80-L111

  • 1. DeviceOrientationEvent.requestPermission()こちらの関数を実行することでデバイスから動作と方向の許可を求めるアラートを出すことができます
  • 2. DeviceOrientationEvent.requestPermission()の返り値は現在の権限の状態deneidgrantedが非同期で渡されます。
    1. 権限を持っている状態grantedだった場合にPlayCanvasのenterVr関数でVRの状態(画面が2分割になり、回転などを取得している)にします。
  • デバイスの取得の方方はこちらを参考にいたしました
    iOS13でWebARとWebVRにおけるデバイスモーション設定が改善しました!

web-vr-ui.js
    const onEnterVrPressedEventIOS13 = function(){

          if ( DeviceOrientationEvent && DeviceOrientationEvent.requestPermission && typeof DeviceOrientationEvent.requestPermission === 'function') {
            DeviceOrientationEvent.requestPermission().then((state) => {
                if(state === "granted"){
                    self.camera.camera.enterVr(function (err) {
                        if (err) {
                            console.warn(err);
                        } 
                });
                }else if(state === "denied"){
                    alert("Permission is denied !")
                }

            })
          }

    }

    if(window.navigator.userAgent.includes("OS 13")){
        this.vrButtonDiv.addEventListener('click', onEnterVrPressedEventIOS13, false);

    }else{
        this.vrButtonDiv.addEventListener('click', onEnterVrPressedEvent, false);
        onEnterVrPressedEvent();
    }

};

表示できました

この一連の動作を見てみるとこのような表示になります。

※Gif

実際に使っている端末で動かす

このツイートまたはQRから飛ぶことができます。


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

【iOS13】新しくなったWebVRの使い方 [PlayCanvas]

はじめに

iOS13でWebVRをやる方法が変わりました。今まで(iOS 12)は、Safariの設定から加速度の許可をする方法だったのですが、iOS13から許可の方法が変わり、ブラウザ上から許可を求めるようになりました。
PlayCanvasを使用した対応方法がなかったため公開いたしました。

iOS 12のWebVRを使用するための権限を取得する方法

  1. iOSの設定を開く
  2. モーションと画面の向きのアクセスを許可する
  3. ブラウザ上でデバイスの動作と方向を取得できるようになる

iOS 13から変わった"動作と方向へのアクセス"

  1. ブラウザからアラートを出す
  2. 許可または拒否のどちらかが選択される
  3. 許可された場合ブラウザ上でデバイスの動作と方向を取得できるようになる

PlayCanvasでWebVRを使用する

  • JavaScriptでゲームを作れるエンジンのPlayCanvasを使用してiOS13に対応をさせます。

このプロジェクトのFork元のStarter Kit: VRを参考にして作成します。

1. PlayCanvasでWebVRのスターターキットを使用して作成をする

スターターキットを使用してiOS13のWebVRに対応をする方法を紹介します。

one

2. シーンの選択をする

two

Rootのweb-vr-ui.jsを書き換える

three

変更後のweb-vr-ui.js

web-vr-ui.js
/*jshint esversion: 6, asi: true, laxbreak: true*/
const WebVrUi = pc.createScript('webVrUi');

WebVrUi.attributes.add("camera", {type: "entity", title: "Camera"});
WebVrUi.attributes.add("enterVrWhite", {type: "asset", title: "Enter VR White Asset"});
WebVrUi.attributes.add("enterVrOrange", {type: "asset", title: "Enter VR Orange Asset"});
WebVrUi.attributes.add("infoBoxLifeSpan", {type: "number", default: 3, title: "Info Box Life Span",});

WebVrUi.prototype.initialize = function() {    
    if (this.app.vr && this.app.vr.display) {
        this.app.vr.display.on("presentchange", this.onVrPresentChange, this);
    }

    this.app.assets.load(this.enterVrWhite);
    this.app.assets.load(this.enterVrOrange);

    // HTML UI setup
    const css = '#vr-button {position: absolute;right: 0px;bottom: 0px;background-image: url("'+ this.enterVrWhite.getFileUrl() +'");width: 146px;height: 104px;display: block;'+
        'background-position: 0px 0px;background-size: 146px 104px; cursor: pointer;}' +        
        '#vr-button:hover {background-image: url("' + this.enterVrOrange.getFileUrl() + '");}' +
        '#info-box {position: absolute; right: 140px;bottom: 26px;display: block;background-color: rgba(0,0,0, 168);color: rgb(218, 218, 218);padding: 5px 10px 5px 10px;max-width: 220px;}' +
        '#info-box a, #info-box a:hover, #info-box a:visited, #info-box a:active {text-decoration: underline;color: rgb(218, 218, 218);}';

    const style = pc.createStyle(css);
    document.head.appendChild(style);

    this.vrButtonDiv = document.createElement("div");
    this.vrButtonDiv.id = "vr-button";
    this.vrButtonDiv.innerHTML = "&nbsp"; 

    document.body.appendChild(this.vrButtonDiv);

    this.infoBoxDiv = document.createElement("div");
    this.infoBoxDiv.id = "info-box";    

    this.infoBoxLifeTime = 0;
    this.infoBoxShowing = false;

    this.vrEntered = false;

    const self = this;

    const onEnterVrPressedEvent = function() {  
        // If WebVR is available and a VrDisplay is attached
        if (self.app.vr && self.app.vr.display) {
            if(self.vrEntered) {
                // Exit vr (needed for non-mobile)
                self.camera.camera.exitVr(function (err) {
                    if (err) {
                        console.warn(err);
                    } 
                });
            }
            else {
                // Enter vr
                self.camera.camera.enterVr(function (err) {
                    if (err) {
                        console.warn(err);
                    } 
                });
            }
        } 
        else {
            if (!self.infoBoxShowing) {
                if (self.app.vr.isSupported) {
                    self.infoBoxDiv.innerHTML = "No VR display or headset is detected.";
                }
                else {
                    self.infoBoxDiv.innerHTML = "Sorry, your browser does not support WebVR :(. Please go <a href='https://webvr.info/' target='_blank'>here</a> for more information.";
                }   

                self.infoBoxLifeTime = self.infoBoxLifeSpan;
                document.body.appendChild(self.infoBoxDiv);
                self.infoBoxShowing = true;
            }
        }
    };


    const onEnterVrPressedEventIOS13 = function(){

        if ( DeviceMotionEvent && DeviceMotionEvent.requestPermission && typeof DeviceMotionEvent.requestPermission === 'function') {
            DeviceMotionEvent.requestPermission();
          }

          if ( DeviceOrientationEvent && DeviceOrientationEvent.requestPermission && typeof DeviceOrientationEvent.requestPermission === 'function') {
            DeviceOrientationEvent.requestPermission().then((state) => {
                if(state === "granted"){
                    self.camera.camera.enterVr(function (err) {
                        if (err) {
                            console.warn(err);
                        } 
                });
                }else if(state === "denied"){
                    alert("Permission is denied !")
                }

            })
          }

    }

    if(window.navigator.userAgent.includes("OS 13")){
        this.vrButtonDiv.addEventListener('click', onEnterVrPressedEventIOS13, false);

    }else{
        this.vrButtonDiv.addEventListener('click', onEnterVrPressedEvent, false);
        onEnterVrPressedEvent();
    }

};

// update code called every frame
WebVrUi.prototype.update = function(dt) {
    if (this.infoBoxShowing) {
        this.infoBoxLifeTime -= dt;
        if (this.infoBoxLifeTime <= 0) {
            document.body.removeChild(this.infoBoxDiv);
            this.infoBoxShowing = false;
        }
    }
};

WebVrUi.prototype.onVrPresentChange = function(display) {
    if (display.presenting) {
        // Only remove the VR button if we are on mobile
        if (pc.isMobile()) {
            document.body.removeChild(this.vrButtonDiv);
        }
        this.vrEntered = true;
    }
    else {
        if (pc.isMobile()) {
            document.body.appendChild(this.vrButtonDiv);
        }
        this.vrEntered = false;
    }
};

WebVrUi.prototype.scaleInfoPanel = function(scale) {
};

変更点について

変更点はGistに差分があります。
既存のパソコンなどの動作はそのまま使用できるので、userAgentを見てiOS13のときのみに許可を求めるスクリプトを追加します。
https://gist.github.com/yushimatenjin/ce6e0dfe346510c94d65ff7e4afde14a#file-web-vr-ui-js-L80-L111

  • 1. DeviceOrientationEvent.requestPermission()こちらの関数を実行することでデバイスから動作と方向の許可を求めるアラートを出すことができます
  • 2. DeviceOrientationEvent.requestPermission()の返り値は現在の権限の状態deneidgrantedが非同期で渡されます。
    1. 権限を持っている状態grantedだった場合にPlayCanvasのenterVr関数でVRの状態(画面が2分割になり、回転などを取得している)にします。
  • デバイスの取得の方方はこちらを参考にいたしました
    iOS13でWebARとWebVRにおけるデバイスモーション設定が改善しました!

web-vr-ui.js
    const onEnterVrPressedEventIOS13 = function(){

          if ( DeviceOrientationEvent && DeviceOrientationEvent.requestPermission && typeof DeviceOrientationEvent.requestPermission === 'function') {
            DeviceOrientationEvent.requestPermission().then((state) => {
                if(state === "granted"){
                    self.camera.camera.enterVr(function (err) {
                        if (err) {
                            console.warn(err);
                        } 
                });
                }else if(state === "denied"){
                    alert("Permission is denied !")
                }

            })
          }

    }

    if(window.navigator.userAgent.includes("OS 13")){
        this.vrButtonDiv.addEventListener('click', onEnterVrPressedEventIOS13, false);

    }else{
        this.vrButtonDiv.addEventListener('click', onEnterVrPressedEvent, false);
        onEnterVrPressedEvent();
    }

};

表示できました

この一連の動作を見てみるとこのような表示になります。

※Gif

実際に使っている端末で動かす

このツイートまたはQRから飛ぶことができます。


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

Slick導入しようとjQuery is not definedが表示されて困っている。

Slick導入しようとjQuery is not definedが表示されて困っている。
最初の記事に書いてあったscript入れても何も起こらなかった。

こちらを参考にして、

<script type="text/javascript" src="http://code.jquery.com/jquery-1.11.0.min.js"></script>

を書くとエラーは表示されなくなった。

srcが問題であった。

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

数字当てゲーム

<!DOCTYPE html>





Document


<h1>Number Guessing Game</h1>

<p>We have selected a random number between 1 to 100. See if you can guess it within 10 times turns or fewer. we'll tell you if your guess was too high or low.</p>

<div class="form">
    <label for="guessField">Enter a guess:</label><input type="text" id="guessField" class="guessField">

    <input type="submit" value="Submit guess" class="guessSubmit">
</div>

<div class="resultParas">
    <p class="guesses"></p>
    <p class="lastResult"></p>
    <p class="lowOrHi"></p>
</div>

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


.....+html

'use strict';
{
var randomNumber = Math.floor(Math.random() * 100) + 1;

var guesses = document.querySelector('.guesses');
var lastResult = document.querySelector('.lastResult');
var lowOrHi = document.querySelector('.lowOrHi');

var guessSubmit = document.querySelector('.guessSubmit');
var guessField = document.querySelector('.guessField');

// 予想する回数
var guessCount = 1;

// ゲームリセットする
var resetButton;

function checkGuess() {
    var userGuess = Number(guessField.value);

    if(guessCount === 1){
        guesses.textContent = '前回の予想:';
    }

    guesses.textContent += userGuess + ' ,';

    if(userGuess === randomNumber){
        lastResult.textContent = 'おめでとう!正解です!';
        lastResult.style.backgroundColor = 'green';
        lowOrHi.textContent = '';
        setGameOver();  // ゲーム終了させ、リセットする感じの関数かな
    }else if(guessCount === 10){
        lastResult.textContent = 'ゲームオーバー!';
        setGameOver();
    }else{
        lastResult.textContent = '間違いです!';
        lastResult.style.backgroundColor = 'red';
        if(userGuess < randomNumber){
            lowOrHi.textContent = '今の予想は小さすぎます!';
        }else if(userGuess > randomNumber){
            lowOrHi.textContent = '今の予想は大きすぎです!';
        }
    }

    guessCount++;
    guessField.value = "";
    guessField.focus(); // .focus()はどんな関数なのかメソッドなのかまだわからん    
}

guessSubmit.addEventListener('click',checkGuess);



function setGameOver() {
    guessField.disabled = true;
    guessSubmit.disabled = true;
    resetButton.document.createElement('button');
    resetButton.textContent = '新しいゲームを始める';
    document.body.appendChild(resetButton);
    resetButton.addEventListener('click', resetGame);
}

function resetGame(){
    guessCount = 1;

    var resetParas = document.querySelectorAll('.resultParas p');
    for(let i = 0; i < resetParas.length; i++){
        resetParas[i].textContent = '';
    }

    resetButton.parentNode.removeChild(resetButton);

    guessField.disabled = false;
    guessSubmit.disabled = false;
    guessField.value = '';
    guessField.focus();

    lastResult.style.backgroundColor = 'white';

    randomNumber = Math.floor(Math.random() * 100) + 1;  //次の問題で別の数字を用意するために再代入を忘れずに
}

}

......+javascript

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

JavaScriptでもHaskellのwhereみたいなサブ関数を書く

function foo(x) {
  return bar(x + 1);

  function bar(y) {
    return y + 10;
  }
}

function baz() {
  return bar(20);  // ここからはbarにアクセスできないので、便利
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Firestore だけで Alogolia を使わず全文検索

『りあクト! Firebaseで始めるサーバーレスReact開発』カバー画像

(この記事は先日2019年9月22日に開催された技術書典7にて頒布した『りあクト! Firebaseで始めるサーバーレスReact開発』の「4-6. Firestore だけで全文検索を実現する」の内容を紹介したものです)

やっぱりまだ Firestore で提供されない全文検索機能

昨日(2019年9月26日)、スペインのマドリードで開催された Firebase Summit 2019 ですが、新機能が大量にリリースされて開発者は嬉しい悲鳴を上げているようです。詳しくは公式ブログセッションの動画を参照していただくとして、個人的には Firestore コレクションの BigQuery 連携が嬉しかったですね。これからめっちゃ活用していきたいです。

しかしそれでも今回も、Firestore の全文検索機能は提供されませんでした。公式ドキュメントは「Alogolia を使え。以上」となってて取り付く島もない。古参の開発者はおとなしくそれに従っているようですが、最近 Firebase を使い始めた私としてはどうにも納得がいきませんでした。

いちばんの理由は値段。月額 29ドルでかつそれに含まれるのは 5万件までのレコードと 25万回までの読み書きだけ。以降は従量制です。会社として使うには安いのでしょうが、個人開発に使うサービスとしては痛い。
さらに、Cloud Functions で Firestore トリガー関数を用意して随時必要なデータを同期させる必要があるのもわずらわしい。

そこで、シンプルでいいので Firestore 単体で全文検索機能を実現できないか模索しました。

Firestore だけの全文検索、やってみたらできてしまった

結果から言うと可能でした。今回の執筆にあたって「Mangarel」というコミック情報アプリを開発したのですが、そこで作った全文検索機能を提供しているので、まずはどんなものか使ってみてください。

Mangarel 検索ページ

生 Firestore なのでパフォーマンスはめちゃくちゃよくて、このようなインクリメンタルサーチでも遅延もほぼなく実現できてしまいます。なんとこれが無料ですよ、奥さん!
どうやってこれを実現しているかを以降で説明します。

全文検索をどうやって実装しているか

MySQL が全文検索機能を提供してなかったころの昔の話ですが、当時勤務していた某 R 大手ネット通販企業の女性サイトで簡易全文検索を実現したときの方法を流用しました。どうやっていたかというと、オープンソースの形態素解析エンジン「MeCab」をつかって分解した形態素(言語として意味の通る最小単位)を配列フィールドに格納して、検索時にも同じアルゴリズムを使って分解した文字列がその配列に含まれているか判別させていました。

これだとマッチ率の高い順に並べ替えるようなことはできませんが、当時その機能がなかった MySQL 単体で、別途 Elasticsearch のようなサーバを立ててメンテすることなく全文検索を実現できました。

当初、今回も同じことをやろうと考えたのですが問題がありました。

  • 巨大な辞書ファイルの置き場所をどうするか
  • Firestore がクエリーで array-contains を使えるのは一度だけ

前者について、某女性サイトで提供していたのは PHP によるサーバーサイドアプリケーションだったので、辞書ファイルはサーバに置いておけば、データ格納時にもクエリー発行時にもその同じ辞書が使えたので問題なかったのですが、今回は SPA なので同じ方法が使えません。それでどうしたか。N-gram を使いました。

N-gram とは「テキストを N 数の隣り合う文字のかたまりに分解するアルゴリズム」です。N の数が 2 なら「bi-gram」、3 なら「tri-gram」とも呼ばれます。たとえば「王様達のヴァイキング」をその二つの方式で分解するとこうなります。

["王様", "様達", "達の", "のヴ", "ヴァ", "ァイ", "イキ", "キン", "ング"]     // bi-gram
["王様達", "様達の", "達のヴ", "のヴァ","ヴァイ", "ァイキ","イキン", "キング"]  // tri-gram

このやり方だと索引となる配列の容量が形態素解析を使ったときと比べて大きくなるというデメリットもありますが、フロントエンドアプリ内に巨大な辞書ファイルを含める必要がなくなります。また形態素解析だと新たな造語や流行語が変なふうに分解されてしまうことがあり、対応しようとすると辞書のアップデートが必要になりますが、それもいりません。今回の要件にピッタリだったので、この N-gram を使うことにしました。

そして後者の「クエリー内で array-contains が使えるのは1回だけ」問題ですが、これも力技でクリアしました。配列がダメならマップにしようということで、こういうマップを作ってフィールドに格納することにしました。

  tokenMap = {
    '王様': true,
    '様達': true,
    '達の': true,
    'のヴ': true,
    'ヴァ': true,
    'ァイ': true,
    'イキ': true,
    'キン': true,
    'ング': true,
  };

そしてたとえば「王様達の」という語句で検索が実行されたときにはこのようなクエリーを発行するようにします。

const searchWords = ['王様', '様達', '達の'];
let query = db.collection('comics').limit(30);

searchWords.forEach(word => {
  query = query.where(`tokenMap.${word}`, '==', true);
});

const snap = await query.get();

これで実際に『王様達のヴァイキング』がヒットするようになります。

この方法の pros & cons

とりあえず簡易的に全文検索を実現するための方法なので、いくつか以下のような制限があります。

cons1. 検索対象となるテキストサイズの制限

Firestore は 1ドキュメント 1MB という制限があるため、上記のトークンマップを作った上でそのオブジェクトも含めて該当ドキュメントが 1MB に収まるようにしなければいけません。
かつ、ひとつのドキュメントがモテるフィールドは最大 2万なので、他のフィールド数を引いて残った数にトークンの数が制限されます。つまり 1万文字も入れられないということです。

cons2. ソートができない

サーバー側で一切のソートができません。マッチ率などは最初から無理ですが、たとえば検索結果のドキュメントを createdAt などで並べ替えるといったことは対応不可能です。

cons3. 意図しないマッチング

たとえば Mangarel の検索で「ルーム」と検索すると、『グランブルーファンタジー外伝』がヒットしてしまいます。これは分解した ['ルー', 'ーム'] が「グランブルー」の 'ルー' と著者ヨミガナの「サイゲーム」の 'ーム' の二つにマッチングするためです。
この手法のデメリットと言うより、N-gram の欠点と言ったほうがいいかもしれません。

 
逆に、Algolia などと比較して以下のような長所もあります。

pros1. 高パフォーマンス

生 Firestore なのでとにかく速いです。

pros2. ノーメンテでのフィルタリング

ソートはできませんが、他のフィールドと併せてのフィルタリングは可能です。しかもあらかじめ、複合インデックスを作る必要もありません。
Algolia などで同じことをしようとすると、そのフィールドデータを Aloglia に送って同期させる必要があります。

まとめ

以上、私が Firestore だけで全文検索を実装した方法を簡単に紹介しました。
なお、『りあクト! Firebaseで始めるサーバーレスReact開発』 ではこの方法がサンプルコード付きでくわしく解説されていますので、興味のある方はそちらをご覧ください。

サンプルコードは Apache ライセンスですので、適当に加工して使っていただいてかまいません。
Firebase、新機能が追加されてどんどん使いでが良くなっているので、日本でも盛り上げていきたいですね!

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

Firestore だけで Algolia を使わず全文検索

『りあクト! Firebaseで始めるサーバーレスReact開発』カバー画像

(この記事は先日2019年9月22日に開催された技術書典7にて頒布した『りあクト! Firebaseで始めるサーバーレスReact開発』の「4-6. Firestore だけで全文検索を実現する」の内容を紹介したものです)

やっぱりまだ Firestore で提供されない全文検索機能

昨日(2019年9月26日)、スペインのマドリードで開催された Firebase Summit 2019 ですが、新機能が大量にリリースされて開発者は嬉しい悲鳴を上げているようです。詳しくは公式ブログセッションの動画を参照していただくとして、個人的には Firestore コレクションの BigQuery 連携が嬉しかったですね。これからめっちゃ活用していきたいです。

しかしそれでも今回も、Firestore の全文検索機能は提供されませんでした。公式ドキュメントは「Algolia を使え。以上」となってて取り付く島もない。古参の開発者はおとなしくそれに従っているようですが、最近 Firebase を使い始めた私としてはどうにも納得がいきませんでした。

いちばんの理由は値段。月額 29ドルでかつそれに含まれるのは 5万件までのレコードと 25万回までの読み書きだけ。以降は従量制です。会社として使うには安いのでしょうが、個人開発に使うサービスとしては痛い。
さらに、Cloud Functions で Firestore トリガー関数を用意して随時必要なデータを同期させる必要があるのもわずらわしい。

そこで、シンプルでいいので Firestore 単体で全文検索機能を実現できないか模索しました。

Firestore だけの全文検索、やってみたらできてしまった

結果から言うと可能でした。今回の執筆にあたって「Mangarel」というコミック情報アプリを開発したのですが、そこで作った全文検索機能を提供しているので、まずはどんなものか使ってみてください。

Mangarel 検索ページ

生 Firestore なのでパフォーマンスはめちゃくちゃよくて、このようなインクリメンタルサーチでも遅延もほぼなく実現できてしまいます。なんとこれが無料ですよ、奥さん!
どうやってこれを実現しているかを以降で説明します。

全文検索をどうやって実装しているか

MySQL が全文検索機能を提供してなかったころの昔の話ですが、当時勤務していた某 R 大手ネット通販企業の女性サイトで簡易全文検索を実現したときの方法を流用しました。どうやっていたかというと、オープンソースの形態素解析エンジン「MeCab」をつかって分解した形態素(言語として意味の通る最小単位)を配列フィールドに格納して、検索時にも同じアルゴリズムを使って分解した文字列がその配列に含まれているか判別させていました。

これだとマッチ率の高い順に並べ替えるようなことはできませんが、当時その機能がなかった MySQL 単体で、別途 Elasticsearch のようなサーバを立ててメンテすることなく全文検索を実現できました。

当初、今回も同じことをやろうと考えたのですが問題がありました。

  • 巨大な辞書ファイルの置き場所をどうするか
  • Firestore がクエリーで array-contains を使えるのは一度だけ

前者について、某女性サイトで提供していたのは PHP によるサーバーサイドアプリケーションだったので、辞書ファイルはサーバに置いておけば、データ格納時にもクエリー発行時にもその同じ辞書が使えたので問題なかったのですが、今回は SPA なので同じ方法が使えません。それでどうしたか。N-gram を使いました。

N-gram とは「テキストを N 数の隣り合う文字のかたまりに分解するアルゴリズム」です。N の数が 2 なら「bi-gram」、3 なら「tri-gram」とも呼ばれます。たとえば「王様達のヴァイキング」をその二つの方式で分解するとこうなります。

["王様", "様達", "達の", "のヴ", "ヴァ", "ァイ", "イキ", "キン", "ング"]     // bi-gram
["王様達", "様達の", "達のヴ", "のヴァ","ヴァイ", "ァイキ","イキン", "キング"]  // tri-gram

このやり方だと索引となる配列の容量が形態素解析を使ったときと比べて大きくなるというデメリットもありますが、フロントエンドアプリ内に巨大な辞書ファイルを含める必要がなくなります。また形態素解析だと新たな造語や流行語が変なふうに分解されてしまうことがあり、対応しようとすると辞書のアップデートが必要になりますが、それもいりません。今回の要件にピッタリだったので、この N-gram を使うことにしました。

そして後者の「クエリー内で array-contains が使えるのは1回だけ」問題ですが、これも力技でクリアしました。配列がダメならマップにしようということで、こういうマップを作ってフィールドに格納することにしました。

  tokenMap = {
    '王様': true,
    '様達': true,
    '達の': true,
    'のヴ': true,
    'ヴァ': true,
    'ァイ': true,
    'イキ': true,
    'キン': true,
    'ング': true,
  };

そしてたとえば「王様達の」という語句で検索が実行されたときにはこのようなクエリーを発行するようにします。

const searchWords = ['王様', '様達', '達の'];
let query = db.collection('comics').limit(30);

searchWords.forEach(word => {
  query = query.where(`tokenMap.${word}`, '==', true);
});

const snap = await query.get();

これで実際に『王様達のヴァイキング』がヒットするようになります。

この方法の pros & cons

とりあえず簡易的に全文検索を実現するための方法なので、いくつか以下のような制限があります。

cons1. 検索対象となるテキストサイズの制限

Firestore は 1ドキュメント 1MB という制限があるため、上記のトークンマップを作った上でそのオブジェクトも含めて該当ドキュメントが 1MB に収まるようにしなければいけません。
かつ、ひとつのドキュメントが持てるフィールドは最大 2万なので、他のフィールド数を引いて残った数にトークンの数が制限されます。つまり 1万文字も入れられないということです。

cons2. ソートができない

サーバー側で一切のソートができません。マッチ率などは最初から無理ですが、たとえば検索結果のドキュメントを createdAt などで並べ替えるといったことは対応不可能です。

cons3. 意図しないマッチング

たとえば Mangarel の検索で「ルーム」と検索すると、『グランブルーファンタジー外伝』がヒットしてしまいます。これは分解した ['ルー', 'ーム'] が「グランブルー」の 'ルー' と著者ヨミガナの「サイゲーム」の 'ーム' の二つにマッチングするためです。
この手法のデメリットと言うより、N-gram の欠点と言ったほうがいいかもしれません。

 
逆に、Algolia などと比較して以下のような長所もあります。

pros1. 高パフォーマンス

生 Firestore なのでとにかく速いです。

pros2. ノーメンテでのフィルタリング

ソートはできませんが、他のフィールドと併せてのフィルタリングは可能です。しかもあらかじめ、複合インデックスを作る必要もありません。
Algolia などで同じことをしようとすると、そのフィールドデータを Algolia に送って同期させる必要があります。

まとめ

以上、私が Firestore だけで全文検索を実装した方法を簡単に紹介しました。
なお、『りあクト! Firebaseで始めるサーバーレスReact開発』 ではこの方法がサンプルコード付きでくわしく解説されていますので、興味のある方はそちらをご覧ください。

サンプルコードは Apache ライセンスですので、適当に加工して使っていただいてかまいません。
Firebase、新機能が追加されてどんどん使いでが良くなっているので、日本でも盛り上げていきたいですね!

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

モーダルウィンドウを開いたときだけ画像をロードする

画像を遅延ロードしてるのにPSIのスコアがなぜか低い、と思ったらモーダルウィンドウ内の画像が遅延ロードされていなかったのでメモ
ただしこの方法だとモーダルウィンドウを開いたときにモーダル内の画像が全て読み込まれるので注意

モーダルウィンドウを開いたときだけモーダル内の画像をロードする

ざっくり書くとimgタグのsrcに読み込み前の画像、data-srcに読み込み後の画像を配置して、
モーダルウィンドウが開いたときにjQueryでdata-srcをsrcに代入しています。

モーダルウィンドウはBootstrap4で実装しています。

modalwindow
<!-- モーダルの設定 -->
<div class="modal fade myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
  <div class="modal-dialog modal-xl" role="document">
    <div class="modal-content">
      <div class="modal-body text-center">
        <!-- srcに読み込み前の画像、data-srcに読み込み後の画像 -->
        <img class="lazy_load" src="noimage.jpg" data-src="something.jpg">
        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
      </div>
    </div><!-- /.modal-content -->
  </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

jQueryは以下のようになります

modalwindowlazyload
$('.myModal').on("show.bs.modal", function () {
  $('.lazy_load').each(function(){
      var img = $(this);
      img.attr('src', img.data('src'));
  });
});

おわり

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

Webpackで複数プロジェクトの一括コンパイルと部分コンパイル

インデックス
* ディレクトリ毎にアプリケーションをコンパイル
* プロジェクト共通のライブラリを設定
* コンパイルするターゲットを決めてビルドする

要件
以下のようなディレクトリ構成があり、それぞれのプロジェクト毎にJavaScriptをコンパイルして、それぞれのプロジェクトディレクトリにコンパイルしたファイルを格納したいといった案件がありました。Webpackでプロジェクトをコンパイルする時、今までは一つのディレクトリにのみ全てのプロジェクトを書き出していたので、ディレクトリ単位に書き出す方法を調べました。また、すべてのプロジェクトではなく、特定のプロジェクトだけビルドする方法も合わせて調べた結果を記載します。

ディレクトリ構造
.
├── package.json
├── src
│   ├── common                <- プロジェクト共通で使うファイル
│   └── projects
│       ├── foo         <- 単一プロジェクト
│       ├── fuga              <- 単一プロジェクト
│       ├── hoge              <- 単一プロジェクト
│       └── pages             <- スペシャルプロジェクト
│           ├── pageOne    <- スペシャルプロジェクトに関連するプロジェクト 
│           ├── pageThree     <- スペシャルプロジェクトに関連するプロジェクト 
│           └── pageTwo       <- スペシャルプロジェクトに関連するプロジェクト 
└── webpack.config.js

通常であれば、webpack.config.jsoutputに以下のような設定をすると複数ページのアプリケーションとしてコンパイルしてくれます(参考)。

特定のディレクトリに別アプリケーションとして書き出し
module.exports = {
  //...
  entry: {
    pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
    pageThree: './src/pageThree/index.js'
  },  
  output: {
    filename: '[name].js',
    path: __dirname + '/dist'
  }
};

ディレクトリ毎にアプリケーションをコンパイル

今回は一つのディレクトリにまとめて書き出すのではなく、プロジェクトディレクトリ毎にそれぞれ書き出したい訳です。

それぞれのプロジェクトディレクトリに書き出し
│   └── projects
│       ├── foo
│       │   ├── app.js
│       │   ├── dist          <- ここに書き出して管理したい
│       ├── fuga
│       │   ├── app.js
│       │   └── dist          <- ここに書き出して管理したい
│       ├── hoge
│       │   ├── app.js
│       │   └── dist          <- ここに書き出して管理したい

そのため、少しトリッキーかもしれませんが、entryoutputの設定を変更し、
複数ディレクトリに書き出しできるような設定に変更しました。今回は各プロジェクトに distというフォルダを作成し、その中にコンパイル後のファイルを書き出します。イメージとしてはこんな感じです。

イメージ
module.exports = {
  //...
  entry : {
  './src/projects/path-to-project/dist/hashfilename.js': './src/projects/path-to-project/app.js',
  },
  output: {
    path: __dirname,
    filename: '[name]'
  }
};

output.pathは書き出す先のパスを指定します。今回は書き出す先のディレクトリをルート直下に指定し、ファイル名にパスを含めた書き出し先を指定しました。outputで使っている[name]はWebpack側で定義されている設定でentrykeyとなっている部分を置換します。なので上記の設定だと ./src/projects/path-to-project/dist/hashfilename.js[name]に入ることになります。

余談ですが、プロジェクト単位で作成されるdistディレクトリをgitの管理から外す場合は.gitignoreに以下のように記述すると良いです。

.gitignoreの設定
/**/dist/

プロジェクト共通のライブラリを設定

それぞれのプロジェクトで共通して使いたいクラスは以下の設定で使用可能になります。

ライブラリパスの設定
module.exports = {
  //...
  resolve: {
    module: [
      path.resolve(__dirname, './src/common'), 
    ];
  }
};

こうすると./src/common が名前空間になり、例えば./src/common/Common.jsのファイルをプロジェクトで使いたい場合、import文は以下の書式になります。

import Common from 'Common';

また、./src/common/particle/flower.jsの場合はそのままサブディレクトリを繋げれば良いです。

import Common from 'particle/flower';

コンパイルするターゲットを決めてビルドする

複数プロジェクトのビルドに対応しておけば、1つのプロジェクトだけビルドしたい場合や、2つのプロジェクトだけビルドしたい場合など、特定のプロジェクトに絞ってビルドすることも可能になります。
方法はnpm run buildとする際に、webpack.config.jsに環境変数(env)としてビルドターゲットのプロジェクト名を渡します。こんな感じです。

npm run build -- --env.target="hoge"

このコマンドには2つの意味が含まれているのですが、--env.target="hoge"の部分がwebpack.config.jsに環境変数(env)を渡している部分となります。
また、webpack.config.jsで環境変数(env)を受け取る場合、エクスポートを関数に変更する必要があります。
https://webpack.js.org/guides/environment-variables/

To use the env variable, you must convert module.exports to a function:

環境変数(env)の渡し方
webpack --env.NODE_ENV=local --env.production --progress
環境変数(env)の受け取り方
const path = require('path');

module.exports = env => {
  // Use env.<YOUR VARIABLE> here:
  console.log('NODE_ENV: ', env.NODE_ENV); // 'local'
  console.log('Production: ', env.production); // true

  return {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };
};

私の場合はenvtargetプロパティを追加し、targetにプロジェクトディレクトリがあれば、そのプロジェクトのみビルドするようにしました。なお、プロジェクトのファイルは全て統一してapp.jsしています。

// NOTE: Use function in order to pass environment variables.
// @see https://webpack.js.org/guides/environment-variables/
module.exports = (env, {watch} ) => {
  let entry = {};

  if (env && env.target) {
    // NOTE: Divide target string into array by comma.
    const targetFiles = env.target.split(',').map(s => s.trim())

    for (file of targetFiles) {
      // NOTE: ここでプロジェクト毎にentryを作成しています(割愛)
      const filePath = './' + join(targetDir, 'app.js');
      const anEntry = createEntry(filePath);
      Object.assign(entry, anEntry);
    }
  } else {
    // NOTE: 全てのエントリー情報
   entry = createAllEntry();
  }

  return {
    //...
    entry: entry,
    output: {
      path: __dirname,
      filename: '[name]'
    },    
  }
})  

ここでもうひとつnpm run build -- --env.target="hoge" 実行時のbuildの後の-- についてですが、これのダブルハイフン以降に定義する変数を実行時のファイルに変数として渡すという意味です。例えば、以下のnpmパッケージがあり、

package.json
 {
    script: 
    {
        server: "node index.js"
    }
 }

以下のコマンドを実行すると、index.jsに環境変数「--port=8080」が渡されます。

npm run server -- --port=8080

これは、通常のnodeコマンドを実行するときと同じ意味になります。

node server.js --port=1337

npmコマンドに渡す変数と、スクリプトに渡す変数の2つの違いがあるということですね。

Conclusion

今回は複数プロジェクトをビルドする時にプロジェクト毎にディレクトリを指定してビルドする方法と、プロジェクトの一部だけビルドする方法を調査しました。部分ビルドについてはプロジェクトが膨大になった時にビルド時間を短縮するために有効そうです。この部分ビルドはwatchオプションをつけた時にも適用されるので、開発していくときに便利かもしれません。

今回の検証で使ったプロジェクトはGihubに置いてありますので、何かの参考になれば幸いです。

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

Google Maps APIのマーカーをDOMでつくる

Google Maps APIで利用して設置できるマーカーは
精々画像が使えるくらいで融通が利かないなと常々思っていた。

画像が使えるのだからPhotoShopなんかで自由にお絵かきすればいいんだけど、
個人的にはDOMをCSSでデザインするほうが楽だし、
クリックなんかのイベントでマーカーの色を変えたりしたい。

Custom Overlays

結構苦労して探したらどうやらCustom OverlaysなるものでDOMは配置できるらしい。

サンプルなんかも拾えた。
http://www.mwsoft.jp/programming/googlemap/google_map_v3_custom_overlay.html

リファレンスでもサンプルでもprototypeの新しいインスタンスに追加って書いてある。

CustomMarker.prototype = new google.maps.OverlayView();

でもどうせならES6でやりたい。
できもしないのに変に拘る。ハマる元。

血眼で探したらようやくあったサンプル

https://codepen.io/mock/pen/rWzPeR

これを参考に

src/js/CustomMarker.js
/**
 * class CustomMarker
 */
export default class CustomMarker extends google.maps.OverlayView{
  /**
   * constructor
   * @param object map
   * @param float lat
   * @param float lng
   * @param boolean isDisplay
   * @param boolean isSelected
   */
  constructor(map,lat,lng,isDisplay = true,isSelected = false){
    super();
    this.position = {
      lat:lat,
      lng:lng
    };
    this.isDisplay = isDisplay;
    this.isSelected = isSelected;

    this.setMap(map);

    this.el = null;
  }

  /**
   * onAdd
   */
  onAdd(){
    this.el = document.createElement('div');

    const className = 'custom-marker' + ((this.isSelected) ? '--selected': '');
    this.el.classList.add(className);

    if(!this.isDisplay){
      this.el.style.display = 'none';
    }

    const panes = this.getPanes();
    panes.overlayLayer.appendChild(this.el);

    // click event
    panes.overlayMouseTarget.appendChild(this.el);
    google.maps.event.addDomListener(this.el, 'click', () => {
      google.maps.event.trigger(this, 'click');
      this.setSelected(true);
    });
  }


  /**
   * draw
   */
  draw(){
    if(!this.el) return;

    const point = this.getProjection().fromLatLngToDivPixel(new google.maps.LatLng(this.position.lat, this.position.lng));
    this.el.style.position = 'absolute';
    this.el.style.left = point.x + 'px';
    this.el.style.top = point.y + 'px';
  }

  /**
   * onRemove
   */
  onRemove(){
    if(!this.el) return;
    this.el.parentNode.removeChild(this.el);
    this.el = null;
  }

  /**
   * 選択状態のset
   */
  setSelected(boolean){
    if(boolean === this.isSelected) return;

    this.isSelected = boolean;
    this.switchClass();
  }

  /**
   * 表示フラグのset
   */
  setDisplay(boolean){
    if(boolean === this.isDisplay) return;

    this.isDisplay = boolean;
    this.applyStlyes();
  }

  /**
   * switch class list
   */
  switchClass(){
    if(!this.el) return;

    const className = 'custom-marker' + ((this.isSelected) ? '--selected': '');
    this.el.classList.replace(this.el.classList.item(0),className);
  }

  /**
   * apply styles
   */
  applyStlyes(){
    if(!this.el) return;

    this.el.style.display = (!this.isDisplay) ? 'none' : '';
  }
}

リファレンスを読む限りではonAdd()draw()onRemove()のメソッドを実装しなければいけないらしい、
- onAdd()でDOMの用意とイベントの追加
- draw()で描画
- onRemove()は削除
って認識で合っているのかな?

ついでに選択や表示のフラグを追加して、class名を変えたりstyleを変えたり。

あとは↑のオブジェクトをimportしてGoogle Mapを初期化した後に呼び出す。

src/js/index.js
import CustomMarker from './CustomMarker';

document.addEventListener('DOMContentLoaded', () => {
  initMap();
});

function initMap(){
  const lat = 36.0652196;
  const lng = 136.219452;

  const options = {
    gestureHandling: 'greedy',
    disableDefaultUI: true,
    mapTypeId: google.maps.MapTypeId.ROADMAP,
    center: new google.maps.LatLng(lat,lng),
    zoom: 14,
    clickableIcons: false
  };

  const map = new google.maps.Map(document.getElementById('map'), options);

  const marker = new CustomMarker(map,lat,lng);

  document.getElementById('deselected').addEventListener('click',() => {
    marker.setSelected(false);
  });

  document.getElementById('switch-display').addEventListener('click',() => {
    marker.setDisplay(!marker.isDisplay);
  });
}

CSSで装飾

    .custom-marker{
      background:#00FF00;
      width:10px;
      height:10px;
      border-radius:50%;
      cursor:pointer;
    }
    .custom-marker--selected{
      background:#FF0000;
      width:10px;
      height:10px;
      border-radius:50%;
      cursor:pointer;
    }

結果

CustomMarker.png

自由にデザインしたいとか言っておいてこのセンス・・・。
とりあえずできたからよし。
あとは用途に合わせてカスタマイズすればいい。
実務ではVueなんかと組み合わせて使ってたりしている。

リポジトリ

https://github.com/torabe/custom-marker

感想

結構重宝すると思うんだけど情報少なすぎない?

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

d3のforcesimulationで自分の指定したidを使う(Uncaught Error: missing: hogehoge)

はじめに

d3-forceのドキュメント(github - d3/d3-force)で、idは自分で決めてもいいよとあったので試したらハマった。

var nodes = [
  {"id": "Alice"},
  {"id": "Bob"},
  {"id": "Carol"}
];

var links = [
  {"source": "Alice", "target": "Bob"},
  {"source": "Bob", "target": "Carol"}
];

対処法

以下のような感じでsimulationを定義する際にlinkidを指定する。

const simulation = d3.forcesimulation()
  .force("link", d3.forceLink().id((d)=>d.id))

おわりに

If id is specified, sets the node id accessor to the specified function and returns this force. If id is not specified, returns the current node id accessor, which defaults to the numeric node.index
github - d3/d3-force

ドキュメントを読んだら普通に書いてあった。説明書を読む習慣をつけないと...

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