Ponytail 新手教程

平台原生优先:最省代码的那一层

梯子的第 4 级单独有一份文档,因为「平台已经做了」是省钱效果最猛的一级。这一章就是那份对照表的中文精选。

省得最多的一级 浏览器 / CSS / Node / SQL platform-native 原文

1为什么这一级省得最多

前三级是「少写」,第 4 级是「整个不用写」。一个手搓的日期选择器组件要包含触发按钮、弹出层、月份网格、键盘导航、无障碍属性、样式表、时区讨论……而浏览器从很多年前就有 <input type="date">。

这就是实测里 -94% 那种数字的来源:不是助手写得更精炼,而是它换掉了整个方案。第 7 章会完整拆一个例子。

提问方式的变化:从「这个组件怎么写」变成「平台是不是本来就有这个」。这一问,往往就跳过了后面几百行。

2浏览器已经有的表单控件

下面每一条左边是「你以为要装的库」,右边是「平台其实自带」。注意这些原生控件还顺带带来了无障碍、移动端原生键盘和系统主题适配——库反而经常做不到。

你以为需要平台其实有
日期选择器库<input type="date">
时间选择器库<input type="time">
颜色选择器库<input type="color">
滑块库<input type="range">
进度条组件<progress value="70" max="100">
仪表盘 / 刻度组件<meter value="0.7">
弹窗 / 模态库<dialog> + dialog.showModal()
折叠面板 / FAQ 组件<details><summary>
搜索下拉框<input list="id"> + <datalist>
自动增高的 textareafield-sizing: content(CSS)
吸顶头部position: sticky; top: 0
<dialog> 和 <details> 尤其值得记住:前者自带焦点陷阱和 Esc 关闭,后者自带键盘与读屏支持。手搓这两个功能的库,通常在这两件事上做得不如浏览器。

3以前要 JS、现在 CSS 就够

以前用 JS 做现在 CSS 能
响应式字号 / 间距clamp(1rem, 2.5vw, 2rem)
深色模式@media (prefers-color-scheme: dark)
减少动效偏好@media (prefers-reduced-motion: reduce)
不用断点的自适应网格repeat(auto-fill, minmax(250px, 1fr))
组件级响应式@container 查询
设计变量 / 换肤CSS 自定义属性 --color-primary
平滑滚动scroll-behavior: smooth
滚动吸附轮播scroll-snap-type: x mandatory
固定宽高比aspect-ratio: 16 / 9
多行文本截断-webkit-line-clamp: 3
父选择器:has(input:checked)

顺带一提:你现在看的这个教程站就是按这套规则做的——折线箭头、渐变卡片、深浅主题全部是 CSS 变量,没有引入任何 UI 框架。

4运行时已经带了,别再 npm install

这一类特别值得看:npm 上周下载量几千万的包,做的事可能只有一行。左列这些包名很多人是条件反射装的。

你以为需要装平台其实有
query-string / qsnew URLSearchParams(location.search)
lodash.clonedeepstructuredClone(obj)
lodash.groupbyObject.groupBy(arr, fn)
uuid(v4)crypto.randomUUID()
numeral / accountingIntl.NumberFormat 带 style:"currency"
date-fns 的格式化Intl.DateTimeFormat
date-fns 的相对时间Intl.RelativeTimeFormat
clipboard.jsnavigator.clipboard.writeText()
无限滚动库new IntersectionObserver(cb)
is-online 之类联网检测navigator.onLine + online/offline 事件
分享面板库navigator.share({ title, text, url })
fetch 超时封装AbortSignal.timeout(5000)

原文对 debounce 的处理很有意思:它没有说「用原生 API」,因为原生确实没有 debounce。它的答案是承认这件事,然后给三行代码:

// ponytail: 3 lines beats a dependency
let t;
const debounce = (fn, ms) => (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
这段注释本身就是项目风格:知道自己在哪里抄了近路,就写清楚代价。「3 行胜过 1 个依赖」是判断,「ponytail:」是标记。

5服务端与数据库:同样的思路

原则不只在浏览器里成立。下面三张表是原文里 Node / Python / SQL 三节的精选。

层你以为要写 / 要装平台其实有
Nodemkdirp / make-dirfs.mkdirSync(p, { recursive: true })
Noderimraffs.rmSync(p, { recursive: true, force: true })
Nodepath-exists / load-json-filefs.existsSync / JSON.parse(fs.readFileSync(p,"utf8"))
Pythonpytzzoneinfo.ZoneInfo("America/New_York")(3.9+)
Pythonpython-dateutil(简单解析)datetime.fromisoformat()(3.7+)
Pythonattrs(简单数据类)@dataclass
Pythonsix / pathlib2 / enum34Python 2 已经没了;pathlib / enum 都在标准库
SQL应用层算分页、跑排名LIMIT / OFFSET、窗口函数 RANK() OVER (PARTITION BY …)
SQL应用层查重、查引用完整性UNIQUE / FOREIGN KEY / CHECK (price > 0)
SQL应用层做去重、软删过滤SELECT DISTINCT / ON CONFLICT DO NOTHING / 生成列 + 部分索引
数据库那几行是最容易被违反的:应用层写一堆校验,却把 UNIQUE 约束留在外面——并发下必然漏。约束放在数据能保证它的地方,才是真正的小 diff。

6包装层的宿命

原文用四行讲完了整个论证,这四行值得抄下来贴在显示器边上:

Platform team spends years solving the problem.
Package author wraps it.
You install the wrapper.
The wrapper goes unmaintained.
You debug the wrapper.

翻译过来:平台团队花几年把问题解决掉,包作者包了一层,你装了这层包装,包装没人维护了,于是你在调试包装。跳过包装。平台是跟着你的 App 一起免费发的。

但要读全——原文紧接着写:当原生方案真的不够用时(要支持旧浏览器、有它处理不了的边界、规模化后在易用性上有实质差别),库才配得上它的位置。那时候再装,不是之前。

7怎么把这一级变成习惯

  1. 写之前先问:浏览器 / 系统 / 数据库本身有没有?搜索词要带 MDN 或 native,而不是带包名
  2. 找到了就先用原生版跑通,再判断够不够。通常够
  3. 不够用时再装库,并且在回复里写清楚为什么原生不够(这正好符合 Ponytail 的「三行说明」契约)
  4. 已经装了 Ponytail 的情况下,你其实不用自己记这些表——助手会被规则逼着先去查第 4 级。这也是它为什么能砍掉 94% 行数的真实原因