總括: 本文采用react+redux+react-router+less+es6+webpack,以實現一個簡易備忘錄(todolist)爲例儘量全面的講述使用react全家桶實現一個完整應用的過程。javascript
代碼地址:React全家桶實現一個簡易備忘錄html
原文博客地址:React全家桶實現一個簡易備忘錄前端
博主博客地址:Damonare的我的博客node
人生不失意,焉能暴己知。react
技術架構:本備忘錄使用react+react-router+redux+less+ES6+webpack實現;webpack
頁面UI參照:TodoList官網實現;git
在線演示地址:Damonare的備忘錄;es6
毫無疑問,當談到React
的時候不能避免的會提到組件化思想。React剛開始想解決的問題只是UI這一層面的問題,也就是MVC中view層面的問題,不成想現在越滾越大,從最先的UI引擎變成了一整套先後端通吃的 Web App 解決方案。對於React
組件的理解一樣要站在view層面的角度出發,一個完整的頁面是由大大小小的組件堆疊而成,就好像搭積木,每一塊積木都是一個組件,組件套組件組成了用戶所能看到的完整的頁面。github
使用React
,不必定非要使用JSX
語法,可使用原生的JS進行開發。可是React
做者強烈建議咱們使用JSX
,由於JSX
在定義相似HTML這種樹形結構時,十分的簡單明瞭。這裏簡單的講下JSX
的由來。
好比,下面一個div元素,咱們用HTML語法描述爲:
<div class="test">
<span>Test</span>
</div>複製代碼
若是換作使用javascript描述這個元素呢?最好的方式能夠簡單的轉化爲json
對象,以下:
{
type:"div",
props:{
className:"test",
children:{
type:"span",
props:{
children:"Test"
}
}
}
}複製代碼
這樣咱們就能夠在javascript中建立一個Virtual DOM
(虛擬DOM)了。固然,這樣是無法複用的,咱們再把它封裝一下:
const Div=>({text}){
return {
type:"div",
props:{
className:"test",
children:{
type:"span",
props:{
children: text,
},
},
},
}
}複製代碼
接下來再實現這個div就能夠直接調用Div('Test')來建立。但上述結構看起來實在讓人不爽,寫起來也很容易寫混,一旦結構複雜了,很容易讓人找不着北,因而JSX
語法應運而生。咱們用寫HTML的方式寫這段代碼,再通過翻譯器轉換成javascript後交給瀏覽器執行。上述代碼用JSX
重寫:
const Div =()=>(
<div className="test"> <span>Test</span> </div>
);複製代碼
多麼簡單明瞭!!!具體的JSX語法
很少說了,學習更多戳這:JSX in Depth
其實上面已經提到了Virtual DOM
,它的存在也是React
長久不衰的緣由之一,虛擬DOM的概念並非FB獨創卻在FB的手上大火了起來(後臺是多麼重要)。
咱們知道真實的頁面對應了一個DOM樹,在傳統頁面的開發模式中,每次須要更新頁面時,都須要對DOM進行更新,DOM操做十分昂貴,爲減小對於真實DOM的操做,誕生了Virtual DOM
的概念,也就是用javascript把真實的DOM樹描述了一遍,使用的也就是咱們剛剛說過的JSX
語法。對好比下:
每次數據更新以後,從新計算Virtual DOM
,並和上一次的Virtual DOM
對比,對發生的變化進行批量更新。React也提供了shouldComponentUpdate
生命週期回調,來減小數據變化後沒必要要的Virtual DOM
對比過程,提高了性能。
Virtual DOM
雖然渲染方式比傳統的DOM操做要好一些,但並不明顯,由於對比DOM節點也是須要計算的,最大的好處在於能夠很方便的和其它平臺集成,好比react-native
就是基於Virtual DOM
渲染出原生控件。具體渲染出的是Web DOM
仍是Android
控件或是iOS
控件就由平臺決定了。因此咱們說react
的出現是一場革命,一次對於native app
的宣戰,就像react-native
那句口號——Learn Once,Write Anywhere.
過去編程方式主要是以命令式編程爲主,什麼意思呢?簡單說電腦的思惟方式和咱們人類的思考方式是不同的。咱們人類的大腦擅長的是分析問題,提出一個解決問題的方案,電腦則是生硬的執行指令,命令式編程就像是給電腦下達命令,讓電腦去執行同樣,如今主要的編程語言(好比:Java,C,C++等)都是由命令式編程構建起來的。
而函數式編程就不同了,這是模仿咱們人類的思惟方式發明出來的。例如:操做某個數組的每個元素而後返回一個新數組,若是是計算機的思考方式,會這樣想:建立一個新數組=>遍歷舊數組=>給新數組賦值。若是是人類的思考方式,會這樣想:建立一個數組方法,做用在舊數組上,返回新數組。這樣此方法能夠被重複利用。而這就是函數式編程了。
在React中,數據的流動是單向的,即從父節點傳遞到子節點。也所以組件是簡單的,他們只須要從父組件獲取props渲染便可。若是頂層的props改變了,React會遞歸的向下遍歷整個組件樹,從新渲染全部使用這個屬性的組件。那麼父組件如何獲取子組件數據呢?很簡單,經過回調就能夠了,父組件定義某個方法供給子組件調用,子組件調用方法傳遞給父組件數據,Over。
這東西我以爲沒啥難度,官方例子都很不錯,跟着官方例子來一遍基本就明白究竟是個啥玩意了,官方例子:react-router-tutorial。
完事之後能夠再看一下阮一峯老師的教程,主要是對一些API的講解:React Router 使用教程。
還有啥不明白的歡迎評論留言共同探討。
隨着 JavaScript 單頁應用開發日趨複雜,JavaScript 須要管理比任什麼時候候都要多的 state (狀態)。 這些 state 可能包括服務器響應、緩存數據、本地生成還沒有持久化到服務器的數據,也包括 UI 狀態,如激活的路由,被選中的標籤,是否顯示加載動效或者分頁器等等。若是一個 model 的變化會引發另外一個 model 變化,那麼當 view 變化時,就可能引發對應 model 以及另外一個 model 的變化,依次地,可能會引發另外一個 view 的變化。亂!
這時候Redux
就強勢登場了,如今你能夠把React
的model看做是一個個的子民,每個子民都有本身的一個狀態,紛紛擾擾,各自維護着本身狀態,我行我素,那哪行啊!太亂了,咱們須要一個King來領導你們,咱們就能夠把Redux
看做是這個King。網羅全部的組件組成一個國家,掌控着一切子民的狀態!防止有人叛亂生事!
這個時候就把組件分紅了兩種:容器組件(King或是路由)和展現組件(子民)。
redux
或是router
,起到了維護狀態,出發action的做用,其實就是King高高在上下達指令。props
傳給他,全部操做經過回調完成。展現組件 | 容器組件 | |
---|---|---|
做用 | 描述如何展示(骨架、樣式) | 描述如何運行(數據獲取、狀態更新) |
直接使用 Redux | 否 | 是 |
數據來源 | props | 監聽 Redux state |
數據修改 | 從 props 調用回調函數 | 向 Redux 派發 actions |
調用方式 | 手動 | 一般由 React Redux 生成 |
Redux三大部分:store
,action
,reducer
。至關於King的直系下屬。
那麼也能夠看出Redux
只是一個狀態管理方案,徹底能夠單獨拿出來使用,這個King不只僅能夠是React的,去Angular,Ember那裏也是能夠作King的。在React中維繫King和組件關係的庫叫作react-redux
。
, 它主要有提供兩個東西:Provider
和connect
,具體使用文後說明。
提供幾個Redux的學習地址:官方教程-中文版,Redux 入門教程(一):基本用法
Store 就是保存數據的地方,它其實是一個Object tree
。整個應用只能有一個 Store。這個Store能夠看作是King的首相,掌控一切子民(組件)的活動(state)。
Redux 提供createStore
這個函數,用來生成 Store。
import { createStore } from 'redux';
const store = createStore(func);複製代碼
createStore接受一個函數做爲參數,返回一個Store對象(首相誕生記)
咱們來看一下Store(首相)的職責:
getState()
方法獲取 state;dispatch(action)
方法更新 state;subscribe(listener)
註冊監聽器;subscribe(listener)
返回的函數註銷監聽器。State 的變化,會致使 View 的變化。可是,用戶接觸不到 State,只能接觸到 View。因此,State 的變化必須是 View 致使的。Action 就是 View 發出的通知,表示 State 應該要發生變化了。即store的數據變化來自於用戶操做。action就是一個通知,它能夠看做是首相下面的郵遞員,通知子民(組件)改變狀態。它是 store 數據的惟一來源。通常來講會經過 store.dispatch()
將 action 傳到 store。
Action 是一個對象。其中的type
屬性是必須的,表示 Action 的名稱。
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};複製代碼
Action建立函數
Action 建立函數 就是生成 action 的方法。「action」 和 「action 建立函數」 這兩個概念很容易混在一塊兒,使用時最好注意區分。
在 Redux 中的 action 建立函數只是簡單的返回一個 action:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}複製代碼
這樣作將使 action 建立函數更容易被移植和測試。
Action 只是描述了有事情發生了這一事實,並無指明應用如何更新 state。而這正是 reducer 要作的事情。也就是郵遞員(action)只負責通知,具體你(組件)如何去作,他不負責,這事情只能是大家村長(reducer)告訴你如何去作才能符合社會主義核心價值觀,如何作才能對建設共產主義社會有利。
專業解釋: Store 收到 Action 之後,必須給出一個新的 State,這樣 View 纔會發生變化。這種 State 的計算過程就叫作 Reducer。
Reducer 是一個函數,它接受 Action 和當前 State 做爲參數,返回一個新的 State。
const reducer = function (state, action) {
// ...
return new_state;
};複製代碼
嚴格的單向數據流是 Redux 架構的設計核心。
Redux 應用中數據的生命週期遵循下面 4 個步驟:
store.dispatch(action)
。工做流程圖以下:
這裏須要再強調一下:Redux 和 React 之間沒有關係。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。
儘管如此,Redux 仍是和 React 和 Deku 這類框架搭配起來用最好,由於這類框架容許你以 state 函數的形式來描述界面,Redux 經過 action 的形式來發起 state 變化。
Redux 默認並不包含 React 綁定庫,須要單獨安裝。
npm install --save react-redux複製代碼
固然,咱們這個實例裏是不須要的,全部須要的依賴已經在package.json裏配置好了。
React-Redux
提供connect
方法,用於從 UI 組件生成容器組件。connect
的意思,就是將這兩種組件連起來。
import { connect } from 'react-redux';
const TodoList = connect()(Memos);複製代碼
上面代碼中Memos
是個UI組件,TodoList
就是由 React-Redux 經過connect
方法自動生成的容器組件。
而只是純粹的這樣把Memos包裹起來毫無心義,完整的connect方法這樣使用:
import { connect } from 'react-redux'
const TodoList = connect(
mapStateToProps
)(Memos)複製代碼
上面代碼中,connect
方法接受兩個參數:mapStateToProps
和mapDispatchToProps
。它們定義了 UI 組件的業務邏輯。前者負責輸入邏輯,即將state
映射到 UI 組件的參數(props
),後者負責輸出邏輯,即將用戶對 UI 組件的操做映射成 Action。
這個Provider 實際上是一箇中間件,它是爲了解決讓容器組件拿到King的指令(state
對象)而存在的。
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp);
render(
<Provider store={store}> <App /> </Provider>,
document.getElementById('root')
)複製代碼
上面代碼中,Provider
在根組件外面包了一層,這樣一來,App
的全部子組件就默認均可以拿到state
了。
講解以前能夠先看一下github上的代碼,你能夠clone下來學習,也能夠在線給我提issue,歡迎戳這:React全家桶實現簡易備忘錄
.
├── app #開發目錄
| |
| ├──actions #action的文件
| |
| ├──components #展現組件
| |
| ├──containers #容器組件,主頁
| |
| ├──reducers #reducer文件
| |
| |——routes #路由文件,容器組件
| |
| |——static #靜態文件
| |
| ├──stores #store配置文件
| |
| |——main.less #路由樣式
| |
| └──main.js #入口文件
|
├── build #發佈目錄
├── node_modules #包文件夾
├── .gitignore
├── .jshintrc
├── webpack.production.config.js #生產環境配置
├── webpack.config.js #webpack配置文件
├── package.json #環境配置
└── README.md #使用說明複製代碼
接下來,咱們只關注app目錄就行了。
import React from 'react';
import ReactDOM from 'react-dom';
import {Route, IndexRoute, browserHistory, Router} from 'react-router';
import {createStore} from 'redux';
import {Provider} from 'react-redux';
import App from './container/App';
import AllMemosRoute from './routes/AllMemosRoute';
import TodoRoute from './routes/TodoRoute';
import DoingRoute from './routes/DoingRoute';
import DoneRoute from './routes/DoneRoute';
import configureStore from './stores';
import './main.less';
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/" component={App}>
<IndexRoute component={AllMemosRoute}/>
<Route path="/todo" component={TodoRoute}/>
<Route path="/doing" component={DoingRoute}/>
<Route path="/done" component={DoneRoute}/>
</Route>
</Router>
</Provider>,
document.body.appendChild(document.createElement('div')))複製代碼
這裏咱們從react-redux
中獲取到 Provider 組件,咱們把它渲染到應用的最外層。
他須要一個屬性 store ,他把這個 store 放在context裏,給Router(connect)用。
app/store/index.jsx
import { createStore } from 'redux';
import reducer from '../reducers';
export default function configureStore(initialState) {
const store = createStore(reducer, initialState);
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
const nextReducer = require('../reducers');
store.replaceReducer(nextReducer);
});
}
return store;
}複製代碼
app/action/index.jsx
'use strict';
/* * @author Damonare 2016-12-10 * @version 1.0.0 * action 類型 */
export const Add_Todo = 'Add_Todo';
export const Change_Todo_To_Doing = 'Change_Todo_To_Doing';
export const Change_Doing_To_Done = 'Change_Doing_To_Done';
export const Change_Done_To_Doing = 'Change_Done_To_Doing';
export const Change_Doing_To_Todo = 'Change_Doing_To_Todo';
export const Search='Search';
export const Delete_Todo='Delete_Todo';
/* * action 建立函數 * @method addTodo添加新事項 * @param {String} text 添加事項的內容 */
export function addTodo(text) {
return {
type: Add_Todo,
text
}
}
/* * @method search 查找事項 * @param {String} text 查找事項的內容 */
export function search(text) {
return {
type: Search,
text
}
}
/* * @method changeTodoToDoing 狀態由todo轉爲doing * @param {Number} index 須要改變狀態的事項的下標 */
export function changeTodoToDoing(index) {
return {
type: Change_Todo_To_Doing,
index
}
}
/* * @method changeDoneToDoing 狀態由done轉爲doing * @param {Number} index 須要改變狀態的事項的下標 */
export function changeDoneToDoing(index) {
return {
type: Change_Done_To_Doing,
index
}
}
/* * @method changeDoingToTodo 狀態由doing轉爲todo * @param {Number} index 須要改變狀態的事項的下標 */
export function changeDoingToTodo(index) {
return {
type: Change_Doing_To_Todo,
index
}
}
/* * @method changeDoingToDone 狀態由doing轉爲done * @param {Number} index 須要改變狀態的事項的下標 */
export function changeDoingToDone(index) {
return {
type: Change_Doing_To_Done,
index
}
}
/* * @method deleteTodo 刪除事項 * @param {Number} index 須要刪除的事項的下標 */
export function deleteTodo(index) {
return {
type: Delete_Todo,
index
}
}複製代碼
在聲明每個返回 action 函數的時候,咱們須要在頭部聲明這個 action 的 type,以便好組織管理。
每一個函數都會返回一個 action 對象,因此在 容器組件裏面調用
text =>
dispatch(addTodo(text))複製代碼
就是調用dispatch(action)
。
app/reducers/index.jsx
import { combineReducers } from 'redux';
import todolist from './todos';
// import visibilityFilter from './visibilityFilter';
const reducer = combineReducers({
todolist
});
export default reducer;複製代碼
app/reducers/todos.jsx
import {
Add_Todo,
Delete_Todo,
Change_Todo_To_Doing,
Change_Doing_To_Done,
Change_Doing_To_Todo,
Change_Done_To_Doing,
Search
} from '../actions';
let todos;
(function() {
if (localStorage.todos) {
todos = JSON.parse(localStorage.todos)
} else {
todos = []
}
})();
function todolist(state = todos, action) {
switch (action.type) {
/* * 添加新的事項 * 並進行本地化存儲 * 使用ES6展開運算符連接新事項和舊事項 * JSON.stringify進行對象深拷貝 */
case Add_Todo:
localStorage.setItem('todos', JSON.stringify([
...state, {
todo: action.text,
istodo: true,
doing: false,
done: false
}
]));
return [
...state, {
todo: action.text,
istodo: true,
doing: false,
done: false
}
];
/* * 將todo轉爲doing狀態,注意action.index的類型轉換 */
case Change_Todo_To_Doing:
localStorage.setItem('todos', JSON.stringify([
...state.slice(0, action.index),
{
todo:state[action.index].todo,
istodo: false,
doing: true,
done: false
},
...state.slice(parseInt(action.index) + 1)
]));
return [
...state.slice(0, action.index),
{
todo:state[action.index].todo,
istodo: false,
doing: true,
done: false
},
...state.slice(parseInt(action.index) + 1)
];
/* * 將doing轉爲done狀態 */
case Change_Doing_To_Done:
localStorage.setItem('todos', JSON.stringify([
...state.slice(0, action.index),
{
todo:state[action.index].todo,
istodo: false,
doing: false,
done: true
},
...state.slice(parseInt(action.index) + 1)
]));
return [
...state.slice(0, action.index),
{
todo:state[action.index].todo,
istodo: false,
doing: false,
done: true
},
...state.slice(parseInt(action.index) + 1)
];
/* * 將done轉爲doing狀態 */
case Change_Done_To_Doing:
localStorage.setItem('todos', JSON.stringify([
...state.slice(0, action.index),
{
todo:state[action.index].todo,
istodo: false,
doing: true,
done: false
},
...state.slice(parseInt(action.index) + 1)
]));
return [
...state.slice(0, action.index),
{
todo:state[action.index].todo,
istodo: false,
doing: true,
done: false
},
...state.slice(parseInt(action.index) + 1)
];
/* * 將doing轉爲todo狀態 */
case Change_Doing_To_Todo:
localStorage.setItem('todos', JSON.stringify([
...state.slice(0, action.index),
{
todo:state[action.index].todo,
istodo: true,
doing: false,
done: false
},
...state.slice(parseInt(action.index) + 1)
]));
return [
...state.slice(0, action.index),
{
todo:state[action.index].todo,
istodo: true,
doing: false,
done: false
},
...state.slice(parseInt(action.index) + 1)
];
/* * 刪除某個事項 */
case Delete_Todo:
localStorage.setItem('todos', JSON.stringify([
...state.slice(0, action.index),
...state.slice(parseInt(action.index) + 1)
]));
return [
...state.slice(0, action.index),
...state.slice(parseInt(action.index) + 1)
];
/* * 搜索 */
case Search:
let text=action.text;
let reg=eval("/"+text+"/gi");
return state.filter(item=> item.todo.match(reg));
default:
return state;
}
}
export default todolist;複製代碼
具體的展現組件這裏就不羅列代碼了,感興趣的能夠戳這:備忘錄展現組件地址
嚴格來講,這個備忘錄並非使用的react全家桶,畢竟還有一部分less代碼,不過這一個應用也算是比較全面的使用了react+react-router+redux,做爲react全家桶技術學習的練手的小項目再適合不過了。若是您對這個小東西感興趣,歡迎戳這:React全家桶實現簡易備忘錄給個star。