20190714のJavaScriptに関する記事は20件です。

1つずつ数字が確定するアニメーションの実装

始めに

カウントアップのアニメーションは調べたらいくつかありましたが、最初はシャッフルされていて、1つずつ数字が確定するようなアニメーションは見つからなかったので記事にしました。(調べ方が悪いだけの可能性もありますが(汗))
以下にサンプルを載せます。データ周りが結構ややこしくなるかと思ったのですが、別に桁ごとにデータを持たなくても実装できたのでそこまで複雑ではありませんでした。

See the Pen 1つずつ数字が確定するサンプル by wintyo (@wintyo) on CodePen.

実装方法

桁数を取得する

最初にランダムに表示する桁数を目標の値から取得する必要があります。ネイティブで実装されていたら楽でしたが、なかったのでメソッド化します。

桁数の取得
/**
 * 桁数を取得する
 * @param {number} number - 数値
 * @returns {number} - 桁数
 */
function getDigits(number) {
  // 0の場合は1桁を返す
  if (number === 0) {
    return 1;
  }

  let digits = 0;
  while (number >= 1) {
    number /= 10;
    digits += 1;
  }
  return digits;
}

ランダムに数字を表示する

まずはシャッフル状態のアニメーションを作ります。アニメーションと言っても、ただintervalを使って表示する数字を差し替えているだけです。単純にランダムにするなら1回の乱数でいいですが、一桁ずつ確定していくことを踏まえて0~9の乱数を各桁ごと行います。各桁を算出できたら後は足し合わせます。各桁を上手く足せるようにべき乗を使用しています。
まとめると以下のようになります。今回は実装を楽するためにlodashのtimesとpadStartを使用しています。

ランダムに数字を表示する
const value = 2048;
const digits = getDigits(value);

window.setInterval(() => {
  // 各桁の数値を算出する
  const numbers = _.times(digits).map(() => {
    return Math.floor(10 * Math.random());
  });
  // 足し合わせる
  const displayValue = numbers.reduce((accumulator, number, index) => accumulator + (10 ** index) * number);

  // 0詰めで表示させる
  countElement.innerHTML = _.padStart(String(displayValue), digits, '0');
});

下の桁から確定していく

後は各桁の数値を算出する場所で表示したい値を出すか乱数を出すか場合分けをしたらいいです。

一部の桁だけ数字を固定する
const value = 2048;
const digits = getDigits(value);

let fixDigits = 1;  // 1桁目だけ数字を出す
window.setInterval(() => {
  // 各桁の数値を算出する
  const numbers = _.times(digits).map((index) => {
    // 固定したい桁の場合は最終値を返す
    if (index < fixDigits) {
      return Math.floor(value / (10 ** index)) % 10;
    }
    return Math.floor(10 * Math.random());
  });
  // 省略
});

イベントをつなぎあわせる

最後にsetTimeoutを使ってfixDigitsのところをカウントアップしていき、最後の桁までたどり着いたらintervalを止めて完成です。

終わりに

そもそも言葉がわからないというのもありますが、今回のようなアニメーションは探しても見つからなかったので記事にしました。数字の表示は他にもいろいろあるようで、リンクを貼っておくので興味があればそちらも参考にしてみるといいかもしれません。

それ以外の数字のアニメーション

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

妄想プロダクト室「ギュtto」のプロトタイプ

main_vidual.jpg

つくったモチベーション

こんな経験ありませんか?
チャットでwを生やすも、顔は真顔、、、?
それが普通だと思いますが、テキストだけのやりとりに私は寂しさを感じることがあります?
( もっと温度感のあるコミュニケーションがしたい、人を感じたい、、、 )
そこで、私はProtoOut LINE Things ハッカソンを機会に、新しい体感型通信デバイスをプロトタイピングしてみることにしてみました!
0b12f4281a6860052b6c9991e892e955.png
https://protoout.connpass.com/event/135942/

「ギュtto」概要

”もふもふハートフルデバイス”「ギュtto」をプロトタイピング。
ぎゅっと抱きしめると、抱きしめてる間だけ、ハートをスマホに送ってくれて、
スマホからもボタンをクリックすることで、ぬいぐるみをバイブレーションさせることができます。


技術・素材

  • LINE Things LIFF
  • obnize
  • サーボモータ
  • 圧力センサー
  • ぬいぐるみ

構成図

image.001.jpeg

今後

取得した圧力センサーの値によって、LIFF上の表現を変えたい。

processingやcanvasで動的に表現したい
(現状はGIF画像を表示しているだけ)

ハード側のバイブレーション部分を機能させたい。

現状、サーボモータをバイブレーション機能として代用しているが、
もっとわかりやすくぬいぐるみがバイブするような実装がしたい

参考

https://qiita.com/n0bisuke/items/1f552f3508fe8cf22759

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

それは「else if」ではない「else」と「if」だ

 言語によっては elseif なんていうのもありますが、「else if」と言われているものは、elseのときに実行する処理がif~だという超小ネタです。

「if や else は必ず {} で囲め」というコーディング規約に書いているのに、そのコーディング規約の中でもelseやifを……

if (/*条件*/) {
  // 略
} else if (/*条件*/) {
  // 略
}

……みたいに書かれていたりすると「こいつ分かってないな」と思ってしまいます。

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

【JavaScript】クロージャとクラスの書き方の違い

もともとswiftで「クロージャ」について調べていました。するとJavaScriptでも同じ概念があるということで、記事数も多いJavaScriptで調べてみました。
特に感じたのが、クラスと似ているということ。そこでクラスとの違いの観点からクロージャの使い方についてまとめてみました。

イメージ

クロージャはクラスと同じようなことができ、比べるとこんなイメージです。

  • クラスは「クラスの中に関数がある」
  • クロージャは「関数の中に関数がある」

クラスの使い方

以前はJavaScriptにクラスという概念がなく、ES6で新しくクラスを作れるようになりました。
以下の例は、Userクラスを定義して名前を変更、取得する動きをします。

class User {
  constructor() {
    this._name = '太郎';
  }
  userName() {
    return this._name;
  }
  setName(name) {
    this._name = name;
  }
}
const user = new User();
console.log(user.userName()); //太郎
user.setName('次郎');
console.log(user.userName()); //次郎
console.log(user.name); //undefined

getsetを関数の前につけるとゲッターとセッターにすることも機能としてありますが、今回はJavaのように書ける方を優先しているため使用していません。
プロパティはconstructor()内で定義するのがちょっと面倒でしょうか。
また_nameなど変数にアンダーバーを付けている理由は、プライベートな変数を表現するためです。ただし本当にプライベート変数になっているわけではなくconsole.log(user1._name)で取得できてしまいます。あくまでルールとしてこのようにすることもできます。個人的な話ですが、実はアンダーバーをつければ勝手にプライベート変数になると勘違いしていたんですよね。。。

クロージャの使い方

下の例は上のクラスの例と同じ動きです。

function funcUser() {
  let name = '太郎';

  return {
    getName: function() {
      return name;
    },
    setName: function(value) {
      name = value;
    }
  };
}

const user = new funcUser();
console.log(user.getName()); //太郎
user.setName('次郎');
console.log(user.getName()); //次郎
console.log(user.name); //undefined

クロージャのイメージは関数の中に関数があるです。この形を取ることで外側の関数=クラスみたいな使い方ができます。
文法の説明ですが、関数の内部でも関数を書くことができるだけでなく、returnで関数を返すこともできます。さらにその返す関数をオブジェクト形式で関数を書くことで、呼び出し方が変数.関数名の形で実行することができます。newするところもクラスの書き方と同じですね。
しかも関数内の変数にはアクセスできないので(関数スコープ)、実質プライベートなプロパティを作ることもできます。

比較して

こうしてみるとクロージャの方が便利なのでは?と思いましたが、以下のようなデメリットもあります。

  • 継承できない(クラスはできる)
  • メモリリーク問題
  • 他言語からきた人にとってとっつきにくい

なので基本的にはクラスを使った方が良く、クロージャは特殊な書き方の例として留めておくのが私の中での結論です。

参考

現代の JavaScript チュートリアル

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

手動テストを圧倒的に効率化するChrome DevTools技3選

はじめに

Chrome DevTools、とっても便利ですよね。
自分はそんなに使いこなせてないので、同僚エンジニアのディスプレイを覗き見ると「ま、またおれの知らないDevTools技使ってる……!」と驚かされることがよくあります1

今回は、自分が同僚から教えてもらった多くのスゴ技のうち、良く使うものを3つご紹介します。

なお、Chromeの話をしていますが、きっと他のブラウザにも似たような機能はあるかと思いますので、Chrome絶対使いたくないマンはぜひお気に入りのブラウザで探してみて下さい。

Javascriptの実行を途中で止める

DevTool上からブレークポイントを設定することができます。

image.png

ブレークポイントをご存じない方のために説明しておくと、「特定の場所まで来たら実行を一時停止する」機能です。
↑の例では 特定のDOM要素に変更があった時に実行を一時停止 させていますが、他にも

  • コードの特定の行に差し掛かったら実行を一時停止する
  • 特定のイベントが発火したら(例えば特定の要素のclickイベントが発火したら)実行を一時停止する

などが出来ます。
テスター的には上記例で挙げた 特定のDOM要素に変更があった時に実行を一時停止 が一番便利でしょう。


停止後は再生ボタンが表示され、停止を解除して次のブレークポイントまで実行したり、コードを1行ずつ実行(ステップ実行と呼びます)することもできます。

image.png

特定の操作を繰り返し実行する

Network タブを開くと、それ以降に発生した通信ログを全て記録してくれます。
SPAとかでAjax通信がたくさん発生するシーンでとても有効でしょう。
不具合の原因がフロントエンドにあるのかバックエンドにあるのかを突き止めるのに使ったりしています。

個人的にとても気に入っているのが、 Copy As Fetch という機能で、これはリクエストを Console タブに貼り付けて実行できる形でコピーしてくれます。
画面上の操作を繰り返し実行したい時に便利です。

image.png

image.png

例えば「コメントを100回投稿する」みたいなことをやりたい時に

  • コメントを投稿する
  • Network タブで該当のリクエストを探し出す
  • Copy As Fetch でリクエストをコピーする
  • Consoleにペーストする
  • オラオラオラオラオラオラオラオラオラオラオラオラオラオラ(略)

みたいにすると簡単に量産することが出来ます2

Cookieを見たり編集したりする

Application タブからStorageやCacheを見ることができます。
Storageというのはブラウザ内で保存しているデータのことです。
よくCookieとかLocalStorageとか聞くと思うんですが、まさしくそれのことです。

バグチケットを切った時に
「こちらの環境では再現しないですね」
「なんか変なCookieが残ったりしてないですかね」
などのように言われたら、とりあえず Application タブをチェックしてみましょう。

image.png

Cookie周りの不具合が発生したときは、Webアプリ側の操作でどのようにCookieの値が変わるのかを見ながらテストするとわかりやすいです。
また、値は編集可能ですので、直接編集して挙動を確認してみるのも良いでしょう。

不具合調査以外で自分が良く利用するユースケースでは、例えば言語設定をCookieに保存していたりする場合、 locale: ja となっているところを en に変更して言語を切り替える、みたいなことをしたりすることがあります。


  1. ごくまれに、せっせとSlackの絵文字作ってるところに出くわすこともあります。 

  2. 実際にはCSRFトークンなどの関連でうまく動かないときもあります。 

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

ライブラリ作ってみました(jQuery編)

自作ライブラリを作った経緯

 ソフトを開発するにあたって、自作のライブラリを使用しています。
理由は、勉強のためと言うこともあるし、自分で作った方が痒い所に手が届くということがあるからです。宜しければ、ご覧ください。

jQueryライブラリ(github)

javascriptライブラリ

 javascript(jQUery)に関するライブラリ

 煩雑な処理をまとめたもの

 ClassLibCanvas.js

  キャンバス描画に関するライブラリ

 ClassLibMath.js

  計算に関するライブラリ

 ClassLibTime.js

  日時操作に関するライブラリ

 ClassLibUtility.js

  一般操作のライブラリ

  配列関連、テーブル操作など

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

Winlogbeat 7.2.0 新機能: ScriptプロセッサのモジュールをJavascriptで作ってみる

はじめに

前記事の「Winlogbeat 7.2.0の新機能: JavascriptプロセッサのSysmonモジュールを使ってイベントログを採取してみる」の続編です。

今回の記事ではそのJavascriptプロセッサ機能を利用してWindowsイベントログをElasticsearchへ転送する前にWinlogbeat側(エンドポイント側)でフィルタまたは加工する独自モジュールをJavascriptで書いてみます。

  • hello, worldをフィールドへ書き込むHelloWorldモジュール
  • PowerShell実行イベントログのBase64エンコードされたコマンド部分(-encオプション)をデコードしてからElasticsearchへ転送するモジュール

※Javascriptプロセッサはまだ新しい機能ですので仕様変更も今後予想されます。詳細は公式リファレンスを参照してください。

※本記事で扱うサンプルモジュールはこちらへ置いておきます。

評価環境

log_network_sysmon_winlogbeat.png

利用するソフトウェア

  • OS: Ubuntu 18.04
  • DNSサーバ: Dnsmasq (2.79)
  • Elasticsearch (6.6.0)
  • Kibana (6.6.0)
  • Winlogbeat (7.2.0)
  • Sysmon (v10.2)
  • VMware Workstation Player等のVMソフトウェア
  • Windows 10 (クライアントPC)

インストール方法や評価環境の構築の詳細は「標的型攻撃に対するJPCERT/CCのおすすめログ設定をElasticsearchで構築してみる - エンドポイントログ編(その2- Winlogbeat/Elasticsearch)」または前記事をご参照ください。

Hello Worldモジュール

まずはWindowsイベントログに「greeting: "hello, world!"」というフィールドを追加する単純なモジュールを書いてみましょう。以下のように非常にシンプルなコードを記述しファイル名をwinlogbeat-helloworld.jsとして保存します。

※UTF-8で保存した方がいいと思います。

winlogbeat-helloworld.js
function process(evt) {
    evt.Put("greeting","hello, world");
    return;
}

HelloWorldモジュールを配置

この自作モジュールのJavascriptファイル(winlogbeat-helloworld.js)を他のSecurityやSysmon標準モジュールと同じフォルダ構成で以下のように配置します。

C:\Program Files\winlogbeat-7.2.0-windows-x86_64\module\helloworld\config\winlogbeat-helloworld.js

C:\Program Files\winlogbeat-7.2.0-windows-x86_64はWinlogbeat」のインストールディレクトリ、「helloworld\config」がモジュール用に作成したサブフォルダです。

このモジュールをWinlogbeatへ組み込んでイベントログをElasticsearchへ転送すると以下のようにgreetingフィールドが追加されています。

Elasticsearchへ加工されたイベントログ
...
{
    "_index" : "winlogbeat-7.2.0-2019.07.14",
    "_type" : "doc",
    "_id" : "klT27WsBgpTg8iKCzG_m",
    "_score" : 0.0,
    "_source" : {
         "@timestamp" : "2019-07-14T00:51:42.880Z",
         "message" : "Process Create:\nRuleName: \nUtcTime: 2019-07-14 00:51:42.880\nProcessGuid: {22052e76-7c9e-5d2a-0000-00106f766e00}\nProcessId: 7720\nImage: C:\\Windows\\System32\\PING.EXE\nFileVersion: 10.0.17763.1 (WinBuild.160101.0800)\nDescription: TCP/IP Ping Command\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: ping.exe\nCommandLine: \"C:\\WINDOWS\\system32\\PING.EXE\" www.youtube.com\nCurrentDirectory: C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\\\nUser: xxxxxxxxx\\xxx\nLogonGuid: {22052e76-11ef-5d2a-0000-002070890a00}\nLogonId: 0xA8970\nTerminalSessionId: 1\nIntegrityLevel: High\nHashes: SHA256=741AD992403C78A8A7DBD97C74FDA06594A247E9E2FA05A40BB6945403A90056,IMPHASH=8C3BE1286CDAD6AC1136D0BB6C83FF41\nParentProcessGuid: {22052e76-670e-5d2a-0000-001062cf5900}\nParentProcessId: 5152\nParentImage: C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\nParentCommandLine: \"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" ",
...
         "greeting" : "hello, world",
...
    }
},
...

自作モジュールを設定ファイルへ追加

Winlogbeatの設定ファイル(例: C:\Program Files\winlogbeat-7.2.0-windows-x86_64\winlogbeat.yml)へモジュール(winlogbeat-helloworld.js)を追加します。今回の例ではSysmonイベントに対してフィルタ・加工処理を行います。そのためwinlogbeat.event_logsの「name: Microsoft-Windows-Sysmon/Operational」フィールド配下へscriptプロセッサとして指定します。

winlogbeat.yml
#======================= Winlogbeat specific options ===========================
...
winlogbeat.event_logs:
  - name: Application
    ignore_older: 72h

  - name: System
...
  - name: Microsoft-Windows-Sysmon/Operational
    processors:
      - script:
          lang: javascript
          id: sysmon
          file: ${path.home}/module/sysmon/config/winlogbeat-sysmon.js
      - script:
          lang: javascript
          id: helloworld
          file: ${path.home}/module/helloworld/config/winlogbeat-helloworld.js
...

編集した設定ファイルをテスト

作成したモジュールと編集した設定内容をテストしておきます。

PS C:\Program Files\winlogbeat-7.2.0-windows-x86_64> .\winlogbeat.exe test config
Config OK

