Wekan 中 Jade 模板引擎完整语法指南:从命令行到 meteor-jade-loader 源码级解析
2026/9/14 18:31:27 网站建设 项目流程

Wekan 中 Jade 模板引擎完整语法指南:从命令行到 meteor-jade-loader 源码级解析

【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan

导读

Jade(后更名为 Pug)是一种受 Haml 影响、用 JavaScript 实现的、对空白敏感的 HTML 模板语言,它以极简的缩进语法取代了冗长的标签书写。本文以仓库内捆绑的 Jade 官方语法文档 jade.md 为骨架,结合 Wekan 项目实际使用的meteor-jade-loader(Rspack/webpack 加载器)源码,系统讲解 Jade 的 CLI 用法、全部核心语法(标签、属性、插值、代码、条件、循环、混入),并揭示.jade文件在 Wekan 中是如何被编译成 Blaze 模板注册代码的。读完本文,你将既能独立编写 Jade 模板,也能理解 Wekan 前端模板的完整编译链路。


一、Jade 是什么

Jade 是为 Node.js 设计的高性能模板引擎,其语法受 Haml 影响,通过缩进(indentation)表达嵌套结构,自动为你补全闭合标签,并支持在模板中内嵌任意 JavaScript 表达式。在 npm-packages/meteor-jade-loader 中捆绑的是由 mquandalle 分支维护的 jade 1.3.0 版本(见 jade-compiler.js 的注释说明),它被进一步改造成可以编译输出 Spacebars/Blaze 模板代码,这正是 Wekan 前端client/components下 130 余个.jade文件所使用的引擎。

一个最简示例即可感受其风格:

doctype html html(lang="en") head title= pageTitle body h1 Jade - node template engine #container.col if youAreUsingJade p You are amazing else p Get on it!

它等价于一段结构相同的 HTML,但书写量大幅减少,且天然保证标签配对正确。

二、命令行用法(CLI)

原文档给出了完整的一行式命令语法:

jade [-h|--help] [-v|--version] [-o|--obj STR] [-O|--out DIR] [-p|--path PATH] [-P|--pretty] [-c|--client] [-D|--no-debug]

各参数含义如下:

