Skip to content

React Native 开发笔记

RN 技巧

单独启动 android studio 模拟器(Linux)

sh
./emulator -netdelay none -netspeed full -avd Nexus_5X_API_25

文字过长隐藏的问题

CSS3 中大家可能都会用到 text-oveflow,然而 RN 的 Text 并没有这个属性,不过我们可以通过设置 numberOfLIne 或者 JS 自动计算来实现:

jsx
<Text numberOfLines={1}>your long text here<Text>

获取窗体大小

jsx
import { Dimensions } from "react-native";
export default {
  ScreenWidth: Dimensions.get("window").width, // 注意,如果这里的参数传递"screen",会将顶部和底部虚拟按键的高也算进去。
  ScreenHeight: Dimensions.get("window").height,
};

关于相对单位

jsx
// 模仿 web 端的 VW,VH,rem
export default {
  ScreenWidth: Dimensions.get("window").width,
  ScreenHeight: Dimensions.get("window").height,
  VW: (value) => (Dimensions.get("window").width / 100) * value,
  VH: (value) => (Dimensions.get("window").height / 100) * value,
  rem: (value) => (Dimensions.get("window").width / 750) * value,
};

阴影效果

android 和 ios 的阴影设置不一样,ios 可以使用 shadow 来直接设置,android 则需要用 elevation 来设置。

js
const styles = {
  container: {
    backgroundColor: "#fff",
    shadowColor: "#000",
    shadowOffset: { h: 10, w: 10 },
    shadowRadius: 3,
    shadowOpacity: 0.3,
    elevation: 4, // 安卓自带的阴影
  },
};

组件通信

如果是普通的组件通信可以用 props 解决,但如果是 react-navigation 等管理的组件,这招就不太好用了,比如返回时触发刷新页面等等。

解决方案有 2 种:

第一种是使用共享的状态,即 redux 这类框架去管理状态,这样状态和视图是保持同步的,不需要去告知页面需要刷新。redux 的使用方式这里不再阐述,可以看看我之前写的博文。

第二种是使用 RN 中的 DeviceEventEmitter

js
// 订阅事件
this.subscription = DeviceEventEmitter.addListener("reloadList", () =>
  this.getList()
);
js
// 移出
this.subscription.remove();
js
// 发射事件
DeviceEventEmitter.emit("reloadList");

上传文件

以上传图片为例:

使用 RN 中自带的 fetch 方法实现

js
export function uploadImage(imgAry) {
  const formData = new FormData();
  for (let i = 0; i < imgAry.length; i++) {
    let file = {
      uri: imgAry[i],
      type: "multipart/form-data",
      name: "image.jpg",
    };
    formData.append("file", file); // 这里的 files 就是后台需要的 key
  }
  console.log("开始上传图片");
  return fetch(server + "/common/uploadFile.do", {
    method: "POST",
    headers: {
      "Content-Type": "multipart/form-data",
    },
    body: formData,
  }).then((response) => response.json());
}

拨打电话

jsx
import { Linking } from "react-native";
function callMe() {
  return Linking.openURL("tel:10086");
}

富文本效果

RN 的Text组件 默认没有提供实现富文本效果的属性

效果图:

1.png

一开始想通过布局样式实现上图效果,但发现不行

2.png

最后通过多个Text组件来实现

jsx
<Text style={{ lineHeight: 16 }}>
  <Text style={{ fontSize: 13, color: "red" }}>{"温馨提示:  "}</Text>
  <Text
    style={{
      fontSize: Unity.SmallFont,
      marginLeft: 5,
      color: Unity.DetailColor,
    }}
  >
    {"你可以对帖子的相关问题进行提问,商家会第一时间回答您的问题!你的咨询" +
      "可能会在上方展示供其他网友参考,登录后咨询你可在个人中心-消息提醒中及时查看商家" +
      "的回复。"}
  </Text>
</Text>

在使用<FlatList />组件时,如果数据源是对象时,更新数据源数组里某项的某个字段的值时,页面 UI 不进行更新。

需要给<FlatList />组件指定 数据源

jsx
<FlatList
  extraData={this.state} // 指定 数据源
  keyExtractor={(item, index) => index.toString()}
  data={this.state.dataList}
  renderItem={(data) => (
    <ListItem data={data} cellClick={(item) => this.cellClick(item)} />
  )}
/>

或者在对数据源进行setState操作时,进行深复制也可以解决

ios 系统 WebView 在底部渲染意外出现黑色边框

将 WebView 的 backgroundColor 设置为透明即可

jsx
<Webview style={{backgroundColor: 'transparent'}} .../>

RN Bug

Android 键盘顶起底部导航的问题

解决方案: 打开 android 工程,在 AndroidManifest.xml 中配置如下:

xml
 <activity
    android:name=".MainActivity"
    android:label="@string/app_name"
    android:launchMode="singleTask"
    android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
    android:windowSoftInputMode="stateAlwaysHidden|adjustPan">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

iOS12 beta 版闪退

(使用友盟分享 SDK6.8.0 和统计版本 5.4.0 时必现的闪退)

闪退的原因:

友盟的重大 bug 导致,在使用分享 SDK6.8.0 和统计版本 5.4.0 进行注册友盟 key 的时候奔溃,必现。

