跳到主要内容

26 篇博文 含有标签「engineering」

查看所有标签

在 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.js、npm 和 Yarn。

初始化​

当你尝试搭建了一个普通的 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.tsx。index.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 元素比如 div、span、h1,而是渲染比如 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 编写的组件库,同时支持网页上的 React 和 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> 变成了第一响应者,其底层实际上是一个 UITextView 或 UITextField 实例。

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

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


在这个例子中,谁拥有 <InputAccessoryView>?还能是 UITextView 或 UITextField 吗?文本输入框 位于 输入辅助视图内部,这听起来像是循环依赖。单独解决这个问题就足够写成一篇 博客文章。剧透一句:拥有者是一个通用的 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 特定实现。例如,如果您希望用户注册、登录然后上传私有照片,只需将 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。该模块非常适合使用 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 DynamoDB、Amazon Elasticsearch 和 AWS 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 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。

在 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 maskElement 和 children。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 的限制是只能更新特定属性,主要是 transform 和 opacity。目前还不能用它来更新背景颜色等属性——未来会增加更多,当然你也能为这个项目提交 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.View、Animated.Text 和 Animated.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 Abbott 和 Houssein 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-maps 和 react-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 支持。我们计划添加对 CRNA 和 Expo 的支持,并改进热模块替换(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 联系我,告诉我们如何改进会议成果。

React Native 在 Marketplace 的性能表现

· 阅读需 6 分钟
Facebook 软件工程师

React Native 在 Facebook 旗下的多个应用中均有使用,包括主 Facebook 应用中的顶级标签页。本文主要聚焦一个高度可见的产品,Marketplace。该产品已在十几个国家上线,帮助用户发现其他用户提供的商品和服务。

在 2017 年上半年,通过 Relay 团队、Marketplace 团队、Mobile JS 平台团队和 React Native 团队的共同努力,我们将 Android 平台上 2010-11 年代级别设备的 Marketplace 交互时间(TTI)缩短了一半。Facebook 历来将这些设备视为低端 Android 设备,它们在所有平台和设备类型中具有最慢的 TTI。

典型的 React Native 启动过程大致如下图所示:

免责声明:图中比例不具代表性,具体情况将根据 React Native 的配置和使用方式有所不同。

我们首先初始化 React Native 核心(亦即“Bridge”),接着运行产品特定的 JavaScript,这部分决定了 React Native 将在本地用哪些视图进行渲染,这一步在“Native Processing Time”中完成。

不同的做法​

我们早期犯的一个错误是让Systrace 和 CTScan主导了我们的性能工作。这些工具帮助我们发现了许多明显的问题(低垂的果实),但我们后来发现 Systrace 和 CTScan 并不能代表真实生产环境的场景,也无法模拟实际使用时的情况。时间分解中的比例常常不准确,有时偏差极大。在极端情况下,一些我们以为只需几毫秒的操作,实际耗时却达数百甚至数千毫秒。话虽如此,CTScan 仍然有用,我们发现它能在生产前捕获三分之一的性能回归。

在 Android 上,我们将这些工具的局限归因于:1)React Native 是一个多线程框架;2)Marketplace 与复杂视图(如新闻推送和其他顶级标签页)共存;3)计算时间波动极大。因此,本半年度,我们几乎完全依靠生产环境的测量数据和时间分解,作为决策和优先级排序的依据。

生产环境检测之路​

在生产环境中做性能监测乍看简单,但实际上过程颇为复杂。该过程经历了多个 2-3 周的迭代周期;原因在于从提交代码到主分支、发布应用到 Play 商店,到收集足够生产环境样本进而确认工作有效性存在较大延迟。每个迭代周期,我们都要验证时间分解的准确性、粒度合适性以及各项数值是否正确累计到整体时间。我们无法依赖 alpha 和 beta 版本,因为它们的数据无法代表整体用户群。其实质是,我们非常细致地基于数百万样本的汇总结果构建了一份非常精确的生产追踪。

