跳到主要内容

可访问性 API 更新

· 阅读需 6 分钟
陈子琪
加州大学伯克利分校学生

动机

随着技术的进步和移动应用在日常生活中的重要性日益增加,创建可访问应用的必要性也同样变得更加重要。

React Native 限制较多的可访问性 API 一直是开发者的痛点,因此我们对可访问性 API 进行了一些更新,以便更容易创建包容性的移动应用。

现有 API 存在的问题

问题一:两个完全不同但又相似的属性——accessibilityComponentType(Android)和 accessibilityTraits(iOS)

accessibilityComponentTypeaccessibilityTraits 是两个用来告知 Android 上的 TalkBack 和 iOS 上的 VoiceOver 用户正在交互的 UI 元素类型的属性。这两个属性存在的最大问题是:

  1. 它们是两个不同的属性,使用方法不同,但目的相同。 在旧 API 中,这两个属性是分开的(分别针对不同平台),这不仅不方便,也令许多开发者困惑。iOS 上的 accessibilityTraits 允许 17 个不同的取值,而 Android 上的 accessibilityComponentType 只允许 4 个取值。更重要的是,这两个属性大部分取值之间没有重叠。甚至这两个属性的输入类型也不同。accessibilityTraits 允许传入数组或单个特征,而 accessibilityComponentType 只允许传入单个值。
  2. Android 上功能非常有限。 使用旧属性时,TalkBack 只能识别 “button”、“radiobutton_checked” 和 “radiobutton_unchecked” 这几种 UI 元素。

问题二:缺乏可访问性提示(Accessibility Hints)

可访问性提示帮助使用 TalkBack 或 VoiceOver 的用户理解在交互某个无障碍元素时会发生什么,这些信息无法仅通过无障碍标签传达。这些提示可以在设置面板中开启或关闭。而之前 React Native 的 API 完全不支持可访问性提示。

问题三:忽视了反转颜色设置

一些视力障碍用户在手机上使用反转颜色以提升屏幕对比度。苹果为 iOS 提供了一个 API,允许开发者忽略特定视图上的反转颜色设置,这样图像和视频使用反转颜色时不会失真。React Native 目前尚不支持该 API。

新 API 设计

解决方案一:合并 accessibilityComponentType(Android)和 accessibilityTraits(iOS)

为了解决 accessibilityComponentTypeaccessibilityTraits 之间的混淆,我们决定将它们合并为一个属性。这是合理的,因为两者技术上功能相同,合并后开发者无需再担心平台差异细节即可构建无障碍功能。

背景

在 iOS 中,UIAccessibilityTraits 是一个可以设置在任何 NSObject 上的属性。javascript 端传入的 17 个特征值分别映射到 Objective-C 中的 UIAccessibilityTraits 元素。每个特征用一个长整型表示,所有设置的特征会使用按位或(OR)运算合并。

而在 Android 上,AccessibilityComponentType 是 React Native 提出的概念,并不直接映射到 Android 的任何属性。Android 通过无障碍代理(accessibility delegate)处理无障碍,每个视图默认有一个无障碍代理。若需定制无障碍行为,开发者必须创建新的无障碍代理,重写特定方法,并将该视图的无障碍代理替换成新代理。当开发者设置 AccessibilityComponentType 时,原生代码会基于传入的组件创建一个新的无障碍代理,并设为该视图的代理。

所做的更改

我们希望新属性能成为两个现有属性的超集。我们决定让新属性主要参考已有的 accessibilityTraits,因为 accessibilityTraits 有更多取值。Android 方面的特征功能则通过修改无障碍代理进行 polyfill。

iOS 上的 accessibilityTraits 有 17 个取值,但我们没把所有都包含进新属性,因为部分特征的使用效果未知且很少用到。

这些特征一般有两种用途:描述 UI 元素的角色,或描述其状态。观察既有用法后,大多组合了一个角色值和“state selected”或“state disabled”,或两者皆有。于是,我们决定创建两个新属性:accessibilityRoleaccessibilityState

accessibilityRole

新属性 accessibilityRole 用于告知 TalkBack 或 VoiceOver UI 元素扮演的角色。它可取以下值之一:

  • none
  • button
  • link
  • search
  • image
  • keyboardkey
  • text
  • adjustable
  • header
  • summary
  • imagebutton

该属性只允许传入一个值,因为 UI 元素通常不会同时扮演多个角色。例外是图像和按钮,故新增合成角色 imagebutton

accessibilityStates

新属性 accessibilityStates 用于告知 TalkBack 或 VoiceOver UI 元素当前的状态。它接受包含以下一个或两个值的数组:

  • selected
  • disabled

解决方案二:添加可访问性提示(Accessibility Hints)

为此,我们新增了 accessibilityHint 属性。设置该属性后,TalkBack 或 VoiceOver 会朗读提示信息。

accessibilityHint

该属性接受一个字符串形式的可访问性提示。

iOS 上,设置该属性会将对应的原生属性 AccessibilityHint 赋值给视图。若 iPhone 里开启了可访问性提示,VoiceOver 会朗读该提示。

Android 上,设置该属性则会将提示值附加到可访问标签后面。此实现方式优点是模拟了 iOS 上提示的行为,但缺点是在 Android 设置中无法像 iOS 那样关闭提示。

我们在 Android 这样设计的原因是,提示通常对应特定操作(例如点击),我们希望平台间行为一致。

解决方案三

accessibilityIgnoresInvertColors

我们将苹果的 AccessibilityIgnoresInvertColors API 暴露给 JavaScript,现在当你不希望某个视图颜色被反转(比如图片)时,可以将这个属性设为 true,其颜色就不会被反转。

新用法

这些新属性将在 React Native 0.57 版本中可用。

升级指南

如果你目前使用 accessibilityComponentTypeaccessibilityTraits,可以按以下步骤升级到新属性。

1. 使用 jscodeshift

最简单的用例可以通过运行 jscodeshift 脚本替换。

这个 脚本 会替换以下示例:

accessibilityTraits=“trait”
accessibilityTraits={[“trait”]}

accessibilityRole=“trait”

此脚本还会删除 AccessibilityComponentType 的实例(假设你设置 AccessibilityComponentType 时也会设置 AccessibilityTraits)。

2. 手动重构

对于没有对应 AccessibilityRole 值的 AccessibilityTraits 用法,以及传入多个特征的用法,则需手动重构。

通常,

accessibilityTraits= {[“button”, “selected”]}

手动改为

accessibilityRole=“button”
accessibilityStates={[“selected”]}

这些属性已经在 Facebook 的代码库中使用。Facebook 的重构过程非常简单,jscodeshift 脚本解决了半数实例,其余的手动调整,整个过程耗时甚至不到几小时。

希望你会觉得更新后的 API 很有用!请大家继续努力让应用更具可访问性!#包容性

发布 0.56 版本

· 阅读需 6 分钟
Lorenzo Sciandra
Drivetribe 核心维护者 & React Native 开发者

备受期待的 React Native 0.56 版本现已发布 🎉。这篇博客文章重点介绍了该新版本中的一些变更。我们也想借此机会说明自三月以来一直让我们忙碌的工作内容。

重大变更的两难,或者说,“何时发布?”

贡献者指南解释了所有 React Native 变更需要经过的集成流程。该项目由许多不同的工具组成,需协调和持续支持以保持一切正常运行。加上充满活力的开源社区对项目的贡献,你就可以感受到这一切规模令人头晕目眩。

随着 React Native 的广泛采用,重大破坏性变更必须非常谨慎地进行,过程并不像我们期望的那样顺畅。我们决定跳过四月和五月的发布版本,让核心团队整合和测试一套新的破坏性变更。整个过程中我们使用了专门的社区沟通渠道,确保 2018 年 6 月的(0.56.0)发布尽量让那些耐心等待稳定版本的用户能毫无困难地采用。

