20200224のNode.jsに関する記事は12件です。

node.jsからMongoDBに接続してDocumentを登録する。

はじめに

node.jsからMongoDBをインストールして、接続しようとしたところ、最初の接続で少し躓いたので、まとめる。

環境

  • Windows10 Pro 64bit
  • node.js v12.14.1
    • mongodb v6.13.4
  • MongoDB 4.2.3

環境構築

node.jsにmongodbをインストール

以下コマンドを実行
npm install mongodb

MongoDBをインストールする。

公式サイトからWindows用のインストーラをダウンロードしてインストールする。
実行して「次へ」ボタン押していく。

インストール後に環境変数にPathを追加する。
C:\Program Files\MongoDB\Server\4.2\bin

MongoDB側の準備

MongoDB Compass Communityを使用して、取得したいdatabaseとDocumentを作成しておく。
左下のボタンで押下すると、Create Databaseのダイアログが出るので、入力するだけ。

image.png

node.js側のコード

以上で準備完了。実際のサンプルコードは以下。
コメントアウト部分は旧VersionのMongoDBの記載方法です。
ネットで見つけた旧Versionの記載方法だとTypeError: db.collection is not a functionのエラーになりはまる。

node.js
const MongoClient = require("mongodb").MongoClient;
const dburl = "mongodb://localhost:27017/";
// const dburl = "mongodb://localhost:27017/myDatabase";

MongoClient.connect(dburl, (error, client) => {
  const collection = client.db('myDatabase').collection('myCollection');
  // const collection = client.collection('myCollection');

  collection
    .insertOne({_id: 1, path: "test"})
    .then(()=> console.log("success"))
    .catch(err => console.log(err));
});

最後に

MongoDB以外にも、Electronもバージョンによって、大きく記述方法が変わっており、ネットで調べたコードでそのまま動かしてもうまく動かず躓くことが多い。。。
公式サイト読めばいいのだけど、時間かかるしで、少しジレンマを感じる今日この頃。

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

同期的に複数のファイルをform-dataを使って送信するサンプル

こんにちは、wattak777です。

一ファイルを送信する、というサンプルは幾つかあるのですが、requestとform-dataを組み合わせて送る場合、複数ファイルの場合、例えばforループで送るようにしても1つ目、2つ目、3つ目と送りきる前にどんどん送ってしまうため同期的に送ることが出来ません。

なので、ちょっとサンプルを作ってみました。

サーバー側はmulterを使った以下のサンプル。

server.js
var express = require( 'express' ) ;
var app = express() ;
var multer = require( 'multer' ) ;

app.post('/file_upload', multer({dest: ファイルを置くパス}).single('my_file'), function (req, res) {
    console.log(req.file.path, req.file.originalname) ;
    res.sendStatus(200) ;
});

var server = app.listen(12345, function() {
    console.log("listening at port %s", server.address().port) ;
});

で、本題のクライアント側は以下の実装。
request-promiseを使って同期的に送るようにしました。

client.js
const fs = require('fs') ;
const request = require('request-promise') ;

const FileNameList = [
    'test1.bin',
    'test2.bin',
    'test3.bin'
] ;
var FileNameIndex = 0 ;

var returnCode = httpPost() ;

function httpPost() {
    const FormData = {
        my_file: {
            value: fs.createReadStream(ファイルのパス + FileNameList[FileNameIndex]),
            options: {
                filename: FileNameList[FileNameIndex],
                contentType: 'application/octet-stream'
            }
        }
    }

    const options = {
        uri: "http://サーバーのIPアドレス:12345/file_upload",
        formData: FormData,
        method: 'post',
        headers: {
            'Content-Type': 'multipart/form-data'
        }
    }

    var response = request(options)
        .then( function(body) {
            console.log( 'then :' + body ) ;
            onEnd() ;
        })
        .catch( function(err) {
            console.log( 'catch error :' + err ) ;
        }) ;
    return response.statusCode ;
}

function onEnd() {
    console.log( 'Index ' + FileNameIndex + ' is finish.' ) ;
    FileNameIndex = FileNameIndex + 1 ;
    if ( FileNameIndex >= FileNameList.length ) {
        console.log( 'End Operation.' ) ;
    } else {
        var res = httpPost() ;
    }
}

console.log( 'Start Operation.' ) ;

とやると、クライアント側の表示は以下のようになります。

$ node client.js
Start Operation.
then :OK
Index 0 is finish.
then :OK
Index 1 is finish.
then :OK
Index 2 is finish.
End Operation.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

同期的に複数のファイルをform-dataを使って送るには

こんにちは、wattak777です。

一ファイルを送信する、というサンプルは幾つかあるのですが、requestとform-dataを組み合わせて送る場合、複数ファイルの場合、例えばforループで送るようにしても1つ目、2つ目、3つ目と送りきる前にどんどん送ってしまうため同期的に送ることが出来ないため、ちょっとサンプルを作ってみました。

サーバー側はmulterを使った以下のサンプル。

server.js
var express = require( 'express' ) ;
var app = express() ;
var multer = require( 'multer' ) ;

app.post('/file_upload', multer({dest: ファイルを置くパス}).single('my_file'), function (req, res) {
    console.log(req.file.path, req.file.originalname) ;
    res.sendStatus(200) ;
});

var server = app.listen(12345, function() {
    console.log("listening at port %s", server.address().port) ;
});

で、本題のクライアント側は以下の実装。
request-promiseを使って同期的に送るようにしました。