我们细致核实每个毫秒在时间分解中的正确计入,原因之一是早期发现检测工具存在遗漏。初期时间分解未考虑到因线程切换导致的阻塞。线程切换本身花费不大,但切换到已经繁忙的线程会非常耗时。我们最终通过在关键时刻人为插入 Thread.sleep() 调用,在本地重现了这些阻塞情形,并通过以下方式予以解决:

  1. 移除对 AsyncTask 的依赖,
  2. 取消在 UI 线程上强制初始化 ReactContext 和 NativeModules,
  3. 取消启动时对 ReactRootView 的强制测量依赖。

这三步合力减少了启动时间超过 25%。

生产数据指标还挑战了我们之前的一些假设。例如,我们过去习惯在启动路径中预加载许多 JavaScript 模块,认为将模块合并到一个包中可降低初始化成本。但事实上,预加载和模块集中加载的成本远大于带来的好处。通过重新配置 inline require 黑名单,并剔除启动路径中的多余模块(如仅需 Relay Modern时不引入 Relay Classic),我们实现了 RUN_JS_BUNDLE 阶段比之前快超过 75%。

产品特定的本地模块优化也带来了显著提升。例如,通过对本地模块依赖采用懒注入策略,我们将其耗时削减了 98%。通过避免 Marketplace 启动时与其他模块竞争资源,我们同样缩短了启动时间。

最棒的是,许多这种改进对所有用 React Native 构建的页面均有广泛适用性。

总结​

许多人认为 React Native 的启动性能瓶颈是 JavaScript 执行缓慢或网络延迟过高。虽然优化 JavaScript 执行确实能非同小可地减少 TTI,但这些因素各自占据的时间比之前普遍认为的要少得多。

到目前为止,最重要的经验是——测量,测量,再测量! 一些优化得益于将运行时开销转移到构建时,比如 Relay Modern 和 Lazy NativeModules;另一些则来自更聪明的代码并行化或死代码剔除;还有一部分依赖 React Native 体系内的大规模架构改进,如解决线程阻塞问题。性能优化没有“一劳永逸”的方案,更长远的成效基于持续的检测和改进。不要被认知偏见左右决策,而应仔细收集并解读生产数据,引导后续工作。

未来规划​

长期来看,我们期望 Marketplace 的 TTI 能与类似原生产品相当,整体上实现 React Native 性能与原生性能并驾齐驱。此外,尽管本半年度我们已将 Bridge 启动成本大幅降低约 80%,后续计划通过类似 Prepack 等项目以及更多构建时处理,将 React Native Bridge 的开销逼近零。

React Native 月刊 #1

· 阅读需 6 分钟
Tomislav Tenodi
Shoutem 产品经理

在 Shoutem ,我们很幸运能够从 React Native 诞生之初就开始使用它。我们决定从第一天起就成为这个了不起社区的一部分。不久后,我们意识到几乎不可能跟上社区日益增长和改进的速度。正因如此,我们决定组织一个月度会议,让所有主要的 React Native 贡献者简短地介绍他们的工作和计划。

月度会议​

我们在 2017 年 6 月 14 日举办了第一次月度会议。React Native 月刊的使命简单明了:提升 React Native 社区。团队的工作展示促进了线下团队之间的协作。

团队​

在第一次会议上,共有 8 个团队参加了会议:

我们希望未来有更多核心贡献者参加接下来的会议!

会议纪要​

鉴于团队的计划可能对更广泛的受众感兴趣,我们会在这里,即 React Native 博客上分享它们。以下是内容:

Airbnb​

  • 计划向 View 组件和 AccessibilityInfo 原生模块添加一些 A11y(无障碍)API。
  • 将调查在 Android 原生模块中添加 API,以便指定它们运行的线程。
  • 正在研究潜在的初始化性能改进。
  • 正在研究一些更复杂的打包策略,以配合“拆包”(unbundle)使用。

