☰
Express.js 后台开发:用 Handlebars Partials 复用 Header 模板
2026/10/23 3:12:46 网站建设 项目流程

这次我们来看《5分钟学编程 · Express.js 篇》第 27 节的内容:在 Handlebars 中复用后台 Header。标题很短,但解决的是一个非常具体的日常问题——Express 渲染后台管理页面时,头部导航、Logo、用户信息、退出按钮往往在每个页面模板里都要写一遍;等到菜单项调整、文案变化或者接入新的功能入口时,就得逐个页面改 HTML,既费时间又容易漏改。这节课的做法是把 Header 抽成 Handlebars 的 Partial(部分模板),让它从一个页面里的局部片段,变成一个全局可复用的组件。

先给出这篇内容的核心关注点:复用机制是 Handlebars Partials,不依赖第三方 UI 框架,也不要求新的数据库或服务,只要项目已经用 Express + Handlebars 就能直接套用。操作上分为注册 partials 目录、新建 header.hbs、用{{> header}}引入三条完整链路。如果你是正在学 Express.js 的读者,或者手头有 Express + Handlebars 的项目需要整理重复模板,这篇文章可以把这几步拆开,并给出可复制的代码和排错清单。

本文会带你完成的内容包括:搭建一个最小的 Express + Handlebars 环境;创建后台 Header Partial;通过 Layout 统一管理页面骨架;实现菜单高亮与登录状态显示;最后补充常见的模板渲染排错经验和性能观察方法。文章按实操顺序推进,每个环节都有可运行的代码示例,建议先跑通项目再看细节。

适合这样读:先看第 1 节的核心概念速览,确认这个方案和你的技术栈是否匹配;然后按第 3 到第 5 节直接搭项目;第 6 节解决动态 Header 的常见需求;第 7 到第 9 节是遇到问题时最值得回看的部分。

1. 核心概念速览

能力项说明
核心机制Handlebars Partials(局部模板)
配套框架Express.js,服务端模板渲染(SSR)
解决的核心问题后台多页面重复编写 Header
运行环境Node.js 与 npm,建议使用当前 LTS 版本
实现方式注册 partials 目录 +{{> header}}引入
扩展能力支持传参、注册自定义 Helper、与 Layout 配合
本教程是否涉及 API不涉及额外 API 服务,渲染数据直接来自路由

补充说明一下这张表:方案不要求 GPU、不要求特殊编译环境,凡是可以正常跑 Express 的机器都能跑通。它解决的问题不是性能,而是代码结构。后台系统页面越多,Header 抽成 partial 的收益越明显;只有三五个静态页面的场景,收益相对有限。

这里也提前给一个常见误解的澄清:Handlebars 的 partial 不等于前端组件。它没有事件绑定、没有生命周期、没有状态管理,只是在服务端渲染时把一段模板片段按名字插入到另一个模板里。所以不要用 Vue/React 的组件思维去要求它,它的定位是“模板复用”。

2. 适用场景与使用边界

适合什么场景非常明确:后台管理系统,尤其是存在多级路由、多个功能模块、每个模块都需要同一套顶部 Header 的时候。比如订单管理、用户管理、商品管理、权限配置这些页面,如果它们顶部都有 logo、导航栏、用户信息、退出按钮,那 Header 就是最典型的抽取对象。另一个合适场景是官网或者工具站,页面标题区域和主导航保持一致,也可以抽成 partial,避免每个落地页重复粘贴。

同样要说明不适合什么场景。如果项目是纯前端渲染的单页应用(SPA),模板最终在浏览器端完成,Express 只提供 JSON 接口,那 Header 应该交给 Vue/React 组件去管理,没有必要折回服务端模板层。如果项目只有单个页面、内容差异极大,也没有必要为了一个页面抽出 partial,直接写在模板里反而更直观。

还有一个边界必须强调:模板层的复用不等于权限边界。后台 Header 里的用户信息、菜单项、操作入口这些数据,要在渲染前由路由中间件完成鉴权,再传进模板;模板里隐藏或显示某个入口,只是前端交互层面的控制,不能替代后端权限校验。涉及用户隐私数据时,也要避免在模板中输出未脱敏的信息。真正要保护的操作,在服务端接口里必须再校验一次。

3. 环境准备与前置条件

在学习本节前,建议先确认机器上已经具备以下环境。因为后面所有命令都依赖 Node.js 生态,如果环境不对,安装依赖时会直接在第一步报错。

  • Node.js 环境,建议使用当前活跃的 LTS 版本。
  • npm 或 yarn 可用,并且可以正常访问 npm registry。
  • 用于存放项目代码的工作目录,建议使用英文路径,避免模板路径解析时出现不必要的问题。

