今回はRailsの素振り
ただ今回のメインはフロントでやりたいことはTypeScript + React + SSR
SSRは今回はAirbnbがOSSとして出しているhypernovaを使用しました
github.com
環境
環境は以前次の記事で使用した環境を使用します
$ ruby -v ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-darwin17] $ rails -v Rails 5.1.6
準備
今回は次のコンポーネントをSSRで表示したいと思います
コードはapp/javascript/components/hello_react.tsx
に追加したとします
import * as React from "react"; import { renderReact } from "hypernova-react"; interface Props { text: number; } interface State { isEnabled: boolean; } class HelloReact extends React.Component<Props, State> { constructor(props: Props) { super(props); this.state = { isEnabled: false }; setTimeout(() => { this.setState({ isEnabled: true }); }, 5000); } handleClick(e) { alert("click button"); } render() { return ( <div> <h1>Hello React!</h1> <p>text is {this.props.text || "none"}</p> <p>state is {this.state.isEnabled ? "enabled" : "disabled"}</p> <button onClick={this.handleClick}>Button!!</button> </div> ); } } export default renderReact("HelloReact", HelloReact);
Hypernovaの導入には公式のdocに沿って必要なものを整えていきます
node側
まずはpackageの追加から
今回はreactを使用するのでhypernova-react
も一緒に追加しておきます
$ yarn add hypernova hypernova-react
次にhypernova.jsをrootディレクトリに追加します
これはnode hypernova.js
で起動することでコンポーネント名をうけとりHTML化したものを返します
const { environment } = require('@rails/webpacker') const hypernova = require("hypernova/server"); const requireFromUrl = require("require-from-url/sync"); const config = environment.toWebpackConfig() hypernova({ devMode: true, port: 3030, getComponent(name) { if (config.devServer) { return requireFromUrl(`http://${config.devServer.public}${config.devServer.publicPath}ssr/${name}`).default } else { return require(`${config.output.path}/ssr/${name}`).default } } });
ポイントとしてはコンポーネント名をうけとって、それを返すgetComponent
です
公式のほうではファイルのパスを指定してましたが、開発時に私はwebpack-dev-serverを使用するので、その時はURLからコンポーネントを読み込みたいです
そのために@rails/webpacker
を使ってwebpackの設定を取り出してdev-serverの設定が入っていれば起動してるとみなしてrequireFromUrlを使ってrequireしています
※ ちゃんとやるなら疎通チェックとかしたほうが良さそう
Ruby側
これでnode側の準備が終わったので次はRuby側
まずはgemを追加する
gem 'hypernova'
次に指定のコントローラーにaround_action :hypernova_render_support
を追記します
class WelcomeController < ApplicationController around_action :hypernova_render_support ~~
後は開発時にdebugしやすいように次の記載をconfig/environments/development.rb
に追記します
これによってhypernova側で出たエラーなどをブラウザ上で各員することが出来るようになります
require 'hypernova' require 'hypernova/plugins/development_mode_plugin' Hypernova.add_plugin!(DevelopmentModePlugin.new)
後はconfig/initializers/hypernova.rb
でhypernova用のinitilizerを追加します
ここでhypernova serverが起動しているhostとportを指定します
Hypernova.configure do |config| config.host = "localhost" config.port = 3030 end
次にコンポーネントを全部読み込んだものをのちほどwebpackでbuildして一つのファイルにまとめたいのでapp/javascript/packs/application.js
を用意しました
const context = require.context("components", true); const obj = {}; context.keys().forEach(function(key) { obj[key] = context(key); }); module.exports = obj;
次はwebpackの設定を行います
webpacker.ymlは今回はwebpackerのinstall時に出てくるものからほとんど変えずに使用しています
ここからがモヤッとしたポイントなんですが、ブラウザ上で読み込むapplication.jsはes5で出力されるのですが、hypernovaで読み込む時のものはcommonjsで出力する必要があるようで、webpack側でブラウザ上で読み込むものとhypernova用でoutputを2種類用意する必要がありました
今回どうしたかで言うとconfig/webpack/environment.js
を次のように変更してoutputをes5, commonjsと2種類用意しました
webpacker.ymlではes5で出力される設定なのでgetSSREnvironment
でlodashのcloneDeepでdeepcopyしたものをSSR用の設定として変更したものを返しています
const { environment } = require("@rails/webpacker"); const typescript = require("./loaders/typescript"); const cloneDeep = require("lodash/cloneDeep"); const { sync } = require("glob"); const extname = require("path-complete-extname"); const { basename, dirname, join, relative, resolve } = require("path"); function getSSREnvironment(environment) { const ssrEnv = cloneDeep(environment); const rootPath = `${__dirname}/../../app/javascript/components`; const paths = sync( join(rootPath, "**/*{" + ssrEnv.config.resolve.extensions.join(",") + "}") ); const result = ssrEnv.entry; result.delete("application"); paths.forEach(path => { const namespace = relative(join(rootPath), dirname(path)); const name = join(namespace, basename(path, extname(path))); result[name] = resolve(path); }); ssrEnv.entry = result; ssrEnv.config.output.path += "/ssr"; ssrEnv.config.set("output.libraryTarget", "commonjs"); ssrEnv.config.set("output.filename", "[name].js"); return ssrEnv; } environment.loaders.append("typescript", typescript); module.exports = [environment, getSSREnvironment(environment)];
ここまできたら後はhypernova gemを追加すると使えるようになるrender_react_component
のビューヘルパーを使って任意のview側に下記の記載を行います
<%= render_react_component('react_sample.js', { text: 'test!!' }) %>
確認
$ export RAILS_ENV=development $ export NODE_ENV=development $ node hypernova.js $ ./bin/webpack-dev-server $ bundle exec rails server
もちろんcurlなどで確認してもhtmlがかえってくる!!
最後に
今回はHypernovaを使用してSSRを実現した ひとまず実現はしたけどwebpacker周りの設定がしっくりこないのでやっていく中で良い感じにしていきたい