那些被忽略但很好用的 Web API / Animation On Scroll

学以致用是最快乐的事情

昨天我们认识了 IntersectionObserver,知道它可以侦测到元素进入画面的时机,而这个特性非常适合用来制作 Animation On Scroll 的卷动动画效果,像是 AOS 就是一个很经典的套件,虽然 AOS 其实不是用 IntersectionObserver 实践的,但我们可以尝试做出类似效果的工具。


设计概念

# 确立需求与功能

  1. 既然是工具,就代表要帮我们简化一些繁复的动作
  2. 这个工具需要在元素进入可视窗口时,进行指定样式的变化
  3. 除了样式变化,还希望可以设定 Callback,让我们做一些额外的操作

 

# 机制规划与设计

1. 既然是工具,就代表要帮我们简化一些繁复的动作
在使用 IntersectionObserver 时,总是要进行的动作就是要建立实体然後注册目标元素,我希望可以只用一个指令就处理完这些事。这个需求应该可以包装一个「建构函式」来处理。

2. 这个工具需要侦测到元素进入可视窗口时,进行指定样式的变化
侦测元素的部分 IntersectionObserver 会帮我们处理,而元素的样式变化最快的方式就是利用 class 的增减了,但用 class 可能会有撞名的风险,所以我们改用 data-* 属性好了。而且改变属性这件事应该由工具处理,而不是我们都要写一遍。

3. 除了样式变化,还希望可以设定 Callback,让我们做一些额外的操作
由於在第一点已经决定设计一个「建构函式」来帮我们创建 IntersectionObserver 实体,那原本在创建时该传入的 Callback Function 和 options 物件就必须要先提供给「建构函式」,它才能帮我们建立实体。

 

开始实践

# 元素的样式变换机制

整理完需求及机制後,就来开始动手吧,首先是为了让元素可以透过 data-* 属性的变化来转换样式,所以为想要有动画效果的元素加上 data-appear="hide",表示元素还没进入画面,未来只要把 data-appear 改成 show ,元素就会有样式的变化。

<div class="box" data-appear="hide"></div>
.box {
  width: 500px;
  height: 500px;
  transition: 0.3s;
}
.box[data-appear="hide"] {
  background: white;
}
.box[data-appear="show"] {
  background: pink;
}

 

# 自动建立实体与注册元素

再来程序部分,先宣告一个叫做 Appear 建构函式,其中会有一个函式 init,要用来创建 IntersectionObserver 实体以及注册要观察的元素,未来只要执行 appear.init() 一行就搞定了,而且因为已经确定只要有 data-appear 属性的元素就是要观察的对象,所以直接全部抓出来注册即可。

const Appear = function () {
  this.init = function (callback, options) {
    this.observer = new IntersectionObserver(callback, options);

    const container = options.root || document;
    const targetList = container.querySelectorAll("[data-appear]");
    targetList.forEach((el) => {
      this.observer.observe(el);
    });
  };
};
const appear = new Appear();

 

# Callback 包装与元素属性切换

前面有说,工具需要自动改变元素的 data-appear 属性,而不是由我们手动写在 Callback 中,所以看来传入 initcallback 参数不能直接放到 IntersectionObserver 中,需要另外在包装一次。

包装後的 Callback 就可以在元素进出画面时进行 data-appear 属性的调整了,另外我们也将 entry.target 和改变後的状态传进 initcallback 中,让使用工具的人可以取得额外资讯。

const Appear = function () {
  this.init = function (callback, options) {
    // IntersectionObserver 要触发的 Callback Function
    const obCallback = function (entries) {
      entries.forEach((entry) => {
        // 取得元素进入当下的状态
        let state = entry.target.getAttribute("data-appear");

        if (state === "hide" && entry.isIntersecting) {
          // 从 hide 的状态下进到画面时...
          state = "show";
          entry.target.setAttribute("data-appear", state);
          callback(entry.target, state);
        } else if (state === "show" && !entry.isIntersecting) {
          // 从 show 的状态下离开画面时...
          state = "hide";
          entry.target.setAttribute("data-appear", state);
          callback(entry.target, state);
        }
      });
    };

    this.observer = new IntersectionObserver(obCallback, options);

    const container = options.root || document;
    const targetList = container.querySelectorAll("[data-appear]");
    targetList.forEach((el) => {
      this.observer.observe(el);
    });
  };
};
const appear = new Appear();

 

