阅读视图

发现新文章,点击刷新页面。

纯css实现一个沙漏动画

说在前面

沙漏大家都见过吧,使用js的话相信大家都能很轻易地实现一个沙漏动画吧,今天我们纯css来实现一个试试。

在线体验

码上掘金

codePen

codepen.io/yongtaozhen…

代码实现

html

<div class="container">
  <div class="hourglass">
    <div class="frame"></div>
    <div class="glass"></div>
    <div class="sand-top-stroke"></div>
    <div class="sand-top"></div>
    <div class="sand-bottom-stroke"></div>
    <div class="sand-bottom"></div>
    <div class="sand-flow"></div>
    <div class="sand-drop"></div>
    <div class="glass-reflection"></div>
  </div>
</div>
  • container:包裹整个沙漏,调整整个沙漏的定位
  • hourglass:沙漏的主容器
  • frame:沙漏的外框,一个木架子

  • sand-top-strokesand-bottom-stroke:存放沙子的玻璃容器

  • sand-topsand-bottom:上下部分的沙子

  • sand-flow:连接上下两部分沙漏的管道

  • sand-drop:滴落的沙子

  • glass-reflection:添加一个玻璃反光效果

css

通用变量

:root {
  --rotateTime: 10s;
}

定义沙漏动画时间,在动画中需要用到。

沙漏翻转

.hourglass {
  position: relative;
  width: 120px;
  height: 200px;
  margin: 0 auto;
  animation: rotate var(--rotateTime) linear infinite;
  transform-origin: center 100px;
}
@keyframes rotate {
  0% {
    transform: rotate(0deg);
  }
  45% {
    transform: rotate(0deg);
  }
  50% {
    transform: rotate(180deg);
  }
  99% {
    transform: rotate(180deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

计时结束(沙漏沙子漏完)后需要将整个沙漏框架翻转,在rotateTime时间内沙漏需要翻转2次,也就是说沙漏漏完一次的时间是rotateTime

沙堆减少和增加

.sand-top {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 90px;
  background-color: #f5d76e;
  clip-path: polygon(0 0, 100% 0, 51% 100%, 49% 100%);
  border-top-left-radius: 10px;
  border-top-right-radius: 10px;
  animation: sand-top var(--rotateTime) linear infinite;
}
.sand-bottom {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 0;
  background-color: #f5d76e;
  clip-path: polygon(49% 0, 51% 0, 100% 100%, 0 100%);
  animation: sand-bottom var(--rotateTime) linear infinite;
  border-bottom-left-radius: 10px;
  border-bottom-right-radius: 10px;
}
/* 上半部分沙子减少动画 */
@keyframes sand-top {
  0% {
    height: 90px;
    width: 100%;
  }
  49% {
    height: 0;
    width: 0;
    left: 50%;
    top: 90px;
  }
  50% {
    height: 0px;
    width: 100%;
    top: 0;
    left: 0;
  }
  99% {
    width: 100%;
    height: 90px;
  }
  100% {
    width: 100%;
    height: 90px;
  }
}

/* 下半部分沙子增加动画 */
@keyframes sand-bottom {
  0% {
    height: 0;
    width: 100%;
    bottom: 0;
    left: 0;
  }
  50% {
    height: 90px;
    width: 100%;
    bottom: 0;
    left: 0;
  }
  51% {
    height: 90px;
    width: 100%;
    bottom: 0;
    left: 0;
  }
  99% {
    height: 0px;
    width: 0;
    bottom: 90px;
    left: 50%;
  }
  100% {
    height: 0px;
    width: 0;
    bottom: 90px;
    left: 50%;
  }
}

上半部分沙堆需要先减少后增加,下半部分沙堆需要先增加后减少;通过改变高度、宽度和位置,模拟沙子流动的过程。

管道沙子滴落

@keyframes sand-drop {
  0% {
    opacity: 1;
    transform: translate(-50%, 0);
  }
  4.9%,
  9.9%,
  14.9%,
  ……,
  44.9%,
  49.9% {
    opacity: 0;
    transform: translate(-50%, 15px);
  }
  5%,
  10%,
  15%,
  ……,
  40%,
  45% {
    opacity: 0;
    transform: translate(-50%, 0);
  }
  5.1%,
  10.1%,
  ……,
  90.1%,
  95.1% {
    opacity: 1;
  }
  50%,
  55%,
  ……,
  100% {
    opacity: 0;
    transform: translate(-50%, -15px);
  }
  54.9%,
  59.9%,
  64.9%,
  ……,
  94.9%,
  99.9% {
    opacity: 0;
    transform: translate(-50%, -30px);
  }
}

通过控制透明度和位移,模拟沙子滴落的连贯动作,每5%的动画时间完成一个滴落动作,以10s为例的话也就是每0.5s完成一个滴落动作;因为沙子漏完之后整个沙漏需要翻转180°,所以沙子滴落的动画应该分为前后两段,前半段是从上往下滴( transform: translate(-50%, 15px) ),后半段是从下往上滴( transform: translate(-50%, -15px) )。

源码

gitee

gitee.com/zheng_yongt…

github

github.com/yongtaozhen…


  • 🌟 觉得有帮助的可以点个 star~
  • 🖊 有什么问题或错误可以指出,欢迎 pr~
  • 📬 有什么想要实现的功能或想法可以联系我~

公众号

关注公众号『 前端也能这么有趣 』,获取更多有趣内容。

发送 加群 还可以加入群聊,一起来学习(摸鱼)吧~

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,偶尔也会在自己的公众号『前端也能这么有趣』发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 🙌。

前端响应式网站编写套路

不知道你有没有和我有一样的疑问,像那种响应式的网站是怎么开发?用到了什么技术?这是一篇带你进入响应式开发的套路,看完基本上就能知道响应式网站开发套路,加上现在以组件化开发更是如虎添翼,废话不多说开始我们的开发之旅

环境

  • 前端:Next
  • Css框架:tailwindcss
  • UI框架:antd(其实也不用,在这个用到的很少,几乎没用)
  • 语言:TypeScript

前言

这次我们要开发的是个响应式官网首页只有首页部分,我们此次要模仿的是影视飓风首页主要包括头部、底部、中间部分三大块,这里只对头部和底部做讲解,首页全部的代码我已经全部上传 源码地址

在这也提醒各位不要频繁去刷别人的官网!!!!!!!!!

tailwindcss

这里简单的介绍下 【Next,antd,TS就不做介绍】,如果你用过像Bootstrap这类的UI框架应该了解

尺寸

响应式尺寸的前缀有 md lg xl等等,这些都代表对应的尺寸

前缀 尺寸
sm 640px及以上
md 768px及以上
lg 1024px及以上
xl 1280px及以上
2xl 15360px及以上

如果没有你要的尺寸或与你需要对应的尺寸有出入你也可以自己设置,tailwindcss 给开发者提供的修改的地方

import type { Config } from 'tailwindcss';

export default {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      screens: {
        xs: '349px', // 设置你需要的尺寸
      },
      colors: {
        background: 'var(--background)',
        foreground: 'var(--foreground)',
      },
    },
  },
  plugins: [],
} satisfies Config;

常用的class名称

  • text- fontSize
  • bg- background
  • mx- my- m- mb- ml- mr- margin
  • px- py- p- pl- pr- padding
  • w- h- width height
  • 还有其他类名在实战中再做介绍,上面一些用法也在实战中做介绍,还有一些组合用法也会在实战做介绍

tailwind CSS 规则 非常重要!!!!!

  • 非响应式类(如 hidden, flex)在所有屏幕尺寸下生效。
  • 响应式类(如 lg:flex, md:hidden)只在指定尺寸及以上生效。响应式类是以断点前缀: + 属性类
  • 优先级规则:当非响应式类与响应式类冲突时,响应式类会在其生效的屏幕尺寸范围内覆盖非响应式类

分析网页

欲先善其事,必先利其器,我先分析下页面

250607 141330.gif 通过上面的gif图我们看到网页便没有随着宽度变化而发生变化,在切换到移动模式后需要再次刷新页面才会发生变化,我们第一步需要将其改造为随着页面变化自动变化,底部导航也是一样这就不截图了

gif录制工具

头部编码与分析

image.png 头部导航主要分为三块左右布局,左边我又将其分为两块,为后面响应式做好准备,现在开始编码与分析,下面展示的都是部分代码,但不影响布局分析

<div
  className="fixed w-dvw overflow-hidden z-50 bg-white md:px-4  xs:px-2   sm:px-1.5  lg:px-20   flex justify-between items-center h-[70px]
">
 
    {/*左侧开始*/}
  <div className="flex ">
    <img className="w-[7.5rem] h-[2.25rem] mr-[70px]" src="/images/banner-login1.png" alt=""/>
    <YSJFNav isTrue={isTrue}/>
  </div>
    {/*左侧结束*/}

  {/*右侧开始*/}
  <div className=" text-gray-300">
    <svg onClick={() => setIsShow(!isShow)} className="lg:flex hidden size-6 rounded-full"
         xmlns="http://www.w3.org/2000/svg"
         fill="none" viewBox="0 0 24 24"
         strokeWidth={1.5} stroke="currentColor"
         style={{color: isTrue ? 'gray' : 'black', background: isTrue ? '#cecece' : '#f4f6f7',}}>
      <path strokeLinecap="round" strokeLinejoin="round"
            d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/>
    </svg>

    <svg onClick={() => setIsNav(!isNav)} className="lg:hidden size-6" xmlns="http://www.w3.org/2000/svg" fill="none"
         viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
      <path strokeLinecap="round" strokeLinejoin="round"
            d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"/>
    </svg>
  </div>
  {/*右侧结束*/}
  
</div>
<ul className="lg:flex hidden  items-center  text-[12px] transition text-[#c9c9c9]">
  <li
    className="w-[70px] mr-[60px] cursor-pointer hover:scale-110 transition hover:tracking-[2px]">影视飓风
  </li>
  <li
    className="w-[70px] mr-[60px] cursor-pointer hover:scale-110 transition hover:tracking-[2px]>飓风素材库
  </li>
  <li
    className="w-[70px] mr-[60px] cursor-pointer hover:scale-110 transition hover:tracking-[2px]>闪电分镜
  </li>
  <li
    className="w-[70px] mr-[60px] cursor-pointer hover:scale-110 transition hover:tracking-[2px]>太空之眼
  </li>
  <li
    className={isTrue ? "w-[70px]  cursor-pointer  hover:scale-110 transition hover:tracking-[2px]" : "w-[70px]  cursor-pointer  hover:scale-110 transition hover:tracking-[2px] hover:text-black"}>加入我们
  </li>
</ul>

最外层分析

  • 在最外层我们设置了fixed属性让他在页面滚动的时候一直固定在头部,设置宽度w-dvw= width: 100dvw让它宽度一直都是和设备宽度一样,同时我们加上 overflow-hidden 让超出部分直接隐藏
    • 在头部X轴我们加了响应式内边距,分别在md xs lg 对应的尺寸设置不同内边距
    • 头部导航高度我们这就直接写死一个高度h-[70px],这就是tailwindcss一个好处,不仅可以使用内置已有属性,还可以自己写需要的单位
    • flex justify-between items-center 两边居中排列

头部导航右侧分析

  • 这里用到两个图表,一个是PC像登录按钮,一个是移动端点击呼出菜单的按钮
  • 这里是需要设置响应式,在不同尺寸下显示不同的图标和事件
    • lg:felx hidde在屏幕大于等1024px显示,小于就不显示
    • lg:hidde在屏幕大于等1024px隐藏,小于显示

头部导航左侧分析

  • 使用felx布局只为了让他们在一行
  • 我们给图片添加了w-[7.5rem]``w-[2.25rem]直接将宽高写死,如果想做成响应式可以自行添加响应式
  • 右侧的导航YSJFNav.tsx
    • 这里也同样需要添加响应式,我们给ul添加响应式:lg:flex hidden在屏幕大于等1024px显示,小于就不显示,因为是在PC下显示我们就将字体大小写死,text-[#c9c9c9] 字体颜色
    • liw-[70px]就不作解释,cursor-pointer hover:scale-110 transition hover:tracking-[2px]鼠标显示成手,在移上去的时候文字放大字间距变宽
    • 到这头部布局和用到的样式就解释完毕

登录界面变化与分析

如果你看过原来官网会发现也是没有进行响应式设置,我们也是要对其进行改造,我们先看下效果

250607 160712.gif

<div className='fixed top-0 left-0 overflow-hidden w-full h-full bg-[#000000b3] text-white'>
  <div className='md:w-[50rem]  xs:w-11/12 absolute top-2/4 left-2/4 -translate-x-2/4 -translate-y-2/4 bg-white text-black '>
    <div className='p-1 flex justify-end cursor-pointer ' onClick={() => closeModal()}>
      <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className=" size-6">
        <path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12" />
      </svg>
    </div>

    <div className='flex'>

      <img className='w-96 md:flex hidden' src="images/login.jpg" />

      <div className='pt-[70px] pr-5 ml-5 w-full'>
        {/*tab*/}
        <ul className='flex text-gray-300 text-[12px] mb-3 lg:w-full relative'>
          <li className={flag ? 'mr-4 cursor-pointer' : "mr-4 cursor-pointer text-black"} onClick={() => loginOrRegister()}>密码登录</li>
          <li className={flag ? 'mr-4 cursor-pointer text-black' : "mr-4 cursor-pointer"} onClick={() => loginOrRegister()}>验证码登录</li>
        </ul>

        {/*登录注册*/}
        {flag ? <Register /> : <Login toRegister={() => loginOrRegister()} />}

        <Divider><span className='text-[12px] text-gray-400'>或</span></Divider>

        {/*微信登录*/}
        <div className='mb-5 flex justify-center  bg-white text-black rounded-md p-[10px] border-[#eee] border text-[12px] text-center'>
          <WechatFilled style={{ fontSize: 16, color: 'green' }} />
          <span className='ml-2'>微信登录</span>
        </div>

        {/*条款*/}
        <Clause />
      </div>
    </div>
  </div>
</div>

最外层分析

在底部我们加了一层遮罩同时为了脱离文档流我们用到了定位,fixed top-0 left-0 overflow-hidden w-full h-full bg-[#000000b3] text-white,固定定位 上左都是0 高度宽度100%,bg-[#000000b3]一种背景色,文字白色

表单区域分析

  • absolute top-2/4 left-2/4 -translate-x-2/4 -translate-y-2/4 居中布局
  • top-2/4 left-2/4 = top:50% left:50%
  • -translate-x-2/4 -translate-y-2/4 = translate-y:-50% translate-x:-50%
  • 如果把-translate-x-2/4前面的负号去掉translate-x-2/4 = translate-x:50%`,同理我们可以推出类似的类,如果要取负数只需要在类名前面添加负号就行
  • 同理这里也需要用到响应式:md:w-[50rem] xs:w-11/12,md是框架自带的xs是我自定义的在前面tailwindcss尺寸哪有介绍
  • 内容区域两块左右结构,在图片部分我们需要做响应式在指定尺寸下隐藏
    • md:flex hidden这里就不解释了

移动端菜单显示

好在现在的前端是以组件化进行开发,我们只需要将移动端单独编写就行了,通过点击事件显示就好了,这里主要还是用到了antdDrawer

 <Drawer width='85%' placement="right" closable={false} open={true} bodyStyle={{ padding: 0 }} >
  <div className='flex justify-between mb-4 p-[24px]'>
    <svg onClick={() => onClose('close')} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
      <path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12H12m-8.25 5.25h16.5" />
    </svg>
    <div onClick={() => onClose('login')} className='bg-[#f4f6f7] rounded-[5px] font-semibold px-[11px] py-[6px] text-[#24252c] text-[12px]'>登录</div>
  </div>
  <ul className=' text-base '>
    <li className='px-[24px] font-semibold py-[24px] active:bg-[#F4F6F7]'>影视飓风</li>
    <li className='px-[24px] font-semibold py-[24px] active:bg-[#F4F6F7]'>飓风素材库</li>
    <li className='px-[24px] font-semibold py-[24px] active:bg-[#F4F6F7]'>闪电风景</li>
    <li className='px-[24px] font-semibold py-[24px] active:bg-[#F4F6F7]'>太空之眼</li>
    <li className='px-[24px] font-semibold py-[24px] active:bg-[#F4F6F7]'>加入我们</li>
  </ul>
</Drawer>

在编写头部的时候,我们在右侧留下可以点击的部分

<div className=" text-gray-300">

    {/*PC登录按钮 */} 
  <svg onClick={() => setIsShow(!isShow)} className="lg:flex hidden size-6 rounded-full"
       xmlns="http://www.w3.org/2000/svg"
       fill="none" viewBox="0 0 24 24"
       strokeWidth={1.5} stroke="currentColor"
       style={{color: isTrue ? 'gray' : 'black', background: isTrue ? '#cecece' : '#f4f6f7',}}>
    <path strokeLinecap="round" strokeLinejoin="round"
          d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/>
  </svg>
  
  {/*移动端呼出菜单按钮 */} 
  <svg onClick={() => setIsNav(!isNav)} className="lg:hidden size-6" xmlns="http://www.w3.org/2000/svg" fill="none"
       viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor">
    <path strokeLinecap="round" strokeLinejoin="round"
          d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0ZM3.75 12h.007v.008H3.75V12Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm-.375 5.25h.007v.008H3.75v-.008Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"/>
  </svg>
</div>

底部编码与粗略分析

我们直接看修改后的响应式效果

250607 162749.gif

 <div className='w-full lg:flex justify-around hidden'>

  {listArray.length && listArray.map((item, index) => {
    return <div key={item.text} className='flex justify-center flex-col items-center mt-[40px] ' onMouseLeave={() => setIsShow(index)} onMouseEnter={() => setIsShow(index)}>
      <img className={`${item.isShow ? styles.entrance : styles.field}  w-[88px] h-[66px]`} src={item.isShow ? item.activeImg : item.img} alt="" />
      <p className={`${item.isShow ? styles['text-entrance'] : styles['text-field']} mt-[40px]`}>{item.text}</p>

      <div className={`{isShow && ${item.isShow ? styles['slogan-entrance'] : styles['slogan-field']} flex flex-col  items-center`}>
        <p className={`text-[12px]`}>{item.slogan}</p>
        <div className='flex'>
          {item.icon.map((img, index) => {
            return <img key={index} className='w-10 h-10  hover:scale-150 transition' src={img} alt="" />
          })}
        </div>
      </div>
    </div>
  })}
</div >
<div className="lg:hidden mt-12 ">

  <div className='px-4 mb-4'>
    <p className='text-sm text-[#646464 ] font-[600]'>ACCOUNT</p>
    <p className="text-[#24252C] font-[600] text-2xl">官方账号</p>
  </div>

  <ul className='px-4'>
    {
      listArray.map((item, index) => {
        return <div key={index} className='mb-6'>
          <li className=" bg-[#F4F6F7] p-4">
            <div className="flex justify-between items-center">
              <div className="flex items-center">
                <img className="w-16 h-12 mr-2" src={item.activeImg} alt=""/>
                <div>
                  <p className="text-[0.85rem] text-[#24252C] font-[600]">{item.text}</p>
                  <p className="text-[0.75rem] text-[#646464] font-[300]">{item.slogan}</p>
                </div>
              </div>
              {!item.isShow &&
                <svg onClick={() => isShow(index)} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
                     strokeWidth={1.5}
                     stroke="currentColor" className="size-6">
                  <path strokeLinecap="round" strokeLinejoin="round"
                        d="M3 4.5h14.25M3 9h9.75M3 13.5h9.75m4.5-4.5v12m0 0-3.75-3.75M17.25 21 21 17.25"/>
                </svg>}


              {item.isShow &&
                <svg onClick={() => isShow(index)} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
                     strokeWidth={1.5}
                     stroke="currentColor" className="size-6">
                  <path strokeLinecap="round" strokeLinejoin="round"
                        d="M3 4.5h14.25M3 9h9.75M3 13.5h5.25m5.25-.75L17.25 9m0 0L21 12.75M17.25 9v12"/>
                </svg>}

            </div>
          </li>

          {
            item.isShow &&
            <div className="bg-[#F4F6F7]  p-4 justify-between"
                 style={{display: 'grid', gridTemplateColumns: 'repeat(4, auto)', gap: '20px'}}>
              {item.icon.map((pItem, index) => {
                return <img key={index} src={pItem} alt=""
                            className="w-10 h-10 hover:scale-110 transition hover:tracking-[2px]"/>
              })}
            </div>
          }

        </div>
      })
    }
  </ul>
</div>

这次我们将底部分为PC和移动端两个组件编写,在两个组件之间进行,只需要在两个组件最外层的上加上lg:flexlg:hidden就能实现在不同尺寸上的显示,是不是很简单

文章只对头部和底部进行的分析,看到这你也就应该知道的响应式的开发套路,对于中间部分你应该也有思路怎么去开发

总结

  • 现在做下总结,响应式的网站就是通过给在不同尺寸下显示不同的内容和布局,但在移动下一些问题还需要特殊处理
  • 如果PC和移动端有较大出路,可以选择分为两个组件进行维护,还可以将一些相同的方法抽成一个HOOK
  • 源码地址

记住这张表

前缀 尺寸
sm 640px及以上
md 768px及以上
lg 1024px及以上
xl 1280px及以上
2xl 15360px及以上

重点记住规则 非常重要!!!!!

  • 非响应式类(如 hidden, flex)在所有屏幕尺寸下生效。
  • 响应式类(如 lg:flex, md:hidden)只在指定尺寸及以上生效。响应式类是以断点前缀: + 属性类
  • 优先级规则:当非响应式类与响应式类冲突时,响应式类会在其生效的屏幕尺寸范围内覆盖非响应式类

你以为的 Tailwind 并不高效,看看这些使用误区

“Tailwind 写得越多,越觉得混乱”“组件样式重复一堆”“设计师完全看不懂这坨 className”…

这些反馈你是否也听说过?

Tailwind CSS 被誉为“实用优先的 CSS 框架”,然而在实际项目中,很多团队用了 Tailwind,效率却不升反降
不是因为 Tailwind 本身不行,而是——你可能正踩在这些使用误区上

本文将围绕 6 个常见误区,逐一剖析:

  1. 把 Tailwind 当成原子 CSS 的“组合器”
  2. 滥用 @apply 和组件式提取
  3. 不配置 Design Token,直接用默认色板
  4. 缺少抽象语义类的规范
  5. 无视团队协作中的语义歧义
  6. 不懂插件生态和可配置性,白白造轮子

🎯 误区一:把 Tailwind 当成“低配版 SCSS”来写

有些团队习惯了 BEM 或 SCSS 的写法,迁移 Tailwind 后陷入一个坑:

“我只会把 .title {} 改写成 <div class="title">,然后 .title@apply text-lg font-bold 合成”

结果就是:

  • 每个组件依然维护 .xxx { @apply ... }
  • 每个 class 被重复“硬编码”,没有复用价值
  • 真正的 Tailwind 优势(原子化组合)彻底失效

这其实是把 Tailwind 当成 SCSS 的语法糖在用,不仅没提高效率,反而多了一层“拼贴工”。

建议:

  • 组件中直接使用原子类组合,不要回退为传统 class 命名法
  • 通过抽象语义 class(例如 btn-primary)统一复杂样式,避免 @apply 滥用

🔥 误区二:滥用 @apply,导致样式复用变得更难维护

Tailwind 支持 @apply,但很多人误解它是“推荐方式”。结果就是:

.btn {
  @apply px-4 py-2 rounded-md bg-blue-500 text-white;
}
.btn-secondary {
  @apply btn bg-gray-500;
}

这本质上已经偏离了 Tailwind 的精神。问题在于:

  • @apply 语义缺失,又回到了传统 CSS 的“找 class 写样式”流程
  • 一旦 .btn 修改,影响范围不可控(链式引用)
  • 无法动态响应状态(例如 hover:bg-blue-600dark:bg-blue-400

正确方式:

  • 抽象出来的 class 应该直接写在 class="" 中,比如 class="btn btn-primary"
  • 配合 UnoCSS 或 Tailwind plugin,使用 语义化原子类 实现动态组合(见下文)

🎨 误区三:直接使用 Tailwind 默认色板,导致主题难以统一

许多初学者习惯直接写:

<div class="bg-blue-500 text-gray-800">按钮</div>

看起来没毛病,但:

  • “blue-500” 具体代表什么品牌色?设计稿里用的是 #378AFF 你知道吗?
  • 一旦品牌换主色,100 个组件都要人工搜索替换?
  • 多人项目中,“每个人对 text-sm 的认知都不同”

这不是视觉认知问题,而是你没有建立设计 token 体系

推荐配置方式(tailwind.config.ts):

theme: {
  colors: {
    brand: {
      DEFAULT: '#378AFF',
      dark: '#2563EB',
      light: '#93C5FD'
    }
  },
  fontSize: {
    base: '16px',
    sm: '14px',
    lg: '18px'
  }
}

然后组件中统一用 bg-brand text-sm,做到真正的“设计系统驱动”。


🧱 误区四:class 混乱、语义缺失,导致组件难复用

很多初级 Tailwind 项目里的组件看起来像这样:

<div class="px-4 py-2 rounded-md bg-blue-500 text-white text-sm shadow-md hover:bg-blue-600">
  提交
</div>

逻辑没问题,但:

  • 这个组件到底是按钮?标签?还是 Toast?
  • 想要复用时只能 copy-paste,一改就坏
  • 业务迭代时,10 个类似组件居然样式不同(改过一点 padding、换了一个 shadow)

Tailwind 推荐语义化命名 + utility 组合方式,例如:

<button class="btn btn-primary">提交</button>

然后在 tailwind.config.ts 里添加:

plugins: [
  require('@tailwindcss/forms'),
  function ({ addComponents }) {
    addComponents({
      '.btn': {
        @apply px-4 py-2 rounded-md text-white text-sm;
      },
      '.btn-primary': {
        @apply bg-brand hover:bg-brand-dark;
      }
    })
  }
]

不仅可复用,还能集中管理样式变更。


🧠 误区五:团队协作不统一,样式风格“各写各的”

当团队使用 Tailwind 却没有协作规范时,经常出现以下现象:

  • A 开发写 text-sm, B 写 text-xs,页面出现 4 种字号
  • 有的写 rounded, 有的写 rounded-md,风格混乱
  • 多人维护组件时,一改颜色就影响全局,因为写死在组件里

Tailwind 鼓励显式地“写出来”,但这不代表可以无限自由拼接

解决方式:

  • 建立 token 规范:颜色、字体、尺寸统一配置
  • 使用 @shadcn/ui 作为组件层抽象(推荐组合 Tailwind + Radix + shadcn)
  • 使用 ESLint 插件强制校验 Tailwind class 的顺序和规范(如 eslint-plugin-tailwindcss

⚙️ 误区六:没用插件、没开 JIT、错失生态红利

很多团队用了 Tailwind,但配置还停留在最基本阶段:

  • 没开 JIT 模式(Just In Time 构建)
  • 没启用 dark mode(Tailwind 支持类选择器控制 dark 样式)
  • 没接入 tailwind-variants / clsx / cva 等原子类组合库
  • 对 UnoCSS 完全不了解(其实比 Tailwind 更自由,兼容性好)

Tailwind 的能力远不止于“写几个 class”——它已经是一个完整的样式编程语言了

举个例子:

你可以使用 tailwind-variants 这样写组件样式组合:

const button = tv({
  base: 'inline-flex items-center justify-center font-medium',
  variants: {
    intent: {
      primary: 'bg-brand text-white hover:bg-brand-dark',
      secondary: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
    },
    size: {
      sm: 'text-sm py-1 px-2',
      md: 'text-base py-2 px-4'
    }
  }
})

然后在组件中使用:

<button className={button({ intent: 'primary', size: 'md' })}>按钮</button>

写起来干净,组合灵活,完全符合设计系统的思想。


✅ 正确的 Tailwind 使用思维:构建语义原子设计系统

Tailwind 的真正优势不在于“快”,而在于:

构建一套“视觉样式与设计语言一致”的原子组件体系。

好的实践是:

  • 所有颜色、字号、间距抽象为 Design Token
  • 所有组件样式统一在配置文件或 plugin 中抽象复用
  • 使用 shadcn/ui + tailwind-variants + clsx 构建语义组件库
  • 使用 prettier-plugin-tailwindcss + ESLint 强化规范
  • 页面代码中只使用 最小必要 class + 语义化 class
  • 样式变化驱动来自配置层变动,而非组件层硬改

🧩 最后

Tailwind 并不是魔法,它只是一个极致实用主义的 CSS 工具包。

真正的工程实践中,Tailwind 的效率取决于你是否理解并规避以下误区:

误区 正确思路
把 Tailwind 当 SCSS 用 原子化组合 + 抽象语义 class
滥用 @apply 提炼插件组件 + 原子类组合方式
使用默认色板无品牌感 建立 Design Token 体系
样式写死、组件无法复用 抽象组件语义 class + 统一配置管理
多人协作风格混乱 使用 lint、格式化插件强制规范
没使用生态工具,错失红利 熟悉 tailwind-variants / UnoCSS 等替代方案

CSS选择器完全手册:精准控制网页样式的艺术

CSS选择器完全指南:从基础到高级应用

CSS选择器是前端开发中最基础也是最重要的概念之一,它决定了样式规则将应用于哪些HTML元素。本文将全面介绍CSS选择器的各种类型和使用方法,帮助您掌握精确控制页面样式的技巧。

一、CSS基础概念回顾

在深入探讨选择器之前,让我们先回顾几个CSS基础概念:

  1. 声明(Declaration) :一个属性与值的键值对,如color: red;
  2. 声明块(Declaration Block) :由大括号{}包围的一组声明
  3. 选择器(Selector) :指定样式规则将应用于哪些HTML元素
  4. 规则集(Ruleset) :选择器加上声明块的完整组合

css

/* 这是一个完整的规则集 */
h1 {
  color: blue;      /* 声明 */
  font-size: 24px;  /* 声明 */
}

二、基本选择器类型

2.1 元素选择器(类型选择器)

元素选择器直接使用HTML标签名来选择元素,是最简单的选择器类型。

css

/* 选择所有<p>元素 */
p {
  color: #333;
}

/* 选择所有<h1>元素 */
h1 {
  font-size: 2em;
}

2.2 类选择器

类选择器以点号(.)开头,选择具有特定class属性的元素。

html

<p class="warning">这是一条警告信息</p>

css

.warning {
  color: red;
  font-weight: bold;
}

一个元素可以有多个类,类名用空格分隔:

html

<p class="warning urgent">紧急警告!</p>

2.3 ID选择器

ID选择器以井号(#)开头,选择具有特定id属性的元素。ID在文档中应该是唯一的。

html

<div id="header">网站标题</div>

css

#header {
  background-color: #f0f0f0;
  padding: 20px;
}

2.4 通配选择器

通配选择器(*)匹配任何元素,常用于重置样式。

css

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

三、组合选择器

组合选择器可以将多个简单选择器组合在一起,实现更精确的选择。

3.1 后代选择器(空格)

选择某个元素内部的所有特定后代元素,不论嵌套层级。

css

/* 选择<div>内所有的<p> */
div p {
  line-height: 1.6;
}

3.2 子元素选择器(>)

只选择直接子元素,不选择更深层级的后代。

css

/* 只选择<ul>的直接<li>子元素 */
ul > li {
  list-style-type: square;
}

3.3 相邻兄弟选择器(+)

选择紧接在另一个元素后的兄弟元素,且二者有相同的父元素。

css

/* 选择紧跟在<h1>后的<p> */
h1 + p {
  margin-top: 0;
}

3.4 通用兄弟选择器(~)

选择某个元素之后的所有同级元素。

css

/* 选择<h1>之后的所有<p>兄弟元素 */
h1 ~ p {
  color: #666;
}

四、属性选择器

属性选择器根据元素的属性及属性值来选择元素。

4.1 基本属性选择器

css

/* 选择有title属性的元素 */
[title] {
  border-bottom: 1px dotted #999;
}

/* 选择type属性值为"text"的<input>元素 */
input[type="text"] {
  border: 1px solid #ccc;
}

4.2 属性值匹配选择器

css

/* 选择href属性以"https"开头的<a>元素 */
a[href^="https"] {
  color: green;
}

/* 选择src属性以".png"结尾的<img>元素 */
img[src$=".png"] {
  border: 2px solid blue;
}

/* 选择class属性包含"logo"的元素 */
[class*="logo"] {
  opacity: 0.8;
}

五、伪类选择器

伪类选择器用于定义元素的特殊状态。

5.1 链接相关伪类

css

/* 未访问的链接 */
a:link {
  color: blue;
}

/* 已访问的链接 */
a:visited {
  color: purple;
}

/* 鼠标悬停状态 */
a:hover {
  color: red;
  text-decoration: underline;
}

/* 激活状态(鼠标按下未释放) */
a:active {
  color: green;
}

5.2 表单相关伪类

css

/* 获得焦点的输入框 */
input:focus {
  outline: 2px solid orange;
}

/* 被禁用的表单元素 */
input:disabled {
  background-color: #eee;
}

/* 被选中的复选框或单选按钮 */
input:checked {
  border-color: blue;
}

5.3 结构伪类

css

/* 选择第一个子元素 */
li:first-child {
  font-weight: bold;
}

/* 选择最后一个子元素 */
li:last-child {
  border-bottom: none;
}

/* 选择第3个子元素 */
li:nth-child(3) {
  color: red;
}

/* 选择奇数子元素 */
tr:nth-child(odd) {
  background-color: #f9f9f9;
}

/* 选择偶数子元素 */
tr:nth-child(even) {
  background-color: #fff;
}

/* 选择唯一的子元素 */
div:only-child {
  margin: 0 auto;
}

六、伪元素选择器

伪元素选择器用于选择元素的特定部分而不是元素本身。

6.1 ::before和::after

css

/* 在每个<p>前插入内容 */
p::before {
  content: "→ ";
  color: green;
}

/* 在每个<p>后插入内容 */
p::after {
  content: " ←";
  color: green;
}

6.2 ::first-letter和::first-line

css

/* 选择第一个字母 */
p::first-letter {
  font-size: 2em;
  float: left;
}

/* 选择第一行 */
p::first-line {
  font-weight: bold;
}

6.3 ::selection

css

/* 选择用户选中的文本 */
::selection {
  background-color: yellow;
  color: black;
}

七、选择器优先级与特异性

当多个选择器应用于同一个元素时,CSS有一套优先级规则决定哪个样式生效。

7.1 特异性计算规则

特异性由四个部分组成:[内联样式, ID选择器, 类/属性/伪类选择器, 元素/伪元素选择器]

  • 内联样式:1,0,0,0
  • ID选择器:0,1,0,0
  • 类/属性/伪类选择器:0,0,1,0
  • 元素/伪元素选择器:0,0,0,1

7.2 优先级示例

css

*               /* 0,0,0,0 - 最低 */
li              /* 0,0,0,1 */
ul li           /* 0,0,0,2 */
ul ol+li        /* 0,0,0,3 */
h1 + [rel=up]   /* 0,0,1,1 */
ul ol li.red    /* 0,0,1,3 */
li.red.level    /* 0,0,2,1 */
#header         /* 0,1,0,0 */
style=""        /* 1,0,0,0 - 最高 */

7.3 !important规则

!important可以覆盖所有其他规则,但应谨慎使用。

css

p {
  color: red !important;
}

八、实用选择器技巧

8.1 组合使用选择器

css

/* 选择class为"btn"且disabled的按钮 */
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* 选择表格中除了第一行外的所有奇数行 */
tr:not(:first-child):nth-child(odd) {
  background-color: #f5f5f5;
}

8.2 选择空元素

css

/* 选择内容为空的<div> */
div:empty {
  display: none;
}

8.3 选择特定语言的元素

html

<p lang="en">Hello!</p>
<p lang="fr">Bonjour!</p>

css

/* 选择法语内容 */
p:lang(fr) {
  font-style: italic;
}

九、现代CSS选择器新特性

9.1 :is()和:where()伪类

css

/* 传统写法 */
header h1, 
header h2, 
header h3 {
  margin-bottom: 0;
}

/* 使用:is()简化 */
header :is(h1, h2, h3) {
  margin-bottom: 0;
}

/* :where()特异性总是0 */
:where(header) h1 {
  margin-bottom: 0;  /* 特异性=0,0,0,1 */
}

9.2 :has()伪类(父选择器)

css

/* 选择包含<img>的<a> */
a:has(img) {
  border: 1px solid #ccc;
}

/* 选择包含至少一个<li>的<ul> */
ul:has(> li) {
  padding-left: 20px;
}

9.3 焦点相关伪类

css

/* 任何获得焦点的元素 */
:focus {
  outline: 2px solid blue;
}

/* 当浏览器认为元素应该显示焦点状态时 */
:focus-visible {
  outline: 2px dashed green;
}

/* 当元素及其后代获得焦点时 */
:focus-within {
  background-color: #f0f8ff;
}

十、选择器最佳实践

  1. 保持简洁:避免过于复杂的选择器
  2. 避免过度使用ID选择器:特异性太高难以覆盖
  3. 合理使用类选择器:可复用性高
  4. 注意性能:浏览器从右向左解析选择器
  5. 使用有意义的命名:如.btn-primary而非.blue-btn

css

/* 不推荐 - 过于具体 */
div#main-content ul.nav li a.active {
  color: red;
}

/* 推荐 - 更简洁 */
.nav-link.active {
  color: red;
}

结语

CSS选择器是前端开发中的强大工具,掌握各种选择器的使用方法可以让你更精确地控制页面样式。从简单的基础选择器到复杂的组合选择器,再到现代的伪类选择器,合理运用这些工具可以大幅提高你的开发效率和样式控制能力。

记住,好的CSS不仅仅是让页面看起来漂亮,还要考虑可维护性、性能和可访问性。选择器的合理使用是实现这些目标的关键之一。

深入浅出 CSS 基础:从概念到选择器实战

引言

在前端开发的世界里,CSS(层叠样式表)就像是一位神奇的化妆师,能够让原本朴素的 HTML 页面变得绚丽多彩。它不仅可以控制页面的布局,还能为元素添加各种动态效果,提升用户的交互体验。今天,就让我们一起来深入了解 CSS 的基础知识,尤其是各种选择器的使用。

一、CSS 基础概念

1.1 声明(Declaration)

在 CSS 里,一个属性与值的键值对被称为声明(declaration)。例如:

color: red;

这里 color 是属性,red 是对应的值,它们共同构成了一个声明。

1.2 声明块(Declaration Block)

一个声明块由一对花括号 {} 包裹,其中可以包含多个声明。每个声明之间用分号 ; 分隔。示例如下:

p {
    color: red;
    font-size: 16px;
    line-height: 1.5;
}

在这个例子中,p 是选择器,大括号内的内容就是声明块,包含了三个声明。

1.3 选择器(Selector)

选择器用于指定声明块作用的元素。简单来说,就是告诉浏览器哪些元素需要应用这些样式。上面例子中的 p 就是选择器,它表示页面中所有的 <p> 元素都会应用声明块里的样式。

1.4 规则集(Ruleset)

多个选择器和对应的声明块组合在一起就构成了规则集。一个 CSS 文件通常包含多个规则集,每个规则集负责为特定的元素或元素组设置样式。

1.5 CSS 层叠样式表

CSS 之所以被称为层叠样式表,是因为当多个样式规则应用到同一个元素时,会根据一定的规则来决定最终显示的样式。这些规则包括选择器的优先级、样式的来源等。

二、CSS 选择器详解

2.1 兄弟选择器

兄弟选择器用于选择与某个元素处于同一层级的其他元素。常见的兄弟选择器有两种:紧邻兄弟选择器和通用兄弟选择器。

紧邻兄弟选择器(+

紧邻兄弟选择器选择的是某个元素之后紧邻的同级元素。例如:

h1 + p {
    color: red;
}

这个选择器会选择所有紧跟在 <h1> 元素之后的 <p> 元素,并将它们的文字颜色设置为红色。

通用兄弟选择器(~

通用兄弟选择器选择的是某个元素之后的所有同级元素。例如:

h1 ~ p {
    color: blue;
}

这个选择器会选择所有在 <h1> 元素之后的 <p> 元素,无论它们之间是否有其他元素间隔,并将它们的文字颜色设置为蓝色。

2.2 伪类选择器

CSS 伪类用于定义元素的特殊状态,如鼠标悬停、激活、获取焦点等。常见的伪类选择器有:

  • :hover:当鼠标悬停在元素上时应用样式。
p:hover {
    background-color: yellow;
}
  • :active:当元素被激活(如按钮被点击)时应用样式。
button:active {
    background-color: red;
    color: white;
}
  • :focus:当元素获取焦点(如输入框被选中)时应用样式。
input:focus {
    border: 2px solid blue;
}

2.3 结构伪类选择器

结构伪类选择器根据元素在文档树中的位置来选择元素。其中比较常用的是 :nth-child() 选择器。

:nth-child() 选择器

:nth-child() 选择器可以根据元素在其父元素中的位置来选择元素。例如:

.container p:nth-child(3) {
    background-color: yellow;
    color: black;
}

这个选择器会选择类名为 container 的元素下的第三个子元素,并且这个子元素必须是 <p> 标签,然后将其背景颜色设置为黄色,文字颜色设置为黑色。

三、总结

CSS 的基础知识和选择器是前端开发中不可或缺的一部分。通过合理运用声明、声明块、选择器和规则集,我们可以为 HTML 页面添加丰富多样的样式。而兄弟选择器、伪类选择器和结构伪类选择器则为我们提供了更精确的元素选择方式,让我们能够实现更加复杂和动态的页面效果。希望大家通过本文对 CSS 有了更深入的理解,在今后的开发中能够灵活运用这些知识。

在实际开发中,我们还可以结合 HTML 结构和 JavaScript 来实现更多交互效果。不断学习和实践,才能让我们在前端开发的道路上越走越远。

CSS函数即将到来——它们将改变你的编码方式

CSS中的函数?没错,伙计!我太需要这个功能了!

就像其他编程语言一样,CSS现在也要引入函数功能了。那些说CSS不是编程语言的反对者们现在还有什么话说?

在过度兴奋之前,让我们深入了解一下!

基础概念:定义和使用函数

想象一下,你正在为一组元素添加样式,它们都需要虚线边框。通常你会这样写:

div {  
 border2px dashed black;  
}  
p {  
 border2px dashed black;  
}  
span {  
 border2px dashed black;  
}

虽然不算太糟,但如果能直接说"嘿CSS,给我一个虚线边框"然后完事,岂不是更好?

这就是@function的用武之地。通过它,你可以像编程语言一样定义可重用的函数:

@function --dashed-border() {  
 result: 2px dashed black;  
}

然后像变魔术一样在任何地方应用它:

div {  
 border--dashed-border();  
}

太棒了!现在每次需要虚线边框时,只需调用--dashed-border()。剩下的工作交给CSS。

带参数的函数

想让功能更酷炫吗?函数可以接受参数。想象一下动态设置不同边框颜色:

@function --dashed-border(--color: red) {  
 result: 2px dashed var(--color);  
}

现在你可以这样做:

div {  
 border--dashed-border(blue); /* 2px dashed blue */  
}

再也不用到处硬编码颜色了。一个函数,无限可能。

不止边框:尺寸和布局函数

因为CSS函数返回值,你可以在widthmargin甚至计算中使用它们:

@function --double-size(--size10px) {  
 result: calc(var(--size) * 2);  
}

.box {  
 padding--double-size(15px); /* 30px */  
}

或者,假设你想为网格创建灵活的间距系统:

@function --gap(--scale1) {  
 result: calc(var(--scale) * 8px);  
}

.grid {  
 display: grid;  
 gap--gap(2); /* 16px */  
}

这使你的间距系统更加可预测,并且易于在设计系统中调整。

类型检查和默认值

CSS函数一个非常酷的特性是支持类型检查。这意味着你可以指定函数应该期望的值类型,有助于防止错误。你可以定义:

  • length:值是有效的CSS长度(如pxem%
  • color:只使用有效颜色
  • number:只允许数值(如123
  • angle:角度值如degrad

示例:

@function --rotate-element(--angle45deg) {  
 result: rotate(var(--angle));  
}

.box {  
 transform--rotate-element(90deg);  
}

如果你尝试传递非角度值(如px),它将不起作用,从而防止潜在的样式错误。

是的,函数也支持默认值,就像前面颜色示例中看到的那样。

函数嵌套函数

当你开始组合函数时,事情会变得更加强大:

@function --shadow-color(--color: black) {  
 result: rgba(var(--color), 0.5);  
}

@function --box-shadow(--color: black, --size10px) {  
 result: 0 0 var(--size--shadow-color(--color);  
}  
.card {  
 box-shadow--box-shadow(red, 20px);  
}

这里,一个函数调用另一个函数来动态生成半透明阴影颜色。这为复杂样式设计开辟了许多创造性的可能性。

浏览器支持

好了,在你过于兴奋并开始重构所有样式表之前,有个问题需要注意:CSS函数仍在开发中

目前,它们仅在Chrome Canary中通过特性标志提供。所以,除非你喜欢冒险,否则你还不能在正式环境中使用它们。如果你想尝试,需要:

  1. 下载Chrome Canary
  2. 访问chrome://flags
  3. 启用Experimental Web Platform features

然后,砰——你就进入未来了。

CSS的未来

是的,CSS函数本质上是混合器(mixins),但原生内置在CSS中。不需要预处理器,不需要额外设置——纯粹的CSS魔法。

它们使你的样式更可重用、更易读、更易维护。你可以避免重复代码,更有效地定义设计标记,甚至可以创建以前只能通过JavaScript或Sass等预处理器实现的动态效果。

在所有浏览器完全支持之前,请密切关注更新,尝试可能的功能,为CSS变得更智能、更简洁、更有趣的未来做好准备。

因为说实话,任何能让我们免于一遍又一遍编写相同样式的东西?那都是胜利。


原文地址medium.com/@arnoldgunt…
作者:Arnold Gunter

如果您觉得内容对您有帮助,欢迎在看、点赞、分享 ⬇️❤️⬇️

欢迎关注公众号【前端小石匠】,一起学习,共同进步~

匀速旋转动画的终极对决:requestAnimationFrame vs CSS Animation

引言:旋转动画的隐藏陷阱

在现代Web开发中,实现一个流畅的无限旋转动画似乎是个简单任务。但当我深入探究时,发现这个看似基础的需求背后隐藏着性能陷阱数学精度问题浏览器渲染机制的深层奥秘。本文将带你从一段常见的requestAnimationFrame实现出发,深度剖析两种技术方案的优劣,并揭示浏览器动画渲染的底层原理。

一、原始方案解剖:requestAnimationFrame的功与过

1.1 值得称赞的设计

const element = document.querySelector('.son');
let startTime;
const rotateSpeed = 360; // 每秒旋转360度

function animate(timestamp) {
    if (!startTime) startTime = timestamp;
    const deltaTime = timestamp - startTime;
    const angle = (deltaTime / 1000) * rotateSpeed;
    element.style.transform = `rotate(${angle}deg)`;
    requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

这段代码的亮点在于:

  • 使用requestAnimationFrame而非setInterval,确保与浏览器刷新率同步
  • 基于时间差计算角度,保证速度恒定
  • 简洁明了的核心逻辑

1.2 致命隐患分析

然而,这段代码存在两个关键问题:

问题一:角度值无限增长

// 运行10分钟后:
angle = (600,000ms / 1000) * 360 = 216,000°

过大的角度值会导致:

  1. 浮点数精度丢失(JavaScript使用64位双精度浮点数)
  2. 内存占用持续增长
  3. 潜在的性能下降

问题二:起始跳变 第一帧渲染时,deltaTime可能已达到16ms(60Hz刷新率),导致元素从0°突然跳到5.76°,产生视觉跳跃。

二、底层原理透视:时间循环与硬件加速

2.1 requestAnimationFrame的时间陷阱

requestAnimationFrame并非完美的定时器:

  • 帧率波动:60Hz显示器目标16.67ms/帧,但实际可能15-20ms
  • 后台标签页节流:浏览器会降低非活动标签的rAF频率
  • 时间戳精度:DOMHighResTimeStamp精度为µs,但受系统限制
graph TD
    A[requestAnimationFrame回调] --> B[计算样式]
    B --> C[布局计算]
    C --> D[绘制]
    D --> E[合成]

2.2 CSS动画的硬件加速奥秘

CSS动画的渲染路径完全不同:

graph LR
    A[CSS Animation] --> B[创建独立图层]
    B --> C[GPU加速]
    C --> D[跳过布局/重绘]
    D --> E[直接合成]

关键优势:

  • 脱离主线程:动画在合成线程运行
  • GPU加速:transform/opacity属性触发硬件加速
  • 自动优化:浏览器内部处理循环逻辑

在CSS中,以下属性会触发GPU加速:

  • transform: translate3d()
  • transform: translateZ()
  • transform: rotate3d()
  • opacity
  • filter
  • will-change

三、工业级实现方案

3.1 优化后的requestAnimationFrame方案

const element = document.querySelector('.son');
let lastTime = null;
let totalAngle = 0;
const rotateSpeed = 360; // 度/秒
const MAX_ANGLE = 360; // 安全阈值

function animate(timestamp) {
    if (lastTime === null) lastTime = timestamp;
    
    // 计算帧时间差(秒)
    const deltaTime = (timestamp - lastTime) / 1000;
    lastTime = timestamp;
    
    // 安全累加角度
    totalAngle += rotateSpeed * deltaTime;
    
    // 防止数值过大导致的精度问题
    if (totalAngle > MAX_ANGLE * 1000) {
        totalAngle = totalAngle % MAX_ANGLE;
    }
    
    element.style.transform = `rotate(${totalAngle % MAX_ANGLE}deg)`;
    element.style.transformOrigin = 'center center';
    
    requestAnimationFrame(animate);
}

// 启动动画
requestAnimationFrame(animate);

// 暂停控制
function toggleAnimation() {
    if (lastTime !== null) {
        cancelAnimationFrame(animationId);
        lastTime = null;
    } else {
        animationId = requestAnimationFrame(animate);
    }
}

3.2 CSS Animation最佳实践

<style>
  .rotating-element {
    transform-origin: center center;
    animation: rotate linear infinite;
    animation-duration: var(--rotate-duration, 1s);
    /* 触发GPU加速 */
  transform: translateZ(0); 
  /* 或者使用 will-change: transform; */
  }
  
  @keyframes rotate {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
  }
</style>

<script>
  // 动态调整速度
  function setRotationSpeed(speed) {
    const duration = 360 / speed; // 秒/圈
    document.documentElement.style.setProperty(
      '--rotate-duration', 
      `${duration}s`
    );
  }
  
  // 暂停控制
  function pauseRotation() {
    document.querySelector('.rotating-element').style.animationPlayState = 'paused';
  }
</script>

四、设计哲学思考

4.1 浏览器渲染管线的启示

现代浏览器的渲染管线分为五个阶段:

  1. JavaScript → 2. Style → 3. Layout → 4. Paint → 5. Composite
graph LR
    A[JavaScript] --> B[Style]
    B --> C[Layout]
    C --> D[Paint]
    D --> E[Composite]

关键洞察

  • rAF动画必须走完所有五个阶段
  • CSS动画在符合条件时可跳过Layout和Paint阶段
  • transform/opacity动画直接进入Composite阶段

4.2 开发者心智模型升级

"让浏览器的归浏览器,让JavaScript的归JavaScript"

这一哲学体现在:

  1. 职责分离:CSS处理声明式表现,JS处理交互逻辑
  2. 性能边界:浏览器对CSS动画的优化远超手动JS实现
  3. 未来兼容:新特性如@scroll-timeline将扩展CSS动画能力

4.3 技术选型决策树

graph TD
    A[需要动画?] --> B{是否简单变换?}
    B -->|是| C[CSS动画]
    B -->|否| D{需要复杂逻辑?}
    D -->|是| E[rAF + WebGL]
    D -->|否| F[Web Animations API]
    C --> G[GPU加速]
    E --> H[主线程控制]
    F --> I[JS/CSS混合]

结论:选择金字塔

根据我们的分析和测试,形成以下优先级金字塔:

  ★ 优先使用CSS Animation
    - 简单变换(旋转/缩放/位移)
    - 透明度变化
    - 基础过渡效果
    
  ★★ 考虑Web Animations API
    - 需要JS控制的复杂序列
    - CSS/JS混合动画
    
  ★★★ 使用requestAnimationFrame
    - 物理引擎集成
    - 画布(Canvas)动画
    - 特殊效果无法用CSS实现时
    
  ★★★★ 终极方案:WebGL
    - 3D复杂场景
    - 粒子系统
    - 高性能游戏

对于匀速旋转动画这个具体需求,CSS Animation是无可争议的最佳选择。它提供了:

  1. 真正的硬件加速
  2. 恒定60FPS的流畅度
  3. 简洁的声明式语法
  4. 接近零的性能开销

最后建议:定期使用Chrome DevTools的Performance和Rendering面板分析动画性能,浏览器自身提供的工具永远是最佳的性能优化指南。

CSS 实现九宫格缩放(9-slice scaling 不变形拉伸)

前言

一些游戏中的窗口通常都有一些炫酷的效果

比如这样的

image.png

这样的

image.png

还有这样的

image.png

哎等等,你发现没有,后面两张的窗口长得很相似,只是第二张比较矮,第三张比较高,那么是不是要做两张素材来支持这种形式呢?

当然不用,在游戏开发中,通常会使用一种叫做 “9宫格缩放(9-slice scaling) ” 的技术,这是一种非常常见且极其实用的图形技术,9-slice scaling 是一种对位图图像进行缩放的方法,它将图像划分成 9 个区域(3 行 × 3 列),像这样

╔════╦═══════╦════╗
║ TL ║  TopTR ║
╠════╬═══════╬════╣
║ L  ║Center ║ R  ║
╠════╬═══════╬════╣
║ BL ║ Bottom║ BR ║
╚════╩═══════╩════╝

四个角(TL, TR, BL, BR) :固定尺寸,不缩放,用来保持 UI 的边角样式(圆角、高光、装饰等)。

四条边(Top, Bottom, Left, Right) :只在一个方向拉伸:

  • Top/Bottom 水平拉伸
  • Left/Right 垂直拉伸

中心区域(Center) :在 水平和垂直方向都可以拉伸,填满剩下的空间。

不得不说,发明九宫格缩放的人就是个天才,这大大减少了美术资源用量。

聪明的你肯定联想到了你那睿智的 UI 同事,他也总是给你一些让你抓耳挠腮的背景图,让你在使用时总是有图片拉伸变形的问题。

那么前端有没有类似的用法呢?

有的兄弟有的

哦?你以为我又要介绍某个 auto 系列插件是吗?

image.png

还真让你猜错了,今天我要说的是几个 CSS 属性。它们分别是:

  • border-image-source
  • border-image-slice
  • border-image-repeat

通过这三个属性,就可以做到让图片做到不变形的拉伸效果。

现在拿一个图片举例

image.png

可以看到,我拿红线将在此图上花了一个井字形,这个井字可不是随便画的,它大有来头,它标记了我允许它形变的地方,和保持不变的地方。

image.png

这样四个角将会保持我们目前看到的样子(原始比例),当图片宽度或高度改变时,只会改变可拉伸区域的表现。

这张图片的原始尺寸是 1064*141,且上面的截图是以原始尺寸展示的。从此图来看(梯形),它大概率要承受比它更长或更短的内容,目测一下,可以看到右上角、左上角的不允许形变区域大概是 50px、而左右不允许形变区域大概是 200px、由于这张梯形图且不考虑上下拉伸,所以下面则不能设置固定部分,否则会使左右两边发生断层(有时斜率不一致)。

所以我们这么写

      .frame-container {
        /* 1. 先设定边框宽度(上 右 下 左)*/
        border-width: 50px 200px 0px 200px;
        border-style: solid;
        border-color: transparent; /* 透明即可,边框颜色会被 image 覆盖 */
      }

我们将正常的边框属性按照刚刚目测的数值写成这样,现在看起来无事发生,因为边框是透明的。

那么我们接着写:

      .frame-container {
        /* 1. 先设定边框宽度(上 右 下 左)*/
        border-width: 50px 200px 0 200px;
        border-style: solid;
        border-color: transparent; /* 透明即可,边框颜色会被 image 覆盖 */

        /* 2. 指定 border-image 源图片 */
        border-image-source: url('./a.png');
        /* 为了演示,就给它一个动态宽度和高度 */
        width: 1064px;
        height: 141px;
        /* 3. 指定 slice,也就是按 30px / 10px / 20px / 15px 把图片分成 9 块 */
        border-image-slice: 50 200 0 200 fill;
      }

这里为了方便演示,我们将容器大小设置为图片的原始大小,目前的效果看起来有点奇怪,左右两边看起来被折断了。

      .frame-container {
        /* 1. 先设定边框宽度(上 右 下 左)*/
        border-width: 50px 200px 0 200px;
        border-style: solid;
        border-color: transparent; /* 透明即可,边框颜色会被 image 覆盖 */

        /* 2. 指定 border-image 源图片 */
        border-image-source: url('./a.png');
        /* 为了演示,就给它一个动态宽度和高度 */
        width: 1064px;
        height: 141px;
        /* 3. 指定 slice,也就是按 30px / 10px / 20px / 15px 把图片分成 9 块 */
        border-image-slice: 50 200 0 200 fill;
        /* 4. 指定拉伸(stretch)—— 
         上边/下边 拉伸水平;左边/右边 拉伸垂直;中间那块自由拉伸/填充 */
        border-image-repeat: stretch;
        transition: 1s;

        /* 5. 你可以给中间区域再加个背景,比如纯白或者另一张图 */
        background-color: white;
        /* 如果想用内容图,也可以启用下面这行: */
        /* background: url("content.png") no-repeat center/cover; */
        box-sizing: border-box; /* 确保 width/height 包括 border */
      }

加上 box-sizing: border-box; 图片看起来正常多了,我还加了其他必须的代码,现在这张图已经具备我们开头说的能力了。

加一个 hover 简单看一下:

      /* 鼠标移进去改变尺寸,看看效果,模拟内容变少 */
      .frame-container:hover {
        width: 400px;
        height: 141px;
      }

GIF 2025-6-4 17-53-17.gif

总结

通过上面的演示,可以发现,通过这种方式可以实现横向纵向(取决于图片异形,有的只能横向,有的只能纵向,有的横纵都可)的不变形拉伸。

学会了这招,再也不用向 UI 要多张切图、拆分切图了,还不谢谢我。

为什么现代前端拒绝原生编程?组件化与框架的降维打击

📖前言

在曾经的年代,前端工程师被戏称为“切图仔”,它们拿着低人一等的工资和待遇,默默地承受着一切...直到后来,Ajax、Node.js、Vue、React等技术的不断涌现,前端迎来了黎明,这些技术不仅给前端带来了技术上的提升,还带来了前端地位的改变,今天我们就来详细聊聊其中一部分,前端是如何由原生CSS到如今的组件化开发,前端是如何由原生DOM编程到如今的各大框架各显神通!




原生CSS

请大家想一想,我们想要将以下的表格进行装饰,让它看起来更加美观整洁,其中的内容排列更加有序,我们该如何实现?🤔🤔🤔

<div class="container">
    <table id="friends" >
    <thead>
        <tr>
            <th>姓名</th>
            <th>家乡</th>
        </tr>
    </thead>

    <tbody>
        <tr>
            <td>小李</td>
            <td>上海</td>
        </tr>
        <tr>
            <td>小邓</td>
            <td>深圳</td>
        </tr>
        <tr>
            <td>小王</td>
            <td>北京</td>
        </tr>
    </tbody>
    </table>
    </div>

如果是曾经在写CSS时,我们只能通过手动去加边框,调整边框粗细大小,加背景色,调整文字大小排列......

 <style>
        /* 表格整体样式 */
        #friends {
            width: 100%;
            border-collapse: collapse; /* 合并边框 */
            margin: 20px 0; /* 上下边距 */
        }

        /* 表头样式 */
        #friends th {
            background-color: #f8f9fa; /* 浅灰背景 */
            color: #212529; /* 深灰文字 */
            padding: 12px; /* 内边距 */
            text-align: left; /* 文本左对齐 */
            border-bottom: 2px solid #dee2e6; /* 底部粗边框 */
        }

        /* 表格行样式 */
        #friends tr {
            height: 48px; /* 行高 */
        }

        /* 表格行悬停效果 */
        #friends tr:hover {
            background-color: #f1f3f5; /* 悬停时背景色 */
        }

        /* 表格单元格样式 */
        #friends td {
            padding: 12px; /* 内边距 */
            border-bottom: 1px solid #dee2e6; /* 底部细边框 */
        }
    </style>

可以看到,这样虽然可行,但却十分的麻烦,特别是在大型项目中,我们应该把时间和精力放在业务中,而不是如何实现CSS样式这样的细枝末节!于是组件化开发应运而生!




组件化开发

当我们引入组件库bootstrap后,你会发现,实现表格美化的效果只需要直接改个类名即可!😎😎😎

<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">  //引入组件库
<table id="friends" class="table table-striped">  //添加类名:table-striped

你能看到,无需我们自己写CSS,我们只需要复用组件库中的CSS样式,通过引入该组件库,就能轻松地完成效果!

image.png

实现效果截图



实际上,组件库就是通过封装可复用的标准化组件,提升开发效率、降低协作成本的,现在市面上有各式各样的组件库,每个组件库有非常多的组件任我们挑选,而且每家公司也有自己专门的组件库,例如微信的WEUI,饿了么的Element等,每家公司通过自己的组件库可以达到统一产品体验的效果

image.png

Element截图

image.png

WEUI截图



VUE & REACT

上面我们了解到,使用组件库可以让我们使用组件库里的组件,无需使用原生的方式自己写CSS,从而提升了开发效率,让我们能更专注于业务。

那么同样是为了专注于业务,我们在JS的DOM编程中是否可以这样做呢?🤔🤔🤔



原生JS的DOM编程

请看下面的代码,我们想要将JS中的JSON数组数据给放在HTML的thead标签中。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>原生JS</title>
    <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
</head>
<body>
    <div class="container">
    <table id="friends" class="table table-striped">
    <thead>
        <tr>
            <th>姓名</th>
            <th>家乡</th>
        </tr>
    </thead>

    <tbody>
        <tr>
            <td></td>
            <td></td>
        </tr>
    </tbody>
    </table>
    </div>
    <script src="https://unpkg.com/element-ui/lib/index.js"></script>
    
    <script>
    const oBody = document.querySelector('#friends tbody')
    //DOM 编程 动态的 数据
    const friends = [
        {
            "name": "王子恒",
            "hometown": "九江"
        },
        {
            "name": "刘红",
            "hometown": "赣州"
        },
    ];
    // html 字符串 = 数组.join 类型转换 
    oBody.innerHTML = friends.map(friend => 
        `
            <tr>
                <td>${friend.name}</td>
                <td>${friend.hometown}</td>
            </tr>
        `
    ).join('')
    </script>
</body>
</html>

如果是曾经,我们只能通过这种复杂的方式,使用DOM编程,通过map函数得到HTML字符串,再通过join转换最终放在tbody标签中。

这种方式是十分正确的,你还可以通过不同的方式来实现相同的效果,如果你是技术大佬,你能使用更高阶的算法,原生API,来实现,非常牛逼!

但请你仔细想想,在业务中,我们应该把时间和精力放在这上面吗?虽然有些场景下,你确实需要做到很多极致的优化,但大多数情况下我们是不是应该主要专注于业务的逻辑?而不是这些具体的实现,况且很多情况下,使用这种原生API的DOM编程会产生大量的重复工作,这会占据我们开发的大部分时间。所以我们的现代前端开发 不应该接受这种DOM编程来实现我们实际的业务

大人,时代变了!



VUE和REACT双子星的横空出世

在2013-2015年,诞生出了VUE和REACT这对双子星,可以说它们的出现改变了前端的历史长河,使前端开发人员从黑暗中看到了黎明的光!

image.png



框架的作用

VUE和REACT都是前端的框架,它们都能解决我们上述的原生DOM编程的问题,让我们无需再使用麻烦的DOM编程来实现上面的效果:将JS里的JSON数组放在HTML的tbody中。

我们就拿VUE来举例,下面是使用VUE实现上述效果的代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>聚焦于业务,而不是底层API</title>
    <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    
    <div class="container" id="app">

        <h1>{{ title }}</h1>
        <!-- 挂载点 -->
         <header></header>
        <table id="friends" class="table table-striped">
        <thead>
            <tr>
                <th>姓名</th>
                <th>家乡</th>
            </tr>
        </thead>
    
        <tbody>
            <tr v-for="friend in friends">
                <td>{{ friend.name }}</td>
                <td>{{ friend.hometown }}</td>
            </tr>
        </tbody>
        </table>
    </div>
    <script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/3.2.31/vue.global.min.js"></script>
    <script>
    const App = {
        // 申明数据的业务
        data(){
            return {
                title: 'ECUT 未来之星',
                friends: [
                {
                    "name": "王子恒",
                    "hometown": "九江"
                },
                {
                    "name": "刘红",
                    "hometown": "赣州"
                },
                ],
                  
            }
            
        }
    }
    // 挂载点 
    Vue.createApp(App).mount('#app')
    </script>
</body>
</html>

你会发现,只需要一个挂载点就能将我们想要的数据给绑定到指定的位置

image.png

实现效果



下面是两种实现方式的区别

需求 原生 JS 实现 Vue 实现
数据绑定到 HTML 手动操作 DOM(如 innerHTML 自动绑定({{ }} 或指令)
动态列表渲染 拼接字符串 + 循环插入 v-for 一键循环
数据更新同步视图 需手动重新渲染 响应式(自动更新)

是不是发现,Vue的实现更加简洁、优雅。

所以当尤雨溪发布了Vue之后,就得到了企业以及民间广泛使用,大家都对这个高效的框架认可,因为它足够强大,能够帮我们显著的提升开发效率!同样React也是,这两个框架都是非常健壮的框架,值得我们去仔细学习!




🎯总结

本文介绍了前端技术由原生开发的刀耕火种到组件化、Vue、React的全新时代,让由于本文是理论性文章,旨在表达Vue、React和组件化的历史与变化,所以就没有详细介绍Vue和React的具体技术点,大家如果想详细了解这两个框架的具体技术实现,推荐大家去阅读官方文档!



🌇结尾

感谢你看到最后,最后再说两点~
①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。
②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生

(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)

作者:3Katrina
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

❌