本文开始分析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个文件内容一致。代码如下:1
2
3
4
5
6 ;
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目录的结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140├── 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.
1 | // calculation of 1 + 2 is immediate |
setup.js代码分析
熟悉React Native都知道,index.android.js和index.ios.js分别是Android和iOS App的js程序入口,当然实际运行是压缩处理后的jsbundle。这个2个文件都是注册了setup组件,AppRegistry.registerComponent('F8v2', setup);
。
setup.js负责配置其它的组件,具体代码如下:
1 | //js/setup.js |
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 们将会承担这个任务。
1 | // ./js/store/configureStore.js |
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的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23async 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异步获取好友的日程和调查问卷。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function 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框架好好学习一下。虽然代码看上去很难,但整个处理流程和模块划分还是很清晰的。
参考文章
- Redux 中文文档
- 《看漫画,学 Redux》 —— A cartoon intro to Redux
- redux源码详解
- 使用Redux管理你的React应用
- 理解 React,但不理解 Redux,该如何通俗易懂的理解 Redux
- 学习 redux.js 的 流水账
[本文独立博客地址](http://www.offbye.com/)