dackdive's blog

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

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 以外のサンプルも載っているので参考になります。

Salesforce:JavaScript Remoting(@RemoteAction)をPromiseで扱う

はじめに

Visualforce で Angular や React などの JavaScript ライブラリを使ったアプリケーションを作ろうと思った場合、
Apex で定義したメソッドの実行には JavaScript Remoting を使うのが一般的かと思います。

似たようなことを実現するための手段として リモートオブジェクト(Remote Object) というのもありますが、オブジェクトの単純な CRUD 処理以上のことをやろうとすると JavaScript Remoting が必要になります。両者の比較は こちら

JavaScript Remoting は以下のような書き方で、JavaScript から対象の Apex クラスおよびメソッドを指定します。

// 方法1
[namespace.]controller.method(
    [parameters...,]
    callbackFunction,
    [configuration]
);

// 方法2
Visualforce.remoting.Manager.invokeAction(
  '{!$RemoteAction.controller.method}',
  [parameters...,]
  callbackFunction
);

ここで問題点としては、どちらの書き方にも言えることですが、 Apex 側からのレスポンスがきた後の処理をコールバック関数として渡さなければならない点です。

コールバック地獄としてたびたび問題になる話ですが、最近では Promise や async/await 構文といった書き方があるので
今回は Promise でこの部分を書けないか試してみます。


コード

できあがったものを先に置いておきます。
なお、アプリケーション自体は React/Redux で書かれています。


Promise を使わない(= コールバックを使った)書き方

はじめに、Redux で JavaScript Remoting をそのまま使った場合の書き方。

// actions.js
import Remoting from '../service';

export function sayHelloCallback() {
  return (dispatch, getState) => {
    dispatch(loadingStart());
    new Remoting().sayHelloCallback((result) => {
      // 実際にはここでレスポンスの正否を確認する
      console.log(result);
      dispatch(loadingEnd());
    });
  };
}
// service.js
export default class SfRemoting {
  getRemoting() {
    // eslint-disable-next-line
    return __NAMESPACE__RemoteActionController;
  }

  sayHelloCallback(callback) {
    this.getRemoting().sayHello((result, event) => {
      console.log('result:', result);
      console.log('event:', event);
      callback(result);
    });
  }
}

Action と JavaScript Remoting 呼び出しを分離するために service.js というファイルを作っています。
Apex 側には sayHello() という引数を取らない RemoteAction メソッドが定義されているという前提です。

// RemoteActionController.cls
public class RemoteActionController {
    @RemoteAction
    public static String sayHello() {
        return 'Hello, World';
    }
}

また、方法1 の書き方で JavaScript Remoting を実行しようとすると namespace の問題をなんとかしないといけないので
webpack の string-replace-loader で(やや強引に)解決しています。

