- 投稿日:2019-07-14T22:26:00+09:00
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を止めて完成です。
終わりに
そもそも言葉がわからないというのもありますが、今回のようなアニメーションは探しても見つからなかったので記事にしました。数字の表示は他にもいろいろあるようで、リンクを貼っておくので興味があればそちらも参考にしてみるといいかもしれません。
それ以外の数字のアニメーション
- 投稿日:2019-07-14T21:55:34+09:00
妄想プロダクト室「ギュtto」のプロトタイプ
つくったモチベーション
こんな経験ありませんか?
チャットでwを生やすも、顔は真顔、、、?
それが普通だと思いますが、テキストだけのやりとりに私は寂しさを感じることがあります?
( もっと温度感のあるコミュニケーションがしたい、人を感じたい、、、 )
そこで、私はProtoOut LINE Things ハッカソンを機会に、新しい体感型通信デバイスをプロトタイピングしてみることにしてみました!
https://protoout.connpass.com/event/135942/「ギュtto」概要
”もふもふハートフルデバイス”「ギュtto」をプロトタイピング。
ぎゅっと抱きしめると、抱きしめてる間だけ、ハートをスマホに送ってくれて、
スマホからもボタンをクリックすることで、ぬいぐるみをバイブレーションさせることができます。
https://t.co/ziTVe4sSRB#prototyping#プロトタイピング
— taichi (@MakingEmo) 2019年7月14日
技術・素材
- LINE Things LIFF
- obnize
- サーボモータ
- 圧力センサー
- ぬいぐるみ
構成図
今後
取得した圧力センサーの値によって、LIFF上の表現を変えたい。
processingやcanvasで動的に表現したい
(現状はGIF画像を表示しているだけ)ハード側のバイブレーション部分を機能させたい。
現状、サーボモータをバイブレーション機能として代用しているが、
もっとわかりやすくぬいぐるみがバイブするような実装がしたい参考
- 投稿日:2019-07-14T20:26:09+09:00
それは「else if」ではない「else」と「if」だ
言語によっては elseif なんていうのもありますが、「else if」と言われているものは、elseのときに実行する処理がif~だという超小ネタです。
「if や else は必ず {} で囲め」というコーディング規約に書いているのに、そのコーディング規約の中でもelseやifを……
if (/*条件*/) { // 略 } else if (/*条件*/) { // 略 }……みたいに書かれていたりすると「こいつ分かってないな」と思ってしまいます。
- 投稿日:2019-07-14T20:03:14+09:00
【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
get
やset
を関数の前につけるとゲッターとセッターにすることも機能としてありますが、今回は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するところもクラスの書き方と同じですね。
しかも関数内の変数にはアクセスできないので(関数スコープ)、実質プライベートなプロパティを作ることもできます。比較して
こうしてみるとクロージャの方が便利なのでは?と思いましたが、以下のようなデメリットもあります。
- 継承できない(クラスはできる)
- メモリリーク問題
- 他言語からきた人にとってとっつきにくい
なので基本的にはクラスを使った方が良く、クロージャは特殊な書き方の例として留めておくのが私の中での結論です。
参考
- 投稿日:2019-07-14T19:50:38+09:00
手動テストを圧倒的に効率化するChrome DevTools技3選
はじめに
Chrome DevTools、とっても便利ですよね。
自分はそんなに使いこなせてないので、同僚エンジニアのディスプレイを覗き見ると「ま、またおれの知らないDevTools技使ってる……!」と驚かされることがよくあります1。今回は、自分が同僚から教えてもらった多くのスゴ技のうち、良く使うものを3つご紹介します。
なお、Chromeの話をしていますが、きっと他のブラウザにも似たような機能はあるかと思いますので、Chrome絶対使いたくないマンはぜひお気に入りのブラウザで探してみて下さい。
Javascriptの実行を途中で止める
DevTool上からブレークポイントを設定することができます。
ブレークポイントをご存じない方のために説明しておくと、「特定の場所まで来たら実行を一時停止する」機能です。
↑の例では特定のDOM要素に変更があった時に実行を一時停止
させていますが、他にも
- コードの特定の行に差し掛かったら実行を一時停止する
- 特定のイベントが発火したら(例えば特定の要素のclickイベントが発火したら)実行を一時停止する
などが出来ます。
テスター的には上記例で挙げた特定のDOM要素に変更があった時に実行を一時停止
が一番便利でしょう。
停止後は再生ボタンが表示され、停止を解除して次のブレークポイントまで実行したり、コードを1行ずつ実行(ステップ実行と呼びます)することもできます。
特定の操作を繰り返し実行する
Network
タブを開くと、それ以降に発生した通信ログを全て記録してくれます。
SPAとかでAjax通信がたくさん発生するシーンでとても有効でしょう。
不具合の原因がフロントエンドにあるのかバックエンドにあるのかを突き止めるのに使ったりしています。個人的にとても気に入っているのが、
Copy As Fetch
という機能で、これはリクエストをConsole
タブに貼り付けて実行できる形でコピーしてくれます。
画面上の操作を繰り返し実行したい時に便利です。例えば「コメントを100回投稿する」みたいなことをやりたい時に
- コメントを投稿する
Network
タブで該当のリクエストを探し出すCopy As Fetch
でリクエストをコピーする- Consoleにペーストする
- オラオラオラオラオラオラオラオラオラオラオラオラオラオラ(略)
みたいにすると簡単に量産することが出来ます2。
Cookieを見たり編集したりする
Application
タブからStorageやCacheを見ることができます。
Storageというのはブラウザ内で保存しているデータのことです。
よくCookieとかLocalStorageとか聞くと思うんですが、まさしくそれのことです。バグチケットを切った時に
「こちらの環境では再現しないですね」
「なんか変なCookieが残ったりしてないですかね」
などのように言われたら、とりあえずApplication
タブをチェックしてみましょう。Cookie周りの不具合が発生したときは、Webアプリ側の操作でどのようにCookieの値が変わるのかを見ながらテストするとわかりやすいです。
また、値は編集可能ですので、直接編集して挙動を確認してみるのも良いでしょう。不具合調査以外で自分が良く利用するユースケースでは、例えば言語設定をCookieに保存していたりする場合、
locale: ja
となっているところをen
に変更して言語を切り替える、みたいなことをしたりすることがあります。
- 投稿日:2019-07-14T19:44:14+09:00
ライブラリ作ってみました(jQuery編)
自作ライブラリを作った経緯
ソフトを開発するにあたって、自作のライブラリを使用しています。
理由は、勉強のためと言うこともあるし、自分で作った方が痒い所に手が届くということがあるからです。宜しければ、ご覧ください。javascriptライブラリ
javascript(jQUery)に関するライブラリ
煩雑な処理をまとめたものClassLibCanvas.js
キャンバス描画に関するライブラリClassLibMath.js
計算に関するライブラリClassLibTime.js
日時操作に関するライブラリClassLibUtility.js
一般操作のライブラリ
配列関連、テーブル操作など
- 投稿日:2019-07-14T19:15:29+09:00
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プロセッサはまだ新しい機能ですので仕様変更も今後予想されます。詳細は公式リファレンスを参照してください。
※本記事で扱うサンプルモジュールはこちらへ置いておきます。
評価環境
利用するソフトウェア
- 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.jsfunction 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 OKElasticsearchへのネットワーク接続テストも行っておきましょう。
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 winlogbeatElasticsearchへ転送されたイベントログに上述のようにgreetingフィールドが追加されます。
HelloWorldモジュールの詳細
winlogbeat-helloworld.jsfunction 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.jsfunction 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.StartServiceCommandPowerShellのStart-Serviceコマンドレットはエラーを返します。
未定義またはgojaで未サポートのJavascript API呼び出し
WinlogbeatのJavascriptプロセッサのエンジンはgojaです。そのため通常のWebブラウザでサポートされているJavascriptのAPIが利用可能とは限らない点に注意が必要です。
※gojaはECMAScript 5.1ベース。以下は未定義の関数hoge()を呼び出した例です。以下のようにエラー(「hoge is not defined at process」)となります。
winlogbeat-helloworld.jsfunction 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.jsfunction 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 OKWinlogbeatサービスへのモジュールのローディングを確認する
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.jsvar 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.jsvar 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プロセッサはまだ新しい機能ですので仕様変更も今後予想されます。詳細は公式リファレンスを参照してください。
- 投稿日:2019-07-14T18:30:21+09:00
【iOS対応】Vue.jsで背景固定のモーダルを作る
モーダルを作る際に面倒なのが、スクロールしたときの背景の固定。
別に背景が動いても直接問題が起きることはないのですが、やっぱり気にはなります。ユーザーにもこのサービス大丈夫かな...と余計な不安感を与えることにもなりますし。ただ、この背景固定、一筋縄ではいかないのが面倒なところ。とくにiOSは他のデバイスと異なる挙動を示し、AndroidやWebではちゃんと固定されるのにiOSだけ上手くいかない...と悩んでいる人も多いと思います。
何か解決方法はないかと探していたところ、非常に便利なライブラリを見つけたので、それで対応することにしました。
Body Scroll Lockを使う
名前の通り、機能を有効にすることで背景のスクロールを無効にできるライブラリです。便利なのは、ページ全てをスクロール無効にするのではなく、指定したDOM内部だけはスクロールできるように設定できること。
モーダル内部はスクロールさせたい!なんてケースにも対応可能ということです。githubでのスターも1400個以上ついていますし、ある程度安心して導入できるのもメリットですね。公式サイトはこちら↓
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が走ることを利用して、モーダルが表示されている場合にのみ背景が固定されるようにしています。まとめ
非常にシンプルで使いやすいので、背景固定に困っているケースで便利です。モーダル系ライブラリとも簡単に組み合わせられそうなのも良さそうですね。
- 投稿日:2019-07-14T18:16:14+09:00
ReactでHTMLを作成し、Clickした際にそれぞれの情報をモーダルに表示・非表示させる(Railsで)
はじめに
ReactでHTMLを作成するまでは案外すんなりと行ったのですが、クリックした際に「それぞれの情報を持たせてモーダルを表示する」ことに苦労したので忘備録としてまとめておきます。
最終的にはこのような感じになります。
たくさんの方法を試したので不必要なものもあるかもしれません。
その点は指摘していただけると幸いです。
下の画像をクリックした際にその情報を表示させています。
表示させる内容は個人で変更してください。
1.gemの導入とインストール
この2つのgemをGemfileに加え、
bundle install
してくださいGemfilegem '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.js
とGraduate.js
を作成します。
これらの名前は自分で決めてくださって結構です。
ではこちらのコードをコピーしてくださいApp.jsimport 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.jsimport 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
orhandleClickClose
)が呼ばれた時に更新しています。この時に注意して欲しいのが、更新する際は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
- 投稿日:2019-07-14T16:56:24+09:00
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-routerconnected-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-formをreact-router-domと一緒に使うと機能しないことが確認できました。reduxForm()とconnect()という2つのHOCで2重にラップした時に、propsが、最終的なコンポーネントにうまく伝わっていかない感じでした。結論としてはredux-formは使わずに、antdのForm.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つです。ページ遷移も含めて以下に紹介します。
ユーザ登録が成功すると、3秒後にログイン画面に自動遷移します。
ログインが成功すると、3秒後にホーム画面に自動遷移します。
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.js4.index.js
index.jsでReduxとreact-redux、connected-react-routerの初期設定を行います。
src/index.jsimport 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.jsimport { 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.jsexport 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.jsimport { 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.jsimport 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.jsimport loadable from '@loadable/component'; export default loadable(() => import('./index'));index.jsはcontainerコンポーネントとして、react-reduxの設定を行い、connect()でcomponentをラップします。加えてantdのForm.create()でラップしています。
2つのHOCを使っているのですが、順番に注意してください。src/containers/Register/index.jsimport { 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 myRegisterantdの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 Register7.ログイン
ログイン画面はユーザ登録画面と構成が全く同じです。説明も重複になるので、省略します。
src/containers/Login/Loadable.jsimport loadable from '@loadable/component'; export default loadable(() => import('./index'));src/containers/Login/index.jsimport { 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 myLoginsrc/containers/Login/Login.jsimport 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 Login8.ホーム
ホーム画面は特にありません。
src/containers/Home/Loadable.jsimport 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.jsimport loadable from "@loadable/component"; export default loadable(() => import("./index"));src/containers/NotFoundPage/index.jsimport React from "react"; export default class NotFound extends React.PureComponent { render() { return <h1>This is the NotFoundPage Page!</h1>; } }9.ヘッダー/フッター
ヘッダーとフッターですが、特に説明は不要でしょう。
src/components/Header.jsimport 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.jsimport 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;今回は以上です。
- 投稿日:2019-07-14T15:46:49+09:00
Electronでメニューバーを消す方法(Windows)
frame: falseにすると消えるとか(フレームレスになる)
titleBarStyle: 'hidden'で消えるとか(IOSでしか働かない)
Windowsでの情報が少なかったので悩みましたが結果的にmainWindow = new BrowserWindow({}); mainWindow.setMenu(null);これだけでした。
- 投稿日:2019-07-14T15:26:32+09:00
プログラミングマスコットのドット絵を書いた
プログラム言語のマスコット、ロゴ
あなたはPCにステッカー貼ってますか?
利用言語を愛していますか?
言語のOSSにプルリクエストを送るためなら睡眠時間を削れますか?そんなあなたに送るプログラムマスコット`sです。
きっかけはUTme!で自作Tシャツを作りたいと思っていた時に、プログラマーが目にするマスコットアニマル`sという記事を見たこと。
あと、三連休なのに予定がないからだZE(2019/07/14)
Gopher (Go)
マスコットといえばgopherくん。
ドット絵と寸胴ボディの親和性がGood。
"gopher" by Renée French CC-BY-3.0Duke (java)
Go言語のライバル?でもあるjavaのDukeさん。
マザー2のザコキャラ感がすんごい。
"Duke" by Sun Microsystems BSDrustacean (rust)
rustのrustaceanさん。
前者2人を圧倒的速度で凌駕する高速カニさん。(強い)Moby Dock (Docker)
DockerのMoby Dock親方。
みんなを支える縁の下の力持ち。
android robot (android)
その名の通りアンドロイドのロボットでandroid robotさん。
ドット絵にしても変わった感が無い・・・
"android robot" by google.com CC BY 3.0jenkins logo (jenkins)
怒り顔など、何かと改変されがちなジェンキンスのロゴ。
あなた、名が無きおじさんだったのね・・・
"jenkins logo" by jenkins.io CC BY-SA 3.0Tux (linux)
linuxのTaxさん。
可愛く見えて実は二段腹のおじさんペンギン。
"Tux" by Larry Ewing、Simon Budig、Anja GerwinskiGitLab logo (GitLab)
昔は怖かったGitLabのロゴ。
狐じゃないよ!たぬきだよ!
"GitLab logo" by gitlab.com CC BY-NC-SA 4.0Python logo (Python)
もはやマスコットではなくなってきた、Pythonのロゴマーク。
"python logo" by Python Software Foundation PSF licensepostgreSQL logo (postgreSQL)
一番難しかった。ポスグレのぞうさん。
"PostgreSQL" by postgresql.org PostgreSQL licenseMySQL logo (MySQL)
ポスグレ書くならこっちもね!MySQLのイルカさん。
笑顔がかわいい。
Slack logo (Slack)
みんなの雑談部屋。slackのロゴ。
新デザインにだんだん慣れてきた。
※著作件に関する記載が見つからなかったので、問題あればコメントなどで指摘してください。HTML5 logo (HTML5)
HTML5のロゴ。
筆者は20代半ばなので、5とそれ以前の違いがわかっていない。
"HTML5" by World Wide Web Consortium CC BY 3.0Javascript logo (JavaScript)
HTMLやったならこっちもね。
JavaScriptさん。
※こちらも著作権に関する記載が見当たらなかったです。UBUNTU logo (UBUNTU)
Macが買えず使っていたUBUNTUさん。
WSL2に期待中。
ReactJS logo (ReactJS)
個人的な思い入れ(業務で利用)だけで書いたReactJS。
複雑な構造をドット絵で書くのは無理あるよね。。。
"ReactJS logo" by facebook.com CC BY 4.0最後に
こんな感じのTシャツを着ている人がいたら声をかけてください。きっと私です。
著作件について
この記事に記載している画像はすべてSlackスタンプ、アイコンに流用していただいてかまいません。
ただし、全てのマスコット、ロゴは原案者がいらっしゃいます。
必ず著作件の確認、表示の上ご利用ください。
(octcat-githubは著作件縛りきつくて書いたけど載せれなかった。)
- 投稿日:2019-07-14T15:11:16+09:00
【Firebase Auth】1つのアカウントにメールアドレスと電話番号を紐付ける
FirebaseのAuthenticationを利用したプロダクトの開発を行う機会があり、一つのアカウントにメールアドレスと電話番号を紐付ける必要があったのでその手順を残しておきます。
とても簡単に実装できました。参考: https://firebase.google.com/docs/auth/web/phone-auth?hl=ja
今回の手順をgitで公開しています。こちらから確認できます。
googleでログインさせた後に電話番号で認証させるといったことも簡単に実現できそうですね。
- 投稿日:2019-07-14T14:07:33+09:00
【初心者向け】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乗する例である。対応するコードは以下のようになる。
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 を引数にとり、
f
がtrue
(もしくは truthy) を返すような要素だけからなる新しい配列を返す。f
がtrue
を返せば合格、false
を返せば不合格、という具合に各要素の合否を調べ、合格した要素だけを集めて新しい配列を作る。下の図は偶数の要素だけを取り出す例である。対応するコードは以下のようになる。
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で、
0と4を足して4、
4と1を足して5、
5と2を足して7、
7と3を足して10、
と計算する。この処理は、「途中結果と要素を受け取って次の途中結果を得る関数」を繰り返し適用することで表現できる、ということがわかる。この関数がf
にあたる。
f
の第1引数が途中結果、第2引数が現在の要素である。今の場合はf
は単に2つの値を足す関数、つまりfunction (x, y) { return x + y }
である。。ini
は初期値にあたり、和を求める場合は当然 0 だが、積を求めたい場合は 1 となる。図で表すと下のようになる。これに対応するコードは以下である。
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 }, {})注意点
map
とfilter
は実行するたびに新しい配列を生成する。巨大な配列に対してこのようなメソッドのチェーンを実行すると、無駄にメモリを圧迫しガーベージコレクションを発生させる状態になりかねない。例えば、下の例では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 に処理できる RxJS や Stream.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])...
- 投稿日:2019-07-14T14:07:33+09:00
【初心者向け】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乗する例である。対応するコードは以下のようになる。
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 を引数にとり、
f
がtrue
(もしくは truthy) を返すような要素だけからなる新しい配列を返す。f
がtrue
を返せば合格、false
を返せば不合格、という具合に各要素の合否を調べ、合格した要素だけを集めて新しい配列を作る。下の図は偶数の要素だけを取り出す例である。対応するコードは以下のようになる。
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で、
0と4を足して4、
4と1を足して5、
5と2を足して7、
7と3を足して10、
と計算する。この処理は、「途中結果と要素を受け取って次の途中結果を得る関数」を繰り返し適用することで表現できる、ということがわかる。この関数がf
にあたる。
f
の第1引数が途中結果、第2引数が現在の要素である。今の場合はf
は単に2つの値を足す関数、つまりfunction (x, y) { return x + y }
である。ini
は初期値にあたり、和を求める場合は当然 0 だが、積を求めたい場合は 1 となる。図で表すと下のようになる。これに対応するコードは以下である。
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 }, {})注意点
map
とfilter
は実行するたびに新しい配列を生成する。巨大な配列に対してこのようなメソッドのチェーンを実行すると、無駄にメモリを圧迫しガーベージコレクションを発生させる状態になりかねない。例えば、下の例では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 に処理できる RxJS や Stream.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])...
- 投稿日:2019-07-14T13:28:03+09:00
ユーザースクリプトで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 へアクセスすると次のスクリーンショットのようになります。上が改造版、下が通常のはてな匿名ダイアリーです。
あらためて見比べてみると、古い方へ5ページ移動するリンクを付け足すだけでもよかったんじゃないかという気もしてきます。
- 投稿日:2019-07-14T12:29:07+09:00
JSで公開鍵を使って暗号化したデータをPHPで秘密鍵を使って復号する
公開鍵暗号を用いて、JSでデータを暗号化したものをPHPで復号してみます。
記載内容に問題があっても責任は取れないので、参考にする場合は自己責任でお願いします。
また、参考にした公式のドキュメントを載せるので、情報が変わっていないかそちらを確認していただければと思います。ちなみに、パスワードは暗号化せずにハッシュ化しましょう。
公開鍵と暗号鍵を作成する
まずは暗号化に使う鍵を作成しておきます。
$ openssl genrsa -out rsa_2048_priv.pem 2048 $ openssl rsa -pubout -in rsa_2048_priv.pem -out rsa_2048_pub.pem1024bitで作成している記事もありますが、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容量の大きいデータを暗号化してサーバ側に送りたい場合には、公開鍵と共通鍵を使ったハイブリッド暗号で対応する必要があると思います。
うまく復号できない場合に確認すること
最初復号できなかったのですが、teratailで同じような質問があり、参考にさせてもらいました。
使うライブラリやメソッドなどによって、設定に差異がないかを確認する必要ありそうです。・暗号アルゴリズム(AES等) ・鍵長(256ビット等) ・モード(CBC等) ・パディング(pcks7等)その他参考情報
- 投稿日:2019-07-14T11:37:33+09:00
いいね機能を非同期で実装【Rails】【jQuery】
この記事の内容
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流れ
- jQueryの準備
- いいねボタンを作成
- コントローラ記述
- remote: trueにてjs.erbファイルを呼び出し
- js.erbファイル作成
実行
1. jQueryの準備
非同期化するにあたり、Rails内でjQueryを使えるように準備します。
まずはgemの導入です。Gemfilegem '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.rbclass 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.rbpost '/dreams/:dream_id/likes' => "likes#create" delete '/dreams/:dream_id/likes' => "likes#destroy"この時点で、非同期ではないですがいいね機能が実装できているはず。
4. remote: trueにてjs.erbファイルを呼び出し
いいねボタンの
link_to
にremote: 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(非同期通信)を実装しよう!(いいね機能のデモ付)
分かりにくい点・間違っている点などがありましたらご指摘いただきますよう、よろしくお願いいたします。
- 投稿日:2019-07-14T02:48:06+09:00
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()
の兼ね合いでこうなっていると思うんですが、よしなに出来るようになるといいですねえ
- 投稿日:2019-07-14T00:17:15+09:00
mobx-react-lite入門 前編: mobx-react-liteのObserver
0.はじめに
JavaScriptの「シンプルかつスケーラブルな」状態管理ライブラリことMobXをReactと結びつけて、楽しくWebアプリケーションを作れるようになってみたいと思いませんか?
当記事ではReactとMobXを組み合わせて使うためのライブラリmobx-react-liteを使って、観測可能な状態と観測者による状態管理を俯瞰してみたいと思います。
前編では、mobx-react-liteで提供されるObserverを紹介します。
環境と想定読者
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を使って、今回のチュートリアルの環境を作っていきましょう。
パッケージマネージャと依存関係のインストール
作業用ディレクトリを作成し、そこで以下のコマンドを実行することで依存関係(パッケージ)をインストールします。
terminalyarn 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.jsxconst Index = () => <p>It Works!</p>; export default Index;そして、以下のコマンドを実行すると開発サーバが立ち上がります。
terminalyarn next
そして、http://localhost:3000 にアクセスした時に以下のように表示されていたら成功です。
開発サーバーはターミナルで
ctr+c
でを打てば終了します。Next.jsの基本
Next.js
はpages/
配下の.jsx
ファイルなどでReact
のコンポーネントをexport default
すると、そのディレクトリ名に対応するページが新規作成されます。また、開発サーバーにおける
Next.js
はファイルの更新を検出し、サーバーを再起動することなく更新を反映させます。次のチュートリアルの準備のために、
pages/counter.jsx
を作成して中をこのようにします。pages/counter.jsxconst CounterPage = () => { return ( <div> <p>ここはカウンターページです</p> <hr/> </div> ); }; export default CounterPage;すると先ほどの説明のように、counterというページがブラウザで読み込めるようになります。
http://localhost:3000/counter
2.Hello MobX
MobXの観測可能な状態と観測者を早速使ってみましょう。
pages/counter.jsximport {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;これで、+を押せばカウントアップされ、-を押せばカウントダウンするページが作れました。
解説
観測可能な状態
mobx-react-lite
ではuseLocalStore
Hookを使えば、観測可能な状態を作ることができます。観測可能な状態の作り方は他にもあります。useLocalStore(() => {return オブジェクト}); // これでオブジェクト型のObservableステートが作れる
useLocalStore
はHookですので、二つのルールがあります。フックは JavaScript の関数ですが、2 つの追加のルールがあります。
- フックは関数のトップレベルのみで呼び出してください。ループや条件分岐やネストした関数の中でフックを呼び出さないでください。
- フックは React の関数コンポーネントの内部のみで呼び出してください。通常の JavaScript 関数内では呼び出さないでください(ただしフックを呼び出していい場所がもう 1 カ所だけあります — 自分のカスタムフックの中です。これについてはすぐ後で学びます)。
したがって、クラス型のコンポーネントからは
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.jsimport { 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.jsxexport 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.lockObserverコンポーネント
Observerコンポーネントは最もよく使う観測者でしょう。使い方はすでに見た通りです。
pages/observer.jsxの全体
pages/observer.jsximport { 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.jsximport { 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;参考: https://mobx-react.netlify.com/observer-component
observer HOC
HOCはコンポーネントを引数として、コンポーネントを返す関数です。mobx-react-liteには、ただのコンポーネントを受け取って、それを観測者にする
observer
というHOCがあります。先ほど出てきたObserver
は先頭が大文字です。注意してください。pages/hoc.jsximport { 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;これは、先ほどの例と同じようにカウンターとして機能します。
落とし穴: 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.jsximport { 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;なんと、動かない例が出てしまいました!単にHOCに繋いだものが更新されないのです。
これを動く例にしてみましょう。以下の二つを追加してみます
const HOCCounterModFixed = observer(props => <CounterMod counter={props.store.counter}/>);<> StoreからアクセスするようにHOCで繋いだ例 <HOCCounterModFixed store={store}/> </>
pages/hoc.jsxの全体
pages/hoc.jsximport { 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;これで予想通りに動くようになりました。
落とし穴の理由
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.jsximport { 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
useObserver
をobserver
HOCのように使いたければ以下のようにしましょう。pages/useobserver.jsximport { 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関数の中に入れる