blog
blog copied to clipboard
开发一个高质量的前端组件,这些姿势一定要知道
题图 | 《蜕变》 | 19年十一作者拍摄于雨岔峡谷
2009年11月8日,在欧洲JSConf大会上,Ryan Dahl第一次正式向业界宣布了Node.js的面世,使JS语言书写后端应用程序成为了可能。在随后的几年里,Node.js受到了Javascript社区狂热地追捧,前端行业也因此进入了一个全新的工程化和全栈时代。
回顾历史,总会让人心潮澎湃。在这股浪潮中,有无数的人和项目在这座丰碑中刻下了自己的名字:React、Vue、Yeoman、RequireJS、Backbone、Antd、Webpack、Mocha、Eslint等等。在这些知名项目的熠熠光辉下,我们可能会忽略为Node.js生态的繁荣之下建立不世之功的NPM,它才是当之无愧的肱骨重臣。
NPM生于2010年1月,它从出生就背负了让Node.js社区更加繁荣的使命。NPM致力于让JS程序员能够更加方便地发布、分享Node.js类库和源码,并且简化模块的安装、更新和卸载的体验。
从今天(2019年)这个时间节点来看,NPM无论从知名度、模块数量、社区的话题数量来看,都算得上是一骑绝尘,将其他语言的模块仓库远远甩在了后面。
数据来源: moudlecounts
NPM的生态既已如此成熟,按说开发者对于NPM包的发布和维护应该非常熟悉才是,但事实真的是这样吗?环顾身边的FE,没有发过任何NPM包的同学大有人在,已经发过包的同学也有相当一部分并未考虑过如何才算规范地、高质量地发布一个包。
如今NPM的模块数量已上升至100W,在这样一个JavaScript组件化开发时代,除了能找到好用的组件,我们自然也需要了解如何才能成为创造这个时代的一员。而第一步就是要知道并掌握如何规范地、负责任地发布一个NPM包?。
这就是本文接下来的主要内容。
1. 组件化思考
发布人生中第一个NPM组件虽然只是在终端命令行中潇洒地敲下npm publish,静等成功通知即可,但这从0到1的跨越却并非易事。这个行为的背后的始作俑者是开发者的大脑中开始萌发组件化思维方式。开始去思考何为组件?、为什么要发布组件?这些更深一层次的问题。
组件的存在的终极意义是为了复用,一个组件只要具备了被复用的条件,并且开始被复用,那么它的价值才开始产生。组件复用的次数越高、被传播的越广,其价值就越大。而要实现组件的价值最大化,需要考虑以下几点:
- 我要写一个什么组件?组件提供什么样的能力?
- 组件的适用范围是什么?某个具体业务系统内还是整个团队、公司或者社区?
- 组件的生产过程是否规范、健壮和值得信赖?
- 组件如何被开发者发现和认识?
以上四点中,前两点是生产组件必须要思考的问题;第四点是组件如何推广运营的问题,这是另外一个话题,本文不展开探讨;第三点是开发者的基本素养,它决定了开发者如何看待这个组件,也间接暴露了开发者的素养和可信赖程度。
2. 组件开发的最佳姿势
一个优秀的组件除了拥有解决问题的价值,还应该具备以下三个特点:
- 生产和交付的规范性
- 优秀的质量和可靠性
- 较高的可用性
只有三者都能满足才可以称其为优秀组件,否则会给使用者带来各种各样的困惑:经常出Bug、坑很多、不稳定、文档太简单、不敢用等等。
2.1 规范性
2.1.1 目录结构
事实上,社区并没有一个官方的或者所有人都认同的目录结构规范,但从耳熟能详的知名项目中进行统计和分析,可以得出一个社区优秀开发者达成非官方共识的一个目录结构清单:
├─ test // 测试相关
├─ scripts // 自定义的脚本
├─ docs // 文档,通常文档较多,有多个md文档
├─ examples // 可以运行的示例代码
├─ packages // 要发布的npm包,一般用在一个仓库要发多个npm包的场景
├─ dist|build // 代码分发的目录
├─ src|lib // 源码目录
├─ bin // 命令行脚本入口文件
├─ website|site // 官方网站相关代码,譬如antd、react
├─ benchmarks // 性能测试相关
├─ types|typings// typescript的类型文件
├─ Readme.md // 仓库介绍或者组件文档
└─ index.js // 入口文件
以上目录清单是一个比较完整的清单,大多数组件只需要根据自己的需求选择性地使用一部分即可。一份几乎适用于所有组件的最小目录结构清单如下:
├─ test // 测试相关
├─ src|lib // 源码目录
├─ Readme.md // 仓库介绍或者组件文档
└─ index.js // 入口文件
2.1.2 配置文件
这里的配置文件主要指的是各种工程化工具所依赖的本地化的配置文件,以及在Github上开源所需要声明的一些文件。一份比较全的配置文件清单如下:
├─ .circleci // 目录。circleci持续集成相关文件
├─ .github // 目录。github扩展配置文件存放目录
│ ├─ CONTRIBUTING.md
│ └─ ...
├─ .babelrc.js // babel 编译配置
├─ .editorconfig // 跨编辑器的代码风格统一
├─ .eslintignore // 忽略eslint检测的文件清单
├─ .eslintrc.js // eslint配置
├─ .gitignore // git忽略清单
├─ .npmignore // npm忽略清单
├─ .travis.yml // travis持续集成配置文件
├─ .npmrc // npm配置文件
├─ .prettierrc.json // prettier代码美化插件的配置
├─ .gitpod.yml // gitpod云端IDE的配置文件
├─ .codecov.yml // codecov测试覆盖率配置文件
├─ LICENSE // 开源协议声明
├─ CODE_OF_CONDUCT.md // 贡献者行为准则
└─ ... // 其他更多配置
以上配置可以根据组件的实际情况,适用范围来进行删减。一份在各种场景都比较通用的清单如下:
├─ .babelrc.js // babel 编译配置
├─ .editorconfig // 跨编辑器的代码风格统一
├─ .eslintignore // 忽略eslint检测的文件清单
├─ .eslintrc.js // eslint配置
├─ .gitignore // git忽略清单
├─ .npmignore // npm忽略清单
├─ LICENSE // 开源协议声明
└─ ... // 其他更多配置
上述清单移除了只有在Github上才用得到的配置,只关注仓库管理、发包管理、静态检查和编译这些基础性的配置,适用于团队内部、企业私有环境的组件开发。如果要在Github上维护,则还需要从大清单中继续挑选更多的基础配置,以便可以使用Github的众多强大的功能。
2.1.3 package.json
如果说NPM官方给出了一个发包规范的话,那么这个规范就是package.json文件,这是发包时唯一不可或缺的文件。一个最精简的package.json文件是执行npm init生成的这个版本:
{
"name": "npm-speci-test", // 组件名
"version": "0.1.0", // 组件当前版本
"description": "", // 组件的一句话描述
"main": "index.js", // 组件的入口文件
"scripts": { // 工程化脚本,使用npm run xx来执行
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "", // 组件的作者
"license": "ISC" // 组件的协议
}
有这样一个版本的package.json文件,我们就可以直接在该目录下直接执行npm publish发布操作了,如果name的名称在npm仓库中尚未被占用的话,就可以看到发包成功的反馈了:
$ npm publish
+ [email protected]
但光有这些基础信息肯定是不够的,作为一个规范的组件,我们还需要考虑:
- 我的代码托管在什么位置了
- 别人可以在仓库里通过哪些关键词找到组件
- 组件的运行依赖有哪些
- 组件的开发依赖有哪些
- 如果是命令行工具,入口文件是哪个
- 组件支持哪些node版本、操作系统等
一份比较通用的package.json文件内容如下:
{
"name": "@scope/xxxx",
"version": "0.1.0",
"description": "description:xxx",
"keywords": "keyword1, keyword2,...",
"main": "./dist/index.js",
"bin": {},
"scripts": {
"lint": "eslint --ext ./src/",
"test": "npm run lint & istanbul cover _mocha -- test/ --no-timeouts",
"build": "npm run lint & npm run test & gulp"
},
"repository": {
"type": "git",
"url": "http://github.com/xxx.git"
},
"author": {
"name": "someone",
"email": "[email protected]",
"url": "http://someone.com"
},
"license": "MIT",
"dependencies": {},
"devDependencies": {
"eslint": "^5.2.0",
"eslint-plugin-babel": "^5.1.0",
"gulp": "^3.9.1",
"gulp-rimraf": "^0.2.0",
"istanbul": "^0.4.5",
"mocha": "^5.2.0"
},
"engines": {
"node": ">=8.0"
}
}
-
name属性要考虑的是组件是否为public还是private,如果是public要先确认该名称是否已经被占用,如果没有占用为了稳妥起见,可以先发一个空白的版本;如果是private的,则需要加上@scope前缀,同样也需要确认名称是否已被占用。 -
version属性必须要符合semver规范,简单理解就是:- 第一个版本一般建议用0.1.0
- 如果当前版本有破坏性变更,无法向前兼容,则考虑升第一位
- 如果有新特性、新接口,但可以向前兼容,则考虑升第二位
- 如果只是bug修复,文档修改等不影响兼容性的变更,则考虑升第三位
-
keywords会影响在仓库中进行检索的结果 -
main入口文件的位置最好可以固定下来,如果组件需要构建,建议统一设置为./dist/index.js, 如果不需要构建,可以指定为根目录下的index.js -
scriptsscripts通常会包含两部分:通用脚本和自定义脚本。无论是个人还是团队,都应该为通用脚本建立规范,避免过于随意的命名scripts;自定义脚本则可以灵活定制,比如:- 通用scripts:start、lint、test、build
- 自定义scripts:copy、clean、doc等
-
repository属性无论在私有环境还是公共环境,都应该加上,以便通过组件可以定位到源码仓库 -
author如果是一个人负责的组件,用author,多个人就用contributors
更详细的package.json 文件规范可以参见npm-package.json
2.1.4 开发流程
很多同学在开发组件时都会使用master分支直接进行开发,觉得差不多可以发版了就直接手动执行一下npm publish,然后下一个版本,继续在master上搞。
这样做是非常不规范的,会存在很多问题,譬如:
- 正在开发一个比较大的版本,此时当前线上版本发现一个重要bug需要紧急修复
- 没有为每一个发布的版本指定唯一的tag标签以便回溯
git的workflow有很多种,各有适合的场景和优缺点。开发组件大多数时候是个人行为,偶尔是team行为,所以不太适合用比较复杂的流程。个人观点是,如果是在github上维护的开源组件,则参照github流程;如果是个人或者公司内私有环境,只要能保障并行多个版本,并且每一个发布的版本可回溯即可,可以在github流程上精简一下,不区分feature和hotfix,统一采用分支开发,用master作为线上分支和预发分支,开发分支要发版需要预先合并到master上,然后再master上review和单测后直接发布,并打tag标签,省略掉pull request的流程。
2.1.5 commit && changelog
一个组件从开发到发布通常会经历很多次的代码commit,如果能在一开始就了解git commit的message书写规范,并通过工具辅助以便低成本地完成规范的实践落地,则会为组件的问题回溯、了解版本变更明细带来很大的好处。我们可能都见过Node.js项目的changelog文件:

非常规范地将当前版本的所有关键Commit记录全部展示出来,每一条commit记录的信息也非常完整,包含了:commit的hash链接、修改范围、修改描述以及修改人和pull request地址。试想一下,如果前期commit阶段如果没有很好的规范和工具来约束,手工完成这个工作需要花多长时间才能搞定呢?
目前社区使用最广泛的commit message规范是:Conventional Commits,由Angular Commit 规范演变而来,并且配备了非常全的工具:从git commit命令行工具commitizen,到自动生成Changelog文件、以及commitlint规范验证工具,覆盖非常全面。
2.3 质量和可维护性
开发组件的出发点是为了复用,其价值也体现在最大程度的复用上。团队内部的组件可能会在整个团队的多个系统间复用;公司内部通用的组件,可以为整个公司带来开发成本的降低;像react、antd这样的优秀开源组件,则会为整个社区和行业带来重大的价值。
组件是否可以放心使用,一个最简单直接的评判标准就是其质量的好坏。质量的好坏,除了上手试用以外,一般会通过几个方面来形成判断:
- 是否有高覆盖率的单元测试用例?
- 源码是否有规范的编码风格和语法检查?
- 源码是否使用了类型系统?
这些都直接决定了开发者对这个组件的评价。试想一下,如果开发了一个公共组件,没有规范的开发流程和编码风格检查,也没有单元测试,随手就发布了带bug的版本。此时用户第一次安装使用时就报错,这会让开发者对组件以产生强烈的不信任感,甚至这种不信任感会波及到作者本身。
因此,一个规范且合格的组件,至少要在保障组件的质量上做两件事情:1)引入JavaScript代码检查工具来规范代码风格和降低出错概率;2)引入单元测试框架,对组件进行必要的单元测试。此外,类型系统(TypeScript)的加入,会帮助组件提高代码质量和可维护性,是组件开发时的推荐选择。
2.3.1 JavaScript检查工具
JavaScript语言第一个检查工具是由前端大神 Douglas Crockford在2002年发布的JSLint,在后续前端行业高速发展的十几年间逐渐演变出了JSHint和ESLint两个检查工具。关于这三个工具的演变历史,可以参考尚春同学在知乎发表的一篇文章:《JS Linter 进化史》。本文不再赘述,我们可以通过google trends来简单了解一下这三个共工具的热度,这里还加上了一个JSCS的对比:

