【第2303期】如何从流程上设计可持续维护的组件
前言
今日前端早读课文章由百度@郑亮亮授权分享。本文系前端早早聊第十六届组件化专场分享。
@郑亮亮, 百度前端程序猿, 曾在 Atliassian 做过打工人, 像素强迫症晚期患者, 并对代码有一定的洁癖. 对任务管理, 团队协同和知识沉淀等领域兴趣极高, 在个人所在小团队充满热情地尝试各类工具和实践, 不断探索各种提效方式以及基础建设. 由于业务和团队特性, 在过往的工作中, 发现基础组件库建设总是趋于不可控, 遂与团队小伙伴一起研究根源问题, 意识到 "组件库建设核心在于规范建设和管控问题", 并与设计团队下决心重新梳理组件化的流程与实践, 目前历经将近两年在众多产品中运行良好, 因故有此分享.
正文从这开始~~
一、分享概要 (tl;dr)
本次分享,旨在讨论组件库建设中的“可持续维护” 问题。
主体内容围绕以下的框架叙述:
交代背景 & 抛出问题:团队内情况的基础介绍
介绍了“涉及多方协同的复杂系统研发”的“不可控性问题”;
提供一个思路上的理解模型“不可避免的熵增” & 我们要做的“千方百计地熵减”;
技术线(提供讨论基础):在进入实际的非技术(文档 & 流程)的解决方案前,提供关于技术建设的一些介绍
提供了方案构筑法的思路(后面介绍文档化时需要到的概念);
并额外给出一些能提供“熵减”能力的技术保障手段;
非技术线(核心讨论点):介绍容易被忽视的“非技术线问题和解决思路”(The dark side of the moon)
用实际工作的模拟例子解释我们遇到的问题和挑战;
通过基础工具,文档沉淀,运作流程三个方面尽可能提供一些思路。
二、谁可能会觉得本分享有用?
如果你在负责(或作为 team leader 在跟进)前端组的组件库开发
并且,你的组件库提供给多个产品线在使用;
并且,你发现组件库工作出现很多的“魔改”,“版本分化”,而且你对这件事情很介意;
那么本分享可能适用于你;
如果你是以上的角色,但是并未发现这方面的问题,或没有这方面的困扰
那么有三种可能:
你可能还未遇到以上问题,所以也推荐可以大概过一下本分享;
你可能已经有自己的解决方式,如果是的话,希望你能了解一下本分享内容,可以在最后扫我的微信,我非常希望 & 乐于听到你的方案和想法;
你已知这个问题,但已经认定这不是一个问题或者不需要被解决,那么推荐可以阅读到“非技术线”之前的部分内容。如果你仍然觉得这不是一个问题,那么大概率本分享并不能提供任何帮助。
如果你的工作与组件库建设无关,但是你发现在工作中有如下的感觉:
需要多人协作完成的技术型工作,难以在理念上达成一致,缺少一种 teamwork 的感觉;
在项目推进中,项目的把控出现困难,如做的事情发散而难以收敛;
与相关协同角色,沟通成本高,如反复沟通类似的事情,由于沟通不清晰导致返工等;
那么,本分享可能值得你看一下(通过组件库讨论一些研发中的思路和技巧)。
三、基础介绍
业务情况
首先介绍一下团队基础的业务情况,我们是 ERP 方向的前端支持团队,横向上有 100 多个产品线,大多数是偏后台产品(当然不是说我们不注重用户交互体验,相反,我们是比较在意提升用户交互体验的)。另外,业务逻辑会相对比较复杂一些,因为是 ERP 方向往往这方面会比较复杂。我们希望我们组件库能够横向去支撑这些产品,然后提高我们的研发效率。
纵向上,每一个产品会去持续的发生一些需求迭代,但是人员跟项目的匹配关系需要考虑当时的资源情况,所以变动也比较大。另外,团队有一定的历史包袱。我们同样希望组件库能够跨越时间,纵向地去支持一个产品不同版本的研发工作。我们希望这种支撑是能相对稳定的,所以这也是为什么我们需要去考虑可持续发展的建设方式。
接下来是关于我们的组件建设的历史,在 17 年的时候当时团队没有一个比较统一的组件建设,这个基础设施缺失的问题很影响我们的研发效率,所以在 17 年的时候团队内部拉通开始建设第一版组件库。
由于最初业务研发跟组件库研发是同步进行的,所以压力比较大,第一版中有些东西做的比较糙。所以,在 17 年的 6 月份,在业务压力缓解时,开始启动 2.0 版本,这个时候开始追求质量,并且由于更加有计划和保障的投入,所以在 11 月份左右,2.0 差不多就比较完备。2.0 基本完备时,上升为 2.1 版本,并开始替换掉 1.0,同步地会持续去优化一些东西。
一开始是相当不错的,但在 2018 年下半年的时候,团队进入了一个失控期,估计可能不少团队都遇到过类似的场景。我们发现在一些业务设计上开始出现很多变种,而且越来越难以兼容。然后,在多个项目中,开始出现项目内大规模的魔改,并且有些项目搞着搞着就开始 fork 一个独立版本自己去维护,于是,后面就越来越难维护了。
所以,这里抛出一个问题:为什么系统性工作最后总是趋向于不可控的?
回答这个问题之前先抛出一个概念,就是熵。熵,是一个系统状态的度量属性,它代表一个系统的无序程度,有时候也可以考虑延伸地理解为一种“不确定性”,或者说是一种“随机性”。
根据热力学第二定律,可以得到一个重要结论:如果没有外部能量的输入,那么封闭系统会趋向越来越混乱,即,系统的熵会越来越大。
那么熵增的源头呢?它是来源自无约束的差异累积。而差异,源于一些随机性。接着,随机性又是源于团队中各个成员在认知上的不确定性,还有流程的不确定性。所以,可以认为“团队内认知 & 流程的不确定性”是熵增的根本源头。
举一个例子,我们现在开发一个日历型组件,有一个需求:利用组件选取一个日期。那么,这个交互的设计与实现,会遇到一系列细节问题,比如说 UI 布局怎么做,UI 样式细节怎么呈现,具体选取操作的交互细节怎么样?
把这种一个个的具体问题抛向不同的设计或开发人员的话,他们可能会有自己的一些思考,然后具化成一些解决方案,但我们能肯定的是,如果没有任何的规范或约束,那么这些决定都或多或少会存在(产生)一些差异。
更甚者,即便是对于同一个人,一年前的他,还有半年前的他,还有今天的他,在历史上不同的时刻,也有不同的个人的成长或变化。在面临相同问题的时候,不同时期的同一个人也有可能给出不一样的解法和决策。
所以,这些细节上的差异积累,最终会导致不可兼容的版本差异。由于量变最终(可能)会引发质变,最后产生的这一个“全新”的版本差异,可以断定几乎是不可兼容的。
比如说,以下是从一些不同的组件库里面,专门挑出来的日历型组件。直观上,感觉每一个组件好像都长得差不多,好像就只有一些很细微的 UI 上的不同而已。
但是,当真正用的时候,会发觉组件整体的交互存在差异,以及视觉在不同状态的呈现也存在差异,把这些都展开来看之后,能切切实实感觉到这些组件之间是互不兼容的。(从研发人员的角度来看,就是一种组件研发方案的不可兼容,或不可简单兼容)。
如果在上述问题产生,并出现不可兼容版本之后,再企图去收敛研发工作,将会耗费极大的投入。现实世界中当投入成本超过一定阀值,我们就不会再去做这种收敛工作,有时可能就会咬咬牙整体推翻重来,但更多情况下,更可能会放任其自由发展。
最后,就会得到一个非常有意思的类似生物物种进化的进化树,但这绝大多数场景下不是一个好事。
我们团队在经历失控期之后的一种思考是,我们如何去把整个基建工作做成可持续的呢?
我们开始了新的一个组件库的建设工作,叫 brick 组件库(好吧,正如上述,这是一种推翻重来)。在打造组件库时候,虽然是从新开始,但我们希望尽可能兼容和借鉴原有设计的优缺点,然后去用上一些业界较新的和较为稳定技术解决方案。但是,这次我们有更核心的理念,就是希望这一套解决方案能够比较稳定的去迭代发展,从而达到一个在研发过程中持续保持熵减(以对抗熵增)的过程。
接下去的分享会分裂成两条线,一条是技术线,这个会不会 Cover 整体研发的所有方面,但还是会给出我们的一些技巧点和基础思路的。另外一条线是非技术线,这个和技术线最后是希望在整体工作介绍上能形成一个思路上的闭环,解释如何进行熵减控制。
一些大概的展示图
一些组件设计(估计全世界差不多)。
技术线
项目全景图
第一部分是技术线介绍。
首先,看一个全景图,如无意外,基础组件库的架子都是八九不离十长的差不多。
首先,有一个基础底层负责构建 & 研发工具链 & CI 周边。然后,一些基础逻辑类库和底层物料,形成一个支撑上层组件研发的基础层。
由于是一个比较基础的可供换肤的 UI 组件库,所以样式会独立进行维护,可以理解为一个独立的样式层。再往上就是我们的最基础的单元 -- 组件集合。
与之对应的还有另外一条线,组件库原生必有的“demo site”,这个分为前端与服务端。
冰山效应
我们不会深入去探讨这个全景图,因为脱离整体历史发展去讨论一个项目片面其实意义不大,这里我们先介绍一个概念 -- “冰山效应”。
什么是冰山效应呢?可以举一个比较形象的例子,比如我们在实际的研发过程中,我们看到某个时刻的代码,某个时刻的架构设计,都只是一个侧面,这个只是冰山浮在水面上的一角,而在水面下还隐藏着更大的部分,比如形成这个冰山一角的庞大的理论依据,支撑知识,业务场景需求,以及这一整套在历史上的演变过程。
举一个我们在组件库中遇到的例子,比如说,在组件库里我们需要一个通用的 utils 库,这里选型是 lodash。一开始,在组件库中的依赖,很简单写 dependency: lodash 。
但是,由于我们希望能更好的去做一个 tree shaking,所以,改成了另外一个 lodash-es 这个 package,所以改为了 dependency: lodash-es 。
同时我们也希望在团队内做一个比较好的技术收敛,所以业务项目里面能够有这么一个统一的选型,并且正确去配置好 Webpack,所以把 dependency 改成了 peerDependency 。
但是,我们发觉有一些项目在引入组件库的时候总会报错,有一些是由于没有 lodash-es,有一些是由于版本较低,所以为了让组件库尽少报错,我们又把它改成 dependency: lodash 。
但是我们又发现,如果这么改的话,其实损失了一种非常好做技术宣贯的机会,而且,如果项目中如果本来就有 lodash 的话,很容易由于版本问题导致打包的时候会导致打包多份同样的东西,这对构建体量是一种损害。所以我们又把 dependency 就改成 peerDependency 。但是,这时候会合理放宽它的版本控制,并且在 post install 的环节,引入一个主动打印发现问题和打印出帮助文档 URL 的步骤。
可以看到,从代码形态上观察,几乎就是一行代码的变动(可能在加上一些主动检测和打印的逻辑)。但是,这么简单的代码底下,其实是一整个的思考和决策过程。同时,它还依赖于很多知识点,比如说这些提到的问题,是需要我们去做必要的调研,参考必要的文档或做一些实验看效果的。
当你在看到某一个现有系统某个小细节时,其实,底下可能有一大堆的东西。
所以,与其去分享一个最终形态,我们换个思路,分享一些具体的思路,这里分享两个思路:
方案构筑法;
技术保障手段。
方案构筑法
大家都知道怎么去“搭一个系统”,无外乎就是“搭一个基础骨架”,然后对它进行“填充和完善”,后面再像做产品一样对他进行持续的迭代优化。这属于道理都懂系列,但我们尝试来看看怎么来把这整个过程说的更具体和明确一些。
首先来看一个 soft development 里面的概念叫 Spike。Spike 是一种研发的方法,它起源于极限编程(XP),它的核心的思路,就是用一个最简单的方式,去验证一个方案的可行性。一句话总结,先别想那么多,撸起袖子就是干!
但这肯定是不够的,我们面临一个问题或需求,用 Spike 的方式找到一个所谓的“方案”,虽然它能提供基本功能层面上的验证能力,但是这终归都只是一个 demo 级别。
我们需要考虑的是,让一个一个独立的方案最终能够结合起来,或者说,让一个方案跟现有的系统能够结合起来。这时候会产生一个概念,叫做方案的集成。通过集成,可以看到(下图)这种感觉其实有那么一点像所谓的“骨架”了。
那么,我们来看一下这整个过程可以划分成几个阶段,首先从拿到一些需求开始(这么讲已经有点像是在做产品的感觉 😉)。这些需求可能是业务上的需求(业务需求映射到组件库上的一些需求),也可能是研发上的需求(如性能上的,研发体验上的)。
接下来,我们会经过一系列调研跟设计的过程,然后去推导出来各个需求的解决方案。然后,为了让这些方案能够实际地 work,我们还需要一个集成过程,形成一系列的最终的集成方案。
最后,根据我们的代码规范,编程理念,代码范式等,把这些方案最终映射成为我们实际的代码,就是我们的 code base。
由于整体研发肯定也是多次迭代的,接下来可以看一下考虑迭代演变的话会怎么样。比如说,在一个新迭代里面,你可以拿到一系列新的需求。这些需求同样地再经过调研设计,我们会得到一系列新的方案或方案的调整。然后,同样的也是经过了集成,还有代码映射,最后变成了我们最后的新一版的 code base,这样就完成了一个迭代的演变。
埋个小伏笔,这两个图待会还会再重新引用到,以解释其他东西。
题外插一个很形象的感觉,不知道大家有没有看过一个古生物形象还原的过程,已灭绝的古生物都是没活体的,一般我们只有一个化石骨架,然后根据各种原理原则去填充肌肉,通过肌肉的设定和物理常识等再去设计它的羽毛表皮,最后再去渲染出假想的效果。
这种思路有一个好处就是,经过这一个比较靠谱跟完整的流程,我们能得出来一些拆分得比较单元的东西,大多是可以横向去复用的(包括知识、规范、方案、实际的实现等),比如说你搭一个组件库,搭一个种子工程,搭一个其他东西,其实很多东西都是可以复用上的。
并且,有这个思路之后,你可以很容易拿到一个现有第三方的比较成熟的例子,按照这个思路反过来去理解和分析它为什么会演变成现在的状态,有利于套用里面可用的架子、方案、思路反过来帮助你搭建你自己的东西。
方案构筑法:一个基础搭建的例子
这里,举其中一个例子,我们挑组件库基础搭建过程的一个演化分支,作为 demo 的例子。
首先,第一个需求:作为组件库使用方,我希望能在在历史项目里按需去引入某一个或几个组件,然后,也希望在新项目能够以合包的形式引入(效率考量)。另外,我也希望在未来一些迭代研发的项目中,如果没有集中的研发投入而需要去独立维护部分的组件时,我可以仅升级其中一个组件,而不用升级合包导致所有东西都升级了
所以我们采用的分包的开发模式(Monorepo),分包的解决方案用的是 Lerna 这一个比较成熟的第三方解决方案。
按照这个思路,每一个组件都是一个子包,每个子包有自己的独立版本,然后根据一定的逻辑把部分子包合起来,合并成一个 Bundle(合包),比如说 main,合包也有自己的独立版本。
然后考虑另外一个研发需求,作为一个组件库,我们尽可能使研发规范化,尽量提高项目的研发体验和效率,所以选型 TypeScript。
还有就作为一个基础组件库,那么我们需要强有力的样式能力,一般常见的选型为 Less 或 Sass。但是,我们更看中 Sass 比较强大的逻辑计算能力,因为这样的话可以去帮我们去保留样式中的各种关联逻辑,并且,很容易进行语义化地定制样式模板。所以,选型 Sass。
还有,由于使用分包的这种设计,并且由于允许使用方能细粒度去管理组件依赖,所以,这个构建过程是一种 tranpile 的过程,不是一个 compile 过程。总包是以 npm 依赖的形式先引入收录的子包,然后再把这些子包整体 re-export 出去。
还有,由于我们是分包的形式,然后也由于我们是一个 UI 组件库,需要去支持主题的定制能力,所以需要分离逻辑跟样式(跟 Ant Design 是一样的感觉)。
然后,大家可以看一下 TS 还有 Sass 它们应该都各自有各自的构建线,组件跟合包也是沿用同样的思路。由此允许使用方在引入组件库样式的时候,可以去自行组合,不用受限于总包。
接下来,对于使用方的引入问题,我们初期得到的一个需求是:使用方希望同时有 CommonJS 还有 ES 两种形式可以引入项目。所以,一开始构建区分成 cjs 还有 es 两种模式,包构建产物分别输出到不同的 output(lib & es)文件夹,然后指定 package.json 里边的 main 还有 module。
接下来另外一个需求:作为使用方,我希望有组件库的 types,还有源文件。
因为,本身项目是 TypeScript 写的,所以对于使用 TS 的项目,使用方希望在项目里面也可以利用上 types。并且当发现问题的时候,有源文件方便进行本地 debug。
所以,在搭建过程中,我们就同时把 src 也同步输出出去,然后我们把 package.json 里面的 types 指定到 src。因为我们当时是想着说 src 本身就是 TS,一并输出的话,使用方就可以得到组件库的所有 TS 类型了。
但是,我们发现引入了一个问题,就是当引入 src 的时候,虽然说有组件库的类型了,但是由于合并是一种 re-export 的方式,所以对 IDE 的代码辅助并不友好。
比如说当输入 import {butto... 的时候,其实我们希望 IDE 帮我们提供自动补全,还有把后面具体引用的路径也直接补全了,但是实际上 IDE 并没能做到这一点。
经过调研之后,采用了另一个方案,就是直接把 src 用 tsc 构建出 *.d.ts 然后输出到一个文件夹叫 types。这些文件代表了组件库的类型,被输出到包发布内容里面去,并且把 package.json 的 types 指向 types 文件夹,这样,使用方就能正确使用 IDE 的功能了。
但是,又引出了一个研发体验问题,作为组件库的开发人员就不爽了,因为现在每个包都设置了自己的 types 文件夹,所以在 IDE 里面在研发的时候,跨包做代码导航总会出现问题。
例如,我们点击一个 Button,希望导航到 Button 的源文件,但其实 IDE 导航到 Button 的 button.d.ts 文件里头。
所以,在这里我们提供了一个命令行工具,在需要的时候可以运行该命令行工具统一将组件库的 types 文件夹软链到 src 文件夹,这样可以比较完美地解决这个问题。
还有,由于团队内比较多项目的选型是 mobx,所以我们希望组件库虽然是以原生的 React 组件在研发,但是能够有一种方案,可以把原生转换成原生支持 mobx 的版本。
所以,我们设计一个(类)jsdoc tag,然后开发了一个 babel plugin,去针对不同的情况(大约 10 种情况),把原生的 React 组件还有比如一些 model 对象去进行包装和二次处理。最后,产出一个原生支持 mobx 的组件版本。当然做这个方案的关键意义,就是这个方案是自动完成的,所以我们只需要愉快的开发原生 React 版本,然后用最少的维护,就可以把 mobx 版本也给自动生成了。
我们进一步聊一下刚才说到的其中一个需求,就是输出 cjs 的需求。在实践中发现,不少项目直接就使用了组件库的 cjs 版本,但因为 cjs 版本从原理上对 Tree Shaking 是不友好的。而我们组件库原生支持 cjs 事实上默认了这种方式的合理性。
考虑到,我们有义务要去收敛团队中构建的最佳实践。所以决定去移除 cjs 的版本,也就是说,我们原来的结构又变回了没 cjs 只剩下 es 版本的情况。
以上的一系列过程,估计大家都有很熟悉的感觉,它基本体现了我们在搭架子或者做任何的任务过程中一个迭代演变例子。很多时候,我们面临的一个问题就是说,不可能一开始就有一个完美的解决方案,但是你得有能力能从一系列需求和问题,运用一定的方式,还有流程,逐渐推导出越来越好的解决方案。
技术保障手段
接下去,我们要想一下,就是说我们是不是得有一定的一些保障措施呢?这结论肯定是的,我们先讨论一个技术层面上的保障措施。技术保障的思路就是尽可能让一些我们已经预设好的规范,不依赖于参与者的自觉,基本上靠人的自觉最终都是难以控制的(也难以规模化),正确的思路是尽可能它以工程化的某种形式融入到我们的研发过程中,用工程化的思路去提供这种保障。
今天就大概的讲一些(如下图)
先说一个技术收敛的问题,不知道有没有同学是讨好型人格,就是说我们一开始做组件库的时候,我们初心是很好,我们希望做什么?我们希望尽最大的努力做最大的兼容,让每一个使用方都非常的 happy,对吧?不是的,现实不应该这么做。
作为一个有节操的技术人员而言,我们应该持有一个观点,就是尽可能的收敛方案,然后推进一个最佳或者说一套最佳的推荐的形态,然后对于异常场景尽早抛错,还有尽最大努力去提供帮助,还有一些迁移与解决问题的指导。
那么,举一些技术收敛可能体现出来的方式:
还有,代码不应只是体现“逻辑部分”,它应该还有如下所述的表现:
比如说,dev warning 可以考虑以下的经验:
然后,我们讨论一下“规范化废弃过程”:
规范化废弃过程的一些经验:
还有,自动化测试:
自动化测试的一些思路:
这是一些自动化测试的经验:
另外,是 Sass,Sass 的核心思考是:让所有能关联起来且应该关联起来的因素,尽可能关联起来:
这里有一些技巧:
下面会讲两个比较具体的例子,首先第一个是 button 样式的一部分。
首先,用一个方式把 padding 的取值逻辑跟基准值 (padding base) 关联起来。然后,利用 list 还有 map 的枚举型的形式,维护 button 在各个形态下的相关参数。使用了模板方式,把这些东西关联起来,通过遍历每一个 button 不同形态,样式产出。这是一个很简单的例子,但是应该挺能说明问题的。它的关键点就是每一个部分内容在梳理好关联关系之后,互相之间干扰不多,而且未来容易维护。
然后说一下颜色,颜色的维护其实不是想像中那么简单,比如就一个颜色给一个 HEX 值或设置个变量,而是会有一整套的关联体系。
比如,首先我们有一个基准颜色的列表,然后会根据一系列设定好的颜色生成函数,生成一个预设的基准的色板。比如说蓝色第三阶蓝色,可以用 color_blue 这个方法从色板预设值中取到。
接下来,我们会考虑设计场景语义化的一个规则,通过这个规则我们可以产出一系列的这种语义化的取值函数。比如说 colorsuccess,colorbrand 之类的。
好,接下来说另外一条线,另外一条线就是说基于语义化的话,我们会订一系列这种颜色变量,然后比如说一个强烈的背景主颜色就是 color_brand(8)(主题色的第 8 号),对于组件的颜色变量(呼,我们终于到这一层了),它是一个具体的某一个地方语义化变量,最终才给组件样式去使用。这个是一个大体的过程,主要要说明其实所有东西都是有关联,并且应该都是串起来的。
还有,内容型脚手架也值得投入,推荐可以去看 plopjs。它可以让我们将一些重复性的繁琐工作打包成为一个整体的自动化任务。
比如说在我们项目里面,可以很方便的通过这一个脚手架去创建一个组件子包,创建一个 React 组件,然后可以去根据一个组件去创建一个完整 demo 页面,再去独立地创建一个 demo case,还有能快速的把某一个组件追加到某个合包里面,这些都可以考虑用自动化的方式去实现。
linting 的话就不详细展开,因为大家可能接触比较多。但这里说几点,首先,(如果你团队没有约定好的配置)推荐从一个开源的较严格的规范开始,然后向下去探索团队 linting 的规范。还有,linting 应该和某个研发环节需要去挂钩,比如说 git 的 pre-commit。或者说如果团队有 CI 平台的话,里面可以配有一个 linting 的过程。
还有一点挺有意思的,就是说我们之前发生过不同的同学有不同的 IDE 代码格式化配置,导致代码格式风格出现多次“抖动”,这里推荐可以考虑配 .editorconfig,或者 prettier 来避免这种情况。
非技术线
The dark side of the moon
先来看一个刺激的故事:
The dark side of the moon:这里指代的是我们非常注重的技术工作后,通常被忽略或无视的非技术性的组织工作和协同工作。
组件库的核心
首先,我们立正,端正一下态度!
所以,重新思考建设一套组件库的难点所在:
非技术线的考虑主要分为如下三个方面(同时,由于时间限制,我们仅有限的讨论其中一部分问题):
基础设施
首先,的有功能上较为优秀的基础设施,按类型分有如下几种关键的:
及时沟通工具;
在线文档协同平台;
任务管理(项目管理)系统;
在线云盘(管理线下文档)。
这一节我们不展开说,毕竟每个团队的情况和能使用的工具集合各不一样。
文档沉淀与管理
我们先讲个故事。。。
这个是个欢乐而又悲伤的故事 😂 。
这个故事引出的思考点,同样是很多团队应该考虑的一件事情,就是更多时候我们应该考虑从“基于人去运作”,转变成“基于文档还有流程去运作”。但是同时,我们能利用文档跟流程的能力去放大人的能力,还有借助团队每一个人的能力来推进我们的工作。
所以,文档是什么呢?用一个比较形象的比喻,就是我们可以把它看一个装满牛奶的水杯。
水杯,代表一个具体的文档,它是一个容器。然后水杯内装的牛奶,是具体的文档内容,也即是其形式内容。而真正有用的东西是在于牛奶的营养,水杯装的溶液的溶质,亦即是信息。信息是用来干嘛?信息是用来消除不确定性,它要做的事情就是熵减,这呼应了我们刚才说的一个非常重要的观点,在整体的系统性工作中,我们要做的是熵减。
文档有两大关键的意义,第一个是帮助梳理思路(消除个体对于事物的不确定性),第二个是传递信息(消除团队中对于事物的不确定性)。
但是,我们需要比较现实一点,就是任何人客观上是不可能(也没有实质价值)去遍历所有的文档。我们目的不是要去建立一套文档体系让所有的参与者都能(或强制)去阅读所有的文档。
事实上,我们是要去建造一个文档体系,让它能够自解释,自洽的存在,并且能渐进式地,像深度懒加载一般满足项目相关的所有角色和个体的信息获取需求:
文档的难点
但这种文档化建设往往很难,比如如下的几个常见问题:
我们会尝试从“写什么” 和 “怎么写” 来阐述如何进行 “文档沉淀与管理”
写什么
先抛出第一个概念叫拆分,拆分是什么呢?拆分跟我们做组件库研发的思路是一样的,文档建设本身首先你要考虑有一个拆分的理念,它有点像是文档的组件化建设。首先,你把一些知识点还有一些经验方案拆分成可复用的单元。
这个拆分有几个技巧和原则,首先你要有一个做拆分的规则,利用这个规则拆分出来的文档单元需要足够清晰,尽量限定其主题还有范围。
其次,跟研发的一个原则是一样,DRY,do not repeat yourself。遵守 DRY 这个原则,要求我们考虑尽大可能的去提升文档的可复用性。
还有一个重要的思路,就是文档要解耦地写,尽可能去把文档写成 “不容易由于任何一个因素变化而需要大范围更改”,就是说一个文档所表达的方面,尽可能跟其他方面能够解耦合,否则你永远拆分不了,也难以真正复用。
回到刚才方案构筑法的演示图。这个东西有什么用?大家可以看一下,其实图示中的每一个 “点”,其实就可以和我们上述所说的 “拆分” 联系起来。
即每一个特定的点,即可考虑拆分成一个基础的文档单元,并沉淀到文档系统中,同时这个项目的运作也应该受益于已有的文档系统,借助这个系统反过来指导整个过程的运作。并且,这么做的话,其实大多数文档是可以横向去复用的。
那这些东西到底是什么?
我们来看下一个图,就刚才方案构筑法的另一个图示。
可以看到,所谓的需求点其实对应的就是需求文档。调研跟设计阶段,这个看起来有点像一个过滤器一样,它其实就是代表一些列关于“调研与设计阶段”的高阶知识,也就是说你可以通过实际的方案制定过程中,得出一些调研方法,设计模式、交互理念等等这些高阶知识。
然后,方案这些,可以等同于一些相对比较面向领域的方案文档。还有方案后的集成阶段,你又可以得到一系列这种高阶知识,系统设计怎么做?模块如何设计?常见的一些实际的技术集成问题是什么?等等......
还有,集成方案就是代表具体项目中的集成方案。最后,这个代码映射,其实它代表着一种关于编码的高阶知识,你的代码模式是什么?你的代码规范是什么?还有对应的项目结构设计是什么?
拆分之后,我们讨论“再组织”。先说一个简单的观点,以上所述的这些东西,每一个其实都能划定一个比较清晰的范围写出相对独立的单元文档,然后通过超链接的形式,跟其他的部分进行结合,最终成为一整套能很好地诠释 “整体系统设计” 和 “项目运作” 的文档体系。这个文档体系落地,的同时也同时在底层产生了一套 “知识框架”。
关于“在组织”有几个比较重要的点。
首先,我们要做好这种导航的作用,比如说树形的这种层级划分,然后读者从哪里入口去找到他要的文档,这是入口管理很重要。然后在每一个分叉的路口,需要树立一个“路牌”介绍说你接下来你可以看到什么东西,你期望看到什么东西,这是一个导航作用(让读者用较低成本感知接下来读的东西,时不时他所预期的)。
还有,我们应该可以利用不同的角色和场景设定,用不同的方式去做多样性的索引,把已有的内容根据不同的场景串起来。所以比如说,by 角色来看,组件库的高级研发人员,刚入职的新手同学,或者说只跟进某个特定组件的研发人员,他们其实要看的东西是不一样的。这个其实并不一定需要产生新的文档内容,而是要做到尽可能的复用。
另外,如果用的文档系统有标签分类能力,那么尽量的利用起来。
还有一个思路是 FAQ,就是 frequently ask question(常见问题列表)。这个是非常优雅的一个 trick(技巧),比如说基于某个特定的问题,有人提出来了,说明这个问题,包括这个问题的周报(比如说相应的规范、理念、知识等等)存在市场需求。
这其实是一个非常好的切入点,我们可以围绕着这些常见问题作为切入点,然后去整理我们的文档内容。
如何写
接下来讨论如何写,首先,必须要让大家感受到,文档写的好不好,是有质的差异的,例如以下的例子 #1:
上述一大段话,大概说每天上午闹钟响了,小明起床后的一系列事情和活动。大家看这一段文字第一直觉什么?要找信息很难,很蛋疼。
如果,我们根据语义多提供几个简单的回车,注意只是简单的回车噢!你会发觉这两个文本之间,虽然承载的信息是完全一样的,但是阅读起来的效果就已经开始很不一样了。你会发现第二个应该是至少清晰了很多。
这个还可以再进一步,整理成另外一个类似提纲的形态,大致也是提供同样的信息。
这次会发觉,不仅阅读简单很多,包括未来重新定位找信息也会更加简单。甚至,你看着它,有时候你都不用去看它的实质内容,你的大脑马上会有大概的一个 “内容框架”。
所以说,所谓的如何写,真的是一个我们需要考虑的问题,并且要找到如何写的技巧。
那么我们分享一些写好文档的原则与技巧:
这里特别提一个点,在阐述某一些内容的时候,不仅要把目标内容说好说明白,你可能还需要考虑加上有一些额外的解释内容(有点像旁边?),比如说,附加信息可能是某个观点是谁提出来的,什么时候,什么场景下,它的确信程度如何,它当前的版本信息是什么。
还有要认识到写文档,不可能一开始就写很完美的文档出来。我们需要考虑用一种渐进式的方式去维护文档,比如说一开始没内容不要紧,先把 to do 写好了(加一些大致的临时思路),然后接着大概的框架写好了,收录把参考到的外部文章留下来,然后随时可以把新的设想写下来,或者说如果你在线上跟别人讨论的话,你现在没时间,你甚至可以把这个线上讨论的对话内容,直接先 copy 下来,回头再整理。
关于流程与人
所以,我们还需要引入另外一个维度,就是人。在整套系统跑起来的时候,人才是至关重要的核心,也就是说我们作为项目组成员,受影响于已有的文档,同时我们是整套方案的执行者,而且我们在执行的过程要同步去维护文档。
讨论人的话就是讨论流程了,这里我们要抛出一个最重要的观点是,我们要做基于文档的协同跟沟通,比如说我们做这件事情的话,尽可能去基于文档去展开讨论,然后当你做讨论的时候,无论是线上或者线下,应该文档化,然后如果没时间,你可以考虑渐进式文档化的这种思路。
还有当你跟别人沟通回答问题的时候,不要一大段一大段文字去打字,尽可能去复用上已有的这种文档,分享出链接,然后抛出去给别人读。
还有像极限编程的 code pairing 的概念一样,你可以考虑 doc pairing,就是说有时候写文档挺难的对吧?我不知道写什么,这时候我拉一个人,我跟他说我要写这个主题,你问我问题,或者说我问你你对哪些感兴趣吗?他说这个问题挺有意思,我就回答你。边提问边回答,一问一答的过程中你要写什么东西,自然就出来了。
还有,有一些时候主研可能任务比较繁重,对不对?你可以不止给他配备研发的副手,你还可以给他配备一些文档的副手。
另外就是技术跟方案的评审,尽量不要开那些就一两次的这种看上去 “看似开了,但是其实不痛不痒没什么卵用的评审会”,尽量是文档去结合我们的技术评审,还有方案评审。
还有,在流程里面非常重要的一个常见问题是冲突。
冲突流程我们应该是这样的态度:基线是要鼓励不同的声音,然后我们应该有明确的透明的冲突解决机制。还有,当发生一些有争议的问题的时候,我们有能力去把这些争议性问题快速地去实验性推进,然后获取反馈再回来进行调整。或者,做着做着我们有新的想法了,有能回去再调整。
如果,当有实在不可调和的问题的话,我们可以把问题升级了(大家都得明确有这样的流程,就是说当冲突双方解决不了的时候,不是说吵架,也不是默默的把某个东西给改了)而是要把它升级了,比如说,让更有经验的同学或更高级别的同学来帮忙看这个问题。
假设最后实在不可调和,那么可以采用投票机制,这个是一个冲突解决的客观而机械的流程。这个流程,看起来虽然没什么,但他最重要的一个核心就是至少有一个最后的方式,让大家先认定一个方案,以便我们暂停学院派争辩,先去推进事情。
文档维护的话我们应该是这样子。
首先,要鼓励所有的成员,让他们说我找不到什么,或者说我看不懂什么。要让大家都知道,你说出这一点不是因为你笨,更有可能是因为文档写的不好,或者是维护的不好。在一个团队里面,如果大家把一个人找不到什么,看不懂什么,把他跟他的智商联系起来的话。大家最终就听不到这样的声音,也就是说这些声音有可能就变成 “房间里的大象”(The elephant in the room)一样。而如果没人发出这些声音,团队也就丧失很多的这种机会,可以去改善文档。
还有,要鼓励大家遇到问题,要去顺手去改正它,而不是忽略它,或者说至少不是直接跳过它而是抛出疑问。但是,有一个前提就是,当你改了之后一定要在相应的群里面去提醒大家说 “我改了什么,大家注意一下”。最后,就是文档应该有比较规律的 review,尤其是一些比较核心的文档,如果当你发现一个文档过时了,大胆的把它标记成过时,并且在群里吼一声。
例子 #1 :文档的拆分 & 再组织
举例,比如说我们的组件库文档有一个入口索引条目(这是我们自研的一个文档工具)。然后条目会有一些,比如说理念宣贯,一些不同的这些方案的入口(链接)等等这些。
然后举例说,其中比如说像刚才提到 peerDependency 的一些讨论,会作为一个独立的文档维护。我们在这边就有相关的一些展开的讨论,并且把当时当刻的一些现有的犯罪现场保留下来了,方便大家去理解为什么这么演变的。
然后,我刚才说的拆分成一个单元文档,比如说这个条目抽取出了关于 peerDepedency 的单元知识文档,但这个单元文档其实跟我们所面临的这些业务的问题是解耦的,只不过,我们可以很方便的用链接把他们关联起来。
然后,我们又可以把相关的单元文档,又收录到 NodeJS 的主线建立出来的一个索引文档,把这些业务中产出的有用的被验证过的这些知识单元又更有意义地收录起来。
例子 #2 :基于文档的讨论
还有,我们是怎么基于文档去和设计师进行讨论的?大家可以看到,这也是我们一个 todo 工具,它是一个树状形式,就是带层次的。我们会根据不同的组件去划分设计和研发中的问题,然后围绕这些问题,我们讨论的点都会沉淀下来。
然后这么一个静态图可能看不太出什么,大家可以看,我做了这么一个简短的视频( 这里可能需要去看原分享视频因为此处没有动图 ),你会发觉展开后,整体研发涉及的东西特别多,演示到这大家应该能感受到了。然后,这样做有什么好处呢?
这样做的好处就是:
首先,比如说你在讨论的时候,有一些同学可能当时没空对不对?但是这个讨论已经发生了,这些记录回头还可以让没能参与讨论的同学来帮忙看一下这个结论对不对?它可以马上获取到一个问题,它的一些 context。
然后,这东西延续下来之后,未来我们可以很方便的去找到当时当刻某个组件,某个问题为什么是这么设计的。而不是说有个同学又遇到相同的问题,但他不知道为什么是这么设计的?然后,他又会反过来问你说,为什么我们不那么设计?而其实你会发觉,那么设计的考虑点,其实以前已经讨论过,而且甚至有可能在业务上已经遇到过一些问题,所以我们不用它了。如果没有这个东西的话,你的工作有可能就浪费在很多的这种不停的回溯解释对同样问题进行试错,不停的去做一些无谓的重复的尝试等等。
结语
结余的话,快速说一下就可以了,就是说我们在很多的这种大型的系统性的这种工作,如无意外会出现熵增,然后我们作为研发人员技术人员,就是需要去做一个熵减的工作。然后我们讨论了方案构筑法,又提供一系列的技术保障手段。
然后我们讨论了文档 & 流程,比如从方案构筑法来讨论一种拆分成独立文档的思路,然后这些拆分出来的这些单元文档,我们又讨论它们怎么被再组织起来。最后,强调了人是最重要的核心要素,我们如何基于整体系统去进行运作,我们的流程是什么,这个就是今天大概的一个分享框架了。
那么,在这里非常高兴说有这么一个机会,给为组件库贡献过的可爱的同学们点个赞。真的,没有大家的努力,我们是做不成什么事儿的。然后,就不一一念出来了,希望大家能够收到这个由心的点赞!
关于本文 作者:郑亮亮 原文:https://juejin.cn/post/6968647527239254029
为你推荐
【第2212期】滴滴开源 LogicFlow:专注流程可视化的前端框架
欢迎自荐投稿,前端早读课等你来。