俄罗斯贵宾会-俄罗斯贵宾会官网
做最好的网站

【俄罗斯贵宾会】在 2017 年学习 React + Redux 的一些建议(中篇)

俄罗斯贵宾会 1对于学习 Redux 的一些建议

React 和 Redux 经常结合在一起使用,Redux 是 flux 架构模式的一种优秀实现,并且在 React 社区被广泛使用,但也不是完全和 React 耦合在一起的。

理解Redux

redux

下面是知乎上对Redux的一个比较好的解释,弄明白了Redux我们才有能力分析f8app js的代码。

理解 React,但不理解 Redux,该如何通俗易懂的理解 Redux?
解答这个问题并不困难:唯一的要求是你熟悉React。
不要光听别人描述名词,理解起来是很困难的。
从需求出发,看看使用React需要什么:

  1. React有props和state: props意味着父级分发下来的属性,state意味着组件内部可以自行管理的状态,并且整个React没有数据向上回溯的能力,也就是说数据只能单向向下分发,或者自行内部消化。
    理解这个是理解React和Redux的前提。
  2. 一般构建的React组件内部可能是一个完整的应用,它自己工作良好,你可以通过属性作为API控制它。但是更多的时候发现React根本无法让两个组件互相交流,使用对方的数据。
    然后这时候不通过DOM沟通(也就是React体制内)解决的唯一办法就是提升state,将state放到共有的父组件中来管理,再作为props分发回子组件。
  3. 子组件改变父组件state的办法只能是通过onClick触发父组件声明好的回调,也就是父组件提前声明好函数或方法作为契约描述自己的state将如何变化,再将它同样作为属性交给子组件使用。
    这样就出现了一个模式:数据总是单向从顶层向下分发的,但是只有子组件回调在概念上可以回到state顶层影响数据。这样state一定程度上是响应式的。
  4. 为了面临所有可能的扩展问题,最容易想到的办法就是把所有state集中放到所有组件顶层,然后分发给所有组件。
  5. 为了有更好的state管理,就需要一个库来作为更专业的顶层state分发给所有React应用,这就是Redux。让我们回来看看重现上面结构的需求:
    a. 需要回调通知state (等同于回调参数) -> action
    b. 需要根据回调处理 (等同于父级方法) -> reducer
    c. 需要state (等同于总状态) -> store
    对Redux来说只有这三个要素:
    a. action是纯声明式的数据结构,只提供事件的所有要素,不提供逻辑。
    b. reducer是一个匹配函数,action的发送是全局的:所有的reducer都可以捕捉到并匹配与自己相关与否,相关就拿走action中的要素进行逻辑处理,修改store中的状态,不相关就不对state做处理原样返回。
    c. store负责存储状态并可以被react api回调,发布action.
    当然一般不会直接把两个库拿来用,还有一个binding叫react-redux, 提供一个Provider和connect。很多人其实看懂了redux卡在这里。
    a. Provider是一个普通组件,可以作为顶层app的分发点,它只需要store属性就可以了。它会将state分发给所有被connect的组件,不管它在哪里,被嵌套多少层。
    b. connect是真正的重点,它是一个科里化函数,意思是先接受两个参数(数据绑定mapStateToProps和事件绑定mapDispatchToProps),再接受一个参数(将要绑定的组件本身):
    mapStateToProps:构建好Redux系统的时候,它会被自动初始化,但是你的React组件并不知道它的存在,因此你需要分拣出你需要的Redux状态,所以你需要绑定一个函数,它的参数是state,简单返回你关心的几个值。
    mapDispatchToProps:声明好的action作为回调,也可以被注入到组件里,就是通过这个函数,它的参数是dispatch,通过redux的辅助方法bindActionCreator绑定所有action以及参数的dispatch,就可以作为属性在组件里面作为函数简单使用了,不需要手动dispatch。这个mapDispatchToProps是可选的,如果不传这个参数redux会简单把dispatch作为属性注入给组件,可以手动当做store.dispatch使用。这也是为什么要科里化的原因。
    做好以上流程Redux和React就可以工作了。简单地说就是:
    1.顶层分发状态,让React组件被动地渲染。
    2.监听事件,事件有权利回到所有状态顶层影响状态。

