选择AWS S3 ,5G存储,免费12个月。其实主要原因是,Vercel 托管项目,免费用户每个月100g流量,超出后不交钱账号都不能用了,必须要加cdn。
1. 注册AWS账号,创建S3 Bucket,具体步骤不说了,拿到以下凭证,粘贴到.env文件中
APP_AWS_ACCESS_KEY = 'xxx'
APP_AWS_SECRET_KEY = 'xxx+xxx'
APP_AWS_REGION = 'ap-southeast-2'
AWS_S3_BUCKET_NAME_ASSETS = 'thisiscz-assets'
NEXT_PUBLIC_AWS_S3_BUCKET_NAME_ASSETS = 'thisiscz-assets'
NEXT_PUBLIC_AWS_S3_ASEETSPREFIX = 'https://thisiscz-assets.s3.ap-southeast-2.amazonaws.com'
2. S3 设置访问权限和CORS
-
允许所有公开访问
-
存储桶策略
{ "Version": "2008-10-17", "Statement": [ { "Sid": "AllowPublicRead", "Effect": "Allow", "Principal": { "AWS": "*" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::thisiscz-assets/*" } ] } -
CORS 配置
允许自己的域名访问
[ { "AllowedHeaders": [ "*" ], "AllowedMethods": [ "PUT", "POST", "DELETE" ], "AllowedOrigins": [ "http://localhost:3000", "https://thisiscz.vercel.app" ], "ExposeHeaders": [] } ]
3. Next.js 构建后将静态资源上传到 S3 bucket 中
// package.json
"build": "next build && node uploadToS3.js",
可以将 public 和 构建后的静态资源 .next/static/ 都传过去
// upload-to-s3.js
const AWS = require('aws-sdk')
const fs = require('fs')
const path = require('path')
require('dotenv').config()
// 添加获取Content-Type的函数
function getContentType(filePath) {
const ext = path.extname(filePath).toLowerCase()
const contentTypes = {
'.css': 'text/css',
'.js': 'application/javascript',
'.html': 'text/html',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject',
'.otf': 'font/otf',
}
return contentTypes[ext] || 'application/octet-stream'
}
const s3 = new AWS.S3({
accessKeyId: process.env.APP_AWS_ACCESS_KEY,
secretAccessKey: process.env.APP_AWS_SECRET_KEY,
region: process.env.APP_AWS_REGION,
})
async function uploadDir(dirPath, s3Path = '') {
const files = fs.readdirSync(dirPath)
for (const file of files) {
const filePath = path.join(dirPath, file)
const s3Key = path.join(s3Path, path.relative(process.cwd(), filePath))
const fileStat = fs.statSync(filePath)
if (fileStat.isDirectory()) {
await uploadDir(filePath, s3Path)
} else {
const fileContent = fs.readFileSync(filePath)
const params = {
Bucket: process.env.AWS_S3_BUCKET_NAME_ASSETS,
Key: s3Key.replace('.next', '_next'),
Body: fileContent,
ContentType: getContentType(filePath), // 添加ContentType
}
try {
await s3.upload(params).promise()
console.log(
`Uploaded ${filePath} to s3://${params.Bucket}/${params.Key} with Content-Type: ${params.ContentType}`,
)
} catch (err) {
console.error(`Error uploading ${filePath}:`, err)
}
}
}
}
const assetsDir = path.join(process.cwd(), '.next/static/')
const publicDir = path.join(process.cwd(), 'public/')
uploadDir(assetsDir)
.then(() => {
console.log('Upload assets complete!')
})
.catch((err) => {
console.error('Upload assets failed:', err)
})
uploadDir(publicDir)
.then(() => {
console.log('Upload public files complete!')
})
.catch((err) => {
console.error('Upload public files failed:', err)
})
4. 生产环境调整静态资源的 assetPrefix
// next.config.ts
const nextConfig: NextConfig = {
/* config options here */
assetPrefix: __IS_PROD__ ? NEXT_PUBLIC_AWS_S3_ASEETSPREFIX : undefined
}