ReactNative-Tips

2,435 阅读36分钟

Native调用RN的class类的static方法

// js
export class YNFLog {
    static logtext() {
        console.log('这是测试用的log');
    }
}

const BatchedBridge = require('react-native/Libraries/BatchedBridge/BatchedBridge');

BatchedBridge.registerLazyCallableModule('YNFLog', () => YNFLog);
// OC
[_rootView.bridge enqueueJSCall:@"YNFLog" method:@"logtext" args:NULL completion:NULL];

工程文件夹怎么划分

自定义控件初始化工作写法

容器控件与内部子控件样式传递

自定义控件属性和方法定义和暴露形式

禁止响应事件

Text居中

点击空白隐藏键盘

View的onPress无效

动态形式创建 dom 控件

	_imageComponent(imageName) {
	    return  (imageName && imageName.length)?(<Image style={styles.leftRightImageStyle source={{uri:imageName}}/>) : null;
	}

method can be static 警告关闭

设置enumeration枚举值就是导出一个字典

  • 使用

    switch (this.props.inputFieldType) {
      case TextInputFieldType.Account :
          text = TextInputFieldType.Account;
          break;
      case TextInputFieldType.Pwd :
          text = TextInputFieldType.Pwd;
          break;
    }
    return text;
    
  • 请教export type是什么意思? - 知乎

    export type ButtonType = 'default' | 'primary' | 'ghost' | 'dashed' | 'danger';
    let buttontype: ButtonType = 'a'; //error
    let buttontype: ButtonType = 'default'
    

安装react-native-popup-dialog第三方时候

像原生那样弹出视图

探究react-native如何像原生那样弹出视图 - 简书

import type与export type 的使用

清除RN缓存

命令:

react-native start --reset-cache

npm start -- --reset-cache // 注意中间要加上 --

fixed的相关问题:

  • error: Unable to resolve module ./withSafeArea from xxx/xxxx.js 等相关报错,实际上库module ./withSafeArea是存在的

this.xx 变量使用

可跨函数变量使用,解决共用变量问题,并且页面销毁后该变量会被自动回收

在页面内通过 this.[变量名] 定义内部使用变量

bind的使用 (容易入坑) :通过this调用函数注意this的实际指向

  • 以上情况是,在_renderItem方法内调用通过this调用_getItemBlockStyle函数,如果在FlatLsit定义renderItem方法属性时,没有给传入的this._renderItem方法绑定this,则会报TypeError类型的错,说_getItemBlockStyle未定义。因为调用_renderItem方法的首要对象为FlatList而不是当前export的组件
  • 但是通过bind的话可能造成一定的冗余操作,在官网FlatList的介绍中有说 使用箭头函数而非 bind 的方式进行绑定,使其不会在每次列表重新 render 时生成一个新的函数,从而保证了 props 的不变性(当然前提是 id、selected和title也没变),不会触发自身无谓的重新 render。

热部署

设置裁剪,子控件超出部分不显示

  • overflow:'hidden'

用border建三角形

React Native Image加载base64照片

  • let baseImg='data:image/png;base64,图片base64编码';

debugger:代码内打断点

通知的使用

变量变成标签使用

在console中测试函数,注意变量名需要每次更新

圆角设置

borderRadius: 5,
overflow: 'hidden', // 裁剪子类超出部分,注意此时设置的阴影也会被裁掉

有下划线的key不能通过点方式获取需通过中括号获取

inputTypeInfo['username_desc']

Image 的 resizeMode 设置

可以在style中,也可以设置到props中,props优先,但是center值在打企业包时候无效

居中FlatList内部的小格子,在FlatList外层的父控件中使用alignItems: "center”。

<View style={{height: this.contentHeight, paddingVertical: 6, alignItems: "center", backgroundColor: "#fff"
]}>
    <FlatList
        data={data}
        numColumns={4}
        keyExtractor={(item, index) => index.toString()}
        renderItem={this.renderItem}
    />
</View>

Image不刷新同一个url

Date的转化坑点:

MDN上介绍Date时说,不建议使用Date构造函数(或与之等价的Date.parase方法)来解析日期字符串,如👇面例子。因为在不同浏览器上的处理方式不同

var date = new Date('2016-12-15 10:20') 在Safari和Firefox上有问题, 在Chrome上正常。

阴影穿透,控件没背景色就会发生

导出原生block给RN当做回调方法

@property (nonatomic, copy, nullable) RCTBubblingEventBlock onGetObserverInfo;

原生调起block,传入参数格式

if (self.onGetObserverInfo) {
        self.onGetObserverInfo(@{@"observedInfo":info});
    }

JS其获取值的方式:

onGetObserverInfo={(event) => {
    console.log('onGetObserverInfo');
    const item = event.nativeEvent;
    console.log(item.observedInfo);
}}

对象判空处理

if (object && Object.keys(object).length ) {
	// 非空
}

安全获值

/** 按路径获取值,否则返回null */
static safeGet = (p, o) => p.reduce((xs, x) => (xs && xs[x]) ? xs[x] : null, o);

const data = {
    WELCOME: {
        "moduleId": 1,
        "moduleType": "WELCOME",
        "moduleContent": {
            "mainTitle": "欢迎来到中邮钱包",
            "subTitle": "信用生活,乐享由你"
        },
        "modulePage": "ZYW_MINE_PAGE"
    }
}

let mainTitle = Utils.safeGet(['WELCOME', 'moduleContent', 'mainTitle'], data);

依赖注入

RN文档中的依赖注入理解: 就是将对象内部依赖的对象创建等行为通过参数传入等形式实现的模式。React 源码中的依赖注入方法

RCTResponseSenderBlock只能调用一次

通过RCTEventEmitter来解决该问题。Calling a callback multiple times in a React Native module

根据文本内容更新父控件size

函数式组件

函数式组件即通过调用一个方法返回一个组件。比如返回一个 class:

start (TopLevelNavigator) {
  let store = this.getStore()
  return class extends Component {
    render () {
      return (
        <Provider store={store}>
          <TopLevelNavigator />
        </Provider>
      )
    }
  }
}

上面通过调用一个 start 方法,返回一个新的组件的类。不过对于无状态的组件来说,可以有更好的写法。因为 class 也是一种 function,因此我们可以写成返回一个 function:

start (TopLevelNavigator) {
  let store = this.getStore()
  return (props) => (
        <Provider store={store}>
          <TopLevelNavigator {...props}/>
        </Provider>
      )
    }
  }
}

