裏側にある多数のAPIに対してそれぞれ特化したクエリーを投げて集約した結果を表示するUIとしてのページを作っています。
過去の遺産の関係上、機能毎のディレクトリを保ちつつもSPA的な動作が出来るようにしたかったのが今回の動機です。
いいたいこと
- 複数のEntry + HtmlWebpackPluginで各ディレクトリごとに
index.html
とbundle.js
の組の生成が出来る - タグ間の連携は
Observer
を使うのが良さそう - 上記の理由でタグのコードは
mixin
にまとめて渡すとスマートに書ける
ビルド周りの前提
- Webpackを使う
- babel(ES2015)を使う
- HTMLtemplateにはpugを使う
webpackでindex.htmlも含めて複数生成をする設定
基本的なwebpackでのビルドする設定はclown0082さんの
などを参照してください。
ここではマルチエントリ部分のみ述べます。
ビルドしたファイルは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の名前を決めて自動でリストを作るほうが良いです。
ディレクトリ構成とファイルの役割
- ディレクトリ間で共有: js関係の
lib
と共通タグのcomp
にまとめる - ディレクトリ内での固定名称を固定化する
- main.js : webpackでentryを自動生成するためのフラグ + 共有タグを読み込み
- [module]/index.js : 共有コードを読み込みと配下のロジック周りの関係性を集約するためのファイル。
window.app
に渡される - [module]/[name].js : ロジックのみを書く。機能/view別にファイルを分ける
- [parts]/[name].tag : tagのHTMLを書く。機能/view別にファイルを分ける + javascriptのファイルと名前を合わせる
- javascriptはtag内に書かずmixinで対応する
- 連携を必要としないタグはこの限りではない。
- 個人的な趣味によりCSSには
Semantic UI
を使用
tag間の連携について
公式のサンプルにもあるとおり、html内のscriptタグへのベタ書きで簡単に書けるのもriot.jsの良いところです。
ですが一定以上のコードを書いたりライブラリが増えてきたら分けて書く方ほうが簡単になります。
- ベタに書いたら再利用できない
- patentやchildなどのタグ間の親子関係で指定をするとレイアウトが制限される
- コードは機能別に適切にファイル分割ないと管理しづらい
あたりを考慮した結果、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周りはもう少しやり方がありそうだと思っているのですが...スマートな解決がればぜひ教えてください。