React: 既然快速刷新取代了react-hot-loader,我们应该如何为HMR设置应用程序?

创建于 2019-08-29  ·  85评论  ·  资料来源: facebook/react

Dan Abramov提到Devtools v4将使react-hot-loader过时: https ://twitter.com/dan_abramov/status/1144715740983046144?s

我:
我有这个钩子:
require("react-reconciler")(hostConfig).injectIntoDevTools(opts);
但是HMR一直没有它就可以完全工作。 现在这是新要求吗?

担:
是的,这就是新机制所使用的。 新机制不需要“ react-hot-loader”,因此在更新时,您希望删除该软件包。 (这是非常侵入性的)

但是,我在Devtools文档中看不到任何有关HMR的信息。 既然react-hot-loader已经过时了(以及require("react-hot-loader/root").hot方法),我们应该如何在以下方面为HMR设置应用程序:

  • 反应DOM应用
  • 反应本地应用
  • 反应自定义渲染器应用

对于特别适合已通过react-hot-loader设置HMR的任何人的迁移指南,我会特别感兴趣。

此外,对于HMR,我们使用的是独立的Devtools还是浏览器扩展的Devtools是否重要?

Question

最有用的评论

好的,去。

什么是快速刷新?

在React的全力支持下,这是对“热重载”的重新实现。 它最初是为React Native发行的,但是大多数实现都是与平台无关的。 该计划是全面使用它-替代纯用户态解决方案(例如react-hot-loader )。

我可以在网上使用快速刷新吗?

从理论上讲,是的,这是计划。 实际上,有人需要将其与网络上常见的捆绑器集成(例如Webpack,Parcel)。 我还没有做到这一点。 也许有人想捡起来。 此评论是您如何操作的粗略指南。

它由什么组成?

快速刷新依赖于以下几个方面:

  • 模块系统中的“热模块更换”机制。

    • 通常捆绑器也提供该功能。

    • 例如在webpack中,使用module.hot API可以做到这一点。

  • React renderer 16.9.0+(例如React DOM 16.9)
  • react-refresh/runtime入口点
  • react-refresh/babel Babel插件

您可能需要在集成部分上工作。 即将react-refresh/runtime与Webpack的“热模块替换”机制集成在一起。

集成是什么样的?

⚠️⚠️⚠️要清楚,这是想要实施集成的人们的指南。 务必谨慎操作!

您需要做的一些最少的事情:

  • 在捆绑程序(例如Webpack)中启用HMR
  • 确保React为16.9.0+
  • react-refresh/babel到您的Babel插件

此时,您的应用应该崩溃。 它应包含对未定义的$RefreshReg$$RefreshSig$函数的调用。

然后,您需要创建一个新的JS入口点,该入口点必须在应用程序中的任何代码(包括react-dom (!))之前运行。 如果它在react-dom之后运行,则将无效。 该入口点应该执行以下操作:

if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') {
  const runtime = require('react-refresh/runtime');
  runtime.injectIntoGlobalHook(window);
  window.$RefreshReg$ = () => {};
  window.$RefreshSig$ = () => type => type;
}

这应该可以解决崩溃问题。 但是它仍然不会做任何事情,因为这些$RefreshReg$$RefreshSig$实现是没有问题的。 连接它们是您需要执行的集成工作的重点。

您如何做取决于您的捆扎机。 我想用webpack可以编写一个加载器,在每个模块执行前后添加一些代码。 或者,也许有一些钩子可以向模块模板中注入一些东西。 无论如何,您要实现的是每个模块如下所示:

// BEFORE EVERY MODULE EXECUTES

var prevRefreshReg = window.$RefreshReg$;
var prevRefreshSig = window.$RefreshSig$;
var RefreshRuntime = require('react-refresh/runtime');

window.$RefreshReg$ = (type, id) => {
  // Note module.id is webpack-specific, this may vary in other bundlers
  const fullId = module.id + ' ' + id;
  RefreshRuntime.register(type, fullId);
}
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;

try {

  // !!!
  // ...ACTUAL MODULE SOURCE CODE...
  // !!!

} finally {
  window.$RefreshReg$ = prevRefreshReg;
  window.$RefreshSig$ = prevRefreshSig;
}

这里的想法是我们的​​Babel插件发出对该函数的调用,然后我们的集成将这些调用与模块ID绑定在一起。 这样,在注册组件时,运行时会收到诸如"path/to/Button.js Button"类的字符串。 (或者,在webpack的情况下,ID就是数字。)不要忘记Babel转换和这种包装都只能在开发模式下进行。

作为包装模块代码的替代方法,也许有某种方法可以在捆绑程序实际初始化模块工厂的地方添加这样的try / finally方法。 像我们这样在这里的地铁站(RN捆绑)。 这可能会更好,因为我们不需要膨胀每个模块,也不必担心引入非法语法,例如,将importtry / finally包裹时。

一旦解决了这个问题,您将面临最后一个问题。 捆绑程序不知道您正在处理更新,因此无论如何它都可能会重新加载页面。 您需要告诉它不要。 这也是特定于bundler的,但是我建议的方法是检查所有导出是否


// ...ALL MODULE CODE...

const myExports = module.exports; 
// Note: I think with ES6 exports you might also have to look at .__proto__, at least in webpack

if (isReactRefreshBoundary(myExports)) {
  module.hot.accept(); // Depends on your bundler
  enqueueUpdate();
}

什么是isReactRefreshBoundary ? 这是一个对出口进行了简单枚举并确定是否仅出口React组件的东西。 这就是您决定是否接受更新的方式。 我没有将其复制粘贴到此处,但是此实现可能是一个好的开始。 (在该代码中, Refresh是指react-refresh/runtime导出)。

您还需要手动注册所有导出,因为Babel转换只会调用$RefreshReg$函数。 如果您不这样做,将不会检测到对类的更新。

最后, enqueueUpdate()函数将在模块之间共享,以防抖动并执行实际的React更新。

const runtime = require('react-refresh/runtime');

let enqueueUpdate = debounce(runtime.performReactRefresh, 30);

至此,您应该已经可以正常工作了。

细微差别

对于“快速刷新”品牌,我关心一些基准经验期望。 它应该能够从语法错误,模块初始化错误或呈现错误中正常恢复。 我不会详细介绍这些机制,但我强烈感到,在处理好这些情况之前,您不应将自己的实验称为“快速刷新”。

不幸的是,我不知道webpack是否可以支持所有这些功能,但是如果您进入某种工作状态却又陷入困境,我们可以寻求帮助。 例如,我注意到webpack的accept() API使错误恢复更加困难(错误发生后,您需要接受_previous_版本的模块),但是有一种解决方法。 我们需要回到另一件事是自动“注册”所有导出,而不仅仅是Babel插件找到的导出。 现在,让我们忽略它,但是如果您有适用于webpack的东西,我可以考虑对其进行完善。

同样,我们需要将其与“错误框”体验集成在一起,类似于Create React App中的react-error-overlay 。 这有一些细微差别,例如修复错误时错误框应消失。 一旦基金会成立,这还需要做更多的工作。

