普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月15日首页

Grid 网格布局:二维世界的布局王者,像下围棋一样掌控页面

作者 kyriewen
2026年3月15日 11:22

如果说Flexbox是“一维战神”,擅长排排坐,那Grid就是“二维霸主”,能同时操控行和列。今天我们就来下这盘“布局围棋”,用网格思想彻底重构网页,让复杂布局变得像填格子一样简单。

前言

还记得小时候玩的方格本吗?一行一行,一列一列,规规矩矩。Grid布局就是把这种“方格本”思维带到了CSS里。你可以在页面上画出任意行、任意列,然后把元素放进去,想放哪格放哪格,甚至可以合并单元格——就像Excel表格,但比Excel灵活一万倍。

Grid是CSS布局的终极武器,尤其适合做页面整体架构、卡片墙、仪表盘这类需要同时控制行和列的场景。如果说Flexbox是特种兵,擅长单兵作战,那Grid就是指挥官,能调动千军万马。

一、Grid的核心概念:容器与项目,行与列

和Flexbox类似,Grid也是作用于父容器和直接子项目。只要在父元素上设置display: griddisplay: inline-grid,你就开启了一个网格世界。

.container {
  display: grid;
}

默认情况下,网格只有一列,行高由内容决定。要真正“画”出网格,你需要用grid-template-rowsgrid-template-columns定义行和列。

二、定义网格:画出你的棋盘

1. 固定行高和列宽

你可以用各种单位定义行列的尺寸,比如像素、百分比、em等。

.container {
  display: grid;
  grid-template-columns: 200px 200px 200px;  /* 三列,每列200px */
  grid-template-rows: 100px 150px;           /* 两行,第一行100px,第二行150px */
}

这样你就画了一个3列2行的网格,一共6个格子。项目会按顺序自动填充每个格子,就像表格里从左到右、从上到下填数据一样。

2. fr单位:分蛋糕神器

Grid引入了fr单位(fraction的缩写),表示剩余空间的比例分配。这比Flexbox的flex-grow更直观。

.container {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr;  /* 三列,中间占两份,两边各占一份 */
}

如果父容器宽度是1200px,那么第一列占300px,第二列600px,第三列300px。fr可以和固定单位混用,比如200px 1fr 2fr,浏览器会先分配200px,剩下的按比例分。

3. repeat() 函数:偷懒必备

如果你要定义很多等宽的列,手动写很累。repeat()函数来救场。

.container {
  grid-template-columns: repeat(3, 1fr);  /* 三列等宽,相当于 1fr 1fr 1fr */
  grid-template-rows: repeat(4, 100px);   /* 四行,每行100px */
}

repeat()还可以组合不同模式,比如repeat(2, 100px 1fr)表示重复两次“100px 1fr”的序列,最终得到四列:100px、1fr、100px、1fr。

4. minmax():给尺寸一个范围

有时候我们希望列宽能在一定范围内弹性变化,比如最小200px,最大自适应。minmax()搞定。

.container {
  grid-template-columns: minmax(200px, 1fr) 2fr;  /* 第一列最小200px,可以放大到1fr,第二列固定2fr */
}

5. auto-fill 与 auto-fit:响应式利器

当列数不确定时,可以用auto-fillauto-fit配合minmax实现类似“流动布局”的效果。

  • auto-fill:尽可能多地填充列,即使某些列是空的。
  • auto-fit:也是尽可能多地填充,但会把空列收缩为0,让有内容的列伸展。
.container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}

这段代码的意思是:每列最小200px,如果容器足够宽,就放尽可能多的列,并且每列等宽;当容器变窄时,列数自动减少,每列仍不小于200px。这就是纯CSS实现的响应式卡片墙!

三、网格线:定位的坐标系

每个网格都由网格线划分。比如3列有4条纵向网格线(从1开始编号),2行有3条横向网格线。你可以用这些线来精确放置项目。

.item {
  grid-column-start: 1;
  grid-column-end: 3;   /* 从第1条纵向线跨到第3条线,即占据前两列 */
  grid-row-start: 1;
  grid-row-end: 3;      /* 从第1条横向线跨到第3条线,即占据前两行 */
}

简写为:

.item {
  grid-column: 1 / 3;
  grid-row: 1 / 3;
}

也可以从哪条线开始,并指定跨度:

.item {
  grid-column: 1 / span 2;  /* 从第1列开始,跨2列,等同于1/3 */
  grid-row: 1 / span 2;
}

网格线也可以命名,比如grid-template-columns: [main-start] 1fr [main-end],然后使用命名来定位。

四、网格区域:给格子起名字

如果你觉得用线编号不够直观,可以给网格区域命名。用grid-template-areas属性来划分区域。

.container {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  grid-template-rows: 100px 1fr 100px;
  grid-template-areas:
    "header header header"
    "sidebar content aside"
    "footer footer footer";
}
.header {
  grid-area: header;
}
.sidebar {
  grid-area: sidebar;
}
.content {
  grid-area: content;
}
.aside {
  grid-area: aside;
}
.footer {
  grid-area: footer;
}

这个布局清晰得像图纸一样,每个区域的名字直接对应一个网格单元格。注意,grid-template-areas里的每个单元格必须填满,不能有空洞;可以用.表示空单元格。