0.56.0 完美吗?不会,和任何软件一样,但我们已经达到了“等待更稳定”与“测试得到成功结果,准备推动发布”之间的权衡点,我们觉得可以发布了。此外,我们也知道在最终的 0.56.0 版本中存在一些尚未解决的问题,例如这个这个这个还有这个。大多数开发者升级到 0.56.0 不会有问题。对于那些因上述问题受阻的开发者,我们希望在讨论中见到你们,也期待和你们一起解决这些问题。

你可以将 0.56.0 看作向更稳定框架迈出的坚实基石:可能需要一两周的广泛采用来消除所有边缘情况,但这将促成 2018 年 7 月(0.57.0)版本的更好表现。

最后,我们要感谢67 位贡献者在共计 818 次提交中付出的努力,他们让你的应用变得更好 👏。

那么,话不多说……

重大变更

Babel 7

你们可能知道,使我们能够使用最新最强大 JavaScript 特性的转译工具 Babel 即将发布其7 版本。由于这个新版本带来了一些重要变化,我们觉得现在是升级的好时机,这也让 Metro 能够利用其改进

如果在升级过程中遇到麻烦,请参考相关文档部分

Android 支持现代化

在 Android 方面,周边工具链发生了诸多变化。我们更新到了 Gradle 3.5Android SDK 26Fresco 1.9.0 和 OkHttp 3.10.0,甚至将NDK API 目标升级到了 API 16。这些变更应该不会带来问题,还能加快构建速度。更重要的是,这有助于开发者遵守 下个月开始生效的 Play 商店新要求

在此,我们特别感谢 Dulmandakh 提交的许多 PR ,使这一切成为可能 👏。

未来我们还将继续推进相关措施,大家可以在专门的 issue中关注 Android 支持的更新规划与讨论(以及一个针对 JSC 的子话题)。

新版 Node、Xcode、React 和 Flow

Node 8 现在是 React Native 的标准版本。虽然我们其实已经开始测试了,但随着 Node 6 进入维护模式,我们现在正式全面采用 Node 8。React 也升级到了 16.4,带来了大量修复。

我们放弃了对 iOS 8 的支持,使 iOS 9 成为可支持的最低版本。我们认为这不会成为问题,因为任何能运行 iOS 8 的设备都可以升级至 iOS 9。此举让我们得以移除为运行 iOS 8 的旧设备编写的鲜少使用的兼容代码。

持续集成工具链已升级为使用 Xcode 9.4,确保所有 iOS 测试都在 Apple 提供的最新开发工具上运行。

我们已升级到 Flow 0.75,采用了很多开发者青睐的新错误格式。同时,我们为更多组件创建了类型定义。如果你的项目尚未强制使用静态类型,请考虑使用 Flow 来在编码过程中而非运行时发现问题。

以及许多其他内容……

例如,YellowBox 已被替换,提供了更佳的调试体验。

完整发布日志请参考完整变更记录。升级时请务必关注升级指南,避免迁移到此版本时出现问题。


最后说明:从本周开始,React Native 核心团队将恢复举办月度会议。我们会确保及时向大家通报会议内容,并将你的反馈纳入后续会议讨论。

祝大家编码愉快!

Lorenzo, Ryan 及整个 React Native 核心团队

**附注:**和往常一样,请大家注意 React Native 仍处于 0.x 版本,因仍有许多变更进行中——升级时请保持警惕,可能仍会遇到崩溃或故障。提交问题和 PR 时请相互帮助,遵守行为准则,屏幕那头总有真实的人。

2018 年 React Native 状况报告

· 阅读需 5 分钟
Sophie Alpert
Facebook React 工程经理

自从我们上次发布关于 React Native 的状态更新已经有一段时间了。

在 Facebook,我们比以往任何时候都更多地使用 React Native 用于许多重要项目。我们的一个最受欢迎的产品是 Marketplace,这是我们应用中的顶级标签之一,每月有 8 亿用户使用。自 2015 年创建以来,整个 Marketplace 都是用 React Native 构建的,包含了应用中不同部分的 100 多个全屏视图。

我们还在应用的许多新部分使用 React Native。如果你观看了上个月的 F8 主旨演讲,你会认识血液捐赠(Blood Donations)、危机响应(Crisis Response)、隐私快捷方式(Privacy Shortcuts)和健康检查(Wellness Checks)——这些都是最近用 React Native 构建的功能。而且,主 Facebook 应用之外的项目也使用了 React Native。新的 Oculus Go VR 头显中包含了一个完全用 React Native 构建的伴随移动应用,更不用说 React VR 驱动着头显中的许多体验。

当然,我们也使用许多其他技术来构建我们的应用。LithoComponentKit 是我们在应用中广泛使用的两个库;它们都提供类似 React 的组件 API 用于构建原生屏幕。React Native 从未以取代所有其他技术为目标——我们专注于让 React Native 本身变得更好,但我们也很高兴看到其他团队从 React Native 借鉴了许多想法,比如将即时重新加载(instant reload)引入到非 JavaScript 代码中。

架构

当我们在 2013 年启动 React Native 项目时,我们设计了一个 JavaScript 与原生之间的单一“桥”,它是异步的、可序列化的并且是批处理的。正如 React DOM 将 React 状态更新转化为对 DOM API 的命令式、变异式调用(例如 document.createElement(attrs).appendChild()),React Native 被设计为返回一个单一的 JSON 消息,列出要执行的变更,如 [["createView", attrs], ["manageChildren", ...]]。我们设计整个系统时,确保永远不依赖于同步响应,并保证这个列表中的所有内容都能完全序列化为 JSON 并且可以还原。我们这样做是为了获得灵活性:基于此架构,我们能够构建诸如Chrome 调试器这样的工具,通过 WebSocket 连接异步运行所有 JavaScript 代码。

在过去的五年中,我们发现这些最初的原则使得某些功能的开发变得更困难。异步桥意味着你无法将 JavaScript 逻辑直接集成到许多期望同步响应的原生 API 中。批处理桥排队原生调用,导致 React Native 应用调用本地实现函数变得更难。可序列化的桥意味着不必要的复制,而不能在两者间直接共享内存。对于完全用 React Native 构建的应用,这些限制通常是可以忍受的。但对于 React Native 与现有应用代码复杂集成的应用,这些限制令人沮丧。

我们正在进行一项针对 React Native 的大规模架构重构,使框架更灵活,更好地集成于混合 JavaScript/原生应用的原生系统中。 通过这个项目,我们将应用过去五年的经验,逐步将架构升级为更现代的架构。我们正在重写 React Native 的许多内部实现,但大多数变化是在底层:现有的 React Native 应用将继续运行,几乎不需要更改。

为了让 React Native 更轻量、更适合现有原生应用,此次重构有三个主要内部改变。首先,我们修改线程模型。更新 UI 时不必总是在三个不同线程上执行工作,而是可以在任意线程同步调用 JavaScript 以处理高优先级更新,同时低优先级任务仍在主线程之外保持响应性。其次,我们将引入 异步渲染功能,支持多种渲染优先级,并简化异步数据处理。最后,我们简化桥的设计,使其更快更轻量;原生与 JavaScript 之间的直接调用更高效,也便于构建诸如跨语言调用栈跟踪的调试工具。

完成这些改动后,将实现更紧密的集成。目前,集成原生导航和手势处理,或使用 UICollectionView 和 RecyclerView 等原生组件需要复杂的 hack。更新线程模型后,构建这类功能将非常简单。

随着项目接近完成,我们将在今年晚些时候发布更多细节。

社区

除了 Facebook 内部的社区外,我们很高兴看到 Facebook 之外有一个繁荣的 React Native 用户和贡献者群体。我们希望更多支持 React Native 社区,既要更好地服务 React Native 用户,也要让项目更易于贡献。

正如架构改变将帮助 React Native 更好地与其他原生基础设施互操作,React Native 在 JavaScript 侧应该更紧凑,更好地融入 JavaScript 生态系统,其中包括使虚拟机和打包工具可替换。我们知道破坏性改动的速度可能让人难以跟上,所以希望找到减少重大版本发布次数的方法。最后,我们知道某些团队需要更全面的文档,比如启动优化方面,我们的专业知识还未完全成文。期待在未来一年看到这些改进。

