Skip to main content

借鉴react-leaflet搭建monorepo组件库

· 14 min read

搭建项目

  1. npm init -y 生成package.json,删除其中的"main": "index.js",设置"name": "react-map-repository"避免和子包name冲突,设置 "packageManager": "pnpm@9.0.6"避免使用turbo时报错missing packageManager field in package.json
  2. 创建pnpm-workspace.yaml
    pnpm-workspace.yaml
    packages:
    - 'packages/*'
  3. 创建packages目录,创建packages/react-map目录,创建packages/core目录
  4. pnpm add -D typescript -w
  5. 创建tsconfig.jsontsconfig.build.json
    tsconfig.json
    {
    "extends": "./tsconfig.build.json",
    "compilerOptions": {
    "baseUrl": "./packages",
    "paths": {
    "@react-map/core": ["core/src"],
    "react-map": ["react-map/src"]
    }
    }
    }
    tsconfig.build.json
    {
    "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "declaration": true,
    "esModuleInterop": true,
    "jsx": "react",
    "lib": ["dom", "es2020"],
    "moduleResolution": "node",
    "strict": true,
    "target": "es2020"
    },
    "include": ["./src/**/*"]
    }
  6. pnpm add -D react react-dom -w
  7. pnpm add -D leaflet @types/leaflet -w
  8. pnpm add -D turbo -w
  9. 创建turbo.json
    turbo.json
    {
    "tasks": {
    "build:clean": {},
    "build:js": {}
    }
    }
    注意使用tasks而不是pipeline
      × found `pipeline` field instead of `tasks`
    ╭─[turbo.json:1:1]
    1 │ {
    2 │ ╭─▶ "pipeline": {
    3 │ │ "build:clean": {},
    4 │ │ "build:js": {}
    5 │ ├─▶ }
    · ╰──── rename `pipeline` field to `tasks`
    6 │ }
    ╰────
    help: changed in 2.0: `pipeline` has been renamed to `tasks`
  10. pnpm add -D @swc/cli @swc/core -w
  11. 创建.swcrc
    .swcrc
    {
    "$schema": "https://swc.rs/schema.json",
    "jsc": {
    "parser": {
    "syntax": "typescript",
    "tsx": true
    },
    "target": "es2020"
    }
    }
  12. pnpm add --save-dev @biomejs/biome -w
  13. 创建biome.json
    biome.json
    {
    "$schema": "https://biomejs.dev/schemas/1.7.1/schema.json",
    "organizeImports": {
    "enabled": true
    },
    "formatter": {
    "enabled": true,
    "formatWithErrors": false,
    "ignore": [],
    "attributePosition": "auto",
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 80
    },
    "javascript": {
    "formatter": {
    "arrowParentheses": "always",
    "bracketSameLine": true,
    "bracketSpacing": true,
    "jsxQuoteStyle": "double",
    "quoteProperties": "asNeeded",
    "quoteStyle": "single",
    "semicolons": "asNeeded",
    "trailingComma": "all"
    }
    },
    "linter": {
    "enabled": true,
    "ignore": ["lib/**", "__tests__/**"],
    "rules": {
    "recommended": true
    }
    }
    }
  14. 创建.gitignore
    node_modules
    .turbo
  15. 创建README.md
  16. pnpm add -D del-cli -w
  17. pnpm add -D cross-env -w
  18. 更新项目根目录package.jsonscripts
    package.json
    "scripts": {
    "lint": "biome check --apply ./packages",
    "test": "jest",
    "build": "turbo run build:clean && pnpm run -r build:types && turbo run build:js"
    },
  19. cd packages/core/然后npm init -y 生成package.json,设置name main types exports files sideEffects
    packages/core/package.json
    {
    "name": "@react-map/core",
    "main": "lib/index.js",
    "types": "lib/index.d.ts",
    "exports": {
    ".": "./lib/index.js"
    },
    "files": ["lib/*"],
    "sideEffects": false,
    }
  20. packages/core/中安装依赖:pnpm add -D @types/react @types/react-dom
  21. 手动设置peerDependencies
    packages/core/package.json
    "peerDependencies": {
    "leaflet": "^1.9.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
    },
  22. 创建packages/core/tsconfig.json
    packages/core/tsconfig.json
    {
    "extends": "../../tsconfig.build.json",
    "compilerOptions": {
    "outDir": "./lib"
    },
    "include": ["./src/**/*"]
    }
  23. 创建packages/core/srcpackages/core/src/index.tspackages/core/README.mdpackages/core/.gitignore
    packages/core/.gitignore
    /coverage
    /lib
  24. 更新packages/core/package.jsonscripts
    packages/core/package.json
    "scripts": {
    "build:clean": "del lib",
    "build:js": "swc src -d ./lib --config-file ../../.swcrc --strip-leading-paths",
    "build:types": "tsc --emitDeclarationOnly",
    "build": "pnpm run build:clean && pnpm run build:types && pnpm run build:js",
    "test:types": "tsc --noEmit",
    "test:unit": "cross-env NODE_ENV=test jest",
    "test": "pnpm run test:types && pnpm run test:unit",
    "start": "pnpm run test && pnpm run build",
    "prepare": "pnpm run build"
    },
  25. cd ../react-map/然后npm init -y 生成package.json,设置main types exports files sideEffects
    packages/react-map/package.json
    {
    "main": "lib/index.js",
    "types": "lib/index.d.ts",
    "exports": {
    ".": "./lib/index.js",
    "./*": "./lib/*.js"
    },
    "files": ["lib/*"],
    "sideEffects": false,
    }
  26. 创建packages/react-map/tsconfig.json
    packages/react-map/tsconfig.json
    {
    "extends": "../../tsconfig.build.json",
    "compilerOptions": {
    "outDir": "./lib"
    },
    "include": ["./src/**/*"]
    }
  27. packages/react-map/中安装依赖:pnpm add -D @types/leaflet @types/react @types/react-dom; pnpm add @react-map/core
    packages/react-map/package.json
    {
    "dependencies": {
    "@react-map/core": "workspace:^"
    }
    }
  28. 手动设置peerDependencies
    packages/react-map/package.json
    {
    "peerDependencies": {
    "leaflet": "^1.9.0",
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
    },
    }
  29. 创建packages/react-map/srcpackages/react-map/src/index.tspackages/react-map/README.mdpackages/react-map/.gitignore
    packages/react-map/.gitignore
    /coverage
    /lib
  30. 更新packages/react-map/package.jsonscripts
    packages/react-map/package.json
    {
    "scripts": {
    "build:clean": "del lib",
    "build:js": "swc src -d ./lib --config-file ../../.swcrc --strip-leading-paths",
    "build:types": "tsc --emitDeclarationOnly",
    "build": "pnpm run build:clean && pnpm run build:types && pnpm run build:js",
    "test:types": "tsc --noEmit",
    "test:unit": "cross-env NODE_ENV=test jest",
    "test": "pnpm run test:types && pnpm run test:unit",
    "start": "pnpm run test && pnpm run build",
    "prepare": "pnpm run build"
    },
    }