可以看到在过去的一年内全球范围内用户在google搜索这些关键词的热度情况,这个图和身处在前端行业的感受是一致的。因此在JavaScript检查工具的选择上,可以毫不犹豫地选择ESLint。
实际使用ESLint时有几点需要考虑:
- 无论团队还是个人,都需要就配置规范达成认知和共识,以便可以将配置沉淀下来,作为通用的脚手架和规范
- 对于不同的组件类型,譬如react或者vue,各有自己的独特的语法,需要特定的ESLint插件才可以支持,而和框架无关的组件,譬如
lodash,则不需要这些插件。因此如何对配置进行分类和抽象,以便沉淀多套配置规范,而不必每次开发组件都需要重新对配置进行调整和修正。一个比较常规的做法是把组件按照应用的端(浏览器、Node、通用、Electron、...)和运行时依赖的框架(React、VUE、Angular等)来进行配置的组合。 - 借助IDE的插件来实现自动修复以便提高效率
- 如果是团队共同的规范,还需要形成一套规范变更的流程,以便组员对规范有争议时,可以有固定的渠道去讨论、决议并最终落实到规范中。
- 引入了ESLint,还需要考虑是否将ESLint加入到验收流程中,以及如何加入验收流程
2.3.2 单元测试和覆盖率
一直以来对于业务类的项目要不要写单测这个问题,个人的选择是可以不写。互联网倡导敏捷开发,快速迭代上线试错,需求变化太快,而为前端代码写单测本身的成本可能并不亚于代码本身。
但是组件的情况就完全不同了,组件是一组边界清晰、效果可预期的接口和能力的集合。而且和业务类代码相比,组件更具备通用性,也就是说不太会随着业务的变更而变更。并且组件的升级通常会对依赖组件的系统造成潜在影响,每一个版本的发布都理应对功能进行详尽的回归测试,以保障发布版本的质量。由于组件的测试通常依靠开发者自己保障,不会有专业的QA资源配备,因此单元测试就是最好的解决方案了。
JavaScript的单元测试解决方案非常之多,呈百花齐放百家争鸣的态势,耳熟能详的譬如:Jasmine、Mocha、Jest、AVA、Tape等,每一个测试框架都有其独特的设计,有些是开箱即用的全套解决方案,有些自身很简约,还需要配合其他库一起使用。
事实上,这些框架并无绝对的好坏,如何选择完全取决于个人和团队的喜好。这有一篇测试框架评测的文章,不妨一读:《JavaScript unit testing frameworks: Comparing Jasmine, Mocha, AVA, Tape and Jest [2018]》。
另外,我们依然可以通过Github上的star数和google trends上的搜索量来略窥流行趋势一二。
| 测试框架 | Github stars |
|---|---|
| Jasmine | 14.5k |
| Jest | 27k |
| Mocha | 18.3k |
| ava | 16.7k |
| tape | 5.1k |
google trends的中国数据

google trends在美国的数据

可以看出Jest从2014年发布以来,增长势头是最猛的,并在短短3年内超过了其他老牌对手,成为目前最炙手可热的Test Framwork。
除了测试框架选型以外,还有一个比较重要的指标要关注,就是测试覆盖率。推荐使用nyc, 很多同学可能还用过一个名字比较特殊的库:istanbul。这两个库之前的渊源可以看这个Issue了解一下。
2.3.3 类型系统
如今的JavaScript已经不是原来那个在浏览器写写动效和交互的愣头小子了,它已经在Web、Server、Desktop、App、IOT等众多场景中证明了自己的价值,证明了自己可以被用来解决复杂的问题。事实上,JavaScript正是通过将众多优秀的高质量组件、框架进行有机组合来提供这种能力的。
但是值得深思的是,JavaScript采用了动态弱类型的设计,过于灵活的类型转换往往会带来一些不好的事情。试想这样的场景:
- 调用一个组件的API函数,却不清楚这个函数的参数类型,只能自己去撸代码
- 对一个组件重要函数的参数做了优化重构,却无法评估影响面
这些问题在强类型语言中有很好的解决方案,很多可能的错误会在编译期就被发现,很多改动的影响也会第一时间就被IDE告警。
事实上,越来越多的知名组件库已经开始引入强类型系统来辅助提高代码的质量和可维护性,比如Vue.js、Angular、Yarn、Jest等等。如果你想让自己具备类型思维,让组件具备更好的质量和可维护性,可以考虑把类型系统加到组件的脚手架中去。
目前可选的为JavaScript增加强类型检查的解决方案有FaceBook的Flow和Microsoft的TypeScript,从当下的流行趋势来看,TypeScript是绝对的首选。
如果想系统、深入地学习TypeScript又不想自己苦逼的撸官方文档,强烈推荐购买学习搜狗高级架构师梁宵在极客时间上开发的TypeScript课程《TypeScript开发实战》。
2.4 可用性
组件的可用性,主要指的是从组件的使用者角度来看待组件的使用体验:
- 组件的文档是否完善并且易于阅读?
- 组件暴露的API是否有详细且规范的输入输出描述?
- 是否有可以直接运行或者借鉴的Demo?
- 文档是否有考虑国际化?
2.4.1 文档
一个好的组件文档至少应该具备以下内容结构:
一句话描述组件是什么,解决什么问题
# Usage
// 如何安装和使用,提供简单并且一目了然的示例
# API文档
// 提供规范且详细的API接口文档,包括示例代码或者示例链接
# 补充信息,譬如兼容性描述等
// 如果是浏览器端组件,最好补充一下兼容性的支持情况;如果是Node端组件,也需要描述一下支持的Node.js版本范围
# ChangeLog
// 描述各个版本的重要变更内容以及commit链接
# 贡献、联系作者、License等
// 如果组件希望他人一起参与贡献,需要有一个参与贡献的指南;除此之外,最好再提供一个可以直接联系上作者的方式
很多优秀的开发者可以很好地驾驭代码,但对如何写好一份组件文档却有些苦恼,这是因为代码是给自己看的,文档是给用户看的,这两种思维方式之间存在天然的差异。写文档时,需要换位思考,甚至可以把用户当小白,尽可能为小白考虑的多一些,如此可以提高文档的可读性,降低上手难度和使用的挫败感。
2.4.2 DEMO
对一个组件而言,Demo的重要性不言而喻,还记得Node.js那个经典的几行代码创建一个http server的招牌式demo吗?可以说它几乎成为了Node.js的招牌和广告。
组件的Demo和文档都是为了可用性负责,但应该互有侧重,相得益彰。文档侧重于介绍关键信息、Demo侧重于交付具体应用场景中的用法。
对于比较小的组件,这两者可以合二为一;对于demo代码量较多,且有多种使用方式和场景的情况,建议在examples目录下为每一种场景写一个可以直接运行的Demo;
3. 结语
组件是开发者创造的产品,在这个产品的生命周期中,第一次发布只是一个开始而已。如何让更多的用户关注到并且成为她的忠实用户,乃至参与贡献才是接下来要重点解决的问题。关于这个话题,本文就点到为止了,欢迎大家在下面留言分享自己在组件推广方面的经验和技巧。
参考文档
你很棒
你很棒
谢谢鼓励😁
写的挺好的了,谢谢