🔥

Viteで開発環境と本番環境の挙動が異なったので原因を調べた

2024/07/12に公開2

Viteを用いたReact開発を行っている際、vite devで立ち上げたDevモードと実際にビルド&デプロイしたProdモードで挙動が異なるという問題が発生したので、その原因究明メモです。

そもそもViteとは

Viteはフロントエンド向けビルドツールです。2020年に登場し2024年現在ではフロントエンド領域で幅広く使われるツールとなっています。GitHubのstar数で見ると、ビルドツールとしてこれまで多く利用されてきたwebpackを追い越しています。

特徴としては高速な開発サーバー立ち上げと高速なHMRです。ブラウザ上で直接実行できるというESModuleの特性をフル活用し、webpackのように全体をバンドルしてから開発サーバを立ち上げるのではなく、まず開発サーバを立ち上げその後モジュールを読み込んでいくことで高速な開発サーバ起動を実現します。また、事前バンドルという過程で外部パッケージ等をesbuildでESModule形式にビルドしておきそれをブラウザ上で積極的にキャッシュすることも行います。

発生した問題

React + Viteで次のようなコードを書いた際、Devモードでは問題なく動作したものの、ビルド成果物を実行したらxx is not functionというエラーが発生しました。どうやらビルド成果物を実行した際に関数でない何かしらのオブジェクトが関数呼び出しされていることが原因のようです。

...
import * as dayjs from "dayjs";

const Component = () => {
  ...
  return (
    <>
      ...
      dayjs("2024-01-01")
      ...
    </>
  )
}

以下の点がポイントです。

  • dayjs(CommonJS形式)をnamespace import(import *)している
  • dayjsを関数として呼び出している

原因はシンプルで、namespace objectであるdayjsを関数呼び出ししてるのが問題です。ESModuleにおいてはnamespace importするとモジュール全体が一つのオブジェクトとしてimportされます。objectですのでもちろん関数呼び出しをすることができません。Day.jsの公式Docsには(特定のtsconfigの設定がない限り)namespace importをするように記載があったのでそれに従ったのですが、誤りだったようです。

問題の解決策

解決策としてはnamespace importをやめてdefault importにすればOKです。つまり以下のようにコードを修正します。

import dayjs from "dayjs";

// 以下同じ

default importをすることによって、module.exportsを通じてexportされたものを直接importすることができます。Dayjsの場合、dayjs関数が直接exportされているので、default importすることによってそれをそのまま関数として利用できます。

もしくはnamespace importのままでdayjs.default("2024-01-01")と呼び出す形でもOKです(この場合dayjsはオブジェクトであり、defaultというプロパティがdayjs関数にあたる)。

tsconfigの設定でそもそも防げた

そもそもIDEツール上でエラーを出すためにはtsconfig.jsonの設定を以下のようにすべきでした。

{
  "compilerOptions": {
    ...
    "esModuleInterop": true,
    ...
  }
}

esModuleInteropはESModuleとCommonJSの互換性を確保するためのオプションです。これを有効にするとトランスパイル時にヘルパー関数を挿入しCommonJSをESModuleに適合した形(namespace importをobjectとしてimportする)でimportすることができるようになります。この設定をすることで、namespace importしたオブジェクトを関数呼び出しすると、IDE上で型エラーをキャッチすることができます。

ちなみにesModuleInteroptrueであれば自動的にallowSyntheticDefaultImportsというオプションも自動的に有効化されます。このオプションははdefault exportが存在しないモジュールからのdefault importを許可するオプションで、CommonJSモジュールのdefault importが型チェック上可能になります。

以上の通り、原因自体はシンプルでそりゃそうだなという感じなのですが、ここで問題なのは なぜDevでは正常に動きProdではエラーとなったのか? です。

なぜDevとProdで挙動が変わったのか?

結論から言うとViteはDevモードにおいてnamespace importをdefault importに置換していることが原因です。事前バンドルフェーズにおいてESModule形式に変換されたdayjsライブラリをdefault importでimportしているためエラーが発生しません。

実際にDev Toolのイニシエーターを見てみると、以下のようにnamespace importがdefault importになっていることが分かります。

次にVite内部のコードを見てみます。以下の通りnamespace importがdefault importに明示的に書き換えられています。

https://github.com/vitejs/vite/blob/6cccef78a52492c24d9b28f3a1784824f34f5cc3/packages/vite/src/node/plugins/importAnalysis.ts#L1000-L1024

CommonJS形式のモジュールは事前バンドルフェーズにおいてESModule形式にビルドされます。このときmodule.exportsされているものはdefault exportとして取り扱われるようになります。しかし、トランスパイルの結果やモジュールの呼び出し方によってはnamespace importをしてしまうとエラーとなる可能性があります。ESModule形式のモジュールはdefault importで問題なく動作するため、Viteは開発者がimport形式やモジュール形式を意識しなくてもいいように内部でdefault importに書き換えているのだと考えられます。

教訓

ViteでDevモードとProdモードで挙動が異なる可能性があることが分かりました。ではViteを使うのをやめるべきなのでしょうか?もちろんそんなことはなく、ここから得られる教訓は2つです。

  1. vite devだけじゃなくてvite previewもしよう

vite buildでビルドしたものをvite previewでビルド成果物を実行することができます。実際のビルド成果物を確認することができるので本番に近いものを試すことができます。今回の問題もvite previewしておけばローカルでも気づくことができました。

  1. tsconfigを適切に設定しよう

今回の問題は先述の通りtsconfigを適切に設定しIDEツール上で型エラーにしてあげれば早期に気づくことが可能でした。なるべく型エラーの方に不具合を寄せてあげてビルドツール固有の問題にはしないであげることが重要そうな気がします(Viteに限った話ではないと思いますが)。

参照

GitHubで編集を提案
Bitkey Developers

Discussion