寫插件是頗有意思,也很鍛鍊人,由於這個過程當中能發現許多的細節問題。在前端發展的過程當中,jQuery無疑是一個重要的里程碑,圍繞着這個優秀項目也出現了不少優秀的插件能夠直接使用,大大節省了開發者們的時間。jQuery最重要的做用是跨瀏覽器,而如今瀏覽器市場雖不完美,但已遠沒有從前那麼慘,數據驅動視圖的思想倍受歡迎,你們開始使用前端框架取代jQuery,我我的比較喜歡Vue.js,因此想試着用Vue.js寫一個組件出來。html
爲了發佈到npm上,因此給項目地址更名字了,可是內部代碼沒有改,使用方法比以前方便。
Demo演示: Here
GitHub地址: Here但願你們能給個star前端
這個datepicker目前僅實現了一些經常使用的功能:vue
選擇時間(這話說得有點多餘node)
最大/最小時間限制webpack
中/英文切換(其實也就星期和月份須要切換)git
能夠以.vue
形式使用,也可在瀏覽器環境中直接使用github
沒了。。。web
萬事的第一步依然是建立項目,只是單一組件,結構並不複雜,Datepicker.vue是最重要的組件文件,dist是webpack的輸出文件夾,index.js是webpack打包的入口文件,最後是webpack的配置文件,用來對咱們的庫文件進行打包用的。所以項目結構就是這樣:npm
. ├── Datepicker.vue ├── LICENSE ├── README.md ├── dist │ └── vue-datepicker.js ├── index.js ├── package.json └── webpack.config.js
以.vue
的方式寫Vue組件是一種特殊寫法,每一個Vue文件包括template
, script
, style
三部分,template
最好不要成爲片斷實例,因此最外層先套一層div
,當作整個組件的根元素。一個datepicker通常由兩部分組成,一個用來顯示日期的input框,一個用來選擇日期的panel,由於我發現input在移動端會自動喚起鍵盤,因此沒有使用input,直接用了div模擬,經過點擊事件決定panel的顯隱。value
是最終的結果,須要和父組件通訊,因此將value寫成了prop,在父組件中使用value.sync="xxx"
,datepicker的value就和父組件的xxx
雙向綁定了。json
<template> <div class="date-picker"> <div class="input" v-text="value" @click="panelState = !panelState"> </div> <div class="date-panel" v-show="panelState"> </div> </template> <scrip> export default { data () { return { panelState: false //初始值,默認panel關閉 } }, props: { value: String } } </script>
一個月最少是28天,若是把週日排在開頭,那麼最少(1號剛好是週日)須要4行,可是每月天數30,31居多,並且1號又不必定是週日,我索性乾脆按最多的狀況設計了,共6行,當月日期沒填滿的地方用上個月或下個月的日期補齊,這樣就方便計算了,並且切換月份時候panel高度不會變化。日期列表的數組須要動態計算,Vue提供了computed這個屬性,因此直接將日期列表dateList
寫成計算屬性。個人方法是將日期列表固定爲長度爲42的數組,而後將本月,上個月,下個月的日期依次填充。
computed: { dateList () { //獲取當月的天數 let currentMonthLength = new Date(this.tmpMonth, this.tmpMonth + 1, 0).getDate() //先將當月的日期塞入dateList let dateList = Array.from({length: currentMonthLength}, (val, index) => { return { currentMonth: true, value: index + 1 } }) //獲取當月1號的星期是爲了肯定在1號前須要插多少天 let startDay = new Date(this.year, this.tmpMonth, 1).getDay() //確認上個月一共多少天 let previousMongthLength = new Date(this.year, this.tmpMonth, 0).getDate() } //在1號前插入上個月日期 for(let i = 0, len = startDay; i < len; i++){ dateList = [{previousMonth: true, value: previousMongthLength - i}].concat(dateList) } //補全剩餘位置 for(let i = 0, item = 1; i < 42; i++, item++){ dateList[dateList.length] = {nextMonth: true, value: i} } return dateList }
這裏用Array.from
來初始化了一個數組,傳入一個Array Like,轉化成數組,在拼接字符串時候採用了arr[arr.length]
和[{}].concat(arr)
這種方式,由於在JsTips上學到這樣作性能更好,文章的最後會貼出相關連接。
這樣,日期列表就構建好了,在template中使用v-for
循環渲染出來
<ul class="date-list"> <li v-for="item in dateList" v-text="item.value" :class="{preMonth: item.previousMonth, nextMonth: item.nextMonth, selected: date === item.value && month === tmpMonth && item.currentMonth, invalid: validateDate(item)}" @click="selectDate(item)"> </li> </ul>
樣式上就能夠本身發揮了,怎麼喜歡怎麼寫。須要注意的是循環日期可能會出現上個月或這個月的日期,我經過previuosMonth
,currentMonth
和nextMonth
分別作了標記,對其餘功能提供判斷條件。
年份和月份的列表都是差很少的道理,年份列表的初始值我直接寫在了data
裏,以當前年份爲第一個,爲了和月份保持一致,每次顯示12個,都經過v-for
渲染。
data () { return { yearList: Array.from({length: 12}, (value, index) => new Date().getFullYear() + index) } }
選擇順序是:年 -> 月 -> 日,因此咱們能夠經過一個狀態變量來控制panel中顯示的內容,綁定適合的函數切換顯示狀態。
<div> <div class="type-year" v-show="panelType === 'year'"> <ul class="year-list"> <li v-for="item in yearList" v-text="item" :class="{selected: item === tmpYear, invalid: validateYear(item)}" @click="selectYear(item)" > </li> </ul> </div> <div class="type-month" v-show="panelType === 'month'"> <ul class="month-list"> <li v-for="item in monthList" v-text="item | month language" :class="{selected: $index === tmpMonth && year === tmpYear, invalid: validateMonth($index)}" @click="selectMonth($index)" > </li> </ul> </div> <div class="type-date" v-show="panelType === 'date'"> <ul class="date-list"> <li v-for="item in dateList" v-text="item.value" track-by="$index" :class="{preMonth: item.previousMonth, nextMonth: item.nextMonth, selected: date === item.value && month === tmpMonth && item.currentMonth, invalid: validateDate(item)}" @click="selectDate(item)"> </li> </ul> </div> </div>
選擇日期的方法就不細說了,在selectYear
,selectMonth
中對年份,月份變量賦值,再分別將panelType
推向下一步就實現了日期選擇功能。
不過在未選擇完日期以前,你可能不但願當前年月的真實值發生變化,因此在這些方法中可先將選擇的值賦給一個臨時變量,等到seletDate
的時候再一次性所有賦值。
selectMonth (month) { if(this.validateMonth(month)){ return }else{ //臨時變量 this.tmpMonth = month //切換panel狀態 this.panelType = 'date' } }, selectDate (date) { //validate logic above... //一次性所有賦值 this.year = tmpYear this.month = tmpMonth this.date = date.value this.value = `${this.tmpYear}-${('0' + (this.month + 1)).slice(-2)}-${('0' + this.date).slice(-2)}` //選擇完日期後,panel自動隱藏 this.panelState = false }
最大/小值是須要從父組件傳遞下來的,所以應該使用props
,另外,這個值能夠是字符串,也應該能夠是變量(好比同時存在兩個datepicker,第二個的日期不能比第一個大這種邏輯),因此應該使用Dynamically bind的方式傳值。
<datepicker :value.sync="start"></datepicker> <!-- 如今min的值會隨着start的變化而變化 --> <datepicker :value.sync="end" :min="start" ></datepicker>
增長了限制條件,對於不合法的日期,其按鈕應該變爲置灰狀態,我用了比較時間戳的方式來判斷日期是否合法,由於就算當前panel中的日期是跨年或是跨月的,經過日期構造函數建立時都會幫你轉換成對應的合法值,省去不少判斷的麻煩:
new Date(2015, 0, 0).getTime() === new Date(2014, 11, 31).getTime() //true new Date(2015, 12, 0).getTime() === new Date(2016, 0, 0).getTime() //true
所以驗證日期是否合法的函數是這樣的:
validateDate (date) { let mon = this.tmpMonth if(date.previousMonth){ mon -= 1 }else if(date.nextMonth){ mon += 1 } if(new Date(this.tmpYear, mon, date.value).getTime() >= new Date(this.minYear, this.minMonth - 1, this.minDate).getTime() && new Date(this.tmpYear, mon, date.value).getTime() <= new Date(this.maxYear, this.maxMonth - 1, this.maxDate).getTime()){ return false } return true
}
當頁面右側有足夠的空間顯示時,datepicker的panel會定位爲相對於父元素left: 0
的位置,若是沒有足夠的空間,則應該置於right: 0
的位置,這一點能夠經過Vue提供的動態樣式和樣式對象來實現(動態class和動態style其實只是動態props的特例),而計算位置的時刻,我放在了組件聲明週期的ready
週期中,由於這時組件已經插入到DOM樹中,能夠獲取style進行動態計算:
ready () { if(this.$el.parentNode.offsetWidth + this.$el.parentNode.offsetLeft - this.$el.offsetLeft <= 300){ this.coordinates = {right: '0', top: `${window.getComputedStyle(this.$el.children[0]).offsetHeight + 4}px`} }else{ this.coordinates = {left: '0', top: `${window.getComputedStyle(this.$el.children[0]).offsetHeight + 4}px`} } } <!-- template中對應的動態style --> <div :style="coordinates"></div>
爲了panel的顯隱能夠平滑過渡,可使用transition
作過渡動畫,這裏我簡單地經過一個0.2秒的透明度過渡讓顯隱更平滑。
<div :style="this.coordinates" v-show="panelState" transition="toggle"></div> //less syntax .toggle{ &-transition{ transition: all ease .2s; } &-enter, &-leave{ opacity: 0; } }
這裏其實也很簡單,這種多語言切換實質就是一個key根據不一樣的type而輸出不一樣的value,因此使用filter能夠很容易的實現它!好比渲染星期的列表:
<ul class="weeks"> <li v-for="item in weekList" v-text="item | week language"></li> </ul> filters : { week (item, lang){ switch (lang) { case 'en': return {0: 'Su', 1: 'Mo', 2: 'Tu', 3: 'We', 4: 'Th', 5: 'Fr', 6: 'Sa'}[item] case 'ch': return {0: '日', 1: '一', 2: '二', 3: '三', 4: '四', 5: '五', 6: '六'}[item] default: return item } } }
對於一個Vue組件,若是是使用webpack + vue-loader
的.vue
單文件寫法,我但願這樣使用:
//App.vue <script> import datepicker from 'path/to/datepicker.vue' export default { components: { datepicker} } </script>
若是是直接在瀏覽器中使用,那麼我但願datepicker
這個組件是暴露在全局下的,能夠這麼使用:
//index.html <html> <script src="path/to/vue.js"></script> <script src="path/to/datepicker.js"></script> <body> <div id="app"></div> <script> new Vue({ el: '#app', components: { datepicker } }) </script> </body> </html>
這裏我選擇了webpack做爲打包工具,使用webpack的output.library
和output.linraryTarget
這兩個屬性就能夠把你的bundle文件做爲庫文件打包。library
定義了庫的名字,libraryTarget
定義了你想要打包的格式,具體能夠看文檔。我但願本身的庫能夠經過datepicker
加載到,而且打包成umd
格式,所以個人webpack.config.js
是這樣的:
module.exports = { entry: './index.js', output: { path: './dist', library: 'datepicker', filename: 'vue-datepicker.js', libraryTarget: 'umd' }, module: { loaders: [ {test: /\.vue$/, loaders: ['vue']}, {test: /\.js$/, exclude: /node_modules/, loaders: ['babel']} ] } }
打包完成的模塊就是一個umd
格式的模塊啦,能夠在瀏覽器中直接使用,也能夠配合require.js等模塊加載器使用!
Vue 2.0已經發布有段時間了,如今把以前的組件適配到Vue 2.0。遷移過程仍是很順利的,核心API改動不大,能夠藉助vue-migration-helper來找出廢棄的API再逐步修改。這裏只列舉一些我須要修改的API。
2.0中的filter只能在mustache綁定中使用,若是想在指令式綁定中綁定過濾後的值,能夠選擇計算屬性。我在月份和星期的顯示中使用到了過濾器來過濾語言類型,但我以前是在指令式綁定中使用的filter,因此須要以下修改,:
//修改前 <div class="month-box" @click="chType('month')" v-text="tmpMonth + 1 | month language"></div> //修改後,filter傳參的方式也變了,變成了函數調用的風格 <div class="month-box" @click="chType('month')">{{tmpMonth + 1 | month(language)}}</div>
$index
和$key
這兩個屬性不會在v-for
中被自動建立了,如需使用,要在v-for
中自行聲明:
<li v-for="item in monthList" @click="selectMonth($index)"></li> // <li v-for="(item, index) in monthList" @click="selectMonth(index)"></li>
ready
生命週期移除ready
從生命週期鉤子中移除了,遷移方法很簡單,使用mounted
和this.$nextTick
來替換。
prop.sync
棄用prop
的sync
棄用了,遷移方案是使用自定義事件,並且Datepicker這種input類型組件,可使用表單輸入組件的自定義事件做爲替換方案。自定義組件也可使用v-model
指令了,可是必須知足兩個條件:
接收一個value
的prop
值發生變化時,觸發一個input
事件,傳入新值。
因此Datepicker的使用方式也不是<datepicker value.sync="now"></datepicker>
了,而是<datepicker v-model="now"></datepicker>
。組件自身向父級傳值的方式也不同了:
//1.x版本,設置了value的值會同步到父級 this.value = `${this.tmpYear}-${('0' + (this.month + 1)).slice(-2)}-${('0' + this.date).slice(-2)}` //2.x版本,須要本身觸發input事件,將新值做爲參數傳遞回去 let value = `${this.tmpYear}-${('0' + (this.month + 1)).slice(-2)}-${('0' + this.date).slice(-2)}` this.$emit('input', value)
以上就是我在寫這個datepicker時的大體思路,自己也是很簡單的事情,沒有到處展開來講,寫在這裏做爲本身的一個總結,若是有剛開始使用Vue的同窗也但願這篇文章能夠在思路上幫助到大家:P,對於各位老鳥若是有什麼指點的地方我也很感謝:D,那麼差很少就這樣,後面貼一些相關推薦閱讀。
高效地向數組中插值
Vue.js-片斷實例
Vue.js-動態綁定
Js日期對象基礎
Webpack: export bundle as library
UMD(universial Module Defination)
Migration from Vue 1.x