五、间距与对齐:让网格透气

1. 行列间距

gap属性设置网格线之间的间距,可以分别设置行间距和列间距:

.container {
  gap: 20px;            /* 行列间距都是20px */
  row-gap: 10px;        /* 单独设置行间距 */
  column-gap: 15px;     /* 单独设置列间距 */
}

2. 项目在单元格内的对齐

项目默认填满整个单元格,但你可以控制它们的位置。

  • justify-items:控制项目在单元格内水平方向的对齐(左中右)。
  • align-items:控制项目在单元格内垂直方向的对齐(上中下)。
  • 取值:startendcenterstretch(默认)。
.container {
  justify-items: center;   /* 所有项目水平居中 */
  align-items: center;     /* 所有项目垂直居中 */
}

如果想单独控制某个项目,用justify-selfalign-self

3. 整个网格在容器内的对齐

如果网格的总尺寸小于容器,可以用justify-contentalign-content控制网格整体的对齐,类似于Flexbox。

.container {
  justify-content: center;   /* 网格整体水平居中 */
  align-content: center;     /* 网格整体垂直居中 */
}

取值同样是startendcenterspace-betweenspace-aroundspace-evenly

六、实战:用Grid搭建常见布局

1. 经典三栏布局(圣杯)

.container {
  display: grid;
  grid-template-columns: 200px 1fr 200px;
  gap: 20px;
}

就这么简单,三栏就出来了,而且中间自适应。

2. 响应式卡片墙

我们希望卡片最小200px,尽量填满容器,而且自动换行。

.container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
}

无论容器多宽,卡片都会自动调整列数,完美响应式。

3. 不规则布局:杂志风格

比如一个封面图占两列,下方三个卡片各占一列。

<div class="magazine">
  <div class="feature">封面大图</div>
  <div class="card">卡片1</div>
  <div class="card">卡片2</div>
  <div class="card">卡片3</div>
</div>
.magazine {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
}
.feature {
  grid-column: 1 / -1;  /* 从第一列线到最后一列线,即占满整行 */
}

-1表示最后一条网格线,很方便。

4. 后台管理仪表盘

后台常有复杂区域划分,用grid-template-areas最合适。

.dashboard {
  display: grid;
  grid-template-columns: 250px 1fr 300px;
  grid-template-rows: 80px 1fr 60px;
  grid-template-areas:
    "header header header"
    "sidebar main widgets"
    "footer footer footer";
  height: 100vh;
}

各区域对号入座,结构一目了然。

5. 叠加效果

Grid还可以让多个元素重叠。通过给它们设置相同的网格区域,然后利用z-index控制层级。

.container {
  display: grid;
  grid-template-columns: 1fr 1fr;
}
.item1 {
  grid-area: 1 / 1 / 2 / 3;  /* 占满整行 */
  background: red;
  z-index: 1;
}
.item2 {
  grid-area: 1 / 2 / 2 / 3;  /* 只占第二列 */
  background: blue;
  opacity: 0.5;
  z-index: 2;  /* 显示在上层 */
}

七、Grid vs Flexbox:怎么选?

很多人纠结什么时候用Grid,什么时候用Flexbox。其实很简单:

  • Flexbox:一维布局,适合控制元素在一条线上的排列(导航、按钮组、等分列表)。
  • Grid:二维布局,适合同时控制行和列(页面整体架构、卡片墙、仪表盘)。

它们不是替代关系,而是配合关系。你可以在Grid单元格里用Flexbox排列内部元素,也可以把Flexbox项目里再嵌套Grid。两者结合,天下无敌。

八、常见坑点与避坑指南

1. 默认不是严格的一行一列

如果只设置display: grid而不定义行列,默认只有一列,行数由项目数量决定(每个项目占一行)。所以一定要定义行列。

2. 项目会自动填充,但可能超出网格

如果项目数量超过网格单元格,会自动创建隐式网格(新行),行高默认auto。你可以用grid-auto-rows控制隐式行的高度。

.container {
  grid-auto-rows: 100px;  /* 隐式创建的行高100px */
}

3. fr 和 minmax 结合时注意死循环

minmax(200px, 1fr)的意思是:优先让列宽为1fr,但不会小于200px。这通常没问题,但如果你把所有列都设成这样,且容器总宽度小于列数*200px,就会出现溢出(因为每列都强制不小于200px)。这时可以改用auto-fitminmax的巧妙组合,或者用max-width限制。

4. gap 会占用 fr 空间

间隙是在分配fr之前扣除的。比如三列1fr,gap 20px,那么每列实际宽度 = (容器宽度 - 40px) / 3。所以计算时要考虑间隙。

5. 网格线编号从1开始,不是0

这个容易搞错。不过可以用-1表示最后一条线,比较方便。

九、总结

Grid布局是CSS给前端开发者的一份大礼,它把网页布局变成了一种直观、可预测的体验。核心要点:

  • grid-template-columnsgrid-template-rows定义网格结构。
  • frrepeat()minmax()灵活控制尺寸。
  • grid-column/grid-rowgrid-area放置项目。
  • gap控制间距,用justify/align控制对齐。
  • auto-fitminmax实现响应式。
  • 复杂布局用grid-template-areas命名,代码如设计图。

Grid不难,关键是多动手画格子。一旦你习惯了这种“下围棋”式的布局思维,你会发现以前那些棘手的布局都变成了填空题。

如果你喜欢这篇文章,欢迎点赞、收藏、分享。明天我们将进入CSS另一个重要话题——响应式设计与移动端适配,教你如何一套代码搞定手机、平板、电脑。


明日预告:响应式设计的核心:媒体查询、流式布局、移动端适配,从零构建一个全端兼容的页面。

昨天 — 2026年3月14日首页

Flexbox 完全指南:从此告别浮动,拥抱一维战神

作者 kyriewen
2026年3月14日 11:35

还在用float做导航?还在为垂直居中写positiontransform?今天我们来认识一位布局界的“一维战神”——Flexbox。它专治各种居中、等分、排列难题,让你写布局像搭积木一样简单。

前言

回想那些年被float支配的日子:清浮动要写clearfix,垂直居中要算半天,几个元素等宽还得用百分比小心翼翼……直到Flexbox的出现,前端布局才真正迎来了春天。

Flexbox的全称是Flexible Box Layout Module,翻译过来就是“弹性盒子”。它的核心思想是:让容器有能力改变子项的宽度、高度、顺序,以最好地填充可用空间。尤其擅长处理一维布局(也就是一行或一列)。今天我们就来彻底掌握这个“一维战神”。

一、Flexbox的两大核心:容器与项目

要使用Flexbox,你只需要在父元素上设置display: flexdisplay: inline-flex。这时,父元素成为flex容器,它的直接子元素自动成为flex项目

.container {
  display: flex;  /* 容器开启flex模式 */
}

就像一支军队有了指挥官,所有士兵(项目)都听从容器(指挥官)的调遣。

二、轴:Flexbox的方向感

Flexbox里有两条轴:主轴交叉轴,所有排列都围绕这两条轴进行。

  • 主轴:默认水平方向,从左到右。你可以通过flex-direction改变它的方向。
  • 交叉轴:始终垂直于主轴。

想象你手里拿着一排士兵,你可以命令他们横着站(主轴水平),也可以竖着站(主轴垂直),甚至可以倒着站。这就是flex-direction的作用。

.container {
  flex-direction: row;            /* 默认值,主轴水平,从左到右 */
  flex-direction: row-reverse;    /* 主轴水平,从右到左 */
  flex-direction: column;         /* 主轴垂直,从上到下 */
  flex-direction: column-reverse; /* 主轴垂直,从下到上 */
}

三、主轴上的排列:justify-content

justify-content控制项目在主轴上的对齐方式。这是最常用的属性之一。

.container {
  justify-content: flex-start;    /* 默认,左对齐/上对齐 */
  justify-content: flex-end;      /* 右对齐/下对齐 */
  justify-content: center;        /* 居中 */
  justify-content: space-between; /* 两端对齐,项目之间间距相等 */
  justify-content: space-around;  /* 每个项目两侧间距相等 */
  justify-content: space-evenly;  /* 项目之间间距相等,边缘间距也是项目间距的一半?不,是均匀分布,包括两端 */
}

其中space-betweenspace-evenly尤其好用:一个让首尾贴边,中间均分;一个让所有间隙相等,包括两端。

四、交叉轴上的对齐:align-items 与 align-content

1. align-items:单行项目的交叉轴对齐

当所有项目在一行(或一列)时,用align-items控制它们在交叉轴上的对齐方式。

.container {
  align-items: stretch;   /* 默认,如果项目未设置高度,则拉伸填满容器 */
  align-items: flex-start; /* 交叉轴起点对齐 */
  align-items: flex-end;   /* 交叉轴终点对齐 */
  align-items: center;     /* 交叉轴居中 */
  align-items: baseline;   /* 按第一行文字基线对齐 */
}

这个属性就是垂直居中的神器:只要容器有高度,设置align-items: center,项目就能垂直居中(当然主轴方向得是row)。

2. align-content:多行项目的整体对齐

当容器在交叉轴方向有多余空间,且项目有多行时,用align-content控制多行整体的对齐方式。它和justify-content类似,只不过作用于交叉轴。

.container {
  flex-wrap: wrap;        /* 先允许换行 */
  align-content: stretch;   /* 默认,拉伸占满 */
  align-content: flex-start;
  align-content: flex-end;
  align-content: center;
  align-content: space-between;
  align-content: space-around;
  align-content: space-evenly;
}

注意:如果项目只有一行,align-content不起作用。

五、项目的灵活性:flex 相关属性

项目自己也可以设置属性,控制自己的尺寸、排列顺序等。

1. flex-grow:如何分剩余空间

当容器还有剩余空间时,flex-grow决定项目是否放大、放大多少。默认值为0,即不放大。如果所有项目都设为1,则它们等分剩余空间;如果一个为2,其他为1,则2的那个多占一倍。

.item {
  flex-grow: 1;   /* 所有项目等分剩余空间 */
}

2. flex-shrink:空间不够时如何缩小

当容器空间不足时,flex-shrink决定项目是否缩小、缩小多少。默认值为1,即所有项目等比例缩小。设为0的项目不会缩小。

