背景
去年 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
新增幾個外掛,先簡單對它們做一些介紹。
-
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-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
的插槽
由於我們的列表項是圖文混排的佈局,和預設的樣式不一樣,因此我們需要用到插槽來自定義列表項佈局,參考文件,我們對模板程式碼的修改如下:
<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 中的元件樣式了。
Slide 元件修改
音樂 App 的推薦頁面用到了輪播圖,如下圖所示:
在我們的專案中已經封裝了輪播圖元件,它恰好可以使用 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
元件下方的列表是根據檢索的關鍵詞動態渲染的,它不僅可以區域性滾動,還有一個上拉載入的功能,它就是移動端場景下分頁功能的實現。我們完全可以用 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 的進階課程,真心想學到知識,請務必購買正版課程,你一定不會失望。