情報共有システム
ogochan
私(生越)が中心になって作るシステムのフロントは、だいたいRiot.jsで作っています。
元々、フロントにはそれ程興味がなかったこと、Javascriptフレームワーク界隈が混沌としていることもあってあまり気合を入れていなかったのですが、さすがに避け続けることは難しいですね。それにまぁ、実際にやってみると結構楽しいことでもあります。などと言えるようになったのは、パッケージシステムが「とりあえずwebpack」になってくれたということも大です。
がっつりフロントを書くとなるとフレームワークどうするんだという問題があるのですが、後述するような事情からRiot.jsを使っていました。それで特に大きな不満もなかったのですが、最近になってSvelteなんてものにも手を出すことにしました。
手を出すのは良いんですが、既にRiot.jsで書いたコンポーネントが多数あるのでどうしたものかと思って、共存させることを実験して成功しました。
Riot.jsを選んでいた理由
以下ちょっとポエム調です。
ちょっと前まではあまりフロントに手を出すつもりがありませんでした。また、その頃主に使っていたのはRailsなので、あまりがっつりしたフロントは書き辛いということもあって、「ちょっとリアクティブに動けば満足」ということでjQueryを主に使っていました。
jQueryは「Javascriptのための環境」を作らなくても使えるということで、「ちょっと」で使うには十分満足でした。とは言え、動的にelementを生成する処理を書くのは結構面倒臭いものです。あのappend()を使いまくると「何がどこにくっついていたかわからない」ということでしばしば頭がこんがらがります。かと言ってhtml()を多用するのも抵抗あるし... だったわけです。どうも「文字列でプログラム書いてevalしてる」感じがしてしまうんですね。とは言え、その辺頑張ってライブラリ的なものを作っても、パッケージシステムが使えないと面倒臭く... ということで、正直苦手だったんですね。
でも、この情報共有システムをどうしても作りたかったこと、そして群雄割拠だったパッケージシステムが「webpackで決まり」という感じに落ちついて来たこと、RailsをやめてNode.jsを主体にしようと思ったこととかあって、がっつりフロントを書いても大丈夫だという機運が(個人的に)高まって来ました。
そこでフレームワークは何にするんだという話になるわけです。
一番ありがちで多分一番妥当なのはReactを使うことでしょう。情報も結構多い上にちょっとハードルが高いお陰で、検索ノイズもそれほど多くありません。特に「応用例」が豊富にあることは特筆すべきことだと思います。
でも、Reactは選択しませんでした。それはたまに起きるTwitter(Reactで書かれている)の不具合やfacebook(Reactの本家)の「重さ」に不快を感じることが多かったので、「Reactを使ったもの」があまり好きではなかったからです。また、HTMLから離れた記法(JSXのこと)でコンポーネントを書くということも、GUIものの歴史を思うとあまり良い感じが持てません。「GUIもの」って、最初は「element生成用のfunctionを呼び出す形式」で始まりますが、Ver 2くらいから「XMLで画面定義」になりますよね。それは水が低いところに流れたり煙が高いところに上がったりするような「必然」だと思うんです。とかまぁ、いろいろ理由はあるんですが、要するに
好きになれなかった
からですね。
Reactのalternativeとして出て来るものはVue.jsでしょう。個人的にもReactの嫌な部分は結構解消されているので悪印象はありません。mayumiはVue.jsで書いたりしているようです。私は
特に選ぶ理由がない
ので選んでないというだけです。「悪い(嫌い)だから選ばない」のではなくて、「好きになるべき(素晴しい)理由がない」だけです。
ところで前に誰かのブログで、「我々が欲しかったのは素晴しい機能を持ったフレームワークではなくて、動的にHTMLを生成するフロント用テンプレートエンジンではなかったか」というのがありました。確かに自分がフレームワークに求めるのはまさにそれです。そう考えると、データやイベントの双方向バインディングはそれ程必要ではなく、むしろ覚えることが増えて邪魔くらいに思えます。なんだかんだで生Javascriptでゴリゴリ書かなきゃいけないわけですから、フレームワークに多くを期待してません。
そういった意味でRiot.jsは覚えるべきことも少なく、Javascriptが透けて見える程度のことしか出来ません。双方向バインディングもありません。つまり、ちょうど良かったので選んでいました。検索ノイズどころか情報そのものもそれ程ないのが難ではありますが、謎も少ないところは良いです。
Svelteに手を出した理由
そんなわけでRiot.jsに特に不満はないのですが、
- あまり(今は)人気がないフレームワークであること
いくらフロントエンド用フレームワークが水物とは言え、あまりに将来が不安なのは困る - やっぱり双方向バインディングがないのは不便
FORM絡みの処理が複雑になると、やっぱり欲しいなと感じる - たまに期待外れの動作をする
なんかこう、妥当な動作なんだってわかるんだけど、期待とは違うんだよ的な
ということで他のフレームワークにも色気を感じるようになりました。そこに「最近売り出し中」な感じのフレームワークとしてSvelteが紹介されていました。それまではDbGateに使われているフレームワーク... くらいしか知らなかったので、マイナーとかカルトなとか言われるものかなと思っていたのですが、どうもそうではないようですね。本題と関係ないですが、DbGateは結構イケてます。個人的にはこの手のアプリの中では一番気に入ってます。時々謎な動きしますけど。
手を出す積極的な理由があったわけでもないのですが、Riot.jsに足りないと感じた部分があるように思えたこと、「ひっかかる点」があまりないということで好感が持てたこと、そして「勢い」を感じたことでしょうかね。
別のフレームワークに手を出した時の問題
そうやって「今まで使っていたフレームワークでないフレームワーク」を導入した時、一番困るのは「過去の資産」をどうするかです。
しかも、現段階では、まだ「移行する」という意思決定はしていません。Riot.jsに大きな不満があるわけでもないですから、どうしても捨てたいというものはありません。つまり、今Svelteを使うのは
お試し
でしかありません。なので、あまり大きな手間をかけたくはありません。
そこで考えるのは、混ぜて使えないのかということです。
webpackのモジュール読み込みの原理
webpackで(コンポーネントの)モジュールを読む時には何が起きているでしょう?
細かい正確な話は各人でコードを読んでもらえばいいのですが、ごく雑なことを言えば
「なんとかloader」を使ってJavascriptにして、それをくっつけている
のがwebpackの仕事です。読み込む(ためのJavascriptを作っている)のは「なんとかloader」の仕事で、webpackは「くっつける」ことをやっているわけです。「くっつける」と言っても生成されたコードを見ると、結構「はぁ〜」みたいな感じのことをやってますが。
どのようなJavascriptにするかはそれぞれのローダがどう作られているかに依りますが、最終的にはそれぞれのローダの作るJavascriptをくっつけているだけです。なので、webpackから見た時には生成されたモジュールはAPIを守ったJavascriptに過ぎないということです。しばしば「Svelteはコンパイラだよ」と言われますが、そういったことも含めてsvelte-loaderの仕事です。どんなコードになっているかはpackされたJavascriptを覗いてみるとわかります。Riot.js用ローダの出すコードは結構人間でも読めます。packされたJavascriptを元にオリジナルソースを復元できる程度には素直です。
そんなわけで、エントリーポイントになるコードをちょっと工夫してやるだけで混ぜて使えるのじゃないかというのが、元々の発想です。
実際にやってみる
では実際にやってみましょう。まずはwebpack.config.js(一部です)
module: { rules: [ { test: /\.svelte$/, use: { loader: 'svelte-loader', options: { compilerOptions: { dev: !prod }, emitCss: prod, hotReload: !prod } } }, { test: /\.riot$/, exclude: /node_modules/, use: [{ loader: '@riotjs/webpack-loader', options: { hot: true } }] },
拡張子が.svelteの時はsvelte-loaderを使い、.riotの時は@riotjs/webpack-loaderを使うということですね。prodという変数はproductionモードの時にtrueになるものです。
これ自体は「混ぜて」使わなくても、1つのプロジェクトで異なるフレームワークを使う時も同じです。
Svelte、Riot.jsのコンポーネントはそれぞれの作法に合わせて作ります。この中身については割愛。と言うか、特に何もなくて、単にそれぞれの作法通りで良くて、それ以外のことを考える必要はありません。
そして読み込む先のテンプレート。私はsprightlyを使っています。どうせほとんどの処理はテンプレートじゃないところに書くわけなので、これくらいがちょうどいいのです。
<!DOCTYPE html> <html> <head> <title>{{title}}</title> <link rel="stylesheet" href="/style/style.css"> <link rel="stylesheet" href="/static/bootstrap-icons/font/bootstrap-icons.css"> <script defer src="/dist/common.js"></script> <base href="/" /> </head> <body class="layout-fixed"> <div class="wrapper"> <nav class="main-header navbar navbar-expand-lg navbar-light bg-light p-3 d-print-none" user="{{user}}" is="common-nav" data-riot-component></nav> <aside class="main-sidebar sidebar-bg-dark sidebar-color-primary shadow d-print-none" is="sidebar" data-riot-component></aside> <main class="content-wrapper"> <div class="container-fluid"> <div id="svelte"></div> </div> </main><!-- /.content-wrapper --> <footer is="common-footer" class="main-footer"></footer> </div> <script defer src="/dist/test.js"></script> </body> </html>
まぁ普通な感じで。
ここでnav, aside, footerの中身がRiot.jsで書かれたものです。isが書かれていますね。これはisでコンポーネントを指定しないと、「コンポーネント名がついた虚無のelement」が生成させるからです。良いこともあるんでしょうが、ここではむしろ邪魔なのでisを使っています。
id="svelte"がSvelteで書かれたモジュールを読み込む予定地です。
さて問題のエントリポイントはこんな感じです。
import '@riotjs/hot-reload'; import observable from '@riotjs/observable'; import { install } from 'riot' import '../svelte/styles/global.css'; import App from '../svelte/App.svelte'; import { registerGlobalComponents,mountComponents } from '../javascripts/register-global-components' // register registerGlobalComponents() let obs = observable(); install((component) => { component.observable = obs; }); mountComponents([ 'common-nav', 'common-footer', 'sidebar', ]); const app = new App({ target: document.getElementById("svelte"), props: { name: 'world' } }); export default app;
明示的にimportしているのはRiot.jsに必要なモジュールと、Svelteで書かれたコンポーネント(App.svelte)。Riot.jsで書かれたコンポーネントはregister-global-componentsの中で読み込まれています。
installはここで書いたRiot.jsのコンポーネントで必要なものなので、今回の話とは直接関係ありません。
mountComponentsはRiot.jsのコンポーネントをmountする処理です。common-nav, sidebar, common-footerがそれです。HTMLテンプレートの方にisで書かれていますね。
その後に書かれているapp = new App以後がSvelteのコンポーネントのマウント処理です。
処理の詳しいことはそれぞれの解説に譲るとして、やっているのはただ単に
それぞれのコンポーネントをそれぞれの作法でmountした
というだけのことです。
なお、今回Riot.jsは「情報共有システム(まだ名はない)」のコンポーネントを流用し、Svelteはサンプルテンプレートに含まれているコンポーネントを流用したので、これを実行すると、
というように表示されます。ね、出来てるでしょう?
結局のところ、特にポイントになるところもなければ、特に難しいところもありません。「やれば出来そうなことをやってみたら出来た」というだけの話だったということです。