普通视图
CSS 选择器全解析:从基础语法到组件库样式修改,解决前端样式定位难题
前言:被 CSS 选择器 “卡壳” 的日常
“写了.btn-active
样式,为什么按钮没反应?”
“#nav .list li
和.nav-list li
到底谁能生效?”
“想改组件库的输入框样式,加了类却被覆盖?”
“用[class=btn]
匹配按钮,多了个类名就失效了?”
CSS 选择器是前端样式的 “定位工具”,但很多开发者停留在 “会用类和 ID” 的初级阶段,面对动态元素、组件库样式修改等场景时,要么写出冗余代码,要么陷入 “样式冲突” 的死循环。本文从 “基础语法→属性选择器深度解读→组件库样式修改实战” 三个维度,结合真实业务场景,帮你彻底掌握选择器的使用逻辑,从此告别 “样式调不通” 的烦恼。
一、CSS 选择器基础:构建样式的 “基石”
基础选择器是前端样式的核心,覆盖 80% 的简单场景。重点不在于 “记住语法”,而在于 “理解定位逻辑与适用场景”,避免滥用导致的样式混乱。
1.1 基础选择器分类与实战
按 “定位维度”,基础选择器可分为 “元素定位”“关系定位” 两类,下表整理了高频用法与场景:
选择器类型 | 语法示例 | 作用 | 适用场景 | 权重(优先级) |
---|---|---|---|---|
元素选择器 |
div p input
|
匹配所有指定标签的元素 | 全局统一标签样式(如 body 字体) | 1 |
ID 选择器 |
#header #login-form
|
匹配唯一 ID 的元素 | 页面唯一模块(如顶部导航) | 100 |
类选择器 |
.btn .card
|
匹配所有同类名元素 | 复用样式(按钮、卡片) | 10 |
后代选择器 |
.nav li #main .text
|
匹配祖先元素下的所有后代 | 嵌套元素(导航列表项) | 父选择器权重之和 |
子代选择器 | .nav > li |
匹配父元素的直接子元素 | 仅控制一级子元素(避免深层影响) | 父选择器权重之和 |
相邻兄弟选择器 | .item + .item |
匹配目标元素的下一个兄弟 | 兄弟元素分隔线(如列表项间距) | 基础权重之和 |
伪类选择器(基础) |
:hover :active
|
匹配元素状态 | 交互效果(按钮 hover 变色) | 10(类级权重) |
1.2 基础选择器核心误区
误区 1:滥用 ID 选择器
ID 选择器权重极高(100),一旦使用,后续很难用类覆盖样式。例如:
/* 错误:用ID定义通用按钮样式,后续无法用类修改 */
#submit-btn {
background: #409eff;
}
/* 即使加了类,权重不够也无法生效 */
#submit-btn.disabled {
background: #ccc; /* 权重100+10=110,可生效,但不如一开始用类灵活 */
}
/* 正确:用类选择器,后续可灵活扩展 */
.btn {
background: #409eff;
}
.btn.disabled {
background: #ccc; /* 权重10+10=20,轻松覆盖基础样式 */
}
误区 2:混淆 “子代” 与 “后代” 选择器
子代选择器(>
)只匹配直接子元素,后代选择器(空格)匹配所有后代,例如:
<ul class="nav">
<li>首页 <!-- 子代选择器匹配这里 -->
<ul>
<li>首页子菜单</li> <!-- 后代选择器匹配,子代选择器不匹配 -->
</ul>
</li>
</ul>
/* 子代选择器:仅匹配.nav的直接子li(首页) */
.nav > li {
font-weight: bold;
}
/* 后代选择器:匹配.nav下所有li(首页+首页子菜单) */
.nav li {
color: #333;
}
二、属性选择器深度解读:动态元素的 “定位神器”
属性选择器是 CSS 中最灵活的选择器之一,它通过 “元素属性名 / 属性值” 定位,无需依赖类名或 ID,尤其适合动态生成的元素(如循环渲染的表单、带自定义data-*
属性的组件)。很多开发者仅会用基础的 “精确匹配”,却忽略了它的高级能力。
2.1 6 种核心匹配模式(附场景对比)
属性选择器按 “匹配精度” 可分为 6 类,覆盖从 “模糊匹配” 到 “精确匹配” 的全场景:
匹配模式 | 语法示例 | 作用 | 适用场景 | 权重 |
---|---|---|---|---|
存在匹配 | [attr] |
匹配包含指定属性的元素 | 所有带data-* 的元素 |
10 |
精确匹配 | [attr=value] |
匹配属性值完全等于 value 的元素 | 精准定位表单控件(如type="text" ) |
10 |
包含匹配 | [attr*=value] |
匹配属性值包含 value 的元素 | 类名含特定关键词(如class*=btn- ) |
10 |
前缀匹配 | [attr^=value] |
匹配属性值以 value 开头的元素 |
data-type 前缀筛选(如data-type^=user- ) |
10 |
后缀匹配 | [attr$=value] |
匹配属性值以 value 结尾的元素 | 按文件格式筛选(如src$=.svg ) |
10 |
完整类名匹配 | [attr~=value] |
匹配属性值含 value 且用空格分隔的元素 | 多类名中精准匹配某一类(如class~=active ) |
10 |
2.2 实战场景:解决真实业务痛点
场景 1:动态表单控件定位(无需手动加类)
痛点:循环渲染的表单(如 Vue 的v-for
、React 的map
)无法提前定义类名,难以区分不同类型的输入框。
解决方案:用属性选择器按name
或type
定位:
<!-- 动态生成的表单(无法提前加类) -->
<form class="user-form">
<input type="text" name="username" placeholder="用户名">
<input type="password" name="password" placeholder="密码">
<input type="email" name="email" placeholder="邮箱">
<input type="tel" name="phone" placeholder="手机号">
</form>
/* 1. 匹配所有带name属性的输入框(存在匹配) */
.user-form input[name] {
width: 100%;
padding: 10px;
margin: 8px 0;
border: 1px solid #ddd;
border-radius: 4px;
}
/* 2. 精准匹配密码框(精确匹配) */
.user-form input[type=password] {
border-color: #e74c3c; /* 密码框红色边框警示 */
}
/* 3. 匹配邮箱和手机号(包含匹配:name含"e"或"phone") */
.user-form input[name*=e],
.user-form input[name*=phone] {
background: #f8f9fa; /* 特殊背景色区分 */
}
场景 2:自定义data-*
属性的状态控制
痛点:通过 JS 动态切换元素状态(如 “已选中”“待审核”),需同步修改样式,手动加类太繁琐。
解决方案:用属性选择器匹配data-status
:
<ul class="order-list">
<li data-status="paid">订单1(已支付)</li>
<li data-status="pending">订单2(待支付)</li>
<li data-status="cancelled">订单3(已取消)</li>
</ul>
.order-list li {
padding: 12px;
margin: 6px 0;
border-radius: 4px;
border: 1px solid #eee;
}
/* 按data-status匹配不同状态 */
.order-list li[data-status=paid] {
border-color: #2ecc71;
color: #27ae60;
background: #f8fff8;
}
.order-list li[data-status=pending] {
border-color: #f39c12;
color: #d35400;
background: #fff9f2;
}
场景 3:图片格式分类样式(后缀匹配)
痛点:页面中有多种格式的图片(PNG、SVG、WEBP),需给不同格式加特殊样式(如 SVG 加边框)。
解决方案:用[src$=格式]
后缀匹配:
<div class="image-gallery">
<img src="logo.png" alt="PNG图标">
<img src="banner.jpg" alt="JPG banner">
<img src="icon.svg" alt="SVG图标">
<img src="avatar.webp" alt="WEBP头像">
</div>
.image-gallery img {
width: 180px;
margin: 10px;
border-radius: 8px;
}
/* SVG图片加蓝色边框 */
.image-gallery img[src$=svg] {
border: 2px solid #3498db;
}
/* WEBP图片加阴影 */
.image-gallery img[src$=webp] {
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
2.3 属性选择器避坑指南
坑点 1:属性值带特殊字符未加引号
问题:属性值含空格(如data-type="user info"
)或连字符(如data-user-id
),未加引号导致选择器失效。
原因:CSS 语法中,属性值含特殊字符时,需用单引号或双引号包裹。
解决方案:
/* 错误:属性值含空格,未加引号,选择器无效 */
[data-type=user info] { color: red; }
/* 正确:用引号包裹属性值 */
[data-type="user info"] { color: red; }
[data-user-id='123'] { font-weight: bold; } /* 连字符建议加引号,更规范 */
坑点 2:混淆 “包含匹配” 与 “完整类名匹配”
问题:用[class*=active]
匹配class="btn-active-danger"
的元素,结果误匹配了不需要的元素。
原因:[class*=active]
是 “包含匹配”,只要类名含 “active” 就生效;若需精准匹配 “独立的 active 类”,需用[class~=active]
。
解决方案:
<button class="btn active">正常激活按钮</button>
<button class="btn-active-danger">危险按钮(含active关键词)</button>
/* 错误:包含匹配,会误匹配btn-active-danger */
[class*=active] { background: #3498db; }
/* 正确:完整类名匹配,仅匹配含独立active类的元素 */
[class~=active] { background: #3498db; }
三、外部修改组件库样式:突破 Scoped 隔离的 4 种正确方式
使用 Element UI、Ant Design Vue 等组件库时,最头疼的莫过于 “样式改不动”——Scoped 隔离、高权重选择器会阻止外部样式生效。以下 4 种方式经过实战验证,兼顾 “样式生效” 与 “避免全局污染”。
3.1 核心痛点:为什么组件库样式难修改?
-
Scoped 隔离:Vue/React 的
scoped
属性会给样式加唯一属性(如data-v-123
),外部样式无法穿透到组件内部; -
高权重选择器:组件库常用 “类 + 元素” 选择器(如
.el-btn span
),外部简单类选择器(如.my-btn
)权重不够; -
样式覆盖冲突:直接写全局样式会污染其他组件,导致意外样式变更。
3.2 4 种实战方案(附代码示例)
以 “修改 Element UI 按钮样式” 为例,演示不同场景的解决方案。
方案 1:深度选择器(穿透 Scoped,推荐局部修改)
适用场景:仅在当前组件内修改组件库样式,不影响全局。
原理:通过::v-deep
(Vue2)、:deep()
(Vue3)穿透 Scoped 的属性隔离,让外部样式作用于组件内部元素。
Vue3 实战示例:
<template>
<div class="custom-btn-group">
<!-- Element UI按钮 -->
<el-button type="primary">自定义主按钮</el-button>
</div>
</template>
<style scoped>
/* 关键:.custom-btn-group父容器 + :deep()穿透 */
.custom-btn-group :deep(.el-button--primary) {
background: #3498db; /* 覆盖默认蓝色 */
border-radius: 8px; /* 圆角 */
padding: 8px 24px; /* 调整内边距 */
}
/* 穿透修改hover状态 */
.custom-btn-group :deep(.el-button--primary:hover) {
background: #2980b9; /* 加深hover色 */
}
</style>
Vue2 实战示例(用/deep/
):
<style scoped>
.custom-btn-group /deep/ .el-button--primary {
background: #3498db;
}
</style>
避坑点:必须加 “父容器选择器”(如.custom-btn-group
),避免直接写:deep(.el-btn)
—— 否则会污染所有 Element UI 按钮。
方案 2:全局样式 + 精准父容器(适合批量修改)
适用场景:多个组件需要统一修改某类组件样式(如所有页面的按钮、输入框)。
原理:在非 Scoped 样式文件(如global.css
)中,用 “父容器 + 组件库选择器” 精准定位,避免全局污染。
实战示例:
/* global.css(无scoped) */
/* 仅修改.app-main容器内的Element UI按钮 */
.app-main .el-button--primary {
font-size: 16px;
border: none;
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.3);
}
/* 仅修改.form-container内的输入框 */
.form-container .el-input__inner {
height: 42px;
border-color: #ddd;
}
关键原则:父容器必须是 “业务相关的唯一容器”(如页面根容器.app-main
、表单容器.form-container
),不能用body
或html
作为父容器。
方案 3:CSS 变量覆盖(组件库支持时优先用)
适用场景:组件库提供 CSS 变量(如 Element Plus、Ant Design Vue),修改变量即可批量变更样式,无需写复杂选择器。
原理:组件库将核心样式(颜色、字体、间距)定义为 CSS 变量,外部只需重定义这些变量,即可 “一键换肤”。
Element Plus 实战示例:
<template>
<div class="variable-btn-group">
<el-button type="primary">变量修改按钮</el-button>
</div>
</template>
<style scoped>
/* 局部重定义Element Plus变量(仅作用于.variable-btn-group内) */
.variable-btn-group {
--el-color-primary: #e74c3c; /* 主色改为红色 */
--el-color-primary-light-3: #f19990; /* 主色浅3度 */
--el-border-radius-base: 8px; /* 基础圆角 */
}
/* 全局重定义(作用于整个项目,需写在无scoped的样式中) */
/* :root {
--el-color-primary: #2ecc71;
} */
</style>
优势:无需关心组件内部结构,避免因组件更新导致选择器失效;支持局部 / 全局修改,灵活性高。
方案 4:主题配置编译(全局定制化)
适用场景:项目初始化阶段,需要全局统一组件库风格(如企业定制主题色、字体)。
原理:通过组件库提供的主题工具(如 Element UI 的theme-chalk
),修改变量后重新编译样式,生成自定义主题包。
Element UI 主题定制步骤:
- 安装主题工具:
npm install element-theme -g
npm install element-theme-chalk -D
- 生成变量配置文件:
et -i element-variables.scss # 生成可修改的变量文件
- 修改
element-variables.scss
中的核心变量:
// 原变量:$--color-primary: #409eff !default;
$--color-primary: #3498db !default; // 自定义主色
$--font-size-base: 14px !default; // 基础字体大小
$--border-radius-base: 6px !default; // 基础圆角
$--button-padding-horizontal: 12px 24px !default; // 按钮内边距
- 编译自定义主题:
et # 生成dist目录,包含定制后的样式
- 在项目中引入自定义主题(替换默认样式):
// main.js
import './dist/index.css'; // 引入定制主题
import ElementUI from 'element-ui';
Vue.use(ElementUI);
优势:从根源修改样式,权重最高、无冲突,适合大型项目的全局主题定制。
3.3 组件库样式修改避坑原则
-
优先用变量覆盖:组件库支持 CSS 变量时,优先修改变量,而非写复杂选择器(减少维护成本);
-
拒绝全局!important:不要用
.el-btn { background: red !important; }
—— 高权重会导致后续无法覆盖,且污染全局; -
用 DevTools 查结构:通过浏览器 F12 查看组件库的 DOM 结构(如
.el-btn
的内部元素),避免 “猜选择器”; -
集中管理修改:将组件库样式修改放在单独文件(如
component-theme.css
),便于后续维护。
四、CSS 选择器权重:解决 “样式不生效” 的核心
很多开发者遇到 “样式不生效”,本质是 “权重不够” 或 “权重冲突”。理解权重计算规则,能从根源避免这类问题。
4.1 权重计算规则(4 级分级)
CSS 选择器的权重按 “优先级从高到低” 分为 4 级,用(a, b, c, d)
表示:
-
a(内联样式):元素的
style
属性(如<div style="color: red">
),a=1; -
b(ID 选择器):每个 ID 计 1 分(如
#header
),b 累加; -
c(类 / 伪类 / 属性选择器):每个类、伪类、属性选择器计 1 分(如
.btn
、:hover
、[type=text]
),c 累加; -
d(元素 / 伪元素选择器):每个元素、伪元素计 1 分(如
div
、::before
),d 累加。
对比逻辑:先比 a,a 大的权重高;a 相等比 b,b 相等比 c,以此类推。
4.2 权重实战示例
选择器语法 | 权重计算(a,b,c,d) | 生效优先级 |
---|---|---|
div |
(0,0,0,1) | 最低 |
.btn |
(0,0,1,0) | 高于元素选择器 |
.btn.active |
(0,0,2,0) | 高于单个类 |
#header .btn |
(0,1,1,0) | 高于双类选择器 |
div#header .btn.active |
(0,1,2,1) | 更高 |
<div style="color: red"> |
(1,0,0,0) | 高于 ID 选择器 |
五、总结:CSS 选择器的使用原则与未来趋势
5.1 核心使用原则
-
优先类选择器:类选择器权重适中(10),便于复用和覆盖,避免滥用 ID;
-
减少选择器嵌套:嵌套不超过 3 层(如
.nav .list .item
),简化结构,提升渲染性能; -
善用属性选择器:动态元素、表单控件优先用属性选择器,减少冗余类名;
-
组件库样式修改:按需选择方案:局部修改用深度选择器,批量修改用全局 + 父容器,全局定制用主题编译。
5.2 未来趋势:CSS4 选择器
CSS4 新增了多个实用选择器,虽未完全兼容所有浏览器,但值得关注:
-
:is(selector)
:简化多选择器写法,如:is(.header, .footer) p
替代.header p, .footer p
; -
:where(selector)
:与:is()
语法相同,但权重为 0,便于后续覆盖; -
:has(selector)
:根据子元素选择父元素(如div:has(p)
选中包含p
的div
),目前 Chrome 已支持。
附录:CSS 选择器速查表
选择器类型 | 语法示例 | 关键场景 | 权重 |
---|---|---|---|
元素选择器 |
div input
|
全局标签样式 | 1 |
ID 选择器 | #header |
页面唯一模块 | 100 |
类选择器 | .btn |
复用样式 | 10 |
后代选择器 | .nav li |
嵌套元素 | 父权重之和 |
子代选择器 | .nav > li |
直接子元素 | 父权重之和 |
属性选择器(存在) | [data-id] |
带自定义属性的元素 | 10 |
属性选择器(精确) | [type=text] |
精准表单控件 | 10 |
伪类选择器 |
:hover :nth-child(2)
|
元素状态 / 位置 | 10 |
深度选择器(Vue3) | :deep(.el-btn) |
组件库局部样式修改 | 父权重之和 |
总而言之,一键点赞、评论、喜欢加收藏吧!这对我很重要!
CSS属性继承与特殊值
🎨 SCSS 高级用法完全指南:从入门到精通
🚀 想让 CSS 写得更爽?本文手把手教你 SCSS 的各种实用技巧,让你的样式代码又好写又好管理!
📚 目录
为了实时查看,我这边使用工程化来练习:
1. 变量与作用域
1.1 局部变量与全局变量
// 全局变量
$primary-color: #3498db;
.container {
// 局部变量
$padding: 20px;
padding: $padding;
.item {
// 可以访问父级局部变量
margin: $padding / 2;
color: $primary-color;
}
}
// $padding 在这里不可用
1.2 !global 标志
.element {
$local-var: 10px;
@if true {
// 使用 !global 将局部变量提升为全局
$local-var: 20px !global;
}
}
// 现在可以在外部访问
.another {
padding: $local-var; // 20px
}
1.3 !default 标志
// 设置默认值,如果变量已存在则不覆盖
$base-font-size: 16px !default;
$primary-color: #333 !default;
// 这在创建主题或库时非常有用
1.4 Map 变量
// 定义颜色系统
$colors: (
primary: #3498db,
secondary: #2ecc71,
danger: #e74c3c,
warning: #f39c12,
info: #9b59b6,
);
// 使用 map-get 获取值
.button {
background: map-get($colors, primary);
&.danger {
background: map-get($colors, danger);
}
}
// 深层嵌套的 Map
$theme: (
colors: (
light: (
bg: #ffffff,
text: #333333,
),
dark: (
bg: #1a1a1a,
text: #ffffff,
),
),
spacing: (
small: 8px,
medium: 16px,
large: 24px,
),
);
// 获取深层值
.dark-mode {
background: map-get(map-get(map-get($theme, colors), dark), bg);
}
2. 嵌套与父选择器
2.1 父选择器 & 的高级用法
// BEM 命名法
.card {
padding: 20px;
&__header {
font-size: 18px;
}
&__body {
margin: 10px 0;
}
&--featured {
border: 2px solid gold;
}
// 伪类
&:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
// 父选择器在后面
.dark-theme & {
background: #333;
}
}
2.2 嵌套属性
.button {
// 嵌套属性值
font: {
family: 'Helvetica', sans-serif;
size: 14px;
weight: bold;
}
border: {
top: 1px solid #ccc;
bottom: 2px solid #999;
radius: 4px;
}
transition: {
property: all;
duration: 0.3s;
timing-function: ease-in-out;
}
}
2.3 @at-root 跳出嵌套
.parent {
color: blue;
@at-root .child {
// 这会在根级别生成 .child 而不是 .parent .child
color: red;
}
@at-root {
.sibling-1 {
color: green;
}
.sibling-2 {
color: yellow;
}
}
}
3. Mixins 高级技巧
3.1 带参数的 Mixin
// 基础 Mixin
@mixin flex-center($direction: row) {
display: flex;
justify-content: center;
align-items: center;
flex-direction: $direction;
}
// 使用
.container {
@include flex-center(column);
}
3.2 可变参数 (...)
// 接收任意数量的参数
@mixin box-shadow($shadows...) {
-webkit-box-shadow: $shadows;
-moz-box-shadow: $shadows;
box-shadow: $shadows;
}
// 使用
.card {
@include box-shadow(0 2px 4px rgba(0, 0, 0, 0.1), 0 4px 8px rgba(0, 0, 0, 0.05));
}
// 传递多个值
@mixin transition($properties...) {
transition: $properties;
}
.button {
@include transition(background 0.3s ease, transform 0.2s ease-out);
}
3.3 @content 指令
// 响应式 Mixin
@mixin respond-to($breakpoint) {
@if $breakpoint == 'mobile' {
@media (max-width: 767px) {
@content;
}
} @else if $breakpoint == 'tablet' {
@media (min-width: 768px) and (max-width: 1023px) {
@content;
}
} @else if $breakpoint == 'desktop' {
@media (min-width: 1024px) {
@content;
}
}
}
// 使用
.sidebar {
width: 300px;
@include respond-to('mobile') {
width: 100%;
display: none;
}
@include respond-to('tablet') {
width: 200px;
}
}
3.4 高级响应式 Mixin
$breakpoints: (
xs: 0,
sm: 576px,
md: 768px,
lg: 992px,
xl: 1200px,
xxl: 1400px,
);
@mixin media-breakpoint-up($name) {
$min: map-get($breakpoints, $name);
@if $min {
@media (min-width: $min) {
@content;
}
} @else {
@content;
}
}
@mixin media-breakpoint-down($name) {
$max: map-get($breakpoints, $name) - 1px;
@if $max {
@media (max-width: $max) {
@content;
}
}
}
// 使用
.container {
padding: 15px;
@include media-breakpoint-up(md) {
padding: 30px;
}
@include media-breakpoint-up(lg) {
padding: 45px;
}
}
3.5 主题切换 Mixin
@mixin theme($theme-name) {
@if $theme-name == 'light' {
background: #ffffff;
color: #333333;
} @else if $theme-name == 'dark' {
background: #1a1a1a;
color: #ffffff;
}
}
// 更灵活的主题系统
$themes: (
light: (
bg: #ffffff,
text: #333333,
primary: #3498db,
),
dark: (
bg: #1a1a1a,
text: #ffffff,
primary: #5dade2,
),
);
@mixin themed() {
@each $theme, $map in $themes {
.theme-#{$theme} & {
$theme-map: $map !global;
@content;
$theme-map: null !global;
}
}
}
@function t($key) {
@return map-get($theme-map, $key);
}
// 使用
.card {
@include themed() {
background: t(bg);
color: t(text);
border-color: t(primary);
}
}
4. 函数的妙用
4.1 自定义函数
// 计算 rem
@function rem($pixels, $base: 16px) {
@return ($pixels / $base) * 1rem;
}
.title {
font-size: rem(24px); // 1.5rem
margin-bottom: rem(16px); // 1rem
}
4.2 颜色操作函数
// 创建颜色变体
@function tint($color, $percentage) {
@return mix(white, $color, $percentage);
}
@function shade($color, $percentage) {
@return mix(black, $color, $percentage);
}
$primary: #3498db;
.button {
background: $primary;
&:hover {
background: shade($primary, 20%);
}
&.light {
background: tint($primary, 30%);
}
}
4.3 字符串操作
@function str-replace($string, $search, $replace: '') {
$index: str-index($string, $search);
@if $index {
@return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index +
str-length($search)), $search, $replace);
}
@return $string;
}
// 使用
$font-family: str-replace('Arial, sans-serif', 'Arial', 'Helvetica');
4.4 深度获取 Map 值
@function deep-map-get($map, $keys...) {
@each $key in $keys {
$map: map-get($map, $key);
}
@return $map;
}
$config: (
theme: (
colors: (
primary: (
base: #3498db,
light: #5dade2,
),
),
),
);
.element {
color: deep-map-get($config, theme, colors, primary, base);
}
5. 继承与占位符
5.1 基础继承
.message {
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.success-message {
@extend .message;
border-color: #2ecc71;
background: #d5f4e6;
}
.error-message {
@extend .message;
border-color: #e74c3c;
background: #fadbd8;
}
5.2 占位符选择器 %
// 占位符不会单独生成 CSS
%flex-center {
display: flex;
justify-content: center;
align-items: center;
}
%text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-title {
@extend %text-truncate;
font-size: 18px;
}
.modal {
@extend %flex-center;
min-height: 100vh;
}
5.3 多重继承
%bordered {
border: 1px solid #ddd;
}
%rounded {
border-radius: 8px;
}
%shadowed {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.card {
@extend %bordered;
@extend %rounded;
@extend %shadowed;
padding: 20px;
}
6. 控制指令
6.1 @if / @else
@mixin theme-color($theme) {
@if $theme == 'light' {
background: white;
color: black;
} @else if $theme == 'dark' {
background: black;
color: white;
} @else {
background: gray;
color: white;
}
}
.app {
@include theme-color('dark');
}
6.2 @for 循环
// 生成网格系统
@for $i from 1 through 12 {
.col-#{$i} {
width: percentage($i / 12);
}
}
// 生成间距工具类
$spacing: (5, 10, 15, 20, 25, 30);
@for $i from 1 through length($spacing) {
$space: nth($spacing, $i);
.m-#{$space} {
margin: #{$space}px;
}
.p-#{$space} {
padding: #{$space}px;
}
.mt-#{$space} {
margin-top: #{$space}px;
}
.pt-#{$space} {
padding-top: #{$space}px;
}
.mb-#{$space} {
margin-bottom: #{$space}px;
}
.pb-#{$space} {
padding-bottom: #{$space}px;
}
}
6.3 @each 循环
// 遍历列表
$colors: primary, secondary, success, danger, warning, info;
@each $color in $colors {
.btn-#{$color} {
background: var(--#{$color}-color);
}
}
// 遍历 Map
$social-colors: (
facebook: #3b5998,
twitter: #1da1f2,
instagram: #e4405f,
linkedin: #0077b5,
youtube: #ff0000,
);
@each $name, $color in $social-colors {
.btn-#{$name} {
background-color: $color;
&:hover {
background-color: darken($color, 10%);
}
}
}
// 多重值遍历
$sizes: (small, 12px, 500, medium, 14px, 600, large, 16px, 700);
@each $size, $font-size, $font-weight in $sizes {
.text-#{$size} {
font-size: $font-size;
font-weight: $font-weight;
}
}
6.4 @while 循环
// 生成渐进式字体大小
$i: 6;
@while $i > 0 {
h#{$i} {
font-size: 2em - ($i * 0.2);
}
$i: $i - 1;
}
7. 模块化系统
7.1 @use 和 @forward
// _variables.scss
$primary-color: #3498db;
$secondary-color: #2ecc71;
// _mixins.scss
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
// _functions.scss
@function rem($px) {
@return ($px / 16px) * 1rem;
}
// main.scss - 新的模块系统
@use 'variables' as vars;
@use 'mixins' as mix;
@use 'functions' as fn;
.container {
@include mix.flex-center;
color: vars.$primary-color;
padding: fn.rem(20px);
}
7.2 命名空间
// _config.scss
$primary: #3498db;
@mixin button {
padding: 10px 20px;
border-radius: 4px;
}
// styles.scss
@use 'config' as cfg;
.btn {
@include cfg.button;
background: cfg.$primary;
}
// 或者移除命名空间前缀
@use 'config' as *;
.btn {
@include button;
background: $primary;
}
7.3 @forward 创建索引文件
// styles/_index.scss
@forward 'variables';
@forward 'mixins';
@forward 'functions';
// main.scss
@use 'styles';
.element {
color: styles.$primary-color;
@include styles.flex-center;
}
8. 内置函数库
8.1 颜色函数
$base-color: #3498db;
.color-demo {
// 颜色调整
color: adjust-hue($base-color, 45deg);
// 亮度
background: lighten($base-color, 20%);
border-color: darken($base-color, 15%);
// 饱和度
&.vibrant {
background: saturate($base-color, 30%);
}
&.muted {
background: desaturate($base-color, 20%);
}
// 透明度
box-shadow: 0 2px 8px rgba($base-color, 0.3);
border: 1px solid transparentize($base-color, 0.5);
// 混合颜色
&.mixed {
background: mix(#3498db, #e74c3c, 50%);
}
// 补色
&.complement {
background: complement($base-color);
}
}
8.2 数学函数
.math-demo {
// 基础运算
width: percentage(5 / 12); // 41.66667%
padding: round(13.6px); // 14px
margin: ceil(10.1px); // 11px
height: floor(19.9px); // 19px
// 最大最小值
font-size: max(14px, 1rem);
width: min(100%, 1200px);
// 绝对值
top: abs(-20px); // 20px
// 随机数
opacity: random(100) / 100;
}
8.3 列表函数
$list: 10px 20px 30px 40px;
.list-demo {
// 获取长度
$length: length($list); // 4
// 获取元素
padding-top: nth($list, 1); // 10px
padding-right: nth($list, 2); // 20px
// 索引
$index: index($list, 20px); // 2
// 追加
$new-list: append($list, 50px);
// 合并
$merged: join($list, (60px 70px));
}
8.4 Map 函数
$theme: (
primary: #3498db,
secondary: #2ecc71,
danger: #e74c3c,
);
.map-demo {
// 获取值
color: map-get($theme, primary);
// 合并 Map
$extended: map-merge(
$theme,
(
success: #27ae60,
)
);
// 检查键是否存在
@if map-has-key($theme, primary) {
background: map-get($theme, primary);
}
// 获取所有键
$keys: map-keys($theme); // primary, secondary, danger
// 获取所有值
$values: map-values($theme);
}
8.5 字符串函数
$text: 'Hello World';
.string-demo {
// 转大写
content: to-upper-case($text); // "HELLO WORLD"
// 转小写
content: to-lower-case($text); // "hello world"
// 字符串长度
$length: str-length($text); // 11
// 查找索引
$index: str-index($text, 'World'); // 7
// 切片
content: str-slice($text, 1, 5); // "Hello"
// 插入
content: str-insert($text, ' Beautiful', 6); // "Hello Beautiful World"
// 去引号
font-family: unquote('"Arial"'); // Arial
}
9. 实战技巧
9.1 响应式字体大小
@function strip-unit($value) {
@return $value / ($value * 0 + 1);
}
@mixin fluid-type($min-vw, $max-vw, $min-font-size, $max-font-size) {
$u1: unit($min-vw);
$u2: unit($max-vw);
$u3: unit($min-font-size);
$u4: unit($max-font-size);
@if $u1 == $u2 and $u1 == $u3 and $u1 == $u4 {
& {
font-size: $min-font-size;
@media screen and (min-width: $min-vw) {
font-size: calc(
#{$min-font-size} + #{strip-unit($max-font-size - $min-font-size)} *
((100vw - #{$min-vw}) / #{strip-unit($max-vw - $min-vw)})
);
}
@media screen and (min-width: $max-vw) {
font-size: $max-font-size;
}
}
}
}
h1 {
@include fluid-type(320px, 1200px, 24px, 48px);
}
9.2 深色模式切换
$themes: (
light: (
bg: #ffffff,
text: #333333,
border: #e0e0e0,
primary: #3498db,
),
dark: (
bg: #1a1a1a,
text: #f0f0f0,
border: #404040,
primary: #5dade2,
),
);
@mixin themed-component {
@each $theme-name, $theme-colors in $themes {
[data-theme='#{$theme-name}'] & {
$theme-map: $theme-colors !global;
@content;
$theme-map: null !global;
}
}
}
@function theme-color($key) {
@return map-get($theme-map, $key);
}
.card {
@include themed-component {
background: theme-color(bg);
color: theme-color(text);
border: 1px solid theme-color(border);
}
&__button {
@include themed-component {
background: themed-component {
background: theme-color(primary);
color: theme-color(bg);
}
}
}
9.3 原子化 CSS 生成器
$spacing-map: (
0: 0,
1: 0.25rem,
2: 0.5rem,
3: 0.75rem,
4: 1rem,
5: 1.25rem,
6: 1.5rem,
8: 2rem,
10: 2.5rem,
12: 3rem,
16: 4rem,
20: 5rem,
);
$directions: (
'': '',
't': '-top',
'r': '-right',
'b': '-bottom',
'l': '-left',
'x': (
'-left',
'-right',
),
'y': (
'-top',
'-bottom',
),
);
@each $size-key, $size-value in $spacing-map {
@each $dir-key, $dir-value in $directions {
// Margin
.m#{$dir-key}-#{$size-key} {
@if type-of($dir-value) == 'list' {
@each $d in $dir-value {
margin#{$d}: $size-value;
}
} @else {
margin#{$dir-value}: $size-value;
}
}
// Padding
.p#{$dir-key}-#{$size-key} {
@if type-of($dir-value) == 'list' {
@each $d in $dir-value {
padding#{$d}: $size-value;
}
} @else {
padding#{$dir-value}: $size-value;
}
}
}
}
9.4 三角形生成器
@mixin triangle($direction, $size, $color) {
width: 0;
height: 0;
border: $size solid transparent;
@if $direction == 'up' {
border-bottom-color: $color;
} @else if $direction == 'down' {
border-top-color: $color;
} @else if $direction == 'left' {
border-right-color: $color;
} @else if $direction == 'right' {
border-left-color: $color;
}
}
.tooltip {
position: relative;
&::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
@include triangle(down, 8px, #333);
}
}
9.5 网格系统生成器
$grid-columns: 12;
$grid-gutter-width: 30px;
$container-max-widths: (
sm: 540px,
md: 720px,
lg: 960px,
xl: 1140px,
xxl: 1320px,
);
@mixin make-container($padding-x: $grid-gutter-width / 2) {
width: 100%;
padding-right: $padding-x;
padding-left: $padding-x;
margin-right: auto;
margin-left: auto;
}
@mixin make-row($gutter: $grid-gutter-width) {
display: flex;
flex-wrap: wrap;
margin-right: -$gutter / 2;
margin-left: -$gutter / 2;
}
@mixin make-col($size, $columns: $grid-columns) {
flex: 0 0 auto;
width: percentage($size / $columns);
padding-right: $grid-gutter-width / 2;
padding-left: $grid-gutter-width / 2;
}
.container {
@include make-container;
@each $breakpoint, $width in $container-max-widths {
@include media-breakpoint-up($breakpoint) {
max-width: $width;
}
}
}
.row {
@include make-row;
}
@for $i from 1 through $grid-columns {
.col-#{$i} {
@include make-col($i);
}
}
9.6 长阴影效果
@function long-shadow($length, $color, $opacity) {
$shadow: '';
@for $i from 0 through $length {
$shadow: $shadow +
'#{$i}px #{$i}px rgba(#{red($color)}, #{green($color)}, #{blue($color)}, #{$opacity})';
@if $i < $length {
$shadow: $shadow + ', ';
}
}
@return unquote($shadow);
}
.text-shadow {
text-shadow: long-shadow(50, #000, 0.05);
}
9.7 动画关键帧生成器
@mixin keyframes($name) {
@keyframes #{$name} {
@content;
}
}
@include keyframes(fadeIn) {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade {
animation: fadeIn 0.5s ease-out;
}
9.8 清除浮动
@mixin clearfix {
&::after {
content: '';
display: table;
clear: both;
}
}
.container {
@include clearfix;
}
🎯 总结
SCSS 的高级特性让我们能够:
- 提高代码复用性 - 通过 mixin、函数和继承
- 增强可维护性 - 使用变量、模块化和命名空间
- 提升开发效率 - 利用循环、条件判断自动生成样式
- 保持代码整洁 - 嵌套、占位符和模块系统
- 创建强大的工具库 - 自定义函数和 mixin 集合
最佳实践建议
-
✅ 变量命名要语义化
// Good $primary-color: #3498db; $spacing-unit: 8px; // Bad $blue: #3498db; $var1: 8px;
-
✅ 避免嵌套层级过深(建议不超过 3-4 层)
// Good .card { &__header { } &__body { } } // Bad - 嵌套太深 .card { .wrapper { .inner { .content { .text { } } } } }
-
✅ 优先使用 @use 而不是 @import
// Modern
@use 'variables';
@use 'mixins';
// Legacy
@import 'variables';
@import 'mixins';
-
✅ 使用占位符代替类继承
// Good %btn-base { } .btn { @extend %btn-base; } // Less optimal .btn-base { } .btn { @extend .btn-base; }
-
✅ 合理组织文件结构 styles/ ├── abstracts/ │ ├── _variables.scss │ ├── _functions.scss │ └── _mixins.scss ├── base/ │ ├── _reset.scss │ └── _typography.scss ├── components/ │ ├── _buttons.scss │ └── _cards.scss ├── layout/ │ ├── _header.scss │ └── _footer.scss └── main.scss
📚 参考资源
如果这篇文章对你有帮助,欢迎点赞收藏! 👍
有任何问题或补充,欢迎在评论区讨论~ 💬
小白也能懂的响应式布局:从 0 到 1 学会适配所有设备
Canvas 入门及常见功能实现
Canvas 绘制基础图形详解
Canvas 是 HTML5 核心绘图 API,支持在网页中动态绘制矢量图形。本文将系统讲解 Canvas 基础图形(线条、三角形、矩形、圆形)及组合图形(笑脸)的绘制方法,并附带完整代码与关键说明。
一、基础环境搭建(HTML + CSS + 初始化)
首先创建 Canvas 容器与绘图上下文,设置基础样式确保绘图区域清晰可见。
<style>
/* 容器样式:优化布局与视觉效果 */
.canvas-container {
background-color: #f8fafc; /* 浅灰背景,区分页面其他区域 */
padding: 20px;
max-width: 600px;
margin: 20px auto; /* 水平居中 */
border-radius: 8px; /* 圆角优化 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 轻微阴影增强层次感 */
}
/* Canvas 样式:明确绘图边界 */
#basic-canvas {
border: 4px dashed #cbd5e1; /* 虚线边框,区分画布区域 */
background-color: #ffffff; /* 白色画布,便于观察图形 */
border-radius: 4px;
}
</style>
<!-- 画布容器 -->
<div class="canvas-container">
<!-- Canvas 核心元素:width/height 需直接设置(非CSS),确保图形不失真 -->
<canvas id="basic-canvas" width="500" height="200"></canvas>
</div>
<script>
// 1. 获取 Canvas 元素与 2D 绘图上下文(核心对象)
const canvas = document.getElementById('basic-canvas')
const ctx = canvas.getContext('2d') // 所有绘图操作都通过 ctx 实现
// 2. 设置公共样式(避免重复代码)
ctx.lineWidth = 2 // 线条宽度(所有图形通用)
ctx.strokeStyle = '#2d3748' // 线条颜色(深灰,比黑色更柔和)
// 3. 页面加载完成后执行绘图(确保 Canvas 已渲染)
window.addEventListener('load', () => {
drawLine() // 绘制线条
drawTriangle() // 绘制三角形
drawRectangle() // 绘制矩形(原 Square 更准确的命名)
drawCircle() // 绘制圆形
drawSmilingFace() // 绘制笑脸(组合图形)
})
</script>
二、Canvas 路径绘制核心 API
在绘制路径之前先介绍几个常用的canvas的api。
- beginPath() 新建一条路径,生成之后,图形绘制命令被指向到路径上生成路径。
- closePath() 闭合路径之后图形绘制命令又重新指向到上下文中。
- stroke() 通过线条来绘制图形轮廓。
- fill() 通过填充路径的内容区域生成实心的图形。
- moveTo(x, y) 将笔触移动到指定的坐标 x 以及 y 上。
- lineTo(x, y) 绘制一条从当前位置到指定 x 以及 y 位置的直线。
三、具体图形绘制实现
1. 绘制直线(基础入门)
通过 moveTo() 定位起点,lineTo() 绘制线段,最后用 stroke() 渲染轮廓。
function drawLine() {
ctx.beginPath() // 开启新路径(避免与其他图形混淆)
ctx.moveTo(25, 25) // 起点:(25,25)(Canvas 左上角为原点 (0,0))
ctx.lineTo(105, 25) // 终点:(105,25)(水平向右绘制)
ctx.stroke() // 渲染直线轮廓
}
2. 绘制三角形(空心 + 实心)
三角形由三条线段组成,空心需手动闭合路径,实心可直接填充(自动闭合)。
function drawTriangle() {
// 1. 绘制空心三角形
ctx.beginPath()
ctx.moveTo(150, 25) // 顶点1
ctx.lineTo(200, 25) // 顶点2(水平向右)
ctx.lineTo(150, 75) // 顶点3(向左下方)
ctx.closePath() // 闭合路径(连接顶点3与顶点1)
ctx.stroke() // 渲染空心轮廓
// 2. 绘制实心三角形(位置偏移,避免与空心重叠)
ctx.beginPath()
ctx.moveTo(155, 30) // 顶点1(右移5px,下移5px)
ctx.lineTo(185, 30) // 顶点2(缩短宽度,更美观)
ctx.lineTo(155, 60) // 顶点3(上移15px,避免超出范围)
ctx.fillStyle = '#4299e1' // 单独设置填充色(蓝色)
ctx.fill() // 填充实心(无需 closePath(),自动闭合)
}
3. 绘制矩形(专用 API,更高效)
Canvas 为矩形提供了专用方法,无需手动写路径,直接指定位置与尺寸即可。
function drawRectangle() {
// 1. 空心矩形:strokeRect(x, y, 宽度, 高度)
ctx.strokeRect(10, 100, 50, 50) // 位置(10,100),尺寸50x50
// 2. 实心矩形:fillRect(x, y, 宽度, 高度)(偏移避免重叠)
ctx.fillStyle = '#48bb78' // 填充色(绿色)
ctx.fillRect(15, 105, 40, 40) // 位置(15,105),尺寸40x40
// 3. 清除矩形区域:clearRect(x, y, 宽度, 高度)(生成“镂空”效果)
ctx.clearRect(25, 115, 20, 20) // 清除中间20x20区域,变为透明
}
4. 绘制圆形(arc () 方法详解)
圆形通过 arc() 方法绘制,核心是理解「弧度制」与「绘制方向」。
arc () 方法语法: arc(x, y, radius, startAngle, endAngle, anticlockwise)
- x, y:圆心坐标
- radius:圆的半径
- startAngle/endAngle:起始 / 结束角度(必须用弧度制,公式:
弧度 = (Math.PI / 180) * 角度)
- anticlockwise:是否逆时针绘制(布尔值,默认 false 顺时针)
function drawCircle() {
// 1. 绘制完整圆形(360° = 2π 弧度)
ctx.beginPath()
ctx.arc(100, 125, 25, 0, Math.PI * 2, false) // 圆心(100,125),半径25
ctx.stroke()
// 2. 绘制上半圆(逆时针,180° = π 弧度)
ctx.beginPath()
ctx.arc(100, 125, 15, 0, Math.PI, true) // 半径15,逆时针绘制上半圆
ctx.stroke()
// 3. 绘制实心下半圆(顺时针)
ctx.beginPath()
ctx.arc(100, 130, 10, 0, Math.PI, false) // 圆心下移5px,半径10
ctx.fillStyle = '#f6ad55' // 填充色(橙色)
ctx.fill()
}
注意事项:为了保证新的圆弧不会追加到上一次的路径中,在每一次绘制圆弧的过程中都需要使用beginPath()方法。
5. 绘制组合图形(笑脸)
通过组合「圆形(脸)+ 小圆(眼睛)+ 半圆(嘴巴)」,实现复杂图形。
function drawSmilingFace() {
// 1. 绘制脸部轮廓(圆形)
ctx.beginPath()
ctx.arc(170, 125, 25, 0, Math.PI * 2, false) // 圆心(170,125),半径25
ctx.stroke()
// 2. 绘制左眼(小圆)
ctx.beginPath()
ctx.arc(163, 120, 3, 0, Math.PI * 2, false) // 左眼位置:左移7px,上移5px
ctx.fillStyle = '#2d3748' // 眼睛颜色(深灰)
ctx.fill() // 实心眼睛,无需 stroke()
// 3. 绘制右眼(小圆,与左眼对称)
ctx.beginPath()
ctx.arc(178, 120, 3, 0, Math.PI * 2, false) // 右眼位置:右移8px,上移5px
ctx.fill()
// 4. 绘制微笑嘴巴(下半圆,顺时针)
ctx.beginPath()
ctx.arc(170, 123, 18, 0, Math.PI, false) // 圆心(170,123),半径18,180°
ctx.stroke()
}
完整效果展示:
四、常见问题与注意事项
- Canvas 尺寸设置: width 和 height 必须直接在 Canvas 标签上设置,若用 CSS 设置会导致图形拉伸失真。
- 路径隔离: 每次绘制新图形前,务必调用 beginPath(),否则新图形会与上一次路径叠加。
-
弧度与角度转换: arc() 方法仅支持弧度制,需用
(Math.PI / 180) * 角度
转换(如 90° = Math.PI/ 2)。 - 样式优先级: 若单个图形需要特殊样式(如不同颜色),需在 stroke()/fill() 前单独设置(如 ctx.fillStyle),否则会继承公共样式。
Canvas 实现电子签名功能
电子签名功能在现代 Web 应用中非常常见,从在线合同签署到表单确认都有广泛应用。本文将带你从零开始,使用 Canvas API 实现一个功能完备的电子签名组件。
一、实现思路与核心技术点
实现电子签名的核心思路是追踪用户的鼠标或触摸轨迹,并在 Canvas 上将这些轨迹绘制出来。
核心技术点:
- Canvas API:用于在网页上动态绘制图形
- 事件监听:监听鼠标 / 触摸的按下、移动和松开事件
- 坐标转换:将鼠标 / 触摸事件的坐标转换为 Canvas 元素内的相对坐标
- 线条优化:通过设置线条属性实现平滑的签名效果
二、HTML 结构设计
这是一份简单到爆的html结构,没错,就是这样简单...
<div class="container">
<p>电子签名</p>
<canvas id="signatureCanvas" class="signature-border"></canvas>
</div>
三、CSS 样式设置
为 Canvas 添加一些基础样式,使其看起来像一个签名板。
.container {
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
}
.signature-border {
width: 98%;
height: 300px;
border: 4px dashed #cbd5e1;
border-radius: 10px;
cursor: crosshair;
}
四、JavaScript 核心实现
这是实现签名功能的关键部分,主要包含以下几个步骤:
- 获取 Canvas 元素和上下文
- 设置 Canvas 的实际绘制尺寸
- 定义变量存储签名状态和坐标
- 实现坐标转换函数
- 编写事件处理函数
- 绑定事件监听器
// 获取Canvas元素和上下文
const canvas = document.getElementById('signatureCanvas')
const ctx = canvas.getContext('2d', { willReadFrequently: true })
// 签名状态变量
let isDrawing = false
let lastX = 0
let lastY = 0
let lineColor = '#000000'
let lineWidth = 2
// 初始化Canvas
function initCanvas() {
// 设置Canvas样式
ctx.strokeStyle = lineColor
ctx.lineWidth = lineWidth
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
resizeCanvas()
window.addEventListener('resize', resizeCanvas)
}
// 响应窗口大小变化
function resizeCanvas() {
const rect = canvas.getBoundingClientRect()
const { width, height } = rect
// 保存当前画布内容
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
// 调整Canvas尺寸
canvas.width = width
canvas.height = height
// 恢复画布内容
ctx.putImageData(imageData, 0, 0)
// 重新设置绘图样式
ctx.strokeStyle = lineColor
ctx.lineWidth = lineWidth
ctx.lineJoin = 'round'
ctx.lineCap = 'round'
}
// 获取坐标(适配鼠标和触摸事件)
function getCoordinates(e) {
const rect = canvas.getBoundingClientRect()
if (e.type.includes('mouse')) {
return [e.clientX - rect.left, e.clientY - rect.top]
} else if (e.type.includes('touch')) {
return [e.touches[0].clientX - rect.left, e.touches[0].clientY - rect.top]
}
}
// 开始绘制
function startDrawing(e) {
isDrawing = true
lastX = getCoordinates(e)[0]
lastY = getCoordinates(e)[1]
}
// 绘制中
function draw(e) {
if (!isDrawing) return
const [currentX, currentY] = getCoordinates(e)
ctx.beginPath()
ctx.moveTo(lastX, lastY)
ctx.lineTo(currentX, currentY)
ctx.stroke()
// 解释: 这里是将当前移动的坐标赋值给下一次绘制的起点,实现线条的流畅。
;[lastX, lastY] = [currentX, currentY]
}
// 结束绘制
function stopDrawing() {
isDrawing = false
}
// 绑定事件监听
function bindEvents() {
canvas.addEventListener('mousedown', startDrawing)
canvas.addEventListener('mousemove', draw)
canvas.addEventListener('mouseup', stopDrawing)
canvas.addEventListener('mouseout', stopDrawing)
// 触摸事件(移动设备)
canvas.addEventListener('touchstart', e => {
e.preventDefault() // 防止触摸事件被浏览器默认处理
startDrawing(e)
})
canvas.addEventListener('touchmove', e => {
e.preventDefault()
draw(e)
})
canvas.addEventListener('touchend', e => {
e.preventDefault()
stopDrawing()
})
}
// 初始化
window.addEventListener('load', () => {
initCanvas()
bindEvents()
})
五、功能亮点与设计思路
-
流畅的绘制体验:通过设置
lineCap: 'round'
和lineJoin: 'round'
让线条更加平滑自然。 -
响应式设计:监听窗口
resize
事件,动态调整 Canvas 尺寸,确保在不同设备和屏幕尺寸下都能正常工作。 - 跨设备支持:同时支持鼠标和触摸事件,兼容桌面和移动设备。
六、完整的代码
七、下一步可以探索的方向
- 颜色和粗细选择:增加 UI 控件让用户自定义签名的颜色和笔触粗细。
- 清空签名和保存签名:增加 UI 控件让用户清空当前的签名,同时支持保存和下载签名。
canvas 实现滚动序列帧动画
前言
在现代网页设计中,滚动触发的动画能极大增强用户体验,其中 Apple 官网的 AirPods Pro 产品页动画堪称经典 —— 通过滚动进度控制序列帧播放,营造出流畅的产品展示效果。本文将简单的实现一下这个动画效果。
一、动画核心逻辑
- 页面分为 3 个楼层:楼层 1(灰色背景)、楼层 2(黑色背景,核心动画区)、楼层 3(灰色背景)
- 楼层 2 高度为200vh(2 倍视口高度),内部有一个sticky定位的容器,包含文字和 Canvas
- 当用户滚动页面时,仅在楼层 2 进入并完全离开视口的过程中,Canvas 会根据滚动进度播放 147 帧 AirPods 序列图
- 窗口尺寸变化时,Canvas 会自动适配,保证动画显示比例正确
二、核心技术栈及原理拆解
要实现滚动序列帧动画,需要解决 3 个核心问题:序列帧加载与管理、滚动进度计算、Canvas 渲染与适配。
- HTML 部分的核心是三层 section 结构和Canvas 动画容器,结构清晰且语义化:
<!-- 楼层1:引导区 -->
<section class="floor1-container floor-container">
<p>楼层一</p>
</section>
<!-- 楼层2:核心动画区(目标楼层) -->
<section class="floor2-container floor-container" id="targetFloor">
<!-- sticky容器:滚动时"粘住"视口 -->
<div class="sticky">
<p>楼层二</p>
<!-- Canvas:用于渲染序列帧 -->
<canvas class="canvas" id="hero-lightpass"></canvas>
</div>
</section>
<!-- 楼层3:结束区 -->
<section class="floor3-container floor-container">
<p>楼层三</p>
</section>
- CSS 的核心作用是控制三层布局、实现 sticky 定位、保证 Canvas 适配,代码注释已标注关键逻辑:
/* 重置默认margin,避免布局偏移 */
body,
p {
margin: 0;
}
/* 楼层1和楼层3样式:灰色背景+居中文字 */
.floor1-container,
.floor3-container {
background-color: #474646; /* 深灰色背景 */
height: 500px; /* 固定高度,模拟常规内容区 */
display: flex; /* Flex布局:实现文字水平+垂直居中 */
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
/* 楼层1/3文字样式:响应式字体 */
.floor3-container p,
.floor1-container p {
font-size: 5vw; /* 5vw:相对于视口宽度的5%,实现响应式字体 */
color: #fff; /* 白色文字,与深色背景对比 */
}
/* 楼层2样式:黑色背景+高高度(动画触发区) */
.floor2-container {
height: 200vh; /* 200vh:2倍视口高度,保证有足够滚动空间触发动画 */
background-color: black; /* 黑色背景,突出产品图片 */
color: #fff; /* 白色文字 */
}
/* 楼层2文字:水平居中 */
.floor2-container p {
text-align: center;
}
/* 核心:sticky定位容器 */
.sticky {
position: sticky; /* 粘性定位:滚动到top:0时固定 */
top: 0; /* 固定在视口顶部 */
height: 500px; /* 与楼层1/3高度一致,保证视觉连贯 */
width: 100%; /* 占满视口宽度 */
}
/* Canvas样式:宽度自适应 */
.canvas {
width: 100%; /* 宽度占满容器 */
height: auto; /* 高度自动,保持图片比例 */
}
- JS 部分是整个动画的核心,负责预加载序列帧、计算滚动进度、控制 Canvas 渲染和窗口适配,我们分模块解析:
模块 1:初始化变量与 DOM 元素
首先定义动画所需的核心变量,包括序列帧数量、图片数组、Canvas 上下文等:
// 1. 动画核心配置
const frameCount = 147 // 序列帧总数(根据实际图片数量调整)
const images = [] // 存储所有预加载的序列帧图片
const canvas = document.getElementById('hero-lightpass') // 获取Canvas元素
const context = canvas.getContext('2d') // 获取Canvas 2D渲染上下文
const airpods = { frame: 0 } // 存储当前播放的帧序号(用对象便于修改)
// 2. 获取目标楼层(楼层2)的DOM元素,用于后续计算滚动位置
const targetFloor = document.getElementById('targetFloor')
// 3. 序列帧图片地址模板(Apple官网的AirPods序列帧地址)
// 作用:通过索引生成每帧图片的URL(如0001.jpg、0002.jpg...)
const currentFrame = index =>
`https://www.apple.com/105/media/us/airpods-pro/2019/1299e2f5_9206_4470_b28e_08307a42f19b/anim/sequence/large/01-hero-lightpass/${(index + 1).toString().padStart(4, '0')}.jpg`
模块 2:预加载所有序列帧图片
序列帧动画需要所有图片加载完成后才能流畅播放,因此必须先预加载图片:
// 循环生成147帧图片,存入images数组
for (let i = 0; i < frameCount; i++) {
const img = new Image() // 创建Image对象
img.src = currentFrame(i) // 给图片设置URL(通过模板生成)
images.push(img) // 将图片存入数组
}
// 当第一张图片加载完成后,执行首次渲染(避免页面空白)
images[0].onload = render
为什么要预加载:
- 如果不预加载,用户滚动时图片可能还在加载,导致动画卡顿或跳帧
- 监听第一张图片的onload事件:保证页面初始化时至少有一张图显示,提升首屏体验
模块 3:Canvas 渲染函数
定义render()函数,负责将当前帧图片绘制到 Canvas 上:
function render() {
// 1. 清除Canvas画布(避免上一帧残留)
context.clearRect(0, 0, canvas.width, canvas.height)
// 2. 绘制当前帧图片
// 参数:图片对象、绘制起点X、Y、绘制宽度、绘制高度
context.drawImage(images[airpods.frame], 0, 0, canvas.width, canvas.height)
}
模块 4:Canvas 窗口适配函数
当窗口尺寸变化时,需要重新调整 Canvas 的宽高,避免图片拉伸或变形:
function resizeCanvas() {
// 1. 获取Canvas元素的实际位置和尺寸(包含CSS样式的影响)
const rect = canvas.getBoundingClientRect()
// 2. 设置Canvas的实际宽高(Canvas的width/height是像素尺寸,而非CSS样式)
canvas.width = rect.width
canvas.height = rect.height
// 3. 重新渲染当前帧(避免尺寸变化后画布空白)
render()
}
易错点提醒:
- Canvas 有两个 "尺寸":一个是 HTML 属性width/height(实际像素尺寸),另一个是 CSS 样式width/height(显示尺寸)
- 如果只改 CSS 样式而不改canvas.width/height,图片会拉伸变形;因此必须通过getBoundingClientRect()获取实际显示尺寸,同步设置 Canvas >的像素尺寸
模块 5:滚动进度计算与帧控制(核心中的核心)
这是整个动画的逻辑核心 —— 根据用户的滚动位置,计算当前应播放的帧序号,实现 "滚动控制动画":
function handleScroll() {
// 1. 获取关键尺寸数据
const viewportHeight = window.innerHeight // 视口高度(浏览器可见区域高度)
const floorTop = targetFloor.offsetTop // 目标楼层(楼层2)距离页面顶部的距离
const floorHeight = targetFloor.offsetHeight // 目标楼层自身的高度(200vh)
const currentScrollY = window.scrollY // 当前滚动位置(页面顶部到视口顶部的距离)
// 2. 计算"滚动结束点":当目标楼层底部进入视口时,动画应播放到最后一帧
const scrollEnd = floorTop + floorHeight - viewportHeight
// 3. 计算滚动进度(0~1):0=未进入楼层2,1=完全离开楼层2
let scrollProgress = 0
if (currentScrollY < floorTop) {
// 情况1:滚动位置在楼层2上方→进度0(显示第一帧)
scrollProgress = 0
} else if (currentScrollY > scrollEnd) {
// 情况2:滚动位置在楼层2下方→进度1(显示最后一帧)
scrollProgress = 1
} else {
// 情况3:滚动位置在楼层2内部→计算相对进度
const scrollDistanceInFloor = currentScrollY - floorTop // 进入楼层2后滚动的距离
const totalScrollNeeded = scrollEnd - floorTop // 楼层2内需要滚动的总距离(触发完整动画的距离)
scrollProgress = scrollDistanceInFloor / totalScrollNeeded // 进度=已滚动距离/总距离
}
// 4. 根据进度计算当前应显示的帧序号
// 公式:目标帧 = 进度 × (总帧数-1) → 保证进度1时显示最后一帧(避免数组越界)
const targetFrame = Math.floor(scrollProgress * (frameCount - 1))
// 5. 优化性能:仅当帧序号变化时才重新渲染
if (targetFrame !== airpods.frame) {
airpods.frame = targetFrame
render() // 重新绘制当前帧
}
}
模块 6:事件监听与初始化
最后,通过事件监听触发上述逻辑,完成动画初始化:
window.addEventListener('load', () => {
// 1. 监听滚动事件:用户滚动时触发进度计算
window.addEventListener('scroll', handleScroll)
// 2. 监听窗口 resize 事件:窗口尺寸变化时适配Canvas
window.addEventListener('resize', resizeCanvas)
// 3. 初始化Canvas尺寸(页面加载完成后首次适配)
resizeCanvas()
})
三、完成代码展示
更多canvas功能敬请期待...
CSS排版布局篇(4):浮动(float)、定位(position) 、层叠(Stacking)
[译] 浏览器里的 Liquid Glass:利用 CSS 和 SVG 实现折射
CSS排版布局篇(2):文档流(Normal Flow)
现代CSS开发环境搭建
第2章: 现代CSS开发环境搭建
🎯 本章重点
- 现代前端工具链配置
- PostCSS和预处理器的使用
- 开发工作流优化
📖 内容概述
2.1 基础开发环境
代码编辑器配置 (VS Code)
// .vscode/settings.json
{
"css.validate": false,
"less.validate": false,
"scss.validate": false,
"editor.formatOnSave": true,
"files.associations": {
"*.css": "css"
}
}
推荐扩展
- PostCSS Language Support
- Auto Rename Tag
- CSS Peek
- Live Server
2.2 构建工具配置
package.json 依赖
{
"devDependencies": {
"postcss": "^8.4.0",
"postcss-preset-env": "^7.0.0",
"autoprefixer": "^10.4.0",
"cssnano": "^5.0.0",
"vite": "^3.0.0"
}
}
2.3 PostCSS 配置
postcss.config.js
module.exports = {
plugins: [
require('postcss-preset-env')({
stage: 3,
features: {
'nesting-rules': true,
'custom-media-queries': true,
'media-query-ranges': true
}
}),
require('autoprefixer'),
require('cssnano')({
preset: 'default'
})
]
}
2.4 现代CSS工作流
开发脚本
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"css:watch": "postcss src/**/*.css --dir dist --watch"
}
}
2.5 浏览器兼容性处理
.browserslistrc
last 2 versions
> 1%
not dead
not ie 11
自动前缀示例
/* 输入 */
.container {
display: grid;
gap: 20px;
}
/* 输出 */
.container {
display: -ms-grid;
display: grid;
-ms-grid-gap: 20px;
gap: 20px;
}
2.6 开发服务器配置
vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
server: {
port: 3000,
open: true
},
css: {
postcss: './postcss.config.js'
}
})
💡 最佳实践
- 使用CSS原生特性 替代预处理器
- 配置PostCSS 处理兼容性和优化
- 设置浏览器列表 确保目标兼容性
- 使用开发服务器 获得实时预览
🛠️ 工具推荐
- 构建工具: Vite, Parcel, Webpack
- CSS处理: PostCSS, Lightning CSS
- 开发服务器: Live Server, BrowserSync
🎯 下一章预览
下一章将深入探讨CSS变量和自定义属性的强大功能。
最后更新: 2024年12月
容器查询 - 组件级响应式设计
第7章: 容器查询 - 组件级响应式设计
🎯 本章重点
- 容器查询基础概念
- 与媒体查询的区别与优势
- 实际应用场景和最佳实践
- 浏览器兼容性处理
📖 内容概述
7.1 容器查询介绍
7.1.1 什么是容器查询
容器查询(Container Queries)允许组件根据其容器尺寸而非视口尺寸来调整样式,实现真正的组件级响应式设计。
7.1.2 解决的问题
- 媒体查询的局限性: 只能基于视口尺寸
- 组件独立性: 组件可以在不同容器中自适应
- 布局灵活性: 组件无需知道外部布局结构
7.2 基础语法
7.2.1 定义容器
/* 创建容器上下文 */
.component-container {
container-type: inline-size;
container-name: main-container;
}
/* 简写形式 */
.component-container {
container: main-container / inline-size;
}
/* 多个容器属性 */
.component-container {
container-type: size; /* 支持尺寸查询 */
container-name: card-layout; /* 容器名称 */
}
7.2.2 容器类型
-
inline-size
: 只查询内联方向尺寸(水平方向) -
size
: 查询两个方向的尺寸(水平和垂直) -
normal
: 不创建容器上下文(默认)
7.2.3 容器查询语法
@container main-container (min-width: 400px) {
.component {
/* 当容器宽度 ≥ 400px 时的样式 */
display: grid;
grid-template-columns: 1fr 1fr;
}
}
@container (max-width: 300px) {
.component {
/* 当容器宽度 ≤ 300px 时的样式 */
flex-direction: column;
}
}
7.3 实际应用案例
7.3.1 卡片组件
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
background: white;
}
.card-container {
container-type: inline-size;
container-name: card;
}
/* 小尺寸卡片 */
@container card (max-width: 300px) {
.card {
display: flex;
flex-direction: column;
text-align: center;
}
.card-image {
width: 100%;
height: 120px;
object-fit: cover;
}
.card-title {
font-size: 1rem;
margin: 8px 0;
}
.card-description {
display: none;
}
}
/* 中等尺寸卡片 */
@container card (min-width: 301px) and (max-width: 500px) {
.card {
display: grid;
grid-template-areas:
"image title"
"image description"
"button button";
grid-template-columns: 100px 1fr;
gap: 12px;
}
.card-image {
grid-area: image;
width: 100%;
height: 80px;
object-fit: cover;
border-radius: 4px;
}
.card-title {
grid-area: title;
font-size: 1.1rem;
}
.card-description {
grid-area: description;
font-size: 0.9rem;
color: #666;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.card-button {
grid-area: button;
}
}
/* 大尺寸卡片 */
@container card (min-width: 501px) {
.card {
display: grid;
grid-template-areas:
"image title button"
"image description button";
grid-template-columns: 150px 1fr auto;
grid-template-rows: auto 1fr;
gap: 16px;
}
.card-image {
grid-area: image;
width: 100%;
height: 120px;
object-fit: cover;
border-radius: 6px;
}
.card-title {
grid-area: title;
font-size: 1.2rem;
margin: 0;
}
.card-description {
grid-area: description;
font-size: 1rem;
line-height: 1.4;
}
.card-button {
grid-area: button;
align-self: center;
}
}
7.3.2 导航组件
.nav-container {
container-type: inline-size;
container-name: navigation;
}
.navigation {
display: flex;
background: #2c3e50;
padding: 0 20px;
}
/* 小尺寸导航 - 汉堡菜单 */
@container navigation (max-width: 600px) {
.navigation {
justify-content: space-between;
padding: 0 16px;
height: 60px;
}
.nav-logo {
font-size: 1.2rem;
color: white;
}
.nav-menu {
display: none;
position: absolute;
top: 60px;
left: 0;
right: 0;
background: #34495e;
flex-direction: column;
padding: 16px;
}
.nav-menu.open {
display: flex;
}
.nav-item {
padding: 12px 0;
border-bottom: 1px solid #4a6278;
}
.hamburger {
display: block;
color: white;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
}
/* 大尺寸导航 - 水平菜单 */
@container navigation (min-width: 601px) {
.navigation {
justify-content: space-between;
align-items: center;
height: 70px;
}
.nav-logo {
font-size: 1.5rem;
color: white;
font-weight: bold;
}
.nav-menu {
display: flex;
gap: 30px;
list-style: none;
margin: 0;
padding: 0;
}
.nav-item {
color: white;
text-decoration: none;
padding: 8px 16px;
border-radius: 4px;
transition: background-color 0.2s;
}
.nav-item:hover {
background-color: #34495e;
}
.hamburger {
display: none;
}
}
7.3.3 表单组件
.form-container {
container-type: inline-size;
container-name: form;
}
.form-group {
margin-bottom: 20px;
}
/* 小尺寸表单 - 垂直布局 */
@container form (max-width: 400px) {
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-label {
font-weight: bold;
font-size: 0.9rem;
}
.form-input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.form-button {
width: 100%;
padding: 12px;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
}
}
/* 大尺寸表单 - 水平布局 */
@container form (min-width: 401px) {
.form-group {
display: grid;
grid-template-columns: 120px 1fr;
gap: 16px;
align-items: center;
}
.form-label {
text-align: right;
font-weight: bold;
}
.form-input {
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
}
.form-button {
grid-column: 2;
justify-self: start;
padding: 12px 24px;
background: #3498db;
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
}
}
7.4 高级特性
7.4.1 容器单位
.component {
/* 使用容器相对单位 */
font-size: clamp(1rem, 5cqi, 2rem);
padding: clamp(1rem, 10cqi, 2rem);
gap: clamp(0.5rem, 2cqi, 1rem);
}
/* 可用单位 */
.example {
width: 50cqi; /* 容器内联尺寸的50% */
height: 30cqb; /* 容器块尺寸的30% */
font-size: 5cqi; /* 容器内联尺寸的5% */
padding: 10cqmin; /* 容器最小尺寸的10% */
margin: 5cqmax; /* 容器最大尺寸的5% */
}
7.4.2 嵌套容器查询
.layout-container {
container-type: inline-size;
container-name: layout;
}
.card-container {
container-type: inline-size;
container-name: card;
}
/* 外层容器查询 */
@container layout (min-width: 800px) {
.card-container {
/* 在大布局中调整卡片容器 */
container-type: size;
}
}
/* 内层容器查询 */
@container card (min-width: 300px) and (min-height: 200px) {
.card {
/* 基于卡片容器尺寸的样式 */
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
}
7.4.3 容器查询与CSS变量
:root {
--card-padding: 16px;
--card-gap: 12px;
}
.card-container {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 400px) {
.card {
--card-padding: 24px;
--card-gap: 20px;
--image-size: 120px;
}
}
@container card (min-width: 600px) {
.card {
--card-padding: 32px;
--card-gap: 24px;
--image-size: 160px;
}
}
.card {
padding: var(--card-padding);
gap: var(--card-gap);
}
.card-image {
width: var(--image-size, 80px);
height: var(--image-size, 80px);
}
7.5 性能优化
7.5.1 避免过度使用
/* 好的做法:合理的断点 */
@container (min-width: 300px) { /* ... */ }
@container (min-width: 600px) { /* ... */ }
@container (min-width: 900px) { /* ... */ }
/* 避免的做法:过多断点 */
@container (min-width: 100px) { /* ... */ }
@container (min-width: 200px) { /* ... */ }
@container (min-width: 300px) { /* ... */ }
/* ... 太多类似的查询 */
7.5.2 使用容器单位
/* 使用容器单位替代多个查询 */
.component {
/* 替代多个min-width查询 */
font-size: clamp(1rem, 4cqi, 1.5rem);
padding: clamp(1rem, 5cqi, 2rem);
}
/* 响应式间距 */
.spacing {
gap: clamp(0.5rem, 2cqi, 1.5rem);
}
7.6 浏览器兼容性
7.6.1 特性检测
/* 现代浏览器支持 */
@supports (container-type: inline-size) {
.component-container {
container-type: inline-size;
}
@container (min-width: 400px) {
.component {
/* 容器查询样式 */
}
}
}
/* 传统浏览器回退 */
@supports not (container-type: inline-size) {
.component {
/* 基于视口的回退样式 */
max-width: 400px;
}
@media (min-width: 768px) {
.component {
/* 媒体查询替代 */
}
}
}
7.6.2 JavaScript检测
// 检测容器查询支持
if (CSS.supports('container-type', 'inline-size')) {
console.log('容器查询支持');
} else {
console.log('容器查询不支持,使用回退方案');
// 添加回退类
document.documentElement.classList.add('no-container-queries');
}
/* 回退样式 */
.no-container-queries .component {
/* 传统响应式设计 */
max-width: 400px;
}
.no-container-queries .component--large {
/* 手动控制的大尺寸样式 */
}
7.7 最佳实践
- 语义化命名: 使用有意义的容器名称
- 合理断点: 基于内容需求设置断点
- 性能意识: 避免不必要的容器查询
- 渐进增强: 提供适当的回退方案
- 测试覆盖: 在不同容器尺寸下测试组件
💡 实战技巧
- 使用容器查询实现真正的组件独立性
- 结合CSS变量创建灵活的组件系统
- 利用容器单位实现流畅的尺寸变化
- 为不支持的环境提供优雅降级
🎯 下一章预览
下一章将探索CSS网格布局的高级技巧,包括子网格、自动布局算法和复杂网格模式。
最后更新: 2024年12月
分享并记录日常开发中的一些CSS布局和样式技巧(持续更新)
重新思考CSS Reset:normalize.css vs reset.css vs remedy.css,在2025年该如何选?
我带团队Review一个新项目的启动代码时,有一个文件我一定会仔细看,那就是CSS Reset
。
它虽然不起眼,但却像我们整个CSS架构的地基。地基打不好,上面的楼盖得再漂亮,也容易出问题,后期维护成本会非常高。
从十多年前 reset.css
横空出世,到后来normalize.css
成为事实标准,再到近几年出现的一些新方案,CSS Reset
的理念,其实也在不断演进。
但现在都2025年10月了,IE早已入土为安,主流浏览器对标准的支持也空前一致。我们还有必要像十年前那样做重置样式吗?
今天,我就想聊聊我对这几个主流方案的看法,以及在我们团队的当前项目中,我是如何选择的。
reset.css
-
它的原理:非常暴力直接——抹平所有浏览器默认样式。
margin
,padding
,font-size
,line-height
...通通归零,h1
、p
、ul
、li
在外观上变得一模一样,所有元素都回到最原始、最裸的状态。 -
代码片段感受一下:
/* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain) */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font-size: 100%; font: inherit; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } body { line-height: 1; } ol, ul { list-style: none; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } table { border-collapse: collapse; border-spacing: 0; }
-
优点:提供了一个绝对干净、可预测,非常适合那些需要从零开始、高度定制视觉风格的网站。
-
2025年的缺点:
-
太粗暴了:它移除了很多有用的默认样式。比如,你写了一个
<ul>
,却发现前面的项目符号没了,还得自己手动加回来。 -
破坏了语义化:一个
<h1>
在视觉上和<p>
毫无区别,这在开发初期,会削弱HTML语义化的默认视觉反馈。 - 调试困难:当你在DevTools里审查一个元素时,你看到的样式,和它本该有的默认样式天差地别,这会增加调试的心智负担。
-
太粗暴了:它移除了很多有用的默认样式。比如,你写了一个
在2025年,对于绝大多数项目,我不推荐再使用这种粗暴的Reset样式。
normalize.css
-
原理:与
reset.css
完全相反——保留有用的浏览器默认样式,只修复已知的浏览器不一致和Bug。它不在重置,而是修正。 -
代码片段感受一下:
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ /* Document ========================================================================== */ /** * 1. Correct the line height in all browsers. * 2. Prevent adjustments of font size after orientation changes in iOS. */ html { line-height: 1.15; /* 1 */ -webkit-text-size-adjust: 100%; /* 2 */ } /* Sections ========================================================================== */ /** * Remove the margin in all browsers. */ body { margin: 0; } /** * Render the `main` element consistently in IE. */ main { display: block; } /** * Correct the font size and margin on `h1` elements within `section` and * `article` contexts in Chrome, Firefox, and Safari. */ h1 { font-size: 2em; margin: 0.67em 0; } /* Grouping content ========================================================================== */ /** * 1. Add the correct box sizing in Firefox. * 2. Show the overflow in Edge and IE. */ hr { box-sizing: content-box; /* 1 */ height: 0; /* 1 */ overflow: visible; /* 2 */ } /** * 1. Correct the inheritance and scaling of font size in all browsers. * 2. Correct the odd `em` font sizing in all browsers. */ pre { font-family: monospace, monospace; /* 1 */ font-size: 1em; /* 2 */ } /* Text-level semantics ========================================================================== */ /** * Remove the gray background on active links in IE 10. */ a { background-color: transparent; } /** * 1. Remove the bottom border in Chrome 57- * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. */ /* 大部分已省略,完整的版本可以查看👉 https://github.com/necolas/normalize.css/blob/8.0.1/normalize.css */
-
优点:
- 保留了元素的默认语义化样式,
h1
就是比h2
大。 - 只修复问题,代码注释清晰,像一本浏览器修复手册。
- 它成为了过去十年里,包括Bootstrap、Ant Design在内,无数框架和组件库的基石。
- 保留了元素的默认语义化样式,
-
2025年的缺点:
-
过于保守:它只修复不一致,但并没有提供一些我们现代开发中普遍认为更好的默认值。比如,它就没有设置
box-sizing: border-box;
。 - 部分规则已过时:它里面的一些修复,是针对我们现在根本不需要支持的、非常古老的浏览器版本的(比如旧版IE)。
-
过于保守:它只修复不一致,但并没有提供一些我们现代开发中普遍认为更好的默认值。比如,它就没有设置
normalize.css
在今天,依然是一个安全、稳妥的选择。它不会犯错,但我觉得,它有点不够看了😫。
最佳选择:remedy.css
-
原理:在
normalize.css
的基础上,再往前走一步。它不仅修正了不一致,还提供了一套我们现代Web开发中,普遍认为 更好的默认样式。 -
核心特性:
-
开箱即用的
border-box
:*, ::before, ::after { box-sizing: border-box; }
这几乎是所有现代CSS项目的第一行代码,它帮你写好了。
-
更好的响应式媒体元素:
img, picture, video, canvas, svg { display: block; max-width: 100%; }
这能天然地防止图片、视频等媒体元素撑破布局,是响应式设计的基础。
-
更平滑的字体渲染和滚动:
html { -moz-text-size-adjust: none; -webkit-text-size-adjust: none; text-size-adjust: none; scroll-behavior: smooth; }
-
更友好的可用性/无障碍默认值:
[disabled] { cursor: not-allowed; }
-
-
优点:它像一个经验丰富的老手,把你开新项目时,那些不得不写的、或者最好要写的样板代码,都提前帮你准备好了。
-
缺点:它带有一定的主观性。比如,它默认移除了所有元素的
margin
,统一用padding
来控制间距,这需要你适应它的理念。
对于我们团队的新项目,尤其是那些需要快速启动的中后台项目,remedy.css
或者类似的现代Reset方案(比如modern-css-reset
),已经成为了我的首选。
选择与建议🤞
Reset 类型 | 哲学思想 | 适用场景 | 在2025年的建议 |
---|---|---|---|
reset.css |
简单粗暴的重置 | 高度定制视觉、几乎没有原生HTML元素的UI | 不推荐❌ |
normalize.css |
保留并修正 | 任何项目,尤其是需要保持浏览器原生感的 | 安全,但略显保守👍 |
remedy.css |
现代最佳实践 | 所有新项目,尤其是中后台、需要快速启动的项目 | 强烈推荐首选👍👍👍 |
自己定义 | 量身定制 | 大型项目、有完整设计系统的团队 | 终极方案,成本高🤔 |
CSS Reset
只有权衡,没有什么可选,不可选。
但在2025年,我们权衡的基点,已经从如何抹平IE的差异,变成了如何以一个更现代、更高效、更符合最佳实践的基点,来开始我们的工作。
所以,下次当你的新项目npm init
之后,别再下意识地npm install normalize.css
了。
或许,remedy.css
会给你一个更好的开始。
祝大家国庆愉快🙌
大家都在找的手绘/素描风格图编辑器它它它来了
我正在做的开源项目:du-editor,一个基于X6的图编辑器
要在 X6 中实现手绘/素描风格,核心是 SVG 滤镜 (Filter) 和 SVG 填充模式 (Pattern) 。X6 的节点和边都基于 SVG,因此我们可以充分利用 SVG 的强大功能来创建这种独特的视觉效果。
这种风格主要包含三个要素:
-
抖动、不规则的边框:这通过 SVG 的
feTurbulence
和feDisplacementMap
滤镜实现。 -
阴影线填充效果:这通过 SVG 的
<pattern>
元素创建可平铺的填充图案来实现。 - 手写体字体:选择一个合适的手写风格字体
第 1 步:定义 SVG 滤镜和填充模式
首先,需要在你的 HTML 文件中,通常是在图表容器 <div>
的旁边,定义好我们需要的 SVG 资源。我们将它们放在一个 <svg>
标签的 <defs>
元素中,这样 X6 就可以通过 ID 引用它们。
HTML
<div id="container"></div>
<svg width="0" height="0">
<defs>
<filter id="filter-sketch">
<feTurbulence
type="fractalNoise"
baseFrequency="0.01"
numOctaves="5"
result="noise"
/>
<feDisplacementMap
in="SourceGraphic"
in2="noise"
scale="5"
xChannelSelector="R"
yChannelSelector="G"
result="displaced"
/>
</filter>
<pattern
id="pattern-hatch"
patternUnits="userSpaceOnUse"
width="8"
height="8"
patternTransform="rotate(45)"
>
<path
d="M -1,1 l 2,-2 M 0,8 l 8,-8 M 7,9 l 2,-2"
stroke="#c58d6a"
stroke-width="1"
/>
</pattern>
</defs>
</svg>
-
<filter id="filter-sketch">
:-
feTurbulence
: 用于生成一种叫做 "Perlin noise" 的伪随机噪点图。baseFrequency
控制噪点的“波纹”大小,值越小,线条抖动越平缓。numOctaves
决定了细节层次。 -
feDisplacementMap
: 将上面生成的噪点图应用到原始图形上,使其像素发生位移,从而产生抖动的“手绘”效果。scale
属性控制抖动的剧烈程度,是调整效果最关键的参数。
-
-
<pattern id="pattern-hatch">
:- 我们创建了一个 8x8 像素大小的单元格,并将其旋转了 45 度。
- 在单元格内部,我们用
<path>
画了几条斜线。 - 这个 pattern 会像瓷砖一样自动平铺,以填充整个形状。你可以通过修改
width
,height
和<path>
的stroke-width
来调整阴影线的密度和粗细。
第 2 步:在 X6 中注册自定义节点和样式
接下来,我们在 X6代码中注册新的节点,并在其 attrs
中引用上面定义的滤镜和填充。
// 准备好图表实例
const graph = new Graph({
container: document.getElementById('container'),
// ... 其他配置
});
// --- 注册手绘风格的节点 ---
// 1. 手绘风格 - 圆角矩形 (对应图片右侧的形状)
Graph.registerNode('sketch-rect', {
inherit: 'rect', // 继承自矩形
attrs: {
body: {
// 关键样式
stroke: '#a26740',
strokeWidth: 2,
fill: 'url(#pattern-hatch)', // 使用阴影线填充
filter: 'url(#filter-sketch)', // 应用手绘滤镜
},
label: {
// 推荐使用手写体
fontFamily: '"Comic Sans MS", "Caveat", cursive',
fill: '#a26740',
fontSize: 14,
},
},
});
// 2. 手绘风格 - 菱形 (对应图片左侧的形状)
// 菱形需要自定义 SVG 路径
const diamondPath = 'M 30 0 L 60 30 L 30 60 L 0 30 Z';
Graph.registerNode('sketch-diamond', {
inherit: 'path', // 继承自路径
width: 60,
height: 60,
attrs: {
body: {
// 关键样式
refD: diamondPath,
stroke: '#a26740',
strokeWidth: 2,
fill: 'url(#pattern-hatch)',
filter: 'url(#filter-sketch)',
},
label: {
fontFamily: '"Comic Sans MS", "Caveat", cursive',
fill: '#a26740',
fontSize: 14,
},
},
});
第 3 步:配置边的样式
边的样式也需要应用相同的滤镜,可以在图表初始化配置中统一设置。
const graph = new Graph({
container: document.getElementById('container'),
grid: true,
// ...
// --- 配置边的连接样式 ---
connecting: {
router: 'manhattan',
createEdge() {
return new Shape.Edge({
attrs: {
line: {
stroke: '#a26740',
strokeWidth: 2,
filter: 'url(#filter-sketch)', // 为边线应用滤镜
targetMarker: {
name: 'block', // 箭头样式
width: 12,
height: 8,
// 注意:需要给箭头也加上滤镜和填充/描边
attrs: {
filter: 'url(#filter-sketch)',
}
},
},
},
});
},
},
});
效果调整
可以实现一个完整的手绘风格主题。如果你想微调效果,可以重点关注:
-
抖动程度:修改
<filter>
中的scale
值。值越大,抖动越厉害。 -
阴影密度:修改
<pattern>
的width
,height
和内部<path>
的stroke-width
。 -
颜色:直接在 X6 节点的
attrs
中修改stroke
和fill
颜色,以及在<pattern>
中修改stroke
颜色。 - 字体:为了更好的效果,你可以通过 CSS 引入一些免费的在线手写字体,例如 Google Fonts 上的 Caveat 或 Patrick Hand。