前端开发需要什么样的状态?

1,137 阅读12分钟
原文链接: mp.weixin.qq.com

本文由我和尹锋共同完成,尹锋完成了文章主体,我写了前面部分,并润色了全文。

尹锋,前端攻城师,目前就职微影时代,任前端基础平台建设部 Team Leader,主要负责前端基础平台建设以及微信演出票和电影票移动站的业务开发。

文章很长,但有价值。

总有读者来问,我该不该学习前端,我说该。我该不该从 Java 转到 JavaScript,我说转。前端有没有前途,我说有。其实很多人去提出一个问题的时候,已经有了预设的答案,他们只是需要一个肯定的答复而已。

做前端、后端和移动端,做好了,都是很好的方向。只是技术不同,场景不同,应用不同而已。互联网已经形成了一个巨大的反应场,好的技术,在这个互联网时代,都会有一席之地。

另外,常常有人抱怨前端的框架太多了,我想这样的人掌握的编程语言不会超过两门。由于浏览器引擎日益强大,平台趋势凸现,前端只是在后端走过的路罢了。模块化,组件化,状态化,异步,等等,不要抱怨框架太多,后端的每一门语言都有无数的框架。在一个技术突飞猛进的时候,更多的选择,往往比更少的选择要好。在大数据发展的初期,更大的数据量永远胜于优秀的算法。无论算法好坏,更多的数据总是能带来更好的效果,也是一个道理。

所以,投身前端领域的同学,要有不怕困难,热爱框架,勇于当炮灰的精神和状态。当然了,我也是站着说话不腰疼,因为我有个前端团队:)

今天的文章内容也和「状态」有关,上一篇讲了「React,一次学习,到处编码」,今天聊聊 Redux。

Redux 是什么?Redux 是 JavaScript 状态容器,提供可预测化的状态管理。Redux 可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试。不仅于此,它还提供了非常棒的开发体验,比如这个时间旅行调试器,可以编辑后实时预览。

github.com/gaearon/red…

为什么要开发 Redux?Redux 的作者如是说:因为基于 JavaScript 的单页面应用越来越多了,现在不再是 HTML 的时代,而是 HTML App,JavaScript 需要管理比任何时候都要多的 State (状态)。这些 State 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。

管理不断变化的 State 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,然后,可能引起另一个 view 的变化,直到你把自己搞晕,State 如何变化似乎已经不受控制。不过,计算机领域就是这样,出现问题就会有解决问题的方案,于是 Redux 应运而生了,它与 React 的结合,很好的解决了这个问题。同时 Redux 并不强依赖 React,它可以配合其他任何框架或者类库一起使用。

但是话说回来,React 和 Redux 搭在一起真的很般配啊,是真爱!

先回顾一下 React。

一、对 React 的理解

1、理想的 UI 编程模型

React 大胆的跳出了 Web 的 DOM 实现约束,实现了高效的渲染机制,通过树形控制描述 UI、统一事件机制、单向数据流。

2、JSX

JSX 是 React 中一颗闪耀的新星,它一个非常好的工程化手段,JSX 允许我们将标签写入 JavaScript 文件中,其实这样做让程序更加聚合,具备良好的开发体验,我们能在编译快速清晰的看到错误信息,指定错误代码行号,方便调试。

说到 JSX,一般都会和模板进行比较,模板?套用一句功夫熊猫里面的台词,这真是太遗憾了,我想对模板的喜爱者说一个不幸的消息,JSX 天生具有模板无法触及的优势。

模板是把 “JavaScript” 放入 HTML 里面,而 JSX 是把 “HTML” 放入 JavaScript 里面。

怕不怕?

JavaScript 远比 HTML 要强大,在 JSX 里面你可以使用 JavaScript 编程语言的能力和工具链来描述页面。因此,增强 JavaScript 使其支持HTML 要比增强 HTML 使其支持逻辑要合理的多。

咦,以前人们怎么没想到呢?因为技术和思想一直在进步啊!

当然了,JSX 也并非十全十美,有坑有陷阱,比如不能很好的处理 if-else 等,不过,这些可以克服的缺陷,不足以盖住 JSX 的光芒。

二、Redux

文章起始我对 Redux 做了简介,如果你已经很熟悉 Redux 了,可以略过这部分,直接跳到最后的 State 数据结构的设计 这一节,这部分是我们团队在开发了好几个大型项目后总结出来的。

1、Redux = reduce + Flux

我们看一下 JavaScript 中 Array.prototype.reduce 的用法,对数组中的所有元素调用指定的回调函数,该回调函数的返回值为累积结果,而且返回值在下一次调用回调函数的时候是作为参数提供的:

