标签 React Native 下的文章

原文:https://pub.ofcrab.com/press/react-native-deep-linking-for-ios-android.html
代码:https://github.com/pantao/react-native-deep-linking-example

我们生活在一个万物兼可分享的年代,而分享的过程,几乎最终都会分享某一个链接,那么,作为开发者,最常遇到的问题中应该包括如何通过一个URL地址快速的打开App,并导航至特定的页面。

什么是深度链接(Deep Link)

深度链接是一项可以让一个App通过一个URL地址打开,之后导航至特定页面或者资源,或者展示特定UI的技术,Deep 的意思是指被打开的页面或者资源并不是App的首页,最常使用到的地方包括但远远不限于 Push Notification、邮件、网页链接等。

其实这个技术在很久很久以前就已经存在了,鼠标点击一下 mailto:pantao@parcmg.com 这样的链接,系统会打开默认的邮件软件,然后将 pantao@parcmg.com 这个邮箱填写至收件人输入栏里,这就是深度链接。

本文将从零开始创建一个应用,让它支持通过一个如 deep-linking://articles/{ID} 这样的 URL 打开 文章详情 页面,同时加载 {ID} 指定的文章,比如:deep-linking://articles/4 将打开 ID4 的文章详情页面。

深度链接解决了什么问题?

网页链接是无法打开原生应用的,如果一个用户访问你的网页中的某一个资源,他的手机上面也已经安装了你的应用,那么,我们要如何让系统自动的打开应用,然后在应用中展示用户所访问的那一个页面中的资源?这就是深度链接需要解决的问题。

实现深度链接的不同方式

有两种方式可以实现深度链接:

  • URL scheme
  • Universal links

前端是最常见的方式,后者是 iOS 新提供的方式,可以一个普通的网页地址链接至App的特定资源。

本文将创建一个名为 DeepLinkingExample 的应用,使得用户可以通过打开 deep-linking://home 以及 deep-linking://articles/4 分别打开 App 的首页以及 App 中 ID 为 4 的文章详情页面。

react-native init DeepLinkingExample
cd DeepLinkingExample

安装必要的库

紧跟 TypeScript 大潮流,我们的 App 写将使用 TypeScript 开发。

yarn add react-navigation react-native-gesture-handler
react-native link react-native-gesture-handler

我们将使用 react-navigation 模块作为 App 的导航库。

添加 TypeScript 相关的开发依赖:

yarn add typescript tslint tslint-react tslint-config-airbnb tslint-config-prettier ts-jest react-native-typescript-transformer -D
yarn add @types/jest @types/node @types/react @types/react-native @types/react-navigation @types/react-test-renderer

添加 tsconfig.json

{
  "compilerOptions": {
    "target": "es2017",                       /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
    "module": "es2015",                       /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    "lib": [                                  /* Specify library files to be included in the compilation:  */
      "es2017",
      "dom"
    ],
    "resolveJsonModule": true,
    "allowJs": false,                         /* Allow javascript files to be compiled. */
    "skipLibCheck": true,                     /* Skip type checking of all declaration files. */
    "jsx": "react-native",                    /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    "declaration": true,                      /* Generates corresponding '.d.ts' file. */
    "sourceMap": true,                        /* Generates corresponding '.map' file. */
    "outDir": "./lib",                        /* Redirect output structure to the directory. */
    "removeComments": true,                   /* Do not emit comments to output. */
    "noEmit": true,                           /* Do not emit outputs. */

    /* Strict Type-Checking Options */
    "strict": true,                           /* Enable all strict type-checking options. */
    "noImplicitAny": true,                    /* Raise error on expressions and declarations with an implied 'any' type. */
    "strictNullChecks": true,                 /* Enable strict null checks. */
    "strictFunctionTypes": true,              /* Enable strict checking of function types. */
    "noImplicitThis": true,                   /* Raise error on 'this' expressions with an implied 'any' type. */
    "alwaysStrict": true,                     /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    "noUnusedLocals": true,                   /* Report errors on unused locals. */
    "noUnusedParameters": true,               /* Report errors on unused parameters. */
    "noImplicitReturns": true,                /* Report error when not all code paths in function return a value. */
    "noFallthroughCasesInSwitch": true,       /* Report errors for fallthrough cases in switch statement. */

    /* Module Resolution Options */
    "moduleResolution": "node",               /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    "baseUrl": "./",                          /* Base directory to resolve non-absolute module names. */
    "paths": {                                /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
      "*": [
        "*.android",
        "*.ios",
        "*.native",
        "*.web",
        "*"
      ]
    },
    "typeRoots": [                            /* List of folders to include type definitions from. */
      "@types",
      "../../@types"
    ],
    // "types": [],                           /* Type declaration files to be included in compilation. */
    "allowSyntheticDefaultImports": true,     /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */

    /* Experimental Options */
    "experimentalDecorators": true,           /* Enables experimental support for ES7 decorators. */
    "emitDecoratorMetadata": true             /* Enables experimental support for emitting type metadata for decorators. */
  },
  "exclude": [
    "node_modules",
    "web"
  ]
}

添加 tslint.json 文件

{
  "defaultSeverity": "warning",
  "extends": [
    "tslint:recommended", 
    "tslint-react",
    "tslint-config-airbnb",
    "tslint-config-prettier"
  ],
  "jsRules": {},
  "rules": {
    "curly": false,
    "function-name": false,
    "import-name": false,
    "interface-name": false,
    "jsx-boolean-value": false,
    "jsx-no-multiline-js": false,
    "member-access": false,
    "no-console": [true, "debug", "dir", "log", "trace", "warn"],
    "no-empty-interface": false,
    "object-literal-sort-keys": false,
    "object-shorthand-properties-first": false,
    "semicolon": false,
    "strict-boolean-expressions": false,
    "ter-arrow-parens": false,
    "ter-indent": false,
    "variable-name": [
      true,
      "allow-leading-underscore",
      "allow-pascal-case",
      "ban-keywords",
      "check-format"
    ],
    "quotemark": false
  },
  "rulesDirectory": []
}

添加 .prettierrc 文件:

{
  "parser": "typescript",
  "printWidth": 100,
  "semi": false,
  "singleQuote": true,
  "trailingComma": "all"
}

编写我们的应用

在项目根目录下创建一个 src 目录,这个将是项目原代码的目录。

添加 src/App.tsx 文件

import React from 'react'

import { createAppContainer, createStackNavigator } from 'react-navigation'

import About from './screens/About'
import Article from './screens/Article'
import Home from './screens/Home'

const AppNavigator = createStackNavigator(
  {
    Home: { screen: Home },
    About: { screen: About, path: 'about' },
    Article: { screen: Article, path: 'article/:id' },
  },
  {
    initialRouteName: 'Home',
  },
)

const prefix = 'deep-linking://'

const App = createAppContainer(AppNavigator)

const MainApp = () => <App uriPrefix={prefix} />

export default MainApp

添加 src/screens/Home.tsx 文件

import React from 'react';

添加 src/screens/About.tsx

import React from 'react'

import { StyleSheet, Text, View } from 'react-native'

import { NavigationScreenComponent } from 'react-navigation'

interface IProps {}

interface IState {}

const AboutScreen: NavigationScreenComponent<IProps, IState> = props => {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>About Page</Text>
    </View>
  )
}

AboutScreen.navigationOptions = {
  title: 'About',
}

export default AboutScreen

const styles = StyleSheet.create({
  container: {},
  title: {},
})

添加 src/screens/Article.tsx

import React from 'react'

import { StyleSheet, Text, View } from 'react-native'

import { NavigationScreenComponent } from 'react-navigation'

interface NavigationParams {
  id: string
}

const ArticleScreen: NavigationScreenComponent<NavigationParams> = ({ navigation }) => {
  const { params } = navigation.state

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Article {params ? params.id : 'No ID'}</Text>
    </View>
  )
}

ArticleScreen.navigationOptions = {
  title: 'Article',
}

export default ArticleScreen

const styles = StyleSheet.create({
  container: {},
  title: {},
})

配置 iOS

打开 ios/DeepLinkingExample.xcodeproj

open ios/DeepLinkingExample.xcodeproj

点击 Info Tab 页,找到 URL Types 配置,添加一项:

  • identifier:deep-linking
  • URL Schemes:deep-linking
  • 其它两项留空

打开项目跟目录下的 AppDelegate.m 文件,添加一个新的 import

#import "React/RCTLinkingManager.h"

然后在 @end 前面,添加以下代码:

- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
  return [RCTLinkingManager application:application openURL:url sourceApplication:sourceApplication annotation:annotation];
}

至此,我们已经完成了 iOS 的配置,运行并测试是否成功。

react-native run-ios

打开 simulator 之后,打开 Safari 浏览器,在地址栏中输入:deep-linking://article/4 ,你的应用将会自动打开,并同时进入到 Article 页面。

同样的,你还可以在命令行工具中执行以下命令:

xcrun simctl openurl booted deep-linking://article/4

配置 Android

要为Android应用也创建 External Linking,需要创建一个新的 intent,打开 android/app/src/main/AndroidManifest.xml,然后在 MainActivity 节点添加一个新的 intent-filter

<application ...>
  <activity android:name=".MainActivity" ...>
    ...
    <intent-filter>
      <action android:name="android.intent.action.VIEW" />
      <category android:name="android.intent.category.DEFAULT" />
      <category android:name="android.intent.category.BROWSABLE" />
      <data android:scheme="deep-linking" />
    </intent-filter>
    ...
  </activity>
</application>

Android 只需要完成上面的配置即可。

执行:

react-native run-android

打开系统浏览器,输入:

deep-linking://article/4

系统会自动打开你的应用,并进入 Article 页面

也可以在命令行工具中使用以下命令打开:

adb shell am start -W -a android.intent.action.VIEW -d "deep-linking://article/3" com.deeplinkingexample;

附录

点击以下链接即可:

最近研究 React Native、Redux Saga 以及 TypeScript 相关的内容,整理成了一个 React Native Template,可以直接使用下面的命令创建一个新的应用:

react-native init MyApp --template=parcmg

初始化完成之后,按下面的方式执行命令:

cd MyApp
node setup.js
npm install
react-native link react-native-gesture-handler

完成之后,即可像往常一样开发了:

react-native run-ios

在 iOS App 开发过程中,经常会遇到该问题:

linker command failed with exit code 1 (use -v to see invocation) 

我在解决该问题的过程中,收集整理了一般引发该问题的原因以及对应的解决方法。

BitCode

新建一个 iOS 项目时, xcode 默认会将 bitcode 项设置为 YES,即启用,如果我们引入了一些不支持 bitcode 的第三方库,会引起这个问题。

bitcode 是一种编译结果中间态,它并不能直接运行,但是它包含了一个程序所需要的所有内容,它最终将被编译成为可运行的二进制包,启用 bitcode 的好处是,苹果可以随时根据自己的优化,基于 bitcode 生成更优化的二进制包,而不需要我们重新上传新的版本。 iOS 默认开启,但是可以关闭,watchOS 下则是必须开启的,mac OS 不支持,如果我们开发的程序只支持 iOS,那么可以选择关闭它。

尝试使用以下方式解决:

  • 打开 Build Settings
  • Enable Bitcode 设置为 NO

添加了第三方库,且不是静态库

如果添加了第三方库,且该库不是静态库之后发生此问题,那么可以尝试:

  • 先按上面 BitCode 的方式解决,若不行
  • 打开 Build Settings
  • 找到 Linking
  • Other Linker Flags 改为 -all_load 或者 -ObjC,视情况而定,多试几次。

引入了重复的包

……
duplicate symbol _OBJC_IVAR_$_RCTHTTPRequestHandler._session in:
    /Users/pantao/Library/Developer/Xcode/DerivedData/chongaiApp-dixtnlpvsctvmwdzrkacqqenegoh/Build/Intermediates.noindex/ArchiveIntermediates/chongaiApp/BuildProductsPath/Release-iphoneos/libReact.a(RCTHTTPRequestHandler.o)
    /Users/pantao/Library/Developer/Xcode/DerivedData/chongaiApp-dixtnlpvsctvmwdzrkacqqenegoh/Build/Intermediates.noindex/ArchiveIntermediates/chongaiApp/BuildProductsPath/Release-iphoneos/libRCTNetwork.a(RCTHTTPRequestHandler.o)
duplicate symbol _OBJC_METACLASS_$_RCTHTTPRequestHandler in:
    /Users/pantao/Library/Developer/Xcode/DerivedData/chongaiApp-dixtnlpvsctvmwdzrkacqqenegoh/Build/Intermediates.noindex/ArchiveIntermediates/chongaiApp/BuildProductsPath/Release-iphoneos/libReact.a(RCTHTTPRequestHandler.o)
    /Users/pantao/Library/Developer/Xcode/DerivedData/chongaiApp-dixtnlpvsctvmwdzrkacqqenegoh/Build/Intermediates.noindex/ArchiveIntermediates/chongaiApp/BuildProductsPath/Release-iphoneos/libRCTNetwork.a(RCTHTTPRequestHandler.o)
ld: 485 duplicate symbols for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

这个问题是我这次遇到的,

一直以为 上面这一段提示只是警告,但是其实它才是导致这个问题产生的原因,我最后是根据提示, symbol _OBJC_METACLASS_$_RCTHTTPRequestHandler 同时在 libReact.alibRCTNetwork.a 中定义了,我后来是一个一个的删除提示里面的多余的引用,解决问题。

  • 打开 Build Phases
  • 找到 Link Binary With Libraries (N items)
  • 根据提示中,删除重复项(名称有可能不同,但是里面的内容可能是一样的)

初始化一个应用

使用 react-native init AppName 命令初始化一个 React Native 应用

react-native init Chongai
mv Chongai chongai-app
cd chongai-app
我习惯性的使用 CamelCase 规范命名 App 名称,同时将文件夹改为 foobar-app 方式,上面那一行 mv 命令不一定都需要,我是为了保证项目的 git 地址没有大写字母

设置 git

git init
git remote add origin git@git.domain.com:namespace/project-name.git
git push -u origin master

安装 PrettierESLint

在本地项目中安装开发依赖

yarn add -D eslint prettier
yarn add -D eslint-config-airbnb
npx install-peerdeps --dev eslint-config-airbnb
yarn add -D eslint-config-prettier eslint-plugin-prettier

添加 .eslintrc.json 文件:

{
  "extends": ["airbnb", "prettier"],
  "plugins": ["prettier"],
  "rules": {
    "prettier/prettier": ["error"]
  }
}

添加 .prettierrc

{
  "printWidth": 100,
  "singleQuote": true
}

配置编辑器

我使用的是 VSCode:

分别安装 ESLintPrettier 插件。

设置配置项目:

{
  ...
  "editor.formatOnSave": true
  ...
}

安装常用库

yarn add react-native-gesture-handler
react-native link react-native-gesture-handler

yarn add react-navigation

yarn add react-native-video
react-native link react-native-video

yarn add react-native-camera
react-native link react-native-camera

yarn add react-native-device-info
react-native link react-native-device-info

yarn add react-native-image-picker
react-native link react-native-image-picker

安装极光插件

yarn add jpush-react-native jcore-react-native

链接插件

react-native link
# rnpm-install info Linking jcore-react-native ios dependency
# rnpm-install info Platform 'ios' module jcore-react-native has been successfully linked
# ? Input the appKey for JPush xxxxxxxxx
# patching android/settings.gradle...
# patching android/**/AndroidManifest.xml...
# patching android/**/build.gradle...
# patching ios/**/AppDelegate.m...
# done!
# rnpm-install info Linking jpush-react-native ios dependency
# rnpm-install info Platform 'ios' module jpush-react-native has been successfully linked
# rnpm-install info Platform 'android' module jpush-react-native is already linked

这一步操作会做下面这些操作:

android/settings.gradle 文件中添加下面这几行:

include ':jcore-react-native'
project(':jcore-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/jcore-react-native/android')

include ':jpush-react-native'
project(':jpush-react-native').projectDir = new File(rootProject.projectDir, '../node_modules/jpush-react-native/android')

android/app/build.gradle 文件里面添加

android {
  ...
  defaultConfig {
    manifestPlaceholders = [
       JPUSH_APPKEY: "xxxxxxxx",
       APP_CHANNEL : "default"
     ]
  }
  ...
}

android/app/src/main/AndroidManifest.xmlapplicaiton 中添加两个 meta-data

<meta-data android:name="JPUSH_APPKEY" android:value="${JPUSH_APPKEY}" />
<meta-data  android:name="JPUSH_CHANNEL" android:value="${APP_CHANNEL}" />

ios/AppName/AppDelegate.m 中添加:

- (void)jpushNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(NSInteger))completionHandler
{
  NSDictionary * userInfo = notification.request.content.userInfo;
  if ([notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
    [JPUSHService handleRemoteNotification:userInfo];
    [[NSNotificationCenter defaultCenter] postNotificationName:kJPFDidReceiveRemoteNotification object:userInfo];
  }

  completionHandler(UNNotificationPresentationOptionAlert);
}

- (void)jpushNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler
{
  NSDictionary * userInfo = response.notification.request.content.userInfo;
  if ([response.notification.request.trigger isKindOfClass:[UNPushNotificationTrigger class]]) {
    [JPUSHService handleRemoteNotification:userInfo];
    [[NSNotificationCenter defaultCenter] postNotificationName:kJPFOpenNotification object:userInfo];
  }

  completionHandler();
}

手动配置

IOS

在 iOS 工程中设置 TARGETS-> BUILD Phases -> LinkBinary with Libraries 找到 UserNotifications.frameworkstatus 设为 optional

在 iOS 工程中如果找不到头文件可能要在 TARGETS-> BUILD SETTINGS -> Search Paths -> Header Search Paths 添加如下路径:

$(SRCROOT)/../node_modules/jpush-react-native/ios/RCTJPushModule

在 xcode8 之后需要点开推送选项: TARGETS -> Capabilities -> Push Notification 设为 on 状态