这样的写法更加直观。

PureComponent 和 Component

PureComponent 和 Component 的不同在于前者提供了一个 shouldComponentUpdate 的默认实现。

继承于 Component 的组件没有实现默认的 shouldComponentUpdate 方法,每一个 props 的变化以及内部的 setState 方法的调用都会触发重绘。

继承于 PureComponent 的组件,默认在 shouldComponentUpdate 中比较将 props 和 state 中的每一项进行浅比较。如果有不同才重绘。所以如果在 PureComponent 内 setState 就一定要保证 setState 的对象和之前的对象的地址不同,否则比如修改对象中的某个字段这种在 PureComponent 中是不会重绘的。

震动

震动使用 RN 自带的 Vibration 即可。这里主要提醒一下,如果是那种连续会触发的震动,可以使用 Vibration.cancel() 方法取消之前的震动,然后再 Vibration.vibrate(50) 继续短频震动。

获取控件的尺寸信息frame

给控件添加 onLayout 方法回调
<TouchableOpacity
  onLayout={({nativeEvent:e}) => {
        let {layout:{height, width, x, y}} = e;
        // 使用 height, width, x, y
  }}
/>
通过UIManager.measure()方法,将节点传入获取对应的布局信息
import {
   UIManager,
   findNodeHandle
} from 'react-native'

