Edited at
NextremerDay 12

Redux用のnpmモジュールを作る

More than 1 year has passed since last update.

これは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としてビルドするための最小構成はこんな感じかと思います。


webpack.config.js

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モジュールとしてビルドするために、まずは以下の設定を追加しましょう。


webpack.config.js

  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に以下の通り設定を追加しましょう。


webpack.config.js

const nodeExternals = require('webpack-node-externals'); // ★ ココ

module.exports = {
target: 'node', // ★ ココ
:
:
externals: [ nodeExternals() ] // ★ ココ
};



package.jsonの設定

package.jsonにもnpmモジュールとしてビルドするための設定をしましょう。といっても、パッケージ名のnameと、インポート対象のファイルとなるmainが設定されていれば大丈夫なようです。


package.json

{

"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


actions.js

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


reducer.js

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


sagas.js

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


component/Count.js

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ファイル


index.js

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モジュールをローカルの別プロジェクトにインストールして使うことが出来ます。(厳密にはシンボリックリンク貼ってるだけなのでインストールってわけじゃないですが)


  1. npmモジュール側でnpm link

  2. npmモジュールを使うプロジェクトでnpm link hoge-modules

を実行すれば1で作ったnpmモジュールを2のプロジェクトで利用することが出来ます。実体がシンボリックリンクなので、npmモジュールを修正して再ビルドすれば即座にリンクしているプロジェクトに反映されるのがいいところですね(・∀・)

ということで、適当なReduxプロジェクトを作って↑で作成したタイマーモジュールを使ってみましょう。


 実際に使ってみる

今回のnpmモジュールでは、Action, Component, Reducer, Sagaの4つが提供されています。

Action, Componentはそのまま使いたい場所で使いたいときに使えばいいですが、Reducer, Sagaはそれぞれ、combineReducersrootSagaに登録してやる必要があります。


reducers/index.js

import { combineReducers } from 'redux';

:
import { timerModuleReducer } from 'timer-module';

export default combineReducers({
:
timerModule: timerModuleReducer,
});



sagas/index.js

import { fork } from 'redux-saga/effects';

:
import { timerModuleRootSaga } from 'timer-module';

export default function* rootSaga() {
:
yield fork( timerModuleRootSaga );
}


こんな感じですかね。

このように、Reduxプラグインの中には、事前にReducerやSagaを登録してやる必要があるものがあります。

後はAction, Componentを適宜呼び出してやればOKです。


components/App.js

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秒ごとにカウンタがインクリメントされるタイマが完成しました。

タイマ.gif


まとめ

Reduxでnpmモジュールを作成しました。

見てもらえば分かるとおり、単純なもの(React Componentにちょっとした非同期処理を付け加えたぐらい)であれば通常のReduxアプリケーションを作成するのとほぼ同じ内容でnpmモジュールを開発できます。(気をつけるのはwebpackpackage.jsonの設定ぐらいですかね。)

なんか色々もっといいやり方あるかもしれないので、また見つけたら報告します。

以上