Vue中jsx不彻底应用指南

前言:文章不介绍任务背景知识,没有原理说明,偏向于实践的总结和经验分享。

文章全部的代码是基于Vue CLI 3.x版本,不会涉及到一步步经过Webpack来配置JSX所须要的知识点。html

在使用Vue开发项目时绝大多数状况下都是使用模板来写HTML,可是有些时候页面复杂又存在各类条件判断来显示/隐藏和拼凑页面内容,或者页面中不少部分存在部分DOM结构同样的时候就略显捉襟见肘,会写大量重复的代码,会出现单个.vue文件过长的状况,这个时候咱们就须要更多的代码控制,这时候可使用渲染函数前端

渲染函数想必平时几乎没有人去写,由于写起来很痛苦(本人也没有写过)。更多的是在Vue中使用JSX语法。写法上和在React中差很少,可是功能上仍是没有React中那么完善。vue

在写JSX的过程当中不得考虑一个样式的问题,虽然能够直接在.vue文件中不写<tempate>部分,只写<script><style>部分,而不用担忧样式做用域问题。可是更多的时候仍是推荐直接使用.js的方式来写组件,这个时候就涉及到样式做用域的问题了。node

在React的生态中,有不少CSS-IN-JS的解决方案,好比styled-jsxemotionstyled-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版本的仍是最初的版本,并且有时候还出现样式不生效的状况。react

接下来进入正题,从简单语法到经验分享(大牛请绕行)git

基本用法

首先须要约定一下,使用JSX组件命名采用首字母大写的驼峰命名方式,样式能够少的能够直接基于vue-styled-components写在同一个文件中,复杂的建议放在单独的_Styles.js_文件中,固然也能够不采用CSS-IN-JS的方式,使用Less/Sass来写,而后在文件中import进来。github

下面是一个通用的骨架:express

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插件来专门处理。segmentfault

样式

在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-forv-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 分别修改成 altKeyshiftKey 或者 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版本后废弃了 slotslot-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,基于解构的特性还能够重命名属性名,在propundefined的时候指定初始值。

<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获取全部(除classstyle)绑定的属性,包括不在props中定义的。

关于事件

前面已经把事件相关的知识点都介绍了,这里主要是说起一下关于jsx事件绑定语法onXx和组件属性(主要是函数prop)以on开头的状况如何处理。

虽然在写组件的时候能够避开命名以on开头,可是在使用第三库的时候,若是遇到了该如何处理呢?好比Element组件Upload不少钩子都是以on开头。 下面提供两种解决办法:

1.使用展开

<el-upload {...{
  props: {
    onPreview: this.handlePreview
  }
}} />
  1. 使用propsXx
<el-upload propsOnPreview={this.handlePreview} />

推荐使用第二种方式,写起来要简单些。

复杂逻辑条件判断

在模板语法中可使用v-ifv-else-ifv-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写法的所有知识点,→_→因此叫不彻底指南^_^"

最后,感谢各位的支持!!!…(⊙_⊙;)… ○圭~○

相关文章
相关标签/搜索