Elasticsearchへのネットワーク接続テストも行っておきましょう。

PS C:\Program Files\winlogbeat-7.2.0-windows-x86_64> .\winlogbeat.exe test output -e -d "*"

Winlogbeatを再起動

Winlogbeatサービスを再起動します。

PS C:\Program Files\winlogbeat-7.2.0-windows-x86_64> Restart-Service winlogbeat

Elasticsearchへ転送されたイベントログに上述のようにgreetingフィールドが追加されます。

HelloWorldモジュールの詳細

winlogbeat-helloworld.js
function process(evt) {
    evt.Put("greeting","hello, world");
    return;
}

モジュール記述は上記のように簡単です。process関数を記述するだけでWinlogbeatがイベントログ毎に呼び出してくれます。

各Windowsイベントログは元のXML形式からJSON形式へ変換された後で、process関数の引数へEventオブジェクト(evt引数)として一つずつ渡されます。Eventオブジェクトにはイベントログを操作するためのメソッドが用意されており、フィルタや加工処理に量できます。例えばHelloWorldモジュールではこのEventオブジェクトのメソッドであるevt.Put("フィールド名","フィールド値")と呼び出すことでイベントログ内へJSONフィールドを挿入追加しています。

※Eventオブジェクトの実体はこちらです。

Eventオブジェクトのメソッド

リファレンスによれば以下のメソッドがEventオブジェクトへ定義されています。

メソッド                                                                                  説明
Get("フィールド名") イベントログ(JSON)のフィールドを取得。戻り値はスカラー、オブジェクトまたはnull(見つからなかった場合)。取得するフィールド名を指定しない場合にはイベントログ全体がオブジェクトとして取得されます。
Put("フィールド名", "フィールド値") イベントログ(Json)へフィールドを追加。すでにフィールドが存在する場合には値が上書きされます。追加されるフィールド値としてスカラー以外に設定可能なものについては後ほど検証してみます
AppendTo("フィールド名", "フィールド値") 配列形式のフィールドへ要素を追加します。ただし追加できるのは文字列値だけです。
Rename("旧フィールド名", "新フィールド名") フィールド名をリネーム。
Delete("フィールド名") フィールドを削除。
Cancel() イベントログ全体を削除
Tag("タグ値") タグフィールドへ(設定されていなければ)追加。

またイベントログ(Json)の各フィールドへはメソッドを利用しなくても<イベントオブジェクト>.fields.<フィールド名>という形式で直接アクセスすることもできます。

helloworld.js
function process(evt) {
    // イベントオブジェクトのフィールドへ直接アクセス
    evt.fields.greeting = "hello, world";
    return;
}

モジュールのデバッグ方法

テストコードを記述

モジュールのJavascriptコード内にtest()関数をテストプログラムとして記述することで、デバッグやテストを行うことができます。

winlogbeat-helloworld.js
// 実装したコード
function process(evt) {
    evt.Put("greeting","hello, world");
    return;
}

// テスト内容を記述する関数
function test() {
    //テストデータ(イベントログ)を生成
    var evt = new Event({
        message: "Windows event log message...",
    });

    // テストデータで実装コードをテスト
    process(evt);

    // テスト期待値の判定
    if (evt.Get("greeting") === "goodbye cruel world") {
        // 期待値と異なる場合には例外を投げる
        throw "expected greeting === hello, world";
    }
}

このtest()関数はWinlogbeatがモジュールをローディングするときに実行されます。そのため通常の設定ファイルテスト手順と同様に、以下のようにtest configを指定してwinlogbeat.exeを実行することでデバッグやテストを行うことができます。

テストOKの場合
PS C:\Program Files\winlogbeat-7.2.0-windows-x86_64> .\winlogbeat.exe test config
Config OK

では以下のようにgreetingへ「goodbye cruel world」と代わりに出力するようにしてみましょう。

winlogbeat-helloworld.js
// 実装したコード
function process(evt) {
    // hello, worldを別の値にしてみる
    evt.Put("greeting","goodbye cruel world");
    return;
}

// テスト内容を記述する関数
function test() {
    //Javascriptのオブジェクト型でテストデータ(イベントログ/JSON)を生成
    var evt = new Event({
        message: "Windows event log message...",
    });

    // テストデータで実装コードをテスト
    process(evt);

    // テスト期待値の判定
    if (evt.Get("greeting") === "goodbye cruel world") {
        // 期待値と異なる場合には例外を投げる
        throw "expected greeting === hello, world";
    }
}

以下のように「failed in test() function: expected greeting === hello, world at test」と例外が投げられエラーとなります。

テストNGの場合
PS C:\Program Files\winlogbeat-7.2.0-windows-x86_64> .\winlogbeat.exe test config
Exiting: Failed to create new event log. 
failed in processor.javascript: failed in test() 
function: expected greeting === hello, world at test 
(C:\Program Files\winlogbeat-7.2.0-windows-x86_64/module/helloworld/config/winlogbeat-helloworld.js:12:15(22))

ちなみにこのテストエラーのままでは以下のようにWinlogbeatサービスは起動しません。

PS C:\Program Files\winlogbeat-7.2.0-windows-x86_64> Start-Service winlogbeat
Start-Service : 次のエラーのため、サービス 'winlogbeat (winlogbeat)' を開始できません: コンピューター '.' でサービス 'w
inlogbeat' を開始できません。
発生場所 行:1 文字:1
+ Start-Service winlogbeat
+ ~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OpenError: (System.ServiceProcess.ServiceController:ServiceController) [Start-Service],
   ServiceCommandException
    + FullyQualifiedErrorId : CouldNotStartService,Microsoft.PowerShell.Commands.StartServiceCommand

PowerShellのStart-Serviceコマンドレットはエラーを返します。

未定義またはgojaで未サポートのJavascript API呼び出し

WinlogbeatのJavascriptプロセッサのエンジンはgojaです。そのため通常のWebブラウザでサポートされているJavascriptのAPIが利用可能とは限らない点に注意が必要です。
※gojaはECMAScript 5.1ベース。

以下は未定義の関数hoge()を呼び出した例です。以下のようにエラー(「hoge is not defined at process」)となります。

winlogbeat-helloworld.js
function process(evt) {
    evt.Put("greeting","hello, world");
    // 未定義またはgoja未サポートのAPI呼び出し
    hoge();
    return;
}

function test() {
    var evt = new Event({
        message: "Windows event log message...",
    });
    process(evt);
    if (evt.Get("greeting") === "goodbye cruel world") {
        throw "expected greeting === hello, world";
    }
}
テスト実行結果
PS C:\Program Files\winlogbeat-7.2.0-windows-x86_64> .\winlogbeat.exe test config
Exiting: Failed to create new event log. 
failed in processor.javascript: 
failed in test() function: ReferenceError: hoge is not defined at process 
(C:\Program Files\winlogbeat-7.2.0-windows-x86_64/module/helloworld/config/winlogbeat-helloworld.js:3:9(10))

ただし、test()関数の延長でその未定義関数呼び出しのコード部分が実行されない場合には、エラーとはならないので注意が必要です。

winlogbeat-helloworld.js
function process(evt) {
    evt.Put("greeting","hello, world");
    // 未定義関数呼び出し部分が実行されない場合
    if(false){
        hoge();
    }
    return;
}

function test() {
    var evt = new Event({
        message: "Windows event log message...",
    });
    process(evt);
    if (evt.Get("greeting") === "goodbye cruel world") {
        throw "expected greeting === hello, world";
    }
}
テストOKになる
PS C:\Program Files\winlogbeat-7.2.0-windows-x86_64> .\winlogbeat.exe test config
Config OK

Winlogbeatサービスへのモジュールのローディングを確認する

Winlogbeatの設定ファイルでログレベルをdebugへ変更することでモジュールのローディングを確認することができます。

winlogbeat.yml
...
#================================ Logging =====================================

# Sets log level. The default log level is info.
# Available log levels are: error, warning, info, debug
# 以下をコメントアウト
logging.level: debug
...

Winlogbeat(例: C:\Program Files\winlogbeat-7.2.0-windows-x86_64\logs\winlogbeat)のログへ以下のように出力されます。

Winlogbeatのログ
2019-07-14T12:15:25.733+0900 DEBUG [processors] processors/processor.go:93  
Generated new processors: 
script=[type=javascript, id=, sources=C:\Program Files\winlogbeat-7.2.0-windows-x86_64/module/sysmon/config/winlogbeat-sysmon.js], 
script=[type=javascript, id=, sources=C:\Program Files\winlogbeat-7.2.0-windows-x86_64/module/helloworld/config/winlogbeat-helloworld.js]

winlogbeat-helloworld.jsがScriptプロセッサのモジュールとして認識されています。

console.log()関数でデバッグ情報などをログへ書き出す

consoleモジュールをローディングすることで、Javascriptでおなじみconsole.log()関数も使えます。ただし、出力先はWinlogbeatのイベントログファイルになります。

console.xxx()の記述例:

  • console.debug("DEBUG: hello, world!");
  • console.log("INFO: hello, world!");
  • console.info("INFO: hello, world! %j", evt.fields);
  • console.warn("WARN: [%s]", evt.fields.message);
  • console.error("ERROR: %j", evt.fields);

以下はconsole.log()関数のサンプルプログラムです。

winlogbeat-helloworld.js
// consoleモジュールをローディング
var console = require("console");

function process(evt) {
    // Debugログでイベント(オブジェクト)全体を出力
    console.debug("process() called %j", evt.fields);
    evt.Put("greeting","goodbye cruel world");
    return;
}

function test() {
    var evt = new Event({
        message: "Windows event log message...",
        winlog : {
            provider_name : "Microsoft-Windows-Sysmon"
        }
    });
    process(evt);
    if (evt.Get("greeting") === "goodbye cruel world") {
        // Errorログを出力
        console.error("test() NG! %s",evt.fields.greeting);
    }else{
        console.log("test() OK!");
    }
    console.info("test() Done");
}

こちらもWinlogbeat(例: C:\Program Files\winlogbeat-7.2.0-windows-x86_64\logs\winlogbeat)のログへ以下のように出力されます。

Winlogbeatログ
...
2019-07-14T14:18:45.023+0900 DEBUG [processor.javascript] 
console/console.go:48   process() called {"message":"Windows event log message...","winlog":{"provider_name":"Microsoft-Windows-Sysmon"}}
2019-07-14T14:18:45.023+0900 ERROR [processor.javascript]
console/console.go:54   test() NG! goodbye cruel world
2019-07-14T14:18:45.023+0900 INFO [processor.javascript]
console/console.go:50   test() Done
...

イベントログをフィルタや加工するパイプラインを作成

processorモジュールを利用すると複数の関数で定義された処理を数珠つなぎにして、イベントログのフィルタや加工処理をパイプライン化することができます。

winlogbeat-helloworld.js
var console = require("console");
// processorモジュールをローディング
var processor = require("processor");

function process(evt) {

    // 処理1: helloメッセージをイベントログへ追加
    var addHello = function(evt) {
        evt.Put("hello","hello, world");
        console.debug("addHello() OK");
    }

    // 処理2: goodbyeメッセージをイベントログへ追加
    var addGoodbye = function(evt) {
        evt.Put("goodbye","goodbye cruel world");
        console.debug("addGoodbye () OK");
    }

    // processorモジュールのChain()メソッドでパイプラインを生成
    var pipeline = new processor.Chain()
        .Add(addHello)   // 処理1をパイプラインへ追加
        .Add(addGoodbye) // 処理2をパイプラインへ追加
        .Build()         // パイプラインを組み立て

    // パイプラインを実行
    pipeline.Run(evt);
    return;
}

function test() {
    var evt = new Event({
        message: "Windows event log message...",
        winlog : {
            provider_name : "Microsoft-Windows-Sysmon"
        }
    });
    process(evt);
    if (evt.Get("hello") !== "hello, world") {
        throw "expected goodbye !== hello, world";
    }
    if (evt.Get("goodbye") !== "goodbye cruel world") {
        throw "expected goodbye !== goodbye cruel world";
    }
    console.debug("test() Done");
}

パイプラインで処理1と処理2を実行し加工されたイベントログは以下のようになります。helloとgoodbyeのフィールドが追加されています。

