修复: 回到顶部按钮和进度条不显示的bug
This commit is contained in:
@@ -48,6 +48,7 @@ pnpm dev
|
||||
| `pnpm build` | 构建到 `dist/`,并跑 Pagefind 建立搜索索引 |
|
||||
| `pnpm preview` | 本地预览构建产物 |
|
||||
| `pnpm check` | Astro 的类型与内容校验(本项目没有单元测试框架,这就是唯一的自动检查) |
|
||||
| `pnpm verify:css` | 单独跑构建产物 CSS 自检(`pnpm build` 和 `deploy.sh` 里都会自动跑) |
|
||||
| `pnpm new-thought "..."` | 快速创建一条「偶得」(见下) |
|
||||
|
||||
## 项目结构
|
||||
@@ -97,6 +98,27 @@ onPage("my-widget", () => {
|
||||
页面级的小交互如果**不需要 JS** 就别写 JS:阅读进度条和回到顶部按钮的显隐都由
|
||||
CSS 滚动驱动动画(`animation-timeline: scroll()`)负责,脚本只管点击与老浏览器降级。
|
||||
|
||||
### ⚠️ 滚动驱动动画的两条硬规矩
|
||||
|
||||
2026-09-21 踩过一次:线上按钮不显示、本地 dev 一切正常。原因是 Lightning CSS
|
||||
(Tailwind v4 的压缩器)把 `animation-timeline` 并进了 `animation` 简写,产出
|
||||
`animation: linear both back-to-top-reveal scroll(root)` —— 而 WebKit 不认简写里的
|
||||
timeline 分量([webkit#293276](https://wiki.webkit.org/show_bug.cgi?id=293276)),
|
||||
**整条声明被判无效丢弃**,元素永远停在初始的隐藏样式;更坑的是
|
||||
`CSS.supports("animation-timeline: scroll()")` 照样返回 true,运行时特性检测看不出问题
|
||||
([lightningcss#815](https://github.com/parcel-bundler/lightningcss/issues/815))。因此:
|
||||
|
||||
1. **写 CSS 时**:`animation-timeline` / `animation-range` 那条规则的选择器要和
|
||||
`animation` 简写那条**不同**(现有代码用 `.reading-progress > .reading-progress-bar`
|
||||
和 `.back-to-top[data-back-to-top]`),否则压缩器会把两条相邻的同选择器规则合并。
|
||||
刻意用后代 / 属性选择器而不是 `:where()`,是为了让特异性不低于简写那条 ——
|
||||
`animation` 简写会把 `animation-timeline` 重置回 `auto`。
|
||||
2. **写 JS 时**:不要用 `CSS.supports()` 判断滚动动画是否可用,直接读
|
||||
`getComputedStyle(el).animationName` —— 只有这个才能反映「规则到底有没有活下来」。
|
||||
`src/scripts/back-to-top.ts` 就是这么做的,CSS 那边一旦失效它会自动接管。
|
||||
|
||||
`pnpm build` 和 `deploy.sh` 都会跑 `scripts/verify-build-css.mjs` 拦截这类回退。
|
||||
|
||||
## 写作
|
||||
|
||||
内容集合定义在 `src/content/config.ts`,共三个:
|
||||
|
||||
@@ -84,6 +84,15 @@ if [ "$PAGES" -lt 5 ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# CSS 自检:拦「滚动驱动动画被压缩器并进 animation 简写」这类只在构建产物里出现、
|
||||
# 本地 dev 完全看不出来的回归(2026-09-21 的回到顶部按钮事故)。
|
||||
# pnpm build 里已经跑过一次,这里再跑是为了覆盖 --skip-build 直接用旧 dist/ 的情况。
|
||||
if ! node scripts/verify-build-css.mjs; then
|
||||
echo ""
|
||||
echo "❌ CSS 自检未通过,已中止部署(没有推送任何内容)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------- 3. 生产环境确认 ----------
|
||||
if [ "$DO_CLOUD" -eq 1 ] && [ "$DRY_RUN" -eq 0 ] && [ "$ASSUME_YES" -eq 0 ]; then
|
||||
echo ""
|
||||
|
||||
+2
-1
@@ -6,9 +6,10 @@
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "node scripts/fetch-reading.mjs --soft && astro build && pagefind --site dist",
|
||||
"build": "node scripts/fetch-reading.mjs --soft && astro build && node scripts/verify-build-css.mjs && pagefind --site dist",
|
||||
"preview": "astro preview",
|
||||
"check": "astro check",
|
||||
"verify:css": "node scripts/verify-build-css.mjs",
|
||||
"new-post": "node scripts/new-post.mjs",
|
||||
"new-thought": "node scripts/new-thought.mjs",
|
||||
"reading": "node scripts/fetch-reading.mjs"
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/* 构建产物 CSS 自检:拦住「滚动驱动动画被压缩器并进 animation 简写」这一类静默回归。
|
||||
*
|
||||
* 背景(2026-09-21 的实际事故):
|
||||
* 线上「回到顶部」按钮不显示,本地 dev 却正常。
|
||||
* 原因是 Lightning CSS(Tailwind v4 的 CSS 压缩器)把
|
||||
* animation: back-to-top-reveal linear both;
|
||||
* animation-timeline: scroll(root block);
|
||||
* 合并成了
|
||||
* animation: linear both back-to-top-reveal scroll(root);
|
||||
* 而 WebKit 不接受 animation 简写里的 <single-animation-timeline> 分量
|
||||
* (webkit#293276),于是**整条 animation 声明被判无效丢弃**,元素永远停在
|
||||
* 初始的 opacity:0 / visibility:hidden。
|
||||
*
|
||||
* 最要命的是它只在构建产物里出现:astro dev 不做这个压缩,所以本地怎么试都正常;
|
||||
* 而 CSS.supports("animation-timeline: scroll()") 又照样返回 true,
|
||||
* 运行时特性检测也发现不了。见 lightningcss#815。
|
||||
*
|
||||
* 这个脚本在 build 之后跑,扫 dist 下所有 CSS:只要 animation 简写的值里出现
|
||||
* scroll() / view() 时间线函数,就让构建失败。
|
||||
*
|
||||
* 用法:
|
||||
* node scripts/verify-build-css.mjs # 失败时退出码 1
|
||||
* node scripts/verify-build-css.mjs --warn # 只警告,不失败
|
||||
*/
|
||||
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
const DIST = "./dist"
|
||||
const WARN_ONLY = process.argv.includes("--warn")
|
||||
|
||||
/** animation 简写里不该出现的时间线函数 */
|
||||
const TIMELINE_FN = /(?:^|[^-\w])(?:scroll|view)\(/
|
||||
|
||||
/** 收集 dist 下所有 css 文件 */
|
||||
function collectCssFiles(dir) {
|
||||
if (!fs.existsSync(dir)) return []
|
||||
const found = []
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) found.push(...collectCssFiles(full))
|
||||
else if (entry.name.endsWith(".css")) found.push(full)
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
/**
|
||||
* 只检查 `animation:` 简写(以及 -webkit-animation:),不碰
|
||||
* `animation-timeline:` / `animation-range:` 这两个长写属性 —— 后者是正确写法。
|
||||
*/
|
||||
function findShorthandViolations(css) {
|
||||
const hits = []
|
||||
// 负向回顾避免匹配到 animation-timeline / animation-name 这类长写属性
|
||||
const re = /(?:^|[;{}(])\s*(?:-webkit-)?animation\s*:\s*([^;}]*)/g
|
||||
let match
|
||||
while ((match = re.exec(css)) !== null) {
|
||||
const value = match[1]
|
||||
if (TIMELINE_FN.test(value)) {
|
||||
hits.push(value.trim().slice(0, 120))
|
||||
}
|
||||
}
|
||||
return hits
|
||||
}
|
||||
|
||||
const files = collectCssFiles(DIST)
|
||||
if (files.length === 0) {
|
||||
console.error(`❌ ${DIST} 下没有找到 CSS,构建产物异常`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const problems = []
|
||||
for (const file of files) {
|
||||
const css = fs.readFileSync(file, "utf-8")
|
||||
for (const value of findShorthandViolations(css)) {
|
||||
problems.push({ file, value })
|
||||
}
|
||||
}
|
||||
|
||||
if (problems.length === 0) {
|
||||
console.log(`✅ 构建产物 CSS 自检通过(${files.length} 个文件,没有时间线函数混进 animation 简写)`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
console.error("❌ 构建产物 CSS 自检失败:animation 简写里混进了时间线函数。")
|
||||
console.error("")
|
||||
for (const { file, value } of problems) {
|
||||
console.error(` ${file}`)
|
||||
console.error(` animation: ${value}`)
|
||||
}
|
||||
console.error("")
|
||||
console.error(" 这类声明会被 WebKit 整条丢弃(webkit#293276),元素会停在初始样式上。")
|
||||
console.error(" 修法:把 animation-timeline / animation-range 挪到**独立的规则块**里,")
|
||||
console.error(" 不要和 animation 简写放在同一组声明中(lightningcss#815)。")
|
||||
console.error("")
|
||||
|
||||
process.exit(WARN_ONLY ? 0 : 1)
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"updatedAt": "2026-09-21T01:32:16.851Z",
|
||||
"updatedAt": "2026-09-21T01:46:31.327Z",
|
||||
"source": "weread",
|
||||
"books": [
|
||||
{
|
||||
|
||||
@@ -7,16 +7,31 @@ import { onPage } from "./lifecycle"
|
||||
* 淡入,不需要任何 scroll 监听(见 global.css 的 .back-to-top)。
|
||||
* 这里只负责两件事:
|
||||
* 1. 点击后平滑回到顶部;
|
||||
* 2. 给不支持滚动驱动动画的浏览器(Firefox < 144 / Safari < 26)做降级。
|
||||
* 2. 兜住「CSS 那条动画没生效」的所有情况 —— 既包括不支持滚动驱动动画的浏览器
|
||||
* (Firefox < 144 / Safari < 26),也包括规则被构建压缩弄坏的情况(见下)。
|
||||
*/
|
||||
|
||||
/** 与 global.css 里 animation-range 的起点保持一致 */
|
||||
/** 与 global.css 里 animation-range 的起点呼应 */
|
||||
const REVEAL_AFTER_PX = 480
|
||||
|
||||
const supportsScrollTimeline = () =>
|
||||
typeof CSS !== "undefined" &&
|
||||
typeof CSS.supports === "function" &&
|
||||
CSS.supports("animation-timeline: scroll()")
|
||||
/** global.css 中 @keyframes 的名字,用来实测动画有没有真的挂上 */
|
||||
const ANIMATION_NAME = "back-to-top-reveal"
|
||||
|
||||
/**
|
||||
* 「CSS 那条滚动驱动动画到底有没有生效」——直接读计算样式,而不是做特性检测。
|
||||
*
|
||||
* 为什么不用 CSS.supports("animation-timeline: scroll()"):它只回答「这个语法浏览器认不认」,
|
||||
* 回答不了「这条规则有没有在构建压缩中活下来」。实际的坑是:Lightning CSS 会把
|
||||
* animation-timeline 合并进 animation 简写,而 WebKit 不认简写里的 timeline 分量,
|
||||
* 整条声明被丢弃 —— 此时 CSS.supports 仍然返回 true,于是降级分支永远不执行,
|
||||
* 按钮就永远停在 opacity:0。改读 animationName 之后,CSS 那边无论因为什么原因没生效,
|
||||
* 这里都能立刻发现并接管(详见 global.css 里 .back-to-top 的注释)。
|
||||
*/
|
||||
const cssDrivesVisibility = (button: HTMLElement) =>
|
||||
window
|
||||
.getComputedStyle(button)
|
||||
.animationName.split(",")
|
||||
.some((part) => part.trim() === ANIMATION_NAME)
|
||||
|
||||
onPage("back-to-top", () => {
|
||||
const button = document.querySelector<HTMLElement>("[data-back-to-top]")
|
||||
@@ -28,7 +43,7 @@ onPage("back-to-top", () => {
|
||||
}
|
||||
button.addEventListener("click", onClick)
|
||||
|
||||
if (supportsScrollTimeline()) {
|
||||
if (cssDrivesVisibility(button)) {
|
||||
return () => button.removeEventListener("click", onClick)
|
||||
}
|
||||
|
||||
|
||||
+33
-1
@@ -1142,6 +1142,18 @@ a.card-github:hover .github-logo {
|
||||
@supports (animation-timeline: scroll()) {
|
||||
.reading-progress-bar {
|
||||
animation: reading-progress-grow linear both;
|
||||
}
|
||||
/*
|
||||
* ⚠️ 这条规则的选择器故意比上面多一层 `.reading-progress >`,不是笔误。
|
||||
*
|
||||
* Lightning CSS 会合并「相邻且选择器完全相同」的规则:把 animation-timeline 并进
|
||||
* 上面那条的 animation 简写,产物变成 `animation: … reading-progress-grow scroll(root)`,
|
||||
* 而 WebKit 不认简写里的 timeline 分量,整条声明被丢弃 → 进度条永远 scaleX(0)。
|
||||
* 选择器不同就没法合并。这里用后代选择器而不是 `:where()`,是为了让特异性**不低于**
|
||||
* 上面那条 —— animation 简写会把 animation-timeline 重置回 auto,特异性更低会被重置压掉。
|
||||
* 完整背景见下面 .back-to-top 段的长注释,以及 scripts/verify-build-css.mjs。
|
||||
*/
|
||||
.reading-progress > .reading-progress-bar {
|
||||
animation-timeline: scroll(root block);
|
||||
}
|
||||
}
|
||||
@@ -1229,8 +1241,28 @@ a.card-github:hover .github-logo {
|
||||
/* 显隐交给滚动时间线,这里只留下颜色类过渡 */
|
||||
transition: color 160ms ease, background-color 160ms ease, border-color 160ms ease;
|
||||
animation: back-to-top-reveal linear both;
|
||||
}
|
||||
/*
|
||||
* ⚠️⚠️ animation-timeline / animation-range 必须写在**独立的规则块**里,
|
||||
* 不能和上面的 animation 简写放在同一组声明中。
|
||||
*
|
||||
* Lightning CSS(Tailwind v4 用的 CSS 压缩器)会把同一组声明里的 animation-timeline
|
||||
* 合并进 animation 简写,构建产物变成:
|
||||
* animation: linear both back-to-top-reveal scroll(root)
|
||||
* 而 WebKit 目前不接受简写里的 <single-animation-timeline> 分量(webkit#293276),
|
||||
* 于是**整条 animation 声明被判无效并丢弃** —— 按钮永远停在 opacity:0 /
|
||||
* visibility:hidden。更坑的是 CSS.supports("animation-timeline: scroll()") 依然返回
|
||||
* true,纯特性检测完全看不出问题;dev 模式不做这个压缩,所以只有构建产物会踩到。
|
||||
* 参考 lightningcss#815。scripts/verify-build-css.mjs 会在构建后拦截这种合并回退。
|
||||
*
|
||||
* ⚠️ 所以这条规则的选择器故意写成 `.back-to-top[data-back-to-top]`(多一个属性选择器)
|
||||
* —— 与上面那条选择器不同,压缩器就无法把它们合并。刻意用属性选择器而不是 :where(),
|
||||
* 是为了让特异性**高于**上面那条:animation 简写会把 animation-timeline 重置回 auto,
|
||||
* 特异性持平或更低都会被那个重置压掉。
|
||||
*/
|
||||
.back-to-top[data-back-to-top] {
|
||||
animation-timeline: scroll(root block);
|
||||
/* 滚过 320px 开始淡入,620px 完全就位(与 back-to-top.ts 的 480px 阈值呼应) */
|
||||
/* 滚过 320px 开始淡入,620px 完全就位(与 back-to-top.ts 的降级阈值呼应) */
|
||||
animation-range: 320px 620px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user