必不可少的UI组件二——组件库开发的基础知识(工程化篇)

原创
2023/11/08 11:58
阅读数 27

组件库工程化概述

必不可少的UI组件——组件库开发的基础知识(Vue篇) 中,我们介绍了一些封装 Vue 组件的过程中高频使用到的框架技巧,但是,这并不足以支持我们实现完善的组件库。

建设一个成熟的组件库就像盖一幢大楼,工程化基础就如同脚手架一般,虽然不是组件库核心、必备的部分,但没有它们,整个施工过程就会充满危险、处处收到掣肘。

构建组件库的工程基础需要的工具又广又杂。考虑一个成熟的组件库,它的工程化应当有以下需求:

  1. 包管理: 你的每个组件都需要发布到 npm 仓库,我们要熟悉 npm 包管理的基础 package.json,熟练运用一款包管理工具(npm/yarn/pnpm)。
  2. 文件组织: 一个组件库由多个组件构成,monorepo 模式提供了在一个代码仓中集中管理多个零散模块的优秀实践,我们需要掌握这种架构。
  3. 构建: 组件库应当通过构建生成尽可能多样化的产物(cjsesmbrowserd.ts、样式),满足用户在各个场景下的使用。
  4. 开发环境: 为了快速调试组件库,我们需要有一个开发服务器,并且要在多个组件相互依赖的前提下,解决热更新问题。
  5. 文档: 为了方便用户上手使用组件库,我们需要搭建一个文档对组件进行展示和说明。
  6. 测试: 为了确保代码的可靠性,我们需要集成测试方案,比如单元测试和端到端测试。
  7. 代码规范: 如果组件库的开发有他人协作,为了日后的可维护性,需要引入代码规范工具确保源码风格统一。
  8. 发布: 每当组件发布新版本时,我们需要一套发布流程,能够完成更新 semver 版本号、git 仓打 tag、生成版本更新记录 CHANGELOG 等操作。
  9. 持续集成: 最后,还需要有 CI / CD 流水线,使代码门禁、文档部署、测试流程、发布流程都趋于自动化,降低维护者和贡献者的心智负担。

本章节主要给大家介绍组件库开发工程化方面的基础知识,先从最基础的包管理文件组织入手,理清下面两个问题。而后续的构建、测试、发布等流程将在未来实践性更强的篇章中分享。

  • 组件库由多个组件包构成,一个“包”都有哪些基本属性?
  • 这么多组件包,为什么需要、以及如何使用 monorepo 架构对它们进行集中的组织管理?

组件库与 monorepo

组件库工程往往会拆分出许多子模块:

  • 以组件为单位划分子模块,可以满足用户单独安装、更新某个组件的需求,提供更加轻量、灵活的产物。
  • 将组件库的公用方法抽离为子模块,可以积累工程能力,有利于组件的后续迭代与维护。

我们可以先来参考 tinyVue 是如何进行拆分的:

  • renderless - 为了使组件的视图与逻辑分离,tinyVue 在这个模块实现组件的逻辑。
  • theme - 实现组件的主题与样式。
  • vue - 组件的主体实现,将 renderless 中的逻辑、theme 中的样式与 <template> 模板关联起来。其中每一个组件都被进一步划分为了子模块:

  • vue-common - 与 Vue 相关的公用方法,包括兼容不同 Vue 版本的适配器模块。
  • vue-icon - 实现 icon 矢量图标。
  • vue-locale - 多语言支持。
  • examples/docs - 组件库的文档。
  • 更多模块...

按照传统的 mutirepo 思路,需要为每一个模块建立一个代码仓进行管理。在这种分散管理的模式下,每一个包都有其独立的版本号、配置、发布流程。

monorepo 模式就是把所有这些模块集中到一个代码仓中,对它们进行集中统一管理。

monorepo当下构建复杂的多模块前端工程更为推崇的一种方式。 我们用下面的例子直观地说明 monorepo 的核心优势:

假设存在以下依赖关系:文档模块 docs 作为本地开发环境,依赖于组件模块 vue,而组件模块又依赖公共方法 vue-common

flowchart TB
  docs --> vue
  vue --> vue-common

