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)
meta、link等标签默认即视为自闭合。如需显式自闭合任意标签,在标签名或标签名加属性后追加/:
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.script、textarea、style默认就是纯文本标签,无需加.:
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.descriptionJade 还提供否定形式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→文件模式,文件可同时包含head、body与多个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包,由三层构成:
- 自定义 Lexer(jade-compiler.js)在 jade 原版词法分析器基础上子类化,新增两类 token:
- 内建组件
if/unless/else if/else/with/each; - 用户组件
+组件名(参数),把 Jade 混入语法对接到 Blaze 的{{> component}}/{{#component}}。
- 内建组件
- 自定义 Parser(jade-compiler.js)覆写
parseMixin,特别处理+markdown混入:开启pipeless模式把后续块按原始文本解析,从而实现 Markdown 文本的传递。 - 两个 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.value、if/else if/else链被重组成嵌套的else块、#{expr}插值被替换为{{expr}}而!{expr}被替换为{{{expr}}}(见 parseText)。
值得注意的是,文档中介绍的传统 Jade 特性在 meteor-jade 语境下是被明确禁用的:visitFilter与visitWhen分别对过滤器(filter)和 case 语句抛出 "not supported in meteor-jade" 错误(jade-compiler.js)。
Meteor 包的沙箱化引导
meteor-packages.js 用 Node 的vm模块把htmljs、html-tools、blaze-tools、spacebars-compiler四个 Meteor 包加载进沙箱,并预先注入Package.meteor、Package.underscore._、Package.tracker.Tracker等全局(其中_以原生 JS 实现each/map/indexOf/extend),之后导出HTML、HTMLTools、BlazeTools、SpacebarsCompiler供编译器使用(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),仅供参考