vite + preactで理解するviteの依存管理
やった
本当はviteで作る超速モノレポ環境!!をやりたかったが、やっていくうちに一番大事なところが微妙になったので調査の知見を残すことにした(なった)
viteとは
そもそもviteがなにかというと、今までバンドル作ってそれをwatchしてたけどなんらかの手段でHMRが実現できたら開発時はバンドルせんでもよくね?という発想から生まれた、バンドルを作らない開発サーバである。(alt VuePressであるVitePressがやりたかっただけ説もあるけど)
仕組みとしては、ブラウザが解釈できる形になんらかの方法でファイルを加工(tsxならtsc、sfcなら@vue/compiler-sfc)してESM形式で読み込ませることでバンドルを作らずにブラウザに即時反映している(と理解している)。
バンドルを作らないためwebpackのhmrの比ではないほどの反映速度で、体験的にはSnowpackに近い。今回はモノレポで親側をvite、子をpackages配下からルートのnode_modules経由でシムリンクにしてコンポーネントライブラリにしたらめちゃくちゃ体験いいんじゃねって思ってやったが、後述する依存最適化周りでうーんって感じになった。
モノレポの説明とかツールとかの説明は省きます。yarn workspace: trueにしてlernaコマンド書いてるだけ。
環境構築
viteのエントリーポイントがある方のpackage.jsonにはコンポーネントライブラリとなる@vite-playgorund/base
とpreactをdepsに入れる。
preactを使う場合は--jsx-factory=h
フラグをつける。
バージョン指定で"*"をつけると、パブリッシュしていなくてもモノレポ内部から同名のpackage.jsonを読み出してシムリンクを張ってくれる。これで@vite-playgorund/base
側のコンポーネントをいちいちビルドせずとも直接読み出すことができ、親側で巻き込んでビルドすることができる。
app/vite-app/package.json
{ "name": "vite-playground", "version": "0.0.0", "scripts": { "dev": "vite --jsx-factory=h", }, "dependencies": { "@vite-playground/base": "*", "preact": "^10.4.1" } }
vite側ではindex.htmlでエントリーポイントを読み込む。
app/vite-app/index.html
<div id="app"></div> <script type="module" src="./main.tsx"></script>
通常のReactアプリケーションと同様にmain.tsxでルートコンポーネントをマウントすれば良い。
app/vite-app/main.tsx
import { h, render } from 'preact' import { useState } from 'preact/hooks' // @ts-ignore import { BaseButton } from '@vite-playground/base' const Counter = () => { const [count, setCount] = useState(0) const increment = () => setCount(count + 1) const decrement = () => setCount((currentCount) => currentCount - 1) return ( <div> <p>Count: {count}</p> <BaseButton text="Increment" handleClick={increment} /> <BaseButton text="Decrement" handleClick={decrement} /> </div> ) } // @ts-ignore render(<Counter />, document.getElementById('app'))
@vite-playground/base
からコンポーネントを読み込んでいる。re export用のindex.jsとcomponents/BaseButton.tsxがあるだけのシンプルな内部パッケージで、自力でビルドする環境はない。
packages/base/BaseButton.tsx
import { h } from 'preact' interface Props { handleClick: () => void; text: string; } const BaseButton = (props: Props) => { return <button onClick={() => props.handleClick()}>{props.text}</button> } export default BaseButton
この状態でyarn dev
すると、viteが起動してBaseButtonの解決も行われてエラーもなく動作する。
これでBaseButton.tsxを変更すれば瞬時に画面反映されるんじゃ!やっぱモノレポなんだよなぁと息巻いていたが普通に変更無視されて虚無になった。くやしいので原因を書く。
depOptimizer
depOptimizerはそもそもページロードの高速化のための関数だが、事前にバンドルされていない依存が通るとそれをビルドする。生成物はキャッシュとしてnode_modules/.vite_opt_cacheの配下に置かれる。
vite.config.jsで無効にできるが、無効にするとビルドされていないnode_modules配下のコンポーネントなどは当然読み込むことができないためエラーになる。(やたらとtext/planeを読み込んどるぞというコンソール出力を目にすることになる)
https://github.com/vitejs/vite/blob/master/src/node/depOptimizer.ts
依存がバンドルを生成しているとキャッシュは作られないので普通にvite側に反映される。ただしページは自前でリロードする必要があるし、結局子はバンドルしとるのでお得感があんまりない。
@vite-component/base
側で以下のようなrollupの設定を書いてwatchしたらBaseButton.tsxの編集がちゃんと反映されるようになった。
packages/base/rollup.config.js
import preact from 'rollup-plugin-preact' import typescript from 'rollup-plugin-typescript2' export default { input: { ui: 'index.ts', }, output: { dir: 'dist', entryFileNames: 'bundle.js', format: 'esm', sourcemap: false, }, plugins: [ typescript(), preact(), ], external: ['preact', 'preact-compat', 'preact-compat2'], }
ちなみにwebpackなどでesm形式以外でビルドしていた場合、以下のような丁寧な標準出力を吐いてくれる。
vite-playground: [vite] The following dependencies seem to be CommonJS modules that vite-playground: do not provide ESM-friendly file formats: vite-playground: @vite-playground/base vite-playground: - If you are not using them in browser code, you can move them vite-playground: to devDependencies or exclude them from this check by adding vite-playground: them to optimizeDeps.exclude in vue.config.js. vite-playground: - If you do intend to use them in the browser, you can try adding vite-playground: them to optimizeDeps.commonJSWhitelist in vue.config.js but they vite-playground: may fail to bundle or work properly. Consider choosing more modern vite-playground: alternatives that provide ES module build formts.
最適化から外すなりホワイトリストに入れるなりなんかした方がいいぜって感じ。気を使ってexcludeに入れてくるのでキャッシュも作られない。優しすぎない? 依存側もrollup使っておけば問題はなさそう。webpackの場合はめんどくさい。
おわり
理想はviteだけで全てのモノレポ内依存が解決され超速hmrが動き出荷するときにvite build
一発で終わる世界だったが、そんなに美味しい話はなかった。
今回preactを使ったのはVueでやるとcompiler-sfcの依存とかが必要でめんどくさいのと前にdepOptimizerを見たときは拡張子決め打ちで.vueを対応してなくてpreactうごくyoって言ってたから。いつのまにかvitejs/viteにリポジトリも移動してるしVueだけのものとして扱わない方針っぽい。
冒頭でも触れたけどSnowpackとは今後連携して動作するようにしていくらしいので、引き続き理想のモノレポ環境を試していきたい。
余談
vite、ヴァイトだと思ってたけどヴィッテ、ヴィーテが発音的には正しいらしい。
はやそう