
ejs 官方 cli 不支持递归处理目录,但通过 node.js 脚本可轻松实现全目录 ejs 模板渲染——本文提供健壮、可配置的递归渲染方案,支持嵌套结构、数据注入与路径映射,无需第三方 fork。
ejs 官方 cli 不支持递归处理目录,但通过 node.js 脚本可轻松实现全目录 ejs 模板渲染——本文提供健壮、可配置的递归渲染方案,支持嵌套结构、数据注入与路径映射,无需第三方 fork。
EJS 是轻量而灵活的 JavaScript 模板引擎,其官方 CLI(ejs 命令)仅支持单文件渲染,例如 ejs template.ejs -o output.html。当项目包含数十个 .ejs 文件(如配置、Markdown 模板、HTML 片段、JSON 配置等),且需保留原始目录结构并批量渲染时,原生命令明显力不从心。此时,编写一个自定义脚本是更可控、可维护且符合工程实践的选择。
以下是一个生产就绪的递归渲染脚本,它不仅遍历整个输入目录,还智能保留子目录层级、自动识别 .ejs 文件(跳过非模板文件)、支持统一数据源注入,并具备基础错误处理能力:
// render-ejs-dir.js
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');
// ✅ 可配置参数
const INPUT_DIR = 'my-template';
const OUTPUT_DIR = 'output';
const DATA_FILE = 'data.json'; // 根目录下统一数据源,支持嵌套对象与函数
const RENDER_EXTENSIONS = ['.ejs', '.html', '.md', '.json', '.yml']; // 按需扩展
// 加载全局渲染数据(支持 JSON 或 JS 导出)
let globalData = {};
try {
const dataPath = path.join(INPUT_DIR, DATA_FILE);
if (fs.existsSync(dataPath)) {
globalData = path.extname(dataPath) === '.json'
? JSON.parse(fs.readFileSync(dataPath, 'utf8'))
: require(dataPath); // 允许 data.js 返回动态数据或函数
}
} catch (e) {
console.warn(`⚠️ 数据文件加载失败: ${DATA_FILE}`, e.message);
}
// 递归处理目录(含路径映射逻辑)
function renderDirectory(srcDir, relPath = '') {
const entries = fs.readdirSync(srcDir);
for (const entry of entries) {
const srcPath = path.join(srcDir, entry);
const stats = fs.statSync(srcPath);
const currentRelPath = relPath ? path.join(relPath, entry) : entry;
const outputPath = path.join(OUTPUT_DIR, currentRelPath);
if (stats.isDirectory()) {
// 创建对应输出子目录
fs.mkdirSync(outputPath, { recursive: true });
renderDirectory(srcPath, currentRelPath);
} else if (RENDER_EXTENSIONS.some(ext => entry.toLowerCase().endsWith(ext))) {
try {
const content = fs.readFileSync(srcPath, 'utf8');
// 渲染前可注入文件级上下文(如 filename、dirname)
const locals = {
...globalData,
__filename: currentRelPath,
__dirname: path.dirname(currentRelPath),
// 示例:支持按路径动态覆盖数据
...(require('./rules.js')?.getLocals?.(currentRelPath) || {})
};
const rendered = ejs.render(content, locals, {
filename: srcPath, // 关键:启用 include/extends 的路径解析
async: false
});
// 移除 .ejs 后缀(如 index.ejs → index.html),其他扩展名保持不变
const outputExt = entry.toLowerCase().endsWith('.ejs')
? path.extname(entry).replace('.ejs', '') || '.html'
: path.extname(entry);
const outputBasename = path.basename(entry, path.extname(entry));
const finalOutputPath = path.join(
OUTPUT_DIR,
path.dirname(currentRelPath),
outputBasename + outputExt
);
fs.writeFileSync(finalOutputPath, rendered);
console.log(`✅ 渲染完成: ${currentRelPath} → ${path.relative(OUTPUT_DIR, finalOutputPath)}`);
} catch (err) {
console.error(`❌ 渲染失败 [${currentRelPath}]:`, err.message);
throw err;
}
} else {
// 非模板文件直接复制(如 assets/ 下的图片、CSS)
fs.copyFileSync(srcPath, outputPath);
console.log(`? 已复制: ${currentRelPath}`);
}
}
}
// 启动渲染
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
renderDirectory(INPUT_DIR);
console.log('\n? 渲染完成!输出目录:', OUTPUT_DIR);? 关键特性说明:
- ✅ 结构保真:完整复刻输入目录树,子目录自动创建;
- ✅ 智能后缀处理:
.ejs文件默认输出为同名无.ejs后缀(如page.ejs→page.html),其他扩展名(.md,.json)原样保留; - ✅ 统一数据管理:支持
data.json(静态)或data.js(动态,可导出函数或 Promise); - ✅ 上下文增强:内置
__filename/__dirname,便于条件渲染或路径生成; - ✅ 安全容错:跳过非目标文件、捕获渲染异常、非模板资源自动复制;
- ✅ 可扩展性强:通过
rules.js可实现按路径定制局部数据(如/blog/*.ejs注入blogConfig)。
? 使用方式:
- 将上述脚本保存为
render-ejs-dir.js; - 在项目根目录创建
my-template/并放入.ejs模板及data.json; - 运行
node render-ejs-dir.js; - 输出将生成于
output/,结构与my-template/严格一致。
? 注意事项:
- 若模板中使用
,请确保filename选项已传入(脚本中已启用),否则相对路径解析会失败; -
data.js中避免阻塞操作;如需异步数据(如远程 API),可改用ejs.renderFile()配合async/await重构主流程; - 生产环境建议添加
--watch模式(借助chokidar)实现热重载。
该方案规避了对非主流 fork 的依赖,完全基于 EJS 官方 API 构建,兼具简洁性、可读性与可维护性,是规模化 EJS 模板工程化的推荐实践。


