client.js
const fs = require('fs') ;
const request = require('request-promise') ;

const FileNameList = [
    'test1.bin',
    'test2.bin',
    'test3.bin'
] ;
var FileNameIndex = 0 ;

var returnCode = httpPost() ;

function httpPost() {
    const FormData = {
        my_file: {
            value: fs.createReadStream(ファイルのパス + FileNameList[FileNameIndex]),
            options: {
                filename: FileNameList[FileNameIndex],
                contentType: 'application/octet-stream'
            }
        }
    }

    const options = {
        uri: "http://サーバーのIPアドレス:12345/file_upload",
        formData: FormData,
        method: 'post',
        headers: {
            'Content-Type': 'multipart/form-data'
        }
    }

    var response = request(options)
        .then( function(body) {
            console.log( 'then :' + body ) ;
            onEnd() ;
        })
        .catch( function(err) {
            console.log( 'catch error :' + err ) ;
        }) ;
    return response.statusCode ;
}

function onEnd() {
    console.log( 'Index ' + FileNameIndex + ' is finish.' ) ;
    FileNameIndex = FileNameIndex + 1 ;
    if ( FileNameIndex >= FileNameList.length ) {
        console.log( 'End Operation.' ) ;
    } else {
        var res = httpPost() ;
    }
}

console.log( 'Start Operation.' ) ;

とやると、クライアント側の表示は以下のようになります。

$ node form2_promise.js
Start Operation.
then :OK
Index 0 is finish.
then :OK
Index 1 is finish.
then :OK
Index 2 is finish.
End Operation.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

face-api.jsでニコラ○・ケ○ジになる

顔認識がjsだけでもそれなりに動くのを今更知ったので触ってみた。
顔晒すのに抵抗感がある古い人間なので文中画像少なめです。

成果物

Git: https://github.com/engabesi/face-nicolas
GithubPages: https://engabesi.github.io/face-nicolas/
2020-02-24_21h23_40.png

やること

  • expressでlocalhostを建てる
  • face-api.jsで顔の矩形を取る
  • 顔に画像を被せる
  • GithubPagesにdeploy

face-api.js

今回顔認識に使うライブラリはこちら
https://github.com/justadudewhohacks/face-api.js
jsだけでそれなりの精度とパフォーマンスを出せます。
複数人も認識可能。
ランドマークや表情、顔認証も出来ます。
demoはこちら
https://justadudewhohacks.github.io/face-api.js/face_and_landmark_detection/

注意点

face-api.jsはWebGLを使用しています。
その為ブラウザの設定でハードウェアアクセラレーションをオフにしている場合上手く動作しない可能性があります。

expressでlocalサーバーを建てる

android等でも気軽にテストするためにまずはlocalhostで動作するようにします。
後にgithubPagesも利用したいため、以下の構成にします。

構成
root/
├─ app.js
└─ docs/
    ├─ index.html
    ├─ images/
    └─ js/
       ├─ index.js
       └─ lib/
          ├─ face-api.min.js
          └─ models/
             ├─ tiny_face_detector_model-shard1
             └─ tiny_face_detector_model-weights_manifest.json

face-api.min.js, 及びmodels内ファイルは以下のrepoから取ってきます
https://github.com/justadudewhohacks/face-api.js/tree/master/dist
https://github.com/justadudewhohacks/face-api.js/tree/master/weights

expressを導入します。

shell
yarn init -y
yarn add express
docs\index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script defer src="./js/lib/face-api.min.js"></script>
    <style>
      body {
        margin: 0;
        padding: 0;
        width: 100vw;
        height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
      }

      canvas {
        position: absolute;
      }
    </style>
  </head>
  <body>
    <video id="video" width="720" height="560" autoplay muted></video>
    <script defer src="js/index.js"></script>
  </body>
</html>
app.js
const express = require("express");
const app = express();

const path = require("path");
app.listen(8080, () => {
  console.log("Running at Port 8080...");
});

app.use(express.static(path.join(__dirname, "docs")));

app.use((req, res) => res.sendStatus(404));

これでshellにnode app.jsと打ち、http://localhost:8080/ にアクセスするとページが開きます(今は真っ白)

webcamera設定

docs\index.js
const startVideo = async video => {
  try {
    const constraints = { audio: false, video: {} };
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    video.srcObject = stream;
  } catch (error) {
    console.error(error);
  }
};

(async () => {
  const video = document.querySelector("video");
  await startVideo(video);
})();

これでlocalhostを開くとwebcameraの映像が取れているはずです。

顔の矩形を取る

まずmodelを読み込みます。

docs\index.js
+ const loadModels = async () => {
+   await Promise.all([
+    faceapi.nets.tinyFaceDetector.loadFromUri(`/js/lib/models`)
+  ]);
+ };

  (async () => {
    const video = document.querySelector("video");
+   await loadModels();
    await startVideo(video);
  })();

次に顔を認識して矩形描画処理を追加します。

docs\index.js
(async () => {
  const video = document.querySelector("video");
  await loadModels();
  await startVideo(video);
// --- ADD ---
  video.addEventListener("play", () => {
    // overlay canvas作成
    const canvas = faceapi.createCanvasFromMedia(video);
    document.body.append(canvas);
    const displaySize = { width: video.width, height: video.height };
    faceapi.matchDimensions(canvas, displaySize);

    const tinyFaceDetectorOption = {
      // default 416
      inputSize: 224,
      // default 0.5
      scoreThreshold: 0.5
    };
    setInterval(async () => {
      const results = await faceapi.detectAllFaces(
        video,
        new faceapi.TinyFaceDetectorOptions(tinyFaceDetectorOption)
      );
      if (results.length <= 0) return;
      // 検出結果をcanvasのサイズにリサイズ
      const resizedResults = faceapi.resizeResults(results, displaySize);
      // canvasの内容をクリア
      canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
      // 矩形描画
      faceapi.draw.drawDetections(canvas, resizedResults);
    }, 100);
  });
// --- ADD ---
})();