<TouchableButton
            ref={(ref) => {
                this.buttonRef = ref
            }}
            onPress={() => {
                UIManager.measure(findNodeHandle(this.buttonRef), (x, y, width, height, pageX, pageY) => {
                    // todo  
                }
            }}
        />

pointerEvents="none" 的使用时机

pointerEvents="none" 可以让设置的视图不响应点击事件。实践下来有两个应用场景:

  1. 比如有一个绝对布局的视图盖住了下面的视图,在 iOS 中可以直接将其 userInteraction 置为 false。RN 中相应的属性就是 pointerEvents
  2. 监听手势事件的时候,我们希望获取当前手势相对于父视图的位置,即 event.nativeEvent.locationY ,但是如果父视图中有子视图,并且手势作用的起始点在子视图上,那么 event.nativeEvent.locationY 是以子视图为参照的,影响我们对于坐标的计算。这时候就可以把它的子视图设置 pointerEvents="none"

再记述一下遇到的坑,在 android 上,如果父视图添加 panresponser,子视图直接设置 pointerEvents="none" 会让子视图的区域无法响应父视图的手势。必须在子视图上再嵌套一层 View。并且这个 View 也是有讲究的不能直接嵌套。因为在 Android 上,空的 View 会被直接移除。所以需要设置 collapsable={false} 让Android也强制渲染,才能正确响应父视图的手势

存在手势的视图中的按钮点击事件不响应

这个问题主要是因为点击按钮的时候手指有略微的滑动,因此,点击事件被识别为了 Move 事件,进而相应手势而不相应按钮了。

做法是在细微移动的时候不让手势相应:

onMoveShouldSetPanResponder (event, gestureState) {
  let touchCapture = Math.abs(gestureState.dx) > 5 || Math.abs(gestureState.dy) > 5
  return touchCapture
}

FlatList 的性能优化

FlatList 的 data 中接收一个数组,作为渲染的数据源。有时候我们点击了 cell,要改变状态引起重绘,但是直接修改 data 中某一项的值,并不会引起重绘。因为 React 中是判断 data 的地址是否变化,显然,data 并没有变化地址。

如果我们 data = data.map(item => item) 这样新建一个数据源就有点费事了。FlatList 提供了一个属性 extraData,当需要重绘的时候,改变 extraData 的值就行。

我们可以将 extraData 等于某一个 state 中的布尔值,每次需要重绘的时候,让这个布尔值取反。这样 FlatList 发现这个布尔值改变了,就会引发整个 FlatList 的重绘。

整个 FlatList 的重绘也是会引发性能问题的。我们要做的是自定义一个 Cell 类,然后将数据源的某个 item 设置为一个新的对象:Object.assign({}, item, {someChange: xxx})这样在 FlatList 重绘触发每一个 cell renderItem 的时候,大部分 Cell 并没有收到新的 Props 就会阻止整个 cell 的重绘。只有数据变化的 cell 才会触发重绘

渐变隐藏和显示

通过设置组件的 opacity 透明度进行渐变效果

透明度为0时,那么节点还在,其后面视图的点击事件是无法响应到的。

可通过设置display或height让组件变成隐藏:

display

通过 css 的布局属性 display ,当它为 none 的时候,点击事件会发生穿透

height

将高度设为 0。注意,子视图不一定会隐藏。这个时候就要结合设置 style 的 overflow: 'hidden'

如果控件的高度是包裹的控件的高度,height并不知道,这种情况怎么办呢? 答案是:在开始渲染的时候,使用 onLayout 拿到高度(可参考本文中的onLayout用法)

带有 Gesture 的父组件会 block 子组件中 TouchableOpacity 的点击事件

把按钮放到有 Move 手势的父组件的时候会经常因为响应了父组件的 Move 手势而不响应按钮的点击事件。这个时候需要给 Move 手势的添加行为做一个限制:

onMoveShouldSetPanResponder: (evt, gestureState) => Math.abs(gestureState.dx) > 5 || Math.abs(gestureState.dy) > 5

当移动的距离超过5之后,才响应滚动事件。

使用绝对路径替代相对路径

如果在 import 的时候使用相对路径,那么有些层级较深的时候会非常难看,可以使用devdependency 中安装 npm 插件:babel-plugin-root-import

然后在*.babelrc* 中加入:

{
  "plugins": [
    [
      "babel-plugin-root-import", {
        "rootPathSuffix": "src",
        "rootPathPrefix": "@"
      }
    ]
  ]
}

这样就可以使用 @ 来代替目录 src.

消除 console.log

我们可以通过 babel 插件将 console 去除。

首先在 devdependency 中安装 npm 插件:babel-plugin-transform-remove-console

然后在更目录下新建一个名为 .babelrc 的文件,在其中加入:

{
  "env": {
    "production": {
      "plugins": ["transform-remove-console"]
    }
  }
}

为 FlatList 设置 ListEmptyComponent

如果直接设置 ListEmptyComponent 占位符你会发现,即使将 ListEmptyComponent 的 style 设置为 {flex:1} 它也并不会填充满 flatList。这是因为包裹它的外层 View 没有设置高度。这就需要我们自己将 FlatList 的高度设置给 ListEmptyComponent。可以使用 onLayout 方法:

<FlatList
  onLayout={e => {
    this.setState({
      fHeight: e.nativeEvent.layout.height
    })
  }}
/>

我们在 FlatList 布局的时候获取到它的高度设置为 state 即可。

standard 进行代码校验

使用 standard 进行代码规范校验:

npm install --save-dev standard

然后在 package.json 中配置:

{
  "standard": {
    "parser": "babel-eslint",
    "globals": [
      "fetch"
    ],
    "ignore": []
  }
}

可以在 globals 中设置需要忽略的全局对象或者方法

husky hook git commit

我们可以使用 husky hook git 的提交方法。安装方式如下:

npm install husky --save-dev

然后在 package.json 中配置:

{  
  "scripts": {
    "lint": "standard --verbose | snazzy",
  },
  "husky": {
    "hooks": {
      "pre-commit": "npm run lint",
    }
  }
}

需要注意的是,husky 安装的时候会在 .git 文件夹下生成 hook 文件夹,如果是拷贝的别人的 node_modules,不会生成 hook 文件夹,所以需要先 uninstall 再 install 一遍

Immutable.js 的使用动机

一般说到 React 性能优化就会提到 Immutable 这个库。这里介绍一下它的使用场景。

当一个页面的状态树很大的时候,我们更新叶节点的时候只希望更新与之相关的节点,比如下图:

如果改变了其他节点的引用,可能会导致用到其他节点数据的视图重绘,浪费性能。一般情况下,当节点层数较浅的时候,我们会使用展开运算符,只改变响应节点的值,其余节点都使用原来的引用。但是节点层数深的时候,也会变得很麻烦。所以就可以使用 Immutable.js 这个库。当你改变某个叶节点的值的时候,它会自动将其相关的根节点更新为新的引用,而其他无关的节点还保持原有引用。

当然,Immutable 提供的类型毕竟不是原生类型,使用起来需要注意一些地方,所以最好还是把状态树设计的扁平一些。

react-native link

react-native link 方法可以把 node_module 中的 RN 库链接到 iOS 以及 Android 工程中。但是其中有一个坑点,就是在 iOS 端,react-native link 只会将 RN 库链接到 default target 中,而自己另外新建的 target 需要自己动手链接到 Link Binary with Libraries

黄色警告

黄色警告以及红屏报错可以手动触发:

console.error('红屏错误')
console.warn('黄屏警告')

我们开发的时候,应该尽可能避免黄色警告。但是如果这个警告是第三方引入的呢?我们可以隐藏特别类型的警告,比如 ant-design 引入的如下警告:

我们可以通过下面的代码隐藏:

import {YellowBox} from 'react-native'

const ignoreCase = [
  // ant design 引入的
  'Warning: NativeButton: prop type `background` is invalid;'
]
YellowBox.ignoreWarnings(ignoreCase)

何时重绘

触发重绘有两种方式:

  • setState 调用的时候。
  • props 变化的时候

现有的例子开看, setState 只是用来标记重绘的,标记了重绘后。React 的 render 方法生成的新的 JSX 对象和老的 JSX 对象比较,看看两个 JSX 对象的各个部分有哪些地方不同。然后渲染不同的部分

由于 componentWillReceiveProps 后面接着的就是 render 方法,所以 componentWillReceiveProps 中不需要使用 this.setState 。直接修改 this 上的属性也是可以的比如:

 componentWillReceiveProps (nextProps) {
    this.count = nextProps.count
 }
 render () {
   return (
     <View>
       <Text>{this.count}</Text>
     </View>
   )
 }

这样也是可以正确渲染出 this.count 的。

性能优化

利用 shouldComponentUpdate

针对有子组件的视图,每次父组件 render 的时候,都会触发子组件的 componentWillReceivePropsrender 方法。所以我们创建子组件的时候,最好重写 shouldComponentUpdate 方法,去判断 props 中的各个属性是否变化。一般出于性能原因,shouldComponentUpdate 方法都是进行浅层判等,即判断之前的属性和现在的属性是否是同一个对象:

shouldComponentUpdate(nextProps, nextState) {
    return (nextProps.completed !== this.props.completed) || (nextProps.text !== this.props.text)
}
style 不要写在 JSX 中

这里有一个注意点,如下代码即使重写了 shouldComponentUpadate 方法也是一直会重绘的:

<Foo style={{color: 'red'}} />

这是因为,{color: 'red'} 相当于每次都传入了一个新的对象。所以传 style的时候,不要直接写在 JSX 中

其实任何属性,包括传一个方法都不应该直接写在 jsx 中,如果都不写在 jsx 中,就会产生很多冗余代码。所以注意 style 写在 styleSheet 中这点即可。

render 时不要使用箭头函数

我们在 render 一个 button 的时候经常这么写:

<Button onClick={()=> this.doClick()}>
</Button>

这样会导致组件的重绘。因为每次渲染的时候会重新创建这个箭头函数,导致传入了新的 props。正确的做法应该是把这一过程提前:

doClick = () => {
}
<Button onClick={this.doClick()}>
</Button>

这样 doClick 方法传递的就是一个引用了。(如果有些方法需要参数,那么把参数作为 props 传入)

并不是说使用箭头函数一定会产生重绘,有些组件内部会重写 shouldComponentUpdate 方法,会无视这种 onClick 事件。但是如果组件内部没有这么做。就需要自己注意箭头函数引起的重绘了。

使用 react-redux 的 connect 方法

我们写组件的时候写 shouldComponentUpdate 判断一个个 props 是否变化其实是一个蛮烦的事。react-redux 其实帮我们做了这件事。使用它提供的 connect 方法,不需要做任何其他的事情,只要在 export 组件的时候做一些改变即可:

export default TodoItem
=> 改为
export default connect()(TodoItem)

connect() 参数为空,表示不从 store 中获取任何状态与方法

key

key 是一个老生常谈的东西。对于一个列表的每一项,需要唯一的 key 值。新老列表,key 值不同的项,视图将会被 Unmount 以及 mount,对于 key 值相同的项,视图只会被更新。

对于一个列表,如果我们不设置 key 值,默认是使用列表数组的索引 index 作为 key。但是这样会产生性能问题。比如删除了列表的第一项,整个列表的每一项都会更新

那如果我们在列表中添加一项的时候,什么值能作为这个唯一的 key 呢?可以依靠时间:Data.now()

比方说在 add 的时候,为添加的项创建一个 key 字段:

addItem: function(e) {
  var itemArray = this.state.items;

  itemArray.push(
    {
      text: this._inputElement.value,
      key: Date.now()
    }
  );

  this.setState({
    items: itemArray
  });
}

这样每次添加的时候,key 就获得了唯一值。

Reselect

使用 react-redux 的时候,还经常搭配另一个常用的库 Reselect。我们存在 redux 中的 state 可能需要经过一些处理。

比如 state.astate.b 可能通过 g(a,b) 衍生出 c。这个 c 如果放在 redux 中,那么每个 state.astate.b 变化的地方都要计算 g(a,b),很容易遗漏出错。如果把 c 放在 render 方法中,即每次 render 的时候计算 g(a,b),又会造成重复计算。

因此,比较好的做法就是在 state.a state.b 变化的时候计算 g(a,b),并且即不把g(a,b) 放在 redux 中,也不放在 render 中。所以,我们可以通过 redux 把数据传给组件的时候添加计算属性的方式来达到目的,即通过 mapStateToProps 方法。

是不是很像 vuex 中的 getter。Vue 真是太人性化了

import { createSelector } from 'reselect'

fSelector = createSelector(
   [state => state.a,
   state => state.b],
   (a, b) => f(a, b)
)
hSelector = createSelector(
   [state => state.b,
   state => state.c],
   (b, c) => h(b, c)
)
gSelector =  createSelector(
   [state => state.a,
   state => state.c],
   (a, c) => g(a, c)
)
uSelector = createSelector(
   [state => state.a,
   state => state.b,
   state => state.c],
   (a, b, c) => u(a, b, c)
)

...
function mapStateToProps(state) {
   const { a, b, c } = state
   return {
       a,
       b,
       c,
       fab: fSelector(state),
       hbc: hSelector(state),
       gac: gSelector(state),
       uabc: uSelector(state)
   }
}

比如上面的例子,fab 是通过 ab 计算得到,通过 createSelector方法,注册了 ab,以及计算方法 f(a,b)。那么只有在 a || b 变化的时候,才会重新计算 fab

setTimeout

比较简单的一个 js 的方法,但是要记住,在某个组件被卸载(unmount)之后,计时器却仍然在运行,要解决这个问题,只需铭记在unmount组件时清除(clearTimeout/clearInterval)所有用到的定时器

import React,{
  Component
} from 'react';

export default class Hello extends Component {
  componentDidMount() {
    this.timer = setTimeout(
      () => { console.log('把一个定时器的引用挂在this上'); },
      500
    );
  }
  componentWillUnmount() {
    // 请注意Un"m"ount的m是小写

    // 如果存在this.timer,则使用clearTimeout清空。
    // 如果你使用多个timer,那么用多个变量,或者用个数组来保存引用,然后逐个clear
    this.timer && clearTimeout(this.timer);
  }
};

主线程渲染

iOS 中渲染视图要在主线程中,所以 RN 中要调用原生方法,并且渲染视图的时候要通过 dispatch_async 到主线程进行。

比如 present 一个页面的时候就要在主线程中,否则 [self.retryBtn setTitle:@"重试" forState:UIControlStateNormal]; 这种设置按钮 title 的方法在非主线程中执行就无法渲染出 button 的 title

setState 原理

在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。

为什么会这样?

在React的 setState 函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新this.state 还是放到队列中回头再说,而isBatchingUpdates默认是 false,也就表示 setState 会同步更新 this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为 true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程 setState 不会同步更新 this.state

Text 控件

对齐

设置了宽高的 Text 控件只会在左上角显示。可以使用 text-align 设置文字的位置,比如 center。但是显示的时候你会发现只是水平居中。你必须再使用 line-height 设置高度为控件高度才能够竖直居中。

对于 text 控件,设置 height 是没用的,默认是 text 的高度。必须要设置 line-height

宽高

Text 控件的默认宽高是正好包裹文字的,因此可以不设置宽高。但是这样的 Text 是不会折行的,如果想要 Text 折行,必须添加宽度。所以包含 Text 的控件最好不要设置 flex 来自适应,而是设置具体的宽度

React/RCTBridgeModule.h file not found 解决方式

这个问题出现在 RN 从 0.40 版本前升级到 0.40 版本后的情况下。0.40 前 react 的头文件都是以 Header Search Paths 加入的。使用 React 的组件都需要添加头文件查找路径。当组件需要使用 React 的时候,如果在组件所在的目录下没有找到,那么就会到 Header Search Paths 指定的路径中查找:

这样带来一个问题,就是每一个第三方的组件都需要设置一下 Header Search Paths 的路径。使用者会非常不方便。

所以 FB 想要把 React 头文件的链接统一起来。于是就有了 0.40 版本的变化:通过 Edit Scheme 然后添加 React 这个 Target,然后取消 Parallelize Build

这样,在编译项目之前,就先把 React 编译好了。也就不再需要在 Header Search Paths 中设置了。

这样带来的改变就是原来引入头文件是相当于头文件在项目中了,使用:

#import "RCTBridgeModule.h"

现在引入头文件是引入的外部的头文件,所以要使用尖括号:

#import <React/RCTBridgeModule.h>

keyboardShouldPersistTaps 的使用

keyboardShouldPersistTaps 这是 scrollview 中的一个属性。

那么场景会用到这个属性呢?就是在一个 scrollview 中有两个 textinput a,b,当 a 输入完之后点击 b,这个时候如果你不设置 keyboardShouldPersistTaps 属性,那么点击 b 后,虚拟键盘消失,你需要再点一次 b 才能将虚拟键盘再打开,也就是说 scrollview 并没有相应 b 控件的点击事件。正常的需求应该是,点击 b 后,键盘仍然代开状态,只不过输入框变为 b。所以要用该属性控制。

该属性有三个枚举值:

  • never: 默认情况,点击 TextInput 以外的子组件会使当前的软键盘收起。此时子元素不会收到点击事件。
  • always: 键盘不会自动收起,ScrollView 也不会捕捉点击事件,但子组件可以捕获。
  • handled:当点击事件被子组件捕获时,键盘不会自动收起。这样切换 TextInput 时键盘可以保持状态。多数带有TextInput的情况下你应该选择此项。

ListView 使用的问题

ListView 的宽高

ListView 在父视图的 flex 方向上默认是铺满的。flex 方向上设置的宽或者高是无效的,非 flex 方向上设置的宽或者高是有效的。

所以最好在 ListView 外面套一个 View,保证 ListView 填充满整个父 View

renderRow 方法的坑

renderRow 方法的几个参数为,rowData, sectionID, rowID 这几个值为字符串类型。 所以根据 id 采取不同行为的时候,要把 id 转化为 number 再比较。或者不要用 === ,否则肯定返回的是 false。

初始渲染数量

ListView 为了保证渲染的性能,在最开始的时候只会部分渲染,所以需要设置 initialListSize 属性,设置首屏的渲染个数。否则很可能首屏需要加载的元素很多,但是实际渲染出来的元素很少。

cloneWithRows 使用的注意事项

我们知道 cloneWithRows 是在 listview 中保存列表数组时使用的方法。使用的时候要注意一点:数组在 cloneWithRows 之后会变成一个特殊的数据结构,数据数组只是这个数据结构中的一个属性。

那么什么时候需要注意呢?一个父控件内的子控件里有一个 listview,那么要么传入数据在里面 cloneWithRows,要么在外面 cloneWithRows 好后直接传入,不能外部 cloneWithRows 一次后再在里面 cloneWithRows 一次。推荐是在外面 cloneWithRows 好后传入,这样更符合封装性。

View 设置宽高

一个视图的显示必须要有宽高。以下是几个注意点:

  • 父控件设置绝对宽高,子控件也设置了绝对宽高。这种情况是最简单的情况,注意子控件可能会超过父控件。

  • 父视图设置了绝对宽高,子控件是 flex 布局:

    • flex-direction 方向上的子视图必须设置明确的长度,即主轴必须明确设置多长。否则就默认为 0
    • 非 flex-direction 方向上的子视图默认为填充满父视图,即 align-items 默认为 sketch。所以这里有一个坑点,如果你在父视图中设置 align-items: center 居中对齐,那么就取消了默认填充满父视图次轴这一设定,子视图一定要设置次轴上相应的长度,否则就是 0,显示不出了。
  • 子控件设置了绝对宽高,父控件可以不设置宽高,父控件正好包裹子控件。

  • 父子控件都是 flex 布局,都没有设置宽高。那么这种情况下很有可能显示不出。因为父控件需要一个 flex-direction 方向上的长度,但是并不能通过子控件推测出。那么就会不显示了。

布局方式

一般布局如果是一个给定的布局,使用 flex 布局非常的直观。但是如果布局中的元素个数会变化的时候,就需要考虑一下了。比如下面这个图。中间的部分可能按情况不同会有增减:

一般有两种方式:

  1. 考虑父级 align-items 设置为 sketch,块1此时和父级一样高。这个然后设置块1,flex-direction 为 space-between。这样块1的子级就会均匀的分布在块1内了,不论有多少元素。所以你需要设置第一个子元素和最后一个子元素相对于块1的上边和下边的距离。

  2. 考虑父级 align-items 设置为 center,块1的高度取决于内部元素的高度,块1内的元素会居中。所以需要设置子元素之间的距离。

一般来说用 center 会比较好一些

设置 Image

Image 图片一定要设置宽高,因为如果图片默认大小是0,加载完图片后,会有个闪烁,可以设置主题的图片模式是 resizeMode = ‘contain’ 这样图片就能在指定大小内适应缩放。

另外,如果拿一个突破作为背景的时候,一定要同时设置 Image 的宽高,以及 Image 包含的 View 的宽高。注意,包含的 View 不会自动填充满 Image

使用 JSX

JSX 中的 this

在 JSX 中调用外部的 js方法,如果要用到 this, 则必须 bind(this) 或者使用箭头函数,否则无法识别。

JSX 标签里一定不能有 {},就比如你要把 View 里的 style 注释掉, 一定不能直接用 cmd+/ 这样会在 <>里加入 {},产生 SyntaxError xxx.js Unexpected token,expected ...的错误

<View > 标签里的属性必须要要遵守如下的形式,即必须要用等号,并且要用 {} 把对象或者返回对象的方法包裹起来:

<View 
    style = {{margin}}>
</View>
JSX 中的代码

JSX 中可以通过 {} 插入代码。但是你不能直接把代码写在里面{} 内允许你调用一个返回 JSX 节点或者以 JSX 节点为元素的数组的方法

这里所说的方法可以是一个外部的方法,或者是一个三元运算符,或者是一个生成数组的方法,如 map 等。

隐藏一个组件

通过 state 的变化,将原来 return <Text>xxxx</Text> 变成 return null

_render() {
    return (
        {this.state.show ?<Text>xxxx</Text> : null}
    )
}

padding 和 margin 使用区别

这两者 android 程序员使用起来恐怕没有任何问题。iOS 程序员如果使用惯了 autoLayout 可能一时反应不过来。

style 究竟是在父控件里用 padding 还是在子控件里用 margin。其实基本没有太大差别,一般用 margin,能让子控件的布局更灵活一些。当然,如果父控件的样式需要复用多次,而子控件各不相同时,直接在父控件设置 padding,可以减少每次设置子控件 margin 的次数。

组件之间的通信

子组件调用父组件方法

父组件将方法以属性的方式传入子组件,子组件通过 this.props.方法名 拿到这个方法。

父组件调用子组件方法

父组件调用子组件的条件是拿到子组件的实例。因此可以为子组件加上 ref 属性。比如:

<Child ref='child'>haha</Child>

这样父组件就可以通过 this.refs.child 来获取 Child 组件的实例,并调用其内部方法了。

比较典型的用法在于一个 View 里嵌了一个 ListView,现在要调用 ListView 的刷新方法。就可以通过 ref 的方式从外部拿到。

ref 属性

上面演示的是 ref 接受一个字符串的使用方式,ref 属性还可以是一个回调函数,这个回调函数会在组件被挂载后立即执行。被引用的组件会被作为参数传递,回调函数可以用立即使用这个组件,或者保存引用以后使用:

  render () {
    return <TextInput ref={(element) => this._input = element} />;
  },
  componentDidMount () {
    this._input.focus();
  },

上面的例子中,在 ref 回调方法中把节点 TextInput 保存为 _input 属性。可以在必要的时候调用。

我认为最好还是用回调函数,通过回调函数可以把要使用的组件提前声明出来,方便调用。

跨级组件通信

跨级组件,如果还是一层层传递 props 非常的不优雅。React 提供了一个 context 属性。不过这并不推荐使用。一般我们使用 react-redux 库的时候,store 就是通过 context 传递的。

没有嵌套关系的组件通信

没有嵌套关系可以使用 EventEmitter 实现。在一个地方注册,另一个地方监听。不过也是不推荐的。所以用法也就不细说了。

State

state 中存放一些与视图有关的变量。与视图无关的变量,直接在构造器里作为自身属性创建。可以有两种方式便便 state:

// 方式一
this.state.someProp = 1
// 方式二
this.setState({ someProp : 1 })

其中,方式二能在改变 State 的同时刷新视图。

Props

简介

组件在创建的时候传入 Props 来完成定制,例如:

<Image source={pic} style={{width: 193, height: 110}} />

其中 source,style 都是传入 image 的 Props。其中 pic 表示一个js对象,类似后面的 {width: 193, height: 110}

{pic} 外面有个括号,表示括号内是一个js变量或者表达式,需要执行后取值,以此在JSX中嵌入单条js语句

子组件内获取 props

有时候,我们想要封装一个组件,在容器组件内多定义几个 props,但是并不希望这些 props 传到子组件内,比如容器组件的 children 属性。我们可以这样做:

render () {
    const {
		style,
		children,
		...restProps,
	} = this.props;
    // 删除多余属性
    [
      'onOpenChange',
      'onDrawerOpen',
      'onDrawerClose',
      'drawerPosition',
      'renderNavigationView',
    ].forEach(prop => {
      if (restProps.hasOwnProperty(prop)) {
        delete restProps[prop];
      }
    });
    
    return (
        <View style={style}>
    		<SomeView {...restProps}/>
        	{...children}
    	</View>
    )
}

注意:

  1. 通过对象展开符,可以获取到 props 中剩余的属性。
  2. 将一个对象作为组件的属性传入的时候要通过 {...obj} 的方式
  3. 通过 hasOwnProperty 进一步删除不想传递给子组件的属性
  4. this.props 的展开要放在 render 方法里,因为 props 可能会变化触发重绘,所以要每次重绘的时候都进行对象展开
Props 使用的注意点

通常我们直接会把 props 放到 render 方法中,比如上面的例子。但是这样其实不太好,比如一个页面跳转的时候,会带一些 props 过来,我们需要修改 props 中的一些属性。但是我们并不希望把这些修改带回到其他页面。

这种时候我们就不能直接修改 props 中的属性了。我们需要在 render 的时候,深拷贝或者不浅不深的拷贝 props 的值:

render () {
    this.props1 = this.props.props1
    this.props2 = this.props.props2
    return (
    	<View/>
    )
}

因为多加了一层 this.props1 我们就不需要担心,到底能不能修改 props 了,如果不能修改 props,那么直接深拷贝一下即可。

更进一步,其实我们只有在不希望修改数据带到其他页面的时候才会使用 this 挂载,一般情况下,我们直接使用结构赋值即可:

render () {
    const {prop1, prop2} = this.props
    return (
    	<View/>
    )
}

如果项目变化需求变化了,再转到把 props 的属性挂在到 this 下:

render () {
    this.props1 = this.props.props1
    this.props2 = this.props.props2
    const {prop1, prop2} = this
    return (
    	<View/>
    )
}

就不需要再更换 View 里的参数了

propTypes

组件的属性可以接受任意值,字符串、对象、函数等等都可以。有时,我们需要一种机制,验证别人使用组件时,提供的参数是否符合要求。组件类的 PropTypes 属性,就是用来验证组件实例的属性是否符合要求。我们需要引入一个 prop-types 库:

import PropTypes from 'prop-types';

class Greeting extends React.Component {
  render() {
    return (
      <Text>{this.props.name}</Text>
    );
  }
}

Greeting.propTypes = {
  name: PropTypes.string
};

上面例子中,如果 name 不是 string 类型,那么就会产生一个警告。还可以设置 name: PropTypes.string.isRequired 表示必须传入属性 name

除了 string 外,还有许多类型的 PropTypes 可以设置:

MyComponent.propTypes = {
  // 可以声明prop是特定的JS基本类型
  // 默认情况下这些prop都是可选的
  optionalArray:PropTypes.array,
  optionalBool: PropTypes.bool,
  optionalFunc: PropTypes.func,
  optionalNumber: PropTypes.number,
  optionalObject: PropTypes.object,
  optionalString: PropTypes.string,
  optionalSymbol: PropTypes.symbol,

  // 任何可以被渲染的事物:numbers, strings, elements or an array
  // (or fragment) containing these types.
  optionalNode: PropTypes.node,

  // A React element.
  optionalElement: PropTypes.element,

  // 声明一个prop是某个类的实例,用到了JS的instanceof运算符
  optionalMessage: PropTypes.instanceOf(Message),

  // 用enum来限制prop只接受特定的值
  optionalEnum: PropTypes.oneOf(['News', 'Photos']),

  // 指定的多个对象类型中的一个
  optionalUnion: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.number,
    PropTypes.instanceOf(Message)
  ]),

  // 指定类型组成的数组
  optionalArrayOf: PropTypes.arrayOf(PropTypes.number),

  // 指定类型的属性构成的对象
  optionalObjectOf: PropTypes.objectOf(PropTypes.number),

  // 一个指定形式的对象
  optionalObjectWithShape: PropTypes.shape({
    color: PropTypes.string,
    fontSize: PropTypes.number
  }),

  // 你可以用以上任何验证器链接‘isRequired’,来确保prop不为空
  requiredFunc: PropTypes.func.isRequired,

  // 不可空的任意类型
  requiredAny: PropTypes.any.isRequired,
  // PropTypes.element指定仅可以将单一子元素作为子节点传递给组件
  children: PropTypes.element.isRequired