假如我更新了最下游 vue-common 包中的工具方法,我当然希望上层组件 vue 也能立即适应更新,并即刻反馈在 docs 模块的 demo 示例中,实现丝滑的热更新。

但是很可惜,在传统的 mutirepo 模式下,我们必须先发布 vue-common 模块,再更新组件 vue 包中的依赖版本,接下来执行 vue 包的发布,最后升级 docs 项目中的依赖,才能够查看到更新后的效果。

上述整个过程在顺序上不能出现失误,否则只能废弃这个版本号重新发布。更难受的是,如果我们的修改不到位,再次微调仍然要走一遍发布流程!

另一方面,这些包虽然功能不同,但是它们的项目配置、构建流程、发布流程是不是也有很多相似之处?分散在多个代码仓中,对于许多相似的配置,免不了一顿复制粘贴,一旦我有整体性修改 CI 流程的需求,是不是也要分别修改多个仓,再分别验证其正确性?

从上面的例子,我们可以梳理出 monorepo 对于大部分库的构建者而言最无法拒绝的优势:

  • 依赖包迭代场景下,可以直接使用最新版本依赖,实时看到修改反馈,极大地优化了开发体验。
  • 构建流程、工程配置、代码规范很容易实现复用与统一管理。

更多关于 mutirepomonorepo 的优劣对比,可以参考下面的表格。

通常来说,只要你的项目由多模块组成,并对于各个模块代码的权限管控不敏感,就可以放心使用 monorepo 架构。这里再给大家推荐两篇文章,它们更加理性、全面地对比了两种模式的优劣:

monorepo 适用案例

monorepo 不仅仅适用于组件库的开发。这里我们列举其他的适用场景,帮助大家更好地理解这种架构。

核心库与周边适配器

例子:

<!---->

├── packages
|   ├── core
|   ├── adapterA
|   ├── adapterB
|   ├── pluginA
|   ├── pluginB
|   ├── ...
├── package.json

常规 Web 应用

即使是传统 Web 应用,采用 monorepo 模式也有利于代码的复用,促使团队成员以组件化的思想进行开发,不断抽离公共模块,产生技术沉淀。

├── packages
|   ├── portal    # 门户网站
|   ├── mis       # 管理后台
|   ├── mobile    # 移动端网站
|   ├── docs      # 开发文档
|   ├── shared    # 公共库
|   ├── api       # API 层
|   ├── ...       # 监控埋码、Nodejs 服务、更多公共模块...
├── package.json

了解包管理基础 package.json

monorepo 的重点在与单仓多包管理,这自然地引出了包管理这一概念。

包管理是处理模块之间依赖关系的核心,在当今的开源模式下,几乎没有任何的项目在开发过程中不需要引用他人发布的公共模块,缺少成熟的包管理机制,我们就只能通过源码拷贝的方式去复用他人的产出。

在正式上手搭建组件库之前,我们应该对“什么是包”有一个清晰的概念,去了解 npm 包的配置文件 package.json

一个包或者子模块不一定发布到 npm 仓库,但一定有 package.json 文件。package.json 所在的目录就代表了一个模块/包,这个 json 文件定义了包的各种配置,例如基本信息、依赖关系、构建配置等等。所有包管理器(npm/yarn/pnpm)以及绝大多数构建工具都依赖于这个配置文件的信息

package.json 中的字段并没有一个绝对统一的标准,除了官方约定的部分标准字段外,很多字段其实是特定的工具约定的。我们在分析配置的时候,要明确各个字段到底由谁读取

我们只介绍后续搭建组件库的过程中将用到的字段,如果你希望对 package.json 建立更加全面的了解,可以前往以下文章:

官方文档:package.json

你真的了解package.json吗?

关于前端大管家 package.json,你知道多少?

标识信息

包管理器、Node.js、构建工具都会读取标识字段,未正确设置会导致模块无法被被识别为 npm 包。

name

name 是区分 npm 包的唯一标识。当一个 npm 仓库中的包被安装到本地,我们能通过名称引用,而不必写复杂的 node_modules/... 引入路径就是得益于此。