和 Flux 一样,Redux 让应用的状态变化变得更加可预测。如果你想改变应用的状态,就必须 dispatch 一个 action。你没有办法直接改变应用的状态,因为保存这些状态的东西(称为 store)只有 getter 而没有 setter。对于 Flux 和 Redux 来说,这些概念都是相似的。

那么为什么要新设计一种架构呢?Redux 的创造者 Dan Abramov 发现了改进 Flux 架构的可能。他想要一个更好的开发者工具来调试 Flux 应用。他发现如果稍微对 Flux 架构进行一些调整,就可以开发出一款更好用的开发者工具,同时依然能享受 Flux 架构带给你的可预测性。

Redux包含了代码热替换(hot reload)和时间旅行(time travel)功能。

命名约定

在软件编程中命名可真是一件令人头疼的事情,这跟给孩子取名一样费劲,哈哈。合适的命名是实现可维护性、易于理解的代码的最好实践,React

  • Redux 的应用中提供了大量的约束来帮助我们组织代码,而且不会在命名上固执己见。无论你的函数封装在 reducer 还是 component 中,在action creator 或是 selector 中,你都应该有一个命名约束,并且在扩展应用之前就确定如何命名,否则经常会让我们陷入难以捉摸的回调和重构当中。

而我习惯为每个类型的函数都加上一个前缀。

  • 在组件的callback中,为每个函数都加上 on 作为前缀,比如 onCreateRplay
  • 在改变 state 的 reducer 中加上 applay 作为前缀,比如 applyCreateReply
  • 在 selector 中 加上 get 作为前缀,比如 getReply
  • 在 action creator 中加上 do 作为前缀,比如 doCreateReply

也许你不一定习惯这种加上前缀的方式,不过我还是推荐给你,同时也建议找到自己喜欢的命名约束规则。

redux-thunk 介绍

先贴官网链接:https://github.com/gaearon/redux-thunk
Thunk的做法就是扩展了这个action creator。
Redux官网说,action就是Plain JavaScript Object。Thunk允许action creator返回一个函数,而且这个函数第一个参数是dispatch。
A thunk is a function that wraps an expression to delay its evaluation.

// calculation of 1 + 2 is immediate
// x === 3
let x = 1 + 2;

// calculation of 1 + 2 is delayed
// foo can be called later to perform the calculation
// foo is a thunk!
let foo = () => 1 + 2;

单一数据源原则

格式化之后的数据可以帮助你按同步的方式来管理 state ,而假如请求后端接口后返回的是深层嵌套的 blogposts 数据结构呢,是不是欲哭无泪啊?! post 字段依然包含 authorcomments字段,不过这次,comments 是一个数组,数组中的每个对象都有 author 字段:

JavaScript

{ post: { author: { id: 'a' }, comments: [ { author: { id: 'b' }, reply: {}, }, { author: { id: 'a' }, reply: {}, }, ], } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  post: {
    author: { id: 'a' },
    comments: [
      {
        author: { id: 'b' },
        reply: {},
      },
      {
        author: { id: 'a' },
        reply: {},
      },
    ],
  }
}

我们可以看到数据结构中 author 字段在 postcomments 中都有维护,这就导致嵌套的数据结构中出现了两次,这就不是单一数据源,当你改变了author 字段的时候就会变得很困难了。

这个时候当你将数据格式化之后, author 这个字段就只有一个了。

JavaScript

{ authors: { a: {}, b: {}, } }

1
2
3
4
5
6
{
  authors: {
    a: {},
    b: {},
  }
}

当你想 follow 一个 author 的时候,就可以轻松的更新一个字段了 — 数据源是单一的:

JavaScript

{ authors: { a: { isFollowed: true }, b: {}, } }

1
2
3
4
5
6
{
  authors: {
    a: { isFollowed: true },
    b: {},
  }
}

应用中所有依赖了 author 这个字段的地方都能得到更新。

智能组件(smart components)和木偶组件(dumb components)

Flux 拥有控制型视图(controller views) 和常规型视图(regular views)。控制型视图就像是一个经理一样,管理着 store 和子视图(child views)之间的通信。