これでscoreが記載された矩形が顔に被さって表示されるようになります。

顔に画像を被せる

検出結果から座標を取ってその位置に画像を被せてみます。

docs\index.js
(async () => {
  const video = document.querySelector("video");
+ // 画像セットアップ
+ const image = new Image();
+ image.src = `/images/cage_neutral.png`;
  await loadModels();
  await startVideo(video);
  video.addEventListener("play", () => {
    const canvas = faceapi.createCanvasFromMedia(video);
    document.body.append(canvas);
    const displaySize = { width: video.width, height: video.height };
    faceapi.matchDimensions(canvas, displaySize);
    const tinyFaceDetectorOption = {
      // default 416
      inputSize: 224,
      // default 0.5
      scoreThreshold: 0.5
    };
    setInterval(async () => {
      const results = await faceapi.detectAllFaces(
        video,
        new faceapi.TinyFaceDetectorOptions(tinyFaceDetectorOption)
      );
      if (results.length <= 0) return;
      const resizedResults = faceapi.resizeResults(results, displaySize);
      canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height);
-     // faceapi.draw.drawDetections(canvas, resizedResults);
+     resizedResults.forEach(detection => {
+       // 矩形のtopはデコあたりなので調整
+       const marginVal = 0.4;
+       // 矩形の情報はdetection.boxに格納されている
+       const width = detection.box.width;
+       const height = detection.box.height * (1.0 + marginVal);
+       const x = detection.box.x;
+       const y = detection.box.y - detection.box.height * marginVal;

+       canvas.getContext("2d").drawImage(image, x, y, width, height);
+     });
    }, 100);
  });
})();

これで自分の顔に画像を貼り付けることが出来ました。
他にもmodelやdetection時に.with~~とするだけで表情やランドマーク等の情報を取れます。
詳しくは公式README参照。自分のgitにあげている物にも画像調達に飽きて中途半端になっていますが表情を取るコードも記載してあります。

GithubPages

コードをrepositoryに上げたら
repository > settings > Options > GithubPages > Sourceを
master branch /docs folderにすればGithubPagesにデプロイすることが出来ます。
だから、ディレクトリ名をdocsにする必要があったんですね。

注意点

上記コードでは画像のpathを直打ちしています。
github.ioにデプロイする場合は問題ありませんが、github.io/SUB_REPO/にデプロイした場合、ルートURLがgithub.io扱いとなり、SUB_REPOが飛ばされてしまってpathがおかしくなってしまいます。
jekyllの使用をgithubは推奨していますが試して遊ぶだけの場合ちょっと面倒です。
暫定対策としてpath前に直接SUB_REPO名をくっつけてあげると一応動きます。

まとめ

たった数分でブラウザ完結の顔認識ができるなんて素晴らしい世の中になりました。
現状のコードだとスマートフォンで見た場合矩形がものすごく横長になってしまっているのでその辺の対応をしだすとヘビーになるかもしれませんがちょっと遊んで見るぐらいだと非常に有用だと思います。

他にもclmtrackr.jspico.js、更にはChromeの機能だけでできるShape Detection API等様々なライブラリがありますので是非触って遊んでみてください。

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

Testable Redux ~ React と Redux のテスト戦略 ~

image

本記事では Redux を使用した場合の React コンポーネントに対するテスト方法を考察します。

Redux に接続された React コンポーネントは、コンポーネント内のプロパティを Redux Store の state と同期させています。 Redux に強く依存しているコンポーネントをどのように Testable にしていけば良いのでしょうか。記事のタイトルは、t-wada 先生の言葉をお借りして、Testable Redux とつけさせていただきました。

純粋な React コンポーネントをテストする

さて、まずはじめに純粋な React のコンポーネントに対するテスト方法を振り返ってみます。Facebook に習えば、jest の公式ドキュメントでも紹介されている jestenzyme を組み合わせた方法が一般的でしょう。

以下のようなシンプルなコンポーネントを例として使用します。

next

count を保持する state をもち、onClick で state を変更します。簡単のために Functional Component とし、State Hooks を使用します。後のテストのために各タグにはカスタムデータ属性(data-test)を付与しておきます。プロダクションビルドでカスタムデータ属性を取り除く方法もあります。こちらの記事にて詳細に解説されていました。

import React, { useState } from "react";
export default () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <h3 data-test="count">count: {count}</h3>
      <button data-test="count-up" onClick={() => setCount(count + 1)}>⬆︎</button>
      <button data-test="count-down" onClick={() => setCount(count - 1)}>⬇︎</button>
    </div>
  );
};

これに対するテストは以下のように記述できます。初期値が 0 であること、ボタンをクリックして 1 に変わることを確認します。

import React from "react";
import { shallow } from "enzyme";
import Counter from "./PureReactCounter";

import testConfigure from "../../testConfigure";
testConfigure();

