项目结构 #
一、项目结构概述 #
良好的项目结构是可维护性和可扩展性的基础。
text
┌─────────────────────────────────────────────────────────────┐
│ 推荐项目结构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ src/ │
│ ├── lib/ 共享代码 │
│ │ ├── components/ 通用组件 │
│ │ ├── stores/ 状态管理 │
│ │ ├── utils/ 工具函数 │
│ │ └── server/ 服务端代码 │
│ ├── routes/ 路由页面 │
│ └── app.css 全局样式 │
│ │
│ static/ 静态资源 │
│ tests/ 测试文件 │
│ │
└─────────────────────────────────────────────────────────────┘
二、SvelteKit 项目结构 #
2.1 基础结构 #
text
my-sveltekit-app/
├── src/
│ ├── lib/
│ │ ├── components/
│ │ │ ├── ui/ 基础UI组件
│ │ │ │ ├── Button.svelte
│ │ │ │ ├── Input.svelte
│ │ │ │ └── index.ts
│ │ │ └── layout/ 布局组件
│ │ │ ├── Header.svelte
│ │ │ ├── Footer.svelte
│ │ │ └── Sidebar.svelte
│ │ ├── stores/
│ │ │ ├── user.ts
│ │ │ ├── cart.ts
│ │ │ └── index.ts
│ │ ├── utils/
│ │ │ ├── format.ts
│ │ │ ├── validation.ts
│ │ │ └── api.ts
│ │ ├── server/
│ │ │ ├── database.ts
│ │ │ ├── auth.ts
│ │ │ └── email.ts
│ │ └── index.ts
│ ├── routes/
│ │ ├── +layout.svelte
│ │ ├── +layout.ts
│ │ ├── +page.svelte
│ │ ├── +page.ts
│ │ ├── +error.svelte
│ │ ├── (auth)/
│ │ │ ├── +layout.svelte
│ │ │ ├── login/
│ │ │ └── register/
│ │ ├── (app)/
│ │ │ ├── +layout.svelte
│ │ │ ├── dashboard/
│ │ │ └── settings/
│ │ └── api/
│ │ └── users/
│ │ └── +server.ts
│ ├── app.html
│ ├── app.css
│ └── app.d.ts
├── static/
│ ├── favicon.ico
│ └── images/
├── tests/
│ ├── unit/
│ └── e2e/
├── svelte.config.js
├── vite.config.ts
├── tsconfig.json
└── package.json
2.2 路径别名 #
tsconfig.json:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"],
"$components": ["src/lib/components"],
"$stores": ["src/lib/stores"],
"$utils": ["src/lib/utils"]
}
}
}
svelte.config.js:
javascript
import adapter from '@sveltejs/adapter-auto';
export default {
kit: {
adapter: adapter(),
alias: {
$components: 'src/lib/components',
$stores: 'src/lib/stores',
$utils: 'src/lib/utils'
}
}
};
使用:
svelte
<script>
import Button from '$components/ui/Button.svelte';
import { user } from '$stores/user';
import { formatDate } from '$utils/format';
</script>
三、组件组织 #
3.1 组件目录结构 #
text
src/lib/components/
├── ui/ 基础UI组件
│ ├── Button/
│ │ ├── Button.svelte
│ │ ├── Button.test.ts
│ │ └── index.ts
│ ├── Input/
│ ├── Modal/
│ └── index.ts
├── layout/ 布局组件
│ ├── Header/
│ ├── Footer/
│ ├── Sidebar/
│ └── index.ts
├── forms/ 表单组件
│ ├── TextField/
│ ├── Select/
│ └── index.ts
├── data/ 数据展示组件
│ ├── Table/
│ ├── Card/
│ └── index.ts
└── index.ts
3.2 组件导出 #
src/lib/components/ui/Button/index.ts:
typescript
export { default as Button } from './Button.svelte';
export type { ButtonProps } from './Button.svelte';
src/lib/components/ui/index.ts:
typescript
export * from './Button';
export * from './Input';
export * from './Modal';
src/lib/components/index.ts:
typescript
export * from './ui';
export * from './layout';
export * from './forms';
export * from './data';
使用:
svelte
<script>
import { Button, Input, Modal } from '$lib/components';
</script>
3.3 组件文件结构 #
svelte
<script lang="ts">
interface Props {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onclick?: () => void;
}
let {
variant = 'primary',
size = 'md',
disabled = false,
onclick,
children
}: Props & { children?: import('svelte').Snippet } = $props();
</script>
<button
class="btn btn-{variant} btn-{size}"
class:disabled
{disabled}
{onclick}
>
{#if children}
{@render children()}
{/if}
</button>
<style>
.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-primary {
background: #ff3e00;
color: white;
}
.btn-secondary {
background: #666;
color: white;
}
.btn-sm { padding: 0.25rem 0.5rem; }
.btn-md { padding: 0.5rem 1rem; }
.btn-lg { padding: 0.75rem 1.5rem; }
.disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
四、状态管理组织 #
4.1 Store 目录结构 #
text
src/lib/stores/
├── user.ts
├── cart.ts
├── theme.ts
├── notifications.ts
└── index.ts
4.2 Store 示例 #
src/lib/stores/user.ts:
typescript
import { writable, derived } from 'svelte/store';
import type { User } from '$lib/types';
function createUserStore() {
const { subscribe, set, update } = writable<User | null>(null);
return {
subscribe,
login: async (credentials: Credentials) => {
const user = await loginApi(credentials);
set(user);
},
logout: async () => {
await logoutApi();
set(null);
},
updateProfile: (data: Partial<User>) => {
update(user => user ? { ...user, ...data } : null);
}
};
}
export const user = createUserStore();
export const isAuthenticated = derived(
user,
$user => $user !== null
);
export const userRole = derived(
user,
$user => $user?.role ?? null
);
src/lib/stores/index.ts:
typescript
export * from './user';
export * from './cart';
export * from './theme';
export * from './notifications';
五、工具函数组织 #
5.1 Utils 目录结构 #
text
src/lib/utils/
├── format.ts
├── validation.ts
├── api.ts
├── storage.ts
└── index.ts
5.2 工具函数示例 #
src/lib/utils/format.ts:
typescript
export function formatDate(date: Date | string): string {
return new Date(date).toLocaleDateString('zh-CN');
}
export function formatCurrency(amount: number): string {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(amount);
}
export function formatBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
src/lib/utils/validation.ts:
typescript
export function isEmail(value: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
export function isPhone(value: string): boolean {
return /^1[3-9]\d{9}$/.test(value);
}
export function isUrl(value: string): boolean {
try {
new URL(value);
return true;
} catch {
return false;
}
}
export function required(value: unknown): boolean {
if (typeof value === 'string') return value.trim().length > 0;
return value !== null && value !== undefined;
}
六、服务端代码组织 #
6.1 Server 目录结构 #
text
src/lib/server/
├── database.ts
├── auth.ts
├── email.ts
├── cache.ts
└── index.ts
6.2 服务端代码示例 #
src/lib/server/database.ts:
typescript
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export const db = {
user: {
findById: (id: string) => prisma.user.findUnique({ where: { id } }),
findByEmail: (email: string) => prisma.user.findUnique({ where: { email } }),
create: (data: CreateUserInput) => prisma.user.create({ data }),
update: (id: string, data: UpdateUserInput) =>
prisma.user.update({ where: { id }, data }),
delete: (id: string) => prisma.user.delete({ where: { id } })
},
post: {
findMany: (params?: PostFindManyParams) => prisma.post.findMany(params),
findById: (id: string) => prisma.post.findUnique({ where: { id } }),
create: (data: CreatePostInput) => prisma.post.create({ data }),
update: (id: string, data: UpdatePostInput) =>
prisma.post.update({ where: { id }, data }),
delete: (id: string) => prisma.post.delete({ where: { id } })
}
};
src/lib/server/auth.ts:
typescript
import { db } from './database';
import { createSession, validateSession } from './session';
import { hash, compare } from 'bcrypt';
export async function register(email: string, password: string, name: string) {
const hashedPassword = await hash(password, 10);
const user = await db.user.create({
email,
password: hashedPassword,
name
});
return createSession(user.id);
}
export async function login(email: string, password: string) {
const user = await db.user.findByEmail(email);
if (!user || !(await compare(password, user.password))) {
throw new Error('Invalid credentials');
}
return createSession(user.id);
}
export async function logout(sessionId: string) {
await invalidateSession(sessionId);
}
export async function getUser(sessionId: string) {
const session = await validateSession(sessionId);
if (!session) return null;
return db.user.findById(session.userId);
}
七、类型定义组织 #
7.1 Types 目录结构 #
text
src/lib/types/
├── user.ts
├── post.ts
├── api.ts
└── index.ts
7.2 类型定义示例 #
src/lib/types/user.ts:
typescript
export interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user';
createdAt: Date;
updatedAt: Date;
}
export interface CreateUserInput {
email: string;
password: string;
name: string;
}
export interface UpdateUserInput {
name?: string;
email?: string;
}
src/lib/types/api.ts:
typescript
export interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
limit: number;
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, unknown>;
}
八、测试组织 #
8.1 测试目录结构 #
text
tests/
├── unit/
│ ├── components/
│ │ └── Button.test.ts
│ ├── stores/
│ │ └── user.test.ts
│ └── utils/
│ └── format.test.ts
└── e2e/
├── login.test.ts
└── dashboard.test.ts
8.2 单元测试示例 #
tests/unit/components/Button.test.ts:
typescript
import { render, fireEvent } from '@testing-library/svelte';
import { describe, it, expect } from 'vitest';
import Button from '$lib/components/ui/Button/Button.svelte';
describe('Button', () => {
it('renders with default props', () => {
const { getByRole } = render(Button);
expect(getByRole('button')).toBeInTheDocument();
});
it('handles click events', async () => {
let clicked = false;
const { getByRole } = render(Button, {
onclick: () => { clicked = true; }
});
await fireEvent.click(getByRole('button'));
expect(clicked).toBe(true);
});
});
九、总结 #
| 目录 | 说明 |
|---|---|
src/lib/components |
可复用组件 |
src/lib/stores |
状态管理 |
src/lib/utils |
工具函数 |
src/lib/server |
服务端代码 |
src/lib/types |
类型定义 |
src/routes |
路由页面 |
static |
静态资源 |
tests |
测试文件 |
项目结构要点:
- 按功能模块组织代码
- 使用路径别名简化导入
- 统一导出入口
- 分离服务端和客户端代码
- 合理组织测试文件
最后更新:2026-03-28