• このエントリーをはてなブックマークに追加

はじめに

<head>
  <script type-"text/javascript" src="jquery-1.9.1.min.js"></script>
  <script type="text/javascript" src="router.js"></script>
  <script type="text/javascript" src="myapp.js"></script>
</head>
<body>
  <div id="my-simple-app">
    <ul>
      <li><a href="#/hello">hello</a></li>
      <li><a href="#/show/123">hello guest</a></li>
      <li><a href="#/show/456">hello guest</a></li>
    </ul>
  </div>
</body>

こんな HTML において,<a> 要素をクリックしたときに,その属性 href="#..." にしたがって挙動を定義したい.そんな要件.

大規模なアプリケーションに縁のない私は,数ある JavaScript フレームワークを利用する機会もない(勉強してないだけ)ので,オレオレな「ルータ」クラスを定義して,「クリックされた要素の URL がこれの場合はこの関数を呼ぶ」といった定義を集約するようなことをすることで,小規模ながら少しはお仕事を省力できているかもしれない今日このごろです.

ちなみに,Perl の CPAN モジュール Router::Simple のインタフェイスに似せているかもしれません.

SYNOPSIS

# ルーティングを定義する
router = new Router()
router.connect( '/hello', -> alert('hello') )
router.connect( '/show/([0-9]+)', _handle_show )
 
# ハンドラを定義する
_handle_show = ( [id] ) ->
    $a = @
    console.log "href: #{ $a.attr('href') }"
    console.log "shows id:#{id}"
 
# イベントリスナを張る
$(...).on( 'click', 'a[href^="#"]', (ev) ->
    $a = $(ev.target)
    router.match( $a.attr('href') )($a)
    return false
)
 
# 直接呼んだり
setTimeout(
    ->
        router.match( '/hello' )()
    10 * 1000
)

実装例

冒頭の HTML における myapp.js に対応する coffeescript,例えばこんな感じで書けます:

# myapp.coffee
class MyApp
    @init: ->
        @$ = $('#my-simple-app')
        @setup_router()
        @listen()
 
    @setup_router: ->
        self = @
        @_router = new Router
            connect:
                '/hello': -> self._handle_hello()
                '/show/([0-9]+)': (m, ev) -> self._handle_show(m, ev)
 
    @listen: ->
        @$.on( 'click', 'a[herf^="#"]', (ev) =>
            $a = $(ev.target)
            href = $a.attr('href')
            @_router.match( href, ev )($a)
            return false
        )
 
    @_handle_hello: ->
        alert 'hello'
 
    @_handle_show: ([id], ev): ->
        alert "shows id:#{id}"
 
$ ->
    app = MyApp.init()

こんな感じに書いておけば,HTML 上に href="#..." な <a> 要素が増えた場合,

  1. ルータにルールを追加する
  2. 対応するハンドラを追加する

といったステップを踏むだけで拡張できたりします.

ルータクラスのコード

router.js に対応する coffeescript. Router クラスの定義のみ:

# router.coffee
class @Router
    constructor: (params = {}) ->
        @_rules = []
        for k, v of params.connect ? {}
            @connect(k, v)
 
    connect: (hash, action) ->
        @_rules.push( hash: hash, action: (action ? ->) )
        return @
 
    match: (url_hash, ev) ->
        ret = null
        url_hash = url_hash.replace( /^.*\#!?/, '' )  # '#' or '#!' を含む以前を削除
 
        for r in @_rules
            {hash: h, action: a} = r
            m = url_hash.match( @_build_regexp(h) )
 
            if m?
                ret = (o) ->
                    m.shift()
                    return a.apply(o, [m, ev])
                break
 
        if !ret?
            ret = (o) =>
                return @_cannot_route.apply(o, [url_hash, ev])
 
        return ret
 
    _build_regexp: (h) ->
        return new RegExp( ("^#{h}$").replace(/^\^+/, '^').replace(/\$+$/, '$') )
 
    _cannot_route: (h, ev) ->
        throw new Error('cannot route: ' + h)

おわりに

以上,大規模なお仕事に触れることのない私が,オレオレな「ルータ」クラスを定義してイベントハンドリングまわりのルールを集約することで,小規模なお仕事の開発やメンテナンスの手間を少しだけ省力しているかもしれないというお話でした.