对于包名称我们还要了解一个概念叫坐标,具有相同坐标的包会被安装到同一子目录下。例如 @vue/reactivity@vue/runtime-core 会被安装到 node_modules 目录的 @vue 目录下,vue 不属于任何坐标,就会被安装到 node_modules 根目录。

📦node_modules
 ┣ 📂@vue
 ┃ ┣ 📂reactivity
 ┃ ┗ 📂runtime-core
 ┣ 📂vue

通常情况下,属于同一个体系、项目下的包会被安排在一个坐标下,比如 OpenTiny 开源项目下的包就都会发布到 @opentiny 这个坐标下,那么包名就需要设定为 @opentiny/xxx

每个人都可以登录 npm.js,建立自己的坐标。

version

version 字段表示包的版本号,符合 major.minor.patch(主版本号.次版本号.修订号) 的格式(例如 1.0.1),如果要进一步了解版本号相关的知识,我们可以阅读以下文章来详细了解什么是语义化版本

语义化版本 2.0.0

semver:语义版本号标准 + npm的版本控制器🧲

可以通过以下命令来查看 npm 包的版本信息,以 @opentiny/vue 为例:

npm view @opentiny/vue versions

基本信息

基本信息主要由 npm 负责读取,未正确设置不影响包的功能,但会导致该包在 npm.js 中缺失信息,不能给用户正确的引导。

这些信息不涉及包管理的核心,简单做一些了解即可。以 vue 的基本信息 为例子:

{
  "name": "vue",
  // 一句话简介,可以作为关键字搜索的依据
  "description": "The progressive JavaScript framework for building modern web UI.",
  // 关键字、标签,正确设置可以提高在 npm 的搜索权重与曝光度
  "keywords": ["vue"],
  // 包的作者,主要 Owner
  "author": "Evan You",
  // 开源许可证
  "license": "MIT",
  // 项目主页
  "homepage": "https://github.com/vuejs/core/tree/main/packages/vue#readme",
  // 源码仓库
  "repository": {
    "type": "git",
    "url": "git+https://github.com/vuejs/core.git"
  },
  // BUG 反馈方式,支持 `bugs.email` 邮箱字段
  "bugs": { 
    "url" : "https://github.com/vuejs/core/issue"
  }
}

入口信息

入口信息主要被 Node.js、各路构建工具(Vite / Rollup / Webpack / TypeScript)所识别。未正确设置会导致 npm 包无法被加载或者实际加载了预料之外的文件。

入口文件的加载机制是比较复杂的,在不同的构建工具中有着不同的加载逻辑,对此给大家分享一篇文章:package.json 导入模块入口文件优先级详解。这里不倾向于深挖细节,只讲到足够我们搭建组件库的程度。

当然,你可能需要有模块化规范的前置知识,前端的模块规范有着源远流长的历史,直到现在也并不是统一的。我们至少应该了解正被广泛使用的 cjsesm 两种现代化规范,这里同样给大家分享一些文章:

阮一峰 - ES6 Module 的加载实现

ESM和CJS模块杂谈

在后续讲解打包,涉及到生成多场景产物时,我们会对做详细一些的讨论。

这里列举几个需要被关注的入口信息字段:mainmoduletypesexports。我们尽量使用贴近实践的描述,以代码中引入方式的不同来分析它们之间的区别:

1. mainexports.*.require 字段用于设置 require() 方式的加载入口(cjs 规范)。

// 入口定义
{
  "name": "my-module",
  "main": "index.js",
  "exports": {
    ".": {
      "require": "index.js"
    },
    // ...
  }
}
// 代码中使用
const app = require('my-module') // 实际路径 node_modules/my-module/index.js

2. moduleexports.*.import 字段用于设置 import 的加载入口(esm 规范)。

// 入口定义
{
  "name": "my-module",
  "main": "index.js",
  "module": "index.mjs",
  "exports": {
    ".": {
      "require": "index.js",
      "import": "index.mjs"
    },
    // ...
  }
}
// 使用
import app from 'my-module' // 实际路径 node_modules/my-module/index.mjs

3. typesexports.*.types 字段用于设置 d.ts 类型声明的加载入口(TypeScript 专属)。

