1. React目录结构
react宿主环境无关的公用方法react-reconciler协调器的实现,宿主环境无关shared公用辅助方法,宿主环境无关- 各种宿主环境的包
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。主要做如下几件事情:
- react元素的构造函数
- 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验证
进入到打包的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,打印的结果如下