如果你正在使用 React Native,你就是我们社区的一员;请继续告诉我们如何让 React Native 更好地为你服务。

React Native 是移动开发者工具箱中的一件工具,但它是我们坚信的工具——过去一年中有 500 多位贡献者提交了超过 2500 次代码,我们每天都在不断改进它。

在 React Native 中使用 TypeScript

· 阅读需 8 分钟
Ash Furrow
Artsy 软件工程师

JavaScript!我们都喜欢它。但我们中的一些人也喜欢类型。幸运的是,有办法为 JavaScript 增加更强的类型。我最喜欢的是TypeScript,但 React Native 开箱即用地支持Flow。你喜欢哪个完全是个人偏好,它们各自有自己将类型魔力赋予 JavaScript 的方法。今天,我们来看看如何在 React Native 应用中使用 TypeScript。

本文借助微软的TypeScript-React-Native-Starter 仓库作为指南。

更新:自从这篇博客文章写成以来,事情变得更简单了。你只需运行一条命令即可替代本博客中所有的设置步骤:

npx react-native init MyAwesomeProject --template react-native-template-typescript

不过,Babel 对 TypeScript 的支持仍存在一些限制,上述博客文章对此有详细说明。本文中介绍的步骤依然有效,Artsy 仍在生产中使用 react-native-typescript-transformer,但最快启动 React Native 和 TypeScript 的方式是使用上述命令。如果需要,之后你也可以随时切换。

无论如何,玩得开心!原始博客内容如下。

前提条件

因为你可能在多种不同平台开发,针对多类型设备,基本设置可能比较繁琐。你应首先确保能运行一个不带 TypeScript 的普通 React Native 应用。请按照React Native 官网的指南开始。当你成功部署到设备或模拟器后,就可以开始开发 TypeScript React Native 应用了。

你还需要安装 Node.jsnpmYarn

初始化

当你尝试搭建了一个普通的 React Native 项目后,便可以开始添加 TypeScript。我们现在动手操作。

react-native init MyAwesomeProject
cd MyAwesomeProject

添加 TypeScript

下一步为项目添加 TypeScript。以下命令将会:

  • 向项目添加 TypeScript
  • 添加 React Native TypeScript Transformer
  • 初始化一个空的 TypeScript 配置文件,我们接下来会配置它
  • 添加一个空的 React Native TypeScript Transformer 配置文件,我们也会配置它
  • 添加 React 和 React Native 的 类型定义

好,开始运行它们。

yarn add --dev typescript
yarn add --dev react-native-typescript-transformer
yarn tsc --init --pretty --jsx react
touch rn-cli.config.js
yarn add --dev @types/react @types/react-native

tsconfig.json 文件包含所有 TypeScript 编译器的设置。上述命令创建的默认配置大多合适,但打开文件并取消注释以下行:

{
/* 找到配置文件中的以下行并取消注释。 */
// "allowSyntheticDefaultImports": true, /* 允许从无默认导出的模块中默认导入。不会影响代码生成,仅用于类型检查。 */
}

rn-cli.config.js 包含 React Native TypeScript Transformer 的设置。打开它并添加以下内容:

module.exports = {
getTransformModulePath() {
return require.resolve('react-native-typescript-transformer');
},
getSourceExts() {
return ['ts', 'tsx'];
},
};

迁移到 TypeScript

将生成的 App.js__tests__/App.js 文件重命名为 App.tsxindex.js 仍须使用 .js 扩展名。所有新文件应使用 .tsx (如果文件含 JSX)或 .ts (不含 JSX)。

如果现在尝试运行应用,会出现类似 object prototype may only be an object or null 的错误。这是因为在同一行既试图导入 React 的默认导出也导入具名导出失败。打开 App.tsx,修改文件顶部的导入:

-import React, { Component } from 'react';
+import React from 'react'
+import { Component } from 'react';

部分原因是 Babel 和 TypeScript 在处理 CommonJS 模块时的互操作差异。未来两者将逐渐统一行为。

此时,你应该能运行 React Native 应用了。

添加 TypeScript 测试基础设施

React Native 默认附带 Jest,所以要用 TypeScript 测试 React Native 应用,我们需要向 devDependencies 添加 ts-jest

yarn add --dev ts-jest

接着打开项目的 package.json,替换 jest 字段为:

{
"jest": {
"preset": "react-native",
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"transform": {
"^.+\\.(js)$": "<rootDir>/node_modules/babel-jest",
"\\.(ts|tsx)$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
},
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"testPathIgnorePatterns": [
"\\.snap$",
"<rootDir>/node_modules/"
],
"cacheDirectory": ".jest/cache"
}
}

这样 Jest 就配置为用 ts-jest 运行 .ts.tsx 文件。

安装依赖的类型声明

为了在 TypeScript 中获得最佳体验,希望类型检查器能够理解我们依赖库的结构和 API。有些库会自带 .d.ts 类型声明文件,能够描述其 JavaScript 形态。其他库则需要我们显式安装 npm @types/ 范围内的对应包。

例如,这里我们需要为 Jest、React、React Native 和 React Test Renderer 安装类型:

yarn add --dev @types/jest @types/react @types/react-native @types/react-test-renderer

我们把这些声明文件包放在了 开发依赖 中,因为这是一个 React Native 应用,这些依赖只在开发阶段使用,而非运行时。如果你发布一个库到 NPM,可能需要将某些类型依赖放入正常依赖。

你可以在这里了解更多关于 .d.ts 文件的信息

忽略更多文件

为了你的源代码管理,你会想开始忽略 .jest 文件夹。如果你使用 git,直接向 .gitignore 添加条目即可。

# Jest
#
.jest/

作为一个检查点,考虑将这些文件提交到版本控制。

git init
git add .gitignore # 先添加这个很重要,这样才能忽略其他文件
git add .
git commit -am "Initial commit."

添加一个组件

我们来给应用添加一个组件。创建一个 Hello.tsx 组件。它是一个教学用途的组件,不是你在生产中必写的东西,但能展示如何在 React Native 中使用 TypeScript。

创建一个 components 目录,添加以下示例:

// components/Hello.tsx
import React from 'react';
import {Button, StyleSheet, Text, View} from 'react-native';

export interface Props {
name: string;
enthusiasmLevel?: number;
}

interface State {
enthusiasmLevel: number;
}

export class Hello extends React.Component<Props, State> {
constructor(props: Props) {
super(props);

if ((props.enthusiasmLevel || 0) <= 0) {
throw new Error(
'You could be a little more enthusiastic. :D',
);
}

this.state = {
enthusiasmLevel: props.enthusiasmLevel || 1,
};
}

onIncrement = () =>
this.setState({
enthusiasmLevel: this.state.enthusiasmLevel + 1,
});
onDecrement = () =>
this.setState({
enthusiasmLevel: this.state.enthusiasmLevel - 1,
});
getExclamationMarks = (numChars: number) =>
Array(numChars + 1).join('!');

render() {
return (
<View style={styles.root}>
<Text style={styles.greeting}>
Hello{' '}
{this.props.name +
this.getExclamationMarks(this.state.enthusiasmLevel)}
</Text>

<View style={styles.buttons}>
<View style={styles.button}>
<Button
title="-"
onPress={this.onDecrement}
accessibilityLabel="decrement"
color="red"
/>
</View>

<View style={styles.button}>
<Button
title="+"
onPress={this.onIncrement}
accessibilityLabel="increment"
color="blue"
/>
</View>
</View>
</View>
);
}
}

// 样式
const styles = StyleSheet.create({
root: {
alignItems: 'center',
alignSelf: 'center',
},
buttons: {
flexDirection: 'row',
minHeight: 70,
alignItems: 'stretch',
alignSelf: 'center',
borderWidth: 5,
},
button: {
flex: 1,
paddingVertical: 0,
},
greeting: {
color: '#999',
fontWeight: 'bold',
},
});

哇!代码挺多,但我们来拆解一下:

  • 我们不是渲染 HTML 元素比如 divspanh1,而是渲染比如 ViewButton 这样的组件。这些是跨平台的原生组件。
  • 样式通过 React Native 提供的 StyleSheet.create 函数指定。React 的样式表允许我们用 Flexbox 控制布局,使用类似 CSS 的构造进行样式设计。

