💎

Vite Rubyを使わずRails+ReactをHMRしてみた

2024/01/24に公開

この記事は?

Ruby on Rails + Vite でReactをHMRできるようにするときにハマりまくった。
でも調べる過程で色々学びがあったのでそのメモ。
踏み込んだ日本語記事もなかったので、誰かやいつかの自分の役に立つかもしれない

背景

  • RailsでERBを返却するMPA
  • 諸事情によりVite Rubyを選定できない。

結論

公式Doc(バックエンドとの統合)に記載されている。
HMRをするためには読み込みたいファイルの他に、

  • @vite/clientを読み込み
  • @react-refreshを読み込んで初期化(@vitejs/plugin-reactを使っている場合のみ)

する必要がある。

最終的な構成

vite.config.ts
import { Plugin, defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import * as fs from 'fs';

export default defineConfig(({ mode }) => ({
  plugins: [react({ jsxRuntime: 'classic' })],
  server: {
    host: 'dev.myapp.com',
    port: 5173,
    strictPort: true,
    https: {
      key: fs.readFileSync('path_to_cert/key.pem'),
      cert: fs.readFileSync('path_to_cert/cert.pem'),
    },
    hmr: {
      protocol: 'wss',
      port: 24678,
    }
  },
  build: {
    manifest: true,
    outDir: '../../public',
    rollupOptions: {
      input: {
        ...getJSEntryPoints(),
      },
    },
  },
}));
js_helper.rb(抜粋)
VITE_BASE_URL = 'https://dev.myapp.com:5173'

def render_vite_tag(file_path)
  tags = []
  tags << javascript_tag(
          "
            import RefreshRuntime from '#{VITE_BASE_URL}/@react-refresh'
            RefreshRuntime.injectIntoGlobalHook(window)
            window.$RefreshReg$ = () => {}
            window.$RefreshSig$ = () => (type) => type
            window.__vite_plugin_react_preamble_installed__ = true
            ",
          type: 'module'
        )
  tags << content_tag(:script, nil, type: 'module', src: "#{VITE_BASE_URL}/@vite/client")
  tags << content_tag(:script, nil, type: 'module', src: "#{VITE_BASE_URL}/#{file_path}")
  safe_join(tags)
end
sample.html.erb(抜粋)
<%= render_vite_tag('src/main.ts') %>

ハマりどころ(備忘録)

SSL化されたページからViteDevサーバにリクエストできない

ブラウザのコンソールのエラー
Mixed Content: The page at 'https://dev.myapp.jp/sample_page' was loaded over HTTPS, 
but requested an insecure script 'http://localhost:5173/@vite/client'. 
This request has been blocked; the content must be served over HTTPS.

RailsアプリがSSL化されていてページのプロトコルがHTTPSのとき(https://dev.myapp.com)、
viteサーバーのlocalhostはhttpのためスクリプトの読み込みができない。
ViteサーバーのHTTPSを有効にする(ドキュメント)ことで解決。

@vitejs/plugin-react がエラーになる

ブラウザのコンソールのエラー
hogehoge.tsx:7 Uncaught Error: @vitejs/plugin-react can't detect preamble. 
Something is wrong. See https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201
    at hogehoge.tsx:7:11

エラーメッセージで言及されているプルリクには、
クラスコンポーネントでは動かない・無名コンポーネントだと動かない・Vite4.1で解消される などと記載がある。
だが今回はいずれにも該当しなかった。

上述の公式Doc(バックエンドとの統合)に記載があるように、

<script type="module">
  import RefreshRuntime from 'http://localhost:5173/@react-refresh'
  RefreshRuntime.injectIntoGlobalHook(window)
  window.$RefreshReg$ = () => {}
  window.$RefreshSig$ = () => (type) => type
  window.__vite_plugin_react_preamble_installed__ = true
</script>

を追加することでエラーを解消できた。

気づくまでの過程
ビルドされたファイル(抜粋)
if (!window.__vite_plugin_react_preamble_installed__) {
    throw new Error("@vitejs/plugin-react can't detect preamble. Something is wrong. See https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201");
}

ここでエラーがthrowされていた。
__vite_plugin_react_preamble_installed__でググったらやっとドキュメントと出会えた...
だいぶ遠回りしてしまった感。

ページ全体がリロードされる

Pluginに@vitejs/plugin-reactを入れていなかった。
プラグインを入れることで解消。

エントリーポイントがtsx以外のファイルを編集したときには、依然としてフルリロードがされてしまう。

リロードの理由の推測

Vanilla JSの公式サンプルでも変更時にページ全体がリロードされたので、そういうものかもしれない。

ViteサーバーとのWebSocketのやり取り を見ると full-reloadというレスポンスが帰ってきてるのがわかる。

このレスポンスが帰ってきたとき、vite/clientはページをリロードするようだ。
https://github.com/vitejs/vite/blob/6c4bf266a0bcae8512f6daf99dff57a73ae7bcf6/packages/vite/src/client/client.ts#L250-L267

このレスポンスは、vite serverでbundleの依存関係が変わったり失敗するとされる?らしい。
https://github.com/vitejs/vite/blob/6c4bf266a0bcae8512f6daf99dff57a73ae7bcf6/packages/vite/src/node/optimizer/optimizer.ts#L318-L329

公式Doc HMR APIを参考にHMRに対応した実装をする必要があるのかな?

GitHubで編集を提案

Discussion