defaultProps

可以在 defaultProps 中注册设置默认属性值。

class Greeting extends React.Component {
  render() {
    return (
      <Text>{this.props.name}</Text>
    );
  }
}

Greeting.defaultProps = {
  name: 'hahaha'
};

结合上面这两个属性,就不必再在构造函数里设置各种值了。

this.props.children

this.props 对象的属性与组件的属性一一对应,但是有一个例外,就是 this.props.children 属性。它表示组件的所有子节点。类似于 TouchableOpaque 里嵌入 Text,通过这种方式可以很方便的嵌套封装控件。

class NewComponent extends React.Component{
	render(){
		return(
			{this.props.children}
		);
	}
}

//调用:
<NewComponent>
	<Text>haha</Text>
</NewComponent>

this.props.children 是一个数组,子节点作为数组元素传入。

TextInput 隐藏键盘

Native 中的 UITextField 可以通过 resignFirstResponder 或者 endEditing 的方式取消第一响应者,从而隐藏虚拟键盘。那么,react 中如何做到隐藏键盘呢?

可以使用 ScrollView 包装我们的 ViewScrollView 可以设置 keyboardDismissModekeyboardShouldPersistTaps 来控制输入法的行为。

<ScrollView 	contentContainerStyle={{flex:1}}//非常重要,让ScrollView的子元素占满整个区域
				keyboardDismissMode='on-drag' //拖动界面输入法退出
				keyboardShouldPersistTaps={false} //点击输入法意外的区域,输入法退出
				>