如果您有任何疑问,请告诉我!

所有85条评论

有一些困惑。 新的DevTools无法启用热重载(或与重载有任何关系)。 相反,Dan一直在进行的热重载更改利用了DevTools和React用于通信的“挂钩”。 它将自身添加到中间,因此可以重新加载。

我已经编辑了标题,以删除对DevTools的提及(因为这可能会引起混乱)。

至于如何使用新的HMR的问题,我想我不知道那里的最新想法。 我看到@gaearon在CRA回购上有
https://github.com/facebook/create-react-app/pull/5958

至于如何使用新的HMR的问题,我想我不知道那里的最新想法。 我看到@gaearon在CRA回购上有

为了向读者澄清,公关已经过时,不再相关。


我需要写下一些有关“快速刷新”如何工作以及如何集成的内容。 还没来得及。

好的,去。

什么是快速刷新?

在React的全力支持下,这是对“热重载”的重新实现。 它最初是为React Native发行的,但是大多数实现都是与平台无关的。 该计划是全面使用它-替代纯用户态解决方案(例如react-hot-loader )。

我可以在网上使用快速刷新吗?

从理论上讲,是的,这是计划。 实际上,有人需要将其与网络上常见的捆绑器集成(例如Webpack,Parcel)。 我还没有做到这一点。 也许有人想捡起来。 此评论是您如何操作的粗略指南。

它由什么组成?

快速刷新依赖于以下几个方面:

  • 模块系统中的“热模块更换”机制。

    • 通常捆绑器也提供该功能。

    • 例如在webpack中,使用module.hot API可以做到这一点。

  • React renderer 16.9.0+(例如React DOM 16.9)
  • react-refresh/runtime入口点
  • react-refresh/babel Babel插件

您可能需要在集成部分上工作。 即将react-refresh/runtime与Webpack的“热模块替换”机制集成在一起。

集成是什么样的?

⚠️⚠️⚠️要清楚,这是想要实施集成的人们的指南。 务必谨慎操作!

您需要做的一些最少的事情:

  • 在捆绑程序(例如Webpack)中启用HMR
  • 确保React为16.9.0+
  • react-refresh/babel到您的Babel插件

此时,您的应用应该崩溃。 它应包含对未定义的$RefreshReg$$RefreshSig$函数的调用。

然后,您需要创建一个新的JS入口点,该入口点必须在应用程序中的任何代码(包括react-dom (!))之前运行。 如果它在react-dom之后运行,则将无效。 该入口点应该执行以下操作:

if (process.env.NODE_ENV !== 'production' && typeof window !== 'undefined') {
  const runtime = require('react-refresh/runtime');
  runtime.injectIntoGlobalHook(window);
  window.$RefreshReg$ = () => {};
  window.$RefreshSig$ = () => type => type;
}

这应该可以解决崩溃问题。 但是它仍然不会做任何事情,因为这些$RefreshReg$$RefreshSig$实现是没有问题的。 连接它们是您需要执行的集成工作的重点。

您如何做取决于您的捆扎机。 我想用webpack可以编写一个加载器,在每个模块执行前后添加一些代码。 或者,也许有一些钩子可以向模块模板中注入一些东西。 无论如何,您要实现的是每个模块如下所示:

// BEFORE EVERY MODULE EXECUTES

var prevRefreshReg = window.$RefreshReg$;
var prevRefreshSig = window.$RefreshSig$;
var RefreshRuntime = require('react-refresh/runtime');

window.$RefreshReg$ = (type, id) => {
  // Note module.id is webpack-specific, this may vary in other bundlers
  const fullId = module.id + ' ' + id;
  RefreshRuntime.register(type, fullId);
}
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;

try {

  // !!!
  // ...ACTUAL MODULE SOURCE CODE...
  // !!!

} finally {
  window.$RefreshReg$ = prevRefreshReg;
  window.$RefreshSig$ = prevRefreshSig;
}

这里的想法是我们的​​Babel插件发出对该函数的调用,然后我们的集成将这些调用与模块ID绑定在一起。 这样,在注册组件时,运行时会收到诸如"path/to/Button.js Button"类的字符串。 (或者,在webpack的情况下,ID就是数字。)不要忘记Babel转换和这种包装都只能在开发模式下进行。

作为包装模块代码的替代方法,也许有某种方法可以在捆绑程序实际初始化模块工厂的地方添加这样的try / finally方法。 像我们这样在这里的地铁站(RN捆绑)。 这可能会更好,因为我们不需要膨胀每个模块,也不必担心引入非法语法,例如,将importtry / finally包裹时。

一旦解决了这个问题,您将面临最后一个问题。 捆绑程序不知道您正在处理更新,因此无论如何它都可能会重新加载页面。 您需要告诉它不要。 这也是特定于bundler的,但是我建议的方法是检查所有导出是否


// ...ALL MODULE CODE...

const myExports = module.exports; 
// Note: I think with ES6 exports you might also have to look at .__proto__, at least in webpack

if (isReactRefreshBoundary(myExports)) {
  module.hot.accept(); // Depends on your bundler
  enqueueUpdate();
}

什么是isReactRefreshBoundary ? 这是一个对出口进行了简单枚举并确定是否仅出口React组件的东西。 这就是您决定是否接受更新的方式。 我没有将其复制粘贴到此处,但是此实现可能是一个好的开始。 (在该代码中, Refresh是指react-refresh/runtime导出)。

您还需要手动注册所有导出,因为Babel转换只会调用$RefreshReg$函数。 如果您不这样做,将不会检测到对类的更新。

最后, enqueueUpdate()函数将在模块之间共享,以防抖动并执行实际的React更新。

const runtime = require('react-refresh/runtime');

let enqueueUpdate = debounce(runtime.performReactRefresh, 30);

至此,您应该已经可以正常工作了。

细微差别

对于“快速刷新”品牌,我关心一些基准经验期望。 它应该能够从语法错误,模块初始化错误或呈现错误中正常恢复。 我不会详细介绍这些机制,但我强烈感到,在处理好这些情况之前,您不应将自己的实验称为“快速刷新”。

不幸的是,我不知道webpack是否可以支持所有这些功能,但是如果您进入某种工作状态却又陷入困境,我们可以寻求帮助。 例如,我注意到webpack的accept() API使错误恢复更加困难(错误发生后,您需要接受_previous_版本的模块),但是有一种解决方法。 我们需要回到另一件事是自动“注册”所有导出,而不仅仅是Babel插件找到的导出。 现在,让我们忽略它,但是如果您有适用于webpack的东西,我可以考虑对其进行完善。

同样,我们需要将其与“错误框”体验集成在一起,类似于Create React App中的react-error-overlay 。 这有一些细微差别,例如修复错误时错误框应消失。 一旦基金会成立,这还需要做更多的工作。

如果您有任何疑问,请告诉我!

在告诉React启动渲染之前,语法错误/初始化错误应该“足够容易”以某种方式处理,但是渲染错误将如何与错误边界相互作用?

