先放一张 winter 对于前端从技术层方向的定级标准:
当然这个定级对于不同的公司也会有相应的偏差,但是可以看出掌握组件开发是通向高 T 前端的必经之路,毕竟从一亩三分地跨越到了前端工程化的领域。
我作为掌门面试官,面试过不少的候选人,在面试的时候能将组件开发的工具链及注意点讲清楚的寥寥,所以今天就带大家一起从设计思维、工程思维和产品思维三个维度概述一下题目组件在掌门落地的思路。
当然了这远远不是看一篇文章就能掌握整个组件开发体系的,工具链可复制,思维模式不可复制,需要大家根据自身的业务场景进行深入实践及剖析。
敲黑板!这篇文章有点长,但是非常的干货,值得加入收藏夹慢慢看。
做前端组件一定是从 UI 作为起点,如果 UI/UE 无法定义一套通用的标准,那基本上业务组件的落地是很难的。做组件的第一个难点就是边界的划分。设计里面有一个著名的“原子理论”,奠定了组件划分的理论基础。
掌门题库从一开始就明确了我们的组件要提供的是题目交互解决方案的能力。整体遵循高效、清晰、专业的设计原则。
Ant Design 提出了“资产一起造”,这套方法论是可以复制的。我们把所有端的页面都进行汇总,把课件、作业、测评、试卷、报告等所有涉及到题目的页面都摞到一起,然后抽取复用模块,把题型进行收拢,统一手势的交互,统一交互的反馈机制等。
上图是我们组件的逻辑分层,参照原子理论,我们从下往上看:
最底层是非常简单的UI层,分为题干、选项、解析等等,很显然这一层即为原子;
上面一层是通过原子组成的题型,称为分子层;
再上面一层也是题型层,这一层可不是画错了,而是由于这一层的题型是对应业务中更加具体的题型,其UI表现也许一致,但是数据结构却不一致,因此这一层就是模块层;
再上一层即服务层,这一层是页面级别的;
最后一层即应用层。
所以通常就是应用、页面、区块、业务组件、基础组件的层级划分,层层抽象一直下沉到原子层。
可以意识到组件的本质即复用。如果一个交互功能点只会被用到一次,便没有了复用的根基,无法产生工程化效应的元素都不应该下沉到组件的范畴,下沉的越深成本越大。
这样的搭积木的思想,因为有了较细的颗粒度,增加了组合的可能性,而且组合是量级的,因此就会产生工程化的效应。
在决定了要做一个组件的需求后,就需要针对性的进行技术选型,考虑的因素有很多,例如团队的技术背景、产品的生命周期、是否多人参与、项目规模等。
现在从仓库模式、框架选择、技术选型三个方面谈谈我们团队在开发题目组件时的抉择。
Monorepo 的核心观点是所有的项目在一个代码仓库中。是否要用 Monorepo 是要看场景的,否则就有因为”有了锤子见到什么都是钉子“的嫌疑了。我们开发组件选择了 lerna 作为大仓库的管理工具,之所以采取这样的模式,是因为我们内部不仅仅有一个组件,多个组件有多人参与、并行开发,并行发布,同时有相互依赖的需求,用 lerna 就能够大大降低开发成本。当然如果没有这样的需求,只是开发一个单一的组件,则没有必要选择如此复杂的架构。
关于Monorepo 可以参考 Dan Luu 的文章 Advantages of monorepos
https://danluu.com/monorepo/
组件开发有一点跟业务开发是完全不一样的,就是组件的用户是开发者,是给开发者赋能的,需要有一个详尽的文档,所以显然就要做好文档工具的选型。这个可以根据需求进行选择:
https://react-styleguidist.js.org/
通过 Markdown 文件写 examples,以前有用过,还不错,推荐。
https://storybook.js.org/
通过 JS(JSX) 文件写 examples,这个比较重,很多公司用的都是这个。
antd 网站采用的网站工具
我们老版本的组件是用的这个,没有文档,我用的时候是边看源码边改的,所以这里不太推荐大家使用。
https://github.com/umijs/father
https://github.com/umijs/dumi
云谦 团队新出的一款文档工具,我们团队重构的版本用的就是这个。支持 lerna、支持多 entry、 css 和 less、 css modules、基于 rollup 和 babel 的组件打包功能、基于 docz 的文档功能。
JS 在调用一个组件的 API 函数时是不清楚这个函数的参数类型的;对于一个组件重要函数的参数做了优化重构后,也是无法评估影响面的。而这两点先天不足上了 Typescript 以后都会得到解决,可大大提高组件的可维护性和易用性。
1npm install husky stylelint lint-staged eslint prettier --save-dev
这一套配置是为了能达到以下的工程效果:
待提交的代码-> git add 添加到暂存区-> 执行 git commit-> husky 注册在 git pre-commit 的钩子调起 lint-staged-> lint-staged 取得所有被提交的文件依次执行写好的任务(ESLint 和 Prettier)-> 如果有错误(没通过 ESlint 检查)则停止任务,等待下次 commit,同时打印错误信息 -> 成功提交
lint 这一系列工具确保的是组件的编码风格和代码质量。
这里特别说明一下 husky 比较容易踩到的坑,其中官方有特别说明对于 node 和 git 的版本要求,同时因为 npm和 yarn 有冲突,如果用 yarn 的话需要安装 husky@next 的版本。
husky 也很可能由于各种原因,导致不能很好的工作;这多半是由于 .git/hooks/pre-commit 与期待目标不匹配所致,可以手动修改,也可以采取如下办法 (备注:如果你的 husky 版本在 0.14 及以上):
1rm -rf .git/hooks/*
2node node_modules/husky/lib/installer/bin install
有的项目可能只会用到模块层的某几个题型,那这个时候对于性能的考虑就必不可少。
在
[email protected]
中,引入了 svg 图标。使用了字符串命名的图标 API 无法做到按需加载,因而全量引入了 svg 图标文件,这大大增加了打包产物的尺寸。在 4.0 中,我们调整了图标的使用 API 从而支持 tree shaking,减少 antd 默认包体积约 150 KB(Gzipped)。
所以我们的库支持ESM导出,因为 tree shaking 只支持 ESM,是通过分析作用域链达到 shaking 的目的。
因为题目中会出现大量的字体文件,所以我们将所有的字体文件放到了公司内网的 CDN 中,这个比较简单,通过配置 webpack 中的 file-loader 的 publicPath 就可以解决。
1{
2 test: /\.(eot|woff|woff2|ttf)(\?.*)?$/,
3 loader: 'file-loader',
4 options: {
5 publicPath: 'https://*****.zm.com/tk-fonts/0.11.1/',
6 name: '[name].[ext]',
7 emitFile: false,
8}
我们直接将react-loadable
放到了组件里面,从而实现按需加载:
1import Loadable from 'react-loadable';
2import MyLoadingComponent from './loading';
3
4interface Iopts {
5 loader: () => Promise;
6}
7export default function MyLoadable(opts: Iopts) {
8 return Loadable({
9 loading: MyLoadingComponent,
10 delay: 200,
11 timeout: 10000,
12 ...opts,
13 });
14}
做完了技术选型后,目录结构就可以定下来了。
目录结构决定的是组件内部架构划分的合理性,也体现了项目开发的规范性。
我曾经接手过一个项目里到处都是 utils 文件夹,同时又有 common 文件夹,最后发现这就是不同的开发者自己的私人工具库,自己用自己的,这就像是一支没有纪律的军队,造成了资源冗余,也增加了项目的复杂度,刚接手项目的新人看到的就是一团杂乱无章的蜘蛛网。而遵循业界通用规范的目录结构不仅能大大降低学习成本,也让项目的架构分层一览无余。
我们目录结构如下,也是现在组件开发的标配架构:
1|-- .eslintrc.js # lint 配置文件
2|-- .gitignore # git 配置文件
3|-- lerna.json # lerna 配置文件
4|-- package-lock.json
5|-- package.json
6|-- packages
7| |-- zm-tk-ace-audio # 项目A文件夹
8| | |-- .fatherrc.js # father 配置文件
9| | |-- README.md
10| | |-- __tests__ # 测试文件夹
11| | |-- dist # umd 版本
12| | |-- es # es6 版本
13| | |-- lib # cjs 版本
14| | |-- package.json # npm 配置文件
15| | |-- src # 源码
16| | `-- tsconfig.json # ts 配置文件
17| `-- zm-tk-ace-utils # 项目B文件夹
18| |-- .fatherrc.js
19| |-- README.md
20| |-- __tests__
21| |-- es
22| |-- lib
23| |-- package.json
24| |-- src
25| `-- yarn.lock
26`-- yarn.lock
组件开发完了以后,需要发布到 npm 服务器中。在发布之前我们还有一些需要准备的工作。
规范你的 commit message 是非常有用的,我们有很多开发同学为了省事,经常会提交一些无用的 message,在无形之中浪费了资源,同时也失去了 commit message 存在的意义。
我们团队的一个架构师离职了很久,他做了一个关于字体文件自动引用 VPN 地址的需求,当时我们有一个需求需要把这部分功能恢复成原来的样子,因为我们的代码都遵循了commit message 的规范,通过搜索很快就找到了当时他提交代码的 PR 及 commit。还可以配合一些自动生成 Label 的工具 git-labelmaker 进一步提供搜索时的定位标签。
这里可以看看 Angular 官方库的 commit message,一目了然。
规范的 commit message
还有集成工具的好处。一方面有利于利用 semantic-release 来自动更新版本号,另一方面也有利于自动生成 changeLog
。
可以参考 antd 的 ChangLog,有了这个开发者使用你的包的时候就可以跟着你的脚步无痛升级,并且知道升级的风险点在哪。
推荐一个生成符合规范的 commit message
的开发者工具,commitizen,其默认使用 Angular commit 规范。
将 commitizen
和 husky
配合使用,修改 package.json
配置:
1 "husky": {
2 "hooks": {
3 "pre-commit": "npm run lint-staged",
4 "prepare-commit-msg": "exec < /dev/tty && git cz --hook || true"
5 }
6 },
当你使用 git commit
的时候就会出现:
根据 commit message
来生成 changelog
:
1# command
2conventional-changelog -p angular -i CHANGELOG.md -s -r 0
-i
: 表示从CHANGELOG.MD
中读取changelog
-s
: 读写changelog
为同一个文件
-r
: 表示生成changelog
的release
版本数量,默认为1,全部则是0
自动生成 changeLog 的工具也是很多的,比如还有一个gitlab-changelog-generator。
npm 包 package.json
规范中的 version 字段应遵循 Semantic Versioning 规范,形如 x.y.z
x 位:重构版本,即包含有 break change
变更的大版本。一般周期一到两年
y 位:重大功能改进。我们通常是每月发布一个带有新特性的向下兼容的版本
z 位:小升级或者 bug 修复。我们是每周三会进行日常 bug 修复版本的更新,紧急问题不受此限制,可以随时发布。
这里讲个小插曲,我们团队的有些小伙伴会对这个版本的数字有“恐高”,其实就是一个数字,不必太在意。
可以直接通过 npm version
命令来发布版本(如果用了lerna就不需要用这个了):
1# v1.0.0
2npm version prepatch # 发布一个预修订版本 v1.0.1-0
3npm version patch # 修改修订号,做了向下兼容的问题修正,v1.0.1
4
5npm version preminor # 发布预次版本 v1.1.0-0
6npm version minor # 修改次版本号,做了向下兼容的功能性新增,v1.1.0
7
8npm version premajor # 发布预备主版本 v2.0.0-0
9npm version prerelease --preid=alpha # alpha 是测试版本
10npm version major # 修改主版本号,做了不兼容的 API 修改,v2.0.0
PS:如果破坏了语义化的规范发布了不兼容的版本,应该继续往下发修复相应兼容版本。
用命令更改版本号后,会自动添加一个 commit:
一般有 3 种线上版本:
alpha: 表示在这个阶段以实现软件功能为主,Bug 较多,需要继续修改。通常只在开发者内部交流。
beta: 该版本相对于 α 版已有了很大的改进,消除了严重的错误,还存在一些缺陷,需要经过多次测试来进一步消除,此版本主要的修改对像是 UI。
release:即最终的正式版本。
也可以用 semantic-release 来实现全自动更新版本号和发布,这个工具会判断Commit Message
的不同,fix 增加修订号,feat 增加次版本号,而包含 BREAKING CHANGE
的提交增加大版本号。所以这就要求我们前期有规范commit message
的意识。
组件的迭代通常是有版本的,然而自己做的组件一般不会想到文档也需要版本。包含 BREAKING CHANGE 的大版本发布就是一个从量变到质变的过程,从产品的生命周期来看,通过文档的版本迭代可以为后续的维护者提供更好的沉淀,所谓站在巨人的肩膀上让这个产品的寿命得以延续。
我觉得文档是一个埋彩蛋的好地方,想一想如果几十年后你的产品还在,人们发现了你的彩蛋,这是一件多么有趣的事情。
开源协议是软件的授权许可,表示别人获得你开源的代码后拥有的权利和义务,可以参考
Choose an open source license | Choose a License
清晰明了的 Readme 不仅有助于帮助开发者了解你的产品的功能特点,
作为用户的使用入口,更像是你的门面。可以从以下几个方面进行阐述:
产品简介
此处要突出特点,打差异化竞争。
产品安装和下载
写清楚快速起服务的流程。
快速使用
详细的使用文档或者二次开发文档,外链即可。
适用范围
对于前端项目来说,最重要的莫过于要写清楚浏览器兼容性了。
交流提问区
你的 npm包某种程度上在初期是没有经历过市场的认证的,肯定会出现很多你无法预料的问题,所谓开源也即开反馈之源,所以一个好的“售后模式”会有助于你的产品的完善及往更符合市场需求的方向发展。
通常业界比较标准的做法是会提供一个钉钉群、一个微信群(QQ群)的二维码。
有了以上基础以后,就可以好好的配置你的 package.json 了。详细的字段说明可以参考官方的说明 https://docs.npmjs.com/files/package.json。懒得看英文的话也可以移步我以前总结的一篇文章https://www.yuque.com/lulu27753/lulu/hoc88x。
这里主要强调几个 npm 包开发中比较重要的字段。
browser && module && main
1{
2"main": "./lib/main.js",
3"module": "./lib/main.m.js",
4 // browser 可定义成和 main/module 字段一一对应的映射对象,也可以直接定义为字符串
5"browser": {
6 "./lib/index.js": "./lib/index.browser.js", // browser + cjs
7 "./lib/index.mjs": "./lib/index.browser.mjs" // browser + mjs
8},
9}
这些字段都是指定引入包的入口文件的,那组合配置时其优先级是什么样子的呢,一图胜千言:
由此可知,要结合组合去看入口到底是哪个,不是像大家通常以为的 main 字段下的就是文件的入口。
如果公司里有 npm 私服的话,建议通过 nrm 来管理源;
1# step 1
2npm install nrm -g
3
4# step 2 添加公司 npm 源
5nrm add taobao https://registry.npm.taobao.org
6
7# step 3 切换公司源
8nrm use taobao
9
10# step 4 完成!
需要在 npm 上注册账号,注意包的名字不能重名。
1npm add user
2npm login
3npm publish .
这样别的团队就可以愉快的使用你提供的组件了。
我们项目用的是 Jenkins + Docker 的方式构建打包组件的官方文档的。
我们的项目有一个比较特殊的情况,就是组件需要兼容 React 15
以及 React 16+
的业务支持,而我们组件的开发环境使用的是React 16+
,这个时候就出现了版本冲突。
配置 package.json
中的 peerDependencies
字段,通过依赖业务项目中的版本来解决低版本业务会安装双版本 React 的情况:
1"peerDependencies": {
2 "react": ">=16.0.0",
3 "react-dom": ">=16.0.0"
4}
同时配置下 webpack alias
保证组件开发环境 React 版本的正常
1if (isDev) {
2 alias.react = require.resolve('react')
3}
1、yarn link
会比 npm link
快很多
1yarn link
你的 npm
包会根据 package.json
上的配置,被链接到全局,路径是 {prefix}/lib/node_modules/
可以通过 npm config get prefix 获取到 prefix 的值。
解除
解除项目和模块 link
,项目目录下,yarn
unlink
解除模块全局link
,模块目录下,yarn
unlink
2、借助工具(推荐): yalc: Work with yarn/npm packages locally like a boss.
可参考文章 Looking for brilliant yarn member who has first-hand knowledge of prior issues with symlinking modules · Issue #1761 · yarnpkg/yarn
保持克制:能不做,就不做;能不先做,就不先做
这里我借用一下林外的观点,做组件的过程中感同身受,为什么这个理念如此重要,因为这是在枪林弹雨中实践得来的真知。
组件类型的扩展保持克制,遵循「一个问题,一个解决方案」的思路,让组件类型和 API 接口都保持足够收敛。这为未来的迭代和扩展,甚至掉头提供可能。因为加一个组件容易,下一个组件极其艰难。这与 Less is More 有着异曲同工之妙。
但如果是做公司内部的业务组件,要做到这一点会非常的困难,我们的解决思路是中间再加一层中间层,增加组件的灵活性,把不想放到组件中的代码都放到中间层中,后期成熟后可以将有必要放入组件层的代码再后移,可进可退。
产品一般会怎么写文档,
What、When、Why?
错错错,只要写 HOW
不要写一堆正确的废话,我们要以方便设计者/开发者使用为核心。
所以我们的文档是非常的简单明了的,只有接口说明、用例及代码三个部分。
做业务组件和通用组件有一个很大的区别,通用组件的需求池非常的大,而业务组件是通用组件的垂直领域,封装了相关的业务逻辑,在公司内部如若没有自上而下的力量来推广的话,业务驱动会导致各个业务线自己玩自己的,直接将逻辑耦合到业务中。因此很有必要联合公司UED及产品团队一起出标准方案,并且作为整个公司的资产工具池,这样才能发挥业务组件的价值。
组件开发当然属于基建的范畴,因为组件本质上是为开发赋能。
通用业务组件解决的不仅仅是技术问题和业务问题,更重要的是工程问题。然而任何时候都要知道任何技术最最本质的核心是解决业务痛点,比如掌门题库组推出的题目组件提供的就是题目的展示及题目交互的解决方案,里面封装了大量的题目解析的逻辑,包含了 latex 语法及对于输入输出数据控制的大量正则表达式的逻辑,这些逻辑复杂、学习成本高,但是解决方案通用且稳定,通过组件封装的形式就能很好的为开发者赋能,即使是一个初级开发者也可以根据文档就快速的提供业务支撑。这才是组件的核心价值。
本文仅对整个工具链流程进行了梳理,虽有细节也未详尽,每一个点都可以深入的挖掘,篇幅有限,便不一一赘述。
一般面试官都会期望得到为什么,而不是怎么做。
我从3个纬度来回答了这个问题:
组件提供什么能力?适用范围是什么?如何划分边界?(设计思维)
如何确保组件的开发过程规范、值得信赖(工程思维)
如何保证组件的寿命,让更多的人使用你的组件?(产品思维)
参考资料:
原子理论:
https://atomicdesign.bradfrost.com/table-of-contents/
Advantages of monorepos:
https://danluu.com/monorepo/
choosealicense:
https://choosealicense.com/
林外的日课:
https://www.yuque.com/lyndon/daylesson
代码风情:
https://www.yuque.com/lulu27753/lulu