Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby on Rails: webpack + SimpackerからVite + Vite Railsに移行するメモ📝

最近個人のサービスのフロントエンド周りをタイトル通りwebpack + SimpackerからVite + Vite Railsに移行したので対応したこととかをメモ

前提事項

  • Vite v.3.2系のHMRは使わない構成になります
    • Vite Rubyを使うとbin/viteで起動しないといけないっぽかったのでフロントエンドのbuildをrubyのコマンドで行うのがちょっと違和感があった。。。
  • フロントエンド系のライブラリはVue.js v3系、TypeScriptを利用しています。
  • Railsは v7.0系、Vite Railsはv3.0系を利用しています

Viteで既存をファイルをbuildしてpublic/pack配下に吐き出すようにする(webpack -> Vite)

Viteの導入方法や各種設定の詳しい説明は省きますが、私は以下のようなvite.config.tsを作成してapp/javascript/packs配下のエントリーをViteでbuildし、webpack + simpacker時代と同じようにpublic/packs or public/packs-testに出力するようにしました。(buildの設定は既存のwebpackの構成によって異なるので自身の環境に合わせて変更してください)

ja.vitejs.dev

import { defineConfig } from "vitest/config";
import vue from "@vitejs/plugin-vue";
import EnvironmentPlugin from "vite-plugin-environment";
import * as glob from "glob";
import * as path from "path";

/**
 * 指定したentryのルートディレクトリ配下のjsまたはtsファイルのファイル名とパスのobjectを取得
 * 例)
 *  - /entries/foo.ts => { foo: "/entries/foo.ts" }
 *  - /entries/bars/bar.ts => { "bars/bar": "/entries/bars/bar.ts" }
 * @param {string} entryRoot entryのルートディレクトリ
 */
const getEntries = (entryRoot) => {
  const result = [];
  const filePaths = glob.sync(`${entryRoot}/**/*.{js,ts,html}`);
  filePaths.forEach((filePath) => {
    const dirName = filePath.replace(entryRoot, "").replace(path.basename(filePath), "");
    result[`${dirName}${path.basename(filePath, path.extname(filePath))}`] = filePath;
  });
  return result;
};

const JAVASCRIPT_ENTRY_PATH = "./app/javascript/packs/";
const outDirPath = (mode: string) => (mode === "test" ? "public/packs-test" : "public/packs");

export default defineConfig(({ command, mode }) => {
  return {
    build: {
      manifest: "manifest.json", // vite_rubyはmanifest.jsonを固定で参照するので名称を固定
      rollupOptions: {
        input: { ...getEntries(JAVASCRIPT_ENTRY_PATH) },
      },
      copyPublicDir: false, // public配下の既存ファイルがpublic/packs配下にコピーされないようにした。
      outDir: outDirPath(mode), // modeでテスト環境ではpublic/packs-testにbuildするようにした
      assetsDir: "", // public/packs/assets配下にbuildされないようにassetsDirには空白を設定
    },
    css: {
      postcss: "postcss.config.js",
    },
    plugins: [
      vue(),
      EnvironmentPlugin({
        NODE_ENV: ""
      }),
    ],
    resolve: {
      alias: {
        vue: "vue/dist/vue.esm-bundler.js",
        "@js": `${__dirname}/app/javascript`,
        "@css": `${__dirname}/app/javascript/stylesheets`,
      },
    },
  };
});

webpackからViteに移行する際に発生したエラー等

requireが使えずにbuild時にエラーになる

requireを使って画像等を読み込んでいる部分がエラーなるのでimportする方針に変更し、 画像のpathの解決が明示的にしとかないのルート/で参照してしまうのでmoduleを作ってpacks配下を明示するようにしました。

多分この辺が影響してそう(?)

https://ja.vitejs.dev/guide/assets.html#new-url-url-import-meta-url

++ import noImage from "@js/assets/noimage.png";
++ import { imageUrl } from "@js/utils/common/ImageUrl";
++ return imageUrl(noImage as string);
-- return require("@js/assets/noimage.png") as string
export const imageUrl = (image: string) => {
  const nodeENV = process.env.NODE_ENV;
  const envPrefix = nodeENV == "test" ? "packs-test" : "packs";
  return `/${envPrefix}${image}`;
};

ViteでbuildしたファイルをRailsで読み込む(Simpacker -> Vite Rails)

Viteで吐き出したファイルを読み込むにはVite Railsを使うと諸々いい感じに実装できるので便利です💎✨(導入方法等は公式ガイドを参照いただければと思います)

vite-ruby.netlify.app

※当初以下の参考にSimpacker経由でViteでbuildしたファイルを読み込もうとしましたが、cssのchunk周りがViteでは調整が難しく利用が複雑になってしまいそうだったのでVite Railsを素直に使う方針としました。

text.hmsk.me

HMRを使わないのであれば設定はシンプルでconfig/simpacker.ymlconfig/vite.jsonに置き換えて、

{
  "all": {
    "sourceCodeDir": "app/javascript",
    "watchAdditionalPaths": []
  },
  "production": {
    "publicOutputDir": "packs"
  },
  "development": {
    "publicOutputDir": "packs"
  },
  "test": {
    "publicOutputDir": "packs-test"
  }
}

既存のjavascript_pack_tag等を置換するのが大変そうに思ったので以下のようなapp/helpers/vite_pack_helper.rbを用意してApplicationHelperにincludeすることでViewへの変更は行わずにVite RailsのHelperに移譲するようにしました。 Vite RailsはHMRの利用が前提のためかmanifestの読み込みが初回起動時に行われなかったので、開発環境では明示的にViteRuby.instance.manifest.refreshを呼び出しmanifestを再読込するようにしました。

# frozen_string_literal: true

module VitePackHelper
  include ViteRails::TagHelpers

  def javascript_pack_tag(*names, typescript: true, **options)
    refresh_manifest unless cache_manifest?
    paths = names.map { |name| entry_path(name) }
    return vite_typescript_tag(*paths, **options) if typescript
    vite_javascript_tag(*paths, **options)
  end

  def stylesheet_pack_tag(*names, **options)
    refresh_manifest unless cache_manifest?
    paths = names.map { |name| entry_path(name) }
    vite_stylesheet_tag(*paths, **options)
  end

  def image_pack_tag(name, **options)
    refresh_manifest unless cache_manifest?
    vite_image_tag(assets_path(name), **options)
  end

  private

  def javascript_root_path
    'app/javascript/'
  end

  def entry_path(name, entry_dir: 'packs')
    File.join(javascript_root_path, entry_dir, name)
  end

  def assets_path(name, assets_dir: 'assets')
    File.join(javascript_root_path, assets_dir, name)
  end

  def refresh_manifest
    ViteRuby.instance.manifest.refresh
  end

  def cache_manifest?
    # NOTE: vite_rubyはHRM前提からかリクエスト時にmanifestのrefreshは行なわれなので開発環境では明示的に行う。
    !Rails.env.development?
  end
end

おわりに

esbuild-loaderを使っていたこともあり、そこまでbuild速度とかは変わらなかったのですが、webpackかViteに乗り換えると依存パッケージが私の個人サービスでも10個程度減らせたのと、VitestやHistoireといった他ライブラリと設定を共有できるのが、非常に管理しやすくて良いですね⚡️

Vite Railsのおかげで、意外と既存との差分も少なくwebpack + Simpackerから乗り換えられるのも体験が良かったです💎✨

参考

zenn.dev