需求背景
最近在项目中遇到了一个类似Collapse的交互需求,因此到github上找了一圈关于Vue Collapse的相关轮子,但是多少都有些问题。有的是实现问题,例如vue2-collapse,伸缩部分采用max-height指定动画,存在缺陷;还有的是扩展性问题,遇到定制场景比较棘手。因此,决定自己撸一个Collapse组件。从项目中的一个需求,到目前已将它开源并发布到npm,还是踩了许多坑的。代码虽然简单,但是过程却不太容易。因此这篇文章不是安利这款组件r-collapse-vue,仅仅是想记录一下整个开发生命周期,需要做什么,以及遇到什么问题。当然了,如果这个组件或是这篇文章对你有帮助,劳烦点进去给个star,万分感谢~
开发流程
我们的整个开发流程,可以简单的总结如下:
- 项目脚手架搭建(Vue CLI3)
- 组件功能开发
- 单元测试(Vue Test Utils + Jest)
- 文档编写(Vue Styleguidist + Github Pages)
- 发布NPM
- 持续集成配置(TravisCI)
我们来详细聊一聊每个过程是如何实施的,且遇到了哪些问题。
脚手架搭建
脚手架我们直接使用Vue CLI来搭建即可,其已经提供了丰富的功能,并且可以通过vue.config.js扩展webpack的能力。但是要注意的是,我们的构建产物是一个模块,而不是我们平时在项目中构建出一个应用。我们希望构建出来的模块是一个兼容CommonJs或是UMD,以便于使用者在不同的环境中引用。所幸,Vue CLI3也给我提供了这样一个功能,详细可参考文档。
其次,本次开发我选择了TypeScript,脚手架默认集成了vue-property-decorator。使用之后直观的感受就是,Vue的整个生态对TS的支持还不够完善,但整体还是比较爽的,期待官方在3.0中能够彻底支持TS。本文主题不是讨论TS,因此简单罗列下使用时遇到的问题:
- 在template中无法做到智能提示,需要智能提示只能使用tsx,这一点是比较痛苦的
- 定义Prop时需要加非空断言(!:),否则会报错,例如:
@Prop({ required: true })
public value!: String;
- 使用Vue Test Utils写单测时,无法对自定义的Vue组件进行类型推导,见下文
- 使用Vue Styleguidist编写文档demo不支持TS,见下文
组件功能开发
在日常写业务的时候,我们可能会在组件当中耦合很多的业务逻辑。但是作为一个通用组件,我们在开发的时候要尽可能保证它的扩展性,因此我们希望达到的一个目标就是:在保证开发体验的前提下提高扩展性。对于Collapse组件,UI方面一般都是按照各自的设计稿来自行编写的,因此我们只需要提供功能即可。更好的方式是提供默认的UI,但又可以支持完全定制,这个是目前r-collapse-vue可以完善的一个点。
在进行功能设计的过程中,我们要先确定我们需要支持哪些功能,以r-collapse-vue举例,需要提供的功能包括:
- 基本的展开/收缩(支持动画)
- 手风琴模式
- 自定义点击事件
- Collapse嵌套
在实现的过程中,我们也需要思考很多细节,举几个例子:
- 使用者如何控制每一个Collapse的状态?
最简单的想法是传递一个类似叫做status的prop,在每一个Collapse内部去维护这个状态。但是这样会有一个问题,我们如何去支持手风琴模式,即一个展开另外的都需要收起。按照这种做法,需要用一个父组件包裹,去获取每一个Collapse子组件的实例,调用实例方法去控制。这样做不是不行,vue2-collapse就是这么做的,但是我认为不够优雅。因此我们重新整理思路,每一个Collapse之间的状态可能会互相影响,我们常用的解决方法是状态提升,因此我的做法是抽象两个组件,Collapse和CollapsePanel,Collapse即是父组件,提供状态控制,将状态传递给其内部嵌套的CollapsePanel,在内部消化掉所有的逻辑,这更加符合单向数据流的思想,站在使用者角度来看,写法也能够相对统一,使用时我们只需这么写:
<r-collapse v-model="activeKeys">
<r-collapse-panel name="a">xxxx</r-collapse-panel>
<r-collapse-panel name="b">xxxx</r-collapse-panel>
</r-collapse>
- 实际场景中经常会对展开和收缩进行样式区分,如何帮助使用者提升开发体验?
见上面的代码,我们在CollapsePanel中传入了一个name属性作为唯一标识,此时使用者可结合activeKeys自行判断当前panel是否展开:
<r-collapse v-model="activeKeys">
<r-collapse-panel
name="a"
:class="activeKeys.includes('a') ? 'active': ''"
>
xxxx
</r-collapse-panel>
<r-collapse-panel name="b">xxxx</r-collapse-panel>
</r-collapse>
这种方法虽然可以,但是存在两个问题:
- 用户需要自行添加逻辑,体验不够友好
- 每次重新渲染都会执行额外的逻辑判断,性能不够友好
因此,可以提供一个activeClass的prop,让使用者可以自定义展开状态的类名,就可以避免以上的问题。
这些细节问题看似简单,但是作为一个通用组件的开发者,我们应该经常站在使用者的角度看问题,才能不断地提升组件的开发体验。
单元测试
一个优秀的开源组件一定少不了单元测试,例如Ant Design等开源库都有着很高的单测覆盖率。一开始写单测可能会觉得耗时、没有必要,但其实单测能够带来诸多的好处:
- 单测相较手动测试,能够减少bug率,覆盖的场景更全,且测试较为方便
- 开源的组件可能会有很多的维护者,单测能够降低模块之间互相影响产生bug的概率
- 使用者一般都会选择单测覆盖率较高的轮子
因此,单测必不可少,目前前端常见的选择包括:
- Jest,FaceBook出品,配置简单,使用JSDOM模拟测试环境,当遇到操作真实DOM的场景,如获取scrollHeight等比较乏力
- Karma + Mocha,Mocha同Jest都是测试框架,而Karma为框架提供了真实的浏览器测试环境,如果代码中对DOM操作较多,建议使用这种组合。但是Mocha配置较复杂,且需要自行安装断言库
Vue当中已经给我们提供了单测相关的工具Vue Test Utils,它提供了很多功能,如组件挂载,获取实例等等,使用它配合Jest或者Mocha能够比较方便的完成单测,详情参考文档。
在编写单测时,我们需要注意,对于UI组件来说,不应一味追求行级覆盖率,应当只关注输入输出,避免涉及过多的实现细节,从而避免琐碎的测试。例如,我们测试展开功能,只需要触发click,检测status是否为true即可,无需关注过程中是触发了xxx事件还是发生了其他事情,这样当我们的逻辑修改后能够保证单测还能有效。同时,在用TS编写单测时,通过Vue Test Utils创建的wrapper是普通的Vue类型,因此自定义的Vue组件无法进行类型推导,此时要获取实例属性时需要通过(wrapper.vm as any).xxx来获取。经查阅资料,官方表示目前没法解决这个问题,只能使用这种方式。
文档编写
一个好的文档能够方便使用者明白你的设计理念,因此我们想要的文档不仅需要有完整的API描述,并且在展示demo时能够同时展示源码,类似于在Ant Design或Element中那样。我们这边使用的是Vue Styleguidist。
它通过vue-docgen-api,能够将注释转换成属性描述展现在页面上。因此我们只需要写注释,就能够生成组件属性相关的文档。而我们的另一个需求,在展示demo时能够同时展示源码,它也能够做到。我们可以通过两种方式:
- 在Vue组件中使用<docs></docs>标签来写demo,这样做对组件有侵入,感觉不太好
- 新建一个markdown文件,内部通过特殊的标记写入vue代码即可
我们选用第二种方式,但是又遇到了许多坑。比如写入md的Vue代码不支持TS,试了很多的方法都没有解决,后来还是改成了JS写法;还有SCSS使用嵌套时,嵌套的内容未被正确解析,后改成了CSS。其实这个东西的实现难度并不高,在md中写Vue无非就是写个webpack插件解析.md格式的文件,取出Vue的部分通过vue-loader处理,鉴于bug这么多且样式我认为不够美观,之后有时间可以再造个轮子玩一玩。
在Vue CLI3中使用Vue Styleguidist十分方便,只要运行:
vue add styleguidist
然后在package.json的scripts中添加:
"serve:doc": "vue-cli-service styleguidist",
"build:doc": "vue-cli-service styleguidist:build"
就可以拆箱即用了。
文档编写完成,我们执行yarn build:doc构建文档,发现输出的是一个html文件,此时我们可以选择使用Github Pages来作为我们的静态资源服务器展示文档,因为它方便部署且免费。过程如下:
- 将styleguide.config.js中的styleguideDir选项改为"docs",即将build的目标目录设置为docs
- 在Github对应仓库的settings中将GitHub Pages的Source选项设置为master branch/docs folder,意味着会自动从仓库的docs目录获取静态资源
这样每次更新docs会自动部署更新文档。
说完文档,我们还需要编写在Github上展示的README,这里推荐一个生成README的库,readme-md-generator,格式非常简洁且美观。在README中,我们可以添加如下的小图标:
这个可以使用shields生成,它能关联你的NPM、Github等等,实时更新icon信息,有了它文档逼格瞬间高多了。
发布NPM
要将包发布到NPM,我们需要做如下的准备工作:
- 到https://www.npmjs.com/上注册一个NPM的账号
- 本地执行
npm login --registry=https://registry.npmjs.org
注意,这边加上registry为了防止在全局或当前环境覆写.npmrc,导致登录的不是NPM源。
- 修改package.json的配置,可以参考v-collapse-vue的部分配置:
{
"name": "r-collapse-vue",
"version": "1.0.0",
"description": "a collapse component for VueJs",
"author": {
"name": "Ray",
"email": "[email protected]"
},
"main": "dist/r-collapse-vue.common.js",
"files": [
"dist"
],
"keywords": [
"Vue",
"collapse"
],
"publishConfig": {
"registry": "https://registry.npmjs.org"
},
"repository": {
"type": "git",
"url": "[email protected]:DanceOnBeat/r-collapse-vue.git"
}
}
- 最后执行npm publish即可完成发布
每次发布新版本之前,我们可以通过
npm version major/minor/patch -m 'xxx'
来修改版本号并且打上tag,此tag非NPM的dist-tag,而是Git的tag。一个版本对应一个tag,并通过
git push origin master --tags
将tag也推到远程仓库,这样在仓库中我们就能清楚地看到发布的记录,方便日后回滚之类的操作。具体的版本规则可以参考semver规范。
持续集成(CI)
当开发结束后,我们需要跑测试,测试通过后,还需要构建生成dist目录,最后发布到NPM。每次修改都做这样一套操作实在繁琐,并且容易遗漏步骤,这时候我们就需要使用CI将我们的流程自动化,我在这边选择了TravisCI。同时,我们还可以通过Codecov,将我们的单测报告上传至Codecov服务器,这样就能同步更新Codecov的icon。
在配置CI时,我原本将生成docs的步骤也添加了进去,此时我们在deploy中会有两个步骤,如下:
deploy:
- provider: npm
email: [email protected]
api_key: $AUTH_TOKEN
on:
tags: true
branch: master
skip_cleanup: true
- provider: pages
skip_cleanup: true
github_token: $GITHUB_TOKEN
keep_history: true
target_branch: master
on:
branch: master
这会造成一个问题是provider: pages会将CI服务器生成的新的docs目录push到我们的Github仓库,这又会触发一次CI,以至于无限循环。后来也没找到合适的解决方案,又考虑到文档不经常更新,就将文档部署相关的部分从CI中移除了。如果大家有合适的解决方案,可以留言告诉我一下,不胜感激。
之前我们提到一个NPM发布版本对应一个tag,因此我们可以在配置中添加
if: tag IS present
限定只在提交了tag才触发一次自动化构建,这样基本上就大功告成了。
总结
这是一次非常有趣的造轮体验,代码虽然不难,但是过程中又学习到了很多新的东西,包括单元测试、文档编写等等,希望这篇文章能给准备造轮或想要造轮的小伙伴提供一点帮助。
原文地址:https://www.cnblogs.com/danceonbeat/p/11063773.html