在 Redux 中,也有一个类似的概念:智能组件和木偶组件。(注:在最新的 Redux 文档中,它们分别叫做容器型组件 Container component 和展示型组件 Presentational component)智能组件的职责就像经理一样,但是比起 Flux 中的角色,Redux 对经理的职责有了更多的定义:

  • 智能组件负责所有的 action 相关的工作。如果智能组件里包含的一个木偶组件需要触发一个 action,智能组件会通过 props 传一个 function 给木偶组件,而木偶组件可以在需要触发 action 时调用这个 function。
  • 智能组件不定义 CSS 样式。
  • 智能组件几乎不会产生自己的 DOM 节点,他的工作是组织若干的木偶组件,由木偶组件来生成最终的 DOM 节点。

俄罗斯贵宾会,不断的重构

随着时间得推移,你会想要重构你的代码,无论是你在应用中使用了 React 、React + Redux 或者其他前端框架,你总会不断的掌握更加高效的代码组织方式,或者是一些很好的设计模式。

如果你的应用中的组件非常的多,你可以找到一个更好的方式来分离和组织木偶组件和容器组件,你会发现他们之间的关系并做一些公共的抽取;如果你还没有使用合适的命名约束,你也可以在重构的时候去做这些事情。

用户登录流程代码分析

下面分析登录页面的代码,代码在login目录下,包括LoginModal.js和LoginScreen.js,实现了通过Oauth登录Facebook帐号的功能。
登录涉及的代码有actions/types.js(定义了所有的Action事件), actions/login.js(实现登录业务逻辑,与服务器交互),js/reducers/user.js(实现对用户相关状态的计算)。
登录的入口是js/tabs/schedule/logIn.js,142行定义了<LoginButton source="My F8" /> ,LoginButton组件封装了登录UI相关的逻辑。
点击LoginButton后会调用logIn函数,logIn函数会调用logInWithFacebook进行OAuth登录或在等待15s后超时返回,下面是logIn的代码:

async logIn() {
  const {dispatch, onLoggedIn} = this.props;

  this.setState({isLoading: true});
  try {
    await Promise.race([
      dispatch(logInWithFacebook(this.props.source)),
      timeout(15000),
    ]);
  } catch (e) {
    const message = e.message || e;
    if (message !== 'Timed out' && message !== 'Canceled by user') {
      alert(message);
      console.warn(e);
    }
    return;
  } finally {
    this._isMounted && this.setState({isLoading: false});
  }

  onLoggedIn && onLoggedIn();
}
}

用到了async,Promise.race等ES6的语法。
logInWithFacebook的实现在js/actions/login.js中,如果登录成功会通过Promise异步获取好友的日程和调查问卷。

function logInWithFacebook(source: ?string): ThunkAction {
  return (dispatch) => {
    const login = _logInWithFacebook(source);

    // Loading friends schedules shouldn't block the login process
    login.then(
      (result) => {
        dispatch(result);
        dispatch(loadFriendsSchedules());
        dispatch(loadSurveys());
      }
    );
    return login;
  };
}

登录是调用Facebook SDK进行登录,logInWithFacebook是个异步方法,用到了ES6的async,
async function _logInWithFacebook(source: ?string): Promise<Array<Action>> {...}
返回值是个Promise,在then方面里面异步调用loadFriendsSchedules,loadSurveys。
这些方法会继续请求数据,并更新store,从而让页面更新。

尽可能让 state tree 扁平化

Redux 中,扁平化的 state tree可以让你的 reducers 更加的简单,这样你就不需要在整个 store 的状态树中深层的查找到某个 state 后再将其修改,而是可以很轻松的就能实现。不过,在 Redux 中却不能做这么做,因为 state 是不可变的。

如果你正在开发一个博客应用,需要维护一个类似这样的列表对象,列表中包含 authorcomment 字段:

JavaScript

{ post: { author: {}, comments: [], } }

1
2
3
4
5
6
{
  post: {
    author: {},
    comments: [],
  }
}

不过实际情况是每个对象都需要有对应的 id 来进行维护:

JavaScript

{ post: { id: '1', author: { id: 'a', ... }, comments: [ { id: 'z', ... }, ... ], } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  post: {
    id: '1',
    author: {
      id: 'a',
      ...
    },
    comments: [
      {
        id: 'z',
        ...
      },
      ...
    ],
  }
}

这个时候,我们将数据序列化之后将会变得更有意义,数据解构变得更加扁平化了。序列化之后的数据通过 id 关联其他字段,之后,你就可以通过实体对象来将其报酬,通过 id 来进行关联数据的查找。

