307 lines
7.2 KiB
JavaScript
307 lines
7.2 KiB
JavaScript
/** @type {import('next').NextConfig} */
|
||
const isProduction = process.env.NODE_ENV === 'production'
|
||
|
||
function trimToNull(value) {
|
||
const normalized = value?.trim()
|
||
return normalized ? normalized : null
|
||
}
|
||
|
||
function buildRemotePatternFromUrl(rawUrl) {
|
||
const normalized = trimToNull(rawUrl)
|
||
if (!normalized) {
|
||
return null
|
||
}
|
||
|
||
try {
|
||
const parsed = new URL(normalized)
|
||
const pathname = parsed.pathname.replace(/\/+$/, '')
|
||
return {
|
||
protocol: parsed.protocol.replace(':', ''),
|
||
hostname: parsed.hostname,
|
||
...(parsed.port ? { port: parsed.port } : {}),
|
||
pathname: pathname ? `${pathname}/**` : '/**',
|
||
}
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
const remotePatterns = [
|
||
{
|
||
protocol: 'http',
|
||
hostname: 'localhost',
|
||
port: '9000',
|
||
pathname: '/**',
|
||
},
|
||
{
|
||
protocol: 'http',
|
||
hostname: 'minio',
|
||
port: '9000',
|
||
pathname: '/**',
|
||
},
|
||
{
|
||
protocol: 'https',
|
||
hostname: 'threejs.org',
|
||
pathname: '/**',
|
||
},
|
||
]
|
||
|
||
for (const candidate of [
|
||
process.env.S3_PUBLIC_DOMAIN,
|
||
process.env.S3_PUBLIC_ENDPOINT,
|
||
process.env.S3_PUBLIC_URL,
|
||
process.env.S3_ENDPOINT,
|
||
]) {
|
||
const pattern = buildRemotePatternFromUrl(candidate)
|
||
if (!pattern) {
|
||
continue
|
||
}
|
||
|
||
const duplicated = remotePatterns.some(
|
||
(existing) =>
|
||
existing.protocol === pattern.protocol &&
|
||
existing.hostname === pattern.hostname &&
|
||
(existing.port || '') === (pattern.port || '') &&
|
||
existing.pathname === pattern.pathname,
|
||
)
|
||
|
||
if (!duplicated) {
|
||
remotePatterns.push(pattern)
|
||
}
|
||
}
|
||
|
||
const nextConfig = {
|
||
// 启用standalone输出,用于Docker部署
|
||
output: 'standalone',
|
||
|
||
// 启用构建时类型检查
|
||
typescript: {
|
||
ignoreBuildErrors: false,
|
||
},
|
||
|
||
// 服务器外部包配置(Next.js 16新配置)
|
||
serverExternalPackages: ['sharp', 'three', 'gsap'],
|
||
|
||
// Turbopack配置(Next.js 16默认启用)
|
||
turbopack: {},
|
||
|
||
// 本地开发允许 localhost 和 127.0.0.1 复用同一套 dev 资源,避免 UI 改动只在其中一个地址生效
|
||
allowedDevOrigins: ['localhost', '127.0.0.1'],
|
||
|
||
// 图像优化配置
|
||
images: {
|
||
remotePatterns,
|
||
unoptimized: true, // 在容器环境中禁用优化以提高性能
|
||
formats: ['image/webp', 'image/avif']
|
||
},
|
||
|
||
// 静态资源配置
|
||
assetPrefix: process.env.NODE_ENV === 'production' ? undefined : undefined,
|
||
|
||
// 压缩配置
|
||
compress: true,
|
||
|
||
// 性能优化
|
||
poweredByHeader: false,
|
||
generateEtags: false,
|
||
|
||
// Webpack配置
|
||
webpack: (config, { dev, isServer }) => {
|
||
// 支持3D文件格式
|
||
config.module.rules.push({
|
||
test: /\.(glb|gltf|fbx|obj|dae)$/,
|
||
type: 'asset/resource',
|
||
generator: {
|
||
filename: 'static/models/[name].[hash][ext]'
|
||
}
|
||
})
|
||
|
||
// 支持音频文件(用于3D场景)
|
||
config.module.rules.push({
|
||
test: /\.(mp3|wav|ogg)$/,
|
||
type: 'asset/resource',
|
||
generator: {
|
||
filename: 'static/audio/[name].[hash][ext]'
|
||
}
|
||
})
|
||
|
||
// 支持着色器文件
|
||
config.module.rules.push({
|
||
test: /\.(glsl|vs|fs|vert|frag)$/,
|
||
type: 'asset/source'
|
||
})
|
||
|
||
// 优化Three.js打包
|
||
if (!isServer) {
|
||
config.resolve.fallback = {
|
||
...config.resolve.fallback,
|
||
fs: false,
|
||
path: false,
|
||
crypto: false
|
||
}
|
||
}
|
||
|
||
// 生产环境优化
|
||
if (!dev) {
|
||
// 启用代码分割
|
||
config.optimization.splitChunks = {
|
||
chunks: 'all',
|
||
cacheGroups: {
|
||
// Three.js单独打包
|
||
three: {
|
||
test: /[\\/]node_modules[\\/](three|@three)[\\/]/,
|
||
name: 'three',
|
||
chunks: 'all',
|
||
priority: 10
|
||
},
|
||
// GSAP单独打包
|
||
gsap: {
|
||
test: /[\\/]node_modules[\\/]gsap[\\/]/,
|
||
name: 'gsap',
|
||
chunks: 'all',
|
||
priority: 10
|
||
},
|
||
// 其他vendor库
|
||
vendor: {
|
||
test: /[\\/]node_modules[\\/]/,
|
||
name: 'vendors',
|
||
chunks: 'all',
|
||
priority: 5
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return config
|
||
},
|
||
|
||
// 环境变量配置
|
||
env: {
|
||
CUSTOM_KEY: process.env.CUSTOM_KEY,
|
||
},
|
||
|
||
// 重定向配置
|
||
async redirects() {
|
||
const redirects = [
|
||
...(!isProduction
|
||
? [
|
||
{
|
||
source: '/:path*',
|
||
has: [
|
||
{
|
||
type: 'host',
|
||
value: '127.0.0.1',
|
||
},
|
||
],
|
||
destination: 'http://localhost:3000/:path*',
|
||
permanent: false,
|
||
},
|
||
]
|
||
: []),
|
||
// 将根路径重定向到主页
|
||
{
|
||
source: '/home',
|
||
destination: '/',
|
||
permanent: true,
|
||
},
|
||
]
|
||
|
||
return redirects
|
||
},
|
||
|
||
// 头部配置(安全性和性能)
|
||
async headers() {
|
||
return [
|
||
{
|
||
source: '/(.*)',
|
||
headers: [
|
||
// 安全头部
|
||
{
|
||
key: 'X-Frame-Options',
|
||
value: 'DENY'
|
||
},
|
||
{
|
||
key: 'X-Content-Type-Options',
|
||
value: 'nosniff'
|
||
},
|
||
{
|
||
key: 'Referrer-Policy',
|
||
value: 'origin-when-cross-origin'
|
||
}
|
||
],
|
||
},
|
||
{
|
||
// Next 静态资源在生产环境可长期缓存;开发环境必须禁用缓存,否则浏览器会继续复用旧 bundle。
|
||
source: '/_next/static/:path*',
|
||
headers: [
|
||
{
|
||
key: 'Cache-Control',
|
||
value: isProduction
|
||
? 'public, max-age=31536000, immutable'
|
||
: 'no-store, max-age=0, must-revalidate'
|
||
}
|
||
],
|
||
},
|
||
{
|
||
// Draco 解码器文件可长期缓存(静态文件)
|
||
source: '/draco/:path*',
|
||
headers: [
|
||
{
|
||
key: 'Cache-Control',
|
||
value: 'public, max-age=31536000, immutable'
|
||
}
|
||
],
|
||
},
|
||
{
|
||
// 上传资源(模型/图片等)属于静态文件,但文件名未必带 hash,使用较保守的缓存策略
|
||
source: '/uploads/:path*',
|
||
headers: [
|
||
{
|
||
key: 'Cache-Control',
|
||
value: 'public, max-age=86400'
|
||
}
|
||
],
|
||
},
|
||
{
|
||
// 3D模型文件特殊缓存策略
|
||
source: '/models/:path*',
|
||
headers: [
|
||
{
|
||
key: 'Cache-Control',
|
||
value: 'public, max-age=86400, s-maxage=31536000'
|
||
},
|
||
{
|
||
key: 'Access-Control-Allow-Origin',
|
||
value: '*'
|
||
}
|
||
],
|
||
},
|
||
{
|
||
// API路由CORS配置
|
||
source: '/api/:path*',
|
||
headers: [
|
||
// API 返回的是动态数据,必须禁用缓存,否则会出现“恢复/删除后列表不更新”等问题
|
||
{
|
||
key: 'Cache-Control',
|
||
value: 'no-store'
|
||
},
|
||
{
|
||
key: 'Access-Control-Allow-Origin',
|
||
value: '*'
|
||
},
|
||
{
|
||
key: 'Access-Control-Allow-Methods',
|
||
value: 'GET, POST, PUT, DELETE, OPTIONS'
|
||
},
|
||
{
|
||
key: 'Access-Control-Allow-Headers',
|
||
value: 'Content-Type, Authorization'
|
||
}
|
||
],
|
||
}
|
||
]
|
||
}
|
||
}
|
||
|
||
module.exports = nextConfig
|