Closed11

ReactでHTMLのコーディングを行えるようにする

Hiroki KamedaHiroki Kameda

https://zenn.dev/qrac/articles/7537521afcd1bf
Reactで綺麗なHTMLが出力されるようにする。

モチベーション

以前Next.jsで納品したが、新しいプロジェクトでクライアント自身が触りたいのでHTMLで納品してほしいとのこと。
今までEJSを使っていたが、Reactの快適さに慣れてしまったのでReactで書けないか試してみる

Hiroki KamedaHiroki Kameda

プロジェクトの作成

プロジェクトディレクトリを作成後

npm init -y
yarn add minista react react-dom

記載してあるようにディレクトリを作成

.
├── yarn.lock
├── package.json
├── public
└── src
    ├── assets
    │   ├── css
    │   │   └── index.css
    │   ├── index.js
    │   └── js
    │       └── hello.js
    ├── components
    │   └── app-layout.js
    └── pages
        └── index.js

index.jsの中身を作成

import React from "react"; // Required!
import { render } from "minista"; // Required!

const Home = () => {
  return render(
    // Required!
    <h1>Hello</h1>
  );
};

export default Home;

package.jsonにministaのスクリプトを書く

  "scripts": {
    "dev": "minista",
    "build": "minista build"
  },

動作テストしてみると動いた!

CSSの動作チェック
CSSを記述

h1{
  color:red;
}

jsで読み込み

src/assets/index.js
import "./css/index.css";

CSSもOK

##Sassの準備

yarn add sass-loader sass

ファイル名のcssをscssに変更
devもbuildも問題なく動作した

Typescriptの準備

参考
https://zenn.dev/nanaki14/articles/html-template-react
Typescriptをインストール

yarn add typescript ts-loader @types/react @types/react-dom

記事と同様にファイル設定をしていく。

VSCodeのエラーはこちらで解決

tsconfing.jsonのcompilerOptionsにoutdirかoutFileを指定するとエラーがなくなるようです。
変換しない場合は、"outFile": "" とするかまたは存在しないデタラメなフォルダを指定すればいいようです。

ライブラリ追加

yarn add babel-plugin-module-resolver

jsをtsx,tsに変更

tsxの動作テスト
ファイル作成

src/pages/index.tsx
import React from "react";
import { render } from "minista";
import Title from "@/components/Title";

const Home = () => {
  return render(<Title title="タイトルです" />);
};

export default Home;
src/components/Title.tsx
import { VFC } from "react";

type Prop = {
  title: string;
}

const Title: VFC<Prop> = (props) => {
  return (<h1>{props.title}</h1>)
}

export default Title

動いた!

Hiroki KamedaHiroki Kameda

Webp対応

https://zenn.dev/k_logic24/articles/generate-webp-with-webpack

ライブラリのインストール

yarn add copy-webpack-plugin file-loader imagemin-webp-webpack-plugin
webpack.config.js
const path = require("path");
const glob = require("glob");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ImageminWebpWebpackPlugin = require("imagemin-webp-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");

const webpackConfig = {
  entry: "./src/assets/index.ts",
  module: {
    rules: [
      {
        test: [/\.ts$/, /\.tsx$/, /\.js$/],
        exclude: /node_modules/,
        use: ["babel-loader", "ts-loader"],
      },
+      {
+       test: /\.(jpe?g|png)$/i,
+        loader: "file-loader",
+        options: {
+          name: "[name].[ext]?[hash]",
+          outputPath: path.resolve(__dirname, "dist/asstes/img"),
+        },
      },
    ],
  },
  resolve: {
    extensions: [".js", ".jsx", ".ts", ".tsx"],
  },
-  plugins:[]
+  plugins: [
+    new CopyWebpackPlugin({
+      patterns: [
+        {
+          from: "src/assets/img/",
+          to: path.resolve(__dirname, "dist/assets/img"),
+        },
+      ],
+    }),
+    new ImageminWebpWebpackPlugin({
+      config: [
+        {
+          test: /\.(jpe?g|png)$/i,
+          options: {
+            quality: 60,
+          },
+        },
+      ],
+      overrideExtension: false,
+      detailedLogs: true,
+    }),
+  ],
};

glob
  .sync("**/*.tsx", {
    cwd: "src/pages",
  })
  .forEach((file) => {
    const extname = path.extname(file);
    const basename = path.basename(file, extname);
    const dirname = path.dirname(file);

    webpackConfig.plugins.push(
      new HtmlWebpackPlugin({
        template: path.resolve("src/pages", file),
        filename: path.join(dirname, basename + ".html"),
      })
    );
  });