JavaScript

{ posts: { 1: { authorId: 'a', commentIds: ['z', ...] } }, authors: { a: { ... } }, comments: { z: { ... } }, }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
  posts: {
    1: {
      authorId: 'a',
      commentIds: ['z', ...]
    }
  },
  authors: {
    a: {
      ...
    }
  },
  comments: {
    z: {
      ...
    }
  },
}

这样,数据结构看起来就不在那么深层嵌套了,当你需要改变数据的时候,就可以轻松的实现数据的不可变性了。

normalizr 是个强大的 library,可以帮助我们进行数据格式化,噢耶~!

本文开始分析f8app核心js部分的源码,这篇文章将非常难理解,原因了Redux框架引入了很多新概念,使用了大量函数式编程思想,建议先把后面的参考文章仔细过一遍,确保理解后再看本文。React Native的理念是Learn once,write anywhere, Android和iOS App端的js代码是放在一起的,以便最大限度的复用业务逻辑,UI部分的可以根据平台特性各自实现,React native分别渲染成安卓和iOS的原生UI界面,对于两个平台UI组件的细微差异和完全不同的UI组件2种情况,react native提供了不同的处理方式。

Generators, Sagas, Observables, Epics, …

Redux 是一个非常优秀的 library,让我们可以体验不同的编程范式和技术。而大家又常常需要不构建不同的类库来实现 async action,这里有几种不同的方式来处理这些 side effects

新手的话建议使用 redux thunk 来处理一些异步操作;等你慢慢的熟悉整个生态及其相关的应用的时候,可以看看其他的相关类库。Redux Saga 是目前被广泛采用的一种实现方式。不过,Redux Observables 目前也被越来越多的人所接受,这可是需要掌握不少关于 rxjs 及其响应式编程的概念及其使用方式。

其实,整体看来,redux 生态圈的本身就产生了非常多的前端类库,真是让人应接不暇啊。但也别烦恼,那些你不需要用到的东西,自然也不需要都去掌握,对吧。

js目录结构分析

首先还是先看下js目录的结构:

├── F8App.js
├── F8Navigator.js
├── FacebookSDK.js
├── Playground.js
├── PushNotificationsController.js
├── actions
│   ├── config.js
│   ├── filter.js
│   ├── index.js
│   ├── installation.js
│   ├── login.js
│   ├── navigation.js
│   ├── notifications.js
│   ├── parse.js
│   ├── schedule.js
│   ├── surveys.js
│   ├── test.js
│   └── types.js
├── common
│   ├── BackButtonIcon.js
│   ├── Carousel.js
│   ├── F8Button.js
│   ├── F8Colors.js
│   ├── F8DrawerLayout.js
│   ├── F8Header.js
│   ├── F8PageControl.js
│   ├── F8SegmentedControl.js
│   ├── F8StyleSheet.js
│   ├── F8Text.js
│   ├── F8Touchable.js
│   ├── ItemsWithSeparator.js
│   ├── ListContainer.js
│   ├── LoginButton.js
│   ├── MapView.js
│   ├── ParallaxBackground.js
│   ├── ProfilePicture.js
│   ├── PureListView.js
│   ├── ViewPager.js
│   └── img
├── env.js
├── filter
│   ├── FilterScreen.js
│   ├── FriendsList.js
│   ├── Header.js
│   ├── Section.js
│   └── TopicItem.js
├── flow-lib.js
├── login
│   ├── LoginModal.js
│   ├── LoginScreen.js
│   └── img
├── rating
│   ├── Header.js
│   ├── RatingCard.js
│   ├── RatingQuestion.js
│   ├── RatingScreen.js
│   └── img
├── reducers
│   ├── __mocks__
│   │   └── parse.js
│   ├── __tests__
│   │   ├── maps-test.js
│   │   ├── notifications-test.js
│   │   └── schedule-test.js
│   ├── config.js
│   ├── createParseReducer.js
│   ├── filter.js
│   ├── friendsSchedules.js
│   ├── index.js
│   ├── maps.js
│   ├── navigation.js
│   ├── notifications.js
│   ├── schedule.js
│   ├── sessions.js
│   ├── surveys.js
│   ├── topics.js
│   └── user.js
├── setup.js
├── store
│   ├── analytics.js
│   ├── array.js
│   ├── configureStore.js
│   ├── promise.js
│   └── track.js
└── tabs
    ├── F8TabsView.android.js
    ├── F8TabsView.ios.js
    ├── MenuItem.js
    ├── img
    ├── info
    │   ├── CommonQuestions.js
    │   ├── F8InfoView.js
    │   ├── LinksList.js
    │   ├── Section.js
    │   ├── ThirdPartyNotices.js
    │   ├── WiFiDetails.js
    │   └── img
    ├── maps
    │   ├── F8MapView.js
    │   ├── ZoomableImage.js
    │   └── img
    ├── notifications
    │   ├── F8NotificationsView.js
    │   ├── NotificationCell.js
    │   ├── PushNUXModal.js
    │   ├── RateSessionsCell.js
    │   ├── allNotifications.js
    │   ├── findSessionByURI.js
    │   ├── img
    │   └── unseenNotificationsCount.js
    └── schedule
        ├── AddToScheduleButton.js
        ├── EmptySchedule.js
        ├── F8FriendGoing.js
        ├── F8SessionCell.js
        ├── F8SessionDetails.js
        ├── F8SpeakerProfile.js
        ├── FilterHeader.js
        ├── FriendCell.js
        ├── FriendsListView.js
        ├── FriendsScheduleView.js
        ├── FriendsUsingApp.js
        ├── GeneralScheduleView.js
        ├── InviteFriendsButton.js
        ├── MyScheduleView.js
        ├── ProfileButton.js
        ├── ScheduleListView.js
        ├── SessionsCarousel.js
        ├── SessionsSectionHeader.js
        ├── SharingSettingsCommon.js
        ├── SharingSettingsModal.js
        ├── SharingSettingsScreen.js
        ├── __tests__
        │   ├── formatDuration-test.js
        │   └── formatTime-test.js
        ├── filterSessions.js
        ├── formatDuration.js
        ├── formatTime.js
        ├── groupSessions.js
        └── img

