redux,一種頁面狀態管理的優雅方案
前端是工程能力比技術能力更重要的領域,而最近一兩年,前端在構建流程、元件化、同構渲染等方面有了深入的發展,其入行門檻也在逐步提高。
前端社群的活躍程度讓人驚歎,各種工具層出不窮,比如grunt、gulp、webpack、fis等,即便你沒有全部用過,也該瞭解過它們中的大部分,這些工具極大的解放了前端的生產力,解決了前端的構建流程問題。而React的出現將前端引入新的境界,它優雅的解決了前端UI層元件化的問題,使得元件化也成了前端專案的標配。為了提高頁面渲染速度,頁面首屏由後端直出,也已經有了很多解決方案。這裡我們探討一下容易被大家忽略的領域:頁面狀態的管理。
1. 頁面狀態
頁面上所有UI層的顯示都可以用對應的狀態描述,比如,比如當前的列表項、當前被選中的標籤等。如下圖所示:
可以簡單的將前端專案抽象為對UI的管理和對狀態的管理,UI和狀態之間相互作用,處理它們之間的相互關係很複雜,行業內有不同的解決方案,比如以angularJS為代表的雙向繫結、以及flux提出的單向資料流。本文我們將抽象的理解facebook提出單項資料流方案flux,以及它的具體實現redux。
2. 單向資料流flux
flux是facebook提出的一種應用程式框架,其基本架構如下入所示,其核心理念是單向資料流,它完善了React對應用狀態的管理。
上圖描述了頁面的啟動和執行原理:
1.通過dispatcher派發action,並利用store中的action處理邏輯更新狀態和view
2.而view也可以觸發新的action,從而進入新的步驟1
其中的action
是用於描述動作的簡單物件,通常通過使用者對view的操作產生,包括動作型別和動作所攜帶的所需引數,比如描述刪除列表項的action:
{
type: types.DELETE_ITEM,
id: id
};
而dispatcher
用於對action
進行分發,分發的目標就是註冊在store
裡的事件處理函式:
dispatcher.register(function (action) {
switch(action.type) {
case 'DELETE_ITEM':
sotre.deleteItem(action.id); //更新狀態
store.emitItemDeleted(); //通知檢視更新
break;
default:
// no op
}
})
store
包含了應用的所有狀態和邏輯,它有點像傳統的MVC模型中的model層,但又與之有明顯的區別,store
包括的是一個應用特定功能的全部狀態和邏輯,它代表了應用的整個邏輯層;而不是像Model一樣包含的是資料庫中的一些記錄和與之對應的邏輯。
3. 一種對flux的實現,redux
隨著前端應用的複雜性指數級的提升,前端頁面需要管理的狀態也越來越多,flux給出了管理狀態的基本資料流,而redux對flux就是對它最好的實現之一,而且其對flux的理念進行了更進一步的擴充套件。
redux倡導三大原則:
1.一個物件儲存整個應用的狀態
2.狀態物件是隻讀的,只能通過action觸發改變
3.通過普通函式處理action的邏輯
其基本流程如下:
其相對於flux有如下不同之處:
1. redux沒有dispacher,其通過普通函式處理action邏輯,並改變應用狀態
2. redux的狀態物件是immutable的,每一個action都會區域性地建立新的狀態物件
需要強調的是,redux不一定要和react搭配,它是一種應用狀態管理方案,不涉及UI層
,你可以任意選擇自己的UI層;正因為redux脫離UI層,提供了整個應用狀態的管理,使得我們的開發流程有了顛覆性的改變。
我們可以在UI層ready之前,完成應用的邏輯設計和實現。比如我們將應用的邏輯設計如下:
ADD_ITEM的action觸發todos列表狀態的改變,SET_FILTER的action觸發filter狀態的改變。因為每一個action都是簡單物件,我們可以輕易的模擬。這也就使得我們可以在UI層ready之前,對前端全部邏輯寫單獨的測試用例。
然後,在UI層ready之後,將UI層或網路層的事件對映為redux的action。比如將表單提交事件對映為ADD_ITEM,將標籤頁切換按鈕點選事件對映為SET_FILTER。UI層和邏輯層相互獨立,並僅僅通過事件與action的對映來建立聯絡,這種方案使得複雜的前端專案有了更清晰的架構。
4.redux與react的配合
redux只負責應用的邏輯層,而通過使用react-redux
模組,其可以天衣無縫的和react配合。
4.1 經典案例
我們簡單瞭解一下,如何使用react+redux實現經典的todolist案例。案例的原始碼在此。最終的介面如下:
(1) 邏輯層設計
前端應用本質上是:通過事件觸發應用狀態的改變。而redux包辦了應用狀態(state)、事件(action)、和事件處理函式(reducer),使得我們可以拋開UI層,先設計應用的邏輯層。redux使用單一物件儲存整個應用的狀態,todolist應用的狀態(state)樹如下:
{
filter: 'show_all'
todos: [
{
id: 1,
text: 'todo1',
marked: true
}
]
}
其中filter為列表過濾策略,用於標誌底部三個按鈕選中的狀態,而todos作為代辦事項列表。上文提到過state是immutable的,只能通過action觸發對state的改變,一個編輯todo條目的action如下:
var editTodo = function (id, text) {
return {
type: types.EDIT_TODO,
id: id,
text: text
};
}
相應的,redux提倡用使用簡單函式(又稱作reducer)處理action,如下為EDIT_TODO的處理邏輯:
module.todo = function(state, action) {
state = state || [];
switch (action.type) {
case types.EDIT_TODO:
return state.map(function(todo) {
return todo.id === action.id ?
assign({}, todo, { text: action.text }) :
todo
});
default:
return state;
}
}
每一個action處理函式都是將action作用在old state上,從而產生new state。state、action、reducer太零散,通過createStore可以將它統一在store中,而store則代表了整個應用的邏輯。
var store = createStore(reducer);
(2) UI層設計
本案例採用react作為UI層的元件化方案,在這裡不再詳述。需要注意的是為了配合redux,在寫react元件時,我們需要對元件類別進行劃分,將其劃分為展示型元件和容器型元件。redux只需要和容器型元件通訊,而不用管理展示型元件。用一個表格可以很好的說明它們和區別:
(3) 組合邏輯層與UI層
上文提到,redux只關注react的容器型元件,而且容器型元件可以由react-redux動態生成,以防止state注入容器型元件時的硬編碼。比如我們使用如下程式碼將應用狀態注入一個容器型元件TodoApp中:
var App = React.createClass({
render: function() {
return (
<Provider store={store}>
{function() { return <TodoApp />; }}
</Provider>
);
}
});
module.exports = App;
上述程式碼通過Provider將store注入容器型元件TodoApp中。在TodoApp元件中,我們可以通過this.props
來獲取store中儲存的應用狀態了:
var TodoApp = React.createClass({
render: function () {
var todos = this.props.todos;
var filter = this.props.filter;
return (
<div>
......
</div>
);
},
});
function mapStateToProps(state) {
return state;
}
module.exports = connect(mapStateToProps)(TodoApp);
其中connect函式用於動態的建立容器型元件。藉助於Provider和容器型元件,我們就將應用的邏輯層和UI層組合在一起了。
4.2 高階特性
瞭解了react和redux結合的基本思路以後,讓我們一起看一看redux的高階特性。
(1) 狀態樹分治
redux提倡用一個物件儲存整個應用的狀態,而複雜應用的狀態物件是很大的,這樣會不會有效能問題?各個容器型元件都對整個應用狀態物件進行操作,會不會引起混亂?對此redux有充分的考慮。首選在邏輯層設計時,我們就應該充分的考慮到狀態樹的分治,比如在設計action的處理函式(reducer)時,針對狀態樹的不同部分,將其對應的actions處理函式儲存在不同的檔案中,redux通過combineReducers對此提供了支援。比如
var todos = require('../reducers/todos');
var filter = require('../reducers/filter');
combineReducers({filter: filter, todos: todos});
其次,在UI層我們也可以很方便的只將部分狀態樹注入某個容器型元件,redux在使用connect生成容器型元件時,接收一個函式(mapStateToProps)作為引數,該函式可以只返回整個狀態樹的部分狀態,因此,connect生成的容器型元件也就只能感知到部分狀態樹。這種方式,避免了應用狀態樹過大的混亂,通過分治降低了複雜度。如下程式碼,建立了一個只關注整個狀態樹中state.todos的容器型元件:
var TodoApp = React.createClass({
render: function () {
var todos = this.props.todos;
return (
<div>
......
</div>
);
},
});
function mapStateToProps(state) {
return state.todos;
}
module.exports = connect(mapStateToProps)(TodoApp);
(2) 非同步action
一般來說,非同步action並不能算是高階特性,因為它太常見了。比如傳送一個網路請求,這是再尋常不過的需求了。只是用redux觸發非同步action並不是那麼直接。我們需要首先了解redux的中介軟體概念,它可以用於在action被觸發和action到達處理函式reducer之前,對action進行處理。
可以在建立store時,通過applyMiddleware函式提供redux的中介軟體:
createStore( todosApp,applyMiddleware(someMiddleWare))
一個典型的redux中介軟體是redux-logger,它在控制檯中記錄每一次action作用前後的應用狀態變化,非常適合在開發階段進行除錯。
var fetchTodos = function () {
return function (dispatch) {
return fetch('/todos');
}
}
我們可以使用dispach函式像派發普通action一樣,派發非同步函式,非同步函式的返回值還可以是Promise,其返回值會透傳過dispch函式。
dispach(fetchTodos)
.then(function(json){
//handle response
})
.catch(function(error){
//handle error
});
通過網路載入資料,並在資料到達時更新應用狀態是一種比較常見的應用場景,對於這種場景,一種最優雅的方案:
1. 派發非同步函式,用於進行網路請求
2. 在網路請求完成時,派發同步action用於更新應用狀態
可以用如下程式碼表示:
var fetchTodos = function () {
return function (dispatch) {
return fetch('/todos')
.then(function (json) {
//派發同步aciton,用於更新應用狀態,初始化todo列表
dispatch(initTodos(json.data || []));
}).catch(function () {
//派發同步action,用於更新應用狀態,設定載入失敗標誌
dispatch(failLoadedTodos());
});
}
}
(3) 同構渲染
前後端同構,應用首屏由後端直出是近年來比較流行的效能優化方案,redux對此也有完善的支援。基本流程是:
1. 服務端初始化state
2. 將服務端state傳遞到應用的頁面端
3. 頁面端用服務端傳遞的狀態初始化應用state
在遵從這個基本流程的情況下,服務端和頁面端的使用方法開發方法基本一致,如下是服務端程式碼:
store.dispatch(todoActions.loadInitTodos()).then(function () {
var contentHtml = React.renderToString(
<Provider store={store}>
{function () {
return <TodoApp />;
}}
</Provider>
);
var initialState = JSON.stringify(store.getState());
res.render('index.ejs', {contentHtml: contentHtml, initialState: initialState});
}).catch(function(error){
res.json({errMsg: 'internal error'})
});
上述服務端程式碼通過派發初始化非同步函式更新應用狀態,該非同步函式返回一個Promise,Promise物件會透傳過dispach函式。在Promise處理完成後,我們得到應用的最新狀態。最後我們將由React輸出的HTML字串contentHtml和初始化應用狀態initialState,傳遞到模板檔案index.ejs中,模板檔案如下:
<html>
<head>
<title>Redux TodoMVC</title>
</head>
<body>
<div class="todoapp" id="root"><%-contentHtml%></div>
</body>
<script>
window.__INITIAL_STATE__ = <%-initialState%>;
</script>
</html>
通過瀏覽器的window物件,我們將服務端的初始狀態傳遞到了頁面端。
var todosApp = combineReducers({filter: filter, todos: todos});
var store = createStore( todosApp,
window.__INITIAL_STATE__,
applyMiddleware(thunkMiddleware, reduxLogger())
);
本文同時發表在我的部落格積木村の研究所 :http://foio.github.io/redux-state-manage/
本文案例原始碼: https://github.com/foio/react-redux-isomorphic-todolist
參考文獻:
https://facebook.github.io/flux/docs/overview.html
相關文章
- 借鑑redux,實現一個react狀態管理方案ReduxReact
- Vue 頁面狀態保持頁面間資料傳輸的一種方法Vue
- React中的另一種狀態管理方案ValtioReact
- react之redux狀態管理ReactRedux
- Android 頁面多狀態佈局管理Android
- 前端狀態管理框架之Redux前端框架Redux
- vue從其他頁面返回保持上一頁的狀態Vue
- Redux複雜應用(一):淺談狀態管理Redux
- React 快速上手 - 08 redux 狀態管理 react-reduxReactRedux
- nginx狀態資訊頁面Nginx
- Flutter 狀態管理之 Scoped Model & ReduxFlutterRedux
- Flutter | 狀態管理探索篇——Redux(二)FlutterRedux
- 一種更優雅的Flutter Dialog解決方案Flutter
- 一個超高自定義度又簡單使用的頁面狀態管理庫
- 基於 Redux + Redux Persist 進行狀態管理的 Flutter 應用示例ReduxFlutter
- React 4 種狀態型別及 N 種狀態管理React型別
- JavaScript 的狀態容器 ReduxJavaScriptRedux
- Flutter狀態管理學習手冊(二)——ReduxFlutterRedux
- 從 Redux 說起,到手寫,再到狀態管理Redux
- DiscuzQ動態頁面SEO方案
- 優雅的redux非同步中介軟體 redux-effectRedux非同步
- Flutter 狀態管理之 Redux,BLoC,Provider 的流程分析FlutterReduxBloCIDE
- 系統狀態列和app頁面一體化APP
- 【Flutter】 介紹一種通用的頁面路由設計方案Flutter路由
- 頁面狀態改變會觸發的一些事件事件
- React資料狀態管理 --- Redux,Redux-Saga以及進階DvaReactRedux
- React hooks 狀態管理方案解析ReactHook
- [譯]開發類 redux 庫來理解狀態管理Redux
- React如何優雅地寫單頁面應用?React
- 使用列舉實現狀態機來優雅你的狀態變更邏輯
- Redux 進階 — 優雅的處理 async actionRedux
- 優雅的在React專案中使用ReduxReactRedux
- Redux 進階 -- 優雅的處理 async actionRedux
- 微信小程式專案重構之Redux狀態管理微信小程式Redux
- 前端 | Vue 路由返回恢復頁面狀態前端Vue路由
- OpenHarmony頁面級UI狀態儲存:LocalStorageUI
- React通過redux快取列表資料以及滑動位置,回退時恢復頁面狀態ReactRedux快取
- Redux/Mobx/Akita/Vuex對比 - 選擇更適合低程式碼場景的狀態管理方案ReduxVue