Facebook F8App-ReactNative項目源碼分析4-js篇

本文開始分析f8app核心js部分的源碼,這篇文章將非常難理解,原因了Redux框架引入了很多新概念,使用了大量函數(shù)式編程思想,建議先把后面的參考文章仔細過一遍,確保理解后再看本文。React Native的理念是Learn once,write anywhere, Android和iOS App端的js代碼是放在一起的,以便最大限度的復用業(yè)務邏輯,UI部分的可以根據(jù)平臺特性各自實現(xiàn),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個文件內(nèi)容一致。代碼如下:

'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目錄結(jié)構(gòu)分析

首先還是先看下js目錄的結(jié)構(gòu):

├── 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 - 架構(gòu)上深受 flux 啟發(fā),實現(xiàn)上卻更接近于 elm,或者說更傾向于函數(shù)式編程的一個數(shù)據(jù)層實現(xiàn)。和 flux 架構(gòu)對數(shù)據(jù)層的描述最大的區(qū)別就在于 Redux 是采用不可變單一狀態(tài)樹來管理應用程序數(shù)據(jù)的。用 redux 充當數(shù)據(jù)層也可以完全兼容 flux 架構(gòu)(但沒好處)并且 redux 對視圖層也沒有傾向性,只是目前用的比較多的還是 react。redux使用了很多函數(shù)式編程的概念,例如柯里化等的。

  • actions目錄下的js實現(xiàn)了業(yè)務層的邏輯。
  • common目錄下是抽取的一些UI組件,react是基于組件化編程的。
  • filter目錄下是一些UI組件頁面,暫時沒有想明白為什么叫filter
  • login目錄下是登錄頁面,提供了通過Facebook帳號登錄F8app的功能
  • rating目錄下是投票和問卷相關(guān)的頁面
  • reduces目錄是redux Reducer相關(guān)的文件。Redux有且只有一個State狀態(tài)樹,為了避免這個狀態(tài)樹變得越來越復雜,Redux通過 Reducers來負責管理整個應用的State樹,而Reducers可以被分成一個個Reducer。
  • store目錄下是redux store相關(guān)的文件
  • tabs目錄下是App 4個tab頁面的源文件
    整個目錄結(jié)構(gòu)劃分還是比較合理的。

理解Redux

redux

下面是知乎上對Redux的一個比較好的解釋,弄明白了Redux我們才有能力分析f8app js的代碼。

