AI 摘要 基于文章说明生成

一次真实的头像发布翻车记录:从 npm 包整理、两步验证到 unpkg 主源与 jsDelivr 备用源,顺手整理成可复用的固定图床流程。

2,418 字 9 分钟

我只是想换个头像,怎么最后折腾出了一套 npm 图床?

2026年9月14日 网站部署
海边风景与开阔天空

我原本只想给博客换个头像。

图片已经准备好了,Edge 里也明明登录着 npm。按我的朴素理解,这件事应该和把新头像塞进微信差不多:选图、确认、结束,最多再欣赏两秒自己的审美。

结果我在终端里敲下:

npm whoami

npm 很冷静地回了一个 ENEEDAUTH

那一刻我才发现,浏览器和命令行虽然住在同一台电脑里,关系却像合租室友:门对门住了半年,谁也不认识谁。更麻烦的是,登录还只是第一道门,真正拦住发布的东西藏在后面。

为什么一个头像要绕到 npm

我之前就有一个放静态资源的 npm 项目,这次只是把站点头像换进去,再发布一个新版本。看起来有点绕,但头像、Logo、友链图标这类文件有几个共同点:

  • 文件小,公开也没关系;
  • 会被多个页面反复使用;
  • 很少更新,但更新后希望旧版本还能找回来;
  • 不值得单独搭一套图床后台。

npm 包刚好有版本号,unpkg、jsDelivr 又能直接读取公开包里的文件。对我这种喜欢折腾、又不想为了一个头像再养一套服务的人来说,确实挺合适。

不过先把话说在前面:npm 不是无限容量免费图床。它更适合少量固定资源,不适合相册、用户上传、私密文件和大视频。真把它当网盘猛塞,省下来的那点钱,可能最后会换一种方式找回来。

第一步:先把包收拾干净

如果从零开始,可以先建一个独立目录:

mkdir your-static-assets
cd your-static-assets
npm init -y

我习惯只把最终要公开的文件放进 dist

your-static-assets/
├─ dist/
│  └─ img/
│     ├─ avatar.jpg
│     └─ logo.webp
└─ package.json

然后把 package.json 控制在够用的范围:

{
  "name": "your-unique-static-assets",
  "version": "1.0.0",
  "description": "Public static assets for a personal website",
  "license": "MIT",
  "files": [
    "dist"
  ]
}

这里最值得盯住的是 files。它相当于发布白名单,只让 dist 进入 npm 包。

我不太怕发布失败,失败了还能重来;我更怕它成功得过于顺滑,顺手把 .npmrc、环境变量、日志和备份一起送上公网。那种成功,大概属于“手术很顺利,病人走错了”这一类。

正式发布前先跑:

npm pack --dry-run

输出里的 Tarball Contents 就是即将公开的文件。我这次确认里面只有头像、两个原有静态资源和 package.json,才继续往下走。

这一步不要靠感觉。凡是看到下面这些东西,先停:

  • .npmrc.env 和其他账号配置;
  • 令牌、Cookie、验证码、两步验证恢复码;
  • 不准备公开的原图、设计稿、备份和日志;
  • 与这批静态资源没有关系的文件。

图片本身也最好先压缩并清理 EXIF。头像不需要保留相机原始尺寸,更没必要把拍摄位置和设备信息夹带上网。

第二步:浏览器登录,不代表终端登录

回到开头那个 ENEEDAUTH。解决方法并不复杂,使用网页授权重新登录 CLI:

npm login --auth-type=web

终端会给出 npm 官方的一次性地址,按 Enter 后在浏览器里确认。完成后再检查:

npm whoami

能返回自己的用户名,才说明 CLI 真正认识你了。

我当时看到用户名出现,以为这回总算稳了,于是执行:

npm publish --access public

然后 npm 又把门关上了,错误是 E403:发布需要两步验证,或者具备相应权限的细粒度令牌。

这件事后面还有一个小反转。我启用了“授权与发布”两步验证,马上重试,结果还是同一个 E403。从后续重新登录就能正常继续来看,更像是启用两步验证前签发的 CLI 令牌没有同步获得新的发布状态,而不是设置本身没生效。

我最后注销旧会话,再重新走一次网页登录:

npm logout
npm login --auth-type=web

如果只是普通手动发布,我更愿意走网页验证,不在命令、聊天或截图里传令牌。自动化发布可以用最小权限、有限有效期的令牌或可信发布,但那是另一套安全账,不能为了省两次点击就随便欠着。

第三步:网络又来插了一脚

旧令牌处理完,我以为这次终于只剩按按钮。偏偏登录接口连续出现了两次 ECONNRESET

registry 不是完全不通,npm ping 还能返回,只是慢得像在思考人生。这个时候我没有改代理,也没有乱换 registry,而是给官方源多一点重试和等待时间:

npm login --auth-type=web `
  --fetch-retries=5 `
  --fetch-retry-mintimeout=1000 `
  --fetch-retry-maxtimeout=10000 `
  --fetch-timeout=120000

这次网页授权正常回调。重新发布时,npm 又给出一个发布专用验证页;在浏览器里完成两步验证后,终端终于出现了那个朴素的加号:包发布成功。

所以遇到连接重置时,先分清是账号拒绝还是链路抖动:

  • ENEEDAUTH 通常是 CLI 没登录;
  • E403 要看权限、两步验证和令牌状态;
  • ECONNRESET 更像连接被中途断开,可以先检查 npm ping 再有限重试。

错误长得都不太友好,但病因不是一回事。把它们混着治,很容易从换头像一路折腾到换系统。

第四步:发布成功还不算结束

发布后先确认 registry 已经看到新版本:

npm view your-unique-static-assets version

然后用固定版本拼出资源地址。假设版本是 1.0.0,文件路径是 dist/img/avatar.jpg,主链接使用 unpkg:

https://unpkg.com/your-unique-static-assets@1.0.0/dist/img/avatar.jpg

备用链接使用 jsDelivr:

https://cdn.jsdelivr.net/npm/your-unique-static-assets@1.0.0/dist/img/avatar.jpg

注意 jsDelivr 的路径里多了一段 /npm/。两个地址都固定到同一版本,不用 latest,也不省略版本号。

我一开始用的是另一个 npm CDN 域名。新版本返回 404 时,我还以为只是同步慢;顺手检查旧版本,居然也是 404。那一刻答案已经很明显了:不是我的新包没传上去,是这条分发链路当前就靠不住。

后来我分别检查 unpkg 和 jsDelivr,两边都返回 200,文件大小和 SHA-256 也一致。于是最终顺序定为:

unpkg 主源 -> jsDelivr 备用源

这里容易漏掉一件事:配置里保存两个 URL,并不等于浏览器会自动回退。真正的备用源还要靠错误监听触发。

第五步:让备用地址真的能接班

HTML 可以这样写:

<img
  src="https://unpkg.com/your-unique-static-assets@1.0.0/dist/img/avatar.jpg"
  data-fallback-src="https://cdn.jsdelivr.net/npm/your-unique-static-assets@1.0.0/dist/img/avatar.jpg"
  width="96"
  height="96"
  alt="站点头像"
  decoding="async"
/>

再在公共布局中统一监听加载失败:

<script type="module">
  document.querySelectorAll('img[data-fallback-src]').forEach((image) => {
    image.addEventListener('error', () => {
      const fallback = image.dataset.fallbackSrc;
      if (fallback) image.src = fallback;
    }, { once: true });
  });
</script>

once: true 是为了避免备用源也失败时反复横跳。这个回退只处理明确的加载错误;如果主源一直卡着不报错,浏览器不会立刻换路。要求严格的场景还得加超时控制,不过对于头像这种非核心资源,我暂时不想把它写成一套小型容灾系统。

Astro 项目里可以把两个地址放到同一份配置:

export const siteAvatar =
  'https://unpkg.com/your-unique-static-assets@1.0.0/dist/img/avatar.jpg';

export const siteAvatarFallback =
  'https://cdn.jsdelivr.net/npm/your-unique-static-assets@1.0.0/dist/img/avatar.jpg';
<img
  src={siteAvatar}
  data-fallback-src={siteAvatarFallback}
  alt="站点头像"
/>

我把这个回退补到了友链列表、申请友链卡片和首页友链预览。因为只改配置、不检查真实渲染位置,最后往往会得到一种很熟悉的效果:代码看起来支持,页面实际上随缘。

以后换头像,不能覆盖原版本

npm 已发布的版本不能原地覆盖。下一次替换图片,要先增加版本:

npm version patch --no-git-tag-version
npm pack --dry-run
npm publish --access public

例如 1.0.0 会变成 1.0.1。发布完成后,主链接和备用链接必须一起更新:

https://unpkg.com/your-unique-static-assets@1.0.1/dist/img/avatar.jpg
https://cdn.jsdelivr.net/npm/your-unique-static-assets@1.0.1/dist/img/avatar.jpg

只改一个地址,会让故障回退时重新看见旧头像。这个问题平时藏得很好,通常要等主 CDN 真出故障才突然出来打招呼。

固定版本还有一个好处:版本号变化就是新的缓存键,不需要赌各个节点什么时候刷新 latest。新版本有问题时,把站点地址改回上一个版本,也比试图覆盖远端文件简单。

我现在会做的发布检查

折腾完这一轮,我把流程缩成了九步:

  1. 替换并压缩图片,清理不需要的元数据。
  2. 使用 npm version patch --no-git-tag-version 增加版本。
  3. 运行 npm pack --dry-run,逐项核对发布内容。
  4. npm whoami 确认 CLI 登录身份。
  5. 确认两步验证可用,再执行 npm publish --access public
  6. 使用 npm view 检查 registry 中的新版本。
  7. 分别请求 unpkg 和 jsDelivr,检查状态码与文件类型。
  8. 比较本地文件和两个 CDN 文件的 SHA-256。
  9. 同时更新主、备用地址,完成站点构建后再上线。

如果站点配置了 CSP,还要把 https://unpkg.comhttps://cdn.jsdelivr.net 加到 img-src。普通 <img> 展示一般不用额外处理 CORS;但如果要把图片画进 Canvas 后读取像素,就得另外确认 CDN 的 CORS 响应。

最后再提醒一次:公共 npm 包里的内容谁都能下载,历史版本也不能当保险箱。原图要单独备份,令牌不要进前端和仓库,包里只放愿意长期公开的文件。

头像换好了,但这事还没完全结束

现在这张头像已经能从 unpkg 加载,失败时也会转去 jsDelivr。回头看,真正花时间的不是上传图片,而是补齐身份验证、版本管理和失败回退这些不起眼的小环节。

手动发布目前挺稳,也让我每次都能看一眼包里到底有什么;但以后资源多了,是继续手动,还是接一套自动发布?自动化当然省事,可一旦引入令牌、权限和 CI,新的坑已经在远处探头了。

这个选择我还没定。如果你也拿 npm 放过头像、字体或博客静态资源,评论区可以说说你最后是继续用,还是某次翻车后搬了家。也许下一次折腾,就从这些答案里开始。

SUPPORT & SHARE

支持与分享

觉得这篇文章有点用,就把它递给下一位读者。

打赏
我只是想换个头像,怎么最后折腾出了一套 npm 图床?https://blog.demius.tech/posts/npm-static-image-hosting-guide/
作者
Demius
发布于
2026年9月14日
许可协议
CC BY-NC-SA 4.0
SMART PICKS
按分类与标签整理
01Artalk自托管评论系统的搭建和使用网站部署 · 10月6日02无限容量的开源免费图床部署方案自由呐 · 3月16日03Hello World自由 · 10月6日04图床Demius主题 · 8月6日
SERENDIPITY

随便看看

每次来都重新洗牌
01收藏夹终于不用放养了:我做了个 Demius导航扩展开发 · 9月16日02新建了一个论坛方便交流讨论论坛 · 9月5日03粉丝专属福利|¥3/月拿下 CN2 GIA 线路云服务器福利 · 7月30日04薅到了!一个不关机、不限时的免费2核4G云服务器,一个账号领2台未分类 · 7月14日05Demigod主题,一款沉浸式视觉体验Astro主题未分类 · 7月14日
SHARE POST

把这篇文章带走

打开后生成分享海报

评论区

分享你的想法,与大家交流讨论。

评论区将在接近时加载

滚轮或双击缩放 · 拖动查看细节 · 方向键切换 · 下拉关闭