....
</ScrollView>

生命周期回调函数总结

componentWillMount()

componentWillMount 会在组件 render 之前执行,并且永远都只执行一次。

componentDidMount()

componentDidMount 会在组件加载完毕之后立即执行。

componentWillReceiveProps(object nextProps)

在组件接收到一个新的 prop 时被执行。这个方法在初始化 render 时不会被调用。

这个方法很重要。组件内部属性的初始化设置只有一次,所以当组件初始化完成后,外部传入的属性值的变化不会直接引起组件内部属性值的变化,而是会回调这个方法。

如果你在组件内部用一个变量去接 props,那么除了在 constructor 里将 props 赋值给变量外,还需要在这个方法里将 props 赋值给变量。

boolean shouldComponentUpdate(object nextProps, object nextState)

返回一个布尔值。在组件的 props 或者 state 改变时被执行。在初始化时或者使用 forceUpdate 时不被执行。

如果 shouldComponentUpdate 返回 false,render() 则会在下一个 state change 之前被完全跳过。(另外 componentWillUpdatecomponentDidUpdate 也不会被执行)默认情况下 shouldComponentUpdate 会返回 true.

componentWillUpdate(object nextProps, object nextState)

组件接收到新的 props 或者 state 但还没有 render 时被执行。在初始化时不会被执行。一般用在组件发生更新之前。