理解 React,但不理解 Redux,該如何通俗易懂的理解 Redux?
解答這個問題并不困難:唯一的要求是你熟悉React。
不要光聽別人描述名詞,理解起來是很困難的。
從需求出發(fā),看看使用React需要什么:

  1. React有props和state: props意味著父級分發(fā)下來的屬性,state意味著組件內(nèi)部可以自行管理的狀態(tài),并且整個React沒有數(shù)據(jù)向上回溯的能力,也就是說數(shù)據(jù)只能單向向下分發(fā),或者自行內(nèi)部消化。
    理解這個是理解React和Redux的前提。
  2. 一般構(gòu)建的React組件內(nèi)部可能是一個完整的應用,它自己工作良好,你可以通過屬性作為API控制它。但是更多的時候發(fā)現(xiàn)React根本無法讓兩個組件互相交流,使用對方的數(shù)據(jù)。
    然后這時候不通過DOM溝通(也就是React體制內(nèi))解決的唯一辦法就是提升state,將state放到共有的父組件中來管理,再作為props分發(fā)回子組件。
  3. 子組件改變父組件state的辦法只能是通過onClick觸發(fā)父組件聲明好的回調(diào),也就是父組件提前聲明好函數(shù)或方法作為契約描述自己的state將如何變化,再將它同樣作為屬性交給子組件使用。
    這樣就出現(xiàn)了一個模式:數(shù)據(jù)總是單向從頂層向下分發(fā)的,但是只有子組件回調(diào)在概念上可以回到state頂層影響數(shù)據(jù)。這樣state一定程度上是響應式的。
  4. 為了面臨所有可能的擴展問題,最容易想到的辦法就是把所有state集中放到所有組件頂層,然后分發(fā)給所有組件。
  5. 為了有更好的state管理,就需要一個庫來作為更專業(yè)的頂層state分發(fā)給所有React應用,這就是Redux。讓我們回來看看重現(xiàn)上面結(jié)構(gòu)的需求:
    a. 需要回調(diào)通知state (等同于回調(diào)參數(shù)) -> action
    b. 需要根據(jù)回調(diào)處理 (等同于父級方法) -> reducer
    c. 需要state (等同于總狀態(tài)) -> store
    對Redux來說只有這三個要素:
    a. action是純聲明式的數(shù)據(jù)結(jié)構(gòu),只提供事件的所有要素,不提供邏輯。
    b. reducer是一個匹配函數(shù),action的發(fā)送是全局的:所有的reducer都可以捕捉到并匹配與自己相關(guān)與否,相關(guān)就拿走action中的要素進行邏輯處理,修改store中的狀態(tài),不相關(guān)就不對state做處理原樣返回。
    c. store負責存儲狀態(tài)并可以被react api回調(diào),發(fā)布action.
    當然一般不會直接把兩個庫拿來用,還有一個binding叫react-redux, 提供一個Provider和connect。很多人其實看懂了redux卡在這里。
    a. Provider是一個普通組件,可以作為頂層app的分發(fā)點,它只需要store屬性就可以了。它會將state分發(fā)給所有被connect的組件,不管它在哪里,被嵌套多少層。
    b. connect是真正的重點,它是一個科里化函數(shù),意思是先接受兩個參數(shù)(數(shù)據(jù)綁定mapStateToProps和事件綁定mapDispatchToProps),再接受一個參數(shù)(將要綁定的組件本身):
    mapStateToProps:構(gòu)建好Redux系統(tǒng)的時候,它會被自動初始化,但是你的React組件并不知道它的存在,因此你需要分揀出你需要的Redux狀態(tài),所以你需要綁定一個函數(shù),它的參數(shù)是state,簡單返回你關(guān)心的幾個值。
    mapDispatchToProps:聲明好的action作為回調(diào),也可以被注入到組件里,就是通過這個函數(shù),它的參數(shù)是dispatch,通過redux的輔助方法bindActionCreator綁定所有action以及參數(shù)的dispatch,就可以作為屬性在組件里面作為函數(shù)簡單使用了,不需要手動dispatch。這個mapDispatchToProps是可選的,如果不傳這個參數(shù)redux會簡單把dispatch作為屬性注入給組件,可以手動當做store.dispatch使用。這也是為什么要科里化的原因。
    做好以上流程Redux和React就可以工作了。簡單地說就是:
    1.頂層分發(fā)狀態(tài),讓React組件被動地渲染。
    2.監(jiān)聽事件,事件有權(quán)利回到所有狀態(tài)頂層影響狀態(tài)。

和 Flux 一樣,Redux 讓應用的狀態(tài)變化變得更加可預測。如果你想改變應用的狀態(tài),就必須 dispatch 一個 action。你沒有辦法直接改變應用的狀態(tài),因為保存這些狀態(tài)的東西(稱為 store)只有 getter 而沒有 setter。對于 Flux 和 Redux 來說,這些概念都是相似的。

那么為什么要新設(shè)計一種架構(gòu)呢?Redux 的創(chuàng)造者 Dan Abramov 發(fā)現(xiàn)了改進 Flux 架構(gòu)的可能。他想要一個更好的開發(fā)者工具來調(diào)試 Flux 應用。他發(fā)現(xiàn)如果稍微對 Flux 架構(gòu)進行一些調(diào)整,就可以開發(fā)出一款更好用的開發(fā)者工具,同時依然能享受 Flux 架構(gòu)帶給你的可預測性。

Redux包含了代碼熱替換(hot reload)和時間旅行(time travel)功能。

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

Flux 擁有控制型視圖(controller views) 和常規(guī)型視圖(regular views)。控制型視圖就像是一個經(jīng)理一樣,管理著 store 和子視圖(child views)之間的通信。