如果发生渲染错误,它将触发最接近的错误边界,该错误边界会将自身快照到错误状态,并且没有通用的方法来告诉错误边界,在实时刷新之后,他们的子级已经神奇地固定了。 /是否每个可刷新组件都应免费获得自己的错误边界,或者在初始化时检测到运行时支持,错误处理在协调器中的工作方式是否有所不同?

所有导出都是React组件,在这种情况下,“接受”更新

有什么办法可以检测到这些成分吗? 据我了解-不。 除export.toString().indexOf('React')>0 ,但它将停止与应用的任何HOC一起使用。
在文件末尾加上_self accepting_不容易出错-不会建立新的accept句柄,并且下一个更新将冒泡到更高的边界,这就是创建require("react-hot-loader/root").hot的原因。

在任何情况下-似乎如果从react-hot-loader抛出所有特定于反应的代码,而保持外部API不变,那就足够了,并且适用于所有现有安装。

使用react-refresh/babel 0.4.0给大量文件提供了此错误:

ERROR in ../orbit-app/src/hooks/useStores.ts
Module build failed (from ../node_modules/babel-loader/lib/index.js):
TypeError: Cannot read property '0' of undefined
    at Function.get (/Users/nw/projects/motion/orbit/node_modules/@babel/traverse/lib/path/index.js:115:33)
    at NodePath.unshiftContainer (/Users/nw/projects/motion/orbit/node_modules/@babel/traverse/lib/path/modification.js:191:31)
    at PluginPass.exit (/Users/nw/projects/motion/orbit/node_modules/react-refresh/cjs/react-refresh-babel.development.js:546:28)

我将文件缩小为最简单的文件:

import { useContext } from 'react'

export default () => useContext()

如果发生渲染错误,它将触发最接近的错误边界,该错误边界会将自身快照到错误状态,并且没有通用的方法来告诉错误边界,在实时刷新之后,其子级可能已被魔术修复。

React内的快速刷新代码会记住当前哪些边界失败。 每当安排了“快速刷新”更新时,它将始终重新安装它们。

如果没有边界,但根更新失败,则快速刷新将重试使用其最后一个元素渲染该根。

如果根目录在挂载时失败, runtime.hasUnrecoverableErrors()会告诉您。 然后,您必须强制重新加载。 我们可以稍后再处理,但我还没有时间解决。

使用react-refresh / babel 0.4.0在大量文件上给我这个错误:

提出新的问题?

有什么办法可以检测到这些成分吗?

我链接到我的实现,该实现本身使用Runtime.isLikelyAReactComponent() 。 这不是完美的,但足够好。

新的接受句柄将不会建立,并且下一个更新将冒泡到更高的边界

你能举个例子吗? 我没有关注。 无论如何,这是捆绑器特有的。 我让Metro做我想要的。 如果缺少API,我们可以要求webpack添加一些东西。

这里的目标是在保证一致性的同时重新执行尽可能少的模块。 我们希望泡沫更新到根对大多数编辑。

好像是如果要从react-hot-loader抛出所有特定于React的代码,则保持外部API不变

也许,尽管我也想删除顶层容器。 我还希望与错误框更紧密地集成。 也许仍然可以称为react-hot-loader

顺便说一下,我编辑了指南,加入了我遗忘的遗失部分- performReactRefresh通话。 那就是实际安排更新的事情。

isLikelyComponentType(type) {
   return typeof type === 'function' && /^[A-Z]/.test(type.name);
},

这样的逻辑我不会感到安全。 即使所有的CapitalizedFunctions几乎都是React组件-许多模块(我的模块)也有其他出口。 例如_exports-for-tests_。 这不是问题,但会产生_unpredictability_的问题-可以在任意点创建热边界...或者在更改一行后就无法创建热边界。
什么会破坏isLikelyComponentType测试:

  • 导出的mapStateToProps (用于测试,未在生产代码中使用)
  • 导出hook (没关系)
  • 导出的Class可能不是反应类(不会,但应该)

所以-有时会建立热边界,但不会建立热边界,但有时不会建立热边界。 听起来像旧的不稳定不稳定的热装弹,我们俩都不是很喜欢:)

在某个地方应用热边界不是那么不可预测,而是可以预期的- thingdomain边界,或目录索引,即index.js导出来自同一目录中$$$ Component.js的“公共API”(不是Facebook风格的afaik)。

换句话说-就像您在Metro中所做的一样,但有更多限制。 其他所有内容,例如为任何延迟加载的组件建立这样的边界的规则规则,都可以用来执行正确的行为。

说到哪个-热快速刷新将处理懒惰? 它是否应该在import的另一侧具有边界?

快速尝试查看浏览器中的魔术,它非常好:)我做了最简单的事情,即对所有检测代码进行硬编码,因此尚无webpack插件

Kapture 2019-09-07 at 23 09 04

在这里回购: https :

只是好奇而已,但是对于webpack,您难道没有一个babel插件来包装try / finally吗? 只想确保在尝试之前我不会丢失任何东西。

Babel插件不是特定于环境的。 我想保持这种方式。 它对模块或更新传播机制一无所知。 那些取决于捆包机。

例如,在Metro中根本没有任何try / finally包装转换。 取而代之的是,我将try / final放在

您当然可以创建另一个Babel插件进行包装。 但是,通过webpack这样做并不会给您带来任何好处。 由于还是Webpack专用的。 您可能会在没有意义的其他环境(不是Webpack)中意外运行Babel插件,这可能会造成混淆。

您可以钩入compilation.mainTemplate.hooks.require瀑布钩。 上一次调用它是__webpack_require__函数的默认主体,因此您可以点击钩子将内容包装到try/finally块中。

问题是在__webpack_require__内部获得对React的引用。 可能,但可能需要一定程度的可重入和递归防护。

有关更多详细信息,请检查webpack源代码中的MainTemplate.jsweb/JsonpMainTemplatePlugin.jsJsonpMainTemplatePlugin本身只是从MainTemplate.js插入了一堆钩子,所以这可能是您需要解决的“难题”。

这是我一起砍伐的原型,有效地完成了Dan上面概述的工作。 这是很不完整的,但是证明了webpack中的lo-fi实现: https ://gist.github.com/maisano/441a4bc6b2954205803d68deac04a716

一些注意事项:

  • react-dom在此处进行了硬编码,因此这不适用于自定义渲染器或子包(例如react-dom/profiling )。
  • 我还没有深入研究webpack的所有模板变体的工作方式,但是包装模块执行的方式非常笨拙。 我不确定如果使用umd库目标,此示例是否可行。

问题是在__webpack_require__内获取对React的引用。 可能,但可能需要一定程度的可重入和递归防护。

我认为您的意思是获取对“刷新运行时”的引用。

在Metro中,我通过尽早执行require.Refresh = RefreshRuntime解决了这一问题。 然后在require实现中,我可以从require函数本身读取属性。 它不会立即可用,但是如果我们设置得足够早的话就没有关系。