(注释:使用 [UMConfigure initWithAppkey:KH_UM_APP_ID channel:@"App Store"]; 注册友盟 key,这是一个综合注册友盟平台的 API)

奔溃信息如下:

[<UIStatusBarTimeItemView 0x101c29cd0> valueForUndefinedKey:]: this class is not key value coding-compliant for the

原因是 Xcode 在 v10 之后合并了一些底层库文件,导致友盟崩溃,推荐的解决办法是去友盟升级 SDK

对应的解析日志如下:

sh
[UMInternalUtil getSysDateFormate]

[UMInternalUtil Value2]

[UMEnvelopeBuild buildInternalData]

[UMWorkDispatch dispatchEvent:inComponent:]

解决办法:

第一种:升级友盟 SDK 中的 UMCommon.framework, 升级地址, 注:记得下载原生模块下的依赖包。

第二种:遗弃友盟 😄😄

触摸组件在 Android 和 iOS 表现不一致

在 Android 上表现为虚拟组件,即最终渲染后不存在。而 iOS 表现为实体组件。

解决方式:可以在 Android 上嵌套一个 View。

jsx
import React, { Component } from "react";
import {
  View,
  TouchableNativeFeedback,
  TouchableOpacity,
  Platform,
} from "react-native";

export default class Touch extends Component {
  render() {
    if (Platform.OS === "ios") {
      return (
        <TouchableOpacity
          activeOpacity={1}
          style={this.props.style}
          onPress={this.props.onPress}
        >
          {this.props.children}
        </TouchableOpacity>
      );
    } else {
      return (
        <TouchableNativeFeedback onPress={this.props.onPress}>
          <View style={this.props.style}>{this.props.children}</View>
        </TouchableNativeFeedback>
      );
    }
  }
}

ScrollView 的一些坑

第一个坑:iOS 上 ScrollView 无法直接指定尺寸,必须要通过父级 View。但 Android 上没有此问题。

第二个坑:iOS 键盘弹起时的遮挡问题

推荐使用组件: react-native-keyboard-aware-scroll-view

填坑 - onMessage failing when there are JS warnings and errors on the page

在某些低版本 RN 上会出现

注入一段补丁代码:

js
(function () {
  var originalPostMessage = window.postMessage;

  var patchedPostMessage = function (message, targetOrigin, transfer) {
    originalPostMessage(message, targetOrigin, transfer);
  };

  patchedPostMessage.toString = function () {
    return String(Object.hasOwnProperty).replace(
      "hasOwnProperty",
      "postMessage"
    );
  };

  window.postMessage = patchedPostMessage;
})();
jsx
const patchPostMessageFunction = function() {
  var originalPostMessage = window.postMessage;

  var patchedPostMessage = function(message, targetOrigin, transfer) {
    originalPostMessage(message, targetOrigin, transfer);
  };

  patchedPostMessage.toString = function() {
    return String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage');
  };

  window.postMessage = patchedPostMessage;
};

const patchPostMessageJsCode = '(' + String(patchPostMessageFunction) + ')();';

...

<WebView injectedJavaScript={patchPostMessageJsCode} />

borderStyle: "dashed" 在安卓无效的解决方案

js
const style = {
  frame: {
    borderWidth: 1,
    borderStyle: "dashed", // 虚线样式对安卓无效
    borderColor: "#dcdcdc",
    height: 160,
    borderRadius: 0.1, // 加了 borderRadius 虚线就有效了, 原因未知
  },
};

某些 RN 版本的 TextInput 在 iOS 上无法输入中文的问题

解决方式如下,重新包装 TextInput:

jsx
import React, { Component } from "react";
import { Platform, TextInput } from "react-native";

class MyTextInput extends Component {
  shouldComponentUpdate(nextProps) {
    return (
      Platform.OS !== "ios" ||
      (this.props.value === nextProps.value && !nextProps.defaultValue) ||
      (this.props.defaultValue === nextProps.defaultValue && !nextProps.value)
    );
  }

  render() {
    return <TextInput {...this.props} />;
  }
}

export default MyTextInput;

但是这个解决方案存在值无法回填的 bug,官方解决方案(修改原生代码): https://github.com/facebook/react-native/pull/18456/files?diff=split

watch ENOSPC (linux)

linux 在启动 react-native start 后,过几秒就会出现 watch * * * * * * * * * * *ENOSPC 这个问题

解决方案:

echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

或重启电脑

WebView 中文显示乱码的问题

某些安卓上出现此类问题

解决方式方式是在 source 中填写 baseUrl: '',具体原因不明。

例如

sh
source={{html: userProtocolStr, baseUrl: ''}}

android 编译时提示编译工具版本不匹配的问题

The SDK Build Tools revision X is too low for project ‘:react-native-picker’. Minimum require X

在根目录下的 gradle 文件中加入以下代码,这样就把每个自工程的 buildToolsVersion 的版本改为要求的版本了

sh
subprojects {
    afterEvaluate {project ->
        if (project.hasProperty("android")) {
            android {
                compileSdkVersion 25
                buildToolsVersion '25.0.0'
            }
        }
    }
}

在 windows 编译时 JS 报错说路径问题

解决方案:使用 PowerShell 编译 JS,不要使用 CMD。

