手把手教你用原生JavaScript造輪子(三)——項目升級&填坑&重寫組件

項目升級&填坑

說明

時隔大半年(錯,應該是有生之年),我決定重啓這個造輪子項目,緣由有幾點:javascript

  1. 老項目結構太過臃腫
  2. 組件的一些實現思路存在比較大的問題
  3. 打包方式比較麻煩

因此我從新創建了一個叫 tiny-wheels 的項目,初衷和舊項目是同樣的,主要是我的對於技術的學習、研究、總結,其次是探索使用原生 JavaScript 實現一些複雜組件的方法css

注:舊項目已廢棄,請直接使用新項目!

因爲如今還未完成每一個組件的單元測試,沒法確保組件的穩定性,因此暫時不推薦在生產環境使用java

本項目的源碼:源碼
關於各個組件的詳細使用方式和效果能夠查看本項目的文檔:文檔git

求點贊,求 star~(✪ω✪)es6

填坑

架構優化

因爲老項目的打包方式不太方便,因此此次的項目從新使用最新版本的WebpackBabel構建,採用UMD的模塊化規範打包,同時兼容多種引入方式,不論是開發和使用都方便了很多,具體的使用方法能夠查看文檔中的相關說明github

組件重寫

有了 Webpack 這個強大的工具,咱們就不須要使用舊語法編寫代碼了,因此新項目直接使用最新語法編寫。

老項目中分頁和輪播的實現較爲臃腫,並且輪播圖的實現思路存在不少問題,此次也從新把這兩個組件實現了一遍,目前組件的基本功能已經實現完畢,能夠查看文檔中的效果以及源碼算法

Pager

Pager

這個組件之前的邏輯沒有太大的問題,核心原理是同樣的,只是使用了一種更簡單的方法實現element-ui

頁碼爲 1,顯示 1 2 3 ... 10
頁碼爲 2,顯示 1 2 3 4 ... 10
頁碼爲 3,顯示 1 2 3 4 5 ... 10
頁碼爲 4,顯示 1 2 3 4 5 6 ... 10
頁碼爲 5,顯示 1 ... 3 4 5 6 7 ... 10
頁碼爲 6,顯示 1 ... 4 5 6 7 8 ... 10
頁碼爲 7,顯示 1 ... 5 6 7 8 9 10
頁碼爲 8,顯示 1 ...6 7 8 9 10
頁碼爲 9,顯示 1 ... 7 8 9 10
頁碼爲 10,顯示 1 ... 8 9 10

要獲得上面這種結構,老項目中使用的方法是:bootstrap

function showPages (page, total, show) {
  var str = page + ''
  for (var i = 1; i <= show; i++) {
    if (page - i > 1) {
      str = page - i + ' ' + str
    }
    if (page + i < total) {
      str = str + ' ' + (page + i)
    }
  }
  if (page - (show + 1) > 1) {
    str = '... ' + str
  }
  if (page > 1) {
    str = 1 + ' ' + str
  }
  if (page + show + 1 < total) {
    str = str + ' ...'
  }
  if (page < total) {
    str = str + ' ' + total
  }
  return str
}

這樣寫,if-else 結構太多,很不利於維護,能夠利用 es6 提供的reduce方法優化這段代碼邏輯:瀏覽器

getPager () {
    const pages = [
        1,
        this.pageCount,
        this.pageCurrent,
        this.pageCurrent - 1,
        this.pageCurrent - 2,
        this.pageCurrent + 1,
        this.pageCurrent + 2
    ]
    const pageNumbers = [
        ...new Set(
        pages.filter(n => n >= 1 && n <= this.pageCount).sort((a, b) => a - b)
        )
    ]
    const pageItems = pageNumbers.reduce((items, current, index, array) => {
        items.push(current)
        if (array[index + 1] && array[index + 1] - array[index] > 1) {
            items.push('···')
        }
        return items
    }, [])
    return pageItems
}

原理是如出一轍的,只是多了去重、排序等幾步操做,可是代碼獲得了極大的簡化
若是有更好的實現思路,歡迎給我提建議~

Carousel

Carousel

老項目中輪播組件的實現思路大概是這樣的:

  1. 使用 JavaScript 計算子元素的序號
  2. 使用 position: absolute + left 或者 transform 控制父元素的位置,達到移動的動畫效果

這種辦法也是你能在網上找到的最多見的實現思路,可是稍微測試一下就會發現這種算法有不少缺陷:

  1. 頻繁的 DOM 操做,性能低
  2. 在部分瀏覽器渲染下會出現閃爍的問題
  3. 動畫算法不利於維護與功能拓展

拜讀了element-uiiviewant-designbootstrap這些比較大型成熟的 UI 框架的源碼後,發現它們都使用了另一種更「先進」聰明的作法

其實要實現輪播的動畫效果,根本不須要去計算每一個子元素的序號以及對應的順序變化,由於無論哪一個方向,自始至終咱們看到的都是兩個子元素的移動效果而已,因此只須要給這兩個移動中的元素添加對應的樣式便可

