vue-music 音乐 App 之 cube-ui 重构

背景

去年 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,这块很简单,直接运行命令就好了。

npm install cube-ui --save

后编译配置

后编译简单的理解就是把编译工作交给应用来完成,也就是使用 cube-ui 的项目vue-music 来完成编译。由于是现成的项目,我们不能用脚手架初始化项目,那么所有的后编译相关的 webpack 配置都需要自己来动手,接下来我会一边教大家配置,一边来解释这些配置的作用。

修改 package.json 并安装依赖

{
  // webpack-post-compile-plugin 依赖 compileDependencies
  "compileDependencies": ["cube-ui"],
  "devDependencies": {
    "babel-plugin-transform-modules": "^0.1.0",
    // 新增 stylus 相关依赖
    "stylus": "^0.54.5",
    "stylus-loader": "^2.1.1",
    "webpack-post-compile-plugin": "^0.1.2"
  }
}

首先需要修改的是 package.json 文件,我们需要在 devDependencies 添加几个插件,先简单对它们做一些介绍。

stylusstylus-loader 是为了编译 stylus 文件用的,因为 cube-ui 源码的 css 部分使用了 stylus 预处理器。

  • webpack-post-compile-plugin

webpack-post-compile-plugin 是为了解决后编译嵌套问题编写的 webpack 插件,因为在默认情况下,webpack 是不会编译 node_modules 目录下的模块的,而我们的 cube-ui 是安装在 node_modules 下的,为了编译它,需要在 webpack 配置文件中显示地声明 include 指向 node_modules 下的 cube-ui,例如:

module: {
  rules: [
    {
      test: /.js$/,
      loader: 'babel-loader',
      include: [resolve('src'), resolve('node_modules/cube-ui')]
    },
    // ...
  ]
 }

但这里会有一个问题,如果 cube-ui 一旦也后编译依赖其它模块,作为编译的应用方也需要把它们显示地写进 include 里,但这显然是不合理的,因为应用不应该知道 cube-ui 依赖的模块,每个模块只应该声明它自身的后编译依赖即可。那么 webpack-post-compile-plugin 就是来解决这个问题的,它会读取每个模块 package.json 文件中声明的 compileDependencies ,并递归去查找后编译依赖,然后添加到应用 webpack 配置的 include 中,所以在我们应用项目中的 package.json 文件中,我们指定了 compileDependencies[cube-ui]

修改 .babelrc

{
  "plugins": [
    ["transform-modules", {
      "cube-ui": {
        // 注意: 这里的路径需要修改到 src/modules 下
        "transform": "cube-ui/src/modules/${member}",
        "kebabCase": true
      }
    }]
  ]
}

这个配置项是为了配合 babel-plugin-transform-modules 使用的,给按需引入提供了一个语法糖。举个例子,当我们在代码中按需引入 cube-ui 的组件,如:

import { Button } from 'cube-ui'

相当于:

import Button from 'cube-ui/src/modules/button'

因为是引入源码,所以 import 的路径指向了 src 目录,显然前者的写法比后者优雅了很多,并且一旦我们不用后编译,也不用去修改源码的 import 方式,只需要修改 .babelrc 文件即可。

修改 webpack.base.conf.js

var PostCompilePlugin = require('webpack-post-compile-plugin')
module.exports = {
  // ...
  plugins: [
    // ...
    new PostCompilePlugin()
  ]
  // ...
}

这里就是对 webpack-post-compile-plugin 插件的应用,把它添加到 plugins 中即可。

修改 build/utils.js 中的 exports.cssLoaders 函数

exports.cssLoaders = function (options) {
  // ...
  const stylusOptions = {
    'resolve url': true
  }
  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
  return {
    css: generateLoaders(),
    postcss: generateLoaders(),
    less: generateLoaders('less'),
    sass: generateLoaders('sass', { indentedSyntax: true }),
    scss: generateLoaders('sass'),
    stylus: generateLoaders('stylus', stylusOptions),
    styl: generateLoaders('stylus', stylusOptions)
  }
}

这里了一个 stylus 的配置项 'resovle url':true ,目的是为了解决被引入的 stylus 文件再去引入资源的相对路径的问题,参考 官方文档

修改 vue-loader.conf.js

