20190211のJavaScriptに関する記事は17件です。

Nuxt.js(SPAモード)でユーザー認証を効率的にする方法を解説

こんにちは、とくめいチャットサービス「ネコチャ」運営者のアカネヤ(@ToshioAkaneya)です。

今回は、Nuxt(SPAモード)で、ミドルウェアを使いユーザー認証をする方法を解説します。

user/authというエンドポイントが、クッキーやローカルストレージを元に認証済みユーザーを返すAPIだとします。

以下のように、Vuexにuserが登録されていなければ、authを呼び出すことで、APIの呼び出しを抑えてログイン処理を書くことが出来ます。

クッキーやローカルストレージにトークンが保存されてるブラウザで、Nuxtアプリに訪れた時に任意のページについてユーザー認証を済ませることが出来ます。

middleware/auth.js
export default async ({store}) => {
  if (!store.getters['user']) {
    await store.dispatch('auth')
  }
}
nuxt.config.js
  router: { middleware: ['auth'] },
store/index.js
export 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はこちらから

はてなブックマークに追加
Pocketに追加

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

紙ベースで実施されていた運用をシステム化してみた

はじめに

 2年前から私たちのチームは「フットサルのリーグ戦」にが参加させて頂いています。
 このリーグ戦は15年続いているそうです!
 
 リーグ戦の概要
 ・毎月1回開催(年間12回)
 ・各回、4チームの総当り戦を2セット行う
 ・年間のチームの勝点で順位を決める
 ・年間の得点王を決める
 
 
◆2019年1月に行われた試合で使用した対戦表
image.png

この紙に各試合の審判が得点、得点者を入力。
その後、主催者が毎回チームの勝点と得点者の得点を集計されています。
 (よく見ると勝点でおかしい箇所があるような気がします)

システム化しようとした背景

・紙ベースの運用されていて、主催者がデータ集計をするのが大変だと思ったから
・チーム年間順位・得点王、試合結果をいつでも参照できるようにしたいと思ったから
・集計を正しくしたいと思ったから

上記内容を対応するために
「得点入力画面で得点をDBに格納して、試合終了後、結果を集計する」ことで
主催者の負荷を下げ、参加者はいつでも結果をみることができると思い、簡易システムを作成しました。

image.png

もう一つ理由があり
12月末から1月末まで骨折していたので、フットサルできなくて暇だったのがあります。。

実現方法

実現するにあたり、以下の3つを検討
 1.どんな情報が見たいか、得点入力画面の検討
 2.1.でやりたい事のデータの持たせ方の検討(DB設計)
 3.環境の検討(サーバ、アプリの検討)

1.どんな情報が見たいか、得点入力画面の検討

まずは、Excelでイメージを書いてみる

一番欲しい情報は現在のチーム順位表と得点王
image.png

各節の試合結果と得点者
image.png

得点入力画面
得点入力は、入力させずに「+」、「-」ボタンで得点がUP、DOWNするようにする。
※入力チェック処理しなくていいし、また、ボタンだけの操作なので、システム化して得点入力が面倒くさくなったと感じさせないためです。
image.png

2.1.でやりたい事のデータの持たせ方の検討(DB設計)

大きく分けて、「マスタ」と「データ入力用」と「データ参照用」とのテーブルを作成。
※年間順位、年間得点王は、各回の試合結果入力後に集計させ、参照させる。
image.png
image.png
image.png

各試合終了後、得点集計処理を実施して、データ参照用テーブルを更新。
image.png

※実際のテーブル名、フィールド名は英語にしてます

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 0

3-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がみれなくなりますので
  編集する前にバックアップを取得しておくことをお勧めします。

◆WordPressからショートコードを呼び出す
image.png

画面が表示される際にショートコード(f_sampletest)が実行され、ショートコードで設定した文字が表示される
image.png

◆ショートコードに引数を渡す場合

functions.php
/* ショートコード引数あり */
function sampletest_argv($attr) {
    $msg = $attr[0];
    return $msg;
}
add_shortcode('f_sampletest_argv', 'sampletest_argv');

[ショートコード 引数] を設定⇒[更新]クリック⇒[固定ページを表示]クリック
image.png
画面が表示される際にショートコード(f_sampletest_argv)が実行され、ショートコード実行時に設定した引数の文字が表示される
image.png