添加组件测试

既然有了组件,我们就来尝试测试它。

我们已经安装了 Jest 作为测试运行器。要对组件编写快照测试,还需要安装用于快照测试的附加库:

yarn add --dev react-addons-test-utils

接着在 components 目录下创建 __tests__ 文件夹,并添加 Hello.tsx 的测试:

// components/__tests__/Hello.tsx
import React from 'react';
import renderer from 'react-test-renderer';

import {Hello} from '../Hello';

it('renders correctly with defaults', () => {
const button = renderer
.create(<Hello name="World" enthusiasmLevel={1} />)
.toJSON();
expect(button).toMatchSnapshot();
});

首次运行测试时,会生成组件渲染的快照并存储于 components/__tests__/__snapshots__/Hello.tsx.snap 文件。当你修改组件时,需要更新快照并审查改动,避免不经意的变更。你可以在这里了解更多关于测试 React Native 组件的信息

下一步

查看官方的React 教程 和状态管理库 Redux。这些资源在编写 React Native 应用时很有帮助。另外,你还可以看看 ReactXP,它是一个完全用 TypeScript 编写的组件库,同时支持网页上的 React 和 React Native。

祝你在更类型安全的 React Native 开发环境中玩得愉快!

使用 React Native 构建 - Build.com 应用

· 阅读需 5 分钟
Garrett McCullough
高级移动工程师

Build.com,总部位于加利福尼亚州奇科,是最大的家居改进商品在线零售商之一。团队已经拥有 18 年以网络为中心的业务,并于 2015 年开始考虑开发移动应用。由于团队规模不大且原生开发经验有限,构建独特的 Android 和 iOS 应用并不实际。相反,我们决定冒险采用全新的 React Native 框架。我们的首次提交是在2015年8月12日,使用 React Native v0.8.0!我们于2016年10月15日在两个应用商店上线。在过去两年中,我们持续升级和扩展应用程序。目前使用的 React Native 版本是 0.53.0。

您可以访问 https://www.build.com/app 来查看该应用。

功能

我们的应用功能完善,包含您对电商应用的所有期望:产品列表、搜索与排序、复杂产品配置功能、收藏等。我们支持标准信用卡付款方式,同时也支持 PayPal,以及为 iOS 用户提供 Apple Pay。

一些您可能没想到的特色功能包括:

  1. 大约 40 个产品配备了 90 种饰面效果的 3D 模型
  2. 增强现实 (AR) 功能,允许用户以98%的尺寸精度预览灯具和水龙头在家中的效果。Build.com React Native 应用被苹果 App Store 评为 AR 购物特色应用!AR 现已支持 Android 和 iOS!
  3. 协作项目管理功能,允许多人为不同阶段的项目制定购物清单并协作选择

我们正在开发许多新颖且令人兴奋的功能,持续提升应用体验,包括下一阶段的沉浸式 AR 购物。

我们的开发流程

Build.com 允许每位开发者选择最适合自己的工具。

  • IDE 包括 Atom、IntelliJ、VS Code、Sublime、Eclipse 等。
  • 单元测试方面,开发者负责为新组件创建 Jest 单元测试,并利用 jest-coverage-ratchet 逐步提升旧组件的测试覆盖率。
  • 我们使用 Jenkins 构建 beta 版本和发布候选版本。该流程运作良好,但仍需大量工作来创建发布说明和其他相关文档。
  • 集成测试包括一个跨桌面、移动和网页的共享测试人员池。我们的自动化工程师利用 Java 和 Appium 构建自动化集成测试套件。
  • 工作流程中还包含详细的 eslint 配置、自定义规则以强制执行测试所需属性,以及阻止有问题代码提交的 pre-push 钩子。

应用中使用的库

Build.com 应用依赖多个常用开源库,包括:Redux、Moment、Numeral、Enzyme 及大量 React Native 桥接模块。我们还使用了一些分叉的开源库,这些库或因被弃用,或因我们需要定制功能而分叉。简单统计约有 115 个 JavaScript 和原生依赖。我们希望探索能移除未使用库的工具。

我们正着手通过 TypeScript 添加静态类型支持,并研究可选链功能。这些功能能帮助我们解决几类仍存在的错误:

  • 类型错误的数据
  • 由于对象不包含预期内容导致数据未定义

开源贡献

由于我们严重依赖开源,团队致力于回馈社区。Build.com 允许团队将自建库开源,并鼓励我们为所用库做贡献。

我们已经发布并维护了多款 React Native 库:

  • react-native-polyfill
  • react-native-simple-store
  • react-native-contact-picker

我们也向许多库做出了贡献,包括:React 和 React Native、react-native-schemes-managerreact-native-swipeablereact-native-galleryreact-native-view-transformerreact-native-navigation

我们的旅程

过去几年,我们见证了 React Native 及其生态的巨大成长。早期,每个 React Native 版本似乎都修复了一些 bug,却引入了更多问题。例如,Android 上的远程 JS 调试曾断裂数月。幸运的是,2017 年开始稳定性大为提升。

导航库

导航库一直是我们面临的大难题之一。很长一段时间内,我们使用 Expo 的 ex-nav 库。它运行良好,但最终被弃用。然而,当时我们正处于功能密集开发期,无暇替换导航库,因此不得不分叉该库并为支持 React 16 和 iPhone X 自行打补丁。最终,我们迁移到了 react-native-navigation,希望它能持续获得支持。

桥接模块

桥接模块同样存在不小挑战。初期,很多关键桥接缺失。我的一位队友开发了 react-native-contact-picker,以便应用访问 Android 通讯录选择器。我们也遇见了许多因 React Native 变动而失效的桥接。例如 React Native v40 版本引入了破坏性变更,升级时我不得不提交 PR 修复 3 至 4 个尚未更新的库。

展望未来

随着 React Native 继续发展,我们对社区的期望包括:

  • 稳定并改进导航库
  • 维护 React Native 生态中的库支持
  • 优化添加原生库及桥接模块的体验

React Native 社区中的公司及个人在改善我们共同使用的工具上做出了巨大努力。如果您还未参与开源,真诚邀请您考虑为您使用的库贡献代码或文档。网上有许多入门文章,过程可能比想象的简单许多!

为 React Native 构建 <InputAccessoryView>

· 阅读需 7 分钟
Peter Argany
Facebook 软件工程师

动机

三年前,有人在 GitHub 上提出了一个 issue,要求 React Native 支持输入辅助视图(input accessory view)。

在随后的几年中,这个 issue 收到了无数的“+1”、各种解决方案,但 React Native 本身却没有做出任何实质性的改变——直到今天。我们从 iOS 开始,新提供了一个用于访问原生输入辅助视图的 API(详细文档),并且很兴奋与大家分享我们的构建过程。

背景

输入辅助视图到底是什么?根据 Apple 的开发文档,它是一个自定义视图,当一个响应者(receiver)成为第一响应者时,可以锚定在系统键盘的顶部。任何继承自 UIResponder 的对象都可以重新声明 .inputAccessoryView 属性为可读写,并管理这里的自定义视图。响应者架构负责挂载该视图,并保持其与系统键盘的同步。像拖拽或点击这些会关闭键盘的手势,会在框架层级应用到输入辅助视图上。这使我们可以构建具有交互式键盘关闭功能的内容,这是顶级消息应用(如 iMessage 和 WhatsApp)中的一个重要特性。

在键盘顶部锚定视图有两个常见应用场景。第一个是创建键盘工具栏,比如 Facebook 创作器中的背景选择器。

在此场景中,键盘聚焦于一个文本输入框,输入辅助视图用于提供额外的键盘功能。该功能与输入框的类型相关。在地图应用中,可能是地址建议;在文本编辑器中,可能是富文本格式化工具。


拥有 <InputAccessoryView> 的 Objective-C UIResponder 应该很明确。<TextInput> 变成了第一响应者,其底层实际上是一个 UITextViewUITextField 实例。

第二个常见场景是“粘性”文本输入框:

这里,文本输入框实际上是输入辅助视图本身的子视图。这在消息应用中很常见,用户可以在滚动浏览之前的消息线程时撰写消息。


在这个例子中,谁拥有 <InputAccessoryView>?还能是 UITextViewUITextField 吗?文本输入框 位于 输入辅助视图内部,这听起来像是循环依赖。单独解决这个问题就足够写成一篇 博客文章。剧透一句:拥有者是一个通用的 UIView 子类,我们手动让它 becomeFirstResponder

API 设计

我们现在知道 <InputAccessoryView> 是什么,也知道它的使用方式。下一步是设计一个既符合这两个用例,又能很好地与现有 React Native 组件(如 <TextInput>)配合使用的 API。

对于键盘工具栏,我们需要考虑以下几点:

  1. 我们希望能够将任何通用的 React Native 视图层级“提升”至 <InputAccessoryView> 中。
  2. 我们希望这个通用且独立的视图层级能接受触摸事件,并能操作应用状态。
  3. 我们希望能将一个 <InputAccessoryView> 关联到特定的 <TextInput>
  4. 我们希望能在多个文本输入框之间共享一个 <InputAccessoryView>,而无需复制任何代码。

我们可以用类似 React 门户(portals) 的概念来实现第 1 点。在此设计中,我们将 React Native 视图“门户”到一个由响应者架构管理的 UIView 层级。因为 React Native 视图本质上是 UIViews,所以做起来相当简单——我们只需要重写:

- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex

将所有子视图导入一个新的 UIView 层级即可。第 2 点上,我们为 <InputAccessoryView> 设置了一个新的 RCTTouchHandler。状态更新通过常规事件回调完成。第 3 和第 4 点,我们通过 nativeID 字段,在创建 <TextInput> 组件时,通过原生代码定位辅助视图的 UIView 层级。相关函数使用了底层的原生文本输入的 .inputAccessoryView 属性,从而实现了在 ObjC 层面的 <InputAccessoryView><TextInput> 关联。

支持粘性文本输入(场景 2)则有更多约束。因为该设计中,输入辅助视图有一个文本输入作为子视图,无法通过 nativeID 关联。相反,我们将一个通用的、位于屏幕外的 UIView.inputAccessoryView 设置为我们的原生 <InputAccessoryView> 层级,通过手动让这个通用 UIView 变成第一响应者,响应者架构将会挂载该层级。这个概念在前文提到的博客文章中有详细解释。

陷阱

当然,构建这个 API 并非一帆风顺。以下是我们遇到的一些坑及其解决方案。

最初搭建这个 API 的想法是监听 NSNotificationCenter 的 UIKeyboardWill(Show/Hide/ChangeFrame) 事件。这种模式在一些开源库和 Facebook App 的部分内部代码中都有使用。不幸的是,UIKeyboardDidChangeFrame 事件在手势滑动时无法及时调用以更新 <InputAccessoryView> 的 frame。此外,键盘高度的变化也未被这些事件捕获。这就导致了如图所示的那类界面异常:

对于 iPhone X,文本键盘和表情键盘高度不同。大多数依靠键盘事件来调整文本输入框的应用都必须修复上述 bug。我们的解决方案是改用 .inputAccessoryView 属性,这样响应者架构会处理 frame 的更新。


另一个棘手的 bug 是避免遮挡 iPhone X 的 Home Pill(底部指示条)。你可能会想,“Apple 为此专门开发了 safeAreaLayoutGuide,这不简单吗?”。我们也曾天真这样想。第一个问题是,原生 <InputAccessoryView> 实现直到即将显示时才有 window 可供锚定。没关系,我们可以重写 -(BOOL)becomeFirstResponder 方法,在那里强制执行布局约束。遵循这些约束会将输入辅助视图上移,但出现了另一个 bug:

输入辅助视图成功避开了 Home Pill,但不安全区域背后的内容会显示出来。解决方案见这个 Radar。我用一个不遵守 safeAreaLayoutGuide 约束的容器包裹了原生 <InputAccessoryView> 层级。原生容器覆盖了不安全区域的内容,而 <InputAccessoryView> 保持在安全区边界以内。


使用示例

下面是一个构建键盘工具栏按钮以重置 <TextInput> 状态的示例。

class TextInputAccessoryViewExample extends React.Component<
{},
*,
> {
constructor(props) {
super(props);
this.state = {text: 'Placeholder Text'};
}

render() {
const inputAccessoryViewID = 'inputAccessoryView1';
return (
<View>
<TextInput
style={styles.default}
inputAccessoryViewID={inputAccessoryViewID}
onChangeText={text => this.setState({text})}
value={this.state.text}
/>
<InputAccessoryView nativeID={inputAccessoryViewID}>
<View style={{backgroundColor: 'white'}}>
<Button
onPress={() =>
this.setState({text: 'Placeholder Text'})
}
title="Reset Text"
/>
</View>
</InputAccessoryView>
</View>
);
}
}

另一个关于 粘性文本输入的示例可以在仓库中找到

什么时候可以使用?

该功能的完整提交记录在 这里<InputAccessoryView> 将在即将发布的 v0.55.0 版本中提供。

祝你输入顺畅 :)

在 React Native 中使用 AWS

· 阅读需 10 分钟
Richard Threlkeld
AWS Mobile 高级技术产品经理

AWS 在技术行业中以提供云服务著称。这些服务包括计算、存储和数据库技术,以及全托管的无服务器产品。AWS Mobile 团队一直与客户和 JavaScript 生态系统的成员紧密合作,致力于使云连接的移动和 Web 应用更安全、可扩展,并且更易于开发和部署。我们一开始提供了一个完整的入门套件,随后又推出了一些新的发展。

本文介绍了 React 和 React Native 开发者可能感兴趣的一些内容:

  • AWS Amplify:一个为使用云服务的 JavaScript 应用设计的声明式库
  • AWS AppSync:一个具备离线和实时功能的全托管 GraphQL 服务

AWS Amplify

使用 Create React Native App 和 Expo 等工具,启动 React Native 应用非常简单。然而,将其连接到云端时,尝试将具体用例映射到基础设施服务可能会较为复杂。例如,您的 React Native 应用可能需要上传照片。这些照片是否需要按用户保护?那很可能意味着您需要某种注册或登录流程。您是想使用自己的用户目录,还是使用社交媒体提供商?可能您的应用还需要在用户登录后调用带有自定义业务逻辑的 API。

为帮助 JavaScript 开发者解决这些问题,我们发布了名为 AWS Amplify 的库。其设计被划分为“类别”,而非 AWS 特定实现。例如,如果您希望用户注册、登录然后上传私有照片,只需将 AuthStorage 类别引入到应用中:

import { Auth } from 'aws-amplify';

Auth.signIn(username, password)
.then(user => console.log(user))
.catch(err => console.log(err));

Auth.confirmSignIn(user, code)
.then(data => console.log(data))
.catch(err => console.log(err));

上面的代码展示了 Amplify 在一些常见任务上的帮助,比如使用多因素认证 (MFA) 码(通过电子邮件或短信)。目前支持的类别包括:

  • Auth:提供凭证自动化。开箱即用实现使用 AWS 凭证进行签名,并通过 Amazon Cognito 的 OIDC JWT 令牌。支持诸如 MFA 之类的常用功能。
  • Analytics:只需一行代码,即可在 Amazon Pinpoint 跟踪已认证或未认证的用户。可根据需求扩展自定义指标或属性。
  • API:以安全方式提供与 RESTful API 的交互,利用 AWS Signature Version 4。该模块非常适合使用 Amazon API Gateway 的无服务器基础设施。
  • Storage:简化命令上传、下载和列出 Amazon S3 中的内容。还可根据用户将数据分组为公共或私有内容。
  • Caching:跨 Web 应用和 React Native 的 LRU 缓存接口,使用特定实现的持久化。
  • i18n 和 Logging:提供国际化和本地化功能,以及调试和日志功能。

Amplify 的一个优点是,它在设计中为您的特定编程环境编码了“最佳实践”。例如,我们与客户及 React Native 开发者合作时发现,开发阶段为快速实现功能所选的捷径往往会最终进入生产环境。这可能会影响扩展性或安全性,并迫使进行基础设施重构和代码重写。

帮助开发者避免此类问题的一个例子是 基于 AWS Lambda 的无服务器参考架构。它展示了结合使用 Amazon API Gateway 和 AWS Lambda 构建后端的最佳实践模式。该模式被编码进 Amplify 的 API 类别中。您可以使用这一模式与多个 REST 端点交互,并将请求头传递给 Lambda 函数以实现自定义业务逻辑。我们还发布了一个 AWS Mobile CLI,方便快速为新的或现有的 React Native 项目引入这些功能。入门只需通过 npm 安装并按照配置提示进行:

npm install --global awsmobile-cli
awsmobile configure

另一个与移动生态系统相关的最佳实践示例是密码安全。默认的 Auth 类别实现利用 Amazon Cognito 用户池进行用户注册和登录。该服务采用安全远程密码协议 来保护用户身份验证过程。如果您有兴趣阅读该协议的数学原理,您会发现计算密码验证器时必须使用一个大素数来生成一个群。在 React Native 环境中,JIT 被禁用,这使得进行类似安全操作所需的大整数计算性能较低。为此,我们发布了适用于 Android 和 iOS 的本地桥接,您可在项目中链接:

npm install --save aws-amplify-react-native
react-native link amazon-cognito-identity-js

此外,我们很高兴看到 Expo 团队已在其最新 SDK中包含了这点,您可以无需弹出(eject)即可使用 Amplify。

最后,针对 React Native(及 React)开发,Amplify 提供了 高阶组件 (HOCs),方便包裹功能,例如对应用进行注册和登录:

import Amplify, { withAuthenticator } from 'aws-amplify-react-native';
import aws_exports from './aws-exports';

Amplify.configure(aws_exports);

class App extends React.Component {
...
}

export default withAuthenticator(App);

底层组件也提供为 <Authenticator />,允许您完全自定义 UI。它还提供了用于管理用户状态(如是否已登录或等待 MFA 确认)以及状态变化时可触发回调的属性。

同样,您还会找到通用的 React 组件,可用于各种用例。您可以根据需求自定义,例如展示来自 Storage 模块中 Amazon S3 的所有私有图片:

<S3Album
level="private"
path={path}
filter={(item) => /jpg/i.test(item.path)} />

如前所示,您可通过属性控制组件的许多功能,包括公共或私有存储选项。甚至提供了用户与某些 UI 组件交互时自动收集分析数据的能力:

return <S3Album track/>

AWS Amplify 推崇约定优于配置的开发风格,支持全局初始化或按类别级别初始化。最快速的入门方式是使用一个 aws-exports 文件。不过开发者也可以独立地使用该库与已有资源协作。

想深入了解其设计理念及完整演示,请参阅 AWS re:Invent 的相关视频

AWS AppSync

在 AWS Amplify 发布不久后,我们还推出了 AWS AppSync。这是一项具备离线和实时能力的全托管 GraphQL 服务。虽然 GraphQL 可用于多种客户端语言(包括原生 Android 和 iOS),但它在 React Native 开发者中非常受欢迎,因为数据模型非常符合单向数据流和组件层次结构。

AWS AppSync 使您能够连接到自己 AWS 账户中的资源,意味着数据归您所有且您能完全控制。这通过使用数据源实现,服务支持 Amazon DynamoDBAmazon ElasticsearchAWS Lambda。这使得您可以在单一 GraphQL API 模式下结合使用多种功能(如 NoSQL 和全文搜索)。AppSync 还支持从模式自动配置资源,如果您不熟悉 AWS 服务,只需编写 GraphQL SDL,点击按钮即可自动完成构建。

AWS AppSync 的实时功能通过使用熟知的事件驱动模式的 GraphQL 订阅实现。由于 AWS AppSync 的订阅受 GraphQL 模式中的指令控制,且模式可以使用任意数据源,这意味着您可以基于 Amazon DynamoDB 和 Amazon Elasticsearch 服务的数据库操作,或基于 AWS Lambda 的其他基础设施触发通知。

类似于 AWS Amplify,您可以在 AWS AppSync 的 GraphQL API 上使用企业级安全功能。该服务允许通过 API 密钥快速启动,但生产环境下可以转而使用 AWS 身份和访问管理 (IAM) 或来自 Amazon Cognito 用户池的 OIDC 令牌。您可以通过类型策略在解析器层级控制访问,甚至可以在运行时进行逻辑检查,实现细粒度访问控制(例如检测用户是否为特定数据库资源的所有者)。此外,还支持基于组成员资格来执行解析器或访问单个数据库记录的权限控制。

为了帮助 React Native 开发者更好地了解这些技术,AWS AppSync 控制台首页内置了一个GraphQL 示例模式。该示例会自动部署 GraphQL 模式,配置数据库表,并连接查询、变更和订阅。另有一个基于此内置模式的功能齐全的AWS AppSync React Native 示例(以及React 示例),可助您在数分钟内启动客户端和云端组件。

使用 AWSAppSyncClient 启动十分简单,它集成于 Apollo ClientAWSAppSyncClient 负责 GraphQL API 的安全性和签名、离线功能,以及订阅的握手和协商过程:

import AWSAppSyncClient from "aws-appsync";
import { Rehydrated } from 'aws-appsync-react';
import { AUTH_TYPE } from "aws-appsync/lib/link/auth-link";

const client = new AWSAppSyncClient({
url: awsconfig.graphqlEndpoint,
region: awsconfig.region,
auth: {type: AUTH_TYPE.API_KEY, apiKey: awsconfig.apiKey}
});

AppSync 控制台提供可下载的配置文件,包含您的 GraphQL 端点、AWS 区域和 API 密钥。您接着可以将客户端与React Apollo一同使用:

const WithProvider = () => (
<ApolloProvider client={client}>
<Rehydrated>
<App />
</Rehydrated>
</ApolloProvider>
);

此时,您可以使用标准 GraphQL 查询:

query ListEvents {
listEvents{
items{
__typename
id
name
where
when
description
comments{
__typename
items{
__typename
eventId
commentId
content
createdAt
}
nextToken
}
}
}
}

上例展示了 AppSync 提供的示例应用模式中的查询。它不仅展示了与 DynamoDB 的交互,还包含了数据分页(包括加密令牌)以及 EventsComments 之间的类型关系。由于应用已配置 AWSAppSyncClient,数据会自动离线持久化,并在设备重新连接时同步。

您可以观看此视频,深入了解其客户端技术及 React Native 演示

反馈

这些库背后的团队期待听到您对它们的使用体验反馈。他们也希望了解还能做些什么,以便让使用云服务的 React 和 React Native 开发更简单。请通过 GitHub 联系 AWS Mobile 团队,相关项目为 AWS AmplifyAWS AppSync

在 React Native 中实现 Twitter 的应用加载动画

· 阅读需 10 分钟
Eli White
Eli White
Software Engineer @ Meta

Twitter 的 iOS 应用有一个我非常喜欢的加载动画。

当应用准备好时,Twitter 标志会令人愉快地放大,展现出应用界面。

我想弄明白如何用 React Native 重新创建这个加载动画。


为了理解_如何_去构建它,我首先必须理解加载动画的不同部分。最简单的查看细微差别的方法是将动画放慢。

这里有几个主要部分我们需要弄清楚如何构建。

  1. 对小鸟进行缩放。
  2. 随着小鸟变大,露出下面的应用。
  3. 在结束时稍微缩小应用。

我花了相当长时间才弄明白如何制作这个动画。

我一开始有一个_错误_的假设,以为蓝色背景和 Twitter 小鸟是在应用的_上层_,当小鸟放大时它变得透明,露出下面的应用。这个方法行不通,因为 Twitter 小鸟变透明会显示蓝色层,而不是下面的应用!

幸运的是,亲爱的读者,你不用经历我那样的挫折。你得到的是这个跳过废话的精彩教程!


正确的方法