module.exports = {
  loaders: utils.cssLoaders({
    sourceMap: sourceMapEnabled,
    extract: false
  }),
  // ...
}

这里需要强制指定 css-loader 的选项 extract 为 false,否则我们通过 npm run build 编译后的项目异步加载 vue 组件会有问题。

那么到这里,后编译的 webpack 配置就告一段落了,核心思想就是让我们的应用引入 cube-ui 的源码,并且接管 cube-ui 的编译工作。

Vue-music 源码修改

这篇文章我不会把所有代码的修改都 forEach 一遍,那样太浪费时间,我会挑重点的地方讲,具体的修改都可以在项目代码的 use-cube-ui 分支里看到。这里我想强调一下,我的项目代码托管在 GitHub 私仓,并不开源,只有购买正版课程的学生才能访问,那些不知道从哪些途径搞到我项目初始代码还开源大肆宣传的人,你们不尊重我的劳动成果看盗版视频也就罢了,拿这个骗 star,不害臊吗?

BTW,官方正版的项目代码是一直维护的,并且修复了 70+ issue,如果真心想学知识的同学,花几百块钱买正版课程一定是物超所值。

接下来就是修改我们项目的源码,我们会用到 cube-ui 的基础样式、 Scroll 滚动组件、 Slide 轮播图组件、 IndexList 索引列表组件以及 createAPI 模块去把我们已有的 Confirm 组件变成 API 式的调用。我们会在 main.js 里引用这些组件和模块:

import {
  Style,
  IndexList,
  Scroll,
  Slide,
  createAPI
} from 'cube-ui'

import Confirm from 'base/confirm/confirm.vue'

Vue.use(IndexList)
Vue.use(Scroll)
Vue.use(Slide)

createAPI(Vue, Confirm, ['confirm', 'click'], true)

这里我们会 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 文件,然后填入如下代码:

@import "./common/stylus/variable.styl"

// index-list
$index-list-bgc := $color-background
$index-list-anchor-color := $color-text-l
$index-list-anchor-bgc := $color-highlight-background
$index-list-nav-color := $color-text-l
$index-list-nav-active-color := $color-theme

这里我们用到了 stylus 的一个 条件赋值 的语法,它会先判断有没有对这个变量赋值,如果已经赋值了,则不会去覆盖这个变量的值。那么这里我们引入了 vue-music 项目中对于颜色定义的一些变量,把它赋值给了 cube-ui 关于 IndexList 组件所引用的一些颜色变量。

接下来配置 webpack,修改 build/utils.js 里的 exports.cssLoaders 函数中的 stylusOptions

const stylusOptions = {
    'resolve url': true,
    // 这里 新增 import 配置项,指向自定义主题文件
    import: [path.resolve(__dirname, '../src/theme')]
  }

这里通过配置 stylus 选项,新增 import 配置项指向我们刚才创建的 theme.styl 文件,可以达到的效果是在 stylus 的编译过程中,对每一个 .styl 文件以及 .vue 中的 stylus 部分都优先 import 这个主题文件,这样就实现了组件颜色的自定义,会优先使用我们在 theme.styl 文件中的颜色。

  • 自定义 IndexList 的插槽

由于我们的列表项是图文混排的布局,和默认的样式不一样,因此我们需要用到插槽来自定义列表项布局,参考文档,我们对模板代码的修改如下:

  
{{item.name}}

我们使用 cube-ui 提供的 cube-index-list-groupcube-index-list-item 做二重循环,因为是组件的循环,所以循环的过程中需要设置 key。这里有个地方需要注意一下,我们给 IndexList 组件传的数据是 singers,而 singers 的数据结构是有要求的,它本身是一个数组,对于数组的每一项,它有组名 name 和数据项 items 。这个字段名和我们项目之前定义的略微不同,所以我们在处理从服务端拿到的歌手数据的时候,需要构造符合 IndexList 约定的数据结构。

最后还有一处细节的修改,我们项目中的每一组的标题样式和 cube-ui 的 IndexList 略微不同,可以通过覆盖 CSS 的方式对样式做修改。

.singer
  .cube-index-list-anchor
    padding: 8px 0 8px 20px

