本文为稀土技能社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!

专栏上篇文章传送门:根据Vite打造事务组件库(开篇介绍)

专栏下篇文章传送门:实战案例:初探工程装备 & 图标组件热身

本节触及的内容源码可在vue-pro-components c1 分支找到,欢迎 star 支撑!

前言

本文是 根据Vite+AntDesignVue打造事务组件库 专栏第 2 篇文章【组件库技能选型和开发环境树立】,为了让读者们沉溺式体会组件库开发,我将会手把手带着读者们树立起一个组件库的 monorepo 开发环境,相关源码可在 vue-pro-components 库房中获得。

为什么挑选 monorepo?

monorepo 这个词大家或多或少都听过,乃至现已在项目中应用过,问题来了,你能给 monorepo 下个界说吗?

组件库技能选型和开发环境树立

别慌,我也不会,咱们来看看维基百科给出的界说。

Inversion control systems, amonorepo(“mono” meaning ‘single’ and “repo” being short for ‘repository’) is a software development strategy where code for many projects is stored in the same repository.

可见,monorepo 的含义便是在一个单体库房中办理多个项目,这种项目办理形式在一些大型项目中现已被广泛应用,比方 Vite, Vue, React, Angular, React Native, Jest, Pinia, Vue CLI, Element Plus, Modern.js, Next.js 等。假如你打开这些项目库房,你能够发现其间一个很明显的共性:它们都选用了packages目录来办理子包,每个子包中都包含一个package.json文件,也便是说子包也是一个独立的npm包。

进一步研究这些库房时,咱们能够发现,这些项目在支撑起整个 monorepo 体系时选用的技能计划是不相同的。

有的项目简略选用了 yarn workspaces,有的则运用了 Lerna,也有的用了 pnpm,还有的用了 Changesets,再卷一点的现已用上了 Turborepo。

Changesets 和 Turborepo 不能界说为 monorepo 计划,而是 monorepo 体系中强有力的配套东西。

鉴于笔者还未全面运用过以上一切计划,关于这些计划中的优缺点,无法给出客观的点评,读者们能够自行去查阅更多资料。

这儿简略给个参阅定见,帮助不了解这块的读者先有个大略的知道,如有错误,还请评论指出:

yarn + workspaces

yarn 内置的 workspaces 特性能够让子包之间的引证变得简略(其间也用到了 symbol link),在此根底上能够衍生出更多上层的才能,Lerna 便是在此根底上发展而来的东西。workspaces 支撑了 monorepo 最根底的才能,可是仅靠它也显得有点单薄,因为它没有供给包的全生命周期办理才能。

Yarn workspaces aim to make working withmonoreposeasy, solving one of the main use cases foryarn linkin a more declarative way. In short, they allow multiple projects to live together in the same repository AND to cross-reference each other – any modification to one’s source code being instantly applied to the others.

组件库技能选型和开发环境树立

Lerna

Lerna 能够处理上面说的问题,它供给了包的全生命周期办理才能,包含但不限于 新建子包 / 删去子包 / 办理子包依靠 / 发包 等等,并且有相关的指令行支撑,能较大程度上提高 monorepo 项目开发和维护功率。除此之外,Lerna 团队还极力提高性能和开发体会,详细见 Why Lerna?

pnpm

pnpm 从设计上就天然支撑了 monorepo,一起还经过 严格的依靠结构 / symbol link / hard link 等才能处理了 鬼魂依靠依靠占用大量存储空间 等问题。pnpm 也能够搭配 Lerna 运用。

Changesets

Changesets 是 pnpm 推荐的一个致力于处理改变记录集、changelog、version 等问题的东西,听说比 Lerna Version 这块的处理更科学。它有一个生产和消费.changeset的进程,用户在一些复杂版别操控场景中有必定的自主操控权,因为你能够对 changeset 等内容做必定调整,自由度更高。

Turborepo

Turbo,涡轮增压嘛,这便是要起飞的节奏,Turborepo 内部的中心代码是根据 Go 来完成的,这跟 esbuild 相同,直接是降维打击啊!

组件库技能选型和开发环境树立

简略看了一些 Turborepo 官网的文档,能够发现 Turborepo是专注于提高构建性能的东西和渠道,它在 Pipeline 编列、Output Caching、Remote Caching、Output Replaying 等方面做了许多尽力,相同的事情不做第二次,这与 Lerna 现在的办理团队 Nrwl 研发的构建渠道 Nx 的发力方向有点类似。

合理的 Pipeline 编列能够最大限度发挥 CPU 性能。缓存用好了真的是一把利刃,关于重复的工作,得到秒级乃至毫秒级的响应是真的香,这在 monorepo 项目中尤其重要,因为你不知道一个 monorepo 可能会演变成多大的工程!而且 Remote Caching 在 CI/CD 中也能发挥很大作用!

为什么这些明星项目都不谋而合挑选了 monorepo 呢?背后的原因可能有这些:

  • 现代前端工程的复杂度在不断提高,使得拆包成为了必然趋势
  • 可是把一个大型项目拆成多个库房又让办理和联调变得十分困难,npm link之类的计划开发体会太差。
  • 现在最佳的计划仍是在一个库房维护,然后经过技能手段改善单体库房的下风,既能保持子包之间的独立性,又能让包之间联调变得简略
  • 使得集成化的 CI/CD 变得更加速捷。
  • 更多亮点值得探究……

技能选型

在组件库的技能选型这块没有太多可说的,根本上是环绕项目的需求和自身的才能打开,依照开发进程中的实践需求引进相关的技能计划。这中间会存在片面意愿,仅供参阅!

  • Monorepo: 我仍是挑选了保守一点的 Lerna,这个我相对熟悉一点,一起也是想多踩踩坑,知道坑在哪里,才能明白为什么要换更好的东西。
  • 前端结构:如专栏标题所述,我挑选了个人更熟悉的 Vue3 生态。
  • 类型支撑: TypeScript。
  • 根底 UI 组件库:AntDesignVue,承继了 AntDesign 的企业级设计风格,个人感觉 AntDesignVue 相对其他 Vue 生态的组件库更有质感,可是它也不是完美的。
  • 构建东西:Vite, Rollup, Gulp 以及相关插件生态。组件库建设是一个相对复杂的工程,很难依靠一种东西把一切事宜处理完毕,所以整合东西链是必要的。
  • CSS预处理器:Less。挑选 Less 主要是考虑后续切换主题,这块 Less 比较好处理(纯属个人片面定见,究竟还有不依靠 preprocessor 的)。
  • 发布流程东西:release-it。
  • CI/CD: Github Actions。这个没有强制要求,有许多挑选。
  • 标准/束缚之类的:ESLint, Prettier, StyleLint, husky, commitizen 等等。
  • 更多……

开发环境树立

说太多概念也不太容易消化,咱们来实操一下。

创立 Lerna 工程

首先咱们需求新建并进入vue-pro-components工程目录,接着经过npx lerna init创立一个工程。

$ mkdir vue-pro-components && cd vue-pro-components
$ npx lerna init
lerna notice cli v4.0.0
lerna info Initializing Git repository
lerna info Creating package.json
lerna info Creating lerna.json
lerna info Creating packages directory
lerna success Initialized Lerna files

能够发现 Lerna 为咱们生成了一个 monorepo 项目的根本骨架:

$ tree
.
|-- lerna.json
|-- package.json
`-- packages

package.json

大略看一下,package.json中的private字段设置为了true

{
  "name": "root",
  "private": true,
  "devDependencies": {
    "lerna": "^4.0.0"
  }
}

这代表什么意思呢?咱们看看 npm 文档中关于 private 的描述。

If you set"private": truein your package.json, then npm will refuse to publish it.

private设置为true时,就代表你不需求在npm公开发布这个包。看到这,有的读者可能会纳闷了,“这如同有点问题吧,组件库一般是要发布的呀!”

不必慌,因为咱们选用的是 monorepo 架构,详细发布的组件库其实是packages目录下的一个子包。而整个工程的主包则是用来组织起整个大结构,不发布到npm也是能够了解的。

一起,这也符合 Yarn 1.X 版别的强制要求,假如需求用到workspaces特性,有必要声明privatetrue。尽管 Yarn Modern Version 现已取消了这个约束,可是迟迟没有作为 Yarn 的默许装置版别,在晋级迁移这块还有不少阻力。

lena.json

咱们再调查一下lerna.json这个文件,它经过packages字段约定了子包都散布在哪些目录下,这儿支撑 glob pattern 匹配,也能够是一个 package 的 path。

{
  "packages": [
    "packages/*"
  ],
  "version": "0.0.0"
}

关于version字段,Lerna 供给了两种版别战略供咱们挑选,咱们应该怎样挑选呢?这儿先不打开说,免得大家发生太多疑问导致不必要的焦虑。

工程树立完毕后,咱们先与 remote 库房(您需求确保长途库房存在)相关一下,便利后续提交代码。假如您现已 fork 该库房,请将库房地址改成您自己的。

git remote add origin https://github.com/cumt-robin/vue-pro-components.git

新建组件库子包

有了上面的根本结构,咱们能够着手新建一个组件库子包,这个包将寄存组件相关源码,这儿用到了lerna create指令。咱们能够根据交互提示填上一些必要的信息,先把子包建好,一些信息能够后续再更改,不必过于纠结。

$ lerna create vue-pro-components
// 在一步步提示下,先将一个npm子包树立起来
package name: (vue-pro-components)
version: (0.0.0)
description: pro components based on vue3
keywords: components,vue3,vite,typescript,unplugin,on demand,pro
homepage:
license: (ISC) MIT
entry point: (lib/vue-pro-components.js) index.js
git repository:

注意,我这儿用到的 package name 是 vue-pro-components,这个 name 也将作为我要发布到 npm 上的包名。

也不必担心这个包名 vue-pro-components 和整个工程的目录名相同,因为主工程是不会发布到 npm 的。

这儿先不急着加详细内容,因为咱们需求先把大的结构理清楚,持续往下看。

新建 playground 子包

playground 翻译过来便是游乐场,这个子包能够作为咱们调试组件体现的当地,这儿直接挑选用 Vite 初始化一个工程。

咱们测验一下不运用lerna create指令新建 package。

cd packages && yarn create vite playground --template vue-ts

用 Vite 创立的这个 playground 包默许也是 private 的,playground 本来的作用便是调试或展现组件的根本作用,能够打包后作为一个 web 应用发布到公网,可是不需求发布到 npm,所以设置为 private 符合预期。

我该用哪种 version 战略?

回到上文留下的疑问,两种版别战略,咱们该怎样选?

  1. Fixed/Locked mode (default)

Fixed mode 意味着version字段对应着详细的版别号,比方0.1.0。在这种形式下,各个子包的版别号相对会集,一般来说能够了解为同一个版别号(也有例外),Lerna 会在履行lerna version指令时根据用户的挑选自动更新version字段,一起会修正发生过代码改变的子包的package.json中的version字段。

咱们能够来实验一下,先把代码 commit & push 到长途,这儿我用了一个新分支c1

// 回到根目录
git checkout -b c1
git add .
git commit -m 'chore: 先将代码提交到长途,便利后续测验lerna version'
git push --set-upstream origin c1

文档中说到,假如当时 major 版别号是 0,则认为一切改变都是破坏性的,这意味着修正任何一个包中的内容,lerna version都会更新一切子包的版别号。咱们来试试,修正其间一个子包vue-pro-components的内容,在index.js加了一行注释,然后 commit,接着运用lerna version更新版别号。

组件库技能选型和开发环境树立

$ git add .
$ git commit -m 'chore: 测验 major 版别号为 0 时修正一个包'
$ lerna version
lerna notice cli v5.3.0
lerna info current version 0.0.0
lerna info Assuming all packages changed
? Select a new version (currently 0.0.0) (Use arrow keys)
> Patch (0.0.1)
  Minor (0.1.0)
  Major (1.0.0)
  Prepatch (0.0.1-alpha.0)
  Preminor (0.1.0-alpha.0)
  Premajor (1.0.0-alpha.0)
  Custom Prerelease
  Custom Version

当咱们挑选 Patch 更新后,能够看到,两个子包的版别号都变成了 0.0.1,并且lerna.json中的version也变成了0.0.1

Changes:
 - playground: 0.0.0 => 0.0.1 (private)
 - vue-pro-components: 0.0.0 => 0.0.1

咱们再试试加一行注释,模仿把一个包的大版别号变成 1 的场景,能够看到两个包的版别号以及lerna.json中的version也变成了1.0.0

组件库技能选型和开发环境树立

Changes:
 - playground: 0.0.1 => 1.0.0 (private)
 - vue-pro-components: 0.0.1 => 1.0.0

此刻,咱们再加一行注释,模仿引进一个 feature,再发起 minor 位的版别号改变,会发现仅有一个包的version变成了1.1.0,一起lerna.json中的version也变成1.1.0,而另一个包的版别号没有变化,这看起来还比较合理,因为咱们认为主版别号 1 以上的是相对安稳的版别,按需更新版别号是比较合理的。

组件库技能选型和开发环境树立

Changes:
 - vue-pro-components: 1.0.0 => 1.1.0

接着,咱们把两个包都改一点内容再测验一次。

Changes:
 - playground: 1.0.0 => 1.1.1 (private)
 - vue-pro-components: 1.1.0 => 1.1.1

做了这些测验咱们能够发现,lerna version做版别改变时,只会让咱们挑选一次版别,这一次挑选将作用到多个包上。

假定某次更新版别时,我期望一个包是 minor 更新,另一个包是 patch 更新,该怎样办呢?咱们持续往下看。

  1. Independent mode

Independent Mode 便是选用独立的版别号操控,会在履行lerna version指令时逐一问询各个 package 的新版别号,咱们能够经过修正lerna.json中的version字段值为independent打开这个形式。

当我只修正其间一个包,lerna version会提示我挑选一个版别号,这个版别号也将只作用到这个包上,其他的包不受影响。

$ lerna version
lerna notice cli v5.3.0
lerna info versioning independent
lerna info Looking for changed packages since v2.0.0
? Select a new version for vue-pro-components (currently 2.0.0) Patch (2.0.1)
Changes:
 - vue-pro-components: 2.0.0 => 2.0.1

接着咱们修正两个包的内容再测验一次,Lerna 会让咱们单独为每个包挑选新的版别号。

$ lerna version
lerna notice cli v5.3.0
lerna info versioning independent
lerna info Looking for changed packages since vue-pro-components@3.0.0
? Select a new version for playground (currently 2.0.0) Minor (2.1.0)
? Select a new version for vue-pro-components (currently 3.0.0) Minor (3.1.0)
Changes:
 - playground: 2.0.0 => 2.1.0 (private)
 - vue-pro-components: 3.0.0 => 3.1.0

也便是说,Independent Mode 下,版别号是各管各的,按需挑选

简略总结一下:在 Fixed Mode 下,lerna.json 中记录了各个包中最新的版别号。假如当时大版别号是 0,则修正任意一个包中的内容都会引起一切包的版别号更新;反之,仅更新变动的包的版别号。还有一个场景,便是持续挑选大版别的更新,也会引起一切包的版别号更新。总的来说,Fixed Mode 下,版别号捆绑性仍是很强的。而在 Independent Mode 下,各个包的版别号相对独立,需求开发者结合包的修正情况来手动挑选各个包的版别号。

个人主张:假如你的整个 monorepo 项目中各个子包联络性十分紧密,目标是对外供给一致的服务,那么 Fixed Mode 是一个不错的挑选,例如 Vue CLI,便是选用了 Fixed Mode。对用户来说,他享用的是整个 Vue CLI x.x.x 版别带来的才能,而不太关心 @vue/cli-ui@vue/cli-service 现在是哪个版别。假如你的 monorepo 项目中各个子包联络性稍弱,对外供给多种才能(比方 Lint 装备、Utils 东西、通用 Hooks、UI 库等等),那挑选 Independent Mode 则是一个不错的挑选,这种做法常见于企业内部,一般 monorepo 是作为整合多种才能的一个重要东西,既能在各个子包之间完成一部分复用,又能单独对外供给输出才能。当然,任何事情都不是一成不变的,假如你对版别操控欲很强,也能够果断挑选 Independent Mode。

我这儿挑选的是 Independent Mode。

子包之间的引证

对版别战略有个大略的知道后,咱们给子包之间树立一点联络,感受一下 monorepo 最大的魅力。

咱们先在vue-pro-components子包写一个简略的组件Icon,无需真正完成图标组件,仅仅用来测验一下。

<template>
    <i>{{ icon }}</i>
</template>
<script>
export default {
    props: {
        icon: {
            type: String,
            default: '默许图标'
        }
    }
}
</script>

一个大略的目录结构大概是这样的:

组件库技能选型和开发环境树立

因为 Vue3 组件需求用到结构依靠,咱们需求在package.json中声明一个peerDependencies

"peerDependencies": {
    "vue": "^3.2.0"
}

然后在项目根目录的package.json中加一个一致装置依靠的脚本。

"scripts": {
    "bootstrap": "lerna bootstrap -- --hoist"
}

接着履行这个bootstrap指令,

yarn bootstrap

咱们能够发现,在根目录中呈现了node_modules目录,而在各个子包中没有呈现node_modules,这是--hoist在起作用,将依靠提高到了根目录,能够节约一部分空间。

咱们还注意到,vue-pro-componentsplayground两个包也呈现在了node_modules目录中,实践上它们是软链接,链接的源目录是packages目录中对应的子包目录,这样的目录结构符合 Node 的模块加载战略,于是子包之间就能够像运用一个普通的 npm 包相同互相引证了。

软链接便是 symbolic link,类似于 Windows 体系中快捷方式的概念。可是在 Windows 体系中, workspace 的详细完成并不是快捷方式,而是选用了 junction。

组件库技能选型和开发环境树立

lerna bootstrap不仅为各个 package 装置了自身的依靠,还将各个 package 以 symlink 的方式装置到了node_modules中,让其他 package 具有了引证自己的才能。

接着咱们试着在playground子包中引证一下vue-pro-components子包的组件 Icon

  1. 首先需求将vue-pro-components作为playground子包的一个依靠。
lerna add vue-pro-components --scope=playground

组件库技能选型和开发环境树立

  1. import 引进组件并运用。
// 1. script 中引进 Icon 组件
import { Icon } from "vue-pro-components"
// 2. template 中运用组件
<Icon icon="icon-up"></Icon>
<br>
<Icon icon="icon-down"></Icon>
  1. 预览作用。这需求把 playground 这个子包的开发环境跑起来,也便是要履行它的dev脚本。为了便利起见,咱们能够在项目根目录的package.json中加一个playground:dev脚本,这儿用到lerna run,它能够根据scope选项履行某个子包的脚本。
"scripts": {
  "bootstrap": "lerna bootstrap -- --hoist",
  // 加入这条脚本
  "playground:dev": "lerna run --scope playground dev"
}

这样,咱们就能够直接在根目录直接跑 playground 的开发环境了。尽管 Icon 组件还没什么太多的内容,可是咱们能够看到,playground 子包现已能够顺利引证 vue-pro-components 子包的组件了。

简略测验发布到 npm

整个组件库的工程装备一股脑说完,也是很难吸收的,咱们先来点简略的,也是最重要的一步,把组件库先发布到 npm 上。

首先你需求有一个 npm 账户。

组件库技能选型和开发环境树立

有了账号后,能够来到你的项目工程目录下,经过终端登录 npm,能够输入npm adduser或许npm login进行登录。

npm adduser

假如登录失利,考虑你的 registry 是不是正确,假如用了国内的 npm 署理,主张登录时带上--registry=https://registry.npmjs.org/参数。

登录成功后,就能够试着发布你的 npm 包了。npm publish 能够发布包,可是在 lerna 项目中,咱们能够用 lerna publish替代。

一般,咱们会在项目中经过.npmrc或许.yarnrc装备一个国内的 registry 署理,加速装置依靠的速度。可是在发包的时候,咱们仍是要发布到 npm 官方的 registry 中,所以就需求给 lerna publish 装备一个 registry 参数,告诉 lerna publish 发布到哪个 registry 中。

咱们修正一下lerna.json

组件库技能选型和开发环境树立

一起,还有一个当地需求修正,那便是vue-pro-components子包的package.json,需求将其publishConfig.access字段设置为"public"

组件库技能选型和开发环境树立

从上图咱们能够知道,假如一个包是 scoped package,也便是带命名空间的包,例如它的包名是@vue-pro-components/utils,关于这样的包,假如不设置access"public",是不能公开发布和装置的。尽管咱们发布的这个 vue-pro-components 不是 scoped package,可是为了养成一个好习惯,咱们仍是给它设置一下access

组件库技能选型和开发环境树立

接着,咱们在根目录package.json中增加一个脚本,便利咱们进行发布操作。

lerna publish from-package --yes

组件库技能选型和开发环境树立

咱们试着履行这个publish:package脚本,假如能看到下面这样的信息,就表示发布成功了。

结语

截至到现在,咱们只是在组件库开发环境树立上做了一些大略的测验,对一些关键节点做了验证,整个项目仍是处于一个十分粗陋的状态,可是读者们也不必担心内容的丰厚度,跟着专栏后续内容的深入,一些工程化装备(包含 TypeScript)也会慢慢完善起来。假如您对我的专栏感兴趣,欢迎您订阅重视本专栏,接下来能够一起讨论和交流组件库开发进程中遇到的问题。

专栏下篇文章传送门:实战案例:初探工程装备 & 图标组件热身

技能交流&闲聊:前端司南