文章目录
-
- [一、CSS 三列布局](#一、CSS 三列布局)
-
- [1.1 什么是三列布局](#1.1 什么是三列布局)
- [1.2 前置知识:格式化上下文(BFC / FFC / GFC)](#1.2 前置知识:格式化上下文(BFC / FFC / GFC))
- [1.3 方案一:Flex 弹性布局(最常用)](#1.3 方案一:Flex 弹性布局(最常用))
- [1.4 方案二:Grid 网格布局(最简洁)](#1.4 方案二:Grid 网格布局(最简洁))
- [1.5 方案三:Float 浮动布局(最古老,需理解 BFC)](#1.5 方案三:Float 浮动布局(最古老,需理解 BFC))
- [1.6 三种方案对比](#1.6 三种方案对比)
- [二、TypeScript 工具类型](#二、TypeScript 工具类型)
-
- [2.1 为什么需要工具类型](#2.1 为什么需要工具类型)
- [2.2 基础:keyof 与 typeof](#2.2 基础:keyof 与 typeof)
- [2.3 Pick:挑选字段](#2.3 Pick:挑选字段)
- [2.4 Omit:剔除字段](#2.4 Omit:剔除字段)
- [2.5 重点:Omit = Pick + Exclude](#2.5 重点:Omit = Pick + Exclude)
- [2.6 Partial:全字段可选](#2.6 Partial:全字段可选)
- [2.7 Record 与 ReturnType](#2.7 Record 与 ReturnType)
- [2.8 Exclude 与 Omit 的区别](#2.8 Exclude 与 Omit 的区别)
- 三、全文总结
- 四、核心知识点复盘
- [五、常见问题 / 避坑指南](#五、常见问题 / 避坑指南)
这是一篇面向「基础偏薄弱但想系统搞懂」的前端同学的文章。前半部分用三种方式(Flex / Grid / Float)讲透 PC 端最常见的三列布局 ,以及它背后的核心概念------格式化上下文(BFC / FFC / GFC) ;后半部分讲 TypeScript 内置工具类型 (Pick、Omit、Partial、Record、ReturnType),重点拆解
Omit = Pick + Exclude这条底层等价关系。每个知识点都按「是什么 → 为什么 → 怎么用 → 易错点」的顺序展开,代码完整可运行。
一、CSS 三列布局
1.1 什么是三列布局
在 PC 端网页里,有一种非常经典的页面骨架:左右两列固定宽度,中间一列自适应撑满剩余空间。典型场景就是下面这种结构:
┌────────────┬──────────────────────────┬────────────┐
│ 左侧导航 │ 中间主内容 │ 右侧广告 │
│ (固定 200px)│ (自适应剩余宽度) │ (固定 200px)│
└────────────┴──────────────────────────┴────────────┘
它的 HTML 骨架长这样:
html
<div class="layout">
<aside class="sidebar left">左侧导航</aside>
<main class="content">中间主内容(自适应)</main>
<aside class="sidebar right">右侧广告</aside>
</div>
这里有一个很关键的细节 :为什么要把 <main>(主内容)放在中间、优先渲染?因为两侧通常是导航、广告这类「次要信息」,而主内容才是用户最想第一时间看到的东西。让主内容先渲染,页面感知速度更快,SEO 也更友好。这个「内容优先」的诉求,决定了我们后面选择布局方案时的取舍。
要真正理解三列布局,得先搞懂一个底层概念------格式化上下文。
1.2 前置知识:格式化上下文(BFC / FFC / GFC)
这部分是 CSS 布局的地基,也是很多同学「知其然不知其所以然」的地方,我们把它讲透。
文档流(Normal Flow) 是浏览器默认的排版规则:
- 块级元素 (
div、p、h1...)从上到下垂直排列; - 行内元素 (
span、a、em...)从左到右水平排列。
<html> 作为根元素,一启动就开启了第一个格式化上下文(BFC),所有元素默认都排在这个「最外层的大容器」里。
BFC(Block Formatting Context,块级格式化上下文) 可以理解为一个「独立的排版作用域」:在这个作用域里,元素按照自己的规则排列,外层的 BFC 不会影响到内部新的格式化上下文,内部也不会反过来干扰外部。
问题来了:默认的 BFC 只能让块级元素从上往下排,没法做「左右分列」这种局部复杂布局 。而 inline 行内元素又天生不适合当容器盒子。所以要实现三列布局,就必须「开启一个新的格式化上下文」,让某个块级元素拥有独立的、更强大的排版能力。
开启新的格式化上下文的常见方式:
| 写法 | 开启的上下文 | 说明 |
|---|---|---|
display: flex |
FFC(Flex 格式化上下文) | 一维布局,主轴方向排列 |
display: grid |
GFC(Grid 格式化上下文) | 二维布局,行列同时控制 |
display: table / inline-block |
新的 BFC | 表格 / 行内块排版 |
float: left / right |
新的 BFC | 元素脱离文档流浮动 |
position: absolute / fixed |
新的 BFC | 脱离文档流定位 |
overflow: hidden / auto / scroll |
新的 BFC | 触发「块级格式化上下文」 |
记住一句话:只要让一个块级元素开启新的格式化上下文,它内部就能独立排版,不再受外层默认文档流的束缚。三列布局正是靠这个实现的。
下面用三种方式分别实现,感受它们各自的特点。
1.3 方案一:Flex 弹性布局(最常用)
Flex 是一维布局:容器设置 display: flex 后,子项会沿着「主轴」排列。中间列用 flex: 1 吃掉所有剩余空间即可。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>三列布局 - Flex</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.layout {
display: flex; /* 开启弹性格式化上下文 FFC */
height: 100vh; /* 占满视口高度,方便观察 */
}
/* 左右两列:固定宽度,且不允许被压缩 */
.sidebar {
width: 200px;
flex-shrink: 0; /* 关键:默认 flex 子项可能被压缩,这里禁止压缩 */
}
.left { background: #f0c0c0; }
.right { background: #c0c0f0; }
/* 中间主内容:自适应剩余宽度 */
.content {
flex: 1; /* 等价于 flex: 1 1 0%,撑满剩余空间 */
background: #fff;
}
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar left">左侧导航</aside>
<main class="content">中间主内容(自适应宽度)</main>
<aside class="sidebar right">右侧广告</aside>
</div>
</body>
</html>
代码思路解析:
.layout上display: flex,把三个子项排成一行;- 两侧
.sidebar固定width: 200px,并加flex-shrink: 0------因为 flex 子项默认允许被「压缩」,不加这个,当内容多时两侧会被挤窄,固定宽度就失效了; - 中间
.content用flex: 1,表示「占据所有剩余空间」,宽度自动适配。
易错点: 很多同学只写 width: 200px 却忘了 flex-shrink: 0,结果窗口变窄时左右两列被压变形。这是 flex 布局里最隐蔽的坑。
1.4 方案二:Grid 网格布局(最简洁)
Grid 是二维布局,可以直接用 grid-template-columns 一次声明三列的宽度。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>三列布局 - Grid</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.layout {
display: grid;
/* 三列:左 200px | 中间 1fr 自适应 | 右 200px */
grid-template-columns: 200px 1fr 200px;
height: 100vh;
}
.left { background: #f0c0c0; }
.right { background: #c0c0f0; }
.content { background: #fff; }
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar left">左侧导航</aside>
<main class="content">中间主内容(自适应)</main>
<aside class="sidebar right">右侧广告</aside>
</div>
</body>
</html>
代码思路解析:
display: grid开启网格格式化上下文 GFC;grid-template-columns: 200px 1fr 200px里,200px是固定列宽,1fr是「按剩余空间等分 1 份」,于是中间列自动撑满。
fr(fraction,份数)是 Grid 独有的单位,非常好用。相比之下,Grid 写三列布局代码最少、语义最清晰,是现代项目里值得优先考虑的方案。它天然不会有 flex 那种「被压缩」的问题。
1.5 方案三:Float 浮动布局(最古老,需理解 BFC)
Float 是「上古」方案,但它能帮我们真正吃透 BFC,所以值得讲。核心思路:左右两列分别 float: left / float: right,中间列触发 BFC 来避开浮动。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>三列布局 - Float</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.layout { height: 100vh; }
.sidebar {
width: 200px;
height: 100vh;
}
.left { float: left; background: #f0c0c0; }
.right { float: right; background: #c0c0f0; }
/* 中间列:触发 BFC,避免被浮动元素覆盖 */
.content {
overflow: hidden; /* 关键:开启新的 BFC */
background: #fff;
}
</style>
</head>
<body>
<!-- 注意:float 方案必须让「浮动元素」写在前面 -->
<div class="layout">
<aside class="sidebar left">左侧导航</aside>
<aside class="sidebar right">右侧广告</aside>
<main class="content">中间主内容(自适应)</main>
</div>
</body>
</html>
代码思路解析:
- 左列
float: left、右列float: right,两者脱离文档流,分别贴在容器左右两侧; - 中间列如果不做处理,会被浮动元素「盖住」,所以给它
overflow: hidden------这会让它开启一个新的 BFC,从而不再与浮动元素重叠,自动占满剩余空间。
两个必须记住的坑:
- DOM 顺序变了:float 要求「浮动元素」必须写在前面,否则浮不上去。这跟「主内容优先渲染」的诉求是冲突的,所以 float 方案其实不利于语义化,这也是它逐渐被淘汰的原因之一;
overflow: hidden的作用是触发 BFC,不是单纯「裁剪溢出」。很多同学以为它只是隐藏滚动条,其实它的真正作用是建立独立的排版上下文。
1.6 三种方案对比
| 方案 | 代码量 | 语义化 | 是否需处理「压缩」问题 | 兼容性 | 适用场景 |
|---|---|---|---|---|---|
| Flex | 中 | 好,可保持 main 居中 | 需 flex-shrink: 0 |
极好 | 最常用,推荐 |
| Grid | 少 | 最好 | 无 | 好 | 现代项目首选 |
| Float | 中 | 差,浮动弹需前置 | 需 overflow: hidden 触发 BFC |
极好 | 理解 BFC / 老代码维护 |
小结:能写 Grid 优先 Grid,其次 Flex,Float 主要用于理解原理和阅读历史代码。
二、TypeScript 工具类型
2.1 为什么需要工具类型
在大型项目里,「类型」会被大量复用。假如我们有一个用户对象接口:
typescript
interface User {
id: number;
name: string;
age: number;
email: string;
}
实际开发中,经常遇到这类需求:
- 列表页只展示
id和name,不想暴露全部字段; - 接口返回给前端时,要剔除 掉敏感字段(比如
email); - 更新用户信息时,所有字段都可选(传哪个改哪个);
- 定义「状态码 → 错误提示」这种字典。
如果每次都手写一个新接口,字段一多就会又啰嗦又容易漏改。TypeScript 内置了一批工具类型(Utility Types),专门用来「基于已有类型,加工出新类型」。下面逐个讲清楚。
2.2 基础:keyof 与 typeof
理解工具类型前,先掌握两个基础操作符。
keyof:取出一个类型「所有键名」组成的联合类型。
typescript
type UserKeys = keyof User;
// 等价于:type UserKeys = 'id' | 'name' | 'age' | 'email'
typeof(类型上下文里):取出一个「值」的类型。
typescript
function fn() {
return { x: 1, y: 2 };
}
type FnResult = ReturnType<typeof fn>; // { x: number; y: number }
注意:这里的
typeof是「类型层面」的用法,和 JS 里的运行时typeof是两码事,别混淆。
2.3 Pick:挑选字段
Pick<T, K> 从类型 T 里挑选 出键集合 K 中的字段,组成新类型。
typescript
// 从 User 中只挑出 id 和 name
type UserPreview = Pick<User, 'id' | 'name'>;
const u: UserPreview = {
id: 1,
name: 'zmt',
};
// 如果写成 { id: 1, name: 'zmt', age: 18 } 会报错:age 不存在于 UserPreview
底层实现(可略读,理解即可):
typescript
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
[P in K] 是「映射类型」:遍历 K 里的每个键,用 T[P] 取出对应的值类型。K extends keyof T 约束 K 必须是 T 的键,防止写错。
2.4 Omit:剔除字段
Omit<T, K> 与 Pick 相反,从类型 T 里剔除 掉键集合 K,保留其余字段。
typescript
// 从 User 中剔除 email,得到不含敏感字段的安全类型
type UserSafe = Omit<User, 'email'>;
const safeUser: UserSafe = {
id: 1,
name: 'zmt',
age: 18,
};
// email 已被剔除,写 email 会报错
典型场景:接口返回给前端的 DTO(数据传输对象)通常要 Omit 掉密码、邮箱等敏感字段。
2.5 重点:Omit = Pick + Exclude
这是本章最核心、也最容易被问到的一条等价关系:
Omit<T, K> 等价于 Pick<T, Exclude<keyof T, K>>
这条式子怎么理解?分三步拆解:
keyof T:拿到T所有键的联合类型,即'id' | 'name' | 'age' | 'email';Exclude<keyof T, K>:从这些键里删掉 要剔除的K(比如'email'),剩下要保留的键'id' | 'name' | 'age';Pick<T, 剩下的键>:再用 Pick 把剩下的键从T里挑出来。
「剔除 = 先算出要保留的键,再挑选」------Omit 本质上是 Pick 的一个「组合拳」,它自己没有独立的新能力,只是把 Exclude 和 Pick 串了起来。TypeScript 内部正是这么实现的:
typescript
// TS 官方 Omit 的等价实现
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
用代码验证一下,结果完全一致:
typescript
type UserKeys = keyof User; // 'id' | 'name' | 'age' | 'email'
type KeepKeys = Exclude<UserKeys, 'email'>; // 'id' | 'name' | 'age'
type MyOmitUser = Pick<User, KeepKeys>; // 等价于 Omit<User, 'email'>
再单独看 Exclude 的底层实现,加深理解:
typescript
type Exclude<T, U> = T extends U ? never : T;
它利用「条件类型的分配律 」:当 T 是联合类型时,会逐个 判断 T 的每个成员是否属于 U,属于就变成 never(相当于丢弃),不属于就保留。比如 Exclude<'id'|'name'|'age'|'email', 'email'>,前三个都不等于 'email' 所以保留,'email' 自己等于 'email' 所以变成 never 被丢弃。
2.6 Partial:全字段可选
Partial<T> 把类型 T 的所有字段都变成可选。
typescript
type PartialUser = Partial<User>;
// 现在所有字段都可选,可以只传部分字段
const patchUser: PartialUser = {
name: '张三',
age: 18,
};
// 甚至可以传一个空对象,完全合法
const emptyObj: PartialUser = {};
典型场景: 更新接口(patch)经常只传「要修改的那几个字段」,用 Partial 表达最合适。
底层实现:
typescript
type Partial<T> = {
[P in keyof T]?: T[P]; // 注意多了一个 ?,表示可选
};
对比 Pick 的实现,差别只在 T[P] 后面多了一个 ?。理解这个「映射类型 + 可选修饰符」的组合,Pick/Omit/Partial 就全通了。
2.7 Record 与 ReturnType
Record<K, V>:构造一个「键类型为 K、值类型为 V」的对象类型,常用于字典 / 映射表。
typescript
// 键是 string,值是 number
type Dict = Record<string, number>;
const obj: Dict = { a: 1, b: 2 };
// 经典场景:HTTP 状态码 → 错误提示
type ErrorMsgMap = Record<number, string>;
const errorMsgMap: ErrorMsgMap = {
400: '请求参数错误',
401: '未登录,请重新登录',
403: '权限不足,拒绝访问',
404: '资源找不到',
500: '服务器内部错误,请稍后重试',
};
// 配合函数使用,取不到时给兜底值
function getErrMsg(code: number): string {
return errorMsgMap[code] ?? '未知错误';
}
补充一个小知识:HTTP 状态码的分段含义------1XX 执行中、2XX 成功、3XX 重定向、4XX 客户端错误、5XX 服务端错误。上面
ErrorMsgMap就是用Record把这套映射固化成了类型安全的字典。
ReturnType<T>:取出一个函数的「返回值类型」。
typescript
function fn() {
return { x: 1, y: 2 };
}
type FunReturn = ReturnType<typeof fn>; // { x: number; y: number }
它依赖 infer 推断,底层实现长这样(了解即可):
typescript
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any;
2.8 Exclude 与 Omit 的区别
很多同学分不清这两个,一句话点破:
Exclude操作的是「联合类型」:从一串键(或字面量)里删除某些成员;Omit操作的是「对象 / 接口类型」:从一个对象的字段里删除某些键。
所以它们的「输入类型」不同、产出也不同:Exclude<'a'|'b'|'c', 'c'> 得到 'a'|'b';Omit<Obj, 'c'> 得到一个新的对象类型。而 Omit 内部正是「借用了 Exclude 删除键,再用 Pick 重建对象」,这就是 2.5 节那条等价关系的由来。
三、全文总结
这篇文章围绕两条主线展开:
-
三列布局 :核心是「左右固定、中间自适应」。实现它的底层原理是格式化上下文 ------
<html>根元素开启第一个 BFC,块级元素默认只能从上到下排列;要左右分列,就必须通过display: flex(FFC)、display: grid(GFC)、float/overflow: hidden等方式开启新的格式化上下文。三种方案里,Grid 最简洁、Flex 最常用、Float 最古老但最能帮助理解 BFC。 -
TS 工具类型 :核心是「基于已有类型加工新类型」。
Pick挑字段、Omit删字段、Partial全转可选、Record造字典、ReturnType取返回值类型。其中最重要的等价关系是Omit<T,K> = Pick<T, Exclude<keyof T, K>>------它说明 Omit 不是「原生能力」,而是 Exclude(删键)+ Pick(挑键)的组合拳,理解了它,整套工具类型就串起来了。
四、核心知识点复盘
| 知识点 | 一句话结论 |
|---|---|
| 文档流 | 块级元素从上到下、行内元素从左到右 |
| BFC / FFC / GFC | 独立的排版作用域,html 根元素开启第一个 BFC |
| 开启新格式化上下文 | flex / grid / float / position / overflow: hidden 等 |
| 三列布局 | 左右定宽 + 中间自适应(flex: 1 / 1fr / overflow: hidden) |
flex-shrink: 0 |
防止 flex 子项被压缩、固定宽度失效 |
keyof T |
取出 T 所有键组成的联合类型 |
Pick<T, K> |
挑选 K 里的字段,组成新类型 |
Omit<T, K> |
剔除 K 里的字段 |
Exclude<T, U> |
从联合类型 T 中删除属于 U 的成员 |
| 等价关系 | Omit<T,K> = Pick<T, Exclude<keyof T, K>> |
Partial<T> |
所有字段转可选,用于 patch 更新 |
Record<K, V> |
构造键 K 值 V 的字典类型 |
ReturnType<typeof fn> |
取函数返回值类型 |
五、常见问题 / 避坑指南
Q1:三列布局里中间列没撑满 / 左右列被压缩了怎么办?
A:最常见原因是 flex 方案里漏写了 flex-shrink: 0 。flex 子项默认 flex-shrink: 1(允许收缩),当空间不足时左右两列会被挤窄。给固定宽度的 .sidebar 加上 flex-shrink: 0 即可。Grid 方案则天然没有这个问题。
Q2:overflow: hidden 在三列布局里到底起了什么作用?
A:它不是「隐藏滚动条」那么简单,而是触发 BFC 。float 布局中,浮动元素会脱离文档流、盖住后面的普通元素;给中间列加 overflow: hidden 会让它开启新的 BFC,从而不再与浮动元素重叠,自动占满剩余宽度。这是理解 float 布局的关键。
Q3:为什么 float 方案里 <main> 要写在两个 aside 后面?
A:float 要求「浮动元素必须出现在文档流中更靠前的位置」才能正常浮起来。这跟「主内容优先渲染」的诉求相冲突,也是 float 被 flex / grid 取代的重要原因之一。
Q4:keyof 和 typeof 老分不清怎么办?
A:一句话区分------keyof 取「键」,typeof 取「类型」 。keyof User 得到键名联合类型;typeof 变量(在类型位置)得到该变量的类型。ReturnType<typeof fn> 就是「先拿到 fn 的类型,再取它的返回值类型」。
Q5:Exclude 和 Omit 到底什么时候用哪个?
A:操作对象是「联合类型」用 Exclude(删几个字面量成员);操作对象是「对象 / 接口」用 Omit(删几个字段)。记不住也没关系,记住 Omit = Pick + Exclude 这条关系,一切都能倒推出来。
Q6:Partial 之后字段都可选,会不会丢失类型安全?
A:不会「丢失」,反而是「放宽」。Partial<User> 要求你写出来的每个字段(如果有)类型仍然正确,只是允许你不写 某些字段。更新接口(patch 语义)用它是安全的,读取/展示场景则应该用完整的 User 或 Pick/Omit 出来的类型,别滥用。
Q7:这些工具类型需要自己写吗?
A:不需要。它们都是 TypeScript 内置的,直接使用即可。但理解它们的底层实现 (尤其是映射类型 [P in keyof T] 和条件类型 T extends U ? X : Y)能让你举一反三,以后遇到 Required、Readonly、Record 等同类工具类型时,一眼就能看懂本质。