作為程序員大家在寫代碼時談的最多的就是代碼的拓展性、復用性。本文就以大家熟悉的輪播效果為案例,講一講寫優質代碼的思路和實踐。

文章分三個步驟。第一步,實現基本功能;第二步,考慮到代碼的封裝性和復用性;第三步,考慮到代碼的拓展性。

實現基本功能

完整代碼查看這裡 ,下面只展示HTML結構和JavaScript

<div class="carousel">
<div class="panels">
<a href="#">
<img src="http://cdn.jirengu.com/book.jirengu.com/img/1.jpg">
</a>
<a href="#">
<img src="http://cdn.jirengu.com/book.jirengu.com/img/2.jpg">
</a>
<a href="#">
<img src="http://cdn.jirengu.com/book.jirengu.com/img/3.jpg">
</a>
<a href="#">
<img src="http://cdn.jirengu.com/book.jirengu.com/img/4.jpg">
</a>
</div>
<div class="action">
<span class="pre">上一個</span>
<span class="next">下一個</span>
<div class="dots">
<span class="active"></span>
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>

JavaScript

//讓document.querySelector 用起來更方便
const $ = s => document.querySelector(s)
const $$ = s => document.querySelectorAll(s)

const dotCt = $(.carousel .dots)
const preBtn = $(.carousel .pre)
const nextBtn = $(.carousel .next)

//把類數組對象轉換為數組,便於之後使用數組方法
//這裡對應的是包含圖片面板的數組
const panels = Array.from($$(.carousel .panels > a))
//這裡對應的是包含小圓點的數組
const dots = Array.from($$(.carousel .dots span))

//要展示第幾頁,就先把所有頁的z-index設置為0,再把要展示的頁面z-index設置為10
const showPage = pageIndex => {
panels.forEach(panel => panel.style.zIndex = 0)
panels[pageIndex].style.zIndex = 10
}

const setDots = pageIndex => {
dots.forEach(dot => dot.classList.remove(active))
dots[pageIndex].classList.add(active)
}

//根據第幾個小點上有active的類來判斷在第幾頁
const getIndex = () => dots.indexOf($(.carousel .dots .active))
const getPreIndex = () => (getIndex() - 1 + dots.length) % dots.length
const getNextIndex = () => (getIndex() + 1) % dots.length

dotCt.onclick = e => {
if(e.target.tagName !== SPAN) return

let index = dots.indexOf(e.target)
setDots(index)
showPage(index)
}

preBtn.onclick = e => {
let index = getPreIndex()
setDots(index)
showPage(index)
}

nextBtn.onclick = e => {
let index = getNextIndex()
setDots(index)
showPage(index)
}

這裡查看代碼效果 。上面的代碼使用了原生ES6語法,核心代碼邏輯是:當用戶點擊小圓點,得到小圓點的位置(index),設置小圓點集合的樣式,切換到對應頁面。頁面(.panels的子元素)使用絕對定位相互重疊到一起,我們通過修改z-index把需要展示的頁面放到最上層。

復用性與封裝性

以上代碼可以實現輪播基本功能,但做為義大利麵條式的代碼,並未做封裝,無法給他人使用。另外也無法滿足頁面上有多個輪播的需求。

下面對代碼做個封裝。

class Carousel {
constructor(root) {
this.root = root
this.panels = Array.from(root.querySelectorAll(.panels a))
this.dotCt = root.querySelector(.dots)
this.dots = Array.from(root.querySelectorAll(.dots span))
this.pre = root.querySelector(.pre)
this.next = root.querySelector(.next)

this.bind()
}

get index() {
return this.dots.indexOf(this.root.querySelector(.dots .active))
}

get preIndex() {
return (this.index - 1 + this.dots.length) % this.dots.length
}

get nextIndex () {
return (this.index + 1) % this.dots.length
}

bind() {
this.dotCt.onclick = e => {
if(e.target.tagName !== SPAN) return

let index = this.dots.indexOf(e.target)
this.setDot(index)
this.showPage(index)
}

this.pre.onclick = e => {
let index = this.preIndex
this.setDot(index)
this.showPage(index)
}

this.next.onclick = e => {
let index = this.nextIndex
this.setDot(index)
this.showPage(index)
}
}

setDot(index) {
this.dots.forEach(dot => dot.classList.remove(active))
this.dots[index].classList.add(active)
}

showPage(index) {
this.panels.forEach(panel => panel.style.zIndex = 0)
this.panels[index].style.zIndex = 10
}
}

new Carousel(document.querySelector(.carousel))

代碼里用了getter,便於或者index的值。這裡需要注意的是,每次調用setDot後 this.index、this.preIndex、this.nextIndex均會自動發生變化,調用showPage的時候需要留意。

現在輪播可以復用了,但仍有缺憾,輪播的效果太單調。假設輪播想使用fade或者slide效果,我們可以在showPage方法內修改代碼。但存在的問題是效果和輪播組件做了強綁定,假設我需要另外一個效果的輪播就得新建一個組件。比如,有這樣一個需求,用戶可以再切頁時可以隨時更改效果,用上面的代碼就很難辦到。

能不能實現組件和效果的解綁呢?當然可以。

代碼拓展性

設計模式中的橋接模式可以實現上述的分離。直接給代碼

class Carousel {
constructor(root, animation) {
this.animation = animation || ((to, from, onFinish) => onFinish())
this.root = root
...
}

...

//showPage傳遞2個參數,toIndex 表示要切換到的頁面(終點頁面)序號,fromIndex 表示從哪個頁面(起始頁面)切換過來
showPage(toIndex, fromIndex) {
//animation函數傳遞3個參數,分別為終點頁面dom元素,起始頁面dom元素,動畫執行完畢後的回調
this.animation(this.panels[toIndex], this.panels[fromIndex], () => {
//這裡是動畫執行完成後的回調
})
}
}