// 入口定义
{
  "name": "my-module",
  "main": "index.js",
  "module": "index.mjs",
  "types": "index.d.ts",
  "exports": {
    ".": {
      "require": "index.js",
      "import": "index.mjs",
      "types": "index.d.ts"
    },
    // ...
  }
}

这里我们以 axios 为例子看看实际效果:

4. exports 比起 mainmoduletypes,它可以暴露更多的出口,而后者只能定义主出口。

// 入口定义
{
  "name": "my-module",
  "main": "index.js",
  "exports": {
    ".": {
      "require": "index.js",
    },
    "./locale/*": {
      "require": "./locale/*",
    },
    "./plugins/*": {
      "require": "./dist/plugins/*",
    }
    // ...
  }
}

// 使用
const app = require('my-module') // 实际路径 node_modules/my-module/index.js
const zhCn = require('my-module/locale/zh-Cn') // 实际路径 node_modules/my-module/locale/zh-Cn.js
const testPlugin = require('my-module/plugins/test') // 实际路径 node_modules/my-module/dist/plugins/test.js
// import 同理

最后,当 exports 和另外三个入口字段出现重复定义时,会有更高的优先级。

更多关于 exports 的规则和细节,可以去 Webpack Package exports 学习,我建议在有需要的时候查阅即可。

依赖信息

依赖信息的读取方只有包管理器。未正确设置会导致项目实际安装的依赖包不符合预期,进而导致项目无法正常运行或构建。

版本约束

依赖信息的结构是一个对象,其中依赖包的名称作为键(key),依赖的版本约束作为值(value)。

{
  "dependencies": {
    "lodash": "^4.17.21",
  },
  "devDependencies": {
    "vite": "~4.2.0"
  }
}

版本约束限制了包管理器为项目安装依赖时可选的版本范围:

  • ^ 的含义是安装最新的 minor 版本。例如 ^1.2.0 的约束下,会为项目安装最新的 minor 版本 1.X.Y,但不会安装下一个 major 版本 2.0.0
  • ~ 的含义是安装最新的 patch 版本。例如 ~1.2.0 的约束下,会为项目安装最新的 patch 版本 1.2.X,但不会安装下一个 minor 版本 1.3.0
  • 如果版本号前面没有任何标识符,表示固定版本号,无论如何都只安装这个固定版本。

关于版本约束的进阶阅读:工程的 package.json 中的 ^~ 该保留吗?

依赖分类

很多情况下,我们其实并没有真正搞懂常见的三种依赖类型—— dependenciesdevDependenciespeerDependencies 的真正含义与表现。这里简单给出一个表格说明帮助大家正确理解。

  • 表格中的 项目中 理解为依赖信息被定义在我们正在开发的模块,对应根目录下的 package.json 中;
  • 依赖中 理解为依赖信息被定义在 node_modules 内的依赖包中(即依赖的依赖),对应 node_modules/${packageName}/package.json
依赖类型 项目中 依赖中 用途
dependencies 会被安装 会被安装 项目运行时依赖
devDependencies 会被安装 不会被安装 项目在开发过程需要的依赖。一般构建工具、测试框架、代码规范工具都会被作为开发依赖
peerDependencies 不会被安装 不会被安装。但是若其中声明的依赖没有被项目安装,或者版本不匹配时,会生成警告信息提示用户 定义项目需要的依赖环境。常用于表示插件和主框架的关系,如 @vitejs/plugin-vuepeerDependencies 中就声明了主框架 vitevue

我自己做的关于 pnpm 的分享中,为了理解“幽灵依赖”现象,也花了不少篇幅去介绍这三个依赖字段的实际效果,大家可以参考阅读:新一代包管理工具 pnpm 使用心得

同样再分享一篇其他介绍这个机制的文章:一文彻底看懂 package.json 中的各种 dependencies

发布信息

发布信息的读取方只有包管理器。未正确设置会导致项目的发布行为不符合预期。

files

files 指定了发布为 npm 包时,哪些文件或目录需要被提交到 npm 服务器中。

{
  "files": [
    "LICENSE",
    "README.md",
    "dist"
  ]
}

private

private 用于指定项目是否为私有包。当我们的项目不想被意外发布到公共 npm 仓库时,就设置 private: true

