どうも、ピクシブでフロントエンドエンジニアをやっている @uzimaru0000 です。
今回は、vite を使ったプロダクト開発をしたのでそれの知見を共有したいと思います。
なぜ vite なのか
弊社のプロダクトのフロントエンドではモジュールバンドラーに webpack を利用しているケースが多いのですが、vite を使う選択をしました。
理由としては以下のような点があります。
- 開発サーバーがとても速い
- 設定ファイルがwebpackほど複雑じゃない
vite の特筆すべき特徴として開発サーバーがとても速いというものがあります。
これは、依存のあるモジュールをネイティブ ES モジュールとして読み込むようにすることで必要最低限のバンドルで済むようにしているためコードの変更がとても速く反映されます。
フロントエンドエンジニアが僕一人であったというのもあり、開発速度を早めるために vite のこの特徴を利用したかったのが大きな理由です。
vite と PHP を組み合わせる
pixiv はバックエンドに PHP を利用して開発しているので vite を PHP と組み合わせる(具体的には Smarty)必要があります。
基本的には公式ドキュメントの「バックエンドとの統合」の通りで良いのですが、pixiv のバックエンドはフレームワークが入っていないので vite との繋ぎこみを自分で書く必要がありました。
PHP 側にmanifest読み込みHelper実装
PHP 側に manifest.json
を読み込む Helper を作ります。
viteは、データを manifest.json
を吐き出すような設定があるのでそれを見て HTML にビルドした結果を埋め込みます。
それを PHP で読み込めるように Helper を作成します。
manifest.json
からデータを取ってきて、CSS, JS を読み込むためのHTMLを作成しています。
getEntryPointHTML
を呼び出すことでそのHTMLを取得できるので Smarty のプラグインなどを作って埋め込みます。
これで vite でビルドした結果を PHP から利用することができるようになりました!
final class Vite_Manifest_Production { ... /** * @param string $name * @param string $sync_kind * @return string */ public static function getEntryPointHTML(string $name, string $sync_kind): string { Util_Assert::string($name); Util_Assert::stringInArray($sync_kind, ['sync', 'async', 'defer']); $css_output = Vite_Manifest_Production::_getCssPath($name); $output = Vite_Manifest_Production::_getJsPath($name); return $css_output . $output; } /** * @param string $name * @return string */ private static function _getCssPath(string $name): string { Util_Assert::string($name); if (!isset(Vite_Manifest_Production::$manifest[$name]['css'])) { return ''; } $css_path = ''; foreach (Vite_Manifest_Production::$manifest[$name]['css'] as $path) { $path = Util_String::h(Vite_Manifest_Production::$public_path . '/' . $path); $css_path .= "<link rel='stylesheet' href='{$path}' crossorigin='anonymous'/>"; } return $css_path; } /** * @param string $name * @return string */ private static function _getJsPath(string $name): string { Util_Assert::string($name); if (Vite_Manifest_Production::$manifest === null) { return ''; } $public_path = Vite_Manifest_Production::getPublicPath(); $js_file = Vite_Manifest_Production::$manifest[$name]['file']; $js_path = Util_String::h($public_path . '/' . $js_file); $js_script = "<script type='module' src='{$js_path}' charset='utf8' crossorigin='anonymous'></script>"; $preload_script = ''; if (isset(Vite_Manifest_Production::$manifest[$name]['imports'])) { foreach (Vite_Manifest_Production::$manifest[$name]['imports'] as $import) { $path = Util_String::h($public_path . '/' . Vite_Manifest_Production::$manifest[$import]['file']); $preload_script .= "<link rel='modulepreload' as='script' crossorigin='' href='{$path}'>"; } } if (isset(Vite_Manifest_Production::$manifest[$name]['dynamicImports'])) { foreach (Vite_Manifest_Production::$manifest[$name]['dynamicImports'] as $import) { $path = Util_String::h($public_path . '/' . Vite_Manifest_Production::$manifest[$import]['file']); $preload_script .= "<link rel='modulepreload' as='script' crossorigin='' href='{$path}'>"; } } return $js_script . $preload_script; } ... }
HMR を使えるようにする
vite を使うのならやはり高速な開発サーバーと HMR を利用したいです。
pixiv では開発環境が開発サーバーで立ち上がっており、そこを見ることで動作確認を行っています。
そのため vite dev
で立ち上がる開発サーバーはそのままでは利用できません。
vite dev時にどのポートで開いているのかわかるようにする
vite dev
のどのポートで開発サーバーを開いているかという情報が取得できないので manifest.json
というファイルで取れるようにします。
vite dev
はビルドをしないので manifest.json
を吐き出しませんが owlsdepartment/vite-plugin-dev-manifest を利用することで吐き出すことができます。
このとき吐き出される manifest.json
は以下のような形式になります
{ // url to base folder in your local dev server "url": "http://localhost:3000/", // inputs specified in 'build.rollupOptions.input' "inputs": { "main": "src/main.ts" } }
この url
を見て開発サーバーが立ち上がってることを確認します。
前項で書いた Helper でも manifest.json
を読み取るようにしていたので開発サーバーの manifest.json
なのかを判定する必要があります。
owlsdepartment/vite-plugin-dev-manifest はデフォルトで manifest.dev.json
という名前で出力するので、そのファイルの有無で開発サーバーを利用するのかを判定します。
/** * @param string $path */ private static function _loadManifest(string $path): void { Util_Assert::string($path); $dev_manifest_path = Vite_Common::_getDevJsonPath($path); // ファイルの有無で `dev` か `prod` かを判断する if (file_exists($dev_manifest_path)) { Vite_Common::$env = 'dev'; } else { Vite_Common::$env = 'prod'; } } /** * @param string $path * @return string */ private static function _getDevJsonPath(string $path): string { return Vite_Common::$manifest_dir . '/' . $path . '.dev.json'; }
PHP 側にmanifest読み込みHelper実装
先程と同じように PHP 側に manifest.json
を読み込む Helper を作成します。
final class Vite_Manifest_Development { ... /** * @param string $name * @param string $sync_kind * @return string */ public static function getEntryPointHTML(string $name, string $sync_kind): string { Util_Assert::string($name); Util_Assert::stringInArray($sync_kind, ['sync', 'async', 'defer']); return Vite_Manifest_Development::_getEntryPointHTML(); } /** * @return string */ private static function _getEntryPointHTML(): string { if (Vite_Manifest_Development::$manifest === null) { return ''; } $localhost = Vite_Manifest_Development::$manifest['url']; $vite_client = "<script type='module' src='{$localhost}@vite/client'></script>"; $entry_src = $localhost . Vite_Manifest_Development::$manifest['inputs']['main']; $entry_path = Util_String::h($entry_src); $entry_script = "<script type='module' src='{$entry_path}'></script>"; $react_refresh = "<script type='module'> import RefreshRuntime from '{$localhost}@react-refresh' RefreshRuntime.injectIntoGlobalHook(window) window.\$RefreshReg$ = () => {} window.\$RefreshSig$ = () => (type) => type window.__vite_plugin_react_preamble_installed__ = true </script>"; return $react_refresh . $vite_client . $entry_script; } ... }
_getEntryPointHTML
の中身は公式ドキュメントを参考にしました。
あとは本番と同じように getEntryPointHTML
の中身を Smarty に埋め込みます。
HMR に使われる WebSocket の設定をする
基本的にはこの状態で動くと思うのですが、 pixiv の開発環境は https
なので HMR に使われる WebSocket の設定をする必要があります。
vite の開発サーバーは、デフォルトだと WebSocket のプロトコルに location.protocol
の値を参照しています。
(https://github.com/vitejs/vite/blob/main/packages/vite/src/client/client.ts#L23-L24)
https
のときは wss
が使われるのですが、WebSocket の接続先は localhost
で証明書を発行していないので ws
にする必要があります。
そのために、 vite.config.ts
の server.hmr.protocol
を明示的に指定する必要があります。
{ ... server: { hmr: { protocol: 'ws', } }, ... }
また、WebSocket の接続先に今の hostname が利用されてしまうので server.hmr.host
も明示的に指定する必要があります。
{ ... server: { hmr: { protocol: 'ws', host: 'localhost' } }, ... }
これで HMR が利用できるようになりました!
まとめ
少しだけ工夫をする必要がありましたがとても快適な開発体験を得ることができました!
vite を利用してますが、開発には React を使用しているので Vue 以外でも問題なく開発することができます。
みなさんも vite を使って最高の開発体験を手に入れてみてください!