React-Native 从零搭建 App

发布于 2022-11-19 15:31:12 字数 25619 浏览 149 评论 0

一、须知

全文技术栈 

  • 核心库:React-Native@0.54.0 
  • 路由导航:React-Native-Navigation 
  • 状态管理:Redux、Redux-Thunk、Redux-Saga、Redux-persist 
  • 静态测试:Flow  

本文适合有对React家族有一定使用经验,但对从零配置一个App不是很熟悉,又想要从零体验一把搭建App的同学。

我自己就是这种情况,中途参与到项目中,一直没有掌控全局的感觉,所以这次趁着项目重构的机会,自己也跟着从零配置了一遍,并记录了下来,希望能跟同学们一起学习,如果有说错的地方,也希望大家指出来,或者有更好的改进方式,欢迎交流。 

如果有时间的同学,跟着亲手做一遍是最好的,对于如何搭建一个真实项目比较有帮助。

整个项目已经上传到github,懒的动手的同学可以直接clone下来跟着看,欢迎一起完善,目前的初步想法是对一部分的同学有所帮助,后面有时间的话,可能会完善成一个比较健壮的RN基础框架,可以直接clone就开发项目那种

该项目 github 仓库传送门

这里对每个库或者内容只做配置和基础用法介绍  

物理环境:mac,xcode 

window系统的同学也可以看,不过需要自己搞好模拟器开发环境

二、快速建立一个RN App

React-native 官网

如果RN的基础配置环境没有配置好,请点击上方链接到官网进行配置

react-native init ReactNativeNavigationDemo
cd ReactNativeNavigationDemo
react-native run-ios

因为一开始就计划好了用React-Native-Navigation作为导航库,所以名字起得长了点,大家起个自己喜欢的吧

成功后会看到这个界面

这时候可以看下目录结构,RN自动集成了babel、git、flow的配置文件,还是很方便的

三、路由导航:React-Native-Navigation

React Native Navigation

为什么用React Native Navigation而不用React Navigation ?

它是目前唯一一款使用原生代码来实现navigator的插件,使用后navigator的push/pop的动画将脱离js线程而改由原生的UI线程处理, 切屏效果会和原生态一样流畅, 再也不会出现由于js线程渲染导致的navigator切屏动画的卡顿效果了, 并且该插件还同时内置实现了原生态版本的tabbar

英文好的同学看着官方文档配就可以了,实在看不懂的可以对照着我下面的图看。 

iOS的需要用到xcode,没做过的可能会觉得有点复杂,所以我跑了一遍流程并截图出来了 至于android的配置,文档写的很清晰,就不跑了。

1、安装

yarn add react-native-navigation@latest

2、添加 xcode 工程文件

图中的路径文件是指./node_modules/react-native-navigation/ios/ReactNativeNavigation.xcodeproj

3、把上面添加的工程文件添加到库中

4、添加路径

$(SRCROOT)/../node_modules/react-native-navigation/ios记得图中第5点设置为recursive

5、修改 ios/[app name]/AppDelegate.m 文件

把整个文件内容替换成下面代码

#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

6、基础使用

1、先新建几个页面,结构如图

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

2、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)
}

3、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,目前模拟器能看到的界面

7、页面跳转和传递参数

在screen/home文件夹下面新建一个NextPage.js文件,记得到src/screen/index.js里面注册该页面

Navigation.registerComponent('nextPage', () => NextPage, store, Provider);

然后在src/screen/home/index.js文件里面加一个跳转按钮,并传递一个props数据

四、状态管理:Redux

redux中文文档

1、初始化

1、安装

yarn add redux react-redux

2、目录构建

目前有以下两种常见的目录构建方式

 一是把同一个页面的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 

这里不讨论正确的书写顺序,我就暂且按照自己的习惯来写吧

3、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对象

4、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

5、创建store

创建好reducer之后,打开store/index.js文件

import {createStore } from 'redux';

import combineReducers from '../reducer/index';

const store = createStore(combineReducers)

export default store;

就是这么简单

6、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);

2、体验Redux

现在来体验一下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

这个时候,我们会发现,虽然状态共享了,但目前还没有办法跟踪状态,以及每一步操作带来的状态变化。

 但总不能每次都手动打印状态到控制台里面吧?

redux-logger该上场了

它大概长下面这样,把每次派发action的前后状态都自动输出到控制台上

具体使用看下官方文档,很简单,直接上代码吧

redux-logger

安装

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是什么请移步 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

注入store

作为一个中间件,它的使用方式和上面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

此处稍微插入一句,关于封装请求函数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 => {
    // 处理错误
  })
}

七、异步管理:Redux-Saga

基本概念请移步

自述 | Redux-saga 中文文档

出发点:需要声明式地来表述复杂异步数据流(如长流程表单,请求失败后重试等),命令式的 thunk 对于复杂异步数据流的表现力有限

安装

yarn add redux-saga

创建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]);
}

把saga注入store

store/index.js文件修改

import createSagaMiddleware from 'redux-saga';
import rootSaga from '../saga/index';
// 生成saga中间件
const sagaMiddleware = createSagaMiddleware(rootSaga);

middleware.push(sagaMiddleware);

八:数据持久化:Redux-persist

GitHub - rt2zz/redux-persist: persist and rehydrate a redux store

顾名思义,数据持久化,一般用来保存登录信息等需要保存在本地的数据,因为store中的数据,在每次重新打开app后,都会回复到reducer中的initState的初始状态,所以像登录信息这种数据就需要持久化的存储了。 

RN自带的AsyncStorage可以实现这个功能,但使用起来比较繁琐,而且没有注入到store中去,没办法实现统一状态管理,所以redux-persist就出场了

安装

yarn add redux-persist

注入store

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(() => { });

九:静态测试:Flow

待更新,这有一篇链接可以先看 React Native 填坑之旅--Flow 篇(番外)

十、后话

到目前为止,我们已经引入了 redux-logger、redux-thunk、redux-saga、redux-persist,核心开发代码库已经配置完毕了。

该项目github仓库传送门

接下来还有一些可以作为开发时的辅助性配置,比如 Flow 、Babel(RN初始化时已经配好了)、Eslint 等等另外,既然是 App,那最终目的当然就是要上架 App Store 和各大安卓市场,后面可能还会分享一下关于极光推送 jPush、热更新 CodePush、打包上传审核等方面的内容。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

滥情空心

暂无简介

文章
评论
809 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文