うしろのこの本ください

なんでもかきます

vite + preactで理解するviteの依存管理

やった

github.com

本当はviteで作る超速モノレポ環境!!をやりたかったが、やっていくうちに一番大事なところが微妙になったので調査の知見を残すことにした(なった)

viteとは

github.com

そもそもviteがなにかというと、今までバンドル作ってそれをwatchしてたけどなんらかの手段でHMRが実現できたら開発時はバンドルせんでもよくね?という発想から生まれた、バンドルを作らない開発サーバである。(alt VuePressであるVitePressがやりたかっただけ説もあるけど)

仕組みとしては、ブラウザが解釈できる形になんらかの方法でファイルを加工(tsxならtscsfcなら@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の解決も行われてエラーもなく動作する。

f:id:apple19940820:20200526013102g:plain

これで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'],
}

f:id:apple19940820:20200526024840g:plain

ちなみに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、ヴァイトだと思ってたけどヴィッテ、ヴィーテが発音的には正しいらしい。

はやそう

f:id:apple19940820:20200526103313p:plain