React Native 的目标是为您提供尽可能最佳的开发体验。其中很大一部分是您保存文件到能够看到更改之间的时间。我们的目标是使这个反馈循环在 1 秒内完成,即使您的应用不断增长。
我们通过三个主要功能实现了这一目标
使用 JavaScript 作为语言,因为它没有较长的编译周期。
实现了一个名为 Packager 的工具,它将 es6/flow/jsx 文件转换为 VM 可以理解的普通 JavaScript。它被设计为一个服务器,将中间状态保存在内存中以实现快速的增量更改,并使用多个内核。
构建了一个名为 Live Reload 的功能,该功能在保存时重新加载应用。
此时,开发人员的瓶颈不再是重新加载应用所需的时间,而是丢失应用的状态。一个常见的场景是处理一个距离启动屏幕多个屏幕的功能。每次重新加载时,您都必须一次又一次地点击相同的路径才能返回到您的功能,使周期长达数秒。
热重载
热重载背后的理念是在运行时保持应用运行并注入已编辑文件的最新版本。这样,您就不会丢失任何状态,这在您调整 UI 时尤其有用。
一张图片胜过千言万语。查看 Live Reload(当前)和 Hot Reload(新)之间的区别。
VIDEO
如果您仔细观察,您会注意到可以从红色框中恢复,并且还可以开始导入之前不存在的模块,而无需进行完全重新加载。
**警告:**由于 JavaScript 是一种非常有状态的语言,因此无法完美地实现热重载。在实践中,我们发现当前的设置适用于大量常见用例,并且如果出现任何问题,始终可以使用完全重新加载。
热重载从 0.22 版本开始可用,您可以启用它
简而言之的实现
既然我们已经了解了为什么要使用它以及如何使用它,那么有趣的部分开始了:它实际上是如何工作的。
热重载构建在 热模块替换 (或 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
时,导出的函数将被执行,它将使用 time
的更改进行执行(步骤 2)
现在假设 log
的代码需要 time
作为顶级需要
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
,它可以放弃,因为它还没有被 require。对于 MovieSearch
,它将必须清除其导出并递归地处理其父级。最后,它将对 MovieRouter
执行相同的操作,并在那里结束,因为没有模块依赖于它。
为了遍历依赖树,运行时在 HMR 更新时从 Packager 接收反向依赖树。对于此示例,运行时将接收如下所示的 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 附带的默认 转换器 使用 babel-preset-react-native
,它被配置 为以与在使用 Webpack 的 React Web 项目中相同的方式使用 react-transform
。
Redux 存储
要启用 Redux 存储上的热重载,您只需要类似于在使用 Webpack 的 Web 项目中那样使用 HMR API 即可。
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 不知道如何接受自身,因此它会查找所有引用它的模块并尝试接受它们。最终,流程将到达唯一的存储,即 configureStore
模块,该模块将接受 HMR 更新。
如果您有兴趣帮助改进热重载,我鼓励您阅读 Dan Abramov 关于热重载未来的文章 并做出贡献。例如,Johny Days 将要使其与多个连接的客户端一起工作 。我们依靠大家来维护和改进此功能。
使用 React Native,我们有机会重新思考构建应用的方式,以使其成为很棒的开发体验。热重载只是拼图的一块,我们还能做哪些疯狂的技巧来使其变得更好?