Riot.js Advent Calendar 2019 の8日目が空いていたので埋めます。
Riot Router
前回と前々回でRiot Routerについて触れてきましたが、トリガーの部分でRawthが出てきました。
Riot.js v3の時もRouterをカスタマイズして使っていたのですが、このRawthのコードを見ているとRiot Routerを使わずに出来そうだったのでチャレンジしました。
気に入らない点
v3のRouterからどうもしっくりこないのが、ページが複数あるときに全部importやmountをしないといけない点です。
複数人でページを分けて開発していると、これがとてもメンドクサイ。
要件
-
index.js
にはimport
やregister
をたくさん書きたくない。ページが増えても変更したくない。 - URLを解析して最初の
/
までの文字をRiot.jsのモジュール名 (モジュール名.riot) とする。 - タグ名の決まりは特にない。
- モジュール名以降のパスは
/
区切りでパラメータとしてタグに渡す。 - クエリパラメータは使わない。(メンドクサイから)
- モジュールが見つからない場合にはエラーページを出す。
- トップページは
home.riot
を表示する。 - ブラウザバックも使えるようにする。
ポイント
- dynamic imports
- async / await
- history.pushState
作ってみた
index.js
import { component } from 'riot'
// ベースURL
const loc = window.location;
const base = `${ loc.protocol }//${ loc.host }/`;
// 読み込み済みコンポーネント
let comp;
// リンククリック時の動作
const linkclick = e => {
// 左クリック以外は何もしない
if (e.which && event.which !== 1) {
return;
}
// 対象ページ読み込み
route(e.target.href);
// ブラウザでのページ遷移をキャンセル
event.preventDefault();
};
// 動的ページ読み込み
const pageload = async (collection, params) => {
try {
// 動的インポート
const page = (await import(`./${ collection || "home" }.riot`)).default;
// マウント済みの場合はアンマウント
const root = document.getElementById('root');
comp && comp.unmount(root);
// グローバルに登録せずに直接コンポーネントを生成&マウント
comp = component(page)(root, { 'params': params });
// リンクイベント
var links = comp.$$('a[data-router][href]');
links.forEach(el => el.addEventListener('click', linkclick, false));
} catch (ex) {
// エラーが発生した場合はエラーページへ
collection !== 'notfound' && pageload('notfound', ex);
}
};
// ルーティング処理
const route = path => {
// 現在のページとURLが異なる場合はブラウザヒストリーに追加
loc.href !== path && history.pushState(null, document.title, path);
// URL分割:最初のパスをページ名とする
const [ collection, ...params ] = path.replace(base, '').split('/');
// ページ読み込み開始
pageload(collection, params);
};
// ブラウザ戻るボタン押下時の処理
window.addEventListener('popstate', e => route(loc.href), false);
// 初回読み込み
route(loc.href);
home.riot
<my-home>
<nav>
<ul>
<li><a data-router href="/hello">Hello</a></li>
<li><a data-router href="/goodbye">Goodbye</a></li>
<li><a data-router href="/greeting/hola">Hola</a></li>
<li><a data-router href="/greeting/adios">Adios</a></li>
<li><a data-router href="/hoge">hoge</a></li>
<li><a href="https://qiita.com/advent-calendar/2019/riotjs">通常リンク</a></li>
</ul>
</nav>
</my-home>
hello.riot
<my-hello>
<p>Hello World!!</p>
</my-hello>
goodbye.riot
<my-goodbye>
<p>Goodbye World!!</p>
</my-goodbye>
greeting.riot
<my-message>
<p>{ props.params[0].slice(0, 1).toUpperCase() }{ props.params[0].slice(1) } World!!</p>
</my-message>
notfound.riot
<my-notfound>
<h3>Oops!!</h3>
<h4>{ props.params.message }</h4>
<pre>{ props.params.stack }</pre>
</my-notfound>
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Riot Router</title>
</head>
<body>
<div id="root"></div>
<script src="/scripts/bundle.js"></script>
</body>
</html>
ここからはお好みで。
package.json
{
"name": "riotv4-router-sample",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --mode production --devtool source-map",
"start": "webpack-dev-server --inline --watch --hot --colors --content-base app/ --open-page index.html --historyApiFallback true -d --port 4500"
},
"keywords": [],
"author": "KAJIKEN <kentaro@kajiken.jp> (http://kajiken.jp)",
"license": "MIT",
"dependencies": {},
"devDependencies": {
"@babel/core": "^7.7.4",
"@babel/polyfill": "^7.7.0",
"@babel/preset-env": "^7.7.4",
"@riotjs/compiler": "^4.5.2",
"@riotjs/hot-reload": "^4.0.0",
"@riotjs/webpack-loader": "^4.0.1",
"babel-loader": "^8.0.6",
"riot": "^4.7.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0"
}
}
webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
//mode: 'production',
entry: ['@babel/polyfill', './src/scripts/index.js'],
output: {
path: path.resolve(__dirname, 'app/scripts'),
filename: 'bundle.js',
publicPath: '/scripts/',
},
devtool: 'inline',
//devtool: 'source-map',
module: {
rules: [
{
test: /\.riot$/,
exclude: /node_modules/,
use: [{
loader: '@riotjs/webpack-loader',
options: {
hot: true, // set it to true if you are using hmr
// add here all the other @riotjs/compiler options riot.js.org/compiler
// template: 'pug' for example
}
}]
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
}
.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"targets": [
">0.25%",
"not ie 11",
"not op_mini all"
]
}
]
]
}
ディレクトリ構成
.
│ .babelrc
│ package.json
│ webpack.config.js
│
├─app
│ │ index.html
│ │
│ └─scripts
│ bundle.js
│ bundle.js.map
│
├─node_modules
│
└─src
└─scripts
goodbye.riot
greeting.riot
hello.riot
home.riot
index.js
notfound.riot