选项作用
-h, --help显示帮助信息
-v, --version显示版本号
-o, --obj STR传入 JSON 字符串作为模板渲染数据对象
-O, --out DIR指定输出目录(0.31.0 起推荐用-O
-p, --path PATH设置模板文件的基准路径,用于extends/include解析
-P, --pretty美化输出 HTML 缩进格式
-c, --client编译为客户端可用的 JavaScript 函数(需要运行时 runtime.js)
-D, --no-debug编译客户端模板时去掉调试插桩,输出更轻量(需配合--client

官方文档给出了若干可直接运行的示例:

# 翻译整个 templates 目录下的所有 .jade 文件 $ jade templates # 生成 {foo,bar}.html $ jade {foo,bar}.jade # 通过标准输入输出流(stdio)转换 $ jade < my.jade > my.html # 管道方式:回显一行 Jade 直接得到 HTML $ echo "h1 Jade!" | jade # 同时编译 foo、bar 两个目录,输出到 /tmp $ jade foo bar --out /tmp # 编译为客户端模板且不做调试插桩,产物轻量 # (项目运行时需要引入 runtime.js) $ jade --client --no-debug < my.jade

值得一提的是,文档中提示自 0.31.0 起script/style的隐式纯文本行为被废弃,需要在标签后显式加.;同时输出目录选项从-o调整为-O,这两点与本仓库捆绑的 1.3.0 版本行为一致。

三、标签与块(Tags & Blocks)

标签通过空白(缩进)进行嵌套,闭合标签由引擎代劳,这些缩进结构称为"块"(blocks):

ul li a Foo li a Bar

同一个块内也可以并列多个兄弟标签:

ul li a Foo a Bar a Baz

这等价于三个<li>下各有一个链接。

四、自闭合标签(Self-closing Tags)

metalink等标签默认即视为自闭合。如需显式自闭合任意标签,在标签名或标签名加属性后追加/

foo/ foo(bar='baz')/

编译结果为:

<foo/> <foo bar="baz"/>

在 vendor/jade/lib/self-closing.js 中维护着默认自闭合标签列表,编译阶段会依据该表自动处理。

五、属性(Attributes)

属性书写与 HTML 类似,但属性值就是普通 JavaScript,因此三元运算、逻辑表达式都可以直接使用:

a(href='google.com') Google a(class='button', href='google.com') Google body(class=user.authenticated ? 'authenticated' : 'anonymous') a(href=user.website || 'http://google.com')

属性支持多行书写,带逗号、不带逗号、以及"任性"的空白排版均可:

input(type='checkbox', name='agreement', checked) input(type='checkbox' name='agreement' checked) input( type='checkbox' name='agreement' checked)

布尔属性

布尔属性接受true/false,省略值时默认为true

input(type="checkbox", checked) // => <input type="checkbox" checked="checked" /> input(type="checkbox", checked=user.agreed) // 当 user.agreed 为 true 时同样输出 checked="checked"

类属性(Class attributes)

class属性可以接收数组,便于由 JS 函数动态生成:

- classes = ['foo', 'bar', 'baz'] a(class=classes) // => <a class="foo bar baz"></a>

类字面量(Class literal)

.CLASSNAME语法声明类,默认生成<div>

.button // => <div class="button"></div> .large.button // => <div class="large button"></div> h1.title My Title // => <h1 class="title">My Title</h1>

ID 字面量(Id literal)

与类字面量对应,用#ID语法声明 id:

#user-1 // => <div id="user-1"></div> ul#menu li: a(href='/home') Home li: a(href='/store') Store li: a(href='/contact') Contact

类、id、属性还可以任意组合,以下写法完全等价:

a.button#contact(style: 'color: red') Contact a.button(style: 'color: red')#contact Contact a(style: 'color: red').button#contact Contact

块展开(Block expansion)

标签后跟一个尾随冒号:即可内联注入一个块:

ul li: a Foo li: a Bar li: a Baz

这与上面的ul#menu例子结合使用,可以写出非常紧凑的导航菜单。

六、文本(Text)

普通文本直接跟在标签后面:

p Welcome to my site // => <p>Welcome to my site</p>

管道文本(Pipe text)

管道符|充当大段文本的"文字边距",适合多行文本:

p | This is a large | body of text for | this tag. | | Nothing too | exciting.

输出:

<p>This is a large body of text for this tag. Nothing too exciting. </p>

管道文本中还可以继续混入普通 Jade 标签:

p | Click to visit a(href='http://google.com') Google | if you want.

纯文本标签(Text only tags)

在标签后加尾随.,表示块内全部是纯文本、不含标签:

p. This is a large body of text for this tag. Nothing too exciting.

scripttextareastyle默认就是纯文本标签,无需加.

script if (foo) { bar(); } style body { padding: 50px; font: 14px Helvetica; }

模板 script 标签

当需要在页面里用<script>嵌入客户端模板片段时,只需给script一个任意type属性(如text/x-template),内部仍可正常使用 Jade:

script(type='text/template') h1 Look! p Jade still works in here!

七、插值(Interpolation)

普通文本与管道文本都支持插值,分为转义与非转义两种形式:

p Welcome #{user.name} // HTML 会被转义,防止 XSS p Welcome !{user.name} // 不转义 HTML,只应使用可信字符串

内联 HTML

也可以在 Jade 中直接嵌入一小段 HTML:

p Welcome <em>#{user.name}</em>

八、代码(Code)

缓冲输出:=!=

行首或标签后的=会将表达式结果缓冲到输出,并转义其中的 HTML:

p= user.description

!=为不转义版本,需谨慎防范 XSS:

p!= user.description

非缓冲代码:-

-用于执行 JavaScript 而输出结果,适合定义变量、写条件等:

- var user = { description: 'foo bar baz' } #user - if (user.description) { h2 Description p.description= user.description - }

编译后的块被包裹在匿名函数中,因此也可以省略大括号:

- var user = { description: 'foo bar baz' } #user - if (user.description) h2 Description p.description= user.description

甚至可以使用.forEach()等任意 JS 手段:

- users.forEach(function(user){ .user h2= user.name p User #{user.name} is #{user.age} years old - })

赋值(Assignment)

Jade 的一等赋值非常简单:使用=运算符即会自动var声明:

- var user = { name: 'tobi' } user = { name: 'tobi' } // 与上一行等价

九、条件(Conditionals)

一等条件语法允许省略括号,也可以省略行首的-,其余仍是普通 JavaScript:

user = { description: 'foo bar baz' } #user if user.description h2 Description p.description= user.description

Jade 还提供否定形式unless,以下两种写法等价:

- if (!(user.isAnonymous)) p You're logged in as #{user.name} unless user.isAnonymous p You're logged in as #{user.name}

十、迭代(Iteration)

Jade 提供更声明式的for循环结构,别名each

for user in users .user h2= user.name p user #{user.name} is #{user.age} year old each user in users .user h2= user.name

可以同时取得索引:

for user, i in users .user(class='user-#{i}') h2= user.name

本质上仍是 JavaScript,直接内嵌数组亦可:

ul#letters for letter in ['a', 'b', 'c'] li= letter

十一、混入(Mixins)

混入用于抽象出大段可复用的 Jade 片段,调用时以+前缀。最简单的无参混入:

mixin hello p Hello +hello

带参数的混入会被编译成 JavaScript 函数:

mixin hello(user) p Hello #{user} +hello('Tobi') // => <p>Hello Tobi</p>

混入可以接收块:传入块时其内容成为隐式的block参数:

mixin article(title) .article .article-wrapper h1= title if block block else p No content provided +article('Hello world') +article('Hello world') p This is my p Amazing article

输出两段结构相同的文章卡片,第二段带内容块:

<div class="article"> <div class="article-wrapper"> <h1>Hello world</h1> <p>No content provided</p> </div> </div> <div class="article"> <div class="article-wrapper"> <h1>Hello world</h1> <p>This is my</p> <p>Amazing article</p> </div> </div>

混入还能像标签一样接收属性,属性会成为隐式的attributes参数,可像普通对象属性一样访问:

mixin centered .centered(class=attributes.class) block +centered.bold Hello world +centered.red p This is my p Amazing article

输出:

<div class="centered bold">Hello world</div> <div class="centered red"> <p>This is my</p> <p>Amazing article</p> </div>

若直接把attributes传给标签,则传入的所有属性都会被使用:

mixin link a.menu(attributes) block +link.highlight(href='#top') Top +link#sec1.plain(href='#section1') Section 1 +link#sec2.plain(href='#section2') Section 2

输出:

<a href="#top" class="highlight menu">Top</a> <a id="sec1" href="#section1" class="plain menu">Section 1</a> <a id="sec2" href="#section2" class="plain menu">Section 2</a>

带参数与属性的混入调用,参数需紧跟混入名,属性放后面的括号中:

mixin list(arr) if block .title block ul(attributes) each item in arr li= item +list(['foo', 'bar', 'baz'])(id='myList', class='bold')

输出:

<ul id="myList" class="bold"> <li>foo</li> <li>bar</li> <li>baz</li> </ul>

十二、从文档语法到 Wekan 源码:meteor-jade-loader 的编译链路

上面是原文档的全部语法内容。在 Wekan 仓库中,这些语法并不是用 jade 原版 CLI 处理的,而是经由一个定制加载器完成,理解它能让你把"怎么写模板"与"模板如何变成可运行代码"打通。

加载器入口

rspack.config.js 中为所有.jade文件注册了加载器:

{ test: /\.jade$/, use: [path.resolve(__dirname, 'npm-packages/meteor-jade-loader')], },

加载器本体 index.js 是一个标准的 Rspack/webpack loader:接收.jade源码,返回一段注册 Blaze 模板的 JavaScript。它按文件名区分两种模式:

  • 文件名以.tpl.jade结尾 →模板模式,整个文件内容即一个模板的 AST;
  • 其余.jade文件模式,文件可同时包含headbody与多个template根节点。

文件模式生成的 JS 形如:

var Template = Package["templating-runtime"].Template; var HTML = Package.htmljs.HTML; var Blaze = Package.blaze.Blaze; var Spacebars = Package.spacebars.Spacebars; var Meteor = Package.meteor.Meteor; Template.body.addContent(renderFunc); Meteor.startup(Template.body.renderToDocument); Template.__checkName("templateName"); Template["templateName"] = new Template("Template.templateName", renderFunc);

其中generateTemplateJS(index.js)负责模板注册,generateBodyJS(index.js)负责<body>内容的挂载。若编译出错,加载器会通过this.emitError把错误透传给构建工具,并返回注释占位模块避免整个构建崩溃(index.js)。

编译器内部:Lexer / Parser / Transpiler

核心编译逻辑在 jade-compiler.js,它改自 Meteor 生态的mquandalle:jade-compiler包,由三层构成:

  1. 自定义 Lexer(jade-compiler.js)在 jade 原版词法分析器基础上子类化,新增两类 token:
    • 内建组件if/unless/else if/else/with/each
    • 用户组件+组件名(参数),把 Jade 混入语法对接到 Blaze 的{{> component}}/{{#component}}
  2. 自定义 Parser(jade-compiler.js)覆写parseMixin,特别处理+markdown混入:开启pipeless模式把后续块按原始文本解析,从而实现 Markdown 文本的传递。
  3. 两个 Transpiler负责把 Jade AST 翻译成 Spacebars AST:
    • FileCompiler(jade-compiler.js)识别head/body/template(name=...)根节点,并主动拒绝doctype(提示 "Meteor sets the doctype for you")、重复定义同名模板、head带属性等非法用法;
    • TemplateCompiler(jade-compiler.js)递归访问节点:textarea/style视为纯文本节点、script被转为attrs.valueif/else if/else链被重组成嵌套的else块、#{expr}插值被替换为{{expr}}!{expr}被替换为{{{expr}}}(见 parseText)。

值得注意的是,文档中介绍的传统 Jade 特性在 meteor-jade 语境下是被明确禁用的:visitFiltervisitWhen分别对过滤器(filter)和 case 语句抛出 "not supported in meteor-jade" 错误(jade-compiler.js)。

Meteor 包的沙箱化引导

meteor-packages.js 用 Node 的vm模块把htmljshtml-toolsblaze-toolsspacebars-compiler四个 Meteor 包加载进沙箱,并预先注入Package.meteorPackage.underscore._Package.tracker.Tracker等全局(其中_以原生 JS 实现each/map/indexOf/extend),之后导出HTMLHTMLToolsBlazeToolsSpacebarsCompiler供编译器使用(meteor-packages.js)。加载结果在进程内缓存,保证每个构建只执行一次。

真实示例:popup.tpl.jade

client/components/main/popup.tpl.jade 是模板模式的典型代表,它以.tpl.jade结尾,整个文件编译为一个名为popup的 Blaze 模板。文件中用到了本文介绍的大量语法:

.pop-over.js-pop-over( class="{{#unless title}}miniprofile{{/unless}}" class=themeColorClass contenteditable="false">【免费下载链接】wekanThe Open Source kanban, built with Meteor. GitHub issues/PRs are only for FLOSS Developers, not for support, support is at https://wekan.fi/commercial-support/ . PR source translation to imports/i18n/data/en.i18n.json, other translations at https://app.transifex.com/wekan/wekan项目地址: https://gitcode.com/GitHub_Trending/we/wekan

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询