パイプラインによる処理結果
      {
        "_index" : "winlogbeat-7.2.0-2019.07.14",
        "_type" : "doc",
        "_id" : "YFQS72sBgpTg8iKCw3VG",
        "_score" : 0.0,
        "_source" : {
          "@timestamp" : "2019-07-14T06:01:51.413Z",
          "hello" : "hello, world",
          "goodbye" : "goodbye cruel world",
...
          "message" : "Process Create:\nRuleName: \nUtcTime: 2019-07-14 06:01:51.413\nProcessGuid: {22052e76-c54f-5d2a-0000-001031f3a900}\nProcessId: 4132\nImage: C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\\winlogbeat.exe\nFileVersion: ?\nDescription: ?\nProduct: ?\nCompany: ?\nOriginalFileName: ?\nCommandLine: \"C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\\winlogbeat.exe\" -c \"C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\\winlogbeat.yml\" -path.home \"C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\" -path.data \"C:\\ProgramData\\winlogbeat\" -path.logs \"C:\\ProgramData\\winlogbeat\\logs\"\nCurrentDirectory: C:\\WINDOWS\\system32\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {22052e76-11b2-5d2a-0000-0020e7030000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA256=16EECCE05D1A4B25CDA0442C743DCD912A1CB3C51BAB8A5E060A423C131E9ECB,IMPHASH=6DA7C12D70F874E2ABB391A456EB1EF0\nParentProcessGuid: {22052e76-11b2-5d2a-0000-00102cae0000}\nParentProcessId: 632\nParentImage: C:\\Windows\\System32\\services.exe\nParentCommandLine: C:\\WINDOWS\\system32\\services.exe",
...
}

他のプロセッサをパイプラインへつなぐ

Winlogbeatの標準プロセッサをこのパイプラインへつなぐことももちろんできます。

例えば以下の例ではConvertプロセッサを利用してフィールド名を大文字の文字列値へ変更しています。

var console = require("console");
// processorモジュールをローディング
var processor = require("processor");

function process(evt) {
    var addHello = function(evt) {
        evt.Put("hello","hello, world");
    }

    var addGoodbye = function(evt) {
        evt.Put("goodbye","goodbye cruel world");
    }

    var pipeline = new processor.Chain()
        .Add(addHello)
        .Add(addGoodbye)
        .Convert({ // Convertプロセッサをつなげる
            fields: [
                {from: "hello", to: "HELLO"}, // フィールド名を大文字の値へ変更
                {from: "goodbye", to: "GOODBYE"}
            ],
            mode: "rename",
            ignore_missing: true,
            fail_on_error: false,
        })
        .Build()

    pipeline.Run(evt);
    return;
}

function test() {
    var evt = new Event({
        message: "Windows event log message...",
        winlog : {
            provider_name : "Microsoft-Windows-Sysmon"
        }
    });
    process(evt);
    if (evt.Get("HELLO") !== "hello, world") {
        throw "expected goodbye !== hello, world";
    }
    if (evt.Get("GOODBYE") !== "goodbye cruel world") {
        throw "expected goodbye !== goodbye cruel world";
    }
}
 {
   "_index" : "winlogbeat-7.2.0-2019.07.14",
   "_type" : "doc",
   "_id" : "cFQ472sBgpTg8iKC8nZ3",
   "_score" : 0.0,
   "_source" : {
      "@timestamp" : "2019-07-14T06:42:59.844Z",
      "message" : "Process Create:\nRuleName: \nUtcTime: 2019-07-14 06:42:59.844\nProcessGuid: {22052e76-cef3-5d2a-0000-001054bbb200}\nProcessId: 2156\nImage: C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\\winlogbeat.exe\nFileVersion: ?\nDescription: ?\nProduct: ?\nCompany: ?\nOriginalFileName: ?\nCommandLine: \"C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\\winlogbeat.exe\" test config\nCurrentDirectory: C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\\\nUser: xxxxxxxxx\\xxx\nLogonGuid: {22052e76-11ef-5d2a-0000-002070890a00}\nLogonId: 0xA8970\nTerminalSessionId: 1\nIntegrityLevel: High\nHashes: SHA256=16EECCE05D1A4B25CDA0442C743DCD912A1CB3C51BAB8A5E060A423C131E9ECB,IMPHASH=6DA7C12D70F874E2ABB391A456EB1EF0\nParentProcessGuid: {22052e76-670e-5d2a-0000-001062cf5900}\nParentProcessId: 5152\nParentImage: C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\nParentCommandLine: \"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" ",
...
      "HELLO" : "hello, world",
      "GOODBYE" : "goodbye cruel world",
...
    }
 },

ちなみに標準のSysmonモジュールでもこのConvertプロセッサをパイプラインにつなげてWindowsイベントログのフィールド名をElastic Common Schema (ECS)へ変更するのに利用しています。

なお利用できる標準プロセッサの最新情報やラベル名はこちらconstructors定義を見るとわかります(次期Winlogbeat 7.3.0ではBase64デコードプロセッサがサポートされそう??? UTF-8だけでなくUTF-16leの場合などもサポートされると嬉しいですね・・・笑)。

その他の標準モジュール

consoleモジュールとprocessorモジュールについてご紹介しましたが、他にもイベントログ解析に便利なモジュールが提供されています。

pathモジュール

pathモジュールはWindowsファイルパスをパーシングしてくれるモジュールです。

メソッド                                   説明
basename ファイルパスからベース名(例:Exeファイル名)を取り出す
dirname ファイルパスからフォルダ名を取り出す
extname ファイル拡張子を取り出す
isAbsolute 絶対パスかどうかを判定
normalize パスを正規化。例: 「C:\Windows\system32\..\system32\system32.dll」を「C:\Windows\system32\system32.dll」へ変換

winlogbeatモジュール

メソッド                                   説明
splitCommandLine コマンドライン文字列をMicrosoft社から提供されているCommandLineToArgvW関数でパースおよび分割し配列化

いろいろな値型でフィールドへ追加してみる

EventオブジェクトPut()メソッドでいろいろな形式で値を追加してみました。

winlogbeat-helloworld.js
var console = require("console");
var processor = require("processor");

function process(evt) {

  var putValueByMethod = function(evt) {

    var numberValue = 1;
    evt.Put("numberValue", numberValue);

    var stringValue = "hello, world";
    evt.Put("stringValue", stringValue);

    var boolValue = false
    evt.Put("boolValue", boolValue);

    var nullValue = null
    evt.Put("nullValue", nullValue);

    var undefinedValue = undefined
    evt.Put("undefinedValue", undefinedValue);

    var arrayValue = ["element1", "element2", "element3"];
    evt.Put("arrayValue", arrayValue);

    var objectValue = {
      key1: "value1",
      key21: "value2"
    };
    evt.Put("objectValue", objectValue);

    var objectArrayValue = [{
      KEY1: "VALUE1",
      KEY2: "VALUE2"
    }, {
      KEY3: "VALUE3",
      KEY4: "VALUE4"
    }]
    evt.Put("objectArrayValue", objectArrayValue);

    var objectObjectValue = {
      Key1: {
        Key2: "Value2",
        Key3: 1,
        Key4: true,
        Key5: [1, 2, 3, 4]
      }
    }
    evt.Put("objectObjectValue", objectObjectValue);
  }

  var putValueToFields = function(evt) {
    var objectObjectValueToFields = {
      Key1: {
        Key2: "ばりゅー1",
        Key3: 1,
        Key4: true,
        Key5: [1, 2, 3, 4]
      }
    }
    evt.fields.objectObjectValueToFields = objectObjectValueToFields;
  }

  var pipeline = new processor.Chain()
    .Add(putValueByMethod)
    .Add(putValueToFields)
    .Build()

  pipeline.Run(evt);
  return;
}

function test() {
  var evt = new Event({});
  process(evt);
  console.log("evt.fields: %j", evt.fields);
}

Elasticsearchへ格納された形式は以下です。特に問題なさそうですね!

{
  "_index" : "winlogbeat-7.2.0-2019.07.14",
  "_type" : "doc",
  "_id" : "HVSD72sBgpTg8iKCsXnz",
  "_score" : 0.0,
  "_source" : {
    "@timestamp" : "2019-07-14T08:05:09.682Z",
...
    "stringValue" : "hello, world",
    "arrayValue" : [
      "element1",
      "element2",
      "element3"
    ],
    "nullValue" : null,
    "undefinedValue" : null,
    "objectArrayValue" : [
      {
        "KEY1" : "VALUE1",
        "KEY2" : "VALUE2"
      },
      {
        "KEY4" : "VALUE4",
        "KEY3" : "VALUE3"
      }
    ],
    "objectObjectValueToFields" : {
      "Key1" : {
        "Key2" : "ばりゅー1",
        "Key3" : 1,
        "Key4" : true,
        "Key5" : [
          1,
          2,
          3,
          4
        ]
      }
    },
    "objectObjectValue" : {
      "Key1" : {
        "Key2" : "Value2",
        "Key3" : 1,
        "Key4" : true,
        "Key5" : [
          1,
          2,
          3,
          4
        ]
      }
    },
    "numberValue" : 1,
    "boolValue" : false,
    "objectValue" : {
      "key1" : "value1",
      "key21" : "value2"
    },
    "message" : "Process Create:\nRuleName: \nUtcTime: 2019-07-14 08:05:09.682\nProcessGuid: {22052e76-e235-5d2a-0000-00109799d600}\nProcessId: 5372\nImage: C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\\winlogbeat.exe\nFileVersion: ?\nDescription: ?\nProduct: ?\nCompany: ?\nOriginalFileName: ?\nCommandLine: \"C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\\winlogbeat.exe\" -c \"C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\\winlogbeat.yml\" -path.home \"C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\" -path.data \"C:\\ProgramData\\winlogbeat\" -path.logs \"C:\\ProgramData\\winlogbeat\\logs\"\nCurrentDirectory: C:\\WINDOWS\\system32\\\nUser: NT AUTHORITY\\SYSTEM\nLogonGuid: {22052e76-11b2-5d2a-0000-0020e7030000}\nLogonId: 0x3E7\nTerminalSessionId: 0\nIntegrityLevel: System\nHashes: SHA256=16EECCE05D1A4B25CDA0442C743DCD912A1CB3C51BAB8A5E060A423C131E9ECB,IMPHASH=6DA7C12D70F874E2ABB391A456EB1EF0\nParentProcessGuid: {22052e76-11b2-5d2a-0000-00102cae0000}\nParentProcessId: 632\nParentImage: C:\\Windows\\System32\\services.exe\nParentCommandLine: C:\\WINDOWS\\system32\\services.exe",
...
  }
},

サンプルモジュールを書いてみる

HelloWorldよりも少し実用的?なサンプルモジュールを書いてみます(笑)

PowerShellのBase64エンコードされたコマンドのデコードモジュール

PowerShellにはEncodedCommandオプション(-EncodedCommandまたは-enc)を利用してコマンド列をBase64文字列としてパッキングしてくれる機能があります。しかしこの形式でPowerShellのコマンドラインがSysmonログに含まれると分析しづらい時があります。そこでPowerShell.exeのオプションに-encオプションが指定された場合、そのオプション値のBase64文字列をデコードしてからElasticsearchへ転送するモジュールを作成してみます。

Base64エンコードされたコマンド例

エンコード前
powershell.exe -executionpolicy bypass Resolve-DnsName -Name www.youtube.com
エンコード後
powershell.exe -executionpolicy bypass -enc "UgBlAHMAbwBsAHYAZQAtAEQAbgBzAE4AYQBtAGUAIAAtAE4AYQBtAGUAIAB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALgBjAG8AbQA="

複数のコマンドや引数からなる「Resolve-DnsName -Name www.youtube.com」の部分がBase64エンコードされて「UgBlAHMAbwBsAHYAZQAtAEQAbgBzAE4AYQBtAGUAIAAtAE4AYQBtAGUAIAB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALgBjAG8AbQA=」と一かたまりにパッキングされています。

Base64デコードライブラリ

Base64デコードプロセッサは(Winlogbeatの開発リポジトリをのぞくと近い将来もしかしたら標準提供されるかもしれませんが)7.2には含まれていないため、Base64デコードライブラリとしてjs-base64を利用します。ただしES6のimport文が使えないのでモジュールソースコードへそのまま組み込みます(泣)。

サンプルソース

サンプルソースは以下の通りです。js-base64の部分は省略してありますが、HelloWorldモジュールを含めてこちらへあげておきます。

winlogbeat-psdecode.js
...
function process(evt) {
    // prosessorモジュールをローディング
    var processor = require("processor");
    // winlogbeatモジュールをローディング
    var winlogbeat = require("winlogbeat");

    // パイプライン処理1:-encオプション値があればBase64デコード
    var decodePsCode = function(evt) {
      var processName = evt.Get("process.name");
      var args = evt.Get("process.args");
      if ( !processName || processName.length < 1 || 
           !args || args.length < 3 || 
           processName.toLowerCase() !== "powershell.exe" ) {
          return;
      }
      for (var i = 0; i < args.length; i++) {  
          if (args[i].toLowerCase().indexOf("-enc") != -1 && i < (args.length - 1)) {
              var decodedArg = Base64.decode(args[i+1]);
              if( decodedArg && decodedArg.length > 0 ){
                evt.Put("winlog.decodedPsCode",decodedArg.replace(/\u0000/gi,'')); //ズルしてる・・・
                break;
              }
          }
      }
    }

    // パイプライン処理2:Base64デコードされたコマンドをパースして配列へ格納
    var splitPsCode = function(evt) {
        var psArgs = evt.Get("winlog.decodedPsCode");
        if (!psArgs) {
            return;
        }
        evt.Put("winlog.decodedPsCodeArgs", winlogbeat.splitCommandLine(psArgs));
    }

    // パイプラインを作成
    var pipeline = new processor.Chain()
        .Add(decodePsCode) // パイプライン処理1を登録
        .Add(splitPsCode)  // パイプライン処理2を登録
        .Build()

    // パイプライン処理を実行
    pipeline.Run(evt);
    return;
}

// テスト関数
function test() {
    var console = require("console");

    var evt = new Event({
      process : {
        name : "powershell.exe",
        args : [
          "powershell.exe",
          "-executionpolicy",
          "bypass",
          "-enc",
          "UgBlAHMAbwBsAHYAZQAtAEQAbgBzAE4AYQBtAGUAIAAtAE4AYQBtAGUAIAB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALgBjAG8AbQA="
        ]
      },
      winlog: {
        event_id: 1
      }
    });
    console.debug("evt: %j",evt.fields);

    process(evt);
    console.debug("evt: %s",evt.Get("winlog.decodedPsCode"));
    console.debug("evt: %j",evt.Get("winlog.decodedPsCodeArgs"));

    if (evt.Get("winlog.decodedPsCode") !== "Resolve-DnsName -Name www.youtube.com") {
        throw "expected winlog.decodedPsCode === Resolve-DnsName -Name www.youtube.com";
    }

    console.debug("test() Done.");
}

自作モジュールを設定ファイルへ追加

Winlogbeatの設定ファイル(例: C:\Program Files\winlogbeat-7.2.0-windows-x86_64\winlogbeat.yml)へモジュール(winlogbeat-helloworld.js)を追加します。今回はSysmonイベントに対してフィルタ・加工処理を行うためwinlogbeat.event_logsの「name: Microsoft-Windows-Sysmon/Operational」フィールド配下へscriptプロセッサとして追加します。

winlogbeat.yml
#======================= Winlogbeat specific options ===========================
...
winlogbeat.event_logs:
  - name: Application
    ignore_older: 72h

  - name: System
...
  - name: Microsoft-Windows-Sysmon/Operational
    processors:
      - script:
          lang: javascript
          id: sysmon
          file: ${path.home}/module/sysmon/config/winlogbeat-sysmon.js
      - script:
          lang: javascript
          id: psdecode
          file: ${path.home}/module/psdecode/config/winlogbeat-psdecode.js
...

作成したモジュールと編集した設定内容をテストしておきましょう。

PS C:\Program Files\winlogbeat-7.2.0-windows-x86_64> .\winlogbeat.exe test config
Config OK

実行結果

実行した結果は以下の通りです。winlog.decodedPsCodeフィールドにBase64デコードした結果、そしてwinlog.decodedPsCodeArgsフィールドにそれをパースした結果が格納されています。

PowerShellのBase64コマンドのデコード結果(抜粋)
...
{
  "_index" : "winlogbeat-7.2.0-2019.07.14",
  "_type" : "doc",
  "_id" : "eVTF72sBgpTg8iKCSXqf",
  "_score" : 0.0,
  "_source" : {
    "@timestamp" : "2019-07-14T09:16:49.667Z",
...
    "winlog" : {
...
      "decodedPsCode" : "Resolve-DnsName -Name www.youtube.com",
      "decodedPsCodeArgs" : [
        "Resolve-DnsName",
        "-Name",
        "www.youtube.com"
      ],
...
    },
...
    "process" : {
      "pid" : 4824,
      "executable" : "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
      "args" : [
        "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
        "-executionpolicy",
        "bypass",
        "-enc",
        "UgBlAHMAbwBsAHYAZQAtAEQAbgBzAE4AYQBtAGUAIAAtAE4AYQBtAGUAIAB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALgBjAG8AbQA="
      ],
...
},
PowerShellのBase64コマンドのデコード結果(イベントログ全体)
...
{
  "_index" : "winlogbeat-7.2.0-2019.07.14",
  "_type" : "doc",
  "_id" : "eVTF72sBgpTg8iKCSXqf",
  "_score" : 0.0,
  "_source" : {
    "@timestamp" : "2019-07-14T09:16:49.667Z",
    "user" : {
      "domain" : "xxxxx",
      "name" : "xxxxx"
    },
    "host" : {
      "os" : {
        "family" : "windows",
        "name" : "Windows 10 Pro",
        "kernel" : "10.0.17763.615 (WinBuild.160101.0800)",
        "build" : "17763.615",
        "platform" : "windows",
        "version" : "10.0"
      },
      "id" : "22052e76-721a-4007-86f6-6346e89d0c86",
      "hostname" : "xxxxx",
      "architecture" : "x86_64",
      "name" : "xxxxx"
    },
    "agent" : {
      "ephemeral_id" : "d34dbafc-8002-4bcc-8f51-8e1911fcd0f0",
      "hostname" : "xxxxx",
      "id" : "d4bdf3a8-1d7e-4c36-a259-2a2451b56656",
      "version" : "7.2.0",
      "type" : "winlogbeat"
    },
    "winlog" : {
      "process" : {
        "thread" : {
          "id" : 3908
        },
        "pid" : 2720
      },
      "record_id" : 1744334,
      "channel" : "Microsoft-Windows-Sysmon/Operational",
      "event_data" : {
        "FileVersion" : "10.0.17763.1 (WinBuild.160101.0800)",
        "LogonId" : "0xa8970",
        "TerminalSessionId" : "1",
        "Description" : "Windows PowerShell",
        "IntegrityLevel" : "High",
        "Product" : "Microsoft® Windows® Operating System",
        "OriginalFileName" : "PowerShell.EXE",
        "Company" : "Microsoft Corporation",
        "LogonGuid" : "{22052e76-11ef-5d2a-0000-002070890a00}"
      },
      "provider_name" : "Microsoft-Windows-Sysmon",
      "task" : "Process Create (rule: ProcessCreate)",
      "api" : "wineventlog",
      "decodedPsCode" : "Resolve-DnsName -Name www.youtube.com",
      "decodedPsCodeArgs" : [
        "Resolve-DnsName",
        "-Name",
        "www.youtube.com"
      ],
      "provider_guid" : "{5770385f-c22a-43e0-bf4c-06f5698ffbd9}",
      "opcode" : "情報",
      "computer_name" : "xxxxx",
      "version" : 5,
      "event_id" : 1,
      "user" : {
        "identifier" : "S-1-5-18",
        "name" : "SYSTEM",
        "domain" : "NT AUTHORITY",
        "type" : "User"
      }
    },
    "process" : {
      "entity_id" : "{22052e76-f301-5d2a-0000-00102a5ce600}",
      "pid" : 4824,
      "executable" : "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
      "args" : [
        "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
        "-executionpolicy",
        "bypass",
        "-enc",
        "UgBlAHMAbwBsAHYAZQAtAEQAbgBzAE4AYQBtAGUAIAAtAE4AYQBtAGUAIAB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALgBjAG8AbQA="
      ],
      "working_directory" : "C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\\",
      "parent" : {
        "entity_id" : "{22052e76-670e-5d2a-0000-001062cf5900}",
        "pid" : 5152,
        "executable" : "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
        "args" : [
          "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
        ],
        "name" : "powershell.exe"
      },
      "name" : "powershell.exe"
    },
    "event" : {
      "created" : "2019-07-14T09:16:51.554Z",
      "kind" : "event",
      "code" : 1,
      "action" : "Process Create (rule: ProcessCreate)"
    },
    "hash" : {
      "sha256" : "de96a6e69944335375dc1ac238336066889d9ffc7d73628ef4fe1b1b160ab32c",
      "imphash" : "741776aaccfc5b71ff59832dcdcace0f"
    },
    "ecs" : {
      "version" : "1.0.0"
    },
    "log" : {
      "level" : "情報"
    },
    "message" : "Process Create:\nRuleName: \nUtcTime: 2019-07-14 09:16:49.667\nProcessGuid: {22052e76-f301-5d2a-0000-00102a5ce600}\nProcessId: 4824\nImage: C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\nFileVersion: 10.0.17763.1 (WinBuild.160101.0800)\nDescription: Windows PowerShell\nProduct: Microsoft® Windows® Operating System\nCompany: Microsoft Corporation\nOriginalFileName: PowerShell.EXE\nCommandLine: \"C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -executionpolicy bypass -enc UgBlAHMAbwBsAHYAZQAtAEQAbgBzAE4AYQBtAGUAIAAtAE4AYQBtAGUAIAB3AHcAdwAuAHkAbwB1AHQAdQBiAGUALgBjAG8AbQA=\nCurrentDirectory: C:\\Program Files\\winlogbeat-7.2.0-windows-x86_64\\\nUser: xxxxx\\xxxxx\nLogonGuid: {22052e76-11ef-5d2a-0000-002070890a00}\nLogonId: 0xA8970\nTerminalSessionId: 1\nIntegrityLevel: High\nHashes: SHA256=DE96A6E69944335375DC1AC238336066889D9FFC7D73628EF4FE1B1B160AB32C,IMPHASH=741776AACCFC5B71FF59832DCDCACE0F\nParentProcessGuid: {22052e76-670e-5d2a-0000-001062cf5900}\nParentProcessId: 5152\nParentImage: C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\nParentCommandLine: \"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" "
  }
},
...

まとめ

Winlogbeat 7.2.0の新機能であるJavascriptプロセッサ機能を使って独自モジュールを書いてみました。様々な処理をパイプライン化するのも容易であり、応用範囲が広がりそうですね。何よりも設定をJavascriptの構文で柔軟にサクッと書けるので非常にお手軽で便利です。

※Javascriptプロセッサはまだ新しい機能ですので仕様変更も今後予想されます。詳細は公式リファレンスを参照してください。

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

【iOS対応】Vue.jsで背景固定のモーダルを作る

モーダルを作る際に面倒なのが、スクロールしたときの背景の固定。
別に背景が動いても直接問題が起きることはないのですが、やっぱり気にはなります。ユーザーにもこのサービス大丈夫かな...と余計な不安感を与えることにもなりますし。

ただ、この背景固定、一筋縄ではいかないのが面倒なところ。とくにiOSは他のデバイスと異なる挙動を示し、AndroidやWebではちゃんと固定されるのにiOSだけ上手くいかない...と悩んでいる人も多いと思います。

何か解決方法はないかと探していたところ、非常に便利なライブラリを見つけたので、それで対応することにしました。

Body Scroll Lockを使う

名前の通り、機能を有効にすることで背景のスクロールを無効にできるライブラリです。便利なのは、ページ全てをスクロール無効にするのではなく、指定したDOM内部だけはスクロールできるように設定できること。
モーダル内部はスクロールさせたい!なんてケースにも対応可能ということです。

githubでのスターも1400個以上ついていますし、ある程度安心して導入できるのもメリットですね。公式サイトはこちら↓

Body Scroll Lock

Body Scroll Lockの使い方

何はともあれ、まずはnpm/yarnでinstallしましょう。

npm install body-scroll-lock

次に、背景固定を行いたいページ(コンポーネント)でimportします。

import { disableBodyScroll, enableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock';

名前からしてそのままですが、背景を固定したい場合はdisableBodyScrollを、解除したい場合はenableBodyScrollを使えばOKです。

たとえば、モーダルコンポーネントに使うならこんな感じ。

ModalLayout.vue
<template>
  <div class="modal-layer">
    <div class="modal">
      <slot />
    </div>
  </div>
</template>

<script>
import {
  disableBodyScroll,
  enableBodyScroll,
  clearAllBodyScrollLocks
} from 'body-scroll-lock';

export default {
  mounted() {
    const modal = document.querySelector('.modal')
    disableBodyScroll(modal)
  },

  beforeDestory() {
    clearAllBodyScrollLocks()
  }
}
</script>

ModalLayoutは親コンポーネントからv-ifで呼ばれる想定のコンポーネントです。
v-ifがtrueになったときにmounted()が走ることと、falseになるとbeforeDestoryが走ることを利用して、モーダルが表示されている場合にのみ背景が固定されるようにしています。

まとめ

非常にシンプルで使いやすいので、背景固定に困っているケースで便利です。モーダル系ライブラリとも簡単に組み合わせられそうなのも良さそうですね。

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

ReactでHTMLを作成し、Clickした際にそれぞれの情報をモーダルに表示・非表示させる(Railsで)

はじめに

ReactでHTMLを作成するまでは案外すんなりと行ったのですが、クリックした際に「それぞれの情報を持たせてモーダルを表示する」ことに苦労したので忘備録としてまとめておきます。
最終的にはこのような感じになります。
たくさんの方法を試したので不必要なものもあるかもしれません。
その点は指摘していただけると幸いです。
下の画像をクリックした際にその情報を表示させています。
表示させる内容は個人で変更してください。
react modal.jpg
876747d1b80a09753cfdb7b53b817923.jpg

1.gemの導入とインストール

この2つのgemをGemfileに加え、bundle installしてください

Gemfile
gem 'react-rails'
gem 'webpacker'

その後、それぞれインストールしてください

Terminal
$ bundle install
$ rails webpacker:install  
$ rails webpacker:install:react 
$ rails generate react:install

するとapp以下にapp/javascript/componentsフォルダが作成されます。
私自身、app/assets以下にJavascriptフォルダがあるのに大丈夫なのかと思いましたが、問題ありません。

2. application.html.hamlにtagを追加

application.html.hamlに以下の記述を加えてください

application.html.haml
= javascript_pack_tag 'application'

この際にTerminalでyarnがどうこうというエラーが出るかもしれません。
その場合$ yarn install等、各自で調べて解決してください。

これらでRails上でReactを使う準備ができましたので、いよいよjsファイルに記述していきます。

3. Components以下にjsファイルの作成

Components以下にApp.jsGraduate.jsを作成します。
これらの名前は自分で決めてくださって結構です。
ではこちらのコードをコピーしてください

App.js
import React from 'react';
import Graduate from './Graduate';

const lessonList = [
  {
    name: '開成太郎(2017)',
    school: '一条高校',
    image: 'https://cdn.pixabay.com/photo/2018/01/03/12/33/graduation-3058263__480.jpg',
    introduction: '中学一年生の時は全然テストの点数を取れなかったけど、ここに通い始めて2ヶ月ほどで目に見えて点数が上がるようになりました',
  },
  {
    name: '開成花子(2016)',
    school: '奈良高校',
    image: 'https://image.shutterstock.com/image-photo/japanese-student-browse-job-information-260nw-1010242525.jpg',
    introduction: '学校の授業では物足りず、塾に通うことに決めました。塾では私に合わせた応用問題を別途用意してもらい、そのおかげで合格できました',
  },
  {
    name: '開成三郎(2019)',
    school: '郡山高校',
    image: 'https://image.shutterstock.com/image-photo/high-school-student-graduation-260nw-1242927238.jpg',
    introduction: '土曜日や日曜日も朝早くから開塾してくれたおかげでたくさんの勉強時間を確保することができました。',
  },
  {
    name: '開成四郎(2015)',
    school: '登美ケ丘高校',
    image: 'https://cdn.pixabay.com/photo/2017/02/05/00/08/graduation-2038864__480.jpg',
    introduction: '二年生まで部活一筋だったため、勉強の進捗度は他の人に比べて劣っていたけれど、その分個別に補習の時間を組んでくれたおかげで合格できました。',
  }
];

class App extends React.Component {
  render() {
    return (
        <div className="performance">
        {lessonList.map((lessonItem) => {
      return (
          <Graduate
            name={lessonItem.name}
            image={lessonItem.image}
            school={lessonItem.school}
            introduction={lessonItem.introduction}
            />
      );
    })}
      </div>
    );
  }
}

export default App;

それでは解説していきます。

import React from 'react';
import Graduate from './Graduate';

まずimportとは輸入という意味です。
その名の通り、一行目ではReactを二行目ではGraduateファイルを読み込んでいます。
このおかげでReactを使うことができ、またGraduateにパラメーター(props)を渡すことができます。

const lessonList = [
  {
    name: '開成太郎(2017)',
    school: '一条高校',
    image: 'https://cdn.pixabay.com/photo/2018/01/03/12/33/graduation-3058263__480.jpg',
    introduction: '中学一年生の時は全然テストの点数を取れなかったけど、ここに通い始めて2ヶ月ほどで目に見えて点数が上がるようになりました',
  },
  {
    name: '開成花子(2016)',
    school: '奈良高校',
    image: 'https://image.shutterstock.com/image-photo/japanese-student-browse-job-information-260nw-1010242525.jpg',
    introduction: '学校の授業では物足りず、塾に通うことに決めました。塾では私に合わせた応用問題を別途用意してもらい、そのおかげで合格できました',
  },
  {
    name: '開成三郎(2019)',
    school: '郡山高校',
    image: 'https://image.shutterstock.com/image-photo/high-school-student-graduation-260nw-1242927238.jpg',
    introduction: '土曜日や日曜日も朝早くから開塾してくれたおかげでたくさんの勉強時間を確保することができました。',
  },
  {
    name: '開成四郎(2015)',
    school: '登美ケ丘高校',
    image: 'https://cdn.pixabay.com/photo/2017/02/05/00/08/graduation-2038864__480.jpg',
    introduction: '二年生まで部活一筋だったため、勉強の進捗度は他の人に比べて劣っていたけれど、その分個別に補習の時間を組んでくれたおかげで合格できました。',
  }
];

この部分ではそれぞれのハッシュを配列に入れています。これをあとでmapメソッドで一つずづ表示させていきます。

class App extends React.Component {
  render() {
    return (
        <div className="performance">
        {lessonList.map((lessonItem) => {
      return (
          <Graduate
            name={lessonItem.name}
            image={lessonItem.image}
            school={lessonItem.school}
            introduction={lessonItem.introduction}
            />
      );
    })}
      </div>
    );
  }
}

この部分でComponentを作成しています。(extendsは広げるという意味)
ConmonentはJavascriptの関数のようなものです。
その中のreturnでHTMLを返し、表示させています。またこのような記法をJSXと言います。
また、JSXには約束事があり、複数の要素を返すことができません。なので図の場合、performanceクラスを親要素としその中に色々と要素を追加しています。

{lessonList.map((lessonItem)ではlessonListの数だけ繰り返し、returnを読み込んでいます。
return内でとすることでGraduateコンポーネントを呼び出しています。これが可能なのはimport Graduate from './Graduate';のおかげです。
sosite
呼び出す際にはname,school,image,introductionのパラメーターを渡しています。(props)
このパラメーターをGraduate.js側で使うわけです。ちなみにJSX内でjavascriptの記法を用いる際は中括弧が必要なため、中括弧内に書いてあります。

export default App;

App.jsの最後の行ですが、exportとは輸出という意味です。
これのおかげでHTMLファイルでApp.jsを呼び出すことができ、returnとして要素をHTMLに追加することができます。App.jsがGraduateを二行目でimportできているのもGraduate.jsで最後にexport default Graduate;としているからです。

Graduate.js
import React from 'react';

class Graduate extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isModalOpen: false};
  }

  handleClickLesson() {
    this.setState({isModalOpen: true});
  }

  handleClickClose() {
    this.setState({isModalOpen: false});
  }
  render() {
    let modal;
    if (this.state.isModalOpen) {
      modal = (
        <div className='modal-area'>
        <div className='modal-inner'>
          <div className='modal-header'></div>
          <div className='modal-introduction'>
            <h2>{this.props.name}</h2>
            <p>{this.props.introduction}</p>
          </div>
          <button
            className='modal-close-btn'
            onClick={() => this.handleClickClose()}
          >
            とじる
          </button>
        </div>
      </div>
      )
    };

    return (
      <div className="graduate">
          <img className="graduate__image" src={this.props.image}
               onClick={() => {this.handleClickLesson()}}
          />
          <div className="graduate__school">{this.props.school} 合格!</div>
          <div className="graduate__student">{this.props.name}</div>
          {modal}
      </div>
    );
  }
}

export default Graduate;

この部分で、実際に表示されるHTMLを作成しています。App.jsから渡されたパラメータ(props)を使って書いていきます。

constructor(props) {
    super(props);
    this.state = {isModalOpen: false};
  }

新しくstate(状態)というものが出てきましたが、propsとstateは少し異なり、stateはそのComponent内で保持されるものであって、propsみたいにComponentからComponentに渡すことはできません。詳しくはこちら
この部分でモーダルの表示・非表示を管理しています。

  handleClickLesson() {
    this.setState({isModalOpen: true});
  }

  handleClickClose() {
    this.setState({isModalOpen: false});
  }

ここで先ほどのstateを関数(handleClickLesson or handleClickClose)が呼ばれた時に更新しています。この時に注意して欲しいのが、更新する際はsetStateとしないといけないことです。

let modal;
    if (this.state.isModalOpen) {
      modal = (
        <div className='modal-area'>
        <div className='modal-inner'>
          <div className='modal-header'></div>
          <div className='modal-introduction'>
            <h2>{this.props.name}</h2>
            <p>{this.props.introduction}</p>
          </div>
          <button
            className='modal-close-btn'
            onClick={() => this.handleClickClose()}
          >
            とじる
          </button>
        </div>
      </div>
      )
    };

ここでisModalOpenがtrueの場合のみ、変数modalに値を代入し、表示させます。
そしてモーダルの中にonClick={() => this.handleClickClose()}があると思いますが、このボタンを押すことでisModalOpenがfalseになり再び非表示になります。
またApp.jsからもらってきたパラメータ(props)を{this.props.name}とすることで代入することができます。
またReactではclassをclassNameと記載します。

return (
      <div className="graduate">
          <img className="graduate__image" src={this.props.image}
               onClick={() => {this.handleClickLesson()}}
          />
          <div className="graduate__school">{this.props.school} 合格!</div>
          <div className="graduate__student">{this.props.name}</div>
          {modal}
      </div>
    );

最後ですね。
この部分で常時表示させるHTMLをApp.jsからもらったpropsを使って作成しています。
そして{modal}の部分で先ほどの変数を代入しているわけですね。imgクラスにonClickが設定されているため、画像をクリックするとisModalOpenがtrueになり、値が代入されたmodalが表示されるわけです。
では最後にHTML側でApp.jsを呼んであげましよう。

hamlの場合は

index.haml.haml
= react_component("App")

htmlの場合は

index.html.erb
<%= react_component("App") %>

これで完成です。
CSSだけ記載しておきます。
お好みで変更してください。

stylesheet.css
.performance {
    .graduate {
      padding: 30px 0;
      display: inline-block;
      width: 25%;
      text-align: center;
      &__student {
        font-size: 15px;
        padding-top: 10px;
        text-align: right;
        padding-right: 20px;
      }
      &__school {
        font-size: 20px;
        padding-top: 10px;
      }
      &__image {
        cursor: pointer;
        height: 160px;
        width: 160px;
        border-radius: 50%;
      }
      .modal-area {
        z-index: 2;
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
        background-color: rgba(0, 0, 0, 0.6);
        .modal-inner {
          position: absolute;
          top: 8%;
          right: 0;
          left: 0;
          width: 480px;
          padding-bottom: 60px;
          margin: auto;
          background-color: rgb(255, 255, 255);
          .modal-header {
            margin-bottom: 60px;
          }
          .modal-introduction p {
            color: #5876a3;
            width: 384px;
            line-height: 32px;
            text-align: left;
            margin: 36px auto 40px;
          }
          .modal-close-btn {
            font-size: 13px;
            color: #8491a5;
            width: 200px;
            padding: 16px 0;
            border: 0;
            background-color: #f0f4f9;
            cursor: pointer;
          }
          .modal-close-btn:hover {
            color: #8491a5;
            background-color: #ccd9ea;
            transition: .3s ease-in-out;
          }
        }
      }
    }
  }

番外編

Click時にモーダルが表示されるが非表示にならない場合

モーダル実装時にモーダルが閉じないというバグが起こりました。
原因を調べてみると閉じるボタンを押した際に一回閉じてから再度開いています。
これがその時のコードです。

<div className="graduate"
     onClick={() => {this.handleClickLesson()}}>
          <img className="graduate__image" src={this.props.image}/>
          <div className="graduate__school">{this.props.school} 合格!</div>
          <div className="graduate__student">{this.props.name}</div>
          {modal}
      </div>

何が問題かというと一番の親要素であるgraduateにopenmodalを設定しているせいで、その子要素であるモーダル内のclossmodalを押した際に同時に親要素のopenmodalも呼ばれてしまうからです。
なのでそれぞれのClick機能は親要素、子要素の関係に注意しましょう。

参考資料

React公式HP
Progate
https://qiita.com/k-penguin-sato/items/e3cc04f787cf3254cfae
https://qiita.com/kyrieleison/items/78b3295ff3f37969ab50

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

Reactアプリの枠組みの雛形を作ってみる

タイトルが「Reactアプリの枠組みの雛形を作ってみる」とありますが、本記事の目的は、以下のような複数のReactライブラリを一緒に使っても、それぞれのライブラリの機能がそこなわれることなく機能することを確認することです。

個人的には、antdが豊富なUIコンポネントを提供してくれているので、今後Material-UIに替えて使っていければなーと思っています。(中国発ですが、トランプ制裁とか関係ないですよね)immutable jsがリストから落ちていますが、今後の課題です。

  • @loadable/component
  • redux
  • react-redux
  • redux-logger
  • redux-thunk
  • react-router-dom
  • connected-react-router
  • antd
  • styled-components

@loadable/component
Reactのコード分割を行うライブラリ。バンドルの肥大化対応。React code splitting made easy.

redux
A predictable state container for JavaScript apps.

react-redux
Official React bindings for Redux
React Reduxの概要を理解する

redux-logger
Logger for Redux。 reduxのstateログを出力するMiddleware。

redux-thunk
Thunk middleware for Redux.
actionとして非同期関数を指定することが可能になります。

react-router-dom
react-router

connected-react-router
A Redux binding for React Router v4 and v5
history methods (push, replace, go, goBack, goForward)のdispatch が、 redux-thunk と redux-sagaの両方に互換性を持ちます。
react-router v4 と Redux

antd
Ant Design of React
ReactのUIライブラリ。豊富なUIコンポネントを比較的簡単に使える。
React UI library の antd について (1) - Button

styled-components
Visual primitives for the component age
JSでstyleを記述するCSS in JSのライブラリ。

【補足】
実は本検証の過程で、redux-formreact-router-domと一緒に使うと機能しないことが確認できました。reduxForm()connect()という2つのHOCで2重にラップした時に、propsが、最終的なコンポーネントにうまく伝わっていかない感じでした。結論としてはredux-formは使わずに、antdForm.create()を使ってvalidateを行うようにしました。私の検証ミスかもしれませんが、丸一日試行錯誤した結果です。

1.インストール

以下のコマンドで環境構築OKです。

yarn create react-app antd-test
cd antd-test
yarn add @loadable/component
yarn add redux react-redux redux-logger redux-thunk
yarn add react-router-dom connected-react-router
yarn add antd styled-components

一応package.jsonも掲載しておきます。

package.json
{
  "name": "antd-test",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@loadable/component": "^5.10.1",
    "antd": "^3.20.2",
    "connected-react-router": "^6.5.2",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-redux": "^7.1.0",
    "react-router-dom": "^5.0.1",
    "react-scripts": "3.0.1",
    "redux": "^4.0.4",
    "redux-logger": "^3.0.6",
    "redux-thunk": "^2.3.0",
    "styled-components": "^4.3.2"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

2.画面イメージ

本アプリのページは、ホーム画面とユーザ登録画面、ログイン画面の3つです。ページ遷移も含めて以下に紹介します。

■ページ構成:上からヘッダー、コンテンツ、フッター
image.png

■ユーザ登録:コンテンツ=ユーザ登録画面
image.png

ユーザ登録が成功すると、3秒後にログイン画面に自動遷移します。

■ログイン:コンテンツ=ログイン画面
image.png

ログインが成功すると、3秒後にホーム画面に自動遷移します。

■ホーム:コンテンツ=ホーム画面(初期画面)
image.png

3.ソースツリー

以下がソースファイルのツリーになります。
containersディレクトリが重要です。その中でもLoginとRegisterがメインです。Loadable.jsがimportポイントとなります。index.jsがreact-reduxのcontainerであり、Register.jsとLogin.jsがcomponentとなります。index.jsがReduxの処理を行い、Register.jsとLogin.jsはReduxについては何も知らないことになっています。

$ tree src
src
├── App.js
├── actions
│   └── actions.js
├── components
│   ├── Footer.js
│   ├── Header.js
│   └── Wrapper.js
├── containers
│   ├── Home
│   │   ├── Loadable.js
│   │   └── index.js
│   ├── Login
│   │   ├── Loadable.js
│   │   ├── Login.js
│   │   └── index.js
│   ├── NotFoundPage
│   │   ├── Loadable.js
│   │   └── index.js
│   ├── Register
│   │   ├── Loadable.js
│   │   ├── Register.js
│   │   ├── Register.org2
│   │   ├── _Register.new
│   │   ├── _Register.org
│   │   └── index.js
│   └── input.js
├── createStore.js
├── form-style.css
├── index.js
└── reducers
    └── index.js

4.index.js

index.jsでReduxreact-reduxconnected-react-routerの初期設定を行います。

src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router';
import createBrowserHistory from 'history/createBrowserHistory';
import App from './App'
import createStore from './createStore'

// connected-react-router - action経由でルーティングが可能、push,replace..
// historyを強化
const history = createBrowserHistory();
const store = createStore(history);

const dest = document.getElementById('root')

let render = () => {
  ReactDOM.hydrate(
    <Provider store={store}>
      <ConnectedRouter history={history}>
        <App />
      </ConnectedRouter>
    </Provider>,
    dest
  )
}

render()

createStore.jsはreduxのオリジナルcreateStore.jsのラッパーです。ReducersとMiddlewareの設定を行います。

src/createStore.js
import { createStore as reduxCreateStore, combineReducers, applyMiddleware } from 'redux'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import { routerMiddleware, connectRouter } from 'connected-react-router'
import * as reducers from './reducers'

// connected-react-router - action経由でルーティングが可能、push,replace..
// createStoreの再定義 - historyを引数で受け、connected-react-routerの利用を抽象化
export default function createStore(history) {
  return reduxCreateStore( // オリジナル createStore の別名
    combineReducers({
      ...reducers,
      router: connectRouter(history)
    }),
    applyMiddleware(
      logger,
      thunk,
      routerMiddleware(history)
    )
  );
}

reducersの定義です。
Redux storeは次の2つのstateを保持します。

  • state.users[]: userオブジェクトの配列
  • state.logined: ログインしているuserオブジェクト
src/reducers/index.js
export const users = (state = [], action) => {
  switch (action.type) {
    case 'ADD_USER': // *** userを追加
      return [
        ...state,    // *** 分割代入、stateに追加
        {
          email: action.user.email,
          name: action.user.name,
          password: action.user.password
        }
      ]
    default:
      return state
  }
}

export const logined = (state = {}, action) => {
  switch (action.type) {
    case 'ADD_LOGINED_USER': // *** userを追加
      return (
        {
          email: action.user.email,
          name: action.user.name,
          password: action.user.password
        })
    default:
      return state
  }
}

redux-thunkを使っているので、actionは非同期関数で定義しています。ただし非同期はsetTimeout()で模擬したものです
connected-react-routerを使っており、提供されるpush()redux-thunkと互換性があります。push()を使って、ユーザ登録成功後にログイン画面へ、ログイン成功後にホーム画面へ、自動リダイレクトしています。

src/actions/actions.js
import { push } from 'connected-react-router';

const addUser = user => ({
  type: 'ADD_USER',
  user: user
})

const addLoginedUser = user => ({
  type: 'ADD_LOGINED_USER',
  user: user
})

export const asyncAddUser = values => {
  return (dispatch, getState) => {
    setTimeout( () => {
        dispatch(addUser(values))
        dispatch(push("/login"))
    }, 3000 );
  }
}

export const asyncLogin = values => {
  return (dispatch, getState) => {
    setTimeout( () => {
      const state = getState()
      for (const user of state.users) {
        if( user.email === values.email && user.password === values.password ) {
          console.log("login succeful!!!",values)
          dispatch(addLoginedUser(user))
          dispatch(push("/"))
          return
        }
      }
      console.log("login failed!!!",values)
    }, 3000 );
  }
}

5.App.js

App.jsはこのアプリのメインになります。サイト全体のページ構成を定義します。

  • react-routerでRouteを定義します。
  • 画面の枠組みを定義して、全体のstyleを実装します

styleはstyled-componentsを利用しているほか、antd.cssとform-style.cssを読み込んでいます。

App.js
import React, { Component } from 'react'
import styled from 'styled-components'
import { Switch, Route } from 'react-router-dom'

import Home from './containers/Home/Loadable'
import LoginPage from './containers/Login/Loadable'
import RegisterPage from './containers/Register/Loadable'
import NotFoundPage from './containers/NotFoundPage/Loadable'
import Header from './components/Header'
import Footer from './components/Footer'

import 'antd/dist/antd.css';
import './form-style.css';

const AppWrapper = styled.div`
  max-width: calc(768px + 16px * 2);
  margin: 0 auto;
  display: flex;
  min-height: 100%;
  padding: 0 16px;
  flex-direction: column;
  background: papayawhip;
  .btn {
    line-height: 0;
  }
`;

class App extends Component {
  render() {
    return (
      <AppWrapper>
        <Header />
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/login" component={LoginPage} />
          <Route exact path="/register" component={RegisterPage} />
          <Route path="" component={NotFoundPage} />
        </Switch>
        <Footer />
      </AppWrapper>
    );
  }
}

export default App;

form-style.cssはForm画面のstyle記述しています。ユーザ登録画面とログイン画面だけで読み込めばいいのですが、面倒なのでApp.js一か所で読み込んでいます。

src/form-style.css
.form-register-containers {
  width: 100%;
  margin: auto;
  max-width: 400px;
  padding: 50px 10px;
}

.form-register-containers .center {
  text-align: center;
}

.form-register-containers .ant-form-item-label {
  line-height: 1;
}

.form-register-containers .ant-form-item-with-help {
  margin-bottom: 0;
}

6.ユーザ登録

loadableを通して、containerコンポーネントのindex.jsを読み込みます。

src/containers/Register/Loadable.js
import loadable from '@loadable/component';
export default loadable(() => import('./index'));

index.jsはcontainerコンポーネントとして、react-reduxの設定を行い、connect()でcomponentをラップします。加えてantdForm.create()でラップしています。
2つのHOCを使っているのですが、順番に注意してください。

src/containers/Register/index.js
import { connect } from 'react-redux'
import { asyncAddUser } from '../../actions/actions'
import Register from './Register';

import { Form } from 'antd';

function mapStateToProps(state) {
  return state
}

function mapDispatchToProps(dispatch) {
  return {
    onSubmit : values => {
        dispatch(asyncAddUser(values))
    }
  }
}

let myRegister = connect(mapStateToProps, mapDispatchToProps)(Register)
myRegister = Form.create({ name: 'register_form' })(myRegister);

export default myRegister

antdのFormを使って、ユーザ登録のform画面を定義しています。Form.create()でラッピングしているので、様々なvalidate関数を利用できます。

src/
import React from 'react';
import { Form, Input, Icon, Button } from 'antd';

class Register extends React.Component {

  state = {
    confirmDirty: false,
    autoCompleteResult: [],
  };

  componentDidMount() {
    // To disabled submit button at the beginning.
    this.props.form.validateFields();
  }

  handleSubmit = e => {
    console.log(this.props)
    e.preventDefault();
    this.props.form.validateFields((err, values) => {
      if (!err) {
        console.log('Submit OK: ', values);
        this.props.onSubmit( values )
      } else {
        console.log('Submit NG: ', values);
      }
    })
  }

  handleConfirmBlur = e => {
    const { value } = e.target;
    this.setState({ confirmDirty: this.state.confirmDirty || !!value });
  };

  compareToFirstPassword = (rule, value, callback) => {
    const { form } = this.props;
    if (value && value !== form.getFieldValue('password')) {
      callback('Two passwords that you enter is inconsistent!');
    } else {
      callback();
    }
  };

  validateToNextPassword = (rule, value, callback) => {
    const { form } = this.props;
    if (value && this.state.confirmDirty) {
      form.validateFields(['confirm'], { force: true });
    }
    callback();
  };

  render() {
    const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form;

    // Only show error after a field is touched.
    const emailError = isFieldTouched('email') && getFieldError('email');
    const nameError = isFieldTouched('name') && getFieldError('name');
    const passwordError = isFieldTouched('password') && getFieldError('password');
    const confirmError = isFieldTouched('confirm') && getFieldError('confirm');
    const buttonDisable = getFieldError('email') || getFieldError('name') || getFieldError('password') || getFieldError('confirm')

    return(
      <Form onSubmit={this.handleSubmit} className="form-register-containers">
        <h1 className="center">
          ユーザ登録
        </h1>

        <Form.Item label="メールアドレス" validateStatus={emailError ? 'error' : ''} help={emailError || ''}>
          {getFieldDecorator('email', {
            rules: [ {type: 'email', message: 'The input is not valid E-mail!',},
                     { required: true, message: 'Please input your email!' }],
            })(
              <Input
                prefix={<Icon type="mail" style={{ color: 'rgba(0,0,0,.25)' }} />}
                placeholder="email"
              />,
          )}
        </Form.Item>

        <Form.Item label="パスワード" validateStatus={passwordError ? 'error' : ''} help={passwordError || ''}>
          {getFieldDecorator('password', {
            rules: [ { required: true, message: 'Please input your password!' },
                     { validator: this.validateToNextPassword,} ],
            })(
              <Input
                prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
                placeholder="password"
              />,
          )}
        </Form.Item>

        <Form.Item label="確認パスワード" validateStatus={confirmError ? 'error' : ''} help={confirmError || ''}>
          {getFieldDecorator('confirm', {
            rules: [ { required: true, message: 'Please input your confirmPassword!' },
                     { validator: this.compareToFirstPassword,} ],
            })(
              <Input
                prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
                placeholder="confirmPassword"
                onBlur={this.handleConfirmBlur}
              />,
          )}
        </Form.Item>

        <Form.Item label="名前" validateStatus={nameError ? 'error' : ''} help={nameError || ''}>
          {getFieldDecorator('name', {
            rules: [{ required: true, message: 'Please input your name!' }],
            })(
              <Input
                prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
                placeholder="name"
              />,
          )}
        </Form.Item>

        <Form.Item className="center">
          <Button
            type="primary"
            htmlType="submit"
            className="btn-submit"
            disabled = {buttonDisable}
          >
            ユーザ登録
          </Button>
        </Form.Item>
      </Form>
    )
  }
}

export default Register

7.ログイン

ログイン画面はユーザ登録画面と構成が全く同じです。説明も重複になるので、省略します。

src/containers/Login/Loadable.js
import loadable from '@loadable/component';
export default loadable(() => import('./index'));
src/containers/Login/index.js
import { connect } from 'react-redux'
import { asyncLogin } from '../../actions/actions'
import Login from './Login';

import { Form } from 'antd';

function mapStateToProps(state) {
  return state
}


function mapDispatchToProps(dispatch) {
  return {
    onSubmit : values => {
        dispatch(asyncLogin(values))
    }
  }
}

let myLogin = connect(mapStateToProps, mapDispatchToProps)(Login)
myLogin = Form.create({ name: 'login_form' })(myLogin);

export default myLogin
src/containers/Login/Login.js
import React from 'react';
import { Form, Input, Icon, Button } from 'antd';

class Login extends React.Component {

  componentDidMount() {
    // To disabled submit button at the beginning.
    this.props.form.validateFields();
  }

  handleSubmit = e => {
    console.log(this.props)
    e.preventDefault();
    this.props.form.validateFields((err, values) => {
      if (!err) {
        console.log('Received values of form: ', values);
        this.props.onSubmit( values )
      }
    })
  }

  render() {
    const { getFieldDecorator, getFieldsError, getFieldError, isFieldTouched } = this.props.form;

    // Only show error after a field is touched.
    const emailError = isFieldTouched('email') && getFieldError('email');
    const passwordError = isFieldTouched('password') && getFieldError('password');
    const buttonDisable = getFieldError('email') || getFieldError('password')

    return(
      <Form onSubmit={this.handleSubmit} className="form-register-containers">
        <h1 className="center">
          ログイン
        </h1>
        <Form.Item label="メールアドレス" validateStatus={emailError ? 'error' : ''} help={emailError || ''}>
          {getFieldDecorator('email', {
            rules: [ {type: 'email', message: 'The input is not valid E-mail!',},
                     { required: true, message: 'Please input your email!' }],
            })(
              <Input
                prefix={<Icon type="mail" style={{ color: 'rgba(0,0,0,.25)' }} />}
                placeholder="email"
              />,
          )}
        </Form.Item>

        <Form.Item label="パスワード" validateStatus={passwordError ? 'error' : ''} help={passwordError || ''}>
          {getFieldDecorator('password', {
            rules: [{ required: true, message: 'Please input your password!' }],
            })(
              <Input
                prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
                placeholder="password"
              />,
          )}
        </Form.Item>


        <Form.Item className="center">
          <Button
            type="primary"
            htmlType="submit"
            className="btn-submit"
            disabled = {buttonDisable}
          >
            ログイン
          </Button>
        </Form.Item>
      </Form>
    )
  }
}

export default Login

8.ホーム

ホーム画面は特にありません。

src/containers/Home/Loadable.js
import loadable from "@loadable/component";
export default loadable(() => import("./index"));
src/
import React from 'react';
import { Route, Link } from 'react-router-dom';

const Home = () => (
  <div>
    <h1>私のホームページへようこそ !!!</h1>
    <ul>
      <li><Link to="/login">Login</Link></li>
      <li><Link to="/register">Register</Link></li>
    </ul>
  </div>
);

export default Home;

URLで指定されたページが見つからい場合は、以下のcomponentが表示されます。

src/containers/NotFoundPage/Loadable.js
import loadable from "@loadable/component";
export default loadable(() => import("./index"));
src/containers/NotFoundPage/index.js
import React from "react";

export default class NotFound extends React.PureComponent {
  render() {
    return <h1>This is the NotFoundPage Page!</h1>;
  }
}

9.ヘッダー/フッター

ヘッダーとフッターですが、特に説明は不要でしょう。

src/components/Header.js
import React from 'react';
import { Link } from "react-router-dom";
import { Breadcrumb } from 'antd';

function Header() {
  return (
    <Breadcrumb>
      <Breadcrumb.Item>
        <Link to="/">ホーム</Link>
      </Breadcrumb.Item>
      <Breadcrumb.Item>
        <Link to="/login">ログイン</Link>
      </Breadcrumb.Item>
      <Breadcrumb.Item>
        <Link to="/register">ユーザ登録</Link>
      </Breadcrumb.Item>
    </Breadcrumb>
  );
}

export default Header;
src/
import React from 'react';
import Wrapper from './Wrapper';

function Footer() {
  return (
    <Wrapper>
      <section>Footer footer Footer!!!</section>
    </Wrapper>
  );
}

export default Footer;
src/components/Wrapper.js
import styled from 'styled-components';

const Wrapper = styled.footer`
  display: flex;
  justify-content: space-between;
  padding: 3em 0;
  border-top: 1px solid #666;
`;

export default Wrapper;

今回は以上です。

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

Electronでメニューバーを消す方法(Windows)

frame: falseにすると消えるとか(フレームレスになる)
titleBarStyle: 'hidden'で消えるとか(IOSでしか働かない)
Windowsでの情報が少なかったので悩みましたが結果的に

mainWindow = new BrowserWindow({});
mainWindow.setMenu(null);

これだけでした。

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

プログラミングマスコットのドット絵を書いた

UT.png

プログラム言語のマスコット、ロゴ

あなたはPCにステッカー貼ってますか?
利用言語を愛していますか?
言語のOSSにプルリクエストを送るためなら睡眠時間を削れますか?

そんなあなたに送るプログラムマスコット`sです。

きっかけはUTme!で自作Tシャツを作りたいと思っていた時に、プログラマーが目にするマスコットアニマル`sという記事を見たこと。

あと、三連休なのに予定がないからだZE:star:(2019/07/14)

Gopher (Go)

マスコットといえばgopherくん
ドット絵と寸胴ボディの親和性がGood。

goper.png
"gopher" by Renée French CC-BY-3.0

Duke (java)

Go言語のライバル?でもあるjavaのDukeさん。
マザー2のザコキャラ感がすんごい。

java.png
"Duke" by Sun Microsystems BSD

rustacean (rust)

rustのrustaceanさん。
前者2人を圧倒的速度で凌駕する高速カニさん。(強い)

rust.png

Moby Dock (Docker)

DockerのMoby Dock親方。
みんなを支える縁の下の力持ち。
docker.png

みんなを載せてるのでこんなイメージ。
無題の図形描画.jpg

android robot (android)

その名の通りアンドロイドのロボットでandroid robotさん。
ドット絵にしても変わった感が無い・・・

android.png
"android robot" by google.com CC BY 3.0

jenkins logo (jenkins)

怒り顔など、何かと改変されがちなジェンキンスのロゴ。
あなた、名が無きおじさんだったのね・・・

jenkins.png
"jenkins logo" by jenkins.io CC BY-SA 3.0

Tux (linux)

linuxのTaxさん。
可愛く見えて実は二段腹のおじさんペンギン。

linux.png
"Tux" by Larry Ewing、Simon Budig、Anja Gerwinski

GitLab logo (GitLab)

昔は怖かったGitLabのロゴ。
狐じゃないよ!たぬきだよ!

gitlub.png
"GitLab logo" by gitlab.com CC BY-NC-SA 4.0

Python logo (Python)

もはやマスコットではなくなってきた、Pythonのロゴマーク。
python.png
"python logo" by Python Software Foundation PSF license

postgreSQL logo (postgreSQL)

一番難しかった。ポスグレのぞうさん。
postgres.png
"PostgreSQL" by postgresql.org PostgreSQL license

MySQL logo (MySQL)

ポスグレ書くならこっちもね!MySQLのイルカさん。
笑顔がかわいい。
mysql.png

Slack logo (Slack)

みんなの雑談部屋。slackのロゴ。
新デザインにだんだん慣れてきた。
slack.png
※著作件に関する記載が見つからなかったので、問題あればコメントなどで指摘してください。:bow_tone1:

HTML5 logo (HTML5)

HTML5のロゴ。
筆者は20代半ばなので、5とそれ以前の違いがわかっていない。
html.png
"HTML5" by World Wide Web Consortium CC BY 3.0

Javascript logo (JavaScript)

HTMLやったならこっちもね。
JavaScriptさん。
js.png
※こちらも著作権に関する記載が見当たらなかったです。

UBUNTU logo (UBUNTU)

Macが買えず使っていたUBUNTUさん。
WSL2に期待中。
ubuntu.png

ReactJS logo (ReactJS)

個人的な思い入れ(業務で利用)だけで書いたReactJS。
複雑な構造をドット絵で書くのは無理あるよね。。。
react.png
"ReactJS logo" by facebook.com CC BY 4.0

最後に

こんな感じのTシャツを着ている人がいたら声をかけてください。きっと私です。
キャプチャ.PNG

著作件について

この記事に記載している画像はすべてSlackスタンプ、アイコンに流用していただいてかまいません。
ただし、全てのマスコット、ロゴは原案者がいらっしゃいます。
必ず著作件の確認、表示の上ご利用ください。
(octcat-githubは著作件縛りきつくて書いたけど載せれなかった。)

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

【Firebase Auth】1つのアカウントにメールアドレスと電話番号を紐付ける

FirebaseのAuthenticationを利用したプロダクトの開発を行う機会があり、一つのアカウントにメールアドレスと電話番号を紐付ける必要があったのでその手順を残しておきます。
とても簡単に実装できました。

参考: https://firebase.google.com/docs/auth/web/phone-auth?hl=ja

以下のようになっていればOKです。
スクリーンショット 2019-07-13 16.34.45.png

今回の手順をgitで公開しています。こちらから確認できます。

スクリーンショット 2019-07-14 15.08.18.png

googleでログインさせた後に電話番号で認証させるといったことも簡単に実現できそうですね。

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

【初心者向け】map, filter, reduce を理解して関数型プログラミングの第一歩【図あり】

イントロ

JavaScript はオブジェクト指向言語ではあるものの、関数型プログラミング言語の影響を少なからず受けている。配列の map, filter, reduce メソッドがその一例である。これらはみな関数を引数に取る高階関数 (higher-order function) と呼ばれるものであり、for ループでよく行われる配列処理のパターンを抽象化したものとみることができる。日常的に行われる配列処理の大半は、これらのメソッドの組み合わせて簡潔に表現できる。

この記事では初心者向けにこれら3つのメソッドの動作を説明する。

Note: 3つのメソッドすべて、元の配列には変更を加えず、別の新しい配列/値を返すことに注意されたい。

map

map メソッド各要素をある関数で変換してできる新しい配列を返す。数学における写像 (map)に対応する。

const new_arr = arr.map(f)

f は関数であり、下の図のように arr の各要素に対して f を適用し、その返り値から新しい配列を生成し、返す。要素の順番は保存される。下の図は配列の各要素を2乗する例である。

arrmap.png

対応するコードは以下のようになる。

const arr = [2, 4, 8, 3, 1, 5]

function f(x) {
  return x * x
}
const new_arr = arr.map(f)

/* arrow function バージョン */
const new_arr = arr.map(x => x * x)

for ループで表現すると以下のようになる。

const arr = [2, 4, 8, 3, 1, 5]
const new_arr = []

for (const item of arr) {
  new_arr.push(item * item)
}

filter

filter メソッドある条件を満たす要素だけを取り出して新しい配列を返す。文字通りフィルターで要素を濾すメソッドである。

const new_arr = arr.filter(f)

条件を表す関数 f を引数にとり、ftrue (もしくは truthy) を返すような要素だけからなる新しい配列を返す。ftrue を返せば合格、false を返せば不合格、という具合に各要素の合否を調べ、合格した要素だけを集めて新しい配列を作る。下の図は偶数の要素だけを取り出す例である。

arrfilter.png

対応するコードは以下のようになる。

const arr = [2, 1, 4, 6, 3, 8]

function f(x) {
  return x % 2 === 0  // 2で割り切れるかどうか
}
const new_arr = arr.filter(f)

/* arrow function バージョン */
const new_arr = arr.filter(x => x % 2 === 0)

for ループで表現すると以下のようになる。

const arr = [2, 4, 8, 3, 1, 5]
const new_arr = []

for (const item of arr) {
  if (item % 2 === 0) {
    new_arr.push(item)
  }
}

reduce

このメソッドは他2つと比べると若干複雑だが、非常に便利な関数である。

reduce メソッド配列を左から見ていって最終的に1つの値を得る。複数の値を含む配列を1つの値へと減らすため、reduce と呼ばれる。accumulate や fold とも呼ばれる。

const result = arr.reduce(f, ini)

例でないとわかりにくいので、和を求める例で説明する。私たち人間が暗算で配列の和を求めようと思えば、普通左から見ていって繰り返し足し算を行うだろう。例えば [4, 1, 2, 3] という配列ならば、初期値は0で、
04を足して4
41を足して5
52を足して7
73を足して10
と計算する。この処理は、途中結果要素を受け取って次の途中結果を得る関数」を繰り返し適用することで表現できる、ということがわかる。この関数が f にあたる

f の第1引数が途中結果、第2引数が現在の要素である。今の場合は f は単に2つの値を足す関数、つまり function (x, y) { return x + y } である。。ini は初期値にあたり、和を求める場合は当然 0 だが、積を求めたい場合は 1 となる。図で表すと下のようになる。

arrreduce.png

これに対応するコードは以下である。

const arr = [4, 1, 2, 3]

function f(x, y) {
  return x + y
}
const sum = arr.reduce(f, 0)

/* arrow function バージョン */
const sum = arr.reduce((x, y) => x + y, 0)

for ループで表現すると以下のようになる。

const arr = [4, 1, 2, 3]
let sum = 0

for (const item of arr) {
  sum = sum + item
}

これだけ見れば、「和とか積みたいな数学的な処理にしか使えないのか」という印象を持つかもしれないが、最初に述べたとおり「配列を左からシーケンシャルに処理する」という形の処理であれば何でもできるということを覚えておきたい。その際、f としてどのような関数を指定すればよいかは上述の例のように考えれば分かるはずである。

例えば下のように配列をオブジェクトに変換したいとき、

const users = [{ "id": "U1321", "name": "John" }, { "id": "U17583", "name": "Mary" }]
// ↓
{ "U1321": "John", "U17583": "Mary" }

reduce を使って次のようにできる。

users.reduce((cur, user) => {
  cur[user.id] = user.name
  return cur
}, {})

注意点

mapfilter は実行するたびに新しい配列を生成する。巨大な配列に対してこのようなメソッドのチェーンを実行すると、無駄にメモリを圧迫しガーベージコレクションを発生させる状態になりかねない。例えば、下の例では2回無駄に配列が作られることになるが、新しく配列を作らずに同じ計算をすることができる。

const arr = [1, 2, ..., 100000000]
const new_arr = arr
  .map(x => x * x)             // <- ここで配列が作られる
  .filter(x => x % 3 == 0)     // <- ここでも
  .reduce((x, y) => x * y, 1)

このようなケースでは stream ベースで lazy に処理できる RxJSStream.js のようなライブラリを使うのがいいだろう。

もっと例

  • ユーザーの配列から、10歳以下のユーザーの名前の配列を求める
users
  .filter(user => user.age <= 10)
  .map(user => user.name)
  • ユーザーの配列から、20歳以上のユーザーの所持アイテム数の合計を求める
users
  .filter(user => user.age >= 20)
  .reduce((cur, user) => cur + uesr.itemCount, 0)
  • 非同期な(Promise を返す)関数の配列をシーケンシャル(逐次的)に実行する
promises
  .reduce((cur, p) => cur.then(() => p()), Promise.resolve())
// ↓
// Promise.resolve().then(() => promises[0]).then(() => promises[1])...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】map, filter, reduce で関数型プログラミングの第一歩【図あり】

イントロ

JavaScript はオブジェクト指向言語ではあるものの、関数型プログラミング言語の影響を少なからず受けている。配列の map, filter, reduce メソッドがその一例である。これらはみな関数を引数に取る高階関数 (higher-order function) と呼ばれるものであり、for ループでよく行われる配列処理のパターンを抽象化したものとみることができる。日常的に行われる配列処理の大半は、これらのメソッドの組み合わせて簡潔に表現できる。

この記事では初心者向けにこれら3つのメソッドの動作を説明する。

Note: 3つのメソッドすべて、元の配列には変更を加えず、別の新しい配列/値を返すことに注意されたい。

map

map メソッド各要素をある関数で変換してできる新しい配列を返す。数学における写像 (map)に対応する。

const new_arr = arr.map(f)

f は関数であり、下の図のように arr の各要素に対して f を適用し、その返り値から新しい配列を生成し、返す。要素の順番は保存される。下の図は配列の各要素を2乗する例である。

arrmap.png

対応するコードは以下のようになる。

const arr = [2, 4, 8, 3, 1, 5]

function f(x) {
  return x * x
}
const new_arr = arr.map(f)

/* arrow function バージョン */
const new_arr = arr.map(x => x * x)

for ループで表現すると以下のようになる。

const arr = [2, 4, 8, 3, 1, 5]
const new_arr = []

for (const item of arr) {
  new_arr.push(item * item)
}

filter

filter メソッドある条件を満たす要素だけを取り出して新しい配列を返す。文字通りフィルターで要素を濾すメソッドである。

const new_arr = arr.filter(f)

条件を表す関数 f を引数にとり、ftrue (もしくは truthy) を返すような要素だけからなる新しい配列を返す。ftrue を返せば合格、false を返せば不合格、という具合に各要素の合否を調べ、合格した要素だけを集めて新しい配列を作る。下の図は偶数の要素だけを取り出す例である。

arrfilter.png

対応するコードは以下のようになる。

const arr = [2, 1, 4, 6, 3, 8]

function f(x) {
  return x % 2 === 0  // 2で割り切れるかどうか
}
const new_arr = arr.filter(f)

/* arrow function バージョン */
const new_arr = arr.filter(x => x % 2 === 0)

for ループで表現すると以下のようになる。

const arr = [2, 4, 8, 3, 1, 5]
const new_arr = []

for (const item of arr) {
  if (item % 2 === 0) {
    new_arr.push(item)
  }
}

reduce

このメソッドは他2つと比べると若干複雑だが、非常に便利な関数である。

reduce メソッド配列を左から見ていって最終的に1つの値を得る。複数の値を含む配列を1つの値へと減らすため、reduce と呼ばれる。accumulate や fold とも呼ばれる。

const result = arr.reduce(f, ini)

例でないとわかりにくいので、和を求める例で説明する。私たち人間が暗算で配列の和を求めようと思えば、普通左から見ていって繰り返し足し算を行うだろう。例えば [4, 1, 2, 3] という配列ならば、初期値は0で、
04を足して4
41を足して5
52を足して7
73を足して10
と計算する。この処理は、途中結果要素を受け取って次の途中結果を得る関数」を繰り返し適用することで表現できる、ということがわかる。この関数が f にあたる

f の第1引数が途中結果、第2引数が現在の要素である。今の場合は f は単に2つの値を足す関数、つまり function (x, y) { return x + y } である。ini は初期値にあたり、和を求める場合は当然 0 だが、積を求めたい場合は 1 となる。図で表すと下のようになる。

arrreduce.png

これに対応するコードは以下である。

const arr = [4, 1, 2, 3]

function f(x, y) {
  return x + y
}
const sum = arr.reduce(f, 0)

/* arrow function バージョン */
const sum = arr.reduce((x, y) => x + y, 0)

for ループで表現すると以下のようになる。

const arr = [4, 1, 2, 3]
let sum = 0

for (const item of arr) {
  sum = sum + item
}

これだけ見れば、「和とか積みたいな数学的な処理にしか使えないのか」という印象を持つかもしれないが、最初に述べたとおり「配列を左からシーケンシャルに処理する」という形の処理であれば何でもできるということを覚えておきたい。その際、f としてどのような関数を指定すればよいかは上述の例のように考えれば分かるはずである。

例えば下のように配列をオブジェクトに変換したいとき、

const users = [{ "id": "U1321", "name": "John" }, { "id": "U17583", "name": "Mary" }]
// ↓
{ "U1321": "John", "U17583": "Mary" }

reduce を使って次のようにできる。

users.reduce((cur, user) => {
  cur[user.id] = user.name
  return cur
}, {})

注意点

mapfilter は実行するたびに新しい配列を生成する。巨大な配列に対してこのようなメソッドのチェーンを実行すると、無駄にメモリを圧迫しガーベージコレクションを発生させる状態になりかねない。例えば、下の例では2回無駄に配列が作られることになるが、実際には新しく配列を作らずに同じ計算をすることができる。

const arr = [1, 2, ..., 100000000]
const new_arr = arr
  .map(x => x * x)             // <- ここで配列が作られる
  .filter(x => x % 3 == 0)     // <- ここでも
  .reduce((x, y) => x * y, 1)

このようなケースでは for ループ1つでまとめるか、stream ベースで lazy に処理できる RxJSStream.js のようなライブラリを使うのがいいだろう。

もっと例

  • ユーザーの配列から、10歳以下のユーザーの名前の配列を求める
users
  .filter(user => user.age <= 10)
  .map(user => user.name)
  • ユーザーの配列から、20歳以上のユーザーの所持アイテム数の合計を求める
users
  .filter(user => user.age >= 20)
  .reduce((cur, user) => cur + uesr.itemCount, 0)
  • 非同期な(Promise を返す)関数の配列をシーケンシャル(逐次的)に実行する
promises
  .reduce((cur, p) => cur.then(() => p()), Promise.resolve())
// ↓
// Promise.resolve().then(() => promises[0]).then(() => promises[1])...
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ユーザースクリプトでWebサイトを改造する

ユーザースクリプトでWebサイトを改造する

はじめに

 ユーザースクリプトは自分のブラウザーに設定しておくJavaScriptです。特定のアドレスにアクセスした際に発動するスクリプトを設定しておくことで、他の人が作ったサイトを独自のページに作り替えることができます。

 入力が面倒なフォームに一瞬で入力できるよう自動入力ボタンを作ったり、大した文章量もないのに複数ページに記事を分けるニュースサイトで記事全体を1ページに結合するということもできます。

 あくまで自分のブラウザーに設定しているスクリプトなので、他の人が同じサイトにアクセスした場合は通常のサイトが表示されます。

 ユーザースクリプトはブラウザーで表示しているWebページ上で実行させるので、ログインを自分で実装する必要がありません。APIを呼び出す必要もないので開発者登録も不要です。ログイン後のページにスクリプトを仕掛けておき、Cookieを送信すればよいだけです。

 その代わり、クロスオリジンの制約を受けます。ユーザースクリプトのために都合良くクロスオリジンを許可してくれたりしないので、異なるオリジンにはアクセスできません。ただ、そもそもの用途からすると十分かと思います。

 また、ユーザースクリプトは自分のブラウザーで動けばいいので古いブラウザとの互換性は気にする必要がありません。自分のブラウザーが対応している分にはclassもfetchもasync/awaitも使い放題です。babelやpolyfilも使わなくて大丈夫です。

Tampermonkeyを導入する

 ユーザースクリプトを実行するためのブラウザ拡張は複数ありますが、私が使ってるのはTampermonkeyです。TampermonkeyはFirefox版、Chrome版、Edge版それぞれあります。

 なお、私はFirefoxを使ってますので、このあと記載するスクリプトはFirefoxで動かしています。Edgeは構文エラーでしたが、Chromeは初期表示ができるところまでは確認済みです。

404ページにスクリプトを設定

 基本的にはユーザースクリプトは既存のページをちょっとだけ変える用途だと思いますが、大きく改造したい場合は本来の機能を阻害しないように適当な404ページに設定します。そして、もともとのコンテンツを捨ててしまって自分で新たにDOMを構築します。

 ひとまず、ここの時点のコードが次の通りです。ここでは「はてな匿名ダイアリー」の特定の404ページにアクセスし、コンテンツをごっそり消しています。ただ、ヘッダーは欲しくなることもあるので残してます。#appの配下にタグを追加する想定です。

// ==UserScript==
// @name         カスタマイズ版匿名日記
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  try to take over the world!
// @author       You
// @require      https://cdn.jsdelivr.net/npm/vue
// @require      http://localhost/files/js/dayjs.min.js
// @match        https://anond.hatelabo.jp/customized
// @grant        none
// ==/UserScript==

class AnonymousDiary {
  constructor() {
    const original = document.createElement('div');
    document.body.appendChild(original);
    original.id = 'original';

    const app = document.createElement('div');
    document.body.appendChild(app);
    app.id = 'app';

    Array.apply(null, document.body.childNodes)
      .filter(child => child.id != 'original' && child.id != 'app')
      .forEach(child => {original.appendChild(child);});

    const queryForHide = '#hatena-anond, #original > p, #original > h1';
    Array.apply(null, document.querySelectorAll(queryForHide)).forEach(node => {
      node.style = 'display:none'
    });
  }
}

new AnonymousDiary();

ライブラリを追加する

JavaScriptのライブラリを追加するのはファイル先頭のコメント部分に@requireの行を追加するだけです。

// @require      https://cdn.jsdelivr.net/npm/vue
// @require      http://localhost/files/js/dayjs.min.js

スタイルシートはもっと適切な追加方法があるのか分かりませんが、<style>タグを動的に追加します。

const head = document.getElementsByTagName('head')[0];

const styleLink = document.createElement('link');
head.appendChild(styleLink);
styleLink.setAttribute('rel', 'stylesheet');
styleLink.setAttribute('type', 'text/css');
styleLink.setAttribute('href', 'https://bootswatch.com/4/litera/bootstrap.min.css');

fetchで本来のページを取得する

ここはあまり説明できることが少ないのですが、次のようなコードを実行すると元々のコンテンツのDOMを取得できるので、それで404ページに独自のコンテンツを作り上げます。

class AnonymousDiary {
  async getItems({page}) {
    const response = await fetch('https://anond.hatelabo.jp/?mode=top&page=' + page);
    const html = await response.text();
    const dom = new DOMParser().parseFromString(html, "text/html");

    // dom.bodyで<body>を取得できるのでなんやかんや実行する;
  }
}
new AnonymousDiary().getItems({page: 1});

スクリーンショット

 上記の続きをひととおり実装したものをGitHubのリポジトリにアップロードしてあります。

https://github.com/kakei-akihiko/customized-anonymous-diary

 このリポジトリのUserScript.jsをTampermonkeyに追加し、はてなラボにログインした状態で https://anond.hatelabo.jp/customized へアクセスすると次のスクリーンショットのようになります。

 上が改造版、下が通常のはてな匿名ダイアリーです。

Sample.png

 あらためて見比べてみると、古い方へ5ページ移動するリンクを付け足すだけでもよかったんじゃないかという気もしてきます。

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

JSで公開鍵を使って暗号化したデータをPHPで秘密鍵を使って復号する

公開鍵暗号を用いて、JSでデータを暗号化したものをPHPで復号してみます。
記載内容に問題があっても責任は取れないので、参考にする場合は自己責任でお願いします。
また、参考にした公式のドキュメントを載せるので、情報が変わっていないかそちらを確認していただければと思います。

ちなみに、パスワードは暗号化せずにハッシュ化しましょう。

公開鍵と暗号鍵を作成する

まずは暗号化に使う鍵を作成しておきます。

$ openssl genrsa -out rsa_2048_priv.pem 2048
$ openssl rsa -pubout -in rsa_2048_priv.pem -out rsa_2048_pub.pem

1024bitで作成している記事もありますが、2048bit以上のRSA鍵を使用するようにしましょう。

JSでの暗号化について

調べてみると、JSで暗号化を行う場合に様々なライブラリが存在しました。
英語ですが、下記のgistにJSで暗号化させる時のライブラリの一覧が載っています。
JavaScript Crypto Libraries

ただ、node.jsだとデフォルトでcryptoというモジュールが使えますし、多くのブラウザではSubtleCrypto オブジェクトが実装されています。

今回はLaravelのプロジェクトで使うことを想定して、node.jsのcryptoモジュールを使用します。

cryptoモジュールで暗号化する

公開鍵で暗号化するにはcrypto.publicEncryptというメソッドを使用します。

const crypto = require('crypto');

// 先ほど作成したpublickey
const publicKey = `-----BEGIN PUBLIC KEY-----
hogehogehogehgoe
-----END PUBLIC KEY-----
`;

const plain = 'hoge fuga';
const encrypted = crypto.publicEncrypt(publicKey, Buffer.from(plain));
console.log(encrypted);
console.log(encrypted.toString('base64'));

ドキュメントを見るとpublicEncryptの第二引数が buffer となっていますが、new Buffer() は廃止予定となっており、使用するとwarningが出たと思います。
いくつか記事を調べると、new Buffer(plain) を使用している記事が多いですが、Buffer.fromを使用して、バイナリデータに変換します。

参考
今年のうちに対応したい、Node.jsのBufferに潜む危険性

暗号化した結果を確認してみる

base64エンコードした結果をサーバ側へ送り、DBなどに保存をする想定です。

Base64は、データを64種類の印字可能な英数字のみを用いて、それ以外の文字を扱うことの出来ない通信環境にてマルチバイト文字やバイナリデータを扱うためのエンコード方式である

$ node publicEncrypt.js
# console.log(encrypted);
<Buffer b7 8c 41 4b 5e 3b 84 59 4a 31 f7 e5 53 a4 41 5d 55 c3 05 7d 27 18 9d 6d d6 eb 15 45 7d 53 01 99 79 eb ea d7 95 d6 2e 31 c1 b1 2c 76 ed cc 48 4a dd ea ... >
# console.log(encrypted.toString('base64'));
t4xBS147hFlKMfflU6RBXVXDBX0nGJ1t1usVRX1TAZl56+rXldYuMcGxLHbtzEhK3equw7qE8cCNulqJfst40B5OSThpkUoEiA9Q1Bo6b5RIhyZi8IQsTSZCkNE2LQaEILl5JZXfioYXmjTfMqDhn/jI6QLagWkRyd2jYIZVQ6ChhjzYG2eVWo5otli/N4Z9j93FKKQ1n8fiIvY62lZsOSv9I0F/ZZzCcYPFvI1DeOuYIw6StHC20lPo49d6quNjAAZwiLI9p43kue1PnD4M2HwwVFWYr4DRveDkR3gIUhOGk8UDI8BeCrwjVsz9jT5MjpAVp/6BT8/W1Q0NgJyctA==

PHPでの復号について

openssl-private-decryptを使うことで復号可能です。

// 先ほど作成した秘密鍵
$privatekey = <<<EOD
-----BEGIN RSA PRIVATE KEY-----
hogehogehgoe
-----END RSA PRIVATE KEY-----
EOD;

// 先ほど暗号化した文字列
$encrypted = 't4xBS147hFlKMfflU6RBXVXDBX0nGJ1t1usVRX1TAZl56+rXldYuMcGxLHbtzEhK3equw7qE8cCNulqJfst40B5OSThpkUoEiA9Q1Bo6b5RIhyZi8IQsTSZCkNE2LQaEILl5JZXfioYXmjTfMqDhn/jI6QLagWkRyd2jYIZVQ6ChhjzYG2eVWo5otli/N4Z9j93FKKQ1n8fiIvY62lZsOSv9I0F/ZZzCcYPFvI1DeOuYIw6StHC20lPo49d6quNjAAZwiLI9p43kue1PnD4M2HwwVFWYr4DRveDkR3gIUhOGk8UDI8BeCrwjVsz9jT5MjpAVp/6BT8/W1Q0NgJyctA==';
// base64エンコードしているのでデコードしてから復号する
$decoded= base64_decode($encrypted);
openssl_private_decrypt($decoded, $decrypted, $privatekey, OPENSSL_PKCS1_OAEP_PADDING);

var_dump(decrypted);

ポイントとしては、openssl_private_decrypt の第四引数にOPENSSL_PKCS1_OAEP_PADDING を指定しているところかなと思います。

crypto.publicEncryptのドキュメントに、RSA_PKCS1_OAEP_PADDING を使用していると記載がありますので、php側でも同じパディングを指定しています。

Otherwise, this function uses RSA_PKCS1_OAEP_PADDING.

下記のように第四引数に指定しない場合には、$decryptedがnullとなってしまいます。

openssl_private_decrypt($decoded, $decrypted, $privatekey);

復号した結果を確認してみる

これでできた

$ php privateDecrypt.php
string(9) "hoge fuga"

連携部分について

公開鍵も秘密鍵もサーバ側で持っておいて、公開鍵のみをcsrf-tokenと同じようにhtmlのmetaに埋め込むか、公開鍵を取得するためのAPIなどを用意してJSで取得をできるようにすれば良いと思います。

注意点

公開鍵暗号では容量の大きいデータをまとめて暗号化できない

当たり前かもしれませんが、公開鍵暗号では大きなサイズのデータを暗号化できません。
そのため、そこそこ長い文章などを暗号化しようとすると、下記のようなエラーが表示されてしまいます。

Error: error:0409A06E:rsa routines:RSA_padding_add_PKCS1_OAEP_mgf1:data too large for key size

容量の大きいデータを暗号化してサーバ側に送りたい場合には、公開鍵と共通鍵を使ったハイブリッド暗号で対応する必要があると思います。

参考
java – RSAで暗号化できるデータ量の制限は?

うまく復号できない場合に確認すること

最初復号できなかったのですが、teratailで同じような質問があり、参考にさせてもらいました。
使うライブラリやメソッドなどによって、設定に差異がないかを確認する必要ありそうです。

・暗号アルゴリズム(AES等) 
・鍵長(256ビット等) 
・モード(CBC等) 
・パディング(pcks7等) 

参考
JSとPHPでの公開鍵・秘密鍵による暗号化と復号化

その他参考情報

RSA暗号運用でやってはいけない n のこと

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

いいね機能を非同期で実装【Rails】【jQuery】

この記事の内容

like.gif
TODOリストを共有できるアプリを作っていて、いいね機能を非同期にて実装しました。
すでにたくさんのQiita記事がありますが、つまったポイントもあったので、自分なりにまとめ直してみます。
(コンセプトは「人生でやりたいこと100のリストの共有」なので、todoをdreamという言葉を使って表現しています。)

前提

Rails 5.2.3

構成

userの詳細ページにdreamリストが表示されています。
viewの構成としては、
/views/users/show.html.erb内で同じ階層の_dream.heml.erbが部分テンプレートとして呼ばれ、userのdreamを繰り返し表示しています。

CSSフレームワークはMaterializeを使用しています。

アソシエーション:
users - has_many :dreams, has_many :likes
dreams - belongs_to :user, has_many :likes
likes - belongs_to :user, belongs_to :dream

流れ

  1. jQueryの準備
  2. いいねボタンを作成
  3. コントローラ記述
  4. remote: trueにてjs.erbファイルを呼び出し
  5. js.erbファイル作成

実行

1. jQueryの準備

非同期化するにあたり、Rails内でjQueryを使えるように準備します。
まずはgemの導入です。

Gemfile
gem 'jquery-rails'

ターミナルで bundle installします。
そしてapplication.jsに記述を追加します。

app/assets/javascripts/application.js
//= require jquery3
//= require rails-ujs
//= require_tree .

順番が重要です。jqueryを最初に読み込む必要があります。

2. いいねボタンを作成

後から使い回ししやすいように、部分テンプレート化しています。

_dream.html.erbではテーブルでtodoリストを表示しているので、いいねの項目がtd内に入っています。
idの記載については5の項目で説明します。

app/views/users/_dream.html.erb
# いいね機能該当部分
<td id="like-<%= dream.id %>">
  <%= render partial: "like", locals: { dream: dream } %>
</td>

_like.html.erbではすでにいいねがあるかないかで★か☆かを出し分けて(Materializeのアイコンを使用しています)、最後にいいね数をdream.likes.lengthで表示しています。

app/views/users/_like.html.erb
<% if Like.find_by(user_id: current_user.id, dream_id: dream.id) %>
  <%= link_to "/dreams/#{dream.id}/likes", method: :delete %>
    <i class="material-icons">star</i>
  <% end %>
<% else %>
  <%= link_to "/dreams/#{dream.id}/likes", method: :post %>
    <i class="material-icons">star_border</i>
    <% end %>
<% end %>
<%= dream.likes.length %>

3. コントローラ記述

いいねのcreateとdestroyを定義していきます。

app/controllers/likes_controller.rb
class LikesController < ApplicationController
  before_action :set_dream

  def create
    @like = Like.create(user_id: current_user.id, dream_id: @dream.id)
  end

  def destroy
    @like = Like.find_by(user_id: current_user.id, dream_id: @dream.id)
    @like.destroy
  end

  private
  def set_dream
    @dream = Dream.find(params[:dream_id])
  end
end

ルーティングも忘れずに。

config/routes.rb
post '/dreams/:dream_id/likes' => "likes#create"
delete '/dreams/:dream_id/likes' => "likes#destroy"

この時点で、非同期ではないですがいいね機能が実装できているはず。

4. remote: trueにてjs.erbファイルを呼び出し

いいねボタンのlink_toremote: trueを追加します。

app/views/users/_like.html.erb
<% if Like.find_by(user_id: current_user.id, dream_id: dream.id) %>
  <%= link_to "/dreams/#{dream.id}/likes", method: :delete, remote: true do %>
    <i class="material-icons">star</i>
  <% end %>
<% else %>
  <%= link_to "/dreams/#{dream.id}/likes", method: :post, remote: true do %>
    <i class="material-icons">star_border</i>
    <% end %>
<% end %>
<%= dream.likes.length %>

この記述により、通常であれば、link_toで呼ばれるアクションに対応するhtml.erbファイルを呼び出すところ、js.erbファイルを呼び出せるようになります。
なのでページ遷移を行わず非同期で通信が行われるようになります。

5. js.erbファイル作成

js.erbファイルはその名前の通り、javascriptのファイルでありながら、ERBタグを使うことでrubyのコードを書ける優れものです。
コントローラーで定義したインスタンスを<% %><%= %>を使うことでそのままjsファイルに記述できます。

まずはcreateとdestroyそれぞれのアクションに対応するjs.erbファイルを作成します。
app/views/likes/create.js.erb
app/views/likes/destroy.js.erb

ここで2.で記述していたidについて説明します。

app/views/users/_like.html.erb(再掲)
<td id="like-<%= dream.id %>">
  <%= render partial: "like", locals: { dream: dream } %>
</td>

jsでイベントを発火させるためには、idまたはclassでセレクタを指定しますが、今回はどのdreamに対するいいねなのかを判別するために、id内にそのdreamのidを含める必要があります。
上のように書くことによって、id="like-1"のようなidを指定することができます。

ここまで来たらあとはjs.erbファイルの記述だけです。

app/views/likes/create.erb
$("#like-<%= @dream.id %>").html("<%= j(render partial: 'users/like', locals: { dream: @dream }) %>");
app/views/likes/destroy.erb
$("#like-<%= @dream.id %>").html("<%= j(render partial: 'users/like', locals: { dream: @dream }) %>");

これだけです。

まずはセレクタの指定ですが、js.erbファイルなのでコントローラで定義した変数が使えます。
id="like-<%= dream.id %>"に対応するように指定します。

そして、jQueryのhtml()メソッドで、指定したセレクタのhtmlを置き換えます。

その置き換える内容が
"<%= j(render partial: 'users/like', locals: { dream: @dream }) %>"の部分です。
部分テンプレートの_like.html.erbを呼び出しています。
(renderの前にあるjは、escape_javascriptのエイリアスで、改行と括弧をエスケープしてくれるメソッドです。)

これにより、likeが更新された状態で、ifで条件分岐されたりlikes.countが表示されたりします。

非同期処理の実現!

一度流れをつかむことができれば応用が効きそうなので、今後もいろいろなところで使ってみようと思います。

参考

Railsで remote: true と js.erbを使って簡単にAjax(非同期通信)を実装しよう!(いいね機能のデモ付)

 

分かりにくい点・間違っている点などがありましたらご指摘いただきますよう、よろしくお願いいたします。

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

Kotlin/JSでobjectの中身を確認するときはJSON.stringifyが便利

Kotlin/JS開発の小ネタです。
Kotlin/JSでフロントエンド開発していると、往々にしてJavaScriptのobjectの中身を確認したくなります。

package example

fun main() {
    val obj: dynamic = js("({key: 'value'})")
    println(obj)
}

しかし、上記のコードは[object Object]と出力します。

そこで便利なのがJSON.stringifyです。

package example

fun main() {
    val obj: dynamic = js("({key: 'value'})")
    println(JSON.stringify(obj))
}

上記のコードの出力は{"key":"value"}となり、期待通りの結果が得られました。
もちろんこれはstringとして出力されるので、開発者ツールで開いたり閉じたりできるアレは使えません。

たぶんtoString()の兼ね合いでこうなっていると思うんですが、よしなに出来るようになるといいですねえ

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

mobx-react-lite入門 前編: mobx-react-liteのObserver

0.はじめに

JavaScriptの「シンプルかつスケーラブルな」状態管理ライブラリことMobXReactと結びつけて、楽しくWebアプリケーションを作れるようになってみたいと思いませんか?

当記事ではReactとMobXを組み合わせて使うためのライブラリmobx-react-liteを使って、観測可能な状態観測者による状態管理を俯瞰してみたいと思います。

前編では、mobx-react-liteで提供されるObserverを紹介します。

筆者の環境
  • OS: macOS Mojave 10.14.5
  • ブラウザ: Safari バージョン12.1.1
  • Node.js: v10.16.0
  • Yarn: 1.15.2

環境と想定読者

node.jsのインストールが必要です。node.jsを使うならば、備え付けのパッケージマネージャのnpmについて詳しく知る必要があります。しかし、当記事ではパッケージマネージャとしてYarnを用います。yarnは以下からダウンロードできます。

MobXの前に、軽くReactに関する知識が必要です。

  • Hello World|React を確認しておきましょう。JSXを知り、関数型コンポーネントが書けるようになればオッケーです。
  • 今回のチュートリアルでは、関数型コンポーネントとHooksをたくさん書くので、以下の大変参考になるQiita記事を目を通しておくかもいいかもしれません。なお、本チュートリアルにおいてはHooksは、都度説明を入れるつもりです。React 16.8: 正式版となったReact Hooksを今さら総ざらいする|Qiita by uhyo

1.準備

Next.js 9を使って、今回のチュートリアルの環境を作っていきましょう。

パッケージマネージャと依存関係のインストール

作業用ディレクトリを作成し、そこで以下のコマンドを実行することで依存関係(パッケージ)をインストールします。

terminal
yarn add next@latest react@latest react-dom@latest mobx mobx-react-lite

執筆当時のpackage.json
package.json
{
  "dependencies": {
    "mobx": "^5.11.0",
    "mobx-react-lite": "^1.4.1",
    "next": "^9.0.1",
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  }
}

開発サーバの立ち上げ

pagesという名前のディレクトリを作成し、その中にindex.jsxというファイルを作成します。

pages/index.jsx
const Index = () => <p>It Works!</p>;

export default Index;

そして、以下のコマンドを実行すると開発サーバが立ち上がります。

terminal
yarn next

そして、http://localhost:3000 にアクセスした時に以下のように表示されていたら成功です。

スクリーンショット 2019-07-13 3.11.53.png

開発サーバーはターミナルでctr+cでを打てば終了します。

Next.jsの基本

Next.jspages/配下の.jsxファイルなどでReactのコンポーネントをexport defaultすると、そのディレクトリ名に対応するページが新規作成されます。

また、開発サーバーにおけるNext.jsはファイルの更新を検出し、サーバーを再起動することなく更新を反映させます。

次のチュートリアルの準備のために、pages/counter.jsxを作成して中をこのようにします。

pages/counter.jsx
const CounterPage = () => {
    return (
        <div>
            <p>ここはカウンターページです</p>
            <hr/>
        </div>
    );
};

export default CounterPage;

すると先ほどの説明のように、counterというページがブラウザで読み込めるようになります。
http://localhost:3000/counter
スクリーンショット 2019-07-13 3.24.13.png

2.Hello MobX

MobXの観測可能な状態観測者を早速使ってみましょう。

pages/counter.jsx
import {Observer, useLocalStore} from "mobx-react-lite"; //追加

const CounterPage = () => {
    /***
     * 観測可能な状態
     * ストア:{counter: number}のように扱える
     */
    const store = useLocalStore(() => ({counter: 0}));

    /***
     * ストアの操作のための関数
     * ボタンに与える
     */
    function increment() {
        store.counter++;
    }

    function decrement() {
        store.counter--;
    }

    return (
        <div>
            <p>ここはカウンターページです</p>
            <hr/>
            {/***
             * 観測者コンポーネント
             * 観測可能な状態の変化に応じて更新される
             */}
            <Observer>{() => (<p>{store.counter}</p>)}</Observer>

            <button onClick={increment}>+</button>
            <button onClick={decrement}>-</button>
        </div>
    );
};

export default CounterPage;

http://localhost:3000/counter
スクリーンショット 2019-07-13 3.53.44.png

これで、+を押せばカウントアップされ、-を押せばカウントダウンするページが作れました。

解説

観測可能な状態

mobx-react-liteではuseLocalStore Hookを使えば、観測可能な状態を作ることができます。観測可能な状態の作り方は他にもあります。

useLocalStore(() => {return オブジェクト}); // これでオブジェクト型のObservableステートが作れる

useLocalStoreはHookですので、二つのルールがあります。

フックは JavaScript の関数ですが、2 つの追加のルールがあります。

  • フックは関数のトップレベルのみで呼び出してください。ループや条件分岐やネストした関数の中でフックを呼び出さないでください。
  • フックは React の関数コンポーネントの内部のみで呼び出してください。通常の JavaScript 関数内では呼び出さないでください(ただしフックを呼び出していい場所がもう 1 カ所だけあります — 自分のカスタムフックの中です。これについてはすぐ後で学びます)。

フック早わかり|React

したがって、クラス型のコンポーネントからはuseLocalStore Hookは使えません。

観測者

mobx-react-liteでは観測者Reactコンポーネントを作ることができます。Observerコンポーネントは、子要素のようにReactのコンポーネントを書くことができず、render関数を渡してやる必要があります。つまりObserverコンポーネントを使う際は以下の形式になることが多いでしょう。

<Observer>{() => (観測可能な状態にアクセスするReact要素)}</Observer>

観測可能な状態へのアクセスとは、短絡的な話だと観測可能な状態.観測可能な状態のメンバーにおける.を含むということです。

3. mobx-react-liteで提供される観測者(HOC, Observerコンポーネント, useObserver)

ちょっと準備:Hooksやコンポーネントを使い回せるようにする

先ほどのチュートリアルでuseLocalStoreによるカウンターのストアを作りました。これを使い回せるようにカスタムHookを作成しましょう。

hooks/counterStore.js
import { useLocalStore } from "mobx-react-lite";

export function useCounterStore() {
  const store = useLocalStore(() => ({
    counter: 0,
    increment: () => {
      store.counter++;
    },
    decrement: () => {
      store.counter--;
    }
  }));
  return store;
}

incrementや、decrementをStoreの中に入れてしまうことで、取り回しが良くなります。

次にボタンもコンポーネントにしましょう。

components/counterButton.jsx
export const CounterButton = props => (
  <>
    <button onClick={props.store.increment}>+</button>
    <button onClick={props.store.decrement}>-</button>
  </>
);

ここまででファイル構成はこのようになっています。

.
├── components
│   └── counterButton.jsx
├── hooks
│   └── useCounterStore.js
├── package.json
├── pages
│   ├── counter.jsx
│   └── index.jsx
└── yarn.lock

Observerコンポーネント

Observerコンポーネントは最もよく使う観測者でしょう。使い方はすでに見た通りです。

pages/observer.jsxの全体
pages/observer.jsx
import { Observer } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.counter}</p>;

const CounterPage = () => {
  const store = useCounterStore();

  return (
    <div>
      <p>オブザーバーコンポーネントの例</p>
      <hr />

      <Observer>{() => <Counter counter={store.counter} />}</Observer>
      <CounterButton store={store} />
    </div>
  );
};

export default CounterPage;

Observerコンポーネントでハマるとすればrender propsです。

Observerコンポーネントが動かない例

Observerコンポーネントは、直下のrender関数の更新をすることができますが、render関数中でされにrender関数を呼び出された場合、子のrender関数の更新をすることができません。したがって以下のような例ではカウンターが動きません。

動かない例

<Observer>
  {() => (
    <Ueshita
      render={props => (
        <>
          ここは{props.name}
          <Counter counter={store.counter} />
        </>
      )}
    >
      {props => (
        <>
          ここは{props.name}
          <Counter counter={store.counter} />
        </>
      )}
    </Ueshita>
  )}
</Observer>

動く例

<Ueshita
  render={props => (
    <>
      ここは{props.name}
      <Observer>{() => <Counter counter={store.counter} />}</Observer>
    </>
  )}
>
  {props => (
    <>
      ここは{props.name}
      <Observer>{() => <Counter counter={store.counter} />}</Observer>
    </>
  )}
</Ueshita>

pages/observer2.jsxの全体
pages/observer2.jsx
import { Observer } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.counter}</p>;

const Ueshita = props => (
  <div>
    {props.render({ name: "上" })}
    <hr />
    {props.children({ name: "下" })}
  </div>
);
const CounterPage = () => {
  const store = useCounterStore();

  return (
    <div>
      <p>オブザーバーHOCの例</p>
      <hr />
      <h2>動かない</h2>
      <Observer>
        {() => (
          <Ueshita
            render={props => (
              <>
                ここは{props.name}
                <Counter counter={store.counter} />
              </>
            )}
          >
            {props => (
              <>
                ここは{props.name}
                <Counter counter={store.counter} />
              </>
            )}
          </Ueshita>
        )}
      </Observer>
      <hr />
      <h2>動く</h2>
      <Ueshita
        render={props => (
          <>
            ここは{props.name}
            <Observer>{() => <Counter counter={store.counter} />}</Observer>
          </>
        )}
      >
        {props => (
          <>
            ここは{props.name}
            <Observer>{() => <Counter counter={store.counter} />}</Observer>
          </>
        )}
      </Ueshita>
      <CounterButton store={store} />
    </div>
  );
};

export default CounterPage;

スクリーンショット 2019-07-13 21.23.56.png

参考: https://mobx-react.netlify.com/observer-component

observer HOC

HOCはコンポーネントを引数として、コンポーネントを返す関数です。mobx-react-liteには、ただのコンポーネントを受け取って、それを観測者にするobserverというHOCがあります。先ほど出てきたObserverは先頭が大文字です。注意してください。

pages/hoc.jsx
import { observer } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.store.counter}</p>;

const HOCCounter = observer(Counter);

const CounterPage = () => {
  const store = useCounterStore();

  return (
    <div>
      <p>オブザーバーHOCの例</p>
      <HOCCounter store={store}/>
      <hr />
      <CounterButton store={store} />
    </div>
  );
};

export default CounterPage;

http://localhost:3000/hoc

スクリーンショット 2019-07-13 19.23.49.png

これは、先ほどの例と同じようにカウンターとして機能します。

落とし穴: observerが機能しない

ちょっと待ってください。 この例のCounterコンポーネントのpropsの取り方が少し冗長なように見えます。この機能を実装するならばいちいちstoreを渡さなくても良さそうに思えますね。つまりこちらの方が汎用性が高いコンポーネントでしょう。

const CounterMod = props => <p>{props.counter}</p>;

これをobserver HOCに繋ぎます。

const HOCCounterMod = observer(CounterMod);

そして表示してみましょう。比較用に、Observerコンポーネントに直繋ぎする例も見てみます。

<>
カウンターModを直にオブサーバーにつなぐ例
<Observer>{() => <CounterMod counter={store.counter} />}</Observer>
単にHOCで繋いだ例
<HOCCounterMod counter={store.counter}/>
</>

pages/hoc.jsxの全体
pages/hoc.jsx
import { observer, Observer } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.store.counter}</p>;

