Vue中jsx不完全应用指南小结
前言:文章不介绍任务背景知识,没有原理说明,偏向于实践的总结和经验分享。
文章所有的代码是基于Vue CLI 3.x版本,不会涉及到一步步通过Webpack来配置JSX所需要的知识点。
在使用Vue开发项目时绝大多数情况下都是使用模板来写HTML,但是有些时候页面复杂又存在各种条件判断来显示/隐藏和拼凑页面内容,或者页面中很多部分存在部分DOM结构一样的时候就略显捉襟见肘,会写大量重复的代码,会出现单个.vue文件过长的情况,这个时候我们就需要更多的代码控制,这时候可以使用渲染函数。
渲染函数想必平时几乎没有人去写,因为写起来很痛苦(本人也没有写过)。更多的是在Vue中使用JSX语法。写法上和在React中差不多,但是功能上还是没有React中那么完善。
在写JSX的过程中不得考虑一个样式的问题,虽然可以直接在.vue文件中不写<tempate>部分,只写<script>和<style>部分,而不用担心样式作用域问题。但是更多的时候还是推荐直接使用.js的方式来写组件,这个时候就涉及到样式作用域的问题了。
在React的生态中,有很多CSS-IN-JS的解决方案,比如styled-jsx、emotion、styled-components等,目前最活跃和用户量最多的是styled-components,目前已经拥有良好的生态圈子。如果需要在样式中作一些像Sass/Less中的颜色计算,可以使用polished来实现,当然不止这么简单的功能。但是在Vue中可使用的方案就太少了,因为Vue使用模板来写HTML本身是开箱即用的样式scoped,在使用JSX写组件的时候就面临着样式问题,一种方案是在组件包裹<div>中取一个特殊的名字,然后样式都嵌套写在这个class下面,但是难免会遇到命名冲突的情况,而且每次还得变着花样取名称。此外,就是引入CSS-IN-JS在Vue对应的实现,但目前来看Styled-components官方提供了一个Vue版本的叫vue-styled-components和emotion的vue-emotion,但是用的人实在太少。像styled-components进行了重大更新和变化,但是Vue版本的还是最初的版本,而且有时候还出现样式不生效的情况。
接下来进入正题,从简单语法到经验分享(大牛请绕行)
基本用法
首先需要约定一下,使用JSX组件命名采用首字母大写的驼峰命名方式,样式可以少的可以直接基于vue-styled-components写在同一个文件中,复杂的建议放在单独的_Styles.js_文件中,当然也可以不采用CSS-IN-JS的方式,使用Less/Sass来写,然后在文件中import进来。
下面是一个通用的骨架:
import styled from 'vue-styled-components' const Container = styled.div` heigth: 100%; ` const Dashboard = { name: 'Dashboard', render() { return ( <Container>内容</Container> ) } } export default Dashboard
插值
在JSX中使用单个括号来绑定文本插值
<span>Message: {this.messsage}</span> <!-- 类似于v-html --> <div domPropsInnerHTML={this.dangerHtml}/> <!-- v-model --> <el-input v-model={this.vm.name} />
在jsx中不需要把v-model分成事件绑定和赋值二部分分开来写,因为有相应的babel插件来专门处理。
样式
在JSX中可以直接使用class="xx"来指定样式类,内联样式可以直接写成style="xxx"
<div class="btn btn-default" style="font-size: 12px;">Button</div> <!-- 动态指定 --> <div class={`btn btn-${this.isDefault ? 'default' : ''}`}></div> <div class={{'btn-default': this.isDefault, 'btn-primary': this.isPrimary}}></div> <div style={{color: 'red', fontSize: '14px'}}></div>
遍历
在JSX中没有v-for和v-if等指令的存在,这些全部需要采用Js的方式来实现
{/* 类似于v-if */} {this.withTitle && <Title />} {/* 类似于v-if 加 v-else */} {this.isSubTitle ? <SubTitle /> : <Title />} {/* 类似于v-for */} {this.options.map(option => { <div>{option.title}</div> })}
事件绑定
事件绑定需要在事件名称前端加上on前缀,原生事件添加nativeOn
<!-- 对应@click --> <el-buton onClick={this.handleClick}>Click me</el-buton> <!-- 对应@click.native --> <el-button nativeOnClick={this.handleClick}>Native click</el-button> <!-- 传递参数 --> <el-button onClick={e => this.handleClick(this.id)}>Click and pass data</el-button>
注意:如果需要给事件处理函数传参数,需要使用箭头函数来实现。如果不使用箭头函数那么接收的将会是事件的对象event属性。
高级部分
在Vue中基于jsx也可以把组件拆分成一个个小的函数式组件,但是有一个限制是必需有一个外层的包裹元素,不能直接写类似:
const Demo = () => ( <li>One</li> <li>Two</li> )
必需写成:
const Demo = () => ( <div> <li>One</li> <li>Two</li> </div> )
而在React中可以使用空标签<></>和<react.Fragment></react.Fragment>来实现包裹元素,这里的空标签其实只是react.Fragment的一个语法糖。同时在React 16中直接支持返回数组的形式:
const Demo = () => [ <li>One</li> <li>Two</li> ]
那么在Vue中就只能通过遍历来实现类似的功能,大体思路就是把数据先定义好数据然后直接一个map生成,当然如果说元素的标签是不同类型的那就需要额外添加标识来判断了。
{ data() { return { options: ['one', 'two'] } }, render() { const LiItem = () => this.options.map(option => <li>{option}</li>) return ( <div> <ul> <LiItem /> </ul> </div> ) } }
事件修饰符
在基础部分简单介绍了事件的绑定用法,这里主要是补充一下事件修饰符的写法。
在模板语法中Vue提供了很多事件修饰符来快速处理事件的冒泡、捕获、事件触发频率、按键识别等。可以直接查看官方文档的事件&按键修饰符部分,这里把相关内容原样搬运过来:
修饰符 | 前缀 |
---|---|
.passive | & |
.capture | ! |
.once | ~ |
.capture.once或.once.capture | ~! |
使用方式如下:
<el-button {...{ '!click': this.doThisInCapturingMode, '!keyup': this.doThisOnce, '~!mouseover': this.doThisOnceInCapturingMode }}>Click Me!</el-button>
下面给出的事件修饰符是需要在事件处理函数中写出对应的等价操作
修饰符 | 处理函数中的等价操作 |
---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
.self | if (event.target !== event.currentTarget) return |
按键: .enter, .13 | if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码) |
修饰键: .ctrl, .alt, .shift, .meta | if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey、shiftKey 或者 metaKey) |
下面是在事件处理函数中使用修饰符的例子:
methods: { keyup(e) { // 对应`.self` if (e.target !== e.currentTarget) return // 对应 `.enter`, `.13` if (!e.shiftKey || e.keyCode !== 13) return // 对应 `.stop` e.stopPropagation() // 对应 `.prevent` e.preventDefault() // ... } }
ref和refInFor
在Vue中ref被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件。
注意:
- 因为 ref 本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在
- $refs不是响应式的,因此你不应该试图用它在模板中做数据绑定。
当 v-for 用于元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的数组。
假如在jsx中想要引用遍历元素或组件的时候,例如:
const LiArray = () => this.options.map(option => ( <li ref="li" key={option}>{option}</li> ))
会发现从this.$refs.li中获取的并不是期望的数组值,这个时候就需要使用refInFor属性,并置为true来达到在模板中v-for中使用ref的效果:
const LiArray = () => this.options.map(option => ( <li ref="li" refInFor={true} key={option}>{option}</li> ))
插槽(v-slot)
在jsx中可以使用this.$slots来访问静态插槽的内容。
注意:在Vue 2.6.x版本后废弃了slot和slot-scope,在模板中统一使用新的统一语法v-slot指令。v-slot只能用于Vue组件和template标签。
<div class="page-header__title"> {this.$slots.title ? this.$slots.title : this.title} </div>
等价于模板的
<div class="page-header__title"> <slot name="title">{{ title }}</slot> </div>
在Vue官方文档中提到:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。因此像下面的示例是无法正常工作的
<current-user> {{ user.firstName }} </current-user>
在<current-user>组件中可以访问到user属性,但是提供的内容却是在父组件渲染的。如果想要达到期望的效果,这个时候就需要使用作用域插槽了。下面是改写后的代码,更多知识点可以直接查看官方文档的作用域插槽。
<!-- current-user组件定义部分 --> <span> <slot v-bind:user="user"> {{ user.lastName }} </slot> </span> <!-- current-user 使用 --> <current-user> <template v-slot:default="slotProps"> {{ slotProps.user.firstName }} </template> </current-user>
上面的示例其实就是官方的示例,这里需要说明的是,其实在Vue中所谓的作用域插槽功能类似于React中的Render Props的概念,只不过在React中我们更多时候不仅提供了属性,还提供了操作方法。但是在Vue中更多的是提供数据供父作用域渲染展示,当然我们也可以把方法提供出去,例如:
<template> <div> <slot v-bind:injectedProps="slotProps"> {{ user.lastName }} </slot> </div> </template> <script> export default { data() { return { user: { firstName: 'snow', lastName: 'wolf' } } }, computed: { slotProps() { return { user: this.user, logFullName: this.logFullName } } }, methods: { logFullName() { console.log(`${this.firstName} ${this.lastName}`) } } } </script>
在父组件中使用:
<current-user> <template v-slot:default="{ injectedProps }"> <div>{{ injectedProps.user.firstName }}</div> <el-button @click="injectedProps.logFullName">Log Full Name</el-button> </template> </current-user>
在上面的代码中我们实际上使用解构的方式来取得injectedProps,基于解构的特性还可以重命名属性名,在prop为undefined的时候指定初始值。
<current-user v-slot="{ user = { firstName: 'Guest' } }"> {{ user.firstName }} </current-user>
如果组件只有一个默认的插槽还可以使用缩写语法,将v-slot:default="slotProps"写成v-slot="slotProps",命名插槽写成v-slot:user="slotProps",如果想要动态插槽名还可以写成v-slot:[dynamicSlotName],此外具名插槽同样也有缩写语法,例如 v-slot:header可以被重写为#header
上面介绍了很多插槽相关的知识点足已说明其在开发过程中的重要性。说了很多在模板中如何定义和使用作用域插槽,现在进入正题如何在jsx中同样使用呢?
// current-user components { data() { return { user: { firstName: 'snow', lastName: 'wolf' } } }, computed: { slotProps() { return { user: this.user, logFullName: this.logFullName } } }, methods: { logFullName() { console.log(`${this.firstName} ${this.lastName}`) } }, render() { return ( <div> {this.$scopedSlots.subTitle({ injectedProps: this.slotProps })} </div> ) } }
然后在父组件中以jsx使用:
<current-user {...{ scopedSlots: { subTitle: ({ injectedProps }) => ( <div> <h3>injectedProps.user</h3> <el-button onClick={injectedProps.logFullName}>Log Full Name</el-button> </div> ) } }}></current-user>
指令
这里需要注意的是在jsx中所有Vue内置的指令除了v-show以外都不支持,需要使用一些等价方式来实现,比如v-if使用三目运算表达式、v-for使用array.map()等。
对于自定义的指令可以使用v-name={value}的语法来写,需要注意的是指令的参数、修饰符此种方式并不支持。以官方文档指令部分给出的示例v-focus使用为例,介绍二种解决办法:
1 直接使用对象传递所有指令属性
<input type="text" v-focus={{ value: true }} />
2 使用原始的vnode指令数据格式
{ directives:{ focus: { inserted: function(el) { el.focus() } } }, render() { const directives = [ { name: 'focus', value: true } ] return ( <div> <input type="text" {...{ directives }} /> </div> ) } }
过滤器
过滤器其实在开发过程中用得倒是不多,因为更多时候可以通过计算属性来对数据做一些转换和筛选。这里只是简单提及一下并没有什么可以深究的知识点。
在模板中的用法如下:
<!-- 在双花括号中 --> {{ message | capitalize }} <!-- 在 `v-bind` 中 --> <div v-bind:id="rawId | formatId"></div>
在jsx中使用方法为:
<div>{this.$options.filters('formatDate')('2019-07-01')}</div>
注意:由于Vue全局的过滤器只用于模板中,如果需要用于组件的方法中,可以把过滤器方法单独抽离出一个公共Js文件,然后引入组件中,然后用于方法中。
一些简单经验分享
并不是说我们在开发Vue项目的时候一定要使用jsx的方式来写,但是多掌握一种方式来灵活变通,提高工作效率,扩展思路何尝不值得一试。而且,在有些场景下释放js的完全编程能力会让你更加能够得心应手。其实在使用模板方式的时候我们并没有完全采用组件的思维方式来做,或者说是做得不彻底,不纯粹,拆分的粒度不够。更多 的时候并没有考虑到组件怎么切分和抽象,多人协作的时候如何处理依赖并明确自己的功能点。
关于DOM属性、HTML属性和组件属性
在React中所有数据均挂载在props下,Vue则不然,仅属性就有三种:组件属性props,普通html属性attrs和DOM属性domProps。在Angular的文档中关于插值绑定部分是重点说明了DOM属性和HTML属性的区别,在大多数情况下两者都有对应的同名属性,也就是1:1映射关系,但是也有例外的情况,比如HTML中colspan,DOM中的textContent。HTML属性的值指定了初始值,并且不能改变,而DOM属性的值表示当前值,是可以改变的。
然后在Vue的模板语法中是不区分DOM属性和HTML属性的,例如:
<template> <div> <div>输入的值:{{ title }}</div> <input type="text" value="我是DOM属性值" v-model="title" @input="logTitle" /> </div> </template> <script> export default { data() { return { title: '' } }, methods: { logTitle(e) { // 输出DOM属性 console.log(e.target.value) // 输出HTML属性 console.log(e.target.getAttribue('value')) } } } </script>
运行示例可以看到input的初始值被设置为了“我是DOM属性值",当我们在输入框中添加或者删除文字时,HTML属性始终没有变化,而绑定的DOM值一值在变动。然后再看一下在jsx中的实现:
<div>输入值:{ this.title }</div> <input type="text" value="我是DOM属性" v-model={this.title} onInput={this.logTitle} />
同样运行后会发现在jsx写法中并没有直接将HTML属性初始化为DOM属性值,即输入框中当前值为空字符串,这符合预期的行为。
此外在模板语法中是无法区分HTML属性和DOM属性命名一样的场景,但是在jsx中可以很好的区分:
<Demo title="我是组件属性" domPropsTitle="我是DOM属性" />
结果会就是在HMTL中显示title="我是DOM属性,而"我是组件属性”传递给了组件。
在React中CSS的样式写义在jsx中的语法是以className="xx"的形式,而在Vue的jsx中可以直接写成class="xx"。实际上由于class是Js的保留字,因此在DOM中其属性名为className而在HTML属性中为class,我们可以在Vue中这样写,经过Babel转译后得到正确的样式类名:
<div domPropsClassName="mt__xs"></div>
注意:如果同时写了class="xx" domPropsClassName="yy"那么后者的优先级较高,和位置无关。所以尽量还是采用class的写法。
有使用过Bootstrap经验的可能会注意到它里面包含了很多ARIA属性,这些属性并不属于DOM,在jsx中可以通过attrsXX或者直接aria-xx的方式来添加:
<label aria-label="title"></label> <label attrsAria-label="title"></label>
但是上面的换成domPropsAria-label就没有任何效果。
注意:在jsx中所有DOM属性(Property)语法为domPropsXx, HTML特性(Attribute)语法为attrsXx。更多的时候建议还是少使用,或者说合理使用。
在jsx中还可以使用混用的写法,例如在组件中写了<Demo title="demo" />,还可以定义一个属性对象,然后使用{...props}的方式写在一起,这个时候就会出现属性合并的问题,同样的事件多个地方声明事件处理函数,都会触发。
最后需要提及一点的是,在Vue中当给一个组件传了很多props,但是有的并不是组件声明的,也有可能是一些通用的HTML或者DOM属性,但是在最终编译后的HTML中会直接显示这些props,如果不希望这些属性显示在最终的HTML中,可以在组件中设inheritAttrs: false。虽然不显示了,但是我们依然可以通过vm.$attrs获取所有(除class和style)绑定的属性,包括不在props中定义的。
关于事件
前面已经把事件相关的知识点都介绍了,这里主要是提及一下关于jsx事件绑定语法onXx和组件属性(主要是函数prop)以on开头的情况如何处理。
虽然在写组件的时候可以避开命名以on开头,但是在使用第三库的时候,如果遇到了该如何处理呢?比如Element组件Upload很多钩子都是以on开头。 下面提供两种解决办法:
1.使用展开
<el-upload {...{ props: { onPreview: this.handlePreview } }} />
使用propsXx
<el-upload propsOnPreview={this.handlePreview} />
推荐使用第二种方式,写起来要简单些。
复杂逻辑条件判断
在模板语法中可以使用v-if、v-else-if和v-else来做条件判断。在jsx中可以通过?:三元运算符(Ternary operator)运算符来做if-else判断:
const Demo = () => isTrue ? <p>True!</p> : null
然后可以利用&&运算符的特性简写为:
const Demo = () => isTrue && <p>True!</p>
对于复杂的条件判断,例如:
const Demo = () => ( <div> {flag && flag2 && !flag3 ? flag4 ? <p>Blash</p> : flag5 ? <p>Meh</p> : <p>hErp</p> : <p>Derp</p> } </div> )
可以采用两种方式来降低判断识别的复杂度
- 最好的办法:将判断逻辑转移到子组件
- 可选的hacky方法:使用IIFE(立即执行表达式)
- 使用第三方库解决:jsx-control-statements
下面是使用IIFE通过内部使用if-else返回值来优化上述问题:
const Demo = () => ( <div> { (() => { if (flag && flag2 &&!flag3) { if (flag4) { return <p>Blah</p> } else if (flag5) { return <p>Meh</p> } else { return <p>Herp</p> } } else { return <p>Derp</p> } })() } </div> )
还可以使用do表达式,但是需要插件@babel/plugin-proposal-do-expressions的转译来支持,
const Demo = () => ( <div> { do { if (flag1 && flag2 && !flag3) { if (flag4) { <p>Blah</p> } else if (flag5) { <p>Meh</p> } else { <p>Herp</p> } } else { <p>Derp</p> } } } </div> )
再就是一种比较简单的可选办法,如下:
const Demo = () => { const basicCondition = flag && flag1 && !flag3; if (!basicCondition) return <p>Derp</p> if (flag4) return <p>Blah</p> if (flag5) return <p>Meh</p> return <p>Herp</p> }
最后一种使用jsx插件的就不详述和举例了,有兴趣的可以直接查看文档。
组件的传值
在单个jsx文件中可以写很多函数式组件来切分更小的粒度,例如之前的文章Vue后台管理系统开发日常总结__组件PageHeader,组件的形态有两种,一种是普通标题,另一种是带有选项卡的标题,那么在写的时候就可以这样写:
render() { // partial html const TabHeader = ( <div class="page-header page-header--tab"></div> ) // function partial const Header = () => ( <div class="page-header"></div> ) <div class="page-header"> {this.withTab ? TabHeader : <Header/>} </div> }
注意在拆分的时候,如果不需要做任何判断可以纯粹是HTML片段赋值给变量,如果需要条件判断就使用函数式组件的方式来写。需要注意的是由于render函数会多次被调用,写的时候注意一下对性能的影响,目前能力有限这方面就不作展开了。
既然使用函数式组件,那么同样可以在函数中传递参数了,参数是一个对象中,包含了以下属性
children # VNode数组,类似于React的children data # 绑定的属性 attrs # Attribute domProps # DOM property on # 事件 injections # 注入的对象 listeners: # 绑定的事件类型 click # 点击事件 ... parent # 父组件 props # 属性 scopedSlots # 对象,作用域插槽,使用中发现作用域插槽也挂在这个下面 slots # 函数,插槽
虽然可以在函数式组件中传参数、事件、slot但是个人觉得不建议这样做,反而搞复杂了。
render() { const Demo = props => { return ( <div> <h3>Jsx中的内部组件 { props.data.title }</h3> { props.children } <br /> { props.scopedSlots.bar() } </div> ) } return ( <div> <Demo title="test" attrsA="a" domPropsB="b" onClick={this.demo}> <h3>我是Children</h3> <template slot="bar"> <p>我是Slot内容</p> </template> </Demo> </div> ) }
上面的示例最终生成的HTML中会将<template>的内容转换为#document-fragment。
总结
接触Vue时间比较早,但是真正的Vue项目开发经验一年不到,平时比较懒,不怎么去深入学习和研究,所以文章在叙述上没有什么条理性,有些知识点可能并没表达清楚,很多东西还是得多实践去检验。如果有问题欢迎留言共同探讨。
其实早一点实践jsx的写法,对于后面的Vue 3.0出现后可以更快的融入其中,就像React对函数式组件中新增了钩子(Hooks)函数,以后Vue也是主推函数式组件,以后模板语法方式的占比会稍有下降。
文章并没有包含所有Vue中jsx写法的全部知识点,→_→所以叫不完全指南^_^"
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。