2. JSX转换

140 阅读3分钟

1. React目录结构

  1. react宿主环境无关的公用方法
  2. react-reconciler协调器的实现,宿主环境无关
  3. shared公用辅助方法,宿主环境无关
  4. 各种宿主环境的包

2. 初始化react

在packages目录下新增react文件夹,并执行pnpm init,生成的package.json中的入口换成module,因为rollup原生支持的es module入口是module

{
  "name": "react",
  "version": "1.0.0",
  "description": "react公用方法",
  "module": "index.ts",
  "keywords": [],
  "author": "",
  "license": "ISC"
}

在react文件夹下新增index.ts,导出一个空对象

export {};

3. jsx转换是什么

使用[Babel转换器](Babel · 下一代 JavaScript 的编译器 --- Babel · The compiler for next generation JavaScript (babeljs.io))可以看到dom树会被编译成运行函数,在react17前是classic即createElement,在react17后是Automatic即jsx,如下:

<div>111</div>

/*#__PURE__*/React.createElement("div", null, "111");

import { jsx as _jsx } from "react/jsx-runtime";
/*#__PURE__*/_jsx("div", {
  children: "111"
});

jsx的转换包含编译时和运行时,编译时babel就已经完成了,需要自定义实现jsx以及createElement方法,以及方法的打包流程和调试打包结果的环境

4. jsx方法的实现

在react文件夹下创建src目录,在src目录下创建jsx.ts。主要做如下几件事情:

  1. react元素的构造函数
  2. jsx函数
import { REACT_ELEMENT_TYPE } from 'shared/ReactSymbols';
import {
  ElementType,
  Key,
  Ref,
  Props,
  ReactElementType,
} from 'shared/ReactTypes';

// ReactElement构造函数
const ReactElement = function (
  type: ElementType,
  key: Key,
  ref: Ref,
  props: Props
): ReactElementType {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE, // 指明当前数据结构是一个reactElement
    type,
    key,
    ref,
    props,
    __mark: 'YH', // 用于和真正的reactElement的区分
  };

  return element;
};

// jsx方法
export const jsx = (type: ElementType, config: any, ...maybeChldren: any) => {
  let key: Key = null;
  let ref: Ref = null;
  const props: Props = {};

  for (const prop in config) {
    const val = config[prop];

    if (prop === 'key') {
      if (val !== undefined) {
        key = '' + val;
      }
      continue;
    }

    if (prop === 'ref') {
      if (val !== undefined) {
        ref = val;
      }
      continue;
    }

    if ({}.hasOwnProperty.call(config, prop)) {
      props[prop] = val;
    }
  }

  const maybeChldrenLength = maybeChldren.length;

  if (maybeChldrenLength) {
    if (maybeChldrenLength === 1) {
      props.children = maybeChldren[0];
    } else {
      props.children = maybeChldren;
    }
  }

  return ReactElement(type, key, ref, props);
};

export const jsxDEV = (type: ElementType, config: any) => {
  let key: Key = null;
  let ref: Ref = null;
  const props: Props = {};

  for (const prop in config) {
    const val = config[prop];

    if (prop === 'key') {
      if (val !== undefined) {
        key = '' + val;
      }
      continue;
    }

    if (prop === 'ref') {
      if (val !== undefined) {
        ref = val;
      }
      continue;
    }

    if ({}.hasOwnProperty.call(config, prop)) {
      props[prop] = val;
    }
  }

  return ReactElement(type, key, ref, props);
};

在packages目录下新增shared文件夹,并执行pnpm init,上述jsx.ts中引用的变量声明与宿主环境无关,都放在这个包中。由于shared包的工具都是被外部引用,所以不需要入口引用,对应的package.json内容如下

{
  "name": "shared",
  "version": "1.0.0",
  "description": "所有公用辅助方法及类型定义",
  "keywords": [],
  "author": "",
  "license": "ISC"
}

此外,react包的package.json中也需要新增shared包的依赖声明

