前言
首先,為什麼我需要做這個專案準備工作呢?因為常年習慣React開發的我,最近突然接手了一個Vue專案,而之前基本沒有過Vue的實踐,這麼突兀讓還在沉溺於React開發的我進行Vue開發,甚是不習慣,那自然我需要想辦法讓Vue開發儘量與React相似,這樣大概讓自己在開發過程中更得心應手吧。
元件開發
特性對比
眾所周知,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-component
vue-class-component 是Vue英文官網推薦的一個包,可以以class的模式寫vue元件,它帶來了很多便利:
- methods,鉤子都可以直接寫作class的方法
- computed屬性可以直接通過get來獲得
- 初始化data可以宣告為class的屬性
- 其他的都可以放到Component裝飾器裡
vue-property-decorator
vue-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/app
preset,該預設包含了@babel/plugin-proposal-class-properties
和@babel/plugin-proposal-decorators
兩個外掛,另外還包含了動態分割載入chunks支援@babel/plugin-syntax-dynamic-import
,同時也包含了@babel/env
preset,.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-class
vuex-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的提案,都帶給了前端不一樣的程式設計體驗,大概也是前端的一個比較大的革命吧,我們應該擁抱這樣的革命變化。