- 投稿日:2019-02-11T23:27:50+09:00
Nuxt.js(SPAモード)でユーザー認証を効率的にする方法を解説
こんにちは、とくめいチャットサービス「ネコチャ」運営者のアカネヤ(@ToshioAkaneya)です。
今回は、Nuxt(SPAモード)で、ミドルウェアを使いユーザー認証をする方法を解説します。
user/auth
というエンドポイントが、クッキーやローカルストレージを元に認証済みユーザーを返すAPIだとします。以下のように、Vuexにuserが登録されていなければ、authを呼び出すことで、APIの呼び出しを抑えてログイン処理を書くことが出来ます。
クッキーやローカルストレージにトークンが保存されてるブラウザで、Nuxtアプリに訪れた時に任意のページについてユーザー認証を済ませることが出来ます。
middleware/auth.jsexport default async ({store}) => { if (!store.getters['user']) { await store.dispatch('auth') } }nuxt.config.jsrouter: { middleware: ['auth'] },store/index.jsexport const state = () => ({ user: null }) export const getters = { user: (state) => state.user, } export const mutations = { setUser(state, { user }) { state.user = user } } export const actions = { async auth({ commit }) { const user = await this.$axios.$get(`/users/auth`) commit('setUser', { user }) } }はてなブックマーク・Pocketはこちらから
- 投稿日:2019-02-11T23:08:28+09:00
紙ベースで実施されていた運用をシステム化してみた
はじめに
2年前から私たちのチームは「フットサルのリーグ戦」にが参加させて頂いています。
このリーグ戦は15年続いているそうです!
リーグ戦の概要
・毎月1回開催(年間12回)
・各回、4チームの総当り戦を2セット行う
・年間のチームの勝点で順位を決める
・年間の得点王を決める
◆2019年1月に行われた試合で使用した対戦表
この紙に各試合の審判が得点、得点者を入力。
その後、主催者が毎回チームの勝点と得点者の得点を集計されています。
(よく見ると勝点でおかしい箇所があるような気がします)システム化しようとした背景
・紙ベースの運用されていて、主催者がデータ集計をするのが大変だと思ったから
・チーム年間順位・得点王、試合結果をいつでも参照できるようにしたいと思ったから
・集計を正しくしたいと思ったから上記内容を対応するために
「得点入力画面で得点をDBに格納して、試合終了後、結果を集計する」ことで
主催者の負荷を下げ、参加者はいつでも結果をみることができると思い、簡易システムを作成しました。もう一つ理由があり
12月末から1月末まで骨折していたので、フットサルできなくて暇だったのがあります。。実現方法
実現するにあたり、以下の3つを検討
1.どんな情報が見たいか、得点入力画面の検討
2.1.でやりたい事のデータの持たせ方の検討(DB設計)
3.環境の検討(サーバ、アプリの検討)1.どんな情報が見たいか、得点入力画面の検討
まずは、Excelでイメージを書いてみる
得点入力画面
得点入力は、入力させずに「+」、「-」ボタンで得点がUP、DOWNするようにする。
※入力チェック処理しなくていいし、また、ボタンだけの操作なので、システム化して得点入力が面倒くさくなったと感じさせないためです。
2.1.でやりたい事のデータの持たせ方の検討(DB設計)
大きく分けて、「マスタ」と「データ入力用」と「データ参照用」とのテーブルを作成。
※年間順位、年間得点王は、各回の試合結果入力後に集計させ、参照させる。
各試合終了後、得点集計処理を実施して、データ参照用テーブルを更新。
※実際のテーブル名、フィールド名は英語にしてます
3.環境の検討(サーバ、アプリの検討)
無料でサーバを使いたいので、無料枠 の GCE(Google Compute Engine) を使用
https://cloud.google.com/free/?hl=ja
また、リーグ戦の参加者は、PC、スマホ(大部分はスマホ)で結果をみると思うので、
PC、スマホと勝手に見せ方を調整してくれるWordPressを選択。
データ格納DBは無料のmariaDB、phpはWordPressを動かす上で必要となります。3-1 GCE(Google Compute Engine)の構築
以下の記事を参考にさせて頂きました。ありがとうございました。
GCP上でWordPressを無料で構築したい
GCP(Google Cloud Platform)での無料GCE(Google Compute Engine)インスタンス作成
GCE の無料枠のサーバを立るときに、初見でハマりそうなところ
無料のドメインを取得する
Google Cloud DNSでIPアドレスとドメイン名を紐付けるGCE(Google Compute Engine)の無料枠だとメモリが少ないので WordPressの画面を開くと、mariaDBがメモリが確保できないためエラーとなってしまい、WordPressの画面がみれない状態になってしまいました。
追加の設定としてSWAPを設定しました。
設定後、mariaDBのエラーは発生してません。$ sudo dd if=/dev/zero of=/swapfile bs=1M count=1024 $ sudo chmod 600 /swapfile $ sudo mkswap /swapfile $ sudo swapon /swapfileインスタンスを再起動した時にswapが自動マウントされるようにfstabに設定をします。
※ /etc/fstab 以下の設定を追加/swapfile none swap sw 0 03-2 WordPressでの開発(はまったところのメモ)
ちょっと躓いたところをメモとして残します。
結局自分でHTMLを書いたので、なにもレイアウトの設定がない「固定ページ」で全て書いていますその1. WordPressで自作のphpコードを実行する場合、WordPress固有の設定が必要
これGCEにWordPress環境構築してから気がつきました。。
WordPressで自分で書いたPHPを実行する場合、
ショートコードというものを使用する必要があります。
ショートコード API◆ショートコード記載場所
WordPressインストールディレクトリの
「/wp-content/themes/テーマ名」直下のfunctions.phpがショートコードを記載するファイルとなります。◆ショートコードを書いてみる
functions.php を編集
サンプルでは、phpのファンクション「sampletest」を定義して
そのファンクション「sampletest」をショートコード「f_sampletest」と紐付けています
※sudo vi functions.php で更新ですfunctions.php/* ショートコードサンプル */ function sampletest() { return "Hello"; } add_shortcode('f_sampletest', 'sampletest');注意点はこのfunctions.phpはWordPressのページ表示/管理画面の表示でコールされます。
functions.phpで構文エラー(;がない等)の場合、WordPressがみれなくなりますので
編集する前にバックアップを取得しておくことをお勧めします。画面が表示される際にショートコード(f_sampletest)が実行され、ショートコードで設定した文字が表示される
◆ショートコードに引数を渡す場合
functions.php/* ショートコード引数あり */ function sampletest_argv($attr) { $msg = $attr[0]; return $msg; } add_shortcode('f_sampletest_argv', 'sampletest_argv');[ショートコード 引数] を設定⇒[更新]クリック⇒[固定ページを表示]クリック
画面が表示される際にショートコード(f_sampletest_argv)が実行され、ショートコード実行時に設定した引数の文字が表示される
◆実際のアプリで使用したショートコード抜粋
functions.phpfunction view_top($attr) { global $wpdb; //引数(年度)を取得 $yyyy = $attr[0]; $listresult = "</tbody></table>"; $listresult .= "<br><strong>◆得点ランキング(上位3まで)</strong><br>"; //得点王取得SQL $strsql ="select "; $strsql .=" r.rank,t.teamname,p.playername ,r.score"; $strsql .=" from "; $strsql .=" topscore r, "; $strsql .=" team t, "; $strsql .=" player p "; $strsql .=" where "; $strsql .=" r.yyyy = %d "; $strsql .=" and r.teamno = t.teamno "; $strsql .=" and r.playerid = p.playerid "; $strsql .=" and rank < 4 "; $strsql .=" order by r.rank,p.playername "; $sql = $wpdb->prepare($strsql, $yyyy); $rows = $wpdb->get_results($sql); //table ヘッダ情報 $listresult .= "<table class=\"wp-table-yoko\"><thead><tr><th>順位</th><th>氏名</th><th>チーム名</th><th>得点</th></tr></thead><tbody>"; //テーブルレコード設定 foreach($rows as $row){ $listresult .= "<tr><td>". $row->rank . "</td><td>" . $row->playername . "</td><td style =\"text-align: left;\">" . $row->teamname . "</td><td>" . $row->score . "</td></tr>"; } $listresult .= "</tbody></table>"; //画面表示 return $listresult; } add_shortcode('f_viewtop', 'view_top');ショートコード(f_viewtop)を設定⇒[固定ページを表示]クリック
ショートコード(f_viewtop)に引数を設定(2019)して実行した結果。
その2. 自作のJavaScriptを使用する場合、WordPress固有の設定が必要
これもWordPressの設定が必要です。
得点入力画面で「+」、「-」ボタンのみで得点入力するようにしました。
そのため、「+」、「-」ボタンが押されたタイミングで、JavaScriptで得点表示を
変更する必要がありました。(赤枠の「+」、「-」ボタンをクリックすると対応する得点が加算、減算されます)
手順1 JavaScriptを作成して格納
「/wp-content/themes/テーマ名/js」に 作成したJavaScript(score.js)を格納しますscore.jsfunction incValue(idname){ // value値を取得する var obj = document.getElementById(idname); //valueに値をセットする obj.value = parseInt(obj.value) + parseInt(1); } function decValue(idname){ // value値を取得する var obj = document.getElementById(idname); score = parseInt(obj.value) if (score > 0){ //valueに値をセットする obj.value = parseInt(obj.value) - parseInt(1); } }手順2 functions.php にjavascriptに関する記載を書く
functions.phpfunction score_scripts() { wp_enqueue_script( 'score-script', get_template_directory_uri().'/js/score.js' , array(), date('U')); } add_action( 'wp_enqueue_scripts', 'score_scripts' );この設定で自前のJavaScriptが動くようになります
その3.データ登録方法(POST送信)
データ登録方法は、「データ入力画面」からPOST送信して、
送信された側の「データ登録終了画面」に設定したショートコードでデータ登録させる方法にしました。
※実際のアプリで使用したデータ登録をサンプルとして残します。手順1 POST送信する固定ページの設定
POST送信する固定ページにformタグを設定して、POST送信するように記載。
formタグの中でショートコードをコールするようにします。
(ショートコードの中で、submitボタンを作成してます)
手順2 POST受信する固定ページの設定
ショートコード(f_postdata)のみ記載
f_postdataのショートコード(抜粋)
functions.phpfunction postdata() { global $wpdb; //POSTされたデータを受け取る $yyyy = $_POST['yyyy']; $mm = $_POST['mm']; $seq = $_POST['seq']; $team1 = $_POST['team1']; $team2 = $_POST['team2']; $score1 = $_POST['score1']; $score2 = $_POST['score2']; <処理省略> //得点を登録 $strsql = "update matchresults "; $strsql .= " set score1 = %d ,"; $strsql .= " score2 = %d "; $strsql .=" where "; $strsql .=" yyyy= %d "; $strsql .=" and mm= %d "; $strsql .=" and seq= %d "; $sql = $wpdb->prepare($strsql,$score1,$score2, $yyyy,$mm,$seq); $rows = $wpdb->get_results($sql); <処理省略 この後、登録したデータを表示する処理> } add_shortcode('f_postdata', 'postdata');POST送信⇒POST受信した固定ページでショートコード(f_postdata)を実行した結果
その4.ランク付け集計方法
mariaDBでOracleのRANK・DENSE_RANKを取得するSQL文使用できない
https://docs.oracle.com/cd/E16338_01/server.112/b56299/functions052.htm簡単に言うと、RANKはある列の値に順位をつけてくれるものになります。
DENSE_RANKは、順位をつけるのですが、同一値の場合に順位を飛ばさずに順位をつけます。
※本リーグ戦の運用では、得点王はDENSE_RANKで順位をつけています。mariaDBでDENSE_RANKするSQLを記載
ダミーテーブルとサンプルデータとRank取得するSQL--得点データ格納するテーブル create table test_scoring( yyyy int NOT NULL, mm int NOT NULL, seq int NOT NULL, playerid int NOT NULL, score int, PRIMARY KEY(yyyy,mm,seq,playerid) ); --ダミーデータを入れる insert into test_scoring values(2019,1,1,17,1); insert into test_scoring values(2019,1,1,18,1); insert into test_scoring values(2019,1,1,19,1); insert into test_scoring values(2019,1,2,9,1); insert into test_scoring values(2019,1,3,13,1); insert into test_scoring values(2019,1,3,11,1); insert into test_scoring values(2019,1,4,3,1); insert into test_scoring values(2019,1,4,20,2); insert into test_scoring values(2019,1,4,18,1); insert into test_scoring values(2019,1,5,17,1); insert into test_scoring values(2019,1,5,12,1); insert into test_scoring values(2019,1,6,14,1); insert into test_scoring values(2019,1,6,15,1); insert into test_scoring values(2019,1,6,5,2); insert into test_scoring values(2019,1,7,12,1); insert into test_scoring values(2019,1,7,4,1); insert into test_scoring values(2019,1,7,8,1); insert into test_scoring values(2019,1,8,16,1); insert into test_scoring values(2019,1,8,20,1); insert into test_scoring values(2019,1,8,21,1); insert into test_scoring values(2019,1,10,18,1); insert into test_scoring values(2019,1,11,15,1); insert into test_scoring values(2019,1,12,3,1); insert into test_scoring values(2019,1,12,8,1); insert into test_scoring values(2019,1,12,18,1); insert into test_scoring values(2019,1,12,22,1); --DENSE_RANKでランク付けするSQL select CASE WHEN @before_sumscore = d.sumscore THEN @rank ELSE @rank:=@rank+1 END AS rank, playerid, @before_sumscore := d.sumscore as sumscore from ( select playerid , sum(score) as sumscore from test_scoring where yyyy=2019 group by playerid ) d, (select @rank:=0, @before_sumscore:=0) s order by sumscore desc ;取得結果
同一得点の場合、Rankは同じ順位に取得されています。
また、順位が飛ばされていません。
作成した画面と遷移図
成績の参照は見たい情報が見れるようにしました。
得点入力画面は簡単なボタンクリックのみでできる画面にしました。2月リーグ戦で主催者への説明
タブレットを持っていき、主催者の前で得点入力、集計処理(得点・得点王)を行い説明をしました。
主催者とお話して以下の予定で進めることになりました。
今後の予定
3月は紙ベース運用とシステムの平行運用
4月からはシステムでの運用残りのタスク
・各試合スケジュールの登録画面の作成。
・使用者からの評価を聞き改善こちらは時間を見つけて対応する予定
- 投稿日:2019-02-11T22:30:18+09:00
JavaScript製のStore管理フレームワークを作りました
作ったもの
javascript製の、Store管理フレームワークです。
ブラウザなどで、使うことができます。(古いブラウザでは、動作しないかもしれません。)画像は、こちらのサービスを使いました。
※フレームワークと言っていいものか分からないですが、この記事ではフレームワークとしておきます。ご了承ください。?♂️
作った経緯
普段React.jsをよく使っていて、Stateの管理にReduxを使ったことがあるのですが、
データフローを理解するのに時間が掛かったのと、データをまとめて管理するため、必要なデータを分けるのがすごく難しく感じました。どうにか簡単にデータを管理できないかと思っていた時に、Typescriptのinterfaceみたいに、
「あらかじめデータの型を定義して置いて、それを使ってデータ管理をしよう!」という発想になりこのフレームワークを作るに至りました。解説
早速ですが、作ったフレームワークの解説をしてこうと思います。
Maker
Maker は、フロントと Store 部分を繋ぐ仲介役です。 Store の更新や、変更の検知などはこの Maker を介して行われます。
以下は、使用例です。
YourStateClass
やChildState
などが出てきますが、それらは、Stateクラスを継承したクラスです。使い方
makerの使い方import Golgua from "golgua"; const maker = Golgua.createMaker( YourStateClass ); // Maker インスタンスを取得 maker.listen( value => console.log(value) ); // { message: Hello Golgua, child_id: 10021 } maker.listenWithState(ChildState, child_id => { console.log(child_id); // 10021 <- update での更新 // 19819 <- updateWithState での更新 }); maker.update({ message: "Hello Golgua", child_id: 10021 }); maker.updateWithState( ChildState, 19819 );createMaker
createMaker
は、Stateクラスを継承したクラスを第一引数にもらってMaker
インスタンスを返します。createMakerconst maker = Golgua.createMaker( YourStateClass );listen
listen
は、データの更新が完了した時に引数に渡されたコールバックを実行します。コールバックの引数には更新した値が渡されます。listenmaker.listen( listen_callback );listenWithState
listenWithState
は、第一引数にStateクラスを継承したクラスのStoreが変更された時のみ、第二引数に渡されたコールバックを実行します。コールバックの引数には更新した値が渡されます。listenWithStatemaker.listenWithState( YourStateClass, listen_callback );update
update
は、引数に渡された値で更新を試みます。この時点では、更新されるかはわからないので注意してください。updatemaker.update( update_value );updateWithState
updateWithState
は、createMaker
で渡したStateクラスの子のStateクラスのStoreのみを更新します。第一引数には、updateWithStatemaker.updateWithState( YourStateClass, update_value );Types
Typesは、StateでのStoreのデータ型を決める関数群です。特定の型を実行することで、Storeの型を定義します。
また、デフォルト値の設定やバリデートなどの昨日もあります。今の所は、以下の型に対応してます。( 今後増やす予定です )
- string型
- number型
- boolean型
- object型
- array型
types定義の例import { Types } from "golgua"; const types = { string: Types.string({ default_value:"Hello Golgua!" }), number: Types.number({ pattern: v => v < 10 }), boolean: Types.boolean({ nullable: false, default_value: false }), object: Types.object({ types: { message: Types.string() } }), array: Types.array({ types: Types.string(), empty: false, default_value: ["Default Message"] }) }上記のコードは、Typesが持つ関数をそれぞれ実行して関数名と同じ型のデータを定義してます。また、引数に
default_value
やpattern
など、オプションを指定して実行することでデフォルト値の設定や更新する時にバリデートをかけたりすることができます。関数を実行するとTypesインスタンスを返します。
対応しているオプションは、以下のとおりです。
Types.string
・Types.number
・Types.boolean
の場合
default_value
: デフォルトの値を設定する時に使います。実行した関数と違う型の値を設定するとエラーになります。( 初期値は、null
です。)pattern
: データが更新される際に、このpattern
に渡された関数が実行され、関数がfalse
を返した時は、データを更新しないようにします。nullable
: nullを許容するかのフラグです。false
に設定すると、nullが値に設定された時にエラーになります。( 初期値は、true
です )
Types.object
・Types.array
の場合
default_value
: 上記と同じです。pattern
: 上記と同じです。nullable
: 上記と同じです。types
: 必須です。objectの場合は、Typesインスタンスのみを含むプレーンなobjectのみ指定可能で、arrayの場合はTypesインスタンスのみ指定可能です。empty
: 空の値を許容するかのフラグです。false
にすると、値が空の時にエラーになります。( 初期値はfalse
)State
Stateは、Typesによって定義されたデータ型を保持し、そのデータ型に沿ったStoreを保持します。また、ライフサイクルを定義することによって、データの更新にフックすることができます。
Stateは、Stateクラスを継承して定義できます。
import { State } from "golgua" class SometingState extends State {}types
types
は、Stateのインスタンス変数です。この変数にTypesで作ったデータ型を設定することで、Storeのデータ型を定義できます。
types
に設定できる値は、Typesインスタンスか、Typesインスタンス又はStateクラスを含むプレーンなオブジェクトだけです。以下に例を示します。↓
Typesインスタンスimport { State, Types } from "golgua"; class SometingState extends State { constructor(){ super(); this.types = Types.string() } }Typesインスタンスを含むプレーンなobjectimport { State, Types } from "golgua"; class SometingState extends State { constructor(){ super(); this.types = { message: Types.string() }; } }TypesインスタンスとStateクラスを含むプレーンなobjectimport { State, Types } from "golgua"; class UserState extends State { constructor(){ super(); this.types = { name: Types.string(), age: Types.number() }; } } class SometingState extends State { constructor(){ super(); this.types = { message: Types.string(), user: UserState }; } }state
stateは、Stateクラスのインスタンス変数です。Storeのデータを保持します。この変数は、参照用なので代入などはしないようにしてください。
this.stateの使用例class SomethingState extends State { /* - 省略 - */ constructor(){ super(); this.types = Types.string({ default_value: "Default Message" }); console.log(this.state); // null この時点では、まだnull this.state = "message"; // NG ☠️ } init(){ console.log( this.state ); // Default Message } }defaultValue
this.types
で設定したdefault_value
の値を取得することができます。defaultValueの使用例import {State,Types} from "golgua"; class SomethingState extends State { constructor(){ super(); this.types = Types.stirng({ default_value: "Hello Golgua!" }); } init(){ console.log(this.defaultValue()); // Hello Goglua! } }ライフサイクル
ライフサイクルは、以下の4つです。
init
willUpdate
didUpdate
updatedCatch
init
Stateクラスが
new
(インスタンス化)された時に、一度実行されます。また、Promiseを返すとresolve
で渡された値で、Storeのデータを更新します。initの使用例class SomethingState extends State { /* - 省略 - */ async init(){ return await ajax_data(); // 通信結果で、Storeのデータを更新 } }willUpdate
Storeのデータが、更新される前に実行されます。また、この関数で返した値が
this.types
で設定した型で判定され、判定が通れば次のStoreのデータとして内部で設定されます。第一引数には、
maker.update
で渡された値が入ってきます。willUpdateの使用例import Golgua, { State, Types } from "golgua"; class SomethingState extends State { constructor(){ this.types = Types.string(); } willUpdate(props){ console.log(props); // Hello return props + " Golgua!"; // この場合は、string型以外を返すとエラーになる } } const maker = Golgua.createMaker(SomethingState); maker.listen(message => console.log(message)); // Hello Golgua! maker.update("Hello");didUpdate
Storeのデータの更新が完了した時に呼ばれます。
didUpdateの使用例import Golgua, { State, Types } from "golgua"; class SomethingState extends State { constructor(){ this.types = Types.string(); } init(){ console.log(this.state); // null } willUpdate(props){ console.log(props); // Hello return props + " Golgua!"; // この場合は、string型以外を返すとエラーになる } didUpdate(){ console.log(this.state); // Hello Golgua! } } const maker = Golgua.createMaker(SomethingState); maker.update("Hello");updatedCatch
このライフサイクルは、更新が失敗した時に呼ばれます。
第一引数には、失敗した時の値。第二引数には、
object
のkey名
やarray
のindex
が渡されます。無い場合は、nullが入ります。第三引数には、更新データ全体が入っています。
以下は、
Types.number
に設定したpattern
が実行されfalse
を返したために更新されなかった時の例です。updatedCatchの使用例import Golgua, { State, Types } from "golgua"; class SomethingState extends State { constructor(){ this.types = { id: Types.number({ pattern: v => v < 10 }), text : Types.string() }; } updatedCatch(value, key, props){ const log = `value:${value}, key:${key}, props:${JSON.stringify(props)}`; console.log( log ); // value: 100, key: id, props: { id: 100, text: "Hello" } } } const maker = Golgua.createMaker(SomethingState); maker.update({ id: 100, text: "Hello" });まとめ&感想
今回 javascript でフレームワークを作ってみましたが、
発想から実装に至るまで体験してみて、フレームワーク(ライブラリ)の作成の難しさを身にしみて感じました。
「どうすれば実装しやすく、管理しやすくなるか?」、「どこまでをフレームワーク側で管理するか?」など、普段のプログラミングとはまた違った難しさなどがありました。
またそれと同時に、今あるフレームワークやライブラリの有り難みや凄さと言ったものも感じることができ、私としてはとても有意義な時間だったと思います。今回作ったフレームワークは、これからもメンテナンスをして行き、もっと使いやすくしていこうと思います。(できれば、githubでスター100個ぐらいは取れるまでにはしたい。。。)
最後に、このフレームワークはOSS(オープンソースソフトウェア)で開発していこうと思います。なので、誰でもお気軽に開発に参加できます!
もし、この記事を見て 「参加してもいいよ!」 と言っていただける方がいましたら、参加してくれると嬉しいです?私自身、javascriptを初めて1年半くらいの若輩者なので、アドバイスなど色々教えてくれると助かりますし、
私と同じで、javascriptの経験が浅い人も一緒に参加して共に技術の向上を目指そうとも思っていますので、熟練者から初心者まで誰でも参加して大丈夫です!ここまで読んで下さってありがとうございます。この記事を読んで質問などがあれば、お気軽にどうぞ。それでは?
- 投稿日:2019-02-11T22:22:42+09:00
canvasを画像としてダウンロードさせたかった話
Qiita初投稿につき不作法等あるかと思うので、ご指摘いただけると幸いです。
プログラミングぢからも底辺なため、そちらも合わせてご指導願います。やりたかったこと
- ユーザがWebページ上のボタンをクリックする
<canvas>
の描画内容がpng画像に変換される- png画像が自動でダウンロードされる
たった3行の要件に結構な時間かかった。つらい。
どのように困ったのか
①「.click()」がFirefoxで動作しない
【問題】
最初に書いたJavaScriptはこんな感じ。仮想の
<a>
要素を作ってからURLを仕込んでクリックさせればええんちゃう?といった感じ。let canvas = document.getElementById('targetCanvas') let link = document.createElement('a') link.href = canvas.toDataURL() link.download = 'cancas.png' link.click()とりあえず自分のメインブラウザたるChromeで検証したら、あっさり意図したとおりに動いた。いいね!
しかし次にFirefoxで検証したところ、ピクリとも動かず。よくないね!【原因】
Firefoxでは実在しない要素に対して
.click()
できない模様。まあ気持ちは分かる。【対策】
A案:
appendChild()
で要素を追加してから.click()
する。
B案:予めHTML内に<a id="hoge">
を書いておき、JS側でhref
属性を書き換える。今回は実装が簡単そうなB案を採用。
<a id="hoge">
がユーザに見えないようCSSでdisplay: none;
したら、見た目も動作も当初案と同じになって無事解決した。②それでも「.click()」がIE/Edgeで動作しない
【問題】
なんとなく「IE/Edgeはすんなり動いてくれないだろうなあ」と思ってたけど、案の定ノーリアクション。もしやと思って
display: none;
外して手動クリックしてみたら、ページ遷移のエラーが発生した。【原因】
今回の
link.href = canvas.toDataURL()
の部分は、いわゆる「https://~」の形式ではなくて、「data:image/png;base64,とても長い文字列~」となる。「データURIスキーム」って言うんだって、知らんかった。
なおこれについてWikipediaで調べてみると……2018年現在、データURIは主要なほとんどのブラウザで完全にサポートされている。ただし、Internet ExplorerとMicrosoft Edgeでは、一部の機能が実装されていない。
あっはいそうなんすね。マイクロソフトさんさすがっす。
【対策】
当初要件の「png画像を自動でダウンロードさせる」は、IE/Edgeでは実装困難と判断して諦めることに。画像をサーバ側に一回保存して、改めて
<a href="https://hoge.com/image.png">
とかやれば不可能ではないんだろうけど、そこまでの技術力は無い流石にめんどい。
代用として、ダウンロードさせたかった画像をページ上に小さく表示させ、手動で「右クリック→名前を付けて保存」してもらうことにした。我ながらダサい……もっとスマートなアイディアあったら誰か教えてください。ちなみにIE/Edgeのときだけ当該部分が表示されるようにCSS弄ってみたりもしたけど、有象無象のスマホブラウザ共が自動ダウンロードできるのかとか、IE/Edge専用CSSがちゃんと反映されるのかとか、色々考えたら(正常動作するChromeとFirefoxも含めて)常に表示しとく方が良いなと思い直した。
というわけで最終形
hoge.html<body> <canvas id="targetCanvas"></canvas> <button onclick="downloadCanvas()">図としてダウンロード</button> <a id="hiddenLink" download="canvas.png">link</a> <!-- CSSで「display: none;」して非表示 --> <p>自動でダウンロードされない場合、下図を右クリックして保存してください。</p> <img id="canvasImage" src="dummy.png"> <!-- CSSで小さめサイズに調整 --> </body>hoge.jsfunction downloadCanvas() { let canvas = document.getElementById('targetCanvas') let link = document.getElementById('hiddenLink') link.href = canvas.toDataURL() document.getElementById('canvasImage').src = canvas.toDataURL() link.click() }これでChromeとFirefoxでは要件通りに動作、IE/Edgeでも手動スクショ強要はせずに済んだ。めでたしめでたし。
いやホントはSafariとかスマホ版Chromeとかでも検証しないといけないんだけどね。おいおいやります。番外編 ~.toDataURL()がエラー吐く話~
ローカルでテストしてたとき、画像ファイルを
<canvas>
要素に取り込んでから.toDataURL()
すると、Chromeが下記のエラーを吐いた。DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported.
汚染されたキャンバスはエクスポートできません?なるほどわからん。
ググったらこんな記事が見つかりました。要約すると「外部コンテンツを取り込んだcanvasはセキュリティの都合でExportできないよ」ってことかな。ええ、いや、自サイト内の画像を取り込んでるだけなんですけど。
結局このエラーが出るのはローカル上で動かしてるときだけで、Web上にデプロイしてからテストすれば何の問題もなく動いた。結構時間食って悲しかったので一応書き添えておきます。根本的な解決方法は知らぬ……。
- 投稿日:2019-02-11T20:47:02+09:00
InDesign JavaScript チートシート
- 投稿日:2019-02-11T20:44:58+09:00
なかなか思うように動かない今日なのでした。#100DaysOfCode #Day3
昨日はCSSを覚えたので、さっそくはてなブログのCSSをカスタマイズしてみたのですが、なぜかウンともスンともいいません。良く分からないので後回しにして。
今日はProgateで Javascript と jQuery の初級編をクリアしました。
JavaScript は ホームページで なんかいろいろ動かしたりするプログラムが作れるみたいです。それから笑い袋を作りました。
昨日コピペで試作してみたのを、JavaScriptでrandom()に笑いが発生するようにしました。くだらねーですね!
http://appdays.herokuapp.com/waraihukuro.htmljQuery は 文字とか写真とかを なんかいろいろ動かしたりとかできるみたいです。こちらはテトリスをコピペしました。7行で テトリスが動くってすごくないですか?
お手本
https://qiita.com/ryuichi1208/items/f9e6ac2b99bbe4fc82d3練習
https://appdays.herokuapp.com/day3-tetoris.htmlそれからRubyとRuby on Railsも。こちらはTwitter みたいなやつが作れるそうです。これも練習アプリにチャレンジしたのですが、なぜかHerokuにアップするとうんともすんとも動きません。原因がわからないので一旦アプリを全部削除して作り直す。ついでにじゃかじゃかアップしたファイルやアプリを整理して。うーん動かず。何でだろう。
ハッシュタグ #100DaysOfCode の存在を知る。3日目だし参加してみようかな。
なかなか思うように動かない今日なのでした。ではまた明日!
(所要時間3時間半)
- 投稿日:2019-02-11T20:34:01+09:00
power-assert をインストールするときはテストコードの配置場所に注意しよう
はじめに
power-assert1 は便利なライブラリですが、インストールでハマったのでその記録と対処について書きます。
エラーの表示が変わらない。。
筆者の環境だと、公式ページの説明の通りインストールしても、エラーの表示が Node.js 標準の assert から変わりませんでした。
package.json{ "name": "power-assert-sample", "version": "1.0.0", "main": "index.js", "license": "MIT", "scripts": { "test": "mocha --require intelli-espower-loader tests/*.js" }, "devDependencies": { "intelli-espower-loader": "^1.0.1", "mocha": "^5.2.0", "power-assert": "^1.6.1" }, "dependencies": { } }src/hello.jsmodule.exports = (person) => { return `Hello, ${person}!`; }tests/helloTest.jsconst assert = require('power-assert'); const hello = require('../src/hello.js'); describe('hello test suit', () => { it('greet Taro', () => { assert(hello('Taro') === 'Hello, Taro!'); }); });テスト実行
yarn test1) hello test suit greet Taro: AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value: func.apply(thisObj, args)原因
power-assert を使うとき、テストコードは
test
に置くことを想定していますが、私のリソースではtests/
においていました。テストのディレクトリが
test
と異なる場合は package.json に明示的に指定するとのこと。
内部で構文解析をしているので、テストコードの場所を知っている必要があるんですね。このことは、
power-assert
でなく、intelli-espower-loader
の README2に説明がありました。The default folder is "test/". You must put your test script in this folder.
If you don't put your test code in the right folder, intelli-espower-loader will work incorrectly.
You can change test folder setting in your package.jsonpackage.jsonを以下を追加したらうまく動きました。
... "directories": { "test": "tests" }, ...表示結果assert(hello('John Smith').match(/^Hello, Taro/)) | | | null "Hello, John Smith!"参考
- 投稿日:2019-02-11T20:12:55+09:00
power-assert 使ってますか?
power-assert とは?
和田卓人さんが開発した javascript のアサーションのライブラリ1です。
普通のアサーションと比べて、簡単な書き方でより多くの情報を提示してくれます。
しかも、既存のテストコードに node.js 標準の assert を使っている場合は、 require の部分を差し替えるだけで移行することができます。以下、Node.js標準のアサーションライブラリと表示を比較してみます。
power-assert のないとき ?
サンプルコード
src/hello.jsmodule.exports = (person) => { return `Hello, ${person}!`; }test/helloTest.jsconst assert = require('assert'); const hello = require('../src/hello.js'); describe('hello test suit', () => { it('greet Taro', () => { assert(hello === 'Hello, Taro!'); }); });実行結果
AssertionError [ERR_ASSERTION]: The expression evaluated to a falsy value:評価が false だった、ということしかわかりません。
power-assert があるとき ?
サンプルコード
src/hello.js// 同じ
test/helloTest.jsconst assert = require('power-assert'); // あとは同じ実行結果
assert(hello === 'Hello, Taro!') | | | false #function#assert が評価されたときの前後の式の評価値まで知ることができます。
何が嬉しいか?
簡単な書き方で豊富な情報を得られること。
ほかのテストライブラリで同じ情報を得ようとすると、assertEqual(), hasAnyKeys(), ...など、検査項目をいちいち指定してやる必要がありました。「オブジェクトを比較する場合は
assertDeepEqual()
」といったように、検査項目に対応する書き方を覚える(もしくはマニュアルを調べる)必要がありました。一方、 power-assert は
assert()
1つで十分な情報を得ることができます。例えば、検査対象の文字列が特定の正規表現にマッチするかを調べたい場合、以下のように書けます。
test/helloTest.jsit('greet Taro', () => { assert(hello('John Smith').match(/^Hello, John-Smith/)); });実行結果assert(hello('John Smith').match(/^Hello, John-Smith/)) | | | null "Hello, John Smith!"使っているメソッドは
assert()
だけですが、テストに失敗した原因を知るための十分な情報を得ることができます。なんでこんなことができるのか?
テストコードの構文解析をしているためです。
アーキテクチャの詳細な説明は参考資料にあげているスライド2を見てください。終わりに
Javascript の便利で強力なアサーションライブラリーである power-assert について紹介しました。
参考
- 投稿日:2019-02-11T18:15:29+09:00
Firebase プロジェクトのディレクトリ構成 ベストプラクティス
まだ1つしかプロジェクト作ったことないのでベストもクソもないのですが、よく見かけるサンプルのディレクトリ構成に違和感があるもので。。。
対象プロジェクト
Firebase プロジェクトで Hostring と Functions を使い、かつ Hosting にデプロイするファイルを Webpack 等を用いてビルドする場合です。
ベストプラクティス
単純な話なのですが、Firebase プロジェクト直下に1つディレクトリを作成してこれを hosting プロジェクトのルートにするのがベストです。
<Filrebase Procject>/ # Firebase プロジェクトのルート firebase.json package.json hosting/ # Hosting プロジェクトのルート build/ # Hosting プロジェクトのビルド成果物 = Hosting のデプロイ対象 src/ # Hosting プロジェクトのソース package.json functions/ # Functions プロジェクトのルート src/ # Functions プロジェクトのソース package.jsonhosting と functions が横並びでディレクトリ構成が統一されます。
どう考えてもこれがキレイな構成ですよね。。。ルート直下の
package.json
にはデプロイスクリプトなどを登録しておきます。package.json"scripts": { "deploy": "yarn --cwd hosting build && firebase deploy" }よく見かけるサンプル(良くない例)
よく見かけるサンプルでは、Firebase プロジェクトのルート = Hosting プロジェクトのルート となっているものが多いです。
<Filrebase Procject>/ # Firebase プロジェクトのルート、Hosting プロジェクトのルート firebase.json src/ # Hosting プロジェクトのソース package.json functions/ # Functions プロジェクトのルート src/ # Functions プロジェクトの package.jsonHosting だけのプロジェクトであればこれでもいいのですが、Functions も使うプロジェクトだと hosting と functions が入れ子になってしまいます。
どう考えても歪な構成ですよね。。。
作成手順が良くない?
このような歪なディレクトリ構成がまかり通っているのは、入門書などで以下のような順番で説明しているからではないでしょうか。
- 最初にローカルで動く SPA を作成
- SPA を Firebase プロジェクトにしてデプロイする
- Functions を使用するコードを追加
最初にSPAとして作成して後付けで Firebase プロジェクトにすると、どうしても
Firebase プロジェクトのルート = Hosting プロジェクトのルート
になってしまいます。。。
- 投稿日:2019-02-11T16:35:03+09:00
「Puppeteer入門ースクレイピング+Web操作自動処理プログラミング」の動かないサンプルコード③
「Puppeteer入門ースクレイピング+Web操作自動処理プログラミング」の動かないサンプルコード③
「Puppeteer入門ースクレイピング+Web操作自動処理プログラミング」を購入し、ただいま勉強中です。
これね↓
Puppeteer入門 スクレイピング+Web操作自動処理プログラミング
動かないサンプルコードがいくつかありますね。
のちのち使えそうなコードはせっかくなので修正して残しておこうと思いました。まずpuppeteerについて
「Puppeteer入門ースクレイピング+Web操作自動処理プログラミング」の動かないサンプルコード① の「まずpuppeteerについて」の項目をご覧ください。
7章の6,「ブログの画像を保存する」
これも「Puppeteer入門ースクレイピング+Web操作自動処理プログラミング」の動かないサンプルコード②と同じです。do { 処理 } while(true)の中にawait Promise.all([ 処理 ]);があって、その中にpage.clickがある。そのせいか?ループ途中でハングアップします。
解決方法も同じ。
const nextに、セレクタオブジェクト'(a[rel="next"])'を代入するのではなく、そのhref属性(リンク先URL)を代入。
const next = await page.evaluate(() => document.querySelector(
'a[rel="next"]'));
const next = await page.evaluate(() => document.querySelector('a[rel="next"]').href);そしてPromise.allの中で、page.click()ではなくpage.goto()を使う。
です。
したがって修正箇所はほんの2箇所です。/// 修正日(2019年2月11日)修正箇所は'///--修正--///'の行です const puppeteer = require('puppeteer'); const path = require('path'); const request = require('request'); const { promisify } = require('util'); const fs = require('fs'); const delay = require('delay'); /** * ファイルのダウンロードを行う. * @param {string} url - ダウンロードするファイルのURL */ const downloadFile = async (url) => { // ダウンロードファイル名の確定. const filename = url.split('/').pop(); // ファイルの取得. const res = await promisify(request)({ method: 'GET', uri: url, encoding: null }); // 成功(200)したかどうか? if (res.statusCode === 200) { // 成功していればjsと同じフォルダーにファイル出力 await promisify(fs.writeFile)(path.join(__dirname, filename), res.body, 'binary'); } else { // 失敗した場合はエラー処理. throw new Error(`${res.statusCode} ダウンロードエラー`); } }; /** * メインロジック. */ (async () => { // Puppeteerの起動. const browser = await puppeteer.launch({ headless: false, // Headlessモードで起動するかどうか. slowMo: 50, // 指定のミリ秒スローモーションで実行する. }); // 新しい空のページを開く. const page = await browser.newPage(); // view portの設定. await page.setViewport({ width: 1200, height: 800, }); // ページの遷移. console.log('----------------------------------------goto'); await page.goto('http://ryoichi0102.hatenablog.com/'); await delay(1000); // スクレイピングする際にはアクセス間隔を1秒あける. // 先頭の記事のurlを取得し、そのurlへ遷移. console.log('----------------------------------------goto'); const firstPage = await page.evaluate(() => document.querySelector('#main article:nth-child(1) h1.entry-title a').href); // const firstPage = 'http://ryoichi0102.hatenablog.com/entry/2018/12/28/101519'; await page.goto(firstPage); await delay(1000); // スクレイピングする際にはアクセス間隔を1秒あける. // 各記事に対してのそれぞれの処理. do { console.log('----------------------------------------do'); const imageUrls = await page.evaluate(() => Array.from(document.querySelectorAll('img.hatena-fotolife')).map(img => img.src)); for (url of imageUrls) { console.log(`Downloading... ${url}`); await downloadFile(url); } console.log('----------------------------------------eval next'); // 最後の記事までたどると次へボタンは表示されないので、その場合はループを抜ける. ///---修正---/// const next = await page.evaluate(() => document.querySelector('a[rel="next"]')); const next = await page.evaluate(() => document.querySelector('a[rel="next"]').href); console.log('--------------------------------------nextのhrefは、' + next); if (next === null) { break; } // process.on('unhandledRejection', console.dir); // Promise内の捕捉されなかった例外について表示する // 次のページを読み込む. console.log('----------------------------------------next'); await Promise.all([ console.log('----------------------------------------inside Promise.all'), page.waitForNavigation({ waitUntil: 'load' }), ///---修正---/// page.goto('a[rel="next"]'), page.goto(next), ]); await delay(1000); // スクレイピングする際にはアクセス間隔を1秒あける. } while (true); // ブラウザの終了. console.log('----------------------------------------close'); await browser.close(); })();
- 投稿日:2019-02-11T16:21:48+09:00
「Puppeteer入門ースクレイピング+Web操作自動処理プログラミング」の動かないサンプルコード②
「Puppeteer入門ースクレイピング+Web操作自動処理プログラミング」の動かないサンプルコード②
「Puppeteer入門ースクレイピング+Web操作自動処理プログラミング」を購入し、ただいま勉強中です。
これね↓
Puppeteer入門 スクレイピング+Web操作自動処理プログラミング
動かないサンプルコードがいくつかありますね。
のちのち使えそうなコードはせっかくなので修正して残しておこうと思いました。まずpuppeteerについて
「Puppeteer入門ースクレイピング+Web操作自動処理プログラミング」の動かないサンプルコード① の「まずpuppeteerについて」の項目をご覧ください。
7章の3,「ブログをPDFでバックアップする」
このサンプルも、アメブロがバックアップに対応していないので使えそうです。
しかし、どうもdo { 処理 } while(true)
の中にawait Promise.all([ 処理 ]);
があって、その中にpage.click()があると途中でハングアップしてしまいます。ワタシだけ?
もしワタシだけなら本記事はスルーしてくだい。修正箇所は///---修正---///の行、2箇所だけです。
/// 修正日(2019年2月11日)修正箇所は'///--修正--///'の行です const puppeteer = require('puppeteer'); const converter = require('convert-filename-ja'); const path = require('path'); const delay = require('delay'); /** * メインロジック. */ (async () => { // Puppeteerの起動. const browser = await puppeteer.launch({ headless: true, // true: Headlessモードで起動する. slowMo: 50, // 指定のミリ秒スローモーションで実行する. }); // 新しい空のページを開く. const page = await browser.newPage(); // view portの設定. await page.setViewport({ width: 1200, height: 800, }); // ページの遷移. console.log('----------------------------------------goto'); await page.goto('http://ryoichi0102.hatenablog.com/'); await delay(1000); // スクレイピングする際にはアクセス間隔を1秒あける. // 先頭の記事のurlを取得し、そのurlへ遷移. console.log('----------------------------------------goto'); const firstPage = await page.evaluate(() => document.querySelector('#main article:nth-child(1) h1.entry-title a').href); // const firstPage = 'http://ryoichi0102.hatenablog.com/entry/2013/06/28/131913'; await page.goto(firstPage); await delay(1000); // スクレイピングする際にはアクセス間隔を1秒あける. // 各記事に対してのそれぞれの処理. do { console.log('----------------------------------------do'); // 投稿日を取得. const entryDate = await page.evaluate(() => document.querySelector('.entry-date').textContent.trim()); // 投稿タイトルを取得. const titleText = await page.evaluate(() => document.querySelector('h1.entry-title').textContent.trim()); // ファイル名として保存できるよう変換. const filename = converter.convert(`${entryDate}-${titleText}`); console.log('ファイル名は、' + filename); // 保存先のパス/ファイル名を保持し、pdfに保存. const filepath = path.join(__dirname, filename); // await page.screenshot({ path: `${filepath}.png` }); await page.pdf({ path: `${filepath}.pdf`, format: 'A4' }); console.log('----------------------------------------eval next'); // 最後の記事までたどると次へボタンは表示されないので、その場合はループを抜ける. ///---修正---/// const next = await page.evaluate(() => document.querySelector('a[rel="next"]')); const next = await page.evaluate(() => document.querySelector('a[rel="next"]').href); console.log('--------------------------------------nextのhrefは、' + next); if (next === null) { break; } console.log('----------------------------------------was not break'); // process.on('unhandledRejection', console.dir); // Promise内の捕捉されなかった例外について表示する // 次のページを読み込む. console.log('----------------------------------------next'); await Promise.all([ console.log('----------------------------------------inside promise.all'), page.waitForNavigation({ waitUntil: 'load' }), ///---修正---/// page.click('a[rel="next"]'), page.goto(next), ]); await delay(1000); // スクレイピングする際にはアクセス間隔を1秒あける. } while (true); // ブラウザの終了. console.log('----------------------------------------close'); await browser.close(); })();[修正箇所1]
const nextに、セレクタオブジェクト'(a[rel="next"])'を代入するのではなく、そのhref属性(リンク先URL)を代入。
const next = await page.evaluate(() => document.querySelector(
'a[rel="next"]'));
const next = await page.evaluate(() => document.querySelector('a[rel="next"]').href);[修正箇所2]
そしてPromise.allの中で、page.click()ではなくpage.goto()を使う。
です。他にもいろいろ試行錯誤しましたが、これがいちばんホントにすんなりうまく行きました。
理由はよくわかりません。ワタシの環境はubuntu18.04(x86_64)、node version 11.8.0、puppeteer:1.12.2、Chromium Version:73.0.3679.0 (Developer Build) (64-bit)、メモリ8GBです。
案外こんなところに原因があったりするかもしれませんが検証してません。 Puppeteer just hangs with default Chrome installation
こちらも同じ問題です↓
「Puppeteer入門ースクレイピング+Web操作自動処理プログラミング」の動かないサンプルコード③
- 投稿日:2019-02-11T14:54:29+09:00
コードに落としこんで 哲学やろうぜ その2 ~海賊王になる方法~
概要
哲学は 日本語で書くからわかりにくい。JavaScript にすれば(少なくとも)プログラマには多少はマシになるだろう、という試み。
第一回 がわりと好評だったので、続編をつくった。内容のつながりはない。今回は帰納の正当性に関するトピック「グルー推論」をJavaScript で実装しながら、その意義を考察する。
最後まで読むと、「明日 目が覚めたら海賊王になれるはず」という信念を持てるようになれるかもしれない。毒虫になる危険性に怯えるかもしれないが。
グルーとブリーン
ブルー(つまり青)とグリーン(つまり緑)という言葉がある。合体させると二つの造語が生まれる。それがグルーとブリーン。定義は以下:
グルーの定義: 「2019年5月1日0:00 以前に観察されたことのある緑色のもの」もしくは「2019年5月1日0:00 以降に初めて観察された青色のもの」に当てはまる性質
ブリーンの定義: 「2019年5月1日0:00 以前に観察されたことのある青色のもの」もしくは「2019年5月1日0:00 以降に初めて観察された緑色のもの」に当てはまる性質
もし今が平成なら エメラルドを採掘したときに それが緑ならグルーだし、青ならブリーンだ。近い将来 新元号の下でエメラルドを採掘して、それが緑ならブリーンだし、青ならグルーだ。
いきなり何言ってんの?
グルーとブリーンの定義を見て、「意味は(たぶん)わかるが、意義がわからない」というのが一般的な反応。もちろん哲学者が 言葉遊びでこんなことを考えているわけではない。この話を続けていくと、興味深い考察が得られる。それをコードを使って納得するのが本稿の趣旨。
興味深い考察: 主張 $S_1$「平成が終わっても新発掘されるエメラルドは緑色だ」は、事実 $F_1$「これまで観察されたエメラルド全部が 例外なく緑色であった」からある程度正当化される。だがよく考えてほしい。$F_1$ とは 事実 $F_2$「これまで観察されたエメラルド全部が 例外なくグルーであった」であるともいえるではないか。ならば、主張 $S_2$「平成が終わっても新発掘されるエメラルドはグルーだ」もある程度正当化できるはずだ。そして、$S_2$ は、主張 $S_2'$「平成が終わった後に発掘されるエメラルドは青色である」と同値だ。以上、$S_1$ と $S_2'$ からいえるのは 過去例外なくエメラルドが緑色だったことを根拠とするならば、新発掘されるエメラルドの色に関して、緑だと主張するのと同じ程度の確度で青であると主張しても構わないということだ。(注:これを拒否するなら別の形での問題提起になる。「なぜ緑に関する帰納的推論は妥当と感じられるが、グルーに関する帰納的推論は妥当と感じられないのか?両者の違いは何か?」)。もっというと、何色であると主張してもそれはある程度正当化されるだろう。
グルー双対定理
ちょっと考えればわかる性質を書いておく
グルーの定義(再掲): 「2019年5月1日0:00 以前に観察されたことのある緑色のもの」もしくは「2019年5月1日0:00 以降に初めて観察された青色のもの」に当てはまる性質
ブリーンの定義(再掲): 「2019年5月1日0:00 以前に観察されたことのある青色のもの」もしくは「2019年5月1日0:00 以降に初めて観察された緑色のもの」に当てはまる性質
緑と同値な表現: 「x が緑である」は以下と同値:「2019年5月1日0:00 以前に観察されたことのあるグルーなもの」もしくは「2019年5月1日0:00 以降に初めて観察されたブリーンのもの」
青と同値な表現: 「x が青である」は以下と同値:「2019年5月1日0:00 以前に観察されたことのあるブリーンなもの」もしくは「2019年5月1日0:00 以降に初めて観察されたグルーのもの」
僕らは冒頭で青と緑をつかってグルーとブリーンを定義したわけだけど、グルーとブリーンを使って青と緑が定義できると再解釈することもできる。
設計のようなもの
オブジェクトに複数のフィールドを用意しよう
color
フィールド: オブジェクトの色 RGB値を表す三つ組。例:[255, 255, 0]
なお、観測していない場合はundefined
が入る(未観測のオブジェクトの時など)。oDate
フィールド: 観測日時を表すDate
オブジェクト。isBlue
フィールド: Boolean値。青ならフラグが立つisGreen
フィールド: Boolean値。ミドリならフラグが立つisGrue
フィールド: Boolean値。グルーならフラグが立つisBleen
フィールド: Boolean値。ブリーンならフラグが立つフラグは独立でないので不整合を避けるのはプログラマの責務(例えば
isBlue
とisGreen
は同時にフラグが立ってはいけない)。フラグでなく都度計算する関数にすれば?と思うかもしれないが、color
がundefined
でもフラグを立てることができる場合がある(次節グリーン判定法参照)のでこうする必要がある。コーディングの前の注意点
平成に採掘したエメラルドの扱い: たった今、発掘したエメラルドに 0 という名前を付けよう。0 は緑色だしグルーだ。この事実は新元号の下でも変わらない。つまり新元号の下でも 0 は緑色だしグルーだ。よくある間違いは、新元号の下で 0 はグルーではなくブリーンだというものである。だがそれは間違えている。一度でもグルー判定されたらグルーのままなのだ(定義をよく読んで!)。一方、新元号の下で発掘されたエメラルドに 1 という名前を付けよう。もし 1が緑色ならば、1 は緑色でブリーンだ。
グリーン判定法: あるオブジェクト
obj
が緑であると確定する方法は(少なくとも)二つある。一つ目はobj.color
が[0, 255, 0]
であると確かめること。もう一つは、グルー双対定理を使うことだ。つまりobj.color
がnull
であっても、「obj.oDate
が平成でobj.isGrue === true
」 もしくは「obj.oDate
が新元号でobj.isBleen === true
」であれば緑であると判断できる。内部のフラグの制御をしっかりして 不整合を起こさないようにしないといけない 責務がプログラマにあるコーディング
エメラルドクラスの仕様と実装: 「観測日時が xxx でその時の RGBが yyyなエメラルドを考えよう」 というのが典型的なユースケース。ただ、「観測日時が xxx で isGrue フラグが立ったエメラルドを考えてみよう」とか「エメラルド x の isGrue フラグを立ててみよう」みたいなケースがあるので、それを受け止められるように柔軟な設計にする必要がある。
使い方=仕様から見た方がいいかな。コンストラクタに渡すオブジェクトが色々変わるという点に注意して仕様を決めよう。まずは普通のエメラルドインスタンスの生成方法。
Emeraldクラスの使い方その1.jsconst T = new Date('2019-05-01'); // 平成と新元号の境目 const T1 = new Date('2019-01-01'); // 平成は T1 で代表させる const T2 = new Date('2019-08-01'); // 新元号は T2 で代表させる const GREEN = [0, 255, 0]; const BLUE = [0, 0, 255]; const a = new Emerald({ oDate: T1, color: [...GREEN] }); const b = new Emerald({ oDate: T1, color: [...BLUE] }); const c = new Emerald({ oDate: T2, color: [...GREEN] }); const d = new Emerald({ oDate: T2, color: [...BLUE] }); [a, b, c, d].forEach(eme => eme.print()); /* output <Emerald: T1, GREEN GRUE > <Emerald: T1, BLUE BLEEN > <Emerald: T2, GREEN BLEEN > <Emerald: T2, BLUE GRUE > */続いてフラグを指定する生成方法:
Emeraldクラスの使い方その2.jsconst x1 = new Emerald({ oDate: T1, isGreen: true }); const x2 = new Emerald({ oDate: T1, isBlue: true }); const x3 = new Emerald({ oDate: T1, isGrue: true }); const x4 = new Emerald({ oDate: T1, isBleen: true }); [x1, x2, x3, x4].forEach(eme => eme.print()); /* output <Emerald: T1, GREEN GRUE > <Emerald: T1, BLUE BLEEN > <Emerald: T1, GREEN GRUE > <Emerald: T1, BLUE BLEEN > */ const y1 = new Emerald({ oDate: T2, isGreen: true }); const y2 = new Emerald({ oDate: T2, isBlue: true }); const y3 = new Emerald({ oDate: T2, isGrue: true }); const y4 = new Emerald({ oDate: T2, isBleen: true }); [y1, y2, y3, y4].forEach(eme => eme.print()); /* output <Emerald: T2, GREEN BLEEN > <Emerald: T2, BLUE GRUE > <Emerald: T2, BLUE GRUE > <Emerald: T2, GREEN BLEEN > */最後に、変な使い方:
Emeraldクラスの使い方その3.jsconst z = new Emerald({ oDate: T1 }); z.props.isBlue = true; z.rebuild(); z.print(); // -> <Emerald: T1, BLUE BLEEN >実装は末尾に記載。
デモンストレーション!
ではコードで、先述の「興味深いトピック」を再現してみよう。
平成某日。エメラルド鉱夫である僕は、100個のエメラルドを採掘することに成功した。いつもより 多めに採掘できたことに満足をしつつも、若干の退屈を覚えている。100 個のエメラルドを検査ボックスにいれ、全て RGB値が (0,255,0) であることを確認し、検品スタンプを押す。今日の仕事はこれで終わりだ:
// 100 個のエメラルドを配列で保持 const emeralds = new Array(100).fill(0).map(() => new Emerald({ oDate: T1, color: [0, 255, 0] }));腰をおろして休憩していると ネル先輩がやってきた。「仕事お疲れ。100個も採掘したなんてすごいな」という先輩に、僕は答える「いくら掘っても給料は変わりませんけどね。青色のエメラルドがでてくればボーナスが出るかもしれませんけど。」
// 当然であるが、100個のエメラルドは全て緑色だ const fact1 = emeralds.every(x => x.props.isGreen); // -> trueネル先輩は 急に真顔になった。「ここだけの話だけどな。明日採掘するエメラルドは青色だって話がある。早めに来て全部かっさらっちまったほうがいいんじゃないか?」僕は答える「冗談はよしてくださいよ。今日も昨日も一昨日も全部エメラルドは緑色でした。
fact1
を見てくださいよ。全部緑でしょ。つまらんもんです。」先輩は真顔を崩そうとしない。「お前さ、その
fact1
報告書を作成するボタンの一つ下のボタンを押してみろ。fact2
って報告書がでてくる」。僕は言われるがままにボタンを押す。報告書が出てくる。100個の 〇がついたつまらない報告書だ。fact1
となにも変わりやしない。const fact2 = emeralds.every(x => x.props.isGrue);僕はいう。「何ですか、この報告書。
fact1
と変わらないようにみえるんですが」。先輩の声に熱気がこもり始める。「いいか、この報告書はエメラルドのグルー性検証報告書というものだ。グルーとはな・・(略)・・。この報告書は、俺らが採掘したエメラルドのグルー性を表したものだ。確かに見た目はfact1
とfact2
は変わらない。しかし意味が違う。俺はこっそり、昨日の採掘分も一昨日の分の採掘分に関してもグルー性検証をやってみた。そしたらなんと、それらも全てグルー性が存在していることを示していた。」先輩は興奮のせいか早口でまくしたてる。「俺はわかったんだよ。この鉱山のエメラルドは全部グルーなんだって。そこから帰結されることは、明日採掘するエメラルドは青色なんだよ。」
// 明日 グルー性を持つエメラルドが採掘したとしよう。それは青色だろうか?→青色だった const X = new Emerald({ oDate: T2, isGrue: true }); console.log(X.props.isBlue) // -> true僕は 鼓動が早まるのを感じつつも 平静を装い応える。「仮に、そうだとしてもですよ。明日いつも通り 緑かもしれないじゃないですか?
fact1
報告書を信じるなら明日だって緑のはずですよ」。先輩。「それは認める。fact1
報告書のいうことを信じるなら明日は緑だ。 ただ、fact2
報告書を信じるなら明日は青だ。明日のエメラルドが青の可能性はせいぜい 50% といったところか。それでも大金持ちになるチャンスだぞ。早番勤務するのが正解じゃないか?」僕の気持ちは決まった。「確かに。これは乗らないのはもったいない。ギャンブルだけど勝算がありそうです。早番で来ることにします。情報ありがとうございました、先輩」。先輩は満足そうにうなずき、帰っていった。僕も今日は早めに家に帰ろう。明日は忙しくなりそうだ。
コード
const T = +new Date('2019-05-01'); // 平成と新元号の境目 const T1 = +new Date('2019-01-01'); // 平成は T1 で代表させる const T2 = +new Date('2019-08-01'); // 新元号は T2 で代表させる const GREEN = [0, 255, 0]; const BLUE = [0, 0, 255]; class Emerald { constructor (obj) { this.props = { ...obj }; if (this.props.color) { // RGB が与えられたら最低限のフラグだけ立てて、残りは rebuild に処理させる if (this.props.color.every((x, i) => x === GREEN[i])) { this.props.isGreen = true; } if (this.props.color.every((x, i) => x === BLUE[i])) { this.props.isBlue = true; } } this.rebuild(); } rebuild () { // フラグ処理。不整合時でも動作させるためにフラグ間に優先順位がある if (!this.props.oDate) throw Error('oDate unspecified!'); // 緑フラグが立っている場合 if (this.props.isGreen) { this.props.isBlue = false; this.props.isGrue = this.props.oDate < T; this.props.isBleen = this.props.oDate > T; return; } // (緑フラグが立っておらず)青フラグが立っている場合 if (this.props.isBlue) { this.props.isGreen = false; this.props.isGrue = this.props.oDate > T; this.props.isBleen = this.props.oDate < T; return; } // (緑フラグ、青フラグが立っておらず)グルーフラグが立っている場合 if (this.props.isGrue) { this.props.isBleen = false; this.props.isGreen = this.props.oDate < T; this.props.isBlue = this.props.oDate > T; return; } // (緑フラグ、青フラグ、グルーフラグが立っておらず)ブリーンフラグが立っている場合 if (this.props.isBleen) { this.props.isGrue = false; this.props.isGreen = this.props.oDate > T; this.props.isBlue = this.props.oDate < T; return; } } print () { // 表示関数 const t = this.props.oDate === T1 ? 'T1' : 'T2'; let flagStr = ''; if (this.props.isBlue) flagStr += ' BLUE '; if (this.props.isGreen) flagStr += ' GREEN '; if (this.props.isGrue) flagStr += ' GRUE '; if (this.props.isBleen) flagStr += 'BLEEN '; console.log(`<Emerald: ${t}, ${flagStr}>`); } } // 今日 1000 個のエメラルドを採掘したら全部 RGB が [0, 255, 0] const emeralds = new Array(1000).fill(0).map(x => new Emerald({ oDate: T1, isGreen: true })); const fact1 = emeralds.every(x => x.props.isGreen); // -> true const fact2 = emeralds.every(x => x.props.isGrue); // -> true const X = new Emerald({ oDate: T2, isGrue: true }); console.log(fact1, fact2, X.props.isBlue); /* const a = new Emerald({ oDate: T1, color: [...GREEN] }); const b = new Emerald({ oDate: T1, color: [...BLUE] }); const c = new Emerald({ oDate: T2, color: [...GREEN] }); const d = new Emerald({ oDate: T2, color: [...BLUE] }); [a, b, c, d].forEach(eme => eme.print()); const x1 = new Emerald({ oDate: T1, isGreen: true }); const x2 = new Emerald({ oDate: T1, isBlue: true }); const x3 = new Emerald({ oDate: T1, isGrue: true }); const x4 = new Emerald({ oDate: T1, isBleen: true }); [x1, x2, x3, x4].forEach(eme => eme.print()); const y1 = new Emerald({ oDate: T2, isGreen: true }); const y2 = new Emerald({ oDate: T2, isBlue: true }); const y3 = new Emerald({ oDate: T2, isGrue: true }); const y4 = new Emerald({ oDate: T2, isBleen: true }); [y1, y2, y3, y4].forEach(eme => eme.print()); const z = new Emerald({ oDate: T1 }); z.props.isBlue = true; z.rebuild(); z.print(); */参考
『規則と意味のパラドックス』飯田隆 ちくま学芸文庫
- 投稿日:2019-02-11T13:29:32+09:00
ソースコードに変更があるたびに行単位のカバレッジをエディターに反映する
はじめに
カバレッジの情報をリアルタイムでエディターに反映できるようにしたときのメモ。
筆者の環境
- yarn
- node.js 8.10
- nyc1
- atom
テスト実行時にカバレッジを出力する
nyc をインストール
yarn add nyc --devpackage.json に yarn test 実行時の script を書く
package.json"scripts": { "test": "nyc -r lcovonly -r text mocha --recursive test/unit/**/" },テスト実行
テスト実行yarn test
coverage/
配下にカバレッジが出力されるようになる。λ tree coverage coverage/ └── lcov.infoターミナルの表示:
ソースコードに変更があるたびにテストコードを実行する
onchange2 をインストール
yarn add onchange --devこれで、コードの変更を監視して、変更があるたびにテストを実行するようになる。
onchange lib/**/*.js test/unit/**/*.js -- yarn testせっかくなのでyarn のscriptに登録する。
package.json"scripts": { "test": "nyc -r lcovonly -r text mocha --recursive test/unit/**/", "watch": "onchange lib/**/*.js test/unit/**/*.js -- yarn test" },カバレッジの情報をリアルタイムにエディタに反映する
Atom の拡張に Coverage Merkers3 というのがあった。
ネイティブ拡張なので再ビルドの必要があるが、何かの拡張 package が node-gyp を使っているらしく、 Python 3.x 系だとビルドに失敗する。仕方ないので python 2.7 に切り替えてインストールする。pyenv locla 2.7.15 apm install coverage-markers apm rebuildMac の場合、
Opt + Ctrl + O
で行単位のカバレッジの表示/非表示をトグルできる。。Atom エディタの画面:
テストコードがカバーされていない行の左側に赤い丸印がつく。
参考
- 投稿日:2019-02-11T08:47:36+09:00
更にReact Hooksだけでライブラリ使わずにgoogle mapを利用する(応用編)
こちらはReact Hooksだけでライブラリ使わずにgoogle mapを利用する(基礎編)の続きになります。
Google MapのReact Hooksでの利用。前回マップを出すとこまでをとしてまとめた。
ここからは応用編としていろんな手段を書いて行きたい。
- 3. クリックしたらマーカーを増やすようにする
- 4. クリックしたら削除もするようにする
- 5. InfoWindowを出す
応用のため遠慮せずすべてのhooksを色々利用していく。
なるべく説明は書いているが不足を感じたら公式ドキュメントをご参照いただきたい
https://reactjs.org/docs/hooks-reference.htmlまた要所要所でリファクタを挟んでいる。
コードは前回の続きからとしてご覧いただきたい。3. クリックしたらマーカーを増やすようにする
クリックされたらマーカーを増やすようなことを試してみる。
追加するhooksは下記の2つになる
// hooks.js // markerをstate管理する export const useMarkerState = (initialMarkers) => { const [markers, setMarkers] = useState(initialMarkers) // マーカーの追加処理はsetMarkersを加工する形に const addMarker = ({ lat, lng }) => { setMarkers([...markers, { lat, lng }]) } return { markers, addMarker } } // Mapがクリックされたらイベントを飛ばすhooks export const useMapClickEvent = ({ onClickMap, googleMap, map }) => { useEffect(() => { if (!googleMap || !map) { return } const listener = googleMap.maps.event.addListener(map, "click", (e) => { onClickMap({ lat: e.latLng.lat(), lng: e.latLng.lng() }) }) // onClickMapが変更されたらつくったイベントをクリアする //(じゃないとクリックするたびにイベントが大量閣下さえる) return () => { googleMap.maps.event.removeListener(listener) } }, [googleMap, map, onClickMap]) }また、
useMapMarker
も書き換える。
markerが多重描画されないように、markerの実体を保存する。
stateで保持しようとすると遅延アップデートがされる都合でうまく保持がされないためexport const useDrawMapMarkers = ({ markers, googleMap, map }) => { // stateだと初回描画ほ保持がうまくいかないのでここではrefを利用する const markerObjectsRef = useRef({}) // markersが変わるたびに実行する useEffect(() => { // 初期化がまだなら何もしない if (!googleMap || !map) { return } const { Marker } = googleMap.maps markers.map((position, i) => { if (markerObjectsRef.current[i]) { // すでに描画済みだったら何もしない。 return } const markerObj = new Marker({ position, map, title: "marker!" }) markerObjectsRef.current[i] = markerObj }) }, [markers, googleMap, map]) }addMarkerのところは
useCallback
を利用してもいいだろうconst addMarker = useCallback(({ lat, lng }) => { setMarkers([...markers, { lat, lng }]) }, [markers]) // markersが変更したら関数自体を変える。そうしないとstateの状態とmapの実体がずれてくるそして利用する側がこんな具合になるだろう。
export const MapApp = () => { const googleMap = useGoogleMap(API_KEY) const mapContainerRef = useRef(null) const map = useMap({ googleMap, mapContainerRef, initialConfig }) // stateとして管理するマーカー const { markers, addMarker } = useMarkerState(initialMarkers) // 描画する useDrawMapMarkers({ markers, googleMap, map }) // クリックイベントを追加 useMapClickEvent({ onClickMap: ({ lat, lng }) => { addMarker({ lat, lng }) }, map, googleMap }) return ( <div style={{ height: "100vh", width: "100%" }} ref={mapContainerRef} /> ) }3をちょっとリファクタ
ちょっとMapAppが膨れてきてしまったのでリファクタしてみる。
MapをマウントするコンテナのCSS部分をstyled-components化するのとマーカーのhooksを利用するだけのコンポーネントで分離するimport styled from "styled-components" // コンテナのCSS部分をstyled-componentsにする const MapContainer = styled.div` height: 100vh; width: 100%; ` // マーカーのhooksを利用する const MapMarkers = ({ googleMap, map }) => { // stateとして管理するマーカー const { markers, addMarker } = useMarkerState(initialMarkers) // 描画する useMapMarker({ markers, googleMap, map }) // クリックイベントを追加 useMapClickEvent({ onClickMap: ({ lat, lng }) => { addMarker({ lat, lng }) }, map, googleMap }) // hooksのためだけのコンポーネントになるのでこのコンポーネント自体は何も返さない。 // nullを返すのが気持ち悪ければ`<script />`, `[]`, `""`を返すなどもアリ return null } export const MapApp = () => { const googleMap = useGoogleMap(API_KEY) const mapContainerRef = useRef(null) const map = useMap({ googleMap, mapContainerRef, initialConfig }) return ( <> <MapContainer ref={mapContainerRef} /> <MapMarkers googleMap={googleMap} map={map} /> </> ) }hooksを利用するだけのコンポーネントがはて良いものかというのは少し悩むところにも感じる...
hooksの部分を独自に切り出して下記のようにするのも良いだろう
// hooksを単純に呼び出してるhooks const useMapMarkerSetup = ({ googleMap, map }) => { const { markers, addMarker } = useMarkerState(initialMarkers) useMapMarker({ markers, googleMap, map }) useMapClickEvent({ onClickMap: ({ lat, lng }) => { addMarker({ lat, lng }) }, map, googleMap }) } const MapMarkers = ({ googleMap, map }) => { useMapMarkerSetup({ googleMap, map }) return null }また、例えばこんな風にMapが初期化されるまで待つようなコンポーネントを作る事もできるだろう
const WaitForMap = ({ googleMap, map, children }) => { if (!googleMap || !map) { return null } return children } export const MapApp = () => { const googleMap = useGoogleMap(API_KEY) const mapContainerRef = useRef(null) const map = useMap({ googleMap, mapContainerRef, initialConfig }) return ( <> <MapContainer ref={mapContainerRef} /> <WaitForMap googleMap={googleMap} map={map}> <MapMarkers googleMap={googleMap} map={map} /> </WaitForMap> </> ) }// hook.js export const useMapMarker = ({ markers, googleMap, map }) => { useEffect(() => { // このチェックが不要になる // if (!googleMap || !map) { // return // } const { Marker } = googleMap.maps // ...DEMO
https://gist.github.com/terrierscript/d8c9665f1a1761c48ee3739c0350ab76
4. クリックしたら削除もするようにする
更に応用。クリックされたら削除されることを考えてみる。ちょっとここからはだいぶ複雑度が増してくる。
ここでは下記2つの方法を示す
- A: markersを配列として描画する
- B: markersを一つずつのコンポーネントとして処理する
共通部分: Stateをreducer化する
ここまでマーカーは配列で処理してきたが、削除のことまで考えるとobjectで暑かったほうが都合が良くなるので
useReducer
を利用してreducer化する。import uuid from "uuid/v4" const markerReducer = (state, action) => { switch (action.type) { case "ADD": const id = uuid() // 追加するたびにuuidをidとして発行 return { ...state, [id]: action.payload } case "REMOVE": const { [action.payload]: removeItem, ...rest } = state return rest default: return state } } // 初期データもreducerを通してあげたいので、initializerを作成 const mapReducerInitializer = (initialMarkers) => { return initialMarkers.reduce((state, marker) => { return markerReducer(state, { type: "ADD", payload: marker }) }, {}) } // markerをstate管理する export const useMarkerState = (initialMarkers) => { const [markers, dispatch] = useReducer( markerReducer, initialMarkers, mapReducerInitializer ) // マーカーの追加・削除のaction関数 // ここも効率化のためにuseCallbackを使っているが必須ではない。 // const addMarker = (position) => dispatch({ type: "ADD", payload: position }) などでも十分だろう const addMarker = useCallback( (position) => dispatch({ type: "ADD", payload: position }), [dispatch] ) const removeMarker = useCallback( (removeUuid) => dispatch({ type: "REMOVE", payload: removeUuid }), [dispatch] ) // 外向けにobjectではなくarrayとして返すためのselector const getMarkers = useCallback( () => Object.entries(markers).map(([id, position]) => ({ id, position })), [markers] ) return { // markers // 元のobjectとしてのmarkerは隠蔽する addMarker, removeMarker, getMarkers } }4-A: markersを配列として描画する
先程までのuseDrawMapMarkersを拡張する形でまずは書いてみる。
こちらの方が手軽といえば手軽だろうexport const useDrawMapMarkers = ({ markers, googleMap, map, onClickMarker }) => { // マーカーの再描画を防ぐためrefsに保持 const markerObjectsRef = useRef({}) useEffect(() => { const { Marker } = googleMap.maps markers.map(({ id, position }) => { // すでに描画済みなmarkerだったら描画しない if (markerObjectsRef.current[id]) { return } const markerObj = new Marker({ position, map, title: "marker!" }) // markerがクリックされた時のイベントを追加する markerObj.addListener("click", (e) => { onClickMarker(id, markerObj, markerObjectsRef.current, e) }) markerObjectsRef.current[id] = markerObj }) }, [markers, googleMap, map]) }利用側はこんな感じになる
const useMapMarkerSetup = ({ googleMap, map }) => { // stateとして管理するマーカー const { addMarker, removeMarker, getMarkers } = useMarkerState(initialMarkers) const markers = getMarkers() // 描画する useDrawMapMarkers({ markers, googleMap, map, // 削除イベントを追加 onClickMarker: (id, markerObj, markerObjectsRef) => { removeMarker(id) markerObj.setMap(null) markerObjectsRef[id] = null } }) // クリックイベントを追加 useMapClickEvent({ onClickMap: ({ lat, lng }) => { addMarker({ lat, lng }) }, map, googleMap }) } const MapMarkers = ({ googleMap, map }) => { useMapMarkerSetup({ googleMap, map }) return null }なかなか分厚い状態なのと、Markerのオブジェクトを利用側でいじる形になるのは少々気持ちが悪いかもしれない。
コード
https://gist.github.com/terrierscript/861df9328f80f077ac0b534569f3e8e1
4-B: markersを一つずつのコンポーネントとして処理する
ということでこれを改修して、「マーカー1つ1つにコンポーネントとフックを対応させる」という方向でやってみる。
export const useDrawMapMarker = ({ position, googleMap, map, onClickMarker }) => { const markerObjectsRef = useRef(null) useEffect(() => { const { Marker } = googleMap.maps // すでに描画済みなmarkerだったら描画しない if (markerObjectsRef.current) { return } const markerObj = new Marker({ position, map, title: "marker!" }) // markerがクリックされた時のイベントを追加する markerObj.addListener("click", (e) => { onClickMarker(e) }) markerObjectsRef.current = markerObj // effectの返却の関数として、コンポーネントが消え場合の処理を書けるので、ここでmarkerもmapから消すように仕掛ける。 return () => { if (markerObjectsRef.current === null) { return } markerObjectsRef.current.setMap(null) } }, [googleMap, map]) }利用側はこのようになる
// marker一つ一つを担当するComponent const Marker = ({ googleMap, map, position, onClickMarker }) => { useDrawMapMarker({ googleMap, map, position, onClickMarker }) return null } const MapMarkers = ({ map, googleMap }) => { const { addMarker, removeMarker, getMarkers } = useMarkerState(initialMarkers) const markers = getMarkers() // クリックイベントを追加 useMapClickEvent({ onClickMap: ({ lat, lng }) => { addMarker({ lat, lng }) }, map, googleMap }) return ( <> {markers.map(({ id, position }) => ( <Marker key={id} // hooksがkeyに紐づく。これがないと適切なマーカーが消えなくなる position={position} onClickMarker={() => { removeMarker(id) }} map={map} googleMap={googleMap} /> ))} </> ) }こうなってくると「なんだかhooks以前のReactと手間変わらない気もする・・・」というのもあるのだが、結局はこの方がスマートになる印象だ。
コード
https://gist.github.com/terrierscript/f57ace64b776848d68be6b5bc736e2ed
リファクタ: onClickイベントを分離する・
先程までの
useDrawMapMarker
だとイベントが変更しても感知しないものになっていた。
もう少しこの点きれいに考えると下記のようになるだろう。
useDrawMapMarker
から帰ってくるMarkerオブジェクトを別途effectしてフックするような形になる。また、これまでhooksの内部でしか使っていなかったため
useRef
で良かったが、Markerが外部に出すものとなったのでuseState
で書き換えている。export const useDrawMapMarker = ({ position, googleMap, map }) => { const [markerObject, setMarkerObject] = useState(null) useEffect(() => { const { Marker } = googleMap.maps // すでに描画済みなmarkerだったら描画しない if (markerObject) { return } const markerObj = new Marker({ position, map, title: "marker!" }) setMarkerObject(markerObj) // コンポーネントが消えたらmarkerもmapから消すように仕掛ける。これはすっ return () => { if (markerObj === null) { return } markerObj.setMap(null) } }, [googleMap, map]) // markerObjectを更新対象にするとすぐ消えてしまうので、対象にしないようにする。この辺の勘所ちょっと慣れが必要そう return markerObject } export const useMarkerClickEvent = (marker, onClickMarker) => { // イベントが変更される事を考慮する useEffect(() => { if (!marker) { return } const listener = marker.addListener("click", (e) => { onClickMarker(e) }) return () => { listener.remove() } }, [marker, onClickMarker]) }const Marker = ({ googleMap, map, position, onClickMarker }) => { const marker = useDrawMapMarker({ googleMap, map, position }) // イベントの呼び出しはこっちで行う useMarkerClickEvent(marker, onClickMarker) return null }もう一つリファクタ: Context化する。
ここまでgoogleMapとmapをやたらと引き回してしまった。
そろそろこの辺をContext化する(正直もうちょっと手前でやっておけば良かった感じがある)export const MapContext = createContext({ googleMap: null, map: null })途中で作成したWaitForMapのタイミングが
Provider
をするのにちょうどよいだろうconst WaitForMap = ({ googleMap, map, children }) => { if (!googleMap || !map) { return null } const value = { googleMap, map } return <MapContext.Provider value={value}>{children}</MapContext.Provider> }例えば
useMapMarkerSetup
などは引数が不要になる。
利用側より親でProviderで囲む事を忘れないようにだけ注意が必要だconst useMapMarkerSetup = () => { const { googleMap, map } = useContext(MapContext) const { addMarker, removeMarker, getMarkers } = useMarkerState(initialMarkers) const markers = getMarkers() useMapClickEvent({ onClickMap: ({ lat, lng }) => { addMarker({ lat, lng }) }, map, googleMap }) return { markers, removeMarker } }5. InfoWindowを出す
最後にInfoWindowを出すところまでやってみる。
内容物としてDOMNodeかHTMLの文字列を渡さなければならずなかなか苦戦するものだった。準備:クリックで削除をダブルクリックで削除にする
クリックの処理をinfoWindowにさせたいので、削除処理はダブルクリックに移動させたい。
// Markerのイベントフックを柔軟にしてどのイベントにも対応できるようにする export const useMarkerEvent = ({ marker, eventHandler, eventName }) => { // イベントが変更される事を考慮する useEffect(() => { if (!marker) { return } const listener = marker.addListener(eventName, (e) => { eventHandler(e) }) return () => { listener.remove() } }, [marker, eventName, eventHandler]) }const Marker = ({ position, onDoubleClickMarker }) => { // 先程context化をしたのでgoogleMap/mapについては気にしなくしている const marker = useDrawMapMarker({position}) // イベントの呼び出しはこっちで行う useMarkerClickEvent({marker, eventName: "dblckick", eventHandler: onDoubleClickMarker) return null }InfoWindowを作る
ほとんどこれまでの応用になるので、あとはhooksとコンポーネントを作っていくだけになる
// googleMapのinfoWindowを生成して返す。 // contentNodeはDOM要素かstringなので、ここではDOMNodeを想定する export const useMapInfoWindow = (content) => { const [infoWindowState, setInfoWindow] = useState(null) const { googleMap } = useContext(MapContext) useEffect(() => { if (!content) { return } // infoWindowの再描画防止 if (infoWindowState) { return } const infoWindowObj = new googleMap.maps.InfoWindow({ content }) setInfoWindow(infoWindowObj) return () => { // 消す時はcloseする infoWindowObj.close() } }, [googleMap, content]) return infoWindowState }// 表には見せない要素。vueのv-cloakから名前を拝借した const Cloak = styled.div` display: none; ` // infoWindowを仕掛けるコンポーネント const MarkerInfoWindow = ({ marker, position }) => { const { map } = useContext(MapContext) const contentRef = useRef(null) // contentRefのDOMNodeを表示要素としたinfoWindowを作る const infoWindow = useMapInfoWindow(contentRef.current) useMarkerEvent({ marker, eventName: "click", // クリックしたら開く eventHandler: () => infoWindow.open(map, marker) }) return ( <Cloak> <div ref={contentRef}> <b>hello</b>, {position.lat}, {position.lng} </div> </Cloak> ) } const Marker = ({ position, onDoubleClick }) => { const marker = useDrawMapMarker({ position }) // ダブルクリックしたら消す useMarkerEvent({ marker, eventName: "dblclick", eventHandler: onDoubleClick }) // markerは先程nullにしていたが、InfoWindowを表示する役割が出来た return <MarkerInfoWindow marker={marker} position={position} /> }DEMO
ここだけCodesandboxのDEMOで。hooksファイルが肥大化してしまったので分割したバージョンにしている
https://codesandbox.io/s/wnk87oykzw感想など
- Hooksは単純なものをすごくライトに書けるようになるのはものすごく効果を感じた
- ちょっとreducerとaction書くだけであればすごく楽だし、reduxやvuexの頃の知識はほぼそのまま使える
- 地味に
selector
に相当する関数もhooksから返せるのはアリかもしれない- やはりそこそこ複雑度のあるコードにはそれなりの複雑度になってくると感じた(それでもだいぶ楽ではある)
- 曲線が変わった印象があり、複雑度が低い時が部分の難易度ぐぐっと下がって、複雑度が上がってくると徐々に変わらなくなってくる印象
- hooksはhooksでハマるタイミングがある
- (それでもやっぱりhooksのほうが楽なのは違いない)
- 誤解を恐れずにいうと「React先生がずーっと無限ループしててその途中でFunctional Componentが呼ばれていてその中でhooksの関数だけがその無限ループの外側で管理されてる」みたいな世界観を脳みそに宿しながら触っていると「あー、そういうことか」となる気がした(あっているかは微妙)
- 地味に4-Aで見せたような「少しReactらしくないコード」でも割となんとかなってしまうというのは結構利点に感じている。
useEffect
は今回のような外部のコード連携はとでも相性がよいし、返却の関数でclean upできるのも非常に良いと感じた。
- ただ更新対象とする値をうっかり間違えると躓くことがしばしばあった
useState
とかの値と組み合わせると無限レンダリングが走ったりするのをやってたりするので、そこらへんは注意点useState
はimmutableな分、mutableに扱える部分でuseRef
`に頼りたくなってしまうが、再レンダリングされず結構ハマるので注意したほうが良さそう
- 特に外部に出す変数は
useRef
はやめたほうが良さそう。ハマる。- 「hooksはトップレベルでしか使ってはいけません」というルールを迂回したくなるとヘンなコードを書いてしまいそうになるなーという感じがする。
- 今回もhooks使うだけで何もしないコンポーネントというのをやったりしてしまったが「ちょっと微妙かも・・・」という気持ちは出てくる
- このへんは今後ちょこちょこプラクティスが出てくる予感がするかもしれない
- 投稿日:2019-02-11T01:27:23+09:00
引数に無名関数を使ってコールバックする配列操作メソッド(JavaScript)
今回取り上げるのは、JavaScriptの中で、引数に無名関数を使ったコールバック(関数)を利用して配列を操作する主なメソッドです。
- some
- 概要 : 配列の少なくとも1つの要素が、無名関数を使ったコールバック(関数)でTRUEになるかチェック
- 戻り値 : TRUE or FALSE(パスしたやつがある時点で
TRUE
を返す)- 構文 :
array.some(callback)
- callback: 個々の要素を判定するための関数
- every
- 概要 : 配列の全要素が、無名関数を使ったコールバック(関数)によるチェック
- 戻り値 : TRUE or FALSE(パスしなかったやつがある時点で
FALSE
を返す)- 構文 :
array.every(callback)
- callback: 全要素を判定するための関数
- find
- 概要 : 配列の要素1つ1つを、無名関数を使ったコールバック(関数)で評価して、trueになった最初の要素の値を返す
- 戻り値 : trueとなった要素の値
- 構文 :
array.find(callback)
- callback: 個々の要素を判定するための関数
- forEach
- 概要 : 配列中のそれぞれの要素に対して無名関数を使ったコールバック(関数)を実行する
- 戻り値 : なし
- 構文 :
array.forEach(callback)
- callback: 個々の要素を処理するための関数
- map
- 概要 : 上のforEachと同じく、配列の要素1つ1つに、第一引数に指定した無名関数を使ったコールバック(関数)を実行する
- 戻り値 : 要素1つ1つ処理したあとの配列
- 構文 :
array.map(callback)
- callback: 個々の要素を判定するための関数
- filter
- 概要 : 上のmapと同じく、配列の要素1つ1つに、第一引数に指定した無名関数を使ったコールバック(関数)を実行する (コールバック関数はboolean値を返す)
- 戻り値 : trueとなった要素からなる配列
- 構文 :
array.filter(callback)
- callback: 個々の要素を判定するための関数
- reduce
- 概要 : 配列の隣接する2つの要素を左から右に指定した関数で計算して、1つの値を返すメソッド
- 戻り値 : 1つの値
- 構文 :
array.reduce(callback)
- callback: 個々の要素を処理するための関数
主なメソッドの説明(簡易版)
メソッド 概要 戻り値 some 配列の少なくとも1つの要素がTRUEかチェック Boolean every 配列の全要素がTRUEかチェック Boolean forEach 配列の要素1つ1つに、第一引数に指定したコールバック関数を実行 なし map 配列の要素1つ1つに、第一引数に指定したコールバック関数を実行 配列 filter 配列の要素1つ1つに、第一引数に指定したコールバック関数を実行 配列 find 配列の要素を一つずつ指定した関数で評価して、最初にtrueになった要素の値を返すやつ trueとなった要素の値 reduce 配列の隣接する2つの要素を左から右に指定した関数で計算 値 個々の詳細を見ていきます。
some
概要 : 配列の少なくとも1つの要素が、無名関数を使ったコールバック(関数)でTRUEになるかチェック
戻り値 : TRUE or FALSE(パスしたやつがある時点でTRUEを返す)
コールバック(関数)引数 : x書き方
someメソッドconst ary = [1,2,3,4,5]; const result = ary.some(x => x == 5); console.log(result); // => truesomeメソッドconst ary = [1,2,3,4,5]; const result = ary.some(x => x == 6); console.log(result); // => falseevery
概要 : 配列の全要素が、無名関数を使ったコールバック(関数)によるチェックを通るか
戻り値 : TRUE or FALSE(パスしなかったやつがある時点でFALSE
を返す)
コールバック(関数)引数 : x書き方
everyメソッドconst ary = [1,2,3,4,5]; const result = ary.every(x => x < 10); console.log(result); // => trueeveryメソッドconst ary = [1,2,3,4,5]; const result = ary.every(x => x == 1); console.log(result); // => falsefind
概要 : 配列の要素1つ1つを、無名関数を使ったコールバック(関数)で評価して、trueになった最初の要素の値を返す
戻り値 : trueとなった要素の値
コールバック(関数)引数 : ↓の3つ
- 第一引数 value → 要素の値
- 第二引数 index → インデックス番号
- 第三引数 array → 元の配列
findメソッドconst ary = [1, 2, 3, 4, 5] const result = ary.find((value, index, array) => value * 2 >= 10); console.log(result); // => 5書き方
forEach
概要 : 配列中のそれぞれの要素に対して無名関数を使ったコールバック(関数)を実行する
戻り値 : なし
コールバック(関数)引数 : ↓の3つ
- 第一引数 value → 要素の値
- 第二引数 index → インデックス番号
- 第三引数 array → 元の配列
書き方
forEachメソッドconst ary = [1, 2, 3, 4, 5] ary.forEach((value, index, array) => { console.log(`${value} : ${index} : ${array}`) }); // "1 : 0 : 1,2,3,4,5" // "2 : 1 : 1,2,3,4,5" // "3 : 2 : 1,2,3,4,5" // "4 : 3 : 1,2,3,4,5" // "5 : 4 : 1,2,3,4,5"map
概要 : 上のforEachと同じく、配列の要素1つ1つに、第一引数に指定した無名関数を使ったコールバック(関数)を実行する
戻り値 : 要素1つ1つ処理したあとの配列
コールバック(関数)引数 : x書き方
mapメソッドconst ary = [1,2,3,4,5] const result = ary.map( x => x * x); console.log(result); // console => [1, 4, 9, 16, 25] // process => [1*1, 2*2, 3*3, 4*4, 5*5]filter
概要 : 上のmapと同じく、配列の要素1つ1つに、第一引数に指定した無名関数を使ったコールバック(関数)を実行する(コールバック関数はboolean値を返す)
戻り値 : trueとなった要素からなる配列
コールバック(関数)引数 : x書き方
filterメソッドconst ary = [1,2,3,4,5] const result = ary.filter( x => x % 2 == 0); console.log(result); // [3, 4, 5]filterメソッドconst ary = [1,2,3,4,5] const result = ary.filter( x => x >= 2); console.log(result); // [2, 3, 4, 5]reduce
- 概要 : 配列の隣接する2つの要素を左から右に指定した関数で計算して、1つの値を返すメソッド
戻り値 : 1つの値
コールバック(関数)引数 : ↓の4つ第一引数 value → 処理される要素
第二引数 value → 次に処理される要素
第三引数 index → インデックス番号
書き方
reduceメソッドconst ary = [1,2,3,4,5] const result = ary.reduce( (x,y) => x+ y ); console.log(result); // console => 15 // process => 1 + 2 + 3 + 4 + 5reduceメソッドconst ary = [1,2,3,4,5] const result = ary.reduce( (x,y,z) => x + y + z ); console.log(result); // console => 25 // process => 1 + 2 + 3 + 4 + 5 + 0 + 1 + 2 + 3 + 4参考
- 投稿日:2019-02-11T01:27:23+09:00
引数に即時関数(無名関数)を使ってコールバック(関数)する配列操作メソッド(JavaScript)
今回取り上げるのは、JavaScriptの中で、引数に即時関数(無名関数)を使ったコールバック(関数)を利用して配列を操作する主なメソッドです。
- some
- 概要 : 配列の少なくとも1つの要素が、即時関数(無名関数)を使ったコールバック(関数)でTRUEになるかチェック
- 戻り値 : TRUE or FALSE(パスしたやつがある時点で
TRUE
を返す)- 構文 :
array.some(callback)
- callback: 個々の要素を判定するための関数
- every
- 概要 : 配列の全要素が、即時関数(無名関数)を使ったコールバック(関数)によるチェック
- 戻り値 : TRUE or FALSE(パスしなかったやつがある時点で
FALSE
を返す)- 構文 :
array.every(callback)
- callback: 全要素を判定するための関数
- find
- 概要 : 配列の要素1つ1つを、即時関数(無名関数)を使ったコールバック(関数)で評価して、trueになった最初の要素の値を返す
- 戻り値 : trueとなった要素の値
- 構文 :
array.find(callback)
- callback: 個々の要素を判定するための関数
- forEach
- 概要 : 配列中のそれぞれの要素に対して即時関数(無名関数)を使ったコールバック(関数)を実行する
- 戻り値 : なし
- 構文 :
array.forEach(callback)
- callback: 個々の要素を処理するための関数
- map
- 概要 : 上のforEachと同じく、配列の要素1つ1つに、第一引数に指定した即時関数(無名関数)を使ったコールバック(関数)を実行する
- 戻り値 : 要素1つ1つ処理したあとの配列
- 構文 :
array.map(callback)
- callback: 個々の要素を判定するための関数
- filter
- 概要 : 上のmapと同じく、配列の要素1つ1つに、第一引数に指定した即時関数(無名関数)を使ったコールバック(関数)を実行する (コールバック関数はboolean値を返す)
- 戻り値 : trueとなった要素からなる配列
- 構文 :
array.filter(callback)
- callback: 個々の要素を判定するための関数
主なメソッドの説明(簡易版)
メソッド 概要 戻り値 some 配列の少なくとも1つの要素がTRUEかチェック Boolean every 配列の全要素がTRUEかチェック Boolean forEach 配列の要素1つ1つに、第一引数に指定したコールバック関数を実行 なし map 配列の要素1つ1つに、第一引数に指定したコールバック関数を実行 配列 filter 配列の要素1つ1つに、第一引数に指定したコールバック関数を実行 配列 find 配列の要素を一つずつ指定した関数で評価して、最初にtrueになった要素の値を返すやつ trueとなった要素の 値
個々の詳細を見ていきます。
some
概要 : 配列の少なくとも1つの要素が、即時関数(無名関数)を使ったコールバック(関数)でTRUEになるかチェック
戻り値 : TRUE or FALSE(パスしたやつがある時点でTRUEを返す)
コールバック(関数)引数 : x書き方
someメソッドconst ary = [1,2,3,4,5]; const result = ary.some(x => x == 5); console.log(result); // => truesomeメソッドconst ary = [1,2,3,4,5]; const result = ary.some(x => x == 6); console.log(result); // => falseevery
概要 : 配列の全要素が、即時関数(無名関数)を使ったコールバック(関数)によるチェックを通るか
戻り値 : TRUE or FALSE(パスしなかったやつがある時点でFALSE
を返す)
コールバック(関数)引数 : x書き方
everyメソッドconst ary = [1,2,3,4,5]; const result = ary.every(x => x < 10); console.log(result); // => trueeveryメソッドconst ary = [1,2,3,4,5]; const result = ary.every(x => x == 1); console.log(result); // => falsefind
概要 : 配列の要素1つ1つを、即時関数(無名関数)を使ったコールバック(関数)で評価して、trueになった最初の要素の値を返す
戻り値 : trueとなった要素の値
コールバック(関数)引数 : ↓の3つ
- 第一引数 value → 要素の値
- 第二引数 index → インデックス番号
- 第三引数 array → 元の配列
findメソッドconst ary = [1, 2, 3, 4, 5] const result = ary.find((value, index, array) => value * 2 >= 10); console.log(result); // => 5書き方
forEach
概要 : 配列中のそれぞれの要素に対して即時関数(無名関数)を使ったコールバック(関数)を実行する
戻り値 : なし
コールバック(関数)引数 : ↓の3つ
- 第一引数 value → 要素の値
- 第二引数 index → インデックス番号
- 第三引数 array → 元の配列
書き方
forEachメソッドconst ary = [1, 2, 3, 4, 5] ary.forEach((value, index, array) => { console.log(`${value} : ${index} : ${array}`) }); // "1 : 0 : 1,2,3,4,5" // "2 : 1 : 1,2,3,4,5" // "3 : 2 : 1,2,3,4,5" // "4 : 3 : 1,2,3,4,5" // "5 : 4 : 1,2,3,4,5"map
概要 : 上のforEachと同じく、配列の要素1つ1つに、第一引数に指定した即時関数(無名関数)を使ったコールバック(関数)を実行する
戻り値 : 要素1つ1つ処理したあとの配列
コールバック(関数)引数 : x書き方
mapメソッドconst ary = [1,2,3,4,5] const result = ary.map( x => x * x); console.log(result); // console => [1, 4, 9, 16, 25] // process => [1*1, 2*2, 3*3, 4*4, 5*5]filter
概要 : 上のmapと同じく、配列の要素1つ1つに、第一引数に指定した即時関数(無名関数)を使ったコールバック(関数)を実行する(コールバック関数はboolean値を返す)
戻り値 : trueとなった要素からなる配列
コールバック(関数)引数 : x書き方
filterメソッドconst ary = [1,2,3,4,5] const result = ary.filter( x => x % 2 == 0); console.log(result); // [3, 4, 5]filterメソッドconst ary = [1,2,3,4,5] const result = ary.filter( x => x >= 2); console.log(result); // [2, 3, 4, 5]参考
- 投稿日:2019-02-11T00:38:10+09:00
JavaScriptの関数定義まとめ
はじめに
JavaScriptって関数の定義方法が何個もあって、どれが何をしてくれるのかよくわからなくなるのでまとめます。
関数定義と呼び出し
よく見る書き方。
関数を一度定義し、使いたい時に呼び出す。
繰り返し・共通で使う処理などをまとめておくときに使う。// 引数なし function hoge() { console.log("hoge"); } // 引数あり function fuga(str) { console.log(str); }呼び出し
hoge(); // -> "hoge" と出力 fuga(fuga); // -> "fuga" と出力変数代入と無名関数(匿名関数)
定義した関数を変数に代入して呼び出せます
const hoge = function hoge() { console.log("hoge"); } hoge(); // -> "hoge" と出力変数に代入するのにわざわざ関数名も書かなくてよくない?
っていうことで省略できます。
これを無名関数(匿名関数)といいます。const hoge = function() { console.log("hoge"); } hoge(); // -> "hoge" と出力即時関数
書いたその場で実行される関数。
定義と呼び出しがセットになってるようなもの。
()で囲います。(function hoge() { console.log("hoge"); }()); // -> すぐに "hoge" と出力こちらも関数名を省略できます。
引数も指定できます。(function(arg1, arg2) { console.log(arg1 + arg2); }("hoge", "fuga")); // -> すぐに "hogefuga" と出力アロー関数
ES6以降に利用可能な 書き方
記法を変えているだけで、使われ方はこれまでに挙げたものと同じです。
=>
という矢(アロー)を使って書くのでアロー関数と呼びます。書き方は以下です。
一行の処理の場合は{}
を省略できます。
(引数) => { 処理 }
function
など文字を省略できるので書くのが楽ですね!最後に使い方の例です。
上で挙げた即時関数をアロー関数で書いたもの。((arg1, arg2) => cosole.log(arg1 + arg2))("hoge", "fuga")