1. Mở đầu

Kịch bản là bạn muốn build ra 2 page là tên là rate.html và trade.html trong cùng một project sử dụng create-react-app.

          multi entry points in create-react-app

Bình thường bạn sẽ phải tạo ra 2 project riêng biệt nhưng bài này sẽ hướng dẫn cách cấu hình webpack để chỉ cần sử dụng 1 project cho cả 2 trang.

Mục tiêu đặt ra là:

  • Không sử dụng npm run eject 
  • Sử dụng được webpackDevServer cho cả 2 page

 

2. Cách làm

Để sử dụng nhiều entry point trong cùng một project, bạn cần phải tuỳ biến cấu hình của webpack nhưng mặc định của CRA (create-react-app) là file webpack.config.js bị ẩn đi nếu như bạn không sử dụng eject .

Cấu hình dưới đây sử dụng "react-scripts": "3.4.0"

  • Đầu tiên, để không phải eject CRA, bạn cần sử dụng react-app-rewired
yarn add -D react-app-rewired

 

  • File myapp/public/trade.html:
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Trade Page</title>
  </head>
  <body>
    <div id="trade"></div>
  </body>
</html>

 

  • File myapp/public/rate.html:
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Rate Page</title>
  </head>
  <body>
    <div id="rate"></div>
  </body>
</html>

 

  • File myapp/src/trade/index.tsx:
import React from "react";
import ReactDOM from "react-dom";
import * as serviceWorker from "../serviceWorker";
import Trade from "./Trade";

ReactDOM.render(<Trade />, document.getElementById("trade"));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

 

  • File myapp/src/trade/Trade.tsx:
import React from "react";
import "./Trade.css";

function Trade() {
  return (
    <div className="Trade">
      This is Trade Page
    </div>
  );
}

export default Trade;

 

  • File myapp/src/rate/index.tsx:
import React from "react";
import ReactDOM from "react-dom";
import * as serviceWorker from "../serviceWorker";
import Rate from "./Rate";

ReactDOM.render(<Rate />, document.getElementById("rate"));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

 

  • File myapp/src/rate/Rate.tsx:
import React from "react";
import "./Rate.css";

function Rate() {
  return (
    <div className="Rate">
      This is Rate Page
    </div>
  );
}

export default Rate;

 

  • Tạo file config-overrides.js chính là file để tuỳ biến cấu hình webpack của CRA. Và cấu hình như sau:
// myapp/config_overrides.js 

/* global require, __dirname, process */
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");

const getPublicUrlOrPath = require("react-dev-utils/getPublicUrlOrPath");
const appPackagePath = path.resolve(__dirname, "package.json");
const appPackageJson = require(appPackagePath);
const appBuild = path.resolve(__dirname, "./build/");
const appSrc = path.resolve(__dirname, "src/");
const publicUrlOrPath = getPublicUrlOrPath(
  process.env.NODE_ENV === "development",
  require(appPackagePath).homepage,
  process.env.PUBLIC_URL
);

module.exports = {
  // The Webpack config to use when compiling your react app for development or production.
  webpack: function(config, env) {
    const isEnvDevelopment = env === "development";
    const isEnvProduction = env === "production";

    config.entry = {
      trade: [
        isEnvDevelopment &&
          require.resolve("react-dev-utils/webpackHotDevClient"),
        path.resolve(__dirname, "src/trade/index.tsx")
      ].filter(Boolean),
      rate: [
        isEnvDevelopment &&
          require.resolve("react-dev-utils/webpackHotDevClient"),
        path.resolve(__dirname, "src/rate/index.tsx")
      ].filter(Boolean)
    };

    config.output = {
      path: isEnvProduction ? appBuild : undefined,
      pathinfo: isEnvDevelopment,
      filename: isEnvProduction
        ? "static/js/[name].[contenthash:8].js"
        : isEnvDevelopment && "static/js/[name].bundle.js",
      futureEmitAssets: true,
      chunkFilename: isEnvProduction
        ? "static/js/[name].[contenthash:8].chunk.js"
        : isEnvDevelopment && "static/js/[name].chunk.js",
      publicPath: publicUrlOrPath,
      devtoolModuleFilenameTemplate: isEnvProduction
        ? info =>
            path.relative(appSrc, info.absoluteResourcePath).replace(/\\/g, "/")
        : isEnvDevelopment &&
          (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, "/")),
      jsonpFunction: `webpackJsonp${appPackageJson.name}`,
      globalObject: "this"
    };

    // NOTE: it is necessary to remove default ManifestPlugin
    config.plugins = config.plugins.filter(
      p => !/ManifestPlugin/.test(p.constructor)
    );

    config.plugins = [
      ...config.plugins,
      new HtmlWebpackPlugin({
        inject: true,
        chunks: ["trade"],
        template: path.resolve(__dirname, "public/trade.html"),
        filename: "trade.html"
      }),
      new HtmlWebpackPlugin({
        inject: true,
        chunks: ["rate"],
        template: path.resolve(__dirname, "public/rate.html"),
        filename: "rate.html"
      }),
      new ManifestPlugin({
        fileName: "asset-manifest.json",
        publicPath: publicUrlOrPath,
        generate: (seed, files, entrypoints) => {
          const manifestFiles = files.reduce((manifest, file) => {
            manifest[file.name] = file.path;
            return manifest;
          }, seed);

          const entrypointFiles = {};
          Object.keys(entrypoints).forEach(entrypoint => {
            entrypointFiles[entrypoint] = entrypoints[entrypoint].filter(
              fileName => !fileName.endsWith(".map")
            );
          });

          return {
            files: manifestFiles,
            entrypoints: entrypointFiles
          };
        }
      })
    ];
    return config;
  },

  devServer: function(configFunction) {
    return function(proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      config.historyApiFallback = {
        disableDotRule: true,
        rewrites: [
          { from: /^\/trade.html/, to: "/build/trade.html" },
          { from: /^\/rate.html/, to: "/build/rate.html" }
        ]
      };
      return config;
    };
  }
};

Chú ý: Cấu hình config-overrides.js ở trên dành cho React có version 16.12.0, nếu bạn sử dụng version khác có thể cấu hình trên sẽ không hoạt động. Bạn nên tạo 2 project, một cái đã eject và một cái sử dụng react-app-rewired để so sánh xem cần sửa ở những config nào.

 

  • Cuối cùng, sửa scripts của file package.json :
{
  ...  
  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
  },
  ...
}

 

Giờ bạn chỉ cần khởi động app yarn start và truy cập vào http://localhost:3000/trade.html hoặc http:localhost:3000/rate.html và xem kết quả.

Bạn cũng có thể xem 2 file build: build/trade.html và build/rate.html bằng lệnh yarn build .