原文地址
组件开发
特性对比
众所周知,Vue 和 React 都有那么一个特性,那就是可以让我们进行组件化开发,这样可以让代码得到更好的重用以及解耦,在架构定位中这个应该叫纵向分层吧。但是,两个框架开发组件的写法都有所不同(这个不同是基于我的开发习惯),下面先看一下不同的地方。
首先是 React,个人习惯于 es6 的写法(从来没用过 es5 的 createClass 的写法):
import React, {Component} from ‘react’;
import propTypes from ‘prop-types’;
export default class Demo extends Component {
state = {
text: ‘hello world’
};
static propTypes = {
title: PropTypes.String
}
static defaultProps = {
title: ‘React Demo’
}
setText = e => {
this.setState({
text: ‘ 点击了按钮 ’
})
}
componentWillReveiveProps(nextProps) {
console.log(` 标题从 ${this.props.title} 变为了 ${nextProps.title}`)
}
render() {
const {title} = this.props;
const {text} = this.state;
return <div>
<h1>{title}</h1>
<span>{text}<span>
<button onClick={this.setText}> 按钮 <button>
</div>
}
}
下面是常见 vue 的写法:
<template>
<div>
<h1>{{title}}</h1>
<span>{{text}}<span>
<button @click=”setText”> 按钮 </button>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: ‘Vue Demo’
}
},
watch: {
title(newTitle, oldTitle) {
console.log(` 标题从 ${oldTile} 变为了 ${newTitle}`)
}
},
data() {
return {
text: ‘hello world’
}
},
methods: {
setText(e) {
this.text = ‘ 点击了按钮 ’;
}
}
}
</script>
这里的视图渲染我们先忽略,下一节在详细对比。
prop 对比:
Vue 的 prop 必须在 props 字段里声明。React 的 prop 不强制声明,声明时也可以使用 prop-types 对其声明约束。Vue 的 prop 声明过后挂在在组件的 this 下,需要的时候在 this 中获取。React 的 prop 存在组件的 props 字段中,使用的时候直接在 this.props 中获取。组件状态对比,Vue 为 data,React 为 state:
Vue 的状态 data 需要在组件的 data 字段中以函数的方式声明并返回一个对象。React 的状态 state 可以直接挂载在组件的 state 字段下,在使用之前初始化即可。Vue 的状态 data 声明后挂在在 this 下面,需要的是时候在 this 中获取。React 的状态 state 存在组件的 state 字段中,使用的时候直接在 this.state 中获取。Vue 的状态更新可以直接对其进行赋值,视图可以直接得到同步。React 的状态更新必须使用 setState,否则视图不会更新。然后是组件方法对比:
Vue 的方法需要在 methods 字段下声明。React 的方法用方法的方式声明在组件下即可。Vue 与 React 使用方法的方式相同,因为都是挂载在组件中,直接在 this 中获取即可。计算属性 computed 对比:
Vue 有计算属性在 computed 字段中声明。React 中无计算属性特性,需要其他库如 mobx 辅助完成。Vue 的计算属性声明后挂载在 this 下,需要的时候在 this 中获取。监听数据对比:
Vue 中可以在 watch 字段中对 prop、data、computed 进行对比,然后做相应的操作。在 React 所有变化需要在声明周期 componentWillReveiveProps 中手动将 state 和 prop 进行对比。对比完后发现,其实 Vue 给我的个人感觉就是自己在写配置,只不过配置是以函数的形式在写,然后 Vue 帮你把这些配置好的东西挂载到组件下面。而且 prop、data、computed、方法所有都是挂载组件下,其实单单从 js 语法上很难以理解,比如说我在 computed 中,想获取 data 的 text 数据,使用的是 this.text 来获取,如果抛开 vue,单单用 js 语法来看,其实 this 大多情况是指向 computed 对象的,所以个人觉得这样的语法是反面向对象的。
这个时候在反过来看 React 的 class 写法,本来就是属于面向对象的写法,状态 state 归状态,属性 prop 归属性,方法归方法,想获取什么内容,通过 this 直接获取,更接近于 JavaScript 编程,相对来说比较好理解。
组件改造
针对 Vue 的反面向对象,我们可以更改其写法,通过语法糖的形式,将其我们自己的写法编译成 Vue 需要的写法。
vue-class-componentvue-class-component 是 Vue 英文官网推荐的一个包,可以以 class 的模式写 vue 组件,它带来了很多便利:
methods,钩子都可以直接写作 class 的方法 computed 属性可以直接通过 get 来获得初始化 data 可以声明为 class 的属性其他的都可以放到 Component 装饰器里 vue-property-decoratorvue-property-decorator 这个包完全依赖于 vue-class-component,提供了多个装饰器,辅助完成 prop、watch、model 等属性的声明。
编译准备
由于使用的是装饰器语法糖,我们需要在我们 webpack 的 babel 编译器中对齐进行支持。
首先是 class 语法支持,针对 babel6 及更低的版本,需要配置 babel 的 plugin 中添加 class 语法支持插件 babel-plugin-transform-class-properties,针对 babel7,需要使用插件 @babel/plugin-proposal-class-properties 对 class 进行语法转换。
然后是装饰器语法支持,针对 babel6 及更低的版本,需要配置 babel 的 plugin 中添加装饰器语法支持插件 babel-plugin-transform-decorators-legacy,针对 babel7,需要使用插件 @babel/plugin-proposal-decorators 对装饰器进行语法转换。
针对 bable6,配置.babelrc 如下
{
“presets”: [“env”, “stage-1”],
“plugins”: [
“transform-runtime”,
“syntax-dynamic-import”,
“transform-class-properties”, // 新增 class 语法支持
“transform-decorators-legacy” // 新增装饰器语法支持
]
}
对于 bable7,官方推荐直接使用 @vue/apppreset,该预设包含了 @babel/plugin-proposal-class-properties 和 @babel/plugin-proposal-decorators 两个插件,另外还包含了动态分割加载 chunks 支持 @babel/plugin-syntax-dynamic-import,同时也包含了 @babel/envpreset,.babelrc 配置如下:
{
“presets”: [
[“@vue/app”, {
“loose”: true,
“decoratorsLegacy”: true
}]
]
}
重写组件
编译插件准备好之后,我们对上面的 Vue 组件进行改写,代码如下
<template>
<div>
<h1>{{title}}</h1>
<span>{{text}}<span>
<button @click=”setText”> 按钮 </button>
</div>
</template>
<script>
import {Vue, Component, Watch, Prop} from ‘vue-property-decorator’;
@Component
export default class Demo extends Vue {
text = ‘hello world’;
@Prop({type: String, default: ‘Vue Demo’}) title;
@Watch(‘title’)
titleChange(newTitle, oldTitle) {
console.log(` 标题从 ${oldTile} 变为了 ${newTitle}`)
}
setText(e) {
this.text = ‘ 点击了按钮 ’;
}
}
</script>
到此为止,我们的组件改写完毕,相对先前的“写配置”的写法,看起来相对来说要好理解一些吧。
注意:Vue 的 class 的写法的 methods 还是没办法使用箭头函数进行的,详细原因这里就不展开,大概就是因为 Vue 内部挂载函数的方式的原因。
视图开发
特性对比
针对视图的开发,Vue 推崇 html、js、css 分离的写法,React 推崇 all-in-js,所有都在 js 中进行写法。
当然各有各的好处,如 Vue 将其进行分离,代码易读性较好,但是在 html 中无法完美的展示 JavaScript 的编程能力,而对于 React 的 jsx 写法,因为有 JavaScript 的编程语法支持,让我们更灵活的完成视图开发。
对于这类不灵活的情况,Vue 也对 jsx 进行了支持,只需要在 babel 中添加插件 babel-plugin-transform-vue-jsx、babel-plugin-syntax-jsx、babel-helper-vue-jsx-merge-props(babel6,对于 babel7,官方推荐的 @vue/app 预设中已包含了 jsx 的转化插件),我们就可以像 React 一样,在组件中声明 render 函数并返回 jsx 对象,如下我们对上一节的组件进行改造:
组件改造
<script>
import {Vue, Component, Watch, Prop} from ‘vue-property-decorator’;
@Component
export default class Demo extends Vue {
title = ‘hello world’;
@Prop({type: String, default: ‘Vue Demo’}) title;
@Watch(‘title’)
titleChange(newTitle, oldTitle) {
console.log(` 标题从 ${oldTile} 变为了 ${newTitle}`)
}
setText(e) {
this.text = ‘ 点击了按钮 ’;
}
render() {
const {title, text} = this;
return <div>
<h1>{title}</h1>
<span>{text}<span>
<button onClick={this.setText}> 按钮 <button>
</div>
}
}
</script>
Vue 的 jsx 使用注意点写到这里,也基本上发现其写法已经与 React 的 class 写法雷同了。那么 Vue 的 jsx 和 React 的 jsx 有什么不同呢。
在 React 的 jsx 语法需要 React 支持,也就是说,在你使用 jsx 的模块中,必须引进 React。
而 Vue 的 jsx 语法需要 Vue 的 createElement 支持,也就是说在你的 jsx 语法的作用域当中,必须存在变量 h,变量 h 为 createElement 的别名,这是 Vue 生态系统中的一个通用惯例,在 render 中 h 变量由编译器自动注入到作用域中,自动注入详情见 plugin-transform-vue-jsx,如果没有变量 h,需要从组件中获取并声明,代码如下:
const h = this.$createElement; 这里借助官方的一个例子,基本包含了所有 Vue 的 jsx 常用语法,如下:
// …
render (h) {
return (
<div
// 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>
)
}
但是,Vue 的 jsx 语法无法支持 Vue 的内建指令,唯一的例外是 v -show,该指令可以使用 v -show={value} 的语法。大多数指令都可以用编程方式实现,比如 v -if 就是一个三元表达式,v-for 就是一个 array.map() 等。
如果是自定义指令,可以使用 v -name={value} 语法,但是该语法不支持指令的参数 arguments 和修饰器 modifier。有以下两个解决方法:
将所有内容以一个对象传入,如:v-name={{value, modifier: true}} 使用原生的 vnode 指令数据格式,如:const directives = [{ name: ‘my-dir’, value: 123, modifiers: { abc: true} }]
return <div {…{ directives}}/> 那么,我们什么时候使用 jsx,什么时候 template 呢?很明显,面对那么复杂多变的视图渲染,我们使用 jsx 语法更能得心应手,面对简单的视图,我们使用 template 能开发得更快。
状态管理
特性对比
针对状态管理,Vue 的 Vuex 和 React 的 Redux 很雷同,都是 Flow 数据流。
对于 React 来说,state 需要通过 mapStateToProps 将 state 传入到组件的 props 中,action 需要通过 mapDispatchToProps 将 action 注入到组件的 props 中,然后在组件的 props 中获取并执行。
而在 Vue 中,store 在组件的 $store 中,可以直接 this.$store.dispatch(actionType) 来分发 action,属性也可以通过 mapState,或者 mapGetter 把 state 或者 getter 挂载到组件的 computed 下,更粗暴的可以直接 this.$store.state 或者 this.$store.getter 获取,非常方便。
组件改造我们为了更贴切于 es6 的 class 写法,更好的配合 vue-class-component,我们需要通过其他的方式将 store 的数据注入到组件中。
vuex-classvuex-class,这个包的出现,就是为了更好的讲 Vuex 与 class 方式的 Vue 组件连接起来。
如下,我们声明一个 store
import Vuex from ‘vuex’;
const store = new Vuex.Store({
modules: {
foo: {
namespaced: true,
state: {
text: ‘hello world’,
},
actions: {
setTextAction: ({commit}, newText) => {
commit(‘setText’, newText);
}
},
mutations: {
setText: (state, newText) => {
state.text = newText;
}
}
}
}
})
针对这个 store,我们改写我们上一章节的组件
<template>
<div>
<h1>{{title}}</h1>
<span>{{text}}<span>
<button @click=”setText”> 按钮 </button>
</div>
</template>
<script>
import {Vue, Component, Watch, Prop} from ‘vue-property-decorator’;
import {namespace} from ‘vuex-class’;
const fooModule = namespace(‘foo’);
@Component
export default class Demo extends Vue {
@fooModule.State(‘text’) text;
@fooModule.Action(‘setTextAction’) setTextAction;
@Prop({type: String, default: ‘Vue Demo’}) title;
@Watch(‘title’)
titleChange(newTitle, oldTitle) {
console.log(` 标题从 ${oldTile} 变为了 ${newTitle}`)
}
setText(e) {
this.setTextAction(‘ 点击了按钮 ’);
}
}
</script>
这里可以发现,store 声明了一个 foo 模块,然后在使用的时候从 store 中取出了 foo 模块,然后使用装饰器的形式将 state 和 action 注入到组件中,我们就可以省去 dispatch 的代码,让语法糖帮我们 dispatch。这样的代码,看起来更贴切与面向对象。。。好吧,我承认这个代码越写越像 Java 了。
然而,之前的我并不是使用 Redux 开发 React 的,而是 Mobx,所以这种 dispatch -> action -> matation -> state 的形式对我来说也不是很爽,我还是更喜欢把状态管理也以 class 的形式去编写,这个时候我又找了另外一个包 vuex-module-decorators 来改写我的 store.module。
下面我们改写上面的 store:
import Vuex from ‘vuex’;
import {Module, VuexModule, Mutation, Action} from ‘vuex-module-decorators’;
@Module
class foo extends VuexModule {
text = ‘hello world’
@Mutation
setText(text) {
this.text = text;
}
@Action({commit: ‘setText’})
setTextAction(text) {
return text;
}
}
const store = new Vuex.Store({
modules: {
foo: foo
})
export default store;
这样,我们的项目准备基本上完毕了,把 Vue 组件和 Vuex 状态管理以 class 的形式来编写。大概是我觉得 es5 的写法显得不太优雅吧,没有 es6 的写法那么高端。
结束 class 语法和装饰器 decorators 语法都是 ES6 的提案,都带给了前端不一样的编程体验,大概也是前端的一个比较大的革命吧,我们应该拥抱这样的革命变化。