vue-music 音樂 App 之 cube-ui 重構

背景

去年 6 月初,我在慕課網上線了一門 Vue.js 2.0 的高級實戰課程音樂 WebApp 課程,教同窗們如何去開發基礎組件和業務組件。在通常大公司的實際項目中,並不會爲每個項目都去開發基礎組件,他們每每會把基礎組件收斂成一個組件庫,供各個項目複用。滴滴也是如此,咱們在去年初使用 Vue.js 去重構了咱們的打車 WebApp,也抽象出了一套移動端組件庫,在通過一年多的業務考驗後,咱們決定作開源,一方面是想把好的東西分享出去,並經過社區的反饋去完善咱們的組件庫;另外一方面也是想讓你們瞭解滴滴的前端,能吸引一些優秀的人才加入滴滴。因而在去年的 11 月份,咱們團隊開源了 cube-ui,到如今爲止收到的反饋還算不錯,也陸續有一些同窗在生產環境也開始使用。css

cube-ui 和其它同類型的開源組件庫有一個很大的不一樣,它內部了使用了一個咱們團隊玩出來的「後編譯」技術,它能幫咱們玩出不少花樣,好比減小組件包體積、支持 rem、支持自定義組件顏色等等,但帶來好處的同時也會有一些不便(webpack 的配置會略顯複雜),所以咱們團隊也爲 cube-ui 在 vue-cli 的基礎上擴展了一套腳手架,方便你們開箱即用。html

其實相對於 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 重構音樂課程項目的經驗。vue

Webpack 配置修改

因爲咱們是現有項目,並不能使用腳手架去初始化項目,因此咱們須要根據官網的文檔去作 webpack 的相關配置。這裏我要稍微提醒一些同窗,在使用一個開源項目的時候,最好的方式就是閱讀它的文檔,遇到問題首先想的是查看它的 issue。那麼 cube-ui 的文檔在這裏,咱們來看一下快速上手部分。node

安裝 cube-ui

首先須要安裝 cube-ui,這塊很簡單,直接運行命令就行了。webpack

npm install cube-ui --save
複製代碼

後編譯配置

後編譯簡單的理解就是把編譯工做交給應用來完成,也就是使用 cube-ui 的項目vue-music 來完成編譯。因爲是現成的項目,咱們不能用腳手架初始化項目,那麼全部的後編譯相關的 webpack 配置都須要本身來動手,接下來我會一邊教你們配置,一邊來解釋這些配置的做用。git

修改 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 添加幾個插件,先簡單對它們作一些介紹。github

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 的進階課程,真心想學到知識,請務必購買正版課程,你必定不會失望。

相關文章
相關標籤/搜索