一次 sourcemap 配置失误如何让 200+ TS 文件全球可下载
关键词:App Store、sourcemap、Svelte、Rollup、代码泄漏、前端工程化
0. TL;DR
- 2025-11-05 苹果上线网页版 App Store,生产环境忘了关 sourcemap
- 任何人打开 DevTools 都能直接读到
.ts/.svelte源码,含变量名 + 注释 - 11-08 苹果 DMCA 批量删除 8 270 个 GitHub 仓库,但备份早已扩散
- 无密钥/后端逻辑泄漏;纯前端“裸奔”,对业务影响有限,但工程细节一次性公开
1. 时间线
| 日期 | 事件 |
|---|---|
| 11-05 14:12 UTC | 苹果将 apps.apple.com 正式切到新版前端 |
| 11-05 20:00 UTC | 国内开发者 rxliuli 发现 .js.map 可下载,整站爬取 |
| 11-06 02:00 UTC | GitHub 仓库 AppStore-Web-Source 创建,24 h 收 1.2 k star |
| 11-07 09:00 UTC | Hacker News 头条,#AppleSourcemap 冲上 Twitter 趋势 |
| 11-08 03:40 UTC | Apple 向 GitHub 发 DMCA,一次性清除 8 270 分叉 |
2. 泄漏范围与文件结构
总大小 38.4 MB,其中源码(不含图片)4.7 MB,目录节选:
src/
├─ lib/
│ ├─ api/ 7 文件 封装 fetch,统一带 `x-apple-auth-js: 1`
│ ├─ components/ 213 文件 PascalCase 命名,.svelte 单文件组件
│ ├─ stores/ 11 文件 轻量状态管理,<200 行
│ └─ utils/ 19 文件 价格/币种/国际化/懒加载
├─ routes/ 16 路由 基于 SvelteKit 的 file-based routing
└─ styles/ 3 文件 PostCSS + Tailwind(前缀 `aps-`)
关键点:零混淆、零压缩变量名,连 // TODO: ask design for 2x asset 这种注释都还在。
3. 技术栈拆解
| 层级 | 选型 | 备注 |
|---|---|---|
| 框架 | Svelte 5(Runes 语法) | 大量 $state()、$derived() 新 API |
| 语言 | TypeScript 5.6 | 严格模式全开,strictNullChecks |
| 构建 | Rollup + Vite 5 | 输出 ES2022,bundle split 按路由 |
| 样式 | Tailwind 3.4 + PostCSS | 类名加 aps- 防止冲突 |
| 路由 | @sveltejs/adapter-static | 全站预渲染,运行时 history.pushState |
| 测试 | Vitest | 仓库含 47 个单测,但未发布 |
4. 源码精读 4 连击
4.1 价格/币种国际化
// lib/utils/formatPrice.ts
export const formatPrice = (
value: number,
locale: string,
currency?: string
) => {
currency ??= locale === 'zh-CN' ? 'CNY' : 'USD';
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
// 保留 Apple 经典 “¥6.00” 格式
minimumFractionDigits: value % 1 === 0 ? 0 : 2,
}).format(value);
};
学习点:利用 Intl 做货币格式化,零依赖;整数去 .00 细节到位。
4.2 虚拟滚动实现(不到 80 行)
// lib/utils/virtualScroll.ts
export function createVirtualScroll(
items: any[],
itemHeight: number,
containerHeight: number
) {
const start = writable(0);
const end = writable(Math.ceil(containerHeight / itemHeight) + 1);
function onScroll(top: number) {
$start = Math.floor(top / itemHeight);
$end = $start + Math.ceil(containerHeight / itemHeight) + 1;
}
const visible = derived([start, end], () =>
items.slice($start, $end)
);
return { visible, onScroll, offsetY: derived(start, (s) => s * itemHeight) };
}
学习点:Svelte 的 derived 当 computed 用,直接绑定到 translateY 做定位,不操作 DOM 节点,性能优于 react-window。
4.3 懒加载 + 代码分割
<!-- routes/app/[id]/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
const HeavyVisual = () => import('$lib/components/AppScreenshots.svelte');
let show = false;
onMount(() => {
const io = new IntersectionObserver(
([e]) => e.isIntersecting && (show = true),
{ rootMargin: '100px' }
);
io.observe(document.querySelector('#screenshots'));
});
</script>
{#if show}
<svelte:component this={HeavyVisual()} />
{/if}
学习点:IntersectionObserver + 动态 import(),首屏减少 42 kB JS。
4.4 请求头“暗号”
// lib/api/base.ts
export const appleFetch = (url: string, init?: RequestInit) =>
fetch(url, {
...init,
credentials: 'include',
headers: {
...init?.headers,
'x-apple-auth-js': '1', // ← 后端凭此返回 JSON 而非 HTML
'x-apple-store-front': get(StorefrontStore),
},
});
学习点:把“我要 JSON” 藏在自定义头里,不依赖 Accept,方便 CDN 区分缓存 key。
5. 泄漏带来的正负面影响
✅ 正面(对社区)
- 真实世界 Svelte 5 大型样例:200+ 组件、10 万行级别
- 无障碍最佳实践:所有按钮
aria-label必国际化,焦点顺序动态计算 - 性能细节:图片 CDN 规则、虚拟滚动、bundle split 策略可直接对标
❌ 负面(对苹果)
- UI 克隆门槛骤降:仿冒 App Store 网页只需“改域名 + 换皮肤”
- 接口字段曝光:虽无密钥,但竞争对手可批量抓包解析排行榜算法
- 品牌声誉:以保密著称的苹果出现“低级配置失误”,打脸 CI/CD 流程
6. 苹果 48 小时止血方案
- 删除源头:CDN purge 全部
.map文件 → 404 - 追加 WAF:不带
x-apple-auth-js: 1的请求直接 302 到官网首页 - 重构流水线:
# 新增强制 lint 规则 --no-sourcemap && echo "PROD_SOURCEMAP=off" >> $GITHUB_ENV - 权限收口:生产构建脚本改为“只读 + 双人审批”,防止再有人
--sourcemap手滑
7. 给开发者的 3 条启示
- sourcemap 即源码,生产上传 ≈ 开源;CI 里强制
sourcemap: false+ 脚本校验。 - 再大厂也会踩坑,Code Review 别只盯业务逻辑,配置项同样要双人双检。
- Svelte 5 已可扛 10 万行项目:Runes 语法简洁、虚拟滚动手写 80 行够用,性能与可维护性兼得,值得在下一期迭代评估。
8. 结语
这次事件对苹果是“黑天鹅”,对前端社区却是罕见的白盒复盘机会:看到世界最高市值公司如何用 Svelte 写大型商业项目,如何组织国际化、无障碍、性能优化。sourcemap 开关虽小,背后却是工程纪律与文化——代码能跑还不够,别让地图把宝藏位置标出来。
愿我们写完每一行
console.log('TODO')时,都记得关掉 sourcemap,再 push。
啃完源码,别忘了补充维生素~朋友家赣南脐橙正当季,现摘现发,需要戳→