const initState = ''
const actions = ['Learn about actions, ','Learn about reducers, ', 'Learn about store']
const newState = actions.reduce((prevState, action)=> { return prevState + action }, initState)

这就是 Redux 的核心所在,State 就是应用程序的数据,给定 initState 之后,随着 action 的值不断传入给计算函数,得到新的 StateArray.prototype.reduce 的第一个参数是一个函数:(prevState, actions)=> { return prevState + actions },这个计算函数被称之为 Reducer

能看到这的读者,大部分都是 Web 前端工程师,我们每天的工作就是构建很多网页,然后呈现给用户。那么,网页的本质是什么呢?

从 Web 前端工程师的角度看,网页的本质是将数据渲染出来,呈现给用户。这里有两个关键字,一是数据,二是渲染,当数据发生改变的时候,网页需要重新渲染。网页渲染和重新渲染,就是 React,数据和数据改变的逻辑就由我们的主角 — Redux 实现。

这里的数据和上面提到的 State 是同一个东西,因为 State 是 React 中的概念,所以后续会使用 State 这个概念。

Redux 约定:

整个应用的 State 被储存在一棵 Object Tree 中,并且这个 Object Tree 只存在于唯一一个 Store 中。

比如说,我们有这么一个 State:

[{
todo: 'Learn about actions'
}]

这个 State 只有一条记录,React 将他渲染成

 Learn about actions 

我们把 State 修改以后

[{
todo: 'Learn about actions'
},{
todo: 'Learn about reducers'
}]

那么渲染也会发生变化

 Learn about actions 
 Learn about reducers 

这就是 Redux 中最基本的概念:

页面中的所有状态,都应该以这种状态树的形式来描述,页面上的任何变化,都应该先去改变这个状态树,然后再渲染到页面上。

下面就可以解释 Redux 几个核心概念了。

2、Action

Action 是把数据从应用传到 Store 的有效负载,它是 Store 数据的唯一来源。通俗的说,就是描述“发生了什么事情”。

在上面的例子中,如何来描述在 todo list “Learn about actions” 中新加入一条记录 “Learn about reducers” 呢?

export function addTodo(text) {
    return {
        type: ‘ADD_TODO',
        text
    }
}

这个函数会返回一个 Action 对象,Action 本质上是 JavaScript 普通对象,这个对象描述“发生了什么事情”。Redux 约定,Action 内使用一个字符串类型的 type 字段来表示将要执行的动作,除此以外,还可以携带动作所需要的数据,随后这个对象会被传入到 Reducer 中。

3、Reducer

Action 只是描述了有事情发生了这一事实,并没有指明应用如何更新 State。而这正是 reducer 要做的事情。reducer 就是一个函数,接收旧的 State 和 Action,返回新的 State。

(prevState, action) => newState

我们现在用这个公式来解决上面的 Action。

const initialState = [{
    text: 'Learn about actions'
}]

export default function reducer(state = initialState, action) {
    switch (action.type) {
        case ADD_TODO:
        return [{
            id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
            completed: false,
            text: action.text
        },
        ...state
        ]
    }
}


4、Store

我们学会了使用 Action 来描述“发生了什么”,和使用 Reducers 来根据 Action 更新 State 的用法。

Store 就是把它们联系到一起的对象。Store 有以下职责:

  • 维持应用的 State

  • 提供 getState() 方法获取 State

  • 提供 dispatch(action) 方法更新 State

  • 通过 subscribe(listener) 注册监听器

再次强调一下 Redux 应用只有一个单一的 Store。

根据已有的 reducer 来创建 Store 是非常容易的。

import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)


5、发起 Actions

用一句话来描述 Redux:

将 动作(Action) 通过 状态转换函数(Reducer),set 到一个统一的地方(Store),然后 UI 从 Store 中获取数据。

接下来,我们验证一下这个逻辑。

store.dispatch(actions.addTodo('Learn about actions')) // 添加一个 todo
store.dispatch(actions.addTodo('Learn about reducers')) // 添加一个 todo
store.dispatch(actions.addTodo('Learn about store')) // 添加一个 todo
store.dispatch(actions.completeTodo(0)) // 完成第零个 todo
store.dispatch(actions.completeTodo(1)) // 完成第一个 todo
store.dispatch(actions.clearCompleted()) // 清除已经完成的 todo

查看图片

