dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

IDDD本もくもく読書会をやってみたメモ#1

はじめに

経緯など

教材

最初に読む書籍はこれにした。IDDD 本などと呼ばれたりする。

おそらく「ドメイン駆動設計とは何か」を知るためには エリック・エヴァンスのドメイン駆動設計 (IT Architects’Archive ソフトウェア開発の実践) を最初に読んだ方がいいんだろうけど、

  • IDDD本の方が発売日が新しく、取り上げているソフトウェアの例とかも馴染みやすいのではないか
  • DDD 本はコードがほぼ出てこない(と聞いている)のでどうしても話が抽象的になりがちで、挫折するんじゃないか

という理由でこちらにした。

なお、後から気づいたんだけど CodeZine で IDDD 本をかいつまんで説明した連載記事があり、
各章に入る前に一度こちらの記事を読んでから書籍に入った方が理解が早い気がしている。

IDDD本から理解するドメイン駆動設計連載一覧:CodeZine(コードジン)


もくもく読書会の進め方

第1回は本当に手探りで、19 時に集まってだらだら喋った後 19:30 から各自その場でもくもくと読書。
20:30 過ぎぐらいから(力尽きたのもあって)読めた範囲についてお互いの理解を確かめるために自由なディスカッションとなった。

第1回で読んだ範囲

第2章から読み始めた。

  • 2.8 全体像
  • 2.9 なぜそれほどまでに戦略的設計を重視するのか
  • 2.10 実世界におけるドメインサブドメイン
  • 2.11 境界づけられたコンテキストの意味を知る

2.11 あたりで頭に全然入ってこなくなったのでギブアップ。


学習メモ

以下、今回学んだことを個人的にメモ。

IDDD本の第2章「ドメイン」「サブドメイン」「境界づけられたコンテキスト」を読み解く (1/4):CodeZine(コードジン)
も一緒に読む。


キーワード


ユビキタス言語

ドメインエキスパート(そのドメインについて一番詳しい人)や開発者を含めたチーム全体で作り上げる共有言語のこと。
チーム全体で「A という用語は B という意味である」というぶれない共通認識を作り上げることが大切。


ドメイン

ドメインとは、広い意味で言うと、組織が行う事業やそれを取り巻く世界のことだ。

ソフトウェアを作るときには必ず対象となるビジネスがあるはずで、その領域のことだという理解。


