PUG在前端项目中的应用与实践

3,090 阅读6分钟

如果你还不认识pug

PUG原名JADE(官网:https://www.pugjs.cn/api/getting-started.html) 是个高性能html模板引擎,用于Node.js和浏览器的JavaScript实现。后因"Jade"为注册商标而改为现名pug。pug是由javascript语言编写,功能强大,只要会一点html和 javascript基础的都能在一小时内上手使用。因学习成本极低、效率高、功能强大得到了包括后端在内的众多开发者的喜爱。其抛弃html尖括号和闭合标签,使用换行+缩进来表示嵌套关系的代码风格也得到了很多程序员的喜爱。

注:pug广泛适用于多种语言,本文仅以前端视角来介绍pug,下面是各个编程语言对pug的支持情况,详情请移步于github

作为一个前端开发同行,你可能会有这样的疑惑:现阶段前端领域vue和react框架已经应用于绝大部分前端项目,vue和react都有其各自功能强大的html模板。pug、ejs、handlerbar等传统模板引擎已无用武之地,应该随着前端技术的发展被淘汰了,再谈pug似乎已经没有多大的意义了。

对于ejs、handlerbar 现阶段确实已经无人问津,然而pug因其简洁高效的编程风格,强大的编译能力和丰富的功能特性并没有被前端同行们抛弃。社区通过各种手段把pug支持到vue和react项目中,延续使用pug的热情不减当年。Vue框架作者尤雨溪为了vue项目能支持pug,配套了 pug-plain-loader ,并默认加在各版本的vue-cli webpack配置中。 不要问我为什么,问就是pug就像你第一次入手机械键盘,用了就停不下来,再也不想用回普通的薄膜键盘(再也不想写原生html)。本文将详细介绍pug在前端项目中的实战应用。

不习惯、不喜欢pug都可以理解, 请不要否定pug 。第一次写文章, 如有错误和不理解欢迎留言。

pug在mvvm框架支持方式

  • vue:以loader形式通过配置webpack后使用。vue-cli默认官方已配置了pug-plain-loader可以做到只要$ npm i vue-plain-loader -D 就可以使用。使用方式:
<template lang="pug">
    - var class = "pugClassName"

    .wrap(

        @click="VuemethodsHandler"

        :class="[pugClassName, 'className', VueDataClassName]"

    )

    span= loaderData + 'loaderData 是从loader带来的变量'

    - var url= 'https://www.klook.com'

    a(href=url, :class="VueDataClassName") 这是个链接
</template>

社区还存在pug-loader, pug-html-loader 等loader。pug+vue 在线体验链接

export const ReactComponent = props => pug`
.wrapper
    if props.shouldShowGreeting
        p.greeting Hello World!
        button(onClick=props.notify) Click Me`

PUG对比原生HTML和其他前端模板引擎

在很多年以前 前后端分离有了实践,VUE 和 REACT 还没有或者不太盛行的时候。传统的nodejs ssr项目在匹配到路由的时候需要返回一个html给浏览器。有时候还需要在nodejs服务端请求后端接口拿到一部分数据供客户端首屏渲染,然而原生html没有这些能力。

原生HTML存在的痛点:

  1. 纯静态,没有动态变量概念
  2. 不能复用,没有组件概念,每个页面对应一个html文件
  3. 当html结构较深,代码多的时候或者没有规范的换行缩进会使代码看的十分杂乱,让人摸不着头绪,难以维护和排查问题
<body><div>123</div>

<div>123</div><p><span>789</span></body>

这时候需要一个前端模板引擎来帮我们制作模板并承载一些变量。EJS、handlerbar、pug 等模板引擎在这些时候得到了大规模应用。

什么是前端模板引擎

模板引擎的核心概念是开发者提供要编译的模板文件,和包含变量集合的对象(我们本文称之为local) 然后交给处理程序来编译输出原生的html的字符串或者html文件。其实现类似于如下代码

function compile(template: string, local: { [key: string]: any }): string {
    // 把上下文使用with指向local后, local对象里的所有key相当于处于顶级作用域的变量
    with(local) {
        // compile template ...
        return compiledHtml
    }
}

ejs 和 handlerbar 等模板引擎是在传统的html文件上通过一些特殊符号(方便正则查找)圈起来一些变量(编译的时候替换为指定的值)和特殊的语法来实现编译。功能十分有限。虽说相对原生html解决了纯静态和不能复用的问题,但用起来极其麻烦,语法规则也很小众,使用体验并不好。并且在使用了这些模板引擎后原生html痛点3的问题更加严重!

  • handlerbar 模板支持的功能十分有限,需要自己注册很多处理函数
<div>
    {{ handlerbar变量 }}
    {{#if a > b}}
    ...
    {{#else}}
    ...
    {{/if}}
    {{#unless isSame a b}}
    ....
    {{/unless}}
</div>
<script>
// 这么简单的功能还需要自己注册个helper来实现
Handlebars.registerHelper('isSame', function (a, b) {
    return a === b
})

</script>
  • EJS更是难用到无法形容这一堆火星文让人看的眼花缭乱
<% if (names.length) { %>
    <ul>
    <% names.forEach(function(name){ %>
        <li foo='<%= name + "'" %>'><%= name %></li>
    <% }) %>
    </ul>
<% }%>

而pug能解决这些痛点。

pug简介

pug不需要尖括号包裹标签名, 甚至最常用的div标签都可以省略。并且不需要闭合标签,以换行缩进表示嵌套关系。如果你没有规范换行缩进,那么编译的结果可能不符合你的预期,甚至pug会给你报错。

本文很多案例出自官方示例,建议你花几十分钟时间阅读官方文档更深入了解pug

注释

编译前编译后
image.pngimage.png

代码 Code

  • - 开始的行不直接进行输出的代码,可以在这行执行javascript 表达式
  • = 开始的行表示带有输出的代码,它应该是可以被求值的一个 JavaScript 表达式。为安全起见,它将被 HTML 转义
  • != 开始的行代表不转义的,带有输出的代码。这将不会做任何转义,所以用于执行用户的输入将会不安全
  • =!=可以写成行内形式,支持所有的 JavaScript 表达式:
  • 在行内插入变量可以使用#{变量名}
编译前编译后
image.pngimage.png

变量:你在写pug模板的同时,也在写Javascript

变量是pug十分强大的功能,每一行和每一个TAB制表符缩进就是一个作用域你可以在任意的行使用- var 来声明一个变量。你在任意行除了可以从最顶级作用域--静态数据local里读取变量外还可以读取或修改本行上层和同级的作用域声明的变量,但是上级作用域不能寻找子作用域的变量。希望你通过下面的代码能理解pug的作用域概念

假设在编译这个模板获得的local对象是{a: 1, b: 2, c: 3}

body
    - let d = 4 // d是 处于body 作用域内的变量,和他同级或它的子级可以找到他
    p=x    // 不会报错,不输出任何内容
    div
        p=d // 可以从上层作用域找到d
        p=a // 可以从顶级作用域找到a
        p=e // undefined 不会输出任何内容 同JS原理,e 还没有被赋值
        - var e = 5 
        ul
            while e > 0 // 这里会迭代5次, 产生5个作用域
                - e--
                - const f = 10 + e // f是 while循环语句作用域的变量 上层作用域拿不到它
                li=f

强大的变量/作用域功能是pug模板的一大利器,它让你感觉同时在写JS 和 html。这对于需要输出计算结果的html十分有用。当你真正的掌握了变量和作用域概念,你在模板编译阶段甚至在编译过程中的迭代时就可以随意处理数据,实时计算数据并根据需求输出你想要的结果。比如你可以不用再单独写js文件就可以制作一份求平均值/汇总等需要计算的表格。

属性 Attribute

基础示例

  • 各个属性之间用空格/逗号/逗号+空格隔开,为了方便阅读代码建议使用逗号+空格隔开或者每个属性都换行
  • 类可以使用 .classname 语法来定义
  • ID 可以使用 #idname 语法来定义
编译前编译后
image.pngimage.png

布尔值属性

布尔值属性是经过映射的,当没有指定值的时候,默认是 true

编译前编译后
image.pngimage.png

样式属性

style(样式)属性可以是一个字符串(就像其他普通的属性一样)还可以是一个对象,这在部分样式是由 JavaScript 生成的情况下非常方便。:

编译前编译后
span(style={color: 'red', background: 'green'})<span style="color:red;background:green;"></span>

类属性

class(类)属性可以是

  • 一个字符串(就像其他普通的属性一样)
  • 包含多个类名的数组,这在类是由 JavaScript 生成的情况下非常方便。
  • 一个将类名映射为 truefalse 的对象,这在使用条件性的类的时候非常有用。 | 编译前 | 编译后 | | :---: | :---: | |image.png|image.png|

&attributes

&attributes 语法可以将一个对象转化为一个元素的属性列表

编译前编译后
image.pngimage.png

条件 Conditional

Pug 的条件判断的一般形式的括号是可选的,所以您可以省略掉开头的 -,效果是完全相同的。类似一个常规的 JavaScript 语法形式。Pug 同样也提供了它的反义版本 unless

编译前编译后
image.pngimage.png

分支条件 case

pug case 是 JavaScript 的 switch 指令的实现,不同之处在于,在 JavaScript 中,传递会在明确地使用 break 语句之前一直进行。而在 Pug 中则是,传递会在遇到非空的语法块前一直进行下去。您可以明确地加上一个原生的 break 语句来终止判断。请看下面示例

编译前编译后
image.pngp>您有 10 个朋友</p>

迭代

Pug 目前支持几种主要的迭代方式: each/for inwhilefor

each in/ for in 循环

编译前编译后
image.pngimage.png

while/ for 循环跟 js 的 语法一样

编译前编译后
image.pngimage.png

PUG的拓展概念

模板继承

Pug 支持使用 blockextends 关键字进行模板的继承。一个称之为“块”(block)的代码块,可以被子模板覆盖、替换。这个过程是递归的。

Pug 的块可以提供一份默认内容,当然这是可选的,见下述 block scriptsblock contentblock foot

pug的block概念和vue的slot的概念一样,都是基于另一份模板(组件)继续拓展。 Pug 允许替换(默认的)、prepend(向头部添加内容),或者 append(向尾部添加内容)这是vue slot 没有提供的

layout.pugpage-a.pug
image.pngimage.png

现在我们来扩展这个布局:只需要简单地创建一个新文件,并如下所示用一句 extends 来指出这个被继承的模板的路径。您现在可以定义若干个新的块来覆盖父模板里对应的“父块”。值得注意的是,因为这里的 foot 块没有被重定义,所以会依然输出“一些页脚的内容”。但是这里向block footer append/prepend 了内容, 则最终输出 prepend 的内容 + 默认内容 + append的内容

<!-- page-a.html -->
<html>
  <head>
    <title>我的站点 - </title>
    <script src="/jquery.js"></script>
    <script src="/pets.js"></script>
  </head>
  <body>
    <h1>title</h1>
    <p></p>
    <p></p>
    <p>我是prepend 添加进来的</p>
    <p>我是foot 的默认内容</p>
    <p>我是append 添加进来的</p>
  </body>
</html>

混入 Mixin

混入mixin是一种允许您在 Pug 中重复使用一整个代码块的方法。它们会被编译成函数形式,您可以传递一些参数包括...剩余参数,也可以把一整个代码块像内容一样传递进来

mixin的概念和我们把JS中经常复用的一些函数抽出去方便复用一样,而这里我们抽出去的是一段html函数。这在VUE的概念里 就是一个支持slot的函数组件,参数就是prop。调用它就会产出一个组件

编译前编译后
image.pngimage.png

包含 Include

包含(include)功能允许您把另外的文件内容插入进来。

编译前index.pug编译后的index.html
image.pngimage.png

你还可以把多个mixin 写到一个mixins.pug文件里(相当于JS的utils.js),在layou模板里包含它。接着所有继承于layou模板的pug文件都能用到mixin.pug里你抽出去的mixin函数。到这里有没有体会到PUG的强大?

PUG的应用

如果你的项目是传统的ssr项目,那么pug可能是目前最好的选择。而如果你的项目是基于webpack等构建工具下的Vue 或者React的SPA或者SSR项目,你可能会考虑有没有必要再给项目里加个loader或者plugin去多一层编译处理html模板。根据爱因斯坦相对论:存在既有意义。 社区既然把它带进了MVVM框架,肯定有它的独特价值:使用pug后代码量减少了,结构清晰了,开发效率提高了,代码复用度高了... 如果你还是不确定, 那么参考完下文再做决定。

pug 是如何应用在项目中

我们前面提到过,模板引擎的核心概念是开发者提供要编译的模板文件,和包含变量集合的对象(我们本文称之为local) 然后交给处理程序来编译输出原生的html的字符串或者html文件。那么在什么时机用什么样的代码交给pug处理是关键。由于在浏览器端运行时的JS体积较大导致性能并不高,不推荐在浏览器端编译pug代码。你现在可以把pug认为是html的预处理语言。就像stylus和css的关系一样

pug提供的API

下面这几个是最常用的API,大多编译功能和pug对应的loader和plugin内部都会调用这其中一个API,想了解更多请查阅官方API文档

  • pug.compile(source, ?options): () => string

  • pug.compileFile(path, ?options): () => string

  • pug.render(source, ?options, ?callback) : string

  • pug.renderFile(path, ?options, ?callback): string< html code>

  • source: string需要渲染的 Pug 代码

  • path: string需要渲染的 Pug 代码文件的位置

  • options: ?options存放选项的对象,同时也直接用作局部变量的对象

  • callback: ?function Node.js 风格的回调函数,用于接收渲染结果。注意:这个回调是同步执行的。 总结:compile/compileFile return 一个函数, 执行这个函数返回html源码的字符串。render/renderFile: 直接返回html源码的字符串

pug的应用方式

1. 在node端自己手动调用pug提供的API编译

以koa2服务端处理路由并返回html的场景为例,这个流程大致是koaRouter匹配路由后,请求后端服务器拉取数据,然后再拿着这些数据渲染模板,最后返回给客户端html字符串

const pug = require('pug');

const axios = require('axios')

...



koaRouter.get('/', async ctx => {
    const locals = await axios.get('/api/xxx')
    var renderhtml = pug.render('code of pug', locals); //=> '<code>of pug</code>'
    var renderFilehtml = pug.renderFile('path/to/file.pug', locals); //=> 'html code' 

    ....

     ctx.type = 'html; charset=utf-8';
     ctx.body=renderhtml || renderFilehtml
})

社区已有封装好的 koa-views 来替我们处理这些流程

然后koa服务端代码会改成类似于这样

const koa= require('koa');
const koaRouter = require('koa-router')
const app = new koa()

const views = require('koa-views');
app.use(views('路由模板pug文件夹路径',{extension: 'pug'}))

...

koaRouter.get('/', async ctx => {
    const locals = await axios.get('/api/xxx')
    // 把公共数据和接口的数据一起传给pug
    await ctx.render('home', Object.assign(locals, ctx.state))
})

app.use(koaRouter.routes())
app.use((ctx, next) => {
    ctx.state = { /*  每个页面都会用到公共静态数据  */ }
    next()
})

app.listen(4008)

2. 通过pug-cli来手动完成文件的编译

你需要安装pug-cli 来通过命令编译输出html。这种场景就像你不想写原生的css通过命令编译less/sass/stylus一样语法 $ pug [options] [dir|file ...]

$ npm i pug-cli -g

$ pug -h

-p, --path 用于解析包含的文件名
-o, --out <dir> 输出的html路径
-O --obj <str|path> Local变量对象  JSON 字符串或者JSON/JavaScript文件地址
-b, --basedir 用作根目录解析绝对包含的路径
-P, --pretty 编译格式化后的的 HTML 输出
-c, --client 输出可用于客户端运行时的编译函数.js文件
-n, --name 编译模板的名称(需要 --client)
-D, --no-debug 编译而不调试(较高的编译效率)
-w, --watch 观察文件的变化并自动重新编译
--doctype 在命令行中指定文档类型(如果模板未指定则很有用)

假设你当前目录有个template.pug 需要编译,下面这两个命令是最常用到,可以在同目录输出压缩后的template.html pugtemplate.pugO//在同目录输出压缩后的template.htmlpug template.pug -O '{}' // 在同目录输出压缩后的template.html pug template.pug -O '{}' -w // 观察文件的变化并自动重新编译在同目录输出压缩后的template.html

3. 在webpack介入的项目中

vue项目

上面我们提到在.vue文件中的template 指定lang=pug 就可以使用pug友好的玩耍了。此时我们已经不需要koa-views帮我们编译pug,我们这时候需要的是loader帮我们处理。假设之前我们按照上面koa的代码,把一些环境变量、等静态数据存在了ctx.state 中,现在方案已改,这些静态数据还可以放进loader的option里。pug-loader/pug-html-loader 都会去option里找data字段,如果存在将作为locals 参与编译

// 尤大pug-plain-loader/index.js 源码
const pug = require('pug')

const loaderUtils = require('loader-utils')

module.exports = function (source) {
  const options = Object.assign({
    filename: this.resourcePath,
    doctype: 'html',
    compileDebug: this.debug || false
  }, loaderUtils.getOptions(this))

  const template = pug.compile(source, options)
  template.dependencies.forEach(this.addDependency)
  // 这里会去拿options.data 作为编译的locals 数据
  return template(options.data || {})
}

下面是个性能优化的例子:生产环境引入CDN依赖,减少总体打包体积; 开发环境使用本地下载好的依赖 而不去等待CDN下载

你的webpack配置应该是这样

const pugLoaderOption = {
  loader: 'pug-html-loader', 
    options: {
     data: {
      aaa: '我是pug-loader定义变量',
      require, // 还可以传function, 此处把node 的require 方法传给了loader的option
      'NODE_ENV': process.env.NODE_ENV || 'development',
      imgPath: process.env.NODE_ENV === development
          ? '/dist/images/'
          : 'http://xxx.cdn.com/images/'
     }
}}



module.exports = {
/*
    @externals
    是告诉webpack 我import externals里某些key 的时候 
    不要去打包这些依赖 我要CDN引入这些依赖
    在运行的时候 去全局变量window 找key 对应的值
    如 代码里import vue 的时候 从 window.Vue 取值
    如 代码里import element-ui 的时候 从 window.ELEMENT 取值
    详细配置 https://webpack.docschina.org/configuration/externals/
    这个配置一般建议只生效在production环境,你有N种手段可以实现
*/ 

  externals: {
    vue: 'Vue',
    vuex: 'Vuex',
    'vue-router': 'VueRouter',
    'element-ui': 'ELEMENT'
  }

............
module: {
 rules: [{
     test: /.pug$/,
     oneOf: [{
         resourceQuery: /^?vue/,
         use: pugLoaderOption
     }, {
         use: [
             {
                 loader: 'html-loader',
                 options: {
                     attrs: ['img:src']
                 }
             },
             pugLoaderOption
         ]
     }
 ]}]}}

pugLoaderOption .option.data在这里就是locals, 需要你自己修改webpack配置添加进去。然后你的index.html 也该改成index.pug了。 有了loader 的 option存的变量,下面这个例子则可以轻松实现生产环境sccript 标签引入CDN依赖

<!DOCTYPE html>
html(lang="en")
  head
    meta(charset="UTF-8")
    meta(name="viewport", content="width=device-width, initial-scale=1.0")
 body
    - const lodash = require('lodash')
    p= lodash.camelCase('abcdEfg')
    .test=aaa // 我是pug-loader定义变量
    .test=NODE_ENV
    .test 66666666666666666
    img(src= imgPath + "aaa.png")
    #app
    if NODE_ENV === 'production'
      -
        var cdns = [{
          name: 'vue',
          vasion: '2.5.21'
        }, {
          name: 'vuex',
          vasion: '3.0.1'
        }, {
          name: 'vue-router',
          vasion: '3.0.2'
        }, {
          name: 'element-ui',
          vasion: '2.6.1'
        }];

      each val,i in cdns
        script(src=`https://cdn.bootcss.com/${val.name}/${val.vasion}/${
            i < 3 ? val.name + '.min' : 'index'
        }.js`)

你在任意意.vue文件的teplate里也可以使用这些变量。

<template lang="pug">
.wrapper
    .test=aaa // 我是pug-loader定义变量
    .test=NODE_ENV
    // 利用node增加拓展能力
    - const lodash = require('lodash')
    p= lodash.camelCase('abcdEfg')
    dl
        dt.act-title(@click="clickHandler") {{ text }}
            dd(v-for="item in arr") {{ item }}

</template>
<script>
export default {
    data() {
        return {
            text: '我是data中的变量',
            arr: [0,1,2,3,4],
            count: 1
        }
    },
    methods: {
        clickHandler() {
            this.count++
        }
    }
}
</script>

那么,既然在vue中使用pug, 除了使用pug的语法,pug-loader传的静态数据,在vue template 中,pug mixin还可以和vue实例中的变量结合做一些有用拓展。我们通常会把复用多次的一段template抽出去当做一个单独的组件。结合了pug,仅仅是一段很少的html抽出去一个组件可能不是最好的选择。

使用pug mixin 编译vue变量

利用 minxin 结合 vue 变量 制作vue模板组件或者做一些简单的类vue filters的功能。注意:在vue中 pug的模板是经过两轮编译

  • 第一轮是pug把模板编译成vue所需要解析的template代码,你想使用的vue的变量应该被编译成字符串。比如说你想利用pug mixin 结合vue data的变量抽出来一个可以多次复用的pug mixin。 pug mixin 定义的参数,在pug内部要当字符串使用。
  • 第二轮交给vue 编译由自身内部的变量 替换模板对应的变量。所以mixin内部的代码应以编译成vue template 为目的

请看下面这个例子

<template lang="pug">
mixin likeVueFilter(value)
    //- value 如果执行时传的vue.data.count 下面的代码则编译成 你兜里有{{ count * 2 }}块钱
    =`你兜里有{{${value} * 2}}块钱`

mixin likeVuecomponent(title, list)
    .wrapper
        dl
            dt.act-title(@click="clickHandler")=`{{ ${title} }}`
                dd(v-for=`item in ${list}`) {{ item }}
        block // 这个block 是个slot 且只能有一个
div.root
    +likeVuecomponent('text', 'arr')
        p
            +likeVueFilter('count')
            +likeVueFilter('money')
    <template v-for="a in 10">
        // 复用10次
        + likeVuecomponent(...)
    </template>

<script>

export default {
    data() {
        return {
            text: '我是data中的变量',
            arr: [0,1,2,3,4],
            count: 1,
            money: 3
        }
    },
    methods: {
        clickHandler() {
            this.count++
        }
    }
}

</script>

React 项目

如果你在react项目中也想把index.html 替换为pug,那么配置loader也是少不了的。这里只把

**babel-plugin-transform-react-pug**的用法复制在下面,github有详细介绍高级用法

1. install

npm install --save-dev babel-plugin-transform-react-pug

  1. 修改.babelrc,在transform-react-jsx 前面添加"transform-react-pug"
{  

  "plugins": [
    "transform-react-pug",
    "transform-react-jsx"
  ]
}