先检查基础工具是否可用,打开终端执行:

node -v npm -v

两条命令如果能正常输出版本号,说明 Node.js 环境基本可用。如果node -v报错,需要先去安装 Node.js 的 LTS 版本,再回来继续。

然后创建项目目录并初始化 package.json:

mkdir express-hbs-demo cd express-hbs-demo npm init -y

这条命令会生成一个最基础的 package.json,里面暂时没有依赖。接下来安装 Express 和 Handlebars 的接入模块hbs:

npm install express hbs

hbs是 Express 生态中接入 Handlebars 最直接的方式之一,它会把 Handlebars 注册为 Express 的视图引擎,同时保留 Handlebars 的 Partial 和 Helper 机制。如果后面想使用express-handlebars的 Layout 能力,也可以再安装它:

npm install express-handlebars

这里没有强调具体版本号,因为 npm 安装时会在本地锁一个实际解析版本。团队协作时,记得把package-lock.json纳入版本管理,这样新同事拉代码后执行npm install得到的依赖树是一致的。

4. 项目结构与依赖说明

下面的目录结构是本篇教程采用的推荐结构,重点是把模板文件按“页面”和“可复用片段”分开。

express-hbs-demo/ ├── app.js ├── package.json ├── public/ │ └── css/ │ └── admin.css └── views/ ├── partials/ │ ├── header.hbs │ └── footer.hbs ├── index.hbs └── dashboard.hbs

views/partials是 Handlebars Partial 的存放目录,header.hbs就是我们这次要复用的 Header;views下的其他文件是页面级模板。public目录放静态资源,对应 Express 的express.static中间件。这套结构在很多 Express + Handlebars 项目里是通用的,能直观看出哪些文件是整体骨架、哪些文件是业务页面、哪些文件是复用组件。

目录明确后,下一节开始写代码。先给出最小可运行的 Express + hbs 服务,再在它上面扩展 Header Partial。

5. 创建 Header Partial 并完成复用

5.1 最小 Express + hbs 服务

在项目根目录创建app.js,内容如下:

const path = require('path'); const express = require('express'); const hbs = require('hbs'); const app = express(); // 设置视图引擎和视图目录 app.set('view engine', 'hbs'); app.set('views', path.join(__dirname, 'views')); // 注册 Handlebars Partial 目录 hbs.registerPartials(path.join(__dirname, 'views', 'partials')); // 静态资源目录 app.use(express.static(path.join(__dirname, 'public'))); // 首页路由,渲染 index.hbs app.get('/', (req, res) => { res.render('index', { title: '后台管理', user: { name: '管理员' }, activeNav: 'home' }); }); app.listen(3000, () => { console.log('Express + hbs demo running at http://localhost:3000'); });

这段代码做了三件关键事情:

  • app.set('view engine', 'hbs')让 Express 使用 Handlebars 渲染.hbs后缀模板。
  • hbs.registerPartials(...)告诉 Handlebars 去views/partials目录里查找所有局部模板。
  • 路由渲染页面时,通过res.render传入模板变量,后续页面模板和 Header 都能读取这些变量。

此时项目里还没有views/index.hbs,先创建views/partials/header.hbs,把 Header 结构写出来。

5.2 编写 Header Partial 模板

创建views/partials/header.hbs:

