JavaScript Library – Swiper
前言
官网已经有很好的教程了, 这篇只是记入一些我用过的东西和冷门知识.
参考
安装
yarn add swiper
JS
import Swiper from 'swiper'; // core js
import { Navigation } from 'swiper/modules'; // modules js
import 'swiper/css'; // core css
import 'swiper/css/navigation'; // module css const swiper = new Swiper('.swiper', {
modules: [Navigation], // modules
// config
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
});
它的调用是 import js & css from node_modules
有分 core module 和其它 modules (比如 navigation, pagination 等等, 都是按需加载的)
setup 就是 import modules 和 configuration.
HTML
<div class="swiper">
<div class="swiper-wrapper">
<div class="swiper-slide">slide1</div>
<div class="swiper-slide">slide2</div>
</div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
第一层 swiper 就是一个大 container, 自带 margin-auto (居中) 哦
第二层 wrapper 是用来把 slide 横排的, 它是 Flex.
第三层就是所有的 slides 了.
内容就放在 slide 里面. 不建议直接把内容当 slide 来使用, 给它 wrap 一层比较好.
常用 config
左右箭头
JS
import Swiper from 'swiper';
import { Navigation } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/navigation'; new Swiper('.swiper', {
modules: [Navigation],
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
});
注: 要 import CSS 哦
Pagination
HTML
JS
import Swiper from 'swiper';
import { Pagination } from 'swiper/modules';
import 'swiper/css';
import 'swiper/css/pagination'; new Swiper('.swiper', {
modules: [Pagination],
pagination: {
el: '.swiper-pagination',
clickable: true, // 默认是不可点击换 slide 的哦
dynamicBullets: true, // 开启的话 bullet 就不会出现到完, 好处是干净, 坏处是用户不知道总共有多少 slide
},
});
无限循环
JS
loop: true,
Autoplay 自动播放
import Swiper from 'swiper';
import { Autoplay } from 'swiper/modules'; new Swiper('.swiper', {
modules: [Autoplay],
autoplay: {
delay: 2500,
disableOnInteraction: false,
pauseOnMouseEnter: true,
},
});
它需要 import module 哦, 有一些交互暂停的配置
如果开启 pauseOnMouseEnter + disableOnInteraction 的话, mouse enter 后就 autoplay 就 stop 了而且不会恢复
如果只是开启 pauseOnMouseEnter 那只是 mouse enter 暂停, mouse leave 就恢复了
Slide per view / group
红色是 1 个 view, 绿色是一个 slide, 可以指定 1 个 view 里面出现都少个 slide. 它会自动去 set slide 的 width.
slidesPerView: 5, // 支持小数点哦, 2.5 表示 show 两个半 slide
spaceBetween: 30,
slidesPerGroup: 5
space between 就是 gap, 间距. unit 是 px
slidesPerGroup 是指 swipe 一下移动多少, 默认是 1 slide. 通常像上面这种会把 group 和 view set 成同等, 那么每 swipe 一次就是一整个 view 换到完.
gap can't swipe issue
目前有一个 issue, 虽然已经 close 了, 但显然还没有 fixed.
注意看, 第一个 view 的 gap 是可以 swipe 的, 但是第二个 view 开始 gap 就不可以 swipe 了.
目前没有什么好的 workaround, 用 padding 做的话需要一些小算法, 不是很好管理, 还是等等看它吧.
Breakpoint
breakpoints: {
640: {
slidesPerView: 2,
spaceBetween: 20,
},
768: {
slidesPerView: 4,
spaceBetween: 40,
},
1024: {
slidesPerView: 5,
spaceBetween: 50,
},
},
不同 breakpoint 下用不同的切换不同的 config
Auto Height
每个 slide 的高度可能不一样, 默认体验是拿最高的 slide 作为整个 swiper 的高度. 但这样可能一开始的时候会比较难看.
所以就有了 auto height. 它会在 change view 的时候拿当前 view 最高的 slide heigth 作为 swiper 高度, 是一个动态 set height 的效果.
new Swiper(swiperElement, {
autoHeight: true,
});
效果
dynamic content auto height
有时候会遇到动态内容, 那可以调用 JS API 来解决
swiper.addEventListener('ellipsisopen', () => {
swiperInstance.updateAutoHeight();
});
效果
init auto height with font family loaded
还有一种情况是 font family load 的慢, 也可能会造成 auto height 计算不准哦, 可以监听 font 加载完成后去 udpate 一下.
document.fonts.ready.then(() => {
swiperInstance.updateAutoHeight();
});
rounding issue
注意看, 第一个卡片的 border 不见了.
原因是 auto height 会 rounding. 把本来的 432.344px round 成了 432px 而已. 所以 border 被吃掉了 (我没有翻源码. 只是推测而已)
解决方法是在 swiper element 添加 overflow-y visible.
这样内容就可以超出范围了. 不用担心会超过太多, 因为 swiper 已经做了 auto height 计算, 高度已经是最大值了.
连续 Next 体验问题
auto height 每次换 slide 时都会改变 Swiper 高度,如果 prev next button 依赖于这个高度,那体验就会被影响。
上面例子中,我无法联系的按 next。解决思路有 2 个方向。
第一,prev next 不要依赖 slide 的高度。比如移到 Swiper 左边,而不是下面。(但有时候空间太少,真的没有地方可以放。)
第二,让这个 auto height 慢一点触发。比如 next 了 1 秒后才 resize。
要达到这个效果,Swiper 没有开放任何配置,我们只能靠 hack。
首先关闭 autoHeight
const swiperInstanace = new Swiper('.swiper', {
autoHeight: false,
});
然后模拟 auto height
swiperInstanace.el.classList.add('swiper-autoheight');
Swiper 的结构是 swiper > wrapper > slide
wrapper display flex + slide height 100% 导致了每个 slide 有不同的高度。
其实这里有点奇妙,通常我们会用 align-items: flex-start + slide height auto 来实现,而不是 slide height 100%。
而 .swiper-autoheight 做的事情就是把 height 100% 换成 auto 和加上 align-items: center。
这 2 种实现最大的区别在于当 wrapper height 小的时候,slide 的 offset-height 是否会跟着变小,100%就会,auto 则不会。
而 auto height 我们需要拿 slide 的 offset-height 所以它当然不应该变小咯。
接着监听 slide change 然后 delay 执行 updateAutoHeight 就可以了。
swiperInstanace.updateAutoHeight(); // for first load
fromEvent(swiperInstanace, 'slideChangeTransitionEnd')
.pipe(debounceTime(500))
.subscribe(() => {
swiperInstanace.updateAutoHeight(1000);
});
这里用了 RxJS 来写,代码比较干净。它做的事情就是监听 slideChangeTransitionEnd
每一次触发后只要 500ms 内没有再触发,那就调用 updateAutoHeight,如果一直触发,那就等到它稳定。
这样就实现了一直 next...等待 500ms 后 resize。
Navigation outside container
Swiper 默认是把 navigation 放到 container 里面的. 有时候它会被挡住, 这样很不好看.
HTML
JS
new Swiper('.swiper', {
modules: [Navigation],
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
slidesPerView: 4,
spaceBetween: 40,
});
解决方案
stackoverflow – CSS - How to have swiper slider arrows outside of slider that takes up 12 column row
它只给了一个思路, 把 navigation 放到 container 外面.
但这样是不够的, prev, next 是绝对定位, 必须在加上一个 wrapper 才可以
添加一个 swiper-container, 把 prev, next 移到外面
<div class="swiper-container">
<div class="swiper">
<div class="swiper-wrapper">
<div class="swiper-slide">
<img src="../images/brand-logo/carrier.jpg" />
</div>
</div>
</div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
CSS Style
.swiper-container {
position: relative;
padding-inline: 6rem;
width: 50%;
margin-inline: auto; .swiper {
.swiper-wrapper {
.swiper-slide {
img {
max-width: 100%;
}
}
}
}
}
关键是让 container 取代 .swiper (提醒: container position 必须是 relative 哦, 这样 button-next/prev 才能定位到)
最终效果:
绿色部分就是 padding 的作用了.
Pagination 也一样
pagination 也有同样的问题, 解决方案也是一样的, 搞 swiper-container 做 padding-bottom
后来我发现, pagination 视乎并没有这个问题. slide 的高度不会覆盖 padding bottom, 所以只要在 swiper 加 padding-bottom 就可以了. 不需要搞 swiper-container.
当遇到 multiple swiper
如果有超过 1 个 swiper. 上面这个 container 方案会失效. 原因是 selector 查找是往 child 找的
当我们把 pagination 移出 .swiper 以后, 如果整个 page 只有一个 swiper 它还能找到, 多个就找错了.
所以需要修改 JS, 自己 select 然后把 element 传给 Swiper
const swiperContainers = document.querySelectorAll('.swiper-container');
for (const container of swiperContainers) {
const swiper = container.querySelector<HTMLElement>('.swiper')!;
const pagination = container.querySelector<HTMLElement>('.swiper-pagination')!;
new Swiper(swiper, {
modules: [Pagination],
pagination: {
el: pagination,
},
});
}
当遇到 override style
通过 CSS Variables 修改 style, 本来 variables 是一定在 .swiper 的 (.swiper-pagination 的 parent)
但这里移出来了, 所以 variables 要改成定义在它的 parent .swiper-container 哦.
Hero Banner Image & Text & Animation
看这篇 CSS & JS Effect – Hero Banner Swiper
和 Swiper 有关的地方是, 当 slide active 时, swiper 会给 slide element 一个 swiper-slide-active class
Text Selection
参考: stackoverflow – how to enable select text in swiper.js
默认情况下, swiper 是 select 不到 slide 中的 text 的, 但是它的 cursor 却会误导用户哦.
解决方法 1 (推荐) : cursor: default
这样就明确让用户知道, text 无法被选择.
解决方法 2:
wrap 一层 span 加上 class swiper-no-swiping, 这样 text 就不能 swiping 同时可以 select 了
解决方法 3:
完全禁止 swiping, 只能通过 navigation 换 slide
Nested Swiper
Child A 无法被 swipe,因为它是 nested Swiper。
解决方法非常简单,只要在 Child Swiper 添加 Config 就可以了。
new Swiper(childSwiperElement, { nested: true });
效果
Multiple Swiper in Page
参考: stackoverflow – How can I have multiple "Swiper" slideshows on a single page
<div class="swiper swiper1">
<div class="swiper-wrapper">
<div class="swiper-slide">slide1</div>
<div class="swiper-slide">slide2</div>
</div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
<div class="swiper swiper2">
<div class="swiper-wrapper">
<div class="swiper-slide">slide1</div>
<div class="swiper-slide">slide2</div>
</div>
<div class="swiper-button-next"></div>
<div class="swiper-button-prev"></div>
</div>
在最外头的 element 加上一个别名 class name, 比如 swiper1, swiper2. 原来的 swiper 依据要放哦.
swiper 里面就不需要别名了, 比如 swiper-wrapper, slide, button-next 等都不需要
JavaScript
new Swiper('.swiper1', {
modules: [Navigation],
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
}); new Swiper('.swiper2', {
modules: [Navigation],
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
});
也是最外头的 selector 需要选中别名就可以了. nextEl .swiper-button-next 不需要.
Tips: 也可以用 child selector 比如 '.hero-banner-section > .swiper' 这样也是可以 selector 到正确的, 而且不需要别名. 看个人管理习惯呗.
Override Style
swiper 的 style 大部分都是用 CSS variable 做的. 这里举一个 override pagination 的例子.
CSS Style
override CSS Style
.swiper {
--swiper-pagination-bullet-size: 100px;
--swiper-pagination-bullet-width: 100px;
--swiper-pagination-bullet-height: 100px; --swiper-theme-color: red;
--swiper-pagination-color: red;
}
这几个变量都是有效的. 要直接 override by selector 也是可以
.swiper {
.swiper-pagination-bullet {
background-color: red;
}
}
效果
Fully Custom Pagination
效果
主要就是写一些交互代码就可以了.
HTML
<div class="container">
<div class="swiper">
<div class="swiper-wrapper">
<div class="swiper-slide">
<img src="./images/tifa1.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="./images/tifa2.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="./images/tifa3.jpg" width="16" height="9" />
</div>
</div>
</div>
<div class="bullet-list">
<div class="bullet active">
<img src="./images/tifa1.jpg" width="16" height="9" />
</div>
<div class="bullet">
<img src="./images/tifa2.jpg" width="16" height="9" />
</div>
<div class="bullet">
<img src="./images/tifa3.jpg" width="16" height="9" />
</div>
</div>
</div>
CSS Style
.container {
width: 400px; .bullet-list {
margin-top: 1rem;
display: flex;
gap: 8px; .bullet {
cursor: pointer;
width: 75px;
border-radius: 4px;
overflow: hidden; &.active {
outline: 2px solid red;
}
}
}
}
没什么特别的, 做一些排版而已.
JavaScript (重点来了)
参考:
import Swiper from 'swiper'; const bullets = document.querySelectorAll('.bullet');
const swiper = new Swiper('.swiper', {
on: {
slideChange: (swiper) => {
bullets.forEach((bullet) => bullet.classList.remove('active'));
bullets.item(swiper.activeIndex).classList.add('active');
},
},
}); bullets.forEach((bullet, index) => {
bullet.addEventListener('click', () => {
swiper.slideTo(index);
});
});
不需要 引入 Pagination Module.
1. 监听 slideChange event, 当用户 slide swiper 的时候更新 bullet style. (用到了 Swiper event)
2. 当用户 click bullet 的时候更新 swiper. (用到了 Swiper method)
提醒:
1. item activeIndex = 0 = first item (这个符合 JS 习惯)
2. 如果 Swiper 开启 loop 模式, 它会有 duplicated 的 slide, 而这些 slide index 是继续增长的.
比如, 有 3 个 slide, [0, 1, 2], 因为 loop 它 duplicated 了 1 个 slide, 那么变成 [0, 1, 2, 3]
当遇上 slidesPerGroup
上面给的例子是比较简单的, slide 和 bullet 的数量一致. 如果遇到有 slidesPerGroup 的情况会比较麻烦一些.
参考 swiper 原生 pagination 的实现方法, 可以看出它是动态创建 bullet 的. 因为 bullet 的数量不仅仅依赖 slide 的数量,
同时也依赖各种配置, 比如 slidesPerGroup, dynamicBullets 等等.
这里我给出一个 slidesPerGroup 的实现方式. 原理依然是用上面教的底层 API, 只是加了一些计算而已.
const swiper = new Swiper('.swiper', {
slidesPerView: 2,
spaceBetween: 16,
slidesPerGroup: 2,
}); // 从 swiper.slides 中获取图片当缩率图
const slideImages = swiper.slides.map(slide => slide.querySelector('img')!);
// 依据 slidesPerGroup 选出呈现的图片第 1 3 5 张
const bulletImages = slideImages.filter((_, index) => index % swiper.params.slidesPerGroup! === 0);
// 创建 bullet 和绑定事件
const bullets = bulletImages.map((bulletImage, index) => {
const bullet = document.createElement('div');
bullet.classList.add('bullet');
// 这里也是需要计算
if (index === swiper.activeIndex / swiper.params.slidesPerGroup!) {
bullet.classList.add('active');
}
bullet.appendChild(bulletImage.cloneNode(true));
bullet.addEventListener('click', () => {
// 这里也是要计算
swiper.slideTo(index * swiper.params.slidesPerGroup!);
});
return bullet;
});
const bulletList = document.querySelector('.bullet-list')!;
bullets.forEach(bullet => bulletList.appendChild(bullet)); swiper.on('slideChange', swiper => {
bullets.forEach(bullet => bullet.classList.remove('active'));
// 这里也是要计算
bullets[swiper.activeIndex / swiper.params.slidesPerGroup!].classList.add('active');
});
补上一个 breakpoint + RxJS 的版本
<div class="custom-swiper-pagination">
<template><div class="bullet"></div></template>
</div> const swiper = new Swiper('.swiper', {
breakpoints: {
768: {
slidesPerView: 2,
spaceBetween: 16,
slidesPerGroup: 2,
},
},
}); const slidesPerGroup$ = fromEvent(swiper, 'breakpoint').pipe(
map(() => swiper.params.slidesPerGroup!),
startWith(swiper.params.slidesPerGroup!),
distinctUntilChanged()
); const pagination = document.viewChild('.custom-swiper-pagination');
const bulletTemplate = pagination.viewChild<HTMLTemplateElement>('template');
let bullets: Element[] = [];
slidesPerGroup$.subscribe(slidesPerGroup => {
bullets.forEach(bullet => pagination.removeChild(bullet)); bullets = swiper.slides
.filter((_, index) => index % slidesPerGroup === 0)
.map((_, index) => {
const frag = bulletTemplate.content.cloneNode(true) as DocumentFragment;
const bullet = frag.firstElementChild!;
if (index === swiper.activeIndex / slidesPerGroup) {
bullet.classList.add('active');
}
bullet.addEventListener('click', () => {
swiper.slideTo(index * slidesPerGroup);
});
pagination.appendChild(frag);
return bullet;
});
}); swiper.on('slideChange', () => {
bullets.forEach(bullet => bullet.classList.remove('active'));
bullets[swiper.activeIndex / swiper.params.slidesPerGroup!].classList.add('active');
});
当遇上 dynamicBullets
dynamic bullets 用于当 bullet 非常多的时候, 它可以只显示一部分。有点小 slider 的感觉.
它的实现主要靠 2 招
1. .prev-prev, .prev, .active, .next, .next-next
这 5 个 class 就是主要的 design
2. translateX
移动是靠 translateX 完成的.
HTML
<div class="pagination">
<div class="bullet-list">
<div class="bullet active"></div>
<div class="bullet next"></div>
<div class="bullet next-next"></div>
<div class="bullet"></div>
<div class="bullet"></div>
<div class="bullet"></div>
<div class="bullet"></div>
<div class="bullet"></div>
<div class="bullet"></div>
<div class="bullet"></div>
</div>
</div>
<div class="action">
<button class="prev-btn">prev</button>
<button class="next-btn">next</button>
</div>
CSS
.pagination {
// 一些变量控制 design
--bullet-size: 2rem;
--gap: 0.5rem;
$bullet-count: 5;
--bullet-count: #{$bullet-count};
// 因为 CSS 没有 Math.Floor 只好用 Sass 替代
--bullet-half-count: #{math.floor(calc($bullet-count / 2))};
--active-index: 0; margin-top: 3rem;
outline: 1px solid blue;
// 计算 display width (依据 bullet 大小, 数量, 间距)
width: calc(
(var(--bullet-count) * var(--bullet-size)) + ((var(--bullet-count) - 1) * var(--gap))
);
overflow-x: hidden;
margin-inline: auto; .bullet-list {
display: flex;
gap: var(--gap);
// 计算 translateX (依据 bullet 大小, 间距, 当前 active index)
transform: translateX(
calc(
(var(--bullet-half-count) * var(--bullet-size) + var(--bullet-half-count) * var(--gap)) -
(var(--active-index) * var(--bullet-size) + var(--active-index) * var(--gap))
)
);
transition: transform 0.4s; .bullet {
flex-shrink: 0;
width: var(--bullet-size);
aspect-ratio: 1 / 1;
border-radius: 50%;
background-color: pink;
&.active {
background-color: red;
}
&.prev,
&.next {
transform: scale(0.66);
}
&.prev-prev,
&.next-next {
transform: scale(0.33);
}
transition-property: background-color, transform;
transition-duration: 0.4s;
}
}
} .action {
margin-top: 2rem;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-inline: auto;
width: max-content; button {
background-color: red;
color: white;
padding: 1rem 2rem;
}
}
JS
代码比较碎, 但其实只是做了 2 件事
1. set .prev-prev, .prev, .active, .next, .next-next
2. set CSS variable --active-index
const pagination = document.querySelector<HTMLElement>('.pagination')!;
const bullets = Array.from(pagination.querySelectorAll('.bullet')); function prevNext(prevOrNext: 'prev' | 'next'): void {
const prevNextMethods = {
prev: getPrevNIndex,
next: getNextNIndex,
};
const prevNextMethod = prevNextMethods[prevOrNext]; const activeIndex = bullets.findIndex(bullet => bullet.classList.contains('active'));
const currIndex = prevNextMethod(bullets, activeIndex);
bullets.forEach(bullet =>
['prev-prev', 'prev', 'active', 'next', 'next-next'].forEach(className =>
bullet.classList.remove(className)
)
);
bullets[currIndex].classList.add('active');
tryAddPrevOrNext('prev', 1);
tryAddPrevOrNext('prev', 2);
tryAddPrevOrNext('next', 1);
tryAddPrevOrNext('next', 2);
pagination.style.setProperty('--active-index', currIndex.toString()); function tryAddPrevOrNext(prevOrNext: 'prev' | 'next', count: number): void {
const prevNextMethod = prevNextMethods[prevOrNext];
let loopIndex = currIndex;
for (let i = 0; i < count; i++) {
const index = prevNextMethod(bullets, loopIndex, { mode: 'null' });
if (index === null) return;
loopIndex = index;
if (i === count - 1) {
bullets[loopIndex].classList.add(
Array(i + 1)
.fill(prevOrNext)
.join('-')
);
}
}
}
}
const nextBtn = document.querySelector('.next-btn')!;
const prevBtn = document.querySelector('.prev-btn')!;
nextBtn.addEventListener('click', () => prevNext('next'));
prevBtn.addEventListener('click', () => prevNext('prev')); // helper 方法
// 告诉它当前你在第几个, 然后想 next 多少个, 当超过的时候它会帮你 loop or 返回 null
export function getNextNIndex(
lengthOrItems: number | unknown[],
currIndex: number,
config?: { n?: number; mode?: 'loop' | 'max' }
): number;
export function getNextNIndex(
lengthOrItems: number | unknown[],
currIndex: number,
config?: { n?: number; mode?: 'null' }
): number | null;
export function getNextNIndex(
lengthOrItems: number | unknown[],
currIndex: number,
config?: { n?: number; mode?: 'loop' | 'max' | 'null' }
): number | null {
const length = Array.isArray(lengthOrItems) ? lengthOrItems.length : lengthOrItems;
const { n = 1, mode = 'loop' } = config ?? {};
const next = currIndex + n;
if (mode === 'max' && next > length - 1) {
return length - 1;
} else if (mode === 'null' && next > length - 1) {
return null;
}
return next % length;
} // helper 方法
// 告诉它当前你在第几个, 然后想 prev 多少个, 当超过的时候它会帮你 loop or 返回 null
export function getPrevNIndex(
lengthOrItems: number | unknown[],
currIndex: number,
config?: { n?: number; mode?: 'loop' | 'max' }
): number;
export function getPrevNIndex(
lengthOrItems: number | unknown[],
currIndex: number,
config?: { n?: number; mode?: 'null' }
): number | null;
export function getPrevNIndex(
lengthOrItems: number | unknown[],
currIndex: number,
config?: { n?: number; mode?: 'loop' | 'max' | 'null' }
): number | null {
const length = Array.isArray(lengthOrItems) ? lengthOrItems.length : lengthOrItems;
const { n = 1, mode = 'loop' } = config ?? {};
const prev = currIndex - n;
if (mode === 'max' && prev < 0) {
return 0;
} else if (mode === 'null' && prev < 0) {
return null;
}
return (length + (prev % length)) % length;
}
最后送上一个完成版本 RxJS + breakpoint + slidesPerGroup + dynamicBullets
HTML
结构 pagination > bullet-list > bullet
bullet-list 负责 translateX
bullet 负责小 design, 比如 .active 高亮, .prev scale(0.66)
<div class="swiper">
<div class="swiper-wrapper">
<div class="swiper-slide">
<img src="../images/tifa1.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa2.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa3.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa1.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa2.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa3.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa1.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa2.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa3.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa1.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa2.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa3.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa1.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa2.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa3.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa1.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa2.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa3.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa1.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa2.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="../images/tifa3.jpg" width="16" height="9" />
</div>
</div>
<div class="swiper-pagination"></div>
</div>
<div class="custom-swiper-pagination">
<div class="bullet-list"></div>
</div>
CSS
控制所有 design, width, overflow hidden, translateX, scale 等等
.swiper {
--swiper-pagination-bullet-size: 16px; .swiper-pagination {
background-color: white;
padding-block: 0.5rem;
}
} .custom-swiper-pagination {
--bullet-size: 2rem;
--gap: 0.5rem;
$bullet-count: 5;
--bullet-count: #{$bullet-count};
--bullet-half-count: #{math.floor(calc($bullet-count / 2))};
--active-index: 0; margin-top: 3rem;
outline: 1px solid blue;
width: calc(
(var(--bullet-count) * var(--bullet-size)) + ((var(--bullet-count) - 1) * var(--gap))
);
overflow-x: hidden;
margin-inline: auto; .bullet-list {
display: flex;
gap: var(--gap);
transform: translateX(
calc(
(var(--bullet-half-count) * var(--bullet-size) + var(--bullet-half-count) * var(--gap)) -
(var(--active-index) * var(--bullet-size) + var(--active-index) * var(--gap))
)
);
transition: transform 0.4s; .bullet {
flex-shrink: 0;
width: var(--bullet-size);
aspect-ratio: 1 / 1;
border-radius: 50%;
background-color: pink;
&.active {
background-color: red;
}
&.prev,
&.next {
transform: scale(0.66);
}
&.prev-prev,
&.next-next {
transform: scale(0.33);
}
transition-property: background-color, transform;
transition-duration: 0.4s;
}
}
}
JS
负责监听 swiper, 读取 swiper config
动态创建 bullet element, 同时 reset .active, .prev, .next 等 class, 还有 CSS variable --active-index
import Swiper, { Pagination } from 'swiper';
import { distinctUntilChanged, fromEvent, map, startWith } from 'rxjs'; const swiper = new Swiper('.swiper', {
modules: [Pagination],
pagination: {
el: '.swiper-pagination',
clickable: true,
dynamicBullets: true,
},
breakpoints: {
640: {
slidesPerView: 2,
spaceBetween: 16,
slidesPerGroup: 2,
},
1024: {
slidesPerView: 3,
spaceBetween: 16,
slidesPerGroup: 3,
},
},
}); const pagination = document.querySelector<HTMLElement>('.custom-swiper-pagination')!;
const slidesPerGroup$ = fromEvent(swiper, 'breakpoint').pipe(
map(() => swiper.params.slidesPerGroup!),
startWith(swiper.params.slidesPerGroup!),
distinctUntilChanged()
);
const bullets: HTMLElement[] = [];
const bulletList = document.querySelector('.bullet-list')!;
slidesPerGroup$.subscribe(() => {
bullets.forEach(bullet => bullet.parentElement!.removeChild(bullet));
bullets.length = 0;
const bulletLength = Math.ceil(swiper.slides.length / swiper.params.slidesPerGroup!);
for (let index = 0; index < bulletLength; index++) {
const bullet = document.createElement('div');
bullet.classList.add('bullet');
if (typeof swiper.params.pagination === 'object' && swiper.params.pagination.clickable) {
bullet.addEventListener('click', () => {
swiper.slideTo(index * swiper.params.slidesPerGroup!);
});
bullet.style.cursor = 'pointer';
}
bullets.push(bullet);
}
bullets.forEach(bullet => bulletList.appendChild(bullet));
resetActive(bullets, swiper.activeIndex / swiper.params.slidesPerGroup!, pagination);
}); swiper.on('slideChange', swiper => {
resetActive(bullets, swiper.activeIndex / swiper.params.slidesPerGroup!, pagination);
}); function resetActive(bullets: HTMLElement[], activeIndex: number, pagination: HTMLElement): void {
const prevNextMethods = {
prev: getPrevNIndex,
next: getNextNIndex,
}; bullets.forEach(bullet =>
['prev-prev', 'prev', 'active', 'next', 'next-next'].forEach(className =>
bullet.classList.remove(className)
)
);
bullets[activeIndex].classList.add('active');
tryAddPrevOrNext('prev', 1);
tryAddPrevOrNext('prev', 2);
tryAddPrevOrNext('next', 1);
tryAddPrevOrNext('next', 2);
pagination.style.setProperty('--active-index', activeIndex.toString()); function tryAddPrevOrNext(prevOrNext: 'prev' | 'next', count: number): void {
const prevNextMethod = prevNextMethods[prevOrNext];
let loopIndex = activeIndex;
for (let i = 0; i < count; i++) {
const index = prevNextMethod(bullets, loopIndex, { mode: 'null' });
if (index === null) return;
loopIndex = index;
if (i === count - 1) {
bullets[loopIndex].classList.add(
Array(i + 1)
.fill(prevOrNext)
.join('-')
);
}
}
}
} export function getNextNIndex(
lengthOrItems: number | unknown[],
currIndex: number,
config?: { n?: number; mode?: 'loop' | 'max' }
): number;
export function getNextNIndex(
lengthOrItems: number | unknown[],
currIndex: number,
config?: { n?: number; mode?: 'null' }
): number | null;
export function getNextNIndex(
lengthOrItems: number | unknown[],
currIndex: number,
config?: { n?: number; mode?: 'loop' | 'max' | 'null' }
): number | null {
const length = Array.isArray(lengthOrItems) ? lengthOrItems.length : lengthOrItems;
const { n = 1, mode = 'loop' } = config ?? {};
const next = currIndex + n;
if (mode === 'max' && next > length - 1) {
return length - 1;
} else if (mode === 'null' && next > length - 1) {
return null;
}
return next % length;
} export function getPrevNIndex(
lengthOrItems: number | unknown[],
currIndex: number,
config?: { n?: number; mode?: 'loop' | 'max' }
): number;
export function getPrevNIndex(
lengthOrItems: number | unknown[],
currIndex: number,
config?: { n?: number; mode?: 'null' }
): number | null;
export function getPrevNIndex(
lengthOrItems: number | unknown[],
currIndex: number,
config?: { n?: number; mode?: 'loop' | 'max' | 'null' }
): number | null {
const length = Array.isArray(lengthOrItems) ? lengthOrItems.length : lengthOrItems;
const { n = 1, mode = 'loop' } = config ?? {};
const prev = currIndex - n;
if (mode === 'max' && prev < 0) {
return 0;
} else if (mode === 'null' && prev < 0) {
return null;
}
return (length + (prev % length)) % length;
}
小结论
这种 fully customize 的做法, 我个人觉得非常不划算. 尤其当你仅仅想要修改 design 而且的时候, 它需要自己实现太多功能了.
所以, 我建议, 尽量用 override CSS 的方式去改 design, 要改 HTML 的话就用 renderBullet 方法.
Auto Height and Same Height
问题
这个 slider 有 2 地方不满意.
1. 2 个 slide 高度不一致产生的空白
2. 多个 slide view 高度不一致产生的空白
simple 解决方案
导致两个 slide 不同高度的原因是 swiper slide 的 height 100%
swiper-slide 的 parent 是 swiper-wrapper, wrapper 是 display flex,
按理说 flex items 默认是 stretch 的状态, 但因为 swiper-slide 给了 height 100% override 掉了这个特性. 所以它就变成了 flex-start. (这个我在 Flex 文章说提过)
所以, 想要 same height 可以通过 override swiper-slide 的 height : auto 来实现.
.swiper-slide {
height: auto;
}
效果
第二个问题: 多个 slide view 高度不一致, 它有 build-in 的解决方案. 那就是用 autoHeight: true
new Swiper('.swiper', {
autoHeight: true,
});
效果
虽然是解决了第二个问题, 但是第一个问题又出现了. 两个 slide 的高度又不一致了. why?
因为 swiper 在 autoheight 的情况下, 把 swiper-wrapper 的 flex align-items 设置成了 flex-start, 之前是默认的 stretch. (要 stretch + height auto 才可以实现 same height)
如果我们用 CSS override 它, 会发现 autoheight 就 stop working 了...鱼与熊掌, 难以兼得啊...
自定义方案
要保持 same heigh 除了 flex stretch 还有一个简单粗暴的方法. 那就是替 slide 加上 min-height. 通过 JS 计算比较高的 height, 然后 set min-height 到矮的那个 slide.
const slides = Array.from(document.querySelectorAll<HTMLElement>('.swiper-slide'));
// 依据 slidesPerView, group 出每一个 view 的 slides
const slideGroups = slides.groupToMap((_, index) =>
Math.floor(index / (swiper.params.slidesPerView as number))
);
for (const [_, groupedSlides] of slideGroups) {
// 找出最高的 slide height
const maxHeight = Math.max(...groupedSlides.map(el => el.offsetHeight));
// 加上 min height
groupedSlides.forEach(el => (el.style.minHeight = `${maxHeight}px`));
}
里头用到了 es2023 的 Array.groupToMap 哦.
效果
这个方案简单, 也容易实现, 而且可以满足 90% 的场景. 但它依然有许多瑕疵.
比如最后一个 slide view 的高度就没有一致. 也没有考虑到 slide resize 的情况.
这里附上一个完整版本, 但我就不解释了. 它只是多了一些繁琐的控制而已.
HTML
<div class="container">
<div class="swiper">
<div class="swiper-wrapper">
<div id="slide1" class="swiper-slide">
<h1>Title</h1>
<p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. A, blanditiis?</p>
</div>
<div id="slide2" class="swiper-slide">
<h1>Title</h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. In molestiae nesciunt ducimus maiores.</p>
</div> <div id="slide3" class="swiper-slide">
<h1>Title</h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, itaque aperiam porro perspiciatis vel in repellendus nam nobis voluptas natus.</p>
</div>
<div id="slide4" class="swiper-slide">
<h1>Title</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Debitis esse, harum magni quibusdam est, modi rem minus culpa eaque architecto recusandae delectus iste excepturi quis commodi sed? Id tempora qui porro iusto, facilis nihil ut, eum nemo, delectus placeat distinctio.</p>
</div> <div id="slide5" class="swiper-slide">
<h1>Title</h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, itaque aperiam porro perspiciatis vel in repellendus nam nobis voluptas natus.</p>
</div>
<div id="slide6" class="swiper-slide">
<h1>Title</h1>
<p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Debitis tempora dicta esse impedit iusto reprehenderit delectus exercitationem animi corporis ratione.</p>
</div> <div id="slide7" class="swiper-slide">
<h1>Title</h1>
<p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. A, blanditiis?</p>
</div>
<div id="slide8" class="swiper-slide">
<h1>Title</h1>
<p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Excepturi, in?</p>
</div> <div id="slide9" class="swiper-slide">
<h1>Title</h1>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolorum, itaque aperiam porro perspiciatis vel in repellendus nam nobis voluptas natus.</p>
</div>
</div>
<div class="swiper-pagination"></div>
</div>
</div>
CSS
.container {
margin-top: 256px; .swiper {
--swiper-pagination-bullet-size: 16px;
padding-bottom: 4rem; .swiper-slide {
padding: 2rem; h1 {
font-size: 4rem;
margin-bottom: 0.5rem;
}
p {
line-height: 1.5;
}
&:nth-child(odd) {
background-color: hsl(0, 0%, 90%);
}
&:nth-child(even) {
background-color: pink;
}
}
}
}
JS 1
这个是用来保持 same height 的
import { EMPTY, Observable, startWith, switchMap } from 'rxjs';
import { assert } from '../module/core/core'; // #region Custom Event
export const sameHeightResizeEventName = 'sameheightresize'; export class SameHeightResizeEvent extends CustomEvent<void> {
constructor(eventInitDict?: CustomEventInit<void>) {
super(sameHeightResizeEventName, { bubbles: true, ...eventInitDict });
}
}
declare global {
interface HTMLElement {
addEventListener<K extends typeof sameHeightResizeEventName>(
type: K,
listener: (this: HTMLElement, ev: SameHeightResizeEvent) => unknown,
options?: boolean | AddEventListenerOptions
): void;
}
}
// #endregion End of Custom Event export function setupSameHeight(config?: {
container?: Document | HTMLElement;
dynamicContent?: boolean;
}): void {
const { container: root = document, dynamicContent = false } = config ?? {};
const contentChanged$ = dynamicContent
? new Observable(subscriber => {
const mo = new MutationObserver(entries => {
const addedHaveSameHeight = Array.from(entries[0].addedNodes)
.filter((el): el is HTMLElement => el instanceof HTMLElement)
.some(el => el.classList.contains('same-height')); const removedHaveSameHeight = Array.from(entries[0].removedNodes)
.filter((el): el is HTMLElement => el instanceof HTMLElement)
.some(el => (el as HTMLElement).classList.contains('same-height')); if (addedHaveSameHeight || removedHaveSameHeight) {
subscriber.next();
}
}); mo.observe(root, { childList: true, subtree: true }); return () => mo.disconnect();
})
: EMPTY; contentChanged$
.pipe(
startWith(null),
switchMap(
() =>
new Observable(() => {
const sameHeightElements = Array.from(
root.querySelectorAll<HTMLElement>('.same-height')
); const groupChanged$ = new Observable(subscriber => {
const mo = new MutationObserver(entries => {
if (
entries.some(entry => {
assert(entry.target instanceof HTMLElement);
return entry.target.dataset.sameHeightGroup !== entry.oldValue;
})
) {
subscriber.next();
}
});
sameHeightElements.forEach(el =>
mo.observe(el, { attributes: true, attributeFilter: ['data-same-height-group'] })
);
return () => mo.disconnect();
}); const subscription = groupChanged$
.pipe(
startWith(null),
switchMap(
() =>
new Observable(() => {
const sameHeightGroups = sameHeightElements.groupToMap(
el => el.dataset.sameHeightGroup ?? 'default'
);
const resizeObservers: ResizeObserver[] = [];
for (const [_, sameHeightGroup] of sameHeightGroups) {
setSameHeight(sameHeightGroup);
const ro = new ResizeObserver(() => {
sameHeightGroup[0].dispatchEvent(new SameHeightResizeEvent());
setSameHeight(sameHeightGroup);
});
sameHeightGroup.forEach(el => ro.observe(el));
resizeObservers.push(ro);
}
return () => resizeObservers.forEach(ro => ro.disconnect());
})
)
)
.subscribe(); return () => subscription.unsubscribe();
})
)
)
.subscribe(); function setSameHeight(elements: HTMLElement[]): void {
const maxHeight = Math.max(...elements.map(elment => getActualHeight(elment)));
for (const element of elements) {
const actualHeight = getActualHeight(element);
if (actualHeight < maxHeight) {
element.style.setProperty('min-height', `${maxHeight}px`);
element.dataset.originalHeight = actualHeight.toString();
} else {
element.style.removeProperty('min-height');
delete element.dataset.originalHeight;
}
} function getActualHeight(element: HTMLElement): number {
const minHeight = parseFloat(element.style.getPropertyValue('min-height'));
if (Number.isNaN(minHeight)) return element.offsetHeight;
const originalHeight = parseFloat(element.dataset.originalHeight!);
if (element.offsetHeight > minHeight) return element.offsetHeight;
return originalHeight;
}
}
}
JS 2
这个是用来对接 swiper 的
import Swiper, { Pagination } from 'swiper';
import { distinctUntilChanged, fromEvent, map, merge } from 'rxjs';
import { setupSameHeight } from '../test/test';
import { assert } from '../module/core/core'; const swiper = new Swiper('.swiper', {
modules: [Pagination],
pagination: {
el: '.swiper-pagination',
clickable: true,
dynamicBullets: true,
},
breakpoints: {
640: {
slidesPerView: 2,
slidesPerGroup: 2,
},
768: {
slidesPerView: 3,
slidesPerGroup: 3,
},
},
spaceBetween: 16,
autoHeight: true,
}); const slides = Array.from(document.querySelectorAll<HTMLElement>('.swiper-slide'));
slides.forEach(el => el.classList.add('same-height'));
setGroupBaseOnSlidesPerView(slides);
setupSameHeight();
const slidesPerView$ = fromEvent(swiper, 'breakpoint').pipe(
map(() => swiper.params.slidesPerView as number),
distinctUntilChanged()
);
merge(fromEvent(swiper, 'slideChange'), slidesPerView$).subscribe(() =>
setGroupBaseOnSlidesPerView(slides)
);
swiper.el.addEventListener('sameheightresize', () => swiper.updateAutoHeight()); function setGroupBaseOnSlidesPerView(elements: HTMLElement[]): void {
for (let index = 0; index < elements.length; index++) {
const element = elements[index];
// group index 是依据 view 里面第一个 slide 的 index
// 0 1 2 3 4 5 6 7 8 9 10 11
// 0 0 0 3 3 3 6 6 6 9 9 9
assert(typeof swiper.params.slidesPerView === 'number');
const slidesPerView = swiper.params.slidesPerView;
const groupIndex = Math.floor(index / slidesPerView) * slidesPerView;
element.dataset.sameHeightGroup = `Group${groupIndex}`; // 特别处理 last slide view (当 slides 不够时, 最后几个 slide 会有 dynamic group, 它 depend on 是 last view or last 2nd view)
if (index >= swiper.activeIndex && index < swiper.activeIndex + slidesPerView) {
element.dataset.sameHeightGroup = `Group${swiper.activeIndex}`;
}
}
}
效果
Re-order / Sort
没有 re-order 的接口,我们要修改 slide 的位置,可以直接操作 dom。
通过 swiper.slides 找出要移动的 slide element,然后直接 DOM append, prepend, insertAfter 等等。
操作完后调用 swiper.updateSlides() 通知 swiper 就可以了。
Limitation
Only Run Swiper in Certain Breakpoint
参考
Github Issue – Add a way to disable swiper for some breakpoints
Github Issue – Question: How to disable swiper for certain breakpoint only?
Github Issue – How to disable style inline on element swiper-wrapper and swiper-slide
有时候可能希望只有手机使用 Swiper, 电脑不需要. 但 Swiper 并没有很好的 way 去关掉它.
虽然它可以 disable() 或者 destroy() 但是 CSS style, inline style 都不会被清除.
我觉得这条路不顺风水. 如果不要兼顾 resize 的情况或许还可以勉强实现一下.
JS 判断 viewport 决定要不要 init Swiper, 如果不需要就顺便把 Swiper 的 class 清除掉, 比如 .swiper, .swiper-wrapper, .swiper-slide 等等
Flex / Grid Container + Swiper max-width = Bug
不是 100% 确定是 bug, 但我也没有时间去研究了. 这里做一个记入.
上图是一个正常的表现. resize viewport 图片和 paragraph 会放大缩小.
HTML
<div class="container">
<div class="swiper">
<div class="swiper-wrapper">
<div class="swiper-slide">
<img src="./images/tifa1.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="./images/tifa2.jpg" width="16" height="9" />
</div>
<div class="swiper-slide">
<img src="./images/tifa3.jpg" width="16" height="9" />
</div>
</div>
</div>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime quod nam
natus velit repellendus? Voluptatem consectetur quam accusantium labore
iure ratione voluptate voluptatum harum enim adipisci consequatur
facilis nemo qui, voluptates soluta pariatur nihil expedita natus amet
cumque impedit id porro. Voluptate sunt natus ipsa nostrum quibusdam
reprehenderit, quas harum?
</p>
</div>
CSS Style
.container {
border: 1px solid red;
padding: 1rem; .swiper {
max-width: 428px;
margin-inline: auto;
}
p {
max-width: 640px;
margin-inline: auto;
}
}
关键是 max-width
好, 现在让 container 变成 Flex 或 Grid
结果 bug 出现了
图片超出了 viewport
原因是 Swiper 给的 style width 是 428px, 这不是 max-width 吗?
相关 Github Issue – PSA: Too big/wide slider on initialization
解决方案
给 Swiper 加上 width 100% 就可以破了...没时间去调查原理. 先用着呗.
或者用 width: min(100%, 428px)
Migration
这里记入一下历届版本我遇到的 breaking change.
Swiper 11
参考: Docs Migration Guide to Swiper 11
1. .swiper overflow 从 visible 改成了 hidden,不清楚它为什么要改,但官方说如果这影响了我们的排版,我们 可以 set 成 clip。我自己是 set 成 visible。
2. Swiper Element (Web Component) dispatch 的 event 有 prefix 了。
以前是 slidechange 现在是 swiperslidechange
我之前都没有注意过 Swiper Element,它是 v9 推出的,简单说就是用 Web Component wrapper 了一层,底层依然是 new Swiper。
这是一小段 SwiperContainer 的代码,它是 Custom Elements 来的。
JavaScript Library – Swiper的更多相关文章
- A javascript library providing cross-browser, cross-site messaging/method invocation. http://easyxdm.net
easyXDM - easy Cross-Domain Messaging easyXDM is a Javascript library that enables you as a develope ...
- Dynamices CRM JS 类库 神器 XrmServiceToolkit - A Microsoft Dynamics CRM 2011 & CRM 2013 JavaScript Library
XrmServiceToolkit - A Microsoft Dynamics CRM 2011 & CRM 2013 JavaScript Library http://xrmservic ...
- Raphaël—JavaScript Library
Raphaël-JavaScript Library What is it? Raphaël is a small JavaScript library that should simplify yo ...
- a Javascript library for training Deep Learning models
w强化算法和数学,来迎接机器学习.神经网络. http://cs.stanford.edu/people/karpathy/convnetjs/ ConvNetJS is a Javascript l ...
- JavaScript 工具库:Cloudgamer JavaScript Library v0.1 发布
JavaScript 工具库:Cloudgamer JavaScript Library v0.1 发布 研究了一年多的js,也差不多写一个自己的js库了.我写这个不算框架,只是一个小型的js工具 ...
- jQuery JavaScript Library v3.2.1
/*! * jQuery JavaScript Library v3.2.1 * https://jquery.com/ * * Includes Sizzle.js * https://sizzle ...
- A JavaScript library for reading EXIF meta data from image files.
exif-js/exif-js: JavaScript library for reading EXIF image metadata https://github.com/exif-js/exif- ...
- javascript library
<!DOCTYPE HTML> <html lang="en-US"> <head> <meta charset="UTF-8& ...
- 转:Build Your First JavaScript Library
http://net.tutsplus.com/tutorials/javascript-ajax/build-your-first-javascript-library/ Step 1: Creat ...
- [React] 01 - Intro: javaScript library for building user interfaces
教学视频: http://www.php.cn/code/8217.html React 教程: http://www.runoob.com/react/react-tutorial.html 本篇是 ...
随机推荐
- Git常用命令汇总以及其它相关操作
--文件目录操作命令 1 mkdir * 创建一个空目录 *指目录名 2 pwd 显示当前目录的路径. 3 cat * 查看*文件内容 4 git rm * 删除**文件 --git初始化操作 1 g ...
- linux date格式化获取时间
转载请注明出处: 在编写shell脚本时,需要在shell脚本中格式化时间,特此整理下date命令相关参数的应用 root@controller1:~# date --help 用法:date [选项 ...
- 基于树莓派的OpenWrt系统打开蓝牙功能
在树莓派设备上的OpenWrt系统打开蓝牙功能 1. 安装必要的软件包 首先,你需要确保OpenWrt系统上安装了必要的蓝牙软件包.你可以通过OpenWrt的包管理器来安装它们.在OpenWrt系统上 ...
- 关于SpringBoot中事务回滚没有生效
在SpringBoot中,事务回滚可以用注解@Transactional标识. Spring声明式事务管理默认对非检查型异常和运行时异常进行事务回滚,而对检查型异常则不进行回滚操作. 1.非检查型异常 ...
- WordPress基础之基本SEO设置
基础内容,不会涉及过深,在谷歌SEO教程中会做详细的介绍,我这里只简单讲下. 1. SEO介绍 SEO,又名搜索引擎优化(Search Engine Optimization,缩写为SEO)是透过了解 ...
- 统计平台广告推送工具支持百度、51拉、CNZZ 用法详解
此软件用于伪造站长统计的搜素关键词,可以模拟百度.360.搜狗等搜索引擎来路 支持自定义刷词次数.多线程支持自定义线程数,速度更快 支持指定网址推广,带来更精确的网站IP来路 一键导入几十万个网站,支 ...
- 15、Spring之基于xml的声明式事务
阅读本文前,建议先阅读Spring之基于注解的声明式事务 15.1.环境搭建 创建名为spring_transaction_xml的新module,过程参考13.1节 15.1.1.配置打包方式和依赖 ...
- 【Dubbo】构建SpringBoot整合Dubbo的Demo
参考乐字节的Dubbo教程 https://www.bilibili.com/video/BV19L4y1n7YE Zookeeper单机部署 (Windows) 因为项目需要,这里我自己学习就采用Z ...
- 【Java-GUI】07 Swing01 入门案例
Swing是Java自己开发出的一套GUI组件,不同于AWT去调用操作系统的GUI 正是因为非系统平台的GUI,所以程序运行的要慢一些 涉及的设计模式:MVC模式 Model(组件对象状态) View ...
- 蔡磊公布渐冻症诊断报告 5月住进ICU一度考虑气切
原文地址: https://baijiahao.baidu.com/s?id=1801485780372006198