文章目录
- 会进入浏览器产物的代码
- [不会进入浏览器 JavaScript 包的代码](#不会进入浏览器 JavaScript 包的代码)
- 代码不公开,不代表返回值不公开
- 模块边界才是真正的判断标准
- 静态资源同样是公开的
在 Remix(现稳定路线为 React Router Framework Mode)中,一次构建通常生成两套产物:
| 目录 | 用途 | 浏览器能否直接获取 |
|---|---|---|
build/client |
JavaScript、CSS、图片、字体、预渲染 HTML 等 | 能,应作为静态资源公开 |
build/server |
SSR、loader、action、数据库访问等 |
不能,只交给服务器运行 |
官方构建说明也明确将 Framework Mode 的输出分成浏览器版本与服务器版本。构建说明
会进入浏览器产物的代码
以下代码会进入客户端模块图,因而应当视为公开代码:
| 部分 | 是否进入客户端 |
|---|---|
app/entry.client.tsx |
是 |
路由的默认组件 export default |
是 |
| 普通 React 组件、Hooks、事件处理代码 | 是 |
ErrorBoundary |
是 |
HydrateFallback |
是 |
根路由的 Layout |
是 |
clientLoader |
是 |
clientAction |
是 |
clientMiddleware |
是 |
shouldRevalidate |
是 |
handle |
通常是 |
meta、links |
是 |
| 上述模块导入的第三方包与工具模块 | 是 |
| 导入的 CSS、图片、字体等资源 | 是 |
例如:
ts
import { formatPrice } from "~/utils/price";
import Chart from "~/components/chart";
export async function clientLoader() {
const cached = localStorage.getItem("products");
return cached ? JSON.parse(cached) : [];
}
export default function Products({ loaderData }) {
return <Chart data={loaderData} format={formatPrice} />;
}
这里的这些内容都可能被浏览器下载:
ProductsclientLoaderChartformatPrice- 它们继续导入的依赖
Framework Mode 会自动按路由拆包。例如访问 /about 时,通常只加载根路由和 /about 对应的代码,不会立即加载 /admin。但这只是延迟下载:其他路由的客户端 chunk 依然属于公开静态资源,可以在导航或预加载时被浏览器获取。自动代码拆分
因此:
"没有在首屏加载"不等于"保密"。
任何客户端 chunk 都可以通过开发者工具下载、格式化和分析。
不会进入浏览器 JavaScript 包的代码
这些路由导出是服务端代码:
| 部分 | 是否进入客户端 |
|---|---|
app/entry.server.tsx |
否 |
loader |
否 |
action |
否 |
服务端 middleware |
否 |
headers |
否 |
| 仅由这些服务端代码引用的模块 | 否 |
.server.ts、.server.tsx |
否 |
.server/ 目录中的模块 |
否 |
例如:
ts
import type { Route } from "./+types/project";
import { db } from "~/db.server";
import { requireUser } from "~/auth.server";
export async function loader({
request,
params,
}: Route.LoaderArgs) {
const user = await requireUser(request);
return db.project.findFirst({
where: {
id: params.projectId,
ownerId: user.id,
},
});
}
export default function Project({ loaderData }) {
return <h1>{loaderData.name}</h1>;
}
浏览器获得的是:
Project组件代码;loader返回并被序列化的数据。
浏览器不会获得:
loader函数实现;db.project.findFirst;- 数据库连接配置;
requireUser的服务端实现。
官方说明,服务端 loader 会从浏览器 bundle 中移除;.server 模块如果意外进入客户端依赖图,构建会直接失败。数据加载、.server 模块
代码不公开,不代表返回值不公开
这是最容易被忽略的一点:
ts
export async function loader() {
return {
user: await getUser(),
secret: process.env.INTERNAL_API_KEY,
};
}
loader 函数本身不会进入浏览器,但它的返回结果会发送给浏览器,因此 secret 仍然泄漏。
同样需要视为公开数据的还有:
loader返回值;action返回值;- 抛给错误边界的数据;
- 写入 HTML 的初始 hydration 数据;
- 响应头和 Cookie;
- Resource Route 返回的 JSON、PDF、图片等;
- 预渲染生成的
.html与.data文件。
正确做法是只返回 UI 真正需要的数据:
ts
export async function loader() {
const user = await getUser();
return {
user: {
id: user.id,
name: user.name,
avatarUrl: user.avatarUrl,
},
};
}
模块边界才是真正的判断标准
不能只看一个函数写在什么文件里,还要看它被谁导入。
下面存在泄漏风险:
ts
// config.ts
export const databaseUrl = process.env.DATABASE_URL;
export const pageSize = 20;
tsx
// route.tsx
import { pageSize } from "~/config";
export default function Page() {
return <p>每页 {pageSize} 项</p>;
}
因为客户端组件导入了 config.ts,该模块进入客户端依赖图。即使构建工具可能通过 tree-shaking 删除未使用字段,也不应把安全性寄托在优化器上。
应当主动拆开:
ts
// config.server.ts
export const databaseUrl = process.env.DATABASE_URL!;
ts
// config.shared.ts
export const pageSize = 20;
建议约定:
text
app/
├── db.server.ts
├── auth.server.ts
├── sessions.server.ts
├── services/
│ └── billing.server.ts
├── utils/
│ └── format-price.ts
└── routes/
└── products.tsx
环境变量也要特别注意
在 Vite 中,以 VITE_ 开头的变量会被直接替换进客户端代码:
env
VITE_PUBLIC_API_URL=https://api.example.com
VITE_SECRET_KEY=绝对不能这样写
所以 VITE_* 只能存公开配置,不能存密钥、数据库地址或服务端凭据。Vite 环境变量说明
静态资源同样是公开的
这些文件也支持浏览器直接获取:
public/中的所有文件;- 构建生成的 JS、CSS、图片和字体;
- source map------如果启用了客户端 source map;
- 预渲染生成的 HTML 和
.data文件。
public/icon.png 会原样复制并通过 /icon.png 访问。Vite 静态资源说明
最终可以用这一条规则判断:
从组件、
clientLoader、clientAction、clientMiddleware或客户端入口能够到达的代码,都按公开代码处理;loader、action和.server模块可以保留服务端实现,但它们发送给浏览器的任何数据仍然是公开的。