本文开始分析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我们才有能力分析f8app js的代码。
理解 React,但不理解 Redux,该如何通俗易懂的理解 Redux?
解答这个问题并不困难:唯一的要求是你熟悉React。
不要光听别人描述名词,理解起来是很困难的。
从需求出发,看看使用React需要什么:
- React有props和state: props意味着父级分发下来的属性,state意味着组件内部可以自行管理的状态,并且整个React没有数据向上回溯的能力,也就是说数据只能单向向下分发,或者自行内部消化。
理解这个是理解React和Redux的前提。 - 一般构建的React组件内部可能是一个完整的应用,它自己工作良好,你可以通过属性作为API控制它。但是更多的时候发现React根本无法让两个组件互相交流,使用对方的数据。
然后这时候不通过DOM沟通(也就是React体制内)解决的唯一办法就是提升state,将state放到共有的父组件中来管理,再作为props分发回子组件。 - 子组件改变父组件state的办法只能是通过onClick触发父组件声明好的回调,也就是父组件提前声明好函数或方法作为契约描述自己的state将如何变化,再将它同样作为属性交给子组件使用。
这样就出现了一个模式:数据总是单向从顶层向下分发的,但是只有子组件回调在概念上可以回到state顶层影响数据。这样state一定程度上是响应式的。 - 为了面临所有可能的扩展问题,最容易想到的办法就是把所有state集中放到所有组件顶层,然后分发给所有组件。
- 为了有更好的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框架好好学习一下。虽然代码看上去很难,但整个处理流程和模块划分还是很清晰的。