You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Express+React的多域名模板服务方案及报错咨询

React多主机模板分发方案问题与优化

项目背景

计划将XSLT模板系统迁移至React,采用Express作为服务端,根据请求主机名返回对应React模板,通过JSON配置文件维护主机名与模板的映射关系。

项目架构

├── templates/
│   ├── template_1/          # React项目模板1
│   │   ├── dist/server/template.js  # 构建后的SSR渲染文件
│   │   └── package.json
│   └── template_2/          # React项目模板2
│       ├── dist/server/template.js
│       └── package.json
├── server/
│   ├── server.js            # Express服务主文件,处理请求分发与模板加载
│   └── renderer.jsx         # SSR渲染通用模板
└── builder/
    ├── esbuild.server.js    # Express服务构建脚本
    └── esbuild.template.js  # React模板SSR构建脚本

核心代码实现

1. Express服务构建脚本(esbuild.server.js)

require('esbuild')
    .build({
        entryPoints: ['server.js'],
        bundle: true,
        format: 'iife',
        external: ['express', 'react', 'react-dom', 'react-router-dom'],
        platform: 'node',
        outfile: 'dist/server.js',
        loader: {'.js': 'jsx'},
        logLevel: 'info',
        define: {'process.env.NODE_ENV': '"production"'},
    })
    .catch(() => process.exit(1));

2. React模板SSR构建脚本(esbuild.template.js)

const {existsSync, readFileSync, mkdirSync, writeFileSync, rmSync} = require('fs');
const {resolve} = require('path');

const {config} = require('dotenv');
config();

const template = process.argv.slice(2)[0] || process.env.npm_config_name;
const CONFIG_FOLDER = process.env.CONFIG_FOLDER || resolve('./config');

if (!template)
    throw new Error('No argument template specified, try: "--name=<template>"');

const TEMPLATES_FOLDER = './templates';
const TEMPLATE_PATH = `${TEMPLATES_FOLDER}/${template}`;

if (!existsSync(TEMPLATE_PATH))
    throw new Error(`The template ${TEMPLATE_PATH} doesn't exist`);

const getTemplateConfig = () => {
    try {
        return JSON.parse(readFileSync(`${TEMPLATE_PATH}/.template.json`));
    } catch (error) {
        return JSON.parse(readFileSync(`${CONFIG_FOLDER}/server/.template.json`));
    }
};

const templateConfig = getTemplateConfig();

if (!existsSync(`${TEMPLATE_PATH}/${templateConfig.app}`))
    throw new Error(
        `The template app ${TEMPLATE_PATH}/${templateConfig.app} doesn't exist`
    );

const renderer = readFileSync('./renderer.jsx', 'utf8').replace(
    /%TEMPLATE_PATH%/gim,
    resolve(`${TEMPLATE_PATH}/${templateConfig.app}`)
);
const rendererFile = `./tmp/renderer_${template}_${new Date().getTime()}.jsx`;

if (!existsSync('./tmp')) mkdirSync('./tmp');
writeFileSync(rendererFile, renderer);

require('esbuild')
    .build({
        entryPoints: [rendererFile],
        bundle: true,
        format: 'iife',
        minify: true,
        platform: 'node',
        outfile: `${TEMPLATE_PATH}/dist/server/template.js`,
        drop: ['debugger', 'console'],
        loader: {'.js': 'jsx'},
        logLevel: 'info',
        legalComments: 'none',
    })
    .then((response) => {
        if (response.errors.length > 0) console.error(response.errors);
        if (response.warnings.length > 0) console.warn(response.warnings);
    })
    .catch((error) => console.log(error))
    .finally(() => {
        rmSync(rendererFile);
    });

3. SSR渲染通用模板(renderer.jsx)

import React from 'react';
import {renderToPipeableStream} from 'react-dom/server';
import {StaticRouter} from 'react-router-dom/server';
import Template from '%TEMPLATE_PATH%';

function App({url}) {
    return (
        <StaticRouter location={url}>
            <Template />
        </StaticRouter>
    );
}

function renderer({url, res, next, writable}) {
    const stream = renderToPipeableStream(<App url={url} />, {
        onShellReady() {
            res.setHeader('Content-type', 'text/html');
            stream.pipe(writable);
        },
        onShellError(error) {
            res.status(500);
            if (next) return next(error);
            console.error(error);
        },
        onError(error) {
            console.error(error);
        },
    });
}