fetch 如何设置“请求超时”

目前也没有很好的办法解决超时,看网上很多方案都不可靠,主流的方式是抛弃这次 http 请求。

setTimeout() 方法,抛弃这次请求。

我看到另外一种,引入一个 polyfill 的文件, 然后在自己的网络请求方法里,采用它定义的 fetch 方法,就可以设置 timeout 参数。

js
import fetch from "react-native-fetch-polyfill";

fetch(url, { timeout: 30 * 1000 })
  .then((response) => {
    // a successful response
  })
  .catch((error) => {
    // an error when the request fails, such as during a timeout
  });

https://github.com/react-navigation/react-navigation/issues/1815

由于我所做的 app 是先进入空白页面,再判断显示主页还是登录页面,空白页面上用来处理 redux 逻辑,以及显示 loading。为了防止可以退回空白页面,需要做如下处理:

jsx
const prevGetStateForAction = RootStack.router.getStateForAction;

RootStack.router.getStateForAction = (action, state) => {
  if (
    action.type === "Navigation/BACK" &&
    state &&
    state.routes[state.routes.length - 1].routeName === "Login"
  ) {
    return null;
  }
  if (
    action.type === "Navigation/BACK" &&
    state &&
    state.routes[state.routes.length - 1].routeName === "Main"
  ) {
    return null;
  }
  // Do not allow to go back to Login
  if (action.type === "Navigation/BACK" && state) {
    const newRoutes = state.routes.filter((r) => r.routeName !== "Login");
    const newIndex = newRoutes.length - 1;
    return prevGetStateForAction(action, {
      index: newIndex,
      routes: newRoutes,
    });
  }
  return prevGetStateForAction(action, state);
};

传送门

用数据线链接调试

sh
adb reverse tcp:8081 tcp:8081

用 adb 模拟菜单键

sh
adb shell input keyevent 82

RN 打包

android

先用 Java 工具生成 my-release-key.keystore

sh
keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

把 my-release-key.keystore 文件放到你工程中的 android/app 文件夹下。

打开 android/app 中的 build.gradle 文件和 android/gradle.properties

build.gradle

sh
signingConfigs {
    release {
        storeFile file(MYAPP_RELEASE_STORE_FILE)
        storePassword MYAPP_RELEASE_STORE_PASSWORD
        keyAlias MYAPP_RELEASE_KEY_ALIAS
        keyPassword MYAPP_RELEASE_KEY_PASSWORD
    }
}

buildTypes {
    release {
        minifyEnabled enableProguardInReleaseBuilds
        proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
        signingConfig signingConfigs.release
    }
}

android/gradle.properties

sh
MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
MYAPP_RELEASE_KEY_ALIAS=my-key-alias
MYAPP_RELEASE_STORE_PASSWORD=123456
MYAPP_RELEASE_KEY_PASSWORD=123456

进入 /android/ 目录,cmd 执行 gradlew assembleRelease

下载 android 依赖慢

修改 build.gradle

sh
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        jcenter()
        maven {
            // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
            url "$rootDir/../node_modules/react-native/android"
        }
    }
}

找不到 android-sdk

可以在 android/local.properties 中手动配置,如果没有这个文件,手动创建一个。

sh
### This file must *NOT* be checked into Version Control Systems,
## as it contains information specific to your local configuration.
#
## Location of the SDK. This is only used by Gradle.
## For customization when using a Version Control System, please read the
## header note.
#Mon Jul 23 17:38:05 CST 2018
sdk.dir=/media/wolfx/HDD/android/sdk

打包时候报错:app:lintVitalRelease

android/app/build.gradle 中加入 lintOptions

sh
android {
    lintOptions {
        checkReleaseBuilds false
        abortOnError false
    }
    ...
}

NDK 找不到 mipsel/mips64el

新版 NDK 去掉了 mipsel,需要下载旧版(16b)替换。 https://developer.android.google.cn/ndk/downloads/older_releases

友盟分享、第三方登陆、推送集成

最好下载原生版本的 SDK,不要下载 RN 版本,RN 版本对新系统存在兼容问题。友盟分享的集成也是。 然后下载 GitHub 上的官方 DEMO,复制必要的文件到自己的项目。

基本配置可以参考官方,下面是接收推送的自定义参数:

Android 端集成

修改 MainActivity.java

java
package com.enjoybuild;

import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.util.Log;

import com.facebook.react.ReactActivity;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.umeng.message.PushAgent;
import com.umeng.socialize.UMShareAPI;

import java.util.Set;

public class MainActivity extends ReactActivity {

    /**
     * Returns the name of the main component registered from JavaScript.
     * This is used to schedule rendering of the component.
     */
    @Override
    protected String getMainComponentName() {
        return "EnjoyBuild";
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        PushModule.initPushSDK(this);
        PushAgent.getInstance(this).onAppStart();
        ShareModule.initSocialSDK(this);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        UMShareAPI.get(this).onActivityResult(requestCode, resultCode, data);
    }

    @Override
    public void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
    }

    @Override
    protected void onResume() {
        // 获取推送传递的参数
        // https://developer.umeng.com/docs/66632/detail/66744#h2-u81EAu5B9Au4E49u53C2u657013
        Bundle bun = getIntent().getExtras();
        if (bun != null) {
            WritableMap params = Arguments.createMap();
            Set<String> keySet = bun.keySet();
            for (String key : keySet) {
                String value = bun.getString(key);
                params.putString(key, value);
            }
            try {
                sendEvent(MyModule.reactContext, "notification", params);
            } catch (Exception e) {
                Log.e("error", e.getMessage());
            }
        }
        super.onResume();
    }

    private void sendEvent(ReactContext reactContext,
                           String eventName,
                           @Nullable WritableMap params) {
        reactContext
                .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit(eventName, params);
    }
}

MyModule.java 是自己定义的模块,为了获取 RN 的上下文。

java
package com.enjoybuild;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;

/**
 * Created by wolfx on 18-11-6.
 */

public class MyModule extends ReactContextBaseJavaModule {
    public static ReactContext reactContext;

    public MyModule(ReactApplicationContext rc) {
        super(rc);
        reactContext = rc;
    }

    @Override
    public String getName() {
        return "MyModule";
    }
}

iOS 端集成

1、下载相关 SDK

1.1、下载用于友盟集成的RN SDK

UTOOLS1557762431548.png

1.2、下载UMCommon.framework

UTOOLS1557762459680.png

注: 这个记得一定要下载 iOS 选项下面的 SDK,后面集成的时候会用到,之前按教程集成的时候没有找到这个文件,就去友盟 github 的 demo 里面复制了这个文件,结果在集成的时候由于不是最新的版本,导致在 Xcode10 上面一直闪退,崩溃提示是[<UIStatusBarTimeItemView 0x101c29cd0> valueForUndefinedKey:]: this class is not key value coding-compliant for the...,后面的朋友如果碰到 iOS 升级之后项目编译异常的也可以考虑升级 SDK 试试。

2、集成分享和授权登陆

2.1、首先进入项目的 ios 目录,进入对应的工程名目录,新建两个文件夹:UMReactBridgeUMComponents

UTOOLS1557762497479.png

2.2、将1.2步骤下载的文件iOS目录下的 common 中的 framework,拷入UMComponents中。

UTOOLS1557762526611.png

1.1步骤下载的文件iOS目录下中share目录中的最后所有文件,拷入UMComponents中的新建文件夹UMShare中,在将push文件里最后的所有文件拷入UMComponents文件夹中。看下图:

UTOOLS1557762561374.png

UTOOLS1557762586561.png

集成之后在 xcode 中的目录结构是这样的:

UTOOLS1557762607401.png

2.3、进入1.1步骤下载文件的 ReactNative 目录,找到common、 share、push目录中对应的common_ios平台中的桥接.h 和 .m文件,全部拷贝至我们项目刚刚新建的UMReactBridge文件夹中,看下图:

UTOOLS1557762648241.png

UTOOLS1557762673808.png

UTOOLS1557762692164.png

集成之后 xcode 中的目录结构为:

UTOOLS1557762708476.png

2.4、在xcode中打开工程目录,右键黄色项目名Add Files to "xxx"options 中选中Create groups 、 Copy items if needed找到我们新建的UMReactBridgeUMComponents,最后点击add添加。看下图

UTOOLS1557762724765.png

2.5、Linked Frameworks and Libraries 加入系统依赖库:可参照链接

sh
libsqlite3.tbd
CoreGraphics.framework
SystemConfiguration.framework
CoreTelephony.framework
libc++.tbd
libz.tbd

2.6 配置 SSO 白名单,右键 info.plist 选择 source code 打开(plist 具体设置在 Build Setting -> Packaging -> Info.plist File 可获取 plist 路径)请根据选择的平台对以下配置进行裁剪:

xml
<key>LSApplicationQueriesSchemes</key>
<array>
    <!-- 微信 URL Scheme 白名单-->
    <string>wechat</string>
    <string>weixin</string>

    <!-- 新浪微博 URL Scheme 白名单-->
    <string>sinaweibohd</string>
    <string>sinaweibo</string>
    <string>sinaweibosso</string>
    <string>weibosdk</string>
    <string>weibosdk2.5</string>

    <!-- QQ、Qzone URL Scheme 白名单-->
    <string>mqqapi</string>
    <string>mqq</string>
    <string>mqqOpensdkSSoLogin</string>
    <string>mqqconnect</string>
    <string>mqqopensdkdataline</string>
    <string>mqqopensdkgrouptribeshare</string>
    <string>mqqopensdkfriend</string>
    <string>mqqopensdkapi</string>
    <string>mqqopensdkapiV2</string>
    <string>mqqopensdkapiV3</string>
    <string>mqqopensdkapiV4</string>
    <string>mqzoneopensdk</string>
    <string>wtloginmqq</string>
    <string>wtloginmqq2</string>
    <string>mqqwpa</string>
    <string>mqzone</string>
    <string>mqzonev2</string>
    <string>mqzoneshare</string>
    <string>wtloginqzone</string>
    <string>mqzonewx</string>
    <string>mqzoneopensdkapiV2</string>
    <string>mqzoneopensdkapi19</string>
    <string>mqzoneopensdkapi</string>
    <string>mqqbrowser</string>
    <string>mttbrowser</string>
    <string>tim</string>
    <string>timapi</string>
    <string>timopensdkfriend</string>
    <string>timwpa</string>
    <string>timgamebindinggroup</string>
    <string>timapiwallet</string>
    <string>timOpensdkSSoLogin</string>
    <string>wtlogintim</string>
    <string>timopensdkgrouptribeshare</string>
    <string>timopensdkapiV4</string>
    <string>timgamebindinggroup</string>
    <string>timopensdkdataline</string>
    <string>wtlogintimV1</string>
    <string>timapiV1</string>