@maisano我不得不更改许多事情,最终我没有看到webpack调用的.accept函数。 我已经尝试了.accept(module.i, () => {}).accept(() => {}) (自我接受,除了在webpack中不起作用)。 hot属性已启用,我看到它降下来并通过接受的模块运行。

因此,我最终修补了webpack以调用自接受模块,这是最终的解决方法。

这是补丁:

diff --git a/node_modules/webpack/lib/HotModuleReplacement.runtime.js b/node_modules/webpack/lib/HotModuleReplacement.runtime.js
index 5756623..7e0c681 100644
--- a/node_modules/webpack/lib/HotModuleReplacement.runtime.js
+++ b/node_modules/webpack/lib/HotModuleReplacement.runtime.js
@@ -301,7 +301,10 @@ module.exports = function() {
                var moduleId = queueItem.id;
                var chain = queueItem.chain;
                module = installedModules[moduleId];
-               if (!module || module.hot._selfAccepted) continue;
+               if (!module || module.hot._selfAccepted) {
+                   module && module.hot._selfAccepted()
+                   continue;
+               }
                if (module.hot._selfDeclined) {
                    return {
                        type: "self-declined",

我知道这与他们的API背道而驰,该API希望将其作为“ errorCallback”,我记得很多年前,我们专门在内部HMR上工作时遇到了这个问题,最终我们最终编写了自己的捆绑器。 我相信包裹支持“自我接受”回调API。 也许值得在webpack上发布一个问题,看看是否可以将其合并? @sokra

所以...我根据@maisano的工作进一步完善了插件:
https://github.com/pmmmwh/react-refresh-webpack-plugin
(我之所以用TypeScript编写它,是因为我不相信自己刚开始时会喜欢webpack内部,我可以将其转换为纯JS / Flow)

我尝试消除了使用webpack Dependency类注入热模块代码的加载器的需求,但是看来这将需要重新解析所有模块(因为即使所有内联函数都需要,我们仍然需要引用react-refresh/runtime在某处)。

另一个问题是,没有简单的方法(afaik)来检测webpack中类似JavaScript的文件-例如html-webpack-plugin使用javascript/auto类型,因此我将似乎是硬编码的用于加载程序注入的可接受的文件掩码(JS / TS / Flow)。

我还在这个5岁的线程中基于@gaearon的注释添加了错误恢复(至少是语法错误)。 接下来是从反应错误中恢复-我怀疑这可以通过注入全局错误边界(类似AppWrapperreact-hot-loader )来完成,这也可以解决错误框界面,但是没有现在还有时间来解决这个问题。

@natew引发的问题也可以避免-通过将enqueueUpdate调用与hot.accpet(errorHandler)调用去耦来实现。

@pmmmwh什么时间! 我刚刚创建了一个回购协议,该

无论如何,我都没有犯错误的习惯,尽管这里的插件比我最初使用的方法要扎实得多。

接下来是从反应错误中恢复-我怀疑这可以通过注入全局错误边界(类似于React-hot-loader的AppWrapper来实现)来完成,这也可以解决错误框界面,但没有时间解决刚刚。

那应该已经可以使用了。 无需自定义错误边界或在此处包装。

接下来是从反应错误中恢复-我怀疑这可以通过注入全局错误边界(类似于React-hot-loader的AppWrapper来实现)来完成,这也可以解决错误框界面,但没有时间解决刚刚。

那应该已经可以使用了。 无需自定义错误边界或在此处包装。

@gaearon奇怪。 我尝试在渲染函数组件中引发错误-如果错误发生在return ,则HMR可以工作,但是如果发生在其他地方,则有时不起作用。

@pmmmwh什么时间! 我刚刚创建了一个回购协议,该

无论如何,我都没有犯错误的习惯,尽管这里的插件比我最初使用的方法要扎实得多。

@maisano我应该怎么说? 我实际上开始着手这项工作,并在上周末陷入了依赖注入问题。您的要旨为我提供了出路:tada:

如果返回错误,则表示HMR有效,但如果错误发生在其他地方,则有时无效。

我需要更多有关您尝试过的内容以及“有效”和“无效”的含义的详细信息。

如果模块捆绑器集成未正确实现(这是主题或该主题),可能会出错。 我希望React本身没有阻止从编辑期间引入的错误中恢复的功能。 您可以验证它是否可以在React Native 0.61 RC3中使用。

@pmmmwh@maisano ,以下检查会跳过具有名为exports的组件的模块,并且不会建立刷新边界:

https://github.com/pmmmwh/react-refresh-webpack-plugin/blob/master/src/loader/utils/isReactRefreshBoundary.ts#L23 -L27

const desc = Object.getOwnPropertyDescriptor(moduleExports, key);
if (desc && desc.get) {
  // Don't invoke getters as they may have side effects.
  return false;
}

我不知道为什么在Metro需要这样做,但是在webpack吸气剂只返回指定的出口,据我所知没有副作用。 因此,应安全删除。

@gaearon React.lazy组件(例如,代码分割路线)未重新呈现。 这是设计使然吗? 我可以看到刷新边界已建立,但是performReactRefresh()似乎没有任何作用。 对懒惰的孩子所做的更改可以很好地刷新,因此这没什么大不了的,但是我想知道我们是否做错了什么...

lazy是一个小状态机-它持有对旧组件的引用,并且该引用必须进行更新。
现在让我们想象一下,现在是指一个全新的lazy对象)-它必须再考虑到loading阶段,这可能会破坏所有嵌套树。

我希望懒惰能工作。 也许出了点问题。 我需要看一个复制盒。

既然有一些原型,我们是否应该选择一个原型,然后将讨论移至其问题? 并在那里迭代。

有:
https://github.com/maisano/react-refresh-plugin
和:
https://github.com/pmmmwh/react-refresh-webpack-plugin
我已经设置了pmmmwh的插件的分支,该插件可与[email protected] (还修复了名为exports的问题):
https://github.com/WebHotelier/webpack-fast-refresh

react-hot-loader呢?

react-hot-loader几乎将fast refresh所有功能都向后移植,但是历史和集成的时刻很少,不能全部向后移植,并且说实话,以“ rhl”术语重新实现它们是没有意义的。 所以-让它退休。

另一个: https

我需要更多有关您尝试过的内容以及“有效”和“无效”的含义的详细信息。

如果模块捆绑器集成未正确实现(这是主题或该主题),可能会出错。 我希望React本身没有阻止从编辑期间引入的错误中恢复的功能。 您可以验证它是否可以在React Native 0.61 RC3中使用。

经过一些调整后,我可以验证它是否有效。

但是-babel插件似乎不适用于类。 我检查了一下,这似乎与实现无关,因为所有注入的代码和react-refresh/runtime都能正常工作。 我不确定这是预期的还是特定于webpack的,如果是后者,我明天可以尝试修复。 (我也仅对Metro预设进行了测试,此处重现了要点)

我确定它适用于RN,但是在我目前的机器上,我没有方便的环境可以在RN上进行测试,因此,如果您能指出我在Metro中实现babel插件或进行转换的话,将非常有用。

既然有一些原型,我们是否应该选择一个原型,然后将讨论移至其问题? 并在那里迭代。

也许我们可以去这里? 自上次发表评论以来,我已将整个项目移植到纯JS中,并添加了一些有关更新排队的修复程序。 我不必为webpack @ 5移植插件,但是在阅读了@apostolos的fork和webpack @ next中的新HMR逻辑

是的,Babel插件不会注册课程。 目的是在模块系统级别上发生这种情况。 应该检查每个导出是否“可能”是React组件。 (运行时提供了检查功能。)如果为true,则像Babel插件一样注册导出。 给它一个像filename exports%export_name的ID。 这就是使类在Metro中工作的原因,因为Babel插件找不到它们。

换句话说,由于我们仍然无法保留类状态,因此我们最好将它们“定位”到模块导出边界,而不是尝试通过转换在源代码中内联找到它们。 导出应该充当我们在插件中找不到的组件的“包罗万象”。

Mailchimp开始使用我最后共享的插件的分支。 它被充实了一些,选择使用它的人似乎很高兴。 我们将继续在本地对其进行迭代。 我计划删除分支并在更远的地方向上游发布更新。

@gaearon关于添加符号的想法,我们可以将其附加到我们知道可以安全更新的事物上,但是不是组件吗? 例如,我们有一个类似的模式:

export default create({
  id: '100'
})

export const View = () => <div />

其中create仅返回一个对象。 我现在已经对其进行了修补,但我们可以轻松地在默认导出对象中添加一个符号,以表明这是一个安全文件。 不确定确切的最佳模式。

编辑:我确实意识到这可以进入刷新实现! 我认为它在运行时可能会更好,但可能并非如此。 加载程序有这么多种不同的提示,采用标准方式可能会更好。

让我们前进10年。 您的代码库是什么样的? 允许在这里,禁止在那里? 如何使这些标志保持最新状态? 如何推理? 就像有_safe到update_位置和_unsafe_一样,您必须保留或由于某种原因无法正确协调。 在每种情况下,哪些原因是有效的原因?

  • 您将获得更多symbols -重新加载force allow ,或重新加载force disallow
  • 为什么您可能想降低更新传播边界(即接受“此”模块边界上的更新),或想提高它(即接受“那个”模块边界上的更新)
  • 如果没有边界,会发生什么? 仅仅是性能问题,还是可能发生更严重的事情?

大家好👋我想在这里伸出援手。 我们是否已达成单一回购/努力协议?

是@pmmmwh共享的此回购协议吗?
https://github.com/pmmmwh/react-refresh-webpack-plugin

还是@maisano共享此回购协议?
https://github.com/maisano/react-refresh-plugin

看起来@pmmmwh的那个最近已经提交了。 除非我听到其他情况,否则我将假设这是重点关注的对象。

Parcel 2的实施已从此处开始: https :

夏季!

对于任何需要它的人,使用Nollup进行开发的Rollup项目React Refresh的实现: https :

可能不是最干净的实现,但它可以工作。

对于webpack解决方案,上述插件似乎尚未正式发布,因此似乎最好的HMR解决方案是Dan的库,仍在这里: https :

我们刚交付了具有快速刷新支持功能的Parcel 2 alpha 3。 随时尝试。 😍https : //twitter.com/devongovett/status/1197187388985860096 =20

R在RHL中添加了弃用说明🥳

我一直在尝试使用@pmmmwh进行中工作react-app-rewiredcustomize-cra在CRA应用程序上尝试使用的食谱

npx create-react-app <project_dir> --typescript

npm install -D react-app-rewired customize-cra react-refresh babel-loader https://github.com/pmmmwh/react-refresh-webpack-plugin

编辑./package.json

  "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },

添加./config-overrides.js文件:

// eslint-disable-next-line
const { addBabelPlugin, addWebpackPlugin, override } = require('customize-cra');
// eslint-disable-next-line
const ReactRefreshPlugin = require('react-refresh-webpack-plugin');

/* config-overrides.js */
module.exports = override(
  process.env.NODE_ENV === 'development'
    ? addBabelPlugin('react-refresh/babel')
    : undefined,
  process.env.NODE_ENV === 'development'
    ? addWebpackPlugin(new ReactRefreshPlugin())
    : undefined,
);

享受到目前为止的经验。 感谢大家参与的所有工作!

谢谢@ drather19

我根据您的指令创建了一个存储库,它可以正常工作:https://github.com/jihchi/react-app-rewired-react-refresh如果有人想尝试一下并保存一些输入内容,请随时克隆存储库。


请参考https://github.com/pmmmwh/react-refresh-webpack-plugin/tree/master/examples/cra-kitchen-sink

AND ... Webpack的v0.1.0刚刚发货🎉

@ drather19 @jihchi
你们可能想切换到该版本-它包含更统一的体验以及有关初始实现的许多错误修复。

@pmmmwh支持ts-loader + babel-loader吗?

@pmmmwh支持ts-loader + babel-loader吗?

我只使用Babel对TS进行了测试,并且可以正常工作,因此如果在使用ts + babel加载程序时无法正常工作,请随时提出问题:)

@ drather19我尝试克隆并运行您的存储库,但开发服务器从未启动。

环境,
作业系统-OSX 10.14.6
节点-v12.13.0
纱-1.19.2

@pmmmwh-仅供参考

react-app-rewired-react-refresh on  master is 📦 v0.1.0 via ⬢ v12.13.0
❯ yarn start
yarn run v1.19.2
$ react-app-rewired start | cat
ℹ 「wds」: Project is running at http://192.168.1.178/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /Users/seanmatheson/Development/temp/react-app-rewired-react-refresh/public
ℹ 「wds」: 404s will fallback to /index.html
Starting the development server...


@ drather19我尝试克隆并运行您的存储库,但开发服务器从未启动。

环境,
作业系统-OSX 10.14.6
节点-v12.13.0
纱-1.19.2

@pmmmwh-仅供参考

react-app-rewired-react-refresh on  master is 📦 v0.1.0 via ⬢ v12.13.0
❯ yarn start
yarn run v1.19.2
$ react-app-rewired start | cat
ℹ 「wds」: Project is running at http://192.168.1.178/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from /Users/seanmatheson/Development/temp/react-app-rewired-react-refresh/public
ℹ 「wds」: 404s will fallback to /index.html
Starting the development server...

该问题已在插件的master分支中修复,并将于明天发布。

我设法使用babel与@Typemm React应用程序一起使用@pmmmwh的webpack插件。 但是,增量构建大约需要12秒,而不是仅使用ts-loader大约需要2秒。 我将继续研究这个问题,以查看是否在babel config方面缺少某些东西,以使性能更接近,但与ts-loader和完整刷新相比,现在这是一笔洗钱。

@IronSean请在该插件的回购中报告它? 这听起来不正常。

我将继续研究这个问题,以查看是否在babel config方面缺少某些东西,以使性能更接近,但与ts-loader和完整刷新相比,现在这是一笔洗钱。

介意在那里发布您的配置/设置吗? 没有更多的背景信息,我将无法解决问题。

@pmmmwh,当我确认确实是您的插件仓库
https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/20

react-refreshReact Fast Refresh ?)是否可以与Preact配合使用,或者react-hot-loader是Preact的长期解决方案?