简直没有比这再清晰的了,你甚至都不用阅读源码,只需要看一下这个 Action 列表(每个 Action 日志中会打印出初始状态、执行动作和执行后的状态),就知道业务逻辑是怎样执行的,也不会出现 MVC 中一个 Model 更新了以后不知道哪些 View 会随之更新的情况了。

这正是 Redux 的一个很大的优点 — 可预测性。因此,我们清楚的知道发生了什么改变(Action),改变之后的数据是什么状态(State),以及发生了哪些改变(Action 记录)。

这段代码可以在所有 JavaScript 环境下执行,这意味我们可以进行业务逻辑的单元测试,也意味着这套业务逻辑可以用于 Web,用于 iOS、Android、tvOS…

查看图片

Redux 与 React 没有直接联系,Redux 用于管理 State,与具体的 UI 框架无关,不过官方和社区提供了很多库,来绑定 Redux 和其他 UI 框架,比如 React、Angular、Vue 等等

三、Redux 搭配 React

为了在 React 中使用 Redux,React 官方提供了一个库 react-redux,用来结合 Redux 和 React 的模块。react-redux 提供了两个接口 Provider、connect。

首先,我们要获取 react-redux 提供的 Provider,并且在渲染之前将根组件包装进 

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './web/containers/App'
import configureStore from 'app/store/configureStore'

const store = configureStore()

render(


,
document.getElementById('root')
)

接下来,我们通过 react-redux 提供的 connect() 方法将包装好的组件连接到 Redux。

import React, { Component, PropTypes } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as TodoActions from 'app/actions'

class App extends Component {
    render() {}
}

export default connect(
    (state) => {
        return {
            todos: state.todos
        }
    },
    (dispatch) => {
        return {
            actions: bindActionCreators(TodoActions, dispatch)
        }
    }
)(App)


四、State 数据结构的设计

第一次在项目中使用 Redux 的时候,对于 State 基本没有设计,后期全部推倒重来,当时,我们的内心是奔溃的。

第二个项目的时候,我们按照页面结构来组织 State,刚开始很好,但是到后来,就会出现,在某个页面需要到其他页面去获取 State,这样虽然可行,但不是好的实现方式,我对于这种做法一直耿耿于怀。

所以,对于 State,我一直在思考,如何回归到网页的本质。我们有数据,然后去渲染它。那应用包含哪些数据呢?

  • 服务器响应

  • 缓存数据

  • 本地生成尚未持久化到服务器的数据

  • 也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器

对数据进行一个分类处理,可以分为 业务逻辑数据 和 页面 UI 数据

举个例子,对于电影票应用而言,业务逻辑数据包含并不限于这些数据:正在热映列表、即将上映列表、电影详情、影院列表、影院详情、座位信息、支付信息、订单中心、用户中心等等,如果按照业务逻辑来组织 State,那么就可以设计如下的数据结构:

let state = {
movie: {
hot: [], // 正在热映
coming: [], // 即将上映
detail: {}, // 当前电影详情
},
cinema: {
list: {}, // 影院列表
detail: {}, // 当前影院详情
scheds: [], // 影院排期
},
seat: {
seats: {}, // 作为信息
locked: {}, // 已锁座位
},
payment: {},
order: {
list: [],
detail: {},
},
routing: {},
pages:{},
}

这样,所有的数据结构一目了然,方便理解和记忆。数据存储好了以后,所有的 page 都从这个 State 里面获取数据,如果使用传统的 select 来计算,当页面比较大的时候,会存在性能问题,所以出现了 Reselect。

Reselect 库能够创建可记忆的、可组合的 selector 函数。Reselect selectors 可以用来高效地计算 Redux Store 里的衍生数据。

所以,我们在 state 和 page 中加了一层 selector。在 selector 里面在组装 page 需要的数据以及数据的计算,这个数据可能会来自多个业务 model。

比如说,在选座页会呈现出影片的信息、影院的信息、座位图的信息,就可以从三个业务 model 中获取数据(调用三次 action),然后计算,传递给选座页使用。

这只是一种方式。业务逻辑不同,数据格式和结构也会多种多样,找到适合自己的就好。

定义好 State,团队可以按照统一的 State 数据结构开发,各自相对独立,互不干扰。

五、不足之处

  • 对从 OOP 开发转过来的程序猿来说,函数式编程的概念接受起来需要一点门槛。

  • JavaScript 对不变对象的支持并不是特别的友好,无论是引入 immutable.js 还是 ES6 的解构语法糖有时候都觉得 Reducer 里的代码读起来有些费力,特别是对刚接触 ES6 的同学来说。

以上源码在这里

https://github.com/ingf/caption

六、参考备注