{
  "name": "react",
  "version": "1.0.0",
  "description": "react公用方法",
  "module": "index.ts",
  "dependencies": {
    "shared": "workspace:*"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

在shared目录下新增ReactSymbols.ts,内容如下:

const supportSymbol = typeof Symbol === 'function' && Symbol.for;

export const REACT_ELEMENT_TYPE = supportSymbol
  ? Symbol.for('react.element')
  : 0xeac7;

在shared目录下新增ReactTypes.ts,内容如下:

export type Key = any;
export type Ref = any;
export type Props = any;
export type ElementType = any;

export interface ReactElementType {
  $$typeof: symbol | number;
  type: ElementType;
  key: Key;
  props: Props;
  ref: Ref;
  __mark: string;
}

react包的打包入口index.ts的内容修改如下:

import { jsxDEV } from './src/jsx';

export default {
  version: '0.0.0',
  createElement: jsxDEV,
};
 

5. jsx打包

主要目的是将上述声明的React.createElement方法、jsx方法、jsxDev方法打包,打包结果放在dist目录下

rollup下新建utils文件,添加获取包路径和打包产物路径的工具函数

import path from 'path';
import fs from 'fs';

// 源码包的路径
const pkgPath = path.resolve(__dirname, '../../packages');

// 打包产物的路径
const distPath = path.resolve(__dirname, '../../dist/node_modules');

// 解析包的路径(源码包的路径/打包产物的路径)
export function resolvePackagePath(pkgName, isDist) {
  if (isDist) {
    return `${distPath}/${pkgName}`;
  }
  return `${pkgPath}/${pkgName}`;
}

// 获取指定包的packages.json对象
export function getPackageJson(pkgName) {
  const path = `${resolvePackagePath(pkgName)}/package.json`;
  const str = fs.readFileSync(path, { encoding: 'utf-8' });

  return JSON.parse(str);
}

rollup下新建rollup的打包配置文件react.config.js,内容如下:

import { getPackageJson, resolvePackagePath } from './utils';

// react包名 + 入口文件
const { name, module } = getPackageJson('react');
// react包路径
const pkgPath = resolvePackagePath(name);
// react打包产物路径
const pkgDistPath = resolvePackagePath(name, true);

export default [
  {
    input: `${pkgPath}/${module}`,
    output: {
      file: `${pkgDistPath}/index.js`,
      name: 'index.js',
      format: 'umd', // 兼容esmodule和commonJs规范
    },
    plugins: [],
  },
  {
    input: `${pkgPath}/src/jsx.ts`,
    output: [
      {
        file: `${pkgDistPath}/jsx-runtime.js`,
        name: 'jsx-runtime.js',
        format: 'umd',
      },
      {
        file: `${pkgDistPath}/jsx-dev-runtime.js`,
        name: 'jsx-dev-runtime.js',
        format: 'umd',
      },
    ],
    plugins: [],
  },
];

在项目根目录下安装rollup的公共plugin

pnpm install -D -w rollup-plugin-typescript2 // 将ts代码转义成js
pnpm install -D -w @rollup/plugin-commonjs // 解析commonJs规范

在rollup的utils文件中,添加rollup的公共打包plugin获取函数

import ts from 'rollup-plugin-typescript2';
import cjs from '@rollup/plugin-commonjs';

// 获取公用的rollup的plugin配置
export function getBaseRollupPlugins({ tsConfig = {} } = {}) {
  return [cjs(), ts(tsConfig)];
}

在打包配置文件react.config.js中使用公共打包plugin获取函数

import {
  getBaseRollupPlugins,
} from './utils';

export default [
  {
    plugins: [...getBaseRollupPlugins()],
  },
];

在根目录的packages.json中新增打包命令,即指定rollup打包时的配置文件(bundleConfigAsCjs表示将配置文件处理为commonjs,因为目前自定义的配置文件是esModule,rollup是不接受的)

"scripts": {
  "build:dev": "rollup --bundleConfigAsCjs --config scripts/rollup/react.config.js"
},

安装模块,在每次打包的时候先将打包产物给删除掉(为了兼容windows,mac可以直接使用rm -rf命令)

pnpm install -D -w rimraf

打包命令修改为

"scripts": {
  "build:dev": "rimraf dist && rollup --bundleConfigAsCjs --config scripts/rollup/react.config.js"
},

执行pmpm build:dev

注意到,打包产物中没有package.json,需要安装包

pnpm install -D -w rollup-plugin-generate-package-json

并修改react.config.js

import {
  getPackageJson,
  resolvePackagePath,
  getBaseRollupPlugins,
} from './utils';

import generatePackageJson from 'rollup-plugin-generate-package-json';

// react包名 + 入口文件
const { name, module } = getPackageJson('react');
// react包路径
const pkgPath = resolvePackagePath(name);
// react打包产物路径
const pkgDistPath = resolvePackagePath(name, true);

export default [
  {
    input: `${pkgPath}/${module}`,
    output: {
      file: `${pkgDistPath}/index.js`,
      name: 'index.js',
      format: 'umd',
    },
    plugins: [
      ...getBaseRollupPlugins(),
      // 将React包下package.json中的字段,写入到打包后的package.json中
      generatePackageJson({
        inputFolder: pkgPath,
        outputFolder: pkgDistPath,
        baseContents: ({ name, description, version }) => ({
          name,
          description,
          version,
          main: 'index.js',
        }),
      }),
    ],
  },
  {
    input: `${pkgPath}/src/jsx.ts`,
    output: [
      {
        file: `${pkgDistPath}/jsx-runtime.js`,
        name: 'jsx-runtime.js',
        format: 'umd',
      },
      {
        file: `${pkgDistPath}/jsx-dev-runtime.js`,
        name: 'jsx-dev-runtime.js',
        format: 'umd',
      },
    ],
    plugins: [...getBaseRollupPlugins()],
  },
];

6. jsx验证

pnpm link | pnpm

image.png

进入到打包的react目录下,执行pnpm link --global命令,此时会将全局node_modules下的react包指向生成的react包中

cd dist/node_modules/react
pnpm link --global

之后,使用create-react-app创建一个react的demo项目,在demo项目中再执行pnpm link react --global,此时就能将demo项目中以来的react指向全局node_modules下的react

npx create-react-app my-react-demo
cd my-react-demo
pnpm link react --global

此时由于还没有实现react-dom包,所以需要修改一些demo项目入口文件的内容,如下:

import React from "react";

const jsx = (
  <div>
    hello <span>my-react</span>
  </div>
);

console.log(React);
console.log(jsx);

执行npm run start,打印的结果如下

image.png

7. git地址

Y_oneP/React源码学习 - Gitee.com