はじめに
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
として引数をパーシャルファイルへ渡すので、静的型付けが可能。
- tsx(/jsx)の機能である
- エラー箇所わかりづらい問題
- eslintが走るのでコーディング時に解決
また、Reactアプリケーションを作るわけではないので学習コストが低いことも利点です。
単にjsxの構文(if
はワンライナー、for
はmap
など)を覚えればよく、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"
を設定する必要があります)以下のようなファイルを用意します。
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の機能を使えば方法はいくつもあるかと思いますが、以下は一例です。
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
ライブラリを使用しました。
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_BUILD
がtrue
の時(ビルド時)のみこのプラグインを読み込むようにします。 - tsxファイルの変更に応じてwebpack-dev-serverをホットリロードさせるために、
devServer.watchFiles
オプションに'src/**/*.tsx'
を指定します。
▼ htmlを吐き出すだけの最小環境はこちらです
おまけ
せっかくwebpack環境を構築したので、どうせならsass, typescriptも一緒にコンパイルできる環境を作ってみました。
yarn start
でローカルサーバーが立ち上がりyarn build
でdist
ディレクトリ配下にビルドします。
画像圧縮は諸々案件毎に設定必要そうですので一旦入れてません。
【さらにおまけ】html → tsxへの変換
エンハンスなどですでに存在するhtmlファイルをtsxへ変換し開発を行う場合、諸々書き換えやバッククォートのエスケープなど必要な処理が発生し手動で行うのは面倒/大変だと感じたので、スクリプトで自動化させました。
pages
ディレクトリ配下に変換前のhtmlファイルを新規で配置し、以下コマンドを実行すると同名のtsxファイルを生成します。(htmlファイルは削除されます)
$ node convertHtmlToTsx.js
gitで新規ファイル差分のファイルを一括で変換するので、複数ファイル同時に変換可能です。
ソースコードはこちらです。
まとめ
htmlテンプレートエンジンとしてtsxを使って環境構築してみました。
昨今はSPA, SSR, SSGが主流となりwebpack自体も枯れてきてますが、まだまだ静的なhtmlを納品しているプロジェクトは残っているかと思います。
どなたかの開発体験向上に貢献できたなら嬉しいです。
参考記事
本記事、リポジトリ作成にあたり参考にさせていただきました。ありがとうございます。