在进入代码之前,了解如何拆解这个效果非常重要。为了帮助可视化这个效果,我在 CodePen 中重新创建了它(嵌入于几段文字中),这样你可以交互式地查看不同的图层。

这个效果主要有三个图层。第一层是蓝色背景层。虽然看起来它出现在应用上方,但实际上它是在后面。

然后是一个纯白色层。最后,最前面是我们的应用。


这个动画的关键是使用 Twitter 标志作为蒙版,并对应用层和白色层进行蒙版处理。我不会深入讲解蒙版的细节,网上有大量 资料 参考

在这里蒙版的基础概念是,蒙版中不透明的像素显示它所蒙版的内容,而透明的像素则隐藏它所蒙版的内容。

我们用 Twitter 标志作为蒙版,同时蒙版两个层;纯白色层和应用层。

为了显示应用,我们将蒙版放大,直到它比整个屏幕还大。

在蒙版放大的同时,我们将应用层的透明度淡入,显示应用并隐藏后面的纯白层。为了完成效果,我们让应用层开始时缩放略大于 1,然后动画结束时缩放到 1。之后隐藏非应用层,因为它们将不再被看到。

俗话说一图胜千言。一个交互式可视化值多少字?点击“下一步”按钮,逐步查看动画。显示不同图层可以让你拥有侧视角度。网格帮助可视化透明图层。

现在,讲讲 React Native 实现

好啦。现在我们知道想做什么以及动画如何工作,可以进入代码了——这才是你真正关心的。

这个谜题的核心是 MaskedViewIOS,这是 React Native 的核心组件。

import {MaskedViewIOS} from 'react-native';

<MaskedViewIOS maskElement={<Text>Basic Mask</Text>}>
<View style={{backgroundColor: 'blue'}} />
</MaskedViewIOS>;

MaskedViewIOS 接收 props maskElementchildren。children 会被 maskElement 蒙版。注意,蒙版不一定非得是图片,可以是任意视图。上面示例的效果是在“Basic Mask”的文字区域显示蓝色视图,其他地方透明。我们刚刚用蓝色文字制造了复杂的效果。

我们想做的是先渲染蓝色层,然后在其上用 Twitter 标志蒙版渲染应用和白色层。

{
fullScreenBlueLayer;
}
<MaskedViewIOS
style={{flex: 1}}
maskElement={
<View style={styles.centeredFullScreen}>
<Image source={twitterLogo} />
</View>
}>
{fullScreenWhiteLayer}
<View style={{flex: 1}}>
<MyApp />
</View>
</MaskedViewIOS>;

这会得到我们下面看到的图层。

现在进入动画部分

我们已有制作动画所需的所有部分,接下来要动起来。为了让动画体验顺畅,我们将使用 React Native 的 Animated API。

Animated 允许我们在 JavaScript 中声明式地定义动画。默认情况下,动画在 JavaScript 中运行,每帧告诉原生层要做哪些变化。即使 JavaScript 尝试每帧更新动画,通常也追不上,可能出现掉帧(卡顿)。这可不是我们想要的!

Animated 有特殊机制避免卡顿。它有个叫 useNativeDriver 的 flag,会在动画开始时将动画定义从 JavaScript 发送到原生,让原生端自己处理动画更新,无须每帧都回传 JavaScript。useNativeDriver 的限制是只能更新特定属性,主要是 transformopacity。目前还不能用它来更新背景颜色等属性——未来会增加更多,当然你也能为这个项目提交 PR,造福社区😀。

既然想让动画顺滑,我们就在这些限制内工作。想了解useNativeDriver内部工作原理,可以看我们的宣布博客

拆解动画步骤

我们的动画包含 4 个部分:

  1. 放大小鸟,露出应用和纯白层。
  2. 让应用淡入。
  3. 缩小应用。
  4. 动画结束后隐藏白层和蓝层。

用 Animated,有两种主流定义动画的方式。第一种是用 Animated.timing,指定动画时长和缓动曲线让移动更自然。另一种是用基于物理的 API,如 Animated.spring,指定阻力和张力参数,让动画由物理模拟驱动。

这里有多段紧密相关的动画同时运行,比如我们希望应用在蒙版中途放大时开始淡入。因为它们紧密相关,我们将用单个 Animated.Value 结合 Animated.timing

Animated.Value 是 Animated 对原生值的包装器,表示动画的当前状态。通常一个完整动画只用一个。大多数 Animated 组件会把它存在 state 里。

我把动画视作不同时刻发生的步骤,让 Animated.Value 从 0 开始,表示 0% 完成度,最终到 100,表示 100% 完成度。

初始组件 state 如下:

state = {
loadingProgress: new Animated.Value(0),
};

当动画准备好开始时,调用 Animated 去动画到 100。

Animated.timing(this.state.loadingProgress, {
toValue: 100,
duration: 1000,
useNativeDriver: true, // 这一点很重要!
}).start();

我接下来大致估算动画不同阶段应该是多少数值,以下是动画不同片段的取值预期随时间进度的表格。

Twitter 小鸟蒙版初始 scale 是 1,会先缩小一点,然后猛涨到超大,动画快结束时大约是 70。70 是个经验值,要确保它完全覆盖屏幕,60 不够😀。这里有趣的是,数字越大,看上去变大的速度越快,因为时间总量不变。这部分调试花了不少时间确保与标志搭配效果好。不同标志和设备,最终 scale 需调整以覆盖全屏。

应用保持不透明一段时间,至少在 Twitter 标志缩小时。官方动画上,我想在小鸟放大到一半时开始显示应用,到动画 30% 处完全显示。

应用初始化 scale 是 1.1,动画结束缩放回 1。

现在,代码实践。

我们在上面做的就是根据动画进度百分比映射到各个部分的数值。用 Animated 的 .interpolate 方法实现。创建三个样式对象,每个对应动画不同部分,用 this.state.loadingProgress 作插值。

const loadingProgress = this.state.loadingProgress;

const opacityClearToVisible = {
opacity: loadingProgress.interpolate({
inputRange: [0, 15, 30],
outputRange: [0, 0, 1],
extrapolate: 'clamp',
// clamp 意味着当输入为 30-100 时,输出保持为 1
}),
};

const imageScale = {
transform: [
{
scale: loadingProgress.interpolate({
inputRange: [0, 10, 100],
outputRange: [1, 0.8, 70],
}),
},
],
};

const appScale = {
transform: [
{
scale: loadingProgress.interpolate({
inputRange: [0, 100],
outputRange: [1.1, 1],
}),
},
],
};

现在有了这几个样式对象,我们可以在渲染时使用它们。注意,只有 Animated.ViewAnimated.TextAnimated.Image 组件支持带有 Animated.Value 的样式。

const fullScreenBlueLayer = (
<View style={styles.fullScreenBlueLayer} />
);
const fullScreenWhiteLayer = (
<View style={styles.fullScreenWhiteLayer} />
);

return (
<View style={styles.fullScreen}>
{fullScreenBlueLayer}
<MaskedViewIOS
style={{flex: 1}}
maskElement={
<View style={styles.centeredFullScreen}>
<Animated.Image
style={[styles.maskImageStyle, imageScale]}
source={twitterLogo}
/>
</View>
}>
{fullScreenWhiteLayer}
<Animated.View
style={[opacityClearToVisible, appScale, {flex: 1}]}>
{this.props.children}
</Animated.View>
</MaskedViewIOS>
</View>
);

耶!动画部分已经符合预期。现在只需清理我们的蓝色和白色层,因为它们动画结束后都不再显示。

为了知道何时清理它们,我们需要知道动画是否完成。幸运的是,调用 Animated.timing.start 方法时,可以传入动画结束时执行的回调。

Animated.timing(this.state.loadingProgress, {
toValue: 100,
duration: 1000,
useNativeDriver: true,
}).start(() => {
this.setState({
animationDone: true,
});
});

有了 state 中的 animationDone 字段,知道动画是否结束后,我们可以根据它动态渲染蓝色和白色层。

const fullScreenBlueLayer = this.state.animationDone ? null : (
<View style={[styles.fullScreenBlueLayer]} />
);
const fullScreenWhiteLayer = this.state.animationDone ? null : (
<View style={[styles.fullScreenWhiteLayer]} />
);

就这样!动画现在可以正常工作,完成后自动清理不需要的图层。我们复刻了 Twitter 应用的加载动画!

等等,我的动画不工作!

别担心,亲爱的读者。我也讨厌那种只给你代码片段,却不提供完整源码的教程。

这个组件已经发布到 npm,且托管在 GitHub 上,地址是 react-native-mask-loader。想在手机上试试,可以在 Expo 上打开

延伸阅读 / 额外练习

  1. 这本 gitbook 是学 Animated 后非常棒的资源。
  2. Twitter 官方动画在末尾会加快蒙版的展开速度。试试修改 loader,用不同的缓动函数(或者 spring!)来更贴近官方效果。
  3. 蒙版最终 scale 目前是硬编码的,可能无法在平板等大屏设备上完整展示应用。根据屏幕尺寸和图片大小计算最终 scale 会是个超赞的 PR 贡献。

React Native 每月速递 #6

· 阅读需 4 分钟
Tomislav Tenodi
Speck 创始人

React Native 月度会议依然如火如荼地进行着!请务必查看本文底部的备注,了解下一次会议时间。

Expo

  • 祝贺 Devin AbbottHoussein Djirdeh 发布了《Full Stack React Native》一书的预发布版!这本书通过构建多个小应用来带你学习 React Native。
  • 发布了第一个(实验性)版本的 reason-react-native-scripts,帮助大家轻松尝试 ReasonML
  • Expo SDK 24 已发布!它使用了 React Native 0.51,包括许多新功能和改进:独立应用中的图片打包(首次加载无需缓存!)、图片处理 API(裁切、缩放、旋转、翻转)、人脸检测 API、全新发布频道功能(为指定频道设置活跃版本及回滚)、用于跟踪独立应用构建的网页仪表盘,以及修复了 Android OpenGL 实现与多任务管理器的长期 bug,仅举几例。
  • 从今年一月开始,我们将投入更多资源支持 React Navigation。我们坚信仅使用 React 组件及 Animated 和 react-native-gesture-handler 等基础库来构建 React Native 导航既可能又理想,我们对计划中的一些改进感到非常兴奋。如果你想为社区贡献力量,可以关注 react-native-mapsreact-native-svg,两者都非常需要帮助!

Infinite Red

Microsoft

  • 已启动一个拉取请求,旨在将 React Native Windows 桥接核心迁移到 .NET Standard,从而实现跨操作系统通用。这样许多其他 .NET Core 平台就能用自己的线程模型、JavaScript 运行时和 UIManager 扩展该桥接(例如 JavaScriptCore、Xamarin.Mac、Linux Gtk#及三星 Tizen 选项)。

Wix

  • Detox
    • 为了让端到端测试规模化并减少 CI 时间,我们正在开发 Detox 的并行支持。
    • 提交了拉取请求,以支持自定义 flavor 构建,更好地支持 E2E 测试的 mock。
  • DetoxInstruments
    • DetoxInstruments 的杀手级功能开发极具挑战性,任意时刻获取 JavaScript 调用栈需要 JSCore 的自定义实现以支持 JS 线程挂起。在 Wix 内部 app 上测试该性能分析器揭示了 JS 线程的一些有趣见解。
    • 该项目尚不够稳定供公众使用,但开发积极进行中,我们希望很快能发布。
  • React Native Navigation
    • V2 版本开发速度显著提升,之前仅有一名开发者兼职(20% 时间)开发,现在已有 3 名开发者全职投入!
  • Android 性能优化
    • 用 RN 捆绑的旧版 JSCore 替换为它的最新版本(基于 webkitGTK 项目的最新代码,带有自定义 JIT 配置)使 JS 线程性能提升了40%。接下来将编译 64 位版本。此工作基于 JSC 安卓构建脚本。可在此处查看当前状态。

下一次会议

目前有讨论考虑将本次会议重心调整为集中讨论单一具体主题(例如导航、将 React Native 模块拆分到独立仓库、文档等)。这样我们觉得能为 React Native 社区贡献更多。这可能会在下次会议中实施。欢迎通过推特告诉我们你希望被讨论的主题。

React Native 月报 #5

· 阅读需 4 分钟
Tomislav Tenodi
Speck 创始人

React Native 月度会议继续进行中!让我们看看各团队最近在忙些什么。

Callstack

  • 我们一直在完善 React Native CI。最重要的是,我们已从 Travis 迁移到 Circle,使 React Native 只保留一个统一的 CI 流程。
  • 我们组织了 Hacktoberfest - React Native 版本,与参与者一起尝试向开源项目提交大量 Pull Request。
  • 我们持续开发 Haul。上个月我们发布了两个新版本,包括 webpack 3 支持。我们计划添加对 CRNAExpo 的支持,并改进热模块替换(HMR)。我们的路线图已在 issue 跟踪器公开。如果你想建议改进或提供反馈,欢迎告诉我们!

Expo

  • 发布了 Expo SDK 22(基于 React Native 0.49),并为此更新了 CRNA
    • 包含改进的启动屏 API、基本的 ARKit 支持、“DeviceMotion” API、iOS 11 上的 SFAuthenticationSession 支持及更多内容
  • 你的 snacks 现在可以包含多个 JavaScript 文件,同时可以通过拖拽直接上传图片和其它资源到编辑器中。
  • 贡献代码到 react-navigation,以新增对 iPhone X 的支持。
  • 聚焦解决使用 Expo 构建大型应用时遇到的难点。例如:
    • 多环境部署的一级支持:包括 staging、生产以及任意频道。频道支持回滚并设置当前发布版本。如果你想成为早期测试者,请告诉我们,@expo_io
    • 我们也正在改进独立应用构建基础设施,新增在独立应用构建时打包图片和其它非代码资源的支持,同时保持通过 OTA 更新资源的能力。

Facebook

  • 更好的 RTL 支持:
    • 我们引入了多种方向感知的样式。
      • 位置:
        • (left|right) → (start|end)
      • 外边距:
        • margin(Left|Right) → margin(Start|End)
      • 内边距:
        • padding(Left|Right) → padding(Start|End)
      • 边框:
        • borderTop(Left|Right)Radius → borderTop(Start|End)Radius
        • borderBottom(Left|Right)Radius → borderBottom(Start|End)Radius
        • border(Left|Right)Width → border(Start|End)Width
        • border(Left|Right)Color → border(Start|End)Color
    • 在 RTL 中,“left”和“right” 在 position、margin、padding 和 border 样式中的含义被交换。几个月内,我们将移除此行为,使“left”始终表示“左”,“right”始终表示“右”。此破坏性更改被隐藏在一个开关下。在 React Native 组件中使用 I18nManager.swapLeftAndRightInRTL(false) 以启用此改动。
  • 正在为我们的内部原生模块添加 Flow 类型,并利用这些类型生成 Java 中的接口和 ObjC 中的协议,原生实现必须遵循这些协议。我们希望这些代码生成工具最早能在明年开源。

Infinite Red

  • 推出一款新的开源工具,帮助 React Native 及其他项目。详情见这里
  • 正在重构 Ignite,为新版本模版做准备(代号:Bowser)。

Shoutem

  • 改善 Shoutem 的开发流程。我们希望简化从创建应用到第一个自定义界面的过程,让新手 React Native 开发者更容易上手。准备了几个工作坊测试新功能。我们还改进了 Shoutem CLI 来支持新的流程。
  • Shoutem UI 收到了若干组件改进和 bug 修复。我们也检查了其与最新 React Native 版本的兼容性。
  • Shoutem 平台收到了若干显著更新,新集成作为 开源扩展项目 的一部分发布。看到其他开发者积极为 Shoutem 扩展贡献代码,我们感到十分兴奋。我们积极联系并提供建议与指导。

下一次会议

下一次会议定于 2017 年 12 月 6 日星期三。如有任何建议,欢迎通过 Twitter 联系我,告诉我们如何改进会议成果。