publishConfig

当我们的项目需要发布到私有的 npm 仓库时(比如公司内网的仓库),需要设置 publishConfig 对象。

{
  "publishConfig": {
    "registry": "https://mynpm.com",
  },
}

脚本信息

脚本信息的读取方只有包管理器。这是包管理器给我们提供的一项福利功能,允许我们给复杂的命令赋予一个简单的别名

{
  "script": {
    "show": "echo 'Hello World!'",
    "dev": "vite"
  },
  "dependencies": {
    "vite": "^4.3.0"
  }
}

在上面的例子中,我们运行 npm run show 就可以执行打印 Hello World 的命令。

运行 npm run dev 就可以调用 vite 的命令行程序,启动 vite 开发服务器。

然而直接在命令行中执行 vite 命令是会报错的,这是因为包管理器会将项目中所有相关的可执行命令二进制文件放入 node_modules/.bin 中,这个目录会在运行时被加入到系统环境变量 PATH

用 pnpm 管理 monorepo 项目

pnpm 选型理由

monorepo 的单仓分模块的要求,使得仓库内的模块不仅要处理与外部模块的关系,还要处理内部之间相互的依赖关系。因此我们需要选择一个强大的包管理工具帮助处理这些任务。

目前前端包管理的根基是 npm,在其基础上衍生出了 yarnpnpm。在 2022 年以后,我们推荐使用 pnpm 来管理项目依赖。pnpm 覆盖了 npmyarn 的大部分能力,且多个维度的体验都有大幅度提升。

pnpm 是一款快速、高效使用磁盘空间的包管理器。

它具有以下优势:

  • 速度快:多数场景下,安装速度是 npm/yarn 的 2 - 3 倍。
  • 基于内容寻址:硬链接节约磁盘空间,不会重复安装同一个包,对于同一个包的不同版本采取增量写入新文件的策略。
  • 依赖访问安全性强:优化了 node_modules 的扁平结构,提供了**限制依赖的非法访问(幽灵依赖)**的手段。
  • 支持 monorepo:自身能力就对 monorepo 工程模式提供了有力的支持。在轻量场景下,无需集成 lernaTurborepo 等工具。

对于 pnpm 选型的更多理由以及其原理、应用的简单说明,可以参考:关于现代包管理器的深度思考——为什么现在我更推荐 pnpm 而不是 npm/yarn?

workspace 模式

pnpm 支持 monorepo 模式的工作机制叫做 workspace(工作空间)

它要求在代码仓的根目录下存有 pnpm-workspace.yaml 文件指定哪些目录作为独立的工作空间,这个工作空间可以理解为一个子模块或者 npm 包。

例如以下的 pnpm-workspace.yaml 文件定义:a 目录、b 目录、c 目录下的所有子目录,都会各自被视为独立的模块。

packages:
  - a
  - b
  - c/*
📦my-project
 ┣ 📂a
 ┃ ┗ 📜package.json
 ┣ 📂b
 ┃ ┗ 📜package.json
 ┣ 📂c
 ┃ ┣ 📂c-1
 ┃ ┃ ┗ 📜package.json
 ┃ ┣ 📂c-2
 ┃ ┃ ┗ 📜package.json
 ┃ ┗ 📂c-3
 ┃   ┗ 📜package.json
 ┣ 📜package.json
 ┣ 📜pnpm-workspace.yaml

需要注意的是,pnpm不是通过目录名称,而是通过目录下 package.json 文件的 name 字段来识别仓库内的包与模块的。

全局管理操作

workspace 模式下,代码仓根目录通常不会作为一个子模块或者 npm 包,而是**主要作为一个管理中枢,执行一些全局操作,安装一些共有的依赖。**下面介绍一些常用的全局管理操作。

  • 创建一个 package.json 文件。
pnpm init
  • 设置用户的全局 .npmrc 配置。
pnpm config set <key> <value>
  • 根据当前目录 package.json 中的依赖声明安装全部依赖,在 workspace 模式下会一并处理所有子模块的依赖安装
pnpm install
  • 安装项目公共开发依赖。-w 选项代表在 monorepo 模式下的根目录进行操作。每个子模块都能访问根目录的依赖,适合把 TypeScriptViteeslint 等公共开发依赖装在这里。
pnpm install -wD xxx
  • 卸载公共依赖。
pnpm uninstall -w xxx
  • 执行根目录的 package.json 中的脚本
pnpm run xxx

子包管理操作

workspace 模式下,pnpm 主要通过 --filter 选项过滤子模块,实现对各个工作空间进行精细化操作的目的。

1. 为指定模块安装外部依赖。 下面的例子为 a 包安装 lodash 外部依赖。与常规安装指令相同,-S-D 选项分别可以将依赖安装为正式依赖(dependencies)或者开发依赖(devDependencies)。

# 为 a 包安装 lodash
pnpm --filter a install -S lodash
pnpm --filter a install -D lodash

2. 指定内部模块之间的互相依赖。 指定内部模块之间的互相依赖。下面的例子演示了为 a 包安装内部依赖 b

# 指定 a 模块依赖于 b 模块
pnpm --filter a install -S b

pnpm workspace 对内部依赖关系的表示不同于外部,它自己约定了一套 Workspace 协议 (workspace:)。下面给出一个内部模块 a 依赖同是内部模块 b 的例子。

{
  "name": "a",
  // ...
  "dependencies": {
    "b": "workspace:^"
  }
}

后续使用 pnpm publish 命令发布 npm 包时,workspace:^ 会被替换成内部模块 b 的对应版本号(对应 package.json 中的 version 字段)。替换规律如下:

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

3. 过滤的高级用法

  • --filter 过滤出目标工作空间集合后,不仅支持 install 安装依赖,run(执行脚本)、publish(发布包) 等绝大多数包管理操作都能够执行。
# 发布所有包名为 @a/ 开头的包
pnpm --filter @a/* publish
  • --filter 筛选出多个包时,默认情况下,它会首先分析多个包之间的内部依赖关系,按照依赖关系拓扑排序的顺序对这些包执行指令,即按依赖树从叶到根的顺序。例如下图所示的依赖关系中,执行顺序为 C -> D -> B -> A
flowchart TB
  A --> B
  A --> C
  B --> C
  B --> D
  • --filter 的还有更多超乎我们想象的能力,它支持依赖关系筛选,甚至支持根据 git 提交记录进行筛选。
# 为 a 以及 a 的所有依赖项执行测试脚本
pnpm --filter a... run test
# 为 b 以及依赖 b 的所有包执行测试脚本
pnpm --filter ...b run test

# 找出自 origin/master 提交以来所有变更涉及的包
# 为这些包以及依赖它们的所有包执行构建脚本
# README.md 的变更不会触发此机制
pnpm --filter="...{packages/**}[origin/master]"
  --changed-files-ignore-pattern="**/README.md" run build

# 找出自上次 commit 以来所有变更涉及的包
pnpm --filter "...[HEAD~1]" run build

更多 pnpm 使用方面的细节还需自行查阅官方文档:pnpm 官方文档

搭建 monorepo 组件库目录结构

最后,我们通过搭建一个 monorepo 组件库工程的雏形,来实践前面讲到的工程化基础知识。

我们先划分出各个 UI 组件、文档的模块,并为组件库工程命名为 my-tiny-vue,指定其 npm 发布坐标为 @mytinyvue

my-tiny-vue
├── docs          # 组件库文档 demo 模块
├── packages      # 组件库的各个组件模块
|   ├── button    # 按钮组件
|   ├── input     # 输入框组件
|   ├── form      # 表单组件
|   ├── theme     # 组件库的样式与主题
|   ├── ...       # 更多 UI 组件
|   ├── ui        # 归纳各个 UI 组件的入口,即组件库的主包
|   ├── shared    # 其他工具方法
├── package.json

初始化 monorepo 工程

我们默认大家已经装好了开发环境,Node.jsnpm 都可以正常工作,首先通过 npm i -g pnpm 安装好 pnpm,后续包管理相关的命令一律使用 pnpm 执行。

首先,我们来创建工程目录:

mkdir my-tiny-vue
cd my-tiny-vue
pnpm init

在项目根目录中生成了 packages.json 文件,但是根目录并不是待发布的模块,它将作为整个组件库 monorepo 项目的管理中枢。我们把对这个 package.jsonname 以外的字段都删去,后续我们要根据自己的需要自定义。

{
  "name": "my-tiny-vue",
- "version": "1.0.0",
- "description": "",
- "main": "index.js",
- "scripts": {
-   "test": "echo \"Error: no test specified\" && exit 1"
- },
- "keywords": [],
- "author": "",
- "license": "ISC"
}

之后,我们在根目录下创建 pnpm-workspace.yaml 文件,这个文件的存在本身,会让 pnpm 要使用 monorepo 的模式管理这个项目。它的内容告知 pnpm 哪些目录将被划分为子模块,这些独立模块被包管理器称作 workspace(工作空间)。

我们在 pnpm-workspace.yaml 中写入以下内容。

packages:
  # 根目录下的 docs 是一个独立的文档应用,应该被划分为一个模块
  - docs
  # packages 目录下的每一个目录都作为一个独立的模块
  - packages/*

那么我们开始实际建立这些工作空间,并将根目录下的 package.json 文件复制到每个工作空间中。为了方便演示,暂时只建立 UI 组件 button(按钮)input(输入框) 以及公共方法模块 shared。这里展示出完成操作后的目录树:

📦my-tiny-vue
 ┣ 📂docs
 ┃ ┗ 📜package.json
 ┣ 📂packages
 ┃ ┣ 📂button
 ┃ ┃ ┗ 📜package.json
 ┃ ┣ 📂input
 ┃ ┃ ┗ 📜package.json
 ┃ ┗ 📂shared
 ┃   ┗ 📜package.json
 ┣ 📜package.json
 ┣ 📜pnpm-workspace.yaml
 ┗ 📜README.md

设置 package.json

接下来,我们要明确每一个模块的属性,设置它们的 package.json 文件。

注意:下面例子中的注释只是为了方便讲解,实操请务必删除注释,带有注释的 package.json 会在执行命令时报错。

根目录的 package.json

// my-tiny-vue/package.json
{
  "name": "my-tiny-vue",
  "private": true,
  "scripts": {
    // 定义脚本
    "hello": "echo 'hello world'"
  },
  "devDependencies": {
    // 定义各个模块的公共开发依赖
    "typescript": "^5.1.6",
    "vite": "^4.4.4",
    "vue": "^3.3.4",
  }
}
  • private: true:根目录在 monorepo 模式下只是一个管理中枢,它不会被发布为 npm 包。
  • devDependencies:所有子模块共享的公共的开发依赖,例如构建工具、TypeScript、Vue、代码规范等,将公共开发依赖安装在根目录可以大幅减少子模块的依赖声明

组件包的 package.json

这里只举一个组件的例子,其他组件包的配置除了 name 以外大体相同。

// my-tiny-vue/packages/button/package.json
{
  // 标识信息
  "name": "@mytinyvue/button",
  "version": "0.0.0",

  // 基本信息
  "description": "",
  "keywords": ["vue", "ui", "component library"],
  "author": "XXX",
  "license": "MIT",
  "homepage": "https://github.com/xxx/my-tiny-vue/blob/master/README.md",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/xxx/my-tiny-vue.git"
  },
  "bugs": { 
    "url" : "https://github.com/xxx/my-tiny-vue/issues"
  },


  // 定义脚本,由于还没有集成实际的构建流程,这里先以打印命令代替
  "scripts": {
    "build": "echo build",
    "test": "echo test"
  },

  // 入口信息,由于没有实际产物,先设置为空字符串
  "main": "",
  "module": "",
  "types": "",
  "exports": {
    ".": {
      "require": "",
      "module": "",
      "types": ""
    }
  },

  // 发布信息
  "files": [
    "dist",
    "README.md"
  ],
  // "publishConfig": {},

  // 依赖信息
  "peerDependencies": {
    "vue": ">=3.0.0"
  },
  "dependencies": {},
  "devDependencies": {}
}
  • name:组件统一发布到 @mytinyvue 坐标下,有坐标限制了命名空间,组件的名称可以尽可能简单。
  • files:我们规定每个包的产物目录为 dist。产物目录必须在发布时被提交,此外还要一并发布 README.md 文档。
  • publishConfig:如果我们需要发布到私有 npm 仓,请取消 publishConfig 的注释并根据实际情况填写。
  • peerDependencies: 既然是使用 vue3 的组件库,我们需要正确声明主框架的版本。这里不将 vue 放入 dependencies 是因为用户项目同样也直接依赖 vue 框架,这样可能造成依赖版本不同的风险。这就是为什么周边库、插件总是要把主框架声明为 peerDependencies 的原因,我们的组件库也不例外。
  • dependencies:项目的运行时依赖,用户安装该包时,这些依赖也将被递归安装
  • devDependencies:项目的开发依赖(公共开发依赖之外),不会在用户安装时被递归安装

项目文档的 package.json

// my-tiny-vue/docs/package.json
{
  "name": "@mytinyvue/docs",
  "private": true,
  "scripts": {
    // 定义脚本,由于还没有集成实际的构建流程,这里先以打印命令代替
    "dev": "echo dev",
    "build": "echo build"
  },
  "dependencies": {
    // 安装文档特有依赖
  },
  "devDependencies": {
    // 安装文档特有依赖
  }
}
  • private: true:项目文档的 packages.json 与根目录类似,它同样不需要被发布到 npm 仓库。

至此,我们的 monorepo 项目的雏形就已经建立完毕,可以通过下面的命令,一次性执行所有子模块的 build 脚本。不难想到,如果我们将 echo build 换成构建命令,就能够实现组件库的整体构建。

> pnpm --filter "*" run build

Scope: 4 of 5 workspace projects
docs build$ echo build
│ build
└─ Done in 58ms
packages/button build$ echo build
│ build
└─ Done in 54ms
packages/input build$ echo build
│ build
└─ Done in 58ms
packages/shared build$ echo build
│ build
└─ Done in 55ms

后续,我们将以这个简单例子为基础,一步步地搭建出完善的组件库项目。

关于OpenTiny

图片

OpenTiny 是一套企业级 Web 前端开发解决方案,提供跨端、跨框架的UI组件库,适配 PC 端 / 移动端等多端,支持 Vue2 / Vue3 / Angular 多技术栈,拥有灵活扩展的低代码引擎,包含主题配置系统 / 中后台模板 / CLI 命令行等丰富的效率提升工具,可帮助开发者高效开发 Web 应用。

核心亮点:

  • 跨端跨框架: 使用 Renderless 无渲染组件设计架构,实现了一套代码同时支持 Vue2 / Vue3,PC / Mobile 端,并支持函数级别的逻辑定制和全模板替换,灵活性好、二次开发能力强。
  • 组件丰富:PC 端有100+组件,移动端有30+组件,包含高频组件 Table、Tree、Select 等,内置虚拟滚动,保证大数据场景下的流畅体验,除了业界常见组件之外,我们还提供了一些独有的特色组件,如:Split 面板分割器、IpAddress IP 地址输入框、Calendar 日历、Crop 图片裁切等。
  • 低代码引擎:低代码引擎使能开发者定制低代码平台。它是低代码平台的底座,提供可视化搭建页面等基础能力,既可以通过线上搭配组合,也可以通过下载源码进行二次开发,实时定制出自己的低代码平台。适用于多场景的低代码平台开发,如:资源编排、服务端渲染、模型驱动、移动端、大屏端、页面编排等。
  • 配置式组件: 组件支持模板式和配置式两种使用方式,适合低代码平台,目前团队已经将 OpenTiny 集成到内部的低代码平台,针对低码平台做了大量优化。
  • 周边生态齐全: 提供了基于 Angular + TypeScript 的 TinyNG 组件库,提供包含 10+ 实用功能、20+ 典型页面的 TinyPro 中后台模板,提供覆盖前端开发全流程的 TinyCLI 工程化工具,提供强大的在线主题配置平台 TinyTheme。

欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~

OpenTiny 官网opentiny.design/

OpenTiny 代码仓库github.com/opentiny/

TinyEngine 源码https://github.com/opentiny/tiny-engine

欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI~

如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部