总括: 本文采用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
支持回车添加新事项;github
支持删除事项(点击X符号);
支持状态转换具体包括:
新建事项->正在进行(点击checkbox选项)
正在进行->已完成(点击文字内容自己)
正在进行->新建事项(点击checkbox选项)
已完成->正在进行(点击文字自己)
支持判断输入空字符,过长字符(20个汉字之内);
支持搜索;
支持本地化存储;
支持状态的展开隐藏(点击标题)
兼容手机端(iPhone6及以上)
支持路由切换
毫无疑问,当谈到React
的时候不能避免的会提到组件化思想。React刚开始想解决的问题只是UI这一层面的问题,也就是MVC中view层面的问题,不成想现在越滚越大,从最先的UI引擎变成了一整套先后端通吃的 Web App 解决方案。对于React
组件的理解一样要站在view层面的角度出发,一个完整的页面是由大大小小的组件堆叠而成,就好像搭积木,每一块积木都是一个组件,组件套组件组成了用户所能看到的完整的页面。
使用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(首相)的职责:
维持应用的 state;
提供 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 个步骤:
Redux store 调用传入的 reducer 函数。
根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
Redux store 保存了根 reducer 返回的完整 state 树。
工做流程图以下:
这里须要再强调一下: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。