跳到主要内容

介绍热重载

· 阅读需 8 分钟
Martín Bigio
Instagram 软件工程师

React Native 的目标是为你提供最佳的开发者体验。重要的一部分是从你保存文件到能够看到更改之间所花费的时间。我们的目标是把这个反馈循环控制在 1 秒以内,即使你的应用日益庞大。

我们通过三个主要特性接近了这个理想:

  • 使用 JavaScript 作为语言,它没有漫长的编译周期。
  • 实现了一个名为 Packager 的工具,将 es6/flow/jsx 文件转换成虚拟机可以理解的普通 JavaScript。它设计成一个服务器,在内存中保持中间状态以实现快速的增量更改,并利用多核 CPU。
  • 构建了一个叫做 Live Reload 的功能,在保存时重新加载应用。

此时,开发者的瓶颈不再是应用加载时间,而是丢失应用状态。一个常见场景是你正在开发一个多屏幕外的功能。每次重载,你都必须反复点击同一路径返回功能界面,使得循环花费数秒。

热重载

热重载的理念是在保持应用运行的同时,在运行时注入你编辑的文件的新版本。这样你就不会丢失任何状态,特别适合调整 UI 时使用。

一张图胜千言。看看 Live Reload(当前)和 Hot Reload(新)的区别。

仔细看,你会发现它可以从红色错误界面恢复,也可以开始导入之前不存在的模块,无需完整重载。

警告: 由于 JavaScript 是一个高度状态化的语言,热重载无法完美实现。实际上,我们发现当前的方案已能较好地应对大多数常用场景,且如若发生错误总可以进行完整重载。

热重载自 0.22 版本起可用,你可以这样开启:

  • 打开开发者菜单
  • 点击“Enable Hot Reloading”

实现简述

了解了为什么要用以及如何使用后,趣味部分来了:它实际上是如何工作的。

热重载基于一种名为 Hot Module Replacement(简称 HMR)的功能。这最初由 webpack 引入,我们在 React Native Packager 中实现了它。HMR 使 Packager 监听文件变更,并向应用中包含的轻量 HMR 运行时发送更新。

简言之,HMR 更新包含了修改后 JS 模块的新代码。运行时收到后,会用新代码替换旧代码:

HMR 更新不仅仅包含要更改模块的代码,因为光替换代码还不足以让运行时认知变更。问题在于模块系统可能已缓存了想要更新模块的 exports。举例说明,假设应用由以下两个模块组成:

// log.js
function log(message) {
const time = require('./time');
console.log(`[${time()}] ${message}`);
}

module.exports = log;
// time.js
function time() {
return new Date().getTime();
}

module.exports = time;

模块 log 会输出一条包含当前日期(由模块 time 提供)的消息。

打包后,React Native 使用 __d 函数注册每个模块到模块系统。对于该应用,log 模块会有如下定义:

__d('log', function() {
... // 模块代码
});

该调用将每个模块代码包裹在匿名函数中,通常称之为工厂函数。模块系统运行时跟踪每个模块的工厂函数,是否已执行过,以及执行结果(exports)。当某模块被引用,模块系统会返回缓存的 exports,或者首次执行工厂函数并缓存结果。

假设启动应用并引用 log 模块,此时 logtime 的工厂函数都未执行,exports 还未缓存。之后用户修改 time 函数让它返回 MM/DD 格式的日期:

// time.js
function bar() {
const date = new Date();
return `${date.getMonth() + 1}/${date.getDate()}`;
}

module.exports = bar;

Packager 会发送 time 模块的新代码到运行时(步骤 1),当 log 模块最终被引用并执行时,会使用修改后的 time(步骤 2):

如果 log 模块中的 require('time') 是顶级调用:

const time = require('./time'); // 顶级 require

// log.js
function log(message) {
console.log(`[${time()}] ${message}`);
}

module.exports = log;

log 被引用时,运行时会缓存它和 time 的 exports(步骤 1)。之后 time 变更时,HMR 进程不能只替换 time 的代码直接结束,否则 log 执行时仍会使用缓存的旧 time 代码。