在 Redux 中,也有一個類似的概念:智能組件和木偶組件。(注:在最新的 Redux 文檔中,它們分別叫做容器型組件 Container component 和展示型組件 Presentational component)智能組件的職責就像經(jīng)理一樣,但是比起 Flux 中的角色,Redux 對經(jīng)理的職責有了更多的定義:

  • 智能組件負責所有的 action 相關(guān)的工作。如果智能組件里包含的一個木偶組件需要觸發(fā)一個 action,智能組件會通過 props 傳一個 function 給木偶組件,而木偶組件可以在需要觸發(fā) action 時調(diào)用這個 function。
  • 智能組件不定義 CSS 樣式。
  • 智能組件幾乎不會產(chǎn)生自己的 DOM 節(jié)點,他的工作是組織若干的木偶組件,由木偶組件來生成最終的 DOM 節(jié)點。

redux-thunk 介紹

先貼官網(wǎng)鏈接:https://github.com/gaearon/redux-thunk
Thunk的做法就是擴展了這個action creator。
Redux官網(wǎng)說,action就是Plain JavaScript Object。Thunk允許action creator返回一個函數(shù),而且這個函數(shù)第一個參數(shù)是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個組件是服務器端相關(guān)的。
然后通過react-redux配置了Provider組件,這個組件包裹在整個組件樹的最外層。這個組件讓根組件的所有子孫組件能夠輕松的使用 connect() 方法綁定 store。Provider 本質(zhì)上創(chuàng)建了一個用于更新視圖組件的網(wǎng)絡(luò)。那些智能組件通過 connect() 方法連入這個網(wǎng)絡(luò),以此確保他們能夠獲取到狀態(tài)的更新。
configureStore提供了對Store的創(chuàng)建和配置,由于Redux只有一個store,如果讓store 完全獨立處理自己的事,store會變的很復雜。因此,Redux 中的 store 首先會保存整個應用的所有狀態(tài),然后將「判斷哪一部分狀態(tài)需要改變」的任務分配下去。而以根 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使用了柯里化方法調(diào)用了applyMiddleware,middleware我們可以簡單的理解成過濾器,作用就是加入一些中間處理過程。最后返回store對象。

用戶登錄流程代碼分析

下面分析登錄頁面的代碼,代碼在login目錄下,包括LoginModal.js和LoginScreen.js,實現(xiàn)了通過Oauth登錄Facebook帳號的功能。
登錄涉及的代碼有actions/types.js(定義了所有的Action事件), actions/login.js(實現(xiàn)登錄業(yè)務邏輯,與服務器交互),js/reducers/user.js(實現(xiàn)對用戶相關(guān)狀態(tài)的計算)。
登錄的入口是js/tabs/schedule/logIn.js,142行定義了<LoginButton source="My F8" /> ,LoginButton組件封裝了登錄UI相關(guān)的邏輯。
點擊LoginButton后會調(diào)用logIn函數(shù),logIn函數(shù)會調(diào)用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的實現(xiàn)在js/actions/login.js中,如果登錄成功會通過Promise異步獲取好友的日程和調(diào)查問卷。

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;
  };
}

登錄是調(diào)用Facebook SDK進行登錄,logInWithFacebook是個異步方法,用到了ES6的async,
async function _logInWithFacebook(source: ?string): Promise<Array<Action>> {...}
返回值是個Promise,在then方面里面異步調(diào)用loadFriendsSchedules,loadSurveys。
這些方法會繼續(xù)請求數(shù)據(jù),并更新store,從而讓頁面更新。

總結(jié)

js部分的代碼用了很多ES6的新語法和函數(shù)式編程思想,特別是使用了Redux框架,代碼量也比較大,分析和理解起來比較困難,本文只分析了部分典型模塊的代碼。特別是在相關(guān)的技術(shù)和框架了解程度不夠深入,缺少實際開發(fā)經(jīng)驗的情況下(這說的就是我自己啊)。建議看代碼之前先把JavaScript ES6和Redux框架好好學習一下。雖然代碼看上去很難,但整個處理流程和模塊劃分還是很清晰的。

參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容