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 处理。
-
对于 template 编译, 正如官方文档所介绍的, Vue template 编译器存在于
vue-loader
,vueify
和Vue.js完整包
里。其中,我们最常用的还是 vue-loader,vue 源码被 wepack vue-loader 处理之后,template 已经被变成了 render 函数, 所以运行时就不需要编译器了。这也就是为什么 Vue 项目 package.json 的 main 字段指向的是不带编译器的运行时版本。 -
对于 jsx 的编译,尤大给我们提供了一列的 babel plugin
如果你不了解 vue 的渲染函数,请先仔细阅读和理解,官方文档: https://cn.vuejs.org/v2/guide/render-function.html#JSX
Vue jsx 的 babel plugin
- babel-plugin-syntax-jsx: 提供对 jsx 语法的基本支持,这个并非由 vue 提供和维护,react 也依赖于这个插件
- babel-helper-vue-jsx-merge-props: 提供了 vue 属性合并的工具函数,用于支持 jsx 里的 spread 语法, 即
...
语法 - babel-plugin-transform-vue-jsx: vue template 编译成 render 函数的核心插件
我们来看一个例子:
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 时,情况比较多:
- 使用
on-[eventName]
格式, 比如on-click-two
,on-click
,on-camelCaseEvent
- 使用
on[eventName]
格式,比如onClick
,onCamelCaseEvent
。click-two
需要这样写onClick-on
,onClickTwo
是不对的。 - 使用 spread 语法,即
{...{on: {event: handlerFunction}}}
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 修饰符
<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