前言
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 包?
这就是本文接下来的主要内容。
《蜕变》 | 2019 年"十一",作者拍摄于雨岔峡谷。
一、组件化思考
发布人生中第一个 NPM 组件虽然只是在终端命令行中潇洒地敲下npm publish,静等成功通知即可,但这从 0 到 1 的跨越却并非易事。这个行为背后的始作俑者是开发者大脑中开始萌发组件化思维方式,开始去思考何为组件?为什么要发布组件?这些更深层次的问题。
组件的存在的终极意义是为了复用,一个组件只要具备了被复用的条件,并且开始被复用,那么它的价值才开始产生。组件复用的次数越高、被传播的越广,其价值就越大。而要实现组件的价值最大化,需要考虑以下几点:
- 我要写一个什么组件?组件提供什么样的能力?
- 组件的适用范围是什么?某个具体业务系统内还是整个团队、公司或者社区?
- 组件的生产过程是否规范、健壮和值得信赖?
- 组件如何被开发者发现和认识?
以上四点中,前两点是生产组件必须要思考的问题;第四点是组件如何推广运营的问题,这是另外一个话题,本文不展开探讨;第三点是开发者的基本素养,它决定了开发者如何看待这个组件,也间接暴露了开发者的素养和可信赖程度。
二、组件开发的最佳姿势
一个优秀的组件除了拥有解决问题的价值,还应该具备以下三个特点:
- 生产和交付的规范性;
- 优秀的质量和可靠性;
- 较高的可用性。
只有三者都能满足才可以称其为优秀组件,否则会给使用者带来各种各样的困惑:经常出 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": "1.0.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
\+ npm-speci-test@1.0.0
但光有这些基础信息肯定是不够的,作为一个规范的组件,我们还需要考虑:
- 我的代码托管在什么位置了;
- 别人可以在仓库里通过哪些关键词找到组件;
- 组件的运行依赖有哪些;
- 组件的开发依赖有哪些;
- 如果是命令行工具,入口文件是哪个;
- 组件支持哪些 node 版本、操作系统等。
一份比较通用的 package.json 文件内容如下:
{
"name": "@scope/xxxx",
"version": "1.0.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": "someone@gmail.com",
"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 规范,简单理解就是:
- 第一个版本一般建议用 1.0.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.2 质量和可维护性
开发组件的出发点是为了复用,其价值也体现在最大程度的复用上。团队内部的组件可能会在整个团队的多个系统间复用;公司内部通用的组件,可以为整个公司带来开发成本的降低;像react、antd这样的优秀开源组件,则会为整个社区和行业带来重大的价值。
组件是否可以放心使用,一个最简单直接的评判标准就是其质量的好坏。质量的好坏,除了上手试用以外,一般会通过几个方面来形成判断:
- 是否有高覆盖率的单元测试用例?
- 源码是否有规范的编码风格和语法检查?
- 源码是否使用了类型系统?
这些都直接决定了开发者对这个组件的评价。试想一下,如果开发了一个公共组件,没有规范的开发流程和编码风格检查,也没有单元测试,随手就发布了带 Bug 的版本。此时用户第一次安装使用时就报错,这会让开发者对组件产生强烈的不信任感,甚至这种不信任感会波及到作者本身。
因此,一个规范且合格的组件,至少要在保障组件的质量上做两件事情:1)引入 JavaScript 代码检查工具来规范代码风格和降低出错概率;2)引入单元测试框架,对组件进行必要的单元测试。此外,类型系统(TypeScript)的加入,会帮助组件提高代码质量和可维护性,是组件开发时的推荐选择。
2.2.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.2.2 单元测试和覆盖率
一直以来对于业务类的项目要不要写单测这个问题,个人的选择是可以不写。互联网倡导敏捷开发,快速迭代上线试错,需求变化太快,而为前端代码写单测本身的成本可能并不亚于代码本身。
但是组件的情况就完全不同了,组件是一组边界清晰、效果可预期的接口和能力的集合。而且和业务类代码相比,组件更具备通用性,也就是说不太会随着业务的变更而变更。并且组件的升级通常会对依赖组件的系统造成潜在影响,每一个版本的发布都理应对功能进行详尽的回归测试,以保障发布版本的质量。由于组件的测试通常依靠开发者自己保障,不会有专业的 QA 资源配备,因此单元测试就是最好的解决方案了。
JavaScript 的单元测试解决方案非常之多,呈百花齐放、百家争鸣的态势,耳熟能详的譬如:Jasmine、Mocha、Jest、AVA、Tape等,每一个测试框架都有其独特的设计,有些是开箱即用的全套解决方案,有些自身很简约,还需要配合其他库一起使用。
事实上,这些框架并无绝对的好坏,如何选择完全取决于个人和团队的喜好。这有一篇测试框架评测的文章,不妨一读:《JavaScript unit testing frameworks: Comparing Jasmine, Mocha, AVA, Tape and Jest [2018]》。
另外,我们依然可以通过 GitHub 上的 star 数和 Google trends 上的搜索量来略窥流行趋势一二。
Google trends 在中国的数据:
Google trends 在美国的数据:
可以看出 Jest 从 2014 年发布以来,增长势头是最猛的,并在短短 3 年内超过了其他老牌对手,成为目前最炙手可热的 Test Framwork。
除了测试框架选型以外,还有一个比较重要的指标要关注,就是测试覆盖率。推荐使用 nyc, 很多同学可能还用过一个名字比较特殊的库:istanbul。这两个库之前的渊源可以看下面这个 Issue 了解一下。
https://github.com/istanbuljs...
2.2.3 类型系统
如今的 JavaScript 已经不是原来那个在浏览器写写动效和交互的愣头小子了,它已经在 Web、Server、Desktop、App、IoT 等众多场景中证明了自己的价值,证明了自己可以被用来解决复杂的问题。事实上,JavaScript 正是通过将众多优秀的高质量组件、框架进行有机组合来提供这种能力的。
但是值得深思的是,JavaScript 采用了动态弱类型的设计,过于灵活的类型转换往往会带来一些不好的事情。试想这样的场景:
- 调用一个组件的 API 函数,却不清楚这个函数的参数类型,只能自己去撸代码;
- 对一个组件重要函数的参数做了优化重构,却无法评估影响面。
这些问题在强类型语言中有很好的解决方案,很多可能的错误会在编译期就被发现,很多改动的影响也会第一时间就被 IDE 告警。
事实上,越来越多的知名组件库已经开始引入强类型系统来辅助提高代码的质量和可维护性,比如 Vue.js、Angular、Yarn、Jest 等等。如果你想让自己具备类型思维,让组件具备更好的质量和可维护性,可以考虑把类型系统加到组件的脚手架中去。
目前可选的为 JavaScript 增加强类型检查的解决方案有 FaceBook 的 Flow 和 Microsoft 的 TypeScript,从当下的流行趋势来看,TypeScript 是绝对的首选。
2.3 可用性
组件的可用性,主要指的是从组件的使用者角度来看待组件的使用体验:
- 组件的文档是否完善且易于阅读?
- 组件暴露的 API 是否有详细且规范的输入输出描述?
- 是否有可以直接运行或者借鉴的 Demo?
- 文档是否有考虑国际化?
2.3.1 文档
一个好的组件文档至少应该具备以下内容结构:
一句话描述组件是什么,解决什么问题
# Usage
// 如何安装和使用,提供简单并且一目了然的示例
# API 文档
// 提供规范且详细的 API 接口文档,包括示例代码或者示例链接
# 补充信息,譬如兼容性描述等
// 如果是浏览器端组件,最好补充一下兼容性的支持情况;如果是 Node 端组件,也需要描述一下支持的 Node.js 版本范围
# ChangeLog
// 描述各个版本的重要变更内容以及 commit 链接
# 贡献、联系作者、License 等
// 如果组件希望他人一起参与贡献,需要有一个参与贡献的指南;除此之外,最好再提供一个可以直接联系上作者的方式
很多优秀的开发者可以很好地驾驭代码,但对如何写好一份组件文档却有些苦恼,这是因为代码是给自己看的,文档是给用户看的,这两种思维方式之间存在天然的差异。写文档时,需要换位思考,甚至可以把用户当小白,尽可能为小白考虑的多一些,如此可以提高文档的可读性,降低上手难度和使用的挫败感。
2.3.2 DEMO
对一个组件而言,Demo 的重要性不言而喻,还记得 Node.js 那个经典的几行代码创建一个 http server 的招牌式 demo 吗?可以说它几乎成为了 Node.js 的招牌和广告。
组件的 Demo 和文档都是为了可用性负责,但应该互有侧重,相得益彰。文档侧重于介绍关键信息、Demo 侧重于交付具体应用场景中的用法。
对于比较小的组件,这两者可以合二为一;对于 demo 代码量较多,且有多种使用方式和场景的情况,建议在 examples 目录下为每一种场景写一个可以直接运行的 Demo。
结语
组件是开发者创造的产品,在这个产品的生命周期中,第一次发布只是一个开始而已。如何让更多用户关注到,并且成为它的忠实用户,乃至参与贡献才是接下来要重点解决的问题。关于这个话题,本文就点到为止了,欢迎大家在下面留言分享自己在组件推广方面的经验和技巧。
因本人能力的局限性,文中难免有解读不正确之处,盼望大家可以交流指正!
想了解更多技术知识欢迎评论区留言或私信我!
欢迎关注公众号:fkdcxy 疯狂的程序员丶发现更多技术知识!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。