前情回顧
在上一篇文章中,我們封裝了一個DOM庫(qnode),為了讓大家直觀地感受到其方便友好的自定義工廠模式,於是給大家帶來了這篇文章。
沒有看過上一篇文章的話,可以在這裡找到:原生js系列之DOM工廠模式。
那麼這篇文章,我們將基於上述的qnode
,從頭開始寫一個無限迴圈輪播圖的元件。
思路講解
先看一張輪播佈局圖:
滑動的時候,整個輪播容器整體前進或後退一格,通過css3過渡效果的設定,來達到滑動的效果。也許你會疑惑,頭尾怎麼會多出兩張圖呢?
其實無限迴圈輪播的核心就在於頭尾多出的兩張圖,從圖三再向後滑動,會滑到紅色圖一(我稱之為佔點陣圖一),這個時候給使用者的感覺就是無縫從最後一張滑動到第一張的,當他滑到佔點陣圖一時,我們再瞬間切換到粉色圖一(即真正的圖一),由於是瞬間變換,使用者是感知不到的。同理,從圖一滑到圖三也一樣。由此,周而復始,無窮無盡,給人的感覺是永遠也不會到盡頭,當然箇中奧妙只有我們知道哈哈。
目錄結構
swiper
├── README.md
├── index.js
├── qnode
│ ├── index.js
│ ├── method.js
│ └── store.js
├── render
│ ├── index.js
│ ├── indicator.js
│ └── list.js
└── styles
├── indicator.mcss
├── list.mcss
└── wrap.mcss
複製程式碼
說明:mcss檔案是通過css-modules
來編譯的,給class名稱生成唯一標識,防止命名衝突。這裡有我配置好的一套腳手架,覺得webpack配置麻煩的話,可以clone我這個專案來編譯程式碼:webpack-build。
程式碼編寫
index.js
import qnode from `./qnode`
import render from `./render`
const defaults = {
initIndex: 1,
autoplay: {
use: true,
delay: 3000
},
slide: {
use: true,
scale: 1 / 3,
speed: 0.2
},
indicator: {
use: true,
bottom: ``,
dotClass: ``,
dotActiveClass: ``
}
}
export default function swiper (node, {
datas,
initIndex,
slide,
autoplay,
indicator
}) {
if (!node || !datas || !datas.length) return
// 儲存資料的前後順序很重要,一定要在呼叫前設定
qnode.setStore(`datas`, datas)
qnode.setStore(`index`, (initIndex || defaults.initIndex) - 1)
qnode.setStore(`slide`, Object.assign({}, defaults.slide, slide))
qnode.setStore(`autoplay`, Object.assign({}, defaults.autoplay, autoplay))
qnode.setStore(`indicator`, Object.assign({}, defaults.indicator, indicator))
// 渲染dom並儲存在qnode,以便後續的獲取和操作
render()
// 自動輪播
qnode.execMethod(`autoplay`)
// 滑動翻頁
qnode.execMethod(`slide`)
// 掛載到真實的節點上
qnode.getNode(`wrap`).appendTo(node)
}
複製程式碼
render/index.js
import qnode from `../qnode`
import renderList from `./list`
import renderIndicator from `./indicator`
import mcss from `../styles/wrap.mcss`
export default function () {
renderList() // 渲染列表
renderIndicator() // 渲染指示器,若沒有開啟則不會渲染
qnode.setNode(`wrap`, `$div`)
.addClass(mcss.wrap)
.append([
qnode.getNode(`list`),
qnode.getNode(`indicator`) // 有可能沒有值,這一層我們的qnode會過濾調,所以放心大膽地寫
])
}
複製程式碼
render/list.js
import { isElement, isString } from `@m/utils/is`
import qnode from `../qnode`
import mcss from `../styles/list.mcss`
function getItemNode (data) {
const qItem = qnode.q(`$div`).addClass(mcss.item)
if (isElement(data)) {
return qItem.append(data)
}
if (isString(data)) {
return qItem.html(data)
}
return qItem.html(`
<a href="${data.href || `javascript:;`}" target="${data.target || `_self`}">
<img src="${data.src}" alt="img" />
</a>
`)
}
export default function () {
const datas = qnode.getStore(`datas`)
const tdTime = qnode.getStore(`tdTime`)
const posIndex = qnode.getStore(`index`) + 1
const qItems = datas.map(item => getItemNode(item))
// 首位多插入一個節點,用於視覺感知,互動完成後瞬間替換到相應的節點
qItems.unshift(getItemNode(datas[datas.length - 1]))
qItems.push(getItemNode(datas[0]))
qnode.setNode(`list`, `$div`)
.addClass(mcss.list)
.style({
transitionDuration: tdTime + `ms`,
transform: `translateX(${posIndex * -100}%)`
})
.append(qItems)
}
複製程式碼
render/indicator.js
import qnode from `../qnode`
import mcss from `../styles/indicator.mcss`
export default function () {
const indicator = qnode.getStore(`indicator`)
const last = qnode.getStore(`datas`).length - 1
const index = qnode.getStore(`index`)
const dotClass = indicator.dotClass || mcss.dot
const dotActiveClass = indicator.dotActiveClass || mcss.dotActive
if (indicator.use) {
let qDots = []
for (let i = 0; i <= last; i++) {
qDots.push(
qnode.q(`$div`).addClass(dotClass, (i === index) && dotActiveClass)
)
}
qnode.setNode(`dots`, qDots)
qnode.setStore(`dotActiveClass`, dotActiveClass)
qnode.setNode(`indicator`, `$div`)
.addClass(mcss.indicator)
.style(`bottom`, indicator.bottom)
.append(qDots)
}
}
複製程式碼
qnode/index.js
import { QNode } from `@m/qnode`
import { tdTime } from `./store`
import { change, autoplay, slide, indicator } from `./method`
const qnode = new QNode()
qnode.setStore(`tdTime`, tdTime)
qnode.setMethod(`change`, change)
qnode.setMethod(`autoplay`, autoplay)
qnode.setMethod(`slide`, slide)
qnode.setMethod(`indicator`, indicator)
export default qnode
複製程式碼
qnode/store.js
// 靜態資料可以放在這裡
export const tdTime = 500
複製程式碼
qnode/method.js
import touchSlide from `./touchSlide`
// 翻頁處理
export function change (isNext) {
let index = this.getStore(`index`)
let cacheIndex = index // 用於記錄上一次的索引,移除指示器啟用樣式時使用
let last = this.getStore(`datas`).length - 1
let tdTime = this.getStore(`tdTime`)
let qList = this.getNode(`list`)
let isNextContinue = isNext && (index === last)
let isPrevContinue = !isNext && (index === 0)
let posIndex = index + (isNext ? 2 : 0)
if (isNextContinue || isPrevContinue) {
// 滑到佔點陣圖
qList.style(`transform`, `translateX(${posIndex * -100}%)`)
index = isNextContinue ? 0 : last
setTimeout(() => {
qList.style({
transitionDuration: `0ms`,
transform: `translateX(${(index + 1) * -100}%)`
})
}, tdTime)
} else {
qList.style({
transitionDuration: tdTime + `ms`,
transform: `translateX(${posIndex * -100}%)`
})
index += isNext ? 1 : -1
}
this.setStore(`index`, index)
this.execMethod(`indicator`, cacheIndex, index)
}
// 自動輪播
export function autoplay () {
let opt = this.getStore(`autoplay`)
if (!opt.use) return
let timer = setInterval(() => {
this.execMethod(`change`, true)
}, opt.delay)
this.setStore(`timer`, timer)
}
// 滑動處理
export function slide () {
let qWrap = this.getNode(`wrap`)
let qList = this.getNode(`list`)
let tdTime = this.getStore(`tdTime`)
let slideData = this.getStore(`slide`)
let self = this
if (!slideData.use) return
touchSlide(qWrap.current(), {
delay: 0,
start () {
// 清除輪播定時器和css3過渡效果
clearTimeout(self.getStore(`timer`))
qList.style(`transitionDuration`, `0ms`)
},
move (info) {
let posIndex = self.getStore(`index`) + 1
let move = info.disX / qWrap.width() * 100
let total = posIndex * -100 + move
qList.style(`transform`, `translateX(${total}%)`)
},
end (info) {
// 開啟輪播和css3過渡效果
self.execMethod(`autoplay`)
qList.style(`transitionDuration`, tdTime + `ms`)
let posIndex = self.getStore(`index`) + 1
let scale = Math.abs(info.disX) / qWrap.width()
let speed = Math.abs(info.speedX)
if (scale >= slideData.scale || speed >= slideData.speed) {
self.execMethod(`change`, info.disX < 0) // 翻頁
} else {
qList.style(`transform`, `translateX(${posIndex * -100}%)`)
}
}
})
}
// 修改指示器索引
export function indicator (lastIndex, currIndex) {
const qDots = this.getNode(`dots`)
const dotActiveClass = this.getStore(`dotActiveClass`)
if (qDots && dotActiveClass) {
qDots[lastIndex].removeClass(dotActiveClass)
qDots[currIndex].addClass(dotActiveClass)
}
}
複製程式碼
touchSlide.js
// 截流
function throttle (fn, delay = 100) {
let wait = false
return function () {
if (!wait) {
fn && fn.apply(this, arguments)
wait = true
setTimeout(() => {
wait = false
}, delay)
}
}
}
/**
*
* 滑動
* @param {HTMLElement} node
* @param {Object} {
* delay = 100, // move截流時間
* start, // 滑動開始
* 引數: pageX, pageY
* move, // 滑動中,會不斷地觸發,可以通過截流來限制觸發頻率
* 引數:
time, // 總時間:ms
disX, // 總路程:px
disY,
addX, // 路程增量:px
addY,
speedX: disX / time, // 平均速度:px/ms
speedY: disY / time
* end, // 滑動結束,引數同move
* }
*/
export default function (node, {
delay = 100,
start,
move,
end
}) {
if (!node) return
let sTouch, eTouch, sTime
let touch, time, disX, disY, addX, addY
node.addEventListener(`touchstart`, e => {
e.preventDefault()
sTime = e.timeStamp
sTouch = eTouch = e.targetTouches[0]
start && start({
pageX: sTouch.pageX,
pageY: sTouch.pageY
})
}, false)
node.addEventListener(`touchmove`, throttle(e => {
touch = e.targetTouches[0]
time = e.timeStamp - sTime
disX = touch.pageX - sTouch.pageX
disY = touch.pageY - sTouch.pageY
addX = touch.pageX - eTouch.pageX
addY = touch.pageY - eTouch.pageY
move && move({
time, // 總時間:ms
disX, // 總路程:px
disY,
addX, // 路程增量:px
addY,
speedX: disX / time, // 平均速度:px/ms
speedY: disY / time
})
// 記錄上一次touch
eTouch = touch
}, delay), false)
node.addEventListener(`touchend`, e => {
touch = e.changedTouches[0]
time = e.timeStamp - sTime
disX = touch.pageX - sTouch.pageX
disY = touch.pageY - sTouch.pageY
addX = touch.pageX - eTouch.pageX
addY = touch.pageY - eTouch.pageY
end && end({
time,
disX,
disY,
addX,
addY,
speedX: disX / time,
speedY: disY / time
})
}, false)
}
複製程式碼
styles/wrap.mcss
.wrap {
position: relative;
overflow: hidden;
transform: translate3d(0, 0, 0);
}
複製程式碼
styles/list.mcss
.list {
display: flex;
flex-direction: row;
transform: translateX(0);
transition: transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.item {
flex-basis: 100%;
flex-shrink: 0;
box-sizing: border-box;
a {
display: block;
font-size: 0;
img {
width: 100%;
height: auto;
}
}
}
複製程式碼
styles/indicator.mcss
.indicator {
position: absolute;
bottom: 1em;
left: 0;
right: 0;
display: flex;
justify-content: center;
}
.dot {
width: 1em;
height: 0.12em;
margin: 0 0.12em;
background-color: rgba(255, 255, 255, 0.5);
&-active {
background-color: #fff;
}
}
複製程式碼
README
引數
- node: 要掛載的dom節點,必須
- options: 如下(其中datas是必要的)
{
initIndex: 1, // 初始化展示的索引
autoplay: { // 自動輪播設定
use: true, // 開關
delay: 3000 // 間隔3s
},
slide: { // 手指滑動設定
use: true, // 開關
scale: 1/3, // 劃過總共寬度的1/3則翻頁
speed: 0.2 // 滑動的速度超過0.2px/ms則翻頁,即快速滑動也可以翻頁
},
indicator: { // 索引指示器設定
use: true, // 開關
bottom: ``, // 底部的距離
dotClass: ``, // 自定義圓點樣式
dotActiveClass: `` // 自定義啟用樣式
},
datas: [ // 圖片資料
{
src: `xxx`, // 圖片URL
href: `/`, // 圖片錨點,可以不設定
target: `_blank` // 點選錨點的跳轉處理(是在當前頁開啟還是新建視窗)
}
]
}
複製程式碼
示例
import swiper from `@c/swiper`
import img1 from `./images/1.jpg`
import img2 from `./images/2.jpg`
import img3 from `./images/3.jpg`
import img4 from `./images/4.jpg`
import img5 from `./images/5.jpg`
import img6 from `./images/6.jpg`
const rootNode = document.getElementById(`root`)
swiper(rootNode, {
// initIndex: 1,
// autoplay: {
// use: true,
// delay: 3000
// },
// slide: {
// use: true,
// scale: 1/3,
// speed: 0.2
// },
// indicator: {
// use: true,
// bottom: ``,
// dotClass: ``,
// dotActiveClass: ``
// },
datas: [
{
src: img1,
href: `/`,
target: `_blank`
},
{
src: img2,
href: `/`,
target: `_blank`
},
{
src: img3,
href: `/`,
target: `_blank`
},
{
src: img4,
href: `/`,
target: `_blank`
},
{
src: img5,
href: `/`,
target: `_blank`
},
{
src: img6,
href: `/`,
target: `_blank`
}
]
})
複製程式碼
使用心得
總體來說使用qnode
來開發的話還是比較方便的,檔案拆分以及資料共享都可以做到,唯一有一點瑕疵的話,就是對於js執行的順序要慎重考慮。想一想為什麼render檔案暴露出來的是函式,原因就是因為此時資料還未儲存到qnode
,因此通過函式來進行惰性載入,在合適的地方執行。
對於qnode
,目前還沒有錯誤提醒,呼叫方式不對的話沒有資訊吐出,後續可以考慮補上這個功能,畢竟其他開發者用的話,可能並不熟悉API,呼叫姿勢不對也是有可能發生的。
以上就是本文的全部內容了。
附: