笔记
0工具
0此页用于记录用户反馈问题后的每一次改进
关于
“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
不想公开、只想临时给人看:点“分享”生成一条带密码和有效期的链接,到期自动失效,你也能随时撤销。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
趋势
如何在 Next.js 16 中展示 S3 图片(2026 实战指南)
在 Next.js 里把图片从 S3 捞出来展示给用户,这事儿看着简单,实际踩坑的人不少。文件上传到 S3 只是第一步,真正头疼的是怎么在页面上把它显示出来——头像、相册、商品卡片,每个场景都有自己的坑。
你是不是也想过这些:直接把 S3 的 URL 塞进 <img> 标签就行?next/image 能正常处理 S3 的图片吗?如果文件是私密的,随便谁拿到链接都能看怎么办?
如果你看过我之前那篇关于 Next.js 16 上传文件到 S3 的指南,会知道文件有三种存储方式:通过服务器代理上传、直接用预签名 URL 直传、或者分块上传大文件。好消息是:怎么展示图片跟你当初怎么上传的完全没关系,唯一决定因素只有一个——这个对象是公开的还是私有的。
在这篇文章里,我会给你讲清楚三种真实场景下的 S3 图片展示方案:
- 公开桶 URL —— 最省事,适合非敏感资源
- 预签名 GET URL —— 私有文件专用,只有对的人才让看
- CloudFront CDN 分发 —— 追求性能和规模,公开和签名两种都支持
看完你就能对号入座,知道自己的场景该用哪种方案,代码也是现成的。
为什么这事儿没那么简单
你可能会想:“S3 不就有个 URL 吗,我直接拿来用不就行了?” 有时候确实可以,但大多数正经项目很快就会碰到这些问题:
- 不是所有东西都应该公开。用户的私密文档或者别人上传的收据,不应该被任何人通过猜 URL 或者复制链接就能看到。
next/image默认不认识 S3。Next.js 的图片优化器会拦截外部域名,你必须手动把它加到白名单里——跳过这一步,你的图片就会静默加载失败,连个像样的报错都没有。- 原始 S3 不是 CDN。每次请求都直接打到你的桶上,而且只在一个区域,没有任何边缘缓存——做个个人项目还行,一旦有真实流量就扛不住了。
所以,每次你要展示一张图片的时候,真正要问自己的问题是:“谁有权看这张图?它需不需要在大流量下也够快?” 答案决定你选哪个方案。
💡 小建议:如果不确定某个文件该不该公开,默认设为私有。把私有文件改成公开容易得很,但要是发现一个“公开”文件泄露了什么敏感信息,那可就晚了。
准备工作
这篇文章是接着我上一篇 S3 上传指南写的,假设你已经有了:
- Next.js 16 搭配 App Router
- TypeScript 启用了严格模式
- 用了 Tailwind CSS 做 UI 示例
- 同一个 S3 桶和
s3Client(来自上传指南里的lib/s3-client.ts)
如果你还没配好 S3 客户端,先去搞定它——下面每个方案都要用到。
在 next.config 里放行 S3(和 CloudFront)
next/image 拒绝处理它不认识的域名上的图片——这是安全机制,不是 bug。你得把桶的主机名加到 remotePatterns 里:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "devstacked-uploads-demo.s3.us-east-1.amazonaws.com",
},
{
protocol: "https",
hostname: "*.cloudfront.net", // 给方案 3 预留
},
],
},
};
export default nextConfig;
⚠️ 常见坑:忘了这一步,然后以为是
next/image坏了。Next.js 抛出的错误(“hostname is not configured”)说的就是这个——不是 bug,是优化器拒绝去没列出来的域名拉图片。
💡 小建议:如果你用的是稳定不变的公开 URL(方案 1 或者方案 3 的公开部分),顺便调一下
minimumCacheTTL:images: { minimumCacheTTL: 60 * 60 * 24 * 30, // 缓存优化后的输出 30 天 remotePatterns: [ /* ... */ ], },Next.js 默认只缓存优化后的图片 4 小时,然后就会去源站重新检查。对于上传后就不变的图片(比如用唯一 key 的头像或商品照片),设一个更长的 TTL 能避免反复做没必要的重新优化。
方案 1:公开桶 URL
工作原理:对象本身是公开可读的,所以它的 S3 URL 对任何人都有效,永远有效——不需要签名,没有过期时间,服务器完全不用参与。
浏览器 → S3 对象 URL 直连(不需要经过服务器)
这个方案适合真正非敏感的资产:博客封面图、公开商品照片、用户主动设为可见的头像。也是开发最快的方案,因为展示图片根本不需要后端逻辑。
步骤 1:只让特定对象可公开读取
不要把整个桶设为公开(千万别干这种事),而是通过桶策略只放行存放公开资源的文件夹:
// s3-public-read-policy.json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadForPublicFolder",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::devstacked-uploads-demo/public/*"
}
]
}
这段策略的意思:只开放 public/ 前缀下的对象的读权限——桶里其他东西(比如上传指南里的 uploads/ 或 large-uploads/)默认还是私有的。
⚠️ 重要提醒:如果你的桶还开着“完全阻止所有公开访问”(上传指南里设置过),AWS 会直接拒绝这个策略,给你一个“access denied”错误,即使策略本身是合法的。去你的桶 → 权限 → 阻止公开访问,取消勾选那两项跟桶策略相关的设置,同时保持跟 ACL 相关的设置开启。这样就能用限定范围的公开桶策略,而不依赖公开 ACL。
⚠️ 常见坑:把公开读策略应用到整个桶,而不是限定某个前缀。如果你的上传代码哪天没注意改 key,写了个敏感文件进去,它就会悄无声息地变成公开的。
步骤 2:拼出公开 URL 的格式
// lib/s3-urls.ts
const BUCKET_NAME = process.env.S3_BUCKET_NAME!;
const REGION = process.env.AWS_REGION!;
export function getPublicUrl(key: string): string {
// encodeURIComponent 处理 key 里的空格和特殊字符——
// 原始 key 比如 "public/my photo.jpg" 不编码的话会生成无效 URL
const encodedKey = key
.split("/")
.map(encodeURIComponent)
.join("/");
return `https://${BUCKET_NAME}.s3.${REGION}.amazonaws.com/${encodedKey}`;
}
因为公开 URL 永不过期,也永远不需要签名,你可以在任何地方拼这个字符串——不需要调 API,甚至不需要经过你自己的服务器。
⚠️ 常见坑:直接用
encodeURIComponent(key)对整个 key 编码。这样会把路径里的/也转义掉,把public/avatars/photo.jpg变成一个乱七八糟的字符串,而不是合法的嵌套路径。要像上面那样逐段编码路径的每一部分。
步骤 3:用 next/image 渲染
// components/public-avatar.tsx
import Image from "next/image";
import { getPublicUrl } from "@/lib/s3-urls";
interface Props {
objectKey: string; // 例如 "public/avatars/uuid-photo.jpg"
alt: string;
}
export function PublicAvatar({ objectKey, alt }: Props) {
return (
<Image
src={getPublicUrl(objectKey)}
alt={alt}
width={96}
height={96}
className="rounded-full object-cover"
/>
);
}
因为我们之前已经把 S3 主机名加到了 remotePatterns 里,Next.js 会愉快地优化这张图片——缩放、格式转换(比如转成 WebP)、懒加载,所有这些都跟处理本地图片一样正常工作。
方案 2:私有文件的预签名 GET URL
工作原理:对象在 S3 里完全保持私有。当某个用户有权查看时,你的服务器生成一个短期有效的签名 URL——就像你在上传指南里看到的预签名 URL 一样,只不过这次是 GET 请求而不是 PUT。
浏览器 → Next.js 服务器(检查权限)→ 生成预签名 GET URL → 302 重定向到 S3(或直接返回 URL)
这个方案适合任何需要权限控制的场景:用户头像(如果用户设为私有)、付费内容、内部文档、用户上传的敏感文件。关键区别在于:每次展示图片都需要服务器参与——要么通过 API 路由获取 URL,要么在服务器组件里生成。
步骤 1:创建预签名 URL 工具函数
// lib/s3-urls.ts(接在 getPublicUrl 后面)
import { GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { s3Client } from "./s3-client";
export async function getPresignedGetUrl(
key: string,
expiresInSeconds: number = 3600 // 默认 1 小时
): Promise<string> {
const command = new GetObjectCommand({
Bucket: BUCKET_NAME,
Key: key,
});
return getSignedUrl(s3Client, command, { expiresIn: expiresInSeconds });
}
这里用 @aws-sdk/s3-request-presigner 包的 getSignedUrl 函数,它和上传用的 PutObjectCommand 是同一套机制,只不过这次是 GetObjectCommand。expiresIn 参数控制 URL 的有效期——设得太短用户可能看到一半就失效了,设得太长又失去了签名的意义。一般经验是:能短则短,刚好覆盖用户预期的浏览时长。
💡 小建议:对于需要长期访问的私有资源(比如用户相册),可以考虑生成一个有效期更长的 URL 并在客户端缓存,或者用方案 3 的 CloudFront 签名 URL + 缓存策略。
步骤 2:通过 API 路由分发签名 URL
// app/api/signed-url/route.ts
import { NextRequest, NextResponse } from "next/server";
import { getPresignedGetUrl } from "@/lib/s3-urls";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const key = searchParams.get("key");
if (!key) {
return NextResponse.json({ error: "Missing 'key' parameter" }, { status: 400 });
}
// 在这里做权限检查!
// 例如:检查当前用户是否有权访问这个 key 对应的资源
// const session = await getSession(request);
// if (!session || !canAccess(session.userId, key)) {
// return NextResponse.json({ error: "Forbidden" }, { status: 403 });
// }
try {
const signedUrl = await getPresignedGetUrl(key);
return NextResponse.json({ url: signedUrl });
} catch (error) {
console.error("Failed to generate signed URL:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
这段代码的关键不是生成 URL 本身(那只是一行调用),而是权限检查。在调用 getPresignedGetUrl 之前,你必须确认当前请求的用户确实有权访问这个资源。注释里的 getSession 和 canAccess 函数需要你根据自己的认证系统来实现——比如用 NextAuth.js、Clerk 或者你自己的 JWT 方案。
⚠️ 常见坑:跳过权限检查,直接生成签名 URL。这样预签名 URL 就失去了意义——虽然它有过期时间,但任何拿到 URL 的人都能在有效期内访问。权限检查是安全的核心,不是可选项。
步骤 3:在客户端获取并展示图片
// components/private-avatar.tsx
"use client";
import { useEffect, useState } from "react";
import Image from "next/image";
interface Props {
objectKey: string;
alt: string;
}
export function PrivateAvatar({ objectKey, alt }: Props) {
const [signedUrl, setSignedUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchSignedUrl() {
try {
const res = await fetch(`/api/signed-url?key=${encodeURIComponent(objectKey)}`);
if (!res.ok) {
throw new Error(res.status === 403 ? "Access denied" : "Failed to load image");
}
const data = await res.json();
setSignedUrl(data.url);
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
}
}
fetchSignedUrl();
}, [objectKey]);
if (error) {
return <div className="text-red-500">Error: {error}</div>;
}
if (!signedUrl) {
return <div className="w-24 h-24 bg-gray-200 animate-pulse rounded-full" />;
}
return (
<Image
src={signedUrl}
alt={alt}
width={96}
height={96}
className="rounded-full object-cover"
/>
);
}
这是个客户端组件,因为我们需要在 useEffect 里异步获取签名 URL。加载状态下显示一个骨架屏,出错时显示错误信息,拿到 URL 后才用 next/image 渲染。注意我们把 objectKey 作为依赖项——如果 key 变了,组件会重新请求新的签名 URL。
💡 小建议:如果同一个用户会在短时间内多次查看同一张图片(比如滚动浏览图片列表),可以考虑在客户端缓存签名 URL,避免每次渲染都发请求。但要注意 URL 过期的问题——缓存时间不要超过 URL 的有效期。
方案 3:CloudFront CDN 分发
工作原理:在 S3 桶前面放一个 CloudFront 分配,作为内容分发网络。CloudFront 会缓存图片到全球边缘节点,用户请求时从最近的节点获取,大大降低延迟和 S3 的请求压力。
浏览器 → CloudFront 边缘节点(缓存命中)→ 直接返回
浏览器 → CloudFront 边缘节点(缓存未命中)→ 回源到 S3 → 缓存并返回
这个方案适合任何需要性能的场景——尤其是面向全球用户的应用。CloudFront 支持两种模式:公开分发(配合方案 1 的公开桶)和签名 URL 分发(配合方案 2 的私有桶)。
步骤 1:创建 CloudFront 分配
在 AWS 控制台里:
- 进入 CloudFront → 创建分配
- 源域:选择你的 S3 桶
- 源访问控制:选择“Origin access control settings (recommended)”,创建一个新的 OAC(Origin Access Control),让 CloudFront 可以用安全的方式访问 S3
- 查看器协议策略:选择“Redirect HTTP to HTTPS”
- 缓存策略:选择“Managed-CachingOptimized”(或者自定义一个更长的缓存时间)
- 价格等级:根据你的用户分布选择(全球用户选“All edge locations”)
- 备用域名(可选):如果你想用自定义域名,在这里添加并申请 SSL 证书
- 创建完成后,记下分配域名(比如
d123.cloudfront.net)
⚠️ 重要提醒:创建 OAC 后,你需要更新 S3 桶策略,只允许 CloudFront 访问,而不是直接公开。具体来说,在桶策略里添加类似这样的语句:
{ "Effect": "Allow", "Principal": { "Service": "cloudfront.amazonaws.com" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::devstacked-uploads-demo/*", "Condition": { "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/DISTRIBUTION_ID" } } }这样 S3 桶就彻底对公众关闭了,只有 CloudFront 能拉取内容。
步骤 2:更新 next.config 允许 CloudFront 域名
// next.config.ts(接在前面)
remotePatterns: [
{
protocol: "https",
hostname: "d123.cloudfront.net", // 替换成你的 CloudFront 分配域名
},
// 如果还保留 S3 直连,可以继续保留
// {
// protocol: "https",
// hostname: "devstacked-uploads-demo.s3.us-east-1.amazonaws.com",
// },
],
步骤 3:生成 CloudFront URL 并渲染
对于公开资源,直接拼 CloudFront URL:
export function getCloudFrontUrl(key: string): string {
const encodedKey = key
.split("/")
.map(encodeURIComponent)
.join("/");
return `https://d123.cloudfront.net/${encodedKey}`;
}
然后像方案 1 一样用 next/image 渲染:
<Image
src={getCloudFrontUrl("public/avatars/photo.jpg")}
alt="Avatar"
width={96}
height={96}
/>
CloudFront 会自动处理缓存——第一次请求会回源到 S3 拉取图片,然后缓存在边缘节点上,后续相同区域的请求直接从缓存返回,快到飞起。
步骤 4(进阶):CloudFront 签名 URL
如果你既要 CDN 的性能,又要私有文件的权限控制,可以用 CloudFront 的签名 URL 或签名 Cookie。这比 S3 预签名 URL 更强大,因为你可以设置更细粒度的策略(比如允许访问的 IP 范围、URL 过期时间、签名者身份等)。
配置步骤比较复杂,大致流程是:
- 创建一个 CloudFront 密钥对(用于签名)
- 在 CloudFront 分配的行为设置中,限制查看器访问(Restrict Viewer Access)
- 使用 AWS SDK 或自定义代码生成签名 URL
// 这只是一个概念示例,实际实现需要 CloudFront Key Pair
import { CloudFrontSigner } from "@aws-sdk/cloudfront-signer";
export function getCloudFrontSignedUrl(
resourceUrl: string,
expiresInSeconds: number = 3600
): string {
const signer = new CloudFrontSigner({
keyPairId: process.env.CLOUDFRONT_KEY_PAIR_ID!,
privateKey: process.env.CLOUDFRONT_PRIVATE_KEY!,
});
return signer.signUrl({
url: resourceUrl,
expires: Math.floor(Date.now() / 1000) + expiresInSeconds,
});
}
⚠️ 常见坑:CloudFront 签名 URL 和 S3 预签名 URL 是完全不同的机制,不要混用。前者是在 CloudFront 层面做权限控制,后者是在 S3 层面。如果你用了 CloudFront,推荐统一用 CloudFront 签名 URL,这样缓存和权限都在一层处理。
总结:怎么选?
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 博客封面、公开商品图 | 方案 1(公开桶 URL) | 最简单,零服务器开销 |
| 用户头像(可公开/可私有) | 方案 2(预签名 URL) | 灵活控制权限 |
| 全球用户的图片展示 | 方案 3(CloudFront) | 性能最好,延迟最低 |
| 付费内容、私密文档 | 方案 2 或方案 3 签名版 | 必须控制访问权限 |
| 高流量网站 | 方案 3(CloudFront + 长缓存) | 减少 S3 请求,边缘缓存 |
最后再强调一遍:如果不确定,默认私有。把私有文件改成公开很简单,但反过来就麻烦了。权限控制是安全的基础,别图省事跳过。
代码都在上面了,拿去用吧。有问题欢迎在评论区讨论。