componentDidUpdate(object prevProps, object prevState)

在组件完成更新后立即执行。在初始化时不会被执行。一般会在组件完成更新后被使用。例如清除 notification 文字等操作。

componentWillUnmount()

主要用来执行一些必要的清理任务。**注意,Unmount 的大小写。**写错就无法调用了!!!

优化切换动画卡顿的问题

使用API InteractionManager,它的作用就是可以使本来 JS 的一些操作在动画完成之后执行,这样就可确保动画的流程性。当然这是在延迟执行为代价上来获得帧数的提高。

InteractionManager.runAfterInteractions(()=>{
	//...耗时较长的同步任务...
	//更新state也需要时间
	this.setState({
		...
	})
	//获取某些数据,需要长时间等待
	this.fetchData(arguements)
})

一般这个方法都放在 componentDidMount 里。

React-Native 原生模块调用(iOS)

在项目中遇到地图,拨打电话,清除缓存等iOS与Andiorid机制不同的功能,就需要调用原生的界面或模块。

创建原生模块,实现“RCTBridgeModule”协议
#import <UIKit/UIKit.h>
#import "RCTBridgeModule.h"

@interface LoginViewController : UIViewController<RCTBridgeModule>

@end
导出模块,导出方法

不仅可以让导出 native 的方法,而且还可以在 js 中添加回调函数,供 native 调用,这样 native 就可以将前面的数据回塞给 js 了。

