【Hybrid开发高级系列】ReactNative(六) ——
1 React基础
1.1 环境准备
1.1.1 cnmp使用
1.1.1.1 cnmp安装
你可以使用我们定制的 cnpm(gzip压缩支持) 命令行工具代替默认的 npm:
$ npm install -g cnpm
--registry=https://registry.npm.taobao.org
或者你直接通过添加 npm 参数 alias 一个新命令:
alias cnpm="npm --registry=https://registry.npm.taobao.org\
--cache=$HOME/.npm/.cache/cnpm \
--disturl=https://npm.taobao.org/dist \
--userconfig=$HOME/.cnpmrc"
# Or alias it in .bashrc or .zshrc
$ echo '\n#alias for cnpm\nalias cnpm="npm--registry=https://registry.npm.taobao.org \
--cache=$HOME/.npm/.cache/cnpm \
--disturl=https://npm.taobao.org/dist \
--userconfig=$HOME/.cnpmrc"' >>
~/.zshrc && source ~/.zshrc
1.1.1.2 安装模块
从 registry.npm.taobao.org安装所有模块. 当安装的时候发现安装的模块还没有同步过来, 淘宝 NPM 会自动在后台进行同步, 并且会让你从官方NPMregistry.npmjs.org进行安装. 下次你再安装这个模块的时候, 就会直接从 淘宝 NPM 安装了.
$ cnpm install [name]
1.2 运行机理
1.2.1 render渲染方法
ReactDOM.render渲染方法是React的最基本方法,用于将模板转为 HTML 语言,并插入指定的 DOM 节点。
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('example')
);
上面代码将一个 h1 标题,插入 example 节点(查看 demo01),运行结果如下。
1.2.2 组件(component)
React允许将代码封装成组件(component),然后像插入普通 HTML 标签一样,在网页中插入这个组件。React.createClass 方法就用于生成一个组件类(查看 demo04)。
var HelloMessage = React.createClass({
render: function() {
return <h1>Hello {this.props.name}</h1>;
}
});
ReactDOM.render(
<HelloMessage name="John" /> ,
document.getElementById('example')
);
上面代码中,变量 HelloMessage 就是一个组件类。模板插入<HelloMessage /> 时,会自动生成 HelloMessage 的一个实例(下文的"组件"都指组件类的实例)。所有组件类都必须有自己的 render 方法,用于输出组件。
注意,组件类的第一个字母必须大写,否则会报错,比如HelloMessage不能写成helloMessage。另外,组件类只能包含一个顶层标签,否则也会报错。
组件的用法与原生的 HTML 标签完全一致,可以任意加入属性,比如<HelloMessage name="John" >,就是 HelloMessage 组件加入一个 name 属性,值为 John。组件的属性可以在组件类的 this.props 对象上获取,比如 name 属性就可以通过 this.props.name 读取。上面代码的运行结果如下。
添加组件属性,有一个地方需要注意,就是 class 属性需要写成 className ,for 属性需要写成 htmlFor ,这是因为 class 和 for 是 JavaScript 的保留字。
1.2.3 this.props.children
this.props 对象的属性与组件的属性一一对应,但是有一个例外,就是 this.props.children 属性。它表示组件的所有子节点(查看 demo05)。
var NotesList = React.createClass({
render: function() {
return (
<ol>
{
React.Children.map(this.props.children, function (child) {
return <li>{child}</li>;
})
}
</ol>
);
}
});
ReactDOM.render(
<NotesList>
<span>hello</span>
<span>world</span>
</NotesList>,
document.body
);
上面代码的 NoteList 组件有两个 span 子节点,它们都可以通过 this.props.children 读取,运行结果如下。
这里需要注意, this.props.children 的值有三种可能:如果当前组件没有子节点,它就是 undefined ;如果有一个子节点,数据类型是 object ;如果有多个子节点,数据类型就是 array 。所以,处理 this.props.children 的时候要小心。
React 提供一个工具方法 React.Children来处理 this.props.children 。我们可以用 React.Children.map 来遍历子节点,而不用担心 this.props.children 的数据类型是 undefined 还是 object。更多的 React.Children 的方法,请参考官方文档。
1.2.4 PropTypes
组件的属性可以接受任意值,字符串、对象、函数等等都可以。有时,我们需要一种机制,验证别人使用组件时,提供的参数是否符合要求。
组件类的PropTypes属性,就是用来验证组件实例的属性是否符合要求(查看 demo06)。
var MyTitle = React.createClass({
propTypes: {
title: React.PropTypes.string.isRequired,
},
render: function() {
return
<h1>{this.props.title}</h1>;
}
});
上面的Mytitle组件有一个title属性。PropTypes 告诉 React,这个 title 属性是必须的,而且它的值必须是字符串。现在,我们设置 title 属性的值是一个数值。
var data = 123;
ReactDOM.render(
<MyTitle title={data} />,
document.body
);
这样一来,title属性就通不过验证了。控制台会显示一行错误信息。
Warning: Failed propType: Invalid prop `title` of type `number` supplied to `MyTitle`, expected `string`.
更多的PropTypes设置,可以查看官方文档。
此外,getDefaultProps 方法可以用来设置组件属性的默认值。
var MyTitle = React.createClass({
getDefaultProps: function () {
return {
title: 'Hello World'
};
},
render: function() {
return
<h1>{this.props.title}</h1>;
}
});
ReactDOM.render(
<MyTitle />,
document.body
);
上面代码会输出"Hello World"。
1.2.5 获取真实的DOM节点
组件并不是真实的 DOM 节点,而是存在于内存之中的一种数据结构,叫做虚拟 DOM (virtual DOM)。只有当它插入文档以后,才会变成真实的 DOM 。根据 React 的设计,所有的 DOM 变动,都先在虚拟 DOM 上发生,然后再将实际发生变动的部分,反映在真实 DOM上,这种算法叫做 DOM diff,它可以极大提高网页的性能表现。
但是,有时需要从组件获取真实 DOM 的节点,这时就要用到 ref 属性(查看 demo07)。
var MyComponent = React.createClass({
handleClick: function() {
this.refs.myTextInput.focus();
},
render: function() {
return (
<div>
<input type="text" ref="myTextInput" />
<input type="button" value="Focus the text input" onClick={this.handleClick} />
</div>
);
}
});
ReactDOM.render(
<MyComponent />,
document.getElementById('example')
);
上面代码中,组件 MyComponent 的子节点有一个文本输入框,用于获取用户的输入。这时就必须获取真实的 DOM 节点,虚拟 DOM 是拿不到用户输入的。为了做到这一点,文本输入框必须有一个 ref 属性,然后 this.refs.[refName] 就会返回这个真实的 DOM 节点。
需要注意的是,由于 this.refs.[refName] 属性获取的是真实 DOM ,所以必须等到虚拟 DOM 插入文档以后,才能使用这个属性,否则会报错。上面代码中,通过为组件指定 Click 事件的回调函数,确保了只有等到真实 DOM 发生 Click 事件之后,才会读取 this.refs.[refName] 属性。
React 组件支持很多事件,除了 Click 事件以外,还有 KeyDown 、Copy、Scroll 等,完整的事件清单请查看官方文档。
1.2.6 this.state
组件免不了要与用户互动,React 的一大创新,就是将组件看成是一个状态机,一开始有一个初始状态,然后用户互动,导致状态变化,从而触发重新渲染 UI (查看 demo08)。
var LikeButton = React.createClass({
getInitialState: function() {
return {liked: false};
},
handleClick: function(event) {
this.setState({liked: !this.state.liked});
},
render: function() {
var text = this.state.liked ? 'like' : 'haven\'t liked';
return (
<p onClick={this.handleClick}>
You{text}this. Click to toggle.
</p>
);
}
});
ReactDOM.render(
<LikeButton />,
document.getElementById('example')
);
上面代码是一个 LikeButton 组件,它的 getInitialState 方法用于定义初始状态,也就是一个对象,这个对象可以通过 this.state 属性读取。当用户点击组件,导致状态变化,this.setState 方法就修改状态值,每次修改以后,自动调用 this.render 方法,再次渲染组件。
由于 this.props 和 this.state 都用于描述组件的特性,可能会产生混淆。一个简单的区分方法是,this.props 表示那些一旦定义,就不再改变的特性,而 this.state 是会随着用户互动而产生变化的特性。
1.2.7 表单数据读取
用户在表单填入的内容,属于用户跟组件的互动,所以不能用 this.props 读取(查看 demo9)。
var Input = React.createClass({
getInitialState: function() {
return {value: 'Hello!'};
},
handleChange: function(event) {
this.setState({value: event.target.value});
},
render: function () {
var value = this.state.value;
return (
<div>
<input type="text" value={value} onChange={this.handleChange} />
<p>{value}</p>
</div>
);
}
});
ReactDOM.render(<Input />,document.body);
上面代码中,文本输入框的值,不能用 this.props.value 读取,而要定义一个 onChange 事件的回调函数,通过 event.target.value 读取用户输入的值。textarea 元素、select元素、radio元素都属于这种情况,更多介绍请参考官方文档。
1.2.8 组件的生命周期
组件的生命周期分成三个状态:
Mounting:已插入真实DOM
Updating:正在被重新渲染
Unmounting:已移出真实DOM
React 为每个状态都提供了两种处理函数,will 函数在进入状态之前调用,did 函数在进入状态之后调用,三种状态共计五种处理函数。
componentWillMount()
componentDidMount()
componentWillUpdate(object nextProps, object nextState)
componentDidUpdate(object prevProps, object prevState)
componentWillUnmount()
此外,React 还提供两种特殊状态的处理函数。
componentWillReceiveProps(object nextProps):已加载组件收到新的参数时调用
shouldComponentUpdate(object nextProps, object nextState):组件判断是否重新渲染时调用
这些方法的详细说明,可以参考官方文档。下面是一个例子(查看 demo10)。
var Hello = React.createClass({
getInitialState: function () {
return {
opacity: 1.0
};
},
componentDidMount: function () {
this.timer = setInterval(function () {
var opacity = this.state.opacity;
opacity-= .05;
if (opacity< 0.1) {
opacity= 1.0;
}
this.setState({
opacity: opacity
});
}.bind(this), 100);
},
render: function () {
return (
<div style={{opacity: this.state.opacity}}>
Hello{this.props.name}
</div>
);
}
});
ReactDOM.render(
<Hello name="world" />,
document.body
);
上面代码在hello组件加载以后,通过 componentDidMount 方法设置一个定时器,每隔100毫秒,就重新设置组件的透明度,从而引发重新渲染。
另外,组件的style属性的设置方式也值得注意,不能写成
style="opacity:{this.state.opacity};"
而要写成
style={{opacity: this.state.opacity}}
这是因为 React 组件样式是一个对象,所以第一重大括号表示这是 JavaScript 语法,第二重大括号表示样式对象。
1.3 组件引用
1.4 工程构建
1.4.1 安装Node.js、RN
(一) 安装命令行工具(只需要执行一次,之后就可以直接从下面的第二部开始):
sudo npm install react-native-cli -g
查看安装的版本:npm -v
1.4.2 利用RN命令创建工程
react-native initHelloWorld //创建一个HelloWorld工程
1.4.3 运行项目
1. 找到创建的HelloWorld项目,双击HelloWorld.xcodeproj即可在xcode中打开项目。xcodeproj是xcode的项目文件。
2.使用终端命令运行项目:
cd 该项目文件夹
react-native run-ios
3.在WebStorm中运行,点击右下角的图标,选择Terminal,输入react-nativerun-ios即可运行。或输入npm start在模拟器打开的情况下运行。
2 开发技巧
2.1 样式
2.1.1 声明样式
在React Native中声明样式的方法如下:
var styles = StyleSheet.create({
base: {
width: 38,
height: 38,
},
background: {
backgroundColor: '#222222',
},
active: {
borderWidth: 2,
borderColor: '#00ff00',
},
});
2.1.2 使用样式
所有的核心组件接受样式属性。
<Text style={styles.base} />
<View style={styles.background} />
它们也接受一系列的样式。
<View style={[styles.base, styles.background]} />
行为与 Object.assign 相同:在冲突值的情况下,从最右边元素的值将会优先,并且falsy值如 false , un defined 和 null 将被忽略。一个常见的模式是基于某些条件有条件地添加一个样式。
<View style={[styles.base, this.state.active && styles.active]} />
2.1.3 样式传递
为了让一个call site定制你的子组件的样式,你可以通过样式传递。使用View.propTypes.style 和 Text.propTypes.style ,以确保只有样式被传递了。
var List = React.createClass({
propTypes: {
style: View.propTypes.style,
elementStyle: View.propTypes.style,
},
render: function() {
return (
<View style={this.props.style}>
{elements.map((element) =>
<View style={[styles.element, this.props.elementStyle]} />
)}
</View>);
}
});
// ... in another file ...
2.2 手势应答系统
触摸应答系统在 ResponderEventPlugin.js中实现了。
2.2.1 TouchableHighlight和Touchable*
应答系统在使用时可能是复杂的。所以我们为应该“可以轻击的”东西提供了一个抽象的Touchable实现。这 使用了应答系统,并且使你以声明的方式可以轻松地识别轻击交互。在网络中任何你会用到按钮或链接的地方使用TouchableHighlight。
2.2.2 应答器生命周期
是否接受触摸事件:通过实施正确的处理方法,视图可以成为接触应答器。有两种方法来询问视图是否想成为应答器:
• View.props.onStartShouldSetResponder:(evt) => true,——这个视图是否在触摸开始时想成为应答器?
• View.props.onMoveShouldSetResponder: (evt)=> true,——当视图不是应答器时,该指令被在视图上移动的;
触摸调用:这个视图想“声明”触摸响应吗?如果视图返回true并且想成为应答器,那么下述的一种情况就会发生:
View.props.onResponderGrant:(evt)=> { }——视图现在正在响应触摸事件。这个时候要高亮标明并显示 给用户正在发生的事情。
• View.props.onResponderReject:(evt)= > { }——其他的东西是应答器并且不会释放它。 如果视图正在响应,那么可以调用以下处理程序:
• View.props.onResponderMove:(evt)= > { }——用户正移动他们的手指;
• View.props.onResponderRelease:(evt)= > { }——在触摸最后被引发,即“touchUp”;
• View.props.onResponderTerminationRequest:(evt)= >true——其他的东西想成为应答器。这种视图应该释放应答吗?返回true就是允许释放;
• View.props.onResponderTerminate:(evt)= > { }——应答器已经从视图获取了。可能在调用onResponderTerminationRequest之后被其他视图获取,也可能是被操作系统在没有请求的情况下获取了(发生在iOS的control center/notificationcenter);
evt是一个综合的触摸事件,有以下形式:
• nativeEvent
• changedTouches——自从上个事件之后,所有发生改变的触摸事件的数组
• identifier——触摸的ID
• locationX——触摸相对于元素的X位置
• locationY——触摸相对于元素的Y位置
• pageX——触摸相对于屏幕的X位置
• pageY——触摸相对于屏幕的Y位置
• target——接收触摸事件的元素的节点id
• timestamp——触摸的时间标识符,用于速度计算
• touches——所有当前在屏幕上触摸的数组
捕捉ShouldSet处理程序
在冒泡模式,即最深的节点最先被调用,的情况下,onStartShouldSetResponder和 onMoveShouldSetResponder 被调用。这意味着,当多个视图为 *ShouldSetResponder 处理程序返回true时,最深的组件会成为应答 器。在大多数情况下,这是可取的,因为它确保了所有控件和按钮是可用的。
然而,有时父组件会想要确保它成为应答器。这可以通过使用捕获阶段进行处理。在应答系统从最深的组件冒泡时,它将进行一个捕获阶段,引发 * ShouldSetResponderCapture 。所以如果一个父视图要防止子视图在触摸开始时成为应答器,它应该有一个 onStartShouldSetResponderCapture 处理程序,返回true。
View.props.onStartShouldSetResponderCapture: (evt) => true,
View.props.onMoveShouldSetResponderCapture: (evt) => true,
PanResponder
更高级的手势解释,看看 PanResponder。
2.3 调用Native模块(iOS)
2.3.1 iOS日历模块的例子
本指南将使用 iOS日历API的例子。假设我们希望能够从JavaScript访问iOS日历。
Native模块只是一个Objectve-C类,实现了 RCTBridgeModule 协议。如果你想知道,RCT是ReaCT的一个 简称。
// CalendarManager.h
#import "RCTBridgeModule.h"
#import "RCTLog.h"
@interfaceCalendarManager : NSObject
@end
React Native不会向JavaScript公开任何 CalendarManager 方法,除非有明确的要求。幸运的是有了RCT_EXPORT ,这会非常简单:
// CalendarManager.m
@implementation CalendarManager
-(void)addEventWithName: (NSString *)name location: (NSString *)location
{
RCT_EXPORT();
RCTLogInfo(@"Pretending to create an event %@ at %@", name, location);
}
@end
现在从你的JavaScript文件中,你可以像这样调用方法:
var CalendarManager = require('NativeModules').CalendarManager;
CalendarManager.addEventWithName('BirthdayParty', '4 Privet Drive, Surrey');
注意,导出的方法名称是从Objective-C选择器的第一部分中生成的。有时它会产生一个非惯用的JavaScript名称(就像在我们的例子中的那个)。你可以通过为 RCT_EXPORT 提供一个可选参数更改名字,如dEvent) 。
方法返回的类型应该是 void 。React Native桥是异步的,所以向JavaScript传递结果的唯一方法是使用回调 或emitting事件(见下文)。
3 常见问题
3.1 配置类问题
3.1.1 Can not find module 'invariant'
解决方案:
npm install invariant -g