.item {
  flex-shrink: 0;   /* 打死我也不缩小 */
}

3. flex-basis:项目的基础尺寸

flex-basis定义项目在分配空间前的默认尺寸,可以理解为在主轴上的“初始宽度”(主轴水平时)。优先级高于width(如果同时设置)。默认值为auto,即参考项目本身的尺寸。

.item {
  flex-basis: 200px;   /* 我希望基础宽度是200px */
}

4. flex 简写

通常我们会用flex属性将上面三个合起来写:flex: grow shrink basis。常见值:

  • flex: 1 = flex: 1 1 0%(等分剩余空间)
  • flex: auto = flex: 1 1 auto(根据内容分配空间)
  • flex: none = flex: 0 0 auto(固定尺寸,不弹性)

六、项目的排序与对齐覆盖

1. order:改变项目顺序

默认所有项目的order为0,按源码顺序排列。你可以给某个项目设置更大的order让它往后排,或更小的order让它往前排。支持负数。

.item:last-child {
  order: -1;   /* 最后一个变成第一个 */
}

2. align-self:覆盖容器的 align-items

如果你想单独改变某个项目在交叉轴上的对齐方式,可以用align-self,它的取值和align-items一样。

.item.special {
  align-self: flex-end;   /* 单独沉底 */
}

七、实战:常见的Flexbox布局套路

1. 水平垂直居中

最简单的居中方案:

.parent {
  display: flex;
  justify-content: center;
  align-items: center;
}

无论子元素是一个还是多个,都能完美居中。

2. 导航栏:Logo左,菜单中,登录右

<nav class="nav">
  <div class="logo">Logo</div>
  <ul class="menu">
    <li>首页</li>
    <li>产品</li>
    <li>关于</li>
  </ul>
  <div class="login">登录</div>
</nav>
.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.menu {
  display: flex;
  gap: 20px;
  list-style: none;
}

如果想菜单绝对居中(不受左右宽度影响),可以给.menumargin: 0 auto

3. 等分布局

比如三个卡片等宽,间距固定:

.container {
  display: flex;
  gap: 20px;
}
.item {
  flex: 1;   /* 三个项目等分剩余空间,宽度相等 */
}

4. 圣杯布局(经典三栏)

左右固定宽度,中间自适应:

.container {
  display: flex;
}
.left {
  width: 200px;
}
.right {
  width: 200px;
}
.main {
  flex: 1;   /* 中间占满剩余空间 */
}

5. 底栏自动贴底

页面内容不足时,footer贴在底部;内容多时,footer被推下:

<body style="display: flex; flex-direction: column; min-height: 100vh;">
  <header>...</header>
  <main style="flex: 1;">...</main>
  <footer>...</footer>
</body>

八、常见坑点与避坑指南

1. 浮动失效

一旦元素成为flex项目,它的floatclearvertical-align都会失效。所以放心用flex,不用再担心浮动了。

2. margin: auto 的妙用

在flex容器中,设置某个项目的margin: auto,它会自动吸收剩余空间,实现“推挤”效果。例如让一个项目单独靠右:

.container {
  display: flex;
}
.item.move-right {
  margin-left: auto;   /* 把自己挤到右边 */
}

3. 文本溢出省略号

在flex项目中设置文本省略号时,可能需要给项目设置min-width: 0overflow: hidden,因为flex项目默认不会缩小到内容最小宽度以下。