@implementation LoginViewController
//导出模块
RCT_EXPORT_MODULE()
- (void)viewDidLoad {
    [super viewDidLoad];
}

RCT_EXPORT_METHOD(showSVProgressHUDErrorWithStatus:(NSString *)state callBack:(RCTResponseSenderBlock)callback){
  NSLog(@"state is %@",state);
  NSArray *events = [[NSArray alloc] initWithObjects:@"hello", nil];
  // 这里callback必须是数组
  callback(events);
  [SVProgressHUD showErrorWithStatus:state];
}

@end
js文件中调用
//创建原生模块实例
let LoginViewController = NativeModules.LoginViewController;

//方法调用
LoginViewController.showSVProgressHUDErrorWithStatus('请输入正确的手机号',(callbackString) => {console.log(callbackString);});     

React Native 调试

首先,必须 保证调试用电脑的和你的设备处于相同的 WiFi 网络环境中下。然后修改AppDelegate.m 文件,设置 ip 为电脑 ip 即可。

然后就可以通过 Chrome 开发工具进行调试。最好不要使用 VSCode 提供的测试工具。不好用。

如果想要快速调样式,建议选上 Enable Hot Reloading 。可以在你每次保存的时候在本页面重新加载。

使用 xcode run 一遍之后,如果没有 native 代码的改动,手机就可以不用再连着电脑了,在项目地址下,使用 npm start 开启本地服务。