为了让 log 拥抱 time 的改变,必须清除 log 的缓存 exports,因为它依赖的模块被热替换了(步骤 3)。最后,下一次 log 被引用时,工厂函数会被重新执行,进而使用新 time 代码。

HMR API

React Native 中的 HMR 扩展了模块系统,新增了 hot 对象。该 API 基于 webpack 的实现。hot 对象暴露了 accept 函数,允许你定义模块被热替换时调用的回调。例如,如果我们修改 time,每次保存时,控制台都会输出 “time changed”:

// time.js
function time() {
... // 新代码
}

module.hot.accept(() => {
console.log('time changed');
});

module.exports = time;

注意手动使用该 API 的情况很少见。热重载默认对大多数场景开箱即用。

HMR 运行时

如前所述,仅仅接受 HMR 更新有时不够,因为使用该模块的上层模块可能已执行且导入被缓存。举例来说,假设电影示例应用的依赖树顶层有个 MovieRouter,其依赖 MovieSearchMovieScreen 视图,这些视图又依赖前面示例中的 logtime 模块:

用户访问了电影搜索视图,但未访问另一个视图,除 MovieScreen 外,其他模块都缓存了 exports。如果修改了 time,运行时会清除 log 的缓存 exports 以反映 time 的变更。这个过程还会递归上溯所有父模块并尝试接受它们的更新。对于未被引用的 MovieScreen,会跳过;对 MovieSearch,清除缓存后递归处理其父级;最终对 MovieRouter 采用相同处理,之后结束,因为没有父模块依赖它。

运行时为进行依赖树递归,会从 Packager 那里在 HMR 更新中获得逆向依赖树。对于此例,运行时会收到了如下类型的 JSON 对象:

{
modules: [
{
name: 'time',
code: /* time 的新代码 */
}
],

inverseDependencies: {
MovieRouter: [],
MovieScreen: ['MovieRouter'],
MovieSearch: ['MovieRouter'],
log: ['MovieScreen', 'MovieSearch'],
time: ['log'],
}
}

React 组件

React 组件更难支持热重载,因为不能简单替换旧代码否则会丢失组件状态。对于 React Web 应用,Dan Abramov 实现了 babel transform,结合 webpack 的 HMR API 解决了这个问题。简而言之,他的方案是在 transform 阶段 为每个 React 组件创建一个代理。这些代理持有组件状态,并将生命周期方法委托给实际组件,后者是被热重载的目标:

除了创建代理组件,该 transform 还定义了 accept 函数,代码强制 React 重新渲染组件。这样,我们就能在不丢失任何应用状态的前提下热重载渲染代码。

React Native 默认的 transformer 使用 babel-preset-react-native,其中 配置react-transform,用法与 React Web + webpack 项目相同。

Redux Store

要为 Redux store 启用热重载,只需像在 webpack Web 项目中一样使用 HMR API:

// configureStore.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import reducer from '../reducers';

export default function configureStore(initialState) {
const store = createStore(
reducer,
initialState,
applyMiddleware(thunk),
);

if (module.hot) {
module.hot.accept(() => {
const nextRootReducer = require('../reducers/index').default;
store.replaceReducer(nextRootReducer);
});
}

return store;
};

修改 reducer 后,接受该 reducer 的代码会被发送到客户端,然后客户端会意识到 reducer 自身无法接受热替换,因此会查找引用它的所有模块并尝试接受。最终,这个流程会到达单一的 store —— configureStore 模块,在那里接受 HMR 更新。

总结

如果你有兴趣帮助改进热重载,建议阅读 Dan Abramov 关于热重载未来的文章 并参与贡献。例如,Johny Days 正在 实现支持多客户端连接。我们期待大家一起维护和改进这项功能。

React Native 给了我们机会重新思考构建应用的方式,以打造极佳的开发体验。热重载只是拼图中的一片,还有哪些疯狂的黑科技,我们能让开发体验更好呢?

