Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ustbhuangyi opened this issue Jan 7, 2018 · 6 comments
Open

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

ustbhuangyi opened this issue Jan 7, 2018 · 6 comments

Comments

@ustbhuangyi
Copy link
Member

背景

去年 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 的歌手页面有一个歌手列表,如下图所示:
singer

它恰好可以使用 cube-ui 提供的 IndexList 组件,在我的教学课程中,我也是把它单独抽象出来的一个基础组件,所以替换就变的很容易了。

学会使用一个组件,最好的方式就是看它的文档。cube-ui 提供的 IndexList 样式如下:

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 的插槽

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

<template>
  <div class="singer" ref="singer">
    <cube-index-list :data="singers" ref="list">
      <cube-index-list-group v-for="(group, index) in singers" :key="index" :group="group" class="list-group">
        <cube-index-list-item v-for="(item, index) in group.items" :key="index" :item="item" @select="selectSinger" class="list-group-item">
          <img class="avatar" v-lazy="item.avatar">
          <span class="name">{{item.name}}</span>
        </cube-index-list-item>
      </cube-index-list-group>
    </cube-index-list>
    <router-view></router-view>
  </div>
</template>

我们使用 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 的推荐页面用到了轮播图,如下图所示:
slide
在我们的项目中已经封装了轮播图组件,它恰好可以使用 cube-ui 的 Slide 组件无缝替换,同样的我们来看一下 Slide 组件 的文档,修改代码如下:

 <cube-slide ref="slider">
   <cube-slide-item v-for="(item,index) in recommends" :key="index">
     <a :href="item.linkUrl">
       <img @load="loadImage" :src="item.picUrl">
     </a>
   </cube-slide-item>
   <template slot="dots" slot-scope="props">
     <span class="dot" :class="{active: props.current === index}" v-for="(item, index) in props.dots"></span>
   </template>
 </cube-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
Suggest 组件下方的列表是根据检索的关键词动态渲染的,它不仅可以局部滚动,还有一个上拉加载的功能,它就是移动端场景下分页功能的实现。我们完全可以用 cube-ui 的 Scroll 组件来实现它,同样我们也是先去阅读它的文档,然后做如下代码的修改:

 <cube-scroll ref="suggest"
             :data="result"
             :options="scrollOptions"
             @pulling-up="searchMore"
>
  <ul class="suggest-list">
    <li @click="selectItem(item)" class="suggest-item" v-for="item in result">
      <div class="icon">
        <i :class="getIconCls(item)"></i>
      </div>
      <div class="name">
        <p class="text" v-html="getDisplayName(item)"></p>
      </div>
    </li>
  </ul>
</cube-scroll>

<script type="text/ecmascript-6">
  // ...
  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()
        })
      }
      // ...
    } 
    // ...
  }
</script>


这里需要注意两个地方,一个是 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,我们简单在使用的地方声明它就好了,就像这样:

<tempalte>
  <xxx/>
</tempalte>

对于一般组件,这样使用并没有问题,但对于全屏类的弹窗组件,如果在一个层级嵌套很深的子组件中使用,仍然通过声明式的方式,很可能它的样式会受到父元素某些 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 能帮你解决一个小小的需求,那么我们觉得开源这件事情都是非常有意义的。如果大家在使用的过程中遇到一些问题,欢迎给我们提 issue & pr,帮助我们一起共建 cube-ui,也可以加 qq 群与我们交流,二维码如下:

QQ Community QR

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

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

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

@tank0317
Copy link

tank0317 commented Jan 7, 2018

刚看完,在黄老师的粉丝们到来之前,我先给黄老师点个赞👍😄

@huabingtao
Copy link

打 call

@maixiaojie
Copy link

👍

@MoonCheung
Copy link

没有提供重构过的源代码吗?我想入手研究呢

@ustbhuangyi
Copy link
Member Author

@MoonCheung 慕课网购买课程,申请代码权限,即可拿到源码。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants
@ustbhuangyi @maixiaojie @tank0317 @MoonCheung @huabingtao and others