Callstack​

  • 正在尝试通过使用 Detox 进行端到端测试来改进发布流程,相关拉取请求即将提交。
  • 一直在开发的 Blob 功能的拉取请求已合并,后续的拉取请求也在进行中。
  • 在内部项目中推广使用 Haul ,以观察其相较于 Metro Bundler 的性能表现。正在与 webpack 团队合作提升多线程性能。
  • 内部已实现更完善的开源项目管理基础设施,未来几周将发布更多相关内容。
  • React Native 欧洲会议正在筹备中,目前暂无特别内容,但大家均被邀请参加!
  • 暂时从 react-navigation 退后,探索其他替代方案(尤其是原生导航)。

Expo​

Facebook​

  • React Native 的打包工具现已变更为独立仓库的 Metro Bundler。伦敦的 Metro Bundler 团队非常期待响应社区需求,提升模块化以支持 React Native 以外的更多用例,并加快对问题和 PR 的响应速度。
  • 接下来几个月,React Native 团队将致力于优化基础组件的 API。期待在布局细节、无障碍以及 Flow 类型方面的改进。
  • 今年团队还计划通过重构改善核心模块化,以全面支持第三方平台如 Windows 和 macOS。

GeekyAnts​

  • 团队正在开发一款 UI/UX 设计应用(代号:Builder),能够直接操作 .js 文件。目前仅支持 React Native。这类似于 Adobe XD 和 Sketch。
  • 团队正在努力实现:你可以在编辑器中加载已有的 React Native 应用,进行视觉化修改(设计师操作),然后将更改直接保存到 JS 文件中。
  • 致力于缩小设计师与开发者之间的差距,让他们共用同一个代码仓库。
  • 此外,NativeBase 最近已获得了 5,000 个 GitHub 星标。

Microsoft​

  • CodePush 现已整合进 Mobile Center,这是实现分发、分析和其他服务更紧密集成体验的第一步。相关公告见 这里。
  • VS Code 存在调试相关的一个 Bug,目前团队正在修复,并会发布新版本。
  • 正在调研使用 Detox 进行集成测试,研究 JSC 上下文以获取变量和崩溃报告。

Shoutem​

  • 正在利用 React Native 社区的工具简化 Shoutem 应用的开发。你将可以使用所有 React Native 命令来运行在 Shoutem 上创建的应用。
  • 正在调研 React Native 的性能分析工具,期间遇到不少问题,将会记录下这些经验分享给社区。
  • Shoutem 正在努力简化 React Native 与现有原生应用的集成,计划公开分享公司内部开发的方案,以便获得社区反馈。

Wix​

  • 内部推广使用 Detox ,目标是实现 Wix 应用的“零手动测试”流程。目前 Detox 已被几十位开发者在生产环境广泛使用,并在快速成熟。
  • 正在为 Metro Bundler 添加支持,允许在构建过程中覆盖任意文件扩展名。除了“ios”和“android”,将支持诸如“e2e”或“detox”等自定义扩展,计划用于端到端模拟测试。目前已有一个叫 react-native-repackager 的库,正在推进相关拉取请求。
  • 研究性能测试的自动化,开发了一个新仓库 DetoxInstruments,欢迎查看,开源开发中。
  • 与 KPN 的贡献者合作,优化 Detox 在 Android 上的表现,并支持真实设备。
  • 构思“Detox 作为平台”的模式,支持搭建其他需要自动化模拟器或设备的工具。例如 React Native 的 Storybook 或 Ram 提出的集成测试方案。

下一次会议​

会议每四周举行一次。下一次会议定于 2017 年 7 月 12 日召开。鉴于我们刚刚开始举办此会议,希望了解这些纪要对 React Native 社区有哪些帮助。如果你有针对未来会议内容的建议,或者对会议产出如何改进有想法,欢迎随时在Twitter上联系我。

React Native 中更好的列表视图

· 阅读需 5 分钟
Spencer Ahrens
Facebook 软件工程师

