dackdive's blog

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

follow us in feedly

IDDD本もくもく読書会メモ#2(第3章 コンテキストマップ)

第1回 に続いて第2回も無事に開催することができました。

※社外からの参加者もお待ちしています(Slack グループ

教材

実践ドメイン駆動設計

実践ドメイン駆動設計

書籍に加え、前回見つけた CodeZine の解説記事

今回は書籍の前に目を通したが、最初に概要を掴んでから書籍に入れるので効果的。今後もこの進め方でいきたい。

第2回で読んだ範囲

第3章を一通り。この章はボリュームが小さくて助かった。


学習メモ

第2章で「境界づけられたコンテキスト」を学んだが、「コンテキストマップ」は複数の「境界づけられたコンテキスト」の関係を俯瞰する図となる。
抽出したコンテキストを線で結び、互いの関係性を整理していくイメージ。

f:id:dackdive:20170717225955p:plain:w320

(図は書籍より引用)

2 つのコンテキスト間にはどちらかが上流(Upstream)でどちらかが下流(Downstream)という関係がある。図中には U または D で書く。

プロジェクトの現状を示す図。こうあって欲しいという将来の図ではない。

コンテキストマップを書く理由

http://codezine.jp/article/detail/9837 より引用。

コンテキストマップを描くことによって、システム間の関係を適切に把握できるメリットがあります。DDDチームは既存システムとの連携方法を把握でき、他チームとのコミュケーションの必要性を判断できるようになります。

 コンテキストマップはアーキテクチャ図というよりも、チーム間のコミュケーション関係を示す図の意味合いが強くなります。コンテキストマップは組織間の問題を見つけ出せる唯一のドキュメントとなるため、プロジェクトの成功に不可欠といわれています。

コンテキストマップの分類

境界づけられたコンテキスト間の関係には、いくつか名前の付いた種類がある。
これは CodeZine で解説されているように、まず大分類として

  1. チーム間の関係を示す「組織パターン」
  2. データとプログラムの連携方法を示す「統合パターン」

に分けて考えると理解しやすい。(書籍では全部いっしょくたにしているが)

  1. チーム間の関係を示す「組織パターン」
    • パートナーシップ
      • 2つのコンテキストを担当するチームが協力的な関係にある
    • 別々の道
      • コンテキスト間で統合を行わない
    • 順応者
      • 上流側が下流側の要求に応える必要がない
      • 例:TwitterGitHub などの API を使った場合
    • 顧客/供給者
      • 上流のチームが成功するかどうかが下流の結果に左右されうるという場合、上流(供給者)は下流(顧客)のニーズに対応する必要がある
      • 例:モバイルアプリ開発における、API 開発チーム(上流)とアプリ開発チーム(下流
  2. データとプログラムの連携方法を示す「統合パターン」
    • 共有カーネル
      • 複数ドメインにおいて共有が必要な部分に、共通で使用するドメインモデルを構築してソースコードレベルで共有する
      • 共有カーネルに変更が必要な場合は他のチームの承認が必要になるため、この部分は極力小さくする
    • 巨大な泥団子
      • (既存システムなど)大規模で複雑なものを、そのまま(適切なモデルに分割せず)大きな塊として捉えること
    • 公開ホストサービス(OHS:Open Host Service)
    • 公表された言語(PL:Published Language)
      • 2 つの境界づけられたコンテキスト内にあるモデル同士で変換するための、共通の言語
      • OHS と組み合わせて使うことが一般的
      • 例:JSON
    • 腐敗防止層(ACL:Anti Corruption Layer)
      • 下流側が上流側の機能を自コンテキストのドメインモデルに変換するレイヤ
      • 上流側と協力関係が築けなかった場合に、上流側に振り回されないように設ける変換層
コンテキストの分析と統合ポイント

境界づけられたコンテキストが適切に分割されているかを分析し、複数の概念が 1 つのコンテキストの中に混じっていた場合は Brandolini の記法では三角の警告アイコンを記載する。

f:id:dackdive:20170717235155p:plain:w320

(図は書籍より引用)


ディスカッションメモ

  • OHS と PL のどちらか一方だけしか使われないことってあるんだろうか

→ あんまりなさそう

  • 競合ポイントはどうやって見つけるのか

書籍に出てきた図が(ひょうたん型になっていて)恣意的な感じが…

  • リモートモデル、ローカルモデルとかのくだりがよくわからない

自立性を確保するには、依存するオブジェクトの状態をローカルシステム側に保持しておけばいい。依存するオブジェクト全体をキャッシュしておけばいいと考える人もいるかもしれない。しかし、DDDでは、この考え方は一般的ではない。その代わりに、ローカルのドメインオブジェクトを作って外部のモデルをそれに変換し、ローカルのモデルに必要な最小限の状態だけを保持する。

→ たとえば、GitHub API を使って Issue 管理アプリを作るとき、

  • API で Issue を GET した結果のプロパティをアプリ内で全部保持するわけはなく、実際には必要なものだけ保持するはず
  • API に仕様変更があったときにも影響を最小限にするため、レスポンスをそのまま使うのではなく何らかのオブジェクトに変換して使うはず(ACL にもなる)

というわけで、API のレスポンスを加工してアプリ内で使うためのモデルに変換する、というのはここに記載されたことそのものズバリな気がする。


次回

7/21(金)19:00 頃からやります。


資料

見つけたものをどんどん追加していきます。

"Redux Architecture Guidelines"を読んだ

こちらの記事を読んで。React&Redux でアプリを作る上で基本的だが重要なことが書かれていたので、後で見返すためにメモ。

なお本文は原文記事の完全な翻訳ではなくポイントだけかいつまんで自分の考えも交えたものです。
また、まとめさせていただく上で著者の許諾を得ています。
(拙い英語でおそるおそる訪ねたら流暢な日本語で快諾いただいてびっくり)


State

state の形を計画(設計)する(Plan your state shape

state オブジェクトの構造をきちんと設計することは非常に重要。
こういった構造にすればいい!という唯一解はないが、設計の手助けとなるチェックリスト(質問リスト)がこちら。

  • users, accounts, items など、API 経由で取得した複数のリソースか(配列かどうか、の意?)
  • ローディングアイコンを出す/出さないなど、loading state を扱うか
  • 成功またはエラー時の通知 UI を扱うか
  • 一貫性がありかつ予測しやすいか。別のチームメンバーがすぐに理解できるか
  • 必要なデータにアクセスしやすいか。不必要にネストしていないか
  • serializable か。ローカルストレージやデータベースに簡単に保存できるようになっているか
  • state のかわりに URL でアクセスできるプロパティはないか
    • URL でアクセスできる = state に保存する必要はない?
  • 重複したデータはないか


state の過剰なネストは避ける(Avoid nesting state objects

こういった state 構造になっていることがたまにある。

{
  foo: {
    bar: {
      baz: {
        qux: ...
      }
    }
  }
}

リレーショナルデータをそのまま state に突っ込むとこういった深いネスト構造になりがちだが、深いネストはそれだけ複雑さを上げる要因となる。

  • コンポーネントレベルでは、欲しい情報を得るためにネストをたどる必要がある
    • これは適宜 container component にすることでいくらか解決しそうだけど
  • reducer レベルでは、state 更新の際のマージ作業が複雑になる
  • 加えてパフォーマンスにも影響する

Normalizing State Shape · Redux
記事中でもリンク貼られてた Redux の公式ドキュメント。知らなかった。

これは今読んでる The Complete Redux Book にも書いてあった。


raw data だけ state に保存する(Storing only raw data in the state

記事中では Redux アプリにおける data には2種類あるとしていて、

  1. raw data:アプリが必要とするデータ。API 叩いて fetch したデータなど
  2. derived data:raw data から計算することで得られるデータ
    • firstNamelastName からユーザーの Name を得る、のような

後者の derived data は raw data からいつでも導出できるので、わざわざ state に保存する必要はない。
state に何か新しい情報を追加するときは、「これって現在の state の情報から作れない?」というのを自問してみるといいとのこと。


React の state よりも Redux の state を使う(Prefer Redux state over React state

React にも state 管理のしくみはあるが、 state の大部分は Redux の state として持たせた方が一貫性があって良い。特にチーム作業においては。

とはいえ例外として React の state で管理した方がいいケースもある。たとえば、複雑な UI の状態など、アプリ全体には重要でないような state を管理するようなとき。


Actions

action の payload は標準化する(Standardize action payloads

特にチーム開発においては、action の型というものも統一しておいた方がいいよねという話。
私も使っているけど Flux Standard Action に従っておけば大体問題ないんじゃないかな。


Action は composable にする(Ensure action creators are composable

composable は「組み立て可能な」ぐらいの意味?
既存の Action を組み合わせたより複雑な Action を作るというケースが実際のプロダクトだとよくあり、そのために Action のインターフェースを統一しておくと良い。
著者は全部 Promise でラップするようにしていて、そうすると全ての Action は then でつなげられる。

redux-thunk 使ってるとそんなに意識することがない?


Component Architecture

Containers & presentational components

コンポーネントの見た目に関する責務を持つ presentational component(純粋な React コンポーネント)と、コンポーネントの振る舞い?に関する責務を持つ container component(connect() したコンポーネント)を明確に分ける。

container と presentational という用語、公式ドキュメントにちゃんと書いてあったの知らなかった。
Usage with React · Redux


コンポーネントツリーの中間でも適切に container を使う(Use intermediary containers

コンポーネントツリーのどこを container にするかはアプリ作ってると悩まされる問題だけど、container にしていいのはアプリの root コンポーネントだけという制約はなくて、むしろそうすると末端のコンポーネントに必要な props を中間のコンポーネントがひたすら “passing through” するだけの無駄が発生する。

こういった不吉な臭いを感じ取ったら適宜 container 化していくといいんじゃないか。

Container にすることによるメリット・デメリットは kuy さんのこの記事が大変わかりやすい。

簡単に言うと connect() した container コンポーネントは Redux に依存することになるので、純粋な UI パーツとして再利用可能なコンポーネントにはならなくなる。そのトレードオフで判断するといい。

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章「ドメイン」「サブドメイン」「境界づけられたコンテキスト」を読み解く (¼):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 行目)を一度削除して再実行する。

webpack3リリース内容まとめ

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) – 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 あたりから既にできていたらしい


参考

より詳しくは公式ドキュメントのここを読んでくれとのこと。
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 にサンプルコードがたくさんあります。


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 することになる。