Pug -- 全新的 Vue 高效书写体验
🤧 前言
在年初 Vue3 刚成为 Vue 的默认版本时,在文档中看到了 Pug 的影子,虽然好奇,由于时间关系一直没能专心去尝试一下 Pug 在 Vue3 🀄️的体验。最近决定不能一拖再拖了,熬夜体验了一番,果然没让人失望。
🥳 Pug 是什么?
Pug 最初是用于服务端的模板引擎,随着一些用于Pug的 loader 出现,它也有了在前端发力的机会。 Pug 可以灵活地生成 HTML 代码,使用方式在一定程度上相似于 JSX ,而又比 JSX 更加强大。
🧐 为什么使用 Pug ?
Pug 的能力非常出众。在 Vue3 🀄️,无论是常规的 HTML 形式的 template ,还是 JSX ,在 Pug 面前都显得笨重且冗余。 Pug 支持行内 JS 语法。而且,JS 代码在这里分为 缓冲代码( Bufferd Code ) 、 无缓冲代码 ( Unbuffered Code ) 以及 不转义无缓冲代码 ( Unescaped Buffered Code ) 。
Bufferd CodeBufferd Code以=开头,后面接一个空格,再之后是JS代码。Bufferd Code会产生实际的HTML内容,常常用来生成文本内容。处于安全考虑,编译时会自动将其中的>、<等敏感字符进行转义。
p(style="background: blue")= '<em>cc ' + 'with' + 'yy !'
如上代码会被编译为:
<p><em>cc with yy !</p>
Unbuffered CodeUnbuffered Code以-开头,空一格,之后是JS代码。Unbuffered Code不会生成实际内容,只起辅助作用(变量、条件控制、循环等)。
- for (let i = 0; < 3; i++)
li 第#{ i }项
这里 for 循环的那一行,起辅助作用,而由 li 第#{ i }项 来生成实际内容。经过编译后生成如下内容:
<li>第1项</li>
<li>第2项</li>
<li>第3项</li>
Unescaped Buffered CodeUnescaped Buffered Code以!=开头,顾名思义,会产生不转义的实际内容,也就是说,可以用来得到HTML元素。
p!= '<em>cc</em>' + 'with' + 'yy'
如上代码将被编译为:
<p><em>cc</em>with yy</p>
在 Pug 模板中,语法极其灵活。代码量更少,更容易写出美观且规范的代码。这里就 Pug 语法进行简要介绍,由于大多数在 Vue 的 template 中用不上,就不详细展开了。有兴趣的同学可以查阅 Pug 官方文档。
- 以每行第一个非空词作为标签,不再需要写成对的标签,不再需要写标签的尖括号,使用缩进来表示标签或语法嵌套:
//- Pug
header
h1 标题
div.content 内容
对应的 HTML 如下:
<header>
<h1>标题</h1>
</header>
<div class="content">内容</div>
- 使用
"()"来包裹属性,对于特殊属性class和id可以分别跟在元素名之后,简写为.和#:
div(class="cc-test" id="cc-id" title="cc-title") cc
//- class 和 id 的简写
div.yy-test#yy-id(title="yy-title") yy
编译为 HTML :
<div class="cc-test" id="cc-id" title="cc-title">cc</div>
<div class="yy-test" id="yy-id" title="yy-title">yy</div>
Pug的原生语法支持使用JS定义变量,通过#{ value }的方式进行插值;
- const age = 18
div.age 年龄:#{age}
编译为:
<div class="age">年龄:18</div>
- 使用
if-else,case - when等分支结构可以进行条件渲染,类似于JS中的if-else和switch-case。注意case-when不是JS代码,因此不要使用-开头。
- const titleVisible = false
- if(titleVisible)
header 标题
- else
div 内容
- let type = 'string'
case type
when 'string'
div 字符串
when 'number'
div 数值
default
div 其他
编译为:
<div>内容</div>
<div>字符串</div>
- 使用
each - in来进行遍历,相当于JSX中使用map。注意,each-in不是JS代码,因此不要用-开头。
- const list = [1, 2, 3, 4, 5]
ul
each number in list
li #{number}号
编译为:
<ul>
<li>1号</li>
<li>2号</li>
<li>3号</li>
<li>4号</li>
<li>5号</li>
</ul>
到这里,基本已经 cover 住了 JSX 的功能,我们也已经体会到了 Pug 的简洁与灵活。但是 Pug 可不止于此,作为模板引擎,它还支持 block ,mixin 等能力,能更好的进行复用,这些在放在后续章节中介绍。
🥰在 Vue3 的 tempalte 中使用 Pug
尽管 Pug 有自己的语法,可以定义变量,可以有 if 和 case-when 条件分支和 each-in 遍历,有自己的插值方式 #{} , 但如果我们在 template 里使用 Pug 语言,则不建议使用这些语法,而应该结合 Vue 的指令(如 v-if , v-for )和 mushache 插值方式。
👀为什么?
主要有三点原因:
Pug声明的变量不具有响应性。 在Vue3里,响应式API有ref和reactive两大系列,通过这两个系列的API来定义具有响应性的变量。而在Pug中无法获取这两个系列的API--Pug中不支持JS模块导入,无法得到具有响应性的变量。Pug无法声明有效函数Pug设计来用作服务端模板引擎,自然是不需要考虑声明函数的。我也尝试着在Pug中进行函数的声明并绑定给元素的click事件,但是无论如何都无法生效。(也许是我的打开方式不对?)Pug自带的语法体系,无法与<script></script>里的数据互通。 也就是说,我们在<script>标签中定义的一些响应式数据,以及一些函数,都无法通过Pug自身的语法访问,因此仍然需要用Vue的指令以及插值方式,来使用在<script>标签中定义的内容。
🐳如何使用?
事实上,.vue 文件中的三剑客 <template> , <script> , 以及 <style> ,都支持设置 lang 属性, 当给 <template> 设置 lang="pug" ,就可以在 template 中使用 pug 了。
<template lang="pug">
section.content 主体内容
</template>
<script lang="ts" setup>
let age:number = 18
</script>
<style lang="scss">
.content {
height: 30px;
}
</style>
当然,如果发现无法对 pug 内容进行编译,那大概没有安装 pug 以及对应的 loader,可以按提示安装响应的 loader 和 pug,例如 pug-plain-loader :
npm i pug pug-plain-loader -D-S
现在,就能快乐地使用 Pug 了。🥰
🐯插值,属性和指令
在 Pug 中使用 Vue 的语法,和在常规的 template 中使用大同小异,看一些示例就能明白:
mushache插值
<template lang="pug">
.container#container
header.header
h6.title {{ title }}
.content {{ content }}
footer.footer
button.confirm 确定
button.cancel 取消
</template>
<script setup>
let title = 'Pug 初体验',
content = '第一次体验pug,真的太好用啦!'
</script>
这段代码中,使用 mushache 对 title 和 content 进行插值。
- 属性 每个元素的属性都应该写在其后的圆括号中,除了
id和class也可以采用上例中的简写形式。
<template lang="pug">
.container(id="container")
header(class="header")
h6(class="title" name="hello") {{ title }}
.content {{ content }}
footer#footer
button.confirm 确定
button.cancel 取消
</template>
<script setup>
let title = 'Pug 初体验',
content = '第一次体验pug,真的太好用啦!'
</script>
v-if条件分支指令 和常规的v-if等指令用法一致,需在同层级的节点才能继续使用v-else-if和v-else。指令也属于一种属性,因此也要写在圆括号()中。
<template lang="pug">
button.confirm(v-if="confirmBtnVisible") 确定
button.cancel(v-else) 取消
</template>
<script setup>
import { ref } from 'vue'
const confirmBtnVisible = ref(true)
</script>
v-for遍历 所有的指令都是属性,都写在圆括号()中。如下例,imgs为从getImgList接口获取到的图片数据列表,通过v-for对其进行遍历,生成一组图片元素。
<template lang="pug">
ul
li(v-for="(item, i) in imgs")
h6 {{ item.title }}
img(:src="item.src")
</template>
<script setup>
import { getImgList } from '@/api/index.js'
let imgs = await getImgList()
</script>
v-bind:attr和v-on:event和在常规的<template>中一样,v-bind:attr="value"可以简写为:attr="value",v-on:event="handler"可以简写为@event="handler"。
<template lang="pug">
ul(@click="handleImgClick")
li(v-for="(item, i) in imgs")
h6 {{ item.title }}
img(:src="item.src")
</template>
<script setup>
import { getImgList } from '@/api/index.js'
let imgs = await getImgList()
const handleImgClick = (event) => console.log(event)
</script>
在上面的例子中,对 img 标签使用了 v-bind (语法糖形式 :),绑定了每个 img 标签的 src 属性;对 ul 使用了 v-on (语法糖形式 @)对其绑定了点击事件的处理程序,利用了事件冒泡达到事件委托的目的,小小地优化了性能。
v-model双向绑定指令 下例🀄️,使用v-model对input输入框的值和变量username的值 (username.value) 进行了双向绑定。
<template lang="pug">
input(v-model="username" :placeholder="placeholder")
</template>
<script setup>
import { ref } from 'vue'
const username = ref(''),
placeholder = '用户名'
</script>
上文演示了在 Pug 使用较为常用的 Vue 指令,其它指令的使用也大都一致。 而在 Vue 的 template 🀄️,另一个尤为重要的东西便是插槽 slot 了。
🦁插槽 slot
Vue 的插槽分为默认插槽,具名插槽 和 作用域插槽 三种。这里不介绍有关插槽的知识,只演示在 Pug 中如何使用。
- 默认插槽 假定我们有一个卡片组件
OlCard。这里解释几个概念: 插槽内容 :父组件中插入子组件插槽的内容,叫做插槽内容; 插槽出口 :子组件中的slot标签,叫做插槽出口; 默认内容 :子组件slot标签内部的内容,即为 默认内容 ,当该父组件未在插槽插入内容时,则显示默认内容。
<template lang="pug">
.card-box
header.card-header
h6 卡片标题
//- 注释:slot标签 是插槽出口
slot
.card-content 卡片内容
footer.card-footer 卡片底部
</template>
在 Home 页面中展示这个卡片,并用一张图片和一句文字通过插槽替换了默认内容。
<template lang="pug">
.home
ol-card
img(:src="imgSrc")
span {{imgText}}
</template>
<script setup>
import OlCard from '@/components'
const imgSrc = 'https://assets.onlyy.vip/icons/dst/Willow.png'
const imgText = 'Willow'
</script>
- 具名插槽 具名插槽要在插入的内容外层加上一个
template标签,并将其v-slot指令(语法糖为#)设置为对应的具名插槽的name。 修改一下OlCard组件,让它具有header,content,footer等多个插槽,其中,content为默认插槽,header和footer为具名插槽。
<template lang="pug">
.card-box
slot(name="header")
header.card-header
h6 卡片标题
slot
.card-content 卡片内容
slot(name="footer")
footer.card-footer 卡片底部
</template>
具名插槽让组件更加灵活了,现在可以分别在不同位置的插槽插入需要的内容。如下,在 header 插槽中插入了标题( "具名插槽示例" )和关闭图标 x ; 默认插槽中依然是图片和文字;footer 插槽中插入了一个文字显示为"确 定"的按钮。
<template lang="pug">
.home
ol-card
template(v-slot:header)
span.title 具名插槽示例
span.close x
img(:src="imgSrc")
span {{imgText}}
template(#footer)
button 确 定
</template>
- 动态插槽名 当有多个具名插槽时,可以用一个变量来代替具体的插槽名,通过改变变量的值,来切换插槽出口。动态插槽名的写法同动态指令参数写法一致:
v-slot:[slotName]或简写为#[slotName],其中变量slotName是表示插槽名的字符串。
<template lang="pug">
.home
ol-card(@click="changeSlot")
template(#[slot])
.slot-content 内容
</template>
<script setup>
import { ref } from 'vue'
import OlCard from './components/OlCard.vue'
const slot = ref('header')
const changeSlot = () => {
slot.value = 'footer'
}
</script>
在上面的代码中,一开始插槽内容是放置在 header 插槽中。当点击卡片触发 changeSlot 函数时,将 slot.value 的值改变为 footer ,于是插槽内容随之被转移到 footer 插槽中。
- 作用域插槽 在一般情况下,插槽内容只能访问父组件的数据作用域,而无法访问到子组件的数据作用域。但某些场景下,又希望插槽内容能访问子组件的数据。这时候就要用到 作用域插槽 了。 作用域插槽 通过在子组件的 插槽出口 上传递
attributes,就如同对组件传递props一样:
<template lang="pug">
.ol-card__wrapper
h6.ol-card__title OL卡片
.ol-card__content-wrapper
slot(text="卡片主体" :author="author")
.ol-card__content-default 默认卡片内容
footer 卡片底部
</template>
<script setup>
const author = 'cc'
</script>
在上面的代码中,子组件中插槽出口上传递了 text 和 author 两个属性。在父组件中使用该子组件时,通过子组件标签上的 v-slot 指令,即可接收到子组件传递来的插槽 props 对象,该对象上包含了子组件传递来的各个属性。
<template lang="pug">
ol-card(v-slot="slotProps")
.content 这里是{{slotProps.text}}
.author 作者:{{slotProps.author}}
</template>
<script setup>
import OlCard from '@/components/OlCard.vue'
</script>
上面的栗例子中,父组件通过在子组件标签上使用 v-slot 指令接收了来自子组件 OlCard 的 props 对象,并命名为 slotProps (这里可以随意命名,之后使用的时候注意下就行)。在 soltProps 有着子组件传递的各个属性。 需要注意的是,这里的 slotProps 虽然写在子组件的标签上,但是它是默认插槽传递的 props 对象,只对默认插槽的插槽内容起作用。如果此时子组件还有具名插槽,那么在具名插槽的插槽内容之中,是无法使用这个 slotProps 的。 假设 子组件 还有个 name 为 footer 的具名插槽。则父组件在使用子组件时,无法在子组件的 footer 插槽中使用上述默认插槽的 slotProps ,尽管看起来 slotProps 是写在子组件的标签上。
<template lang="pug">
//- slotProps 写在 ol-card 标签上
ol-card(v-slot="slotProps")
.content 这里是{{slotProps.text}}
.author 作者:{{slotProps.author}}
//- 如下在 footer 插槽中使用默认插槽的 slotProps 是错误的!!
template(#footer)
.footer 卡片底部内容 {{slotProps.text}}
</template>
<script setup>
import OlCard from '@/components/OlCard.vue'
</script>
上面的代码中就是在具名插槽中错误地使用了默认插槽传递的 props 的示例。事实上,默认插槽也可以看作具名插槽,它的名字为 default 。 因此,在默认插槽和具名插槽都存才时,为了避免引起作用域混乱,最好采用如下写法,为默认插槽的插槽内容套上一层 template 标签,并以 v-slot="slotProps" 的指令形式将 slotProps 写在该 template 标签上,这样可以让它的作用域更加明确。当然,如果不喜欢命名为 slotProps ,也可以选择其它命名。 此外,v-slot 后面没有 :slotName ,则它对接的是默认插槽,也可以写为 v-slot:default ,或简写为 #default 。对于作用域插槽,父组件要接受 props 时,则可以在该 template 上写 v-slot="slotProps" 或 v-slot:default="slotProps" 或简写为 #default="slotProps" 。
<template lang="pug">
//- slotProps 写在 ol-card 标签上
ol-card
//- 写在template上,可以让 slotProps 作用域范围更加明确
template(#default="slotProps")
.content 这里是{{slotProps.text}}
.author 作者:{{slotProps.author}}
//- 如下在 footer 插槽中使用默认插槽的 slotProps 是错误的!!
template(#footer)
.footer 卡片底部内容
</template>
<script setup>
import OlCard from '@/components/OlCard.vue'
</script>
- 具名作用域插槽 既然默认插槽对应有作用域插槽,具名插槽自然也得有。 前面说到,将默认插槽看作名为
default的具名插槽,那么其它“正牌”的具名插槽的使用,与这个名为default的具名插槽如出一辙。 子组件:
<template lang="pug">
.ol-card__wrapper
h6.ol-card__title
slot(name="header" text="标题")
.ol-card_default-header OL卡片
.ol-card__content-wrapper
slot(text="卡片主体" :author="author")
.ol-card__content-default 默认卡片内容
footer
slot(name="footer" text="卡片底部")
.ol-card_default-footer 默认卡片底部
</template>
<script setup>
const author = 'cc'
</script>
在这个子组件中,有两个具名作用域插槽:header 和 footer ;一个默认的作用域插槽。它们都传递了各自的 props ,且各自的 props 上都有不一样的 text 属性。 在父组件中使用时,只需要在各自插槽内容外层的 template 标签上使用 v-slot="props" / v-slot:slotName="props" ( props 的命名可随自己喜好 )来接收子组件各个插槽传递来的 props ,即可在各自的插槽内容中使用。
<template lang="pug">
ol-card
template(#default="props")
.content 这里是 {{props.text}}
.author 作者:{{props.author}}
template(#header="props")
.title {{props.text}}
template(#footer="props")
.footer {{props.text}}
</template>
<script setup>
import OlCard from '@/components/OlCard.vue'
</script>
如果对具名作用域插槽的使用有疑问,可以移步Vue3官方文档。此处只结合 Pug 写法作简要介绍。
🐯 props 和 emit
props 和 emit 是父子组件传参最常用的方式。在 vue3 的 setup 特性中需要使用 defineProps 和 defineEmits 来声明组件接收的参数以及自定义事件。记得新版本的 Vue3 里好像不需要引入这两个 API ,可以直接使用。示例的版本中仍然需要先引入才能使用。
下例组件接收一个 cardData 参数,其中 const props = ... ,如果不需要在 <script> 标签中使用到 props 对象,就不用声明 props,直接使用 defieneProps({ ... }) 即可。
<template lang="pug">
.ol-card__wrapper
h6.ol-card__title(@click="emit('click-header', '别敲我头')") {{cardData.title}}
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
const props = defineProps({
cardData: {
type: Object,
default(){
return {}
}
}
})
const emit = defineEmits(['click-header'])
</script>
父组件直接在子组件标签上传递参数以及监听事件。
<template lang="pug">
ol-card(
v-for="(card, i) in cardList"
:key="card.id"
:card-data="card"
@click-header ="handleCardClick"
)
</template>
<script setup>
import OlCard from '@/components/OlCard.vue'
const cardList = [
{
id: 'card1',
title: '卡片1'
},
{
id: 'card2',
title: '卡片2'
},
{
id: 'card3',
title: '卡片3'
},
]
const handleCardClick = (e) => console.log(e) // 打印 别敲我头
</script>
🦁 递归组件
在子组件中嵌入组件本身,实现组件的递归,常用于树形结构的组件(例如分级菜单)。和递归函数一样,需要有递归出口。如下是一个没什么意义的递归组件 OlCard 的示例:OlCard 组件接收 cardTree 参数,在 class 为 ol-card__content-wrapper 的 div 元素中递归嵌入自身实例。
<template lang="pug">
.ol-card__wrapper
h6.ol-card__title {{cardTree.title}}
.ol-card__content-wrapper
ol-card(
v-if="cardTree.children && cardTree.children.length"
v-for="(subTree, i) in cardTree.children"
:key = "subTree.id"
:card-tree="subTree"
)
</template>
<script setup>
import { defineProps } from "vue";
const props = defineProps({
cardTree: {
type: Object,
default(){
return {}
}
}
})
</script>
在父组件中使用该递归组件,通过属性 cardTree 传入数据:
<template lang="pug">
ol-card(
v-for="(card, i) in data"
:key="card.id"
:card-tree="card"
)
</template>
<script setup>
import OlCard from '@/components/OlCard.vue'
const data = [
{
title: 'ROOT卡片',
id: '0',
children: [
{
title: '卡片1',
id: '1',
children: [
{
title: '卡片1-1',
id: '1-1',
children: []
}
]
},
{
title: '卡片2',
id: '2'
}
]
}
]
</script>
现在,学会了如何 Vue 中使用 Pug ,如果喜欢的话,不妨也推荐给身边的人,一起找点新鲜感。
转载自:https://juejin.cn/post/7176624421761712187