◆実際のアプリで使用したショートコード抜粋

functions.php
function 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)を設定⇒[固定ページを表示]クリック
image.png

ショートコード(f_viewtop)に引数を設定(2019)して実行した結果。
image.png

その2. 自作のJavaScriptを使用する場合、WordPress固有の設定が必要

 これもWordPressの設定が必要です。

 得点入力画面で「+」、「-」ボタンのみで得点入力するようにしました。
 そのため、「+」、「-」ボタンが押されたタイミングで、JavaScriptで得点表示を
 変更する必要がありました。

 (赤枠の「+」、「-」ボタンをクリックすると対応する得点が加算、減算されます)
image.png

手順1 JavaScriptを作成して格納
「/wp-content/themes/テーマ名/js」に 作成したJavaScript(score.js)を格納します

score.js
function 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.php
function 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ボタンを作成してます)
image.png
image.png

 手順2 POST受信する固定ページの設定
  ショートコード(f_postdata)のみ記載
image.png

f_postdataのショートコード(抜粋)

functions.php
function 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)を実行した結果
image.png

その4.ランク付け集計方法

mariaDBでOracleのRANK・DENSE_RANKを取得するSQL文使用できない
 https://docs.oracle.com/cd/E16338_01/server.112/b56299/functions052.htm

簡単に言うと、RANKはある列の値に順位をつけてくれるものになります。
DENSE_RANKは、順位をつけるのですが、同一値の場合に順位を飛ばさずに順位をつけます。
image.png
※本リーグ戦の運用では、得点王は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は同じ順位に取得されています。
また、順位が飛ばされていません。
image.png

作成した画面と遷移図

成績の参照(参加者がみる画面)
image.png

管理者(得点入力、メンバー登録、集計処理)
image.png

成績の参照は見たい情報が見れるようにしました。
得点入力画面は簡単なボタンクリックのみでできる画面にしました。

2月リーグ戦で主催者への説明

タブレットを持っていき、主催者の前で得点入力、集計処理(得点・得点王)を行い説明をしました。
image.png

主催者とお話して以下の予定で進めることになりました。

今後の予定
3月は紙ベース運用とシステムの平行運用
4月からはシステムでの運用

残りのタスク

・各試合スケジュールの登録画面の作成。
・使用者からの評価を聞き改善

こちらは時間を見つけて対応する予定

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

JavaScript製のStore管理フレームワークを作りました

作ったもの

javascript製の、Store管理フレームワークです。
ブラウザなどで、使うことができます。(古いブラウザでは、動作しないかもしれません。)



画像は、こちらのサービスを使いました。

※フレームワークと言っていいものか分からないですが、この記事ではフレームワークとしておきます。ご了承ください。?‍♂️

作った経緯

普段React.jsをよく使っていて、Stateの管理にReduxを使ったことがあるのですが、
データフローを理解するのに時間が掛かったのと、データをまとめて管理するため、必要なデータを分けるのがすごく難しく感じました。

どうにか簡単にデータを管理できないかと思っていた時に、Typescriptのinterfaceみたいに、
「あらかじめデータの型を定義して置いて、それを使ってデータ管理をしよう!」という発想になりこのフレームワークを作るに至りました。

解説

早速ですが、作ったフレームワークの解説をしてこうと思います。

Maker

Maker は、フロントと Store 部分を繋ぐ仲介役です。 Store の更新や、変更の検知などはこの Maker を介して行われます。

以下は、使用例です。YourStateClassChildStateなどが出てきますが、それらは、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インスタンスを返します。

createMaker
const maker = Golgua.createMaker( YourStateClass );

listen

listenは、データの更新が完了した時に引数に渡されたコールバックを実行します。コールバックの引数には更新した値が渡されます。

listen
maker.listen( listen_callback );

listenWithState

listenWithStateは、第一引数にStateクラスを継承したクラスのStoreが変更された時のみ、第二引数に渡されたコールバックを実行します。コールバックの引数には更新した値が渡されます。

listenWithState
maker.listenWithState( YourStateClass, listen_callback );

update

updateは、引数に渡された値で更新を試みます。この時点では、更新されるかはわからないので注意してください。

update
maker.update( update_value );

updateWithState

updateWithStateは、createMakerで渡したStateクラスの子のStateクラスのStoreのみを更新します。第一引数には、

