gsap--《pink老师vivo官网实现》

html部分(图片和视频自行替换)

js 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vivo官网</title>
  <link rel="stylesheet" href="./css/index.css">
</head>

<body>
  <!-- 第一屏 -->
  <section class="screen section1">
    <div class="kv-content">
      <img src="./img/kv.webp" alt="">
    </div>
  </section>


  <!-- 第二屏幕 -->
  <section class="screen section2">
    <div class="summary-content">
      <video src="./media/summary.mp4" class="summary"></video>
      <p class="text1">
        流动山海纹¹ <br>
        光影层叠,山海流淌。
      </p>
      <p class="text2">
        东方灵韵 山海情
      </p>
    </div>
  </section>

  <!-- 第三屏幕 -->

  <section class="screen section3">
    <div class="color-img">
      <img src="./img/color1.webp" alt="" class="color1">
      <img src="./img/color2.webp" alt="" class="color2">
      <img src="./img/color3.webp" alt="" class="color3">
      <img src="./img/color4.webp" alt="" class="color4">
    </div>
  </section>

  <!-- 第四屏动画 -->
  <section class="screen section4">
    <div class="parallel">
      <div class="page1">
        <p class="title">美学创作大师</p>
        <video src="./media/video1.mp4" class="video1" muted></video>
        <div class="info">
          重塑美一秒 <br>
          自在享受视频拍摄吧!理
          想身材相机直出,让每一
          刻的出镜更自由纯粹。
        </div>
      </div>
      <div class="page2">
        <video src="./media/video2.mp4" class="video2" muted></video>
        <div class="info">
          <h4>放手去拍,自动成片⁹</h4>
          <p>Vlog 拍摄配套电影级滤镜,实时更新,免费</p>
        </div>
      </div>
    </div>
  </section>

  
  <!-- 第五屏动画 -->

    <!-- 第五屏动画 -->

    <section class="screen section5">
      <div class="rom-content">
        <div class="rom-txt">
          512GB
        </div>
        <div class="rom-img">
          <img src="./img/1.webp" alt="" class="pic1">
          <img src="./img/2.webp" alt="" class="pic2">
          <img src="./img/3.webp" alt="" class="pic3">
          <img src="./img/4.webp" alt="" class="pic4">
          <img src="./img/5.webp" alt="" class="pic5">
          <img src="./img/6.webp" alt="" class="pic6">
          <img src="./img/7.webp" alt="" class="pic7">
        </div>
      </div>
    </section>
  



  <!-- js部分 -->

  <script src="./js/gsap.min.js"></script>
  <script src="./js/ScrollTrigger.min.js"></script>
  <script src="./js/index.js"></script>
</body>

</html>

css部分

js 复制代码
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html {
  /* 计算方法  15 / (1920/100) */
  font-size: 0.78125vw !important;
  overflow-x: hidden;
}

body {
  /* height: 50000px; */
  background-color: #fafafa;
}

.screen {
  position: relative;
  width: 100%;
  height: 100vh;
}

.kv-content {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 100%;
  height: 100vh;
}

.kv-content img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* 第二屏幕的样式 */

.summary-content {
  position: absolute;
  top: 0;
  left: 50%;
  width: 100%;
  height: 100vh;
  transform: translate(-50%, 0);
}

.summary-content video {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.text1 {
  position: absolute;
  top: 60rem;
  left: 21rem;
  opacity: 0;
  font-size: 4rem;
  color: #fff;
}

.text2 {
  position: absolute;
  top: 29.266666666rem;
  left: 29.333333333rem;
  opacity: 0;
  font-size: 8rem;
  color: #fff;
}

/* 第三屏幕样式 */

.color-img {
  width: 49em;
  position: absolute;
  top: 12em;
  left: 40em;
}

.color1 {
  width: 100%;
  position: absolute;
  top: 0;
  left: 0;
  margin-left: 90em;
}

.color2 {
  width: 100%;
  position: absolute;
  top: 0;
  left: 0;
  margin-left: 100em;
  scale: 1.1;
}

.color3 {
  width: 100%;
  position: absolute;
  top: 0;
  left: 0;
  margin-left: 110em;
  scale: 1.2;
}

.color4 {
  width: 100%;
  position: absolute;
  top: 0;
  left: 0;
  margin-left: 120em;
  scale: 1.3;
}

/* 第四屏动画 */
.parallel {
  width: 100%;
  height: 100%;
  margin-top: 0;
  position: relative;
  overflow: hidden;
  z-index: 50;
  box-sizing: border-box;
}

.page1 {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  /* background-color: pink; */
}
.page2 {
  display: flex;
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 128em;
  /* background-color: skyblue; */
}


.page1 .title {
  width: 100%;
  text-align: center;
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-image: -webkit-linear-gradient(300deg, #4acbff 40%, #93ebff 47%, #ffd178 65%);
  font-size: 8em;
  font-family: HanyiVar-vivo-hwid-65;
  color: #000000;
  position: absolute;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
}

.page1 .video1 {
  width: 20.3em;
  position: absolute;
  left: 40em;
  top: 50%;
  transform: translateY(-50%);
}

.page1 .info {
  position: absolute;
  left: calc(50% + 7em);
  top: 0;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  opacity: 0;
  font-size: 1em;
  font-family: HanyiVar-vivo-hwid-65;
  color: #272727;
}



.page2 .video2 {
  margin-left: 10em;
  width: 68em;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.page2 .info {
  padding-top: 45px;
  margin-left: 10em;

  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
}


/* 第五屏动画 */


.rom-content {
  width: 100%;
  height: 100%;
  position: relative;
}

.rom-txt {
  width: 100%;
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  left: 0;
  font-size: 20em;
  text-align: center;
  font-family: HanyiVar-vivo-hwid-65;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-image: -webkit-linear-gradient(110deg, #58b7f4 35%, #9ee6fa 55%);
  background-size: 200% 100%;
  background-position: left center;
  
}

.rom-img {
  position: absolute;
  width: 127em;
  left: 0;
  top: calc(50% + 45px);
  transform: translateY(-50%);
}

.pic1,
.pic7 {
  width: 11.266666666em;
  position: absolute;
  bottom: 5.26em;
}

.pic2,
.pic6 {
  width: 13.066666666em;
  position: absolute;
  bottom: 3.17em;
}

.pic3,
.pic5 {
  width: 14.866666666em;
  position: absolute;
  bottom: 1.06em;
}

.pic4 {
  width: 18.75em;
  bottom: 0;
  margin-left: 54.125em;
}

.pic1,
.pic2,
.pic3 {
  left: 55.125em;
  z-index: -1;
}

.pic5,
.pic6,
.pic7 {
  right: 55.125em;
  z-index: -1;
}

.pic img {
  position: absolute;
}

js部分

js 复制代码
// 第一屏:图片缩放 + 视频展开
gsap.timeline({
  scrollTrigger: {
    trigger: ".section1",
    start: "top top",
    end: "+=1000",
    scrub: true
  }
})
.fromTo(".kv-content", { scale: 1 }, { scale: 0.5 })
.fromTo(".summary-content", { width: "50%", height: "50vh" }, { width: "100%", height: "100vh" }, "<");

// 第二屏:文字显隐 + 视频进度联动
gsap.timeline({
  scrollTrigger: {
    trigger: ".section2",
    start: "top top",
    end: "+=5000",
    scrub: true,
    pin: true,
    onUpdate: (self) => {
        // 播放视频
    const summary = document.querySelector('.summary')
    try {
      // 视频的播放进度随着滚动条变化
      // self.progress 整体进度  0~1 
      // summary.duration 视频的总时长
      // console.log(self.progress)
      summary.currentTime = self.progress * summary.duration
    }
    catch (e) {
      console.log(e)
    }
    }
  }
})
.to(".text1", { top: "20rem", opacity: 1 })
.to(".text1", { top: 0, opacity: 0 })
.to(".text2", { top: "27rem", opacity: 1 })
.to(".text2", { top: "24rem", opacity: 0 });

// 第三屏:色块流行动画(需保留独立 pin 固定)
ScrollTrigger.create({ trigger: ".section3", start: "top top", end: "+=1000", pin: true }); // 仅固定
gsap.timeline({
  scrollTrigger: {
    trigger: ".color-img",
    start: "top-=500 top",
    end: "+=3000",
    scrub: true
  }
})
.fromTo(".color1", { marginLeft: "90em", opacity: 0 }, { marginLeft: 0, opacity: 1 }, "<")
.fromTo(".color2", { marginLeft: "100em", scale: 1.3 }, { marginLeft: 0, scale: 1 }, "<")
.fromTo(".color3", { marginLeft: "110em", scale: 1.6 }, { marginLeft: 0, scale: 1 }, "<")
.fromTo(".color4", { marginLeft: "120em", scale: 1.9 }, { marginLeft: 0, scale: 1 }, "<")
.fromTo(".color1", { marginLeft: 0, opacity: 1 }, { marginLeft: "-120em", opacity: 1 }, ">")
.fromTo(".color2", { marginLeft: 0, scale: 1 }, { marginLeft: "-110em", scale: 1.3 }, "<")
.fromTo(".color3", { marginLeft: 0, scale: 1 }, { marginLeft: "-100em", scale: 1.6 }, "<")
.fromTo(".color4", { marginLeft: 0, scale: 1 }, { marginLeft: "-90em", scale: 1.9 }, "<");

// 第四屏:双页面切换 + 视频精准控制
ScrollTrigger.create({ trigger: ".section4", start: "top top", end: "+=3000", pin: true }); // 仅固定
gsap.timeline({
  scrollTrigger: {
    trigger: ".parallel",
    start: "top top",
    end: "+=3000",
    scrub: true
  }
})
.fromTo(".title", { opacity: 1 }, { opacity: 0 })
.fromTo(".video1", { marginTop: "100%" }, { 
  marginTop: 0,
  onStart: () => {
    const v = document.querySelector(".page1 .video1");
    if (v) { v.currentTime = 0; v.play(); }
  }
})
.fromTo(".info", { opacity: 0 }, { opacity: 1 })
.fromTo(".page1", { left: 0 }, { left: "-128em" }, ">")
.fromTo(".page2", { left: "128em" }, { 
  left: 0,
  onStart: () => {
    const v = document.querySelector(".page2 .video2");
    if (v) { v.currentTime = 0; v.play(); }
  }
}, "<");

// 第五屏:图片聚合动画
ScrollTrigger.create({ trigger: ".section5", start: "top top", end: "+=3000", pin: true }); // 仅固定
gsap.timeline({
  scrollTrigger: {
    trigger: ".rom-content",
    start: "top+=500 top",
    end: "+=2000",
    scrub: true
  }
})
.fromTo(".rom-txt", { opacity: 1, marginTop: 0 }, { opacity: 0, marginTop: "-7em" })
.fromTo(".pic4", { width: "18.75em" }, { width: "16.75em" })
.fromTo(".pic1", { left: "55.125em" }, { left: "14.3em" }, "<")
.fromTo(".pic7", { right: "55.125em" }, { right: "14.3em" }, "<")
.fromTo(".pic2", { left: "55.125em" }, { left: "26.1em" }, "<")
.fromTo(".pic6", { right: "55.125em" }, { right: "26.1em" }, "<")
.fromTo(".pic3", { left: "55.125em" }, { left: "39.8em" }, "<")
.fromTo(".pic5", { right: "55.125em" }, { right: "39.8em" }, "<");

这是一个非常典型且高质量的视差滚动案例,使用了业界顶尖的动画库 GSAP 及其插件 ScrollTrigger

一、 JavaScript 部分详细解析

这段 JS 代码主要负责监听页面滚动,并将滚动进度映射为元素的动画进度。

1. 第一屏:图片缩放 + 视频展开

代码作用: 当用户向下滚动时,大图缩小,同时底部的视频容器从屏幕中间一小块区域展开至全屏,制造一种"拉开帷幕"的视觉效果。

javascript 复制代码
// 创建一个 GSAP 时间轴,用于管理一连串的动画
gsap.timeline({
  // 配置滚动触发器
  scrollTrigger: {
    trigger: ".section1", // 触发动画的元素是第一屏
    start: "top top",     // 当 .section1 的顶部碰到视口的顶部时开始
    end: "+=1000",        // 动画持续滚动 1000px 的距离
    scrub: true           // 关键属性:将动画进度与滚动条进度绑定,滚动条拖动则动画倒退/前进
  }
})
// 动画1:图片缩小。从 scale: 1 到 scale: 0.5
.fromTo(".kv-content", { scale: 1 }, { scale: 0.5 })
// 动画2:视频容器展开。
// "<" 表示该动画与上一个动画同时开始(默认是等待上一个结束后才开始)
// 从宽50%高50vh 变为 宽100%高100vh(全屏)
.fromTo(".summary-content", { width: "50%", height: "50vh" }, { width: "100%", height: "100vh" }, "<");
2. 第二屏:文字显隐 + 视频进度联动

代码作用: 固定屏幕,随着滚动播放视频,并在特定时间点显示介绍文字。这是类似苹果官网的经典交互。

javascript 复制代码
gsap.timeline({
  scrollTrigger: {
    trigger: ".section2",
    start: "top top",
    end: "+=5000",      // 这里的滚动距离很长(5000px),意味着需要滚动很久,相当于把视频"拉长"了
    scrub: true,
    pin: true,          // 关键属性:钉住当前屏幕,直到动画结束。这创造了"一镜到底"的效果
    onUpdate: (self) => {
      // 滚动更新时的回调函数
      const summary = document.querySelector('.summary') // 获取 video 元素
      try {
        // 原理核心:利用滚动进度控制视频播放进度
        // self.progress 是一个 0 到 1 的值,代表当前动画完成了百分之多少
        // 视频当前时间 = 进度 * 总时长
        summary.currentTime = self.progress * summary.duration
      }
      catch (e) {
        console.log(e)
      }
    }
  }
})
// 文字动画逻辑:文字从下方移入,停留,然后向上移出消失
.to(".text1", { top: "20rem", opacity: 1 }) // 移入
.to(".text1", { top: 0, opacity: 0 })       // 移出
.to(".text2", { top: "27rem", opacity: 1 }) // 第二段文字移入
.to(".text2", { top: "24rem", opacity: 0 }); // 第二段文字移出
3. 第三屏:色块流行动画

代码作用: 展示多张色彩图片,它们从屏幕右侧飞入,汇聚展示,然后再次飞出屏幕左侧。

javascript 复制代码
// 仅仅用来固定屏幕,不做动画逻辑,为了让用户停留在这一屏看完动画
ScrollTrigger.create({ trigger: ".section3", start: "top top", end: "+=1000", pin: true });
gsap.timeline({
  scrollTrigger: {
    trigger: ".color-img",
    start: "top-=500 top", // 提前500px开始,为了让动画衔接更自然
    end: "+=3000",
    scrub: true
  }
})
// 入场动画:从右侧大距离移入
// "<" 符号表示所有图片同时开始移动,形成阵列感
.fromTo(".color1", { marginLeft: "90em", opacity: 0 }, { marginLeft: 0, opacity: 1 }, "<")
.fromTo(".color2", { marginLeft: "100em", scale: 1.3 }, { marginLeft: 0, scale: 1 }, "<")
// ... 省略部分类似代码
// 出场动画:向左侧飞出
// ">" 符号表示在上一个动画完成后执行,或者这里使用了 "<" 意味着同步开始?
// 代码中写的是 ">" 在第一个出场动画后,后续使用了 "<" 同步
.fromTo(".color1", { marginLeft: 0, opacity: 1 }, { marginLeft: "-120em", opacity: 1 }, ">")
.fromTo(".color2", { marginLeft: 0, scale: 1 }, { marginLeft: "-110em", scale: 1.3 }, "<");
// ... 省略部分类似代码
4. 第四屏:双页面切换 + 视频精准控制

代码作用: 类似幻灯片切换,第一页向左滑出,第二页从右侧滑入,并在切换时自动播放对应的视频。

javascript 复制代码
ScrollTrigger.create({ trigger: ".section4", start: "top top", end: "+=3000", pin: true });
gsap.timeline({
  scrollTrigger: {
    trigger: ".parallel",
    start: "top top",
    end: "+=3000",
    scrub: true
  }
})
.fromTo(".title", { opacity: 1 }, { opacity: 0 }) // 标题淡出
.fromTo(".video1", { marginTop: "100%" }, { 
  marginTop: 0,
  // onStart: 视频出现在屏幕时触发的回调
  onStart: () => {
    const v = document.querySelector(".page1 .video1");
    if (v) { v.currentTime = 0; v.play(); } // 重置并播放视频
  }
})
.fromTo(".info", { opacity: 0 }, { opacity: 1 })
// 页面滑动切换效果
.fromTo(".page1", { left: 0 }, { left: "-128em" }, ">") // 第一页移出视野
.fromTo(".page2", { left: "128em" }, { 
  left: 0, // 第二页移入视野
  onStart: () => {
    const v = document.querySelector(".page2 .video2");
    if (v) { v.currentTime = 0; v.play(); } // 播放第二个视频
  }
}, "<"); // "<" 确保 page1 和 page2 同时移动,形成无缝衔接
5. 第五屏:图片聚合动画

代码作用: 多张图片从散乱的位置向中心聚合,模拟"收纳"或"产品全家福"的展示效果。

javascript 复制代码
ScrollTrigger.create({ trigger: ".section5", start: "top top", end: "+=3000", pin: true });
gsap.timeline({
  scrollTrigger: {
    trigger: ".rom-content",
    start: "top+=500 top",
    end: "+=2000",
    scrub: true
  }
})
.fromTo(".rom-txt", { opacity: 1, marginTop: 0 }, { opacity: 0, marginTop: "-7em" }) // 文字上移消失
.fromTo(".pic4", { width: "18.75em" }, { width: "16.75em" }) // 中心图片微调
// 图片位置移动,这里的 left/right 值变化实现了聚合效果
.fromTo(".pic1", { left: "55.125em" }, { left: "14.3em" }, "<")
.fromTo(".pic7", { right: "55.125em" }, { right: "14.3em" }, "<")
// ... 其他图片同理,通过改变 left/right 值向中间靠拢

二、 HTML 结构解析

HTML 结构非常清晰,采用了语义化标签模块化布局

  1. 容器划分 (section) : 每个 <section> 对应一个全屏的功能模块。class="screen" 确保了每个模块最小高度为视口高度。
  2. 资源引用 :
    • <video>: 视频标签,注意 muted 属性(静音),因为现代浏览器策略不允许自动播放有声视频。
    • <img>: 图片资源。
  3. 层级关系 : 例如第四屏,使用了 .parallel 容器包裹 .page1.page2,这是为了实现水平滑动切换做铺垫,父容器需要有 overflow: hidden 和相对定位。

三、 CSS 样式解析

CSS 部分主要处理了布局基准和元素的初始状态。

  1. REM 适配方案 :

    css 复制代码
    html { font-size: 0.78125vw !important; }

    原理 : 这是一个经典的移动端/PC端适配方案。

    • 假设设计稿宽度为 1920px。
    • 100vw = 1920px
    • 如果我们希望 1rem = 15px(设计稿上的量度),那么 1rem = 15/1920 * 100vw ≈ 0.78125vw
    • 这样,当屏幕变小时,font-size 变小,所有用 rem 做单位的元素都会等比缩放,实现响应式。
  2. 绝对定位 :

    • 大量使用了 position: absolute。这是为了配合 GSAP 动画。因为 GSAP 经常需要控制 top, left, margin 等属性,绝对定位可以让元素脱离文档流,随意移动而不影响其他元素位置。
  3. 初始状态隐藏 :

    • 很多元素(如 .text1, .text2)初始 opacity: 0
    • GSAP 动画负责将它们显示出来。不要让 CSS 动画和 JS 动画冲突,初始状态交给 CSS 定义。

四、 核心原理与设计思路

1. 为什么要用 scrub: true

这是 ScrollTrigger 最核心的功能。普通的动画是播放完就结束了,而 scrub 将动画与滚动条绑定。

  • 作用: 用户向前滚,动画播放;向后滚,动画倒退。这赋予了用户对时间的控制权,极大地增强了交互感。
2. 为什么要用 pin: true

在长页面滚动中,如果内容不够长,用户滚动太快会错过动画。

  • 作用 : pin 将当前部分"钉"在屏幕上,强制用户继续滚动来"消耗"设定的 end 距离(如 +=5000),但页面看起来还停留在这一屏。这相当于延长了时间轴,让导演(开发者)有足够的时间展示细节。
3. 视频联动原理 (onUpdate)

视频播放通常是线性的(时间流逝)。要实现滚动控制视频,关键公式是: currentTime = progress * duration 这打破了视频原本的时间流速,让视频变成了一个可以被用户" scrubbing (擦洗)"的素材,而非被动播放的媒体。

五、 拓展知识点

位置参数 (<>) : * 在 GSAP Timeline 中,控制动画时机非常关键。 * 默认是排队执行(一个接一个)。 * < 表示"在上一个动画开始时开始"。 * > 表示"在上一个动画结束时开始"。 * 熟练使用这两个符号,可以轻松编排复杂的并行/串行动画。

这里为什么要用两个固定?

js 复制代码
ScrollTrigger.create({
trigger: ".section3",
start: "top top",
end: "+=1000",
pin: true 
}); // 仅固定
gsap.timeline({
scrollTrigger: {
trigger: ".color-img",
start: "top-=500 top",
end: "+=3000",
scrub: true 
}
}) 

实际上,这里并不是使用了"两个固定" ,而是将**"固定容器" "内部动画"这两个逻辑分离了。 让我详细拆解一下为什么要这么写,以及这段代码里隐藏的一个关键逻辑冲突**。

1. 为什么看起来像"写了两个"?

我们来看这两段代码的区别:

  • 代码块 A(负责固定):

    javascript 复制代码
    ScrollTrigger.create({ 
      trigger: ".section3", 
      start: "top top", 
      end: "+=1000", 
      pin: true 
    });
    • 作用 :它的唯一职责是把 .section3 这个大容器"钉"在屏幕上,不随着页面滚动。
    • 对象:外层容器。
  • 代码块 B(负责动画):

    javascript 复制代码
    gsap.timeline({
      scrollTrigger: {
        trigger: ".color-img", // 注意:触发源变了,是内部的图片容器
        start: "top-=500 top",
        end: "+=3000",
        scrub: true
        // 这里没有 pin: true
      }
    })
    • 作用:负责控制图片飞入飞出的动画逻辑。
    • 对象:内部元素。

2. 为什么要拆开写?(核心原理)

如果合并在一起写,通常会有冲突。这里拆开的主要原因是为了实现**"入场动画提前"**的效果。 请看时间轴的对比:

  • 固定的开始时间start: "top top"
    • 意思是:当 .section3 顶部碰到屏幕顶部时,开始固定。
  • 动画的开始时间start: "top-=500 top"
    • 意思是:当 .color-img 距离屏幕顶部还有 500px 的时候,动画就开始了。 如果不拆开写: 如果你把 pin: true 写在 gsap.timeline 里,那么固定和动画会同时开始。用户必须先把屏幕滚到完全固定位置,图片才开始动,这样会显得很死板。 拆开写的效果:
  1. 用户向下滚动。
  2. 当图片距离顶部还有 500px 时,动画先开始(图片开始飞入)。
  3. 用户继续滚动,当 section 顶部到达屏幕顶部时,容器被固定住(pin 生效)。
  4. 用户继续滚动,动画继续进行。 结论: 拆开是为了让动画的触发点比固定的触发点更早,实现一种"先动起来,再停住展示"的流畅衔接感。

总结

  1. 不是两个固定:实际上是一个固定+ 一个动画控制。
  2. 为什么要拆:为了分离"固定触发点"和"动画触发点",实现更灵活的入场效果(动画比固定更早开始)。
相关推荐
lxh01135 小时前
记忆函数 II 题解
前端·javascript
我不吃饼干5 小时前
TypeScript 类型体操练习笔记(三)
前端·typescript
华仔啊5 小时前
除了防抖和节流,还有哪些 JS 性能优化手段?
前端·javascript·vue.js
CHU7290355 小时前
随时随地学新知——线上网课教学小程序前端功能详解
前端·小程序
清粥油条可乐炸鸡5 小时前
motion入门教程
前端·css·react.js
这是个栗子6 小时前
【Vue3项目】电商前台项目(四)
前端·vue.js·pinia·表单校验·面包屑导航
前端Hardy6 小时前
Electrobun 正式登场:仅 12MB,JS 桌面开发迎来轻量化新方案!
前端·javascript·electron
树上有只程序猿6 小时前
新世界的入场券,不再只发给程序员
前端·人工智能
confiself6 小时前
deer-flow前端分析
前端
刘宇琪6 小时前
Vite 生产环境代码分割与懒加载优化
前端