<header class="app-header"> <a class="logo" href="/">后台管理系统</a> <nav class="app-nav"> <a class="{{#if (eq activeNav 'home')}}active{{/if}}" href="/">首页</a> <a class="{{#if (eq activeNav 'users')}}active{{/if}}" href="/users">用户管理</a> <a class="{{#if (eq activeNav 'settings')}}active{{/if}}" href="/settings">系统设置</a> </nav> <div class="user-info"> {{#if user}} <span class="user-name">{{user.name}}</span> <a class="btn-logout" href="/logout">退出</a> {{else}} <a class="btn-login" href="/login">登录</a> {{/if}} </div> </header>

这个模板本身没有任何“页面特有”的东西,它只依赖activeNav和user两个变量。这样做的好处是,Header 作为一个整体可以接受外部传入参数,谁需要它,谁就在渲染时把当前页面对应的菜单项和用户信息传进来。

这里用到了eq这个判断 helper,它的作用是判断两个值是否相等,从而给当前菜单项加上 active 样式。需要强调的是,Handlebars 原生并没有内置eq,所以要在app.js里注册它:

hbs.registerHelper('eq', (a, b) => a === b);

把这段加在hbs.registerPartials之后即可。如果忘记注册直接刷新页面,模板中的(eq ...)会被当作普通表达式处理,可能渲染不出预期效果,排查时优先检查这一步。

5.3 在页面模板中引入 Header

创建views/index.hbs:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{title}}</title> <link rel="stylesheet" href="/css/admin.css"> </head> <body> {{> header}} <main class="app-main"> <h1>首页内容</h1> <p>这里是页面主体,业务内容按需输出。</p> </main> </body> </html>

关键一行是{{> header}},它表示把views/partials/header.hbs渲染到当前位置。运行服务后,浏览器访问http://localhost:3000,页面顶部就会显示 Header。

如果再新建一个views/dashboard.hbs,同样在<body>里写上{{> header}},那么两个页面都能获得同一个 Header。后续要修改 logo 文案、调整导航菜单,只需要改header.hbs一个文件,两个页面会同时变化。这就是 Header Partial 最核心的价值。

5.4 用 Layout 再抽一层页面骨架

第一个方案解决了 Header 复用,但页面模板里仍然有重复的 HTML、head、body 骨架。如果后台页面多,最好再把“页面外层结构”也抽成 Layout。这里推荐使用express-handlebars,它对 Layout 的支持更明显。

安装依赖后,新建app-express-handlebars.js:

const path = require('path'); const express = require('express'); const exphbs = require('express-handlebars'); const app = express(); const hbsEngine = exphbs.create({ extname: '.hbs', defaultLayout: 'admin', layoutsDir: path.join(__dirname, 'views', 'layouts'), partialsDir: path.join(__dirname, 'views', 'partials'), helpers: { eq: (a, b) => a === b } }); app.engine('.hbs', hbsEngine.engine); app.set('view engine', '.hbs'); app.set('views', path.join(__dirname, 'views')); app.use(express.static(path.join(__dirname, 'public'))); app.get('/', (req, res) => { res.render('dashboard', { title: '控制台', user: { name: '管理员' }, activeNav: 'dashboard' }); }); app.listen(3001, () => { console.log('express-handlebars demo running at http://localhost:3001'); });

新建views/layouts/admin.hbs:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{{title}}</title> <link rel="stylesheet" href="/css/admin.css"> </head> <body> {{> header activeNav=activeNav user=user}} <main class="app-main"> {{{body}}} </main> </body> </html>

再看页面模板views/dashboard.hbs,现在只要写页面主体内容:

<section class="page-title"> <h1>控制台概览</h1> <p>欢迎回来,这里是系统的核心工作区。</p> </section>

Layout 里的{{> header activeNav=activeNav user=user}}会把 Header 渲染到所有以admin为默认布局的页面中,并且把路由传入的两个变量透传给 Header。页面模板里的{{{body}}}是 Layout 的占位点,最终内容会被插入到这个位置。

这种结构的好处是:Header、Footer、页面骨架的职责分得非常清楚。后台新增页面时,只需要写业务主体模板,不用再关心外层 HTML 结构。

5.5 验证复用是否生效

跑通上面的任意一种方案后,可以做三个验证动作:

第一,访问页面根路径,查看渲染结果中是否出现 Header 的整段 HTML。第二,打开浏览器开发者工具,确认 Header 中的链接、用户信息、class 是否按预期输出。第三,修改header.hbs中的一行文本,比如把“后台管理系统”改成“后台管理平台”,然后刷新页面,看是否所有复用 Header 的页面都同步变化。

如果只改了一个页面、另一个页面没有变化,优先检查页面模板中是否漏写{{> header}},或者是否配置了模板缓存,生产环境启用缓存后新模板不会立即生效。

6. 进阶:让后台 Header 动态化

6.1 路由向 Header 传数据

Header 如果写死菜单和用户,那和静态页没有区别。实际后台项目里,不同用户看到的功能菜单、当前页面状态、用户昵称和头像通常都来自服务端。因此在路由渲染时,需要把当前用户信息和当前菜单状态传给模板。

继续使用前面的路由方式:

app.get('/admin/dashboard', (req, res) => { res.render('dashboard', { title: '控制台', activeNav: 'dashboard', user: { name: '张三', role: 'operator' } }); });

模板中就能直接通过activeNav和user读取这些值。真实生产环境里,user通常来自登录会话,比如从req.session.user取出,再传给模板;activeNav则根据当前请求的 URL 或路由名称由服务端算好,尽量避免在模板里做复杂的地址解析。

6.2 菜单高亮与自定义 Helper

菜单高亮是后台 Header 最常见的动态需求。要让“当前页面”的导航项高亮,比较稳妥的方式是在路由层传入一个activeNav字段,然后在模板里对比当前项是否与它相等。因为 Handlebars 没有内置eqhelper,需要先注册。

以hbs模块为例,在app.js里加上:

hbs.registerHelper('eq', (a, b) => a === b);

然后在header.hbs中使用:

<nav class="app-nav"> <a class="{{#if (eq activeNav 'home')}}active{{/if}}" href="/">首页</a> <a class="{{#if (eq activeNav 'users')}}active{{/if}}" href="/users">用户管理</a> <a class="{{#if (eq activeNav 'settings')}}active{{/if}}" href="/settings">系统设置</a> </nav>

如果不想注册 helper,也可以直接使用嵌套if做判断,比如{{#if activeNav}}只有真假判断,无法精确匹配,所以不建议在菜单高亮场景使用。eq这种小 helper 是 Handlebars 生态里非常常见的扩展点,除了eq,还可以按需注册ne、includes、contains等纯函数型 helper,让模板更接近声明式写法。

6.3 登录状态与用户信息展示

Header 中通常有登录状态区域。未登录时显示“登录”按钮,已登录时显示用户昵称和退出按钮。这部分可以完全由 Handlebars 的条件语法完成:

<div class="user-info"> {{#if user}} <span class="user-name">{{user.name}}</span> <span class="user-role">{{user.role}}</span> <a class="btn-logout" href="/logout">退出</a> {{else}} <a class="btn-login" href="/login">登录</a> {{/if}} </div>

这里有一个容易被忽略的点:用户昵称等字段如果是用户可控输入的,渲染时务必使用双花括号{{user.name}},不要用三花括号。双花括号在 Handlebars 中会做 HTML 转义,能避免把用户输入的内容当作 HTML 代码输出;三花括号是原样输出原始 HTML,适用于确实要渲染富文本内容的场景,不适合放到用户昵称这类普通文本上。

6.4 行内 Partial 与插槽扩展

Handlebars 还支持行内 Partial,用途是给 Header 留一个“插槽”,允许使用 Header 的页面在默认结构之外追加自己的内容。写法是在页面模板里使用{{#> header}} ... {{/header}},然后在header.hbs内部通过{{> @partial-block}}把这段内容输出到指定位置。

页面模板示例:

{{#> header activeNav=activeNav user=user}} <span class="header-extra">本周销量报表</span> {{/header}}

对应的header.hbs在合适位置放:

<div class="header-right"> {{> @partial-block}} </div>

行内 Partial 的优势是:默认 Header 结构不变,个别页面还能在右侧增加自定义按钮或提示信息。这个能力不用每次都用,但知道后排查问题时思路会宽一些。如果页面里的插槽内容重复出现,也可以把插槽内容再抽成一个小 partial,保持层次清晰。

7. 常见问题与排查方法

问题现象可能原因排查方式解决方案
页面 500 报错,提示找不到 partialviews/partials路径未注册检查 registerPartials 调用和目录是否存在修正为绝对路径path.join(__dirname, 'views', 'partials')
Header 区域渲染为空页面模板中没有写{{> header}}查看页面模板源码在需要的位置补上{{> header}}
eqhelper 不生效helper 未注册检查 app.js 是否调用 registerHelper注册eq,或在模板中改用其他条件写法
修改 header.hbs 后页面无变化模板缓存开启观察启动环境变量开发环境关闭缓存,生产更新后重新构建
Header 的 CSS 样式错乱partial 内 class 与 layout 冲突浏览器开发者工具查看计算样式给 Header 根元素统一命名空间,比如app-header
退出/登录状态显示不对路由没传 user检查控制台渲染日志或临时输出变量确认用户信息在渲染前从会话中取出并传参
端口被占用3000 或 3001 已被其他进程使用查看启动日志换端口,或杀掉占用进程
页面显示[object Object]直接输出了对象而没有取属性检查模板中变量写法改为输出user.name这类具体字段

补充两个实际经验。第一,Handlebars 读取不存在的属性时通常不会直接崩溃,而是输出空字符串或 undefined,这类问题隐蔽在正常页面里,不容易一眼看出。排查时可以先在模板里临时放一个{{this}}或直接输出变量,观察路由传参到底到了哪一层。第二,模板报错信息经常指向views/partials/xxx.hbs,但问题不一定在 partial 本身,可能是调用方没有传够参数,比如activeNav没传,导致模板里的判断分支全部走了空。排查时优先核对调用方传参。

8. 模板渲染成本与性能观察

Handlebars 的模板渲染发生在 Node.js 进程内,属于 CPU 操作,不涉及数据库或外部网络请求。对普通后台页面来说,Header partial 的渲染成本很低,通常不会是性能瓶颈,真正需要关注的反而是每次请求里做了多少次业务查询、有没有 Redis 缓存、接口响应体有多大。

开发环境下,Handlebars 会在模板文件变化后重新编译模板,方便修改后直接刷新看到效果;生产环境则会开启模板缓存,避免每个请求都重新编译一次模板。使用express-handlebars时,如果在生产环境修改了模板文件,必须重新部署并重启服务,缓存中的旧模板才会被替换。这个机制值得记住,不然很容易出现“文件明明改了,页面就是不更新”的情况。

观察模板渲染性能可以从三个角度入手。第一,使用浏览器开发者工具里的网络面板,看页面 HTML 返回时间;如果整页返回时间很长,多半是路由中服务端逻辑耗时,而不是模板渲染本身。第二,如果怀疑某个页面模板过于复杂,可以把它拆开,逐个 partial 做最小化替换,观察页面响应是否有明显变化。第三,尽量避免在 helper 中做重逻辑,比如在模板里循环请求接口或者做加密计算,模板层应该保持纯展示,复杂的计算放到路由层预先处理。

还有一点和 Header 直接相关:Header 里如果菜单项很多,或者每个菜单项都要查库判断当前用户是否有权限,这类判断放在请求生命周期中做就好,不要放到模板里。正确的做法是路由层先根据用户角色生成当前用户可见的菜单数组,把数组传给 Header,Header 只负责循环渲染。

9. 最佳实践与使用建议

第一,Partial 命名要语义化。header.hbs、footer.hbs、sidebar.hbs、pagination.hbs这种命名比part1.hbs、part2.hbs清晰得多,项目里文件增多以后可维护性差距会非常大。

第二,Partial 内部尽量弱依赖外部变量。Header 需要的变量最好由调用方显式传入,比如{{> header activeNav=activeNav user=user}},而不是在 partial 内部直接引用全局上下文里的某个字段。显式传参的好处是,Header 被挪到其他页面时,自己能带齐参数,不依赖调用处是否设置了某个变量。

第三,目录结构在一开始就固定下来。用hbs还是express-handlebars,选择一种主要方案并保持一致,不要在一个项目里混用。团队协作时,可以把views/partials、views/layouts的目录约定写进项目 README,减少新成员上手成本。

第四,模板输出要做安全边界。Handlebars 的双花括号默认转义 HTML,这对普通文本内容是最安全的输出方式;三花括号{{{body}}}用于 Layout 中输出子页面内容时是必要的,但对于业务模板中直接展示的用户数据,不要随意使用三花括号,否则容易引入脚本注入风险。

第五,后台系统的权限验证不能只依赖模板。Header 里隐藏了“导出数据”按钮,并不等于用户不能调用导出接口;登录鉴权、操作权限、接口级校验都必须在服务端完成。模板层只做展示控制,安全边界在路由和中间件层。

第六,批量模板改造时建议小步提交。如果老项目已经有大量页面重复写 Header,不要一次性把几十个页面全部改完再重启,可以先抽出一个页面验证,再逐步推进;每次改动后跑一遍页面清单,确认菜单高亮、登录状态、CSS 样式都正常。模板缓存和生产环境不一致的情况,也更容易在这个阶段暴露出来。

10. 总结与下一步

这篇内容围绕一个非常具体的动作展开:在 Express + Handlebars 项目中,把后台 Header 抽成一个 Partial,然后在页面中复用。核心链路是注册 partials 目录、创建header.hbs、通过{{> header}}引入,再配合 Layout 让整个后台页面骨架统一起来。动态菜单高亮和登录状态显示,可以通过路由传参和自定义 Helper 完成,这部分做完以后,后台页面的重复模板就能得到明显收敛。

最容易踩坑的点有三个:一是 partials 路径没有注册对,二是eq这类自定义 Helper 忘记注册,三是生产环境模板缓存导致修改不生效。这三个点对应到排错清单里,基本能覆盖大多数人遇到的页面渲染问题。

下一步可以继续扩展的方向很多:把 Header 里的菜单改成从配置中心读取,让不同管理员看到不同菜单;把 Header 拆成多个更小粒度的组件,比如user-card.hbs、menu-link.hbs;也可以把公共 partial 提取到独立的 npm 包中,供多个后台项目复用。无论往哪个方向走,先把手头这套 Express + Handlebars 的模板复用结构跑顺,后面改造成本都会小很多。建议把本文的连接方式收藏备用,下次写后台管理页面时直接照这套结构落地。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询