给 React 组件自动加上 react-hot-loader

在 React 项目中使用 Webpack HMR 时,通常会使用 react-hot-loader来进行局部热更新。但使用 react-hot-loader 需要对原有代码进行修改,这对多入口的老项目非常的不友好。

为了使用上 HMR 这一“激动人心”的功能,需要在构建时在原有代码上自动添加 react-hot-loader 的相关代码。 因此我们需要创建一个 webpack loader 来在 babel 处理前将 react-hot-loader 相关代码添加到源 码中。

react-hot-loader 的用法

react-hot-loader 的用法非常简单,安装 react-hot-loader后在 babel 配置中加上 react-hot-loader/babel插件:

// .babelrc
{
  "plugins": ["react-hot-loader/babel"]
}
json

并且在根组件文件中添加如下代码即可:

// App.js
import 'react-hot-loader';
import { hot } from 'react-hot-loader/root';
const App = () => <div>Hello World!</div>;
export default hot(App);
js

通过 webpack loader 修改代码

这里我们需要创建一个 webpack loader 来做这件事情,在代码被 babel-loader处理前进行插入与修改。

Webpack Loader 用于对模块源码的转换,所以这里我们在源码顶部添加 import 'react-hot-loader'import { hot } from 'react-hot-loader/root',并将默认导出修改为 export default hot(xxx)

如何对模块源码进行分析并修改呢?答案是 @babel/parser 与 @babel/traverse,通过 @babel/parser 解析为 AST 之后使用 @babel/traverse 遍历节点并进行修改。

将模块代码转换为 AST

const ast = parse(source, {
  sourceType: 'module',
  plugins: ['jsx'],
});
js

对于 React 我们需要启用 jsx 插件(.babelrc 中需要有 babel-preset-react)才能正确的解析代码。如果使用了其他特性的话也需要启用相对应的插件,具体有哪些插件可以查看@babel/parser · Babel

解析成 AST 之后我们就可以进行一些操作了,首先我们先把导入 react-hot-loader 的代码都加到模块源码的顶部。

导入 react-hot-loader

ast.program.body.unshift(
  // insert `import 'react-hot-loader';`
  t.importDeclaration([], t.stringLiteral('react-hot-loader')),
  // insert `import { hot } from 'react-hot-loader/root';`
  t.importDeclaration(
    [t.importSpecifier(t.identifier('hot'), t.identifier('hot'))],
    t.stringLiteral('react-hot-loader/root')
  )
);
js

这里的 t指的是 @babel/types,这个包包含了一些用于判断与创建 AST 节点对象的工具方法。上述代码在模块代码的最顶端导入了 react-hot-loader以及从 react-hot-loader/root导入需要调用的 hot方法。

导入完之后我们需要对导出进行修改,将默认导出的组件包上一层 HOC。

修改模块导出

traverse(ast, {
  ExportDefaultDeclaration: (path) => {
    path.node.declaration = t.callExpression(
    t.identifier(identifier),
      [path.node.declaration]
    );
  },
});
js

这样就能将 export default xxx修改为 export default hot(xxx)。但现实世界是残酷的,我们还可能遇到这样的代码:

export default const App = () => {};
export default class App {};
// 或者这样
export default () => {}
export default class {}
js

对于直接导出匿名函数或者类的,我们无能为力(其实也是有办法的,给它命一个名再导出,但是我不想这样干,使用暂时先不管了)。 但是对于导出非匿名的函数或者类的话,我们就可以进行修改了。把 export default抽到底部,再把组件调用 hot()之后进行导出。

稍微修改一下实现:

const insertNodes = [];
traverse(ast, {
  const { declaration } = path.node;
  // 如果是 export default const App = () => {} 或者 export default class App {}
  if (t.isClassDeclaration(declaration) || t.isFunctionDeclaration(declaration)) {
    if (t.isIdentifier(declaration.id)) {
      path.replaceWith(declaration);
      // 在源码尾部进行默认导出
      insertNodes.push(
        t.ExportDefaultDeclaration(
          t.callExpression(
            t.identifier(identifier),
            [declaration.id]
          )
        )
      );
    }
    return;
  }
  // 如果是 export default App
  if (t.isIdentifier(declaration)) {
    path.node.declaration = t.callExpression(
    t.identifier(identifier),
      [path.node.declaration]
    );
    return;
  }
});
if (insertNodes.length > 0) {
  ast.program.body.push(...insertNodes);
}
js

好的,我们完成了解析代码为 AST 与修改 AST。是时候把它转回代码交给下一个 loader了。

AST 转换为代码

