素人が一週間でMMDモデル作ってみた②
さて、前回モデルv.0.1.0を作成して、とりあえず踊らせるところまでは来ました。
では次は前回放り投げた目線・表情・物理演算を改善したv.0.2.0モデルを作成していきます。
Sinatraで肥大化したファイルを分割する
SinatraとVueの黒魔術化
次なる実験場はこのジェネレータ。
ベースにSinatraを使っているので、そこに乗っける形でVueによるSPA化を施していく。
ぶっちゃけ、下記ページに必要なことは8割方書いてある。
以下、備忘録
index.htmlの返し方
上記ページでは「フロントエンドサーバがnpm、バックエンドサーバがsinatra」となっていたりして
そいつらの橋渡しをするようにしているようだが、
「別にSinatraあるんだから、そこで全部やっちゃえばよくね?」ということで今回はSinatraのみで対処した。
要するに、"/"にアクセスされたらSinatraで拾って"/index.html"を内部的に返せばよいわけである。
これは、Sinatraのreadmeにある「別ルーティングの誘発」を利用することで実現できる。
プログラム内でページ遷移
this.$router.pushを使う。
ちなみに、動的ルーティングのパラメータはthis.$route.params、こいつはthis.$router。
めちゃめちゃ紛らわしい💢
yarn
最近ではnpmの代わりにyarnを使うのが流儀らしい。
早いらしいが体感的にそこまで違いを感じなかった。
実際に数値で比べてみないとわかんないものだししょうがないね。
Vueいじってみた
今更ながら「Vue.jsを使いたい」という欲求を満たすため、自分のサイトをゴリゴリ改造しました。
従来mithril.js(in livescript)だったものを、Vue.js+Vue-router(in javascript)に変更してます。
以下、備忘録。
開発の流れ/詰まった点
Vue-router
最初はメインのページをindex.htmlにゴリゴリ書いていったんだが、
うちのサイトはメインページの他に本用のページがある。
この2種類のページを使い分けるために、メインページが一段落ついてからVue-routerを用いてSPA化に着手した。
Vue-routerはrouter-viewタグ以下にページ個別の内容を記述する。
その”ページ個別の内容”は、通常コンポーネントとして提供する。
つまり、現状からSPA化するためには、下記のようなフローを辿ることになる。
単一ファイルコンポーネント
コンポーネント — Vue.js
Vueのコンポーネントは気の遠くなるくらい多機能だけど、ここではごく基本的な機能のみを利用する。
問題は、コンポーネント化するためにはtemplateにJSの文字列リテラルとして持たせないといけない点。
できれば、リテラルでなく純粋なhtmlとして記述したい。
その欲求を解決してくれるのが単一ファイルコンポーネントである。
これは、例えばhome.vueというファイルを作って、そこのtemplateタグ内にコンポーネント化したいhtmlを記述するというものである。
単一ファイルコンポーネントであれば、ほぼ純粋なhtmlとしてページを記述できる。
単一ファイルコンポーネント — Vue.js
vue.jsのcomponentをwebpackで.vueにして単一ファイルコンポーネントにする - Qiita
ここでの注意点は2つ。
templateタグ直下の要素は必ず1個のみにするという点*1と、
単一ファイルコンポーネントを使うためには、.vueファイルのコンパイルが必要であるという点だ。
Webpack
vueファイルはそのままでは使えない。コンパイルしてJS(VDOM)化しないといけない。
モジュールバンドラが必要になる。
私はモジュールバンドラといえばbrowserifyしか知らなかったのだが、
Webで調べたところ、WebpackというものがVueとよく併用されているようなので、そちらを利用してみることにした。
webpack
webpack 入門 (v3系 対応) - Qiita
vueファイルのコンパイルには、通常の設定に加えて、vue-loaderを使うよう設定する必要がある。
vue-loaderはnpmでインストールする。
Vue.jsを使った大規模開発に必要なもの - Qiita
Introduction · vue-loader
また、vue-template-complierもあわせてインストールする必要がある。
ページが切り替わらない(1)
本用のページは一つ前のページと一つ後ろのページにそれぞれ遷移できる。
最初、ページを切り替えようとすると白いページに遷移してしまうことがあった。
理由は単純で、aタグを使っていたから。
Vue-routerでの遷移はrouter-linkタグを使う必要がある。
遷移先もhrefでなくtoで指定する必要があるため注意。
以下蛇足
ちなみにaタグで単純に遷移できない理由は、SPAではルートが"/"でなく"/#/"や"/?/"などになり、ルートの変換が必要になるためである。
aタグで"/"を直接変更してしまったら、シングルページでなくなってしまうのでSPAでなくなってしまう。よくよく考えてみると自明の理であった。
ページが切り替わらない(2)
上記問題が解決したあとも、ページを切り替えようとするとURLは変更されるのにページの内容は変更されないことがあった。
これについては、下記ページが詳しい。
[vue-router] パラメーター違いのページリンクをクリックしても更新されない時は ? - atuweb : つながりを作るWebプログラマ
以下蛇足
公式のページにも「$routeをwatchしろ」と書いてはいるのだが、
watchで何をすればいいかはいまいちピンときてなかった。
上記ページのようにwatchメソッド内でto.params.idを引数としてdataの各変数を更新してしまうのが直感的でわかりやすい。
その場合、更新用メソッドが必要になる。初期読み込み時に更新用メソッドを読み込むためには、createdプロパティを利用する。
gulpとwebpack
今までgulpを使っていたので、引き続きメインのタスクランナーにはgulpを利用しwebpackはモジュールバンドラに徹してもらうことにする。
gulpとwebpackの連携については、下記ページが詳しい。
以下蛇足
webpackを導入した際、webpackの実行でエラーが発生していた。
"=>"*3を認識していなかったようなので、決断的npmバージョンアップを実行。
(io.js 2.0.2→v8.9.4にバージョンアップ。かなり浦島太郎状態である……)
ところがバージョンアップ後、gulpの途中でよく止まるようになってしまった。
issueのページを見てもピンとこなかったので、古いnodeモジュールをすべて削除。
gulpfileを頼りに、必要最低限のモジュールを入れ直す羽目になった……。
デプロイ
とりあえず諸々整理して、npm startを実行すれば静的サーバが立ち上がるように設定し終わったので、ここでherokuにデプロイ。
しかしデプロイ後、アプリケーションエラーが発生するようになってしまった。
原因は、npm startで利用している一部のモジュールをdevDependenciesとしてインストールしていたため。
静的ファイルはすべてnpm startで生成していたため、heroku上でも同じモジュールが使えないといけないのだが、どうやらherokuではdepencenciesはインストールしてもdevDependenciesはインストールしてくれないようだ。
お行儀がいいとはいえないが、すべてdepencenciesに突っ込むことで解決した。
Vueの感想
- VDOMをhtmlとして書けるのが嬉しい。それでいてコンパイルするから最終的にはVDOMとして動作しているのが面白い
- 最初の動機は「vue.js+vue-loader.jsだけ使えばnpm非依存で作れるのでは……?」だったが、終わってみればnpmからは逃れられそうになかった
- vueファイル内ではpugやstylusも使えるようなので、そっちも使ってみたい
- 「『htmlとして書けるのが嬉しい』のにpug使うの?」って言われるかもしれないけど、JSのリテラルでない点が重要なのでhtml or pugはそこまで重要ではない。むしろvueファイルごとに使い分けられるので、そういう楽しみもある(と思う)
- テンプレートは奥が深そう(親子関係とか)なので、もう少し勉強が必要そう
- アニメーションさせたい
- 既存のruby製サイト(Sinatraなど)に組み込めれば強そうなので、研究したい
以上。
*1:ただしv-if/v-elseを用いて”結果的に要素1個のみになる状況”にしても、許容される
*2:私はvue.jsをダウンロードして利用していたので、vue-cliはインストールしていない
*3:アロー関数( https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/arrow_functions)
遅延処理に使えそうなgem
処理速度を向上させようとして失敗した話
気付けばもう2018年です。
去年は結局ブログのエントリを1本しか出しませんでした。
ブログのエントリは、twitterよりはまとまった話がしやすいですし大概追いやすいので、少し去年の四方山話をまとめておこうと思います。
閑話休題。
去年の12月、実装していたプログラムの納期が迫っていた頃の失敗談です。
そのプログラムは、複数のラジコンを制御するようなプログラムで、
MQTT*1を受信した際の処理とリアルタイム処理の二本柱で制御していました。
このリアルタイム処理が曲者で、
「0.2秒毎にDBのテーブルXの全レコードを最新の状態に更新し処理Aを行う」スレッドAと
「0.5秒毎にDBのテーブルXの全レコードを最新の状態に更新し処理Bを行う」スレッドBが併存していました*2。
この「全レコードを最新の状態に更新」は処理A、処理Bとも”直前”*3に行う必要があるため、まとめることができなかったわけです。
レコード件数は高々10件程度ですが、
プログラムの性質上、当該更新処理にかかる数十ミリ秒は馬鹿にできない時間でした。
かと言って、スレッドでの処理間隔を不用意に開けることはできません。
なぜなら処理間隔が開きすぎるとラジコンの制御が間に合わず、ラジコン同士が衝突したりコースアウトしたりするリスクが増大するためです。
なんとかできないか……。
そこで着目したのは「MQTT受信処理」の方でした。
下記のように処理を修正すれば全レコードを更新するタイミングが減ると考え、実際にプログラムを修正して様子を見ました。
- 特定のMQTTを受信した際にスレッドA,Bで行っている処理を行うべきタイミングかチェックする
- 処理を行うべきタイミングでなければ、行うべきタイミングになるまで待機し、再度チェックを掛ける*4
結論から言うと、これは大失敗でした。
修正後のプログラムを走らせると、10分くらい経過した時点でDBエラーが発生するようになりました。
ログを検証すると「全レコードを更新している途中のスレッドが殺された」ために起きていた現象のようでした。
プログラムを修正した際に一部のスレッドは「そのスレッドが請負う処理を行う必要がなくなった場合*5、外部から当該スレッドを殺す」ようにしていました。
「全レコードを更新している途中でスレッドを殺したらそりゃダメよなぁ」ということで、「レコード更新中はフラグを立てて、外部からスレッドを殺せないようにする」よう修正を掛けましたが、これは何故か失敗。
「面倒だし、スレッドを殺すこと自体を止めよう」と再度修正を掛けましたが、今度はスレッドが無尽蔵に作成され、処理がパンクする事態に。
ここに至ってこの修正部分にこれ以上時間を費やすことは不毛であると判断し、結局、以前通りのリアルタイム処理に巻き戻しました。
この件の教訓としては、概ね以下のような感じです。
- スレッドを外部から殺すような処理を設ける場合、DBなどの外部リソースを変更している最中にスレッドを殺さないようにする
- そもそも一度立てたスレッドはできるだけ外部から殺さない。スレッド内の適切なタイミングで終了チェック処理などを設ける
- (上記に関連して)スレッドなどの非同期処理は基本Fire&Forget(撃ちっぱなし)の精神を厳守する
- 常駐スレッドが無尽蔵に立つような設計は厳に慎む(例えあるスレッドの生存期間が短くても、そのスレッドが後続のスレッドを延々と立て続けるなら、それは常駐と変わらない)
- エンバグなどで困った時のために、修正部分をいつでも切り捨てられるようにする(修正用ブランチを切ってそこで処理するなど)
- 処理速度が致命的に遅いわけでないなら、わざわざ処理速度を上げるための修正をしない。プログラムの修正には常にエンバグのリスクが伴う