Nunjucks模板继承:利用block与extends实现主页模块化维护的方法
Nunjucks的extends和block通过抽取公共骨架layout njk实现主页结构维护,一处修改全局生效。block命名需语义化并设默认内容,避免业务逻辑。路径需相对于视图目录,注意大小写差异。动态extends可能引发上下文污染,建议基座内用条件判断控制局部差异。
直接给出结论:Nunjucks 的 extends 和 block 并非“锦上添花”的附加功能,而是当主页结构频繁变动时,唯一可掌控的维护手段——前提是,你不再将 header、footer、sidebar 等重复区块硬编码到每个页面。一句话总结:抽取公共骨架,修改一处,全局生效,彻底告别硬编码带来的重复劳动与不可追溯难题。

主页结构变了,为什么修改一个地方却要动十几个文件?
这个问题非常典型。最常见的陷阱,就是把导航栏、页脚等公共区域直接写死在 index.njk、about.njk、post.njk 里。结果呢?一旦设计需要调整——比如新增一个菜单项,或更新版权年份——就得逐个文件打开、逐个替换、逐个测试。这个过程,经历过的人都深有体会。
说一个真实场景:有一次 Hexo 主题升级,layout.njk 里新增了一个 data-skip="true" 属性,但因为所有页面模板都没有继承它,导致页面加载异常。找到问题根源时,简直想拍桌子——没有提取公共骨架,逻辑散落各处,修改起来根本无法追溯。
正确的做法其实很简单:只保留一个 layout.njk 来定义 HTML 骨架和公共区块,其他页面用 {% extends "layout.njk" %} 继承即可。这样,改动一次,全局生效,维护成本瞬间降低。
如何编写一个真正可复用的 layout.njk?
这里的关键不在于“写得全”,而在于“留得准”——只暴露那些必要且稳定的 block,千万别把具体的样式或 JS 初始化逻辑塞进去。否则,layout 本身就成了新的负担。
具体来说,有几点值得注意:
block命名要有语义:使用head、header、main、footer这类名字,避免用content1或sectionA,那会让人摸不着头脑。- 给
block设置默认内容:比如{% block head %}{% endblock %}可以留空,但{% block footer %}© {{ now | date('YYYY') }}{% endblock %}这种就需要提供一个兜底,确保即使子模板未覆盖,页面也不至于空白。 - 避免在
layout.njk里写业务逻辑:不要把{% if isPost %}这类判断放进去,那是子模板该做的事。layout 只需关心骨架结构。
一个示例布局模板片段,供参考:
{{ title or 'My Site' }} {% block head %}{% endblock %} {% block header %}{% endblock %} ... {% block main %}{% endblock %} {% block footer %}© {{ now | date('YYYY') }}{% endblock %}
子模板里 extends 路径写错,为什么报错信息不提示真实位置?
这个问题挺让人头疼的。Nunjucks 默认报错只显示一句“Template render error: Template not found: ./layout.njk”,但实际原因可能是路径、大小写、扩展名或目录权限中的任意一项出了问题。你只能靠经验去猜。
要避开这个坑,可以从几个方面入手:
- 路径必须相对于
views目录(Koa)或source/_layouts(Hexo),而不是相对于当前文件。所以即使post.njk和layout.njk同级,你也得写{% extends "_layouts/layout.njk" %},而不是简单的layout.njk。 - Hexo 用户尤其要注意:
layout.njk默认应放在themes/your-theme/layouts/layout.njk,而不是项目根目录。放错位置,一切免谈。 - 别忘了操作系统的差异:Windows 下大小写不敏感,但 Linux/macOS 是严格区分的。
LAYOUT.NJK在本地可能跑得通,一上服务器就会 404。 - 还有一点:确认 Nunjucks 配置中的
autoescape和watch开关不会影响路径解析——它们只控制渲染行为,但路径错了,这些都白搭。
想让首页使用特殊布局,但又不想复制整个 layout?
这个需求很常见:首页想要一个完全不同的样式,但不能因此就把整个 layout.njk 复制一遍。这时候可以用动态 extends,但得小心上下文污染和编译缓存这两个陷阱。
具体做法是:在路由层传入一个变量,比如 await ctx.render('index', { layout: 'home-layout.njk' })。然后在子模板里写:{% extends layout or 'layout.njk' %}。如果 layout 变量未定义,就默认使用 layout.njk。
但这里有个风险点:如果 home-layout.njk 里没有定义某个 block(比如 sidebar),而父模板却调用了 {{ super() }},那就会报错。这种错误往往很难定位。
所以一个更稳妥的做法是:保持 layout.njk 作为基座,在其内部用 {% if isHome %}...{% endif %} 这样的条件判断来控制局部差异。这样就不用拆分出新的 layout 文件,维护起来也更省心。
说到底,真正麻烦的从来不是语法本身。block 命名一旦定下,后续所有子模板都得去对齐它。删一个 block,表面上看是省事了,但可能让二十个页面突然丢失 SEO meta 信息,这种连锁反应才是最头疼的。所以,设计时多留点余地,总比事后强行补坑要好得多。

