shared/ 目录
shared/ 是 Nuxt 4 新增的目录,用于存放前后端共享的代码。
为什么需要 shared/?
在 Nuxt 4 中,app/ 和 server/ 使用不同的 TypeScript 项目,它们不能直接互相导入。这带来了一个实际问题:
| 场景 | 没有 shared/ 时 | 有 shared/ 时 |
|---|---|---|
| 定义用户类型 | 在 app/ 和 server/ 各写一遍 | 写在 shared/types/ 一处即可 |
| 前后端共用的格式化函数 | 复制粘贴两份 | 写在 shared/utils/ 一处即可 |
| 分页常量 | 两边各定义一次 | 写在 shared/constants.ts 一处即可 |
核心问题
Nuxt 4 的类型隔离是为了安全(防止客户端误用服务端 API),但也导致前后端无法直接共享代码。shared/ 就是连接两者的桥梁。
类比
app/ 是前端的地盘,server/ 是后端的地盘,shared/ 是双方共享的公共区域。
目录结构
shared/
├── types/
│ ├── user.ts # 用户类型定义
│ └── api.ts # API 响应类型
├── utils/
│ └── format.ts # 前后端共用的格式化函数
└── constants.ts # 常量推荐的子目录结构
types/— 类型定义(interface、type)utils/— 工具函数constants.ts— 常量validators/— 校验逻辑(如手机号、邮箱验证)
使用场景
1. 共享类型定义(最常用)
// shared/types/user.ts
export interface User {
id: number
name: string
email: string
role: 'admin' | 'user'
createdAt: string
}
export interface UserProfile extends User {
avatar: string
bio: string
}为什么类型定义要放 shared/?
在一个典型的 CRUD 应用中:
- 服务端 API 返回
User类型数据 - 客户端页面接收和显示
User类型数据 - 如果类型定义只在一方,另一方就得重新定义或用
any(失去类型检查)
放在 shared/types/ 后,两边都能用,类型保持一致。
在服务端和客户端都可以使用:
// server/api/users/[id].get.ts
import type { User } from '~~/shared/types/user'
export default defineEventHandler((event): User => {
// 返回值有类型检查,确保返回的数据结构正确
return { id: 1, name: 'Alice', email: 'a@test.com', role: 'user', createdAt: '...' }
})<!-- app/pages/profile.vue -->
<script setup lang="ts">
import type { User } from '~~/shared/types/user'
const { data } = await useFetch<User>('/api/users/1')
// data.value 的类型是 User | undefined,有完整的类型提示
</script>2. 共享工具函数
// shared/utils/format.ts
export const formatDate = (date: string) => {
return new Date(date).toLocaleDateString('zh-CN')
}
export const truncateText = (text: string, maxLength: number) => {
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
}哪些工具函数适合放 shared/?
- ✅ 纯函数(输入 → 输出,没有副作用)
- ✅ 格式化函数(日期、货币、数字)
- ✅ 校验函数(邮箱、手机号)
- ✅ 计算函数(折扣、税费)
- ❌ 依赖浏览器 API 的(如
localStorage) - ❌ 依赖 Node.js API 的(如
fs、path)
3. 共享常量
// shared/constants.ts
export const PAGE_SIZE = 20
export const MAX_UPLOAD_SIZE = 10 * 1024 * 1024 // 10MB
export const ROLES = ['admin', 'user'] as const为什么 as const?
as const 让 TypeScript 把 ROLES 推断为只读元组 ['admin', 'user'],而不是 string[]。这样你在代码中使用 ROLES 时,类型是精确的,不会出现 ROLES[0] 是 string 的问题。
4. 共享校验逻辑
// shared/utils/validation.ts
export const isValidEmail = (email: string) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
export const isValidPhone = (phone: string) => {
return /^1[3-9]\d{9}$/.test(phone)
}为什么校验逻辑也要共享?
- 服务端:API 接收请求时验证数据合法性
- 客户端:表单提交前验证数据合法性
- 逻辑必须一致!如果服务端允许 10 位手机号,客户端只允许 11 位,就会出现"客户端通过但服务端拒绝"的 bug。
导入路径
从 shared/ 导入时,使用 ~~/shared/ 或 #shared/ 路径:
import type { User } from '~~/shared/types/user'
import { formatDate } from '~~/shared/utils/format'~~/ 和 #shared/ 的区别
~~/是路径别名,指向项目根目录,~~/shared/就是完整的路径#shared/是模块别名,直接指向shared/目录- 两者效果相同,
#shared/更简洁
注意
shared/utils/ 下的函数也支持自动导入,和 app/utils/、server/utils/ 一样,不需要手动 import。
自动导入
shared/utils/ 下的函数也支持自动导入,与 app/utils/ 和 server/utils/ 类似。
// shared/utils/format.ts
export const formatDate = (date: string) => {
return new Date(date).toLocaleDateString('zh-CN')
}在 app/ 和 server/ 中都可以直接使用 formatDate(),无需 import。
注意事项
不要在 shared/ 中使用仅客户端或仅服务端的 API
| 不可用 | 原因 | 替代方案 |
|---|---|---|
ref()、reactive() | Vue 响应式 API,服务端不需要 | 放在 app/composables/ |
useState() | Nuxt 客户端状态,服务端无法使用 | 放在 app/composables/ |
readBody()、getQuery() | Nitro 服务端 API,客户端无法使用 | 放在 server/utils/ |
window、document | 浏览器 API,服务端没有 | 放在 app/ 代码中 |
fs、path | Node.js API,浏览器没有 | 放在 server/ 代码中 |
确保代码在两端都能运行
shared/ 的铁律:纯 TypeScript,不依赖任何运行时环境。
// ✅ 正确:纯函数,两端都能用
export const add = (a: number, b: number) => a + b
// ❌ 错误:依赖浏览器 API
export const getWidth = () => window.innerWidth
// ❌ 错误:依赖 Node.js API
export const readFile = (path: string) => fs.readFileSync(path)
// ✅ 正确:类型定义,两端都能用
export interface User { id: number; name: string }三个 utils 目录的对比
| 目录 | 作用域 | 自动导入 | 典型内容 |
|---|---|---|---|
app/utils/ | 仅客户端(+ SSR 时的服务端渲染) | ✅ | UI 工具函数、DOM 操作辅助 |
server/utils/ | 仅服务端 | ✅ | 数据库连接、加密函数 |
shared/utils/ | 前后端都可用 | ✅ | 格式化、校验、纯函数 |
选择原则
如果函数前后端都要用 → shared/utils/;只前端用 → app/utils/;只后端用 → server/utils/。拿不准就放 shared/utils/。