const HOCCounter = observer(Counter);

const CounterMod = props => <p>{props.counter}</p>;

const HOCCounterMod = observer(CounterMod);

const CounterPage = () => {
  const store = useCounterStore();

  return (
    <div>
      <p>オブザーバーHOCの例</p>
      <HOCCounter store={store} />
      <hr />
      カウンターModを直にオブサーバーにつなぐ例
      <Observer>{() => <CounterMod counter={store.counter} />}</Observer>
      単にHOCで繋いだ例
      <HOCCounterMod counter={store.counter}/>
      <CounterButton store={store} />
    </div>
  );
};

export default CounterPage;

スクリーンショット 2019-07-13 19.31.06.png

なんと、動かない例が出てしまいました!単にHOCに繋いだものが更新されないのです。

これを動く例にしてみましょう。以下の二つを追加してみます

const HOCCounterModFixed = observer(props => <CounterMod counter={props.store.counter}/>);
<>
StoreからアクセスするようにHOCで繋いだ例
<HOCCounterModFixed store={store}/>
</>

pages/hoc.jsxの全体
pages/hoc.jsx
import { observer, Observer } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.store.counter}</p>;

const HOCCounter = observer(Counter);

const CounterMod = props => <p>{props.counter}</p>;

const HOCCounterMod = observer(CounterMod);