</array>

UTOOLS1557762776354.png

2.7、配置 URL Scheme

URL Scheme 是通过系统找到并跳转对应 app 的一类设置,通过向项目中的 info.plist 文件中加入 URL types 可使用第三方平台所注册的 appkey 信息向系统注册你的 app,当跳转到第三方应用授权或分享后,可直接跳转回你的 app。

UTOOLS1557762805820.png

2.8 初始化AppDelegate.m设置友盟appkeyappSecret(如果有推送功能的话需要加)以及各个平台的appkeysecret.

objective-c
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  /* 打开调试日志 */
  [[UMSocialManager defaultManager] openLog:YES];

  /* 设置友盟appkey */
  [RNUMConfigure initWithAppkey:@"5bd*********0db" channel:@"App Store"];
  /*
   * 关闭强制验证https,可允许http图片分享,但需要在info.plist设置安全域名
   */
  [UMSocialGlobal shareInstance].isUsingHttpsWhenShareContent = NO;

  /*
   设置微信的appKey和appSecret
   [微信平台从U-Share 4/5升级说明]http://dev.umeng.com/social/ios/%E8%BF%9B%E9%98%B6%E6%96%87%E6%A1%A3#1_1
   */
  [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_WechatSession appKey:@"wxd******" appSecret:@"e7b246a7bdcfe75100e77eb9e999bf37" redirectURL:nil];

  /* 设置分享到QQ互联的appID
   * U-Share SDK为了兼容大部分平台命名,统一用appKey和appSecret进行参数设置,而QQ平台仅需将appID作为U-Share的appKey参数传进即可。
   100424468.no permission of union id
   [QQ/QZone平台集成说明]http://dev.umeng.com/social/ios/%E8%BF%9B%E9%98%B6%E6%96%87%E6%A1%A3#1_3
   */

  [[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_QQ appKey:@"*****"/*设置QQ平台的appID*/  appSecret:@"*******" redirectURL:@"http://mobile.umeng.com/social"];

  // Push's basic setting 推送设置!!
  UMessageRegisterEntity * entity = [[UMessageRegisterEntity alloc] init];
  //type是对推送的几个参数的选择,可以选择一个或者多个。默认是三个全部打开,即:声音,弹窗,角标
  entity.types = UMessageAuthorizationOptionBadge|UMessageAuthorizationOptionAlert|UMessageAuthorizationOptionSound;
  [UNUserNotificationCenter currentNotificationCenter].delegate=self;

  [UMessage registerForRemoteNotificationsWithLaunchOptions:launchOptions Entity:nil completionHandler:^(BOOL granted, NSError * _Nullable error) {
    if (granted) {
      NSLog(@"deviceToken+++++++++");
    } else {
    }
  }];
return YES;
}

//从支付、分享、第三方登陆等跳转会本APP时的回调,必须实现,否则收不到分享成功时的回调!!!!
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
  {
    BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url];
    if (!result) {
      // 其他如支付等SDK的回调
    }
    return result;
  }

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url
  {
    BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url];
    if (!result) {
      // 其他如支付等SDK的回调
    }
    return result;
  }

2.9、提供给 RN 调用的方法在UMShareModule.m中查看.

2.10、进入下载目录的ReactNative找到common下的js中的ShareUtil.js(包含分享和登陆的方法),拷贝到我们RN目录下,即可在 RN 中导入使用分享的方法啦。

UTOOLS1557762840547.png

2.11、在 RN 中调用

jsx
import ShareUtil from "./ShareUtil";
//分享
ShareUtile.shareboard(text, img, url, title, list, (code, message) => {
  if (code == 200) {
    this.setState({ result: message });
  }
});

// text 为分享内容
// img 为图片地址,可以为链接,本地地址以及res图片(如果使用res,请使用如下写法:res/icon.png)
// url 为分享链接,可以为空
// title 为分享链接的标题
// list 为分享平台数组,如:var list = [0,1,2]
// callback中code为错误码,当为0时,标记成功。message为错误信息

//第三方登陆
ShareUtile.auth(0, (code, result, message) => {
  if (code == 200) {
    // iOS平台
    //登陆成功,返回第三方账号的信息,
    //比如openID、nickName、headPath等信息,这时就可以传给自己的后台小伙伴入库啦!!!
  }
});

注:android 与 ios 平台成功回调中的 code 值不一致,ios 成功时 code===200,android 成功时 code===0,文档写的是 0,当时还被小坑啦一把。

3、集成消息推送