让 React Native 应用支持无障碍功能

· 阅读需 2 分钟
Georgiy Kassabli
Facebook 软件工程师

随着 React 在网页端和 React Native 在移动端的推出,我们为开发者提供了一个全新的前端框架以构建产品。构建一个强健产品的关键之一是确保任何人都能使用它,包括视力障碍或其他残疾人士。React 和 React Native 的无障碍 API 使您能够让任何基于 React 的体验为可能使用辅助技术的人所用,比如盲人和视障者使用的屏幕阅读器。

在本文中,我们将重点介绍 React Native 应用。我们设计的 React 无障碍 API 与 Android 和 iOS 的 API 看起来和感觉上都相似。如果你之前为 Android、iOS 或网页开发过无障碍应用,你应该会对 React AX API 的框架和命名感到熟悉。例如,你可以让一个 UI 元素 accessible(因此它能被辅助技术访问),并使用 accessibilityLabel 来为该元素提供一个字符串描述:

<View accessible={true} accessibilityLabel=”这是一个简单视图”>

让我们通过看看 Facebook 自家的一个基于 React 的产品 —— 广告管理器应用 来演示 React AX API 更加复杂的应用场景。

这是节选内容。阅读全文请访问 Facebook Code

Android 版 React Native:我们如何构建第一个跨平台的 React Native 应用

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

今年早些时候,我们推出了iOS 版 React Native。React Native 将开发者在网页端使用 React 时所熟悉的声明式自包含 UI 组件和快速开发周期带到了移动平台,同时保持了原生应用的速度、逼真度和使用感受。今天,我们很高兴地发布了 Android 版 React Native。

在 Facebook,我们已经在生产环境中使用 React Native 超过一年。几乎正好一年前,我们团队开始开发广告管理器应用。我们的目标是创建一款新应用,让数百万在 Facebook 上投放广告的用户能够随时管理账户并创建新广告。最终,这不仅是 Facebook 第一个完全基于 React Native 的应用,也是第一个跨平台应用。在这篇文章中,我们想与大家分享我们如何构建这款应用,React Native 如何帮助我们提速,以及我们所学到的经验教训。

以上为节选内容。请在Facebook Code阅读全文。

React Native:将现代网页技术引入移动端

· 阅读需 2 分钟
Tom Occhino
Facebook 工程经理

我们在两年前向世界介绍了React,此后它在 Facebook 内外都取得了令人瞩目的增长。如今,虽然没人被强制使用它,但 Facebook 上的新网页项目通常会以某种形式使用 React,并且它正在业界被广泛采用。工程师们每天选择使用 React,是因为它让他们能更多地专注于产品,而不是花费大量时间与框架“斗争”。不过,直到我们使用 React 开发了一段时间之后,才真正理解它为何如此强大。

React 强制我们将应用拆分为离散的组件,每个组件表示一个单独的视图。这些组件让我们更容易迭代产品,因为修改某一部分时无需在脑中保持整个系统的全貌。但更重要的是,React 用声明式的方式包装了 DOM 的变异式、命令式 API,这提升了抽象层次,简化了编程模型。我们的发现是,使用 React 开发时,代码更加可预测。这种可预测性让我们能更有信心快速迭代,也让应用变得更加可靠。此外,使用 React 构建应用不仅更易于扩展,我们还发现团队规模的扩展也更为便捷。

结合网页的快速迭代周期,我们用 React 构建了一些很棒的产品,包括 Facebook.com 的许多组件。此外,我们基于 React 构建了强大的 JavaScript 框架,比如 Relay,它能够大幅简化大规模数据获取。当然,网页只是故事的一部分。Facebook 还有广泛使用的 Android 和 iOS 应用,它们基于彼此独立且封闭的技术栈构建。在多个平台之上进行应用开发,使我们的工程组织分裂开来,但这只是移动原生应用开发难点之一。

以上为节选。请在Facebook Code阅读完整文章。