OverRainbow

how to debug react without pain

☕️ 2 min read

如何愉快的调试react

React是一个huge code base。研究它,有许多方法。我希望可以一边开着一个app(SPA?),这个app引入一个本地的react,在里面搞事情,并可以实时看到效果。

思路主要是在于两端(app端和store端)如何链接起来。

原有方案:store端push,app端pull

我在之前主要用yalc改包发布到本地store,app引入yalc的store中的react。这样的好处是完全不需要修改app编译的过程,但是build,push package,pull package,pull,restart,restart 的4步走过程并算不上友好。(只适用于简单的调试,频繁的修改会shi的)

新方案:整合package到app端,live-reload

如果修改webpack打包过程,让app识别这一个package副本(webpack alias),并借助hmr能力,使得修改源码可以实时生效。岂不是很美?当然,这也并不是什么新技术,只是整合一下已有的技术。

实现

Step1 Base

首先需要一个solid的基础,毫无疑问,选择最新版CRA,并且eject。

npx create-react-app debug-react-ts --template typescript
cd debug-react-ts
yarn run eject

Step2 下载源码,魔改config

首先下载React源码(建议17),放入src目录下。

然后准备修改config/webpack.config.js文件。

全文搜索alias,该部分内容替换成如下:

      alias: {
        react: path.resolve(__dirname, "../src/react/packages/react"),        "react-dom": path.resolve(__dirname, "../src/react/packages/react-dom"),        "legacy-events": path.resolve(          __dirname,          "../src/react/packages/legacy-events"        ),        shared: path.resolve(__dirname, "../src/react/packages/shared"),        "react-reconciler": path.resolve(          __dirname,          "../src/react/packages/react-reconciler"        ),        "react-events": path.resolve(__dirname, "../src/react/packages/events"),        scheduler: path.resolve(__dirname, "../src/react/packages/scheduler")      },

接着,如果start的话会报错,大致意思是react的源码里面有flow的语法,ts不认识。我们只要安装插件忽略掉这个问题即可。

yarn add -D @babel/plugin-transform-flow-strip-types

在这个位置加入这个插件

plugins: [
            [
            require.resolve('babel-plugin-named-asset-import'),
            {
                loaderMap: {
                svg: {
                    ReactComponent:
                    '@svgr/webpack?-svgo,+titleProp,+ref![path]',
                },
                },
            },
            ],
            [require.resolve("@babel/plugin-transform-flow-strip-types")],            isEnvDevelopment &&
            shouldUseReactRefresh &&
            require.resolve('react-refresh/babel'),
        ].filter(Boolean),

Step3 修复报错

run起来,可以看到报错

Failed to compile.

./src/react/packages/react-reconciler/src/ReactFiberCommitWork.js
Attempted import error: 'appendChild' is not exported from './ReactFiberHostConfig'.

找到src/react/packages/react-reconciler/src/ReactFiberHostConfig.js文件,修改如下:

// invariant(false, 'This module must be shimmed by a specific renderer.');export * from "./forks/ReactFiberHostConfig.dom";

接着会看到报错

./src/react/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
Attempted import error: 'unstable_flushAllWithoutAsserting' is not exported from 'scheduler' (imported as 'Scheduler').

找到

文件src/react/packages/scheduler/index.js增加内容

'use strict';

export * from './src/Scheduler';
export {    unstable_flushAllWithoutAsserting,    unstable_flushNumberOfYields,    unstable_flushExpired,    unstable_clearYields,    unstable_flushUntilNextPaint,    unstable_flushAll,    unstable_yieldValue,    unstable_advanceTime  } from './src/SchedulerHostConfig.js';

再在src/react/packages/scheduler/src/SchedulerHostConfig.js修改并增加内容

// throw new Error('This module must be shimmed by a specific build.');export {  unstable_flushAllWithoutAsserting,  unstable_flushNumberOfYields,  unstable_flushExpired,  unstable_clearYields,  unstable_flushUntilNextPaint,  unstable_flushAll,  unstable_yieldValue,  unstable_advanceTime} from "./forks/SchedulerHostConfig.mock.js";export {  requestHostCallback,  requestHostTimeout,  cancelHostTimeout,  shouldYieldToHost,  getCurrentTime,  forceFrameRate,  requestPaint} from "./forks/SchedulerHostConfig.default.js";

然后cli里面不报错了,但是browser里面报错

ReferenceError: __DEV__ is not defined
Module.<anonymous>
src/react/packages/react/src/ReactBaseClasses.js:13

找到config/env.js,在末尾修改

// Stringify all values so we can feed into webpack DefinePlugin
  const stringified = {
    'process.env': Object.keys(raw).reduce((env, key) => {
      env[key] = JSON.stringify(raw[key]);
      return env;
    }, {}),
    __DEV__: false, //true,    __PROFILE__: false, // true,    __UMD__: true,    __EXPERIMENTAL__: true  };

可以看到又报错了,

TypeError: Cannot destructure property 'ReactCurrentDispatcher' of 'shared_ReactSharedInternals__WEBPACK_IMPORTED_MODULE_3__.default' as it is undefined.
Module.<anonymous>
src/react/packages/shared/ReactComponentStackFrame.js:28

修改src/react/packages/shared/ReactSharedInternals.js文件,

import * as React from 'react';
// const ReactSharedInternals =//   React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;import ReactSharedInternals from "../react/src/ReactSharedInternals";export default ReactSharedInternals;

然后又报错惹!

×
Error: Internal React error: invariant() is meant to be replaced at compile time. There is no runtime version.

修改src/react/packages/shared/invariant.js文件

export default function invariant(condition, format, a, b, c, d, e, f) {
  if (condition) return;  throw new Error(
    'Internal React error: invariant() is meant to be replaced at compile ' +
      'time. There is no runtime version.',
  );
}

linter配置修改

首先eslint会报错,说react的包中fbjs的eslint-config不存在,我们让eslint忽略掉src/react就行。

新增.eslintignore

src/react

修改tsconfig.json,排除掉react代码

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react"
  },
  "include": [
    "src"
  ],
  "exclude": [    "src/react"  ]}

总结和最终代码

完成了这些设定之后,我们就可以愉快的在app里玩react的源码啦~

工程的代码在github