LoginSignup
4
1

More than 5 years have passed since last update.

Hyperapp V2 のルーターを自作する

Posted at

Hyperapp V2を使いたいけど公式ルーターがない!でも欲しい!!ということで自作しました。TypeScriptで書いています。型定義はこちら

サンプル

JavaScriptにしたサンプル
https://codesandbox.io/embed/zkml2q59j3

router.ts

import { h, SubscriptionEffectRunner, SubscriptionEffect, VNode, DispatchType, Effect, Action } from "hyperapp";

export interface RouterProps {
    routes: Route[],
    matched: (route: Route | undefined, dispatch: DispatchType<any, any, any>) => void,
}

export interface Route {
    path: string;
    view: (state: any) => VNode;
}

const routerRunner: SubscriptionEffectRunner<RouterProps> = (props, dispatch) => {
    console.log('routing');
    function onLocationChanged() {
        console.log(window.location.pathname);
        for (const r of props.routes) {
            if (r.path === window.location.pathname) {
                props.matched(r, dispatch);
                return;
            }
        }
        props.matched(undefined, dispatch);
    }

    const push = window.history.pushState;
    const replace = window.history.replaceState;
    window.history.pushState = function (data, title, url) {
        push.call(this, data, title, url);
        onLocationChanged();
    }
    window.history.replaceState = function (data, title, url) {
        replace.call(this, data, title, url);
        onLocationChanged();
    }
    window.addEventListener("popstate", onLocationChanged);

    onLocationChanged();

    return () => {
        console.log('unrouting');
        window.history.pushState = push;
        window.history.replaceState = replace;
        window.removeEventListener("popstate", onLocationChanged);
    };
}

export const createRouter: SubscriptionEffect<RouterProps> = props => ({
    effect: routerRunner,
    ...props,
});

export const pushHistory: Effect<{ pathname: string }> = props => ({
    effect: (props, dispatch) => {
        window.history.pushState(null, '', props.pathname);
    },
    ...props,
});

export interface LinkProps {
    to: string;
}

export function Link(props: LinkProps, children: any) {
    return h('a', { onClick: [MoveTo, props.to], href: props.to }, children);
}

const MoveTo: Action<any, string, Event> = (state, to, ev) => {
    ev.preventDefault();
    return [state, pushHistory({ pathname: to })]
}

index.tsx

import { h, app, DispatchableType, Action } from "hyperapp";
import { createRouter, Route, pushHistory, Link } from './router';

function act<S extends object, P, D>(value: DispatchableType<S, P, D>) {
  return value;
}

const mainState = {
  count: 0,
  route: undefined as Route | undefined,
};

type MainState = typeof mainState;
type MainAction<P = {}, D = {}> = Action<MainState, P, D>;

const MoveTo: MainAction<{ pathname: string }> = (state, props) => [state, pushHistory({ pathname: props.pathname })];

const SetCount: MainAction<{ count: number }> = (state, props) => ({ ...state, count: props.count });
const SetRoute: MainAction<{ route: Route }> = (state, props) => ({ ...state, route: props.route });

const router = createRouter({
  routes: [{
    path: '/',
    view: (state: MainState) => <div>home</div>
  }, {
    path: '/abc',
    view: (state: MainState) => (
      <div>
        <button onClick={act([SetCount, { count: state.count + 1 }])}>increment</button>
        <div>count: {state.count}</div>
      </div>
    )
  }, {
    path: '/xyz',
    view: (state: MainState) => <div>xyz</div>
  }],
  matched: (route, dispatch) => dispatch([SetRoute, { route: route }]),
});

app({
  init: mainState,
  view: state => (
    <div>
      <ul>
        <li><Link to="/">home</Link></li>
        <li><Link to="/abc">abc</Link></li>
        <li><Link to="/xyz">xyz</Link></li>
        <li><Link to="/unknown">unknown</Link></li>
      </ul>
      <button onClick={act([MoveTo, { pathname: '/' }])}>home</button>
      <button onClick={act([MoveTo, { pathname: '/abc' }])}>abc</button>
      <button onClick={act([MoveTo, { pathname: '/xyz' }])}>xyz</button>
      <button onClick={act([MoveTo, { pathname: '/unknown' }])}>unknown</button>
      {state.route ? state.route.view(state) : <div>404</div>}
    </div>
  ),
  subscriptions: state => router,
  container: document.body
});

説明

ルーターをimportします。

import { createRouter, Route, pushHistory, Link } from './router';

StateにRoute型のrouteを作っておきます。

const mainState = {
  count: 0,
  route: undefined as Route | undefined,
};

createRouterはルーター機能を提供するSubscriptionです。

routesプロパティに、Route型の配列を設定します。Routeは、URLのlocation.pathnameに一致するpathと、VDOM作成関数viewを持ちます。

matchedプロパティに、現在のURLが変化したときに呼び出される関数を設定します。第1引数にroutesで指定したpathと現在のURLが一致したRoute(一致しない場合はundefined)が、第2引数にdispatchが渡されます。

const router = createRouter({
  routes: [{
    path: '/',
    view: (state: MainState) => <div>home</div>
  }, {
    path: '/abc',
    view: (state: MainState) => (
      <div>
        <button onClick={act([SetCount, { count: state.count + 1 }])}>increment</button>
        <div>count: {state.count}</div>
      </div>
    )
  }, {
    path: '/xyz',
    view: (state: MainState) => <div>xyz</div>
  }],
  matched: (route, dispatch) => dispatch([SetRoute, { route: route }]),
});

SetRoutematchedで与えられるrouteをStateに設定するActionです。

const SetRoute: MainAction<{ route: Route }> = (state, props) => ({ ...state, route: props.route });

appviewでStateに設定されたrouteviewを呼び出します。URLがroutepathに一致しない場合はStateのrouteundefinedになるので、404ページなどの表示に使えます。

{state.route ? state.route.view(state) : <div>404</div>}

Linkでリンク(a要素)を生成します。

<ul>
  <li><Link to="/">home</Link></li>
  <li><Link to="/abc">abc</Link></li>
  <li><Link to="/xyz">xyz</Link></li>
  <li><Link to="/unknown">unknown</Link></li>
</ul>

pushHistoryhistory.pushStateを実行するEffectです。Actionで使えます。

const MoveTo: MainAction<{ pathname: string }> = (state, props) => [state, pushHistory({ pathname: props.pathname })];

<button onClick={[MoveTo, { pathname: '/abc' }]}>abc</button>

createRouterで作成したSubscriptionを、subscriptionsで返して完了です。

app({
  subscriptions: state => router
});

まとめ

そこそこシンプルなルーターができました。pathのパターンマッチング機能はありませんが、必要最低限のルーターということでまあまあ使えそうです。
Hyperappのモジュール仕様が決まれば公式ルーターも作られることでしょう。:bow:

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1