module.exports = webpackConfig;

Pictureコンポーネントを作成

webp対応はhtaccessではなくコンポーネントの方で対応します。
毎回書くのはだるいので、Pictureコンポーネントを作成。

src/components/Picture.tsx
import { VFC } from "react";

type Props = {
  srcPc: string;
  srcSp: string;
  alt: string | undefined;
  webp: boolean;
  height: number;
  width: number;
  loadingLazy: boolean;
};

const Picture: VFC<Props> = (props) => {
  return (
    <picture>
      {props.srcSp ? (
        <source
          type="image/webp"
          media="(max-width: 767px)"
          srcSet={`${props.srcSp}.webp`}
        />
      ) : null}
      {props.webp && props.srcSp ? (
        <source
          type="image/webp"
          media="(max-width: 767px)"
          srcSet={`${props.srcSp}.webp`}
        />
      ) : null}
      {props.webp ? (
        <source type="image/webp" srcSet={`${props.srcPc}.webp`} />
      ) : null}
      <img
        src={props.srcPc}
        alt={props.alt}
        width={props.width}
        height={props.height}
        decoding="async"
        loading={props.loadingLazy ? "lazy" : "eager"}
      />
    </picture>
  );
};

export default Picture;

こんな感じで使います

src/pages/index.tsx
const Home = () => {
  return render(
    <>
      <Title title="タイトルです" />
      <Picture
        webp={true} // webp対応するか
        srcPc="./assets/img/noimage.png" // PCのパス
        srcSp="./assets/img/noimage.png" // SPのパス
        alt=""  //alt属性
        width={50} // width
        height={100}  // height
        loadingLazy={true}  // trueにしたらloading="lazy"を指定、通常decoding="async"が指定されている
      />
    </>
  );
};

Hiroki KamedaHiroki Kameda

ポリフィルの追加

picturefill

パッケージインストール

yarn add picturefill

index.tsに記述

src/assets/js/index.ts
import picturefill from 'picturefill'
picturefill();

エラーが出るので、型をインストール

yarn add @types/picturefill

object-fit

パッケージインストール

yarn add object-fit-images

index.tsに記述

src/assets/js/index.ts
import picturefill from 'picturefill'
+  import objectFitImages from 'object-fit-images';

picturefill();
+  objectFitImages();

フォントファミリーを書く必要があるので、postcssを入れて自動化します。

yarn add postcss-object-fit-images

postcss.config.jsに設定を追加

postcss.config.js
module.exports = {
  plugins: [
    require('postcss-object-fit-images'),
  ]
}
Hiroki KamedaHiroki Kameda

一案件終えてアップデート

  • パスが絶対パスなので、相対パスに変更
  • 余分なfile-loaderを削除
webpack.config.js
const path = require("path");
const glob = require("glob");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ImageminWebpWebpackPlugin = require("imagemin-webp-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");

const webpackConfig = {
  entry: "./src/assets/index.ts",
  output: {
    path: path.resolve("dist"),
    publicPath: "./",
    filename: "assets/scripts.js",
  },
  module: {
    rules: [
      {
        test: [/\.ts$/, /\.tsx$/, /\.js$/],
        exclude: /node_modules/,
        use: ["babel-loader", "ts-loader"],
      },
    ],
  },
  resolve: {
    extensions: [".js", ".jsx", ".ts", ".tsx"],
    alias: {
      "~": path.resolve(__dirname, "./src/assets/sass/"),
      imagePath: path.resolve(__dirname, "./assets/img/"),
    },
  },
  plugins: [
    new CopyWebpackPlugin({
      patterns: [
        {
          from: "src/assets/img/",
          to: path.resolve(__dirname, "dist/assets/img"),
        },
      ],
    }),
    new ImageminWebpWebpackPlugin({
      config: [
        {
          test: /\.(jpe?g|png)$/i,
          options: {
            quality: 60,
          },
        },
      ],
      overrideExtension: false,
      detailedLogs: true,
    }),
  ],
};

glob
  .sync("**/*.tsx", {
    cwd: "src/pages",
  })
  .forEach((file) => {
    const extname = path.extname(file);
    const basename = path.basename(file, extname);
    const dirname = path.dirname(file);

    webpackConfig.plugins.push(
      new HtmlWebpackPlugin({
        template: path.resolve("src/pages", file),
        filename: path.join(dirname, basename + ".html"),
      })
    );
  });

module.exports = webpackConfig;
Hiroki KamedaHiroki Kameda

CSSの背景画像を指定するとエラーになるので、
style-loaderのoptionにurl: false,を指定

このスクラップは2021/09/08にクローズされました