// webpack.config.js
module.exports = {
  module: {
    loaders: [
      ...
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'string-replace-loader',
        query:{
          search: '__NAMESPACE__',
          replace: process.env.NAMESPACE ? `${process.env.NAMESPACE}.` : '',
        }
      },

ビルド実行時に NAMESPACE=foo というように環境変数を渡したときだけ namespace を付与します。

Action の

new Remoting().sayHelloCallback((result) => {

のところでコールバックによるネストが発生しており、複数の JavaScript Remoting の呼び出しがあるとネストが深くなります。
また並列に実行するような書き方もできません。(私の知る限りですが)


Promise を使った書き方

service.js 内のメソッドを Promise を使った書き方に直してみます。自信はないですがこんな感じになるはず...!

// service.js
export default class SfRemoting {
  execute(methodName, ...args) {
    return new Promise((resolve, reject) => {
      this.getRemoting()[methodName](...args, (result, event) => {
        if (event.status) {
          resolve(result);
        } else {
          console.log('Remote Action error occured:', event);
          reject({ message: event.message, where: event.where });
        }
      });
    });
  }

  sayHelloPromise() {
    return this.execute('sayHello');
  }
}

RemoteAction メソッドのコールを execute メソッドに集約し、その中で Promise を使っています。
また、コールバック関数内でレスポンスの内容を元に resolve() または reject() を実行しています。

このような書き方にすると、Action 側は以下のように書けます。

export function sayHello() {
  return (dispatch, getState) => {
    dispatch(loadingStart());
    new Remoting().sayHelloPromise()
      .then((result) => {
        console.log(result);
        dispatch(loadingEnd());
      })
      .catch((err) => {
        console.error(err.message, err.where);
        dispatch(loadingEnd());
        dispatch(raiseError(err.message, err.where));
      });
  };
}

ネストすることなく後続の処理を書くことができました。
また sayHelloPromise の後に RemoteAction メソッドの呼び出しが続く場合も

.then((result) => {
  return new Remoting().anotherRemoteActionMethod();
})
.then((result) => {
  ...
})

というように then 内で別の RemoteAction メソッドの呼び出しを行うことで、ネストを深くせずにメソッドをチェインすることができます。


補足:エラーハンドリングについて

JavaScript Remoting のレスポンスは第一引数が Apex メソッドの戻り値、第二引数が以下の項目を含む event オブジェクトです。
(参考:リモート応答の処理 | Visualforce 開発者ガイド | Salesforce Developers

項目 説明
event.status 成功のときは true、エラーのときは false になります。
event.type 応答の種別: 成功したコールは rpc、リモートメソッドが例外を返した場合は exception のようになります。
event.message 返されたエラーメッセージが含まれます。
event.where リモートメソッドにより生成された場合は、Apex スタック追跡が含まれます。

そのため、基本的には event.status の値を見て成功/失敗を判断すれば良いことになります。
また event.message には Apex 側で throw した Exception にセットしたメッセージです。

// RemoteActionController.cls
public class RemoteActionController {

    public class MyException extends Exception {}

    @RemoteAction
    public static void sayHelloError() {
        throw new MyException('Something bad happened!');
    }
}

(画面)

f:id:dackdive:20170505211953p:plain:w320

詳細はサンプルコードをご参照下さい。


おまけ

引数の型や数が間違ったときなどに

Visualforce Remoting: Parameter length does not match remote action parameters: expected 1 parameters, got

のようなエラーがブラウザのコンソールに出力されますが、これは補足できないぽい...?

参考:名前空間および JavaScript Remoting | Visualforce 開発者ガイド | Salesforce Developers

invokeAction のコール時に発生したエラーは JavaScript コンソールでのみレポートされます。たとえば、$RemoteAction で複数の名前空間に一致する @RemoteAction メソッドが見つかった場合、最初に一致したメソッドを返し、JavaScript コンソールに警告を記録します。 一致するコントローラまたはアクションが見つからない場合は、そのコールはエラーを表示することなく失敗し、エラーは JavaScript コンソールに記録されます。


リファレンス

Visualforce 開発者ガイドより、

webpack+ES2015でGoogle Apps Scriptローカル開発するためのテンプレート作った

gapps を使って Google Apps Script をローカルで開発するための方法については 以前 Qiita に書いた し、それにより ES2015 や TypeScript で書いて手元でビルド、みたいなことはみんなやるようになったんだけど
webpack を使った構成が探してもあまり見つからなかったので自分用に作った。


特徴

ES2015 で書いて webpack でビルド、ESLint で構文チェック

業務でも使っているスタックで GAS が書けるようになった。 import/export でファイルを適切に分割できてうれしい。
最終的なビルド結果を GAS で使える用にエクスポートする方法がわからなかったんだけど、gas-webpack-plugin というのを使わせていただいた。

// webpack.config.babel.js
import GasPlugin from 'gas-webpack-plugin';

export default {
...
  plugins: [
    new GasPlugin(),
  ]
}

と書き、スクリプト側は

global.hello = function () {
}

とすれば良い。


.env に定義した環境変数を読み込めるように

GAS で何かしたいときって Slack などの外部サービスと連携することが多く、トークンなどの情報をコード中に埋め込むとスクリプトが公開しづらくなるかと思って .env から読み込めるようにした。


watch モードつき

ファイルの変更監視して再ビルド・アップロードをできるように watch コマンドも作っておいた。
https://github.com/mikeal/watch というのがあるらしい。npm scripts だけでいけるんですね。