基于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
相关产品推荐
相关产品推荐

