React Native 的目标是为您提供最佳的开发者体验。其中很重要的一部分是您保存文件到看到更改所需的时间。我们的目标是使这个反馈循环在应用程序增长时也能保持在1秒以内。
我们通过以下三个主要功能接近这个理想状态:
使用 JavaScript 作为语言,因为它没有漫长的编译周期。
实现一个名为 Packager 的工具,将 ES6/Flow/JSX 文件转换为虚拟机可理解的普通 JavaScript。它被设计为一个服务器,在内存中保存中间状态以实现快速增量更改,并使用多核。
构建一个名为“实时重载”的功能,在保存时重新加载应用程序。
此时,开发人员的瓶颈不再是重新加载应用所需的时间,而是丢失了应用的状态。一个常见场景是处理一个离启动屏幕有多个屏幕距离的功能。每次重新加载时,你都必须一遍又一遍地点击相同的路径才能回到你的功能,这使得周期长达数秒。
热重载
热重载的理念是保持应用程序运行,并在运行时注入您编辑的文件的最新版本。这样,您不会丢失任何状态,这在调整 UI 时尤其有用。
一图胜千言。看看实时重载(当前)和热重载(新)之间的区别。
VIDEO
如果您仔细观察,您会发现可以从红框中恢复,并且还可以开始导入以前不存在的模块,而无需完全重新加载。
警告: 由于 JavaScript 是一种非常注重状态的语言,热重载无法完美实现。在实践中,我们发现当前的设置对于大量常见用例都运行良好,并且在出现问题时始终可以使用完全重载。
热重载在 0.22 版本中可用,您可以启用它:
实现简介
现在我们已经了解了为什么需要它以及如何使用它,有趣的部分开始了:它实际是如何工作的。
热重载基于一个名为 热模块替换 (Hot Module Replacement),简称 HMR 的功能构建。它最初由 webpack 引入,我们在 React Native Packager 中实现了它。HMR 使 Packager 能够监视文件更改,并将 HMR 更新发送到应用程序中包含的精简 HMR 运行时。
简而言之,HMR 更新包含已更改的 JS 模块的新代码。当运行时收到它们时,它会将旧模块的代码替换为新代码。
HMR 更新不仅仅包含我们想要更改的模块代码,因为仅仅替换它不足以让运行时识别更改。问题是模块系统可能已经缓存了我们想要更新的模块的导出 。例如,假设你有一个由这两个模块组成的应用程序
function log ( message ) { const time = require ( './time' ) ; console . log ( ` [ ${ time ( ) } ] ${ message } ` ) ; } module . exports = log ;
function time ( ) { return new Date ( ) . getTime ( ) ; } module . exports = time ;
模块 `log` 打印提供的消息,包括模块 `time` 提供的当前日期。
当应用程序打包时,React Native 使用 `__d` 函数在模块系统上注册每个模块。对于这个应用程序,在许多 `__d` 定义中,会有一个 `log` 的定义。
__d ( 'log' , function ( ) { ... } ) ;
此调用将每个模块的代码封装在一个匿名函数中,我们通常称之为工厂函数。模块系统运行时会跟踪每个模块的工厂函数,它是否已执行,以及执行结果(导出)。当需要一个模块时,模块系统要么提供已缓存的导出,要么首次执行该模块的工厂函数并保存结果。
所以假设你启动了你的应用程序并需要 `log`。此时,`log` 和 `time` 的工厂函数都还没有执行,所以没有导出被缓存。然后,用户修改 `time` 以便返回 `MM/DD` 格式的日期
function bar ( ) { const date = new Date ( ) ; return ` ${ date . getMonth ( ) + 1 } / ${ date . getDate ( ) } ` ; } module . exports = bar ;
Packager 会将 time 的新代码发送到运行时(步骤 1),当 `log` 最终被 require 时,导出的函数会与 `time` 的更改一起执行(步骤 2)。
现在,假设 `log` 的代码将 `time` 作为顶层 `require`。
const time = require ( './time' ) ; function log ( message ) { console . log ( ` [ ${ time ( ) } ] ${ message } ` ) ; } module . exports = log ;
当需要`log`时,运行时将缓存其导出和`time`的导出(步骤1)。然后,当`time`被修改时,HMR进程不能简单地在替换`time`的代码后结束。如果这样做,当`log`执行时,它将使用`time`的缓存副本(旧代码)来执行。
为了让 `log` 接收 `time` 的更改,我们需要清除其缓存的导出,因为其依赖的模块之一已被热交换(步骤 3)。最后,当 `log` 再次被请求时,其工厂函数将被执行,请求 `time` 并获取其新代码。
HMR API
React Native 中的 HMR 通过引入 hot 对象来扩展模块系统。这个 API 基于 webpack 的 API。hot 对象公开了一个名为 accept 的函数,它允许你定义一个回调函数,当模块需要热交换时,该回调函数将被执行。例如,如果我们像下面这样更改 time 的代码,每次保存 time 时,我们都会在控制台中看到“time changed”。
function time ( ) { ... } module . hot . accept ( ( ) => { console . log ( 'time changed' ) ; } ) ; module . exports = time ;
请注意,您很少需要手动使用此 API。对于大多数常见用例,热重载应该开箱即用。
HMR 运行时
正如我们之前看到的,有时仅接受 HMR 更新是不够的,因为使用正在热交换的模块的模块可能已经执行并且其导入已缓存。例如,假设电影应用程序示例的依赖树有一个顶层 `MovieRouter`,它依赖于 `MovieSearch` 和 `MovieScreen` 视图,而这些视图又依赖于前面示例中的 `log` 和 `time` 模块
如果用户访问电影搜索视图而不是另一个视图,除了 `MovieScreen` 之外的所有模块都将缓存导出。如果对模块 `time` 进行了更改,运行时将不得不清除 `log` 的导出,以便它能接收 `time` 的更改。这个过程不会就此结束:运行时将递归地重复这个过程,直到所有父级都被接受。因此,它将获取依赖于 `log` 的模块并尝试接受它们。对于 `MovieScreen`,它可以跳过,因为它还没有被请求。对于 `MovieSearch`,它将不得不清除其导出并递归处理其父级。最后,它将对 `MovieRouter` 执行相同的操作并在那里结束,因为没有模块依赖于它。
为了遍历依赖树,运行时从 Packager 接收 HMR 更新中的反向依赖树。对于这个例子,运行时将接收一个像这样的 JSON 对象:
{ modules : [ { name : 'time' , code : } ] , inverseDependencies : { MovieRouter : [ ] , MovieScreen : [ 'MovieRouter' ] , MovieSearch : [ 'MovieRouter' ] , log : [ 'MovieScreen' , 'MovieSearch' ] , time : [ 'log' ] , } }
React 组件
React 组件与热重载配合使用会稍微困难一些。问题在于我们不能简单地用新代码替换旧代码,因为我们会丢失组件的状态。对于 React Web 应用程序,Dan Abramov 实现了一个 Babel 转换 ,它使用 webpack 的 HMR API 来解决这个问题。简而言之,他的解决方案是在转换时 为每个 React 组件创建一个代理。代理负责持有组件的状态,并将生命周期方法委托给实际的组件,而这些组件是我们热重载的对象。
除了创建代理组件外,转换还定义了 `accept` 函数,其中包含一段强制 React 重新渲染组件的代码。通过这种方式,我们可以在不丢失任何应用程序状态的情况下热重载渲染代码。
React Native 附带的默认 transformer 使用 babel-preset-react-native,它被 配置 为使用 react-transform,就像你在使用 webpack 的 React Web 项目中会使用它一样。
Redux Stores
要在 Redux store 上启用热重载,你只需要像在 Redux store 上启用热重载一样,使用 HMR API,就像你在使用 webpack 的 Web 项目中会做的那样。
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,我们有机会重新思考构建应用程序的方式,以创造出色的开发者体验。热重载只是其中一部分,我们还能做些什么疯狂的“黑科技”来做得更好呢?