LoginSignup
6
1

More than 3 years have passed since last update.

Riot.js Advent Calendar 2019 の8日目が空いていたので埋めます。

Riot Router

前回前々回でRiot Routerについて触れてきましたが、トリガーの部分でRawthが出てきました。
Riot.js v3の時もRouterをカスタマイズして使っていたのですが、このRawthのコードを見ているとRiot Routerを使わずに出来そうだったのでチャレンジしました。

気に入らない点

v3のRouterからどうもしっくりこないのが、ページが複数あるときに全部importやmountをしないといけない点です。
複数人でページを分けて開発していると、これがとてもメンドクサイ。

要件

  • index.jsにはimportregisterをたくさん書きたくない。ページが増えても変更したくない。
  • 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

rrc_2019_12_06_210631(slt)(raw).gif
突貫工事なのでこのままでは実用には耐えられないだろうが、思い描くSPAの動作になりました!
今後育てていこう。

6
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
6
1