const sel = (id: string) => `[data-test="${id}"]`;
describe("<Counter /> コンポーネント", () => {
  const Component = shallow(<Counter />);
  it("ボタンをクリックしてカウントアップする", () => {
    expect(Component.find("h3").text()).toEqual("count: 0");
    Component.find(sel("count-up")).simulate("click");
    expect(Component.find("h3").text()).toEqual("count: 1");
  });
});

なお、testConfigure には以下を記述しておきます。enzyme を使用する場合のお約束のようなものです。enzyme の installationにて解説されています。

import Enzyme from "enzyme";
import EnzymeAdapter from "enzyme-adapter-react-16";
export default () => Enzyme.configure({ adapter: new EnzymeAdapter() });

結果は 8ms でした。このテストでは shallow() を使用しています。Shallow レンダリングは、コンポーネントをユニットテストの範囲でテストできるように制限します。テストが子コンポーネントの動作を間接的にアサートしないようにしてくれます。コンポーネントは子コンポーネントに依存させず、常にステートレスに保つことによって Shallow レンダリングによるテストができ、高速です。

 PASS  src/components/PureReactCounter.test.tsx
  <Counter /> コンポーネント
     ボタンをクリックしてカウントアップする (8ms)

ちなみに、mount() は完全な DOM レンダリングを行います。DOM API とやり取りする可能性のあるコンポーネントである場合や、より高次のコンポーネントにラップされているコンポーネントをテストする必要がある場合に最適です。ただしその一方で多くの依存関係を考慮することによりテストに要する時間は増える傾向にあります。

 PASS  src/components/PureReactCounter.test.tsx
  <Counter /> コンポーネント
    Shallowレンダリング
       ボタンをクリックしてカウントアップする (8ms)
    Fullレンダリング
       ボタンをクリックしてカウントアップする (8ms)

結果として実行時間はほとんど変わりませんでした。これは今回実装したコンポーネントが他のコンポーネントや API などと依存していないシンプルなコンポーネントであるためです。

Redux の使用を開始する

さて、先ほど実装したコンポーネントは、count を保持する state をもち、onClick で state を変更していました。この count という state を Redux の store で管理します。実装例を紹介する前に Redux のライフサイクルのおさらいをしましょう。

  1. View: ユーザが操作を行い、handleClick() などのイベント起動 function が実行される。
  2. ActionCreater: イベント起動 function は ActionCreater を通して action を生成する。
  3. Dispacther: action は Dispater に渡され、Reducer に流れる。
  4. Reducer: Reducer は action の type に応じて新しい state を返却する。

lifecycle

説明の粒度は荒いですが、おおまかにこのような流れで Redux による状態の管理が行われます。

さて、ディレクトリ構成は以下のようにして実装を進めていきます。

src/
├── index.tsx
├── App.tsx
├── components
│   ├── Counter.tsx
│   └── Counter.test.tsx
└── store
    ├── actions
    │   └── counterActions.ts
    ├── reducers
    │   └── counterReducer.ts
    └── store.ts

Action (actions/counterActions.ts)

Action の定義を行います。今回はカウントアップとカウントダウンする2種類の action があるので事前に定義しておき、他のファイルから参照できるようにしておきましょう。

export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

export interface ICountReducerAction {
  type: typeof INCREMENT | typeof DECREMENT;
}

Reducer (reducers/counterReducer.ts)

Reducer は飛んできた action の type に応じて state 返却する処理を書くのでした。INCREMENT の場合は +1DECREMENTの場合は -1された新しい state を返却しています。

import {
  ICountReducerAction,
  INCREMENT,
  DECREMENT
} from "../actions/counterActions";

const initialState = 0;
const counterReducer = (state = initialState, action: ICountReducerAction) => {
  switch (action.type) {
    case INCREMENT:
      return state + 1;
    case DECREMENT:
      return state - 1;
    default:
      return state;
  }
};
export default counterReducer;

Store

先に定義した reducer を束ねて、storeを作成しましょう。今回 reducer は1つですが、複数使用することになることも考慮して combineReducers() を使用しています。

import { createStore } from "redux";
import { combineReducers } from "redux";
import counterReducer from "./reducers/counterReducer";
const reducer = combineReducers({ count: counterReducer });
export default createStore(reducer);

Component から Redux の store を参照する

Redux の state を View に表示できるようにコンポーネントを実装していきましょう。まず初めに純粋に Redux の store を直接参照するような実装を考えます。

ReactDOM.render()store から subscribe() します。

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import store from "./store/store";

const render = () => ReactDOM.render(<App />, document.getElementById("root"));
render();
store.subscribe(render);

コンポーネントは以下のように実装します。この場合はどうでしょう、テストは容易でしょうか。

import React from "react";
import store from "../../store/store";
import { INCREMENT, DECREMENT } from "../../store/actions/counterActions";

export default () => {
  const count = store.getState().count; // store から直接参照する
  return (
    <div>
      <h3 data-test="count">count: {count}</h3>
      <button onClick={() => store.dispatch({ type: INCREMENT })}>⬆︎</button>
      <button onClick={() => store.dispatch({ type: DECREMENT })}>⬇︎</button>
    </div>
  );
};

テストコードを振り返ってみましょう。このままでは Redux の store を参照ができないため、ボタンをクリックした後に発火するはずの Reducer が起動しません。結果として count は 0 のままです。

