Vue 起源于 Angular 1, 并且继承和发扬了 Angular 1 template 系统,然而模板系统天生有着自己的缺陷,比如官网的这个例子:

<script type="text/x-template" id="anchored-heading-template">
  <h1 v-if="level === 1">
    <slot></slot>
  </h1>
  <h2 v-else-if="level === 2">
    <slot></slot>
  </h2>
  <h3 v-else-if="level === 3">
    <slot></slot>
  </h3>
  <h4 v-else-if="level === 4">
    <slot></slot>
  </h4>
  <h5 v-else-if="level === 5">
    <slot></slot>
  </h5>
  <h6 v-else-if="level === 6">
    <slot></slot>
  </h6>
</script>

写起来真是又臭又长。还好尤大给提供了更高级的方案, render 函数 和 jsx. 其中 render 函数的方案在文档中还有一些介绍, jsx 说的就很少了,本文就主要介绍一下,vue jsx 的一些实战经验。

template,jsx 和 render 函数的关系

Vue 给我们提供了 3 种方式来写页面的 html。其中,render 函数是本质,Vue 渲染虚拟 Dom 都是通过调用 render 函数来完成的。render 函数是可以直接手写的,但是写起来过于繁琐,所以 Vue 给我们提供了 template 和 jsx 的书写方式。无论是 template 还是 jsx,都最终被编译(转换)成 render 函数,然后交付给 Vue 处理。

如果你不了解 vue 的渲染函数,请先仔细阅读和理解,官方文档: https://cn.vuejs.org/v2/guide/render-function.html#JSX

Vue jsx 的 babel plugin

我们来看一个例子:

const Babel = require('babel-core')
const vueJsx = require('babel-plugin-transform-vue-jsx')

let code = `
function render() {
  return (
    <div>
      <div leon={a} onClick={this.click()}
        {...{
          props: {
            prop1: 1,
            prop2: 2
          },
          on: {
            event: this.handleEvent()
          }
        }}
      >
      </div>
    </div>
  )
}`

let result = Babel.transform(code, {
  plugins: [
    vueJsx,
  ]
})

console.log(result.code)

输出:

import _mergeJSXProps from "babel-helper-vue-jsx-merge-props";

function render() {
  return h("div", [h("div", _mergeJSXProps([{
    attrs: { leon: a },
    on: {
      "click": this.click()
    }
  }, {
    props,
    on: {
      event: this.handleEvent()
    }
  }]))]);
}

我们可以清晰地看到:经过 babel-plugin-transform-vue-jsx 的转换,jsx 写的渲染函数,被转成了 Vue 所能处理的纯 js 的 render 函数了。

refer the gist and try you self.

一些常用语法的 jsx 怎么写?

本文的示例代码项目: https://github.com/njleonzhang/vue-jsx-sample

v-if

  <template>
    <div class='wrapper'>
      <div class='content' v-if='hello'>hello</div>
    </div>
  </template>

jsx 中使用 && 来代替:

  render() {
    return (
      <div class='wrapper'>
        {
          this.hello && (
            <div class='content'>hello</div>
          )
        }
      </div>
    )
  }

v-if and v-else

  <template>
    <div class='wrapper'>
      <div class='content1' v-if='hello'> content1 </div>
      <div class='content2' v-else> content2 </div>
    </div>
  </template>

jsx 中使用 三元表达式 来代替:

  render() {
    return (
      <div class='wrapper'>
        {
          this.hello ? (
           <div class='content1'>content1</div>
          )
          : (
            <div class='content2'> content2 </div>
          )
        }
      </div>
    )
  }

v-for

  <template>
    <div class='wrapper'>
      <ul>
        <li v-for='item in items' :key='item.id'></li>
      </ul>
    </div>
  </template>

jsx 中使用 Array.prototype.map 来代替:

  render() {
    return (
      <div class='wrapper'>
        <ul>
          {
            this.items.map(item => (
              <li>{ item.name }</li>
            ))
          }
        </ul>
      </div>
    )
  }

slot

  // Wrapper.vue
  <template>
    <div class='wrapper'>
      <span>I am a component</span>
      <slot></slot>
      <slot name='namedSlot'></slot>
    </div>
  </template>

  // main.vue
  <template>
    <wrapper>
      <div>
        I am the slot
      </div>

      <div slot='namedSlot'>I am the named slot</div>
    </wrapper>
  </template>

slot 是挂在 this.$slots 的这个属性上的,this.$slot['property'] 可以直接拿到 slot 的 vNode.

