JavaScript Library – Embla Carousel
<h2> 前言</h2><p>2022 年 4 月,我写了一篇 Swiper 介绍。</p>
<p>Swiper 是当时前端最多人使用的 Slider 库,没有之一,一骑绝尘。</p>
<p>但是!时过境迁,这两年已经有一匹神秘的黑马悄悄杀上来了。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250405200326843-1037942657.png"></p>
<p>它就是本篇的主角 -- Embla Carousel。</p>
<p> </p>
<h2>Embla Carousel 的卖点</h2>
<p>Embla Carousel (简称 Embla) 何德何能?它凭什么在 Swiper 垄断的市场里能杀出一条血路🤔?</p>
<ol>
<li>
<p>lightweight</p>
<p>Embla 最大的卖点是 lightweight。</p>
<p>据说它非常非常轻,且性能非常好。</p>
<p>p.s.:具体多轻我不清楚,但肯定比 Swiper 轻很多,我就是嫌 Swiper 又重又慢,才在 research 替代方案时找到了 Embla。</p>
</li>
<li>
<p>framework integration</p>
<p>Embla 可以很容易得集成到各种前端框架,比如:React/Next.js,Svelte,Vue,Solid.js 等 (哎哟,不错哦,没有 Angular)。</p>
</li>
<li>
<p>Customization and independent of CSS</p>
<p>Embla 不掺和 styling CSS,它只负责 JS 逻辑,并且开放底层 API 接口,让使用者可以根据自己项目需求订做专属的 Carousel (a.k.a Slider)。</p>
</li>
</ol>
<p>以上三点无疑是近几年前端的趋势和刚需,雷军说过,站在风口上,猪也能飞,Embla 在这里做了最好的示范👍。</p>
<p> </p>
<h2>Swiper to Embla Carousel</h2>
<p>想从 Swiper 直接切换到 Embla Carousel 并不容易,因为 Embla 比 Swiper low level,我们需要自己补上许多上层的封装才行。</p>
<p>本篇我会把我使用到的 Swiper 范围 (这篇里的内容) 用 Embla 来实现一遍,大家可以感受一下它俩在使用上的区别。</p>
<p>注:本篇不会从 0 基础讲起,最好你使用过 Swiper 或者其它 Slider Library。</p>
<p> </p>
<h2>参考:</h2>
<p>官网 – Embla Carousel</p>
<p> </p>
<h2>安装</h2>
<div class="cnblogs_code">
<pre>yarn add embla-carousel</pre>
</div>
<h3>HTML</h3>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slider"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide-list"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/yangmi.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="yangmi"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/tifa.webp"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="tifa"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/nana.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="nana"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<p>HTML 结构和 Swiper 是一样的,slider > slide-list > slide 三层。</p>
<h3>Styles</h3>
<p>和 Swiper 不同,Embla 不涉及 CSS (注:first render 的时候不涉及 CSS 而已,交互时它肯定是要改 CSS 的)。</p>
<p>我们需要给 first render 的 CSS Styles,像这样</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(128, 0, 0, 1)">.slider </span>{<span style="color: rgba(255, 0, 0, 1)">
max-width</span>:<span style="color: rgba(0, 0, 255, 1)"> 512px</span>;<span style="color: rgba(255, 0, 0, 1)">
overflow</span>:<span style="color: rgba(0, 0, 255, 1)"> hidden</span>;<span style="color: rgba(255, 0, 0, 1)">
.slide-list {
display</span>:<span style="color: rgba(0, 0, 255, 1)"> flex</span>;<span style="color: rgba(255, 0, 0, 1)">
.slide {
flex-shrink</span>:<span style="color: rgba(0, 0, 255, 1)"> 0</span>;<span style="color: rgba(255, 0, 0, 1)">
width</span>:<span style="color: rgba(0, 0, 255, 1)"> 100%</span>;<span style="color: rgba(255, 0, 0, 1)">
img {
width</span>:<span style="color: rgba(0, 0, 255, 1)"> 100%</span>;<span style="color: rgba(255, 0, 0, 1)">
height</span>:<span style="color: rgba(0, 0, 255, 1)"> auto</span>;<span style="color: rgba(255, 0, 0, 1)">
aspect-ratio</span>:<span style="color: rgba(0, 0, 255, 1)"> 16 / 9</span>;<span style="color: rgba(255, 0, 0, 1)">
object-fit</span>:<span style="color: rgba(0, 0, 255, 1)"> cover</span>;
}<span style="color: rgba(128, 0, 0, 1)">
}
}
}</span></pre>
</div>
<p>目前的效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250405234554742-150365079.png"></p>
<p>只会看见一张图,因为另外两个 slide 被 overflow hide 起来了。</p>
<h3>Scripts</h3>
<div class="cnblogs_code">
<pre>import emblaCarousel from 'embla-carousel'<span style="color: rgba(0, 0, 0, 1)">;
const sliderElement </span>= document.querySelector<HTMLElement>('.slider')!<span style="color: rgba(0, 0, 0, 1)">;
const slider </span>=<span style="color: rgba(0, 0, 0, 1)"> emblaCarousel(sliderElement, {
container: </span>'.slide-list'<span style="color: rgba(0, 0, 0, 1)">,
slides: </span>'.slide'<span style="color: rgba(0, 0, 0, 1)">,
});</span></pre>
</div>
<p>emblaCarousel 是一个函数,调用这个函数,传入 slider element 就可以了。</p>
<p>container 如果是 slider 的 first child 那可以不需要指定。(我指定只是为了演示)</p>
<p>slides 如果是 container 的 children 也可以不需要指定。(我指定只是为了演示)</p>
<p>相关源码在 EmblaCarousel.ts</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406000006838-882342887.png"></p>
<p>到这里就已经可以跑起来了</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406000322529-1273586734.gif"> </p>
<p> </p>
<h2>Navigation</h2>
<p>参考:官网</p>
<p>Swiper 有 built-in 完整的 navigation,Embla 没有。</p>
<p>Embla 只提供了底层操作 slider 的 API,上层需要我们自己写。</p>
<h3>HTML</h3>
<div class="cnblogs_code"><img class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><img class="code_img_opened lazyload" style="display: none" data-src="http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif">
<div id="cnblogs_code_open_9b9c1a92-5777-4436-a2f4-1bdc31031458" class="cnblogs_code_hide">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slider-container"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slider"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide-list"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/yangmi1.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="yangmi"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/tifa.webp"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="tifa"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/nana.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="nana"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="navigation"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">button </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="prev"</span><span style="color: rgba(0, 0, 255, 1)">></span><span style="color: rgba(255, 0, 0, 1)">&lt;</span><span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">button </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="next"</span><span style="color: rgba(0, 0, 255, 1)">></span><span style="color: rgba(255, 0, 0, 1)">&gt;</span><span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>增加一个 container 还有 navigation buttons</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406102418363-191294777.png"></p>
<h3>Styles</h3>
<p>给一点 Styles 美观一下</p>
<div class="cnblogs_code"><img class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><img class="code_img_opened lazyload" style="display: none" data-src="http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif">
<div id="cnblogs_code_open_157c2a01-6dcf-46fc-8f83-317d74621d0f" class="cnblogs_code_hide">
<pre><span style="color: rgba(128, 0, 0, 1)">.slider-container </span>{<span style="color: rgba(255, 0, 0, 1)">
max-width</span>:<span style="color: rgba(0, 0, 255, 1)"> 512px</span>;<span style="color: rgba(255, 0, 0, 1)">
overflow</span>:<span style="color: rgba(0, 0, 255, 1)"> hidden</span>;<span style="color: rgba(255, 0, 0, 1)">
.slider {
width</span>:<span style="color: rgba(0, 0, 255, 1)"> 100%</span>;<span style="color: rgba(255, 0, 0, 1)">
.slide-list {
display</span>:<span style="color: rgba(0, 0, 255, 1)"> flex</span>;<span style="color: rgba(255, 0, 0, 1)">
.slide {
flex-shrink</span>:<span style="color: rgba(0, 0, 255, 1)"> 0</span>;<span style="color: rgba(255, 0, 0, 1)">
width</span>:<span style="color: rgba(0, 0, 255, 1)"> 100%</span>;<span style="color: rgba(255, 0, 0, 1)">
img {
width</span>:<span style="color: rgba(0, 0, 255, 1)"> 100%</span>;<span style="color: rgba(255, 0, 0, 1)">
height</span>:<span style="color: rgba(0, 0, 255, 1)"> auto</span>;<span style="color: rgba(255, 0, 0, 1)">
aspect-ratio</span>:<span style="color: rgba(0, 0, 255, 1)"> 16 / 9</span>;<span style="color: rgba(255, 0, 0, 1)">
object-fit</span>:<span style="color: rgba(0, 0, 255, 1)"> cover</span>;
}<span style="color: rgba(128, 0, 0, 1)">
}
}
}
.navigation </span>{<span style="color: rgba(255, 0, 0, 1)">
display</span>:<span style="color: rgba(0, 0, 255, 1)"> grid</span>;<span style="color: rgba(255, 0, 0, 1)">
grid-template-columns</span>:<span style="color: rgba(0, 0, 255, 1)"> 1fr 1fr</span>;<span style="color: rgba(255, 0, 0, 1)">
gap</span>:<span style="color: rgba(0, 0, 255, 1)"> 16px</span>;<span style="color: rgba(255, 0, 0, 1)">
.prev,
.next {
font-size</span>:<span style="color: rgba(0, 0, 255, 1)"> 32px</span>;<span style="color: rgba(255, 0, 0, 1)">
font-weight</span>:<span style="color: rgba(0, 0, 255, 1)"> 700</span>;<span style="color: rgba(255, 0, 0, 1)">
padding</span>:<span style="color: rgba(0, 0, 255, 1)"> 16px 24px</span>;<span style="color: rgba(255, 0, 0, 1)">
background-color</span>:<span style="color: rgba(0, 0, 255, 1)"> lightblue</span>;<span style="color: rgba(255, 0, 0, 1)">
color</span>:<span style="color: rgba(0, 0, 255, 1)"> blue</span>;<span style="color: rgba(255, 0, 0, 1)">
border-width</span>:<span style="color: rgba(0, 0, 255, 1)"> 0</span>;<span style="color: rgba(255, 0, 0, 1)">
cursor</span>:<span style="color: rgba(0, 0, 255, 1)"> pointer</span>;<span style="color: rgba(255, 0, 0, 1)">
& {
opacity</span>:<span style="color: rgba(0, 0, 255, 1)"> 0.4</span>;<span style="color: rgba(255, 0, 0, 1)">
cursor</span>:<span style="color: rgba(0, 0, 255, 1)"> unset</span>;<span style="color: rgba(255, 0, 0, 1)">
pointer-events</span>:<span style="color: rgba(0, 0, 255, 1)"> none</span>;
}<span style="color: rgba(128, 0, 0, 1)">
}
}
}</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>目前的效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406102702490-178760766.gif"></p>
<p>两个 button 还只是摆设,点击不会有任何效果。</p>
<h3>Scripts</h3>
<div class="cnblogs_code">
<pre>const sliderContainer = document.querySelector<HTMLElement>('.slider-container')!<span style="color: rgba(0, 0, 0, 1)">;
const sliderElement </span>= sliderContainer.querySelector<HTMLElement>('.slider')!<span style="color: rgba(0, 0, 0, 1)">;
const slider </span>=<span style="color: rgba(0, 0, 0, 1)"> emblaCarousel(sliderElement);
const prevBtn </span>= sliderContainer.querySelector<HTMLButtonElement>('.navigation .prev')!<span style="color: rgba(0, 0, 0, 1)">;
const nextBtn </span>= sliderContainer.querySelector<HTMLButtonElement>('.navigation .next')!<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (const button of ) {
const direction </span>= button === prevBtn ? 'Prev' : 'Next'<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 监听 prev next button click</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 当 user 点击后,调用 slider.scrollPrev() 或 scrollNext 方法来移动 slide</span>
button.addEventListener('click', () =><span style="color: rgba(0, 0, 0, 1)"> slider[`scroll${direction}`]());
}</span></pre>
</div>
<p>监听 prev 和 next button click,当 user 点击后,调用 slider.scrollPrev 或 scrollNext 来移动 slide。</p>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406105125652-1610868177.gif"></p>
<h4>disabled 体验</h4>
<p>navigation button 通常会有 disabled 体验。</p>
<p>当 user next 到最后一个 slide,我们需要 disable next button,让 user 知道已经到头了,不可以再继续 next。</p>
<p>相反,一开始在第一个 slide 时,我们需要 disable prev button。</p>
<p>首先,定义一个 handler</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> handleStateChange() {
prevBtn.disabled </span>= !<span style="color: rgba(0, 0, 0, 1)">slider.canScrollPrev();
nextBtn.disabled </span>= !<span style="color: rgba(0, 0, 0, 1)">slider.canScrollNext();
}</span></pre>
</div>
<p>透过 slider.canScrollPrev 或 canScrollNext 方法来 detect 当前 slider 是否可以 next or prev。</p>
<p>如果当前是在第一个 slide 那 canScrollPrev 将返回 false,如果当前是在最后一个 slide 那 canScrollNext 将返回 false。</p>
<p>注:这两个方法还会考量 slider 是否支持 looping,如果支持 looping 的话,那不管当前在哪一个 slide,它们一定返回 true。</p>
<p>接着我们要监听 slider 的 slide 变更,然后 apply handler。</p>
<div class="cnblogs_code">
<pre>slider.on('init'<span style="color: rgba(0, 0, 0, 1)">, handleStateChange);
slider.on(</span>'select'<span style="color: rgba(0, 0, 0, 1)">, handleStateChange);
slider.on(</span>'reInit', handleStateChange);</pre>
</div>
<p>有三个事件我们需要监听。</p>
<p>init 就是初始化完成,此时会是第一个 slide,所以 prev button 会 disabled。</p>
<p>select 是每一次 slide change,比如我们 click next / prev button,或者 swipe slide 的时候。</p>
<p>reInit 是当 slider 被修改 (e.g. add/remove slide, options change) 重置,或者 window / container / slides resize 的时候。 </p>
<h4>最终效果</h4>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406110710682-1894228618.gif"></p>
<p> </p>
<h2>looping 无限循环</h2>
<p>Embla 支持 looping,配置 options 就可以了。</p>
<div class="cnblogs_code">
<pre>const slider = emblaCarousel(sliderElement, { loop: <span style="color: rgba(0, 0, 255, 1)">true</span> });</pre>
</div>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406113614659-241602868.gif"></p>
<p>超级丝滑...这个 slide 的体验秒杀 Swiper。</p>
<p>上一 part 我们提到的 canScrollPrev 和 canScrollNext 在 looping 的情况下,一定返回 true。</p>
<p> </p>
<h2>Autoplay Plugin</h2>
<p>参考:官网</p>
<p>Autoplay 是自动 swipe 功能,体验是这样 -- delay 几秒后,自动 swipe to next slide,然后又 delay 又 swipe 以此类推。</p>
<p>Embla Carousel 可以透过 Autoplay Plugin 实现这个功能。</p>
<h3>安装 Plugin</h3>
<p>首先,需要另外安装 npm package</p>
<div class="cnblogs_code">
<pre>yarn add embla-carousel-autoplay</pre>
</div>
<h3>setup & options</h3>
<p>然后配置</p>
<div class="cnblogs_code">
<pre>import autoplay from 'embla-carousel-autoplay'<span style="color: rgba(0, 0, 0, 1)">;
const slider </span>= emblaCarousel(sliderElement, { loop: <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)"> }, [
autoplay(),
]);</span></pre>
</div>
<p>emblaCarousel 第三个参数是用来配置 plugins 的。</p>
<p>autoplay 是一个函数,调用它会返回 plugin 实例。</p>
<p>它有一些 options 可以调</p>
<ol>
<li>
<p>autoPlay({ delay: 1000 })</p>
<p>delay 多久后 auto swipe to next slide,默认是 4000 milliseconds。</p>
</li>
<li>
<p>playOnInit</p>
<p>是不是一开始就 start autoplay,默认是 true,如果我们想自己决定何时 start 那就 set to false,然后自己调用 API 让它 start,下面会教。</p>
</li>
<li>
<p>stopOnFocusIn</p>
<p>当 slider 内有任何 element 被 focused,autoplay 就会终止 (不是 pause,是 stop),默认是 true。</p>
</li>
<li>
<p>stopOnMouseEnter</p>
<p>mouse hover 到 slider,autoplay 就终止,默认是 false。</p>
</li>
<li>
<p>stopOnInteraction</p>
<p>interaction 指的是 slider 被 pointerdown,默认是 true。</p>
比如 swipe to next slide 这个交互就涉及到了 pointerdown,所以它会 stop autoplay。
<p>我个人觉得 swipe 和 pointer down 是不同的交互,pointer down 应该只是 reset timer,等 pointer up 以后 autoplay 依然继续,只有 swipe to next slide 才真的会 stop autoplay,这样体验会比较好,尤其是在手机。</p>
<p>注:如果我们是透过 click navigation button 来 next slide,这可不会 stop autoplay 哦,因为 button 是在 slider 外面,click button 不会触发 slider 的 pointerdown。</p>
</li>
<li>
<p>stopOnLastSnap</p>
<p>autoplay 到最后一个 slide 就 stop,默认是 false。</p>
如果没有设置 looping,在最后一个 slide 它依然会倒退回到第一个 slide。
<p>另外,它这个 stop 并不是完全 stop 死掉哦。</p>
<p>比如它在最后一个 slide stop了,但如果我们手动 swipe 去最后第二个 slide,这个 autoplay 还没死的哦,它会继续 auto swipe to 最后一个 slide 然后又 stop。嗯...这个设计还挺让人意外的。</p>
</li>
<li>
<p>默认 options</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406125430954-1549584363.png"></p>
</li>
</ol>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406132301775-2146755429.gif"></p>
<h3>Autoplay plugin 实例、方法、事件</h3>
<p>想操控 autoplay,我们可以从 slider 里面取出 autoplay plugin 实例,然后调用它的各种方法</p>
<div class="cnblogs_code">
<pre>const autoplayPlugin = slider.plugins().autoplay; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> autoplay plugin 实例</span></pre>
</div>
<p>或者先创建实例,再传给 embla slider 也行。</p>
<div class="cnblogs_code">
<pre>const autoplayPlugin = autoplay({ delay: 1000 }); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> autoplay plugin 实例</span>
const slider = emblaCarousel(sliderElement, { loop: <span style="color: rgba(0, 0, 255, 1)">false</span> }, ); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 传入 embla slider</span></pre>
</div>
<p>常用方法:</p>
<ol>
<li>
<p>autoplayPlugin.play();</p>
<p>启动 timer,到点 auto swipe to next slide。</p>
<p>如果我们配置 playOnInit: false,那 autoplay 就不会开启,我们需要手动调用 play 方法让它启动。</p>
<p>小心坑:</p>
EmblaCarousel.reInit 是一个重置 slider 的方法,细节下面会教,这里我们要知道它会导致 Autoplay 也 reInit,此时会依据回 playOnInit: false,autoplay 会 stop 掉。
<p>所以要特别留意,如果我们是 playOnInit: false 选择自己控制 play,那在 reInit 的时候也要决定是要继续 play 还是 stop。</p>
</li>
<li>
<p>stop()</p>
<p>stop 就是完全停掉 autoplay,timer 会马上被 clear 掉。</p>
<p>stop 了以后,可以用 play 让它恢复。</p>
</li>
<li>
<p>isPlaying()</p>
<p>返回 boolean,判断当前 autoplay 是否是启动状态。</p>
</li>
<li>
<p>reset()</p>
reset 的意思是重算 timer。(注:autoplay 在启动状态下才能 reset 哦)
<p>比如说,timer delay 4 秒后会 auto swipe,当前是第二秒,我们执行 reset,那 timer 就重算,要再等 4 秒后才会 auto swipe。</p>
</li>
<li>
<p>timeUntilNext()</p>
<p>距离下一次 auto swipe 的时间,它返回的是 millisecond。</p>
<p>比如说,timer delay 4 秒后会 auto swipe,当前是第三秒,我们执行 timeUntilNext 会得到 1000,代表 1 秒后会 auto swipe。</p>
</li>
<li>
<p>init()</p>
<p>所有 plugin 都必须实现 init 方法,这个是给 slider 初始化 plugin 时用的,我们一般不会直接调用它。</p>
</li>
<li>
<p>destroy()</p>
<p>所有 plugin 都必须实现 destroy 方法,这个是给 slider destroy 时用的,我们一般不会直接调用它。</p>
</li>
<li>
<p>name</p>
<p>每个 plugin 都有名字,autoplay plugin 的名字叫 'autoplay'。</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406173947949-1727507415.png"></p>
</li>
<li>
<p>options</p>
<p>这个 options 对象就是我们调用 autoplay 函数时传入的那个 options 对象。</p>
<p>特别要留意的地方是,这个对象不包含 default options。</p>
<p>比如说,传入的 options 对象是 { delay: 1000 },default options 是这样</p>
<img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406125430954-1549584363.png">
<p>拿 autoplayPlugin.options.stopOnInteraction 将得到 undefined,而不是 true,因为 stopOnInteraction 是定义在 default options 里,而不是在我们传入的 options 对象里。</p>
<p>我个人觉得它这样设计很不方便,应该要提供一个 merged options 给我们用才对。</p>
</li>
</ol>
<p>常用事件:</p>
<ol>
<li>autoplay:stop<br>
<div class="cnblogs_code">
<pre>const slider = emblaCarousel(sliderElement, { loop: <span style="color: rgba(0, 0, 255, 1)">false</span> }, );
const autoplayPlugin </span>=<span style="color: rgba(0, 0, 0, 1)"> slider.plugins().autoplay;
slider.on(`autoplay:stop`, () </span>=> console.log('stop')); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 监听事件</span>
window.setTimeout(() => autoplayPlugin.stop(), 1000);<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 触发 stop event</span></pre>
</div>
<p>事件监听是透过 slider.on 绑定的,事件名的规范是 autoplayPlugin.name + ':' + supportedEventName。</p>
<p>stop event 会在 autoplay stop 的时候触发,很多情况会导致 autoplay stop,比如 focus, hover, interaction, last slide, call stop method,不管什么情况,只要状态从 play to stop,它就会触发。</p>
</li>
<li>
<p>autoplay:play</p>
<p>状态从 stop to play 时触发。</p>
<p>注:play on init 不会触发,因为我们监听的比较晚,它的顺序是这样:</p>
<p>emblaCarousel 里面会调用 autoplayPlugin.init,init 里面会调用 startAutoplay,因为默认 playOnInit: true,startAutoplay 里面会 fire 'autoplay:play' event,</p>
<p>等 emblaCarousel 跑完,我们才调用 slider.on('autoplay:play'),此时 event 已经 fire 掉了。</p>
</li>
<li>
<p>autoplay:select</p>
<p>autoplay 开启后,会先 delay,等 timer 到点后,它会 auto swipe,这个 select event 就是在 auto swipe 的时候触发的。</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406175508419-719929577.png"></p>
</li>
<li>
<p>autoplay:timerset</p>
<p>autoplay 的流程是 delay > swipe > delay > swipe,每一次 delay 都是用 setTimeout 完成的,每一次 set 这个 timer 都会 fire 'autoplay:timerset' event。</p>
</li>
<li>
<p>autoplay:timerstopped</p>
<p>顾名思义,当 clearTimeout 时它就会 fire 'autoplay:timerstopped' event。(e.g. when autoplay stop 的时候,注:stop 会比 timerstopped 早一拍 fire)。</p>
</li>
</ol>
<h3>Change autoplay options?</h3>
<p>如果我们想修改 options 可以吗?</p>
<p>比如说,一开始配置 delay: 1000,2 秒后我想改去 delay: 4000。</p>
<p>我们先天真的试一试</p>
<div class="cnblogs_code">
<pre>const autoplayPlugin = autoplay({ delay: 1000<span style="color: rgba(0, 0, 0, 1)"> });
const slider </span>= emblaCarousel(sliderElement, { loop: <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)"> }, );
window.setTimeout(() </span>=><span style="color: rgba(0, 0, 0, 1)"> {
autoplayPlugin.options.delay </span>= 4000; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 2 秒后修改 delay 从 1 秒变 4 秒</span>
}, 2000);</pre>
</div>
<p>结果什么都没有改变,依然维持 delay 1 秒。</p>
<p>如果我们加一句 reset 呢?</p>
<div class="cnblogs_code">
<pre>autoplayPlugin.reset();</pre>
</div>
<p>还是不行。</p>
<p>那 stop > change delay > play 呢?</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 0, 1)">autoplayPlugin.stop();
autoplayPlugin.options.delay </span>= 4000; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 2 秒后修改 delay 从 1 秒变 4 秒</span>
autoplayPlugin.play();</pre>
</div>
<p>通通不行。</p>
<p>why? 看一看源码</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406190423808-249940787.png"></p>
<p>每当 setTimeout 的时候,它会从 delay 对象中拿出 options 的 4 秒。</p>
<p>这个 delay 对象是在 plugin.init 时制作好的,并且后续没有监听 options 变更,所以我们修改 options 它是不管的。</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406190510317-285358920.png"></p>
<p>因此,倘若我们想修改 options,唯一的方法就是手动调用 destory,然后再调用 init,让它整个 plugin 重启。</p>
<p>调用 destroy 不难,但调用 init 就有点困难了。</p>
<p>init 方法需要一个 optionsHandler 对象</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406190905743-740707092.png"></p>
<p>这个对象是透过一个内部函数 OptionsHandler 创建的 (在执行 emblaCarousel 函数的时候,注:Embla 的函数命名规范是 PascalCase,而不是我们常用的 camelCase,在翻阅源码的时候要看得懂哦)</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406191014466-754162237.png"></p>
<p>Embla 没有公开这个 OptionsHandler 函数,所以我们无法调用 plugin.init 方法。</p>
<h4>emblaCarousel.reInit (a.k.a reActive)</h4>
<p>我们只剩下最后一条路 -- emblaCarousel.reInit 方法</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406191744498-714409209.png"></p>
<p>这个方法会重启整个 slider,所有的 plugin 会被 destroy 然后再 init。</p>
<p>重启不会把 slide 跳回第一个,而是保持在当前位置。</p>
<p>如果我们传入新的 options 或 plugins,那它会 merge 之前的。</p>
<p>最后的实现代码是这样</p>
<div class="cnblogs_code">
<pre>window.setTimeout(() =><span style="color: rgba(0, 0, 0, 1)"> {
autoplayPlugin.options.delay </span>= 4000; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 2 秒后修改 delay 从 1 秒变 4 秒</span>
<span style="color: rgba(0, 0, 0, 1)">slider.reInit();
}, </span>2000);</pre>
</div>
<h3>当 Autoplay 遇上 Navigation</h3>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406181301328-741681226.gif"></p>
<p>看到吗,Autoplay 和 Navigation 打起来了。</p>
<p>虽然 stopOnInteraction: true,但 navigation 操作对 autoplay plugin 来说并不算是 interaction,只有 slider pointerdown 才算是 interaction,所以 navigation 操作不会 stop autoplay。</p>
<p>官方给的例子是这样解决的</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250406181731454-42957772.png"></p>
<p>监听 navigation button click,然后调用 reset 或 stop 来控制 autoplay。</p>
<p>这个做法可以达到效果,但有一点点扣管理分。</p>
<p>因为这样做会把 navigation 和 autoplay 的关系绑的很紧,而且倘若哪天再出现一个 pagination (另一种操作 slide 的方式),我们又得再写一套类似的逻辑给它,这样很繁琐。</p>
<p>我这里有一个 idea,我们可以监听 select change 事件,如果是 select change by not autoplay,那我们就 stop or reset autoplay。</p>
<p>这样就可以 cover navigation 和 pagination 甚至其它更多的 slide 操作。</p>
<p>代码大概是这样</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 监听每一次的 select</span>
slider.on('select', () =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 判断这一次的 select 是 trigger by autoplay or not</span>
let isAutoSelect = <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 因为是先触发 select 后触发 autoplay:select (同步)</span>
<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 所以我们可以利用这一点来判断 select 是 trigger by autoplay or not</span>
const callback = () =><span style="color: rgba(0, 0, 0, 1)"> {
isAutoSelect </span>= <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
slider.off(</span>'autoplay:select'<span style="color: rgba(0, 0, 0, 1)">, callback);
};
slider.on(</span>'autoplay:select'<span style="color: rgba(0, 0, 0, 1)">, callback);
queueMicrotask(() </span>=><span style="color: rgba(0, 0, 0, 1)"> {
slider.off(</span>'autoplay:select'<span style="color: rgba(0, 0, 0, 1)">, callback);
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 如果是 autoplay 那就 skip</span>
<span style="color: rgba(0, 0, 255, 1)">if</span> (isAutoSelect) <span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 如果 select trigger by navigation, pagination 或其它的,那我们就 stop or reset autoplay。</span>
(autoplayPlugin.options.stopOnInteraction ?? <span style="color: rgba(0, 0, 255, 1)">true</span>) ?<span style="color: rgba(0, 0, 0, 1)"> autoplayPlugin.stop() : autoplayPlugin.reset();
});
});</span></pre>
</div>
<p>RxJS 的写法是这样</p>
<div class="cnblogs_code">
<pre>fromEvent(slider, 'select'<span style="color: rgba(0, 0, 0, 1)">)
.pipe(
switchMap(() </span>=><span style="color: rgba(0, 0, 0, 1)">
merge(fromEvent(slider, </span>'autoplay:select').pipe(map(() => <span style="color: rgba(0, 0, 255, 1)">true</span>)), of(<span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">).pipe(observeOn(asapScheduler))).pipe(
take(</span>1<span style="color: rgba(0, 0, 0, 1)">),
),
),
filter(isAutoplaySelect </span>=> !<span style="color: rgba(0, 0, 0, 1)">isAutoplaySelect),
)
.subscribe(() </span>=><span style="color: rgba(0, 0, 0, 1)">
(autoplayPlugin.options.stopOnInteraction </span>?? <span style="color: rgba(0, 0, 255, 1)">true</span>) ?<span style="color: rgba(0, 0, 0, 1)"> autoplayPlugin.stop() : autoplayPlugin.reset(),
);</span></pre>
</div>
<p>提醒:</p>
<p>上面使用 autoplayPlugin.stop 或许并没有很恰当,因为</p>
<p><img alt="" data-src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250518210756037-172710569.png"></p>
<p>然后</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202505/641294-20250518210843345-1750429813.png"></p>
<p>所以,用 autoplayPlugin.destroy 可能更合适。(我觉得是因为它源码的实现方式是这样,所以我们才被迫得使用 destroy,总之不算是一个正规方案,算是一种 hacking way,谨用)</p>
<p> </p>
<h2>Slides per view & Slides to scroll</h2>
<p><img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407000250277-1600177919.png"></p>
<p>红框是 slide,绿框是 view (a.k.a scroll snap)。</p>
<p>slides per view 是指,一个 view 里面有多少个 slide。</p>
<p>我们上面提过的例子,都是一个 view 一个 slide,而这一个则是一个 view 两个 slides。</p>
<p>那要如何实现它呢?</p>
<p>Swiper 的 slides per view 主要是靠 JavaScript 来完成的 (包括布局)。</p>
<p>而 Embla 的 slides per view 则主要是靠 CSS Styles 来完成的 (交互依然是靠 JavaScript)。</p>
<h3>Styles</h3>
<p>我个人比较习惯用 grid 做 slider 布局</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407001236860-1642687010.png"></p>
<p>所以这里把之前的 flex 改成 grid (注:两种布局方式都可以达到最终效果,所以选哪个看个人喜好就好)。</p>
<p>每一个 column (也就是 slide) width 是 50%,那就代表一个 view 里会有两个 slides。</p>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407000311722-1683133523.png"></p>
<h4>add slide gap</h4>
<p>slide 与 slide 之前没有 gap,不好看,我们加 gap 进去</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(128, 0, 0, 1)">.slide-list </span>{<span style="color: rgba(255, 0, 0, 1)">
--slides-per-view</span>:<span style="color: rgba(0, 0, 255, 1)"> 2</span>;<span style="color: rgba(255, 0, 0, 1)">
--slide-gap</span>:<span style="color: rgba(0, 0, 255, 1)"> 16px</span>;<span style="color: rgba(255, 0, 0, 1)">
display</span>:<span style="color: rgba(0, 0, 255, 1)"> grid</span>;<span style="color: rgba(255, 0, 0, 1)">
grid-auto-flow</span>:<span style="color: rgba(0, 0, 255, 1)"> column</span>;<span style="color: rgba(255, 0, 0, 1)">
grid-auto-columns</span>:<span style="color: rgba(0, 0, 255, 1)"> calc((100% - (var(--slide-gap) * (var(--slides-per-view) - 1))) / var(--slides-per-view))</span>;<span style="color: rgba(255, 0, 0, 1)">
gap</span>:<span style="color: rgba(0, 0, 255, 1)"> var(--slide-gap)</span>;
}</pre>
</div>
<p>直接加 gap 会影响到 slide width,所以我们需要写一些简单的 calculation。</p>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407002642845-1135189412.png"></p>
<p>排版虽然是对的,但交互会有一些体验问题</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407024712500-570068282.gif"></p>
<p>当鼠标在 gap 局域 swipe 时,它会不小心 select 到 slide。</p>
<p>这是因为 gap 区域是 div.slide-list 的 area,它是 slide 的 parent 了。</p>
<p>我们可以参考官网的实现方式来解决这个问题,它的 gap 是用 slide padding-left 做出来的。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(128, 0, 0, 1)">.slide-list </span>{<span style="color: rgba(255, 0, 0, 1)">
--slides-per-view</span>:<span style="color: rgba(0, 0, 255, 1)"> 2</span>;<span style="color: rgba(255, 0, 0, 1)">
--slide-gap</span>:<span style="color: rgba(0, 0, 255, 1)"> 16px</span>;<span style="color: rgba(255, 0, 0, 1)">
display</span>:<span style="color: rgba(0, 0, 255, 1)"> grid</span>;<span style="color: rgba(255, 0, 0, 1)">
grid-auto-flow</span>:<span style="color: rgba(0, 0, 255, 1)"> column</span>;<span style="color: rgba(255, 0, 0, 1)">
grid-auto-columns</span>:<span style="color: rgba(0, 0, 255, 1)"> calc(100% / var(--slides-per-view))</span>;<span style="color: rgba(255, 0, 0, 1)">
margin-left</span>:<span style="color: rgba(0, 0, 255, 1)"> calc(-1 * var(--slide-gap))</span>;<span style="color: rgba(255, 0, 0, 1)">
.slide {
padding-left</span>:<span style="color: rgba(0, 0, 255, 1)"> var(--slide-gap)</span>;
}<span style="color: rgba(128, 0, 0, 1)">
}</span></pre>
</div>
<p>首先给每个 slide 一个 padding-left 作为 slide gap。</p>
<p>第二步是给 .slide-list 一个 negative margin-left,目的是把第一个 slide 的 padding-left 吃掉。</p>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407025650444-967245399.gif"></p>
<h4>小心坑</h4>
<p>需要特别留意的一点是 slide-list 和 slide 的 bounding client rect 和 width。</p>
<p><img alt="" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250418143319987-1286528850.png"></p>
<p>绿色线条是我们直觉中的 rect 和 width。</p>
<p>但由于 slide-list 加了 negative margin-left,slide 加了 padding-left 所以 bounding client rect 和 width 都被影响了。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250418143355332-884287010.png"></p>
<p>红色才是它真实的 bounding client rect 和 width。</p>
<p>slide-list 和 slide 的 width 都大了,slide-list 和 slide 的 rect.left 都少了。</p>
<p>所以如果有使用到 bounding client rect 或 width 一定要注意,它是不符合我们直觉的。</p>
<h4>计算 slide width</h4>
<p>这里给一个计算 slide width 的例子</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202509/641294-20250910152446928-693586796.png"></p>
<p>slider 的 width 是 512px</p>
<p>slider-list 的 gap 是 12px(也就是 slider-list 有 margin-left: -12px 和每个 slide 的 padding-left: 12px)</p>
<p>因此,slider-list 的 width 是 512px + 12px = 524px(因为 margin-left: -12px 所以 width 变大了)。</p>
<p>要求 slide per view 显示 1.1 个 slide:</p>
<p>slide width 的 formula 是 100% / 1.1</p>
<p>100% 指的是 slide-list 的 width,也就是 524px。</p>
<p>524px / 1.1 = 476.36px</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202509/641294-20250910153254691-863690886.png"></p>
<p>这个是有包含 padding-left 的哦,剪掉 padding-left 就是 476.36px - 12px = 464.36px。</p>
<h3>slides per group</h3>
<p>设置 slides per view = 2 之后,我们去 swipe 它会发现体验怪怪的。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407015636605-162480180.gif"></p>
<p>swipe 一下只移动了半个 slide。原因是 alignment 跑掉了。</p>
<h4>EmblaOptions.align</h4>
<p>slider 默认 align 是 'center',我们 swipe 多几下就能看出这个 align: 'center' 的含义了</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407020817931-255649218.gif"></p>
<p>center 会是一个完整的 slide,然后左右 slide 各占 50% width,这就是 align: center 的意思。</p>
<p>我们把 align 换成 'start' 看看效果</p>
<div class="cnblogs_code">
<pre>const slider = emblaCarousel(sliderElement, { align: 'start' });</pre>
</div>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407021419344-1035073067.gif"></p>
<p>yes,这是我们比较熟悉的 swipe 体验。</p>
<h4>EmblaOptions.slidesToScroll</h4>
<p>Swiper 有一个感念叫 slides per group,意思是当我们 swipe 的时候,它会移动多少个 slide。</p>
<p>比如说,在一个 view 一个 slide 的情况下,swipe 通常就是一个 slide。</p>
<p>而在一个 view 两个 slides 的情况下,swipe 一次我们可以选择移动一个 slide 或者移动两个 slides。</p>
<p>上面是一个 swipe 一个 slide 的体验,下面我们看看一个 swipe 两个 slides 的体验。</p>
<div class="cnblogs_code">
<pre>const slider = emblaCarousel(sliderElement, { align: 'start', slidesToScroll: 2 });</pre>
</div>
<p>slidesToScroll: 2 表示 scrollNext 会直接跳两个 slides,而不是默认的一个。</p>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407022006752-206566684.gif"></p>
<p>另外,slidesToScroll 还支持 'auto' 值。</p>
<div class="cnblogs_code">
<pre>const slider = emblaCarousel(sliderElement, { align: 'start', slidesToScroll: 'auto' });</pre>
</div>
<p>'auto' 的意思就是依据 slides per view。</p>
<p>比如 slides per view 是 3 的话,那 slidesToScroll 也自动会是 3。</p>
<p> </p>
<h2>SlidesInView</h2>
<p>EmblaCarousel.slidesInView 是一个方法,它会返回当前有哪些 slides 在 view 里面 (这个 view 指的是 slider 可见区域)。</p>
<p>我们看一个官方的例子</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250408102628379-388110910.png"></p>
<p>一个 view 一个 slide,目前显示的是一号 slide,也就是第 0 个,index 0。</p>
<div class="cnblogs_code">
<pre>emblaApi.on('slidesInView', () => console.log(emblaApi.slidesInView())); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> </span></pre>
</div>
<p>slidesInView 返回的是 ,意思是说,index 0 和 1 slide 目前显示在 view 里。</p>
<p>呃...这不对啊🤔明明显示的只有 index 0 啊...</p>
<p>Github Issue – slidesInView returns one too many slides</p>
<p>作者给出了解答</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250408110204795-1476624800.png"></p>
<p>slidesInView 是依靠 IntersectionObserver 来计算的,源码在 SlidesInView.ts。</p>
<p>我们自己用 IntersectionObserver 测一下看看</p>
<div class="cnblogs_code">
<pre>window.setTimeout(() =><span style="color: rgba(0, 0, 0, 1)"> {
const io </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> IntersectionObserver(entries =><span style="color: rgba(0, 0, 0, 1)"> {
console.log(entries.map(e </span>=> e.isIntersecting)); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> </span>
<span style="color: rgba(0, 0, 0, 1)">});
const slides </span>= Array.from(viewportNode.querySelectorAll('.embla__slide'<span style="color: rgba(0, 0, 0, 1)">));
slides.forEach(slide </span>=><span style="color: rgba(0, 0, 0, 1)"> io.observe(slide));
}, </span>2000); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> delay 是为了等它 render 完</span></pre>
</div>
<p>可以看到 5 个 slides 里,头两个 (index 0 和 1) isIntersecting 真的是 true。</p>
<p>这种诡异的现象通常是微差或者 "刚刚好动到要不要算" 造成的,作者给的解方是 -- 设置 threshold。</p>
<div class="cnblogs_code">
<pre>const emblaApi = EmblaCarousel(viewportNode, { inViewThreshold: 0.01 });</pre>
</div>
<p>不懂原理想明白的读友可以看这篇。</p>
<p>另外,上面有提到,用 padding-left 和 negative margin-left 做 slide gap 会对 bounding client rect 有影响,这同样会影响到 slidesInView 的计算。</p>
<p>为此我提了一个 issue,不过我觉得作者未必会想解决这类问题。</p>
<p>我自己的解法是,添加一个 .slide-content 作为 slide 真正的内容区域,这样就不包含 padding-left 了,然后自己写一个 intersectionObserver 监听弄 slidesInView。</p>
<p>但,我后来又发现了另一个问题 -- 慢。</p>
<p><img alt="gif" data-src="https://img2024.cnblogs.com/blog/641294/202511/641294-20251111235748065-95535551.gif"></p>
<p>embla 是它的,stg 是我的。</p>
<p>可以看到,最终 stg 是正确的。</p>
<p>不过整体反应很慢,因为 swipe 有余力,需要等到余力耗尽,它才准。</p>
<p>我的结论是,用 IntersectionObserver 计算 slidesInView 虽然准,但要等它静下来有点久,未必是一个最佳方案。</p>
<h3>slidesInView event</h3>
<div class="cnblogs_code">
<pre>slider.on('slidesInView', () => console.log(slider.slidesInView()));</pre>
</div>
<p>每当 slidesInView 变更就会触发。</p>
<p>两个知识点:</p>
<ol>
<li>
<p>first render 它也会触发,但它会慢半拍,因为它底层是靠 intersectionObserver 实现的,而 IO 的触发时机是在 reflow 之后。</p>
因此用户会先看到 first render 的画面,然后 slidesInView 才触发。这时你才依据它修改 styles,其实慢半拍了。
<p>要解决这个问题,就必须自己用 getBoundingClient 计算。</p>
</li>
<li>
<p>slider 从 active 切换到 inactive,slidesInView 会变成 empty array,但是不会触发 event。</p>
<p>从 inactive 切换到 active,就会触发 event。</p>
<p>简单说,只要 slider 是 inactive 的,那它自然是 empty array 也不会触发 event,清空安静就对了。</p>
</li>
</ol>
<p> </p>
<h2>Text Selection</h2>
<p>slide 里面的 text 是很难被 select 的。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407100251583-1187816634.gif"></p>
<p>double click select text 可以,但 drag select 就不行。</p>
<p>因为 drag 会移动 slide,这和 select text 交互是打架的。</p>
<p>Swiper 可以透过 class swiper-no-swiping 解决这种冲突,很遗憾 Embla 没有支持。</p>
<p>相关 Issue:</p>
<p>Stack Overflow – Embla Carousel - select inner text</p>
<p>三个思路,</p>
<p>第一,给 slider 添加 cursor styles</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(128, 0, 0, 1)">.slider </span>{<span style="color: rgba(255, 0, 0, 1)">
cursor</span>:<span style="color: rgba(0, 0, 255, 1)"> grab</span>;<span style="color: rgba(255, 0, 0, 1)">
user-select</span>:<span style="color: rgba(0, 0, 255, 1)"> none</span>;
}</pre>
</div>
<p>告知 user 无法 select text。</p>
<p>第二,配置 watchDrag: false</p>
<div class="cnblogs_code">
<pre>const slider = emblaCarousel(sliderElement, { watchDrag: <span style="color: rgba(0, 0, 255, 1)">false</span> });</pre>
</div>
<p>直接 disable 掉 drag 的功能,user 只能透过其它方式移动 slide,比如 navigation 或 pagination。</p>
<p>第三,模拟 swiper-no-swiping</p>
<p>添加 'drag-disabled' class 到 slide 里的 heading element</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">h1 </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="drag-disabled"</span><span style="color: rgba(0, 0, 255, 1)">></span>Yang Mi<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">h1</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<p>代表这个 element 不可以 drag。</p>
<p>接着一样是用 watchDrag,但这一次是提供一个判断函数</p>
<div class="cnblogs_code">
<pre>const slider =<span style="color: rgba(0, 0, 0, 1)"> emblaCarousel(sliderElement, {
watchDrag: (_slider, event) </span>=><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> ((event.target as HTMLElement).classList.contains('drag-disabled'<span style="color: rgba(0, 0, 0, 1)">)) {
</span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">;
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">;
},
});</span></pre>
</div>
<p>return false 或 undefined 就是阻止移动 slide,return true 则是允许移动 slide,源码长这样</p>
<p><img alt="" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250416013740725-208327915.png"></p>
<p>题外话:watchDrag 还可以用来实现 nested slider 哦。</p>
<p>另外,补上一个烂招,监听 slider mousedown 和 touchstart 事件,然后 stopPropagation 阻止 Embla drag。</p>
<div class="cnblogs_code">
<pre>const slider =<span style="color: rgba(0, 0, 0, 1)"> emblaCarousel(sliderElement);
</span><span style="color: rgba(0, 0, 255, 1)">for</span> (const eventName of ['touchstart', 'mousedown'<span style="color: rgba(0, 0, 0, 1)">]) {
sliderElement.addEventListener(
eventName,
e </span>=><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">if</span> ((e.target as HTMLElement).classList.contains('drag-disabled'<span style="color: rgba(0, 0, 0, 1)">)) {
e.stopPropagation();
}
},
{ capture: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)"> },
);
}</span></pre>
</div>
<p>这里必须赶在 Embla 的前面,所以需要使用 capture: true。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407162542664-91920001.png"></p>
<p>Embla 会 binding 各种事件到 node (这个 node 就是 slider element) 上,我们赶在它之前 stopPropagation 就可以阻止掉它们了。</p>
<p>效果</p>
<p><img alt="gif" data-src="https://img2024.cnblogs.com/blog/641294/202509/641294-20250910133117375-1617912088.gif"></p>
<p>Yang Mi 可以 select text 了。</p>
<p> </p>
<h2>Breakpoints</h2>
<p>要在不同的 viewport size 呈现不同的 slide 布局或 options,我们需要配置 breakpoints。</p>
<h3>CSS media query</h3>
<p>slides 布局通常只需要定义 CSS media query 就可以了。</p>
<p>Embla 本身会监听 window resize,然后 getComputedStyle 拿到当前的 Styles 做相应的处理。</p>
<p>比如</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407183146639-1844187054.png"></p>
<p>slides per view 默认是 1,slide gap 默认是 0px。</p>
<p>在 viewport width 1920px 时,slides per view 变成 2,slide gap 变成 16px。</p>
<p>我们只需要 CSS 就够了,JavaScript 不需要写。</p>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407183555660-15200390.gif"></p>
<h3>breakpoints options</h3>
<p>需求:默认要 looping,大过 1024px 不要 looping,大过 1920 又要 looping。</p>
<div class="cnblogs_code">
<pre>const slider =<span style="color: rgba(0, 0, 0, 1)"> emblaCarousel(sliderElement, {
align: </span>'start'<span style="color: rgba(0, 0, 0, 1)">,
slidesToScroll: </span>'auto'<span style="color: rgba(0, 0, 0, 1)">,
loop: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
breakpoints: {
</span>'(min-width: 1024px)'<span style="color: rgba(0, 0, 0, 1)">: {
loop: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
},
</span>'(min-width: 1920px)'<span style="color: rgba(0, 0, 0, 1)">: {
loop: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
},
},
});</span></pre>
</div>
<p>上面有三个 loop options 定义,它的覆盖逻辑 (Object.assign) 是从下到上 (下面盖上面,下面赢),所以通常我们定义 media query 是从小(上)到大(下)。</p>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407190149358-1314121885.gif"></p>
<p>另外,Embla 有一点比 Swiper 强,Embla 可以依据 braekpoints 配置 acitve or inactive。(我印象中,Swiper 是无法 inactive 的)</p>
<div class="cnblogs_code">
<pre>const slider =<span style="color: rgba(0, 0, 0, 1)"> emblaCarousel(sliderElement, {
align: </span>'start'<span style="color: rgba(0, 0, 0, 1)">,
slidesToScroll: </span>'auto'<span style="color: rgba(0, 0, 0, 1)">,
breakpoints: {
</span>'(min-width: 1024px)'<span style="color: rgba(0, 0, 0, 1)">: {
active: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
},
</span>'(min-width: 1920px)'<span style="color: rgba(0, 0, 0, 1)">: {
active: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
},
},
});</span></pre>
</div>
<p>直接改 active 属性就可以了。</p>
<h4>get current options on reInit</h4>
<p>breakpoint change 导致 options change,Embla 底层会使用 reInit 方法重置,reInit 事件会触发。</p>
<p>另外,我们可以监听 reInit 事件,并获取当前的 options 做逻辑</p>
<div class="cnblogs_code">
<pre>slider.on('reInit', () => console.log('reInit', slider.internalEngine().options));</pre>
</div>
<p>这个 options 是完整 (merged & breakpoints 过滤过) 的 options,而不是我们传入的 partial options。</p>
<p> </p>
<h2>Pagination</h2>
<p>Swiper 有 bulit-in 的 pagination,也支持 full custom pagination。</p>
<p>Embla 没有 built-in 的 pagination,我们需要像 navigation 那样,使用 Embla 底层 API,自己写上层实现代码。</p>
<h3>实现要点</h3>
<p>paignation 长这样</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250408202532268-741240510.gif"></p>
<p>下面一粒一粒的叫 bullet。</p>
<p>三个要点:</p>
<ol>
<li>
<p>点击 bullet 会移动 slide</p>
</li>
<li>
<p>active bullet</p>
<p>active bullet 就是那颗比较亮的 bullet,slide 在第几个,active bullet 就要在第几个。</p>
</li>
<li>
<p>bullet 的数量</p>
上面的例子有 6 个 slides (6 张图),一个 view 显示一个 slide,bullet 有 6 粒。<br>
<p>下面这个例子一样是 6 个 slides,但一个 view 显示了两个 slides,bullet 变成了 3 粒。</p>
<img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250408203222514-1926556897.png">
<p>所以,bullet 的数量是看有多少个 view 决定的。</p>
</li>
</ol>
<h3>具体实现</h3>
<h4>HTML</h4>
<p>首先是 HTML</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250408204045488-1192846187.png"></p>
<div class="cnblogs_code"><img class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><img class="code_img_opened lazyload" style="display: none" data-src="http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif">
<div id="cnblogs_code_open_357adf86-73a8-4938-a7f3-4c5d8f9223f6" class="cnblogs_code_hide">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="pagination"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">template</span><span style="color: rgba(0, 0, 255, 1)">><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="bullet"</span><span style="color: rgba(0, 0, 255, 1)">></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></</span><span style="color: rgba(128, 0, 0, 1)">template</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>bullet 的数量依据 view count,我们用 JavaScript 动态输出会比较容易管理。(用 CSS 只能稿 display: none 会比较乱)</p>
<p>HTML 定义一个 bullet template 就好。</p>
<h4>Styles</h4>
<p>没什么特别的,就是美观一下而已</p>
<div class="cnblogs_code"><img class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><img class="code_img_opened lazyload" style="display: none" data-src="http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif">
<div id="cnblogs_code_open_f532341c-b047-43d2-a624-b998ff6c0fde" class="cnblogs_code_hide">
<pre><span style="color: rgba(128, 0, 0, 1)">.pagination </span>{<span style="color: rgba(255, 0, 0, 1)">
--bullet-size</span>:<span style="color: rgba(0, 0, 255, 1)"> 24px</span>;<span style="color: rgba(255, 0, 0, 1)">
margin-top</span>:<span style="color: rgba(0, 0, 255, 1)"> 16px</span>;<span style="color: rgba(255, 0, 0, 1)">
display</span>:<span style="color: rgba(0, 0, 255, 1)"> flex</span>;<span style="color: rgba(255, 0, 0, 1)">
justify-content</span>:<span style="color: rgba(0, 0, 255, 1)"> center</span>;<span style="color: rgba(255, 0, 0, 1)">
gap</span>:<span style="color: rgba(0, 0, 255, 1)"> 16px</span>;<span style="color: rgba(255, 0, 0, 1)">
height</span>:<span style="color: rgba(0, 0, 255, 1)"> var(--bullet-size)</span>; <span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)"> 提早给空间 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(255, 0, 0, 1)">
.bullet {
width</span>:<span style="color: rgba(0, 0, 255, 1)"> var(--bullet-size)</span>;<span style="color: rgba(255, 0, 0, 1)">
height</span>:<span style="color: rgba(0, 0, 255, 1)"> var(--bullet-size)</span>;<span style="color: rgba(255, 0, 0, 1)">
border-radius</span>:<span style="color: rgba(0, 0, 255, 1)"> 999px</span>;<span style="color: rgba(255, 0, 0, 1)">
border</span>:<span style="color: rgba(0, 0, 255, 1)"> 1px solid blue</span>;<span style="color: rgba(255, 0, 0, 1)">
cursor</span>:<span style="color: rgba(0, 0, 255, 1)"> pointer</span>;<span style="color: rgba(255, 0, 0, 1)">
&.active {
background-color</span>:<span style="color: rgba(0, 0, 255, 1)"> lightblue</span>;
}<span style="color: rgba(128, 0, 0, 1)">
}
}</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<h4>Scripts</h4>
<div class="cnblogs_code">
<pre>const pagination = document.querySelector<HTMLElement>('.pagination')!<span style="color: rgba(0, 0, 0, 1)">;
const bulletTemplate </span>= pagination.querySelector('template')!<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> rebuildPagination() {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 当前在第几个 view</span>
const currentViewIndex =<span style="color: rgba(0, 0, 0, 1)"> slider.selectedScrollSnap();
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 总共有几个 view</span>
const viewCount =<span style="color: rgba(0, 0, 0, 1)"> slider.scrollSnapList().length;
const bulletsFrag </span>=<span style="color: rgba(0, 0, 0, 1)"> document.createDocumentFragment();
</span><span style="color: rgba(0, 0, 255, 1)">for</span> (let index = 0; index < viewCount; index++<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 创建 bullet element based on view count</span>
const bulletTemplateFrag = bulletTemplate.content.cloneNode(<span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) as DocumentFragment;
const bullet </span>= bulletTemplateFrag.firstElementChild!<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> add click event to bullet</span>
bullet.addEventListener('click', () =><span style="color: rgba(0, 0, 0, 1)"> slider.scrollTo(index));
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (index ===<span style="color: rgba(0, 0, 0, 1)"> currentViewIndex) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> set active class to bullet</span>
bullet.classList.add('active'<span style="color: rgba(0, 0, 0, 1)">);
}
bulletsFrag.appendChild(bullet);
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> clear and re-append bullets</span>
pagination.innerHTML = ''<span style="color: rgba(0, 0, 0, 1)">;
pagination.appendChild(bulletsFrag);
}
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 三种情况有可能导致 bullet 数量或 active 变更,当变更时我们就 rebuild pagination</span>
slider.on('select'<span style="color: rgba(0, 0, 0, 1)">, rebuildPagination);
slider.on(</span>'init'<span style="color: rgba(0, 0, 0, 1)">, rebuildPagination);
slider.on(</span>'reInit', rebuildPagination);</pre>
</div>
<p>使用到了两个 Embla API</p>
<ol>
<li>
<p>EmblaCarousel.selectedScrollSnap 方法</p>
<p>scroll snap 就是 view 的别名 (alias)。</p>
<p>selectedScrollSnap 会返回当前 view index (当前在第几个 view)。</p>
</li>
<li>
<p>EmblaCarousel.scrollSnapList 方法</p>
<p>它会返回一个 array,长这样 [-0, 0.2, 0.4, 0.6, 0.8, 1] 或着这样 [-0, 0.5, 1]。</p>
<p>里面的号码不重要,array.length 代表 view 的数量,也就是我们要的 bullet 数量。</p>
</li>
</ol>
<p>最终效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250408202532268-741240510.gif"></p>
<h3>Dynamic bullets</h3>
<p>当 bullets 太多的时候会不好看,我们可以做成 dynamic bullets 限制它的数量。</p>
<p>长这样</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409012135281-1480196394.gif"></p>
<p>附上完整代码,就不解释了。</p>
<p>HTML</p>
<div class="cnblogs_code"><img class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><img class="code_img_opened lazyload" style="display: none" data-src="http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif">
<div id="cnblogs_code_open_3bb0e46b-7326-4245-ba0b-dc4bd795480d" class="cnblogs_code_hide">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slider-container"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slider"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide-list"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/yangmi1.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="yangmi1"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/tifa.webp"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="tifa"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/nana.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="nana"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/yangmi2.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="yangmi2"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/yangmi3.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="yangmi3"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/dilireba.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="dilireba"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/yangmi1.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="yangmi1"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/tifa.webp"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="tifa"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/nana.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="nana"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/yangmi2.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="yangmi2"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/yangmi3.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="yangmi3"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/dilireba.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="dilireba"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="pagination"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">template</span><span style="color: rgba(0, 0, 255, 1)">><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="bullet"</span><span style="color: rgba(0, 0, 255, 1)">></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></</span><span style="color: rgba(128, 0, 0, 1)">template</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="bullet-list"</span><span style="color: rgba(0, 0, 255, 1)">></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>Styles</p>
<div class="cnblogs_code"><img class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><img class="code_img_opened lazyload" style="display: none" data-src="http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif">
<div id="cnblogs_code_open_a857e854-ffe2-4b4c-8f44-2d22fa3502da" class="cnblogs_code_hide">
<pre><span style="color: rgba(128, 0, 0, 1)">.slider-container </span>{<span style="color: rgba(255, 0, 0, 1)">
max-width</span>:<span style="color: rgba(0, 0, 255, 1)"> 512px</span>;<span style="color: rgba(255, 0, 0, 1)">
overflow</span>:<span style="color: rgba(0, 0, 255, 1)"> hidden</span>;<span style="color: rgba(255, 0, 0, 1)">
.slider {
width</span>:<span style="color: rgba(0, 0, 255, 1)"> 100%</span>;<span style="color: rgba(255, 0, 0, 1)">
.slide-list {
--slides-per-view</span>:<span style="color: rgba(0, 0, 255, 1)"> 1</span>;<span style="color: rgba(255, 0, 0, 1)">
--slide-gap</span>:<span style="color: rgba(0, 0, 255, 1)"> 0px</span>;<span style="color: rgba(255, 0, 0, 1)">
display</span>:<span style="color: rgba(0, 0, 255, 1)"> grid</span>;<span style="color: rgba(255, 0, 0, 1)">
grid-auto-flow</span>:<span style="color: rgba(0, 0, 255, 1)"> column</span>;<span style="color: rgba(255, 0, 0, 1)">
grid-auto-columns</span>:<span style="color: rgba(0, 0, 255, 1)"> calc(100% / var(--slides-per-view))</span>;<span style="color: rgba(255, 0, 0, 1)">
margin-left</span>:<span style="color: rgba(0, 0, 255, 1)"> calc(-1 * var(--slide-gap))</span>;<span style="color: rgba(255, 0, 0, 1)">
.slide {
padding-left</span>:<span style="color: rgba(0, 0, 255, 1)"> var(--slide-gap)</span>;<span style="color: rgba(255, 0, 0, 1)">
img {
display</span>:<span style="color: rgba(0, 0, 255, 1)"> block</span>;<span style="color: rgba(255, 0, 0, 1)">
width</span>:<span style="color: rgba(0, 0, 255, 1)"> 100%</span>;<span style="color: rgba(255, 0, 0, 1)">
height</span>:<span style="color: rgba(0, 0, 255, 1)"> auto</span>;<span style="color: rgba(255, 0, 0, 1)">
aspect-ratio</span>:<span style="color: rgba(0, 0, 255, 1)"> 16 / 9</span>;<span style="color: rgba(255, 0, 0, 1)">
object-fit</span>:<span style="color: rgba(0, 0, 255, 1)"> cover</span>;
}<span style="color: rgba(128, 0, 0, 1)">
}
@media (width >= 768px) </span>{<span style="color: rgba(255, 0, 0, 1)">
--slides-per-view</span>:<span style="color: rgba(0, 0, 255, 1)"> 2</span>;<span style="color: rgba(255, 0, 0, 1)">
--slide-gap</span>:<span style="color: rgba(0, 0, 255, 1)"> 16px</span>;
}<span style="color: rgba(128, 0, 0, 1)">
}
}
.pagination </span>{<span style="color: rgba(255, 0, 0, 1)">
margin-top</span>:<span style="color: rgba(0, 0, 255, 1)"> 16px</span>;<span style="color: rgba(255, 0, 0, 1)">
--max-bullet-count</span>:<span style="color: rgba(0, 0, 255, 1)"> 5</span>;<span style="color: rgba(255, 0, 0, 1)">
--bullet-size</span>:<span style="color: rgba(0, 0, 255, 1)"> 24px</span>;<span style="color: rgba(255, 0, 0, 1)">
--bullet-gap</span>:<span style="color: rgba(0, 0, 255, 1)"> 16px</span>;<span style="color: rgba(255, 0, 0, 1)">
max-width</span>:<span style="color: rgba(0, 0, 255, 1)"> calc(
(var(--max-bullet-count) * var(--bullet-size)) + ((var(--max-bullet-count) - 1) * var(--bullet-gap))
)</span>;<span style="color: rgba(255, 0, 0, 1)">
margin-inline</span>:<span style="color: rgba(0, 0, 255, 1)"> auto</span>;<span style="color: rgba(255, 0, 0, 1)">
overflow</span>:<span style="color: rgba(0, 0, 255, 1)"> hidden</span>;<span style="color: rgba(255, 0, 0, 1)">
.bullet-list {
--active-index</span>:<span style="color: rgba(0, 0, 255, 1)"> 0</span>;<span style="color: rgba(255, 0, 0, 1)"> // JS will fill in
margin-left</span>:<span style="color: rgba(0, 0, 255, 1)"> calc(50% - (var(--bullet-size) / 2))</span>;<span style="color: rgba(255, 0, 0, 1)">
transition</span>:<span style="color: rgba(0, 0, 255, 1)"> transform 0.4s</span>;<span style="color: rgba(255, 0, 0, 1)">
transform</span>:<span style="color: rgba(0, 0, 255, 1)"> translateX(calc(-1 * (var(--active-index) * (var(--bullet-size) + var(--bullet-gap)))))</span>;<span style="color: rgba(255, 0, 0, 1)">
display</span>:<span style="color: rgba(0, 0, 255, 1)"> flex</span>;<span style="color: rgba(255, 0, 0, 1)">
gap</span>:<span style="color: rgba(0, 0, 255, 1)"> var(--bullet-gap)</span>;<span style="color: rgba(255, 0, 0, 1)">
height</span>:<span style="color: rgba(0, 0, 255, 1)"> var(--bullet-size)</span>;<span style="color: rgba(255, 0, 0, 1)">
.bullet {
flex-shrink</span>:<span style="color: rgba(0, 0, 255, 1)"> 0</span>;<span style="color: rgba(255, 0, 0, 1)">
width</span>:<span style="color: rgba(0, 0, 255, 1)"> var(--bullet-size)</span>;<span style="color: rgba(255, 0, 0, 1)">
height</span>:<span style="color: rgba(0, 0, 255, 1)"> var(--bullet-size)</span>;<span style="color: rgba(255, 0, 0, 1)">
border-radius</span>:<span style="color: rgba(0, 0, 255, 1)"> 999px</span>;<span style="color: rgba(255, 0, 0, 1)">
border</span>:<span style="color: rgba(0, 0, 255, 1)"> 1px solid blue</span>;<span style="color: rgba(255, 0, 0, 1)">
cursor</span>:<span style="color: rgba(0, 0, 255, 1)"> pointer</span>;<span style="color: rgba(255, 0, 0, 1)">
transition</span>:<span style="color: rgba(0, 0, 255, 1)"> transform 0.4s</span>;<span style="color: rgba(255, 0, 0, 1)">
&.active {
background-color</span>:<span style="color: rgba(0, 0, 255, 1)"> lightblue</span>;
}<span style="color: rgba(128, 0, 0, 1)">
&:not(.active) </span>{<span style="color: rgba(255, 0, 0, 1)">
transform</span>:<span style="color: rgba(0, 0, 255, 1)"> scale(0.5)</span>;
}<span style="color: rgba(128, 0, 0, 1)">
&:has(+ .active) </span>{<span style="color: rgba(255, 0, 0, 1)">
transform</span>:<span style="color: rgba(0, 0, 255, 1)"> scale(0.75)</span>;
}<span style="color: rgba(128, 0, 0, 1)">
&.active + .bullet </span>{<span style="color: rgba(255, 0, 0, 1)">
transform</span>:<span style="color: rgba(0, 0, 255, 1)"> scale(0.75)</span>;
}<span style="color: rgba(128, 0, 0, 1)">
}
}
}
}</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>Scripts</p>
<div class="cnblogs_code"><img class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><img class="code_img_opened lazyload" style="display: none" data-src="http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif">
<div id="cnblogs_code_open_06919db9-7720-4ff1-a750-55a604ec7a72" class="cnblogs_code_hide">
<pre>import emblaCarousel from 'embla-carousel'<span style="color: rgba(0, 0, 0, 1)">;
const sliderContainer </span>= document.querySelector<HTMLElement>('.slider-container')!<span style="color: rgba(0, 0, 0, 1)">;
const sliderElement </span>= sliderContainer.querySelector<HTMLElement>('.slider')!<span style="color: rgba(0, 0, 0, 1)">;
const slider </span>=<span style="color: rgba(0, 0, 0, 1)"> emblaCarousel(sliderElement, {
align: </span>'start'<span style="color: rgba(0, 0, 0, 1)">,
slidesToScroll: </span>'auto'<span style="color: rgba(0, 0, 0, 1)">,
inViewThreshold: </span>0.1<span style="color: rgba(0, 0, 0, 1)">,
});
const pagination </span>= document.querySelector<HTMLElement>('.pagination')!<span style="color: rgba(0, 0, 0, 1)">;
const bulletList </span>= pagination.querySelector<HTMLElement>('.bullet-list')!<span style="color: rgba(0, 0, 0, 1)">;
const bulletTemplate </span>= pagination.querySelector('template')!<span style="color: rgba(0, 0, 0, 1)">;
const cachedBullets: HTMLElement[] </span>=<span style="color: rgba(0, 0, 0, 1)"> [];
</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> rebuildPagination() {
const currentViewIndex </span>=<span style="color: rgba(0, 0, 0, 1)"> slider.selectedScrollSnap();
const viewCount </span>=<span style="color: rgba(0, 0, 0, 1)"> slider.scrollSnapList().length;
bulletList.style.setProperty(</span>'--active-index'<span style="color: rgba(0, 0, 0, 1)">, currentViewIndex.toString());
cachedBullets.forEach(bullet </span>=> bullet.classList.remove('active'<span style="color: rgba(0, 0, 0, 1)">));
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (cachedBullets.length ><span style="color: rgba(0, 0, 0, 1)"> viewCount) {
const bulletsToRemove </span>=<span style="color: rgba(0, 0, 0, 1)"> cachedBullets.splice(viewCount);
bulletsToRemove.forEach(bullet </span>=><span style="color: rgba(0, 0, 0, 1)"> bullet.remove());
}
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (cachedBullets.length <<span style="color: rgba(0, 0, 0, 1)"> viewCount) {
const gap </span>= viewCount -<span style="color: rgba(0, 0, 0, 1)"> cachedBullets.length;
const bulletsToAdd </span>= <span style="color: rgba(0, 0, 255, 1)">new</span> Array(gap).fill(undefined).map((_, index) =><span style="color: rgba(0, 0, 0, 1)"> {
const bulletTemplateFrag </span>= bulletTemplate.content.cloneNode(<span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">) as DocumentFragment;
const bullet </span>=<span style="color: rgba(0, 0, 0, 1)"> bulletTemplateFrag.firstElementChild as HTMLElement;
const scrollToIndex </span>= cachedBullets.length +<span style="color: rgba(0, 0, 0, 1)"> index;
bullet.addEventListener(</span>'click', () =><span style="color: rgba(0, 0, 0, 1)"> slider.scrollTo(scrollToIndex));
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> bullet;
});
cachedBullets.push(...bulletsToAdd);
const frag </span>=<span style="color: rgba(0, 0, 0, 1)"> document.createDocumentFragment();
bulletsToAdd.forEach(bullet </span>=><span style="color: rgba(0, 0, 0, 1)"> frag.appendChild(bullet));
bulletList.appendChild(frag);
}
cachedBullets.classList.add(</span>'active'<span style="color: rgba(0, 0, 0, 1)">);
}
slider.on(</span>'select'<span style="color: rgba(0, 0, 0, 1)">, rebuildPagination);
slider.on(</span>'init'<span style="color: rgba(0, 0, 0, 1)">, rebuildPagination);
slider.on(</span>'reInit', rebuildPagination);</pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p> </p>
<h2>Auto Height</h2>
<p>我们来看一个场景</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409141627057-276375915.png"></p>
<p>粉色是整个 slider,为什么下半段会空空?</p>
<p>因为有隐藏的 slide 内容很多,很高。</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409140834902-659741754.gif"></p>
<p>后面隐藏的 slide 把整个 slider 撑高了。</p>
<p>显然对用户来说这个体验不 ok,因为这会让用户感到困惑 -- 怎么下面空空的🤔?</p>
<p>我们可以用 Auto Height Plugin 来解决这个问题 (注:Swiper 也有这个功能)。</p>
<p>安装 package</p>
<div class="cnblogs_code">
<pre>yarn add embla-carousel-auto-height</pre>
</div>
<p>import plugin 函数,调用它创建 plugin 实例,再传给 Embla Carousel 就行了。(和 Autoplay Plugin 玩法一样)</p>
<div class="cnblogs_code">
<pre>import autoHeight from 'embla-carousel-auto-height'<span style="color: rgba(0, 0, 0, 1)">;
const slider </span>=<span style="color: rgba(0, 0, 0, 1)"> emblaCarousel(
sliderElement,
{ align: </span>'start', slidesToScroll: 'auto', inViewThreshold: 0.1<span style="color: rgba(0, 0, 0, 1)"> },
,
);</span></pre>
</div>
<p>添加 Styles</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409142059304-1633576951.png"></p>
<p>align-items: flex-start 的目的是让每一个 slide height 变成 hug content (默认是 stretch,会被其它 slide 拉大,这不是我们要的)。</p>
<p>transition 只是为了体验丝滑</p>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409143027453-1394687677.gif"></p>
<p>当用户 swipe 到比较高的 slide 时,slider 的 height 才会撑开。</p>
<h3>Auto Height 的计算方式</h3>
<p>上面例子有 6 个 slides (6 张图),每一个 view 显示两个 slides。</p>
<p>我们删除最后一个 slide,变成 5 个 slides,然后 swipe 到最后一个 view,它长这样</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409143415801-368104120.png"></p>
<p>第 4 个 slide 没有显示所有的内容,这是为什么呢?</p>
<p>我翻了一下源码,发现它使用的是 slideRegistry 来获取当前 view 的 slides,而不是我们上面提过的 slidesInView。</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409143830625-604979339.png"></p>
<p>我们测一下</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> detect() {
window.setTimeout(() </span>=><span style="color: rgba(0, 0, 0, 1)"> {
console.log(</span>'slidesInView'<span style="color: rgba(0, 0, 0, 1)">, slider.slidesInView());
console.log(</span>'slideRegistry'<span style="color: rgba(0, 0, 0, 1)">, slider.internalEngine().slideRegistry);
}, </span>500<span style="color: rgba(0, 0, 0, 1)">);
}
slider.on(</span>'select'<span style="color: rgba(0, 0, 0, 1)">, detect);
slider.on(</span>'init', detect);</pre>
</div>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409144256178-675340532.png"></p>
<p>可以看到,最后一个 view,slideRegistry 只拿到了 slide index 4 (也就是第 5 个 slide),所以在计算 auto height 时,它只用了第 5 个 slide 的高度,没有把第 4 个 slide 考量进去。</p>
<p>而第 4 个 slide 比第 5 个高,那最终第 4 个 slide 就被 overflow clip 掉了。</p>
<p>我提了一个 Issue,希望有人能解释清楚这是不是他们预想中的体验。</p>
<p>更新 2025-11-11:我的 Issue 有回复了,下一个版本会支持 based on slidesInView。</p>
<p>不过即便如此也要注意哦,slidesInView 是基于 IntersectionObserver 的,如上面提到的,如果我们使用 slide padding-left 做 gap,那它的结果会被影响,另外它需要等到 swipe 余力耗尽才准,这个体验不是很好。</p>
<p>我才也是因为这些原因,作者一开始才选择使用 slideRegistry 来实现吧。</p>
<p>其实还有一个思路:既然 Embla 可以不透过 Intersection 计算出 slideRegistry,那我们同样可以算,只是算法做一些调整就好了。</p>
<p>首先我们需要一个至关重要的信息 -- scrollSnaps</p>
<div class="cnblogs_code">
<pre>console.log(slider.internalEngine().scrollSnaps); <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> </span></pre>
</div>
<p>它有三个值,代表将会有 3 个 view。</p>
<p>10, -384, -799 就是 view 需要调节的 translateX</p>
<p><img alt="image" data-src="https://img2024.cnblogs.com/blog/641294/202511/641294-20251112010016181-2033838836.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202511/641294-20251112010018927-619266213.png"></p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202511/641294-20251112010021549-2010050398.png"></p>
<p>我们有所有 slide 的 bounding client rect,再加上每一个 view 的 translateX,那就可以计算出准确的 slidesInView 了。</p>
<p>看看效果:</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202511/641294-20251112155235840-11868764.gif"></p>
<p>可以看到 stg eager 立刻就出现了,比起使用 Intersection Observer 的快多了。</p>
<p>但是,这个方案无法支持 dragFree 哦,因为 dragFree 没有固定的 view position。要支持 dragFree 只能配 Intersection Observer slidesInview 方案。</p>
<p>题外话:</p>
<p>我在 Swiper 文章里有提到一个问题 -- Auto Height and Same Height。</p>
<p>在 Embla 也会遇到相同的问题,我们可以用同样的解决方案,只不过那个方案依赖 slides in view,</p>
<p>放过来 Embla 的话,要嘛我们自己计算 slides in view,要嘛学 Auto Height Plugin 用 slideRegistry 就好。</p>
<h3>Handle content resize</h3>
<p>在没有 Auto Height 的情况下,slider 的高度是 hug content (依据 slide 的高度),假如 slide 的内容增加了,那 slider 的高度也会跟着增加。</p>
<p>在有 Auto Height 的情况下就不是这样,Auto Height 会给 slide-list 添加 height 固定它的高度,这会导致 slider hug content 失效。</p>
<p>当 slide 内容增加后,由于 slide-list 限高了,所以它只会被 overflow hidden,slider 高度不会跟着变高。</p>
<p>要解决这个问题,我们需要监听 slide 高度变更,然后通知 Auto Height,让它重新计算去 update slide-list 的 height。</p>
<p>看例子:</p>
<p>加一个 more content 和 read more button</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409145130802-108861946.png"></p>
<p>点击 button 显示 more content</p>
<div class="cnblogs_code">
<pre>const readMoreBtn = document.querySelector<HTMLElement>('.read-more-btn')!<span style="color: rgba(0, 0, 0, 1)">;
readMoreBtn.addEventListener(</span>'click', () =><span style="color: rgba(0, 0, 0, 1)"> {
const moreContent </span>= document.querySelector<HTMLElement>('.more-content')!<span style="color: rgba(0, 0, 0, 1)">;
moreContent.style.display </span>= 'revert'<span style="color: rgba(0, 0, 0, 1)">;
});</span></pre>
</div>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409145329114-2078376240.gif"></p>
<p>点击后完全没有反应,因为 Auto Height 破坏了原本的 slider hug content。</p>
<p>相关 Issue – Auto Height and slide changing height</p>
<p>作者给的解方是在 resize 后调用 EmblaCarousel.reInit 方法</p>
<div class="cnblogs_code">
<pre>readMoreBtn.addEventListener('click', () =><span style="color: rgba(0, 0, 0, 1)"> {
const moreContent </span>= document.querySelector<HTMLElement>('.more-content')!<span style="color: rgba(0, 0, 0, 1)">;
moreContent.style.display </span>= 'revert'<span style="color: rgba(0, 0, 0, 1)">;
slider.reInit(); </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> resize 后调用 reInit 方法通知 Auto Height Plugin</span>
});</pre>
</div>
<p>这样就行了。(注:感觉有点小题大做,但也没有其它管道了,或许作者是想统一接口吧)</p>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409150023310-1132410405.gif"></p>
<p>题外话:</p>
<p>Embla 内部是有监听 slider 和 slides resize 的</p>
<p><img alt="" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250414060454418-1639932843.png"></p>
<p>每当 resize 它就会调用 reInit,但是它监听的是 width,而不是 height。</p>
<h3>连续 Next 体验问题</h3>
<p>这个问题我在 Swiper 那篇也有提过。</p>
<p>auto height 每次换 slide 时都会改变 slider 高度,如果 navigation / pagination button 依赖这个高度,那体验就会被影响。</p>
<p><img src="https://img2023.cnblogs.com/blog/641294/202308/641294-20230808173121721-911321289.gif"></p>
<p>上面例子中,我们无法连续按 next button,因为它会跳上跳下。</p>
<p>解决思路有两个方向。</p>
<p>第一,navigation button 不要依赖 slide 的高度,比如我们把它从 slider 下面移到 slider 左边。(但有时候空间太少,真的没有地方可以放)</p>
<p>第二,让这个 auto height 慢一点触发,比如 next 了一秒后才 update height。</p>
<p>for 第二个方向,我们可以这样写</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> updateHeight() {
let slidesInView </span>=<span style="color: rgba(0, 0, 0, 1)"> slider.slidesInView();
</span><span style="color: rgba(0, 0, 255, 1)">if</span> (slidesInView.length === 0<span style="color: rgba(0, 0, 0, 1)">) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> init or reInit 时 slidesInView 可能是 empty array</span>
slidesInView =<span style="color: rgba(0, 0, 0, 1)"> slider.internalEngine().slideRegistry;
}
const slideRects </span>= slider.internalEngine().slideRects.filter((_, index) =><span style="color: rgba(0, 0, 0, 1)"> slidesInView.includes(index));
const height </span>= Math.max(...slideRects.map(rect =><span style="color: rgba(0, 0, 0, 1)"> rect.height));
slider.containerNode().style.height </span>=<span style="color: rgba(0, 0, 0, 1)"> `${height}px`;
}
slider.on(</span>'init'<span style="color: rgba(0, 0, 0, 1)">, updateHeight);
slider.on(</span>'reInit'<span style="color: rgba(0, 0, 0, 1)">, updateHeight);
slider.on(</span>'settle', updateHeight);</pre>
</div>
<p>不需要使用 Auto Height Plugin,单纯 Embla 底层 API 就可以了。(其实 Auto Height Plugin 内部也是调用这几个 API 实现的)</p>
<p>settle 事件会在 slide moving transition 结束后触发,非常非常的晚。</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409163839748-137626450.gif"></p>
<p> </p>
<h2>Add / Remove / Sort Slides</h2>
<p>没有 add / remove / sort 接口,我们要增加 / 减少 / 改 slide 的位置的话,直接 DOM manipulation 就好。</p>
<p>DOM manipulation 完后调用 EmblaCarousel.reInit() 就可以了。</p>
<p>总之,它就只有一个接口,不管是 change options, change plugin, change size, change elements 都是调用 reInit 就对了。</p>
<p> </p>
<h2>CSS 优化手法</h2>
<p>参考官网的 example,我们会看到几个 CSS 优化手法。</p>
<p>HTML 结构长这样</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407170900870-939972374.png"></p>
<p>CSS</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407170735358-967878858.png"></p>
<p>touch-action 是告诉游览器,它只负责 pan-y (vertical scroll) 和 pinch-zoom (scale 放大) 就好,其它手势交给我们负责。</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407171024898-491543438.png"></p>
<p>transform: translate3d(0, 0, 0); 是让游览器使用 GPU 来渲染每个 slide。</p>
<p>embla__container 肯定会使用 GPU 渲染,因为它负责 transform 嘛,slides 则不会,所以要快就要特别声明。</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250407165657225-664343833.png"></p>
<p>touch-action: manipulation 是告诉游览器,这个 button 只需要最基本的 tap,不需要其它手势。</p>
<p>Embla Carousel 的其中一个卖点就是快,所以它的 example 尽可能优化到极致。</p>
<p>但我们一般上不需要跟着这么做,性能优化请等到用户有感觉到慢了才做。</p>
<p> </p>
<h2>当 Emble Carousel 遇上 YouTube Iframe</h2>
<p>和 Swiper 一模一样的问题,解决方法也一模一样,在 Swiper 那篇已经讲解过了,这里就不复述了。</p>
<p> </p>
<h2>Navigation Plugin</h2>
<p>上面虽然教了如何实现 Navigation,但手法过于粗糙,只能作为教材,还无法用于实战项目。</p>
<p>在真实项目中,我们会把它封装进一个 plugin 里,然后像 Autoplay 那样去使用它。</p>
<p>封装成 plugin 有两个好处:</p>
<ol>
<li>
<p>plug & play</p>
<p>navigation 不是必须的,前端嘛,没有用到就尽量 tree-shake,让项目体积小一点。</p>
</li>
<li>
<p>统一支持 breakpoints 和 active</p>
<p>Embla 要求所有的 plugin 都要实现统一接口,支持 breakpoints options 和 active inactive 功能。 </p>
</li>
</ol>
<p>总之,即便只是为了顺风水,我们也该尽量把功能封装成 plugin 就对了。</p>
<p>好,这里我给一个 Navigation Plugin 的简单示范。</p>
<h3>HTML</h3>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slider"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide-list"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/yangmi1.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="yangmi"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/tifa.webp"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="tifa"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="slide"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">img </span><span style="color: rgba(255, 0, 0, 1)">src</span><span style="color: rgba(0, 0, 255, 1)">="../images/nana.jpg"</span><span style="color: rgba(255, 0, 0, 1)"> alt</span><span style="color: rgba(0, 0, 255, 1)">="nana"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">button </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="prev-btn"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">svg </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="icon"</span><span style="color: rgba(255, 0, 0, 1)"> fill</span><span style="color: rgba(0, 0, 255, 1)">="currentColor"</span><span style="color: rgba(255, 0, 0, 1)"> xmlns</span><span style="color: rgba(0, 0, 255, 1)">="http://www.w3.org/2000/svg"</span><span style="color: rgba(255, 0, 0, 1)"> viewBox</span><span style="color: rgba(0, 0, 255, 1)">="0 0 320 512"</span><span style="color: rgba(0, 0, 255, 1)">><</span><span style="color: rgba(128, 0, 0, 1)">path </span><span style="color: rgba(255, 0, 0, 1)">d</span><span style="color: rgba(0, 0, 255, 1)">="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z"</span><span style="color: rgba(0, 0, 255, 1)">/></</span><span style="color: rgba(128, 0, 0, 1)">svg</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">button </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="next-btn"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">svg </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="icon"</span><span style="color: rgba(255, 0, 0, 1)"> fill</span><span style="color: rgba(0, 0, 255, 1)">="currentColor"</span><span style="color: rgba(255, 0, 0, 1)"> xmlns</span><span style="color: rgba(0, 0, 255, 1)">="http://www.w3.org/2000/svg"</span><span style="color: rgba(255, 0, 0, 1)"> viewBox</span><span style="color: rgba(0, 0, 255, 1)">="0 0 320 512"</span><span style="color: rgba(0, 0, 255, 1)">><</span><span style="color: rgba(128, 0, 0, 1)">path </span><span style="color: rgba(255, 0, 0, 1)">d</span><span style="color: rgba(0, 0, 255, 1)">="M278.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-160 160c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L210.7 256 73.4 118.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l160 160z"</span><span style="color: rgba(0, 0, 255, 1)">/></</span><span style="color: rgba(128, 0, 0, 1)">svg</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">button</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<p>关键是多了两个 navigation button。</p>
<h3>Styles</h3>
<div class="cnblogs_code"><img class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><img class="code_img_opened lazyload" style="display: none" data-src="http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif">
<div id="cnblogs_code_open_0a5272fa-7219-4918-a1e7-fa4eb6c3fe78" class="cnblogs_code_hide">
<pre><span style="color: rgba(128, 0, 0, 1)">.slider </span>{<span style="color: rgba(255, 0, 0, 1)">
max-width</span>:<span style="color: rgba(0, 0, 255, 1)"> 512px</span>;<span style="color: rgba(255, 0, 0, 1)">
overflow</span>:<span style="color: rgba(0, 0, 255, 1)"> hidden</span>;<span style="color: rgba(255, 0, 0, 1)">
position</span>:<span style="color: rgba(0, 0, 255, 1)"> relative</span>;<span style="color: rgba(255, 0, 0, 1)">
.slide-list {
display</span>:<span style="color: rgba(0, 0, 255, 1)"> flex</span>;<span style="color: rgba(255, 0, 0, 1)">
.slide {
flex-shrink</span>:<span style="color: rgba(0, 0, 255, 1)"> 0</span>;<span style="color: rgba(255, 0, 0, 1)">
width</span>:<span style="color: rgba(0, 0, 255, 1)"> 100%</span>;<span style="color: rgba(255, 0, 0, 1)">
img {
width</span>:<span style="color: rgba(0, 0, 255, 1)"> 100%</span>;<span style="color: rgba(255, 0, 0, 1)">
height</span>:<span style="color: rgba(0, 0, 255, 1)"> auto</span>;<span style="color: rgba(255, 0, 0, 1)">
aspect-ratio</span>:<span style="color: rgba(0, 0, 255, 1)"> 16 / 9</span>;<span style="color: rgba(255, 0, 0, 1)">
object-fit</span>:<span style="color: rgba(0, 0, 255, 1)"> cover</span>;
}<span style="color: rgba(128, 0, 0, 1)">
}
}
.prev-btn,
.next-btn </span>{<span style="color: rgba(255, 0, 0, 1)">
border-width</span>:<span style="color: rgba(0, 0, 255, 1)"> 0</span>;<span style="color: rgba(255, 0, 0, 1)">
cursor</span>:<span style="color: rgba(0, 0, 255, 1)"> pointer</span>;<span style="color: rgba(255, 0, 0, 1)">
position</span>:<span style="color: rgba(0, 0, 255, 1)"> absolute</span>;<span style="color: rgba(255, 0, 0, 1)">
top</span>:<span style="color: rgba(0, 0, 255, 1)"> 50%</span>;<span style="color: rgba(255, 0, 0, 1)">
transform</span>:<span style="color: rgba(0, 0, 255, 1)"> translateY(-50%)</span>;<span style="color: rgba(255, 0, 0, 1)">
background-color</span>:<span style="color: rgba(0, 0, 255, 1)"> pink</span>;<span style="color: rgba(255, 0, 0, 1)">
color</span>:<span style="color: rgba(0, 0, 255, 1)"> red</span>;<span style="color: rgba(255, 0, 0, 1)">
border-radius</span>:<span style="color: rgba(0, 0, 255, 1)"> 999px</span>;<span style="color: rgba(255, 0, 0, 1)">
display</span>:<span style="color: rgba(0, 0, 255, 1)"> block</span>;<span style="color: rgba(255, 0, 0, 1)">
width</span>:<span style="color: rgba(0, 0, 255, 1)"> 40px</span>;<span style="color: rgba(255, 0, 0, 1)">
height</span>:<span style="color: rgba(0, 0, 255, 1)"> 40px</span>;<span style="color: rgba(255, 0, 0, 1)">
visibility</span>:<span style="color: rgba(0, 0, 255, 1)"> hidden</span>;<span style="color: rgba(255, 0, 0, 1)">
opacity</span>:<span style="color: rgba(0, 0, 255, 1)"> 0</span>;<span style="color: rgba(255, 0, 0, 1)">
transition-property</span>:<span style="color: rgba(0, 0, 255, 1)"> visibility, opacity</span>;<span style="color: rgba(255, 0, 0, 1)">
transition-duration</span>:<span style="color: rgba(0, 0, 255, 1)"> 0.4s</span>;<span style="color: rgba(255, 0, 0, 1)">
.icon {
width</span>:<span style="color: rgba(0, 0, 255, 1)"> 20px</span>;<span style="color: rgba(255, 0, 0, 1)">
height</span>:<span style="color: rgba(0, 0, 255, 1)"> 20px</span>;
}<span style="color: rgba(128, 0, 0, 1)">
}
.prev-btn </span>{<span style="color: rgba(255, 0, 0, 1)">
left</span>:<span style="color: rgba(0, 0, 255, 1)"> 8px</span>;
}<span style="color: rgba(128, 0, 0, 1)">
.next-btn </span>{<span style="color: rgba(255, 0, 0, 1)">
right</span>:<span style="color: rgba(0, 0, 255, 1)"> 8px</span>;
}<span style="color: rgba(128, 0, 0, 1)">
&:hover </span>{<span style="color: rgba(255, 0, 0, 1)">
.prev-btn,
.next-btn {
&</span>:<span style="color: rgba(0, 0, 255, 1)">not() {
visibility: unset</span>;<span style="color: rgba(255, 0, 0, 1)">
opacity</span>:<span style="color: rgba(0, 0, 255, 1)"> unset</span>;
}<span style="color: rgba(128, 0, 0, 1)">
}
}
}</span></pre>
</div>
<span class="cnblogs_code_collapse">View Code</span></div>
<p>没什么,就只是一些美观的 styling 而已。</p>
<p>Embla 不掺和 HTML 和 CSS,所以上面这两部分都不算 plugin 封装。</p>
<p>目前效果</p>
<p><img alt="" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250413133627358-1249948124.gif"></p>
<p>有 styles 了,但没有 script,还不能交互。</p>
<h3>Scripts</h3>
<p>创建一个 file -- slider-navigation-plugin.ts</p>
<p>我先讲解一个通用的 plugin 轮廓。</p>
<h4>定义 Options</h4>
<div class="cnblogs_code">
<pre>import { CreateOptionsType } from 'embla-carousel'<span style="color: rgba(0, 0, 0, 1)">;
type Options </span>= CreateOptionsType<<span style="color: rgba(0, 0, 0, 1)">{
prevBtn: HTMLButtonElement;
nextBtn: HTMLButtonElement;
}</span>>;</pre>
</div>
<p>options 就是 plugin 和项目沟通的管道。</p>
<p>navigation button element 是项目定义的,但 plugin 需要给 button 绑定事件,所以 plugin 需要认识 button,这时就要靠 options 把 button 传进来。</p>
<p>CreateOptionsType 是 Embla 内置的 TypeScript 方法,它的效果是这样</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250413143024938-709261157.png"></p>
<p>active 和 breakpoints 是所有 plugin options 必备的属性,prevBtn 和 nextBtn 则是 Navigation Plugin 独有的。</p>
<h4>定义 Plugin 对象</h4>
<div class="cnblogs_code">
<pre>export type SliderNavigationPlugin = CreatePluginType<<span style="color: rgba(0, 0, 0, 1)">
{
value: string;
doSomething: () </span>=> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)">;
},
Options
</span>>;</pre>
</div>
<p>每个 plugin 创建后都是一个对象,除了 Embla 可以用,项目也可以用。</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250413143349793-1649932286.png"></p>
<p>所有 plugin 对象都要有 name, options, init, destroy 属性和方法,这些都是 Embla 需要用到的,另外我们还可以定义其它属性方法让我们自己用,比如上图里的 value 和 doSomething。</p>
<p>如果没有额外的属性方法,定义的时候放一个空对象即可</p>
<div class="cnblogs_code">
<pre>export type SliderNavigationPlugin = CreatePluginType<Record<string, unknown>, Options>;</pre>
</div>
<h4>定义 create plugin 函数</h4>
<p>轮廓长这样</p>
<div class="cnblogs_code">
<pre>export <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> createSliderNavigationPlugin(userOptions: PartialOptions): SliderNavigationPlugin {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 我习惯用 RxJS</span>
const destroySubject = <span style="color: rgba(0, 0, 255, 1)">new</span> Subject<<span style="color: rgba(0, 0, 255, 1)">void</span>><span style="color: rgba(0, 0, 0, 1)">();
</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> init(slider: EmblaCarouselType, optionsHandler: OptionsHandlerType) {
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 关键代码都在这里...</span>
<span style="color: rgba(0, 0, 0, 1)">}
</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> destroy() {
destroySubject.next();
}
</span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> {
name: </span>'navigation'<span style="color: rgba(0, 0, 0, 1)">,
options: userOptions,
init,
destroy,
};
}</span></pre>
</div>
<p>每当 Embla init 或 reInit,destroy 方法会被执行,接着如果 Embla 是 active 同时 plugin 也是 active (depend on breakpoints options) 的话,init 方法会被执行。</p>
<p>我们在 init 方法里做事件绑定,在 destroy 方法里做解绑。</p>
<p>init 方法会被 Embla 调用,调用的时候会传入两个参数,第一个是 EmblaCarousel 对象,第二个是 OptionsHandler 对象。</p>
<p>OptionsHandler 对象有 2 个方法</p>
<ol>
<li>
<p>mergeOptions</p>
<img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250413153125500-408310428.png">
<p>简单说就是一个 deep merge,支持 nested object merge。</p>
</li>
<li>
<p>optionsAtMedia</p>
<img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250413153436107-1137186107.png">
<p>依据当前的 viewport media query 匹配出对应的 options</p>
</li>
</ol>
<p>于是,我们可以这么写</p>
<div class="cnblogs_code">
<pre>type DefaultOptions = Omit<Options, OptionsRequiredKeys><span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)"> init(slider: EmblaCarouselType, optionsHandler: OptionsHandlerType) {
const { mergeOptions, optionsAtMedia } </span>=<span style="color: rgba(0, 0, 0, 1)"> optionsHandler;
const defaultOptions: DefaultOptions </span>=<span style="color: rgba(0, 0, 0, 1)"> {
active: </span><span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">,
breakpoints: {},
};
const mergedOptions </span>=<span style="color: rgba(0, 0, 0, 1)"> mergeOptions(defaultOptions, userOptions) as Options;
const options </span>=<span style="color: rgba(0, 0, 0, 1)"> optionsAtMedia(mergedOptions);
const { prevBtn, nextBtn } </span>=<span style="color: rgba(0, 0, 0, 1)"> options;
console.log(</span>'bind event to prevBtn, nextBtn'<span style="color: rgba(0, 0, 0, 1)">, );
}</span></pre>
</div>
<p>把 options merge 一 merge,提取出 prevBtn 和 nextBtn,接着做 binding。</p>
<div class="cnblogs_code">
<pre>const stateChangeEventNames: EmblaEventType[] = ['select', 'init', 'reInit'<span style="color: rgba(0, 0, 0, 1)">];
merge(...stateChangeEventNames.map(eventName </span>=><span style="color: rgba(0, 0, 0, 1)"> fromEvent(slider, eventName)))
.pipe(takeUntil(destroySubject))
.subscribe(() </span>=><span style="color: rgba(0, 0, 0, 1)"> {
prevBtn.disabled </span>= !<span style="color: rgba(0, 0, 0, 1)">slider.canScrollPrev();
nextBtn.disabled </span>= !<span style="color: rgba(0, 0, 0, 1)">slider.canScrollNext();
});
</span><span style="color: rgba(0, 0, 255, 1)">for</span><span style="color: rgba(0, 0, 0, 1)"> (const button of ) {
fromEvent(button, </span>'click'<span style="color: rgba(0, 0, 0, 1)">)
.pipe(takeUntil(destroySubject))
.subscribe(() </span>=> slider[`scroll${button === prevBtn ? 'Prev' : 'Next'}`]());</pre>
</div>
<p>我习惯写 RxJS,看不懂的请让 ChatGPT / DeepSeek 为你讲解哦。</p>
<h4>使用 plugin</h4>
<p>用法和 Autoplay Plugin 大同小异。</p>
<div class="cnblogs_code">
<pre>const sliderElement = document.querySelector<HTMLElement>('.slider')!<span style="color: rgba(0, 0, 0, 1)">;
const prevBtn </span>= sliderElement.querySelector<HTMLButtonElement>('.prev-btn')!<span style="color: rgba(0, 0, 0, 1)">;
const nextBtn </span>= sliderElement.querySelector<HTMLButtonElement>('.next-btn')!<span style="color: rgba(0, 0, 0, 1)">;
const navigationPlugin </span>=<span style="color: rgba(0, 0, 0, 1)"> createSliderNavigationPlugin({
prevBtn,
nextBtn,
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 可以设置 breakpoint</span>
<span style="color: rgba(0, 0, 0, 1)">breakpoints: {
</span>'(min-width: 1920px)'<span style="color: rgba(0, 0, 0, 1)">: {
active: </span><span style="color: rgba(0, 0, 255, 1)">false</span>, <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 可以 inactive</span>
<span style="color: rgba(0, 0, 0, 1)"> },
},
});
const slider </span>= emblaCarousel(sliderElement, undefined, );</pre>
</div>
<h4>效果</h4>
<p><img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250413155428513-659882608.gif"></p>
<h4>卡卡问题</h4>
<p>注意看最后几幕会卡卡的,因为 button click 和 slider swipe 打架了。</p>
<p>我们来解决它,首先加一个 options 属性</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250413155851232-1419871817.png"></p>
<p>因为 plugin 不晓得 navigation button 是否在 slider 里,只有在 slider 里才会有打架的问题,才需要处理。</p>
<p>接着在 init 方法里加上这些代码</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (buttonInsideSlider) {
merge(...[</span>'touchstart', 'mousedown'].map(eventName => fromEvent(slider.rootNode(), eventName, { capture: <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)"> })))
.pipe(
filter(e </span>=> .some(navBtn =><span style="color: rgba(0, 0, 0, 1)"> navBtn.contains(e.target as HTMLElement))),
takeUntil(destroySubject),
)
.subscribe(e </span>=><span style="color: rgba(0, 0, 0, 1)"> e.stopPropagation());
}</span></pre>
</div>
<p>做了一个 stopPropagation 阻止 swipe,这样就不打架了。</p>
<h3>Plugin event & instace</h3>
<p>项目要操作 plugin instance (比如 call method 和 addEventListener) 需要这样写:</p>
<p>首先定义 plugin 和 事件类型</p>
<div class="cnblogs_code">
<pre>declare module 'embla-carousel'<span style="color: rgba(0, 0, 0, 1)"> {
interface EmblaPluginsType {
navigation: SliderNavigationPlugin;
}
interface EmblaEventListType {
navigationClick: </span>'navigation:click'<span style="color: rgba(0, 0, 0, 1)">;
}
}</span></pre>
</div>
<p>定义了之后,我们就可以这样去使用它。</p>
<div class="cnblogs_code">
<pre>const navigation =<span style="color: rgba(0, 0, 0, 1)"> slider.plugins().navigation;
slider.on(</span>'navigation:click', () => console.log());</pre>
</div>
<p>在 init 方法内发布事件</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250413161923731-1970445416.png"></p>
<p>注:好像是无法 passing event data 的,呃...这么瞎的吗🤔?</p>
<p> </p>
<h2>与 Slider active / inactive 打交道</h2>
<div class="cnblogs_code">
<pre>const sliderElement = document.querySelector<HTMLElement>('.slider')!<span style="color: rgba(0, 0, 0, 1)">;
const emblaSlider </span>=<span style="color: rgba(0, 0, 0, 1)"> emblaCarousel(sliderElement, {
breakpoints: {
</span>'(min-width: 1280px)'<span style="color: rgba(0, 0, 0, 1)">: {
active: </span><span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)">,
},
},
});<br></span></pre>
</div>
<p>当 viewport 超过 1280px,embla-slider 就 inactive。</p>
<p>假设,我们的需求是在 embla-slider inactive 后,用 my-slider 去接手。</p>
<p>第一个想到的思路是去监听 reInit,然后判断当前 embla-slider 是 active 或 inactive -- inactive 就启动 my-slider;active 就关闭 my-slider。</p>
<div class="cnblogs_code">
<pre>const mySlider =<span style="color: rgba(0, 0, 0, 1)"> setupMySlider(sliderElement);
emblaSlider.on(</span>'reInit', () =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (emblaSlider.internalEngine().options.active) {
mySlider.destrop();
}
</span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
mySlider.init();
}
});</span></pre>
</div>
<p>可这方案有一个问题:</p>
<p>当 viewport 从大变小,embla-slider 从 inactive 转成 active 时,如果我们拦截的点是 reInit,那意味着 embla-slider 已经 reInit 完毕。</p>
<p>这样不行,正确的顺序应该是先 destroy my-slider,后 reInit embla-slider。</p>
<p>反过来也是一样。总之,必须先关闭那个正在 active 的 slider,关闭好后才能开启下一个 slider。</p>
<p>否则就会出现两个 slider 同时 active 的瞬间,这样可能会引入 bug。</p>
<p>换一个思路,去监听 media query</p>
<div class="cnblogs_code">
<pre>const mediaQueryList = window.matchMedia('(min-width: 1280px)'<span style="color: rgba(0, 0, 0, 1)">);
mediaQueryList.addEventListener(</span>'change', e =><span style="color: rgba(0, 0, 0, 1)"> {
</span><span style="color: rgba(0, 0, 255, 1)">if</span><span style="color: rgba(0, 0, 0, 1)"> (e.matches) {</span>
emblaSlider.on('reInit', () =><span style="color: rgba(0, 0, 0, 1)"> mySlider.init());
} </span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)"> {
mySlider.destroy();</span>
<span style="color: rgba(0, 0, 0, 1)">}
});</span></pre>
</div>
<p>destroy 可以立刻执行,init 就必须等到 embla-slider destroy 之后才能执行。</p>
<p>这个思路有一个前提条件 -- my-slider 的 media query 必须比 embla-slider 触发的早。</p>
<p>试一个</p>
<div class="cnblogs_code">
<pre>const sliderElement = document.querySelector<HTMLElement>('.slider')!<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 先执行 emblaCarousel</span>
const emblaSlider =<span style="color: rgba(0, 0, 0, 1)"> emblaCarousel(sliderElement, {
breakpoints: { </span>'(min-width: 1280px)': { active: <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)"> } },
});
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 后监听 media query</span>
const mediaQueryList = window.matchMedia('(min-width: 1280px)'<span style="color: rgba(0, 0, 0, 1)">);
mediaQueryList.addEventListener(</span>'change', e => console.log('my-slider active', e.media));</pre>
</div>
<p>viewport 从小变大,再从大变小,触发顺序:</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202511/641294-20251120145655536-1314513214.png"></p>
<p>1, 2 是合理的,因为 embla 内部也是使用 window.matchMedia,它执行监听的早,自然触发的早。</p>
<p>3, 4 怎么顺序调转了呢?原因是 embla 每一次 reInit 都会退订、重新监听 window.matchMedia,因此在第一次 reInit 后,my-slider 就变成先触发了。</p>
<p>好,无论如何,最佳实践就是把 window.matchMedia 放到 embla-slider 之前</p>
<div class="cnblogs_code">
<pre>const sliderElement = document.querySelector<HTMLElement>('.slider')!<span style="color: rgba(0, 0, 0, 1)">;
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 先监听 media query</span>
const mediaQueryList = window.matchMedia('(min-width: 1280px)'<span style="color: rgba(0, 0, 0, 1)">);
mediaQueryList.addEventListener(</span>'change', e => console.log('my-slider active'<span style="color: rgba(0, 0, 0, 1)">, e.media));
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 后执行 emblaCarousel</span>
const slider =<span style="color: rgba(0, 0, 0, 1)"> emblaCarousel(sliderElement, {
breakpoints: { </span>'(min-width: 1280px)': { active: <span style="color: rgba(0, 0, 255, 1)">false</span><span style="color: rgba(0, 0, 0, 1)"> } },
});</span></pre>
</div>
<p>这样就 100% 可控了。</p>
<p> </p>
<h2>碎碎的小知识</h2>
<p>记入一些日常小知识:</p>
<h3>init 何时触发?</h3>
<div class="cnblogs_code">
<pre>const slider =<span style="color: rgba(0, 0, 0, 1)"> emblaCarousel(sliderElement);
slider.on(</span>'init', () => console.log('init'<span style="color: rgba(0, 0, 0, 1)">));
console.log(</span>'sync');</pre>
</div>
<p>先执行同步代码,还是 init callback?</p>
<p>答案是:同步代码,因为</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202511/641294-20251120154156291-1320827094.png"></p>
<p>init event 被 setTimeout 压后执行了。</p>
<p>源码在 EmblaCarousel.ts。</p>
<p> </p>
<h2>碎碎的小技巧</h2>
<p>记入一些日常小技巧:</p>
<h3>当 slider 遇上 box-shadow</h3>
<p>HTML</p>
<pre class="language-html highlighter-hljs"><code><div class="container">
<div class="slider">
<div class="slide-list">
<div class="slide">
<div class="box">Lorem ipsum dolor sit amet.</div>
</div>
<div class="slide">
<div class="box">Lorem ipsum dolor sit amet.</div>
</div>
<div class="slide">
<div class="box">Lorem ipsum dolor sit amet.</div>
</div>
</div>
</div>
</div></code></pre>
<p>Styles</p>
<pre class="language-css highlighter-hljs"><code>
.container {
margin-top: 128px;
padding-inline: 32px;
.slider {
overflow: hidden;
.slide-list {
display: flex;
gap: 16px;
.slide {
flex-shrink: 0;
width: 100%;
.box {
border: 2px solid red;
padding: 16px;
box-shadow: 0 0 10px 0 blue;
}
}
}
}
}</code></pre>
<p> 效果</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202601/641294-20260130104740053-2032109877.gif"></p>
<p>左右蓝色的部分是 slide 内容的 box-shadow,但由于 slider 有 overflow hidden,所以 box-shadow 出不来。</p>
<p>这问题不好解决,只能依据不同场景做一些 workaround。</p>
<p>这里提供其中一个解决思路:</p>
<p>给 slider 添加 padding(让 box-shadow 有空间显示),同时利用 negative margin 让 slider 变大,吃外面的空间。</p>
<p>最终的视觉效果就是 slider 大小不变,但 box-shadow 能溢出来了。</p>
<pre class="language-css highlighter-hljs"><code>.container {
margin-top: 128px;
padding-inline: 32px;
.slider {
overflow: hidden;
padding: 15px; /* 让 box-shadow 有显示空间,之所以是 15px 而不是 10px,是因为 box-shadow 的 blur 不是准准的,它会多一点 */
margin: -15px; /* 抵消掉 padding,让 slider 保持原有大小 */
.slide-list {
display: flex;
gap: 30px; /* 间距也必须增加到 15px * 2 = 30px,否则 box-shadow 重叠,左右看上去会出问题 */
.slide {
flex-shrink: 0;
width: 100%;
.box {
border: 2px solid red;
padding: 16px;
box-shadow: 0 0 10px 0 blue;
}
}
}
}
}</code></pre>
<p>效果</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202602/641294-20260202134334892-1213039909.gif"></p>
<p>padding margin 的部分没有什么问题,比较容易出状况的是 gap 的部分。</p>
<p>它一定要增大,但这不一定符合场景需求,但如果不增大,看上去会变成这样</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202602/641294-20260202134539585-972465904.png"></p>
<p>思来想去,好像没有一个完美的解决方案...以后再研究呗。</p>
<p> </p>
<h2>掉坑里了</h2>
<p>Issue – Use positive relative on slider wrapper cause 1px offset</p>
<p><img src="https://img2024.cnblogs.com/blog/641294/202602/641294-20260205151959387-759119400.png"></p>
<p>我在 slider 的 parent 放了一个 position: relative,结果就出现了 border missing。</p>
<p>原因是 Embla 算错了 1px 微差。</p>
<p>它之所以算错是因为 slide width 有小数点,而 Embla 内部不使用精准的 getBoundingClientRect.left,而是使用 offsetLeft。</p>
<p>为什么作者不要用 getBoundingClientRect ?他有他的考虑,总之他给的 solution 是使用 CSS round 函数,不要让 slide width 出现小数点。</p>
<p> </p>
<h2>总结</h2>
<p>本篇简单的介绍了 Slider Library 的明日之星 – Embal Carousel。</p>
<p>希望它赶快取代 Swiper,不然我写这篇干嘛呢...😊</p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p> </p>
<p>TODO</p>
<p>之前写的草稿,不要了,但留着先</p>
<p>我的猜测是这样,slidesInView 依赖 IntersectionObserver,如果要依靠它的话,需要等到 slide 完全停下来才准,这会导致 auto height 很晚才去 update height,可能这个体验也不 ok,所以作者在这里做了一个 trade-off。</p>
<p>要达到我预期的效果,唯一的办法就是不要靠 IntersectionObserver,而是自己依据 slide 的 boundingClientRect 计算出 slides in view。</p>
<h3>Auto height based on slides in view</h3>
<p>我尝试了一下自己计算 slides in view,果然有点难度,可能就是这个原因 Embla 才不基于 slides in view 吧。</p>
<p>这里分享我的尝试</p>
<p>HTML</p>
<div class="cnblogs_code"><img class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><span class="cnblogs_code_collapse">View Code</span></div>
<p>Styles</p>
<div class="cnblogs_code"><img class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><span class="cnblogs_code_collapse">View Code</span></div>
<p>Scripts</p>
<div class="cnblogs_code"><img class="code_img_closed lazyload" data-src="http://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif"><span class="cnblogs_code_collapse">View Code</span></div>
<p>效果</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409202243493-2032989656.gif"></p>
<p>和 Auto Heigh Plugin 的区别是在最后一个 view,它的第 4 个 slide 会被 overflow,我的不会。</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409202342173-862465446.png"></p>
<p>我解释一下实现思路:</p>
<p>首先,拿三个信息</p>
<ol>
<li>slide-list boundingClientRect</li>
<li>slide boundingClientRect</li>
<li>view index</li>
</ol>
<p>然后模拟计算出这个 view index 内会出现哪些 slides,然后拿最高的 slide 就可以了。</p>
<p>有三个 options 会影响到 slides in view -- containScroll,slidesToScroll,align,特别讲一下 containScroll</p>
<div class="cnblogs_code">
<pre>const sliderOptions: EmblaOptionsType = {
containScroll: 'trimSnaps',
};</pre>
</div>
<p>有三个值可以放,默认是 'trimSnaps',另外一个 'keepSnaps',还有一个是 false。</p>
<p>我不清楚 'keepSnaps' 和 'trimSnaps' 有什么区别 (没找到文档,看源码有点昏),但我知道 trimSnaps 和 false 在体验上有区别。</p>
<p>上述例子有 5 个 slides,每一个 view 可以显示两个 slides,一共有三个 views。</p>
<p>关键在第三个 view 长什么样</p>
<p>containScroll: false 长这样</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409203313240-546398609.png"></p>
<p>因为有三个 view,每个 view 显示两个 slides,那最后一个 view 理应显示第 5 和第 6 个 slide。</p>
<p>不过我们只有 5 个 slides,所以第 6 个 slide 的位置就留空了。</p>
<p>containScroll: 'trimSnaps' 长这样</p>
<p><img alt="" class="medium-zoom-image lazyload" data-src="https://img2024.cnblogs.com/blog/641294/202504/641294-20250409203528607-1018445145.png"></p>
<p>它不会留空,第三个 view 会显示第 4 和第 5 个 slide。</p>
<p> </p><br><br>
来源:https://www.cnblogs.com/keatkeat/p/18817678
頁:
[1]