# koa2加载模板引擎
30 秒速记
- 模板引擎 = 模板文件 + 数据 →
HTML字符串,Koa里用koa-views接入,ctx.render()渲染 ctx.render是异步的,必须await,否则Koa先返回404- 用户内容用转义输出(
EJS的<%= %>),这是防XSS的第一道关 - 模板名不能直接用用户输入,
ctx.render(ctx.query.page)这种写法要拦 - 渲染报错交给统一错误中间件,生产别把模板路径和堆栈返回给前端
模板引擎干的事很简单:拿一份带占位符的 HTML 模板,填进数据,生成最终的 HTML 返回给浏览器。 在 Koa 里一般用 koa-views 中间件把模板引擎接进来,它会在 ctx 上挂一个 render 方法。最常见的坑是忘了 await ctx.render(),中间件函数提前结束,Koa 发现没有 body 就直接回了 404。安全上要记住两点:用户输入的内容用转义输出,模板名别拿用户参数拼。
理解「koa2加载模板引擎」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 middleware 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
Koa 接模板引擎的标准写法:
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const app = new Koa()
// 统一错误处理:放最前面,包住后面所有中间件
app.use(async (ctx, next) => {
try {
await next()
} catch (err) {
ctx.status = err.status || 500
ctx.body = process.env.NODE_ENV === 'production' ? '服务器内部错误' : err.stack
ctx.app.emit('error', err, ctx)
}
})
app.use(views(path.join(__dirname, 'view'), { extension: 'ejs' }))
app.use(async (ctx, next) => {
ctx.state.user = { name: 'Tom' } // 所有模板都能直接用 user
await next()
})
app.use(async (ctx) => {
await ctx.render('index', { title: 'hello koa2' }) // 少了 await 会返回 404
})
app.listen(3000)
几个要点:模板目录用 __dirname 拼绝对路径,不依赖启动目录;错误中间件放最前面,因为 Koa 的洋葱模型里只有外层能 catch 到内层抛出的异常;模板名写死在代码里,不从请求参数里取。EJS 自带编译缓存选项 cache,开发时关掉方便改模板立即生效,生产打开避免每次请求重复编译。
面试官追问
追问 1页面一直返回 Not Found,但模板文件明明存在,最可能漏了什么?
漏了 await。ctx.render 返回 Promise,不 await 的话中间件已经结束,Koa 看 body 为空就回 404。写成 await ctx.render('index', data)。
追问 2模板里用了 product.title,接口偶尔没返回 product,页面直接 500,怎么兜?
模板里访问 undefined 的属性会抛错。数据层先保证结构,缺了就走 404 或降级数据;模板里也可以写 product?.title,EJS 支持现代 JS 语法。错误要被统一错误中间件接住,别把堆栈吐给用户。
追问 3多租户想按 ctx.query.theme 选模板目录,直接拼路径有什么问题?
用户能传 ../../xxx 去加载不该加载的文件。先用白名单把参数映射成固定主题名,不在名单里就用默认主题。
追问 4全局要用的 user,每个 render 都传一遍太麻烦,有更好的写法吗?
在前面的中间件里写 ctx.state.user = user,koa-views 会把 ctx.state 合并进模板变量,模板里直接用 user。
追问 5现在都前后端分离了,服务端模板还有用吗?
有。内容页、营销页、后台小工具这类交互少、要首屏 HTML 和 SEO 的场景,用模板直出成本最低。交互复杂的应用才值得上 React / Vue 加 SSR 框架。
# koa2加载模板引擎
# 快速开始
30 秒速记
- 三步:装
koa-views+ejs→app.use(views(dir, { extension: 'ejs' }))→await ctx.render('index', data) - 目录结构:
index.js加一个view/目录放.ejs文件 render只传模板名,不带扩展名,koa-views按extension补全- 跑起来访问
localhost:3000,右键查看源码,能看到数据已经填进HTML
快速开始就三步:装依赖、注册 koa-views、在中间件里 await ctx.render。 目录结构也很简单,入口 index.js 旁边放一个 view 文件夹,模板都放里面。注册时告诉 koa-views 模板目录和扩展名,之后 render('index') 就会去找 view/index.ejs。跑起来以后我会右键看一下页面源代码,确认标题这些已经是填好的文字,而不是 <%= title %> 原样输出,这说明渲染发生在服务端。
理解「快速开始」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
面试官追问
追问 1页面源码里直接显示 <%= title %>,没被替换,为什么?
模板没走引擎,多半是被当静态文件发出去了,比如 koa-static 的目录里也有这个文件。或者 extension 和文件后缀对不上,koa-views 没找到引擎。检查请求到底命中了哪个中间件。
追问 2模板文件想用 .html 后缀,又想用 EJS 语法,怎么配?
views(dir, { map: { html: 'ejs' } }),意思是 .html 文件交给 ejs 渲染,render('index') 就会找 index.html。
追问 3从别的目录启动服务,报找不到模板,怎么改?
模板目录要用 path.join(__dirname, 'view'),别写 './view',相对路径是按启动目录算的。
追问 4改了模板刷新没变化,要重启才生效,为什么?
开了模板缓存。开发环境关掉缓存,生产再打开;或者开发时用 nodemon 监听 .ejs 文件自动重启。
# 安装模块
30 秒速记
koa-views是适配层,负责在ctx上挂render;ejs才是真正解析模板的引擎,两个都要装npm install --save koa-views ejs,npm 5以后--save是默认行为,可以省略- 依赖必须写进项目的
package.json,全局装的包require找不到 koa-views后续版本迁到了@ladjs/koa-views,新项目装之前看一眼当前维护的包名
接 EJS 要装两个包:koa-views 负责把模板渲染能力接到 Koa 上,ejs 负责真正解析模板语法。 koa-views 本身不认识任何模板语法,它根据扩展名去找对应的引擎来渲染,所以只装它会报找不到引擎。两个包都要装在项目里,写进 package.json,换台机器 npm install 就能复现。npm 5 之后 install 默认就会写进 dependencies,--save 可写可不写。
# 安装koa模板使用中间件
npm install --save koa-views
# 安装ejs模板引擎
npm install --save ejs
面试官追问
追问 1只装了 koa-views 没装 ejs,会怎样?
渲染时报找不到 ejs 模块。koa-views 只是个调度层,具体引擎要单独装。
追问 2同事说他电脑全局装过 ejs,项目里就不用装了,对吗?
不对。require 不会去全局目录找包,他本机能跑可能是上层目录碰巧有 node_modules。依赖一定要写进项目 package.json,CI 和别人才装得上。
追问 3--save 和 --save-dev 该用哪个?
--save。koa-views 和 ejs 是运行时依赖,生产环境渲染页面要用,放 devDependencies 的话
