基于Vite打造业务组件库(开篇介绍)

7,622 阅读12分钟

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

专栏下篇文章传送门:组件库技术选型和开发环境搭建

专栏介绍

大家好,我是 Tusi,最近在写 基于Vite打造业务组件库 相关专栏,欢迎读者们关注 我的专栏 一起交流学习。

我的专栏聚焦于如何搭建一个 业务组件库,并不会讲述怎么从 0 到 1 搭建一个类似于 AntDesign 或者 ElementPlus 这样的基础组件库,因为各个社区中这样的课程或者专栏也不少,大家从这些专栏中可以更详细地了解怎么造一些基础组件,进而掌握建设组件库的方法。

同时也是因为我认为 没必要重复造基础轮子,在我看来,你很难做出一个Button组件,它会比 AntDesign 的Button组件的设计更完美、更好用,这并不是意味着不可能做到,只是这种投入跟最后产出的价值是不是能匹配,用圈内的话术说就是,“你做的事情,他的价值点在哪里?你是否做出了壁垒,形成了核心竞争力?你做的事情,和 AntDesign 团队的差异化在哪里?”

🐶狗头保命

没错,站在公司或者团队角度,Leader 重点关心你做的东西是不是真的对业务产生了价值。当然,这是我站在个人冲 KPI/OKR 的角度来思考这个事情的,这种造重复性基础轮子的产出,基本上很难得到 Leader 的认可,对 Leader 来说,他想的很可能是,“你为什么不直接用 AntDesign / Lodash / Dayjs 等等呢?为什么要做个差不多的东西,但是质量还没别人高?”

换个角度,其实对个人来说,从头到尾搞一次基础设施建设,是会有很大提升和收获的。如果你想要深入某个领域,不 彻底钻进去一探究竟 是很难发现那些开拓者在落地的过程中都遇到了什么困难(踩了什么坑)以及解决这些难点都走了什么样的路子(也就是技术方案的演进路线)。

那么什么是有价值的产出呢?我认为有这么两个方向值得我们探索。

  • 第一,还是造轮子,造新的并且能落地的轮子,新轮子是有价值的,因为它不具备重复性,暂时也没有替代物,甚至它的出现可能影响技术社区的发展。
  • 第二,创造附加价值。在现有轮子的基础上,塑造一个更强大的工具。我建议大家可以从一些门槛比较低的方向入手,比如基于现有的工具链,整合出一个贴近实际业务的 CLI / 可视化工具,用于提升开发效率;或者如本专栏主题一般,基于已有的三方基础组件库打造一个服务于业务开发的专用组件库。

回到正题,既然本专栏讲的内容不是从 0 到 1 搭建基础组件库,那是不是意味着学习专栏过后并不能掌握搭建一个类似 AntDesign 这样大而全的组件库的核心方法呢?

答案显然是否定的。组件库的搭建理念都是类似的,是殊途同归的,最终从输出的产物形式上看都是要满足诸如 按需加载兼容各个模块规范Typescript类型支持Unplugin 这类的核心诉求,而组件的内容丰富度是其次考虑的,提供Button, Icon, Table, Form 这些组件与否,都不影响这个组件库的核心架构,即便你后续想要自行实现这些基础组件,随时都可以开始。

最后,在我看来,一个采纳了业务组件库的应用,它的整体架构可能会呈现出这样一种形态:博采众长,提升效率的同时也不会束缚住开发者的手脚,不会限制开发者局限于使用某一个特定的 UI 框架,这在按需加载的支持下是可行的。

image.png

在本专栏中,我会基于自己在多个项目中摸索总结得到的实战经验,带着读者一起探索组件库从设计到开发,再到自动化发布的全链路流水线。本专栏大致会围绕 设计思路组件开发实战构建打包流程发布流程文档建设等方面入手介绍,把组件库研发链路的一些关键节点讲清楚,帮助读者掌握基于 Vite 构建现代组件库的核心方法

前言

UI组件库 对前端工程师而言,早已不是一个陌生的概念,BootStrap, Material, AntDesign, Element, Vant, iView,以及最近某些大厂开源出来的 AcroDesign, TDesign, Semi Design 等,这些名词对我们来说都应该不算陌生,甚至可以说日常工作中我们都至少需要与其中的一员打交道(大佬略过)。看多了组件库,自己动手写一两个小组件自然也不是什么难事,毫不夸张地说,95%以上的前端工程师都敢拍着自己的胸脯说:“我会开发组件!”

我们认同组件开发简单易上手,但同时也不得不承认这一切都建立在不断繁荣发展的前端框架和工具链生态圈之上!

伴随着 前端框架构建工具UI/UX设计理念 的更新换代,UI组件库的发展也是日新月异!确实,在现代前端框架的加持下,开发一个组件的门槛很低,开发者只需要使用框架提供的 声明式 语法,约定好 输入输出,将组件内部 逻辑 组织好,一个组件就有了雏形。

父子组件模型.png

然而门槛低不代表开发组件就很简单,开发优质的组件实际上非常考验开发者的综合设计能力。

组件设计基本原则

那么开发一个组件到底要考虑什么呢?结合我自己的实战开发经验,我想大概会有这样几个方面:

清晰明了的输入输出

输入输出是由组件提供的功能决定的,它们就像是一个组件的 用户使用手册,决定了用户对组件产品的第一印象。开发者应该提供语义化的、简单易懂的 API,降低使用的门槛和心智负担!

高内聚,低耦合

高内聚显得很好理解,在编写组件时,我们的出发点都是把逻辑聚合到组件中,基本上是竭尽所能做到内聚,但是这也非常考验个人的逻辑抽象能力。

低耦合,则体现在不要与外部产生太多的联系。用函数思维来说就是:组件尽可能是一个纯函数,不要对外部产生副作用。

当你在提供通用组件时,应当尽可能不显式依赖外部状态,比如依赖 Store, Context 之类的 App 全局状态。如果实在有需要,可以尝试通过 props 或者 inject 之类的渠道注入到组件中。类似地,也不要显式依赖 Storage, Cookie 之类的浏览器存储,因为这很容易产生冲突,对 SSR 也不友好,在必要时,稍微靠谱的方法是加上命名空间。

// bad case
const store = useStore(key)
// 依赖了全局状态
const innerState = computed(() => store.state.xxxModule.xxxState)

同时,不要在组件中破坏外部状态。举个栗子,假设你为了实现某个功能而扩展出一个原型方法,直接把 Array 或者 Object 的原型给修改了,这就属于破坏外部状态了,也就是对外部产生了 副作用

// bad case
// 为了解决这个问题,我决定给 Array 原型加一个神奇的方法
Array.prototype.blingbling = function() {
  // balabala 一堆代码
  console.log("反正就是牛逼地解决了这个问题")
}

要知道,考虑到Object.defineProperty无法处理数组场景,即便 Vue2 为了实现数组的响应式特性,也没有直接修改Array.prototype上的原型方法,而是采用了一个巧妙的方式处理,具体可以看 Vue2 源码里的core/observer/index.js中的 Observer 实现以及core/observer/array.js中针对 Array 的特殊处理。

可定制,可扩展

一个组件要想支撑大量的业务场景,必然应该是可定制和可扩展的。我们在开发一个组件时,会预设一种最常见的场景,这种预设一般是基于大量的实际案例总结出的经验,大概意思就是:当大家想到使用这个组件时,大部分人能想到的模样就是我预设的这个样子,这就达到了一种拿来即用的效果,大部分人都基本满意。

但是,满足了 80% 的使用需求,也不代表全部,你必须足够包容。用函数思维来看,就是组件跟随用户输入的条件的变化而变化。

image.png

反映到组件上,最基础的做法是:可以通过Props接收用户的条件,然后在模板或者逻辑中,根据用户的条件呈现出不同的效果。

甚至,我们可以将部分内容的渲染权限完全交给用户,在 React 中,一切皆Props,自定义渲染也只是通过Props传递过来一段渲染函数。而在 Vue 中,具体的实现可以分为插槽、作用域插槽等。使用形式并不重要,背后的原理都是相通的。

友好的告警提示

告警信息在框架源码中会很常见。当你的使用方式与框架的指导性意见不一致时,框架内部就会通过一段判断逻辑给出一些告警信息,方便你排查问题出现的原因。

// vue源码中关于 mount 用法的告警
if (__DEV__ && (rootContainer as any).__vue_app__) {
    warn(
      `There is already an app instance mounted on the host container.\n` +
        ` If you want to mount another app on the same host container,` +
        ` you need to unmount the previous app by calling \`app.unmount()\` first.`
    )
}

在组件设计中也可以采纳这种做法,如果用户错误地使用了组件的某个属性,组件内部就应该给出友好的告警提示。站在 TypeScript 的角度来看,也能通过类型做一些约束,但是如果 TypeScript 的覆盖率不是很高,也很难考虑到所有场景,同时也没法做到兼顾运行时,所以在代码中留下一些必要的告警提示还是有必要的。

同时,我们可以注意到,这些告警信息都将环境信息考虑在内,仅仅会在开发环境中出现。在 Tree Shaking 时,这些不会抵达的条件分支代码,会被判定为 Dead Code 而被裁剪掉。

我们之所以费这么大劲做这种告警信息,就是为了降低用户使用时的心智负担,用户很大程度上可以从告警信息中排查出问题起因。有了这些告警信息,就不用麻烦用户通过 debug 源码得以解决问题,毕竟每个人的精力都有限,阅读源码是最后的退路,非到万不得已就没必要去读源码。

组件文档

即便我们在前面做了这么多努力,也不可能完全解决用户的疑问和焦虑。此时,一份健全的使用文档则是对用户最大的安慰。文档建设本身是一项巨大的脏活累活,如何高效又全面地把文档做好,非常考验开发者的工程能力、技巧以及耐心。

完备的TypeScript类型支持

一个没有类型支持的组件,确实很难用。React 由于与 JSX 结合紧密,本身对 TypeScript 的支持度就不错,再结合 IDE 内置的 TypeScript 类型推导能力,即便我们不写太多类型声明,得到的开发体验也不会太差。

Vue 在这一方面相对处于劣势,SFC(单文件组件)本身就是一个新的 DSL(领域特定语言),默认情况下与 JSX 相比,其 TypeScript 支持度自然处于下风,特别在泛型组件等场景下显得更加乏力。但是,基于 @vue/compiler-sfc 官方提供的 parse 和 compile 能力,再配合tsc或者ts-morph之类的工具,我们也能给 SFC 提供不错的类型支持。

配套的工具链

为了更大程度提高 DX(开发者体验),组件开发者还可以根据实际情况提供一些配套工具,目的可以是提供代码的自动补全/智能提示之类的能力;也可以是提供脚手架工具,以便快速搭建起开发环境。形式上,可以选择 CLI,或者是 IDE 插件等。总之在这方面还有很大的想象空间等着大家去发掘。

总结

这是一篇 Vite 业务组件库专栏的开篇介绍,本文首先阐述了我在做专栏选题时的初衷,也简述了本专栏接下来写作的一些着重发力的方向。接着我分享了自己在做组件开发过程中总结的一些组件设计原则和经验,希望能给读者带来一些启发和帮助!如果您对我的专栏感兴趣,欢迎您订阅关注本专栏,接下来可以一同探讨和交流组件库开发过程中遇到的问题。

专栏下篇文章传送门:组件库技术选型和开发环境搭建

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