从0到1发布一个Vue Collapse组件

释放双眼,带上耳机,听听看~!

需求背景

最近在项目中遇到了一个类似Collapse的交互需求,因此到github上找了一圈关于Vue Collapse的相关轮子,但是多少都有些问题。有的是实现问题,例如vue2-collapse,伸缩部分采用max-height指定动画,存在缺陷;还有的是扩展性问题,遇到定制场景比较棘手。因此,决定自己撸一个Collapse组件。从项目中的一个需求,到目前已将它开源并发布到npm,还是踩了许多坑的。代码虽然简单,但是过程却不太容易。因此这篇文章不是安利这款组件r-collapse-vue,仅仅是想记录一下整个开发生命周期,需要做什么,以及遇到什么问题。当然了,如果这个组件或是这篇文章对你有帮助,劳烦点进去给个star,万分感谢~

开发流程

我们的整个开发流程,可以简单的总结如下:

  1. 项目脚手架搭建(Vue CLI3)
  2. 组件功能开发
  3. 单元测试(Vue Test Utils + Jest)
  4. 文档编写(Vue Styleguidist + Github Pages)
  5. 发布NPM
  6. 持续集成配置(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嵌套

在实现的过程中,我们也需要思考很多细节,举几个例子:

  1. 使用者如何控制每一个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>
  1. 实际场景中经常会对展开和收缩进行样式区分,如何帮助使用者提升开发体验?

见上面的代码,我们在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等开源库都有着很高的单测覆盖率。一开始写单测可能会觉得耗时、没有必要,但其实单测能够带来诸多的好处:

  1. 单测相较手动测试,能够减少bug率,覆盖的场景更全,且测试较为方便
  2. 开源的组件可能会有很多的维护者,单测能够降低模块之间互相影响产生bug的概率
  3. 使用者一般都会选择单测覆盖率较高的轮子

因此,单测必不可少,目前前端常见的选择包括:

  • 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来作为我们的静态资源服务器展示文档,因为它方便部署且免费。过程如下:

  1. 将styleguide.config.js中的styleguideDir选项改为\"docs\",即将build的目标目录设置为docs
  2. 在Github对应仓库的settings中将GitHub Pages的Source选项设置为master branch/docs folder,意味着会自动从仓库的docs目录获取静态资源

这样每次更新docs会自动部署更新文档,类似于这样https://danceonbeat.github.io/r-collapse-vue/,唯一的缺点就是国内打开有点慢。

说完文档,我们还需要编写在Github上展示的README,这里推荐一个生成README的库,readme-md-generator,格式非常简洁且美观。在README中,我们可以添加如下的小图标:

从0到1发布一个Vue Collapse组件

这个可以使用shields生成,它能关联你的NPM、Github等等,实时更新icon信息,有了它文档逼格瞬间高多了。

发布NPM

要将包发布到NPM,我们需要做如下的准备工作:

  1. https://www.npmjs.com/上注册一个NPM的账号
  2. 本地执行
npm login --registry=https://registry.npmjs.org

注意,这边加上registry为了防止在全局或当前环境覆写.npmrc,导致登录的不是NPM源。

  1. 修改package.json的配置,可以参考v-collapse-vue的部分配置:
{
  \"name\": \"r-collapse-vue\",
  \"version\": \"1.0.0\",
  \"description\": \"a collapse component for VueJs\",
  \"author\": {
    \"name\": \"Ray\",
    \"email\": \"zhurui0904@gmail.com\"
  },
  \"main\": \"dist/r-collapse-vue.common.js\",
  \"files\": [
    \"dist\"
  ],
  \"keywords\": [
    \"Vue\",
    \"collapse\"
  ],
  \"publishConfig\": {
    \"registry\": \"https://registry.npmjs.org\"
  },
  \"repository\": {
    \"type\": \"git\",
    \"url\": \"git@github.com:DanceOnBeat/r-collapse-vue.git\"
  }
}
  1. 最后执行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: zhurui0904@gmail.com
    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才触发一次自动化构建,这样基本上就大功告成了。

总结

这是一次非常有趣的造轮体验,代码虽然不难,但是过程中又学习到了很多新的东西,包括单元测试、文档编写等等,希望这篇文章能给准备造轮或想要造轮的小伙伴提供一点帮助。

给TA打赏
共{{data.count}}人
人已打赏
随笔日记

Python 爬虫从入门到进阶之路(三)

2020-11-9 5:29:23

随笔日记

微服务之旅:从Netflix OSS到 Istio Service Mesh

2020-11-9 5:29:26

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索