🤔

Preact + TypeScript + MUI + Vite 環境構築備忘録(2022/01時点)

2022/01/23に公開

背景

諸事情により、reactで書いたコードの一部をpreactで書いて、気持ちばかりの軽量化を試した。
その時、ネット上の文献やブログ解説を漁ろうとしたけど、あまりにもReactが強いためか検索ノイズが酷い上、公式リファレンスがよくわからぬ。

なんとかビルドを通すまで進めて、プロダクトに載せられそうなとこまできたので、ここまでの環境構築時の知見をメモしておく。

事前知識

  • react 触ったことがあること
  • vite の基本的な使い方を知っていること
  • MUI, TypeScript についての解説はしない

手順

vite init

Vite:Getting Started の説明の通り、ウィザード形式でのイニシャライザを呼び出し、preact-tsを選択する。

$ npm init vite@latest
$ cd [project dir]
$ npm i

npm install

以下のものをインストールする。

  • Material-Ui v5 (通称 mui v5)
    • 現時点の最新を入れてしまえば、とりあえず動くみたい。
  • Day.js
    • Luxon, date-fns, js-joda でも良いと思う。理由はない。
  • preact-iso
    • preact内蔵のlazyではTypeScriptのエラーを超えられないので。
  • Recoil
    • 状態管理で一番盛り上がっているやつらしい。
    • 今までunstated-nextを使ってきたが、複数のProviderを作ったときスコープを意識するのが大変だったので、それよりはシンプルに使えるのが良い。
$ npm i @emotion/react @emotion/styled @mui/icons-material @mui/material dayjs preact preact-iso recoil

npm install -D

eslint とか prettier とか開発で使いたいものを入れる。イニシャライザで既に入っている依存も、バージョンが若干古い場合があるので、改めてインストールを実行する。

以下のプラグインだけはバージョン指定する。

  • eslint@7.32.0
    • 依存する別のプラグインが8.x台に対応していないので、これで固定して逃げる。
    • ぶっちゃけここらへんの最新トレンドを知らない...
$ npm i -D @preact/preset-vite @types/eslint @types/eslint-plugin-prettier @types/node @types/prettier @types/sass @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint@7.32.0 eslint-config-preact eslint-config-prettier eslint-plugin-preact eslint-plugin-prettier jest prettier sass typescript vite vite-plugin-replace 

srcとindex.htmlのデフォルト位置変更

viteの基本設定の思想が個人的に苦手なので、ディレクトリ位置を変える。

$ mkdir vite
$ mv src vite/
$ mv index.html vite/

開発中/ビルド時のディレクトリ指定は次項目にて。

tsconfig.jsonの修正

基本設定は、各自の好みをベースにすること。以下の記述は、今回の開発で必要そうな設定についてだけを抽出。

tsconfig.json
{
  "compilerOptions": {
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": false,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "jsxFactory": "h",
    "jsxFragmentFactory": "Fragment",
    "baseUrl": "./",
    "paths": {
      "@/*": ["vite/src/*"],
      "AppConfig": ["vite/src/config/development.ts"],
      "react": ["preact/compat"]
    }
  },
  "include": ["vite/src"]
}
  • path
    • "@/*"
      • srcディレクトリをルートとして絶対パス開発を行えるようにする。
      • これをしておくと、開発途中のディレクトリごと移植したりするときに便利。
    • "AppConfig"
      • DEV/PRODの設定値を切り替えられるようにするために指定する。
    • "react"
      • reactの記述をpreactに流す。プラグインの利用においては、ほぼ必須。
  • include
    • ソースコードは、先ほど移動した配置になるで、ここも正しく修正する。

vite.config.tsの修正

vite.config.ts
import preact from "@preact/preset-vite";
import dayjs from "dayjs";
import path from "path";
import { defineConfig } from "vite";
import { replaceCodePlugin } from "vite-plugin-replace";
import packageJson from "./package.json";

const PRODUCTION = "production";
const DEVELOPMENT = "development";

// -- vite は、production or undefined で判定されるので、明示的に変数を生成する。
const environment = process.env.NODE_ENV === PRODUCTION ? PRODUCTION : DEVELOPMENT;
const is_production = environment === PRODUCTION;
console.log(`is_production:${is_production} -- ${environment}\n`);

const root = `${process.cwd()}/vite`;