与 @babel/parser 类似,babel 也提供了将 AST 转换回代码的包: @babel/generator。不需要什么乱七八糟的魔法或者咒语,只需要 const { code } = generate(ast);就可以获得崭新的 hot exportd 组件代码了。将它返回之后就可以愉快的使用 webpack HMR 了!!

还有一些坑

  • 导入了就必须调用,没有例外 如果对每个模块代码都进行以上操作的话,会发现页面上会提示 hot update was not successful。这是因为在调用 ReactDOM.render()的文件中 import { hot } from 'react-hot-loader'之后没有进行调用。所以我们需要添加一些判断,只在导出 React 组件的模块中进行代码修改。

  • hot exportd 的组件被继承无效 如果 A 组件继承 B 组件,而 B 组件被自动添加了 react-hot-loader 相关代码的话,A 组件将无法继承 B 组件的 state 与 methods。这个时候 B 组件已经不是 B 组件了,而是 hot()这个 HOC 返回的 ExportedComponent。理论上被继承的组件也是不应该调用 hot()的,因此我们需要添加配置函数,用来判断是否需要修改模块代码。

基于以上的实现以及发现的坑点,我写了一个 react-hot-export-loader用来给 React 组件自动加上 react-hot-loader,并且添加了一些判断或者配置来避免上面的坑。具体的使用方式这里就不在赘述了,请移步 README.md

写在最后

其实按照一般套路,我们只需要在项目的入口处加上几句 react-hot-loader 代码就可以了。但是无奈的是有些时候总是不会按照套路出牌,例如 webpack 打包的逻辑是公共的,打包的是多入口,或者入口你根本就不知道是什么样的。所以才会出现这样一篇文章,这里权当做记录解决这一问题的方案,顺带输出一个 loader 给有缘人。

原文链接:www.ahonn.me

上一篇:wait-for-event
下一篇:从数组入手浅析Vue响应式原理

相关推荐

  • 高频数据交换下Flutter与ReactNative的对比

    (标题图片来自网络,侵删) 后端使用go写的socketio服务模拟期货行情数据,每10ms推送10条行情数据 ReactNative已经尽力优化了。 Flutter由于没fluttersock...

    2 年前
  • 高性能迷你React框架 anu1.3.0 发布

    anujs1.3.0是一款高性能Reactlike框架,是目前世界上对React16兼容最好的迷你库。 自React16起,相继推出createContext,createPortal, creat...

    2 年前
  • 高德地图 react-amap 实战

    clipboard.png(https://img.javascriptcn.com/5a33946ad8c0ea8ee7870f74f331d0c0 "clipboard.png") reacta...

    10 个月前
  • 高品质 React UI 组件

    (https://img.javascriptcn.com/cca319311c2ea59a2b5cdaa63b997828)(https://link.funteas.com?target=http...

    2 年前
  • 骚操作!在react中使用vuex

    原文地址(https://github.com/zyl1314/blog/issues/12) 前言 笔者最近在学习使用,提到react就绕不过去。redux是一个状态管理架构,被广泛用于rea...

    2 年前
  • 项目文档说明:react + Ant Design 的 blog-react-admin

    效果图(https://img.javascriptcn.com/734ce56fe3a37ab11e9744473f56bae1 "效果图") 前言 此 blogreactadmin 项目是基...

    2 年前
  • 面试题:Hooks 与 React 生命周期的关系

    React 生命周期很多人都了解,但通常我们所了解的都是 单个组件 的生命周期,但针对 Hooks 组件、多个关联组件(父子组件和兄弟组件) 的生命周期又是怎么样的喃?你有思考和了解过吗,接下来我们将...

    1 年前
  • 面试官:请你在React中引入Vue3的@vue/reactivity,实现响应式。

    前言 React的状态管理是一个缤纷繁杂的大世界,光我知道的就不下数十种,其中有最出名immutable阵营的redux,有mutable阵营的mobx,reacteasystate,在hooks诞生...

    5 个月前
  • 面试官我想做个Reacter(React路由)

    路由的基础用法回顾,源码study,文章首发于docs,求个star 依赖 路由依赖于 reactrouter ,但是一般 pc 路由使用 reactrouterdom ,它依赖于 reactrout...

    4 个月前
  • 面试中React与Vue的比对

    1.virtual dom 用JS模拟DOM结构,DOM变化的对比,放在JS层做,以提高重绘性能 DOM操作昂贵,JS运行效率高,要减少DOM操作 使用:snabbdom的使用 ...

    2 年前

官方社区

扫码加入 JavaScript 社区