obsidian-computer
前端识记前端面试

04-css-html

HTML 与 CSS

最后更新 2026年8月29日

HTML 与 CSS

大厂前端面试标准答案(美团 / 快手 / 字节方向)。每题结构:【核心答案】30 秒口述版 → 【展开】原理 + 可运行代码 → 【追问 & 加分点】。

目录


1. head 与 body 的职责;meta、link、style 标签

【核心答案】

<head> 存放文档的元信息——不直接渲染给用户,但决定浏览器如何解析、渲染和对待这个文档:字符编码、视口、标题、SEO 描述、样式表、图标、脚本引用。<body> 存放实际渲染的内容——所有可见的 DOM 结构。二者的分工本质上是"渲染指令 vs 渲染内容"。<meta> 描述文档级元数据(编码、视口、SEO、http-equiv 伪响应头);<link> 声明当前文档与外部资源的关系,靠 rel 属性表达关系类型,能引入样式表、图标、字体预载、DNS 预连接、RSS、规范链接等,绝不只是"引 CSS 的标签";<style> 内嵌 CSS。type 属性用于声明资源的 MIME 类型,在现代 HTML5 里对 CSS/JS 已经是可省略的历史包袱,但在 <script type="module">、<script type="application/json">、<link rel="preload" as="font" type="font/woff2"> 这些场景仍然有实际作用。

【展开】

1.1 head 的职责与关键内容

<head>
  <!-- 必须放在最前 1024 字节内,否则浏览器可能已用错误编码解析出乱码 -->
  <meta charset="UTF-8">

  <!-- 移动端适配的基石:告诉浏览器视口宽度等于设备宽度,别用 980px 虚拟视口再缩放 -->
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

  <title>页面标题</title>            <!-- SEO 权重最高的单个标签,也是标签页/书签/分享标题 -->
  <base href="/app/">                <!-- 全局相对 URL 基准,一个文档只能有一个,慎用 -->

  <meta name="description" content="页面描述,搜索结果的摘要来源">
  <meta name="theme-color" content="#1677ff">   <!-- 移动端浏览器地址栏染色 -->

  <!-- 社交分享卡片(Open Graph),产品项目基本必配 -->
  <meta property="og:title" content="标题">
  <meta property="og:image" content="https://x.com/cover.png">

  <!-- 伪造 HTTP 响应头。注意 charset 之外的用法多数已被真实响应头取代 -->
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta http-equiv="Content-Security-Policy" content="default-src 'self'">

  <link rel="canonical" href="https://x.com/article/1">   <!-- 告诉搜索引擎哪个是权威 URL -->
  <link rel="icon" href="/favicon.ico" sizes="any">
  <link rel="stylesheet" href="/main.css">
  <style>body { margin: 0 }</style>
</head>

为什么 CSS 放 head、JS 传统上放 body 末尾?

  • CSS 影响 CSSOM,而渲染树 = DOM + CSSOM。样式放头部可以让 CSSOM 尽早构建完成,避免"先渲染无样式内容再重排"(FOUC)。
  • 经典 <script> 会阻塞 HTML 解析(下载 + 执行期间解析暂停),放末尾可以让内容先出来。
  • 现代做法:直接用 <script defer> 放 head——下载与解析并行,执行时机在 DOM 解析完成后、DOMContentLoaded 之前,且多个 defer 按文档顺序执行。<script type="module"> 默认就是 defer 行为。async 则是下载完立即执行、顺序不保证,只适合独立的埋点/统计脚本。

1.2 <meta> 分类

形式作用例子
charset字符编码,必须最靠前<meta charset="UTF-8">
name + content文档级元数据viewport / description / keywords(已基本无 SEO 价值)/ robots / referrer / theme-color / format-detection
http-equiv + content模拟 HTTP 响应头Content-Security-Policy / refresh / X-UA-Compatible
property + content第三方协议扩展Open Graph og:*

几个实用的:

<meta name="robots" content="noindex, nofollow">          <!-- 禁止收录,测试环境必配 -->
<meta name="referrer" content="no-referrer-when-downgrade"><!-- 控制 Referer 泄露 -->
<meta name="format-detection" content="telephone=no">      <!-- 阻止 iOS 把数字识别为电话 -->
<meta http-equiv="refresh" content="3;url=/new">           <!-- 3 秒后跳转,无障碍差,慎用 -->

<link> 的语义是"声明当前文档与外部资源的关系",rel 决定这个关系是什么,因此能力远超"引 CSS":

rel用途
stylesheet引入 CSS。配合 media 可条件加载:media="(max-width:768px)"(不匹配的表仍会下载,但优先级降低且不阻塞渲染)
icon / apple-touch-icon网站图标 / iOS 主屏图标
preload高优先级预加载当前页必需资源,需配 as
prefetch低优先级预取未来页面资源
preconnect / dns-prefetch提前建连 / 提前 DNS 解析
modulepreload预载 ES Module 并递归其依赖
manifestPWA 清单文件
canonicalSEO 权威地址
alternate备用版本:多语言 hreflang、RSS/Atom feed
prev / next分页关系
author / license作者、许可信息
<link rel="stylesheet" href="/print.css" media="print">
<link rel="alternate" hreflang="en" href="https://x.com/en/">
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed.xml">
<link rel="preload" href="/f/Inter.woff2" as="font" type="font/woff2" crossorigin>

注意 <link> 只能放在 <head> 里(HTML5 规范中,除了带 itemprop 的特殊情况)。而 @import 虽然也能引 CSS,但它是串行加载(必须等外层 CSS 下载解析完才发现 @import 的地址),严重拖慢首屏,生产环境禁止使用(构建工具在编译期内联的 @import 不算,那已经被打平了)。

1.4 type 属性做什么用

type 声明所链接/内嵌资源的 MIME 类型,让浏览器知道该如何处理。

场景说明
<link rel="stylesheet" type="text/css">可省略。HTML5 中 stylesheet 默认就是 CSS,写不写都一样
<style type="text/css">同上,可省略
<script type="text/javascript">可省略,是 HTML4 遗留
<script type="module">有实际作用:启用 ES Module 语义(严格模式、独立作用域、import/export、默认 defer、跨域按 CORS 规则)
<script type="application/json"> / type="text/template">有实际作用:type 为未知类型时浏览器不执行该脚本,内容可被 JS 读取。这是内嵌数据(如 SSR 的 __INITIAL_STATE__)和模板的常用手法
<script type="importmap">定义模块路径映射
<link rel="preload" as="font" type="font/woff2">有实际作用:浏览器不支持该格式时可跳过下载
<input type="...">完全不同的东西,是控件类型,不是 MIME
<!-- 类型未知 → 不执行,作为数据容器 -->
<script type="application/json" id="state">{"user":{"id":1}}</script>
<script>
  const state = JSON.parse(document.getElementById('state').textContent);
</script>

【追问 & 加分点】

  1. "为什么 charset 必须在前 1024 字节?" —— 浏览器先用默认编码试探性解析文档开头,找到 charset 后若与猜测不符会丢弃已解析内容重新解析。超过 1024 字节则可能已经渲染出乱码或触发重解析,浪费性能。
  2. "<style> 能不能放 body?" —— 语法上可以(HTML5 允许,Chrome 也支持),但会导致已渲染内容重新计算样式引起重排闪烁。唯一合理场景是关键 CSS 内联优化时的极端手段,正常项目应放 head。
  3. "defer 和 async 的选择?" —— 有 DOM 依赖或脚本间有依赖顺序 → defer;完全独立的第三方统计脚本 → async;需要在 HTML 解析途中同步执行的(如 A/B 分流的防闪烁脚本)→ 内联无属性 script。
  4. "viewport 里的 viewport-fit=cover 干什么?" —— 让页面延伸到 iPhone 刘海屏的安全区之外,配合 CSS 的 env(safe-area-inset-bottom) 做底部导航栏避让。这是移动端项目的必备细节。

2. Less / Sass 的原理与 CSS Modules 的原理

【核心答案】

Less / Sass(scss)是 CSS 预处理器:它们定义了一套 CSS 的超集语法(变量、嵌套、混入 mixin、函数、条件循环、模块导入),通过一个编译器在构建期把源码转成标准 CSS,浏览器最终拿到的仍然是普通 CSS。编译过程就是典型的编译器三段式:词法分析 → 语法分析生成 AST → 遍历 AST 求值(展开嵌套、替换变量、执行 mixin 与函数)→ 生成目标 CSS 字符串。所以它们的能力边界完全在编译期,运行时没有任何动态能力——这也是 CSS 自定义属性(CSS 变量)无法被预处理器变量替代的根本原因。CSS Modules 解决的是另一个问题:全局类名冲突。它不是新语法,而是构建工具(css-loader)的一种编译策略——把每个 .module.css 文件里的类名按"文件路径 + 类名 + 内容"哈希成全局唯一的字符串(如 Button_primary__x7f2a),同时导出一个 { 原类名: 哈希类名 } 的映射对象供 JS 引用。作用域隔离是通过重命名实现的,不是浏览器提供的隔离机制。

【展开】

2.1 预处理器的编译流程

源码 .scss / .less
   ↓ ① 词法分析(Lexer / Tokenizer)
   把字符流切成 token:选择器、属性名、变量 $primary、操作符、括号……
   ↓ ② 语法分析(Parser)
   按语法规则组装成 AST(抽象语法树),节点类型有 Rule、Declaration、Mixin、
   Variable、Function、Media、Import……
   ↓ ③ 求值 / 转换(Evaluate / Transform)
   - 变量替换:$primary → #1677ff(作用域链查找)
   - 嵌套展开:把父选择器逐层拼接,处理 & 引用
   - mixin 展开:把 @include 的内容原地复制并替换实参
   - 函数求值:darken()、map-get()、calc 的编译期部分
   - 控制流:@if / @each / @for 展开成多条规则
   - @import / @use 递归编译并合并
   ↓ ④ 代码生成(Generator)
   遍历处理后的 AST,序列化成标准 CSS 字符串(+ Source Map)
   ↓
输出 .css → 浏览器

嵌套展开的实质是选择器字符串拼接:

// 源码
.card {
  padding: 16px;
  &__title { font-size: 18px; }          // & 指代父选择器全文
  &:hover { box-shadow: 0 2px 8px #0002; }
  .icon { margin-right: 4px; }           // 后代选择器
  > .footer { border-top: 1px solid #eee; }
}
/* 编译产物 */
.card { padding: 16px; }
.card__title { font-size: 18px; }
.card:hover { box-shadow: 0 2px 8px #0002; }
.card .icon { margin-right: 4px; }
.card > .footer { border-top: 1px solid #eee; }

面试常问的坑:嵌套过深会生成很长的后代选择器,既增大产物体积又提高特异性(导致后续难以覆盖)。业界共识是嵌套不超过 3 层(Inception Rule)。

2.2 Less 与 Sass(scss) 的差异

维度LessSass / SCSS
实现语言JavaScript(less.js)原为 Ruby,现主流是 Dart Sass(sass 包),LibSass 已废弃
变量符号@primary: #1677ff$primary: #1677ff
混入.mixin() { } + .mixin();@mixin name { } + @include name
条件守卫 when (@x > 0)@if / @else,更接近编程语言
循环递归 mixin(较绕)@for / @each / @while
数据结构有限maps(map.get),非常强
模块系统@import@use / @forward(有命名空间,不会重复引入)
运行时编译支持(less.js 可在浏览器跑,antd v4 主题定制就靠它)不支持浏览器端
生态主要是 antd v4 等历史项目社区主流

Less 的浏览器端运行能力是它一个特殊卖点:antd v4 的动态换肤就是在运行时用 less.js 重新编译变量。而 antd v5 改用 CSS-in-JS 的 token 体系后就不再依赖 Less 了。

2.3 预处理器 vs CSS 变量(高频追问)

$color: red;                    // 编译期:产物里根本不存在 $color,只有 red
.a { color: $color; }
:root { --color: red; }         /* 运行时:浏览器真实持有这个属性 */
.a { color: var(--color); }
.dark { --color: white; }       /* 可被 JS / 媒体查询 / 类名动态改变,且会级联继承 */
预处理器变量CSS 自定义属性
生效阶段编译期运行时
能否 JS 修改否能(el.style.setProperty('--color', '#000'))
作用域词法作用域(文件/块)DOM 层级作用域,随级联继承
媒体查询内可变否是(响应式换值的关键)
能用在 @media 条件里是否
浏览器体积编译后消失,无成本保留在产物中

结论:换肤、暗色模式、响应式 token 必须用 CSS 变量;纯粹的编译期常量、mixin、循环生成工具类用预处理器。二者是互补而非替代。

2.4 CSS Modules 原理

问题:CSS 的类名是全局的,两个组件都写 .title 就会互相覆盖。传统解法是 BEM 命名约定(靠人自觉),CSS Modules 则是靠编译器强制保证。

编译过程:

/* Button.module.css */
.primary { background: #1677ff; color: #fff; }
.large   { padding: 12px 24px; }
:global(.ant-btn) { border-radius: 8px; }        /* 显式声明为全局,不被哈希 */
.danger  { composes: primary; background: #f5222d; }  /* 组合复用 */

css-loader 处理后:

/* 产物 CSS:类名被重命名 */
.Button_primary__x7f2a { background: #1677ff; color: #fff; }
.Button_large__k91bc   { padding: 12px 24px; }
.ant-btn               { border-radius: 8px; }   /* :global 保持原样 */
.Button_danger__m3d0e  { background: #f5222d; }
// 同时生成一个 JS 映射对象作为该模块的默认导出
export default {
  primary: 'Button_primary__x7f2a',
  large:   'Button_large__k91bc',
  danger:  'Button_danger__m3d0e Button_primary__x7f2a', // composes 展开为多个类名
};
// 使用
import styles from './Button.module.css';

export default function Button({ size }) {
  // styles.primary 拿到的是编译后的哈希类名
  return <button className={`${styles.primary} ${size === 'lg' ? styles.large : ''}`}>确定</button>;
}

关键机制拆解:

  1. 命名规则可配置(webpack / Vite 都支持):
// vite.config.js
export default {
  css: {
    modules: {
      // 开发期用可读格式便于调试,生产用纯 hash 减小体积
      generateScopedName:
        process.env.NODE_ENV === 'production'
          ? '[hash:base64:6]'
          : '[name]__[local]___[hash:base64:5]',
      localsConvention: 'camelCaseOnly',   // .btn-primary → styles.btnPrimary
    },
  },
};
  1. :local / :global:默认所有类名是 :local(会被哈希)。用 :global(.xxx) 或 :global { ... } 块可以逃逸到全局,这正是覆盖 antd 样式的常用手段。
  2. composes:CSS Modules 特有的组合语法,编译时把被组合类的哈希名一起塞进导出的字符串,实现"CSS 层面的继承"而不用写 @extend 那种会污染选择器的方案。只能用于同一规则的第一个声明位置,且只能组合本地类名或显式 from 的其他模块。
  3. 它不隔离什么::root 变量、元素选择器(div{})、@keyframes 名(其实动画名也会被哈希)、!important、以及继承属性依然是全局生效的。所以 CSS Modules 只是"类名作用域",不是真正的样式沙箱。

真正的样式隔离是 Shadow DOM(Web Components),样式无法穿透进出,但代价是难以主题化(只能通过 CSS 变量和 ::part() 开口子),React 生态用得少。

2.5 与其他方案的定位

方案隔离方式生效阶段动态能力
BEM 约定靠人——
CSS Modules编译期重命名编译期弱(只能切类名)
CSS-in-JS(styled-components / emotion)运行时生成 hash 类名并插入 style 标签运行时强(props 驱动样式),但有运行时开销
零运行时 CSS-in-JS(vanilla-extract、Linaria)编译期抽取成静态 CSS编译期中(靠 CSS 变量)
原子化 CSS(Tailwind、UnoCSS)不隔离,靠原子类无副作用编译期扫描生成弱,但产物体积随项目增长趋于收敛
Shadow DOM浏览器原生运行时—

【追问 & 加分点】

  1. "CSS Modules 的哈希是怎么保证唯一的?" —— 默认摘要输入包含文件相对路径 + 类名(部分配置还含文件内容)。所以同名类在不同文件里哈希不同;同一文件内容变化会导致哈希变化,这对长效缓存友好但也意味着不能把哈希类名硬编码在别处。
  2. "为什么 composes 比 @extend 好?" —— Sass 的 @extend 是把选择器合并进同一条规则(.a, .b { }),在嵌套和媒体查询下会产生选择器爆炸且难以预测;composes 只是让 JS 侧多输出一个类名,产物 CSS 完全不变,可预测性强得多。
  3. "预处理器会不会被 CSS 原生嵌套取代?" —— 原生 CSS Nesting 已在主流浏览器落地,覆盖了嵌套这一最常用功能;配合 CSS 变量、@layer、color-mix()、相对颜色语法,预处理器的独占价值确实在收缩。但 mixin、编译期循环生成、maps、跨文件模块化仍无原生替代,短期不会消失。
  4. "Source Map 在这里起什么作用?" —— 编译后浏览器看到的是产物 CSS,DevTools 里定位不到源文件。Source Map 记录了产物位置到源码位置的映射,让 DevTools 直接显示 Button.module.scss:12。生产环境应关闭或只上传给监控平台,避免暴露源码。

3. 如何正确覆盖 antd 等组件库的样式

【核心答案】

先要明确"覆盖不生效"的根因通常有三个:特异性不够(组件库选择器往往是 .ant-btn-primary 这类多层组合,你写的单类名权重更低)、类名被 CSS Modules 哈希掉了(你写的 .ant-btn 变成了 .Foo_ant-btn__x1,根本没匹配上)、样式插入顺序在组件库之前(同权重时后者胜出,而组件库 CSS 常常在你的 CSS 之后被注入)。正确的解决顺序应该是从"官方支持的定制入口"到"暴力覆盖"逐级降级:antd v5 优先用 ConfigProvider 的 theme token / algorithm(改的是设计变量,全局一致且升级安全),其次用组件的 className / styles 属性做局部定制,再次用 CSS 变量,然后才是提升特异性 + :global 的选择器覆盖,最后才是 !important。!important 不是解法而是债务——它会让后续所有覆盖都被迫升级为 !important。

【展开】

3.1 先诊断:为什么没覆盖上

打开 DevTools 的 Elements → Styles 面板:

  • 你的规则根本没出现 → 选择器写错了,或者被 CSS Modules 哈希了
  • 你的规则出现但被划掉 → 特异性低,或顺序靠前
  • 出现且生效但视觉没变 → 被内联样式或子元素的样式覆盖了

3.2 特异性(Specificity)复习

特异性用四元组 (行内, ID, 类/属性/伪类, 元素/伪元素) 比较,从左往右逐位比较,高位大者胜,不进位:

!important                      →  凌驾于一切(同为 important 时再按特异性比)
style="color:red"               →  (1,0,0,0)
#app .btn                       →  (0,1,1,0)
.ant-btn.ant-btn-primary        →  (0,0,2,0)
.ant-btn:hover                  →  (0,0,2,0)
.ant-btn                        →  (0,0,1,0)
button                          →  (0,0,0,1)
* / :where(...)                 →  (0,0,0,0)   ← :where() 内部选择器不计权重
:is(...) / :not(...)            →  取括号内特异性最高的那个

特异性相同时,后定义的胜出——这就是"样式插入顺序"问题的由来。

3.3 方案一(首选):antd v5 的 Theme Token

antd v5 用 CSS-in-JS + 设计 token 体系,官方的定制入口就是 ConfigProvider:

import { ConfigProvider, theme, Button } from 'antd';

export default function App() {
  return (
    <ConfigProvider
      theme={{
        // 算法:内置暗色 / 紧凑,可组合
        algorithm: [theme.darkAlgorithm, theme.compactAlgorithm],
        // 全局 Seed Token:影响整个设计体系派生出的所有 Map/Alias token
        token: {
          colorPrimary: '#722ed1',
          borderRadius: 8,
          fontFamily: 'Inter, system-ui, sans-serif',
        },
        // 组件级 token:只影响单个组件,比全局改动更安全
        components: {
          Button: { controlHeight: 36, paddingContentHorizontal: 20 },
          Table:  { headerBg: '#fafafa' },
        },
      }}
    >
      <Button type="primary">按钮</Button>
    </ConfigProvider>
  );
}

为什么这是最优解:改的是设计变量而非具体选择器,不依赖组件库的内部 DOM 结构和类名,因此升级 antd 版本时不会因为内部结构调整而失效。局部区域还可以嵌套 ConfigProvider 只影响子树。

antd v4 对应的做法是 less 变量覆盖(modifyVars / less-loader 的 additionalData),改 @primary-color 等变量后在编译期生成新的 CSS。二者思路一致:在源头改变量,而不是在末端覆盖选择器。

3.4 方案二:组件暴露的 className / styles API

antd v5 大量组件提供了 classNames 和 styles 语义化属性,可以直接定位到内部节点:

<Modal
  classNames={{ header: 'my-modal-header', body: 'my-modal-body' }}
  styles={{ mask: { backdropFilter: 'blur(4px)' }, body: { padding: 24 } }}
/>

这比猜 .ant-modal-header 稳定得多,因为它是公开 API,有版本兼容承诺。

3.5 方案三:CSS 变量

自己的设计系统应该建立在 CSS 变量之上,这样换肤和局部定制都不需要覆盖选择器:

:root {
  --brand: #1677ff;
  --radius-md: 8px;
  --space-4: 16px;
}
[data-theme='dark'] {
  --brand: #4096ff;
}
.my-card { border-radius: var(--radius-md); border-color: var(--brand); }
// 运行时换肤:只改一个变量,所有引用处同步生效,无需重排选择器
document.documentElement.style.setProperty('--brand', '#722ed1');

3.6 方案四:选择器覆盖(含 CSS Modules 场景)

在 CSS Modules 里,组件库类名必须用 :global 包起来,否则会被哈希:

/* MyPage.module.less */
.wrapper {                        /* 这个会被哈希,作为限定作用域的"锚点" */
  /* 用 :global 声明内部的 ant- 类名不参与哈希 */
  :global {
    .ant-btn-primary {
      background: #722ed1;
      border-color: #722ed1;
    }
    .ant-table-thead > tr > th {
      background: #fafafa;
    }
  }
}
// 关键:外层套一个自己的哈希类名,让覆盖只作用于本页面,不污染全局
<div className={styles.wrapper}>
  <Button type="primary">按钮</Button>
  <Table ... />
</div>

编译后是 .MyPage_wrapper__a1b2 .ant-btn-primary,特异性 (0,0,2,0) 且带作用域,比全局裸写 .ant-btn-primary 既更安全又更容易赢。

提升特异性的几种正规手段(优先于 !important):

/* ① 重复类名——最干净的提权手段,不引入无关结构依赖 */
.ant-btn.ant-btn { color: red; }               /* (0,0,2,0) */

/* ② 用自己的容器限定 */
.my-scope .ant-btn { color: red; }             /* (0,0,2,0) + 作用域 */

/* ③ :is() 提权(括号内取最高特异性) */
:is(#app) .ant-btn { color: red; }             /* (0,1,1,0) */

/* ④ 反向:用 :where() 主动降权,写基础样式时非常有用 */
:where(.reset) button { margin: 0; }           /* (0,0,0,1),任何人都容易覆盖 */

3.7 方案五:@layer(现代方案,加分项)

CSS Cascade Layers 让你在特异性之前先按层级排序,层的顺序优先于特异性:

/* 先声明层顺序,后面的层永远赢前面的层,与特异性无关 */
@layer reset, vendor, components, utilities;

@layer vendor {
  /* 即使这里写了 #app .ant-btn.ant-btn 高特异性 */
  #app .ant-btn.ant-btn { color: blue; }
}
@layer utilities {
  /* 这个低特异性的规则依然赢,因为 utilities 层在后面 */
  .text-red { color: red; }
}

这从根本上解决了"和组件库比特异性"的军备竞赛。缺点是需要组件库配合(或者用 @import url(antd.css) layer(vendor) 把整个库塞进低优先级层)。

3.8 方案对比

方案侵入性升级安全性适用
Theme Token / less 变量低高(官方 API)全局品牌色、圆角、间距、暗色模式
组件 classNames / styles低高单个组件的局部微调
CSS 变量低高自建设计系统、动态换肤
:global + 作用域选择器中中(依赖内部类名,可能因版本变化失效)组件库没开放 token 的细节
@layer中高有条件时的架构级解法
提升特异性(重复类名)中中应急
!important高低只在覆盖第三方内联样式时使用

3.9 !important 的唯一合理场景

组件库通过 JS 写了内联样式(style 属性,特异性 (1,0,0,0)),普通选择器无论如何都赢不了,此时只能:

.ant-tooltip-inner { max-width: 320px !important; }

即便如此也要加作用域限制影响范围,并写注释说明原因和替代方案,避免变成技术债。

【追问 & 加分点】

  1. "为什么样式顺序会出问题?" —— 组件库 CSS 常由 import 'antd/dist/reset.css' 或 CSS-in-JS 运行时注入,注入时机可能晚于你的样式表。webpack 的 MiniCssExtractPlugin 抽取顺序、Vite 的模块图顺序都会影响最终产物顺序。稳妥做法是不依赖顺序——用作用域 + 足够特异性,或者用 @layer 显式定序。
  2. "antd v5 的 CSS-in-JS 有性能问题吗?" —— 有首屏运行时开销(要动态生成并插入样式)。官方给了 @ant-design/cssinjs 的 StyleProvider + SSR 提取,以及 hashPriority: 'high' 降低选择器特异性。大型项目也可以做静态样式提取。
  3. "怎么防止自己的覆盖污染整个应用?" —— 永远在自己的作用域类名下写覆盖;用 CSS Modules 的 :global 时必须嵌套在本地类内,绝不在文件顶层裸写 :global .ant-btn。
  4. "微前端下多个应用用不同版本 antd 怎么办?" —— antd 支持 prefixCls 配置(ConfigProvider prefixCls="app1"),让类名前缀不同从而互不干扰;CSS-in-JS 还可以配置不同的 container 插入位置。

4. flex: 1 0 auto 是什么意思

【核心答案】

flex 是 flex-grow、flex-shrink、flex-basis 三个属性的简写。flex: 1 0 auto 表示:flex-grow: 1——当容器有剩余空间时,该项按比例 1 参与瓜分(可以变大);flex-shrink: 0——当容器空间不足时,该项不允许被压缩(保持自己的尺寸,可能溢出);flex-basis: auto——分配空间前的初始主轴尺寸取自该项自身的 width(主轴为横向时)或内容宽度。所以它的语义是"可以长大,但绝不缩小,起始尺寸看自己"。这与常写的 flex: 1(= 1 1 0%,可长可缩、且忽略自身宽度平均分配)有本质区别:flex: 1 会让多个项等宽,而 flex: 1 0 auto 会在自身内容宽度的基础上再瓜分剩余空间,所以内容多的项更宽。

【展开】

4.1 三个子属性拆解

flex-grow(默认 0):剩余空间的分配比例。

容器 600px,三个子项 basis 各 100px → 剩余 300px
flex-grow 分别为 1 / 2 / 0
→ 项1 得 300 × 1/3 = 100,最终 200px
→ 项2 得 300 × 2/3 = 200,最终 300px
→ 项3 得 0,最终 100px

flex-shrink(默认 1):空间不足时的收缩比例。注意收缩是按 flex-shrink × flex-basis 的加权比例计算的,不是单纯按 shrink 值:

容器 400px,两个子项 basis 分别 300px 和 300px → 超出 200px
flex-shrink 分别为 1 和 2
加权和 = 300×1 + 300×2 = 900
项1 收缩 200 × (300×1/900) = 66.7  → 233.3px
项2 收缩 200 × (300×2/900) = 133.3 → 166.7px

设计成加权是合理的:大元素理应承担更多收缩量,否则小元素会先被压没。

flex-basis(默认 auto):分配空间前的初始主轴尺寸。

值含义
auto回退看自身 width/height;若也是 auto,则用内容尺寸(max-content 受 min/max 约束)
0 / 0%初始尺寸视为 0,完全由 grow 比例决定最终大小(这是等分的关键)
具体长度 200px直接指定
content按内容尺寸,忽略 width(兼容性较差,少用)

flex-basis 优先级高于 width(主轴为横向时)。但 min-width / max-width 会最终钳制结果——这一点非常重要,见下面的经典 bug。

4.2 常见简写对照表

简写等价于语义
flex: initial(默认值)0 1 auto不放大、可缩小、看自身尺寸
flex: 11 1 0%可放大可缩小,basis 为 0 → 完全按比例等分,忽略内容宽度
flex: 22 1 0%同上,占 2 份
flex: auto1 1 auto可放大可缩小,basis 看内容 → 内容多的更宽
flex: none0 0 auto完全刚性,不放大不缩小,固定为自身尺寸
flex: 1 0 auto—可放大、不缩小、basis 看内容
flex: 0 1 auto= initial只缩不放
flex: 100px1 1 100px单值为长度时,赋给 basis,grow/shrink 取 1

单值语义规则:单个无单位数值 → flex-grow,且 basis 补 0%(不是 auto,这是最容易记错的点);单个长度值 → flex-basis;none / auto / initial 是关键字。

4.3 flex: 1 vs flex: auto 的可视差异

<div class="row a">
  <div>短</div>
  <div>这是一段很长很长很长的文字内容</div>
</div>
<div class="row b">
  <div>短</div>
  <div>这是一段很长很长很长的文字内容</div>
</div>

<style>
.row { display: flex; width: 500px; margin-bottom: 12px; }
.row > div { background: #e6f4ff; outline: 1px solid #1677ff; padding: 8px; }

/* basis: 0 → 内容宽度不参与计算,两项严格各 250px */
.a > div { flex: 1; }

/* basis: auto → 先按内容宽度分配,再把剩余空间平均加到两项上,长文那项明显更宽 */
.b > div { flex: auto; }
</style>

记忆口诀:要严格等分用 flex: 1;要按内容比例分配再摊剩余用 flex: auto;要固定不动用 flex: none。

4.4 flex: 1 0 auto 的典型用途

① 撑满剩余高度的粘性布局:

.page {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
.header, .footer { flex: none; }   /* 高度固定,绝不被压缩 */
.main { flex: 1 0 auto; }          /* 撑满剩余高度;内容超出时不被压扁,而是撑高页面 */

对比:如果 main 写 flex: 1(shrink 为 1),内容超长时它会被压缩,min-height 的保护也可能失效,导致 footer 覆盖内容。shrink: 0 保证了"内容优先"。

② 横向不换行的卡片列表:

.scroller { display: flex; overflow-x: auto; gap: 12px; }
.card { flex: 1 0 200px; }   /* 至少 200px,有空间时一起变宽,永不被挤瘦 */

4.5 必考陷阱:flex 项无法收缩到内容以下

/* ❌ 长文本或 <pre> 会把这个 flex 项撑破,overflow: hidden / text-overflow 全部失效 */
.item { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

原因:flex 项的 min-width 计算值默认是 auto(不是 0),意味着它的最小宽度是内容的最小宽度(一个长单词、一张图片的固有宽度)。收缩计算被这个下限钳住,所以再怎么 shrink 也压不下去。

/* ✅ 修复:显式解除下限 */
.item {
  flex: 1;
  min-width: 0;              /* 关键。纵向布局对应 min-height: 0 */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 或者 overflow 不为 visible 时,min-width:auto 会被解析为 0,
   但显式写 min-width: 0 更可靠、意图更明确 */

这是面试和实战中最高频的 flex 问题,Grid 里同样存在(grid item 的 min-width: auto,解法一样,或用 minmax(0, 1fr))。

【追问 & 加分点】

  1. "flex-basis: 0 和 width: 0 有什么不同?" —— flex-basis 只影响主轴的初始分配尺寸且优先级高于 width;width: 0 在交叉轴方向仍然是宽度。另外 flex-basis: 0% 参与百分比解析,在容器尺寸未确定(如 height: auto 的纵向布局)时可能行为不同,flex-basis: 0(无单位)更稳妥。
  2. "为什么 flex: 1 的 basis 是 0% 而不是 auto?" —— 规范如此定义,目的是让 flex: 1 的默认语义就是"严格等分",这是最常见的诉求。如果 basis 保持 auto,等分就会被内容宽度干扰,反直觉。
  3. "收缩比例为什么要乘 basis?" —— 若不加权,一个 1000px 和一个 100px 的项按相同 shrink 收缩相同绝对值,小项会先被压到 0 而大项几乎没变。加权保证收缩量正比于自身尺寸,视觉上更均衡。
  4. "gap 会影响 flex 计算吗?" —— 会。gap 占用的空间在剩余空间计算之前就被扣除,所以 flex: 1 等分的是"容器宽度 - 所有 gap"。这也是 flex gap 相比 margin 的优势:不需要处理首尾项的负边距。

5. 三列布局的多种实现

题设:左右两列定宽(左 200px、右 150px),中间自适应,且中间栏内容优先渲染(这是圣杯/双飞翼诞生的原因——早期为了 SEO 和首屏体验,要把主内容写在 HTML 最前面,但视觉上又要居中)。

【核心答案】

现代方案首选 Grid(一行 grid-template-columns: 200px 1fr 150px 解决,语义最清晰)或 Flex(order 或直接 DOM 顺序 + flex: 1)。圣杯布局和双飞翼布局是 float 时代的产物:都靠"三个 float 元素 + 负 margin 把左右列拉到中间列同一行",区别在于防止中间内容被左右列遮挡的方式——圣杯用父容器 padding 留槽位再用相对定位把左右列推出去,双飞翼在中间列内部再套一层 div 用 margin 留白。它们今天已无实用价值,但面试常考,因为能看出对浮动、BFC、负 margin、相对定位的理解深度。

【展开】

方案一:Grid(首选)

<div class="grid-layout">
  <main>中间内容(DOM 中可以写在最前)</main>
  <aside class="left">左 200px</aside>
  <aside class="right">右 150px</aside>
</div>

<style>
.grid-layout {
  display: grid;
  /* 直接声明三列尺寸,1fr 表示中间吃掉全部剩余空间 */
  grid-template-columns: 200px 1fr 150px;
  gap: 16px;
  min-height: 200px;
}
/* grid-area 可以任意指定位置,彻底解耦 DOM 顺序与视觉顺序 */
.left  { grid-column: 1; }
main   { grid-column: 2; min-width: 0; }  /* min-width:0 防止长内容撑破 */
.right { grid-column: 3; }
</style>

更语义化的写法用命名区域,一眼看懂布局形状:

.grid-layout {
  display: grid;
  grid-template-columns: 200px 1fr 150px;
  grid-template-areas: "left main right";
}
.left { grid-area: left; } main { grid-area: main; } .right { grid-area: right; }

/* 响应式:窄屏一键改为上下堆叠,无需改 HTML */
@media (max-width: 768px) {
  .grid-layout {
    grid-template-columns: 1fr;
    grid-template-areas: "main" "left" "right";
  }
}

方案二:Flex

<div class="flex-layout">
  <main>中间内容(DOM 顺序在最前,SEO 友好)</main>
  <aside class="left">左 200px</aside>
  <aside class="right">右 150px</aside>
</div>

<style>
.flex-layout { display: flex; gap: 16px; }
.left  { flex: 0 0 200px; order: -1; }  /* order 负值 → 视觉上排到最前 */
main   { flex: 1; min-width: 0; }       /* 吃掉剩余空间;min-width:0 防撑破 */
.right { flex: 0 0 150px; order: 1; }
</style>

order 只改变视觉顺序,不改变 DOM 顺序与 Tab 焦点顺序——这既是它解决 SEO 问题的原理,也是无障碍隐患(键盘 Tab 顺序会与视觉顺序不一致)。生产环境应谨慎使用,能靠 Grid 定位就别用 order。

方案三:圣杯布局(Holy Grail,float + 负 margin + 相对定位)

<!-- 中间列必须写在最前 -->
<div class="holy-grail">
  <div class="center">中间自适应,内容优先</div>
  <div class="left">左 200</div>
  <div class="right">右 150</div>
</div>

<style>
.holy-grail {
  /* ① 父容器用 padding 给左右列预留"槽位",中间列的可用宽度因此被压缩 */
  padding: 0 150px 0 200px;
  overflow: hidden;              /* 触发 BFC 清除浮动(也可用 ::after clearfix) */
}
.holy-grail > div { float: left; min-height: 120px; }

.center {
  width: 100%;                   /* ② 100% 是相对父容器 content box,即已扣掉 padding */
  background: #e6f4ff;
}
.left {
  width: 200px;
  /* ③ -100% 等于中间列的宽度,把 left 从下一行拉回到中间列的起始位置 */
  margin-left: -100%;
  /* ④ 此时 left 压在 center 头上,用相对定位向左推出 padding 区域 */
  position: relative;
  left: -200px;
  background: #fff1f0;
}
.right {
  width: 150px;
  margin-left: -150px;           /* ⑤ 负自身宽度,回退到上一行末尾 */
  position: relative;
  right: -150px;                 /* ⑥ 向右推进 padding 区域 */
  background: #f6ffed;
}
</style>

缺陷:当窗口宽度小于 左宽 + 右宽 时布局会崩坏,所以必须给父级加 min-width。

方案四:双飞翼布局(玉伯 / 淘宝方案)

思路同为负 margin,但不用父容器 padding + 相对定位,而是给中间列内部再套一层:

<div class="wing">
  <div class="center">
    <!-- 关键:内层 div 用 margin 让出左右空间,父级无需 padding,也不需要相对定位 -->
    <div class="center-inner">中间自适应,内容优先</div>
  </div>
  <div class="left">左 200</div>
  <div class="right">右 150</div>
</div>

<style>
.wing { overflow: hidden; }             /* 清浮动 */
.wing > div { float: left; min-height: 120px; }

.center { width: 100%; }                /* 占满整行 */
.center-inner {
  margin: 0 150px 0 200px;              /* 内层让出左右槽位,内容不会被遮挡 */
  background: #e6f4ff;
}
.left  { width: 200px; margin-left: -100%; background: #fff1f0; }
.right { width: 150px; margin-left: -150px; background: #f6ffed; }
</style>

圣杯 vs 双飞翼:目标和负 margin 手法完全一样,唯一区别是给中间内容留白的方式——圣杯是"父容器 padding + 子元素 relative 反推"(结构少一层,CSS 多两步),双飞翼是"多一层 DOM + margin"(HTML 多一层,CSS 更简单直白、不会因窗口过窄而错位)。双飞翼更健壮,这也是它在淘宝落地的原因。

方案五:定位 + margin(简单场景够用)

.pos-layout { position: relative; min-height: 200px; }
.pos-layout .left  { position: absolute; left: 0;  top: 0; width: 200px; }
.pos-layout .right { position: absolute; right: 0; top: 0; width: 150px; }
.pos-layout main   { margin: 0 150px 0 200px; }   /* 用 margin 避开绝对定位的两列 */

缺陷:绝对定位脱离文档流,父容器不会被左右列撑高。如果侧栏比主内容高就会溢出,必须靠 min-height 或 JS 兜底。

方案六:table 布局(等高特性)

.table-layout { display: table; width: 100%; table-layout: fixed; }
.table-layout > div { display: table-cell; }
.table-layout .left  { width: 200px; }
.table-layout .right { width: 150px; }
/* 天然等高,无需额外处理;但 DOM 顺序即视觉顺序,中间内容无法前置 */

综合对比

方案代码量内容优先等高响应式友好推荐度
Grid极少✅(grid-area 任意排布)✅ 天然✅ 改一行媒体查询⭐⭐⭐⭐⭐
Flex少✅(order)✅ 天然(align-items: stretch)✅ 改 flex-direction⭐⭐⭐⭐⭐
双飞翼多✅❌ 需 padding-bottom + 负 margin hack❌⭐(面试用)
圣杯多✅❌ 同上❌⭐(面试用)
绝对定位少✅❌❌⭐⭐
table少❌✅❌⭐⭐

这就是为什么现代大厂不再用 float 布局:float 的设计初衷是"文字环绕图片",用它做整页布局属于滥用,代价是必须理解负 margin、BFC、清除浮动等一堆与业务无关的机制,还换不来等高和响应式。Flex/Grid 是为布局而生的规范,一行代码表达一个布局意图。

【追问 & 加分点】

  1. "为什么 margin-left: -100% 能把元素拉到上一行?" —— 负 margin 会缩减元素在流中占据的空间,-100% 是相对父容器 content 宽度的,恰好等于中间列(width:100%)的宽度,所以左列的起点被拉回到中间列的起点。而 .right 用 -150px(自身宽度)只需回退自己一格即可落在上一行末尾。
  2. "float 布局怎么做等高列?" —— 经典 hack 是 padding-bottom: 9999px; margin-bottom: -9999px; + 父级 overflow: hidden,用超大 padding 撑满再用负 margin 抵消。这个 hack 本身就说明 float 不适合做布局。
  3. "清除浮动有哪些方式?" —— ① 父级 overflow: hidden/auto(触发 BFC,副作用是裁剪溢出内容)② ::after { content:''; display:block; clear:both; }(clearfix,最通用)③ 父级也浮动 ④ display: flow-root(现代最优解,专门用于创建 BFC,无任何副作用)。
  4. "Grid 的 1fr 和 auto 有什么区别?" —— auto 按内容尺寸,剩余空间不一定分给它;1fr 表示"剩余空间的一份"。要注意 1fr 的隐含最小值是 auto,长内容仍会撑破,需要写 minmax(0, 1fr) 或给子项 min-width: 0。

6. 垂直水平居中的多种实现

【核心答案】

按现代优先级排:① Flex(display:flex; justify-content:center; align-items:center)——最通用,一行搞定,子元素尺寸任意;② Grid(display:grid; place-items:center)——代码最短,且能同时处理多个子元素的独立对齐;③ 绝对定位 + transform: translate(-50%,-50%)——不需要改父容器的显示模式,适合浮层/弹窗/遮罩这类脱离文档流的场景,且不要求子元素定宽高;④ 绝对定位 + inset:0; margin:auto——需要子元素定宽高,但无需 transform(对老浏览器和不希望触发合成层的场景友好)。此外还有行内元素专用的 line-height 单行居中、text-align:center + vertical-align:middle 的伪元素方案,以及 table-cell 方案,主要用于兼容老环境或特定场景。选择的核心判据是:父容器能否改布局模式、子元素是否定宽高、是否需要脱离文档流。

【展开】

统一的 HTML 结构:

<div class="box">
  <div class="item">居中内容</div>
</div>
.box  { width: 400px; height: 240px; border: 1px solid #1677ff; }
.item { background: #e6f4ff; padding: 12px 20px; }

方案一:Flex(最推荐)

.box {
  display: flex;
  justify-content: center;   /* 主轴(默认横向)居中 */
  align-items: center;       /* 交叉轴(默认纵向)居中 */
}

也可以只在子元素上写 margin: auto——flex 容器里的 auto margin 会吸收所有剩余空间,在两个方向同时生效(这是 flex 特有的,普通流中 margin: auto 不能做垂直居中):

.box { display: flex; }
.item { margin: auto; }      /* 单行搞定,且对多方向都有效 */

适用:绝大多数场景。子元素宽高任意、可自适应内容、可多行文本、易于扩展成多个子元素排列。这是默认答案。

方案二:Grid(代码最短)

.box { display: grid; place-items: center; }
/* place-items 是 align-items + justify-items 的简写 */

或者用 place-content / 子项自己控制:

.box  { display: grid; }
.item { place-self: center; }   /* 只让这一个子项居中,其他子项不受影响 */

适用:需要多个子元素各自独立对齐时,Grid 比 Flex 更灵活(place-self 逐项控制)。

方案三:绝对定位 + transform(不需要定宽高)

.box  { position: relative; }
.item {
  position: absolute;
  top: 50%;
  left: 50%;
  /* top/left 的 50% 是相对父容器;transform 的 50% 是相对自身,
     两者结合即可在不知道自身尺寸的情况下精确居中 */
  transform: translate(-50%, -50%);
}

适用:弹窗、Toast、遮罩层等本来就需要脱离文档流的元素;父容器不方便改成 flex/grid 时。 注意:transform 会创建层叠上下文和包含块——子元素中的 position: fixed 会相对它定位而非视口,这是一个高频坑。另外半像素偏移可能导致文字模糊(尺寸为奇数时),可加 will-change: transform 或改用整数尺寸。

方案四:绝对定位 + inset: 0 + margin: auto(需定宽高)

.box  { position: relative; }
.item {
  position: absolute;
  inset: 0;                  /* 等价 top:0; right:0; bottom:0; left:0 */
  width: 160px;              /* 必须定宽 */
  height: 60px;              /* 必须定高 */
  margin: auto;              /* 四向 auto 均分剩余空间 → 居中 */
}

原理:绝对定位元素在四边都被约束(top/bottom 同时非 auto)且尺寸确定时,margin: auto 会把剩余空间平均分配到两侧。这是唯一一个能用 margin:auto 做垂直居中的普通流外场景。 适用:尺寸已知的浮层;不希望引入 transform(避免创建合成层/包含块)时。

方案五:table-cell(兼容性最好)

.box  { display: table-cell; vertical-align: middle; text-align: center; }
.item { display: inline-block; text-align: left; }  /* 重置内部文本对齐 */

适用:需要兼容极老浏览器(IE8)。缺点是 display:table-cell 会让元素表现得像单元格(宽度行为异常、margin 失效),通常需要外面再包一层 display:table。

方案六:行内元素专用

/* 单行文本垂直居中:line-height 等于容器高度 */
.box { height: 240px; line-height: 240px; text-align: center; }
/* 缺点:只适用单行,多行会散开;且 line-height 会被子元素继承 */
/* 多行文本 / 未知尺寸的行内块:伪元素撑起 100% 高度做基准 */
.box { text-align: center; font-size: 0; }   /* font-size:0 消除 inline-block 间隙 */
.box::before {
  content: '';
  display: inline-block;
  height: 100%;
  vertical-align: middle;   /* 与后面的 inline-block 兄弟对齐中线 */
}
.item {
  display: inline-block;
  vertical-align: middle;
  font-size: 16px;          /* 恢复字号 */
  max-width: 99%;           /* 给伪元素留出一点点空间,避免换行 */
}

方案对比表

方案需定宽高需改父容器脱离文档流多行文本兼容性适用场景
Flex否是否✅IE10+(部分需前缀)默认首选
Grid否是否✅IE 不支持多子项独立对齐
absolute + transform否是(relative)是✅IE9+弹窗、Toast、浮层
absolute + margin auto是是(relative)是✅IE8+尺寸已知的浮层
table-cell否是否✅IE8+极老环境
line-height高度需已知是否❌ 仅单行全按钮、标签、单行提示
伪元素 + vertical-align否是否✅IE8+老项目的多行居中

补充:几个实战细节

/* ① 全屏遮罩里居中弹窗——Flex 最省事 */
.mask {
  position: fixed; inset: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,.45);
  /* 内容超高时可滚动,且保证顶部不被裁掉 */
  overflow: auto;
  padding: 24px;
}

/* ② 图标与文字的视觉居中(基线对齐问题) */
.btn { display: inline-flex; align-items: center; gap: 4px; }
/* 比 vertical-align: middle 稳定得多,不受字体基线影响 */

/* ③ 未来方案:anchor 定位(Chrome 已支持,尚未跨浏览器普及) */

【追问 & 加分点】

  1. "transform: translate(-50%,-50%) 的百分比参照谁?" —— 参照元素自身的边框盒尺寸,这正是它能在不知道尺寸时居中的原因。而 top/left 的百分比参照的是包含块。二者参照物不同是这个方案成立的核心。
  2. "为什么普通流里 margin: 0 auto 能水平居中但不能垂直居中?" —— 规范规定,普通流中块级元素垂直方向的 margin: auto 会被计算为 0(因为高度通常由内容决定,没有"剩余空间"的概念)。只有在绝对定位且上下都被约束、或者flex 容器内,垂直方向的 auto margin 才有剩余空间可分。
  3. "align-items 和 align-content 的区别?" —— align-items 对齐的是每一行内的项;align-content 对齐的是多行整体(只在 flex-wrap: wrap 且有多行时生效)。单行 flex 容器里设 align-content 无效,这是常见困惑点。
  4. "居中导致文字模糊怎么办?" —— translate(-50%,-50%) 在元素尺寸为奇数时会产生 0.5px 偏移,在某些渲染路径下触发亚像素抗锯齿。解法:改用 flex/grid 居中,或保证元素尺寸为偶数。

7. CSS 画三角形、梯形、扇形、圆、半圆

【核心答案】

核心原理只有两个:① 利用 border 的斜接(miter)特性——相邻边框在交界处是 45 度斜切的,所以把元素宽高设为 0、只保留一条边有颜色、其余透明,就得到三角形;若保留一点宽度(而非 0),斜切部分就变成梯形的两腰。② 利用 border-radius 塑形——border-radius: 50% 得到圆;配合 overflow: hidden 或只给部分角设置圆角可得半圆;扇形则是"用 border-radius 把方块变成四分之一圆,再用 transform: rotate 调整角度",更灵活的任意角度扇形要用 conic-gradient 或 clip-path。现代方案里 clip-path 可以用多边形顶点直接裁剪出任意形状,代码更直观且支持渐变背景与动画,是这类需求的首选,只是不能像 border 方案那样天然带描边。

【展开】

7.1 等腰三角形

原理演示(先理解 border 的斜接):

/* 一个宽高为 0、四边都有粗边框的元素,会呈现为四个三角形拼成的方块 */
.demo {
  width: 0; height: 0;
  border: 50px solid;
  border-color: #f5222d #52c41a #1677ff #faad14;  /* 上 右 下 左 */
}
/* 向上的等腰三角形:只留下边着色,左右透明形成两腰,上边宽度为 0 */
.tri-up {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 86px solid #1677ff;   /* 底宽 100,高 86 → 接近等边 */
  /* 等边三角形:高 = 底边一半 × √3 ≈ 50 × 1.732 = 86.6 */
}

/* 向下 */
.tri-down {
  width: 0; height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-top: 86px solid #1677ff;
}

/* 向右 */
.tri-right {
  width: 0; height: 0;
  border-top: 50px solid transparent;
  border-bottom: 50px solid transparent;
  border-left: 86px solid #1677ff;
}

/* 直角三角形(左下直角):只用两条边 */
.tri-rt {
  width: 0; height: 0;
  border-bottom: 100px solid #1677ff;
  border-right: 100px solid transparent;
}

clip-path 方案(推荐,支持渐变和动画):

.tri-clip {
  width: 100px;
  height: 86px;
  background: linear-gradient(135deg, #1677ff, #722ed1);  /* border 方案做不到 */
  /* 三个顶点:上中、左下、右下 */
  clip-path: polygon(50% 0, 0 100%, 100% 100%);
  transition: clip-path .3s;      /* 顶点数相同即可平滑动画 */
}

典型应用——气泡框的小尖角:

.bubble {
  position: relative;
  padding: 12px 16px;
  background: #1677ff; color: #fff; border-radius: 8px;
}
.bubble::after {
  content: '';
  position: absolute;
  top: 100%; left: 24px;
  border: 8px solid transparent;
  border-top-color: #1677ff;   /* 尖角颜色跟随气泡背景 */
}

7.2 等腰梯形

原理:三角形是"宽度为 0 + 两侧透明边框";梯形就是给元素一个非零宽度,上底 = 元素宽度,下底 = 元素宽度 + 左右边框宽度之和。

/* 上窄下宽的等腰梯形 */
.trapezoid {
  width: 100px;             /* 上底 */
  height: 0;
  border-left: 40px solid transparent;
  border-right: 40px solid transparent;
  border-bottom: 80px solid #1677ff;   /* 高 */
  /* 下底 = 100 + 40 + 40 = 180 */
}

/* 上宽下窄:把着色边换成 border-top */
.trapezoid-down {
  width: 100px;
  height: 0;
  border-left: 40px solid transparent;
  border-right: 40px solid transparent;
  border-top: 80px solid #52c41a;
}

clip-path 方案(更直观,且可以有内容和背景图):

.trapezoid-clip {
  width: 180px;
  height: 80px;
  background: #1677ff;
  /* 四个顶点:左上内缩 22%、右上内缩 22%、右下、左下 */
  clip-path: polygon(22% 0, 78% 0, 100% 100%, 0 100%);
}

border 方案的限制:元素 height: 0,内部无法放内容(会溢出),只能靠 ::before/::after 做装饰。需要放文字的梯形(如 Tab 页签)必须用 clip-path 或 transform: perspective 方案。

7.3 圆与半圆

/* 圆 */
.circle {
  width: 100px; height: 100px;
  border-radius: 50%;        /* 用 50% 而非固定 px,尺寸变化时自适应 */
  background: #1677ff;
}

/* 上半圆:宽 100 高 50,只给上面两个角设置圆角,
   水平半径 = 宽度一半,垂直半径 = 高度全部 */
.semi-top {
  width: 100px; height: 50px;
  background: #1677ff;
  border-radius: 50px 50px 0 0;   /* 左上 右上 右下 左下 */
}

/* 下半圆 */
.semi-bottom {
  width: 100px; height: 50px;
  background: #1677ff;
  border-radius: 0 0 50px 50px;
}

/* 左半圆 */
.semi-left {
  width: 50px; height: 100px;
  background: #1677ff;
  border-radius: 50px 0 0 50px;
}

/* 用百分比写法更通用(水平半径/垂直半径 用 / 分隔) */
.semi-top-pct {
  width: 100px; height: 50px;
  background: #1677ff;
  border-radius: 100% 100% 0 0 / 100% 100% 0 0;
  /* 斜杠前是四个角的水平半径,斜杠后是垂直半径 */
}

/* 半圆环(进度条端帽等场景) */
.semi-ring {
  width: 100px; height: 50px;
  border: 10px solid #1677ff;
  border-bottom: none;
  border-radius: 60px 60px 0 0 / 60px 60px 0 0;
  box-sizing: content-box;
}

7.4 扇形

方案 A:四分之一扇形(border-radius 单角)

/* 90 度扇形:一个正方形,只把一个角设为全圆角 */
.sector-90 {
  width: 100px; height: 100px;
  background: #1677ff;
  border-radius: 100px 0 0 0;   /* 左上角圆角 = 边长 → 圆心在右下 */
}
/* 想要圆心在左上的 90 度扇形,把圆角放在右下角 */
.sector-90-lt {
  width: 100px; height: 100px;
  background: #1677ff;
  border-radius: 0 0 100px 0;
}

方案 B:任意角度扇形(conic-gradient,最推荐)

/* 圆锥渐变:从 0 度到指定角度是实色,之后透明,再用 border-radius 裁成圆 */
.sector {
  width: 120px; height: 120px;
  border-radius: 50%;
  --deg: 135deg;                 /* 扇形角度,可用 JS 动态改 */
  background: conic-gradient(#1677ff 0 var(--deg), #f0f0f0 var(--deg) 360deg);
}

这也是画环形进度条的标准做法:

.progress-ring {
  width: 120px; height: 120px;
  border-radius: 50%;
  --p: 65;                        /* 百分比 */
  background: conic-gradient(#1677ff calc(var(--p) * 1%), #f0f0f0 0);
  display: grid; place-items: center;
  position: relative;
}
/* 中间挖空成环形 */
.progress-ring::before {
  content: '65%';
  width: 90px; height: 90px;
  border-radius: 50%;
  background: #fff;
  display: grid; place-items: center;
  font-size: 20px; color: #1677ff;
}

想让 conic-gradient 的角度可动画,需要用 @property 注册自定义属性并声明类型为 <angle>,否则浏览器把它当字符串无法插值:

@property --deg { syntax: '<angle>'; initial-value: 0deg; inherits: false; }
.sector { transition: --deg .6s ease; }

方案 C:任意角度扇形(clip-path + 三角函数)

/* 用多边形顶点逼近扇形边缘;顶点越多越圆滑,但小于 180 度时用三点即可近似 */
.sector-clip {
  width: 120px; height: 120px;
  border-radius: 50%;
  background: #1677ff;
  /* 圆心 + 顶部 + 右侧:约 90 度扇形 */
  clip-path: polygon(50% 50%, 50% 0, 100% 0, 100% 50%);
}

方案 D:两个半圆遮罩(老方案,兼容性最好) 用两层 transform: rotate 的半圆叠加,超过 180 度时切换层级。代码较长,现代项目直接用 conic-gradient 即可,了解思路即可。

7.5 方案选型总结

图形首选备选说明
三角形borderclip-pathborder 兼容性最好、无渲染成本;clip-path 支持渐变/动画
梯形clip-pathborder需要放内容时只能 clip-path
圆border-radius: 50%—无争议
半圆border-radius 部分角clip-path: inset()—
扇形 / 环形进度conic-gradientclip-path / SVG角度可变时 conic-gradient 最简洁
复杂图形SVG—超过基本形状就该用 SVG,可维护性和精度都更好

加分认知:CSS 画图形的意义在于避免为一个小图标发起额外请求,也便于跟随主题色变化。但一旦形状复杂(多段路径、需要描边、需要精确控制),SVG 才是正确工具——它有原生的 path、stroke、viewBox 缩放,代码可读性远高于一堆 border hack。面试时说出这个边界比多背两个 hack 更有价值。

【追问 & 加分点】

  1. "三角形为什么必须 width: 0; height: 0?" —— 因为要让四条边框在中心点汇聚。有宽高时中间会剩一块内容区,斜切只发生在四角,得到的是梯形(这正是梯形的画法)。
  2. "border 三角形有锯齿怎么办?" —— 浏览器对 border 斜边不做抗锯齿,在缩放或旋转时明显。解法:改用 clip-path(有抗锯齿)或 SVG;或者加一个极轻微的 transform: rotate(0.05deg) 强制走合成层路径(hack,不推荐)。
  3. "conic-gradient 的起始角度在哪?" —— 默认从正上方(12 点方向)顺时针开始,可以用 from 45deg 调整起点,at 30% 70% 调整圆心位置。
  4. "这些图形怎么做无障碍?" —— 纯装饰图形应加 aria-hidden="true";承载信息的(如进度环)要用 role="progressbar" + aria-valuenow 补充语义,否则屏幕阅读器读不到任何内容。

8. 如何实现响应式布局

【核心答案】

响应式的目标是"一套代码适配多种屏幕",实现手段按演进顺序是:① 视口设置(<meta name="viewport" content="width=device-width, initial-scale=1">,一切的前提);② 弹性布局(Flex/Grid 天然自适应,grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)) 这类"内在响应式"甚至不需要媒体查询);③ 相对单位(%、rem、vw/vh、em、ch);④ 媒体查询做断点级的布局重排;⑤ clamp() 实现流体排版,在最小值和最大值之间随视口平滑变化,替代大量断点;⑥ 容器查询 @container,让组件根据自身容器宽度而非视口宽度响应,这是真正的组件级响应式,解决了媒体查询"组件不知道自己被放在哪"的根本缺陷。移动端适配方案的演进则是:定宽 + 缩放 → 媒体查询 → rem 等比缩放(flexible.js / postcss-pxtorem) → vw 方案 → 现在的 Flex/Grid + clamp + 容器查询,趋势是从"等比缩放整个页面"转向"弹性布局 + 关键处约束"。

【展开】

8.1 前提:viewport

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

不写这行,移动浏览器会使用 980px 左右的布局视口再整体缩小显示,媒体查询的断点全部失效。viewport-fit=cover 配合 env() 处理刘海屏:

.footer-bar {
  padding-bottom: calc(12px + env(safe-area-inset-bottom));  /* iPhone 底部横条避让 */
}

不建议加 user-scalable=no / maximum-scale=1——会破坏无障碍(视障用户无法放大),且 iOS Safari 已经忽略它。

8.2 媒体查询与断点

/* 移动优先(Mobile First):基础样式面向小屏,用 min-width 逐级增强 */
.container { padding: 16px; }

@media (min-width: 640px)  { .container { padding: 24px; } }   /* 大手机 / 小平板 */
@media (min-width: 768px)  { .container { padding: 32px; } }   /* 平板 */
@media (min-width: 1024px) { .container { max-width: 960px; margin: 0 auto; } }
@media (min-width: 1280px) { .container { max-width: 1200px; } }

为什么移动优先更好:基础样式最简单、小屏设备(性能最弱)解析的 CSS 最少;min-width 的层层增强比 max-width 的层层覆盖更少产生特异性冲突。

媒体查询的其他维度(体现深度):

@media (prefers-color-scheme: dark)      { :root { --bg: #141414; } }  /* 暗色模式 */
@media (prefers-reduced-motion: reduce)  { * { animation: none !important; transition: none !important; } }
@media (hover: hover) and (pointer: fine) { .card:hover { transform: translateY(-2px); } }
/* ↑ 关键:只在真有鼠标的设备上启用 hover,避免移动端点击后 hover 态"粘住" */
@media print                              { .no-print { display: none; } }
@media (orientation: landscape)           { .video { height: 100vh; } }

/* 现代区间语法,比 min-width/max-width 更直观(Chrome/FF/Safari 均已支持) */
@media (768px <= width < 1024px) { /* ... */ }

8.3 单位选型

单位参照适用
%父元素对应属性宽度自适应;注意 height: % 要求父级有确定高度
em当前元素字号(用于 font-size 时是父级字号)与文字相关的间距(按钮 padding 随字号缩放);会层层复合,易失控
rem根元素 <html> 的字号间距、圆角、组件尺寸的主力单位,不复合,可控
vw / vh视口宽/高的 1%全屏区块、随屏缩放的字号;vh 在移动端有地址栏伸缩问题
dvh/svh/lvh动态/小/大视口高度解决移动端 100vh 被地址栏遮挡的问题,用 100dvh
ch / ex字符 0 的宽度 / x 高度控制阅读行宽(max-width: 65ch 是排版最佳实践)
vmin / vmax视口较小/较大边的 1%需要在横竖屏都保持比例的元素
/* 移动端全屏容器的正确写法 */
.full { height: 100vh; height: 100dvh; }  /* 降级 + 现代方案 */

8.4 rem 等比缩放方案(历史主流)

思路:用 JS 或媒体查询把根字号设为视口宽度的固定比例,页面上所有 rem 尺寸就随屏幕等比缩放。

// 简版 flexible:设计稿 750px,按 1rem = 设计稿宽度 / 10 = 75px 换算
(function setRem() {
  const DESIGN_WIDTH = 750;
  const set = () => {
    // 限制最大宽度,防止在平板/PC 上元素被拉得巨大
    const w = Math.min(document.documentElement.clientWidth, 600);
    document.documentElement.style.fontSize = (w / DESIGN_WIDTH) * 100 + 'px';
    // 这里用 /10*... 的变体:1rem = 视口宽 / 7.5,便于 100px = 1rem 的心算
  };
  set();
  window.addEventListener('resize', set);
  window.addEventListener('pageshow', (e) => e.persisted && set());  // bfcache 恢复
})();

配合 PostCSS 自动换算,开发时按设计稿直接写 px:

// postcss.config.js
module.exports = {
  plugins: {
    'postcss-pxtorem': {
      rootValue: 75,              // 设计稿 750 时,1rem = 75px
      propList: ['*', '!border*'],// 边框保持 px,避免缩放后小于 1px 消失
      minPixelValue: 2,           // 小于 2px 不转换
    },
  },
};

缺点:依赖 JS(有闪烁风险,需内联在 head)、在大屏上必须限制最大值否则字号巨大、整体等比缩放不符合"大屏应该展示更多内容"的直觉(PC 端不该用)。

8.5 vw 方案(纯 CSS,无 JS)

/* 设计稿 750px:1px = 100/750 vw = 0.1333vw */
.title { font-size: 4.2667vw; }   /* 32px 设计稿 */

/* 用 postcss-px-to-viewport 自动换算,开发时仍写 px */
// postcss.config.js
module.exports = {
  plugins: {
    'postcss-px-to-viewport': { viewportWidth: 750, unitPrecision: 5, minPixelValue: 1 },
  },
};

优点:纯 CSS 无 JS 依赖、无闪烁。缺点:同样是无限等比缩放,大屏需要额外限宽:

/* 常见做法:给 body 限宽并居中,超出部分留白 */
body { max-width: 600px; margin: 0 auto; }

8.6 clamp() 流体排版(现代推荐)

clamp(最小值, 理想值, 最大值) 一行代码替代多个断点,且是平滑变化而非跳变:

:root {
  /* 字号:最小 16px,随视口在 375~1440 之间平滑增长,最大 24px */
  --fs-body: clamp(1rem, 0.86rem + 0.6vw, 1.5rem);
  --fs-h1:   clamp(1.75rem, 1.2rem + 2.8vw, 3.5rem);
  /* 间距同理,形成"流体间距系统" */
  --space-section: clamp(2rem, 5vw, 6rem);
}
body { font-size: var(--fs-body); }
h1   { font-size: var(--fs-h1); }
.section { padding-block: var(--space-section); }

/* 容器宽度:小屏占满并留 16px 边距,大屏最多 1200px */
.container {
  width: min(100% - 32px, 1200px);
  margin-inline: auto;
}

计算公式:要让值在视口 w1 → w2 之间从 v1 变到 v2,中间项写成 calc(斜率 * 100vw + 截距),其中 斜率 = (v2-v1)/(w2-w1)。注意中间项必须包含 vw(或其他视口单位),否则就不会变化。另外纯 vw 字号会导致用户无法通过浏览器缩放调节文字大小,混入 rem 是无障碍要求(如上面的 0.86rem + 0.6vw)。

8.7 内在响应式(Intrinsic Web Design)—— 无需媒体查询

这是当前最推崇的做法:让布局自己适应可用空间。

/* 卡片网格:每列至少 240px,能放几列放几列,剩余空间平分。
   从手机 1 列到宽屏 5 列,全程零媒体查询 */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 16px;
}
/* min(240px, 100%) 是为了防止视口小于 240px 时产生横向滚动 */

/* 自动换行的标签组 */
.tags { display: flex; flex-wrap: wrap; gap: 8px; }

/* 侧栏 + 主区:空间够就并排,不够自动堆叠("汉堡包"技巧) */
.holy {
  display: flex;
  flex-wrap: wrap;
  gap: 24px;
}
.holy > .sidebar { flex: 1 1 200px; }   /* basis 200px,放不下就换行 */
.holy > .main    { flex: 999 1 60%; }   /* 超大 grow:有空间时几乎独占;60% basis 决定换行阈值 */

auto-fit vs auto-fill:auto-fit 会折叠空轨道让现有项拉伸填满;auto-fill 保留空轨道,项目保持 minmax 的最小宽度。想让少量卡片也铺满一行用 auto-fit,想让卡片保持固定大小靠左排用 auto-fill。

8.8 容器查询(组件级响应式,最前沿)

媒体查询的根本缺陷:组件不知道自己被放在多宽的位置。同一个卡片放在全宽区域和放在 300px 侧栏里,需要不同布局,但视口宽度是一样的。容器查询解决了这个问题:

/* 1. 声明容器 */
.card-wrapper {
  container-type: inline-size;   /* 以内联轴(通常是宽度)作为查询基准 */
  container-name: card;
}

/* 2. 基于容器宽度而非视口宽度响应 */
@container card (min-width: 400px) {
  .card { display: grid; grid-template-columns: 120px 1fr; gap: 16px; }
}
@container card (max-width: 399px) {
  .card { display: block; }
}

还有容器查询单位:cqw / cqh / cqi / cqb / cqmin / cqmax(1cqw = 容器宽度的 1%):

.card-title { font-size: clamp(1rem, 5cqi, 2rem); }  /* 字号随容器而非视口变化 */

这是设计系统 / 组件库的关键能力:一个组件真正做到"放到哪都对",不再需要为不同场景写变体类名。Chrome / Safari / Firefox 均已支持。

8.9 图片与媒体的响应式

<!-- srcset + sizes:让浏览器根据 DPR 和布局宽度自己挑最合适的图,省流量 -->
<img
  src="/img/hero-800.jpg"
  srcset="/img/hero-400.jpg 400w, /img/hero-800.jpg 800w, /img/hero-1600.jpg 1600w"
  sizes="(max-width: 768px) 100vw, 50vw"
  width="800" height="450"
  loading="lazy" decoding="async"
  alt="示意图"
>

<!-- picture:需要按格式或艺术指导(不同尺寸用不同构图)切换时使用 -->
<picture>
  <source srcset="/img/hero.avif" type="image/avif">
  <source srcset="/img/hero.webp" type="image/webp">
  <img src="/img/hero.jpg" alt="示意图">
</picture>
img, video { max-width: 100%; height: auto; }   /* 响应式基础 */
.cover { aspect-ratio: 16 / 9; object-fit: cover; }  /* 固定比例防布局抖动 */

给 <img> 写死 width/height 属性(配合 CSS 的 height:auto)能让浏览器提前预留空间,显著改善 CLS(累积布局偏移)这一核心 Web 指标。

8.10 移动端适配方案演进小结

阶段方案问题
1固定宽度 + 浏览器缩放文字小、要双指放大
2百分比 + 媒体查询断点跳变生硬,百分比参照混乱
3rem + flexible.js依赖 JS、有闪烁、大屏需限宽
4vw + postcss纯 CSS,但仍是无限等比缩放
5Flex/Grid + clamp + 内在响应式当前主流
6+ 容器查询组件级响应式,最终形态

当前推荐组合:viewport 正确设置 → Flex/Grid 做骨架 → clamp() 做流体字号与间距 → 少量媒体查询处理布局大重排(如导航从横向变汉堡) → 组件内部用容器查询 → srcset 优化图片 → dvh、env() 处理移动端视口细节。

【追问 & 加分点】

  1. "1px 边框在高 DPR 屏幕上变粗怎么办?" —— CSS 的 1px 在 DPR=2 的屏上是 2 个物理像素。解法:① transform: scaleY(0.5) + 伪元素(最常用)② box-shadow: 0 0.5px 0 #eee ③ border-image 用 9 宫格图 ④ 媒体查询 @media (-webkit-min-device-pixel-ratio: 2) { border-width: 0.5px }(现代 iOS 支持 0.5px)。
  2. "移动端 100vh 为什么会出问题?" —— iOS Safari 的 100vh 是不含地址栏的大视口高度,地址栏显示时内容会被截断,滚动时又会跳动。用 100dvh(动态视口高度)解决;需要兼容时先写 100vh 再写 100dvh 做渐进增强。
  3. "响应式和自适应(Adaptive)有什么区别?" —— 响应式是一套 HTML/CSS 在所有断点间连续流动;自适应是准备若干套固定布局按断点离散切换(甚至服务端按 UA 返回不同页面)。前者维护成本低,是主流;后者在需要为移动端做完全不同体验时(如淘宝的 m 站)仍有价值。
  4. "断点该怎么定?" —— 不要照着具体机型定(机型无穷无尽),应该由内容决定断点:把浏览器慢慢拉宽,当布局开始难看时就是一个断点。行业常用值 640 / 768 / 1024 / 1280 / 1536 只是参考起点。
  5. "容器查询有性能问题吗?" —— container-type: inline-size 会给元素施加 size containment(内联轴方向),意味着其内容不再影响该轴尺寸。这既是它能高效工作的原因,也是使用限制:不能给依赖内容撑开宽度的元素声明容器。

9. 为什么现代大厂倾向 flex/grid 而不是 float

【核心答案】

根本原因是工具与目的的匹配:float 从未被设计用来做页面布局——它的规范意图是"让文字环绕图片",即让一个元素脱离常规流的垂直排布但仍参与行盒的排斥计算。用它做整页布局属于借用副作用,代价是必须额外处理高度塌陷(清除浮动)、BFC、负 margin、等高列 hack、DOM 顺序与视觉顺序耦合这一堆与业务无关的机制,而且换不来垂直居中、等高、间距(gap)、顺序控制这些基本能力。Flex 和 Grid 是 CSS 工作组专门为布局设计的规范:Flex 解决一维(一行或一列)的空间分配与对齐,Grid 解决二维(行列同时)的结构划分。它们把"我想要什么布局"直接表达为声明式属性,而不是"我怎么骗浏览器排出这个样子"。工程上的收益是代码量下降一个数量级、可读性和可维护性大幅提升、响应式改造只需改一两行、天然支持等高与居中,且现代浏览器对二者的布局性能已经优于层层嵌套的 float 结构。

【展开】

9.1 float 做布局的固有成本

问题表现float 的解法Flex/Grid
高度塌陷父元素高度为 0clearfix 伪元素 / overflow:hidden / display:flow-root不存在,子项在流内
等高列列高度不一致padding-bottom:9999px; margin-bottom:-9999px hackalign-items: stretch 是默认值
垂直居中无解各种 hackalign-items: center
元素间距需处理首尾项的 margin:last-child { margin-right: 0 } 或负 margin 容器gap 一行搞定
顺序控制与 DOM 顺序强耦合负 margin 硬拉(圣杯/双飞翼)order / grid-area
空间分配只能定宽或百分比手算百分比,被 border/padding 破坏flex: 1 / 1fr
响应式每个断点重写整套浮动逻辑—改 flex-direction 或 grid-template 一行

9.2 同一个需求的代码量对比

/* 三列等宽等高、间距 16px —— float 版本 */
.f-row { overflow: hidden; margin: 0 -8px; }        /* 负 margin 抵消子项 padding */
.f-col {
  float: left;
  width: 33.3333%;                                   /* 手算,且不能有 border */
  padding: 0 8px;
  box-sizing: border-box;                            /* 忘了这行整个布局就崩 */
}
.f-row::after { content: ''; display: block; clear: both; }
/* 等高?还需要 padding-bottom:9999px + margin-bottom:-9999px + 父级 overflow:hidden */
/* Grid 版本 —— 天然等高、天然间距、天然等宽 */
.g-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }

9.3 Flex 与 Grid 的分工

不是替代关系,是维度关系:

  • Flex 是一维的:你只控制一个方向上的排列与分配(主轴),另一个方向靠对齐。适合内容驱动的布局——导航条、工具栏、按钮组、卡片内部的图文排列、"两端对齐"这类需求。子项尺寸由内容决定,容器负责分配剩余空间。
  • Grid 是二维的:同时定义行和列,先画格子再放内容。适合布局驱动的结构——页面整体框架、仪表盘、图片墙、表单的标签-控件对齐。

经验法则:整体页面骨架用 Grid,组件内部的排列用 Flex。二者可以自由嵌套(Grid 项本身可以是 Flex 容器)。

/* 页面骨架:Grid */
.app {
  display: grid;
  grid-template: "header header" auto
                 "sidebar main"  1fr
                 "footer footer" auto / 240px 1fr;
  min-height: 100dvh;
}
/* 组件内部:Flex */
.header { grid-area: header; display: flex; align-items: center; justify-content: space-between; gap: 16px; }

9.4 什么时候 float 仍然是正确答案

诚实地说,float 并没有被废弃,只是回到了它本来的位置:

/* 文字环绕图片——这才是 float 的设计初衷,Flex/Grid 做不到 */
.article img.thumb {
  float: left;
  margin: 0 16px 8px 0;
  shape-outside: circle(50%);   /* 甚至可以让文字沿圆形轮廓环绕 */
}

/* 首字下沉、引用块的浮动侧栏,同理 */

9.5 性能与工程视角

  • 布局性能:Flex/Grid 的布局算法虽然单次计算比块级流复杂,但 float 布局往往需要更深的 DOM 嵌套(wrapper 套 wrapper)和更多的 hack 元素,实际重排成本未必更低。真正影响性能的是布局抖动(读写 DOM 交替)和大量元素的重排,与用哪种布局方式关系不大。Grid 还能通过 contain: layout 隔离布局影响范围。
  • 可维护性:这是最主要的收益。grid-template-areas 用字符串画出布局形状,新人一眼看懂;而"为什么这里有个 margin-left: -100%"需要注释三行才能解释清楚。
  • 兼容性已不是问题:Flex 从 IE10(带前缀)起支持,Grid 在所有现代浏览器全面支持(IE 的旧版 Grid 语法不兼容,但 IE 已停止支持)。国内业务在 2023 年后基本不再需要为 IE 做兼容。
  • 无障碍:Flex 的 order 和 Grid 的 grid-area 只改变视觉顺序不改变 DOM 顺序,滥用会导致键盘 Tab 顺序与视觉顺序不一致,这是使用它们时需要新注意的点——但至少是可控的,而 float 时代为了顺序问题必须用负 margin 硬拉,更难维护。

【追问 & 加分点】

  1. "float 元素为什么会导致父级高度塌陷?" —— 浮动元素脱离了常规流,父级在计算高度时不把它算进去(除非父级建立 BFC)。display: flow-root 是专门用来创建 BFC 的现代属性,比 overflow: hidden 更干净(不会意外裁剪子元素的溢出内容,比如下拉菜单和阴影)。
  2. "Grid 有什么 Flex 做不到的?" —— 二维对齐(行列同时约束)、grid-template-areas 的可视化布局、显式的行列线定位、subgrid(让子网格继承父网格的轨道,解决嵌套卡片内部元素跨卡片对齐的经典难题)、masonry(瀑布流,规范仍在推进)。
  3. "用了 Flex/Grid 还需要 BFC 知识吗?" —— 需要。BFC 依然解释 margin 折叠、浮动包含、overflow 行为等基础现象。不过Flex 和 Grid 容器的子项之间不会发生 margin 折叠,这本身就消除了一大类困惑。
  4. "从 float 迁移到 Flex/Grid 有什么风险?" —— 主要是 flex/grid 项的 min-width: auto 默认值导致的溢出问题(见第 4 题),以及旧代码里依赖 float 副作用的样式(如 clear、行盒排斥)会失效。建议按组件粒度渐进迁移而非全站一次性重写。

反向链接(0)

还没有笔记链接到这里。在 Obsidian 里用 [[笔记名]] 引用它,这里就会出现,关系图谱上也会多出一条连线。

评论

关系图谱

01-js-core02-browser03-network04-css-html05-react06-vue07-engineering08-scenarios09-handwritten-js10-algorithms11-ai-frontend前端识记前端面试

On this page

HTML 与 CSS目录1. head 与 body 的职责;meta、link、style 标签【核心答案】【展开】1.1 head 的职责与关键内容1.2 <meta> 分类1.3 <link> 能引入哪些资源1.4 type 属性做什么用【追问 & 加分点】2. Less / Sass 的原理与 CSS Modules 的原理【核心答案】【展开】2.1 预处理器的编译流程2.2 Less 与 Sass(scss) 的差异2.3 预处理器 vs CSS 变量(高频追问)2.4 CSS Modules 原理2.5 与其他方案的定位【追问 & 加分点】3. 如何正确覆盖 antd 等组件库的样式【核心答案】【展开】3.1 先诊断:为什么没覆盖上3.2 特异性(Specificity)复习3.3 方案一(首选):antd v5 的 Theme Token3.4 方案二:组件暴露的 className / styles API3.5 方案三:CSS 变量3.6 方案四:选择器覆盖(含 CSS Modules 场景)3.7 方案五:@layer(现代方案,加分项)3.8 方案对比3.9 !important 的唯一合理场景【追问 & 加分点】4. flex: 1 0 auto 是什么意思【核心答案】【展开】4.1 三个子属性拆解4.2 常见简写对照表4.3 flex: 1 vs flex: auto 的可视差异4.4 flex: 1 0 auto 的典型用途4.5 必考陷阱:flex 项无法收缩到内容以下【追问 & 加分点】5. 三列布局的多种实现【核心答案】【展开】方案一:Grid(首选)方案二:Flex方案三:圣杯布局(Holy Grail,float + 负 margin + 相对定位)方案四:双飞翼布局(玉伯 / 淘宝方案)方案五:定位 + margin(简单场景够用)方案六:table 布局(等高特性)综合对比【追问 & 加分点】6. 垂直水平居中的多种实现【核心答案】【展开】方案一:Flex(最推荐)方案二:Grid(代码最短)方案三:绝对定位 + transform(不需要定宽高)方案四:绝对定位 + inset: 0 + margin: auto(需定宽高)方案五:table-cell(兼容性最好)方案六:行内元素专用方案对比表补充:几个实战细节【追问 & 加分点】7. CSS 画三角形、梯形、扇形、圆、半圆【核心答案】【展开】7.1 等腰三角形7.2 等腰梯形7.3 圆与半圆7.4 扇形7.5 方案选型总结【追问 & 加分点】8. 如何实现响应式布局【核心答案】【展开】8.1 前提:viewport8.2 媒体查询与断点8.3 单位选型8.4 rem 等比缩放方案(历史主流)8.5 vw 方案(纯 CSS,无 JS)8.6 clamp() 流体排版(现代推荐)8.7 内在响应式(Intrinsic Web Design)—— 无需媒体查询8.8 容器查询(组件级响应式,最前沿)8.9 图片与媒体的响应式8.10 移动端适配方案演进小结【追问 & 加分点】9. 为什么现代大厂倾向 flex/grid 而不是 float【核心答案】【展开】9.1 float 做布局的固有成本9.2 同一个需求的代码量对比9.3 Flex 与 Grid 的分工9.4 什么时候 float 仍然是正确答案9.5 性能与工程视角【追问 & 加分点】