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