@Jumblemuddle取决于Preact,但如果需要,它们应该能够与Fast Refresh集成。

对于想要使用Fast Refresh进行操作的CRA人士,我现在可以通过以下craco.config.js来让craco更好的运气(vs. react-app-rewired + customize-cra):

// eslint-disable-next-line
const { whenDev } = require('@craco/craco');
// eslint-disable-next-line
const ReactRefreshPlugin = require('react-refresh-webpack-plugin');

module.exports = {
  webpack: {
    configure: webpackConfig => {
      if (process.env.NODE_ENV === 'development') {
        webpackConfig.module.rules.push({
          test: /BabelDetectComponent\.js/,
          use: [
            {
              loader: require.resolve('babel-loader'),
              options: {
                plugins: [require.resolve('react-refresh/babel')],
              },
            },
          ],
        });
        webpackConfig.module.rules.push({
          test: /\.[jt]sx?$/,
          exclude: /node_modules/,
          use: [
            {
              loader: require.resolve('babel-loader'),
              options: {
                presets: [
                  '@babel/react',
                  '@babel/typescript',
                  ['@babel/env', { modules: false }],
                ],
                plugins: [
                  '@babel/plugin-proposal-class-properties',
                  '@babel/plugin-proposal-optional-chaining',
                  '@babel/plugin-proposal-nullish-coalescing-operator',
                  'react-refresh/babel',
                ],
              },
            },
          ],
        });
      }
      return webpackConfig;
    },
    plugins: [
      ...whenDev(
        () => [new ReactRefreshPlugin({ disableRefreshCheck: false })],
        [],
      ),
    ],
  },
};

