これはNextremer Advent Calendar 2016の第12日目の記事です。
はじめに
Reduxに限らず、よく使う処理やプラグイン的に追加したい処理なんかを別途npmモジュールとして作りたいというケースは多いと思います。
Reactの場合だと、パラメータや子コンポーネントを受け取るようなコンポーネントを作ってnpmモジュールとして提供すればいいのですが、Reduxの場合はコンポーネント以外にもActionやReducerなどが絡んでくるため、それらを(必要なら)全て提供する必要があります。
今回はそれらを提供する手順をざっとまとめてみます。
※今回の開発環境
- Node.js 6.2.2
- npm 3.10.5
作ってみる
早速作っていきましょう。
下準備
適当な作業ディレクトリを作って、そこで作業します。
npm init
を実行し、適宜情報を入力しましょう。
今回は、以下の構成で作りたいと思うのでそれぞれインストールします。
- Webpack
- Babel(ECMAScript 2015 + Stage 0)
npm i -D webpack babel-core babel-loader babel-polyfill babel-preset-react babel-preset-es2015 babel-preset-stage-0 babel-plugin-transform-decorators-legacy
もちろん、React, Redux(saga, actionsも一緒に)も必要なのでそれもインストール
npm i -S react redux react-redux redux-saga redux-actions
Webpackの準備
おそらく、React-Reduxとしてビルドするための最小構成はこんな感じかと思います。
const webpack = require('webpack');
module.exports = {
entry: [ 'babel-polyfill', __dirname + '/js/index.js' ],
output: {
path: './',
filename: 'index.js',
},
module: {
loaders: [{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel'
}]
},
plugins: [],
externals: []
}
npmモジュールとしてビルドするために、まずは以下の設定を追加しましょう。
output: {
path: './',
filename: 'index.js',
library: 'hoge-module', // ★ ココ
libraryTarget: 'umd' // ★ ココ
},
library
はビルドするnpmモジュール名、libraryTarget
はビルド形式の設定です。
umdはUniversal Module Definitionの略で、大体AMDとかCommonJSで読み込める形式にしてくれる指定らしいです。(詳しくはココを参照)
続いて、npmモジュールとして出力する際に、node_modules
の出力は不要なので、node_modules
以下のファイルを出力しないよう設定しましょう。
webpack-node-externals
というライブラリを使用します。
npm i -D webpack-node-externals
でインストールして、webpack.config.js
に以下の通り設定を追加しましょう。
const nodeExternals = require('webpack-node-externals'); // ★ ココ
module.exports = {
target: 'node', // ★ ココ
:
:
externals: [ nodeExternals() ] // ★ ココ
};
package.jsonの設定
package.json
にもnpmモジュールとしてビルドするための設定をしましょう。といっても、パッケージ名のname
と、インポート対象のファイルとなるmain
が設定されていれば大丈夫なようです。
{
"name": "hoge-module",
"main": "index.js",
}
モジュールの作成
いよいよモジュール本体を作成しましょう。
今回は、Action, Reducer, Component, Sagaの4つを提供するモジュールを作ります。
内容は、、、簡単に 1秒ごとにカウンタの値を増やして表示するだけのモジュールにでもしましょう。
この辺は今回はさっくり飛ばしていきますよ。
ファイル構成
/js
/components
/Count.js
/actions.js
/index.js
/reducers.js
/sagas.js
Action
import { createAction } from 'redux-actions';
export const START_TIMER = 'START_TIMER';
export const startTimer = createAction( START_TIMER );
export const STOP_TIMER = 'STOP_TIMER';
export const stopTimer = createAction( STOP_TIMER );
export const TIMER_EXPIRED = 'TIMER_EXPIRED';
export const timerExpired = createAction( TIMER_EXPIRED );
Reducer
import * as actions from './actions';
export const initial = 0;
const handlers = {
[actions.TIMER_EXPIRED]: ( state, { payload } ) => {
return ++state;
},
};
export default ( state = initial, action ) => {
const handler = handlers[ action.type ];
return !handler ? state : handler( state, action );
}
Saga
import { takeEvery, delay } from 'redux-saga';
import { fork, call, take, put, race, actionChannel } from 'redux-saga/effects';
import * as actions from './actions';
export function* startTimerEventWatcher() {
const channel = yield actionChannel( actions.START_TIMER );
while( yield take( channel ) ) {
while( true ) {
const winner = yield race({
stopped: take( actions.STOP_TIMER ),
expired: call( delay, 1000 )
});
if ( winner.stopped ) {
break;
} else {
yield put( actions.timerExpired() );
}
}
}
}
export function* timerModuleRootSaga() {
yield fork( startTimerEventWatcher );
}
Component
import React, { Component } from 'react';
import { connect } from 'react-redux';
@connect( state => ({
count: state.timerModule,
}), {})
export default class Count extends Component {
render() {
return (
<div>{ `count: ${ this.props.count }` }</div>
);
}
}
最後に↑のを全部まとめるためのindexファイル
import * as actions from './actions';
export { actions }
export { default as timerModuleReducer } from './reducers';
export Count from './components/Count';
export { timerModuleRootSaga } from './sagas';
出来上がったらビルド
ここまでざっと作ったらwebpack
コマンドでビルドしましょう。
ビルドが通ればnpmモジュールの完成です。
使ってみる
さっそく使ってみましょう。って言われてもどうやって使うのん?って話ですよね。
自作のnpmモジュールには3つの使い方があります。(他にもあるかもだけど、知らない('A`)
npmモジュールの使い方
1. npmに登録して使う
汎用的に使えるモジュールが出来たらnpm公式に登録しちゃいましょう。
登録方法はココが詳しいです。
登録が終われば、 npm install hoge-module
のような形でインストールして使うことが出来ます。
2. githubからインストールして使う
他の人の役には立たないかもしれないけど、自分ではよく使う。プライベートなプロジェクトで使うから公開するわけにはいかないけど、プロジェクトではよく使う。などの場合は、githubリポジトリにそのままあげちゃいましょう。
npm install github:<githubリポジトリ名>
でgithubリポジトリからインストールして使えますョ。
3. ローカルのnpmモジュールをローカルのプロジェクトで使う
要するにデバッグです。npm link
というコマンドで、ローカルのnpmモジュールをローカルの別プロジェクトにインストールして使うことが出来ます。(厳密にはシンボリックリンク貼ってるだけなのでインストールってわけじゃないですが)
- npmモジュール側で
npm link
- npmモジュールを使うプロジェクトで
npm link hoge-modules
を実行すれば1で作ったnpmモジュールを2のプロジェクトで利用することが出来ます。実体がシンボリックリンクなので、npmモジュールを修正して再ビルドすれば即座にリンクしているプロジェクトに反映されるのがいいところですね(・∀・)
ということで、適当なReduxプロジェクトを作って↑で作成したタイマーモジュールを使ってみましょう。
## 実際に使ってみる
今回のnpmモジュールでは、Action, Component, Reducer, Sagaの4つが提供されています。
Action, Componentはそのまま使いたい場所で使いたいときに使えばいいですが、Reducer, Sagaはそれぞれ、combineReducers
、rootSaga
に登録してやる必要があります。
import { combineReducers } from 'redux';
:
import { timerModuleReducer } from 'timer-module';
export default combineReducers({
:
timerModule: timerModuleReducer,
});
import { fork } from 'redux-saga/effects';
:
import { timerModuleRootSaga } from 'timer-module';
export default function* rootSaga() {
:
yield fork( timerModuleRootSaga );
}
こんな感じですかね。
このように、Reduxプラグインの中には、事前にReducerやSagaを登録してやる必要があるものがあります。
後はAction, Componentを適宜呼び出してやればOKです。
import { connect } from 'react-redux';
import { Count, actions } from 'timer-module';
@connect( state => ({}), {
start: actions.startTimer,
stop: actions.stopTimer,
})
export default class App extends React.Component {
render() {
return (
<div className="hgoe">
<Count />
<button onClick={ ::this.props.stop }>終了</button>
<button onClick={ ::this.props.start }>開始</button>
</div>
);
}
}
こんな感じですね。これで1秒ごとにカウンタがインクリメントされるタイマが完成しました。
まとめ
Reduxでnpmモジュールを作成しました。
見てもらえば分かるとおり、単純なもの(React Componentにちょっとした非同期処理を付け加えたぐらい)であれば通常のReduxアプリケーションを作成するのとほぼ同じ内容でnpmモジュールを開発できます。(気をつけるのはwebpack
とpackage.json
の設定ぐらいですかね。)
なんか色々もっといいやり方あるかもしれないので、また見つけたら報告します。
以上