Обзор прошлой ситуации
В предыдущей статье мы инкапсулировали DOM-библиотеку (qnode), чтобы каждый мог интуитивно ощутить ее удобный и дружественный кастомный фабричный шаблон, поэтому представляем вам эту статью.
Если вы не читали предыдущую статью, вы можете найти ее здесь:Заводской режим DOM нативной серии js.
Затем в этой статье мы будем опираться на вышеизложенное.qnode
, написать компонент карусели с бесконечным циклом с нуля.
Объяснение идей
Первый взгляд на макет карусели:
При скольжении весь контейнер карусели перемещается вперед или назад на одну позицию целиком, а эффект скольжения достигается установкой эффекта перехода css3. Может быть, вы удивитесь, а почему в голове и хвосте еще две картинки?
На самом деле, сердцевина карусели с бесконечным циклом заключается в двух дополнительных картинках в начале и в конце. время пользователь чувствует, что плавно переходит от последнего к первому. Когда он переходит к замещающему изображению 1, мы мгновенно переключаемся на розовое изображение 1 (то есть реальное изображение 1). Потому что это мгновенное изменение, пользователь не может его воспринять. То же самое верно и для перехода от рисунка 1 к рисунку 3. От этого цикл бесконечен, и такое чувство, что он никогда не закончится.Конечно, только мы знаем секрет ха-ха.
Структура каталогов
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
Для компиляции сгенерируйте уникальный идентификатор для имени класса, чтобы предотвратить конфликты имен. Вот набор скаффолдинга, который я настроил.Если вы чувствуете, что конфигурация веб-пакета хлопотна, вы можете клонировать мой проект, чтобы скомпилировать код: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 должен быть
- варианты: следующим образом (где нужно данные)
{
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. Подумайте, почему файл рендеринга предоставляет функции, ведь данные в это время еще не сохранены.qnode
, поэтому ленивая загрузка выполняется через функцию и выполняется в соответствующем месте.
дляqnode
,пока нет напоминания об ошибке.Если метод вызова неверный,информация не выплевывается.Можно подумать о добавлении этой функции в будущем.Ведь если ею пользуются другие разработчики,то они могут быть не знакомы с API, и не исключено, что вызывающая поза неправильная.
Это все для этой статьи.
Прикрепил: