带有“工程”标签的 26 篇文章
查看所有标签React Native 0.71-RC0 Android 故障后记
现在 0.71 已可用,我们希望分享一些关于导致所有 React Native 版本的 Android 构建中断事件的关键信息,该事件发生在 2022 年 11 月 4 日发布 React Native 和 Expo Android 构建的第一个 0.71 发布候选版本时。
最近,帮助解决此事件的贡献者参加了一次事后分析会议,详细讨论了事件发生的过程、我们从中吸取的教训以及我们将采取哪些措施来避免将来发生类似的故障。
TypeScript 的一等公民支持
随着 0.71 的发布,React Native 通过以下更改对 TypeScript 体验进行了投资
在这篇文章中,我们将介绍这些更改对您作为 TypeScript 或 Flow 用户意味着什么。
为 iOS 15 和 Android 12 准备您的应用
大家好!
随着新的移动操作系统版本在今年晚些时候发布,我们建议您提前准备您的 React Native 应用,以避免在这些版本普遍可用时出现回归。
引入新的 iOS WebViews
长期以来,Apple 一直鼓励使用 WKWebView 而不是 UIWebViews。在即将发布的 iOS 12 中,UIWebViews 将正式弃用。React Native 的 iOS WebView 实现严重依赖 UIWebView 类。因此,鉴于这些发展,我们为 WebView React Native 组件构建了一个新的原生 iOS 后端,该后端使用 WKWebView。
这些更改的结尾部分已在此提交中完成,并将可在 0.57 版本中使用。
要选择加入此新实现,请使用useWebKit
属性
<WebView
useWebKit={true}
source={{url: 'https://www.google.com'}}
/>
改进
UIWebView
没有合法的方法来促进在 WebView 中运行的 JavaScript 与 React Native 之间的通信。当从 WebView 发送消息时,我们依赖于一个技巧将其传递给 React Native。简而言之,我们将消息数据编码到具有特殊方案的 URL 中,并导航 WebView 到该 URL。在原生端,我们拦截并取消了此导航,从 URL 中解析数据,最后调用 React Native。此实现容易出错且不安全。我很高兴地宣布,我们已利用 WKWebView
功能完全替换了它。
与 UIWebView 相比,WKWebView 的其他优势包括更快的 JavaScript 执行和多进程架构。请参阅此2014 年 WWDC以了解更多详细信息。
注意事项
如果您的组件使用以下属性,则在切换到 WKWebView 时可能会遇到问题。目前,我们建议您避免使用这些属性
不一致的行为
automaticallyAdjustContentInsets
和 contentInsets
(提交)
当您向 WKWebView
添加 contentInsets 时,它不会更改 WKWebView
的视口。视口的大小保持与框架相同。使用 UIWebView
时,视口大小实际上会发生变化(如果内容内边距为正,则会变小)。
backgroundColor
(提交)
使用 WebView 的新 iOS 实现时,如果使用此属性,则背景颜色可能会闪烁到视图中。此外,WKWebView
对透明背景的渲染方式与 UIWebview
不同。请查看提交说明以了解更多详细信息。
不支持
scalesPageToFit
(提交)
WKWebView 不支持 scalesPageToFit 属性,因此我们无法在 WebView React Native 组件上实现此属性。
可访问性 API 更新
动机
随着技术的进步和移动应用在日常生活中变得越来越重要,创建可访问的应用程序的必要性也日益凸显。
React Native 有限的可访问性 API 一直是开发人员的一大痛点,因此我们对可访问性 API 进行了一些更新,以便更轻松地创建包容性的移动应用程序。
现有 API 的问题
问题一:两个完全不同但类似的属性 - accessibilityComponentType(Android)和 accessibilityTraits(iOS)
accessibilityComponentType
和 accessibilityTraits
是两个用于告诉 Android 上的 TalkBack 和 iOS 上的 VoiceOver 用户正在交互的 UI 元素类型的属性。这两个属性最大的两个问题是
- 它们是两个具有不同使用方法但具有相同目的的不同属性。在之前的 API 中,这两个属性是分开的(每个平台一个),这不仅不方便,而且也让许多开发人员感到困惑。iOS 上的
accessibilityTraits
允许 17 个不同的值,而 Android 上的accessibilityComponentType
仅允许 4 个值。此外,这些值在大多数情况下没有重叠。即使这两个属性的输入类型也不同。accessibilityTraits
允许传入一个特征数组或单个特征,而accessibilityComponentType
仅允许单个值。 - Android 上的功能非常有限。使用旧属性,Talkback 唯一能够识别的 UI 元素是“button”、“radiobutton_checked”和“radiobutton_unchecked”。
问题二:不存在的可访问性提示
可访问性提示帮助使用 TalkBack 或 VoiceOver 的用户理解在对可访问性元素执行操作时会发生什么,而仅通过可访问性标签并不能清楚地了解这些操作。这些提示可以在设置面板中打开或关闭。以前,React Native 的 API 完全不支持可访问性提示。
问题三:忽略反转颜色
一些视力受损的用户在手机上使用反转颜色来获得更高的屏幕对比度。Apple 为 iOS 提供了一个 API,允许开发人员忽略某些视图。这样,当用户打开反转颜色设置时,图像和视频就不会失真。React Native 目前不支持此 API。
新 API 的设计
解决方案一:合并 accessibilityComponentType(Android)和 accessibilityTraits(iOS)
为了解决 accessibilityComponentType
和 accessibilityTraits
之间的混淆,我们决定将它们合并到一个属性中。这样做是有意义的,因为它们在技术上具有相同的预期功能,并且通过合并它们,开发人员在构建可访问性功能时不再需要担心特定于平台的复杂性。
背景
在 iOS 上,UIAccessibilityTraits
是可以设置在任何 NSObject 上的属性。通过 Javascript 属性传递到本地的 17 个特征中的每一个都映射到 Objective-C 中的 UIAccessibilityTraits
元素。每个特征都由一个长整型表示,并且设置的每个特征都进行按位或运算。
然而,在 Android 上,AccessibilityComponentType
是 React Native 发明的概念,并不直接映射到 Android 中的任何属性。可访问性由可访问性委托处理。每个视图都有一个默认的可访问性委托。如果要自定义任何可访问性操作,则必须创建一个新的可访问性委托,覆盖要自定义的特定方法,然后将正在处理的视图的可访问性委托设置为与新委托关联。当开发人员设置 AccessibilityComponentType
时,原生代码根据传入的组件创建一个新的委托,并将视图设置为具有该可访问性委托。
所做的更改
对于我们的新属性,我们希望创建一个这两个属性的超集。我们决定使新属性主要基于现有的属性 accessibilityTraits
建模,因为 accessibilityTraits
具有明显更多的值。通过修改可访问性委托来填充这些特征在 Android 上的功能。
iOS 上的 accessibilityTraits
可以设置为 17 个 UIAccessibilityTraits 值。但是,我们并没有将所有这些值都包含在新属性的可能值中。这是因为设置其中一些特征的效果实际上并不为人所知,并且许多这些值实际上从未使用过。
UIAccessibilityTraits 设置的值通常具有两种用途之一。它们要么描述 UI 元素具有的角色,要么描述 UI 元素所处的状态。我们观察到的以前属性的大多数用法通常使用一个表示角色的值,并将其与“state selected”、“state disabled”或两者结合使用。因此,我们决定创建两个新的可访问性属性:accessibilityRole
和 accessibilityState
。
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
解决方案二:添加可访问性提示
为此,我们添加了一个新属性 accessibilityHint
。设置此属性将允许 Talkback 或 Voiceover 向用户朗读提示。
accessibilityHint
此属性以字符串的形式接收要读取的可访问性提示。
在 iOS 上,设置此属性将设置视图上相应的原生属性 AccessibilityHint。如果在 iPhone 上启用了可访问性提示,则 Voiceover 将读取该提示。
在 Android 上,设置此属性会将提示的值附加到可访问性标签的末尾。此实现的优点在于它模拟了 iOS 上提示的行为,但缺点在于这些提示无法像在 iOS 上那样在 Android 的设置中关闭。
我们在 Android 上做出此决定的原因是,通常,可访问性提示与特定操作(例如点击)相对应,我们希望跨平台保持行为一致。
问题三的解决方案
accessibilityIgnoresInvertColors
我们将 Apple 的 api AccessibilityIgnoresInvertColors 公开给了 JavaScript,因此现在,当您拥有不想反转颜色的视图(例如图像)时,可以将此属性设置为 true,它就不会被反转。
新用法
这些新属性将在 React Native 0.57 版本中可用。
如何升级
如果您目前正在使用 accessibilityComponentType
和 accessibilityTraits
,以下是如何升级到新属性的步骤。
1. 使用 jscodeshift
最简单的用例可以通过运行 jscodeshift 脚本替换。
此 脚本 替换以下实例
accessibilityTraits=“trait”
accessibilityTraits={[“trait”]}
为
accessibilityRole= “trait”
此脚本还删除了 AccessibilityComponentType
的实例(假设您在设置 AccessibilityComponentType
的所有地方也设置了 AccessibilityTraits
)。
2. 使用手动代码修改
对于使用 AccessibilityTraits
但没有相应 AccessibilityRole
值的用例,以及将多个特征传递给 AccessibilityTraits
的用例,则必须进行手动代码修改。
一般来说,
accessibilityTraits= {[“button”, “selected”]}
将手动替换为
accessibilityRole=“button”
accessibilityStates={[“selected”]}
这些属性已在 Facebook 的代码库中使用。Facebook 的代码修改令人惊讶地简单。jscodeshift 脚本修复了大约一半的实例,另一半是手动修复的。总的来说,整个过程花费不到几个小时。
希望您会发现更新后的 API 有用!并请继续创建可访问的应用程序!#inclusion
2018 年 React Native 的现状
我们已经有一段时间没有发布关于 React Native 的状态更新了。
在 Facebook,我们比以往任何时候都更多地使用 React Native,并且将其用于许多重要的项目。我们最受欢迎的产品之一是 Marketplace,它是我们应用中顶级标签之一,每月有 8 亿人使用。自 2015 年创建以来,整个 Marketplace 都是使用 React Native 构建的,包括应用不同部分的 100 多个全屏视图。
我们还在应用的许多新部分使用 React Native。如果您上个月观看了 F8 主题演讲,您会认出献血、危机响应、隐私快捷方式和健康检查——所有这些都是最近使用 React Native 构建的功能。并且 Facebook 应用之外的项目也正在使用 React Native。新的 Oculus Go VR 耳机包含 一个配套的移动应用,该应用完全使用 React Native 构建,更不用说 React VR 为耳机本身的许多体验提供支持了。
当然,我们也使用许多其他技术来构建我们的应用程序。 Litho 和 ComponentKit 是我们在应用程序中广泛使用的两个库;两者都提供了类似 React 的组件 API 来构建原生屏幕。React Native 从来没有目标要取代所有其他技术——我们专注于改进 React Native 本身,但我们也喜欢看到其他团队借鉴 React Native 的想法,例如将 即时重载 也应用到非 JavaScript 代码中。
架构
当我们在 2013 年启动 React Native 项目时,我们将其设计为在 JavaScript 和原生之间有一个异步、可序列化和批量处理的“桥梁”。就像 React DOM 将 React 状态更新转换为对 DOM API(如 document.createElement(attrs)
和 .appendChild()
)的命令式、变异性调用一样,React Native 被设计为返回一个包含要执行的变异列表的单个 JSON 消息,例如 [["createView", attrs], ["manageChildren", ...]]
。我们设计了整个系统,使其永远不会依赖于获取同步响应,并确保该列表中的所有内容都可以完全序列化为 JSON 并恢复。我们这样做是为了它带来的灵活性:在此架构之上,我们能够构建诸如 Chrome 调试 之类的工具,这些工具通过 WebSocket 连接异步运行所有 JavaScript 代码。
在过去的 5 年里,我们发现这些初始原则使得构建某些功能变得更加困难。异步桥意味着您无法将 JavaScript 逻辑直接与许多期望同步答案的原生 API 集成。批量处理桥会将原生调用排队,这意味着更难以让 React Native 应用程序调用以原生方式实现的函数。而可序列化桥意味着不必要的复制,而不是直接在两个世界之间共享内存。对于完全使用 React Native 构建的应用程序,这些限制通常是可以忍受的。但是对于在 React Native 和现有应用程序代码之间进行复杂集成的应用程序,它们令人沮丧。
我们正在对 React Native 进行大规模的重新架构,以使框架更灵活,并更好地与混合 JavaScript/原生应用程序中的原生基础设施集成。 通过这个项目,我们将应用我们在过去 5 年中学到的知识,并逐步将我们的架构转变为更现代的架构。我们正在重写 React Native 的许多内部组件,但大多数更改都在幕后进行:现有的 React Native 应用程序将继续工作,几乎无需或无需任何更改。
为了使 React Native 更轻量级,并更好地融入现有的原生应用程序,此重新架构包含三个主要的内部更改。首先,我们正在更改线程模型。与其让每个 UI 更新都需要在三个不同的线程上执行工作,不如能够在任何线程上同步调用 JavaScript 以进行高优先级更新,同时仍将低优先级工作从主线程中移除以保持响应能力。其次,我们将 异步渲染 功能集成到 React Native 中,以允许多种渲染优先级并简化异步数据处理。最后,我们正在简化我们的桥梁,使其更快、更轻量级;原生和 JavaScript 之间的直接调用效率更高,并且将使构建诸如跨语言堆栈跟踪之类的调试工具变得更容易。
这些更改完成后,将可以实现更紧密的集成。如今,不可能在没有复杂 hack 的情况下合并原生导航和手势处理或诸如 UICollectionView 和 RecyclerView 之类的原生组件。在我们对线程模型进行更改后,构建此类功能将变得简单直接。
我们将在今年晚些时候发布有关此工作的更多详细信息,届时它将接近完成。
社区
除了 Facebook 内部社区之外,我们很高兴看到 Facebook 之外也有一群蓬勃发展的 React Native 用户和协作者。我们希望更多地支持 React Native 社区,方法是更好地服务于 React Native 用户,并使项目更容易参与贡献。
就像我们的架构更改将有助于 React Native 更干净地与其他原生基础设施进行互操作一样,React Native 应该在 JavaScript 端更精简,以更好地适应 JavaScript 生态系统,其中包括使 VM 和打包器可互换。我们知道破坏性更改的速度可能难以跟上,因此我们希望找到减少主要版本发布次数的方法。最后,我们知道有些团队正在寻找有关启动优化等主题的更全面的文档,而我们的专业知识尚未记录在案。预计在未来一年中会看到其中一些变化。
如果您正在使用 React Native,那么您就是我们社区的一员;请继续告诉我们如何使 React Native 对您更有用。
React Native 只是移动开发者工具箱中的一个工具,但我们坚信它——并且我们每天都在改进它,在过去一年中,来自 500 多位贡献者的 2500 多次提交证明了这一点。
在 React Native 中使用 TypeScript
JavaScript!我们都喜欢它。但我们中的一些人也很喜欢 类型。幸运的是,存在一些选项可以为 JavaScript 添加更强大的类型。我最喜欢的是 TypeScript,但 React Native 原生支持 Flow。您更喜欢哪一个取决于个人喜好,它们各自都有自己将类型魔法添加到 JavaScript 的方法。今天,我们将了解如何在 React Native 应用程序中使用 TypeScript。
这篇文章使用 Microsoft 的 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 应用程序了。
初始化
一旦您尝试构建一个普通的 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 编译器的所有设置。上面命令创建的默认设置大部分都很好,但打开该文件并取消注释以下行
{
/* Search the config file for the following line and uncomment it. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
}
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.tsx
。index.js
需要使用 .js
扩展名。所有新文件都应该使用 .tsx
扩展名(或者如果文件不包含任何 JSX,则使用 .ts
)。
如果您现在尝试运行应用程序,您将收到类似 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 应用程序,我们需要将 ts-jest 添加到我们的 devDependencies
中。
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 的形状。对于其他库,我们需要在 @types/
npm 范围内显式安装相应的包。
例如,这里我们需要 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 # import to do this first, to ignore our files
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>
);
}
}
// styles
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',
},
});
哇!有很多东西,但让我们分解一下
- 我们不再渲染像
div
、span
、h1
等 HTML 元素,而是渲染像View
和Button
这样的组件。这些是原生组件,可以在不同的平台上运行。 - 样式使用 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 编写的组件库,支持 Web 上的 React 以及 React Native。
在更类型安全的 React Native 开发环境中玩得开心!
为 React Native 构建
动机
三年前,在 GitHub 上提交了一个问题,要求 React Native 支持输入附件视图。
在随后的几年里,出现了无数的“+1”,各种变通方法,并且在这个问题上对 RN 没有进行任何具体的更改——直到今天。从 iOS 开始,我们公开了一个 API 用于访问原生输入附件视图,我们很高兴与大家分享我们是如何构建它的。
背景
输入附件视图到底是什么?阅读Apple 的开发者文档,我们了解到它是一个自定义视图,每当接收者成为第一响应者时,它可以锚定到系统键盘的顶部。任何继承自UIResponder
的元素都可以将.inputAccessoryView
属性重新声明为读写属性,并在其中管理自定义视图。响应者基础设施挂载视图,并使其与系统键盘保持同步。像拖动或点击这样的隐藏键盘的手势,在框架级别应用于输入附件视图。这使我们能够构建具有交互式键盘关闭功能的内容,这是 iMessage 和 WhatsApp 等顶级消息应用程序中不可或缺的功能。
将视图锚定到键盘顶部的两种常见用例。第一个是创建键盘工具栏,例如 Facebook 撰写器背景选择器。
在这种情况下,键盘聚焦于文本输入字段,输入附件视图用于提供额外的键盘功能。此功能与输入字段的类型相关。在地图应用程序中,它可能是地址建议,或者在文本编辑器中,它可能是富文本格式工具。
在这种情况下,拥有<InputAccessoryView>
的 Objective-C UIResponder 应该很清楚。<TextInput>
已成为第一响应者,在后台它成为UITextView
或UITextField
的实例。
第二个常见场景是粘性文本输入。
在这里,文本输入实际上是输入附件视图本身的一部分。这通常用于消息应用程序中,在其中可以撰写消息,同时滚动浏览以前的邮件线程。
在这个例子中,谁拥有<InputAccessoryView>
?它可以再次是UITextView
或UITextField
吗?文本输入位于输入附件视图内部,这听起来像是一个循环依赖关系。仅解决此问题本身就是另一篇博文。剧透:所有者是一个通用的UIView
子类,我们手动告诉它成为第一响应者。
API 设计
现在我们知道了<InputAccessoryView>
是什么,以及我们如何使用它。下一步是设计一个对这两种用例都有意义的 API,并且与<TextInput>
等现有的 React Native 组件配合良好。
对于键盘工具栏,我们需要考虑以下几点。
- 我们希望能够将任何通用的 React Native 视图层次结构提升到
<InputAccessoryView>
中。 - 我们希望这个通用且分离的视图层次结构能够接受触摸并能够操作应用程序状态。
- 我们希望将
<InputAccessoryView>
链接到特定的<TextInput>
。 - 我们希望能够在多个文本输入之间共享
<InputAccessoryView>
,而无需复制任何代码。
我们可以使用类似于React 门户的概念来实现 #1。在此设计中,我们将 React Native 视图门户到响应者基础设施管理的UIView
层次结构中。由于 React Native 视图渲染为 UIViews,因此这实际上非常简单——我们只需覆盖
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
并将所有子视图传递到新的 UIView 层次结构。对于 #2,我们为<InputAccessoryView>
设置了一个新的RCTTouchHandler。状态更新是通过使用常规事件回调来实现的。对于 #3 和 #4,我们使用nativeID字段在创建<TextInput>
组件期间在原生代码中查找附件视图 UIView 层次结构。此函数使用底层原生文本输入的.inputAccessoryView
属性。这样做有效地将<InputAccessoryView>
链接到它们 ObjC 实现中的<TextInput>
。
支持粘性文本输入(场景 2)增加了更多约束。对于此设计,输入附件视图具有作为子项的文本输入,因此通过 nativeID 链接不是一种选择。相反,我们将通用屏幕外UIView
的.inputAccessoryView
设置为我们的原生<InputAccessoryView>
层次结构。通过手动告诉这个通用UIView
成为第一响应者,层次结构由响应者基础设施挂载。这篇博文中详细解释了这个概念。
陷阱
当然,在构建此 API 期间并非一切顺利。以下是一些我们遇到的陷阱,以及我们如何解决它们。
构建此 API 的最初想法涉及侦听NSNotificationCenter
的 UIKeyboardWill(Show/Hide/ChangeFrame) 事件。此模式用于一些开源库,以及 Facebook 应用程序的某些内部部分。不幸的是,UIKeyboardDidChangeFrame
事件没有及时调用以在滑动时更新<InputAccessoryView>
框架。此外,键盘高度的变化不会被这些事件捕获。这会产生一类表现为这样的错误
在 iPhone X 上,文本和表情符号键盘的高度不同。大多数使用键盘事件来操作文本输入框架的应用程序都必须修复上述错误。我们的解决方案是致力于使用.inputAccessoryView
属性,这意味着响应者基础设施处理类似这样的框架更新。
我们遇到的另一个棘手的错误是避免 iPhone X 上的 Home 栏。您可能在想,“Apple 开发了safeAreaLayoutGuide就是为了这个目的,这很简单!”。我们也同样天真。第一个问题是原生<InputAccessoryView>
实现直到即将出现的那一刻才具有要锚定的窗口。没关系,我们可以覆盖-(BOOL)becomeFirstResponder
并在那里强制执行布局约束。遵守这些约束会将附件视图向上推,但会出现另一个错误:
输入附件视图成功避免了 Home 栏,但现在不安全区域后面的内容可见。解决方案在于此radar。我将原生<InputAccessoryView>
层次结构包装在一个不符合safeAreaLayoutGuide
约束的容器中。原生容器覆盖了不安全区域中的内容,而<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
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 特定的实现。例如,如果您希望用户注册、登录,然后上传私有照片,您只需将 Auth
和 Storage
类别引入您的应用程序即可。
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。API 模块在使用 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 中,以便您无需弹出即可使用 Amplify。
最后,针对 React Native(和 React)开发,Amplify 包含 高阶组件 (HOC),用于轻松包装功能,例如应用程序的注册和登录。
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)}/>
您可以通过 props 控制许多组件功能,如前所示,具有公共或私有存储选项。甚至可以在用户与某些 UI 组件交互时自动收集分析数据的功能。
return <S3Album track/>
AWS Amplify 采用约定优于配置的开发风格,使用全局初始化例程或类别级别的初始化。最快的入门方法是使用 aws-exports 文件。但是,开发人员也可以独立使用该库以及现有的资源。
要深入了解其理念并查看完整演示,请查看来自 AWS re:Invent 的视频。
AWS AppSync
在 AWS Amplify 发布后不久,我们还发布了 AWS AppSync。这是一种完全托管的 GraphQL 服务,具有脱机和实时功能。虽然您可以在不同的客户端编程语言(包括原生 Android 和 iOS)中使用 GraphQL,但它在 React Native 开发人员中非常流行。这是因为数据模型非常适合单向数据流和组件层次结构。
AWS AppSync 使您能够连接到您自己的 AWS 账户中的资源,这意味着您拥有并控制您的数据。这是通过使用数据源来完成的,并且该服务支持 Amazon DynamoDB、Amazon Elasticsearch 和 AWS Lambda。这使您能够将功能(如 NoSQL 和全文搜索)组合到单个 GraphQL API 中作为模式。这使您可以混合和匹配数据源。AppSync 服务还可以 根据模式进行配置,因此,如果您不熟悉 AWS 服务,您可以编写 GraphQL SDL,点击一个按钮,您就可以自动启动并运行。
AWS AppSync 中的实时功能通过 使用众所周知的基于事件的模式的 GraphQL 订阅 来控制。由于 AWS AppSync 中的订阅 由 GraphQL 指令在模式上控制,并且模式可以使用任何数据源,这意味着您可以触发来自 Amazon DynamoDB 和 Amazon Elasticsearch Service 的数据库操作的通知,或者来自您基础设施其他部分的 AWS Lambda 的通知。
与 AWS Amplify 类似,您可以使用 企业安全功能 在您的 GraphQL API 上使用 AWS AppSync。该服务允许您快速开始使用 API 密钥。但是,当您迁移到生产环境时,它可以转换为使用 AWS Identity and Access Management (IAM) 或来自 Amazon Cognito 用户池的 OIDC 令牌。您可以使用类型上的策略在解析器级别控制访问。您甚至可以使用逻辑检查来进行 细粒度访问控制 检查,例如检测用户是否为特定数据库资源的所有者。还有一些功能可以检查组成员资格,以执行解析器或单个数据库记录访问。
为了帮助 React Native 开发人员了解这些技术,有一个 内置的 GraphQL 示例模式,您可以在 AWS AppSync 控制台主页上启动它。此示例部署一个 GraphQL 模式,配置数据库表,并自动连接查询、变异和订阅。还有一个功能性的 用于 AWS AppSync 的 React Native 示例,它利用此内置模式(以及 React 示例),使您能够在几分钟内运行客户端和云组件。
当您使用 AWSAppSyncClient
时,入门非常简单,它插入到 Apollo Client 中。AWSAppSyncClient
处理 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 的交互,还包括数据的分页(包括加密令牌)以及 Events
和 Comments
之间的类型关系。由于该应用程序使用 AWSAppSyncClient
进行配置,因此数据会自动脱机持久化,并在设备重新连接时同步。
您可以查看 此视频中此技术的客户端技术和 React Native 演示的深入探讨。
反馈
这些库背后的团队渴望了解这些库和服务对您的工作效果如何。他们还想知道我们还能做些什么,以便让您更轻松地使用云服务进行 React 和 React Native 开发。请通过 GitHub 联系 AWS Mobile 团队了解 AWS Amplify 或 AWS AppSync 的信息。