const sel = (id: string) => `[data-test="${id}"]`;
describe("<Counter /> コンポーネント", () => {
  describe("Shallowレンダリング", () => {
    const Component = shallow(<Counter />);
    it("ボタンをクリックしてカウントアップする", () => {
      expect(Component.find("h3").text()).toEqual("count: 0");
      Component.find(sel("count-up")).simulate("click"); // Redux store の state が変更されない
      expect(Component.find("h3").text()).toEqual("count: 1"); // 0 のまま
    });
  });
  describe("Fullレンダリング", () => {
    const Component = mount(<Counter />);
    it("ボタンをクリックしてカウントアップする", () => {
      expect(Component.find("h3").text()).toEqual("count: 0");
      Component.find(sel("count-up")).simulate("click");
      expect(Component.find("h3").text()).toEqual("count: 1");
    });
  });
});

テスト結果(クリックして開く)
 FAIL  src/components/DirectAccessReduxStore.test.tsx
  <Counter /> コンポーネント
    Shallowレンダリング
      ✕ ボタンをクリックしてカウントアップする (11ms)
    Fullレンダリング
      ✕ ボタンをクリックしてカウントアップする (6ms)

  ● <Counter /> コンポーネント › Shallowレンダリング › ボタンをクリックしてカウントアップする

    expect(received).toEqual(expected) // deep equality

    Expected: "count: 1"
    Received: "count: 0"

      13 |       expect(Component.find("h3").text()).toEqual("count: 0");
      14 |       Component.find(sel("count-up")).simulate("click");
    > 15 |       expect(Component.find("h3").text()).toEqual("count: 1");
         |                                           ^
      16 |     });
      17 |   });
      18 |   describe("Fullレンダリング", () => {

      at Object.<anonymous> (src/components/DirectAccessReduxStore.test.tsx:15:43)

  ● <Counter /> コンポーネント › Fullレンダリング › ボタンをクリックしてカウントアップする

    expect(received).toEqual(expected) // deep equality

    Expected: "count: 1"
    Received: "count: 0"

      21 |       expect(Component.find("h3").text()).toEqual("count: 0");
      22 |       Component.find(sel("count-up")).simulate("click");
    > 23 |       expect(Component.find("h3").text()).toEqual("count: 1");
         |                                           ^
      24 |     });
      25 |   });
      26 | });

      at Object.<anonymous> (src/components/DirectAccessReduxStore.test.tsx:23:43)

Redux と React のコンポーネントが強く依存する関係になっており、React のコンポーネント単体としてテストしづらくなっています。この構成を変えていきましょう。まずは React と Redux の間の依存関係を切り離す方法を考えます。

react-reduxconnect() を使用して React と Redux の依存を引き剥がず

最も代表的な実装パターンとして react-reduxconnect() を使用する方法が挙げられます。これは、コンポーネントをマウントする時点で子コンポーネントや依存関係にある構成要素を考える必要がなく、シンプルです。

Counter コンポーネントと Redux store が強い依存関係にある実装から、なるべく疎結合になるように配慮します。いわゆる DI (Dependency Injection) の考え方にしたがって、Counter コンポーネントの store を外から props として注入できるように変更します。ただ単に props によって注入するだけではなく、Redux store との接合は connect() によって行います。詳細な手続きは公式チュートリアルを参照しましょう。このようなコンポーネントを HOC (Higher Order Component) と呼びます。詳細はこちらの記事が参考になります。

import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import { INCREMENT, DECREMENT } from "../../store/actions/counterActions";

interface ICounterState {
  counter: number;
}

interface ICounterProps {
  count: number;
  increment: any;
  decrement: any;
}

// props として store を流し込んで DI できるるようにする。
export const Counter = (store: ICounterProps) => {
  const { count, increment, decrement } = store;
  return (
    <div>
      <div data-testid="count">count: {count}</div>
      <button onClick={increment}>⬆︎</button>
      <button onClick={decrement}>⬇︎</button>
    </div>
  );
};

const mapStateToProps = (state: ICounterState) => ({
  count: state.counter
});
const mapDespatchToProps = (dispatch: Dispatch) => ({
  increment: () => dispatch({ type: INCREMENT }),
  decrement: () => dispatch({ type: DECREMENT })
});

// 第一引数の mapStateToProps は component に渡す props を制御する
// 第二引数の mapDespatchToProps は reducer を呼び出して、redux で管理している state を更新する
// Counter は取得したデータを props として扱いたい component を指定する
export default connect(mapStateToProps, mapDespatchToProps)(Counter);

さて、ここまでできれば勝てる気がしてきました。テストを書きます。

import React from "react";
import { mount, shallow } from "enzyme";
import { Provider } from "react-redux";
import { createStore } from "redux";
import sinon from "sinon";
import ConnectedCounter, { Counter } from "./ReactRedux";
import { reducer } from "../../store/store";
import testConfigure from "../../testConfigure";
testConfigure();
const sel = (id: string) => `[data-test="${id}"]`;
describe("<Counter /> コンポーネント", () => {
  describe("Shallowレンダリング", () => {
    const props = {
      count: 0,
      increment: sinon.spy(), // カウントアップするボタンを押した挙動を確認するためにスパイを差し込む
      decrement: sinon.spy()
    };
    const shallowComponent = shallow(<Counter {...props} />);
    it("ボタンをクリックしてカウントアップする", () => {
      expect(shallowComponent.find("h3").text()).toEqual("count: 0");
      shallowComponent.find(sel("count-up")).simulate("click");
      expect(props.increment).toHaveProperty("callCount", 1);
    });
  });
  describe("Fullレンダリング", () => {
    const getWrapper = (mockStore = createStore(reducer, { count: 0 })) =>
      mount(
        <Provider store={mockStore}>
          <ConnectedCounter />
        </Provider>
      );
    it("ボタンをクリックしてカウントアップする", () => {
      const wrapper = getWrapper();
      expect(wrapper.find("h3").text()).toEqual("count: 0");
      wrapper.find(sel("count-up")).simulate("click");
      expect(wrapper.find("h3").text()).toEqual("count: 1");
    });
  });
});