具體實現思路是使用 CSS3 的transform屬性來控制子元素的位置變化,配合transition添加過渡動畫,在 JS 代碼中只須要在合適的時機添加對應的類名,而後移出對應的類名

首先仍是須要計算出當前正在過渡的兩個元素的序號:

getCurrentIndex () {
    return [...this.$$dots].indexOf(
        this.$container.querySelector('.carousel-dot.active')
    )
}

getPrevIndex () {
    return (
        (this.getCurrentIndex() - 1 + this.$$dots.length) % this.$$dots.length
    )
}

getNextIndex () {
    return (this.getCurrentIndex() + 1) % this.$$dots.length
}

計算方法如上,很是簡單,再也不贅述
而後就能夠給這兩個元素加上對應的類名了:

setCarouselPanel ($from, $to, direction) {
    this.isAnimate = true
    window.requestAnimationFrame(() => {
        const { fromClass, toClass } = this.resetCarouselPanel($to, direction)
        window.requestAnimationFrame(() => {
        this.moveCarouselPanel(fromClass, toClass, $from, $to)
        })
    })
}
resetCarouselPanel ($to, direction) {
    let fromClass = ''
    let toClass = ''
    const type = direction === 'left' ? 'next' : 'prev'
    $to.setAttribute('class', `carousel-panel ${type}`)
    fromClass = `carousel-panel active ${direction}`
    toClass = `carousel-panel ${type} ${direction}`
    return { fromClass, toClass }
}
moveCarouselPanel (fromClass, toClass, $from, $to) {
    $from.setAttribute('class', fromClass)
    $to.setAttribute('class', toClass)
    setTimeout(() => {
        $from.setAttribute('class', 'carousel-panel')
        $to.setAttribute('class', 'carousel-panel active')
        this.isAnimate = false
    }, this.duration)
}

這裏會遇到一個很是難解決的問題,由於咱們的思路是先把兩個元素的樣式給重置,再添加新的樣式,因此須要添加兩次類名,而後讓它們的樣式依次做用到對應元素身上,最開始我是這樣實現的:

let fromClass = ''
let toClass = ''
const type = direction === 'left' ? 'next' : 'prev'
$to.setAttribute('class', `carousel-panel ${type}`)
fromClass = `carousel-panel active ${direction}`
toClass = `carousel-panel ${type} ${direction}`
setTimeout(() => {
    $from.setAttribute('class', fromClass)
    $to.setAttribute('class', toClass)
}, 0)
setTimeout(() => {
    $from.setAttribute('class', 'carousel-panel')
    $to.setAttribute('class', 'carousel-panel active')
    this.isAnimate = false
}, 400)

第一個setTimeout是防止瀏覽器自動合併樣式,第二個setTimeout是當動畫結束後清除樣式,而400ms恰好就是transition的時間,乍看上去好像沒什麼太大的問題,可是實際測試的時候發現會有不少詭異的Bug,檢查了好久,最後定位到是第一個setTimeout的問題

緣由就是使用setTimeout並不能100%的確保樣式不會被自動合併,在一些很特殊的狀況下瀏覽器仍然會自動合併樣式,好比這種須要加載動畫的狀況,雖然大部分狀況下是可行的。具體原理初步猜想和瀏覽器的渲染機制有關,查閱了好久的資料,先是在layui源碼中發現了它的處理辦法,直接給第一個setTimeout的延時設置爲50ms,雖然不知道爲何要設置爲這個值,可是通過我實際測試後發現確實解決了這個問題,後來繼續搜索其餘方案,最後仍是在stackoverflow上找到了靠譜的解決方案

window.requestAnimationFrame(function(){
document.getElementById("two").setAttribute("style", "height: 0px");
 window.requestAnimationFrame(function(){
    document.getElementById("two").setAttribute("style", "height:  200px");
  });
});

使用了兩次requestAnimationFrame方法,把改變樣式的代碼放入其中依次執行,通過我在多種瀏覽器上測試後,發現這種方案明顯比setTimeout的辦法更穩定可靠,因此最終我採用了這種辦法處理瀏覽器自動合併樣式的問題,具體的一些緣由能夠參考這個stackoverflow帖子裏的答案

若是你們有更完美的避免樣式合併的辦法,或者更優雅高效的輪播算法,也歡迎分享給我~

關於分頁和輪播組件的填坑差很少就這些了,基本功能都實現了,效果通過初步測試也沒有太大的問題,一些更復雜的功能會在後續的更新裏添加進去

進度

  • [x] Tabs-選項卡
  • [x] Collapse-摺疊面板
  • [x] Pager-分頁
  • [x] Carousel-走馬燈
  • [ ] Calendar-日曆
  • [ ] Tree-樹形控件
  • [ ] 單元測試
  • [ ] TypeScript 重構

目前 TabsCollapsePagerCarousel 等四個組件的基本功能已經初步完成了,TabsCollapse以及後續更新組件的教程文章會陸續發佈,敬請期待

本套教程會優先在個人博客更新,歡迎關注個人 我的網站Github

相關文章
相關標籤/搜索