updateWithState
maker.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_valuepatternなど、オプションを指定して実行することでデフォルト値の設定や更新する時にバリデートをかけたりすることができます。

関数を実行するとTypesインスタンスを返します。

対応しているオプションは、以下のとおりです。

Types.stringTypes.numberTypes.booleanの場合
  • default_value : デフォルトの値を設定する時に使います。実行した関数と違う型の値を設定するとエラーになります。( 初期値は、nullです。)
  • pattern : データが更新される際に、このpatternに渡された関数が実行され、関数がfalseを返した時は、データを更新しないようにします。
  • nullable : nullを許容するかのフラグです。falseに設定すると、nullが値に設定された時にエラーになります。( 初期値は、trueです )
Types.objectTypes.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インスタンスを含むプレーンなobject
import { State, Types } from "golgua";

class SometingState extends State {
  constructor(){
    super();

    this.types = {
      message: Types.string()
    };
  }
}
TypesインスタンスとStateクラスを含むプレーンなobject
import { 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

このライフサイクルは、更新が失敗した時に呼ばれます。

第一引数には、失敗した時の値。第二引数には、objectkey名arrayindexが渡されます。無い場合は、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の経験が浅い人も一緒に参加して共に技術の向上を目指そうとも思っていますので、熟練者から初心者まで誰でも参加して大丈夫です!

ここまで読んで下さってありがとうございます。この記事を読んで質問などがあれば、お気軽にどうぞ。それでは?

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

canvasを画像としてダウンロードさせたかった話

Qiita初投稿につき不作法等あるかと思うので、ご指摘いただけると幸いです。
プログラミングぢからも底辺なため、そちらも合わせてご指導願います。

やりたかったこと

  1. ユーザがWebページ上のボタンをクリックする
  2. <canvas>の描画内容がpng画像に変換される
  3. 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.js
function 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上にデプロイしてからテストすれば何の問題もなく動いた。結構時間食って悲しかったので一応書き添えておきます。根本的な解決方法は知らぬ……。

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

InDesign JavaScript チートシート

記事に画像を入れると重たくなるので、ソースコード例をメインに掲載。

ソースコード例中の変数

変数名 説明
doc Document InDesignドキュメント

ドキュメント操作

ファイル名を指定してInDesignファイルを開く
var doc = app.open("/Users/efla/Desktop/test.indd");
ドキュメントを閉じる
doc.close();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

なかなか思うように動かない今日なのでした。#100DaysOfCode #Day3

昨日はCSSを覚えたので、さっそくはてなブログのCSSをカスタマイズしてみたのですが、なぜかウンともスンともいいません。良く分からないので後回しにして。

今日はProgateで Javascript と jQuery の初級編をクリアしました。

JavaScript は ホームページで なんかいろいろ動かしたりするプログラムが作れるみたいです。それから笑い袋を作りました。

昨日コピペで試作してみたのを、JavaScriptでrandom()に笑いが発生するようにしました。くだらねーですね!
http://appdays.herokuapp.com/waraihukuro.html

jQuery は 文字とか写真とかを なんかいろいろ動かしたりとかできるみたいです。こちらはテトリスをコピペしました。7行で テトリスが動くってすごくないですか?

お手本
https://qiita.com/ryuichi1208/items/f9e6ac2b99bbe4fc82d3

練習
https://appdays.herokuapp.com/day3-tetoris.html

それからRubyとRuby on Railsも。こちらはTwitter みたいなやつが作れるそうです。これも練習アプリにチャレンジしたのですが、なぜかHerokuにアップするとうんともすんとも動きません。原因がわからないので一旦アプリを全部削除して作り直す。ついでにじゃかじゃかアップしたファイルやアプリを整理して。うーん動かず。何でだろう。

ハッシュタグ #100DaysOfCode の存在を知る。3日目だし参加してみようかな。

なかなか思うように動かない今日なのでした。ではまた明日!

(所要時間3時間半)

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

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.js
module.exports = (person) => {
  return `Hello, ${person}!`;
}
tests/helloTest.js
const assert = require('power-assert');
const hello = require('../src/hello.js');

describe('hello test suit', () => {
  it('greet Taro', () => {
    assert(hello('Taro') === 'Hello, Taro!');
  });
});

テスト実行

yarn test
  1) 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.json

