全文技术栈 html
本文适合有对React家族有必定使用经验,但对从零配置一个App不是很熟悉,又想要从零体验一把搭建App的同窗。前端
我本身就是这种状况,中途参与到项目中,一直没有掌控全局的感受,因此此次趁着项目重构的机会,本身也跟着从零配置了一遍,并记录了下来,但愿能跟同窗们一块儿学习,若是有说错的地方,也但愿你们指出来,或者有更好的改进方式,欢迎交流。 node
若是有时间的同窗,跟着亲手作一遍是最好的,对于如何搭建一个真实项目比较有帮助。
react
整个项目已经上传到github,懒的动手的同窗能够直接clone下来跟着看,欢迎一块儿完善,目前的初步想法是对一部分的同窗有所帮助,后面有时间的话,可能会完善成一个比较健壮的RN基础框架,能够直接clone就开发项目那种android
这里对每一个库或者内容只作配置和基础用法介绍 git
物理环境:mac,xcode github
window系统的同窗也能够看,不过须要本身搞好模拟器开发环境
json
React-native官网redux
若是RN的基础配置环境没有配置好,请点击上方连接到官网进行配置
react-native init ReactNativeNavigationDemo
cd ReactNativeNavigationDemo
react-native run-ios
复制代码
由于一开始就计划好了用React-Native-Navigation做为导航库,因此名字起得长了点,你们起个本身喜欢的吧
成功后会看到这个界面
这时候能够看下目录结构,RN自动集成了babel、git、flow的配置文件,仍是很方便的
为何用React Native Navigation而不用React Navigation ?
它是目前惟一一款使用原生代码来实现navigator的插件,使用后navigator的push/pop的动画将脱离js线程而改由原生的UI线程处理, 切屏效果会和原生态同样流畅, 不再会出现因为js线程渲染致使的navigator切屏动画的卡顿效果了, 而且该插件还同时内置实现了原生态版本的tabbar
英文好的同窗看着官方文档配就能够了,实在看不懂的能够对照着我下面的图看。
iOS的须要用到xcode,没作过的可能会以为有点复杂,因此我跑了一遍流程并截图出来了 至于android的配置,文档写的很清晰,就不跑了。
yarn add react-native-navigation@latest复制代码
图中的路径文件是指./node_modules/react-native-navigation/ios/ReactNativeNavigation.xcodeproj
$(SRCROOT)/../node_modules/react-native-navigation/ios
记得图中第5点设置为recursive
把整个文件内容替换成下面代码
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import "RCCManager.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSURL *jsCodeLocation;
#ifdef DEBUG
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
[[RCCManager sharedInstance] initBridgeWithBundleURL:jsCodeLocation launchOptions:launchOptions];
return YES;
}
@end
复制代码
一、先新建几个页面,结构如图
cd src
mkdir home mine popularize
touch home/index.js mine/index.js popularize/index.js
复制代码
每一个index.js
文件里面都是同样的结构,很是简单
import React, { Component } from 'react';
import { Text, View } from 'react-native';
type Props = {};
export default class MineHome extends Component<Props> {
render() {
return (
<View>
<Text>MineHome</Text>
</View>
);
}
}
复制代码
二、src/index.js
注册全部的页面,统一管理
import { Navigation } from 'react-native-navigation';
import Home from './home/index';
import PopularizeHome from './popularize/index';
import MineHome from './mine/index';
// 注册全部的页面
export function registerScreens() {
Navigation.registerComponent('home',() => home);
Navigation.registerComponent('popularize',() => popularize);
Navigation.registerComponent('mine',() => mine);
}
复制代码
在这里先插一句,若是要引入Redux的话,就在这里直接传入store和Provider
export function registerScreens(store,Provider) {
Navigation.registerComponent('home',() => PageOne,store,Provider)
}
复制代码
三、App.js
文件修改app的启动方式,并稍微修改一下页面样式
import { Navigation } from 'react-native-navigation';
import { registerScreens } from './src/screen/index';
// 执行注册页面方法
registerScreens();
// 启动app
Navigation.startTabBasedApp({
tabs: [
{
label: 'home',
screen: 'home',
title: '首页',
icon: require('./src/assets/home.png'),
},
{
screen: 'popularize',
title: '推广',
icon: require('./src/assets/add.png'),
iconInsets: {
top: 5,
left: 0,
bottom: -5,
right: 0
},
},
{
label: 'mine',
screen: 'mine',
title: '我',
icon: require('./src/assets/mine.png'),
}
],
appStyle: {
navBarBackgroundColor: '#263136',//顶部导航栏背景颜色
navBarTextColor: 'white'//顶部导航栏字体颜色
},
tabsStyle: {
tabBarButtonColor: '#ccc',//底部按钮颜色
tabBarSelectedButtonColor: '#08cb6a',//底部按钮选择状态颜色
tabBarBackgroundColor: '#E6E6E6'//顶部条背景颜色
}
});
复制代码
启动App,目前模拟器能看到的界面
在screen/home文件夹下面新建一个NextPage.js
文件,记获得src/screen/index.js
里面注册该页面
Navigation.registerComponent('nextPage', () => NextPage, store, Provider);复制代码
而后在src/screen/home/index.js
文件里面加一个跳转按钮,并传递一个props数据
一、安装
yarn add redux react-redux复制代码
二、目录构建
目前有如下两种常见的目录构建方式
一是把同一个页面的action和reducer写在同一个文件夹下面(能够称之为组件化),以下
二是把全部的action放在一个文件夹,全部的reducer放在一个文件夹,统一管理
这两种方式各有好坏,不在此探究,这里我用第二种
一通操做猛如虎,先创建各类文件夹和文件
cd src
mkdir action reducer store
touch action/index.js reducer/index.js store/index.js
touch action/home.js action/mine.js action/popularize.js
touch reducer/home.js reducer/mine.js reducer/popularize.js
复制代码
以上命令敲完后,目录结构应该长下面这样,每一个页面都分别拥有本身的action和reducer文件,但都由index.js
文件集中管理输出
关于建立这三块内容的前后顺序,理论上来讲,应该是先有store,而后有reducer,再到action
但写的多了以后,就比较随心了,那个顺手就先写哪一个。
按照我本身的习惯,我喜欢从无写到有,好比说 store里面要引入合并后的reducer,那我就会先去把reducer给写了
import combinedReducer from '../reducer'复制代码
但写reducer以前,好像又须要先引入action,因此我由可能跑去先写action
这里不讨论正确的书写顺序,我就暂且按照本身的习惯来写吧
三、action
我喜欢集中管理的模式,因此全部的antion我都会集中起来 index.js
文件做为总的输出口
这里定义了全部的action-type常量
// home页面
export const HOME_ADD = 'HOME_ADD';
export const HOME_CUT = 'HOME_CUT';
// mine页面
export const MINE_ADD = 'MINE_ADD';
export const MINE_CUT = 'MINE_CUT';
// popularize页面
export const POPULARIZE_ADD = 'POPULARIZE_ADD';
export const POPULARIZE_CUT = 'POPULARIZE_CUT';
复制代码
而后去写其余各自页面的action.js文件,这里只以home页面做为例子,其余页面就不写了,打开action/home.js
文件
import * as actionTypes from './index';
export function homeAdd(num) {
return {
type: actionTypes.HOME_ADD,
num
}
}
export function homeCut(num) {
return {
type: actionTypes.HOME_CUT,
num
}
}
复制代码
最是返回了一个最简单的action对象
四、reducer
先写一个home页面的reducer,打开reducer/home.js
文件 其余页面也同理
import * as actionTypes from '../action/index';
// 初始state,我先随手定义了几个,后面可能会用到
const initState = {
initCount: 0,
name: '',
age: '',
job: ''
}
export default function count(state = initState, action) {
switch (action.type) {
case actionTypes.HOME_ADD:
return {
...state,
...action.initCount:
}
case actionTypes.HOME_CUT:
return {
...state,
...action.initCount
}
default:
return state;
}
}
复制代码
而后把全部子reducer页面合并到reducer/index.js
文件进行集中输出
import homeReducer from './home';
import popularizeReducer from './popularize';
import mineReducer from './mine';
const combineReducers = {
home: homeReducer,
popularize: popularizeReducer,
mine: mineReducer
}
export default combineReducers复制代码
五、建立store
建立好reducer以后,打开store/index.js
文件
import {createStore } from 'redux';
import combineReducers from '../reducer/index';
const store = createStore(combineReducers)
export default store;
复制代码
就是这么简单
六、store注入
使用过redux的同窗都知道,react-redux上场了,它提供了Provider和connect方法
前面有提到react-native-navigation注入redux的方式,其实差很少 但须要每一个子页面都注入store、Provider
src/index.js
修改以下
import { Navigation } from 'react-native-navigation';
import Home from './home/index';
import PopularizeHome from './popularize/index';
import MineHome from './mine/index';
// 注册全部的页面
export function registerScreens(store, Provider) {
Navigation.registerComponent('home', () => Home, store, Provider);
Navigation.registerComponent('popularize', () => PopularizeHome, store, Provider);
Navigation.registerComponent('mine', () => MineHome, store, Provider);
}
复制代码
App.js
修改执行页面注册的方法便可
import { Provider } from 'react-redux';
import store from './src/store/index';
// 执行注册页面方法
registerScreens(store, Provider);
复制代码
如今来体验一下redux,打开src/screen/home/index.js
文件
import两个方法
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
复制代码
导入action
import * as homeActions from '../../action/home';
复制代码
定义两个方法,并connect起来
function mapStateToProps(state) {
return {
home: state.home
};
}
function mapDispatchToProps(dispatch) {
return {
homeActions: bindActionCreators(homeActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
复制代码
如今在页面上打印initCount
来看一下,只要被connect过的组件,以及从该组件经过路由push跳转的子页面均可以经过this.props
拿到数据
src/screen/home/index.js
完整代码以下
import React, { Component } from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as homeActions from '../../action/home';
type Props = {};
class Home extends Component<Props> {
render() {
return (
<View style={styles.container}>
<Text>Home</Text>
<Text>initCount: {this.props.home.initCount}</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#ccc',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
})
function mapStateToProps(state) {
return {
home: state.home
};
}
function mapDispatchToProps(dispatch) {
return {
homeActions: bindActionCreators(homeActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
复制代码
从页面能够看到,已经读到状态树里面的数据了,initCount为0
让咱们再来试一下action的加法和减法
src/screen/home/index.js
完整代码以下
import React, { Component } from 'react';
import { Text, View, StyleSheet, TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as homeActions from '../../action/home';
type Props = {};
class Home extends Component<Props> {
render() {
return (
<View style={styles.container}>
<Text>Home</Text>
<Text>initCount: {this.props.home.initCount}</Text>
<TouchableOpacity
style={styles.addBtn}
onPress={() => {
this.props.homeActions.homeAdd({
initCount: this.props.home.initCount + 2
});
}}
>
<Text style={styles.btnText}>加2</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.cutBtn}
onPress={() => {
this.props.homeActions.homeCut({
initCount: this.props.home.initCount - 2
});
}}
>
<Text style={styles.btnText}>减2</Text>
</TouchableOpacity>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#ccc',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
addBtn: {
backgroundColor: 'green',
marginVertical: 20,
width: 200,
height: 59,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 10
},
cutBtn: {
backgroundColor: 'red',
width: 200,
height: 59,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 10
},
btnText: {
fontSize: 18,
color: 'white'
}
});
function mapStateToProps(state) {
return {
home: state.home
};
}
function mapDispatchToProps(dispatch) {
return {
homeActions: bindActionCreators(homeActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
复制代码
如今点击两个按钮都应该能获得反馈
如今再来验证下一个东西,这个页面改完store里面的状态后,另外一个页面mine会不会同步,也就是全局数据有没有共享了
src/mine/index.js
文件修改以下
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as homeActions from '../../action/home';
type Props = {};
class MineHome extends Component<Props> {
render() {
return (
<View>
<Text>initCount: {this.props.home.initCount}</Text>
</View>
);
}
}
function mapStateToProps(state) {
return {
home: state.home
};
}
function mapDispatchToProps(dispatch) {
return {
homeActions: bindActionCreators(homeActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(MineHome);
复制代码
在该页面上读取同一个数据this.props.home.initCount
,而后在第一个页面home上加减数据,再看mine页面,会发现initCount也同步变化 也就是说明:咱们已经在进行状态管理了
到这里是否是很开心,redux虽然有点绕,但若是跟着作下来应该也有了必定的轮廓了
这个时候,咱们会发现,虽然状态共享了,但目前尚未办法跟踪状态,以及每一步操做带来的状态变化。
但总不能每次都手动打印状态到控制台里面吧?
redux-logger该上场了
它大概长下面这样,把每次派发action的先后状态都自动输出到控制台上
具体使用看下官方文档,很简单,直接上代码吧
安装
yarn add redux-logger
复制代码
它做为一个中间件,中间件的用法请回 redux中文文档 查阅
store/index.js
文件修改以下
import { createStore, applyMiddleware } from 'redux';
import combineReducers from '../reducer/index';
import logger from 'redux-logger';
const store = createStore(combineReducers, applyMiddleware(logger));
export default store;
复制代码
command+R刷新一下模拟器,再点击一下+2,看看控制台是否是长下面这样?
接下来每次派发action,控制台都会自动打印出来,是否是省心省事?
redux-thunk是什么请移步 redux-thunk github
出发点:须要组件对同步或异步的 action 无感,调用异步 action 时不须要显式地传入 dispatch
经过使用指定的 middleware,action 建立函数除了返回 action 对象外还能够返回函数。这时,这个 action 建立函数就成为了 thunk
当 action 建立函数返回函数时,这个函数会被 Redux Thunk middleware 执行。这个函数并不须要保持纯净;它还能够带有反作用,包括执行异步 API 请求。这个函数还能够 dispatch action,就像 dispatch 前面定义的同步 action 同样 > thunk 的一个优势是它的结果能够再次被 dispatch
yarn add redux-thunk
复制代码
做为一个中间件,它的使用方式和上面logger同样,stroe/index.js
直接引入便可
import thunk from 'redux-thunk';
middleware.push(thunk);
复制代码
action/home.js
文件修改以下
import post from '../utils/fetch';
export function getSomeData() {
return dispatch => {
post('/get/data',{}, res => {
const someData = res.data.someData;
dispatch({
type: actionTypes.HOME_GET_SOMEDATA,
someData
})
})
}
}复制代码
此处稍微插入一句,关于封装请求函数post(如下是精简版,只保留了核心思想)
cd src
mkdir utils
touch utils/fetch.js
复制代码
公用的方法和函数都封装在utils文件夹中
utils/fetch.js
文件以下
export default function post(url, data, sucCB, errCB) {
// 域名、body、header等根据各自项目配置,还有部分安全,加密方面的设置,
const host = 'www.host.com';
const requestUrl = `${host}/${url}`;
const body = {};
const headers = {
'Content-Type': 'application/json',
'User-Agent': ''
};
// 用的是fetch函数
fetch(requestUrl, {
method: 'POST',
headers: headers,
body: body
}).then(res => {
if (res && res.status === 200) {
return res.json();
} else {
throw new Error('server');
}
}).then(res => {
// 精简版判断
if(res && res.code === 200 && res.enmsg === 'ok') {
// 成功后的回调
sucCB(res);
}else {
// 失败后的回调
errCB(res);
}
}).catch(err => {
// 处理错误
})
}
复制代码
基本概念请移步
出发点:须要声明式地来表述复杂异步数据流(如长流程表单,请求失败后重试等),命令式的 thunk 对于复杂异步数据流的表现力有限
yarn add redux-saga复制代码
建立顺序有点像reducer 咱们先建立saga相关文件夹和文件,最后再来注入store里面
cd src
mkdir saga
touch saga/index.js saga/home.js saga/popularize.js saga/mine.js复制代码
先修改saga/home.js
文件
import { put, call, takeLatest } from 'redux-saga/effects';
import * as actionTypes from '../action/index';
import * as homeActions from '../action/home';
import * as mineActions from '../action/mine';
import post from '../utils/fetch';
function getSomeThing() {
post('/someData', {}, res => {}, err => {});
}
// 这个函数中的请求方法都是随手写的,没引入真实API,
function* getUserInfo({ sucCb, errCB }) {
try {
const res = yield call(getSomeThing());
const data = res.data;
yield put(homeActions.getSomeData())
yield put(homeActions.setSomeData(data))
yield call(sucCb);
} catch (err) {
yield call(errCB, err);
}
}
export const homeSagas = [
takeLatest(actionTypes.HOME_GET_SOMEDATA, getUserInfo)
]
复制代码
saga/mine.js
文件
export const mineSagas = []
复制代码
saga/popularize.js
文件
export const popularizeSagas = []
复制代码
saga/index.js
文件做为总输出口,修改以下
import { all } from 'redux-saga/effects';
import { homeSagas } from './home';
import { mineSagas } from './mine';
import { popularizeSagas } from './popularize';
export default function* rootSaga() {
yield all([...homeSagas, ...mineSagas, ...popularizeSagas]);
}
复制代码
store/index.js
文件修改
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../saga/index';
// 生成saga中间件
const sagaMiddleware = createSagaMiddleware(rootSaga);
middleware.push(sagaMiddleware);
复制代码
GitHub - rt2zz/redux-persist: persist and rehydrate a redux store
顾名思义,数据持久化,通常用来保存登陆信息等须要保存在本地的数据
由于store中的数据,在每次从新打开app后,都会回复到reducer中的initState的初始状态,因此像登陆信息这种数据就须要持久化的存储了。
RN自带的AsyncStorage能够实现这个功能,但使用起来比较繁琐,并且没有注入到store中去,没办法实现统一状态管理,因此redux-persist就出场了
yarn add redux-persist复制代码
store/index.js
文件完整代码以下
import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import createSagaMiddleware from 'redux-saga';
import { persistStore, persistCombineReducers } from 'redux-persist';
import storage from 'redux-persist/es/storage';
import combineReducers from '../reducer/index';
import rootSaga from '../saga/index';
const persistConfig = {
key: 'root',
storage,
// 白名单:只有mine的数据会被persist
whitelist: ['mine']
};
// 对reducer数据进行persist配置
const persistReducer = persistCombineReducers(persistConfig, combineReducers);
const sagaMiddleware = createSagaMiddleware();
// 中间件
const createStoreWithMiddleware = applyMiddleware(
thunk,
sagaMiddleware,
logger
)(createStore);
const configuerStore = onComplete => {
let store = createStoreWithMiddleware(persistReducer);
let persistor = persistStore(store, null, onComplete);
sagaMiddleware.run(rootSaga);
return { persistor, store };
};
export default configuerStore;
复制代码
这个地方,再也不把middleware当作数组,而是直接写入applyMiddleware方法中
store也再也不直接导出,而是处处一个生成store的函数configuerStore 相应,
App.js
文件的引入也要修改一点点
import configuerStore from './src/store/index';
const { store } = configuerStore(() => { });复制代码
待更新,这有一篇连接能够先看React Native填坑之旅--Flow篇(番外)
到目前为止,咱们已经引入了redux-logger、redux-thunk、redux-saga、redux-persist
核心开发代码库已经配置完毕了
项目已经传上github,欢迎star
接下来还有一些能够做为开发时的辅助性配置,好比Flow 、Babel(RN初始化时已经配好了)、Eslint等等
另外,既然是App,那最终目的固然就是要上架App Store和各大安卓市场,后面可能还会分享一下关于极光推送jPush、热更新CodePush、打包上传审核等方面的内容。
感谢您耐心看到这里,但愿有所收获!
若是不是很忙的话,麻烦点个star⭐【Github博客传送门】,举手之劳,倒是对做者莫大的鼓励。
我在学习过程当中喜欢作记录,分享的是本身在前端之路上的一些积累和思考,但愿能跟你们一块儿交流与进步,更多文章请看【amandakelake的Github博客】