后臺小白前端入門--React+React-Router+Redux

導語:

其實demo早在四五月份就已經寫好了,只是那段時間后臺項目太忙,加上自己拖延癥晚期,到最近才寫這篇文章。文章結構大概從介紹本文使用的前端技術,使用的原因出發,同時結合代碼進行使用講解,最后在配上如何基于本文demo進行進一步開發,盡量做到淺顯易懂,延續系列的特點。這篇文章應該是我近期內寫的最后一篇前端文章。寫完這篇文章,也算是從React系前端勉強畢業了,同時也是給自己這一年來前端學習的一個交代。文章有點長,大家慢慢看。也可以直接到最底下獲取我的github地址,直接下載demo跑起來,只需要一個npm start,童嫂無欺。
React

框架概念介紹

所使用的相關前端技術框架:React+React-Router+Redux+webpack2.0,ui框架用的是Ant Design。異步請求用的是fetch api。

1.React:這個就不用介紹了吧,這個還用介紹,估計這篇文章不太適合你。這個不熟的建議先學習一下,可以從我之前寫的后臺小白前端入門--React看起。

2.Reac-Router:使用React-Router,可以讓你輕松使用React進行多頁面開發。React—Router是React官方的貌似也是唯一的React路由解決方案。簡單來說,就是url改變,但是不經過后臺,在前端進行路由,根據url的不同選擇并渲染不同的路由組件。React-Router其實沒有多少東西,就一個路由容器,還有一些api,現在也出中文文檔了,可以跟著官方文檔的demo敲一遍就好了。

3.Redux:個人認為這是技術棧里面比較難學的一個。不僅你要學習Redux是什么,也要學習如何和React搭配使用。簡而言之,Redux就是在瀏覽器內存中弄了一個容器,用于存放及管理js狀態的地方。然后通過一系列的規范和限制,讓你能清晰地管理這些狀態,同時用這些狀態去改變頁面。

使用React做過復雜頁面的人可能會深有體會,當一個嵌套了三四層的組件,最底層的子組件想要改變父組件的狀態,使得父組件通過這個狀態去改變另外一個最底層的子組件的時候,不使用消息訂閱方式的時候,無非就是寫一個回調一直傳到子組件供其調用,來改變這個父組件的狀態(其實就是父組件的state),然后父組件又把這個狀態一層層傳到另外一個子組件,然后另外一個子組件通過props值獲取到狀態的改變,來渲染頁面。這種代碼寫起來非常地惡心,一多起來,基本沒辦法閱讀和維護。

所以這個時候,你就需要在一個地方去統一管理這些狀態了,讓所有的組件都去這個地方獲取狀態的變化,或者改變狀態。這個就是Redux做的事情。React最初起步的時候,由官方推出的Flux也是做著類似的事情。不過后來Redux的出現,以更加清晰的store機制和使用方式,取代了Flux成為了主流。我自己也試著寫過Flux的代碼,寫起來Redux會更簡潔一些,所以這里就選擇使用Redux。

Redux學習的話,可以先從Redux中文文檔開始看起,大概了解一下Redux的相關機制。如果英文好一下的話,可以去看一個英文的Redux使用視頻,大概20多個,照著敲下來,應該也差不多。當然,本文后續也會介紹Redux結合React,React-Router使用。

4.框架作用小總結:為什么寫個React還要加著么多東西呢?簡單來說,就是你在最初使用React開發頁面的時候,你發現你的頁面只有一個url(單頁),沒辦法做到通過url到達不同的頁面,然后你使用React-Router,就可以使用React進行“多頁面”開發。然后你的網站越來越復雜了,抽出來的公共組件越來越多,頁面組件嵌套越來越深,狀態(父組件于子組件之間的state)之間的關系影響越來越復雜。這時候你就需要使用Redux來統一管理這些狀態了。

上面提到的相關資料站,都可以從本文最底下獲取到相關的鏈接,大家按需獲取。本demo的相關代碼放到了github里面,鏈接也放到底下了。

框架使用及說明