SDK 端集成已经在分享和登陆步骤中已经集成,都有截图参考,下面直接进入功能实现步骤吧:

  • 其实对于推送功能,本质还是集成原生系统端推送功能,大部分代码逻辑还是在原生端实现,比如 注册推送权限、获取 DeviceToken、设置推送提醒的类型,设置收到推送时的回调等。
  • 比较烦的是对推送证书的配置,注: 如果做推送的话必须是付费的开发者账号生成证书才可以,否则在 Xcode 里设置开启推送权限的时候会找不到该菜单项。看下图 UTOOLS1557762871589.png
  • 具体的集成步骤以及证书生成参考友盟官网教程: 证书配置集成步骤
  • 在推送的时候,可以通过设置不通的 tag 值对用户分组进行推送,也可以通过用户 id 进行精准推送,RN 也提供来对应的方法 PushUtil.addTag(tag,(code,remain) =>{ })和 PushUtil.addAlias(alias,type,(code) =>{ }),具体参考文档
  • 还有待优化的功能: 关于 iOS 如何在接收到推送后打开指定页面, 官方给的地址以及找不到了, 我自己的实现方法是在原生端收到推送消息后参考 RN 文档中原生端调用 RN 的方法使用通知来实现,期待朋友们有好的方法,期待赐教。 推送主要代码:

AppDelegate.h页面

objective-c
#import <UIKit/UIKit.h>
#import <UMPush/UMessage.h>

//!!!!一定要实现这个代理方法 :UNUserNotificationCenterDelegate
@interface AppDelegate : UIResponder <UIApplicationDelegate,UNUserNotificationCenterDelegate>

@property (nonatomic, strong) UIWindow *window;

@end

AppDelegate.m页面

objective-c
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>

#import "RNUMConfigure.h"
#import <UMShare/UMShare.h>

#import "YouMengNotice.h"

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  // Push's basic setting
  UMessageRegisterEntity * entity = [[UMessageRegisterEntity alloc] init];
  //type是对推送的几个参数的选择,可以选择一个或者多个。默认是三个全部打开,即:声音,弹窗,角标
  entity.types = UMessageAuthorizationOptionBadge|UMessageAuthorizationOptionAlert|UMessageAuthorizationOptionSound;
// 需要实现代理,用于下面代理方法的实现。
  [UNUserNotificationCenter currentNotificationCenter].delegate=self;

  [UMessage registerForRemoteNotificationsWithLaunchOptions:launchOptions Entity:nil completionHandler:^(BOOL granted, NSError * _Nullable error) {
    if (granted) {
      NSLog(@"deviceToken+++++++++");
    } else {
    }
  }];

}

#pragma mark - Push
//在这个方法打印出 deviceToken, 便于在友盟后台添加测试设备进行测试
-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken{
  [UMessage registerDeviceToken:deviceToken];

  NSLog(@"deviceToken+++++++++%@",[[[[deviceToken description] stringByReplacingOccurrencesOfString: @"<" withString: @""]
                stringByReplacingOccurrencesOfString: @">" withString: @""]
               stringByReplacingOccurrencesOfString: @" " withString: @""]);
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error{

}
// 次方法在iOS10以后,已经不再支持
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
  //关闭友盟自带的弹出框
  [UMessage setAutoAlert:NO];
  [UMessage didReceiveRemoteNotification:userInfo];

  [YouMengNotice OCsendMessageToReactNative:userInfo];

}