// https://vitejs.dev/config/
export default defineConfig({
    root: root,
    esbuild: {
	jsxFactory: "h",
	jsxFragment: "Fragment",
	jsxInject: `import { h, Fragment } from 'preact'`
    },
    resolve: {
        alias: {
            // -- tsconfig の設定をviteにも反映させる。
            "@/": `${path.resolve(root, "src")}/`,
            AppConfig: `${path.resolve(root, `src/config/${environment}`)}`,
            react: "preact/compat"
        }
    },
    build: {
        outDir: `${path.resolve(__dirname)}/dist`,
        emptyOutDir: true,
        assetsDir: "assets",
        minify: is_production ? "terser" : undefined,
        chunkSizeWarningLimit: 800,
        rollupOptions: {
            input: {
	        // -- マルチエンドポイントで開発するならここを増やす
                index: `${path.resolve(root, "index.html")}/`
            },
            output: {
                manualChunks: {
                    vendor: ["preact", "preact-iso", "recoil"],
                    mui: ["@emotion/react", "@emotion/styled", "@mui/icons-material", "@mui/material"]
                },
                entryFileNames: "assets/[name].[hash].js",
                chunkFileNames: is_production ? "assets/[hash].js" : "assets/[name]-[hash].js",
                assetFileNames: is_production ? "assets/[hash][extname]" : "assets/[name]-[hash][extname]"
            }
        },
        terserOptions: is_production
            ? {
            // ... 略
        } : undefined
    },
    plugins: [
        preact(),
        replaceCodePlugin({
            // -- AppConfig にビルド日とかを埋める処理など好みで.
        })
    ]
})

eslint/prettierの設定

割愛。正しく設定しないとvscodeなどで動いてくれない。

2022/01/30 追記

npx eslint --init
が便利だと知った。今まで手でやっていたのはなんだったのか

開発

index.tsx
// -- 個人的に、機能別に /vite/src/feature/[機能] で区切る
import IndexEntry from "@/feature/index/IndexEntry";
import { render } from "preact";

const rootDiv = document.getElementById("app")!;
render(<IndexEntry />, rootDiv);
IndexEntry.tsx
import { CssBaseline, ThemeProvider } from "@mui/material";
import { ErrorBoundary, lazy } from "preact-iso";

// -- lazy で書いておくことで、code splittingが効く.
const AnyViewLazy = lazy(() => import("./AnyViewLazy"));

const IndexEntry = () => {
  return (
    <ThemeProvider>
      <CssBaseline />
      <main>
        <ErrorBoundary>
          <AnyViewLazy />
        </ErrorBoundary>
      </main>
    </ThemeProvider>
  );
};

export default IndexEntry;

後はviteのお作法で npm run dev / build / serve を駆使して開発を進める。

preact-isoを用いたコード分割

「Preactでマルチエンドポイントを用意して開発」なんてシチュエーションの場合、vite先生優秀だからそれぞれのエンドポイントから上手にjsをコード分割してくれる...なんて思ってビルドしたら、2MBを超えるでっかいjsが生成されて🤔🤔🤔になった。

まずは、manualChunksである程度分解するにしても、それ以外の部分で共用コードが合体しちゃったりするので、lazyを使って明示的にコード分割をさせるのが手っ取り早い。

先のソースコードのこの部分がそれに該当する。

IndexEntry.tsx
// -- 略
import { ErrorBoundary, lazy } from "preact-iso";
const AnyViewLazy = lazy(() => import("./AnyViewLazy"));

const IndexEntry = () => {
  return (
// -- 略
        <ErrorBoundary>
          <AnyViewLazy />
        </ErrorBoundary>
// -- 略
  );
};
// -- 略

シンプルなPreactプロジェクトにおいてなら、manualChunksだけで綺麗にコード分割が効くはずだけど、Route処理で多くのページを作ったり、マルチエンドポイントで切り分けるような開発になるのであれば、開発環境を発す時点でlazyを使えるようにしておくべき。

所感

  • Preactというのが「Reactからちょっと変更すれば良いだけですよ〜」ってのがウリだけど、実際に環境を作ると、結構しんどかった。
  • だけど、環境さえなんとかなれば、Reactと「ほぼ」同じような開発が行える。
    • 「ほぼ」というのがミソで、ちょっとトリッキーなコードを書くだけで、型で怒られる。
    • 特に、React用のプラグインなどで自作エレメントを混ぜて使うようなシチュエーションがあったとき、Preactで読み替えられない型を指定されていることがしばしばあって、自作のPreactコンポーネントだと型チェックエラーが起きる。それを無視するようにしても、今度はビルド時にエラーになる。どうしようもない。
  • ぶっちゃけ、バンドルサイズがシビアじゃない現場であればReactを使用すべきであり、本当にシビアであるならReactでもPreactでもなく、せめてSvelteを採択する選択肢が今なら取れるはずである。
    • あくまで開発責任を持った開発についての話として。
  • 一方、個人的に以前Svelteを触って「サイズが小さい!すごい!!」とキャッキャしていたけど、今回こうやってPreactの環境をなんとかできたので「Reactと遜色なく使える!ちょっとサイズ小さい、便利!!」ってなってる。
    • Reactばっかり触っていると、Reactに関連したものに縋りたくなる病気になってしまったかもしれない。

ということで、環境についてここまで書いておいてなんだけど、お金と責任が絡んでいるなら、(開発途中で面倒ごとになりたくないので)Reactが良い。

追記/2022/07/30

react 18対応周りの影響で、mui v5の現行最新とpreact現行最新でビルドが通らなくなってる。
muiのバージョンをreact18対応以前のもので固定しといた方が良さそう。
こういうトラブルの解決が厄介だし、初めからreactにすれば良かったなんて思ってる。

Discussion