去年 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 的相關配置。這裏我要稍微提醒一些同窗,在使用一個開源項目的時候,最好的方式就是閱讀它的文檔,遇到問題首先想的是查看它的 issue。那麼 cube-ui 的文檔在這裏,咱們來看一下快速上手部分。node
首先須要安裝 cube-ui,這塊很簡單,直接運行命令就行了。webpack
npm install cube-ui --save
複製代碼
後編譯簡單的理解就是把編譯工做交給應用來完成,也就是使用 cube-ui 的項目vue-music 來完成編譯。因爲是現成的項目,咱們不能用腳手架初始化項目,那麼全部的後編譯相關的 webpack 配置都須要本身來動手,接下來我會一邊教你們配置,一邊來解釋這些配置的做用。git
{
// 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
babel-plugin-transform-modules babel-plugin-transform-modules
是從 babel-transform-imports
fork 來的,加上了對 style 的支持,爲了解決組件按需引入的問題。web
stylus & stylus-loadervue-cli
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,例如:
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]
。
{
"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 文件便可。
var PostCompilePlugin = require('webpack-post-compile-plugin')
module.exports = {
// ...
plugins: [
// ...
new PostCompilePlugin()
]
// ...
}
複製代碼
這裏就是對 webpack-post-compile-plugin
插件的應用,把它添加到 plugins
中便可。
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 文件再去引入資源的相對路徑的問題,參考官方文檔。
module.exports = {
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: false
}),
// ...
}
複製代碼
這裏須要強制指定 css-loader
的選項 extract
爲 false,不然咱們經過 npm run build
編譯後的項目異步加載 vue 組件會有問題。
那麼到這裏,後編譯的 webpack 配置就告一段落了,核心思想就是讓咱們的應用引入 cube-ui 的源碼,而且接管 cube-ui 的編譯工做。
這篇文章我不會把全部代碼的修改都 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 式的調用,這塊兒稍後咱們會詳細說明。
音樂 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
的插槽因爲咱們的列表項是圖文混排的佈局,和默認的樣式不同,所以咱們須要用到插槽來自定義列表項佈局,參考文檔,咱們對模板代碼的修改以下:
<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-group
和 cube-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 中的組件樣式了。
音樂 App 的推薦頁面用到了輪播圖,以下圖所示:
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
移除。
音樂 App 項目在 better-scroll 的基礎上插件封裝了 Scroll 組件,並在項目中大量應用,好比推薦頁面、歌手詳情頁、搜索頁面、歌曲列表、甚至是歌詞列表。cube-ui 中也基於 better-scroll 封裝了 Scroll
組件,它的功能更完善,因此咱們決定替換 Scroll
組件。
Scroll
組件在項目中應用的地方很是多,這裏我挑一個比較有表明性的場景,就是搜索頁面的 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 羣與咱們交流,二維碼以下:
若是 cube-ui 對你有幫助,也不要吝嗇你的 star。
另附上 vue-music 項目的線上地址,掃下方二維碼體驗:
若是想跟着我學習這門 Vue.js 的進階課程,真心想學到知識,請務必購買正版課程,你必定不會失望。