【Hybrid开发高级系列】ReactNative(六) —— ReactNative开发技巧总结

2023-10-16 12:29:01 浏览数 (2)

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 cnpmnalias 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

0 人点赞