Vue JSX插件依赖及语法实践
# # Vue JSX插件依赖及语法实践
文章内容主要分两块。
第一块是了解jsx运行环境,因为jsx只是语法糖,最终都是需要babel来转译语法,所以需要配置相关babel插件。vue-cli3脚手架工具生成的应用工程默认支持jsx/tsx,省去了自己配置的繁琐,但了解相关babel插件对理解和书写jsx非常有帮助。
第二块是实践jsx在vue中的语法以及相关案例。了解jsx是如何生成最终的VNode。tsx应用Demo代码放在github vue-tsx-demo (opens new window) (opens new window)
# # 1. 环境基础babel
vue-cli3自动生成的app项目中,babel.config.js预设了 presets: ["@vue/app"],该插件为babel-preset-app (opens new window) (opens new window)
里面包含插件,主要是babel解析,以支持许多扩展语法。比如jsx、es6语法
等:
"@babel/core": "^7.9.0",
"@babel/helper-compilation-targets": "^7.8.7",
"@babel/helper-module-imports": "^7.8.3",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-jsx": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/preset-env": "^7.9.0",
"@babel/runtime": "^7.9.2",
"@vue/babel-preset-jsx": "^1.1.2",
"babel-plugin-dynamic-import-node": "^2.3.0",
2
3
4
5
6
7
8
9
10
11
12
13
里面重点插件有:
# # 1.1 Babel JSX插件集合
- @vue/babel-preset-jsx (opens new window) (opens new window):vue jsx插件集合,vue官方出品
- 解析jsx为vnode函数(核心)@vue/babel-plugin-transform-vue-jsx (opens new window) (opens new window)
- 支持解析jsx
- 支持jsx props扩展
- 支持vue directives
- 支持直接导入component
- v-model语法糖 @vue/babel-sugar-v-model (opens new window) (opens new window)
- 自动在render函数注入h方法 @vue/babel-sugar-inject-h (opens new window) (opens new window)
- 支持在jsx中 functional @vue/babel-sugar-functional-vue (opens new window) (opens new window)
- 支持v-on事件 @vue/babel-sugar-v-on (opens new window) (opens new window)
- 自动合并jsx props @vue/babel-helper-vue-jsx-merge-props (opens new window) (opens new window)
- 解析jsx为vnode函数(核心)@vue/babel-plugin-transform-vue-jsx (opens new window) (opens new window)
# # 1.2 Babel预设插件集合
- @babel/preset-env (opens new window) (opens new window) babel智能预设
- 集成了browserslist (opens new window) (opens new window),该插件包含其他核心插件:
- postcss-preset-env (opens new window) (opens new window),将现代css转换成浏览器理解的
- autoprefixer (opens new window) (opens new window),解析css成兼容多个浏览器
- 众多ES6 Stage语法支持,比如:
- 参数targets:为项目支持/目标的环境
- 默认转换所有ECMAScript 2015+代码
- chrome,opera,edge,firefox,safari,ie,ios,android 确定最低版本要求
- node
- electron
- 参数targets.esmodules: 定位为支持ES模块的浏览器(
直接支持ES6模块语法,浏览器能自己解析import语法
,可显著减少包体积)。注意:指定esmodules目标时,浏览器目标将被忽略 - 参数modules:
启用将ES6模块语法转换为其他模块类型的功能
。"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默认为"auto"。设置为false不会转换模块
- 参数targets.browsers: 同上targets作用
- 参数target.node: 同上targets作用
- 集成了browserslist (opens new window) (opens new window),该插件包含其他核心插件:
# # 1.3 其他
- @babel/core (opens new window) (opens new window): 根据本地配置的config文件 (opens new window) (opens new window)(babel7考虑到monorepos项目存在,所以采用了babel.config.json配置文件,以作用全局),把代码转换。(所有babel插件必备前置包)
- @babel/cli (opens new window) (opens new window): 客户端执行,依赖上面@babel/core。
# # 2. JSX在Vue中语法
react和vue底层vnode diff对比不是使用相同的数据结构,所以导致两者jsx书写方式有些许不同。目前两者大部分jsx语法一致,是因为有各种babel插件辅助做了这部分事。但对于动态属性这样自由化较高的地方,需要我们知道两者本质区别(即不同的VNode数据结构)。
vue template模板本质上最终生成render函数,而render函数本质上是生成VNode,所以有必要了解这个VNode数据结构。Vue2.x VNode diff核心算法借鉴的是snabbdom (opens new window) (opens new window)库,所以数据结构也有snabbdom数据结构的影子,比如事件需要放置在on属性上,方便最终patch时挂载到真实dom元素上(react则自定义模拟事件系统,所有事件都冒泡到顶层document处理)。更多VNode信息可查看Vue官方文档 - 深入数据对象 (opens new window) (opens new window)。
以下查看jsx转译为h函数:
render (h) {
return (
<div
// Component props
propsMsg="hi"
// Normal attributes or component props.
id="foo"
// DOM properties are prefixed with `domProps`
domPropsInnerHTML="bar"
// event listeners are prefixed with `on` or `nativeOn`
onClick={this.clickHandler}
nativeOnClick={this.nativeClickHandler}
// other special top-level properties
class={{ foo: true, bar: false }}
style={{ color: 'red', fontSize: '14px' }}
key="key"
ref="ref"
// assign the `ref` is used on elements/components with v-for
refInFor
slot="slot">
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
以上jsx语法糖等同于如何h函数生成VNode:
render (h) {
return h('div', {
// Component props
props: {
msg: 'hi'
},
// Normal HTML attributes
attrs: {
id: 'foo'
},
// DOM props
domProps: {
innerHTML: 'bar'
},
// Event handlers are nested under "on", though
// modifiers such as in v-on:keyup.enter are not
// supported. You'll have to manually check the
// keyCode in the handler instead.
on: {
click: this.clickHandler
},
// For components only. Allows you to listen to
// native events, rather than events emitted from
// the component using vm.$emit.
nativeOn: {
click: this.nativeClickHandler
},
// Class is a special module, same API as `v-bind:class`
class: {
foo: true,
bar: false
},
// Style is also same as `v-bind:style`
style: {
color: 'red',
fontSize: '14px'
},
// Other special top-level properties
key: 'key',
ref: 'ref',
// Assign the `ref` is used on elements/components with v-for
refInFor: true,
slot: 'slot'
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
使用jsx代替vue需要解决的系列问题:
- 与vue兼容,能被识别
- 支持使用v-model v-on(babel插件解决,见以上)
- 支持使用vue watch/computed/methods
- 支持使用created/mounted等生命周期
- 支持css module
- 支持动态props传值问题
- 支持插槽
- 支持Vue原型链上问题,如$xxx、ref等功能。
# # 2.1 基础语法
bable jsx插件会通过正则匹配的方式在编译阶段将书写在组件上属性进行“分类”。 onXXX的均被认为是事件,nativeOnXXX是原生事件,domPropsXXX是Dom属性。
class,staticClass,style,key,ref,refInFor,slot,scopedSlots这些被认为是顶级属性,至于我们属性声明的props,以及html属性attrs,不需要加前缀,插件会将其统一分类到attrs属性下,然后在运行阶段根据是否在props声明来决定属性归属(即属于props还是attrs)。
export default {
name: "button-counter",
props: ["count"],
methods: {
onClick() {
this.$emit("change", this.count + 1);
}
},
render() {
return (
<button onClick={this.onClick}>You clicked me {this.count} times.</button>
);
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# # 2.2 动态属性
在React中所有属性都是顶级属性,直接使用{...props}就可以了,但是在Vue中,你需要明确该属性所属的分类,如一个动态属性value和事件change,你可以使用如下方式(延展属性)传递:
const dynamicProps = {
props: {},
on: {},
}
if(haValue) dynamicProps.props.value = value
if(hasChange) dynamicProps.on.change = onChange
<Dynamic {...dynamicProps} />
2
3
4
5
6
7
8
尽量使用明确分类的方式传递属性,而不是要babel插件帮你分类及合并属性。
# # 2.3 指令
常用的v-if和v-for,使用js语法中的if/for语句就能实现了。v-model属于prop + input事件语法糖,也可以使用babel插件自动实现。
// v-show,同理v-if
render(){
return (
<div>
{this.show?'你帅':'你丑'}
</div>
)
}
// v-for
render(){
return (
<div>
{this.list.map((v)=>{
return <p>{v}</p>
})}
</div>
)
}
// v-model: 传值和监听事件改变值(babel插件已支持)
data(){
return{
text:'',
}
},
methods:{
input(e){
this.text=e.target.value
}
},
render(){
return (
<div>
<input type="text" value={this.text} onInput={this.input}/>
<p>{this.text}</p>
</div>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# # 2.4 slot插槽
slot直接通过this.$slots
对象拿到,scopedSlot通过this.$scopedSlots
对象拿到($scopedSlots每项是待调用函数)。
export default {
render(h) {
return <div>
{
(this.title || this.$slots.header) && (
<div class="header">
<span class="title">{this.title}</span>
{this.$slots.header}
</div>
)
}
{this.$slots.default}
</div>
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# # 2.5 组件
不需要注册,直接使用
import Todo from './Todo.jsx'
export default {
render(h) {
return <Todo /> // no need to register Todo via components option
},
}
2
3
4
5
6
7
8
# # 2.6 functional函数
export default {
functional:true,
render(h,context){
return (
<div class="red" {...context.data}>
{context.props.data}
</div>
)
}
}
2
3
4
5
6
7
8
9
10
11
# # 2.7 v-model
安装@vue/babel-sugar-v-model
babel插件后即可自动解析v-model,官方更推荐使用vModel
或者value + onInput事件
。
<el-input vModel_trim={inputValue}/>
// 或者使用
<el-input
value={this.inputValue}
onInput={val => this.inputValue = val.trim()}/>
2
3
4
5
6
# # 参考文章
- JavaScript modules 模块 (opens new window) (opens new window)
- babel-preset-env (opens new window) (opens new window)
- babel jsx语法扩展说明 (opens new window) (opens new window)
- https://cn.vuejs.org/v2/guide/render-function.html#v-model
- https://juejin.im/post/5b221e2951882574aa5f4c5a
- https://juejin.im/post/5affa64df265da0b93488fdd