把react什么的用起來——我不是雙向綁定
先弄個(gè)什么例子呢?如果是現(xiàn)代的MVVM框架,可能會(huì)用雙向綁定來吸引你。那react有雙向綁定嗎?
沒有。
也算是有吧,有插件。不過雙向綁定跟react不是一個(gè)路子的。react強(qiáng)調(diào)的是單向數(shù)據(jù)流。 當(dāng)然,即便是單向數(shù)據(jù)流也總要有個(gè)數(shù)據(jù)的來源,如果數(shù)據(jù)來源于頁面自身上的用戶輸入,那效果也就等同于雙向綁定了。
下面就展示一下如何達(dá)到這個(gè)效果。我們來設(shè)計(jì)一個(gè)登錄的場(chǎng)景,用戶輸入用戶名后,會(huì)在問候語的位置展示用戶名,像下圖這樣:
預(yù)警一下先,我要用這個(gè)小東西展示react+redux的數(shù)據(jù)流工作方式,所以代碼看起來比較多, 肯定比一些MVVM框架雙向綁定一對(duì)雙大括號(hào)代碼要多得多。但正如我前面說的,它倆不是一個(gè)路子, react這種模式的好處后面你一定會(huì)看出來,這里先耐著性子把這幾段貌似很羅嗦的代碼看完。 react和redux很多重要的思想在這就開始提現(xiàn)出來了。
先把組件寫出來。為了簡(jiǎn)便,我們把整個(gè)登錄頁面作為一個(gè)組件,放在containers目錄下。 還記得前面說過containers和components目錄嗎?把組件放在containers目錄下,意味著這個(gè)組件要跟外界打交道。 不過一開始,我們先別管打交道的事兒,就寫一個(gè)簡(jiǎn)單的,普通的組件:
- import React from 'react'
- class Login extends React.Component{
- render(){
- return (
- <div>
- <div>早上好,{this.props.username}</div>
- <div>用戶名:<input/></div>
- <div>密 碼:<input type="papssword"/></div>
- <button>登錄</button>
- </div>
- )
- }
- }
- export default Login
為了能讓我們寫的東西顯示出來,得改點(diǎn)模板代碼,現(xiàn)在來修改一下src/index.js,里面原來的代碼都不需要了,改成:
- import React from 'react';
- import { render } from 'react-dom';
- import { Provider } from 'react-redux';
- import configureStore from '../stores';
- import Login from '../containers/Login';
- const store = configureStore();
- render(
- <Provider store={store}>
- <Login />
- </Provider>,
- document.getElementById('app')
- );
搭建環(huán)境時(shí)自動(dòng)打開的瀏覽器頁面還沒關(guān)吧?保存代碼后少等片刻就可以看到我們做的登陸頁面了。
目前這個(gè)登錄組件里問候語里顯示的用戶名和用戶輸入的用戶名毫無關(guān)系,如何將它們聯(lián)系起來呢? 既然看到了{(lán)this.props.username}你肯定會(huì)想到有一個(gè)數(shù)據(jù)模型。的確是有這么個(gè)東西,不過在redux里, 這個(gè)數(shù)據(jù)模型很壯觀,整個(gè)應(yīng)用只有一個(gè)數(shù)據(jù)模型,所以更應(yīng)該管它叫數(shù)據(jù)倉(cāng)庫(kù)。這個(gè)倉(cāng)庫(kù)的代碼在stores/index.js里面。 代碼很簡(jiǎn)單,就是用reducers和initialState兩個(gè)參數(shù)來創(chuàng)建一個(gè)倉(cāng)庫(kù)。看剛才run.js里面的代碼, 有個(gè)叫Provider的組件使用了倉(cāng)庫(kù),意思很明顯:在provider這個(gè)組件內(nèi)部,已經(jīng)給我們提供好了倉(cāng)庫(kù)的訪問條件, 也就是說我們的Login組件已經(jīng)可以訪問倉(cāng)庫(kù)了。怎么訪問呢?需要把我們的組件跟倉(cāng)庫(kù)連接起來。 登錄組件代碼***一行“export default Login”要改成這樣:
- function mapStateToProps(state) {
- return {}
- }
- export default connect(mapStateToProps)(Login);
connect是react-redux這個(gè)庫(kù)提供的函數(shù),功能就是把組件連接到rudux的倉(cāng)庫(kù)。注意在文件頂部加上一句“import { connect } from 'react-redux'”。 這里有個(gè)函數(shù)mapStateToProps,它返回的對(duì)象就是從倉(cāng)庫(kù)取出的數(shù)據(jù),具體的數(shù)據(jù)等我們寫完reducer再補(bǔ)充。
那么reducer是什么呢?
我們考慮一下倉(cāng)庫(kù)的數(shù)據(jù)是要變化的,怎么讓它變化呢?我們得給個(gè)規(guī)則,這個(gè)規(guī)則描述起來就是: “在發(fā)生某一動(dòng)作(action)時(shí),倉(cāng)庫(kù)中的一部分?jǐn)?shù)據(jù)要進(jìn)行相應(yīng)的變化”。我們管會(huì)因動(dòng)作而變化的這一部分?jǐn)?shù)據(jù)叫做狀態(tài), 許許多多瑣碎的狀態(tài)組成了倉(cāng)庫(kù)數(shù)據(jù),所以整個(gè)倉(cāng)庫(kù)其實(shí)就是一個(gè)大的狀態(tài)。在程序運(yùn)行過程中,我們主要關(guān)心的就是這個(gè)倉(cāng)庫(kù)的狀態(tài)如何變化。 如何變化?那就要靠reducer。針對(duì)一個(gè)動(dòng)作,倉(cāng)庫(kù)里會(huì)有一個(gè)或多個(gè)狀態(tài)發(fā)生變化,reducer就是要指導(dǎo)狀態(tài)如何變化。
等等,那動(dòng)作是哪來的?從具體上說,動(dòng)作一般是來源于用戶的操作或者網(wǎng)絡(luò)請(qǐng)求的回應(yīng)。在代碼里需要對(duì)動(dòng)作規(guī)范一下, 其實(shí)也就是跟reducer進(jìn)行一個(gè)約定,讓它知道有動(dòng)作來了。其實(shí)怎樣表示動(dòng)作都可以,只要具有唯一性就行。 一般我們就用字符串就行了,即容易制造唯一,又能夠表義,在使用中小心點(diǎn)別重了就行。下面就來定義一個(gè)用戶輸入用戶名的動(dòng)作:
- const INPUT_USERNAME = 'INPUT_USERNAME'
咋不直接用字符串呢?為了避免低級(jí)錯(cuò)誤,定義了這個(gè)常量以后,發(fā)起動(dòng)作時(shí)用這個(gè)常量,reducer也根據(jù)這個(gè)常量辨別動(dòng)作類型。
我們光告訴reducer發(fā)生了“用戶輸入”這個(gè)動(dòng)作還不夠,還要告訴reducer用戶輸入了什么內(nèi)容。所以完整的動(dòng)作得是一個(gè)具有豐富信息的對(duì)象。 為了方便,我們寫一個(gè)動(dòng)作生成器,也就是個(gè)函數(shù):
- function inputUsername (value) {
- return {
- type: INPUT_USERNAME,
- value: value
- }
- }
現(xiàn)在reducer就能得到足夠的信息來指導(dǎo)狀態(tài)的變化了。reducer要做的就是把倉(cāng)庫(kù)里一個(gè)叫做“username”的狀態(tài)的值修改一下。 由于狀態(tài)可以是一層套一層的,所以reducer也被設(shè)計(jì)成可以一層套一層。單個(gè)reducer就是它上級(jí)reducer的一分子。 其實(shí)reducer本身也就是個(gè)函數(shù):
- function username (state='', action) {
- switch(action.type){
- case INPUT_USERNAME:
- return action.value
- defalut:
- return state
- }
- }
reducer的函數(shù)名對(duì)應(yīng)著狀態(tài)名稱,函數(shù)接受兩個(gè)參數(shù):***個(gè)是當(dāng)前狀態(tài),如果是程序開始運(yùn)行的時(shí)候, 很可能沒有當(dāng)前狀態(tài),就給個(gè)默認(rèn)值,這里是空字符串;第二個(gè)是前面動(dòng)作生成器生成的action對(duì)象。 一個(gè)reducer可以處理多種動(dòng)作,目前我們只有一個(gè),以后有別的就直接加case分支。對(duì)于每種動(dòng)作, reducer都要返回一個(gè)新的狀態(tài)值,這個(gè)值就可以根據(jù)action傳來的信息按照業(yè)務(wù)要求生成了。 ***一定要加一個(gè)默認(rèn)情況返回當(dāng)前狀態(tài)。在redux里,任何一個(gè)action都會(huì)在所有的reducer里過一遍, 所以對(duì)于一個(gè)reducer來說實(shí)際上絕大多數(shù)情況action都不是它能處理的,***還是返回當(dāng)前狀態(tài)值。 覺得很低效嗎?😉別怕,只是空走了一遍分支,這對(duì)諸如修改DOM這樣的重頭戲來說根本不算什么。
reducer是一層又一層的樹狀結(jié)構(gòu),怎么把它們組合到一起呢?rudex提供了一個(gè)組合工具combineReducers。 加入我們已經(jīng)寫好了另一個(gè)名為password的reducer,組合它們就是這個(gè)樣子:
- combineReducers({username, password})
注意,combineReducers接收的參數(shù)是一個(gè)對(duì)象,而不是多個(gè)函數(shù),上面的代碼用的是es6的簡(jiǎn)寫方式。
很容易發(fā)現(xiàn),上面的reducer和action生成器都是非常死板的代碼,今后我們會(huì)寫大量的這樣的代碼, 那會(huì)出現(xiàn)滿篇樣板代碼的情形,那可有點(diǎn)蠢笨了。所以我們把重復(fù)的東西盡可能的抽取出來,寫個(gè)reucer生成器以及action生成器的生成器:
- // reducer生成器,為了以后使用方便,起名為create reducer的簡(jiǎn)寫
- function cr (initialState, handlers) {
- return function reducer(state = initialState, action) {
- if (handlers.hasOwnProperty(action.type)) {
- return handlers[action.type](state, action);
- } else {
- return state;
- }
- }
- }
- // actiong生成器的生成器,同樣原因,起名為create action creator的簡(jiǎn)寫
- return function(...args) {
- let action = { type }
- argNames.forEach((arg, index) => {
- action[argNames[index]] = args[index]
- })
- return action
- }
這倆函數(shù)完成的事情跟我們寫樣板代碼做的事情完全相同。具體說明一下:
cr的兩個(gè)參數(shù):initialState是初始狀態(tài);handlers是由一堆函數(shù)組成的對(duì)象,每個(gè)函數(shù)的名稱對(duì)應(yīng)著一個(gè)action的類型, 每個(gè)函數(shù)接受的參數(shù)與reducer一樣,是action和當(dāng)前狀態(tài),返回值會(huì)被當(dāng)做新狀態(tài)。默認(rèn)情況就不用我們處理了。
cac接受的***個(gè)參數(shù)是action的類型名稱,后面參數(shù)是所有附帶數(shù)據(jù)的屬性名稱。
好了,把代碼規(guī)整一下。對(duì)現(xiàn)在小小的模擬雙向綁定的功能來說,我們還不需要記錄密碼的狀態(tài),不過我們也先寫上,后面會(huì)用到。
***先寫action。因?yàn)橐话銇碚f,只要你想好了你得應(yīng)用有什么功能,action就可以寫了,而且action不依賴其它東西。
src/actions/login.js:
- import {cac} from '../utils'
- export const INPUT_USERNAME = 'INPUT_USERNAME'
- export const INPUT_PASSWORD = 'INPUT_PASSWORD'
- export const inputUsername = cac(INPUT_USERNAME, 'value')
- export const inputPassword = cac(INPUT_PASSWORD, 'value')
action類型名稱的常量現(xiàn)在都寫到了action文件里,不過也許把所有這些常量放到一個(gè)單獨(dú)的文件里比較好, 這樣在es6語法的幫助下就可以避免重復(fù)了。
這里我們把所有的東西都導(dǎo)出了,action類型名稱reducer會(huì)用到,action生成器組件會(huì)用到。
然后寫reducer。當(dāng)你想好應(yīng)用的功能后,接下來就是要考慮背后的數(shù)據(jù)結(jié)構(gòu)了。而reducer一寫出來,數(shù)據(jù)結(jié)構(gòu)就確定了。
src/reucers/login.js:
- import {combineReducers} from 'redux';
- import {cr} from '../utils'
- import {INPUT_USERNAME, INPUT_PASSWORD} from 'actions/login'
- export default combineReducers({
- username: cr('', {
- [INPUT_USERNAME](state, {value}){return value}
- }),
- password: cr('', {
- [INPUT_PASSWORD](state, {value}){return value}
- })
- })
對(duì)action文件的引用,路徑里沒有用../,這樣寫是因?yàn)閍ctions是一個(gè)別名,它代表actions目錄的絕對(duì)路徑,這是webpack幫我們做的。 當(dāng)然你也可以定義自己的別名,修改cfg/base.js就行,比如在resolve.alias對(duì)象里加一個(gè)自己的工具集:“utils:srcPath + '/utils.js'”。
rducer最終是要注冊(cè)到store那里的,這個(gè)過程在src/storces/index.js里面已經(jīng)寫了, 可以看到里面的代碼用的是../reducers這個(gè)文件(這是個(gè)目錄,實(shí)際的文件是里面index.js), 所以我們也需要把新寫的reducer注冊(cè)到這里面去。修改src/reducers/index.js:
- import { combineReducers } from 'redux';
- import login from './login'
- const reducers = {
- login
- };
- module.exports = combineReducers(reducers);
在reducers/index里,所有的reducer也是通過combineReducers組合到一起的,只不過現(xiàn)在我們只有一個(gè)孤零零的子reducer:login。
終于,是時(shí)候回到組件上來了。src/containers/Login.js現(xiàn)在要修改成這樣:
- import React from 'react'
- import { connect } from 'react-redux'
- import {inputUsername, inputPassword} from 'actions/login'
- class Login extends React.Component{
- inputUsernameHandler(evt){
- this.props.dispatch(inputUsername(evt.target.value))
- }
- inputPasswordHandler(evt){
- this.props.dispatch(inputPassword(evt.target.value))
- }
- render(){
- return (
- <div>
- <div>早上好,{this.props.username}</div>
- <div>用戶名:<input onChange={this.inputUsernameHandler.bind(this)}/></div>
- <div>密 碼:<input type="papssword" onChange={this.inputPasswordHandler.bind(this)}/></div>
- <button>登錄</button>
- </div>
- )
- }
- }
- function mapStateToProps(state) {
- return {
- username: state.login.username,
- password: state.login.password
- }
- }
- export default connect(mapStateToProps)(Login);
有幾處變化:
首先,前面已經(jīng)說過,要把組件連接到倉(cāng)庫(kù),就要用connect。并且現(xiàn)在我們已經(jīng)確定了倉(cāng)庫(kù)里login對(duì)應(yīng)狀態(tài)的數(shù)據(jù)接口, 那么mapStateToProps返回的內(nèi)容也就確定了。login狀態(tài)里的兩個(gè)屬性映射成了組件的屬性, 所以用this.props.username就可以訪問到倉(cāng)庫(kù)里的login.username。
然后兩個(gè)input上都加上了change事件處理。當(dāng)change事件被觸發(fā)時(shí),通過this.props.dispatch函數(shù)就可以通知倉(cāng)庫(kù)有動(dòng)作發(fā)生了, 倉(cāng)庫(kù)此時(shí)就會(huì)調(diào)用所有的reducer來應(yīng)對(duì)這個(gè)事件。
好了,到這里小小的雙向綁定功能實(shí)現(xiàn)了😓試試吧。
在MVVM框架里只需要建立一個(gè)視圖模型,用一對(duì)雙大括號(hào)就能完成的事情,到react加redux里面為何如此大費(fèi)周折?
其實(shí)我是專門在展示完整的redux+react開發(fā)流程。如果只是要單個(gè)頁面上的這點(diǎn)功能,用事件處理來改變組件的state就行了。 那么redux為什么要引入這么個(gè)流程?我在開發(fā)中覺得有這么幾個(gè)特點(diǎn):從直觀上看在視野不一樣。還是跟MVVM比吧, MVVM框架的視野在于局部,而redux的視野在于全局。MVVM對(duì)一個(gè)controller對(duì)應(yīng)一個(gè)模型,模型里的數(shù)據(jù)只能自己用, 模型之間通信需要其它的數(shù)據(jù)傳遞方式。redux(或者說是flux的模式)管理著一個(gè)大數(shù)據(jù)倉(cāng)庫(kù), 任何時(shí)候都可以從這個(gè)倉(cāng)庫(kù)中取到一切細(xì)節(jié)的狀態(tài)(有沒有云的感覺?),當(dāng)開發(fā)單頁應(yīng)用的時(shí)候,這一優(yōu)勢(shì)會(huì)特別明顯。 從編程語言角度上看,redux+react方式充分利用了函數(shù)式編程的優(yōu)勢(shì)。redux(flux)強(qiáng)調(diào)單向數(shù)據(jù)流, 單向數(shù)據(jù)流就像生產(chǎn)流水線,原料被各個(gè)工序依次加工,最終成為產(chǎn)品,而在這個(gè)過程中要避免外界因素對(duì)各個(gè)階段的原料產(chǎn)生影響, 否則就會(huì)出現(xiàn)非預(yù)期的產(chǎn)品(次品)。純函數(shù)就像這個(gè)流水線中的工序,讓數(shù)據(jù)處理的過程簡(jiǎn)單明了。 發(fā)現(xiàn)了嗎?前面的代碼中純函數(shù)是主力。reducer很明顯是純函數(shù)。組件也是純函數(shù),注意,我們的組件并沒有直接被狀態(tài)控制, 而是有個(gè)connect的過程,狀態(tài)是被映射成組件的屬性的,對(duì)于組件來說,根本不知道狀態(tài)為何物。 這樣我們的組件、reducer都非常獨(dú)立,非常容易測(cè)試,意義也非常直白。
吹噓了這么多,靠目前這點(diǎn)簡(jiǎn)單的代碼也不容易看出來。畢竟這些代碼還沒啥實(shí)際意義,作為一個(gè)現(xiàn)代的前端應(yīng)用,連異步都沒有。。。
那么下一節(jié),我們就加點(diǎn)異步進(jìn)來。