# 自定义的函式参数

再来其实我觉得原本 IntersectionObserver 的 options 参数设定有点不是很容易懂,所以我们自己设计一个新的物件做为 init 的参数设定,以下是它的属性名称及预设值,顺便也把 callback 一起放进去了:

const defaultOptions = {
  container: null,
  offsetTop: 0,
  offsetRight: 0,
  offsetBottom: 0,
  offsetLeft: 0,
  threshold: 0,
  callback: function () {},
};

这样在使用工具的人只要传一个参数到 init 中就好,而且属性名称也比较好了解,传进去後只要在 init 内部再转换成原本 IntersectionObserver 接受的格式即可。

const Appear = function () {
  this.init = function (userOptions) {
    const options = { ...defaultOptions, ...userOptions };
    const obOptions = {
      root: options.container,
      rootMargin: [
        `${options.offsetTop}px`,
        `${options.offsetRight}px`,
        `${options.offsetBottom}px`,
        `${options.offsetLeft}px`,
      ].join(" "),
      threshold: options.threshold,
    };
    //...其他省略
    this.observer = new IntersectionObserver(obCallback, obOptions);
  };
};
const appear = new Appear();

 

# 防呆机制与注销

最後为了避免有人重复执行 init,我们在最前面进行判断来阻挡,额外也可以再做一个关闭 IntersectionObserver 的功能:

const Appear = function () {
  this.init = function (userOptions) {
    if (this.observer) return;
    //...其他省略
  };
  // 注销所有观察元素并释放 observer
  this.destroy = function () {
    if (!this.observer) return;
    this.observer.disconnect();
    this.observer = null;
  };
};
const appear = new Appear();

 

实际使用

以上的 JS 程序码我们可以包成一支 appear.js 档案,只要未来有专案需要使用时直接引入并且呼叫 init 就可以了:

<style>
  .box {
    width: 500px;
    height: 500px;
    margin: 30px auto;
    transition: 0.3s;
  }
  .box[data-appear="hide"] {
    background: white;
  }
  .box[data-appear="show"] {
    background: pink;
  }
</style>

<div class="box" data-appear="hide"></div>
<div class="box" data-appear="hide"></div>
<div class="box" data-appear="hide"></div>

<script src="appear.js"></script>
<script>
  appear.init({
    threshold: 0.5,
    callback: function (target, state) {
      console.log(target, state);
    },
  });
</script>

 

这样的工具是不是非常实用呢?而且能够自己做出一个以後也能够不断使用的小套件真的是非常有成就感,有兴趣的小夥伴们也可以发挥自己的创新,把这个小工具不断的扩充,增加新功能喔。完整原始码,我就放在 CodePen,文章中如果有不清楚的,可以再去看看。


<<:  Day23 - 针对 Metasploitable 3 进行渗透测试(4) - 认识 Metasploit

>>:  Leetcode: 99. Recover Binary Search Tree | 含C++笔记

子元件向父元件传值与讯息

人类沟通需要技巧,程序语言靠的是方法。 我们除了可以透过 Props 来让父元件传值给子元件外,也...

Day 18 : 静态爬虫

今天就来讲讲最基本的静态爬虫吧,这里要装的套件是bs4(BeautifulSoup4),它是用来做静...

Day31 ATT&CK for ICS - Inhibit Response Function(3)

T0814 Denial of Service 攻击者为了破坏设备的功能,使用阻断服务的攻击,会在短...

Tcl语言和你 SAY HELLO!!

第七天 各位点进来的朋友,你们好阿 小的不才只能做这个系列的文章,但还是希望分享给点进来的朋友,知道...

[Day 07] 特徵图想让人分群 ~模型们的迁移学习战~ 第二季 (k-means 实作篇)

前言 昨天我们使用预训练模型EfficientNet去提取一张表情的高阶特徵图(1280张特徵图),...