手动构建React SSR应用(非NextJS)配置自定义基础URL路径
Great question—this is a common pitfall when setting up SSR with custom base paths, especially when you're not using CRA or Next.js which handle a lot of this out of the box. Let's walk through how to fix the 404s and duplicate path issues step by step:
First, avoid hardcoding the base path everywhere—define it once in a config file and reference it across your app. This ensures consistency and makes it easy to update later.
// src/config.js // Use an environment variable for flexibility, fall back to root path export const BASE_PATH = process.env.BASE_PATH || "/"; // *Critical note:* Ensure BASE_PATH starts with a / and doesn’t end with one (e.g., "/a/b"), or is an empty string for the root path.
Inject this variable into both your client and server Webpack builds by adding it to the DefinePlugin:
// webpack.client.js and webpack.server.js const webpack = require("webpack"); const { BASE_PATH } = require("./src/config"); module.exports = { // ... other config plugins: [ new webpack.DefinePlugin({ "process.env.BASE_PATH": JSON.stringify(BASE_PATH), }), ], };
Both client-side BrowserRouter and server-side StaticRouter need to know the base path, and you’ll need to clean up the incoming request URL on the server.
Client-Side Router
// src/client/index.js import { BrowserRouter } from "react-router-dom"; import { BASE_PATH } from "./config"; ReactDOM.hydrate( <BrowserRouter basename={BASE_PATH}> <App /> </BrowserRouter>, document.getElementById("root") );
Server-Side Router
The server receives requests with the full base path (e.g., /a/b/page1), so you need to strip the base path before passing it to StaticRouter:
// src/server/render.js import { StaticRouter } from "react-router-dom/server"; import { BASE_PATH } from "../config"; export function render(req, res) { // Strip the base path from the request URL to match your app's route definitions const strippedUrl = req.url.replace(new RegExp(`^${BASE_PATH}`), "") || "/"; const context = {}; const appHtml = ReactDOMServer.renderToString( <StaticRouter location={strippedUrl} basename={BASE_PATH} context={context} > <App /> </StaticRouter> ); // ... generate final HTML (see step 4) }
This is the most critical step for fixing missing styles/bundles. Configure Webpack’s publicPath to match your base path so assets are served correctly.
Client Webpack Config
// webpack.client.js const path = require("path"); const { BASE_PATH } = require("./src/config"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { entry: "./src/client/index.js", output: { filename: "client.bundle.js", path: path.resolve(__dirname, "dist/client"), // Set publicPath to your base path + trailing slash publicPath: `${BASE_PATH}/`, }, plugins: [ new MiniCssExtractPlugin({ filename: "styles.css" }), ], module: { rules: [ { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"], }, // For static assets like images { test: /\.(png|jpeg|svg)$/, loader: "file-loader", options: { outputPath: "assets", publicPath: `${BASE_PATH}/assets/`, }, }, ], }, };
Server Webpack Config
For server-side bundling, ensure static assets are emitted to the client’s dist folder so they’re accessible via the base path:
// webpack.server.js const path = require("path"); const { BASE_PATH } = require("./src/config"); module.exports = { entry: "./src/server/index.js", output: { filename: "server.bundle.js", path: path.resolve(__dirname, "dist/server"), libraryTarget: "commonjs2", }, module: { rules: [ { test: /\.css$/, use: ["css-loader/locals"], // No need to emit CSS for server }, { test: /\.(png|jpeg|svg)$/, loader: "file-loader", options: { outputPath: "../client/assets", // Emit to client dist folder publicPath: `${BASE_PATH}/assets/`, emitFile: true, }, }, ], }, };
When generating the server-rendered HTML, reference assets using the base path to avoid broken links:
// src/server/render.js import { BASE_PATH } from "../config"; function generateHtml(appHtml) { return ` <!DOCTYPE html> <html> <head> <title>SSR App</title> <link rel="stylesheet" href="${BASE_PATH}/styles.css"> </head> <body> <div id="root">${appHtml}</div> <script src="${BASE_PATH}/client.bundle.js"></script> </body> </html> `; }
If you’re using Express, map the base path to your client’s dist folder so static assets are served correctly:
// src/server/index.js import express from "express"; import path from "path"; import { BASE_PATH } from "../config"; import { render } from "./render"; const app = express(); // Serve client assets from the base path app.use(BASE_PATH, express.static(path.resolve(__dirname, "../../dist/client"))); // Handle all routes with SSR app.get("*", render); app.listen(3000, () => console.log("Server running on http://localhost:3000"));
Run your app with a custom base path by setting the environment variable:
BASE_PATH="/a/b" npm run build && npm run start
- Visit
http://localhost:3000/a/b(homepage) - Visit
http://localhost:3000/a/b/page1(page1) - Check the network tab: assets should load from
/a/b/client.bundle.jsand/a/b/styles.csswith 200 status codes - Client-side navigation should keep the base path intact (no duplicate
/a/b/a/b/paths)
内容的提问来源于stack exchange,提问作者se22as

