总括: 本文采用react+redux+react-router+less+es6+webpack,以实现一个简易备忘录(todolist)为例尽可能全面的讲述使用react全家桶实现一个完整应用的过程。
代码地址:React全家桶实现一个简易备忘录
原文博客地址:React全家桶实现一个简易备忘录
知乎专栏&&简书专题:前端进击者(知乎)&&前端进击者(简书)
博主博客地址:Damonare的个人博客
人生不失意,焉能暴己知。
技术架构:本备忘录使用react+react-router+redux+less+ES6+webpack实现;
页面UI参照:TodoList官网实现;
在线演示地址:Damonare的备忘录;
毫无疑问,当谈到React
的时候不能避免的会提到组件化思想。React刚开始想解决的问题只是UI这一层面的问题,也就是MVC中view层面的问题,不成想如今越滚越大,从最早的UI引擎变成了一整套前后端通吃的 Web App 解决方案。对于React
组件的理解同样要站在view层面的角度出发,一个完整的页面是由大大小小的组件堆叠而成,就好像搭积木,每一块积木都是一个组件,组件套组件组成了用户所能看到的完整的页面。
使用React
,不一定非要使用JSX
语法,可以使用原生的JS进行开发。但是React
作者强烈建议我们使用JSX
,因为JSX
在定义类似HTML这种树形结构时,十分的简单明了。这里简单的讲下JSX
的由来。
比如,下面一个div元素,我们用HTML语法描述为:
<div class="test">
<span>Testspan>
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>Testspan>
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 |
数据修改 | 从 props 调用回调函数 |
调用方式 | 手动 |
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。