作ってみました。
と言ってもクエリパラメータやハッシュに対応してなかったり、ページ遷移時のフック機能がなかったりと
ライブラリとしてはかなりチャチなので、実用に耐えうるものではありません。
https://github.com/YutakaHorikawa/minutemen
exampleディレクトリ配下に雑なサンプルアプリケーションが置いてあります。
npm start
でwebサーバーが起動しポート8091をリッスンします。
名前に深い意味はないです。
私の好きなのバンドの一つに同名のバンドがいて
名前考える時にたまたまそれを聴いていたからです。
なぜ作ったのか
生まれて初めて投稿するアドベントカレンダーのネタを考える際、普段利用しているライブラリを自分で書いたら面白そう!という軽い気持ちでルーティングライブラリを書くことに決めました。
業務ではreact-router + react-router-reduxを使用しています。
機能
minutemenが提供する機能は以下の通りです。
- URLパターンから表示するコンポーネントを切り替える
- ルーティング定義
- ルーティング処理を行うmiddleware
- ページ遷移するためのActionCreator
- ロケーション情報をstoreに格納
一つずつ見ていきます。
URLパターンから表示するコンポーネントを切り替える
document.addEventListener("DOMContentLoaded", () => {
const historyWrapper = createHistory();
const store = configureStore(undefined, historyWrapper);
ReactDOM.render(
<Provider store={store}>
<Router historyWrapper={historyWrapper}>
<AppContainer />
<FooContainer />
<BarContainer />
</Router>
</Provider>,
document.getElementById('app')
);
});
minutemenではコンポーネントの切替を行うRouterComponent
を提供しています。
このコンポーネントがsotreに格納されたロケーション情報とルーティング定義の情報をもとに
出力するコンポーネントの出し分けを行います。
RouterComponent
のpropsに渡しているのはヒストリーオブジェクトのラッパーです。
これもminutemenで提供する機能です。
ルーティング定義
ルーティング定義はオブジェクトで行います。
react-routerではjsxで定義しますが、個人的に好きではないため、今回はjsx以外のやり方にしてみました。
routing
に代入しているのが、ルーティング定義です。
keyがURLパターンでvalueが付加情報です。
URLパターンは正規表現の利用が可能です。
const routing = {
'^/$': {
index: 0,
root: true,
component: 'AppContainer'
},
'^/foo$': {
index: 1,
component: 'FooContainer'
},
'^/bar/([0-9]*)/([0-9]*)/$': {
index: 2,
component: 'BarContainer'
}
};
定義したルーティングの情報はcreateSelector
と一緒にミドルウェアを生成する
関数に渡してあげます。
export default function configureStore(initialState, historyWrapper) {
const routing = {
'^/$': {
index: 0,
root: true,
component: 'AppContainer'
},
'^/foo$': {
index: 1,
component: 'FooContainer'
},
'^/bar/([0-9]*)/([0-9]*)/$': {
index: 2,
component: 'BarContainer'
}
};
const minutemen = createMinutemen(createSelector(routing), historyWrapper);
return createStore(
rootReducer,
initialState,
applyMiddleware(
createLogger(),
minutemen
)
);
}
valueに格納されるオブジェクトについて説明します。
{
index: 'RouterComponentのchildrenのindex',
root: 'ルートのコンポーネントか'
component: 'URLの名前(nameというプロパティ名に変更するの忘れてました)'
}
index
URLに対応するコンポーネントの情報を記載します。
ここに記載する情報はRouterComponentのchildrenのindexです。
例えば下記のルーティングとRouterComponentの定義があり、この状態で/
に
アクセスすると表示されるのはAppContainer
となります。
'^/$': {
index: 0,
root: true,
component: 'AppContainer'
}
<Router historyWrapper={historyWrapper}>
<AppContainer />
<FooContainer />
<BarContainer />
</Router>
indexではRouterComponentの何番目の子要素を表示するか。
という情報を記載します。(最高にダサいですね)
root
ルートかどうかをboolで記載します。
デフォルトはfalseです。
component
URLに任意の名前を付けることが可能です。
これを使用すると、ページ遷移の名前からURLを逆引きすることが可能となります。
本当はname
というプロパティにするつもりだったんですが、修正するの忘れてました。
ルーティング処理を行うmiddleware
実態はredux-logicのラッパーです。
actionのpayloadで渡された、URLや名前から表示するべきコンポーネント情報を取得して
actionのpayloadに渡しています。
コンポーネントの情報が取得できなかった場合、そのactionはrejectされstoreの情報が更新されることはありません。
この辺はredux-logicの機能をそのまま利用しています。
validate
でhistoryオブジェクトの操作してしまっているのでそのへんは切り離していきたいです。
TRANSITION_BY_NAME
を発行するとvalidateTransitionByNameが実行されます。
action.payload.nameにはルーティング定義で記載したURL名が渡されます。
selector.getPayloadByName
に名前を渡すと、名前からルーティング情報を逆引きします。
取得したルーティング情報を元に、payloadの変更とhistory.pushStateが呼び出されます。
const validateTransitionByName = createLogic({
type: TRANSITION_BY_NAME,
latest: true,
validate({ getState, action }, allow, reject) {
const { name, params } = action.payload;
const routes = selector.getPayloadByName(name, params, historyWrap.pathname());
if (routes === null || routes.uri === null) {
reject();
} else {
action.payload = {
...action.payload,
...routes
};
if (action.payload.pushState) {
pushState(action, routes.component, routes.uri);
} else {
replaceState(routes.uri);
}
allow(action);
}
}
});
ページ遷移するためのActionCreator
ページ遷移する場合は、専用のActionCreatorをコールします。
URLを指定
import { transitionTo } from 'minutemen';
transitionTo('/foo');
URLの名前で指定
import { transitionByName } from 'minutemen';
transitionByName('FooContainer');
ロケーション情報をstoreに格納
これはreducerを提供しています。
import { combineReducers } from 'redux';
import { minutemenReducer } from 'minutemen';
import app from './app';
const rootReducer = combineReducers({
app,
minutemenReducer(0)
});
export default rootReducer;
最後に
まだまだ機能が足りないですし、イケてない作りですが
せっかく作ったので、ちょっとづつ育てていきnpmで公開したいなと思っています。
コードが汚いのでリファクタをやりつつですが。。。