下面將介紹框架代碼的結構和使用。先附上目錄結構,介紹各個文件夾存放的文件及文件的結構:

front
|---actions(使用Redux開發,對應的action都放到這個目錄下,稍后詳解)
|---build(webpack打包編譯后生成的js文件,實際html引用的也是這里的文件)
|---common(一些公共的變量和api都放在這個文件夾下)
|---css
|---data(node服務器mock后臺數據的文件放在這)
|---jsx(組件代碼文件都放在這)
|---node_modules
|---reducers(使用Redux開發,對應的action都放到這個目錄下,稍后詳解)
|---store(使用Redux開發,store文件存放路徑)
|---index.html(主頁面)
|---package.json(npm)
|---proxy.config.js(node服務器請求接口映射配置文件)
|---webpack.config.js

建議大家把demo下下來直接跑起來,先安裝好node和webpack,然后在front目錄下,使用npm install安裝以來,再使用webpack打包,最后使用npm start啟動node服務器,然后瀏覽器訪問http://localhost:8000/即可。demo比較簡單,只做了登陸頁,注冊頁和主頁。不過也夠用了。不知道node和webpack用法的可以先在我的上一篇博客搭個簡單例子,你就會有所了解了。后臺小白前端入門--React前端框架搭建Demo及介紹。

啟動后,你就能看到這樣的頁面:


image.png

點擊右上角的名字,退出,可以跳到登陸頁,登陸頁點注冊,可到注冊頁。

關鍵代碼詳解

1.jsx/index.jsx

//省略引入第三方庫的代碼。
import store, {history} from '../store/store.jsx';
class RouterComponent extends React.Component {
    render() {
        return (
            <Provider store={store}>
                <Router history={history}>
                    <Route path={env.homePage} component={ContainerComponent}>
                        <IndexRoute component={MainComponent}/>
                        <Route path={env.login } component={LoginComponent}/>
                        <Route path={env.register } component={RegisterComponent}/>
                    </Route>
                </Router>
            </Provider>
        );
    }
}

前面說到,我們已經將state交給了Redux進行管理,放在了store里面。那么如何在負責渲染頁面的組件里面獲取到這些state呢?在React-Redux中,提供了Provider組件,把它作為根組件,同時把store作為參數傳給它,你就能在嵌套在它下面的組件中拿到state。

接下來就是Router組件了,Router組件使用起來和不用Redux一樣,這里就不用詳細介紹。不過我們可以把history和store做個綁定,下面有詳細介紹。

2.store/store.jsx

//省略引入第三方庫的代碼。
import * as reducers from '../reducers/rootreducer.jsx';

const rootReducer = combineReducers({
    ...reducers,
    routing: RouterReducer
});

const store = createStore(rootReducer, applyMiddleware(thunk));

export const history = syncHistoryWithStore(browserHistory, store);

export default store;

上面的combineReducers,用于將多個負責改變不同的state的reducer合并成同一個reducer函數。這樣你就可以分別根據業務情況編寫不同的reducer了,代碼結構清晰。

接著是syncHistoryWithStore,用于將History中的變化反映到store里的state中。這里我自己也不是很清楚,demo里面應該是沒有用到對應的例子,因為我把這個代碼換成export const history = browserHistory; 編譯運行都正常,有興趣的話可以做個深入研究。

3.jsx/component/MainComponent.jsx

接下來,寫完上面那兩個配置文件。React+React-Router+Redux的框架就簡單的搭建起來了?,F在開始寫一些比較簡單的頁面組件。下面主頁的代碼比較簡單,相信大家看懂應該不費吹灰之力。如果我們沒有用到Redux,其實就是把下面的loadData改成異步請求,同創建組件的相關state,把請求回來的參數傳給這個內部的state,通過state組件的更新來更新頁面。代碼如下:

//以上略去了使用Redux需要聲明和綁定和函數,后面會給出。
export default class MainComponent extends React.Component {
    constructor(props) {
        super(props);

    }