上記のテストでは、Shallow レンダリングと Full レンダリングの2つの方法でテストを記述しました。

Shallow レンダリングでは Redux の store を含めず、コンポーネント単体でテストを行います。そのため、カウントアップするボタンを押した際のハンドラ関数は simon.spy() を使用してスパイを差し込んで挙動を確認します。

一方で、Full レンダリングの場合は Redux の store だけをモックとして作成し、<Provider>の store に DI します。こちらの手法の方が Redux の store を含めたテストにはなるのですが、実行時間が増える傾向にあるため注意が必要です。ちなみに実行時間を比較すると以下のようになります。

 PASS  src/components/container/ReactRedux.test.tsx
  <Counter /> コンポーネント
    Shallowレンダリング
       ボタンをクリックしてカウントアップする (9ms)
    Fullレンダリング
       ボタンをクリックしてカウントアップする (44ms)

mount するのはインテグレーションテストだユニットテストではない、shallow の方が高速だ、などの論争があります。個人的な意見ですが、チーム開発を行う上ではシンプルかつ可読性を維持したテストコードを書いた方がむしろチームとしてアジリティが上がるのではないかと考えています。学習コストも高いですしね。
それでもプロダクトコードの量が大きくなるにつれて、テストの実行時間の増加によりアジリティが下がるケースもあります。shallow レンダリングと full レンダリングの実行時間の差は ms 程度ですが、塵も積もれば山となるということです。テストの方針を決めるのは難しいですね。

Redux Hooks を使用してコード量を減らす

怠惰で傲慢な我々は、現状に満足することはありません。上に挙げた実装は React と Redux の依存関係を引き剥がすことで、Shallow レンダリングによるテストが実現できるようになりました。その一方で少し複雑な実装をしなければならないように感じます。この章では Redux Hooks を使用して簡単に記述できる方法をご紹介します。

React の Hooks API は、Functional コンポーネントに対してローカルコンポーネントの state を使用できるようになる優秀な機能です。react-redux でも、既存の connect() を使用して実装された HOC (Higher Order Component) の代わりとして Hooks API を提供するようになりました。これを使用すると、前章の実装のようにコンポーネントをにラップして HOC を作るような面倒な作業は必要ありません。Redux store に subscribe して action を dispatch できます。

導入は非常に簡単です。useSelector()useDispatch() を使用しましょう。

import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { INCREMENT, DECREMENT } from "../../store/actions/counterActions";

export default () => {
  const count = useSelector((state: any) => state.count);
  const dispatch = useDispatch();
  const increment = () => { dispatch({ type: INCREMENT })};
  const decrement = () => { dispatch({ type: DECREMENT })};
  return (
    <div>
      <h3>count: {count}</h3>
      <button data-test="count-up" onClick={increment}>⬆︎</button>
      <button data-test="count-down" onClick={decrement}>⬇︎</button>
    </div>
  );
};

コードがとても短くなりました。かなりスマートです。あれ? でもおかしいですね、少し振り返ってみましょう。connect() を使用して HOC を作ったときには純粋な React コンポーネントと Redux との依存関係を引き剥がすことに成功していました。今回、Redux Hooks を使用した実装では、また依存関係が強くなってしまいました。これでは Full レンダリングを行うようなテストしか記述できません。

describe("<Counter /> コンポーネント", () => {
  describe("Fullレンダリング", () => {
    const getWrapper = (mockStore = createStore(reducer, { count: 0 })) =>
      mount(
        <Provider store={mockStore}>
          <ConnectedCounter />
        </Provider>
      );
    it("ボタンをクリックしてカウントアップする", () => {
      const wrapper = getWrapper();
      expect(wrapper.find("h3").text()).toEqual("count: 0");
      wrapper.find(sel("count-up")).simulate("click");
      expect(wrapper.find("h3").text()).toEqual("count: 1");
    });
  });
});

Redux Hooks でも Redux と React を分離する

基本的な考え方は今までと同様です。Redux と接続する部分を DI できるようにすれば OK です。useSelector()useDispatch() を使用している箇所を外に出してやりましょう。

import { useSelector, useDispatch } from "react-redux";
import { IRootState } from "../../store/store";
import { INCREMENT, DECREMENT } from "../../store/actions/counterActions";
import { Dispatch } from "redux";

interface ICounterProps {
  count: number;
  increment: Dispatch;
  decrement: Dispatch;
}
export const Counter = (props: ICounterProps) => {
  const { count, increment, decrement } = props;
  return (
    <div>
      <h3>count: {count}</h3>
      <button data-test="count-up" onClick={increment}>⬆︎</button>
      <button data-test="count-down" onClick={decrement}>⬇︎</button>
    </div>
  );
};

export default (props: any) => {
  const count = useSelector<IRootState>(state => state.count);
  const dispatch = useDispatch();
  const _props = {
    count,
    increment: () => dispatch({ type: INCREMENT }),
    decrement: () => dispatch({ type: DECREMENT }),
    ...props
  };
  return <Counter {..._props} />;
};

当然といえば当然ですが、テストコードは react-reduxconnect() を使用した場合の実装と同じになります。

テストコード(クリックして開く)

