Facebook F8App-ReactNative项目源码分析

作者: offbye西涛 | 来源:发表于2016-06-04 21:50 被阅读3583次

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

    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框架好好学习一下。虽然代码看上去很难,但整个处理流程和模块划分还是很清晰的。

    参考文章

    相关文章

      网友评论

      • e6a56e3205c8:建议谷歌系平台google plus,结合了:视频推广、多语言推广、community SNS推广。
      • 小如99:除了Redux-thunk还没理解,其他都仿佛理解了,但还是看不懂代码

      本文标题:Facebook F8App-ReactNative项目源码分析

      本文链接:https://www.haomeiwen.com/subject/ejnldttx.html