Facebook F8App-ReactNative项目源码分析4-js篇

674 查看

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

js入口分析

React Native Android App和iOS App的入口jsbundle对应的默认js源文件分别是index.android.js和index.ios.js,在f8app中这2个文件内容一致。代码如下:

'use strict';

const {AppRegistry} = require('react-native');
const setup = require('./js/setup');

AppRegistry.registerComponent('F8v2', setup);

React Native采用了组件化编程的思想,在React Native项目中,所有展示的界面,都可以看做是一个组件(Component)。
index.android.js利用Node.js的require机制引入setup包,然后注册到AppRegistry.

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页面的源文件
    整个目录结构划分还是比较合理的。

理解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)功能。

智能组件(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 节点。

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;

setup.js代码分析

熟悉React Native都知道,index.android.js和index.ios.js分别是Android和iOS App的js程序入口,当然实际运行是压缩处理后的jsbundle。这个2个文件都是注册了setup组件,AppRegistry.registerComponent('F8v2', setup);
setup.js负责配置其它的组件,具体代码如下:

//js/setup.js

var F8App = require('F8App');
var FacebookSDK = require('FacebookSDK');
var Parse = require('parse/react-native');
var React = require('React');
var Relay = require('react-relay');

var { Provider } = require('react-redux');
var configureStore = require('./store/configureStore');

var {serverURL} = require('./env');

function setup(): React.Component {
  console.disableYellowBox = true;
  Parse.initialize('oss-f8-app-2016');
  Parse.serverURL = `${serverURL}/parse`;

  FacebookSDK.init();
  Parse.FacebookUtils.init();
  Relay.injectNetworkLayer(
    new Relay.DefaultNetworkLayer(`${serverURL}/graphql`, {
      fetchTimeout: 30000,
      retryDelays: [5000, 10000],
    })
  );

  class Root extends React.Component {
    constructor() {
      super();
      this.state = {
        isLoading: true,
        store: configureStore(() => this.setState({isLoading: false})),
      };
    }
    render() {
      if (this.state.isLoading) {
        return null;
      }
      return (
        <Provider store={this.state.store}>
          <F8App />
        </Provider>
      );
    }
  }

  return Root;
}

global.LOG = (...args) => {
  console.log('/------------------------------\\');
  console.log(...args);
  console.log('\\------------------------------/');
  return args[args.length - 1];
};

module.exports = setup;

setup.js负责对整个app进行配置,首先配置了Parse,FacebookSDK和Relay,这3个组件是服务器端相关的。
然后通过react-redux配置了Provider组件,这个组件包裹在整个组件树的最外层。这个组件让根组件的所有子孙组件能够轻松的使用 connect() 方法绑定 store。Provider 本质上创建了一个用于更新视图组件的网络。那些智能组件通过 connect() 方法连入这个网络,以此确保他们能够获取到状态的更新。
configureStore提供了对Store的创建和配置,由于Redux只有一个store,如果让store 完全独立处理自己的事,store会变的很复杂。因此,Redux 中的 store 首先会保存整个应用的所有状态,然后将「判断哪一部分状态需要改变」的任务分配下去。而以根 reducer(root reducer)为首的 reducer 们将会承担这个任务。

// ./js/store/configureStore.js

'use strict';

var {applyMiddleware, createStore} = require('redux');
var thunk = require('redux-thunk');
var promise = require('./promise');
var array = require('./array');
var analytics = require('./analytics');
var reducers = require('../reducers');
var createLogger = require('redux-logger');
var {persistStore, autoRehydrate} = require('redux-persist');
var {AsyncStorage} = require('react-native');

var isDebuggingInChrome = __DEV__ && !!window.navigator.userAgent;

var logger = createLogger({
  predicate: (getState, action) => isDebuggingInChrome,
  collapsed: true,
  duration: true,
});

var createF8Store = applyMiddleware(thunk, promise, array, analytics, logger)(createStore);

function configureStore(onComplete: ?() => void) {
  // TODO(frantic): reconsider usage of redux-persist, maybe add cache breaker
  const store = autoRehydrate()(createF8Store)(reducers);
  persistStore(store, {storage: AsyncStorage}, onComplete);
  if (isDebuggingInChrome) {
    window.store = store;
  }
  return store;
}

module.exports = configureStore;

createF8Store使用了柯里化方法调用了applyMiddleware,middleware我们可以简单的理解成过滤器,作用就是加入一些中间处理过程。最后返回store对象。

用户登录流程代码分析

下面分析登录页面的代码,代码在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,从而让页面更新。

总结

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

参考文章