把react什么的用起来——我不是双向绑定互联网
先弄个什么例子呢?如果是现代的MVVM框架,可能会用双向绑定来吸引你。那react有双向绑定吗?
没有。
也算是有吧,有插件。不过双向绑定跟react不是一个路子的。react强调的是单向数据流。 当然,即便是单向数据流也总要有个数据的来源,如果数据来源于页面自身上的用户输入,那效果也就等同于双向绑定了。
下面就展示一下如何达到这个效果。我们来设计一个登录的场景,用户输入用户名后,会在问候语的位置展示用户名,像下图这样:
预警一下先,我要用这个小东西展示react+redux的数据流工作方式,所以代码看起来比较多, 肯定比一些MVVM框架双向绑定一对双大括号代码要多得多。但正如我前面说的,它俩不是一个路子, react这种模式的好处后面你一定会看出来,这里先耐着性子把这几段貌似很罗嗦的代码看完。 react和redux很多重要的思想在这就开始提现出来了。
先把组件写出来。为了简便,我们把整个登录页面作为一个组件,放在containers目录下。 还记得前面说过containers和components目录吗?把组件放在containers目录下,意味着这个组件要跟外界打交道。 不过一开始,我们先别管打交道的事儿,就写一个简单的,普通的组件:
- import React from 'react'
- class Login extends React.Component{
- render(){
- return (
- <div>
- <div>早上好,{this.props.username}</div>
- <div>用户名:<input/></div>
- <div>密 码:<input type="papssword"/></div>
- <button>登录</button>
- </div>
- )
- }
- }
- export default Login
为了能让我们写的东西显示出来,得改点模板代码,现在来修改一下src/index.js,里面原来的代码都不需要了,改成:
- import React from 'react';
- import { render } from 'react-dom';
- import { Provider } from 'react-redux';
- import configureStore from '../stores';
- import Login from '../containers/Login';
- const store = configureStore();
- render(
- <Provider store={store}>
- <Login />
- </Provider>,
- document.getElementById('app')
- );
搭建环境时自动打开的浏览器页面还没关吧?保存代码后少等片刻就可以看到我们做的登陆页面了。
目前这个登录组件里问候语里显示的用户名和用户输入的用户名毫无关系,如何将它们联系起来呢? 既然看到了{this.props.username}你肯定会想到有一个数据模型。的确是有这么个东西,不过在redux里, 这个数据模型很壮观,整个应用只有一个数据模型,所以更应该管它叫数据仓库。这个仓库的代码在stores/index.js里面。 代码很简单,就是用reducers和initialState两个参数来创建一个仓库。看刚才run.js里面的代码, 有个叫Provider的组件使用了仓库,意思很明显:在provider这个组件内部,已经给我们提供好了仓库的访问条件, 也就是说我们的Login组件已经可以访问仓库了。怎么访问呢?需要把我们的组件跟仓库连接起来。 登录组件代码最后一行“export default Login”要改成这样:
- function mapStateToProps(state) {
- return {}
- }
- export default connect(mapStateToProps)(Login);
connect是react-redux这个库提供的函数,功能就是把组件连接到rudux的仓库。注意在文件顶部加上一句“import { connect } from 'react-redux'”。 这里有个函数mapStateToProps,它返回的对象就是从仓库取出的数据,具体的数据等我们写完reducer再补充。
那么reducer是什么呢?
我们考虑一下仓库的数据是要变化的,怎么让它变化呢?我们得给个规则,这个规则描述起来就是: “在发生某一动作(action)时,仓库中的一部分数据要进行相应的变化”。我们管会因动作而变化的这一部分数据叫做状态, 许许多多琐碎的状态组成了仓库数据,所以整个仓库其实就是一个大的状态。在程序运行过程中,我们主要关心的就是这个仓库的状态如何变化。 如何变化?那就要靠reducer。针对一个动作,仓库里会有一个或多个状态发生变化,reducer就是要指导状态如何变化。
等等,那动作是哪来的?从具体上说,动作一般是来源于用户的操作或者网络请求的回应。在代码里需要对动作规范一下, 其实也就是跟reducer进行一个约定,让它知道有动作来了。其实怎样表示动作都可以,只要具有唯一性就行。 一般我们就用字符串就行了,即容易制造唯一,又能够表义,在使用中小心点别重了就行。下面就来定义一个用户输入用户名的动作:
- const INPUT_USERNAME = 'INPUT_USERNAME'
咋不直接用字符串呢?为了避免低级错误,定义了这个常量以后,发起动作时用这个常量,reducer也根据这个常量辨别动作类型。
我们光告诉reducer发生了“用户输入”这个动作还不够,还要告诉reducer用户输入了什么内容。所以完整的动作得是一个具有丰富信息的对象。 为了方便,我们写一个动作生成器,也就是个函数:
- function inputUsername (value) {
- return {
- type: INPUT_USERNAME,
- value: value
- }
- }
现在reducer就能得到足够的信息来指导状态的变化了。reducer要做的就是把仓库里一个叫做“username”的状态的值修改一下。 由于状态可以是一层套一层的,所以reducer也被设计成可以一层套一层。单个reducer就是它上级reducer的一分子。 其实reducer本身也就是个函数:
- function username (state='', action) {
- switch(action.type){
- case INPUT_USERNAME:
- return action.value
- defalut:
- return state
- }
- }
reducer的函数名对应着状态名称,函数接受两个参数:第一个是当前状态,如果是程序开始运行的时候, 很可能没有当前状态,就给个默认值,这里是空字符串;第二个是前面动作生成器生成的action对象。 一个reducer可以处理多种动作,目前我们只有一个,以后有别的就直接加case分支。对于每种动作, reducer都要返回一个新的状态值,这个值就可以根据action传来的信息按照业务要求生成了。 最后一定要加一个默认情况返回当前状态。在redux里,任何一个action都会在所有的reducer里过一遍, 所以对于一个reducer来说实际上绝大多数情况action都不是它能处理的,最后还是返回当前状态值。 觉得很低效吗?