skip to content
静修记

从 0 实现 react18 第一课学习记录

/ 7 min read

pnpm 的 workspace

根目录的包管理

上一章 我们学到了在 pnpm 的 workspace 的根目录安装依赖的命令的是在pnpm add后面加入 -w

子项目的包管理

为指定模块安装外部依赖。

在 workspace 模式下,pnpm 主要通过 —filter 选项过滤子模块

Terminal window
// a 包安装 lodash
pnpm --filter a i -S lodash // 生产依赖
pnpm --filter a i -D lodash // 开发依赖

指定内部模块之间的互相依赖

参考官网的 Workspace 协议说明。我们内部使用 shared时给 react 添加 dependencies

{
"name": "react",
// ...
"dependencies": {
"shared": "workspace:*"
}
}

在实际发布 npm 包时,workspace:* 会被替换成内部模块 b 的对应版本号。替换结果如下所示:

{
"dependencies": {
"a": "workspace:*", // 固定版本依赖,被转换成 x.x.x
"b": "workspace:~", // minor 版本依赖,将被转换成 ~x.x.x
"c": "workspace:^" // major 版本依赖,将被转换成 ^x.x.x
}
}

react 包中jsx 的实现

我们先来看一下现有的 react 和对 jsx 的转换

首先创建一个 react-app

Terminal window
npx create-react-app react-app
cd react-app
npm start

修改 index.js

index.js
import React from "react";
const jsx = (
<div>
hello <span>world</span>
</div>
);
console.log(React);
console.log(jsx);

结果如下图所示

alt text

所以最终我们的 react 导出一个 createElement 方法,jsx 接收两个参数,type 和 config。最终返回一个 ReactElement 包括 type,key,ref,props 等参数

alt text

最终初步实现的代码如下:

jsx.ts
import { REACT_ELMENT_TYPE } from "../../shared/ReactSymbols";
import { Ref, ElementType, Key, Props, ReactElement } from "../../shared/ReactTypes";
const REACTELEMENT = function (type: ElementType, key: Key, ref: Ref, props: Props): ReactElement {
const _element = {
$$typeof: REACT_ELMENT_TYPE,
type,
key,
ref,
props,
__mark: "zippo",
};
return _element;
};
export const jsx = function (type: ElementType, config: any) {
let key: Key = null;
const props: Props = {};
let ref: Ref = null;
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);
};
export const jsxDEV = jsx; //这里我们先保持 dev 环境和 build 环境的一致

rollup 的配置

  1. 根据 官网教程 我们创建一个 react.config.mjs 文件.

  2. 我们需要根据包名获取当前的包路径,以及获取包的 package.json 的数据,我们写在 utils.js

  3. 因为我们的包是通过 ts 写的,所以需要引入 @rollup/plugin-typescript 插件。又因为我们的包里面没有写 module 或者 type 字段,所以还需要引入 @rollup/plugin-commonjs 插件来解析我们的包。rollup 的 plugins 字段应该是跟 webpack 的原理类似,都是在合适的时机解析包内容。

  4. ES module 中不能直接使用 __dirname,因为 ESM 设计上是静态的,在模块编译时确定就已经确定模块的依赖关系和输入输出变量,而 __dirname__filename 提供的是运行时的路径信息,这与 ESM 的设计理念不符。所以需要使用 import.meta.url来确定当前目录

utils.js
import { fileURLToPath } from "url";
import path from "path";
import fs from "fs";
import ts from "@rollup/plugin-typescript";
import cjs from "@rollup/plugin-commonjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
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 getPkgJson(pkgName) {
// 包路径
const pkgPath = `${resolvePkgPath(pkgName)}/package.json`;
const str = fs.readFileSync(pkgPath, "utf-8");
return JSON.parse(str);
}
export function getBaseRollupPlugins({ typescript = {} } = {}) {
return [cjs(), ts(typescript)];
}

最终实现的 react.config.mjs 代码如下

react.config.mjs
import { getBaseRollupPlugins, getPkgJson, resolvePkgPath } from "./utils.js";
import generatePackageJson from "rollup-plugin-generate-package-json";
const { name, main: module } = getPkgJson("react");
// react 包路径
const pkgPath = resolvePkgPath(name);
// dist 包路径
const pkgDistPath = resolvePkgPath(name, true);
export default [
// react
{
input: `${pkgPath}/${module}`,
output: {
file: `${pkgDistPath}/index.js`,
name: "React",
format: "umd",
},
plugins: [
...getBaseRollupPlugins(),
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(),
},
];

package.json 中添加命令后执行,生成 dist 文件夹

"scripts": {
"build:dev": "rm -rf dist && rollup --config scripts/rollup/react.config.mjs"
}, //注意 windows 平台不能直接使用 rm -rf。推荐 rimraf 跨平台

本地 react 项目验证我们的包

首先想到使用 npm 包本地调试的方法 npm linknpm link 是一个 npm 命令,用于在本地开发 npm 包时,将本地的包目录链接到全局 node_modules 目录,或者将全局包链接到项目的 node_modules 目录。这样,你可以在不发布到 npm registry 的情况下,测试本地的包。

本地包链接到全局

当你正在开发一个本地包,并想要在其他项目中测试它时,可以使用以下步骤:

  1. 在本地包的根目录下运行 npm link。这会在全局 node_modules 目录中创建一个符号链接,指向你的本地包。
  2. 在你的项目目录中,运行 npm link package-name,其中 package-name 是你的本地包的名称。这会在项目的 node_modules 目录中创建一个符号链接,指向全局 node_modules 中的包。 这样,你对本地包所做的任何更改都会立即反映到使用该包的项目中,无需每次都重新发布和安装。

取消链接

当你完成测试,想要取消链接时,可以执行以下操作:

  1. 在项目目录中,运行 npm unlink package-name 来取消项目对本地包的链接。
  2. 在本地包的目录中,运行 npm unlink 来取消全局链接。

但是这里我们使用 pnpm link。看下官方教程。我们可以直接在本地项目替换react 包,执行命令

Terminal window
pnpm link ../my-react/dist/node_modules/react

重启服务npm start。查看结果 alt text 取消 link 并不生效 查看 issue,通过下面方法重新加载官方 react 包

pnpm remove react
pnpm add react