.item {
  min-width: 0;        /* 允许项目缩小到比内容宽度小 */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

4. gap 属性

gap是较新的属性,可以方便地设置项目之间的间距,不用再为margin头疼。支持row-gapcolumn-gap,也可简写gap: 10px 20px

.container {
  display: flex;
  gap: 20px;   /* 项目之间左右、上下间距都是20px(如果换行) */
}

九、总结

Flexbox是现代布局的基石,掌握它,你就能轻松应对绝大多数一维布局场景。再回顾一下核心:

  • 容器设置display: flex,开启弹性世界。
  • flex-direction定主轴,用justify-content定主轴排列,用align-items定单行交叉轴对齐。
  • 项目用flex控制弹性,用order改变顺序,用align-self独立对齐。
  • 记住几个常用套路:居中、等分、导航、贴底。
  • 避坑:浮动失效、margin auto推挤、最小宽度限制。

Flexbox并不难,关键是理解“主轴”和“剩余空间分配”这两个概念。多动手写几个例子,你就能成为布局大师。

如果你喜欢这篇文章,欢迎点赞、收藏、分享。明天我们接着讲Grid布局——二维世界的终极武器,敬请期待!


明日预告:Grid网格布局从入门到精通——用网格思想重构网页,让二维布局不再头疼。

昨天以前首页

别再滥用 iframe 了!这些场景下它其实是最优解

作者 kyriewen
2026年3月13日 18:02

引言

“这个页面要嵌入另一个系统的报表,怎么办?”

“用 iframe 啊!”

“我想在咱们站里放个B站视频,但不想跳转。”

“用 iframe 啊!”

“微前端怎么搞?”

“用 iframe 啊!”——等等,微前端真的适合用 iframe 吗?

作为前端开发者,我们几乎每天都在和 iframe 打交道。它像一个“万能容器”,能轻松地把另一个页面塞进当前页面。但你真的了解它的能力边界吗?为什么有时候 iframe 会让页面卡顿?为什么有些网站死活不让你嵌入?为什么安全报告总提醒你注意 iframe 风险?

今天,我们抛开浅层用法,深入 iframe 的每一个毛孔,看看这个 25 岁的 HTML 元素,在现代前端开发中究竟扮演什么角色。

一、初见 iframe:不只是“页面里的页面”

1.1 基础语法

<iframe src="https://example.com" width="600" height="400"></iframe>

就这么简单,一个外部页面就被嵌入了。但它背后的行为远比看到的复杂:

  • 它创建了一个独立的浏览上下文(browsing context),有自己的历史记录、DOM 树、全局对象。
  • 可能与父页面同源(同协议、域名、端口),也可能跨域。
  • 它的加载、渲染、脚本执行几乎完全独立,但资源(如网络连接、线程)又共享自浏览器。

1.2 常用属性

除了 srcwidthheight,还有几个现代属性至关重要:

属性 作用
sandbox 对 iframe 内容施加额外限制(后文详述)
allow 控制特性权限,如麦克风、摄像头、全屏
allowfullscreen 是否允许全屏
loading 懒加载(lazy / eager
referrerpolicy 控制 Referer 头的发送策略

二、核心应用场景:什么时候非它不可?

2.1 嵌入第三方内容

视频(YouTube、B站)、地图(Google Maps)、社交帖子(Twitter、Instagram),这些平台提供的嵌入代码几乎都是 iframe。为什么?

  • 安全隔离:第三方脚本不能直接访问你的页面 DOM,防止恶意操作。
  • 样式独立:不会被你的 CSS 意外污染,也不用担心污染你的页面。
  • 功能完整:播放器、地图交互等复杂功能可以直接用对方提供的代码,不用自己实现。

2.2 广告系统

广告往往是跨域的,且需要沙盒化运行。iframe 天生适合:广告脚本在独立环境运行,无法窃取主站数据,同时又可以通过 postMessage 进行必要的通信(如上报尺寸变化)。

2.3 微前端架构的“兜底方案”

现代微前端框架(single-spa、qiankun)大多采用 JS 沙箱 + 路由分发的方式,但遇到老旧的、必须用全局变量或修改原型链的子应用时,iframe 成了最后的防线。虽然它有通信成本高、加载慢、UI 不同步等缺点,但胜在隔离彻底。

2.4 保持页面状态的“快照”

例如在线代码编辑器(CodePen、JSFiddle)的预览区域,用 iframe 执行用户代码,即使代码崩溃也不会影响主页面。

三、深入原理:iframe 与父页面的爱恨情仇

3.1 独立王国的边界

iframe 内部的所有内容(包括 JS 变量、定时器、事件监听)都局限在自己窗口内。但以下几个资源是跨上下文共享的:

  • 浏览器缓存src 里的资源会被正常缓存。
  • 网络连接:TCP 连接数限制是全局的,过多的 iframe 可能耗尽连接池。
  • localStorage / sessionStorage:同源 iframe 可以读写父页面的存储,跨域则不能(会抛出安全错误)。

3.2 通信:同源 vs 跨域

同源 iframe

父页面可以像操作自己的 DOM 一样操作 iframe 内部:

// 父页面
const iframe = document.getElementById('my-iframe');
iframe.contentDocument.getElementById('btn').click(); // 直接访问内部元素
iframe.contentWindow.someGlobalFunction(); // 调用内部全局函数

但要注意,必须等待 iframe 加载完成,否则 contentDocument 可能为空。

跨域 iframe

浏览器强制的同源策略会阻止父页面访问跨域 iframe 的 DOM。这时唯一安全的通信方式是 window.postMessage

父页面发送消息:

iframe.contentWindow.postMessage({
  type: 'UPDATE_USER',
  payload: { id: 123, name: 'Alice' }
}, 'https://iframe-domain.com'); // 目标源,必须指定

iframe 内监听消息:

window.addEventListener('message', (event) => {
  // 务必验证来源!
  if (event.origin !== 'https://parent-domain.com') return;
  
  if (event.data.type === 'UPDATE_USER') {
    // 更新界面
  }
});

安全原则: 永远检查 event.originevent.source,防止恶意页面冒充。

3.3 嵌套与层级

iframe 可以多层嵌套,形成“子→孙”结构。每一层都有独立的 window,但可以通过 window.parentwindow.top 访问父窗口和顶层窗口。跨域时访问这些属性也会被安全策略限制,只能通过 postMessage 向上传递。

四、安全性与沙盒:给 iframe 戴上枷锁

4.1 sandbox 属性

sandbox 是 iframe 最强大的安全工具,它可以启用一系列限制:

<iframe src="https://untrusted.com" sandbox></iframe>
<!-- 完全沙盒化:不允许脚本、表单、弹窗、导航等 -->

可以选择性放宽限制:

<iframe src="https://example.com" sandbox="allow-scripts allow-same-origin"></iframe>

常见 sandbox 值:

含义
allow-scripts 允许执行脚本
allow-same-origin 允许视为同源(如果不加,即使 URL 同源也会被当作跨域处理)
allow-forms 允许提交表单
allow-popups 允许弹窗(window.open
allow-modals 允许调用 alert() 等模态框
allow-orientation-lock 允许锁定屏幕方向
allow-pointer-lock 允许指针锁定
allow-top-navigation 允许导航到顶层窗口(危险)
allow-presentation 允许启动演示模式

重要: 不加 allow-same-origin 时,iframe 会被分配一个独特的来源(null),即使 URL 看起来同源。这是为了防止恶意脚本利用 iframe 绕过同源策略。

4.2 allow 属性(功能策略)

allow 属性用于控制更精细的权限,如摄像头、麦克风:

<iframe src="https://meet.example.com" 
        allow="camera; microphone; fullscreen">
</iframe>

这些权限需要配合 Feature Policy(现称 Permissions Policy)使用,浏览器会向用户请求授权。

4.3 防止页面被嵌入(X-Frame-Options 与 CSP)

如果你的页面不想被别人的 iframe 嵌入(比如防止点击劫持),可以设置响应头:

  • X-Frame-Options: DENY(完全禁止)或 SAMEORIGIN(只允许同源页面嵌入)
  • Content-Security-Policy: frame-ancestors 'self' https://example.com(更精细的控制)
X-Frame-Options: DENY
# 或
Content-Security-Policy: frame-ancestors 'none';

五、性能影响:看不见的代价

5.1 加载阻塞

<iframe> 的加载会阻塞主页面 onload 事件。即使使用 loading="lazy" 懒加载,也依然需要额外的连接开销。

5.2 内存占用

每个 iframe 都是一个独立的文档环境,会占用大量内存(尤其是包含复杂交互时)。过多 iframe 可能导致页面卡顿甚至崩溃。

5.3 最佳实践

  • 延迟加载:对不可见的 iframe(如下方广告)设置 loading="lazy"
  • 动态创建:只在需要时创建 iframe,用完及时销毁(iframe.remove())。
  • 限制数量:不要超过 2~3 个活跃 iframe。
  • 预先连接:如果知道 iframe 来源,可以用 <link rel="preconnect"> 提前建立连接。

六、现代替代方案:iframe 不是唯一解

场景 iframe 的问题 替代方案
嵌入外部页面 样式隔离但交互受限 Web Components(Shadow DOM)可以隔离样式,但不能隔离 JS
微前端 通信复杂、加载慢 single-spa、qiankun 等基于 JS 沙箱的微前端框架
显示富文本内容 需要安全展示用户生成内容 使用专门的 HTML 渲染库(如 DOMPurify)搭配 Shadow DOM
显示 PDF iframe 会接管整个窗口 <object><embed>,或使用 PDF.js 自建渲染
跨域通信 只能 postMessage 如果只是获取数据,可以用 Fetch API + CORS

但 iframe 在完全隔离不可信代码的场景中,依然不可替代。例如在线代码编辑器、广告系统。

七、常见问题与解决方案

Q1:如何让 iframe 高度自适应内容?

跨域 iframe 无法直接读取内部高度,但可以通过 postMessage 由内部通知父页面:

iframe 内:

const height = document.documentElement.scrollHeight;
window.parent.postMessage({ type: 'resize', height }, 'https://parent.com');

父页面:

window.addEventListener('message', (e) => {
  if (e.data.type === 'resize') {
    document.getElementById('my-iframe').style.height = e.data.height + 'px';
  }
});

Q2:为什么 iframe 内的 localStorage 无法共享?

跨域 iframe 的 localStorage 是与 iframe 的源绑定的,无法访问父页面源的数据。如果需要共享,可以考虑 postMessage 让父页面代为存储。

Q3:如何检测 iframe 是否加载完成?

iframe.onload = () => {
  console.log('iframe loaded');
};
// 或者用 addEventListener
iframe.addEventListener('load', () => {});

对于跨域 iframe,onload 仍然可触发,但不能访问内部文档。

Q4:如何防止 iframe 内的链接跳转导致父页面变化?

给 iframe 设置 sandbox(不加 allow-top-navigation)即可阻止链接导航到顶层。

Q5:如何让 iframe 内的 PDF 自动打印?

这无法直接控制,因为 PDF 插件是浏览器内置行为。但可以提示用户使用插件菜单。

八、总结:iframe 仍是工具箱里的重要角色

iframe 就像前端开发的“瑞士军刀”——看似笨重,但在需要强隔离的场景里,它依然是唯一可靠的选择。了解它的工作原理、性能影响和安全策略,你就能在合适的场景下让它发挥最大价值,而不是盲目滥用。

下一次当你想用 iframe 时,不妨问自己三个问题:

  1. 真的需要完全隔离吗?能否用 Web Components 替代?
  2. 如果非用不可,是否设置了合适的 sandboxallow 属性?
  3. 如何优化它的加载,避免拖慢主页面?

思考题:假设你想在自己的博客里嵌入一个来自“example.com”的页面,但对方设置了 X-Frame-Options: SAMEORIGIN,你有什么办法让它强制显示吗?为什么?

(答案下期揭晓,也欢迎留言讨论)


每日一问:你在实际开发中遇到过哪些 iframe 的坑?是怎么解决的?欢迎在评论区分享你的经验。

构建无障碍组件之Switch Pattern

作者 anOnion
2026年3月8日 23:03

Switch Pattern 详解:构建无障碍开关组件

开关(Switch)是一种模拟物理开关的控件,用于在两个状态(通常是"开"和"关")之间切换。在一些 UI 组件库中,它也被称为 Toggle(切换开关)。本文基于 W3C WAI-ARIA Switch Pattern 规范,详解如何构建无障碍的开关组件。

一、Switch 的定义与核心概念

1.1 什么是 Switch

Switch 是一种特殊的二元状态控件,它:

  • 模拟物理开关的行为
  • 在两个互斥状态之间切换(开/关、启用/禁用)
  • 与 Checkbox 不同,Switch 的状态改变通常会立即生效,无需提交表单

1.2 Switch 与 Checkbox 的区别

特性 Switch Checkbox
视觉表现 滑动开关样式 方框勾选样式
状态语义 开/关(On/Off) 选中/未选中(Checked/Unchecked)
操作反馈 通常立即生效 通常需要提交表单
使用场景 设置项切换、功能启用/禁用 多选项选择、表单提交
ARIA 角色 role="switch" role="checkbox"

1.3 何时使用 Switch

适合使用 Switch 的场景:

  • 系统设置(如:开启/关闭通知)
  • 功能启用(如:启用暗黑模式)
  • 即时生效的选项(如:开启/关闭 WiFi)

适合使用 Checkbox 的场景:

  • 表单中的多选项
  • 需要提交后才生效的选择
  • 列表中的批量选择

二、原生 HTML Switch 实现

HTML5.2 起,<input type="checkbox"> 新增了 switch 属性,可以直接创建原生 Switch:

<label>
  开启通知
  <input
    type="checkbox"
    role="switch" />
</label>

2.1 原生 Switch 的浏览器支持

目前原生 Switch 的支持情况:

  • Safari:完全支持(包括 iOS Safari)
  • Chrome/Edge:需要通过 CSS 自定义样式
  • Firefox:需要通过 CSS 自定义样式

由于跨浏览器兼容性考虑,实际项目中通常使用自定义样式实现。

三、WAI-ARIA 角色与属性

3.1 基本角色

Switch 具有 role="switch"

3.2 状态属性

注意:Switch 只支持 truefalse 两种状态,不支持 mixed(与 Checkbox 不同)。

3.3 可访问标签

Switch 的可访问标签可以通过以下方式提供:

  • 可见文本内容:直接包含在具有 role="switch" 的元素内的文本
  • aria-labelledby:引用包含标签文本的元素的 ID
  • aria-label:直接在开关元素上设置标签文本
<!-- 方式一:可见文本内容 -->
<div
  role="switch"
  aria-checked="false">
  开启通知
</div>

<!-- 方式二:aria-labelledby -->
<span id="wifi-label">WiFi</span>
<div
  role="switch"
  aria-checked="true"
  aria-labelledby="wifi-label"></div>

<!-- 方式三:aria-label -->
<div
  role="switch"
  aria-checked="false"
  aria-label="开启暗黑模式"></div>

3.4 描述属性

如果包含额外的描述性静态文本,使用 aria-describedby

<div
  role="switch"
  aria-checked="false"
  aria-describedby="airplane-desc">
  飞行模式
</div>
<p id="airplane-desc">关闭所有无线连接</p>

四、键盘交互规范

当 Switch 获得焦点时:

按键 功能
Space 切换开关状态(开 ↔ 关)
Enter(可选) 某些实现中也支持切换开关状态

五、实现方式

5.1 原生 HTML + CSS 实现

<label class="switch">
  <input
    type="checkbox"
    role="switch" />
  <span class="slider"></span>
  开启通知
</label>

<style>
  .switch {
    display: flex;
    align-items: center;
    gap: 12px;
    cursor: pointer;
  }

  .switch input {
    appearance: none;
    width: 48px;
    height: 24px;
    background: #ccc;
    border-radius: 12px;
    position: relative;
    cursor: pointer;
    transition: background 0.3s;
  }

  .switch input::after {
    content: '';
    position: absolute;
    width: 20px;
    height: 20px;
    background: white;
    border-radius: 50%;
    top: 2px;
    left: 2px;
    transition: transform 0.3s;
  }

  .switch input:checked {
    background: #005a9c;
  }

  .switch input:checked::after {
    transform: translateX(24px);
  }

  .switch input:focus {
    outline: 2px solid #005a9c;
    outline-offset: 2px;
  }
</style>

5.2 ARIA 实现(自定义样式)

<div
  role="switch"
  tabindex="0"
  aria-checked="false"
  aria-labelledby="switch-label"
  onclick="toggleSwitch(this)"
  onkeydown="handleKeydown(event, this)">
  <span class="switch-track">
    <span
      class="switch-thumb"
      aria-hidden="true"></span>
  </span>
  <span id="switch-label">开启通知</span>
</div>

<script>
  function toggleSwitch(switchEl) {
    const isChecked = switchEl.getAttribute('aria-checked') === 'true';
    switchEl.setAttribute('aria-checked', !isChecked);
  }

  function handleKeydown(event, switchEl) {
    if (event.key === ' ') {
      event.preventDefault();
      toggleSwitch(switchEl);
    }
  }
</script>

<style>
  [role='switch'] {
    display: flex;
    align-items: center;
    gap: 12px;
    cursor: pointer;
  }

  .switch-track {
    width: 48px;
    height: 24px;
    background: #ccc;
    border-radius: 12px;
    position: relative;
    transition: background 0.3s;
  }

  [role='switch'][aria-checked='true'] .switch-track {
    background: #005a9c;
  }

  .switch-thumb {
    position: absolute;
    width: 20px;
    height: 20px;
    background: white;
    border-radius: 50%;
    top: 2px;
    left: 2px;
    transition: transform 0.3s;
  }

  [role='switch'][aria-checked='true'] .switch-thumb {
    transform: translateX(24px);
  }

  [role='switch']:focus {
    outline: 2px solid #005a9c;
    outline-offset: 2px;
  }
</style>

六、常见应用场景

6.1 系统设置项

<fieldset>
  <legend>通知设置</legend>

  <label>
    <div>
      <span>推送通知</span>
      <p>接收应用推送消息</p>
    </div>
    <input type="checkbox" checked />
  </label>

  <label>
    <div>
      <span>邮件通知</span>
      <p>接收每日摘要邮件</p>
    </div>
    <input type="checkbox" />
  </label>

  <label>
    <div>
      <span>短信通知</span>
      <p>接收重要提醒短信</p>
    </div>
    <input type="checkbox" checked />
  </label>
</fieldset>

6.2 功能开关

<div>
  <label>
    <div>
      <span>🌙</span>
      <div>
        <span>暗黑模式</span>
        <p>使用深色主题保护眼睛</p>
      </div>
    </div>
    <input type="checkbox" />
  </label>

  <label>
    <div>
      <span>🔒</span>
      <div>
        <span>自动锁定</span>
        <p>闲置 5 分钟后自动锁定</p>
      </div>
    </div>
    <input type="checkbox" checked />
  </label>
</div>

6.3 隐私设置

<fieldset>
  <legend>隐私设置</legend>

  <label>
    <div>
      <span>公开个人资料</span>
      <p>允许其他用户查看您的资料</p>
    </div>
    <input type="checkbox" />
  </label>

  <label>
    <div>
      <span>显示在线状态</span>
      <p>让好友知道您在线</p>
    </div>
    <input type="checkbox" checked />
  </label>

  <label>
    <div>
      <span>允许搜索到我</span>
      <p>通过用户名搜索可以找到您</p>
    </div>
    <input type="checkbox" checked />
  </label>
</fieldset>

七、最佳实践

7.1 优先使用原生 Checkbox

原生 HTML <input type="checkbox"> 配合 CSS 样式是最可靠的方式,它自动继承了浏览器的无障碍特性。

7.2 提供清晰的标签

始终为 Switch 提供清晰的标签,说明开关控制的功能:

<!-- 推荐 -->
<label>
  <span>开启自动保存</span>
  <input type="checkbox" />
</label>

<!-- 不推荐:没有标签或标签不清晰 -->
<input type="checkbox" />
<span>开启</span>

7.3 使用描述文本

对于复杂的设置项,提供额外的描述文本:

<label>
  <div>
    <span>数据同步</span>
    <p>自动将数据备份到云端</p>
  </div>
  <input type="checkbox" />
</label>

7.4 避免在 Switch 上嵌套其他交互元素

<!-- 不推荐 -->
<label>
  <input type="checkbox" />
  开启功能 <a href="/help">了解更多</a>
</label>

<!-- 推荐 -->
<div>
  <div>
    <span>开启功能</span>
    <a href="/help">了解更多</a>
  </div>
  <input type="checkbox" />
</div>

7.5 状态反馈

确保用户能够清楚地看到开关的当前状态:

  • 使用颜色变化表示开关状态(如:蓝色表示开启,灰色表示关闭)
  • 提供焦点样式以便键盘用户识别
  • 禁用状态使用较低的透明度并禁用鼠标交互

7.6 移动端触摸区域

确保 Switch 有足够的触摸区域(至少 44x44px),可以通过增加 padding 或增大开关尺寸实现。

八、Switch、Checkbox 与 Radio 的选择

场景 推荐组件 原因
即时生效的设置项 Switch 模拟物理开关,立即反馈
表单中的多选项 Checkbox 需要提交后才生效
单选场景 Radio 互斥选择
列表中的批量操作 Checkbox 支持多选

九、总结

Switch 是一种直观的状态切换控件,适用于需要即时反馈的设置场景。与 Checkbox 相比,Switch 更强调"开/关"的语义,通常用于控制功能的启用和禁用。

构建无障碍的 Switch 组件需要注意:使用正确的 ARIA 角色(role="switch")、提供清晰的标签、确保键盘可访问性(Space 键切换),以及为屏幕阅读器用户提供准确的状态反馈。

开发者应优先使用语义化的 HTML 元素,确保所有用户都能顺畅地使用开关功能。

文章同步于 an-Onion 的 Github。码字不易,欢迎点赞。

❌
❌