const sel = (id: string) => `[data-test="${id}"]`;
describe("<Counter /> コンポーネント", () => {
  describe("Shallowレンダリング", () => {
    const props = {
      count: 0,
      increment: sinon.spy(),
      decrement: sinon.spy()
    };
    const shallowComponent = shallow(<Counter {...props} />);
    it("ボタンをクリックしてカウントアップする", () => {
      expect(shallowComponent.find("h3").text()).toEqual("count: 0");
      shallowComponent.find(sel("count-up")).simulate("click");
      expect(props.increment).toHaveProperty("callCount", 1);
    });
  });
  describe("Fullレンダリング", () => {
    const getWrapper = (mockStore = createStore(reducer, { count: 0 })) =>
      mount(
        <Provider store={mockStore}>
          <ConnectedCounter />
        </Provider>
      );
    it("ボタンをクリックしてカウントアップする", () => {
      const wrapper = getWrapper();
      expect(wrapper.find("h3").text()).toEqual("count: 0");
      wrapper.find(sel("count-up")).simulate("click");
      expect(wrapper.find("h3").text()).toEqual("count: 1");
    });
  });
});

さいごに

結局のところ、どのアプローチを採用した場合も実際の Redux store を使用した React コンポーネントのテストは高速です。Redux の設計は、Action、Reducer、State のそれぞれが互いに分離される方法のため、テストに非常に適しています。
このように Dependency Injection しやすい設計のもと Redux が作られているので、このようなシンプルな構造を取ることができました。今回はご紹介できませんでしたが、Redux SagaRedux Thunk を使用した場合のアーキテクチャでも同様にテストはシンプルに記述できます。React をとりまくエコシステムは素晴らしいですね。

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

javascriptのfor in(オブジェクトのプロパティ名についての繰り返し)

for-in文は、オブジェクトに存在するプロパティについて、プロパティの名前を順不同で取り出していき、処理を実行する構文です。

for in の書き方

for([プロパティ名を格納する変数] in [オブジェクト]) {
  
}

指定したオブジェクトについて、プロパティ名を順不同で取り出し、inの前に定義した変数に格納してから処理を実行します。この時、繰り返しはすべてのプロパティについて処理を行うと終了します。

for in の例

var sum = 0;
var obj = { a:1, b:2, c:3 };
for(var name in obj) { // オブジェクトの中のプロパティ名を取り出す。
  sum += obj[name];
}

参考

http://www.ituore.com/entry/javascript-for#for-in%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E5%90%8D%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E3%81%AE%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%97

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

Node.js 事始め

はじめに

TypeScriptを覚えたいのですが、まずはNode.jsの勉強から始めた方が良さそうな感じでしたので、Node.jsの勉強を始めました。

Noda.jsのインストール

公式サイトからインストーラーをダウンロードしてインストールします。

nodejs1.jpg

LTS版の方が無難だと思います。
Node.jsはmacやLinuxのイメージが強かったのですが、Windowsでも問題なく動きました。

環境

OS:Windows 10 Pro 64bit
DB:SQL Server 2019(Cent OS 8 on Hyper-V)
node.js:v12.16.1
npm:v6.13.4
Editor:Visual Studio Code

作業フォルダの作成

今回は「D:\Node」を作業フォルダにしました。

Hello World

作業フォルダに以下のファイルを作成します。

sample01.js
var http = require('http');

var server = http.createServer( function(req, res) { 
    res.writeHead( 200, {'Content-Type': 'text/plain'});
    res.write('Hello World'); 
    res.end(); 
}); 

server.listen(3000);
console.log('サーバを起動しました'); 

コマンドプロンプトを起動し、作業フォルダに移動します。

D:
CD Node

Node.jsを起動します。
起動方法は「node ファイル名」です

node sample01.js

問題がなければ、コマンドプロンプトに以下の様に表示されます。

サーバを起動しました

http://localhost:3000
にアクセスします
nodejs2.jpg

ブラウザに「Hello World」と表示されればOKです。

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

Node.js (サーバサイド JavaScript) 事始め

はじめに

TypeScriptを覚えたいのですが、まずはNode.jsの勉強から始めた方が良さそうな感じでしたので、Node.jsの勉強を始めました。

Noda.jsのインストール

公式サイトからインストーラーをダウンロードしてインストールします。

nodejs1.jpg

LTS版の方が無難だと思います。
Node.jsはmacやLinuxのイメージが強かったのですが、Windowsでも問題なく動きました。

環境

OS:Windows 10 Pro 64bit
DB:SQL Server 2019(Cent OS 8 on Hyper-V)
node.js:v12.16.1
npm:v6.13.4
Editor:Visual Studio Code

作業フォルダの作成

今回は「D:\Node」を作業フォルダにしました。

Hello World

作業フォルダに以下のファイルを作成します。

sample01.js
var http = require('http');

var server = http.createServer( function(req, res) { 
    res.writeHead( 200, {'Content-Type': 'text/plain'});
    res.write('Hello World'); 
    res.end(); 
}); 

server.listen(3000);
console.log('サーバを起動しました'); 

コマンドプロンプトを起動し、作業フォルダに移動します。

D:
CD Node

Node.jsを起動します。
起動方法は「node ファイル名」です

node sample01.js

問題がなければ、コマンドプロンプトに以下の様に表示されます。

サーバを起動しました

http://localhost:3000
にアクセスします
nodejs2.jpg

ブラウザに「Hello World」と表示されればOKです。

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

Node.js日記#2