所以,jsx 中,通过访问 this.$slots 来代替 slot 的定义:

  // Wrapper.vue
  render() {
    return (
      <div class='wrapper'>
        <span>I am a component</span>
        {
          this.$slots.default
        }

        {
          this.$slots.namedSlot
        }
      </div>
    )
  }

  // main.vue
  render() {
    return (
      <wrapper>
        <div>
          I am the slot
        </div>

        <div slot='namedSlot'>I am the named slot</div>
      </wrapper>
    )
  }

scoped slots

  // Wrapper.vue
  <template>
    <div class='wrapper'>
      <span>I am a component</span>
      <slot :data='data'></slot>
    </div>
  </template>

  // main.vue
  <template>
    <wrapper>
      <div slot-scope='{ data }'>
        
      </div>
    </wrapper>
  </template>

scopedSlot 是挂在 this.$scopedSlots 的这个属性上的,this.$scopedSlots['property'] 可以直接拿到一个函数,这个函数的参数就是 scopeSlots 外传的数据,返回值是 VNode.

所以,jsx 中,通过访问 this.$scopedSlots 来代替 slot 的定义,通过传递 scopedSlots 属性来使用 scopedSlots.

  // Wrapper.vue
  render() {
    return (
      <div class='wrapper'>
        {
          this.$scopedSlots.default({
            data: this.data
          })
        }
      </div>
    )
  }

  // main.vue
  render() {
    return (
      <wrapper {...{
        scopedSlots: {
          default: ({ data }) => {
            return (
              <div>{ data }</div>
            )
          }
        }
      }}>
      </wrapper>
    )
  }

事件

event-emitter 这个组件会 emit 4个事件 click, click-two, test-event, test-event-two, camelCaseEvent. 使用 template 时, 我们用 v-on,或者其缩写 @, 来监听事件.

<template>
  <event-emitter
    @click='handleClick'
    @click-two='handleClickTwo'
    @camelCaseEvent='handleCamelCaseEvent'
    @test-event='handleTestEvent'
    @test-event-two='handleTestEventTwo'>
  </event-emitter>
</template>

使用 jsx 时,情况比较多:

render () {
  return (
    <event-emitter
      onClick={ this.handleClick }
      on-click-two={ this.handleClickTwo }
      on-camelCaseEvent={ this.handleCamelCaseEvent }
      {...{
        on: {
          'test-event': this.handleTestEvent,
          'test-event-two': this.handleTestEventTwo
        }
      }}>
    </event-emitter>
  )
}

这里的代码只是为了展示,这样的场景,全部都写在 spread 语法里,最为简洁。 我建议,如果事件多就使用 spread 语法,如果少就使用 on-[eventName] 的格式来写。on[eventName] 格式很奇怪容易搞错,最好不要用。

v-model

v-model 实际上就是一个语法糖。

<Component v-model='test'></Component>

等价于

<component :value='test' @input='test = arguments[0]'></component>

所以,如果使用 jsx 写法,

<component
  value={ this.test }
  onInput={ val => { this.test = val } }
>
</component>

这样写就回归原始了啊,好在 nickmessing 给我们提供了一个 plugin, babel-plugin-jsx-v-model,用于在 vue jsx 里支持 v-model 语法。使用这个 plugin 后,我们就可以这样写了:

<component
  v-model={ this.test }
>
</component>

sync 修饰符

v-model 相似,sync 修饰符

<component :data.sync='test'></component>

等价于

<component :data='test' @update:data='test = arguments[0]'></component>

所以,如果使用 jsx 写法,

  <component
    data={ this.test }
    {
    ...{
      on: {
        'update:data': val => { this.test = val }
      }
    }
    }
  >
  </component>

这样写 sync 修饰符的属性当然也是繁琐的,所以参照 nickmessing 的 plugin,我写了一个plugin, babel-plugin-vue-jsx-sync, 来处理 sync 修饰符.

使用了这个插件后,我们就可以这样写了:

  <component
    data$sync={ this.test }
  >
  </component>

更多的事件语法糖

为了处理 .ctrl, .alt, .shift 等事件语法糖,nickmessing 还提供了 babel-plugin-jsx-event-modifiers 插件,如果需要的话,可以查看其文档。

总结

通过 jsx 语法来开发 vue 项目,还是挺小众的,但是在一些场景下(复杂的 template 逻辑,组件封装),可以使得代码简洁很多。Vue 官方对 jsx 写法的介绍实在是太少了,本文总结了一些自己实践的经验,供大家参考。

本文的示例代码项目: https://github.com/njleonzhang/vue-jsx-sample