LoginSignup
4
3

More than 5 years have passed since last update.

Riot.jsでDirectory + riot-routeの二階層で構成するサンプル

Last updated at Posted at 2017-06-11

裏側にある多数のAPIに対してそれぞれ特化したクエリーを投げて集約した結果を表示するUIとしてのページを作っています。
過去の遺産の関係上、機能毎のディレクトリを保ちつつもSPA的な動作が出来るようにしたかったのが今回の動機です。

動くサンプルがこちらです
レポジトリはこちら

いいたいこと

  1. 複数のEntry + HtmlWebpackPluginで各ディレクトリごとにindex.htmlbundle.jsの組の生成が出来る
  2. タグ間の連携はObserverを使うのが良さそう
  3. 上記の理由でタグのコードはmixinにまとめて渡すとスマートに書ける

ビルド周りの前提

  1. Webpackを使う
  2. babel(ES2015)を使う
  3. HTMLtemplateにはpugを使う

webpackでindex.htmlも含めて複数生成をする設定

基本的なwebpackでのビルドする設定はclown0082さんの

Riot.js + webpack + ES6(Babel, buble)での開発環境構築例 ※追記webpack2

などを参照してください。
ここではマルチエントリ部分のみ述べます。

ビルドしたファイルはnginxやhttpdの適当なディレクトリにそのまま置くことを考えるとbundle.jsごとにアクセスを受けるindex.htmlを一緒に生成する必要があるため、公式にあるプラグインhtmlWebpackPluginを利用します。
そのまま使うとエントリーされた全てのjsを読み込む一つのhtmlができてしまいますが、
オプションとしてどのエントリーに対応するか制御できるchunkと生成パスを指定するfilenameのオプションがあるのでエントリー毎に設定を変えて複数渡せば良いです。

plugins = [
  new HtmlWebpackPlugin({
    chunks: ['index']
    filename: 'index.html',
    template: 'template/index.pug',
  }),
  new HtmlWebpackPlugin({
    chunks: ['sub/']
    filename: 'sub/index.html',
    template: 'template/index.pug',
  })
]

上記の場合、出力結果は以下の形になります
chunkでフィルタする性質上indexというフォルダ内にbundle.jsが作られたり、
bundle.jsへのリンクが冗長な書き方なっていますが、とりあえず動くので良しとします。

/
├ index.html
├ index
│ └ bundle.js
└ sub
     ├ index.html
     ├ bundle.js

ディレクトリを増やすたびにコンフィグを修正するのが面倒なので、エントリーにするjsの名前を決めて自動でリストを作るほうが良いです。

ディレクトリ構成とファイルの役割

  1. ディレクトリ間で共有: js関係のlibと共通タグのcompにまとめる
  2. ディレクトリ内での固定名称を固定化する
    • main.js : webpackでentryを自動生成するためのフラグ + 共有タグを読み込み
    • [module]/index.js : 共有コードを読み込みと配下のロジック周りの関係性を集約するためのファイル。 window.appに渡される
    • [module]/[name].js : ロジックのみを書く。機能/view別にファイルを分ける
    • [parts]/[name].tag : tagのHTMLを書く。機能/view別にファイルを分ける + javascriptのファイルと名前を合わせる
  3. javascriptはtag内に書かずmixinで対応する
    • 連携を必要としないタグはこの限りではない。
  4. 個人的な趣味によりCSSにはSemantic UIを使用

tag間の連携について

公式のサンプルにもあるとおり、html内のscriptタグへのベタ書きで簡単に書けるのもriot.jsの良いところです。
ですが一定以上のコードを書いたりライブラリが増えてきたら分けて書く方ほうが簡単になります。

  1. ベタに書いたら再利用できない
  2. patentやchildなどのタグ間の親子関係で指定をするとレイアウトが制限される
  3. コードは機能別に適切にファイル分割ないと管理しづらい

あたりを考慮した結果、mixinで実装してグローバル変数app内に集約し、
各タグは対応するmixinを読み込む形で落ち着いてきています。

さらにmixinで返ってくるAPIがObservableなためListenerを登録すれば連携できます。
複雑なメッセージをやり取りする需要はまだ無いのでどこまで拡張しても大丈夫かは実験が必要ですね。

//- ページレイアウト部のpug
app-a-main
    part-riot-pagenation
    div(class='ui container')
        h2 Index Page - App:A
        part-app-a-reqdate
        entry
    script.
        this.mixin(window.app.app_a.mainpage(opts)) // app-aのmainpageを読み出す

// 上記のレイアウト用タグが読み出すmixin。
// 入力用と出力用のタグをマウントして連携のためにAPIを渡す
// 入力タグへの初期値などはこちらで処理することで、入力用タグの再利用性を維持。
export function mainpage(){
  return {
    init: function(){
      this.one('mount', function(){
        // クエリーストリングを読んで入力タグに初期値として渡す
        const qs = route.query();
        qs.start = qs.start || new Date().addDays(-7).toFormat("YYYY-MM-DD");
        qs.end = qs.end || new Date().addDays(1).toFormat("YYYY-MM-DD");

        // 初期値を入力タグに渡してマウントし、APIを受け取る
        const ctrl = riot.mount('part-app-a-reqdate', qs)[0];

        // 表示用のtagにAPIを渡してマウント
        const tag = riot.mount('entry', "app-request-sample", {ctrl:ctrl})[0];
      });
    }
  };
}

// 入力側はObserverを通してパラメータを渡す
export function requestDate(opt){
  const mixin = {
    init: function(){
      // v3.x系ではSubmit後の移動がキャンセルされないので...
      this.one('mount', ()=>{
        this.refs.requestForm.onsubmit = function(){
          return false;
        };
      });
    },
    load: function(){
      const query = this.getVariable();
      // tagが持つObserver機能を使う
      this.trigger("load-by-date", query);
    },
    getVariable: function(){
      return Object.keys(this.refs).reduce((a, b)=>{
        if(/input|textarea|select/i.test(this.refs[b].tagName)){
          a[b] = this.refs[b].value;
        }
        return a;
      }, {});
    },
    write: function(opts){
      this.opts = opts;
      this.update();
    },
  };
  return mixin;
}

// 出力側タグ。何らかの入力はObserverのイベントを通して受け取り、独自の入力は持たない
// ただしページ内で完結する、例えばテーブルのソートなどは仕込むことがある
export function request_sample(opt){
  const mixin = {
    init: function(){
      //マウント時に渡される入力タグのAPIにListenerを登録
      opt.ctrl.on("load-by-date", (data)=>{
        this.load(data);
      });
    },
    load: function(data){
      /**
       * ダミーの処理
       */
      const self = this;
      co(function*(){
        const records = yield common.request.get({
          url: "/"
        });
        self.text = records.text;
        self.query = Object.keys(data).map((d)=>{
          return {
            name: d,
            value: data[d],
          };
        });
      })
      .then((data)=>{
        self.update();
      })
      .catch((err)=>{
        self.errorMessage = err.message;
        self.update();
      });
    }
  };
  return mixin;
}

感想

riot.js半年ぐらい試行錯誤しながら使ってみて、なんとなくスマートな書き方が見えてきたのでサンプルにしてみました。
タグ毎にキレイに分割できれば再利用やバージョン変更も比較的容易にできそうです。
特に個人的には複雑なビルドパイプラインを作ったり、CSS周りに手を入れる余裕が無いのでとても助かります。圧倒的感謝。
webpack周りはもう少しやり方がありそうだと思っているのですが...スマートな解決がればぜひ教えてください。

4
3
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
4
3