コアドメインサブドメイン(支援サブドメイン、汎用サブドメイン

ソフトウェアが対象とする事業領域を1つのドメインと捉えて全部入りのドメインモデルを構築すればいいかというとそうではなくて(むしろ逆で)、事業のドメインを分野ごとに適切に小さなドメインに分割していって、分割したドメインを組み合わせて全体を構築することになる。
この分割したドメインコアドメインサブドメイン と呼ぶ。

コアドメインドメイン全体の中でも事業的に最も重要な部分。
ビジネス的に最も価値があり、他社との競争を行う上で差別化要因となる部分のこと?

コアドメインではない補助的な部分を サブドメイン と呼ぶ。
サブドメインはさらに、コアドメインほど需要ではないが業務に不可欠なものを 支援サブドメイン、業務上特別なことはないがシステム上必要なドメイン汎用サブドメイン と呼ぶ。


境界づけられたコンテキスト

境界づけられたコンテキストは明示的な境界であり、ドメインモデルがどこに属するのかを表すものである。ドメインモデルは、ユビキタス言語をソフトウェアモデルとして表したものだ。

これがまだうまく言葉で説明できない。
別の書籍や web 上の解説を読むと「ユビキタス言語が適用できる範囲」などと言われてたりする。

例として本書では「アカウント」という用語を取り上げている。
アカウントという用語は、銀行取引コンテキストでは「口座」という意味になるが、文学コンテキストでは「報告書」という意味になる。

それぞれのアカウントの特徴は、名前だけでは区別できない。区別するには、それぞれが属する概念的なコンテナ、つまり境界づけられたコンテキストに注目する必要がある。これを見てはじめて、両者の違いを理解できるというわけだ。

境界づけられたコンテキスト内では、それぞれの用語はただ1つの意味を持つようになる。すなわち、ユビキタス言語が複数の意味を持たないようになる。


「境界づけられたコンテキスト」と「コアドメインサブドメイン」の関係性

上述したように境界づけられたコンテキストの中では用語についての意味がブレず、ただ1つに決まることが望ましい。ので、コアドメインサブドメインとは1:1の関係になることが望ましい設計といえる、はず。


わからなかったこと

ここで出てきた言葉や概念についてはなんとなくわかった気になれたが、結局

についてはわかっていない。最後の項目については後半で明らかになりそうだけど。


今後の予定

次回は 7/5(水) 19:00〜 にまた集まることになった。

順番にいくと次は2章の残りと3章なんだけど、これはもくもく会と言いつつ一度は各自で目を通してこないとついていけなくなるねーという話になり
できる範囲で読んでくることになった。

とばしてしまった1章も読んだ方がいい気がしてきたので、次回までに頑張って読みたい。。。!

参加者募集してますので、同じように DDD 興味あったけど今まで手を出せてなかったーとかって人がいたらぜひ一緒に勉強しましょう。お気軽にご連絡ください。


Slack グループ

読書会中に話したことをまとめたり、それ以外に書籍を読んでいてわからなかったことなどを共有するために Slack グループを作った。

https://iddd-mokumoku.herokuapp.com/

誰でも参加できるのでよかったらどうぞ。

CircleCIにssh接続したら"Permission denied (publickey)"と表示されたときの対処法

メモ。
CircleCI に SSH でログインするため

f:id:dackdive:20170623182941p:plain

に従い接続しようとしたところ、Permission denied (publickey) が表示された。

$ ssh -p 64543 ubuntu@13.59.112.96
The authenticity of host '[13.59.112.96]:64543 ([13.59.112.96]:64543)' can't be established.
ECDSA key fingerprint is SHA256:Rz1/kWO9LDOBndW9AYUN2XORNU++R87RmzthsSEzMK8.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[13.59.112.96]:64543' (ECDSA) to the list of known hosts.
Permission denied (publickey).

公式ドキュメントのここに書いてあった。
SSH access to builds - CircleCI

$ ssh -v git@github.com

# or

$ ssh -v git@bitbucket.com

を実行すると

$ ssh -v git@bitbucket.com
OpenSSH_6.9p1, LibreSSL 2.1.8
debug1: Reading configuration data /etc/ssh/ssh_config
...

debug1: Offering RSA public key: /Users/yamazaki/.ssh/id_rsa

...

というように Offering... という行がある。

同じように CircleCI に対して -v オプションつきでログインすると

$ ssh -v -p 64543 ubuntu@13.59.112.96
...
debug1: Offering RSA public key: ...
...

というように同じような行が見つかるので、そこに書かれているパスが一致しているか確認する。

一致していない場合、GitHub/Bitbucket に接続したときに表示されていたパスを -i オプションで指定してログインする。

$ ssh -i ~/.ssh/id_rsa -p 64543 ubuntu@13.59.112.96

なお、接続しようとしたときに

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
...
Offending ECDSA key in /Users/yamazaki/.ssh/known_hosts:30
...

のようなメッセージが表示された場合、~/.ssh/known_hosts から該当行(ここでは 30 行目)を一度削除して再実行する。

webpack 3 リリース内容まとめ

2系に上げるかどうかを悩んでいる間に 3.0.0 がリリースされてしまった。

を参考にアップデート内容をメモ。

概要、v2 からのマイグレーション

大きな feature は Scope Hoisting と Magic Comment ぐらい。
v2 からの移行も特別な作業は必要なく、たいていはバージョン上げるだけで機能するとのこと。

それでもメジャーバージョンを上げたのは内部的に breaking changes があって、いくつかのプラグインには影響しそうだったから、とのこと。
ということはプラグインが動くかどうか一応確認した方がいい...?


Scope Hoisting

目玉機能その1。
これまでの webpack ではモジュールを1つ1つクロージャでラップしており、それによりそれぞれのモジュールの独立性が保たれていた(関数名の衝突とか)ものの、ブラウザでの実行速度が重くなる原因になっていた。
これに対し Closure Compiler やRollupJS は各モジュールを連結して1つのスコープにまとめてしまうので(hoist)ブラウザでの実行速度が速い。
(これができるのは ES Modules の import/export によって静的にモジュールの依存関係がわかるから?みたいなことが書かれていた)

webpack3 では以下のプラグインを指定することにより、RollupJS などと同じ Scope Hoisting が可能になった。

module.exports = {  
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
};

余計な関数でのラップがなくなったのでバンドル後のファイルサイズも小さくなったが、注目すべきは上述したように実行速度の向上ぽい。


参考

webpack freelancing log book (week 5–7) | by Tobias Koppers | webpack | Medium

冒頭のブログからリンクされていた。
Scope Hoisting のメリットや webpack でこれを実現するためにどうしたかみたいなことが詳しく書かれている。
特に気になったのは Scope Hoisting によるデメリットのところで、

There are some disadvantages, but most of them are not really relevant, when Scope Hoisting is seen as production-only optimization.

  • HMR can only replace modules if they are isolated with a function wrapper. (DX)
  • Minimizing can be more expensive when the scope is bigger. (Build Performance)
  • Modules can no longer processed individually, but need to be processed combined. (Build Performance)

HMR が効かなくなるみたいだし、プラグインは Production Build 時のみ有効にした方が良さそう。

サンプル

たとえば、

// main.js
import cube from './module_1';
import cube2 from './module_2';

console.log(cube(5) + cube2(5));

// module_1.js
export default function cube(x) {
    return x * x * x;
}

// module2.js
export default function cube(x) {
    return x * x * x;
}

というモジュール(module1 と 2 で同一名の関数をエクスポート)があって、
webpack 2.6.1 だと

/******/ (function(modules) { // webpackBootstrap

...

/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = cube;
function cube(x) {
  return x * x * x;
}


/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = cube;
function cube(x) {
  return x * x * x;
}

...

というようにたしかに関数でラップされているが、webpack 3.0.0 だと

/******/ (function(modules) { // webpackBootstrap

...

/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

// CONCATENATED MODULE: ./module-1.js
function cube(x) {
  return x * x * x;
}

// CONCATENATED MODULE: ./module-2.js
function module_2_cube(x) {
  return x * x * x;
}

...

というように関数のラップがなくなり、関数名の衝突も自動的に解決してくれてる。
※それ以外の部分の理解ができていないので解釈が間違ってるかもしれないけど

Rollup.js だと REPL で確認した限りもっと簡潔になる


Magic Comments

目玉機能その2。
元々、v2 から import() による動的インポートが可能になっていた。
(参考:webpack2でTree Shaking以外に何ができるようになったのかメモ - dackdive's blog

動的にインポートする部分のモジュールは chunk として別のファイルに出力されていたが、その名前が付けられないせいで 0.bundle.js のようなわかりづらいファイルが生成されていた。

webpack3 では import() 時に以下のコメントを入れることで任意のチャンクファイル名をつけることができる。

import(/* webpackChunkName: "my-chunk-name" */ 'module');

未検証。
※v2.4 あたりから既にできていたらしい


参考

より詳しくは公式ドキュメントのここを読んでくれとのこと。
https://webpack.js.org/guides/code-splitting-async/


その他の機能

https://github.com/webpack/webpack/releases/tag/v3.0.0 を見るしかないのかな?

  • output.libraryExport をサポート。default しかエクスポートしないよう選択可能に(ライブラリ向け)
  • node: false とすると Node.js 向けの機能すべて無効化

あたりが気になったが、詳細は調べきれてません。


今後

どこかのタイミングでリリースに含める feature は投票(vote)制になったらしい。
https://webpack.js.org/vote/

webpack-dev-serverをExpressに組み込んで使う(webpack-dev-middleware, webpack-hot-middleware)

はじめに

1年以上前ですがこんな記事を書きました。

このときは webpack-dev-serverスタンドアロンなサーバーとして使う方法しか知らなかったのですが
既存の Express アプリに組み込んで使うこともできます。
そのためには webpack-dev-middleware および HMR のために webpack-hot-middleware を使います。

今回はその設定手順をメモ。


公式ドキュメント

Webpack v1: https://webpack.github.io/docs/webpack-dev-middleware.html
Webpack v2: https://webpack.js.org/guides/development/#webpack-dev-middleware


設定手順

パッケージをインストールする
$ npm install -D webpack-dev-middleware webpack-hot-middleware


webpack.config.js を修正する

以下のように2箇所修正します。(ES2015 で書いてます)

 // webpack.config.dev.babel.js
 import webpack from 'webpack';

 export default {
   entry: [
+    'webpack-hot-middleware/client',
     './index',
   ],
 
   // Configuration for dev server
   devServer: {
     contentBase: path.resolve(path.join(__dirname, 'public')),
     port: 3000,
   },
 
   devtool: 'cheap-module-eval-source-map',
   plugins: [
       ...
+     new webpack.HotModuleReplacementPlugin(),
   ],
 });


Express アプリ内で middleware を読み込む

続いて、Express アプリ(ここでは server.js)内で上述した2つの middleware を使用します。
(サーバーサイドも ES2015 で書いてる想定です)

 // server.js
 import express from 'express';
 import path from 'path';
 
 const app = express();
 const port = process.env.PORT || 8080;
 
+if (process.env.NODE_ENV !== 'production') {
+  const webpack = require('webpack');
+  const webpackDevMiddleware = require('webpack-dev-middleware');
+  const webpackHotMiddleware = require('webpack-hot-middleware');
+  const config = require('./webpack.config.dev.babel').default;
+  const compiler = webpack(config);
+
+  app.use(webpackHotMiddleware(compiler));
+  app.use(webpackDevMiddleware(compiler, {
+    noInfo: true,
+    publicPath: config.output.publicPath,
+  }));
+}
 
 // Serve static files
 app.use(express.static(path.join(__dirname, 'public')));
 
 console.log(`Served: http://localhost:${port}`);
 app.listen(port, (err) => {
   if (err) {
     console.error(err);
   } else {
     console.info(`==> 🌎  Listening on port ${port}. Open up http://localhost:${port}/ in your browser.`);
   }
 });

基本的には webpack の config ファイルを読み込んで一度 webpack() に渡した後、それ(compiler)をそれぞれの middleware に引数として渡してあげればよいです。

また、webpackDevMiddleware の第二引数に渡しているオプションは

  • noInfo:info レベルのログを console に出力するかどうか。true(しない)にしてる人が多い印象
  • publicPath(必須): webpack.config.js の output.publicPath を指定しておけば OK

それ以外のオプションについては README を参照してください。

NOTE:
webpack.config を ES5 で書いてる場合

const config = require('./webpack.config.dev.babel').default;

.default は不要です。

また、これらの middleware は本番環境では無効にしておく必要があるため
process.env.NODE_ENV で切り替えができるようにしておきます。


サーバーを起動する

設定は以上です。後は package.json

"scripts": {
  "start": "babel-node server.js"
}

などと書き、

$ npm start

でサーバーを起動すると webpack-dev-server および HMR が有効になります。


注意事項

ここまでで開発時の設定としては十分なのですが、この webpack.config.js で本番環境用にビルドをすると
entryplugins に HMR が含まれているため実行時にエラーになります。

GET http://localhost:8080/__webpack_hmr 404 (Not Found)

そのため、実際には開発用(dev server 用)と本番環境向けビルド用に config ファイルを分けるか、または条件分岐を行う必要があります。

このあたりはサンプルコードを作ったので参考にしてください。

config ファイルを

  • webpack.config.base.babel.js:開発用でも本番環境用でも共通の設定
  • webpack.config.dev.babel.js:開発用の設定(↑で挙げたような)を記載
  • webpack.config.prod.babel.js:本番環境用の設定(圧縮など)を記載

というように3つに分割し、webpack-merge を使って base を残り2つにマージする形でそれぞれの環境用の config を生成しています。

react-autosuggestでサジェスト(Autocomplete)項目を作る

Google 検索や乗換検索サービスとかでよく見るこれ。サジェストと呼ぶのかオートコンプリートと呼ぶのか。

f:id:dackdive:20170606033420g:plain

react-autosuggest というライブラリを使って実装してみます。
Codepen にサンプルコードがたくさんあります。

https://codepen.io/collection/DkkYaQ/


React のみの場合

Basic Usage のコードを参考に。

import React from 'react';
import { render } from 'react-dom';
import Autosuggest from 'react-autosuggest';

import './styles/index.scss';

// サジェストに表示する項目
const languages = [
  {
    name: 'C',
    year: 1972
  },
  ...
];


// 入力値に対するサジェスト項目を取得するロジック
const getSuggestions = (value) => {
  const inputValue = value.trim().toLowerCase();
  const inputLength = inputValue.length;

  return inputLength === 0 ? [] : languages.filter((lang) =>
    lang.name.toLowerCase().slice(0, inputLength) === inputValue,
  );
};

// サジェスト項目が Object の場合、サジェスト選択時に Object のどの項目を表示するか決める
const getSuggestionValue = (suggestion) => suggestion.name;

// サジェスト部分のレンダリングロジック
const renderSuggestion = (suggestion) => (
  <div>
    {suggestion.name}
  </div>
);

class Example extends React.Component {
  constructor() {
    super();

    // Autosuggest is a controlled component.
    // This means that you need to provide an input value
    // and an onChange handler that updates this value (see below).
    // Suggestions also need to be provided to the Autosuggest,
    // and they are initially empty because the Autosuggest is closed.
    this.state = {
      value: '',
      suggestions: [],
    };
  }

  onChange = (event, { newValue }) => {
    this.setState({
      value: newValue,
    });
  };

  // Autosuggest will call this function every time you need to update suggestions.
  // You already implemented this logic above, so just use it.
  onSuggestionsFetchRequested = ({ value }) => {
    console.log('onSuggestionsFetchRequested');
    this.setState({
      suggestions: getSuggestions(value),
    });
  };

  // Autosuggest will call this function every time you need to clear suggestions.
  onSuggestionsClearRequested = () => {
    console.log('onSuggestionsClearRequested');
    this.setState({
      suggestions: [],
    });
  };

  render() {
    const { value, suggestions } = this.state;

    // Autosuggest will pass through all these props to the input element.
    const inputProps = {
      placeholder: 'Type a programming language',
      value,
      onChange: this.onChange,
    };

    // Finally, render it!
    return (
      <Autosuggest
        suggestions={suggestions}
        onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
        onSuggestionsClearRequested={this.onSuggestionsClearRequested}
        getSuggestionValue={getSuggestionValue}
        renderSuggestion={renderSuggestion}
        inputProps={inputProps}
      />
    );
  }
}

render(
  <Example />,
  document.getElementById('root'),
);

NOTE:スタイルを指定しないとキャプチャのように <ul><li> のリスト表示になるので
https://codepen.io/moroshko/pen/XRgbxR を参考にスタイルを当てる必要がある。

f:id:dackdive:20170606034840p:plain


必須 props

以下、<Autosuggest> に必ず渡す必要があるデータや関数。

なお、便宜的にサジェスト項目として扱う Object を suggestion と表現する。↑の例だと

  {
    name: 'C',
    year: 1972
  },

がサジェスト項目1個分に相当する。


- suggestions: Array<suggestion>

文字通り、サジェストに表示するデータのリスト。 Array であれば良い。


- getSuggestionValue(): Function(suggestion) -> String

サジェスト項目がクリックされたとき、または上下カーソルキーでサジェスト内容を選択したときに
input 項目に表示するテキストを決めるロジックになる。

戻り値は String じゃないといけない。


- renderSuggestion(): Function(suggestion) -> JSX

サジェスト項目をどのように render するか。


- onSuggestionsFetchRequested(): Function(現在の入力値)

ドキュメントには

Will be called every time you need to recalculate suggestions.

と書かれており、発火タイミングがはっきりしないけど、入力テキストが変更されたときに呼ばれる関数。
基本的にはここで state で管理している suggestions をアップデートする。


- onSuggestionsClearRequested(): Function()

こちらも発火タイミングが謎。サジェストを非表示にしたときに呼ばれる関数。
ここで suggestions[] をセットするなどしてクリアーする。


- inputProps: Object

内部的に使われている <input> 項目へ渡す props。value, onChange などは基本必須になるかと。


サジェスト結果をセクションに分ける

サジェスト結果をカテゴライズして表示したいということがある。Yahoo!路線情報 のような。

f:id:dackdive:20170606041335p:plain:w160

この場合、suggestions として渡すデータは

const languages = [
  {
    title: '1970s',
    languages: [
      {
        name: 'C',
        year: 1972
      }
    ]
  },
  ...
];

というように一段ネストが深くなる。
title がセクションタイトル、languages がそのセクションにおける suggestions となる。

新たに以下の props を指定する。


- multiSection: bool

true にする。


- renderSectionTitle(): Function(section) -> JSX

セクションのタイトルの render ロジック。
↑の例で title を指定する場合は

function renderSectionTitle(section) {
  return (
    <strong>{section.title}</strong>
  );
}


- getSectionSuggestions(): Function(section) -> suggestions

セクションごとの suggestions を取得するロジック。
↑の例の場合は

function getSectionSuggestions(section) {
  return section.languages;
}


Redux と組み合わせる

https://github.com/zaki-yama/react-autosuggest-sample を参照。
基本的に value, suggestions を Redux の state で管理し、onChange, onSuggestionsFetchRequested, onSuggestionsClearRequested のタイミングで action を dispatch することになる。

Numeral.jsのlocaleを変更して桁区切り文字をカンマからピリオドにする

日本だと桁区切り文字は,(カンマ)、小数点は. (ピリオド)だけど世界的に見ると国によって異なる。
世界各国での数字の区切り方 | コリス

JavaScript で数値を扱うときには Numeral.js を使っていたんだけど、Numeral でも(Moment.js などと同じく)locale という概念があり、これを使うと桁区切り文字と小数点を変更することができる。

1.x 系と 2.x 系でやり方がだいぶ変わっていたので、メモ。


v1.5.6

公式ドキュメント は 2.x 系の話しかないので、 https://github.com/adamwdraper/Numeral-js/tree/1.5.6 の README や テストメソッド を参考にする。
locale() ではなく language() らしい。

import numeral from 'numeral';
import it from 'numeral/languages/it';

// locale のセット。必要なものを import して `language` の第二引数に指定する必要がある
numeral.language('it', it);
// 実際の locale の変更のためにもう一度実行
numeral.language('it');

numeral.language(); // 'it'

console.log(numeral(1234.567).format('0,0.0000')); // 1.234,5670

numeral.reset();
numeral.language(); // 'en'

console.log(numeral(1234.567).format('0,0.0000')); // 1,234.5670


v2.0.6

2.x 系だとこんな感じになる。

import numeral from 'numeral';
import 'numeral/locales/it';

// numeral.register() は不要

// locale のセット
numeral.locale('it');
console.log(numeral.locale()); // 'it'

console.log(numeral(1234.567).format('0,0.0000')); // 1.234,5670

numeral.reset();
console.log(numeral.locale()); // 'en'

console.log(numeral(1234.567).format('0,0.0000')); // 1,234.5670

いくつか補足すると

1.x -> 2.x での変更

2.0.0 のときの Changelog を参考にすると、locale まわりに関係のある変更は

Breaking change: All language now renamed to locale and standardized to all lowercase filenames
Breaking change: The locale function no longer loads locales, it only sets the current locale

ということで language -> locale へ変更になった。


numeral.register()

最初、ドキュメントの Locales の項 を読むと
locale の load には numeral.register() を使えと書いてある。

// load a locale
numeral.register('locale', 'fr', {
    delimiters: {
        thousands: ' ',
        decimal: ','
    },
    abbreviations: {
        thousand: 'k',
        million: 'm',
        billion: 'b',
        trillion: 't'
    },
    ordinal : function (number) {
        return number === 1 ? 'er' : 'ème';
    },
    currency: {
        symbol: '€'
    }
});

// switch between locales
numeral.locale('fr');

なので、1.x 系と同じように必要な locale ファイルを import したあと、

import it from 'numeral/locales/it';

numeral.register('locale', 'it', it);

で登録する必要があるのかなと思ったんだけど、違った。

TypeError: it locale already registered.

というエラーが出る。

これは、実際の locales/xx.js ファイル
https://github.com/adamwdraper/Numeral-js/blob/master/locales/it.js

を見るとよくわかるんだけど、読み込むと自動的に register() されるみたい。

なので、用意されている locale を使わずに自分で定義する場合のみ register() を使う。


おわりに

最初、format() 文字列の 0,0.00 とかのカンマやピリオドを変更しないといけないのか!と思っていたけど
そんなことなくてよかった。

import numeral from 'numeral';
numeral.register('locale', 'foo', {
  delimiters: {
    thousands: 'あああ',
    decimal: 'えええ'
  },
});
numeral.locale('foo');
console.log(numeral(1234.567).format('0,0.0000')); // 1あああ234えええ5670

作って学ぶRedux Middleware

はじめに

Redux で非同期処理を行うための Middleware である redux-thunk は、わずか 10 数行のこのようなコードです。

https://github.com/gaearon/redux-thunk/blob/v2.2.0/src/index.js

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

最初にこれを見たとき、「なんでこんなにアロー関数が連続してるんだろう?」とか「 next ってなんだ?」とかさっぱりわかりませんでした。

というわけで公式のドキュメント

Redux Fundamentals, Part 4: Store | Redux

を読んでみたところ、Step by Step で Middleware の概念を説明していてわかりやすかったので、その流れにそって Middleware とは何かを自分用にメモしておきます。

コード

https://github.com/zaki-yama/learn-redux-middleware

公式ドキュメントを写経しただけですが、コードはこちらに置いてあります。
いちおう各章に対応したタグもあります。


Problem: Logging

ここでは redux-logger のような logging 機能の実装を例にして、Middleware の仕組みを学んでいきます。
すなわち、ある action が dispatch されたとき、action の内容と、それによって変更された後の state の情報を console に出力するような機能です。


Attempt #1: Logging Manually

最も単純な実装は、 dispatch(action) のたびに手動で console.log を仕込むことでしょう。

(source)

let action = addTodo('Use Redux')

console.log('dispatching', action)
store.dispatch(action)
console.log('next state', store.getState())

当たり前ですがこれを毎回やるのはつらいです。


Attempt #2: Wrapping Dispatch

次に、logging まで含めた一連の処理を wrap した関数を作るというのはどうでしょうか。

(source)

function dispatchAndLog(store, action) {
  console.log('dispatching', action);
  store.dispatch(action);
  console.log('next state', store.getState());
}

dispatchAndLog(store, addTodo('Use Redux'));

先ほどよりはマシになりましたが、dispatch する箇所すべてでこの関数を import してやる必要があります。


Attempt #3: Monkeypatching Dispatch

store の dispatch 関数を別の関数で置き換えてはどうでしょうか?いわゆる monkeypatch です。
store はメソッドがいくつか定義された plain object なので可能です。

(source

let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action);
  let result = next(action);
  console.log('next state', store.getState());
  return result;
}

store.dispatch(addTodo('Use Redux'));

next が登場しました。元の dispatch を指すようですね。
dispatch そのものの定義を書き換えたので、以後どこで dispatch が呼ばれても logging されることになります。


Problem: Crash Reporting

先ほどの monkeypatch でうまくいったように見えましたが、logging と同じようなことを 2 つ以上やろうとするとどうなるでしょうか。
ドキュメントで挙げている例としては、Sentry というエラーレポートサービスへの Crash Reporting です。

こんな感じで patch 用関数を logging と crash reporting それぞれで用意し、順番に store に適用する必要があります。

(source)

function patchStoreToAddLogging(store) {
  let next = store.dispatch;
  store.dispatch = function dispatchAndLog(action) {
    console.log('dispatching', action);
    let result = next(action);
    console.log('next state', store.getState());
    return result;
  }
}

// Dummy
const Raven = {
  captureException: function(err, optional) {
    console.error(err);
    console.log(optional);
  },
};

function patchStoreToAddCrashReporting(store) {
  let next = store.dispatch;
  store.dispatch = function dispatchAndReportErrors(action) {
    try {
      return next(action);
    } catch (err) {
      console.error('Caught an exception!', err);
      Raven.captureException(err, {
        extra: {
          action,
          state: store.getState(),
        },
      });
      throw err;
    }
  }
}

patchStoreToAddLogging(store);
patchStoreToAddCrashReporting(store);

store.dispatch(addTodo('Use Redux'));

NOTE:Raven というのが Sentry というサービスを使う際に利用するオブジェクトのようですが、サンプルなので適当なダミーオブジェクトとしています

patch 用関数を順番に store に適用しないといけないあたりがいまいちです。


Attempt #4: Hiding Monkeypatching

メソッドを好きなように置き換えてしまう monkeypatch はハックのようなもので、置換するかわりに新しい dispatch 関数を返すようにしてはどうでしょうか。

(source)

function logger(store) {
  let next = store.dispatch;

  // Previously:
  // store.dispatch = function dispatchAndLog(action) {

  return function dispatchAndLog(action) {
    console.log('dispatching', action);
    let result = next(action);
    console.log('next state', store.getState());
    return result;
  }
}

そして、実際に monkeypatch する部分をこのように定義します。

function applyMiddlewareByMonkeypatching(store, middlewares) {
  middlewares = middlewares.slice();
  middlewares.reverse();

  middlewares.forEach(middleware => {
    store.dispatch = middleware(store);
  });
}

applyMiddlewareByMonkeypatching(store, [logger, crashReporter]);

middlewares.forEach のところで順番に middleware を適用しており、これが結果的に順番に dispatch を置換するような処理になっています。
うまくまとまってきましたが、monkeypatch していることには変わりません。


Attempt #5: Removing Monkeypatching

そもそもどうして dispatch を overwrite しないといけなかったかというと、(middleware を適用した) dispatch を後で実行するからというのももちろんありますが、重要なのは「すべての middleware は直前の middleware が適用された後の dispatch を参照できる」必要があったからでした。

※訳が怪しい。原文は

Why do we even overwrite dispatch? Of course, to be able to call it later, but there's also another reason: so that every middleware can access (and call) the previously wrapped store.dispatch:

つまり、先ほどの

function logger(store) {

  let next = store.dispatch;
  ...

store.dispatch は、一番最初の dispatch ではなく、直前の middleware が適用された結果になっていないと chaining が成立しません。

が、dispatch を置き換える方法以外にもこれを実現する手段はあって、それは dispatch を引数として受け取るようにしてしまうというものです。

function logger(store) {
  return function wrapDispatchToAddLogging(next) {
    return function dispatchAndLog(action) {
      console.log('dispatching', action)
      let result = next(action)
      console.log('next state', store.getState())
      return result
    }
  }
}

そんでもってこれは ES2015 のアロー関数を使うとこのように書けます。

(source)

const logger = store => next => action => {
  console.log('dispatching', action);
  let result = next(action);
  console.log('next state', store.getState());
  return result;
}

最初に見た redux-thunk の実装とほとんど同じになりました。

ここまでで middelware は next という dispatch 関数を取り、新しい dispatch 関数を返す関数になりました。
store は別に使ってないので一番外側の関数はいらないんじゃないか?と思いますが
getState() を使って state 全体にアクセスできた方が色々と都合がいいのでそうなっているそうです。


Attempt #6: Naïvely Applying the Middleware

Attempt #4 で実装した applyMiddlewareByMonkeypatching() のかわりに、monkeypatch を排除した middleware を適用するための applyMiddleware() を書いてみます。

(source)

function applyMiddleware(store, middlewares) {
  middlewares = middlewares.slice();
  middlewares.reverse();

  let dispatch = store.dispatch;
  middlewares.forEach((middleware) => {
    dispatch = middleware(store)(dispatch);
  });

  return Object.assign({}, store, { dispatch });
}

const storeWithMiddlewares = applyMiddleware(store, [logger, crashReporter]);

storeWithMiddlewares.dispatch(addTodo('Use Redux'));

middleware を順次適用している部分の書き方が変わりました。

NOTE:ドキュメントには書いてませんが、この時点で実際の動きを確かめる場合は applyMiddleware() が新しい store を返していることに注意です。↑の例では storeWithMiddlewares に格納しています。

Redux の実際の applyMiddleware() も似たような実装ですが、以下の3点が異なります。

1)middleware の引数になっているのは実際には store そのものではなく、store の API の一部である dispatch(action)getState() です。

NOTE:なので、redux-thunk の一番外側は
return ({ dispatch, getState }) => next => action => { となっていますね。

2)実際の applyMiddleware() では、middleware 内で next(action) ではなく store.dispatch(action) が呼ばれたときも、それまでの middleware が適用された dispatch が実行されるような考慮がされています。

3)store に対して middleware の適用が1回きりになるよう、実際の applyMiddleware() は store そのものに対してではなく createStore() を扱う関数になっています。
つまり

(store, middlewares) => store

(store と middlewares を受け取り、新しい store を返す関数)

ではなく

(...middlewares) => (createStore) => createStore

(middlwares を受け取り、「createStore() 関数を受け取ると middleware が適用された後の createStore() を返す」関数を返す関数)

です。

ただ、実際には使う前に createStore() に関数を適用するのは面倒なので、
createStore() の最後の optional な引数として applyMiddleware() を受け取れるようになっています。

const store = createStore(
  todoApp,
  applyMiddleware(logger, crashReporter), // createStore() 時に middleware を適用してる
);

NOTE:このへんいまいち理解できてませんが


The Final Approach

というわけで最終的にはこのようになりました。

(source)

// middleware
const logger = store => next => action => {
  console.log('dispatching', action);
  let result = next(action);
  console.log('next state', store.getState());
  return result;
}
// crashReporter は省略
import { createStore, applyMiddleware } from 'redux';
import { addTodo } from './actions';
import todoApp from './reducers';

const store = createStore(
  todoApp,
  applyMiddleware(logger, crashReporter),
);

store.dispatch(addTodo('Use Redux'));

NOTE:Attempt #6 で「1)実際の applyMiddleware() は store そのものではなく dispatchgetState() を受け取る」とあるのにこれでうまく動いているのはなぜ?と思いましたが
インターフェースが store => next => ... でも ({ dispatch, getState }) => next => ... でも
一番外側は object を受け取ってることには変わりないですね。


おわりに

というわけで順を追っていくとなんとなく middleware の仕組みや middleware 関数の読み方が理解できました。

  • middleware は dispatch 関数を拡張する役割
  • next は元の dispatch 関数

ということだけ覚えておいて、あとは「こういう書き方をするんだなー」ぐらいでもいいかなと思いました。

ちなみに公式ドキュメントの最後には Seven Examples という章があり、そこで logger や crashReporter 以外のサンプルも載っているので参考になります。