很多人已经开始尝试我们的新列表组件了,这要归功于我们之前在社区群组中的预告公告,今天我们正式发布它们!不再需要 ListView 或 DataSource,告别死板的行、被忽略的 Bug 和过度的内存消耗 —— 使用最新的 React Native 2017 年 3 月发布候选版(0.43-rc.1),你可以从新推出的一系列组件中选择最符合你使用场景的,开箱即用,性能和功能兼备:

<FlatList>​

这是用于简单且高性能列表的主力组件。只需提供一个数据数组和一个 renderItem 函数即可:

<FlatList
data={[{title: '标题文本', key: 'item1'}, ...]}
renderItem={({item}) => <ListItem title={item.title} />}
/>

<SectionList>​

如果你想渲染一组按逻辑分段的数据,带有分区头部(例如按字母排序的通讯录),或者数据和渲染异构(例如一个配置文件视图,包含一些按钮、接着是一个编写器、然后是图片网格、好友网格,最后是故事列表),这就是你要用的组件。

<SectionList
renderItem={({item}) => <ListItem title={item.title} />}
renderSectionHeader={({section}) => <H1 title={section.key} />}
sections={[ // 各分区渲染均匀
{data: [...], key: ...},
{data: [...], key: ...},
{data: [...], key: ...},
]}
/>

<SectionList
sections={[ // 各分区渲染异构
{data: [...], key: ..., renderItem: ...},
{data: [...], key: ..., renderItem: ...},
{data: [...], key: ..., renderItem: ...},
]}
/>

<VirtualizedList>​

这是后台实现的组件,提供了更灵活的 API。尤其适合你的数据不是普通数组的情况(例如不可变列表)。

功能​

列表在许多场景中都会用到,因此我们给新组件添加了丰富的功能以覆盖大多数使用案例:

  • 滚动加载(onEndReached)。
  • 下拉刷新(onRefresh / refreshing)。
  • 可配置的 可视回调(VPV)(onViewableItemsChanged / viewabilityConfig)。
  • 横向模式(horizontal)。
  • 智能的行与分区分割线。
  • 多列支持(numColumns)。
  • scrollToEnd,scrollToIndex,和 scrollToItem。
  • 更完善的 Flow 类型支持。

一些注意事项​

  • 当内容滚动出渲染窗口时,项目子树的内部状态不会被保留。请确保所有数据都被包含在项目数据中,或存储在 Flux、Redux、Relay 等外部存储中。

  • 这些组件基于 PureComponent,意味着如果 props 浅比较相等,不会重新渲染。确保你的 renderItem 函数直接依赖的所有内容,都作为非 === 相等的 prop 传递,否则 UI 可能不会实时更新。这包括 data 属性和父组件的状态。例如:

    <FlatList
    data={this.state.data}
    renderItem={({item}) => (
    <MyItem
    item={item}
    onPress={() =>
    this.setState(oldState => ({
    selected: {
    // 新实例打破了 `===`
    ...oldState.selected, // 复制旧数据
    [item.key]: !oldState.selected[item.key], // 切换选中状态
    },
    }))
    }
    selected={
    !!this.state.selected[item.key] // renderItem 依赖于state
    }
    />
    )}
    selected={
    // 可以是任何不冲突的 prop
    this.state.selected // selected 变化应触发 FlatList 重新渲染
    }
    />
  • 为限制内存并支持流畅滚动,内容是异步离屏渲染的。这意味着可能会滚动速度快于填充速度,短暂看到空白内容。这是一个折中,可以根据应用需求调整,我们也在后台持续改进。

  • 默认情况下,这些新列表会查找每个项目上的 key 属性作为 React key。你也可以提供自定义的 keyExtractor 属性。

性能​

除了简化 API,新列表组件还带来了显著的性能提升,最主要的是对任意行数几乎保持恒定的内存使用。实现方式是对渲染窗口外的元素进行“虚拟化”,即将它们完全从组件树卸载,回收 React 组件的 JS 内存,以及 Shadow 树和 UI 视图的原生内存。但这意味着组件内部状态不会被保留,因此请确保你把重要状态存储在组件外部,比如 Relay、Redux 或 Flux 存储。

