29
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ejs, pugやめてtsxでhtml書かね?

Last updated at Posted at 2022-10-06

はじめに

htmlテンプレートエンジン選定のお話です。
『モダンフロントエンド』とは呼ばれないような案件では、まだまだejs, pugなどのhtmlテンプレートエンジンライブラリを用いてhtmlを記述、納品しています。
今回はそんなテンプレートエンジンとしてtsxを採用しようじゃないかというお話です。

結論

tsxファイルをreact-domの機能であるrenderToStaticMarkupから文字列化し、htmlファイルとして吐き出します。これによりtsxの記法でhtmlを生成することができます。
webpackでコンパイルを自動化した一例は以下です。

ejs, pugの限界

ejs, pugはhtml内にJavaScriptを記述することができ、if, for, switch, try/catchなどのjsシンタックスやlet, constによる変数宣言、またincludeによるパーシャルファイルの切り出し・読み込みなどが行え、効率よくhtmlを記述することができます。
しかしプロジェクトが大きくなってくると色々と限界を感じてくることかと思います。

  • lintチェックができない(探せばIDEの拡張機能とかはあるかも)
  • includeの引数に型がつけられない
  • コンパイルまでエラーがわからない&失敗時のエラー箇所の特定がしづらい
  • その他皆さんが感じている限界

上記を解決すべく、htmlのテンプレートエンジンとしてReactの構文tsxを導入してみようと思います。

tsxの利点

そもそもtsxって?

JavaScriptの構文を拡張したものであるjsxを、TypeScriptで記述したものです。
jsxの詳しい説明はこちら。

ejsが「htmlの中にJavaScriptを記述できる」というイメージに対して、jsxは「JavaScriptの中でhtmlを記述できる」というイメージです。

ejs, pugで感じた限界はtsxで解決できる

前提として上記で登場したejs, pugの機能であるif, for, const, includeなどはすべてtsxで実現可能です。
その上で、ejs, pugで感じた問題を解決することができます。

  • lintチェックできない問題
    • eslintがそのまま走るので解決。
  • includeの引数に型がつけられない問題
    • tsx(/jsx)の機能であるpropsとして引数をパーシャルファイルへ渡すので、静的型付けが可能。
  • エラー箇所わかりづらい問題
    • eslintが走るのでコーディング時に解決

また、Reactアプリケーションを作るわけではないので学習コストが低いことも利点です。
単にjsxの構文(ifはワンライナー、formapなど)を覚えればよく、ejs, pugの構文を覚えるのと大差ないと思います。

tsxをhtmlに変換する方法

では具体的な実装方法を見ていきましょう。
まずtsxをhtmlに変換する方法としてReactDOMServerオブジェクトのrenderToStaticMarkupメソッドを使用します。

tsxはReactによって一旦JavaScriptに変換されますが、上記の機能を用いることで静的なhtml文字列へとNodeサーバー上で変換することができます。

webpackを使う方法、使わない方法の2つの方法をご紹介します。

1. webpack使わない方法

まずはwebpackを使わずにシンプルにtsxをts-nodeでコンパイルし、htmlを吐き出す方法を見ていきましょう。
必要なパッケージをインストールします。

yarn add -D @types/node @types/react @types/react-dom react react-dom ts-node typescript

適当にtsconfig.jsonを用意し("jsx": "react-jsx"を設定する必要があります)以下のようなファイルを用意します。

page.tsx
import ReactDOMServer from "react-dom/server";

const App = () => {
  return (
    <div>
      <h1>App Page.</h1>
      <p>description.</p>
    </div>
  );
};

const pageString = ReactDOMServer.renderToString(<App />);