js部分的代码理解起来还是比较困难的,首先要熟悉javascript ES6,React Native和Redux的常见语法,还需要弄明白redux-react,redux-promise,redux-thunk等插件的作用和原理,否则直接看代码会很困难,主要涉及的新概念比较多,语法比较奇怪。

Redux - 架构上深受 flux 启发,实现上却更接近于 elm,或者说更倾向于函数式编程的一个数据层实现。和 flux 架构对数据层的描述最大的区别就在于 Redux 是采用不可变单一状态树来管理应用程序数据的。用 redux 充当数据层也可以完全兼容 flux 架构(但没好处)并且 redux 对视图层也没有倾向性,只是目前用的比较多的还是 react。redux使用了很多函数式编程的概念,例如柯里化等的。

  • actions目录下的js实现了业务层的逻辑。
  • common目录下是抽取的一些UI组件,react是基于组件化编程的。
  • filter目录下是一些UI组件页面,暂时没有想明白为什么叫filter
  • login目录下是登录页面,提供了通过Facebook帐号登录F8app的功能
  • rating目录下是投票和问卷相关的页面
  • reduces目录是redux Reducer相关的文件。Redux有且只有一个State状态树,为了避免这个状态树变得越来越复杂,Redux通过 Reducers来负责管理整个应用的State树,而Reducers可以被分成一个个Reducer。
  • store目录下是redux store相关的文件
  • tabs目录下是App 4个tab页面的源文件
    整个目录结构划分还是比较合理的。

在 2017 年学习 React + Redux 的一些建议(中篇)

2017/09/11 · JavaScript · React, Redux

原文出处: 郭永峰   

总结

js部分的代码用了很多ES6的新语法和函数式编程思想,特别是使用了Redux框架,代码量也比较大,分析和理解起来比较困难,本文只分析了部分典型模块的代码。特别是在相关的技术和框架了解程度不够深入,缺少实际开发经验的情况下(这说的就是我自己啊)。建议看代码之前先把JavaScript ES6和Redux框架好好学习一下。虽然代码看上去很难,但整个处理流程和模块划分还是很清晰的。

本文由俄罗斯贵宾会发布于Web前端,转载请注明出处:【俄罗斯贵宾会】在 2017 年学习 React + Redux 的一些建议(中篇)

您可能还会对下面的文章感兴趣: