其实相对于 PC 端的组件库,移动端组件库有一个比较大的不同就是定制化要求较高。比如做 PC 端的 MIS 类的项目,如果使用 Vue 技术栈,大家往往会选择 element 或者是 iview,几乎都是拿来即用,最多换一下主题,很少会抠组件的细节,因为 MIS 类的项目是 to b 的,很多也是内部人员使用,所以对一些细节的要求并不高。而对于移动端项目,往往都是 to c 的,都有专门的 UI 设计,很少有完全符合要求的现成组件库能拿来用,所以 cube-ui 尽量提供一些通用性强的组件,并提供了自定义组件颜色的能力、和组件扩展能力,目的是让使用方 cube-ui 的基础上做二次开发,去满足自己的定制化需求。
Scroll 组件在其它地方都可以直接替换,另外除了有上拉加载和下拉刷新的场景,我们可以不给 Scroll 组件传 data 了,因为 1.5+ 版本的 better-scroll 已经有了根据 DOM 变化在合适时机自动 refresh 的能力了。
createAPI 的应用
前面我们简单地提到了 createAPI 的作用是把我们之前声明式的组件使用方式改变成 API 式的调用,为什么会有这样的需求呢?我们知道 Vue 推荐的就是声明式的组件使用方式,比如在使用一个组件 xxx,我们简单在使用的地方声明它就好了,就像这样:
<tempalte>
<xxx/>
</tempalte>
对于一般组件,这样使用并没有问题,但对于全屏类的弹窗组件,如果在一个层级嵌套很深的子组件中使用,仍然通过声明式的方式,很可能它的样式会受到父元素某些 CSS 的影响导致渲染不符合预期。这类组件最好的使用方式就是挂载到 body 下,但是我们如果是声明式地把这些组件挂载到最外层,对它们的控制也非常不灵活。其实最理想的方式是动态把这类组件挂载到 body 下,createAPI 就是干这个事情的。
先来看一下 createAPI 的文档,它可以把任何组件变成 API 式的调用。在我们的项目中有一个 Confirm 组件,它就是一个弹窗类型的组件。cube-ui 提供了所有弹窗类组件的基类组件 Popup,如果是新增一个弹窗类组件,推荐基于 Popup 做二次开发,不过我们的项目已经实现了全屏 Confirm 组件,目前需要实现的是调用它的使用可以动态挂载到 body 下,首先我们使用 createAPI 包装一下它:
背景
去年 6 月初,我在慕课网上线了一门 Vue.js 2.0 的高级实战课程音乐 WebApp 课程,教同学们如何去开发基础组件和业务组件。在一般大公司的实际项目中,并不会为每一个项目都去开发基础组件,他们往往会把基础组件收敛成一个组件库,供各个项目复用。滴滴也是如此,我们在去年初使用 Vue.js 去重构了我们的打车 WebApp,也抽象出了一套移动端组件库,在经过一年多的业务考验后,我们决定做开源,一方面是想把好的东西分享出去,并通过社区的反馈去完善我们的组件库;另一方面也是想让大家了解滴滴的前端,能吸引一些优秀的人才加入滴滴。于是在去年的 11 月份,我们团队开源了 cube-ui,到现在为止收到的反馈还算不错,也陆续有一些同学在生产环境也开始使用。
cube-ui 和其它同类型的开源组件库有一个很大的不同,它内部了使用了一个我们团队玩出来的“后编译”技术,它能帮我们玩出很多花样,比如减少组件包体积、支持 rem、支持自定义组件颜色等等,但带来好处的同时也会有一些不便(webpack 的配置会略显复杂),因此我们团队也为 cube-ui 在 vue-cli 的基础上扩展了一套脚手架,方便大家开箱即用。
其实相对于 PC 端的组件库,移动端组件库有一个比较大的不同就是定制化要求较高。比如做 PC 端的 MIS 类的项目,如果使用 Vue 技术栈,大家往往会选择 element 或者是 iview,几乎都是拿来即用,最多换一下主题,很少会抠组件的细节,因为 MIS 类的项目是 to b 的,很多也是内部人员使用,所以对一些细节的要求并不高。而对于移动端项目,往往都是 to c 的,都有专门的 UI 设计,很少有完全符合要求的现成组件库能拿来用,所以 cube-ui 尽量提供一些通用性强的组件,并提供了自定义组件颜色的能力、和组件扩展能力,目的是让使用方 cube-ui 的基础上做二次开发,去满足自己的定制化需求。
因为毕竟 cube-ui 是从滴滴的业务中抽象出来的,在做滴滴相关业务的时候,这些组件都能很好的满足需求,但是换成一个新的项目,cube-ui 好不好用呢,于是我想到了我的音乐课程项目,它有一些基础组件是可以从 cube-ui 里拿的,但是整体的配色风格和 cube-ui 的默认配色又完全不一样,正好可以来检验一波,接下来我分享一下 cube-ui 重构音乐课程项目的经验。
Webpack 配置修改
由于我们是现有项目,并不能使用脚手架去初始化项目,所以我们需要根据官网的文档去做 webpack 的相关配置。这里我要稍微提醒一些同学,在使用一个开源项目的时候,最好的方式就是阅读它的文档,遇到问题首先想的是查看它的 issue。那么 cube-ui 的文档在这里,我们来看一下快速上手部分。
安装 cube-ui
首先需要安装 cube-ui,这块很简单,直接运行命令就好了。
后编译配置
后编译简单的理解就是把编译工作交给应用来完成,也就是使用 cube-ui 的项目vue-music 来完成编译。由于是现成的项目,我们不能用脚手架初始化项目,那么所有的后编译相关的 webpack 配置都需要自己来动手,接下来我会一边教大家配置,一边来解释这些配置的作用。
修改 package.json 并安装依赖
首先需要修改的是 package.json 文件,我们需要在
devDependencies
添加几个插件,先简单对它们做一些介绍。babel-plugin-transform-modules
babel-plugin-transform-modules
是从babel-transform-imports
fork 来的,加上了对 style 的支持,为了解决组件按需引入的问题。stylus & stylus-loader
stylus
和stylus-loader
是为了编译 stylus 文件用的,因为 cube-ui 源码的 css 部分使用了 stylus 预处理器。webpack-post-compile-plugin
是为了解决后编译嵌套问题编写的 webpack 插件,因为在默认情况下,webpack 是不会编译node_modules
目录下的模块的,而我们的 cube-ui 是安装在node_modules
下的,为了编译它,需要在 webpack 配置文件中显示地声明include
指向node_modules
下的 cube-ui,例如:但这里会有一个问题,如果 cube-ui 一旦也后编译依赖其它模块,作为编译的应用方也需要把它们显示地写进
include
里,但这显然是不合理的,因为应用不应该知道 cube-ui 依赖的模块,每个模块只应该声明它自身的后编译依赖即可。那么webpack-post-compile-plugin
就是来解决这个问题的,它会读取每个模块 package.json 文件中声明的compileDependencies
,并递归去查找后编译依赖,然后添加到应用 webpack 配置的include
中,所以在我们应用项目中的 package.json 文件中,我们指定了compileDependencies
为[cube-ui]
。修改 .babelrc
这个配置项是为了配合
babel-plugin-transform-modules
使用的,给按需引入提供了一个语法糖。举个例子,当我们在代码中按需引入 cube-ui 的组件,如:相当于:
因为是引入源码,所以
import
的路径指向了src
目录,显然前者的写法比后者优雅了很多,并且一旦我们不用后编译,也不用去修改源码的import
方式,只需要修改 .babelrc 文件即可。修改 webpack.base.conf.js
这里就是对
webpack-post-compile-plugin
插件的应用,把它添加到plugins
中即可。修改 build/utils.js 中的
exports.cssLoaders
函数这里了一个 stylus 的配置项
'resovle url':true
,目的是为了解决被引入的 stylus 文件再去引入资源的相对路径的问题,参考官方文档。修改 vue-loader.conf.js
这里需要强制指定
css-loader
的选项extract
为 false,否则我们通过npm run build
编译后的项目异步加载 vue 组件会有问题。那么到这里,后编译的 webpack 配置就告一段落了,核心思想就是让我们的应用引入 cube-ui 的源码,并且接管 cube-ui 的编译工作。
Vue-music 源码修改
接下来就是修改我们项目的源码,我们会用到 cube-ui 的基础样式、
Scroll
滚动组件、Slide
轮播图组件、IndexList
索引列表组件以及createAPI
模块去把我们已有的Confirm
组件变成 API 式的调用。我们会在 main.js 里引用这些组件和模块:这里我们会
import Style
,它的作用是引入 cube-ui 提供的一些 reset 样式、基础样式和字体图标样式,那么对于我们的项目,就可以把 reset 样式移除了。对于组件的引用我们会使用
Vue.use
注册插件的方式,它内部会调用Vue.component
全局注册组件,这样我们就可以在任何组件内部里使用这些组件了。createAPI
是把我们之前声明式的组件使用方式改变成 API 式的调用,这块儿稍后我们会详细说明。IndexList 组件修改
音乐 App 的歌手页面有一个歌手列表,如下图所示:
它恰好可以使用 cube-ui 提供的
IndexList
组件,在我的教学课程中,我也是把它单独抽象出来的一个基础组件,所以替换就变的很容易了。学会使用一个组件,最好的方式就是看它的文档。cube-ui 提供的
IndexList
样式如下:可以看到相对于 cube-ui 的
IndexList
,我们的歌手页面的背景颜色、列表的样式都有所不同,幸好 cube-ui 支持自定义组件颜色和IndexList
的插槽功能,我们可以很好的解决这两个问题。IndexList
组件的颜色cube-ui 提供了自定义组件颜色的能力,我们打开它的文档,实际上只需要做两件事情。 首先在 src 目录下新建
theme.styl
文件,然后填入如下代码:这里我们用到了 stylus 的一个条件赋值的语法,它会先判断有没有对这个变量赋值,如果已经赋值了,则不会去覆盖这个变量的值。那么这里我们引入了 vue-music 项目中对于颜色定义的一些变量,把它赋值给了 cube-ui 关于
IndexList
组件所引用的一些颜色变量。接下来配置 webpack,修改
build/utils.js
里的exports.cssLoaders
函数中的stylusOptions
这里通过配置 stylus 选项,新增
import
配置项指向我们刚才创建的theme.styl
文件,可以达到的效果是在 stylus 的编译过程中,对每一个.styl
文件以及.vue
中的 stylus 部分都优先import
这个主题文件,这样就实现了组件颜色的自定义,会优先使用我们在theme.styl
文件中的颜色。IndexList
的插槽由于我们的列表项是图文混排的布局,和默认的样式不一样,因此我们需要用到插槽来自定义列表项布局,参考文档,我们对模板代码的修改如下:
我们使用 cube-ui 提供的
cube-index-list-group
和cube-index-list-item
做二重循环,因为是组件的循环,所以循环的过程中需要设置 key。这里有个地方需要注意一下,我们给IndexList
组件传的数据是 singers,而 singers 的数据结构是有要求的,它本身是一个数组,对于数组的每一项,它有组名name
和数据项items
。这个字段名和我们项目之前定义的略微不同,所以我们在处理从服务端拿到的歌手数据的时候,需要构造符合IndexList
约定的数据结构。最后还有一处细节的修改,我们项目中的每一组的标题样式和 cube-ui 的
IndexList
略微不同,可以通过覆盖 CSS 的方式对样式做修改。这里要注意的是,一旦我们要覆盖某个子组件的样式,那么引用该子组件的父组件(在我们这个 case 是
Singer
组件)样式部分就不能使用scoped
特性,因为如果设置了scoped
,Vue 在初始化的过程中会给组件的样式加上属性 id,那么就不能够覆盖 cube-ui 中的组件样式了。Slide 组件修改
音乐 App 的推荐页面用到了轮播图,如下图所示: 在我们的项目中已经封装了轮播图组件,它恰好可以使用 cube-ui 的
Slide
组件无缝替换,同样的我们来看一下Slide
组件 的文档,修改代码如下:对于
Slide
组件内部的元素,我们用cube-slide-item
组件来做循环,由于底部的dots
样式很不一样,我们使用了作用域插槽,因为需要根据子组件的 current 来决定它渲染的active
样式;并且我们想让 dots 的位置向上偏移,所以我们依然采用覆盖 CSS 的方式:同样,我们也需要把
Recommend
组件 stylus 部分的scoped
移除。Scroll 组件修改
音乐 App 项目在 better-scroll 的基础上插件封装了 Scroll 组件,并在项目中大量应用,比如推荐页面、歌手详情页、搜索页面、歌曲列表、甚至是歌词列表。cube-ui 中也基于 better-scroll 封装了
Scroll
组件,它的功能更完善,所以我们决定替换Scroll
组件。Scroll
组件在项目中应用的地方非常多,这里我挑一个比较有代表性的场景,就是搜索页面的Suggest
组件,如下所图所示:Suggest
组件下方的列表是根据检索的关键词动态渲染的,它不仅可以局部滚动,还有一个上拉加载的功能,它就是移动端场景下分页功能的实现。我们完全可以用 cube-ui 的Scroll
组件来实现它,同样我们也是先去阅读它的文档,然后做如下代码的修改:这里需要注意两个地方,一个是
scrollOptions
,另一个是pullingUp
事件的回调函数searchMore
。scrollOptions
这个参数是 better-scroll 的 options 配置,由于我们使用了上拉加载的功能,所以需要配置pullUpLoad
,这里我们指定了threshold
为 0,也就是刚到底部就触发pullingUp
事件,txt
设置为空因为在我们的项目中上拉加载不需要任何文案。searchMore
这个回调函数的作用就是根据条件去加载新的数据,如果没有更多数据了,我们直接调用this.$refs.suggest.forceUpdate()
通知 Scroll 组件结束上拉的过程,另外单次加载数据发生任何异常的时候我们也都应该调用一次this.$refs.suggest.forceUpdate()
。Scroll
组件在其它地方都可以直接替换,另外除了有上拉加载和下拉刷新的场景,我们可以不给Scroll
组件传 data 了,因为 1.5+ 版本的 better-scroll 已经有了根据 DOM 变化在合适时机自动refresh
的能力了。createAPI
的应用前面我们简单地提到了
createAPI
的作用是把我们之前声明式的组件使用方式改变成 API 式的调用,为什么会有这样的需求呢?我们知道 Vue 推荐的就是声明式的组件使用方式,比如在使用一个组件 xxx,我们简单在使用的地方声明它就好了,就像这样:对于一般组件,这样使用并没有问题,但对于全屏类的弹窗组件,如果在一个层级嵌套很深的子组件中使用,仍然通过声明式的方式,很可能它的样式会受到父元素某些 CSS 的影响导致渲染不符合预期。这类组件最好的使用方式就是挂载到 body 下,但是我们如果是声明式地把这些组件挂载到最外层,对它们的控制也非常不灵活。其实最理想的方式是动态把这类组件挂载到 body 下,
createAPI
就是干这个事情的。先来看一下
createAPI
的文档,它可以把任何组件变成 API 式的调用。在我们的项目中有一个Confirm
组件,它就是一个弹窗类型的组件。cube-ui 提供了所有弹窗类组件的基类组件Popup
,如果是新增一个弹窗类组件,推荐基于Popup
做二次开发,不过我们的项目已经实现了全屏Confirm
组件,目前需要实现的是调用它的使用可以动态挂载到 body 下,首先我们使用createAPI
包装一下它:接着我们就可以在组件内部通过
this.$createConfirm
的方式调用它,我们在Search
组件中改变一下Confirm
组件的调用方式:当执行
.show
的时候,cube-ui 内部会把Confirm
组件动态挂载到 body 下。总结
到此这篇文章的主体内容就介绍完了,看似简单,但实际上我在重构的过程中还是发现了一些问题,顺便也对 cube-ui 和 better-scroll 做了一些优化。希望我的学生在看完这篇文章后能真正自己尝试着做一遍重构,因为很多细节的问题只有你去尝试做了才能发现,只有发现并解决问题你才能积累更多的经验;重构的过程中务必要看文档,遇到问题一定要自己先思考一遍,实在解决不了再求助。另外我也希望大家也多多使用 cube-ui,哪怕 cube-ui 能帮你解决一个小小的需求,那么我们觉得开源这件事情都是非常有意义的。如果大家在使用的过程中遇到一些问题,欢迎给我们提 issue & pr,帮助我们一起共建 cube-ui,也可以加 qq 群与我们交流,二维码如下:
如果 cube-ui 对你有帮助,也不要吝啬你的 star。
另附上 vue-music 项目的线上地址,扫下方二维码体验:
如果想跟着我学习这门 Vue.js 的进阶课程,真心想学到知识,请务必购买正版课程,你一定不会失望。