环境搭建
本次开发环境
node: v8.9.4
npm: 5.6.0
react: 16.0.0
react-native: 0.51.0
代码编辑器:webstorm
模拟器:ios => siMulator or 真机 / android => Android Studio自带模拟器 or 真机
- 安装Chocolatey,并使用Chocolatey安装python2和node.js
cmd下输入如下命令安装Chocolatey(或需翻墙)
@powershell -NoProfile -ExecutionPolicy Bypass -Command "iex ((new-object net.webclient).DownloadString('https://chocolatey.org/install.ps1'))" && SET PATH=%PATH%;%ALLUSERSPROFILE%\chocolatey\bin
安装python2
choco install python2
安装node.js
choco install nodejs.install
设置npm镜像
npm config set registry https://registry.npm.taobao.org --global
npm config set disturl https://npm.taobao.org/dist --global
- Yarn、React Native的命令行工具
安装react-native脚手架
npm install -g yarn react-native-cli
配置yarn镜像
yarn config set registry https://registry.npm.taobao.org --global
yarn config set disturl https://npm.taobao.org/dist --global
- 安装Android SDK 并 配置ANDROID_HOME
- 创建并初始化项目,运行
react-native init projectDemo
cd projectDemo
react-native run-ios // ios启动
react-native run-android // android启动
最终生成项目目录
android: 安卓开发源码
ios: ios开发源码
node_modules: 项目依赖
.babelrc: babel的配置文件,将各种js版本的语言转换成运行环境所能识别的
App.js: react-native入口模板文件,供index.js使用,非固定
index.js: 项目入口文件
package.json 依赖配置文件
根据项目需求小做修改
可以看到我们为前端专门创建了一个项目文件src,并将 react-native入口模板文件迁移到项目文件夹里。根据项目需求分包
assets: 资源目录
imgs: 图片资源
styles: 样式资源
|—common.js: 公用样式资源
|—color.js: 定义项目颜色
|—layout.js: 定于项目布局
|—size.js: 定义项目尺寸
|—index.js: 整合以上资源
|—entry.js 对应页面路由样式资源
utils: 工具资源
|—api.js: 集成了整个项目的请求
|—http.js: 封装了fetch请求
|—screen.js: 有关屏幕的各种属性资源
|—system.js: 有关系统的各种资源
|—utils.js: 其他工具封装
components: 全局组件封装
pages: 页面
router: 路由
store: 全局状态管理
App.js: 入口模板文件
PS:编译错误一般会提示在node.js中,运行错误手机或模拟器弹出红色错误页面。修改js代码无需重装,点击红色错误页面的【RELOAD】即可或摇动手机弹出开发菜单【RELOAD】
组件 控件
RN中每个页面都是由【Component 】组成,也可用【Component 】来做组合控件
基本组件代码
//引入React Component
import React, {Component} from 'react'
//引入控件
import {
View,
Text,
} from 'react-native'
//变量的定义只能放Component不可放Component里面,否则调用报错。函数可以放Component里面
let val = 1;
//export default 才可以把这个组件暴露出去,否则其他文件调用会报错(提示找不到Demo组件或者找到的Demo组件不是Component)
export default class Demo extends Component {
//构造函数,props作为父子控件直接传递数据,后面讲
constructor(props) {
super(props)
}
//渲染 最终的UI效果在此
render() {
return (
<View>
<Text style={{marginTop: 20, fontSize: 16}}>react-native通用基础模板</Text>
<View>
{/*<Text>{listStore.num}</Text>*/}
</View>
</View>
)
}
}
常用控件
Text Button Image TextInput ScrollView ListView ,具体控件和属性、方法参考 React Native 中文网文档
布局 与 导航
布局 在控件的style中直接设置,共有5种属性,可搭配使用(正常是Flex Direction确认主轴后搭配其他)
<View style={{flex: 1, flexDirection: 'row'}}>
Flex Direction:确定主轴即确定布局方向,可以有从左往右【row】、从上往下【column】、从右往左【row-reverse】、从下往上【column-reverse】
Justify Content:沿着主轴的排列方式,可以有:flex-start、center、flex-end、space-around以及space-between
Align Items:决定其子元素沿着次轴(与主轴垂直的轴,比如若主轴方向为row,则次轴方向为column)的排列方式,可以有flex-start、center、flex-end以及stretch
Flex wrap:根据子元素内容大小自适应布局。可以有nowrap、warp、wrap-reverse
Align Content:根据子元素内容大小自适应布局。可以有stretch、flexstart、center、flex-end、space-between、space-around
导航 :路由导航器【StackNavigator】 Tab选项卡【TabNavigator】侧滑抽屉【DrawerNavigator】
【StackNavigator】 组件采用堆栈式的页面导航来实现各个界面跳转。它的构造函数:
StackNavigator(RouteConfigs, StackNavigatorConfig)
RouteConfigs 参数表示各个页面路由配置
StackNavigatorConfig 参数表示导航器的配置
const RouteConfigs = {
Home: {
screen: HomePage,
navigationOptions: ({navigation}) => ({
title: '首页',
}),
},
Find: {
screen: FindPage,
navigationOptions: ({navigation}) => ({
title: '发现',
}),
},
Mine: {
screen: MinePage,
navigationOptions: ({navigation}) => ({
title: '我的',
}),
},
};
const StackNavigatorConfig = {
initialRouteName: 'Home',
initialRouteParams: {initPara: '初始页面参数'},
navigationOptions: {
title: '标题',
headerTitleStyle: {fontSize: 18, color: '#666666'},
headerStyle: {height: 48, backgroundColor: '#fff'},
},
mode: 'card',
headerMode: 'screen',
cardStyle: {backgroundColor: "#ffffff"},
transitionConfig: (() => ({
screenInterpolator: CardStackStyleInterpolator.forHorizontal,
})),
onTransitionStart: (() => {
console.log('页面跳转动画开始');
}),
onTransitionEnd: (() => {
console.log('页面跳转动画结束');
}),
};
navigation:导航器对象,在导航器中的每一个页面,都有 navigation 属性,该属性有以下几个属性/方法:
navigate - 跳转到其他页面
state - 当前页面导航器的状态
setParams - 更改路由的参数
goBack - 返回
dispatch - 发送一个action
跳转示例
this.props.navigation.navigate('MinePage', { key: 'val' })
【TabNavigator】
构造函数
TabNavigator(RouteConfigs, TabNavigatorConfig)
api和 StackNavigator 类似
参数 RouteConfigs 是路由配置,
参数 TabNavigatorConfig是Tab选项卡配置。
【DrawerNavigator】
基本配置和上面两者api相似
属性(Props) 与 状态(State)
props 组件属性,只可以用来保存组件间数据传递
eg:父组件传递参数给子组件
// Father.js
export default class Father extends Components {
render() {
return (
<View>
<Son val="18" />
</View>
)
}
}
// Son.js
export default class Son extends Component {
render() {
return (
<View>
<Text>{this.props.val}</Text>
</View>
)
}
}
eg:子组件传递参数给父组件
// Father.js
export default class Father extends Components {
render() {
return (
<View>
<Son age="18" todo={this.show} />
</View>
)
}
show(sonV) {
}
}
// Son.js
export default class Son extends Components {
render() {
return (
<View>
<Text
onPress={() => {this.props.todo('子组件传递值')}}
>{this.props.age}</Text>
</View>
)
}
}
state 组件状态,组件是更新渲染依赖于其state属性,所以不是跟渲染相关的属性不要设置到这里,加大RN的渲染负担(state有所变动都会导致RN进行是否渲染的判断)。
import React, { Component } from 'react';
import { AppRegistry, Text, View,} from 'react-native';
export default class ResultScreen extends Component {
constructor(props){
super(props);
//设置state属性
this.state = { showData: false };
//设置变量
this.dataText = "";
}
render() {
return (
<View>
<Text tyle={{backgroundColor:'white',textAlign:'center',textAlignVertical:'center'}} onPress={this._onPressButton.bind(this)}>
{this.state.showData ? this.dataText : "点击改变状态"}
</Text>
</View>
);
_onPressButton() {
this.dataText = "Success";
this.setState({
showData: true,
});
}
}
手势识别
https://reactnative.cn/docs/0.51/gesture-responder-system.html#content
定时器
setTimeout, clearTimeout
setInterval, clearInterval
setImmediate, clearImmediate
requestAnimationFrame, cancelAnimationFrame
requestAnimationFrame(fn)和setTimeout(fn, 0)不同,前者会在每帧刷新之后执行一次,而后者则会尽可能快的执行(在iPhone5S上有可能每秒1000次以上)。
setImmediate则会在当前JavaScript执行块结束的时候执行,就在将要发送批量响应数据到原生之前。注意如果你在setImmediate的回调函数中又执行了setImmediate,它会紧接着立刻执行,而不会在调用之前等待原生代码。
Promise的实现就使用了setImmediate来执行异步调用。
网络访问
async getMoviesFromApi() {
try {
// 注意这里的await语句,其所在的函数必须有async关键字声明
let response = await fetch('https://facebook.github.io/react-native/movies.json');
let responseJson = await response.json();
return responseJson.movies;
} catch(error) {
console.error(error);
}
}
调试
- Reload: 重载rn项目
- Debug JS Remotely : 打开浏览器debug模式,会在浏览器打开一个新的标签页面,
地址是http://localhost:8081/debugger-ui - Live Reload : 实时刷新 当你的js代码发生变化后,React Native会自动生成bundle然后传输到模拟器或手机上
- Hot Reloading: 热加载 当你每次保存代码时Hot Reloading功能便会生成此次修改代码的增量包,然后传输到手机或模拟器上以实现热加载。相比 Enable Live Reload需要每次都返回到启动页面,Enable Live Reload则会在保持你的程序状态的情况下,就可以将最新的代码部署到设备上
- Toggle Inspector 调试网络、布局、点击等
Show Perf Monitor 打开性能监控
【debug调试】
在工程目录下输入npm start启动服务
- 手机与电脑同一个wifi环境下,真机摇晃弹出上述菜单,选择【Dev Settings】–【Debug server host & port for device】填写代理的ip和端口(eg:172.22.185.210:8081)。
- Chrome浏览器打开 http://localhost:8081/debugger-ui/ 然后手机调试菜单【Reload】下,会自动连接到Chrome上(没有UI)
- 连上后画面如下
原生与React-Native交互
【例子为已有老项目,接入少量RN模块,仍以原生为主流】
按上面教程搭建好环境,
初始化一个新项目
react-native init ReactTest
cd ReactTest
react-native run-android
用AS打开android目录,里面默认生成的是一个MainActivity和MainApplication,且MainActivity继承自ReactActivity,即app一启动打开MainActivity,MainActivity又去通过【原生调用RN界面】方式去启动我们的RN项目。(老项目迁移建议按照环境搭建步骤创建一个新项目,然后再对android目录下代码进行修改,把老项目copy过来比较简单)
然后把自己的老项目迁移过来,启动页的Activity也换成自己的,MainActivity可以删除,MainApplication里的代码是需要的可以跟自己原有的Application合并下。
【自动更新 和 手动更新 index.android.bundle 】
React-Native的内容依赖于【index.android.bundle】文件,必须保证【index.android.bundle】文件是最新的才能看到最新修改的效果,在项目目录下启动服务(即npm start)并配置到代理,每次run项目就会自动更新;或者打离线【index.android.bundle】包也可不依赖RN服务(一般打包的时候用,否则RN内容打开会白屏)。
【自动更新】
用Node.js命令行进入项目工程输入
npm start
启动服务(如果服务连接不上可直接运行react-native run-android重新跑下)
摇晃手机调出RN的调试界面,选择【Dev Settings】-> 【Debug server host & port for device】输入电脑的ip,端口号是8081,格式如【172.22.185.190:8081】。
【手动更新 即 离线包】
如果无法自动更新,可以产用手动更新的方法
在android下创建assets【android/app/src/main/assets】目录。然后打开Node.js命令行工具,进入根目录,运行如下代码
react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/
这会在assets下生成两个文件
PS:运行的app可能会报如下【index.android.bundle文件找不到错误】也是这个原因,启动服务,配置代理即可或打离线包均可解决。
【原生调RN界面】
修改MainActivity变成我们的项目,这样app启动起来就不调用RN而是我们的MainActivity了
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.one).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this,OneReactActivity.class));
}
});
findViewById(R.id.two).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, TwoReactActivity.class));
}
});
}
}
这个原生界面有两个Button,点击后分别跳转OneReactActivity和TwoReactActivity两个模块,OneReactActivity和TwoReactActivity是跳转RN界面的入口Activity,继承自ReactActivity。
OneReactActivity
public class OneReactActivity extends ReactActivity {
@Nullable
@Override
protected String getMainComponentName() {
return "One";
}
}
Two也类似,getMainComponentName返”Two”即可
MainApplication保持原来的不动。
这样原生部分的代码编写完毕,现在写两个RN的界面并注册
One和Two的代码类似,One的代码如下
import React from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View
} from 'react-native';
export default class One extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.hello}>One</Text>
</View>
)
}
}
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
hello: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
});
然后在index.js中注册下
import { AppRegistry } from 'react-native';
import One from './One';
import Two from './Two'
AppRegistry.registerComponent('Two', () => Two);
AppRegistry.registerComponent('One', () => One);
ok编译运行。
【RN调用原生/原生界面】
这部分的流程就是原生将自己的方法注册到RN中,RN对方法进行调用,跳转界面也是RN调用原生方法,再在原生方法中进行跳转。其实原生跳RN也一样,只不过这部分封装在ReactActivity中所以感知不深。
编写ReactContextBaseJavaModule子类,写一个MyJavaModule吧
public class MyJavaModule extends ReactContextBaseJavaModule {
public MyJavaModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return MyJavaModule.class.getSimpleName();
}
@ReactMethod
public void JumpResultActivity(){
Activity activity = getCurrentActivity();
activity.startActivity(new Intent(activity, ResultActivity.class));
}
}
注意 【ReactMethod】这个注解
编写ReactPackage子类,编写一个MyReactPackage吧
public class MyReactPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new MyJavaModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
最后再MainApplication中注册下
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),new MyReactPackage()
);
}
写一个ResultActivity来提供跳转,其实你做其他的事情也可以。原生部分结束。
RN部分修改下One.js
import React from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
NativeModules
} from 'react-native';
let JavaModule = NativeModules.MyJavaModule;
export default class One extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.hello} onPress={this.buttonClick}>One</Text>
</View>
)
}
buttonClick(){
JavaModule.JumpResultActivity();
}
}
var styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
hello: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
});
很简单,主要是NativeModules部分。
【原生与RN通信】
MyJavaModule添加如下代码
public static void sendEvent(String eventName, WritableMap params){
if(context != null){
context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, paramss);
}
}
context是ReactApplicationContext对象,取自MyJavaModule的构造函数
原生部分这样就可以了,我们在需要的地方调用sendEvent即可,params参数可以为空
RN部分,我们在接收的地方加入以下代码即可
componentDidMount(){
DeviceEventEmitter.addListener('result', function() {
alert("send success");
});
}