特别是,添加webpackConfig.optimization.runtimeChunk = false;将允许您添加/删除钩子,并且仍然可以优雅地快速刷新。

现在享受更多改善的体验。 感谢@ mmhand123通过https://github.com/pmmmwh/react-refresh-webpack-plugin/issues/25提供的提示 (<-解决了!)

根据@ drather19的建议,我发布了一个简化它。 请参阅esetnik / customize-cra-react-refresh

感谢@ drather19,我现在稍微修改了代码,现在它可以在纱线工作区monorepo设置中工作了。

首先,将以下内容安装在要启用快速刷新的子包中:

"@craco/craco": "^5.6.3", "@pmmmwh/react-refresh-webpack-plugin": "^0.2.0", "webpack-hot-middleware": "^2.25.0"

然后将其添加到craco.config.js

;(function ForbidCRAClearConsole() {
    try {
        require('react-dev-utils/clearConsole')
        require.cache[require.resolve('react-dev-utils/clearConsole')].exports = () => {}
    } catch (e) {}
})()

const { whenDev } = require('@craco/craco')
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin')

module.exports = {
    webpack: {
        configure: webpackConfig => {
            whenDev(() => {
                // Work around monorepo setup when using yarn workspace hoisted packages
                // without the need to list 'babel-loader' and 'babel-preset-react-app' as
                // dependencies to avoid duplication since 'react-scripts' already has them.
                const reactLoaderConfig = webpackConfig.module.rules
                    .find(x => Array.isArray(x.oneOf))
                    .oneOf.find(
                        x =>
                            x.options &&
                            x.options.presets &&
                            x.options.presets.some(p => p.includes('babel-preset-react-app')) &&
                            x.loader &&
                            typeof x.loader.includes === 'function' &&
                            x.loader.includes('babel-loader') &&
                            x.test &&
                            typeof x.test.test === 'function' &&
                            x.test.test('x.tsx') &&
                            x.test.test('x.jsx'),
                    )

                if (reactLoaderConfig) {
                    webpackConfig.module.rules.push({
                        test: /BabelDetectComponent\.js/,
                        use: [
                            {
                                loader: reactLoaderConfig.loader,
                                options: {
                                    plugins: [require.resolve('react-refresh/babel')],
                                },
                            },
                        ],
                    })

                    webpackConfig.module.rules.push({
                        test: /\.[jt]sx?$/,
                        exclude: /node_modules/,
                        use: [
                            {
                                loader: reactLoaderConfig.loader,
                                options: {
                                    presets: reactLoaderConfig.options.presets,
                                    plugins: [require.resolve('react-refresh/babel')],
                                },
                            },
                        ],
                    })
                } else {
                    console.error('cannot find react app loader')
                }

                // console.debug(require('util').inspect(webpackConfig.module.rules, { colors: true, depth: null }))
            })

            return webpackConfig
        },
        plugins: [whenDev(() => new ReactRefreshPlugin({ disableRefreshCheck: false }))].filter(Boolean),
    },
}

@gaearon我们是否期望在某个时间点默认情况下在CRA中提供“快速刷新”?
如果是这样,那需要什么?

:-)当前需要完成的工作量。

如果使用HMR函数将被调用? 例如componentDidMount。
我使用react-proxy和componentDidMount将被调用。
而反应15.X可以使用快速刷新吗?

  • componentDidMount将被调用。 以及unmount -类将全部重载。
  • 现在是停止使用react-proxy的好时机。 好吧,您可能应该在几年前停止。
  • 15.X ? - 绝对不。 快速刷新是反应的一部分,因此仅在现代版本中存在。

所以我们应该使用快速刷新或react-hot-loader来代替react-proxy?
有没有办法防止功能(componentDidMount)为HMR执行? -它将调用方法以获取新数据。

我应该如何在JIT中使用react-hot-loader? -浏览器实时编译

  • 所以我们应该使用快速刷新或react-hot-loader来代替react-proxy?

    首先尝试fast refresh ,然后尝试RHL

  • 有没有办法防止功能(componentDidMount)为HMR执行? -它将调用方法以获取新数据。

    (使用钩子...),不要依赖组件lifeCycle,在需要时获取数据。 尝试react-queryswr或其他解决方案。

至于如何使用新的HMR的问题,我想我不知道那里的最新想法。 我看到@gaearon在CRA回购上有

为了向读者澄清,公关已经过时,不再相关。

我需要写下一些有关“快速刷新”如何工作以及如何集成的内容。 还没来得及。

截止到今天,该PR仍然开放。 如果只有仍然有机会被合并的相关PR可以保持开放状态以获得更好的概览,那就太好了。 如果您只是将它们作为参考,则建议将内容移至分支,标签或其他存储库。

我一直得到Error: [React Refresh] Hot Module Replacement (HMR) is not enabled! React Refresh requires HMR to function properly.我一直在遵循文档,但是好像我可能错过了什么?

我不断收到错误消息:[Rere Refresh] Hot Module Replacement(HMR)未启用! React Refresh需要HMR正常运行。 我已经阅读了文档,但似乎可能错过了什么?