这里要注意的是,一旦我们要覆盖某个子组件的样式,那么引用该子组件的父组件(在我们这个 case 是 Singer 组件)样式部分就 不能 使用 scoped 特性,因为如果设置了 scoped ,Vue 在初始化的过程中会给组件的样式加上属性 id,那么就不能够覆盖 cube-ui 中的组件样式了。

Slide 组件修改

音乐 App 的 推荐页面 用到了轮播图,如下图所示:

在我们的项目中已经封装了轮播图组件,它恰好可以使用 cube-ui 的 Slide 组件无缝替换,同样的我们来看一下 Slide 组件 的 文档 ,修改代码如下:

   
     
       
     
   
   
     
   
 

对于 Slide 组件内部的元素,我们用 cube-slide-item 组件来做循环,由于底部的 dots 样式很不一样,我们使用了作用域插槽,因为需要根据子组件的 current 来决定它渲染的 active 样式;并且我们想让 dots 的位置向上偏移,所以我们依然采用覆盖 CSS 的方式:

.recommend
  .cube-slide-dots
    bottom: 12px

同样,我们也需要把 Recommend 组件 stylus 部分的 scoped 移除。

Scroll 组件修改

音乐 App 项目在 better-scroll 的基础上插件封装了 Scroll 组件,并在项目中大量应用,比如推荐页面、歌手详情页、搜索页面、歌曲列表、甚至是歌词列表。cube-ui 中也基于 better-scroll 封装了 Scroll 组件,它的功能更完善,所以我们决定替换 Scroll 组件。

Scroll 组件在项目中应用的地方非常多,这里我挑一个比较有代表性的场景,就是搜索页面的 Suggest 组件,如下所图所示:

Suggest 组件下方的列表是根据检索的关键词动态渲染的,它不仅可以局部滚动,还有一个上拉加载的功能,它就是移动端场景下分页功能的实现。我们完全可以用 cube-ui 的 Scroll 组件来实现它,同样我们也是先去阅读它的 文档 ,然后做如下代码的修改:

  
// ... export default { data() { return { // ... scrollOptions: { pullUpLoad: { threshold: 0, txt: '' } } } }, methods: { searchMore() { if (!this.hasMore) { this.$refs.suggest.forceUpdate() return } this.page++ search(this.query, this.page, this.showSinger, perpage).then((res) => { if (res.code === ERR_OK) { this.result = this.result.concat(this._genResult(res.data)) this._checkMore(res.data) } else { this.$refs.suggest.forceUpdate() } }).catch(() => { this.$refs.suggest.forceUpdate() }) } // ... } // ... }

这里需要注意两个地方,一个是 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 包装一下它:

createAPI(Vue, Confirm, ['confirm', 'click'], true)

接着我们就可以在组件内部通过 this.$createConfirm 的方式调用它,我们在 Search 组件中改变一下 Confirm 组件的调用方式:

methods: {
  showConfirm() {
    this.$createConfirm({
       text: '是否清空所有搜索历史',
       confirmBtnText: '清空',
       onConfirm: () => {
         this.clearSearchHistory()
       }
     }).show()
   },
}

当执行 .show 的时候,cube-ui 内部会把 Confirm 组件动态挂载到 body 下。

总结

到此这篇文章的主体内容就介绍完了,看似简单,但实际上我在重构的过程中还是发现了一些问题,顺便也对 cube-ui 和 better-scroll 做了一些优化。希望我的学生在看完这篇文章后能真正自己尝试着做一遍重构,因为很多细节的问题只有你去尝试做了才能发现,只有发现并解决问题你才能积累更多的经验;重构的过程中务必要看文档,遇到问题一定要自己先思考一遍,实在解决不了再求助。另外我也希望大家也多多使用 cube-ui,哪怕 cube-ui 能帮你解决一个小小的需求,那么我们觉得开源这件事情都是非常有意义的。

如果 cube-ui 对你有帮助,也不要吝啬你的 star

另附上 vue-music 项目的线上地址,扫下方二维码体验:

如果想跟着我学习这门 Vue.js 的进阶课程,真心想学到知识,请务必购买正版课程,你一定不会失望。

慕课网责编内容来自:慕课网 (源链) | 更多关于

阅读提示:酷辣虫无法对本内容的真实性提供任何保证,请自行验证并承担相关的风险与后果!
本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 前端开发 » vue-music 音乐 App 之 cube-ui 重构

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录