Sails 布局系统(Layouts)完全指南:EJS 视图布局的原理、配置与实战
【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails
导读
在 Sails(Realtime MVC Framework for Node.js)应用中,当页面数量增多时,重复的 HTML 骨架(<head>、导航栏、<footer>等)会导致大量冗余代码。本篇文章以 Sails 官方文档中的 Layouts 章节为核心,系统讲解 Sails 内置的布局系统:如何创建布局文件、如何通过全局配置与局部变量指定布局、布局与视图的"夹心"渲染机制,并结合仓库源码(lib/hooks/views/下的实现)剖析其底层原理。读完本文,你将掌握布局的完整配置矩阵(默认布局、按路由/动作覆盖、全局禁用),理解为何布局支持仅限 EJS,并能在自己的 Sails 项目中熟练使用<%- body %>与布局局部变量写出可维护的页面骨架。
什么是布局(Layout)?
在构建包含许多不同页面的应用时,把多个 HTML 文件共享的标记抽取到一个布局文件中,是一种经典的工程实践。这样做的好处非常直接:
- 减少项目中的重复代码总量(Don't Repeat Yourself 原则);
- 后续修改共享标记(如站点导航、统计脚本)时,只需改动一处,无需在多个文件中同步修改。
在 Sails 与 Express 生态中,布局通常由视图引擎自身实现。例如jade(现名 pug)拥有自己独立的布局系统与语法。为了让开发者开箱即用,Sails 针对默认视图引擎 EJS 内置了特殊的布局支持;如果使用其他视图引擎,则需要查阅该引擎自身的布局/局部模板语法(可参考 ViewEngines 一节)。
布局的语义非常直观:Sails 布局是位于应用
views/目录下的特殊.ejs文件,用于"包裹"(wrap)或"夹住"(sandwich)其他视图。布局通常包含 HTML 文档的前缀(<!DOCTYPE html><html><head>....</head><body>)与结尾(</body></html>),原始视图文件通过<%- body %>注入到布局之中。布局永远不会脱离视图单独使用——那就像"只上一片面包的三明治"(bread sandwich)。
布局支持在 Sails 中的应用范围与默认行为
布局支持可以在config/views.js中配置或整体禁用,也可以针对某一条路由或某一个动作通过设置一个名为layout的特殊 局部变量(local) 来覆盖。
默认情况下,Sails 会使用位于views/layouts/layout.ejs的布局编译所有视图。这一点可以从源码得到印证:views 钩子的隐式默认配置(get-implicit-defaults.js)中定义了:
views: { // 视图文件扩展名 extension: 'ejs', // 布局默认开启,位于视图目录顶层 // false === 不使用布局 // string === 布局路径(绝对路径或相对 views 目录的路径,不含扩展名) layout: 'layout' }同时隐式注册了两个路径:paths.views指向应用的views/目录,paths.layout指向views/layout.ejs。因此,默认布局文件的完整约定为views/layout.ejs,而 Sails 脚手架(sails new生成的应用)中实际使用的是views/layouts/layout.ejs这一组织方式,其作用是相同的:该文件充当所有服务端渲染视图的默认布局,在自定义视图发送给客户端之前被注入其中(参见 layout.ejs 说明)。
值得注意的是config/views.js中还处理了旧配置的兼容:若使用已被弃用的sails.config.views.engine,Sails 会打印弃用提示,并将其ext值迁移到sails.config.views.extension(见 configure.js)。
创建布局文件
创建一个布局,就是在应用的views/目录(或views/layouts/子目录)下新建一个.ejs文件。一个典型的默认布局形如:
<!DOCTYPE html> <html> <head> <title><%= _.escape(title) %></title> <%- blocks.stylesheets %> </head> <body> <header><!-- 全站导航 --></header> <%- body %> <footer><!-- 全站页脚 --></footer> <%- blocks.scripts %> </body> </html>关键点在于:原始视图文件的内容会通过<%- body %>(非转义输出语法)注入到布局中。之所以使用<%- %>而不是<%= %>,是因为视图本身是已经编译好的 HTML 字符串,不应再做 HTML 转义。
布局文件也是放置"每个视图都要用到"的 JavaScript 与 CSS 引用的最佳位置——这样你就不必在每个自定义.ejs文件里重复引入它们。
布局中的 blocks 机制
从仓库的默认渲染函数(default-view-rendering-fn.js)可以看到,Sails 在渲染时还会为每个请求注入一组blocks局部变量:
var blocks = { scripts: new Block(), stylesheets: new Block() }; options.locals.blocks = blocks; options.locals.scripts = blocks.scripts; options.locals.stylesheets = blocks.stylesheets; options.locals.block = block.bind(blocks); options.locals.stylesheet = stylesheet.bind(blocks.stylesheets); options.locals.script = script.bind(blocks.scripts);这意味着你可以在子视图中通过block、stylesheet、script这三个辅助函数,向布局中的占位区域追加内容(Block支持append/prepend/replace三种操作,见同文件 Block 实现):
<% block('stylesheets', '<link rel="stylesheet" href="/styles/account.css" />') %> <% script('/js/account-page.js') %>然后在布局中通过<%- blocks.stylesheets %>与<%- blocks.scripts %>统一输出。这是一种轻量级的"页面级资源注入"方案,非常适合多页面应用的按页定制。
全局配置:在 config/views.js 中控制布局
布局的全局开关位于config/views.js。Sails 1.x 中相关的核心配置项(由 configure.js 处理)如下:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
sails.config.views.extension | string /false | 'ejs' | 视图文件扩展名(不带前导.,若写成.ejs会自动去除)。设为false可用于某些特殊场景 |
sails.config.views.layout | string / boolean / falsy | 'layout' | 布局路径(相对views/目录、不含扩展名);true表示使用默认位置;false表示完全禁用布局 |
sails.config.views.getRenderFn | function / undefined | undefined | 自定义渲染函数工厂;一旦提供,Sails 内置的 EJS 布局支持将失效(见下文"其他视图引擎") |
配置示例:
// config/views.js module.exports = { // 默认布局位于 views/layouts/layout.ejs layout: 'layouts/layout', // 或:完全禁用布局(适合纯 API 应用或使用自定义引擎的场景) // layout: false };关于layout配置值的细节,源码(configure.js)中有清晰注释:
- 若配置为字符串,它被解释为从
views/目录出发的相对路径(不含扩展名); - 若配置为非字符串但有值(如
true),相对路径默认回退到layout.<extension>; - 若配置为假值(
false),则不使用任何布局。
按路由指定布局
你可以为某条路由指定其专属布局。下面的示例路由会把位于views/users/privacy.ejs的视图,渲染进位于views/users.ejs的布局中:
// config/routes.js module.exports = { routes: { 'get /privacy': { view: 'users/privacy', locals: { layout: 'users' } } } };这里view指明视图文件的相对路径(不含扩展名),locals.layout指明该路由使用的布局。这种写法依赖 views 钩子对{ view: '...' }路由目标语法的支持:路由系统抛出route:typeUnknown事件后,views 钩子通过 onRoute.js 将视图路径转换为res.view(...)调用并绑定到路由。从该源码还可以看到一个限制:视图名中不能包含.(会打印错误并忽略该绑定);若指定的视图在视图哈希(views hash,由statViews构建)中是一个目录,则会自动回退渲染该目录下的index视图。
在控制器动作中指定布局
在控制器动作里,通过res.view(path, { layout: '...' })的第二个参数(局部变量)即可指定布局。下面的动作会渲染views/users/privacy.ejs,并套用views/users.ejs布局:
// api/controllers/UserController.js module.exports = { privacy: function (req, res) { return res.view('users/privacy', { layout: 'users' }); } };这里res.view()是 Sails 对 Expressres.render()的增强版本(原始res.render()仍可通过该对象访问)。其完整实现位于 res.view.js:它会根据req.options.action或req.options.view推断默认视图路径,将req.options.locals与sails.config.views.locals合并进局部变量,最后调用底层渲染。
布局解析的优先级
从 res.view.js 的源码可以还原出布局解析的完整优先级链(前提是未设置getRenderFn,即使用默认 EJS 布局支持):
- 局部变量
locals.layout——显式传入res.view(path, { layout: '...' })或路由locals中的layout; - 若
locals.layout为undefined或true,回退到全局配置sails.config.views.layout; res.locals.layout(若被设置为非undefined)拥有最高覆盖权;- 最终若布局值不是字符串,则视为
false(不使用布局)。
确定布局后,Sails 会计算布局文件相对于视图文件的路径,并把它写入res.locals._layoutFile局部变量(res.view.js),供底层渲染函数消费。
布局渲染的底层原理:body 注入与递归编译
布局的"夹心"效果究竟是如何实现的?答案在默认渲染函数 default-view-rendering-fn.js 中。该文件是 Sails 对已停止维护的ejs-locals包(Express 2 时代的布局/局部模板实现)针对 EJS >= 2.3.4 的改写,其核心流程如下:
- 先渲染子视图本身,得到 HTML 字符串;
- 读取
options.locals._layoutFile(即上面提到的_layoutFile)得到布局路径;若布局为true,回退到默认位置/layout.ejs;若布局缺少扩展名,自动补上.ejs; - 清空
_layoutFile与filename,避免无限递归(这同时也支持了布局嵌套——布局内部还可以再套布局); - 解析布局的绝对路径:以
/开头的路径相对于views目录解析,否则相对于当前模板所在目录解析; - 把子视图渲染结果赋给
options.locals.body; - 递归调用
renderFile(layout, options, fn)渲染布局,此时布局中的<%- body %>便输出了子视图的 HTML。
若渲染过程中发生视图错误,Sails 会构造带E_VIEW_FAILED代码的错误对象,并尽可能给出"试图加载的视图路径 + 布局路径"的诊断信息(res.view.js)。
与之平行的是程序化渲染入口sails.renderView()(render.js),它同样支持通过options.layout指定布局,并遵循"局部变量未指定则回退全局配置"的同一规则(render.js);若配置了getRenderFn(自定义引擎),内置布局会被强制关闭(render.js)。你可以在任何能访问sails对象的地方使用它生成 HTML 字符串(例如发送 HTML 邮件或拼装 XML)。
为什么布局只对 EJS 生效?
这是 Sails 布局体系最常被问到的问题,官方文档的 Notes 部分给出了完整的历史背景:
几年前,Express 内置的布局/局部模板支持被弃用,官方期望开发者依赖视图引擎自身来实现该功能(参见 balderdashy/sails issue #494)。Sails 出于兼容性与便捷性考虑保留了这套传统
layouts特性:它与 Express 2.x、Sails 0.8.x 应用保持向后兼容,也让来自其他 MVC 框架的社区新人感到熟悉。因此,布局只针对默认视图引擎(ejs)经过测试。
这一约束在源码层面体现得非常彻底:
res.view()中布局逻辑的前提条件是!sails.config.views.getRenderFn(res.view.js);sails.renderView()在检测到getRenderFn时强制layout = false(render.js);- 配置阶段若检测到
getRenderFn且同时设置了sails.config.views.layout,会打印错误日志:"Sails 内置布局仅适用于默认 EJS 引擎,使用自定义引擎时请自行实现布局"(configure.js)。
结论:如果你不喜欢这套布局机制,或者当前正在使用 EJS 之外的服务端视图引擎(如 Jade/pug、handlebars、haml、dust 等),请在sails.config.views中设置layout: false,转而依赖所选视图引擎自带的 layout/partial 支持。
实战速查:布局使用场景一览
| 需求 | 做法 | 位置 |
|---|---|---|
| 全站使用统一骨架 | 保持默认即可,编辑views/layouts/layout.ejs | 布局文件 |
| 全局更换布局路径 | 设置sails.config.views.layout = 'layouts/admin' | config/views.js |
| 全局禁用布局 | 设置sails.config.views.layout = false | config/views.js |
| 某条路由用特殊布局 | 路由 target 的locals: { layout: 'users' } | config/routes.js |
| 某个动作用特殊布局 | res.view('users/privacy', { layout: 'users' }) | 控制器动作 |
| 子视图向布局注入资源 | 在子视图中用block/script/stylesheet,布局中用<%- blocks.scripts %>等输出 | 视图文件与布局文件 |
| 非 EJS 引擎 | layout: false,改用引擎自带布局语法 | config/views.js |
延伸阅读
- Views 概览:视图的创建、编译与单页应用引导方式
- Locals:视图局部变量的完整机制,以及用
<%- exposeLocalsToBrowser() %>安全地向浏览器注入数据 - ViewEngines:如何通过
extension+getRenderFn切换视图引擎 - config/views.js 详解 与 sails.config.views 参考
- 源码级参考:默认渲染函数(布局/局部模板实现)、res.view 实现、views 钩子入口
【免费下载链接】sailsRealtime MVC Framework for Node.js项目地址: https://gitcode.com/gh_mirrors/sa/sails
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考