const page = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Title</title>
</head>
<body>
${pageString}
</body>
</html>
`;

console.log(page);
export default page;

以下コマンドを実行するとコンソールに文字列化したhtmlが出力されます。
tsxからhtml文字列を生成できることがわかります。

$ ts-node page.tsx

あとはこの文字列を元にhtmlファイルを生成すればよいです。
Node.jsの機能を使えば方法はいくつもあるかと思いますが、以下は一例です。

index.ts
import fs from "fs";
import path from "path";
import page from "./page";

const writeFile = async (file: string, data: string): Promise<void> => {
  await fs.promises.mkdir(path.dirname(file), { recursive: true });
  fs.promises.writeFile(file, data);
};

const filename = path.basename(__filename, ".ts");
writeFile(path.resolve(__dirname, `build/${filename}.html`), page);

実行するとbuild/index.htmlが吐き出されます。

$ ts-node index.ts

htmlテンプレートエンジンとしてtsxを使うことができました!

2. webpackを使う方法

さて、上記方法でtsxからhtmlを生成できることがわかりました。が、必要なページが増えた時少し面倒そうです。
ということでwebpackを用いて自動コンパイル環境を構築しましょう。
(webpack不要です、という方は以降不要です。)

まずは必要なパッケージをインストールします。

$ yarn add -D @types/react @types/react-dom esbuild-loader globule html-webpack-plugin html-webpack-skip-assets-plugin prettier react react-dom@17.0.2 typescript webpack webpack-cli webpack-dev-server

※注意※
react-domライブラリの最新版は22/10/05現在ver.18ですが、なぜだかver.18だとError: TextEncoder is not definedというエラーでコンパイルに失敗します。(理由をご存知の方はぜひ...)
ということで、ver.17をインストールしてます。

先程のpage.tsxのようなhtml文字列をexportしているtsxファイルをgrepし、HtmlWebpackPluginでhtmlファイルへと自動コンパイルします。
MPAらしく画面が増えてもgrepできるようにスクリプトを書いていきます。grepにはglobuleライブラリを使用しました。

webpack.config.js
const path = require('path');
const globule = require('globule');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {HtmlWebpackSkipAssetsPlugin} = require('html-webpack-skip-assets-plugin');

const assignPlugins = (env) => {
  const globuleFiles = ['**/*.tsx', '!**/_*.tsx', '!**/_partials/**/*.tsx'];

  /** 指定されたディレクトリからgrepしたtsxファイル */
  const templateFiles = globule.find([...globuleFiles], {cwd: `${__dirname}/src/pages`});

  /** entryファイルを格納したオブジェクトを作成 */
  const entriesList = templateFiles.reduce((temp, current) => {
    temp[`${current.replace(new RegExp(`.tsx`, 'i'), `.html`)}`] = `${__dirname}/src/pages/${current}`;
    return temp;
  }, {});

  const assignObject = { plugins: [] };
  for (const [htmlFileName, tempFileName] of Object.entries(entriesList)) {
    assignObject.plugins.push(new HtmlWebpackPlugin({
      filename: htmlFileName,
      template: tempFileName
    }));
    env.WEBPACK_BUILD && assignObject.plugins.push(new HtmlWebpackSkipAssetsPlugin({ excludeAssets: [/entry.js/] }));
  }

  return assignObject;
};

module.exports = (env) => (
  Object.assign({
    entry: './src/entry',
    output: {
      path: path.join(__dirname, 'dist'),
      filename: 'entry.js'
    },
    devtool: false,
    watchOptions: {
      ignored: /node_modules/
    },
    resolve: {
      extensions: ['.js', '.ts', '.tsx'],
    },
    module: {
      rules: [
        {
          test: /\.tsx$/,
          use: [
            {
              loader: 'esbuild-loader',
              options: {
                loader: 'tsx',
              }
            }
          ]
        },
      ],
    },
    devServer: {
      ...
      watchFiles: [
        'src/**/*.tsx'
      ],
    },
  }, assignPlugins(env));
);

エントリーファイルを用意する必要があるので、適当にsrcディレクトリ直下にentry.jsを用意します。

Point

  • エントリーファイルであるentry.jsはビルド時に自動でhtml内にscript要素から読み込まれますが不要ファイルなのでHtmlWebpackSkipAssetsPluginで吐き出されないようにします。
    ただ開発中はこのエントリーファイルを読み込まないとコンパイル対象となってくれないためenv.WEBPACK_BUILDtrueの時(ビルド時)のみこのプラグインを読み込むようにします。
  • tsxファイルの変更に応じてwebpack-dev-serverをホットリロードさせるために、devServer.watchFilesオプションに'src/**/*.tsx'を指定します。

▼ htmlを吐き出すだけの最小環境はこちらです

おまけ

せっかくwebpack環境を構築したので、どうせならsass, typescriptも一緒にコンパイルできる環境を作ってみました。

yarn startでローカルサーバーが立ち上がりyarn builddist ディレクトリ配下にビルドします。
画像圧縮は諸々案件毎に設定必要そうですので一旦入れてません。

【さらにおまけ】html → tsxへの変換

エンハンスなどですでに存在するhtmlファイルをtsxへ変換し開発を行う場合、諸々書き換えやバッククォートのエスケープなど必要な処理が発生し手動で行うのは面倒/大変だと感じたので、スクリプトで自動化させました。
pages ディレクトリ配下に変換前のhtmlファイルを新規で配置し、以下コマンドを実行すると同名のtsxファイルを生成します。(htmlファイルは削除されます)

$ node convertHtmlToTsx.js

gitで新規ファイル差分のファイルを一括で変換するので、複数ファイル同時に変換可能です。
ソースコードはこちらです。

まとめ

htmlテンプレートエンジンとしてtsxを使って環境構築してみました。
昨今はSPA, SSR, SSGが主流となりwebpack自体も枯れてきてますが、まだまだ静的なhtmlを納品しているプロジェクトは残っているかと思います。
どなたかの開発体験向上に貢献できたなら嬉しいです。

参考記事

本記事、リポジトリ作成にあたり参考にさせていただきました。ありがとうございます。

29
27
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?