const HOCCounterModFixed = observer(props => <CounterMod counter={props.store.counter}/>);


const CounterPage = () => {
  const store = useCounterStore();

  return (
    <div>
      <p>オブザーバーHOCの例</p>
      <HOCCounter store={store} />
      <hr />
      カウンターModを直にオブサーバーにつなぐ例
      <Observer>{() => <CounterMod counter={store.counter} />}</Observer>
      単にHOCで繋いだ例
      <HOCCounterMod counter={store.counter}/>
      StoreからアクセするようにHOCで繋いだ例
      <HOCCounterModFixed store={store}/>
      <CounterButton store={store} />
    </div>
  );
};

export default CounterPage;

スクリーンショット 2019-07-13 19.35.52.png

これで予想通りに動くようになりました。

落とし穴の理由

MobXにおいて観測者が追跡しているものは観測可能な状態へのアクセスであり、シンプルな言い方をすれば観測可能な状態.観測可能な状態のメンバーにおける.を追っているのです。

const CounterMod = props => <p>{props.counter}</p>;
const HOCCounterMod = observer(CounterMod);

つまり

const HOCCounterMod = observer(props => <p>{props.counter}</p>);

<HOCCounterMod counter={store.counter}/>

と使ったところで、observerの引数の中でstore.counter.が見えていません。