@silkfire我假设您正在使用webpack插件。 如果是,请在webpack插件存储库中提交您的问题: https :

截止到今天,该PR仍然开放。 如果只有仍然有机会被合并的相关PR可以保持开放状态以获得更好的概览,那就太好了。 如果您只是将它们作为参考,则建议将内容移至分支,标签或其他存储库。

我很感谢您的建议,但是由于有成千上万条未读的通知,有时我很难记得重新审阅旧的PR。 我相信Create React App存储库维护者可以做正确的事情,如果他们认为它不再有用,则可以关闭。

我要关闭这个。

我们有https://github.com/pmmmwh/react-refresh-webpack-plugin/作为webpack的参考实现。
https://github.com/facebook/react/issues/16604#issuecomment -528663101解释了如何进行自定义集成。

我一直得到Error: [React Refresh] Hot Module Replacement (HMR) is not enabled! React Refresh requires HMR to function properly.我一直在遵循文档,但是好像我可能错过了什么?

似乎您尚未启用Webpack HMR。 如需进一步的帮助,请在插件的仓库中提交问题。

由于热替换现在已成为React的一部分-如果它在React文档中有单独的位置,则指出要用于特定捆绑程序和平台的其他库,并说明一些仍存在的陷阱,例如自更新css模块。

此类信息不应埋在github问题和博客文章中。

@theKashey它在React中,但是其中的react-dom实现只是实验性的。
另外,有一个快速刷新实现将与create-react-app捆绑在一起,但尚未发布:pmmmwh / react-refresh-webpack-plugin#7。 也许它将在下一个react-scripts版本中。

因此,在此实验阶段,React团队目前可能尚不适合谈论React-dom的Fast Refresh。

它在React中,但是React-dom的实现只是实验性的。

需要明确的是,就像在React Native中一样, react-dom本身的实现是稳定的。 只是集成并不都稳定。

它应该在React文档中是否有单独的位置,以指向要用于特定捆绑程序和平台的其他库,并说明一些仍存在的陷阱,例如自更新css模块。

听起来很合理。 我很乐意接受PR,也许将其添加到“高级指南”部分,也许基于类似的RN页面

