🐤

テンプレートエンジンに React を使いつつ、きれいな HTML を生成したいんじゃ!!

2020/11/09に公開
4

イントロダクション

きれいな HTML で納品してほしいという案件、まだまだあるかと思います。

HTML を生成するツールとして選択肢に出てくるのは、 EJS / Pug / Nunjucks といった JavaScript のテンプレートエンジンだと思いますが、個人的には React を知ってしまってから JSX 以外でマークアップをすることがとても億劫になってしまっています。

HTML はロジックが紛れ込むことを考慮されてない言語だと思うので、 HTML をベースにしたテンプレートは使いづらいです(個人の感想です)。逆に JSX は JavaScript の中に HTML があるので、 HTML の分割のしやすさも相まってロジックの中に紛れ込ませやすく、ほしい要素の塊をコンポーネントとして切り出して使う、コンポーネント指向にとても向いています。

テンプレートエンジンに React を使いたいんじゃ!!

Next.js や Gatsby.js 等の静的サイトジェネレーターを使えば、レンダリングされた HTML を出力することはできるのですが、人が触れるようなきれいな HTML にはなりません。

なので、きれいな HTML を生成できる開発環境を作ってみました。

チュートリアル

renderToStaticMarkup() で React を HTML 文字列に変換

ReactDomServer の renderToStaticMarkup() を使うと、 React 要素を初期状態の HTML 文字列へ変換することができます。

https://ja.reactjs.org/docs/react-dom-server.html#rendertostaticmarkup

以下のように、 body 要素の中で <App /> を HTML 文字列に変換すれば、いい感じの HTML が文字列となって export default されます。

import React from "react";
import { renderToStaticMarkup } from "react-dom/server";

const App = () => <div>Use React !!</div>;

export default () => `
  <!DOCTYPE html>
  <html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>タイトル</title>
  </head>
  <body>
    ${renderToStaticMarkup(<App />)}
  </body>
  </html>
`;

次に、こいつを HtmlWebpackPlugin で HTML ファイルに変換します。

HtmlWebpackPlugin で HTML ファイルに変換

適当な ディレクトリを作り、 webpack と HtmlWebpackPlugin 、そして React をビルドするためのパッケージ一式を npm install しましょう。

$ mkdir react-to-html
$ cd react-to-html
$ npm init -y
$ npm i -D webpack webpack-cli html-webpack-plugin react react-dom babel-loader @babel/preset-react @babel/core

webpack.config.js を作成し、 HtmlWebpackPlugin と bable-loader の設定をします。

const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      title: "My App",
      template: "src/index.jsx",
    }),
  ],
  module: {
    rules: [
      {
        test: /\.js(|x)$/,
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: ["@babel/react"],
            },
          },
        ],
      },
    ],
  },
};

さきほどの HTML 文字列を作る JavaScript は、 src/index.jsx で保存します。

HtmlWebpackPlugin の templatesrc/index.jsx を指定すれば、 HTML ファイルとして出力してくれます。

また、 title: "My App" でタイトルを指定している部分は引数で受け取ることができます。

import React from "react";
import { renderToStaticMarkup } from "react-dom/server";

const App = () => <div>Use React !!</div>;

export default ({ htmlWebpackPlugin }) => `
  <!DOCTYPE html>
  <html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${htmlWebpackPlugin.options.title}</title>
  </head>
  <body>
    ${renderToStaticMarkup(<App />)}
  </body>
  </html>
`;

エントリーポイントとして src/index.js を空で作っておきます。あとは、以下のコマンドで src/index.jsx が HTML 化した dist/index.html として出力されます。

$ npx webpack

Prettier で HTML をきれいにする

prettier で HTML をきれいにしてあげましょう。

$ npm i -D prettier

npm scripts に prettier で dist/index.html が整形されるように設定します。

{
  "scripts": {
    "build": "webpack && prettier --write dist/index.html"
  }
}
$ npm run build

きれいな HTML になりましたね!

あとは、よしなに @babel/preset-env なり css-loader なりを入れて、いい感じに開発してください!

参考: https://dev.to/jantimon/html-webpack-plugin-4-has-been-released-125d

Discussion

8484

とても参考になる記事、ありがとうございます。
同じように構築させていただきましたが、以下のエラーが発生します↓
ReferenceError: TextEncoder is not defined
解決策、わかりますでしょうか?

node v 16.15.0 です。

sonicweaversonicweaver

同じ問題にはまりました。
多分React v18系を使うと出る問題だと思います。

https://github.com/reactjs/React.NET/issues/1309

react と react-dom 16.12.0 にしたらいけました。

また下の方のコメントに

I added https://github.com/anonyco/FastestSmallestTextEncoderDecoder as a polyfill in my server webpack configuration and that seemed to have fixed it.
サーバーのwebpackの設定でhttps://github.com/anonyco/FastestSmallestTextEncoderDecoder をpolyfillとして追加したら、直ったようです。

とあるのですが具体的にwebpack.cofig.jsにどの様に設定したら解決するかわかりません。

sonicweaversonicweaver

暫定的ですが解決策を発見しました!

といっても他の方が見つけたのですが、以下のissuesに載っていました。
https://github.com/jantimon/html-webpack-plugin/issues/1747

html-webpack-pluginのindex.jsの130行目あたりのvm.createContext()をに TextEncoder: TextEncoder,TextDecoder: TextDecoderを追加する形です。

    const vmContext = vm.createContext({
      ...global,
      HTML_WEBPACK_PLUGIN: true,
      require: require,
      htmlWebpackPluginPublicPath: publicPath,
      URL: require('url').URL,
      __filename: templateWithoutLoaders,
+     TextEncoder: TextEncoder,
+     TextDecoder: TextDecoder
    });

こちらはあくまで暫定的な方法ですがとりあえずこれで動きました。

otsukayuhiotsukayuhi

すみません、ものすごい亀レスになってしまいました。。。。
該当のエラーの調査はできていないのですが、本質的には「renderToStaticMarkup() で React を HTML 文字列に変換することができる」という部分なので、あとの細かい部分は適にカスタマイズしていただければと思います。
(エラー自体は余力あるときに確認してみます)