const Animation = {
fade(during) {
return function(to, from, onFinish) {
//to表示終點頁面dom元素,from表示起始頁面dom元素
//對這兩個元素進行適當的處理即可實現平滑過渡效果
...
}
},

zoom(scale) {
return function(to, from, onFinish) { /*todo...*/}
}

}

new Carousel(document.querySelector(.carousel), Animation.fade(300))

上述代碼中,我們把動畫類型作為參數傳遞給Carousel,在執行setPage的時候調用動畫。 而動畫函數本身做的事情比較簡單:處理兩個絕對定位並且相互重疊的DOM元素,以特定效果讓一個元素消失另外一個元素出現。

動畫的實現

動畫可以用JS來實現(requestAnimationFrame來實現動畫),也可以用CSS3來實現。相比JS實現動畫,用CSS3性能更好並且代碼更簡單。

const Animation = (function(){
const css = (node, styles) => Object.entries(styles)
.forEach(([key, value]) => node.style[key] = value)
const reset = node => node.style =

return {
fade(during = 400) {
return function(to, from, onFinish) {
css(from, {
opacity: 1,
transition: `all ${during/1000}s`,
zIndex: 10
})
css(to, {
opacity: 0,
transition: `all ${during/1000}s`,
zIndex: 9
})

setTimeout(() => {
css(from, {
opacity: 0,
})
css(to, {
opacity: 1,
})
}, 100)

setTimeout(() => {
reset(from)
reset(to)
onFinish && onFinish()
}, during)

}
},

zoom(scale = 5, during = 600) {
return function(to, from, onFinish) {
css(from, {
opacity: 1,
transform: `scale(1)`,
transition: `all ${during/1000}s`,
zIndex: 10
})
css(to, {
zIndex: 9
})

setTimeout(() => {
css(from, {
opacity: 0,
transform: `scale(${scale})`
})
}, 100)

setTimeout(() => {
reset(from)
reset(to)
onFinish && onFinish()
}, during)

}
}
}
})()

以下是最終代碼,大家可以再Animation對象里增加更多特效,比如把前段時間流行的滅霸特效加進去。

class Carousel {
constructor(root, animation) {
this.animation = animation || ((to, from, onFinish) => onFinish())
this.root = root
this.panels = Array.from(root.querySelectorAll(.panels a))
this.dotCt = root.querySelector(.dots)
this.dots = Array.from(root.querySelectorAll(.dots span))
this.pre = root.querySelector(.pre)
this.next = root.querySelector(.next)

this.bind()
}

get index() {
return this.dots.indexOf(this.root.querySelector(.dots .active))
}

get preIndex() {
return (this.index - 1 + this.dots.length) % this.dots.length
}

get nextIndex () {
return (this.index + 1) % this.dots.length
}

bind() {
this.dotCt.onclick = e => {
if(e.target.tagName !== SPAN) return

let lastIndex = this.index
let index = this.dots.indexOf(e.target)
this.setDot(index)
this.showPage(index, lastIndex)
}

this.pre.onclick = e => {
let index = this.preIndex
this.setDot(index)
this.showPage(index, this.nextIndex)
}

this.next.onclick = e => {
let index = this.nextIndex
this.setDot(index)
this.showPage(index, this.preIndex)
}
}

setDot(index) {
this.dots.forEach(dot => dot.classList.remove(active))
this.dots[index].classList.add(active)
}

showPage(toIndex, fromIndex) {
//執行動畫,執行完成後最終結果
//如果沒傳遞動畫,直接執行結果
this.animation(this.panels[toIndex], this.panels[fromIndex], () => {
this.panels.forEach(panel => panel.style.zIndex = 0)
this.panels[toIndex].style.zIndex = 10
})
}

setAnimation(animation) {
this.animation = animation
}
}

const Animation = (function(){
const css = (node, styles) => Object.entries(styles)
.forEach(([key, value]) => node.style[key] = value)
const reset = node => node.style =

return {
fade(during) {
return function(to, from, onFinish) {
css(from, {
opacity: 1,
transition: `all ${during/1000}s`,
zIndex: 10
})
css(to, {
opacity: 0,
transition: `all ${during/1000}s`,
zIndex: 9
})

setTimeout(() => {
css(from, {
opacity: 0,
})
css(to, {
opacity: 1,
})
}, 100)

setTimeout(() => {
reset(from)
reset(to)
onFinish && onFinish()
}, during)

}
},

zoom(scale = 5, during = 1000) {
return function(to, from, onFinish) {
css(from, {
opacity: 1,
transform: `scale(1)`,
transition: `all ${during/1000}s`,
zIndex: 10
})
css(to, {
zIndex: 9
})

setTimeout(() => {
css(from, {
opacity: 0,
transform: `scale(${scale})`
})
}, 100)

setTimeout(() => {
reset(from)
reset(to)
onFinish && onFinish()
}, during)

}
}
}
})()

const carousel = new Carousel(document.querySelector(.carousel), Animation.fade(300))
//new Carousel(document.querySelector(.carousel), Animation.zoom(3, 500))

document.querySelector(select).onchange = function(e) {
carousel.setAnimation(Animation[this.value]())
}

查看效果 查看源碼

完。

歡迎進群探討技術,點此 微信掃碼進群。

飢人谷新課程來了,點此查看


推薦閱讀:
相关文章