限制渲染窗口还减少了 React 和原生层需要处理的工作量,例如视图遍历。即使是在渲染数百万元素的末尾,也无需遍历所有元素。你甚至可以用 scrollToIndex 直接跳转到列表中间,无需额外渲染。

我们还对调度做了改进,提升应用响应速度。渲染窗口边缘的项目会在任何活动的手势、动画或其它交互完成后,以较低优先级和较少频率渲染。

高级用法​

与 ListView 不同,渲染窗口中的所有项目只要任何 prop 变化都会重新渲染。通常这没问题,因为窗口大小限制了项目数量,但如果你的项目非常复杂,应遵循 React 性能优化最佳实践,使用 React.PureComponent 和/或 shouldComponentUpdate 来限制递归子树的重渲染。

如果你能在不渲染的情况下计算行高,可以通过提供 getItemLayout 属性来提升用户体验。这使得用 scrollToIndex 等方法滚动到指定项目更平滑,也能提升滚动条 UI,因为内容高度不用渲染就能确定。

如果你使用其他类型数据,比如不可变列表,<VirtualizedList> 是更好的选择。它接受一个 getItem 属性,可返回任意索引的项目数据,Flow 类型也更宽松。

对于特殊需求,还可以调整多个参数,例如用 windowSize 在内存使用和用户体验之间权衡,maxToRenderPerBatch 调整填充率和响应速度,onEndReachedThreshold 控制滚动加载触发门槛等。

未来工作​

  • 迁移现有界面(最终废弃 ListView)。
  • 根据反馈添加更多功能(欢迎告诉我们!)。
  • 支持固定分区头。
  • 更多性能优化。
  • 支持带状态的函数式项目组件。

idx:存在性函数

· 阅读需 2 分钟
Timothy Yung
Facebook 工程经理

在 Facebook,我们经常需要访问通过 GraphQL 获取的数据结构中深层嵌套的值。在访问这些深层嵌套值的过程中,一个或多个中间字段通常是可空的。这些中间字段可能为 null 的原因有很多,比如隐私检查失败,或者仅仅因为 null 是表示非致命错误的最灵活方式。

不幸的是,当前访问这些深层嵌套值非常繁琐且冗长。

props.user &&
props.user.friends &&
props.user.friends[0] &&
props.user.friends[0].friends;

有一个ECMAScript 提案引入存在性运算符,它将使这件事变得更加方便。但在该提案最终确定之前,我们需要一个解决方案来提升生活质量,保持现有语言语义,并鼓励使用 Flow 进行类型安全。

我们想出了一个叫做 idx 的存在性_函数_。

idx(props, _ => _.user.friends[0].friends);

这段代码中的调用行为类似于上面代码段中的布尔表达式,但重复显著减少。idx 函数恰好接收两个参数:

  • 任何值,通常是一个对象或数组,你希望从中访问嵌套值。
  • 一个函数,接收第一个参数并访问其上的嵌套值。

理论上,idx 函数会利用 try-catch 捕获访问 null 或 undefined 属性时产生的错误。如果捕获到此类错误,它将返回 null 或 undefined。(你可以查看它的实现方式:idx.js)

实际上,每次访问嵌套属性都用 try-catch 非常慢,并且区分特定类型的 TypeError 也很脆弱。为了解决这些不足,我们创建了一个 Babel 插件,将上述 idx 调用转换成以下表达式:

props.user == null
? props.user
: props.user.friends == null
? props.user.friends
: props.user.friends[0] == null
? props.user.friends[0]
: props.user.friends[0].friends;

最后,我们为 idx 添加了自定义的 Flow 类型声明,允许在第二个参数中的遍历进行正确的类型检查,同时支持对可空属性的嵌套访问。

该函数、Babel 插件和 Flow 类型声明现在都托管在 GitHub 上,使用时安装 idx 和 babel-plugin-idx npm 包,并在 .babelrc 文件中的插件列表中添加 “idx” 即可。