export default renderer;

4. Express请求处理端点代码

const render = require(`${templateDirPath}/dist/server/template.js`).default;

const writable = new HtmlWritable();
writable.on('finish', () => {
    const html = writable.getHtml();
    // 修正原代码中的HTML语法错误
    const response = indexHtml.replace(
        '<div id="root"></div>',
        `<div id="root">${html}</div>`
    );
    res.send(response);
});

render({req, res, next, writable});

当前遇到的问题

加载模板渲染函数时出现以下错误:

  • 使用vm.Script加载时,报错:Error: Dynamic require of "stream" is not supported
  • 使用ESM(.mjs)运行服务时,同样出现上述错误
  • 若编译时不指定platform: 'node',构建产物会缺失renderToPipeableStream方法

问题解决与优化方案

1. 修复模板加载错误

错误根源是用IIFE格式编译Node环境代码,IIFE是浏览器环境的模块格式,Node环境下应使用CommonJS(CJS)或ESM格式。

修改esbuild.template.js中的构建配置:

require('esbuild')
    .build({
        // ...其他配置不变
        format: 'cjs', // 替换原有的iife格式
        external: ['react', 'react-dom', 'react-router-dom'], // 新增:将核心依赖设为外部依赖,复用服务端实例
        // ...其他配置不变
    })

修改后,构建出的模板文件为CJS格式,服务端可直接require加载,不会出现动态依赖的错误。

2. 更优的多主机模板分发方案

方案一:预加载模板映射

服务启动时读取配置文件,预加载所有模板的渲染函数并缓存,避免请求时动态加载的性能开销:

// server.js 启动阶段代码
const templateConfig = require('./config/templates.json'); // 主机名 -> 模板路径的映射配置
const templateRenderers = {};

// 预加载所有模板
for (const [host, templatePath] of Object.entries(templateConfig)) {
    try {
        templateRenderers[host] = require(`${templatePath}/dist/server/template.js`).default;
    } catch (err) {
        console.error(`Failed to load template for host ${host}:`, err);
    }
}

// 请求处理中间件
app.use((req, res, next) => {
    const host = req.hostname;
    const render = templateRenderers[host];
    if (!render) return res.status(404).send('Template not found');

    // 渲染逻辑...
});

方案二:动态ESM导入(适合大量模板场景)

若模板数量多或需动态更新,可使用ESM的动态导入实现按需加载:

// server.mjs(改用ESM格式)
import { readFile } from 'fs/promises';

const templateConfig = JSON.parse(await readFile('./config/templates.json', 'utf8'));

app.use(async (req, res, next) => {
    const host = req.hostname;
    const templatePath = templateConfig[host];
    if (!templatePath) return res.status(404).send('Template not found');

    try {
        const { default: render } = await import(`${templatePath}/dist/server/template.js`);
        // 渲染逻辑...
    } catch (err) {
        next(err);
    }
});

方案三:反向代理分发(高隔离性场景)

若模板间差异极大或需独立部署,可使用Nginx等反向代理,根据主机名直接转发到对应模板的独立服务:

server {
    server_name template1.example.com;
    location / {
        proxy_pass http://localhost:3001; # template1的服务端口
    }
}

server {
    server_name template2.example.com;
    location / {
        proxy_pass http://localhost:3002; # template2的服务端口
    }
}

3. 行业标准做法

这类多主机/多站点模板分发场景,行业通用做法包括:

  • 配置驱动路由:用JSON/YAML配置维护主机与模板的映射,便于扩展和维护;
  • 共享核心依赖:将React、ReactDOM等核心库设为外部依赖,避免重复打包,同时杜绝多React实例问题;
  • 流式SSR渲染:使用React 18的renderToPipeableStream实现流式渲染,提升首屏加载性能;
  • 缓存优化:对相同URL的渲染结果进行缓存(如内存缓存、Redis缓存),减少重复计算;
  • 模板隔离:若模板复杂度高,可采用微前端架构(如Module Federation)实现模板间的资源隔离,不过SSR场景需额外处理模块共享逻辑。

内容的提问来源于stack exchange,提问作者Marc J Cabrer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 08:36:14