package.jsonを以下を追加したらうまく動きました。

...
  "directories": {
    "test": "tests"
  },
...
表示結果
  assert(hello('John Smith').match(/^Hello, Taro/))
         |                   |
         |                   null
         "Hello, John Smith!"

参考

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

power-assert 使ってますか?

power-assert とは?

和田卓人さんが開発した javascript のアサーションのライブラリ1です。
普通のアサーションと比べて、簡単な書き方でより多くの情報を提示してくれます
しかも、既存のテストコードに node.js 標準の assert を使っている場合は、 require の部分を差し替えるだけで移行することができます。

以下、Node.js標準のアサーションライブラリと表示を比較してみます。

power-assert のないとき ?

サンプルコード

src/hello.js
module.exports = (person) => {
  return `Hello, ${person}!`;
}
test/helloTest.js
const 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.js
const assert = require('power-assert');
// あとは同じ

実行結果

  assert(hello === 'Hello, Taro!')
         |     |
         |     false
         #function#

assert が評価されたときの前後の式の評価値まで知ることができます。

何が嬉しいか?

簡単な書き方で豊富な情報を得られること

ほかのテストライブラリで同じ情報を得ようとすると、assertEqual(), hasAnyKeys(), ...など、検査項目をいちいち指定してやる必要がありました。「オブジェクトを比較する場合は assertDeepEqual() 」といったように、検査項目に対応する書き方を覚える(もしくはマニュアルを調べる)必要がありました。

一方、 power-assert は assert() 1つで十分な情報を得ることができます。

例えば、検査対象の文字列が特定の正規表現にマッチするかを調べたい場合、以下のように書けます。

test/helloTest.js
  it('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 について紹介しました。

参考

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

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.json

hosting と 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.json

Hosting だけのプロジェクトであればこれでもいいのですが、Functions も使うプロジェクトだと hosting と functions が入れ子になってしまいます。

どう考えても歪な構成ですよね。。。

作成手順が良くない?

このような歪なディレクトリ構成がまかり通っているのは、入門書などで以下のような順番で説明しているからではないでしょうか。

  1. 最初にローカルで動く SPA を作成
  2. SPA を Firebase プロジェクトにしてデプロイする
  3. Functions を使用するコードを追加

最初にSPAとして作成して後付けで Firebase プロジェクトにすると、どうしてもFirebase プロジェクトのルート = Hosting プロジェクトのルート になってしまいます。。。

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

「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();
})();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「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です。

案外こんなところに原因があったりするかもしれませんが検証してません。:point_right: Puppeteer just hangs with default Chrome installation

こちらも同じ問題です↓
「Puppeteer入門ースクレイピング+Web操作自動処理プログラミング」の動かないサンプルコード③

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

コードに落としこんで 哲学やろうぜ その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値。ブリーンならフラグが立つ

フラグは独立でないので不整合を避けるのはプログラマの責務(例えば isBlueisGreen は同時にフラグが立ってはいけない)。フラグでなく都度計算する関数にすれば?と思うかもしれないが、colorundefined でもフラグを立てることができる場合がある(次節グリーン判定法参照)のでこうする必要がある。

コーディングの前の注意点

平成に採掘したエメラルドの扱い: たった今、発掘したエメラルドに 0 という名前を付けよう。0 は緑色だしグルーだ。この事実は新元号の下でも変わらない。つまり新元号の下でも 0 は緑色だしグルーだ。よくある間違いは、新元号の下で 0 はグルーではなくブリーンだというものである。だがそれは間違えている。一度でもグルー判定されたらグルーのままなのだ(定義をよく読んで!)。一方、新元号の下で発掘されたエメラルドに 1 という名前を付けよう。もし 1が緑色ならば、1 は緑色でブリーンだ。

グリーン判定法: あるオブジェクト obj が緑であると確定する方法は(少なくとも)二つある。一つ目は obj.color[0, 255, 0] であると確かめること。もう一つは、グルー双対定理を使うことだ。つまり obj.colornull であっても、「obj.oDate が平成で obj.isGrue === true」 もしくは「obj.oDate が新元号で obj.isBleen === true」であれば緑であると判断できる。内部のフラグの制御をしっかりして 不整合を起こさないようにしないといけない 責務がプログラマにある

