背景

在开发 React 应用中,可能会有这么一个需求,对于浏览器默认的 hovering title 行为,如下图: Jietu20200216-161740.jpg

有一天,PM 或者 UI MM 突然要求换成自定义的样式,甚至希望显示的时间更可控,如下图

对于这个需求,我们一般就需要全局搜索 title 这种关键字,然后进行如下替换

<p title="我是一个无辜的title">Hi!</p>
// =>
// HoverTitle 为实现了 Hovering title 交互的组件
<HoverTitle title="我是一个无辜的title">
    <p>Hi!</p>
</HoverTitle>

这样改动量比较大,也容易有几个漏网之鱼。 有没有什么一劳永逸的方法呢?

有什么方式

下面有几种方式来实现以上需求: 但实现思路都是覆盖 React.createElement,如下代码:

const rawCreateElement = React.createElement;
const myCreateElement = (name, props, ...children) => {
    const next = () => rawCreateElement(name, props, ...children);
    if (typeof name === 'string' && typeof props.title === 'string' && props.title) {
        return <HoverTitle title={props.title}>{next()}</HoverTitle>
    }
    return next()
}

那么我们应该如何才能快捷地全局覆盖 React.createElement 呢? 下面介绍如下几种方法

语法匹配替换用

使用 babel-plugin-transform-react-jsx 来更改 jsx 转化逻辑,将 jsx 用到的 createElement 替换成 myCreateElement

但是这样并不完备,比如直接显示使用 React.createElment 或者 React.createFactory 的地方则不能涵盖到,所以我们还需要额外书写 Babel 插件来替换 React.createElment React.createFactory 关键字

替换引入路径

我们还可以换种思路,将 import React from 'react' 替换成 import React from 'my-custom-react'

'my-custom-react' 文件中,覆盖 React

const React = require('react')

module.exports = Object.assign({}, React, {
    createElement: require('./my-custom-create-element'),
    createFactory: require('./my-custom-create-factory')
})

同时我们在 ./my-custom-create-element./my-custom-create-factory 文件中用到的 React.createElementReact.createFactory 需要是原始的,不然会陷入无穷套娃中。

下面有两种方式来实现这种方式 1. babel 插件修改规则 2. enhanced-resolve 插件修改规则

enhanced-resolve 是 Webpack 中使用的可拔插的 resolve 路径包,其实现了 Node.js 中的 require.resolve 算法(支持异步),我们可以利用其暴露的一些勾子来自定义自己的 resovle 逻辑。即我们可以将 resolve('react') 输出为 'my-custom-react'

以上两种方式各有优劣 |方式 | 优点 | 缺点| |---|----|----| |Babel插件|生态好,基本可以方便接入任何构建工具webpack/rollup 等|对于动态引入的模块,匹配麻烦,如 require('rea' + 'ct')| |enhanced-resolve插件|webpack 接入方便,实现方便|在非webpack生态中,可能不好接入|

除了以上两种思路之外,还有一个不大合适的思路

自定义 React Renderer

在这里不深入展开,主要是自己使用react-reconciler实现 ReactDOM.render

对于该需求,自己实现一个 ReactDOM.render 有些舍近求远,该方式更适合于渲染同一份 React 代码,在不同的运行环境(如浏览器: ReactDOM、服务端 ReactDOMServer、移动端 ReactNative

实现

在这里,我使用的是 enhanced-resolve 插件实现 module-mock-plugin,这里有一篇相当详尽的 Webpack resolve 介绍

使用该插件,我们可以如下组织我们的项目代码

src/
    __mock/
        react/ # custom react
node_modules/
    react/

src/__mock/react/index.js 中可以覆写 React 实现