This commit is contained in:
hailin 2025-06-25 15:27:50 +08:00
parent 2c1daebf74
commit f6ae45debe
4 changed files with 108 additions and 138 deletions

View File

@ -3,16 +3,16 @@ import { GlobalState } from "@/components/utility/global-state"
import { Providers } from "@/components/utility/providers" import { Providers } from "@/components/utility/providers"
import TranslationsProvider from "@/components/utility/translations-provider" import TranslationsProvider from "@/components/utility/translations-provider"
import initTranslations from "@/lib/i18n" import initTranslations from "@/lib/i18n"
import { Database } from "@/supabase/types" //import { Database } from "@/supabase/types"
import { getSupabaseServerClient } from "@/lib/supabase/server" import { getSupabaseServerClient } from "@/lib/supabase/server"
import { Metadata, Viewport } from "next" import { Metadata, Viewport } from "next"
import { Inter } from "next/font/google" import { Inter } from "next/font/google"
import { cookies } from "next/headers" import { cookies } from "next/headers"
import { ReactNode } from "react" import { ReactNode } from "react"
import "./globals.css" import "./globals.css"
import { getRuntimeEnv } from "@/lib/ipconfig" // 新增引入 //import Script from "next/script"
import Script from "next/script"
import { Suspense } from "react" import { Suspense } from "react"
import { RuntimeEnvProvider } from "@/components/utility/runtime-env-provider"
const inter = Inter({ subsets: ["latin"] }) const inter = Inter({ subsets: ["latin"] })
@ -31,20 +31,20 @@ interface RootLayoutProps {
// 新增的 `getValidSupabaseUrl` 函数,带有超时机制和错误处理 // 新增的 `getValidSupabaseUrl` 函数,带有超时机制和错误处理
async function getValidSupabaseUrl(): Promise<string> { // async function getValidSupabaseUrl(): Promise<string> {
let url = getRuntimeEnv("SUPABASE_URL"); // let url = getRuntimeEnv("SUPABASE_URL");
const timeout = Date.now() + 30000; // 设置最大等待时间为30秒 // const timeout = Date.now() + 30000; // 设置最大等待时间为30秒
while (!url || !url.includes(":")) { // 检查是否包含合法的 IP 和端口 // while (!url || !url.includes(":")) { // 检查是否包含合法的 IP 和端口
if (Date.now() > timeout) { // if (Date.now() > timeout) {
throw new Error("获取有效的 Supabase URL 超时"); // throw new Error("获取有效的 Supabase URL 超时");
} // }
console.log("等待有效的 Supabase URL..."); // console.log("等待有效的 Supabase URL...");
await new Promise(resolve => setTimeout(resolve, 1000)); // 每1秒检查一次 // await new Promise(resolve => setTimeout(resolve, 1000)); // 每1秒检查一次
url = getRuntimeEnv("SUPABASE_URL"); // url = getRuntimeEnv("SUPABASE_URL");
} // }
return url; // return url;
} // }
@ -113,15 +113,15 @@ export default async function RootLayout({
console.log(`🍪 Cookie: ${cookie.name} = ${cookie.value}`); console.log(`🍪 Cookie: ${cookie.name} = ${cookie.value}`);
} }
let supabaseUrl = ""; // let supabaseUrl = "";
try { // try {
// 等待直到获取到有效的 Supabase URL // // 等待直到获取到有效的 Supabase URL
supabaseUrl = await getValidSupabaseUrl(); // supabaseUrl = await getValidSupabaseUrl();
//console.log("==========>获取到有效的 Supabase URL: ", supabaseUrl); // //console.log("==========>获取到有效的 Supabase URL: ", supabaseUrl);
} catch (error) { // } catch (error) {
console.error("Supabase URL 获取失败:", error); // console.error("Supabase URL 获取失败:", error);
return <div>Failed to fetch Supabase configuration, please try again later.</div>; // 出现错误时返回一个友好的提示 // return <div>Failed to fetch Supabase configuration, please try again later.</div>; // 出现错误时返回一个友好的提示
} // }
const supabase = getSupabaseServerClient() const supabase = getSupabaseServerClient()
const { data, error } = await supabase.auth.getSession(); const { data, error } = await supabase.auth.getSession();
@ -137,27 +137,29 @@ export default async function RootLayout({
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<head> <head>
<Script {/* <Script
src="/env.js" src="/env.js"
strategy="beforeInteractive" // 确保在 React 启动之前加载 strategy="beforeInteractive" // 确保在 React 启动之前加载
/> /> */}
</head> </head>
<body className={inter.className}> <body className={inter.className}>
<Providers attribute="class" defaultTheme="dark"> <RuntimeEnvProvider>
<TranslationsProvider <Providers attribute="class" defaultTheme="dark">
namespaces={i18nNamespaces} <TranslationsProvider
locale={locale} namespaces={i18nNamespaces}
resources={resources} locale={locale}
> resources={resources}
<Toaster richColors position="top-center" duration={3000} /> >
<Suspense fallback={null}> <Toaster richColors position="top-center" duration={3000} />
<div className="bg-background text-foreground flex h-dvh flex-col items-center overflow-x-auto"> <Suspense fallback={null}>
{data.session ? <GlobalState>{children}</GlobalState> : children} <div className="bg-background text-foreground flex h-dvh flex-col items-center overflow-x-auto">
</div> {data.session ? <GlobalState>{children}</GlobalState> : children}
</Suspense> </div>
</TranslationsProvider> </Suspense>
</Providers> </TranslationsProvider>
</Providers>
</RuntimeEnvProvider>
</body> </body>
</html> </html>
) )

View File

@ -0,0 +1,25 @@
// app/utility/runtime-env-provider.tsx
import { headers, useServerInsertedHTML } from "next/headers";
import React from "react";
export function RuntimeEnvProvider({ children }: { children: React.ReactNode }) {
/* ① 计算本次请求的 Supabase URL按你的内网场景改即可 */
const h = headers();
const proto = h.get("x-forwarded-proto") ?? "http";
const host = h.get("x-forwarded-host") ?? h.get("host"); // 内网一般就是 host
const port = process.env.SUPABASE_PORT ?? "8000";
const supabaseUrl = `${proto}://${host?.split(",")[0]}:${port}`;
/* ② 注入脚本:保证在 <head> 中且早于所有客户端 JS */
useServerInsertedHTML(() => (
<script
dangerouslySetInnerHTML={{
__html: `window.RUNTIME_ENV = { SUPABASE_URL: ${JSON.stringify(
supabaseUrl,
)} };`,
}}
/>
));
return <>{children}</>;
}

View File

@ -1,60 +0,0 @@
// lib/ipconfig.ts
// export function getRuntimeEnv(key: string): string | undefined {
// if (typeof window !== "undefined" && typeof window.RUNTIME_ENV !== "undefined") {
// return window.RUNTIME_ENV[key];
// }
// return process.env[key];
// }
let _env: Record<string, string> | null = null
// 最大重试次数
const MAX_RETRIES = 5
// 每次重试的延时(毫秒)
const RETRY_DELAY = 1000
export function getRuntimeEnv(key: string): string | undefined {
//console.log("============>>Getting Supabase API URL.")
if (typeof window !== "undefined") {
if (!_env && typeof window.RUNTIME_ENV !== "undefined") {
_env = window.RUNTIME_ENV
console.log("[browser-side] Retrieved API endpoint from window.RUNTIME_ENV:", _env)
}
// 尝试读取缓存的 _env
if (_env) {
console.log("[browser-side] Returning cached API from _env:", _env)
return _env[key]
}
console.log("[browser-side] No window.RUNTIME_ENV found in browser, _env=", _env)
let retries = 0
// 定义一个同步延时重试的函数
const tryGetEnvSync = () => {
if (typeof window.RUNTIME_ENV !== "undefined") {
_env = window.RUNTIME_ENV
console.log("[browser-side] Retrieved API endpoint from window.RUNTIME_ENV:", _env)
return _env[key] // 成功获取,返回结果
} else {
retries += 1
if (retries <= MAX_RETRIES) {
console.log(`[browser-side] Retry ${retries}/${MAX_RETRIES} - Retrying in ${RETRY_DELAY}ms...`)
// 延时并重试
setTimeout(tryGetEnvSync, RETRY_DELAY)
} else {
console.log("[browser-side] Failed to retrieve window.RUNTIME_ENV after retries.")
return undefined // 重试次数耗尽,返回 undefined
}
}
}
return tryGetEnvSync() // 调用同步重试方法
}
// 服务端始终动态读取 process.env
const val = process.env[key]
//console.log("[server-side] Falling back to process.env for key:", key, "value:", val)
return val // 服务端直接返回 process.env 的值
}

View File

@ -1,53 +1,56 @@
// import { createServerClient, type CookieOptions } from "@supabase/ssr" // import { createServerClient, type CookieOptions } from "@supabase/ssr"
// import { cookies } from "next/headers" // import { cookies } from "next/headers"
// import { getRuntimeEnv } from "@/lib/ipconfig" // 新增引入 // import { getRuntimeEnv } from "@/lib/ipconfig"
// import { Database } from "@/supabase/types"
// export const createClient = (cookieStore: ReturnType<typeof cookies>) => { // export function getSupabaseServerClient() {
// return createServerClient( // const supabaseUrl = getRuntimeEnv("SUPABASE_URL") ?? "http://localhost:8000"
// getRuntimeEnv("SUPABASE_URL") ?? "http://localhost:8000", // const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
// process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
// { // const cookieStore = cookies()
// cookies: {
// get(name: string) { // return createServerClient<Database>(supabaseUrl, supabaseAnonKey, {
// return cookieStore.get(name)?.value // cookies: {
// }, // get(name: string) {
// set(name: string, value: string, options: CookieOptions) { // return cookieStore.get(name)?.value
// try { // },
// cookieStore.set({ name, value, ...options }) // set(name: string, value: string, options: CookieOptions) {
// } catch (error) { // try {
// // The `set` method was called from a Server Component. // cookieStore.set({ name, value, ...options })
// // This can be ignored if you have middleware refreshing // } catch (_) {}
// // user sessions. // },
// } // remove(name: string, options: CookieOptions) {
// }, // try {
// remove(name: string, options: CookieOptions) { // cookieStore.set({ name, value: "", ...options })
// try { // } catch (_) {}
// cookieStore.set({ name, value: "", ...options })
// } catch (error) {
// // The `delete` method was called from a Server Component.
// // This can be ignored if you have middleware refreshing
// // user sessions.
// }
// }
// } // }
// } // }
// ) // })
// } // }
// lib/supabase-server.ts
import { createServerClient, type CookieOptions } from "@supabase/ssr" import { createServerClient, type CookieOptions } from "@supabase/ssr"
import { cookies } from "next/headers" import { cookies } from "next/headers"
import { getRuntimeEnv } from "@/lib/ipconfig" import { getRuntimeEnv } from "@/lib/get-runtime-env" // ← 改成新的工具函数
import { Database } from "@/supabase/types" import { Database } from "@/supabase/types"
export function getSupabaseServerClient() { export function getSupabaseServerClient() {
const supabaseUrl = getRuntimeEnv("SUPABASE_URL") ?? "http://localhost:8000" /* URL RuntimeEnv RuntimeEnvProvider
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! 退 .env localhost */
const supabaseUrl =
getRuntimeEnv("SUPABASE_URL") ??
process.env.SUPABASE_URL ??
"http://localhost:8000"
/* ② 匿名 KEY 只放 .env别暴露到 RuntimeEnv */
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
if (!supabaseAnonKey) {
throw new Error("⛔️ NEXT_PUBLIC_SUPABASE_ANON_KEY 未设置")
}
/* ③ Cookie 透传Next 13/14 App Router 写法) */
const cookieStore = cookies() const cookieStore = cookies()
return createServerClient<Database>(supabaseUrl, supabaseAnonKey, { return createServerClient<Database>(supabaseUrl, supabaseAnonKey, {
@ -58,12 +61,12 @@ export function getSupabaseServerClient() {
set(name: string, value: string, options: CookieOptions) { set(name: string, value: string, options: CookieOptions) {
try { try {
cookieStore.set({ name, value, ...options }) cookieStore.set({ name, value, ...options })
} catch (_) {} } catch {}
}, },
remove(name: string, options: CookieOptions) { remove(name: string, options: CookieOptions) {
try { try {
cookieStore.set({ name, value: "", ...options }) cookieStore.set({ name, value: "", ...options })
} catch (_) {} } catch {}
} }
} }
}) })