@gaearon
我的应用程序可以进行一些样式化的组件更改,并且可以正确应用这些更改而不会出现任何问题。
但是,当我在Redux的reducer中更改某些代码时,整个应用程序将被硬刷新并丢失所有的Redux状态。
我是否需要使用其他一些库(例如redux-persistreact-fast-refresh一起保存当前状态?

我们走了整整一个圈,再来一次😅

这就是低级HMR的工作方式,不属于快速刷新的职责范围。 请参考redux或webpack文档

我们走了整整一个圈,再来一次😅

这就是低级HMR的工作方式,不属于快速刷新的职责范围。 请参考redux或webpack文档

您会链接完整的圈子参考吗?

@ jwchang0206请确保您有类似的代码在您的商店。

您会链接完整的圈子参考吗?

对于React Hot Loader,询问了相同的问题。 给出了相同的答案。 我们正处于一个新的周期的开始。

@ jwchang0206查看redux-reducers-injector ,这是我为解决此问题而编写的一个小型库。
它将允许您通过热重装来支持减速器重装。
确保您在减速器中遵循Redux不变性原则,并且可以正常工作💯
如果使用的是Sagas,则可以使用redux-sagas-injector

@gaearon我对window的使用有些困惑。 在我看来,这不是真的必要,因为实现已换出来了吗? 这有什么意义呢?

var prevRefreshReg = window.$RefreshReg$; // these are dummies
var prevRefreshSig = window.$RefreshSig$; // these are dummies
var RefreshRuntime = require('react-refresh/runtime');

window.$RefreshReg$ = (type, id) =>{ /*...*/ }
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;

try {
  // ...
} finally {
  window.$RefreshReg$ = prevRefreshReg; // these are dummies again
  window.$RefreshSig$ = prevRefreshSig; // these are dummies again
}

我有自己的自定义捆绑程序,我正在执行此程序,但我看不出为什么这是绝对必须的,或者它的意义是什么...最初,我怀疑一些内存使用/泄漏优化,但是这些只是将呼叫转移到RefreshRuntime ...

@leidegre我不能评论在window对象上设置$ RefreshSig $的决定,但是在React NativeScript中使用Fast Refresh时,与浏览器环境的耦合给我带来了问题。 @pmmmwh改编了他的Fast Refresh Webpack插件以克服Fast Refresh与浏览器的耦合(在此线程中讨论了遇到和克服的问题:https://github.com/pmmmwh/react-refresh-webpack-plugin/问题/ 79)。 我想知道在您的自定义打包程序的Fast Refresh集成中,所使用的方法是否对您有用?

我的捆绑器主要是TypeScript编译器的包装器。 该实现主要是从react-refresh/babel访问者那里改编而来的。

这只是一个简单的方法,但是不如react-refresh/bable访问者完整。

import ts = require("typescript")

import { IndexModule } from "./registry"

/** Enables the use of `react-refresh` for hot reloading of React components. */
export function hotTransform(m: IndexModule, hot: boolean) {
  // see https://github.com/facebook/react/issues/16604#issuecomment-528663101
  return (ctx: ts.TransformationContext) => {
    return (sourceFile: ts.SourceFile) => {
      const refreshRuntime = ts.createUniqueName("ReactRefreshRuntime")

      const createSignatureFunctionForTransform = ts.createPropertyAccess(
        refreshRuntime,
        "createSignatureFunctionForTransform"
      )

      const register = ts.createPropertyAccess(refreshRuntime, "register")

      let hasComponents = false

      function visitor(node: ts.Node): ts.VisitResult<ts.Node> {
        if (ts.isFunctionDeclaration(node)) {
          if (_hasModifier(node, ts.SyntaxKind.ExportKeyword)) {
            // assert component naming convention

            if (node.name === undefined) {
              console.warn("unsupported export of unnamed function in ...")
              return node
            }

            const name = node.name
            if (!_isComponentName(name.text)) {
              console.warn(
                `warning: unsupported export '${name.text}' in ${m.path} (${m.id}) does not look like a function component, component names start with a capital letter A-Z. TSX/JSX files should only export React components.`
              )
              return node
            }

            if (!hot) {
              return node // opt-out
            }

            hasComponents = true

            let hookSignatureString = ""

            function hookSignatureStringVisitor(
              node: ts.Node
            ): ts.VisitResult<ts.Node> {
              const hookSig = _getHookSignature(node)
              if (hookSig !== undefined) {
                if (0 < hookSignatureString.length) {
                  hookSignatureString += "\n"
                }
                hookSignatureString += hookSig
              }
              return node
            }

            // update function body to include the call to create signature on render

            const signature = ts.createUniqueName("s")

            node = ts.visitEachChild(
              node,
              (node) => {
                if (ts.isBlock(node)) {
                  return ts.updateBlock(
                    ts.visitEachChild(node, hookSignatureStringVisitor, ctx),
                    [
                      ts.createExpressionStatement(
                        ts.createCall(signature, undefined, [])
                      ),
                      ...node.statements,
                    ]
                  )
                }
                return node
              },
              ctx
            )

            const signatureScope = ts.createVariableStatement(
              undefined,
              ts.createVariableDeclarationList(
                [
                  ts.createVariableDeclaration(
                    signature,
                    undefined,
                    ts.createCall(
                      createSignatureFunctionForTransform,
                      undefined,
                      undefined
                    )
                  ),
                ],
                ts.NodeFlags.Const
              )
            )

            const createSignature = ts.createExpressionStatement(
              ts.createCall(signature, undefined, [
                name,
                ts.createStringLiteral(hookSignatureString),
              ])
            )

            const registerComponent = ts.createExpressionStatement(
              ts.createCall(register, undefined, [
                name,
                ts.createStringLiteral(m.path + " " + name.text),
              ])
            )

            return [signatureScope, node, createSignature, registerComponent]
          }
        }

        if (!hot) {
          // if hot reloading isn't enable, remove hot reloading API calls
          if (ts.isExpressionStatement(node)) {
            const call = node.expression
            if (ts.isCallExpression(call)) {
              if (
                _isPropertyAccessPath(
                  call.expression,
                  "module",
                  "hot",
                  "reload"
                )
              ) {
                return undefined
              }
            }
          }
        }

        return node
      }

      sourceFile = ts.visitEachChild(sourceFile, visitor, ctx)

      if (hot && hasComponents) {
        let reactIndex = sourceFile.statements.findIndex((stmt) => {
          if (ts.isImportEqualsDeclaration(stmt)) {
            const ref = stmt.moduleReference
            if (ts.isExternalModuleReference(ref)) {
              const lit = ref.expression
              if (ts.isStringLiteral(lit)) {
                return lit.text === "react"
              }
            }
          }
          return false
        })

        if (reactIndex === -1) {
          console.warn(`cannot find import React = require('react') in ...`)
          reactIndex = 0
        }

        // insert after

        sourceFile = ts.updateSourceFileNode(sourceFile, [
          ...sourceFile.statements.slice(0, reactIndex + 1),
          ts.createImportEqualsDeclaration(
            undefined,
            undefined,
            refreshRuntime,
            ts.createExternalModuleReference(
              ts.createStringLiteral("react-refresh/runtime")
            )
          ),
          ...sourceFile.statements.slice(reactIndex + 1),
          ts.createExpressionStatement(
            ts.createCall(
              ts.createPropertyAccess(
                ts.createPropertyAccess(
                  ts.createIdentifier("module"),
                  ts.createIdentifier("hot")
                ),
                ts.createIdentifier("reload")
              ),
              undefined,
              undefined
            )
          ),
          ts.createExpressionStatement(
            ts.createBinary(
              ts.createPropertyAccess(
                ts.createIdentifier("globalThis"),
                ts.createIdentifier("__hot_enqueueUpdate")
              ),
              ts.createToken(ts.SyntaxKind.AmpersandAmpersandToken),
              ts.createCall(
                ts.createPropertyAccess(
                  ts.createIdentifier("globalThis"),
                  ts.createIdentifier("__hot_enqueueUpdate")
                ),
                undefined,
                undefined
              )
            )
          ),
        ])
      }

      return sourceFile
    }
  }
}

function _hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
  const modifiers = node.modifiers
  if (modifiers !== undefined) {
    for (let i = 0; i < modifiers.length; i++) {
      if (modifiers[i].kind === kind) {
        return true
      }
    }
  }
  return false
}

function _isComponentName(name: string): boolean {
  // ^[A-Z]
  const ch0 = name.charCodeAt(0)
  return 0x41 <= ch0 && ch0 <= 0x5a
}

function _isPropertyAccessPath(
  node: ts.Expression,
  ...path: ReadonlyArray<string>
): node is ts.PropertyAccessExpression {
  for (let i = 0; i < path.length; i++) {
    if (ts.isPropertyAccessExpression(node)) {
      if (!(node.name.text === path[path.length - (i + 1)])) {
        return false
      }
      node = node.expression
    }
  }
  return true
}

function _getHookSignature(node: ts.Node): string | undefined {
  if (ts.isExpressionStatement(node)) {
    const call = node.expression
    if (ts.isCallExpression(call)) {
      const prop = call.expression
      if (ts.isPropertyAccessExpression(prop)) {
        const text = prop.name.text
        if (text.startsWith("use") && 3 < text.length) {
          // todo: add additional checks and emit warnings if the hook usage looks non standard

          return text
        }
      }
    }
  }
  return undefined
}

一开始我不确定如何使用createSignatureFunctionForTransform ,但这只是一个工厂函数,它为每个组件创建一个小的状态机。 因此,您需要为具有静态钩子签名的每个函数调用一次(这只是一个不透明的值,类似于哈希值)。 然后,您可以从渲染调用它以完成设置工作。

它会更改如下内容:

import React = require("react")

export function App() {
  const [state, setState] = React.useState(0)

  return (
    <React.Fragment>
      <p>
        Click Count !!!<strong>{state}</strong>!!!
        <br />
        <button onClick={() => setState((acc) => acc + 1)}>Click me</button>
      </p>
    </React.Fragment>
  )
}

变成这个:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const React = require("react");
const ReactRefreshRuntime_1 = require(6);
const s_1 = ReactRefreshRuntime_1.createSignatureFunctionForTransform();
function App() {
    s_1();
    const [state, setState] = React.useState(0);
    return (React.createElement(React.Fragment, null,
        React.createElement("p", null,
            "Click Count !!!",
            React.createElement("strong", null, state),
            "!!!",
            React.createElement("br", null),
            React.createElement("button", { onClick: () => setState((acc) => acc + 1) }, "Click me"))));
}
exports.App = App;
s_1(App, "useState");
ReactRefreshRuntime_1.register(App, "./App App");
module.hot.reload();
globalThis.__hot_enqueueUpdate && globalThis.__hot_enqueueUpdate();

请注意,访客不完整。 它仅处理最基本的用例。

我对window的使用感到困惑。 在我看来,这不是真的必要,因为实现已换出来了吗? 这有什么意义呢?

@leidegre

我认为Metro中的实现未使用window ,而是实际的global范围。

我不知道有关此实现的原始原理,但是根据我的经验,它很有用-它确保实际的需求逻辑独立于快速刷新逻辑(这意味着react-refresh/babel转换几乎可以与任何捆绑器)。 与交换一样,它也充当保护措施,以确保不会处理不应由运行时处理的模块:

考虑使用@babel/runtime的情况,这会将帮助程序作为捆绑包的导入注入,而您只想对非node_modules代码进行HMR。 如果您不先初始化空的辅助程序,而是将辅助程序分配给全局范围,则极少数情况可能会发生,注入了Babel的辅助程序会在用户界面模块实际完成初始化之前调用cleanup (因为它们是子级的进口)。

此页面是否有帮助?
0 / 5 - 0 等级