Help us understand the problem. What is going on with this article?

Riot.js v4 自分でRouterを作る

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の動作になりました!
今後育てていこう。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away