エラーメモ

node.jsで作成したプログラム実行時、
 Cannot read property '〇〇' of undefined
と表示された。これはundefinedの〇〇が読み込めないという(直訳)ことなので、このひとつ前の行でエラーが起きている可能性が高い。

忘れない用メモ

Node.jsでは入出力はStreamという概念で扱われる

ライブラリを探す習慣をつける。

  • yarn(ヤーン)
    Node.js のためのパッケージマネージャ

  • npm
    Node.jsが標準で用意されているパッケージマネージャ

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

javascript(node.js)のsetHeaderとwriteHeadの違い

setHeader

ヘッダー情報を設定する

response.setHeader(名前,);

writeHead

ヘッダー情報を出力する

response.writeHead(コード番号,メッセージ);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS LambdaによるスクレイピングでQiita Organizationの最新記事を取得しSlackに通知するBotを作った

Organizationページの「最新の投稿」をAWS Lambda(node.js)でスクレイピングし、新しい投稿の記事のタイトルとURLをSlackに投稿するBotを作ってみました

コードはここに置いてます(大分汚いです)
https://github.com/uji/qiita-organization-scraping/blob/master/app.js

AWS Lambdaの処理の流れ

  1. 「最新の投稿」にある記事をスクレイピングで取得
  2. AWS S3にあるtxtファイルを確認し、Slack未投稿の記事の有無を確認
  3. 未投稿の記事をSlackに投稿
  4. 最新記事のタイトルをtxtファイルでAWS S3に保存

投稿済みの記事を永続化する必要があるのですが、DB使うのは大げさな気がしたのでS3にtxtで保存します

スクレイピング

AWS Lambda 用のchromium chrome-aws-lambda を使ってスクレイピングしました
Google Cloud Functionsでも使えるっぽいです
puppeteerと使用感はほぼ同じです

.of-ItemLink_header-title classを取ってくると「最新の投稿」の要素を取ってこれます

const chromium = require("chrome-aws-lambda");
exports.handler = async (event, context) => {
  let browser = null;
  let elems = [];

  try {
    browser = await chromium.puppeteer.launch({
      args: chromium.args,
      defaultViewport: chromium.defaultViewport,
      executablePath: await chromium.executablePath,
      headless: chromium.headless,
    });

    let page = await browser.newPage();

    await page.goto("http://qiita.com/organizations/" + process.env.ORGANIZATION_NAME);
    const selector = ".of-ItemLink_header-title";
    elems = await page.$$eval(selector, es => es.map(e => [e.textContent, e.href]));
  } catch (error) {
    return context.fail(error);
  } finally {
    if (browser !== null) {
      await browser.close();
    }
  }

  return context.succeed(elems);
};

Slackに通知する

@slack/boltまたは@slack/web-apiでSlack APIを叩き、取得した記事をSlackに投げます
Slack Botでメッセージを送るには、singning secretbot tokenchannel id が必要になります

boltの場合
const { App, ExpressReceiver } = require("@slack/bolt");

const receiver = new ExpressReceiver({
  signingSecret: process.env.SLACK_SIGNING_SECRET
});

const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  receiver: receiver
});

app.client.chat.postMessage({
  channel: process.env.SLACK_CHANNEL,
  text: "message",
  token: process.env.SLACK_BOT_TOKEN
});

S3のテキストファイル読み込み、上書き

投稿済みの記事のタイトルを永続化します
aws-sdk を使うと結構簡単にS3にアクセスできます

読み込み
const AWS = require("aws-sdk");
const s3 = new AWS.S3({ region: "ap-northeast-1" });
let s3Params = {
  Bucket: process.env.BACKET_NAME,
  Key: process.env.FILE_NAME
};

s3.getObject(s3Params, (err, data) => {
  if (err) return context.fail(err);
  else latest = data.Body.toString();
});
上書き
const AWS = require("aws-sdk");
const s3 = new AWS.S3({ region: "ap-northeast-1" });
let s3Params = {
  Bucket: process.env.BACKET_NAME,
  Key: process.env.FILE_NAME,
  Body: "タイトル"
};

s3.putObject(s3Params, (err) => {
  if (err) return context.fail(err);
});

AWS Lambdaの設定

ランタイムにnode.jsを指定してLambdaを作成します

まあまあ時間のかかる処理なので、基本設定からメモリ、タイムアウト時間を変更しておきます
メモリは512MB、タイムアウト時間は1分にしました

node_modulesはzipにまとめてLayerに登録します
AWS Lambda レイヤー

AWS Lambdaで動かす

コードをLambdaに書いてテストを実行してみます

スクリーンショット 2020-02-24 0.53.23.png

スクリーンショット 2020-02-23 0.20.28.png

Slackに通知されました

AWS Cloud Watch Eventsで定期実行

AWS LambdaのトリガーにAWS Cloud Watch Eventsを追加して定期実行されるようにします

スクリーンショット 2020-02-24 1.38.12.png

毎朝9時に実行される設定にしました

まとめ

今回はスクレイピングで取得しましたがQiita APIを使ってユーザーごとの最新記事をとる方法もあります
そっちの方が良さそう

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

node は入っているのに npm が動かなくなった

背景

何も考えずnpm installをしたら、そんなんないぞって言われてしまった。
npm_qiita.png

解決方法

.bashrc
export PATH="$PATH:"/usr/local/Cellar/node/NODEのバージョン/bin/""

のを追記

$ source .bashrc

で反映させる。

要はPATHが通っていないだけだった。

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