MobXは観測可能な状態のメンバーそのもの、つまり値そのものの変化に対して反応することはできません。

したがってobserver HOCにおいては、propsで観測可能な状態を渡すようにしましょう。観測可能な状態のメンバーをコピーしたものや、観測可能な状態にアクセスした後の値を渡しても、表示は更新されません。

つまりHOCオブジェクトにおいてはJSXの要素の中で.があってもダメなのです。

<HOCCounterMod counter={store.counter}/>

この仕様のため、慣れてくると多くの場合でObserverコンポーネントを使うことが一番都合が良いと思うようになってきます。

つまり、以下の内容はちゃんと値の変化に応じて描画されます。

<Observer>{() => <HOCCounterMod counter={store.counter}/> />}</Observer>

こんなことをするのならば、observer HOCを使った意味がありませんね。

とはいえ、observer Hookを使えばこのようなことができます。

pages/hoc2.jsx
import { observer } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.counter}</p>;

const CounterPage = observer(() => {
  const store = useCounterStore();

  return (
    <div>
      <p>オブザーバーHOCの例2</p>
      <hr />

      <Counter counter={store.counter} />

      <CounterButton store={store} />
    </div>
  );
});

export default CounterPage;

これはちゃんと動きます。しかし再描画の範囲がCounterだけではなく、CounterPage全体ということに注意をしてください。

やたらobserver HOCをディスリましたが、使い用はあるはずです。

でも、もう一つディスりポイントがあって、observer HOCはReactのLegacy Contextに依存しています。その点でもobserver HOCは気味が悪いですね。

参考: https://mobx-react.netlify.com/observer-hoc

useObserver Hook

useObserver Hookは前述の二つの観測者で内部的に利用されているReact Hookです。observer HOCの代わりのような使い方ができます。

実際に現在のObserverコンポーネントの実装(TypeScript)は以下のようになっています。

function ObserverComponent({ children, render }: IObserverProps) {
    const component = children || render
    if (typeof component !== "function") {
        return null
    }
    return useObserver(component)
}

useObserverはmobx-react-liteの心臓部と言って差し支えないでしょう。
参考: https://github.com/mobxjs/mobx-react-lite/blob/master/src/ObserverComponent.ts

useObserverobserver HOCのように使いたければ以下のようにしましょう。

pages/useobserver.jsx
import { useObserver } from "mobx-react-lite";

import { useCounterStore } from "../hooks/useCounterStore";
import { CounterButton } from "../components/counterButton";

const Counter = props => <p>{props.counter}</p>;

const HookCounter = props => {
  //普通はこれをそのままreturnでオッケー:Hookっぽさを出すためにこうした。
  const Component = useObserver(() => (
    <Counter counter={props.store.counter} />
  ));
  return Component;
};

const CounterPage = () => {
  const store = useCounterStore();

  return (
    <div>
      <h2>オブザーバーHOOKの例</h2>
      <HookCounter store={store} />
      <hr />
      <CounterButton store={store} />
    </div>
  );
};

export default CounterPage;

まとめ

  • 基本はObserverコンポーネントを利用しましょう。
  • 観測者が動かない時は、観測可能な状態からのアクセスを観測できているかチェックする。
    • Render Propsで動かない時は、Observerをrender関数の中に入れる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む