History API があるんだから、WebComponents ベースで SPA 作ってるときはルーターは自前で書いちゃえばいいんじゃない?
ということで、約100行のシンプルなルーターを書いてみました。
- Lazy Loading
- Inner HTML
にも対応しています
例
index.html
<li route=/>Home</li>
<li route=/img/https%3A%2F%2Fupload.wikimedia.org%2Fwikipedia%2Fcommons%2Fthumb%2Fa%2Fac%2FJordens_inre.jpg%2F230px-Jordens_inre.jpg>Sample image</li>
<jp-route
path=/
title=Home
data="<b>HOME</b>"
spec=html
></jp-route>
<jp-route
path=/img/:src
title=Image
data="img"
spec=tag
></jp-route>
<script type=module src=./jp-router.js></script>
<jp-router></jp-router>
こんな html を書いておいて
-
Home
をクリックしたら
-
Sample image
をクリックしたら
となるようにしたい。またブラウザのバック/フォワードボタンが効くようにしたい。
実装
jp-router.js
const
PathSegs = uri => uri.split( '/' ).filter( $ => $.length )
const
Key = $ => {
const _ = /^:(.+)/.exec( $ )
return _ ? _[ 1 ] : _
}
const
Match = ( route, data ) => {
const rSegs = PathSegs( route )
const dSegs = PathSegs( data )
if ( rSegs.length != dSegs.length ) return null
const _ = rSegs.flatMap( ( $, _ ) => $ == dSegs[ _ ] ? [] : [ [ Key( $ ), decodeURIComponent( dSegs[ _ ] ) ] ] )
return _.every( $ => $[ 0 ] ) ? Object.fromEntries( _ ) : null
}
class
Router extends HTMLElement {
connectedCallback() {
this.UpdateLinks()
window.onpopstate = ev => this.Navigate( location.pathname )
window.onpopstate()
}
disconnectedCallback() {
window.onpopstate = null
}
UpdateLinks() {
document.querySelectorAll( '[route]' ).forEach(
$ => $.onclick = ev => {
const route = $.getAttribute( 'route' )
this.Navigate( route )
history.pushState( null, null, route )
}
)
}
Navigate( url ) {
const routes = Array.from( document.querySelectorAll( 'jp-route' ) ).map(
$ => (
{ path : $.getAttribute( 'path' )
, title : $.getAttribute( 'title' )
, data : $.getAttribute( 'data' )
, spec : $.getAttribute( 'spec' )
}
)
)
routes.forEach( $ => $.params = Match( $.path, url.split( '?' )[ 0 ] ) )
const _ = routes.filter( $ => $.params )
switch ( _.length ) {
case 1:
{ const { path, title, data, spec, params } = _[ 0 ]
while ( this.firstChild ) this.removeChild( this.firstChild )
document.title = title || url
const
AttachView = view => {
for ( let key in params ) view.setAttribute( key, params[ key ] )
this.appendChild( view )
this.UpdateLinks()
}
switch ( spec ) {
case 'html':
this.innerHTML = data
AttachView( this.firstChild )
break
case 'tag':
AttachView( document.createElement( data ) )
break
case 'source':
import( data ).then( $ => AttachView( new $.default() ) )
break
}
}
break
case 0:
document.title = '404'
this.innerHTML = '404 Page not found. URL: ' + url
break
default:
document.title = 'MULTIPLE ROUTE'
this.innerHTML = 'Internal logic error: MULTIPLE ROUTE, see console'
console.error( 'MULTIPLE ROUTE', JSON.stringify( _ ) )
break
}
}
}
customElements.define( 'jp-router', Router )
解説
window
のonpopstate
をトラップします。
Document
中のroute
という属性を持ってるHTML
要素のonclick
を以下のように設定します。
-
route
属性とマッチするpath
属性を持つjp-route
を探し、そのjp-route
の内容を作成して、自分の子エレメントにします。 -
History API
のpushState
を使って、ブラウザの履歴に登録します。(pushState
の2番目のパラメータは履歴に現れるtitle
ですが、null
にしておくと、その時のtitle
を設定してくれます。)
マッチとパラメータ
route属性 | path属性 | マッチ | パラメータ |
---|---|---|---|
/a/b | /a/b/ | true | {} |
/a/b | /a/ | false | - |
/img/:src | /img/sample | true | {src:'sample'} |
パラメータは作成されたHTML
要素の属性としてセットします。
NPM
これで十分って方のために、NPM に登録しておきました。
https://www.npmjs.com/package/@satachito/jp-router
$ npm i @satachito/jp-router --save
node_modules/@satachito/jp-router/demo/index.html
にLazy Loading
など他のパターンも入れてありますので、よかったら参考にしてください。