    loadData() {
        //這里是Redux的寫法,如果是不使用Redux,則直接寫異步請求并且將返回的數據setState到本組件的state中。
        const {actions} = this.props;
        actions.fetchArticleList();
    }


    componentDidMount() {
        this.loadData();
    }

    render() {
        var articleListHtml = [];
        //這里是Redux的寫法,如果是不使用Redux,則直接從this.state中獲得數據。
        const {articleList} = this.props;
        if (common.isNotEmpty(articleList) && articleList.length > 0) {
            for (var i = 0; i < articleList.length; i++) {
                articleListHtml.push(
                    <tr key={i}>
                        <td className="td-align-center" style={{width:"50%"}}>
                            <a href="" target="_blank">{articleList[i].title}</a>
                        </td>
                        <td className="td-align-center" style={{width:"25%"}}>{articleList[i].creator}</td>
                        <td className="td-align-center"
                            style={{width:"25%"}}>{articleList[i].commentNum}/{articleList[i].readedNum}</td>
                    </tr>
                )
            }
        }
        return (
            <div>
                <Row type="flex" justify="center">
                    <h1>博文列表</h1>
                </Row>
                <Row type="flex" className="add-code-lib-row standard-margin-top">
                    <Col span={24}>
                        <div className="code-manage-main">
                            <table>
                                <thead>
                                <tr>
                                    <th className="td-align-center">標題</th>
                                    <th className="td-align-center">作者</th>
                                    <th className="td-align-center">回復/查看</th>
                                </tr>
                                </thead>
                                <tbody>
                                {articleListHtml}
                                </tbody>
                            </table>
                        </div>
                    </Col>
                </Row>
            </div>
        );
    }
}

--.Redux怎么用,用的時候該注意些什么
寫完UI組件,這里有必要花一些篇幅簡單介紹一下Redux的一些機制和原則,以便于實在不想看Redux是什么以及怎么用的讀者理解接下來的代碼。

前面有說到,Redux就是用來管理狀態(state)的,不過它自己有一套自己的管理機制和規范,大概的示意圖如下:

image.png

(1)UI組件通過connet,綁定action和state(即圖中的1和2),使得組件可以dispatch action,同時也能在props里面獲取到store里對應的state。

(2)組件生成action,并且進行dispatch(即圖中的3),這里主要為了調用異步請求,獲取服務端數據。

(3)請求成功或者失敗后,會再在3的Action里,dispatch一個Action(圖中的4),這個action,才是用來通知reducer,讓其去改變store中對應的狀態的。

(4)所有的Reducer都會收到該請求(圖中的5),如果符合條件的reducer會更改store中的state,并且返回新的state到store中,如果不符合,則直接返回舊的state,不做任何更改。

上面大概就是使用Redux的大致開發流程及數據流向了。同時Redux規定了3條原則,來保證store中狀態改變和使用足夠清晰。1.只有一個store,所有的狀態都存在于這個store中。2.通過action來觸發store中狀態的改變,reducer執行具體的state變化。其它任何時間任何方式的state是只讀的。3.reducer函數必須是一個純函數,即對應于任何兩個相同的輸入,只會有一種相同的輸出。舉個例子,你在這個純函數里面,Redux是不允許你使用諸如Math.random()或者new Date()等函數來改變state的。這樣做是為了保證狀態變化的可預測性和清晰性。

通過上面一系列原理和原則,Redux讓前端開發者和維護者對前端的代碼和邏輯更佳清楚易懂,對于狀態變化清晰可預測。

4.reducers/articleList.jsx

import * as constants from '../common/constants.jsx'

const initialState = {};

export default function (state = initialState, action) {
    switch (action.type) {
        case constants.RECEIVE_ARTICLELIST:
            const {articleList} = action;
            return {
                ...state,
                articleList
            };
            break;
        default:
            return state
    }
}


//同時在common/constants.jsx中添加如下內容,因為action和reducer都會用到,所以放倒一個公共變量里面比較好。編程習慣編程習慣
export const RECEIVE_ARTICLELIST = 'RECEIVE_ARTICLELIST';

