理解 CSS 层叠上下文:为什么 z-index 有时不生效
2026.10.14
·
CSS
·
约 6 分钟阅读
刚接触 CSS 定位的时候,几乎每个人都遇到过同一个困惑:明明给元素写了很高的 z-index,
它却依然被另一个看起来“层级更低”的元素盖住。这个问题的答案,通常指向一个容易被忽略的概念——层叠上下文。
z-index 并不是全局生效的
很多人的直觉是:z-index 越大,元素就越靠上,像一个全局的“楼层号”。
但实际规则是,z-index 只在同一个层叠上下文内部比较才有意义。
如果两个元素分属不同的层叠上下文,它们的 z-index 根本不在同一个比较体系里。
换句话说,层叠上下文像一个独立的“房间”。房间里的元素可以互相比较高低,
但一个房间里 z-index: 9999 的元素,也跳不出这个房间去压住另一个房间里的元素。
哪些属性会创建层叠上下文
根元素 <html> 天然是一个层叠上下文。除此之外,以下情况也会创建新的层叠上下文:
- 元素设置了
position 为 relative / absolute / fixed / sticky,且 z-index 不为 auto;
- 元素设置了
opacity 小于 1;
- 元素设置了
transform、filter、perspective 等不为 none 的值;
- 元素设置了
isolation: isolate;
flex 或 grid 容器的子元素,且 z-index 不为 auto;
will-change 指定了上述任一属性。
这个列表值得记住,因为很多“莫名其妙”的层级问题,都是因为某个父元素无意中创建了层叠上下文。
一个常见的翻车例子
考虑下面这段结构:
<div class="parent">
<div class="child">我是子元素</div>
</div>
<div class="sibling">我是兄弟元素</div>
样式如下:
.parent {
position: relative;
z-index: 0; /* 创建了层叠上下文 */
}
.child {
position: absolute;
z-index: 9999; /* 只在这个上下文里有效 */
}
.sibling {
position: relative;
z-index: 1;
}
很多人以为 .child 的 9999 能压过 .sibling 的 1,
但实际上 .parent 因为 z-index: 0 创建了层叠上下文,
.child 的层级被“锁”在 .parent 内部。
对外比较时,是 .parent(层级 0)和 .sibling(层级 1)在比,
所以 .sibling 反而在上面。
排查思路
遇到层级问题,我现在的习惯是:先在开发者工具里逐层向上检查祖先元素,
看是否有元素意外创建了层叠上下文。找到那个“房间”的边界,
再决定是调整父元素的 z-index,还是干脆换一种布局方式。
层级问题的本质,往往不是 z-index 写小了,而是比较的对象选错了。
理解层叠上下文之后,z-index 就不再是一个可以随便堆数字的属性,
而是一个需要结合 DOM 结构和上下文边界来使用的工具。
从零实现一个极简的静态站点构建脚本
2026.09.22
·
Node.js
·
约 7 分钟阅读
我的笔记一直用 Markdown 写,但每次手动复制粘贴成 HTML 太麻烦。
于是花了两个晚上,用 Node.js 写了一个不到 200 行的构建脚本,够用,也足够简单。
这篇笔记记录一下它的核心思路。
目标
- 读取
content/ 目录下的所有 .md 文件;
- 把 Markdown 转成 HTML;
- 套上统一模板,输出到
dist/;
- 生成一个简单的首页列表。
不引入框架,只用一个 Markdown 解析库,其余全部用 Node 内置模块。
目录结构
site/
├── content/
│ ├── hello.md
│ └── notes.md
├── templates/
│ └── layout.html
├── build.js
└── dist/
读取与解析
先遍历目录,收集所有 Markdown 文件:
const fs = require('fs');
const path = require('path');
const contentDir = path.join(__dirname, 'content');
const files = fs.readdirSync(contentDir)
.filter(f => f.endsWith('.md'));
然后用 marked 把内容转成 HTML。为了拿到标题,我约定每个文件的第一行是 # 标题,
解析时顺手提取出来:
const { marked } = require('marked');
function parsePost(filename) {
const raw = fs.readFileSync(
path.join(contentDir, filename), 'utf8'
);
const titleMatch = raw.match(/^#\s+(.+)$/m);
const title = titleMatch ? titleMatch[1] : filename;
const html = marked.parse(raw);
return { filename, title, html };
}
套用模板
模板里留一个占位符,构建时替换掉:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
</head>
<body>
<main>{{content}}</main>
</body>
</html>
替换逻辑只有一行:
const layout = fs.readFileSync(
path.join(__dirname, 'templates/layout.html'), 'utf8'
);
function render(post) {
return layout
.replace('{{title}}', post.title)
.replace('{{content}}', post.html);
}
输出与首页
最后把每篇文章写进 dist/,同时拼一个首页列表。
文件名直接用原文件名替换扩展名,简单可靠:
const distDir = path.join(__dirname, 'dist');
if (!fs.existsSync(distDir)) fs.mkdirSync(distDir);
const posts = files.map(parsePost);
posts.forEach(post => {
const outName = post.filename.replace(/\.md$/, '.html');
fs.writeFileSync(
path.join(distDir, outName),
render(post)
);
});
const listHtml = posts
.map(p => `<li><a href="${p.filename.replace(/\.md$/, '.html')}">${p.title}</a></li>`)
.join('\n');
fs.writeFileSync(
path.join(distDir, 'index.html'),
`<ul>${listHtml}</ul>`
);
一点体会
这个脚本没有任何缓存、增量构建或热更新,每次全量重建也就几十毫秒。
对于个人笔记这种规模,简单比完备更重要。
真正需要优化的时候再加,而不是一开始就设计一套复杂的流水线。
阅读笔记:关于“注意力”的几种技术解释
2026.08.05
·
阅读
·
约 5 分钟阅读
最近读了几篇讨论“注意力”的文章,角度各不相同:有认知心理学的,有人机交互的,
也有从工程系统借喻的。把它们放在一起看,能拼出一个挺有意思的图景。
资源模型:注意力是有限的池子
最经典的解释把注意力看作一种有限资源。人同时能处理的信息量有上限,
任务越难,占用的资源越多;资源耗尽,表现就会明显下降。
这个模型解释了很多日常现象:为什么边打电话边开车很危险,
为什么连续开会之后很难再写代码。它给出的建议也很直接——减少同时进行的任务。
过滤器模型:注意力是一道筛子
另一种解释强调选择:外界信息远多于我们能处理的量,注意力负责筛掉不相关的部分。
有意思的是,被筛掉的信息并非完全消失,只是没有被“加工”。
你听见了,但没有在听。这两件事在机制上是不同的。
这解释了“鸡尾酒会效应”:在嘈杂环境里,你依然能立刻注意到有人叫你的名字。
筛子会放行某些高优先级信号,哪怕你并没有主动关注。
工程视角:注意力是一种上下文切换成本
我更喜欢的一个类比来自工程系统。把人的工作记忆想成一个很小的缓存,
每次切换任务,都要把旧上下文换出、新上下文换入。这个换入换出本身是要花时间的。
所以“被打断”的代价,往往不是打断的那几分钟,而是重新进入状态所需的十几分钟。
这也解释了为什么深度工作需要一整块不被打扰的时间,而不是碎片时间的简单相加。
三种解释的关系
资源模型讲的是容量,过滤器模型讲的是选择,
工程视角讲的是切换成本。它们并不互斥,更像是同一件事的三个切面。
- 容量有限,所以要学会取舍;
- 筛选机制会自动放行高优先级信号,所以要管理好环境里的“信号”;
- 切换有成本,所以要保护整块时间。
对我自己的启发
读完这些之后,我做了两个很小的改变:一是把手机通知全部关掉,只保留电话;
二是每天上午留出两小时不安排任何会议,专门用来做需要连续思考的事情。
效果谈不上立竿见影,但那种“一天下来什么也没做成”的感觉确实少了很多。
也许注意力和肌肉一样,重要的不是某一刻有多用力,而是有没有给它恢复和集中的节奏。
用 Python 批量整理照片文件的一次尝试
2026.07.18
·
Python
·
约 6 分钟阅读
手机相册里积累了几千张照片,文件名全是 IMG_20240718_153012.jpg 这种格式,
想按日期归档却无从下手。于是写了个小脚本,按拍摄日期把照片分到不同月份的文件夹里。
思路
- 遍历指定目录下的所有图片文件;
- 读取 EXIF 中的拍摄时间;
- 如果 EXIF 缺失,退回到文件修改时间;
- 按
年-月 创建目录并移动文件;
- 遇到重名文件时自动加序号,避免覆盖。
读取 EXIF
用 Pillow 读取拍摄时间,这一步是核心:
from PIL import Image
from PIL.ExifTags import TAGS
from datetime import datetime
def get_shot_time(path):
try:
img = Image.open(path)
exif = img._getexif()
if not exif:
return None
for tag_id, value in exif.items():
tag = TAGS.get(tag_id, tag_id)
if tag == 'DateTimeOriginal':
return datetime.strptime(
value, '%Y:%m:%d %H:%M:%S'
)
except Exception:
return None
return None
这里用 _getexif() 是个私有方法,但在实际使用中足够稳定。
如果照片没有 EXIF,函数返回 None,交给调用方兜底。
兜底:文件修改时间
很多截图或经过处理的图片没有拍摄时间,这时用文件修改时间做个近似:
import os
def fallback_time(path):
ts = os.path.getmtime(path)
return datetime.fromtimestamp(ts)
移动与重名处理
按年月建目录,重名时在文件名后面加序号:
import shutil
from pathlib import Path
def move_photo(src, base_dir, shot_time):
folder = base_dir / shot_time.strftime('%Y-%m')
folder.mkdir(parents=True, exist_ok=True)
target = folder / Path(src).name
counter = 1
while target.exists():
stem = Path(src).stem
suffix = Path(src).suffix
target = folder / f'{stem}_{counter}{suffix}'
counter += 1
shutil.move(str(src), str(target))
主流程
def organize(source_dir, base_dir):
exts = {'.jpg', '.jpeg', '.png', '.heic'}
source = Path(source_dir)
for file in source.iterdir():
if file.suffix.lower() not in exts:
continue
shot = get_shot_time(file) or fallback_time(file)
move_photo(file, base_dir, shot)
print(f'已归档: {file.name} -> {shot:%Y-%m}')
运行前的小心
这类会移动文件的脚本,务必先备份,或者先在小范围目录里试运行。
我自己的做法是先把 move_photo 里的 shutil.move 换成 print,
确认分类逻辑没问题之后再真正执行。
自动化脚本的第一原则:先在只读模式下跑通,再让它动手。
结果
几千张照片,跑完不到一分钟,全部按月份归好了类。
脚本本身没什么技术含量,但它解决的是一个真实存在、且每次整理相册都会遇到的麻烦。
这种“小而具体”的工具,往往比宏大的项目更让人有成就感。