React Native 读取本地的json文件

可以以导入的形式,来读取本地的json文件,导入的文件可以作为一个js对象使用,这样方便调试的时候加载数据。

导入json文件:
var langsData = require('./json/langs.json');

现在你可以操作langsData对象了。

json格式
[
  {
    "path":"",
    "name":"123",
    "checked":false
  },
  {
    "path":"aa",
    "name":"1234",
    "checked":false
  },
  {
    "path":"ddd",
    "name":"123123123",
    "checked":true
  }
]
使用
for (var i=0,l=langsData.length;i<l;i++){
	console.log(langsData[i]);
}

布局属性

flexdirection:控制flex布局的main axis(主轴)的方向,RN的flexdirection默认值为column,而CSS中默认是row
aspectratio:宽高比例
  • 只设置width,height = width / aspectratio
  • 只设置height,width = height * aspectratio
  • 设置flexBasis(flex-basis 指定了 flex 元素在主轴方向上的初始大小),aspectRatio控制元素的corss axis交叉轴范围.
  • 元素同时设置了width和height,忽略aspectRatio
  • 被计算的尺寸同样受到对应max/minWidth或max/minHeight限制

原生获取RN的props

  • 通过导出原生UI的属性,让RN进行属性props设置,然后原生通过set方法进行设值拦截。

注意:RN是优先设置props后再进行render布局的,所以可以在原生通过set方法拦截获取相关props,再执行其他计算

监听RN设置控件的frame

  • 可通过UIView+React分类中的reactSetFrame:方法监听RN设置控件的frame
/**
 * Used by the UIIManager to set the view frame.
 * May be overriden to disable animation, etc.
 */
- (void)reactSetFrame:(CGRect)frame;

TextInput 金额输入限制

  • 这里问题是方法_onChangeText的回调时setState之后的触发的,也就是返回的text此时已经是显示在input上了,再进一步对数据进行过滤处理后,如果只是简单的setState新内容时,是不会导致RN刷新input 的内容的,所有这里采用一个小技巧,让内容发生前后变化后,再设置回原来的,强制RN对内容进行刷新
this.state = {
            inputText: '', // 输入框数字内容
        };
        
 // ...

<TextInput style={styles.textInput}
            placeholder={'请输入金额'}
            placeholderTextColor={'#bbbbbb'}
            keyboardType={'numeric'} // 带小数点键盘
            maxLength={9} // 限制字符个数包括小数点
            value={this.state.inputText} // 根据限制内容重新渲染input的文本内容
            onChangeText={this._onChangeText.bind(this)}
            onEndEditing={this._onEndEditing.bind(this)}
            >
</TextInput>

// ...

    _onChangeText(text) {
        // 1:不允许超出最大长度200000.00 9位 只允许输入小数点和数字,2:不允许以小数点开头,3:小数点后最多2位 且 只能输入一个小数点
        let temText = text;
        temText = temText.replace(/[^\d.]/g, '');// 清除"数字"和"."以外的字符
        temText = temText.replace(/^\./g, '');// 验证第一个字符是数字而不是字符
        temText = temText.replace(/\.{2,}/g, '.');// 只保留第一个.清除多余的
        // temText = temText.replace('.', '$#$').replace(/\./g, '').replace('$#$', '.');
        temText = temText.replace(/^(\-)*(\d+)\.(\d\d).*$/, '$1$2.$3');// 只能输入两个小数
        this.setState({ inputText: `${temText} ` }); // 注意这里多放了一个空格
        setTimeout(() => {
            // 移除最后的空格,目的就是让dom发生变化,强制刷新内容
            this.setState((previousState) => ({
                ...previousState,
                inputText: previousState.inputText.substr(0, previousState.inputText.length - 1), 
            }));
        }, 0);
    }

    _onEndEditing() {
        this.store.changeInputRightImage();
        this.setState((previousState) => ({ ...previousState, inputText: Number(previousState.inputText).toString() }));
    }

参考:

Promise.allSettled 使用例子

  • 每一个请求单独处理自身的逻辑后,将每一个请求的promise组合至Promise.allSettled中,用于处理所有请求都settled后的逻辑
  • 贴合简单需求就是:当前有3个请求,每个请求独立处理各自逻辑,然后监听所有请求都完成后再执行第4个请求,最后已除loading hud
// Promise.allSettled 的 Polyfill
if (!Promise.allSettled) {
    Promise.allSettled = function (promises) {
        return Promise.all(promises.map((p) => Promise.resolve(p).then((value) => ({
            status: 'fulfilled',
            value,
        }), (reason) => ({
            status: 'rejected',
            reason,
        }))));
    };
}

// 这个例子主要是用于处理如下需求:

    _test() {
        const promise1 = new Promise((resolve, reject) => {
            setTimeout(() => resolve('done1!'), 2000);
        });
        promise1.then((res) => {
            console.log(res);
        }).catch((e) => {
            console.log(e.message);
        });

        const promise2 = new Promise((resolve, reject) => {
            setTimeout(() => reject(new Error('Whoops!')), 1000);
        });
        promise2.then((res) => {
            console.log(res);
        }).catch((e) => {
            console.log(e.message);
        });

        const promise3 = new Promise((resolve, reject) => {
            setTimeout(() => resolve('done3!'), 2000);
        });
        promise3.then((res) => {
            console.log(res);
        }).catch((e) => {
            console.log(e.message);
        });

        console.log('abd');
        Promise.allSettled([promise1, promise2, promise3])
            .then((results) => { // (*)
                console.log(results);
                results.forEach((result) => {
                    if (result.status === 'fulfilled') {
                        console.log(`${result.value}`);
                    }
                    if (result.status === 'rejected') {
                        console.log(`${result.reason}`);
                    }
                });
            });
    }

KeyboardAvoidingView 使用参考