本文へジャンプ

流行りのフロントエンド開発環境を使ってみる pnpm ✕ Vite ✕ Typescript

Posted by kenta sugiyama

以前こちらにも書きましたが、
MONSTER DIVEではベースとなるフロントエンド開発環境を用意して日々開発を行っています。

Node.jsnode_modules のアップデートやメンテナンスはもちろん行っていますが、2年たった現在でもこちらの環境は現役で使用しています。
JSフレームワークに対応してはいないので、2年の間に Next.js の環境を作ったりはしましたが、
様々なオーダーがある開発においてメンテナンスや調整だけで対応できているのは、それはそれで成功だったのかと思います。

しかし、JSフレームワークに流行があるように開発環境にも流行り廃りはあり、
実際に案件で使用しないにしても情報としては最新のものをキャッチアップし、時には試してみる必要があります。
そこで今回TypeScriptはいままでと特に変わらないですが、
パッケージマネージャーにpnpm
ビルドツールにVite
を使用した環境を構築したのでその紹介をしたいと思います。

pnpm

pnpmは、npmやyarnと同じくパッケージマネージャーです。
ですが、他のパッケージマネージャーに比べると以下のようなメリットがあるようです。

  • ディスク容量の節約
  • インストール速度の向上
  • フラットではない node_modules ディレクトリの作成

詳しくは公式サイトの説明を読んでいただければですが、GitHubを見てみると、npmyarnと比べて最大2倍程度速度が速いことが書かれています。
実際に使ってみて2倍かは定かではないですがnpmyarnと比べて速いように感じました。

pnpm導入方法

Mac / WindowsそれぞれHomebrew / PowerShell があればコマンドひとつでインストールできます。

Macの場合

brewを使ったインストール方法

$ brew install pnpm

Windows(PowerShell の使用)の場合

PowerShellを使ったインストール方法

$ iwr https://get.pnpm.io/install.ps1 -useb | iex

またはnpmを使ってインストールもできます。 npmを使ったインストール方法

$ npm install -g pnpm

導入は簡単ですね。

Vite

Viteは高速な開発環境を構築することができるフロントエンドのビルドツールになります。
公式サイトが日本語化されており、ドキュメントも日本語で読むことができます。
開発者がVue.jsの作者であるEvan You氏であるため、Vue.jsのツールであると誤解されることもありますが、プレーンなJavaScript(Vanilla JS)からVue.js・React・Svelteといった流行りのフレームワークまで、さまざまな環境で利用できる汎用的なツールで以下のような特徴があります。(一部)

  • NPM の依存関係の解決と事前バンドル
  • TypeScriptやVue/React等のライブラリが設定なしですぐに使える

WebpackやRollupなど従来のバンドルツールは、アプリケーションの起動前にアプリケーション全体を走査、依存解決し、バンドルしたソースコードを出力していました。
これに対してViteは開発時は事前に依存解決と最低限のバンドル(pre-bundle)だけ行い、全体をバンドルすることなくESModulesのimportを介してソースコードを読み込むことで高速な開発サーバを提供してくれます。

Viteでは一般的な開発での面倒な設定が不要です。
シンプルなJavaScriptだけのウェブページからReact.jsとTypeScriptを使った本格的なウェブアプリケーションまで、ほとんど同じ早さで開発を始めることができます。

Vite導入方法

他のパッケージマネージャーでも同様ですがコマンドひとつでインストールできます。

pnpmの場合

$ pnpm create vite

あとは画面表示に従って選択していくだけです。

Viteのカスタマイズ

Viteの導入まではここまでで完了ですが、普段の環境に合わせてカスタマイズしていきます。

ejsの導入

MONSTER DIVEのエンジニアはマークアップするのにテンプレートエンジンを使用していますが、ejsの使用率が高いです。
Viteでejsを利用する場合、vite-plugin-ejsというViteのプラグインを使用します。
このプラグインを利用するとベースのページファイルは.htmlですが、ファイル内でejsの記法が使用できるようになります。
インクルードは.ejsファイルを利用することができます。なんとも不思議な感じです。

HTMLを複数出力したい時の設定

Viteの初期設定ではHTMLファイルは1つにまとめられてしまうので、HTMLを複数出力したい時には、vite.config.tsに出力するページを追記していく必要があります。

import { defineConfig } from 'vite';

//import設定を追記
import { resolve } from 'path';

export default defineConfig({
  root: './src', //開発ディレクトリ設定
  build: {
    outDir: '../dist', //出力場所の指定
    rollupOptions: { //ファイル出力設定
      output: {
        〜省略〜
      },
      input: {
        index: resolve(__dirname, './src/index.html'),
        /*
          複数HTMLページを出力したい時にここへ追記していく
          xxx: resolve(__dirname, './src/xxx.html'),
        */
        list: resolve(__dirname, './src/list.html'),
      },
    },
  },
});

