Help us understand the problem. What is going on with this article?

オレオレルーティングライブラリ「minutemen」

More than 3 years have passed since last update.

作ってみました。
と言ってもクエリパラメータやハッシュに対応してなかったり、ページ遷移時のフック機能がなかったりと
ライブラリとしてはかなりチャチなので、実用に耐えうるものではありません。
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で公開したいなと思っています。
コードが汚いのでリファクタをやりつつですが。。。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away