//iOS10新增:处理前台收到通知的代理方法
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler{
  NSDictionary * userInfo = notification.request.content.userInfo;
  if([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {

    //应用处于前台时的远程推送接受
    //关闭友盟自带的弹出框
    [UMessage setAutoAlert:NO];
    //必须加这句代码
    [UMessage didReceiveRemoteNotification:userInfo];

    // 在这里自己实现来在接收到远程推送时,主动调用RN端的,把通知内容传给RN,这样就可以控制RN收到推送时跳转到指定页面来。
    [YouMengNotice OCsendMessageToReactNative:userInfo];

  }else{
    //应用处于前台时的本地推送接受
  }
  completionHandler(UNNotificationPresentationOptionSound|UNNotificationPresentationOptionBadge|UNNotificationPresentationOptionAlert);
}

//iOS10新增:处理后台点击通知的代理方法
//这个方法是在APP接收到通知之后,点击通知栏跳转到APP时才调用的, 注意::!!如果直接点击APP是没用的
//知识点!! iOS的通知 分APP在前台时显示和点击通知栏跳转到APP显示, 如果直接点击APP的话是不会走上面的两个回调方法的,之前理解错误
-(void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler{
  NSDictionary * userInfo = response.notification.request.content.userInfo;
  if([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {

    //应用处于后台时的远程推送接受
    //必须加这句代码
    [UMessage didReceiveRemoteNotification:userInfo];
    [YouMengNotice performSelector:@selector(OCsendMessageToReactNative:) withObject:userInfo afterDelay:15];
//    [YouMengNotice OCsendMessageToReactNative:userInfo];


  }else{
    //应用处于后台时的本地推送接受
  }
}

RN 端实现:

jsx
import PushUtil from "../Tools/PushUtil.js";
import { NativeModules, NativeEventEmitter } from "react-native";

const { YouMengNotice } = NativeModules;
const myNativeEvt = new NativeEventEmitter(YouMengNotice);  //创建自定义事件接口

componentDidMount() {
  //收到原生端发来的推送消息,这里处理页面跳转逻辑
   myNativeEvt.addListener("OCSendToRN", (e) => {
      console.log("原生端的消息");
      msg(JSON.stringify(e));
    });
//添加tag,即分组,在推送时可以对不同分组用户进行推送
    // PushUtil.addTag("ccj",(code, res) =>{
    //   Alert.alert("提示", ""+code+JSON.stringify(res));
    // })

    PushUtil.addTag("个人用户", (code, res) => {
      // Alert.alert("提示", ""+code+JSON.stringify(res));
    });

}

制作 Android 启动页

1.首先编写样式文件 res/values/style.xml 加入:

xml
<style name="Dialog_Fullscreen">
    <item name="android:windowTranslucentStatus">true</item>
    <item name="android:windowTranslucentNavigation">true</item>
</style>

2.编写 res/layout/activity_launch.xml

xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/ic_launcher">
</LinearLayout>

3.编写界面 SplashScreen.java

java
import android.app.Activity;
import android.app.Dialog;

public class SplashScreen {
    private static Dialog mSplashDialog; // 显示启动页

    public static void show(final Activity activity) {
        mSplashDialog = new Dialog(activity, R.style.Dialog_Fullscreen);
        mSplashDialog.setContentView(R.layout.activity_launch);
        mSplashDialog.setCancelable(false);
        mSplashDialog.show();
    }

    public static void hide(Activity activity) {
        if (mSplashDialog != null) {
            mSplashDialog.dismiss();
        }
        mSplashDialog = null;
    }
}

4.定义暴露给 RN 桥接的方法

java
import android.content.Context;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

public class ModuleHideSplash extends ReactContextBaseJavaModule {
    private Context context;

    public ModuleHideSplash(ReactApplicationContext reactContext) {
        super(reactContext);
        context = reactContext;
    }

    @Override
    public String getName() {
        return "SplashScreen";
    }

    @ReactMethod
    public void show() {
        SplashScreen.show(getCurrentActivity());
    }

    @ReactMethod
    public void hide() {
        SplashScreen.hide(getCurrentActivity());
    }

}

5.定义桥接包:

java
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class MyPackage implements ReactPackage {
    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new ModuleHideSplash(reactContext));
        return modules;
    }
}

6.初始化桥接包:

MainApplication.java 中添加 MyPackage

java
@Override
protected List<ReactPackage> getPackages() {
    return Arrays.<ReactPackage>asList(
            ...,
            new MyPackage()
    );
}

7.设置启动时展示:

MainActivity.java 中加入

java
@Override
public void onCreate(Bundle savedInstanceState) {
    SplashScreen.show(this);
    super.onCreate(savedInstanceState);
}

8.RN 端调用

js
import {NativeModules} from "react-native";
...
NativeModules.SplashScreen.hide();

第三方控件推荐

字母表控件

5cd98b972ff0f35877.gif

https://github.com/i6mi6/react-native-alphabetlistview

原生渲染富文本

https://github.com/jsdf/react-native-htmlview

轮播

react-native-swiper@nart/react-native-swiper,后者是修正 Android 上无法垂直滚动的版本。

查看大图

推荐组件 react-native-image-zoom-viewer

jsx
import ImageViewer from "react-native-image-zoom-viewer";
// ...
<ImageViewer
  imageUrls={imagePathFormat(this.state.contractPicture).map((url) => {
    return { url: url };
  })}
  index={this.state.imageIndex}
  enableImageZoom={true}
/>;

图片上传、拍照、图片压缩

推荐组件 react-native-image-picker

下面代码是我们项目中的代码片段,可以配合上面的文件上传代码使用。

jsx
import React, { Component } from "react";
import { Alert, Image, TouchableOpacity, View } from "react-native";
import ImagePicker from "react-native-image-picker";

// 图片选择器参数设置
const options = {
  title: "请选择图片来源",
  cancelButtonTitle: "取消",
  takePhotoButtonTitle: "拍照",
  chooseFromLibraryButtonTitle: "相册图片",
  quality: 0.7,
  maxWidth: 800,
  maxHeight: 800,

  // 此参数用于照片是否存储照片到手机相册
  // storageOptions: {
  //   skipBackup: true,
  //   path: 'images'
  // }
};

const styles = {
  frame: {
    borderTopWidth: 1,
    borderTopColor: "#707070",
    borderLeftWidth: 1,
    borderLeftColor: "#707070",
    borderRightWidth: 1,
    borderRightColor: "#707070",
    borderBottomWidth: 1,
    borderBottomColor: "#707070",
    justifyContent: "center",
    alignItems: "center",
    width: 80,
    height: 80,
  },
};

export default class MyImagePicker extends Component {
  selectImage() {
    ImagePicker.showImagePicker(options, (response) =>
      this.selectImageCallback(response)
    );
  }

  selectImageCallback(response) {
    if (response.didCancel) {
      Alert.alert("提示", "用户取消了选择!");
    } else if (response.error) {
      Alert.alert("ImagePicker发生错误:", response.error);
    } else {
      const source = { uri: response.uri };
      // You can also display the image using data:
      // let source = { uri: 'data:image/jpeg;base64,' + response.data };
      this.props.onChange(source);
    }
  }

  remove(index) {
    Alert.alert(
      "提示",
      "确认移除这张图?",
      [
        {
          text: "确认",
          onPress: () => {
            const newFiles = this.props.files;
            newFiles.splice(index, 1);
            this.props.onChange(newFiles);
          },
        },
        { text: "取消", onPress: () => {}, style: "cancel" },
      ],
      { cancelable: false }
    );
  }

  render() {
    return (
      <View style={{ flexDirection: "row" }}>
        {this.props.files.map((file, index) => {
          return (
            <TouchableOpacity onPress={() => this.remove(index)}>
              <Image
                resizeMode={"cover"}
                style={{
                  width: 80,
                  height: 80,
                  marginLeft: index === 0 ? 0 : 10,
                  marginBottom: 10,
                }}
                source={{ uri: file.url }}
              />
            </TouchableOpacity>
          );
        })}
        <TouchableOpacity onPress={() => this.selectImage()}>
          {this.props.selectable && (
            <View
              style={[
                styles.frame,
                { marginLeft: this.props.files.length > 0 ? 10 : 0 },
              ]}
            >
              <Image
                resizeMode={"contain"}
                style={{ width: 50, height: 50 }}
                source={require("../Assets/imgs/plus.png")}
              />
            </View>
          )}
        </TouchableOpacity>
      </View>
    );
  }
}

react-native Android 全面屏手机 底部留有一大块黑屏

解决方案:在 AndroidManifest.xml 中 配置

xml
<meta-data android:name="android.max_aspect" android:value="2.1" />

Could not find com.android.tools.build:aapt2:3.2.0-alpha14-4748712

在最上级的 builde.gralde 增加谷歌库 解决问题

allprojects {
    repositories {
        google()
        jcenter()
    }
}

为 R-N 配置 TypeScript 开发环境

typescript 是 javascript 的超集,在 javascript 的基础上添加了可选的静态类型,非常适合团队开,这次我们尝试使用 typescript 来开发 react-native 应用。 搭建 react-native 开发环境

安装 yarn 和 react-native 命令行工具

sh
npm install -g yarn react-native-cli

Yarn 是 Facebook 提供的替代 npm 的工具,可以加速 node 模块的下载。React Native 的命令行工具用于执行创建、初始化、更新项目、运行打包服务(packager)等任务。

React Native 目前需要 Xcode 7.0 或更高版本。你可以通过 App Store 或是到 Apple 开发者官网上下载。这一步骤会同时安装 Xcode IDE 和 Xcode 的命令行工具。

虽然一般来说命令行工具都是默认安装了,但你最好还是启动 Xcode,并在 Xcode | Preferences | Locations 菜单中检查一下是否装有某个版本的 Command Line Tools。Xcode 的命令行工具中也包含一些必须的工具,比如 git 等。 搭建 typescript 开发环境

先安装 typescript

sh
npm install -g typescript

接下来安装 typings typings 是 typescript 的依赖管理器,如果你使用 sublime text 或者 vscode,会非常方便的补全代码

sh
npm install -g typings

typings install 安装缓慢:追加参数 --registry=https://registry.npm.taobao.org 解决

使用 react-native 命令行工具初始化 react-native 项目

sh
react-native init ReactNativeApp

等待片刻后,进入刚刚新建的项目,创建一个名为"tsconfig.json" 的文件。tsconfig.json 是一个 typescript 项目的配置文件,可以通过读取它来设置 ts 编译器的编译参数 内容如下:

json
{
  "compilerOptions": {
    "target": "es6",
    "allowJs": true,
    "jsx": "react",
    "outDir": "./dist",
    "sourceMap": true,
    "noImplicitAny": false
  },
  "include": ["typings/**/*.d.ts", "src/**/*.ts", "src/**/*.tsx"],
  "exclude": ["node_modules"]
}

在项目下新建一个目录"src",typescripe 源代码就放在这里

现在安装 typings 依赖

sh
typings install npm~react --save
typings install dt~react-native --globals --save

编写 Hello world

在 src 目录下新建 myview.tsx,内容如下

jsx
import * as React from "react";
import { Text } from "react-native";

/**
 * Hello
 */
export default class Hello extends React.Component<null, null> {
  render() {
    return <Text>Hello world!</Text>;
  }
}

返回根目录,编译刚刚写好的 tsx 文件

sh
tsc

修改 index.ios.js

jsx
import React, { Component } from "react";
import Hello from "./dist/myview";
import { AppRegistry, StyleSheet, Text, View } from "react-native";

export default class ReactNativeApp extends Component {
  render() {
    return (
      <View style={styles.container}>
        <Hello />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#F5FCFF",
  },
});

AppRegistry.registerComponent("ReactNativeApp", () => ReactNativeApp);

运行 run-ios 试试效果

sh
react-native run-ios

Gradle DSL method not found

错误提示:

sh
Gradle DSL method not found: 'compileOnly()' or Gradle DSL method not found: 'implementation()'

解决方式有两种:

  • 升级 gradle 版本
  • 把对应的方法更改成当前 gradle 中的方法。比如,compileOnly()implementation() 都可以换成compile()

发布后运行闪退的问题

大概率是 RN 的包没有打进 APK,运行下面代码:

sh
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/

Version 4.2 (core-1.3.4)