ページ数が少ないウェブサイトでは個別に足していくでも良いですが、大規模サイトでは1ページ1ページ足していくわけにもいかないので工夫が必要です。(後述)
よくよくJSフレームワークでSSGするなどの開発との親和性が高いように感じます。

SASSの導入

SASSについてはHTMLで type="module" として読み込んだTypeScriptファイル内でimportすればbuild時にCSSとして読み込んでくれます。

import '../../../sass/style.scss';

assetの利用

画像などを格納するのに/assetを利用できますが、自動でハッシュ化されます。
例えば、開発中は /img.png となり、本番用ビルドでは /assets/img.2d8efhg.png となります。
またassetディレクトリ内の子ディレクトリまでしか認識されないようで、孫ディレクトリを設置しても無視されてしまいます。
ハッシュ化したくない、ファイルを細かく分類したい等の場合はpublicディレクトリにアセットを置くことで変換なくそのまま出力することができます。

pnpm ✕ Vite ✕ Typescriptの環境を使ってみて

pnpmについては速度、ディスク容量を圧迫しない点からも今使用しているyarnから置き換えても良いかと思えるものでした。
逆にViteは規模感、使用ツール(JSフレームワーク)、CMS連携など考えると使用するシーンを選ぶな、というのが所感です。
ページ数が多い、CMSが入る企業サイトでは設定次第かもしれないですが不向きな印象で、JSメインの開発案件では積極的に利用したいと感じました。

assetに格納すればwebpに変換も自動でしてくれるので便利ですが、ディレクトリでの階層化が難しいというのは個人的にはいただけない。
ページ数が多い、CMSが入る企業サイトには不向きな印象で、しっかりを設計した上で導入しないと思わぬ落とし穴がありそう。
逆にLPなどの単発物、スピード優先で開発するのもには向いているのかなと思います。

また、JSメインの開発案件では積極的に利用したいと感じました。

おまけ

Viteの環境を更に使いやすくするために1ページ1ページinputに追記する必要がある複数HTML出力の自動化と、
環境変数に応じて出力するHTMLを分ける設定を追加しました。

複数出力の自動化はこちらの記事を参考にしましたが、
案件でCMS出力するページとそのままアップロードするページの分ける必要があったため、
build時はそのまま出力し(マークアップ確認用)、production build時は指定のファイルのみ出力するようにしました。

■vite.config.ts

import fs from 'fs';
import path, { join, resolve } from 'path';

import react from '@vitejs/plugin-react';
import { defineConfig, loadEnv } from 'vite';

interface File {
  name: string;
  path: string;
}
interface InputFile {
  [key: string]: string;
}
const files: File[] = [];
// 静的アップするファイルのパスを指定
const uploadHtml = [
  '/xx/yyyy/zzzzzzz/index.html',
  '/xx/yyyy/zzzzzzz/index2.html',
];

export default defineConfig(({ mode }) => {
      const envPrefix = ['VITE_', 'APP_ENV'];
      const env = loadEnv(mode, '.', envPrefix);
      const readDirectory = (dirPath: fs.PathLike): void => {
      const items = fs.readdirSync(dirPath);

      for (const item of items) {
        const itemPath = path.join(dirPath as string, item);

        if (fs.statSync(itemPath).isDirectory()) {
          // componentsディレクトリを除外
          if (item !== 'components') {
            readDirectory(itemPath);
          }
        } else {
          // htmlファイル以外を除外
          if (path.extname(itemPath) !== '.html') {
            continue;
          }

          // nameを決定
          let name;
          if (dirPath === path.resolve(__dirname, 'src/html')) {
            name = path.parse(itemPath).name;
          } else {
            const relativePath = path.relative(
              path.resolve(__dirname, 'src/html'),
              dirPath as string
            );
            const dirName = relativePath.replace(/\//g, '_');
            name = `${dirName}_${path.parse(itemPath).name}`;
          }

          // pathを決定
          const relativePath = path.relative(
            path.resolve(__dirname, 'src/html'),
            itemPath
          );
          const filePath = `/${relativePath}`;

          files.push({ name, path: filePath });
        }
      }
    };
    readDirectory(path.resolve(__dirname, 'src/html'));
    const inputFiles: InputFile = {};
    for (let i = 0; i < files.length; i += 1) {
      const file: File = files[i] as File;
      if (env.VITE_APP_ENV === 'production') {
        if (uploadHtml.indexOf(file.path) !== -1) {
          inputFiles[file.name] = resolve(__dirname, `${file.path}`);
        }
      } else {
        inputFiles[file.name] = resolve(__dirname, `./src/html${file.path}`);
      }
    }
  };
});
Recent Entries
MD EVENT REPORT
What's Hot?