We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
我们先了解一下React 源码的项目结构,React 使用的是 Mono-repo 的结构管理各个包,源码中主要包括如下部分:
其中,主要的包在 packages 目录下,主要包含以下模块:
我们先来实现 react 包中的 createElement 和 jsx 方法,并实现 react 包的打包流程。
createElement
jsx
在 React 中使用 JSX 语法描述用户界面,JSX 语法就是一种语法糖,是 一种 JavaScript 语法扩展,它允许开发者在 JavaScript 代码中直接编写类似 HTML 的代码,并在运行时将其转换为 React 元素。
JSX 转换就是将 JSX 源代码变成浏览器可以理解的 JavaScript 代码的过程,以下面的代码为例:
// JSX 源代码 import React from 'react'; function App() { return <h1>Hello World</h1>; } // React 17之前,JSX 转换结果 import React from 'react'; function App() { return React.createElement('div', null, 'Hello world!'); } // React 17之后,JSX 转换结果 import { jsx as _jsx } from 'react/jsx-runtime'; function App() { return _jsx('div', { children: 'Hello world!' }); }
JSX 转换的过程大致分为两步:
React.createElement
因此,我们只需要实现运行时的部分即可,即 jsx 方法和 React.createElement 方法,包括 dev 和 prod 两个环境。
我们先在 packages 文件夹下新建 react 文件夹,进入到这个文件夹下,执行 pnpm init:
packages
react
pnpm init
cd packages/react pnpm init
初始化的 package.json文件如下所示:
package.json
// packages/react/package.json { "name": "react", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }
其中 main 字段代表了 react 包的入口文件,main 对应的是 CommonJS 规范,由于我们的项目是使用 rollup 打包的,rollup 是原生支持 esModule 的,esModule 规范中对应 main 的字段为 module,所以我们将入口改为:"module": "index.ts";然后,删除 scripts 字段,在 description 字段中增加包描述,dependencies 字段指明了包的依赖,此时的 package.json 文件如下所示:
main
module
"module": "index.ts"
scripts
description
dependencies
// packages/react/package.json { "name": "react", "version": "1.0.0", "description": "react common functions", "module": "index.ts", "dependencies": { "shared": "workspace:*" }, "keywords": [], "author": "", "license": "ISC" }
在 react 包下新建一个 src 目录,在 src 目录下新建一个 jsx.ts 文件。
执行 jsx 方法和 React.createElement 方法的返回结果是一种被称为 ReactElement 的数据结构,所以我们首先要定义一下 ReactElement 的构造函数:
ReactElement
// packages/react/src/jsx.ts import { REACT_ELEMENT_TYPE } from 'shared/ReactSymbols'; import { Type, Ref, Key, Props, ReactElementType, ElementType } from 'shared/ReactTypes'; const ReactElement = function ( type: Type, key: Key, ref: Ref, props: Props ): ReactElementType { const element = { $$typeof: REACT_ELEMENT_TYPE, type, key, ref, props, __mark: 'erxiao' }; return element; };
其中 $$typeof 是一个内部使用的字段,通过这个字段来指明当前这个数据结构是一个ReactElement,_mark 字段是为了与官方 react 包做区分的一个自定义字段。
$$typeof
_mark
我们将所有的类型定义和公共方法都放在一个公用的 shared 包中。在 packages 文件夹下新建 shared 文件夹,进入到这个文件夹下,执行 pnpm init:
shared
cd packages/shared pnpm init
shared 包不需要入口文件,因为它里面的所有方法都会直接在其他包里面被引用,代码如下:
// packages/shared/package.json { "name": "shared", "version": "1.0.0", "description": "shared hepler functions and symbols", "keywords": [], "author": "", "license": "ISC" }
// packages/shared/ReactSymbols.ts const supportSymbol = typeof Symbol === 'function' && Symbol.for; // 表示普通的 React 元素,即通过 JSX 创建的组件或 DOM 元素 export const REACT_ELEMENT_TYPE = supportSymbol ? Symbol.for('react.element') : 0xeac7;
// packages/shared/ReactTypes.ts export type Type = any; export type Key = any; export type Props = any; export type Ref = any; export type ElementType = any; export interface ReactElementType { $$typeof: symbol | number; key: Key; props: Props; ref: Ref; type: ElementType; __mark: string; }
接着我们来实现 jsx 方法.
import { jsx as _jsx } from 'react/jsx-runtime'; function App() { return _jsx('div', { children: 'Hello world!' }); }
从以上示例可以看出, jsx 方法接收两个参数,第一个参数 type 为组件的 type,第二个参数是其他配置,可能有第三个参数为组件的 children,返回一个 ReactElement 数据结构。
type
children
// packages/react/src/jsx.ts // ...之前的代码 export const jsx = (type: ElementType, config: any, ...children: 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 childrenLength = children.length; if (childrenLength) { if (childrenLength === 1) { props.children = children[0]; } else { props.children = children; } } return ReactElement(type, key, ref, props); };
这就是完整的 jsx 方法的实现。
为了区分生产环境和开发环境,这里再定义一个 jsxDEV 方法,唯一的区别是,开发环境不处理 children 参数,方便多做一些额外的检查:
jsxDEV
// packages/react/src/jsx.ts // ...之前的代码 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); };
新增 index.ts 文件,这个文件是 react 包的入口,导出一个对象,包含版本号 version 和 React.createElement 方法。其中,React.createElement 方法就是刚才实现的 jsx 方法。
index.ts
version
// packages/react/index.ts import { jsx } from './src/jsx'; export default { version: '1.0.0', createElement: jsx };
至此,我们已经实现了 jsx 方法和 React.createElement 方法,并支持了 dev 和 prod 两个环境,接下来实现打包流程。
dev
prod
根据上面的示例,我们实现了 jsx 方法、jsxDEV 方法和 React.createElement 方法,需要将打包到对应的文件中:
我们的打包脚本都在 scripts/rollup 目录下,新增一个 react.config.js 文件,里面是 react 包的打包配置,再新增一个 utils.js 文件,里面是一些公用的方法。
scripts/rollup
react.config.js
utils.js
需要先安装几个包:
pnpm i -D -w rollup-plugin-generate-package-json pnpm i -D -w rollup-plugin-typescript2 pnpm i -D -w @rollup/plugin-commonjs pnpm i -D -w rimraf
rollup-plugin-generate-package-json
rollup-plugin-typescript2
@rollup/plugin-commonjs
rimraf
react.config.js 导出一个数组,数组中的第一个对象即为 react/index.js 的配置,定义一下输入文件和输出文件,然后配置插件和 package.json;数组中的第二个对象为 react/jsx-runtime.js 和 react/jsx-dev-runtime.js 的配置。
react/index.js
react/jsx-runtime.js
react/jsx-dev-runtime.js
// scripts/rollup/react.config.js import { getPackageJSON, resolvePkgPath, getBaseRollupPlugins } from './utils'; import generatePackageJson from 'rollup-plugin-generate-package-json'; const { name, module } = getPackageJSON('react'); const pkgPath = resolvePkgPath(name); const pkgDistPath = resolvePkgPath(name, true); export default [ // react { input: `${pkgPath}/${module}`, output: { file: `${pkgDistPath}/index.js`, name: 'React', format: 'umd' }, plugins: [ ...getBaseRollupPlugins(), // 生成 package.json 文件 generatePackageJson({ inputFolder: pkgPath, outputFolder: pkgDistPath, baseContents: ({ name, description, version }) => ({ name, description, version, main: 'index.js' }) }) ] }, // jsx-runtime { input: `${pkgPath}/src/jsx.ts`, output: [ // jsx-runtime { file: `${pkgDistPath}/jsx-runtime.js`, name: 'jsx-runtime', format: 'umd' }, // jsx-dev-runtime { file: `${pkgDistPath}/jsx-dev-runtime.js`, name: 'jsx-dev-runtime', format: 'umd' } ], plugins: getBaseRollupPlugins() } ];
// scripts/rollup/utils.js import path from 'path'; import fs from 'fs'; import ts from 'rollup-plugin-typescript2'; import cjs from '@rollup/plugin-commonjs'; const pkgPath = path.resolve(__dirname, '../../packages'); const distPath = path.resolve(__dirname, '../../dist/node_modules'); export function resolvePkgPath(pkgName, isDist) { if (isDist) { return `${distPath}/${pkgName}`; } return `${pkgPath}/${pkgName}`; } export function getPackageJSON(pkgName) { const path = `${resolvePkgPath(pkgName)}/package.json`; const str = fs.readFileSync(path, { encoding: 'utf-8' }); return JSON.parse(str); } export function getBaseRollupPlugins({ typescript = {} } = {}) { return [cjs(), ts(typescript)]; }
现在我们到根目录下的 package.json 文件中新增一个 scripts 命令:
// package.json // ... "scripts": { "lint": "eslint --ext .ts,.jsx,.tsx --fix --quiet ./packages", "build-dev": "rimraf dist && rollup --config scripts/rollup/react.config.js --bundleConfigAsCjs" }, // ...
运行 npm run build-dev,可以看到,根目录下的 dist/node_modules/react 文件夹中出现了 react 包的打包产物:
npm run build-dev
dist/node_modules/react
react 包打包完之后,我们可以使用 npm link 来调试以下打包结果,流程如下图所示:
npm link
首先我们在 my-react 项目中,生成了一个 react 包的打包产物,即 dist/node_modules/react 文件夹中的内容。
然后进入到 dist/node_modules/react目录下,通过 pnpm link --global 命令,就将全局 node_modules 下的 react 指向了我们刚刚生成的 react 包。
pnpm link --global
node_modules
接着,用 create-react-app 创建一个新的 Demo 项目,在这个 Demo 项目中,再执行 pnpm link react --global 命令,就能将 Demo 项目中依赖的 react 从这个项目的node_modules/react 变成全局 node_modules 下的 react。
create-react-app
pnpm link react --global
node_modules/react
这样我们就能通过 Demo 项目直接调用我们刚刚生成的 react 包了。
这种方式的优点是:可以模拟实际项目引用 React 的情况;缺点是:不支持热更新,每次更新 my-react 项目之后都需要重新打包,并在 Demo 项目中重新执行 npm run dev,比较繁琐。
npm run dev
如果想支持热更新调试,可以使用 Vite。
在根目录运行 pnpm create vite demos --template react,语言选择 Typescript,然后新建文件夹 demos/test-1,将 index.html 和 main.tsx 挪进去,删除其余的文件。
pnpm create vite demos --template react
Typescript
demos/test-1
index.html
main.tsx
在 package.json 中增加指令 npm run demo: "vite serve demos/test-1 --config scripts/vite/vite.config.js --force",并安装以下依赖:
npm run demo
"vite serve demos/test-1 --config scripts/vite/vite.config.js --force"
"@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", "@vitejs/plugin-react": "^4.2.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "vite": "^5.1.4"
新建 scripts/vite/vite.config.js 文件:
scripts/vite/vite.config.js
import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import replace from '@rollup/plugin-replace'; import { resolvePkgPath } from '../rollup/utils'; import path from 'path'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), replace({ __DEV__: true, preventAssignment: true })], resolve: { alias: [ { find: 'react', replacement: resolvePkgPath('react') }, { find: 'react-dom', replacement: resolvePkgPath('react-dom') }, { find: 'hostConfig', replacement: path.resolve( resolvePkgPath('react-dom'), './src/hostConfig.ts' ) } ] } });
最后,新增 packages/react/jsx-dev-runtime.ts 文件:
packages/react/jsx-dev-runtime.ts
export { jsxDEV } from './src/jsx';
这样只需执行 npm run demo 即可实时调试代码,实现热更新。
至此,我们就完成了 JSX 方法的开发、打包、调试。
相关代码可在 git tag v1.2 查看,地址:https://github.com/2xiao/my-react/tree/v1.2
git tag v1.2
The text was updated successfully, but these errors were encountered:
2xiao
No branches or pull requests
1. 源码目录结构
我们先了解一下React 源码的项目结构,React 使用的是 Mono-repo 的结构管理各个包,源码中主要包括如下部分:
其中,主要的包在 packages 目录下,主要包含以下模块:
我们先来实现 react 包中的
createElement
和jsx
方法,并实现 react 包的打包流程。2. 实现 JSX 方法
在 React 中使用 JSX 语法描述用户界面,JSX 语法就是一种语法糖,是 一种 JavaScript 语法扩展,它允许开发者在 JavaScript 代码中直接编写类似 HTML 的代码,并在运行时将其转换为 React 元素。
JSX 转换就是将 JSX 源代码变成浏览器可以理解的 JavaScript 代码的过程,以下面的代码为例:
JSX 转换的过程大致分为两步:
jsx
方法 和React.createElement
方法;因此,我们只需要实现运行时的部分即可,即
jsx
方法和React.createElement
方法,包括 dev 和 prod 两个环境。我们先在
packages
文件夹下新建react
文件夹,进入到这个文件夹下,执行pnpm init
:cd packages/react pnpm init
初始化的
package.json
文件如下所示:其中
main
字段代表了 react 包的入口文件,main
对应的是 CommonJS 规范,由于我们的项目是使用 rollup 打包的,rollup 是原生支持 esModule 的,esModule 规范中对应main
的字段为module
,所以我们将入口改为:"module": "index.ts"
;然后,删除scripts
字段,在description
字段中增加包描述,dependencies
字段指明了包的依赖,此时的package.json
文件如下所示:在 react 包下新建一个 src 目录,在 src 目录下新建一个 jsx.ts 文件。
执行
jsx
方法和React.createElement
方法的返回结果是一种被称为ReactElement
的数据结构,所以我们首先要定义一下ReactElement
的构造函数:其中
$$typeof
是一个内部使用的字段,通过这个字段来指明当前这个数据结构是一个ReactElement
,_mark
字段是为了与官方 react 包做区分的一个自定义字段。我们将所有的类型定义和公共方法都放在一个公用的
shared
包中。在packages
文件夹下新建shared
文件夹,进入到这个文件夹下,执行pnpm init
:cd packages/shared pnpm init
shared
包不需要入口文件,因为它里面的所有方法都会直接在其他包里面被引用,代码如下:接着我们来实现
jsx
方法.从以上示例可以看出,
jsx
方法接收两个参数,第一个参数type
为组件的 type,第二个参数是其他配置,可能有第三个参数为组件的children
,返回一个ReactElement
数据结构。这就是完整的
jsx
方法的实现。为了区分生产环境和开发环境,这里再定义一个
jsxDEV
方法,唯一的区别是,开发环境不处理children
参数,方便多做一些额外的检查:新增
index.ts
文件,这个文件是 react 包的入口,导出一个对象,包含版本号version
和React.createElement
方法。其中,React.createElement
方法就是刚才实现的jsx
方法。至此,我们已经实现了
jsx
方法和React.createElement
方法,并支持了dev
和prod
两个环境,接下来实现打包流程。3. 实现打包流程
根据上面的示例,我们实现了
jsx
方法、jsxDEV
方法和React.createElement
方法,需要将打包到对应的文件中:我们的打包脚本都在
scripts/rollup
目录下,新增一个react.config.js
文件,里面是 react 包的打包配置,再新增一个utils.js
文件,里面是一些公用的方法。需要先安装几个包:
rollup-plugin-generate-package-json
:用于生成package.json
文件。rollup-plugin-typescript2
:用于编译 Typescript。@rollup/plugin-commonjs
:用于将 CommonJS 模块转换为 ES 模块,以便在 Rollup 中进行打包。CommonJS 是一种用于在浏览器之外执行 JavaScript 代码的模块规范,而 Rollup 默认只支持 ES 模块。rimraf
:用于删除之前的打包产物react.config.js
导出一个数组,数组中的第一个对象即为react/index.js
的配置,定义一下输入文件和输出文件,然后配置插件和package.json
;数组中的第二个对象为react/jsx-runtime.js
和react/jsx-dev-runtime.js
的配置。现在我们到根目录下的
package.json
文件中新增一个scripts
命令:运行
npm run build-dev
,可以看到,根目录下的dist/node_modules/react
文件夹中出现了 react 包的打包产物:4. 调试打包结果
1. npm link
react 包打包完之后,我们可以使用
npm link
来调试以下打包结果,流程如下图所示:首先我们在 my-react 项目中,生成了一个 react 包的打包产物,即
dist/node_modules/react
文件夹中的内容。然后进入到
dist/node_modules/react
目录下,通过pnpm link --global
命令,就将全局node_modules
下的react
指向了我们刚刚生成的react
包。接着,用
create-react-app
创建一个新的 Demo 项目,在这个 Demo 项目中,再执行pnpm link react --global
命令,就能将 Demo 项目中依赖的react
从这个项目的node_modules/react
变成全局node_modules
下的react
。这样我们就能通过 Demo 项目直接调用我们刚刚生成的
react
包了。这种方式的优点是:可以模拟实际项目引用 React 的情况;缺点是:不支持热更新,每次更新 my-react 项目之后都需要重新打包,并在 Demo 项目中重新执行
npm run dev
,比较繁琐。2. vite
如果想支持热更新调试,可以使用 Vite。
在根目录运行
pnpm create vite demos --template react
,语言选择Typescript
,然后新建文件夹demos/test-1
,将index.html
和main.tsx
挪进去,删除其余的文件。在
package.json
中增加指令npm run demo
:"vite serve demos/test-1 --config scripts/vite/vite.config.js --force"
,并安装以下依赖:新建
scripts/vite/vite.config.js
文件:最后,新增
packages/react/jsx-dev-runtime.ts
文件:这样只需执行
npm run demo
即可实时调试代码,实现热更新。至此,我们就完成了 JSX 方法的开发、打包、调试。
相关代码可在
git tag v1.2
查看,地址:https://github.com/2xiao/my-react/tree/v1.2The text was updated successfully, but these errors were encountered: