Hypernova で Vue.js のコンポーネントを Rails でレンダリングする

個人メモ。

Rearchitecting Wantedly's Frontend | Wantedly Engineer Blog の記事を読んで、副業先でも使えるかもと思い、Hypernova を触っていた。

Hypernova を使う個人的なモチベーション

リリースまで至った、それなりの規模の SPA ではないブラウザナビゲーションで遷移する Rails アプリを想定する。リッチな UI の実現と保守性の観点から JavaScript フレームワークの導入をおこなう。導入自体は Webpacker のような仕組みで簡単にできるようになったが、ビューのロジックがあちこちに存在するという課題が生まれる。ロジックとは、erb や haml などのテンプレートに書く(サーバーサイドで一度決定したら変わることはないという意味で)静的なロジックと、JavaScript フレームワークが担うUI操作などのイベントに応じて実行される動的なロジックのふたつである。
これらのファイル、あるいはテンプレートの仕様を行き来し( erb, haml <-> react, vue, angular )、頭を切り替えながら実装するのは個人的にはけっこうキツい。

JavaScript フレームワークに静的なロジックを担わせることもできるが、SEOや初期表示の観点から、どこまでをクライアントサイドでレンダリングするか、という問題が出てくる。 理想を言えば、JavaScriptコンポーネントのコードを Railshaml などのテンプレートエンジンと同様に扱えるとよい。Hypernova を使えば、これを実現することができる。

Node.js プロセスの面倒をみるという運用の手間が増えるが、エラーハンドリングの仕組みがよくできているので、最悪 Node.js のプロセスが落ちても、クライアントサイドのレンダリングにうまくフォールバックしてくれる。

Hypernova を Vue.js で使う

Hypernova、React に限らず SSR ができる JavaScript フレームワークならば何でも扱えそうな造りに見えつつ、Vue.js のコンポーネントを扱う話をあまり見かけない。こういう issueがあるということは多分やろうとしている人はいるんだろう、というのと、hypernova-react のコードを見たらファイルが一枚だけあって、これと同じことをするラッパーを用意すればよさそうだな、と思って書いた。こんなかんじ。

import Vue from 'vue'
import hypernova, { serialize, load } from 'hypernova';

export const renderVue = (name, component) => {
    const Component = Vue.extend(component);
    return hypernova({
        server() {
            return (props) => {
                const { createRenderer } = __non_webpack_require__('vue-server-renderer');
                const renderer = createRenderer();
                const vm = new Component({ propsData: props });
                return new Promise((resolve, reject) => {
                    renderer.renderToString(vm, (err, contents) => {
                        if (err) {
                            console.error(err);
                            reject(err);
                            return;
                        }
                        resolve(serialize(name, contents, props));
                    });
                });
            };
        },
        client() {
            const payloads = load(name);
            if (payloads) {
                payloads.forEach((payload) => {
                    const { node, data } = payload;
                    const vm = new Component({ propsData: data });
                    vm.$mount(node);
                });
            }
            return component;
        }
    });
};

サーバーとクライアント両方で、Webpack でビルドする前提。Webpack 依存のコード(__non_webpack_require__)が入っているのがイケてない。Webpack の設定でクライアントのビルドの場合は vue-server-renderer を除外してもよさそう。ここらへんの問題が解決できたら、hypernova-vueとして npm に公開したい。

使う時は、Vue コンポーネントのオプションオブジェクトを先のファイルで export している renderVue 関数でラップする。後は、Hypernova の設定で、name に応じて、ラップしたコンポーネントを返すようにする。

import {renderVue} from './hypernova-vue';
import Like from './Like.vue';

export default renderVue('like', Like);

Rails のテンプレートからコンポーネントを呼び出す場合は、以下のようになる。ヘルパー名が react 決め打ちなのがアレだけれども alias か自前で render_vue_component ヘルパーを定義すればいいと思う。

<%= render_react_component('like', liked: false) %>