版本管理

使用Changesets:

  1. 在项目根目录下安装依赖:pnpm add -D @changesets/cli -w

  2. 初始化 Changesets 配置: pnpm [exec] changeset init,这将创建一个 .changeset 目录和配置文件.changeset/config.json

    .changeset/config.json
    {
    "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
    "changelog": "@changesets/cli/changelog", // changelog 生成方式
    "commit": false, // 不要让 changeset 在 publish 的时候帮我们做 git add
    "fixed": [],
    "linked": [], // 配置哪些包要共享版本
    "access": "restricted", // 公私有安全设定,内网建议 restricted ,开源使用 public
    "baseBranch": "main", // 项目主分支
    "updateInternalDependencies": "patch", // 确保某包依赖的包发生 upgrade,该包也要发生 version upgrade 的衡量单位(量级)
    "ignore": [] // 不需要变动 version 的包
    }
  3. 添加变更文件:pnpm [exec] changeset(注意:该命令生效的前提是远程存在配置文件中指定的baseBranch分支,默认为main添加变更文件 添加变更文件 添加变更文件 成功执行该命令后,会在.changeset目录生成一个变更文件: 生成变更文件

  4. 当你准备好发布时,执行 pnpm [exec] changeset version 命令来应用变更文件中的更改,并更新包的版本号。成功执行该命令后,将更新 package.json文件中的version,并且生成 CHANGELOG.md 文件,以记录这些更改的详细信息。同时,相应的变更文件被消耗,即会被自动删除。

  5. 执行 pnpm [exec] changeset publish 命令来发布更新的包到 npm。成功执行该命令后,将自动根据更新的版本号发布包,并将变更文件移动到 .changeset/README.md 中。

  6. 将命令集成到项目根目录package.jsonscripts中:

    package.json
    {
    "scripts": {
    // Include build, lint, test - all the things you need to run
    // before publishing
    "publish-packages": "turbo run build lint test && changeset version && changeset publish"
    }
    }

不知道为啥执行pnpm [exec] changeset时,没有选择semver bump type的引导,默认是major

🦋  Which semver bump type should this change have? …
❯◯ patch
◯ minor
◯ major

可以在生成的变更文件中手动修改semver bump type

本地测试npm包

  1. cd packages/然后执行pnpm create vite创建一个react项目
    ✔ Project name: demo
    ✔ Select a framework: › React
    ✔ Select a variant: › TypeScript
  2. cd packages/demo安装依赖:pnpm add react-map
    packages/demo/package.json
    {
    "dependencies": {
    // ...
    "react-map": "workspace:^"
    },
    }

开发组件

泛型类型参数默认值

在 TypeScript 中,Props extends MapContainerProps = MapContainerProps 这种语法用于定义泛型类型参数的默认值。

  • Props:这是一个泛型类型参数的名称。
  • extends MapContainerProps:这表示 Props 必须是 MapContainerProps 或其子类型。
  • = MapContainerProps:这表示如果在使用这个泛型时没有显式地提供 Props 类型参数,那么 Props 的默认类型将是 MapContainerProps

组件库的依赖

组件的依赖使用peerDependenciesdependenciesdevDependencies:

  • dependencies 用于依赖的自开发组件库
  • peerDependencies 用于依赖的基本库和插件,从 npm v7 开始会自动安装peerDependencies指定的包(pnpm也会自动安装peerDependencies指定的包)
    tip

    使用pnpm add --save-peer <pkg> 将安装依赖到 peerDependencies 和 devDependencies

  • devDependencies 用于依赖的类型声明
{
"devDependencies": {
"@types/leaflet": "^1.9.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0"
},
"peerDependencies": {
"@geoman-io/leaflet-geoman-free": "^2.16.0",
"leaflet": "^1.9.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"dependencies": {
"react-map-core": "^1.0.0"
},
}

遇到的问题

  1. 在 Monorepo 项目中,开发的两个包都需要发布,并且这两个包有依赖关系,这个时候使用pnpm changeset publish发布两个包时发生了报错:Cannot find module 'react-map-core' or its corresponding type declarations. 有依赖关系的包是不是需要分开发布(按依赖关系先后发布)?

  2. 当在执行 pnpm changeset publish 或其他 npm 发布命令时,遇到提示 This operation requires a one-time password from your authenticator,这意味着你的 npm 账户启用了双因素认证(2FA)。在这种情况下,你需要提供一次性密码(OTP)才能完成发布操作。如果是发布内部包,则需要设置publishConfig,避免发布到外部npm上。

  3. 测试npm包的项目启动时报错:UnhandledPromiseRejectionWarning: SyntaxError: Unexpected token '??=',这个错误通常是由于你的 JavaScript 环境(如 Node.js 或浏览器)不支持新的语法特性。??= 是一个新的逻辑赋值运算符(Nullish Coalescing Assignment),它在较新的 ECMAScript 版本中引入。更换node版本到16即可。

  4. Argument of type '(e: DrawEvent) => void' is not assignable to parameter of type 'LeafletEventHandlerFn'. 使用类型断言as unknown as LeafletEventHandlerFn解决,如:map.on('draw:created', handleDrawEvent as unknown as LeafletEventHandlerFn);

  5. 使用 pnpm -F 过滤项目执行命令时报错:No projects matched the filters。假如项目结构如下:

    my-monorepo/
    ├── package.json
    ├── pnpm-workspace.yaml
    ├── packages/
    │ ├── package-a/
    │ │ ├── package.json
    │ │ └── index.js
    │ └── package-b/
    │ ├── package.json
    │ └── index.js
    pnpm-workspace.yaml
    packages:
    - 'packages/*'

    执行pnpm -F package-a build报错:No projects matched the filters,这是因为package-apackage.jsonname不是package-a,如下情况应该执行pnpm -F a build。如果package-apackage.jsonname@example/a,则还是执行pnpm -F a build

    package-a/package.json
    {
    "name": "a",
    "version": "1.0.0",
    "scripts": {
    "build": "echo Building package-a"
    }
    }
  6. 在 TypeScript 中,接口(interface)只能扩展对象类型或具有静态已知成员的对象类型的交集。

    在 TypeScript 中,接口(interface)只能扩展对象类型或具有静态已知成员的对象类型的交集。如果你尝试扩展一个不符合这些条件的类型,就会遇到类似 An interface can only extend an object type or intersection of object types with statically known members 的错误。

    确保你扩展的类型是一个对象类型或对象类型的交集。以下是一些常见的解决方法:

    // 确保你扩展的类型是一个对象类型
    interface Person {
    name: string;
    age: number;
    }

    interface Employee extends Person {
    employeeId: number;
    }
    // 如果你需要扩展多个类型,可以使用类型别名和交叉类型
    type Person = {
    name: string;
    age: number;
    };

    type Contact = {
    email: string;
    phone: string;
    };

    interface Employee extends Person, Contact {
    employeeId: number;
    }

    确保你扩展的类型是静态已知的,而不是动态生成的。例如,不要扩展一个泛型类型参数,除非它被约束为一个对象类型。

    // 扩展对象类型
    interface Person {
    name: string;
    age: number;
    }

    interface Employee extends Person {
    employeeId: number;
    }

    const employee: Employee = {
    name: "John Doe",
    age: 30,
    employeeId: 1234
    };
    // 使用类型别名和交叉类型
    type Person = {
    name: string;
    age: number;
    };

    type Contact = {
    email: string;
    phone: string;
    };

    interface Employee extends Person, Contact {
    employeeId: number;
    }

    const employee: Employee = {
    name: "John Doe",
    age: 30,
    email: "john.doe@example.com",
    phone: "123-456-7890",
    employeeId: 1234
    };

    如果你需要扩展一个泛型类型参数,确保它被约束为一个对象类型:

    interface Person {
    name: string;
    age: number;
    }

    interface Employee<T extends Person> {
    employeeId: number;
    details: T;
    }

    const employee: Employee<Person> = {
    employeeId: 1234,
    details: {
    name: "John Doe",
    age: 30
    }
    };

    以下是一些会导致错误的示例:

    // 错误:不能扩展非对象类型
    interface Employee extends string {
    employeeId: number;
    }
    // 错误:不能扩展动态生成的类型
    function createType<T>() {
    return class {
    value: T;
    };
    }

    interface Employee extends createType<number> {
    employeeId: number;
    }

发布包

在 Monorepo 项目中,使用 workspace: 协议来管理依赖关系是一种常见的做法,特别是在使用 Yarn Workspaces 或 pnpm Workspaces 时。workspace: 协议允许你在 Monorepo 项目中引用其他工作区包,而不需要发布它们到 npm registry。

Changesets 在生成版本时会自动处理 workspace: 协议,将其替换为实际的版本号。以下是 Changesets 的工作原理:

  1. 创建变更集:当你运行 pnpm changeset 时,Changesets 会引导你创建一个变更集文件,记录需要发布的包和版本号。
  2. 生成版本:当你运行 pnpm changeset version 时,Changesets 会根据变更集文件生成新的版本号,并自动将 workspace: 协议替换为实际的版本号。
  3. 发布包:当你运行 pnpm changeset publish 时,Changesets 会将新版本发布到 npm registry。
tip
  1. 在每个要发布的包的 package.json 文件中,配置 publishConfig 字段以指定发布时使用的 npm registry。
package.json
{
"publishConfig": {
"registry": "https://your-internal-registry.com/"
},
}
  1. 执行pnpm changeset publish前,先登录npm registry: npm login
  2. 执行pnpm publish发布包的话也能自动将 workspace: 协议替换为实际的版本号。