//在reducers/rootreducer.jsx中添加如下代碼,使得上面寫的reducer能綁定到store中。
export articleList from './articleList.jsx'


Redux部分的代碼講解將按照Redux流程從后到前講解,即從reducer,action,再到前端UI component 的順序,方便大家更加清楚地了解如何進行Redux開發。

上面部分的代碼就是寫一個負責改變文章列表狀態的reducer,當有一個type為'RECEIVE_ARTICLELIST'的action被分派的時候,這個reducer會進行響應,同時修改狀態里的articleList,重新計算并返回新的state到store中。需要說明的是,當一個action被分派的時候,所有綁定的reducer都會被觸發執行。如果符合則更新狀態,如果沒有則直接返回原state(即default:return state),store里的狀態就不會變化。

我們寫完了這個reducer,就要把它綁定到store中,前面我們已經通過如下代碼進行配置了,所以只需要在reducers/rootreducer.jsx 添加即可綁定到store中。


import * as reducers from '../reducers/rootreducer.jsx';
const rootReducer = combineReducers({
    ...reducers,
    routing: RouterReducer
});

const store = createStore(rootReducer, applyMiddleware(thunk));

5.actions/receiveArticleList.jsx

接著往前寫,寫到Action,這里分派到Reducer的Action,是一個對象,其中有一個type屬性,是必須的,用來和reducer里面接受參數里面的action.type是一致的,也就是constants.jsx中的RECEIVE_ARTICLELIST = 'RECEIVE_ARTICLELIST'。另外你可以在這個對象里面添加一些其它的參數,以便于reducer匹配到以后拿這些參數去改變store中的狀態。如下代碼:

const constants = require('../common/constants.jsx');

export function receiveArticleList(articleList) {
    return {
        type: constants.RECEIVE_ARTICLELIST,
        articleList
    }
}

6.actions/fetchArticleList.jsx

這里這個Action,其實在我看來,是一個偽“action”,因為它并沒有分派到reducer中,而是觸發另外一個Action。這里這個Action是由UI組件分派的,為了進行異步請求,在請求成功或者失敗后,再分派一個真正的Action,去改變store中的state。

在Redux中,把異步請求數據的代碼和邏輯放在Action里面是比較好的。當然你可以直接在UI組件里面直接進行異步請求,然后派發Action去改變store中的狀態。但是這么做的話,你的業務代碼和你的UI組件又耦合在一起了。代碼也不夠通用,感覺不太好。這部分的代碼如下:

import fetch from 'isomorphic-fetch'
require('es6-promise').polyfill();
import * as actions from './actions.jsx';
import env from '../common/env.js';

export function fetchArticleList() {
    return (dispatch, getState) => {
        fetch(env.getArticleList, {
            credentials: 'same-origin',
            headers: {
                "Content-Type": "application/json"
            }
            // body: formData
        }).then(function (response) {
            if (response.status >= 400) {
                throw new Error("Bad response from server");
            }
            return response.json();
        }).then(function (result) {
            var code = result.code;
            if (code == "0") {
                    //這里才是真正觸發Aciton的地方,去store的狀態。
                dispatch(actions.receiveArticleList(result.data))
            }
        }.bind(this)).catch(function (error) {
            alert("請求失敗,請檢查網絡");
            //這里才是真正觸發Aciton的地方,去store的狀態。
            dispatch(actions.receiveArticleList([]))
        });
    }
}


7.jsx/component/MainComponent.jsx

最后,又回到了我們的UI組件。我們在前面已經寫好了異步請求的Action,改變Store中狀態的Action,以及如何改變Store狀態的Reducer。

接下來,就是UI組件如何去獲取到store中的狀態,如何去直接或者間接(需要異步請求的時候)去派發一個Action,以及store中的state變化時,UI組件是如何過獲取到變化的。先貼上代碼,下面再進行代碼說明。

import env from '../../common/env.js';
import common from '../../common/common.js';
import {fetchArticleList} from '../../actions/actions.jsx';
import {connect} from 'React-Redux';

const mapDispatchToProps = dispatch => {
    return {
        actions: {
            fetchArticleList: () => dispatch(fetchArticleList())
        }
    }
};


// Map Redux state to component props
const mapStateToProps = createSelector(
    state => state.articleList.articleList,
    (articleList) => {
        return {
            articleList
        }
    }
);


@connect(mapStateToProps, mapDispatchToProps)
export default class MainComponent extends React.Component {

//這部分的代碼和  3.jsx/component/MainComponent.jsx中貼出的代碼一樣,不再給出。

}

接下里,我們一一解決上面的問題。

我們首先寫一個mapDispatchToProps函數,封裝一下需要派發的action,使得UI組件中的代碼能比較方便調用action。

其次,再寫一個mapStateToProps函數,返回想要訂閱的store的狀態(在本例子中,就是存儲在store中的articleList)。需要額外說明的是,這個state.articleList.articleList,
中間的articleList,是你在rootreducer.jsx中export的值。

最后使用React-Redux庫中的connet,將上面兩個函數綁定到UI組件上。這樣,UI組件就可以通過this.props中拿到store中的狀態值了,并且每一次store中的狀態值變化,也會觸發React組件中props的改變,觸發React生命周期的相關函數回調。同時,如果UI組件中想要觸發一次Action派發,也是在props里面進行this.props.actions.fetchArticleList();即可。上面兩個函數已經把action和store中的state都綁定到props對象里了。

至此,已經把整個Demo的關鍵代碼進行一一說明。詳細的代碼建議大家去我的github上下載下來看。這里就不提供全量代碼了。

基于demo代碼進行相關開發步驟

上面代碼已經詳細到,從零開始搭一套React大禮包了。但是如果你實在不想自己搭一套,去折騰什么依賴包問題,webpack問題,中間件問題,fetch請求兼容性問題等一大堆問題。想在本demo上直接開發,也是可以的。大概步驟如下:

1.寫好UI組件。

2.在index.jsx中配置好路由。

如果有異步請求,則還需要以下步驟:

3.在constants.jsx中設置action.type,供reducer和action使用。

4.在reducers文件夾中添加reducer。

5.在rootreducer。jsx中添加4中的文件

6.在actions文件夾中添加相關action,如果有ajax請求,建議分成fetch,receive兩個action

7.在actions。jsx中添加6中的文件

8.在對應的組件中編寫對應的邏輯。etc:@connect(mapStateToProps, mapDispatchToProps)等

總結

當然,這個demo代碼比較簡單,只是為了用React大禮包而用React大禮包,太過于復雜的代碼也不適合進行講解說明。而且,這套框架搭起來了,仍然有很多問題,很多規范需要去解決。比如什么時候把組件中的state放到store中,怎樣接著精簡代碼,去掉rootreducer.jsx、actions.jsx等等等等。都值得進行深入的學習和研究,也歡迎大家這篇文章下面評論討論,提出解決方案。

想把這套框架吃下來,合理運用起來,光是這些代碼和學習也是遠遠不夠的。還需要更多的學習和實踐,不過希望這篇文章提供一些信心,成為一個好的起點。

臨走之前,我還有話要說

其實在15年的時候就已經開始寫前端代碼了,用的是jQuery。然后到去年,開始認真研究React及其相關生態系統。感覺現在的前端,處于一個群雄割據的年代。老派的還在用jQuery,然后現在比較主流的三大框架React,Vue,Angular,都占有不小的使用比例。這對于前端學習者,我覺得已經算是不小的負擔,并且這些技術還在飛速迭代著。當然這也無可厚非,大家都想一統天下,但是,在這種急功近利的趨勢,導致很多時候,自己都沒統一自己。我在用webpack的時候,剛好經歷webpack1過渡到webpack2,然后發現webpack2竟然不兼容webpack1,要做代碼升級及兼容。聽前端的同時說,Angular2和Angular1,差別也很大。這一系列的變動,也導致了對應的插件和api庫跟著不兼容,需要升級。導致這個過程非常的痛苦。暫別前端,希望下次回來,前端有一番新氣象。

相關技術資料站

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容