本文開始分析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我們才有能力分析f8app js的代碼。
理解 React,但不理解 Redux,該如何通俗易懂的理解 Redux?
解答這個問題并不困難:唯一的要求是你熟悉React。
不要光聽別人描述名詞,理解起來是很困難的。
從需求出發(fā),看看使用React需要什么:
- React有props和state: props意味著父級分發(fā)下來的屬性,state意味著組件內(nèi)部可以自行管理的狀態(tài),并且整個React沒有數(shù)據(jù)向上回溯的能力,也就是說數(shù)據(jù)只能單向向下分發(fā),或者自行內(nèi)部消化。
理解這個是理解React和Redux的前提。 - 一般構(gòu)建的React組件內(nèi)部可能是一個完整的應用,它自己工作良好,你可以通過屬性作為API控制它。但是更多的時候發(fā)現(xiàn)React根本無法讓兩個組件互相交流,使用對方的數(shù)據(jù)。
然后這時候不通過DOM溝通(也就是React體制內(nèi))解決的唯一辦法就是提升state,將state放到共有的父組件中來管理,再作為props分發(fā)回子組件。 - 子組件改變父組件state的辦法只能是通過onClick觸發(fā)父組件聲明好的回調(diào),也就是父組件提前聲明好函數(shù)或方法作為契約描述自己的state將如何變化,再將它同樣作為屬性交給子組件使用。
這樣就出現(xiàn)了一個模式:數(shù)據(jù)總是單向從頂層向下分發(fā)的,但是只有子組件回調(diào)在概念上可以回到state頂層影響數(shù)據(jù)。這樣state一定程度上是響應式的。 - 為了面臨所有可能的擴展問題,最容易想到的辦法就是把所有state集中放到所有組件頂層,然后分發(fā)給所有組件。
- 為了有更好的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框架好好學習一下。雖然代碼看上去很難,但整個處理流程和模塊劃分還是很清晰的。