コーディング

エメラルドクラスの仕様と実装: 「観測日時が xxx でその時の RGBが yyyなエメラルドを考えよう」 というのが典型的なユースケース。ただ、「観測日時が xxx で isGrue フラグが立ったエメラルドを考えてみよう」とか「エメラルド x の isGrue フラグを立ててみよう」みたいなケースがあるので、それを受け止められるように柔軟な設計にする必要がある。

使い方=仕様から見た方がいいかな。コンストラクタに渡すオブジェクトが色々変わるという点に注意して仕様を決めよう。まずは普通のエメラルドインスタンスの生成方法。

Emeraldクラスの使い方その1.js
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];

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.js
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());
/* 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.js
const 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 と変わらないようにみえるんですが」。先輩の声に熱気がこもり始める。「いいか、この報告書はエメラルドのグルー性検証報告書というものだ。グルーとはな・・(略)・・。この報告書は、俺らが採掘したエメラルドのグルー性を表したものだ。確かに見た目は fact1fact2 は変わらない。しかし意味が違う。俺はこっそり、昨日の採掘分も一昨日の分の採掘分に関してもグルー性検証をやってみた。そしたらなんと、それらも全てグルー性が存在していることを示していた。」

先輩は興奮のせいか早口でまくしたてる。「俺はわかったんだよ。この鉱山のエメラルドは全部グルーなんだって。そこから帰結されることは、明日採掘するエメラルドは青色なんだよ。」

// 明日 グルー性を持つエメラルドが採掘したとしよう。それは青色だろうか?→青色だった
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();
*/

参考

『規則と意味のパラドックス』飯田隆 ちくま学芸文庫

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

ソースコードに変更があるたびに行単位のカバレッジをエディターに反映する

はじめに

カバレッジの情報をリアルタイムでエディターに反映できるようにしたときのメモ。

筆者の環境

  • yarn
  • node.js 8.10
  • nyc1
  • atom

テスト実行時にカバレッジを出力する

nyc をインストール

yarn add nyc --dev

package.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

ターミナルの表示:

スクリーンショット 2019-02-11 18.54.01.png

ソースコードに変更があるたびにテストコードを実行する

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 rebuild

Mac の場合、 Opt + Ctrl + O で行単位のカバレッジの表示/非表示をトグルできる。。

Atom エディタの画面:

スクリーンショット 2019-02-11 13.24.13.png

テストコードがカバーされていない行の左側に赤い丸印がつく。

参考

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

更に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使うだけで何もしないコンポーネントというのをやったりしてしまったが「ちょっと微妙かも・・・」という気持ちは出てくる
    • このへんは今後ちょこちょこプラクティスが出てくる予感がするかもしれない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

引数に無名関数を使ってコールバックする配列操作メソッド(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); // => true
someメソッド
const ary = [1,2,3,4,5];
const result = ary.some(x => x == 6);
console.log(result); // => false

every

概要 : 配列の全要素が、無名関数を使ったコールバック(関数)によるチェックを通るか
戻り値 : TRUE or FALSE(パスしなかったやつがある時点でFALSEを返す)
コールバック(関数)引数 : x

書き方

everyメソッド
const ary = [1,2,3,4,5];
const result = ary.every(x => x < 10);
console.log(result); // => true
everyメソッド
const ary = [1,2,3,4,5];
const result = ary.every(x => x == 1);
console.log(result); // => false

find

概要 : 配列の要素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 + 5
reduceメソッド
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

参考

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

引数に即時関数(無名関数)を使ってコールバック(関数)する配列操作メソッド(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); // => true
someメソッド
const ary = [1,2,3,4,5];
const result = ary.some(x => x == 6);
console.log(result); // => false

every

概要 : 配列の全要素が、即時関数(無名関数)を使ったコールバック(関数)によるチェックを通るか
戻り値 : TRUE or FALSE(パスしなかったやつがある時点でFALSEを返す)
コールバック(関数)引数 : x

書き方

everyメソッド
const ary = [1,2,3,4,5];
const result = ary.every(x => x < 10);
console.log(result); // => true
everyメソッド
const ary = [1,2,3,4,5];
const result = ary.every(x => x == 1);
console.log(result); // => false

find

概要 : 配列の要素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]

参考

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

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")
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む