Svelte 代码组织:构建可维护的项目结构
项目目录结构基础
在 Svelte 项目中,一个良好的初始目录结构是构建可维护项目的基石。通常,我们会有一个根目录,其中包含 src
目录用于存放源代码,public
目录用于存放静态资源,如图片、字体等,以及 package.json
文件用于管理项目依赖。
my - svelte - project
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── assets
│ ├── logo.png
│ └── styles.css
├── src
│ ├── components
│ │ ├── Button.svelte
│ │ └── Card.svelte
│ ├── stores
│ │ └── userStore.js
│ ├── routes
│ │ ├── home.svelte
│ │ └── about.svelte
│ ├── main.js
│ └── App.svelte
├── package.json
└── README.md
src/components
目录
这个目录专门用于存放 Svelte 组件。将不同功能的组件分类存放,比如 UI 组件(按钮、卡片等)可以放在一起,业务相关的组件可以根据业务模块进一步细分。例如,在一个电商项目中,可能会有 src/components/products
目录存放与产品展示相关的组件,src/components/cart
目录存放购物车相关组件。
以一个简单的 Button.svelte
组件为例:
<script>
let buttonText = 'Click me';
function handleClick() {
console.log('Button clicked');
}
</script>
<button on:click={handleClick}>
{buttonText}
</button>
<style>
button {
background - color: blue;
color: white;
padding: 10px 20px;
border: none;
border - radius: 5px;
}
</style>
src/stores
目录
在 Svelte 中,状态管理通常会用到 stores。将所有的 store 相关代码放在这个目录下,便于管理和维护。比如,一个用于管理用户登录状态的 userStore.js
可能如下:
import { writable } from'svelte/store';
const userStore = writable({
isLoggedIn: false,
userInfo: null
});
export function login(user) {
userStore.update((state) => {
state.isLoggedIn = true;
state.userInfo = user;
return state;
});
}
export function logout() {
userStore.update((state) => {
state.isLoggedIn = false;
state.userInfo = null;
return state;
});
}
export default userStore;
src/routes
目录(如果使用路由)
当项目涉及多页面或单页应用的路由功能时,src/routes
目录就很有用。每个路由对应的页面组件可以放在这里。例如,home.svelte
可以是首页组件:
<script>
// 首页逻辑代码
</script>
<h1>Welcome to the Home Page</h1>
<p>This is the content of the home page.</p>
组件设计与代码组织
单一职责原则
在 Svelte 组件设计中,遵循单一职责原则至关重要。每个组件应该只负责一项特定的功能。比如,一个 Card.svelte
组件应该专注于展示卡片内容,而不是同时处理复杂的业务逻辑或与其他组件的交互逻辑。
<script>
let cardTitle;
let cardContent;
export let data;
$: cardTitle = data.title;
$: cardContent = data.content;
</script>
<div class="card">
<h2>{cardTitle}</h2>
<p>{cardContent}</p>
</div>
<style>
.card {
border: 1px solid gray;
border - radius: 5px;
padding: 10px;
margin: 10px;
}
</style>
这样,当需要复用这个卡片组件时,只需要传入不同的 data
数据即可,而不用担心组件内部的逻辑过于复杂导致复用困难。
组件层次结构
合理的组件层次结构有助于提高代码的可维护性。一般来说,高层组件负责整体布局和协调子组件,而底层组件专注于具体的 UI 展示或功能实现。例如,在一个博客展示页面,可能有一个 BlogPage.svelte
作为高层组件,它包含 Header.svelte
、ArticleList.svelte
和 Footer.svelte
等子组件。
BlogPage.svelte
:
<script>
// 博客页面逻辑,例如获取文章列表等
</script>
<Header />
<ArticleList />
<Footer />
ArticleList.svelte
又可以进一步包含 ArticleCard.svelte
等底层组件来展示每一篇文章的卡片。
<script>
let articles = [];
// 假设从 API 获取文章列表
// fetch('api/articles')
// .then(response => response.json())
// .then(data => {
// articles = data;
// });
</script>
{#each articles as article}
<ArticleCard {article} />
{/each}
组件通信
在 Svelte 中,组件之间的通信有多种方式。
- 父子组件通信:父组件可以通过属性(props)向子组件传递数据。例如,在上面的
ArticleCard.svelte
中,ArticleList.svelte
通过{article}
传递文章数据给ArticleCard.svelte
。
<script>
export let article;
</script>
<div class="article - card">
<h3>{article.title}</h3>
<p>{article.excerpt}</p>
</div>
<style>
.article - card {
border: 1px solid lightgray;
border - radius: 5px;
padding: 10px;
margin: 10px;
}
</style>
子组件可以通过 createEventDispatcher
来触发事件,通知父组件。比如,ArticleCard.svelte
有一个点击查看全文的功能,点击后通知父组件:
<script>
import { createEventDispatcher } from'svelte';
const dispatch = createEventDispatcher();
export let article;
function viewFullArticle() {
dispatch('view - full - article', { articleId: article.id });
}
</script>
<div class="article - card">
<h3>{article.title}</h3>
<p>{article.excerpt}</p>
<button on:click={viewFullArticle}>View Full Article</button>
</div>
<style>
.article - card {
border: 1px solid lightgray;
border - radius: 5px;
padding: 10px;
margin: 10px;
}
</style>
在父组件 ArticleList.svelte
中监听这个事件:
<script>
let articles = [];
function handleViewFullArticle(event) {
console.log('View full article event received with articleId:', event.detail.articleId);
// 处理查看全文逻辑,例如导航到文章详情页
}
</script>
{#each articles as article}
<ArticleCard {article} on:view - full - article={handleViewFullArticle} />
{/each}
- 非父子组件通信:对于非父子组件通信,可以使用 Svelte 的 stores。例如,一个全局的
userStore
可以被多个不同层次的组件访问和修改。假设在一个应用中,有一个UserProfile.svelte
组件和一个Navigation.svelte
组件,它们都需要根据用户登录状态来显示不同的内容。
UserProfile.svelte
:
<script>
import userStore from '../stores/userStore.js';
</script>
{#if $userStore.isLoggedIn}
<p>Welcome, { $userStore.userInfo.name }</p>
{:else}
<p>Please log in</p>
{/if}
Navigation.svelte
:
<script>
import userStore from '../stores/userStore.js';
</script>
{#if $userStore.isLoggedIn}
<a href="/logout">Logout</a>
{:else}
<a href="/login">Login</a>
{/if}
状态管理与代码组织
局部状态与组件逻辑
在 Svelte 组件中,局部状态对于实现组件的特定功能很重要。例如,在一个 ToggleButton.svelte
组件中,有一个表示按钮开关状态的局部状态。
<script>
let isOn = false;
function toggle() {
isOn =!isOn;
}
</script>
<button on:click={toggle}>
{isOn? 'Turn Off' : 'Turn On'}
</button>
<style>
button {
background - color: {isOn? 'green' : 'gray'};
color: white;
padding: 10px 20px;
border: none;
border - radius: 5px;
}
</style>
这里的 isOn
状态只与 ToggleButton.svelte
组件自身的功能相关,不需要在整个应用中共享,因此适合作为局部状态。
全局状态与 stores
当状态需要在多个组件之间共享时,就需要使用全局状态,也就是 stores。前面提到的 userStore
就是一个很好的例子。除了简单的登录状态管理,还可以扩展到管理用户的偏好设置、购物车数据等。
例如,一个电商应用的购物车 store:
import { writable } from'svelte/store';
const cartStore = writable([]);
export function addToCart(product) {
cartStore.update((cart) => {
const existingProduct = cart.find((p) => p.id === product.id);
if (existingProduct) {
existingProduct.quantity++;
} else {
product.quantity = 1;
cart.push(product);
}
return cart;
});
}
export function removeFromCart(productId) {
cartStore.update((cart) => cart.filter((product) => product.id!== productId));
}
export function clearCart() {
cartStore.set([]);
}
export default cartStore;
在组件中使用这个购物车 store:
<script>
import cartStore from '../stores/cartStore.js';
import Product from './Product.svelte';
let products = [];
// 假设从 API 获取产品列表
// fetch('api/products')
// .then(response => response.json())
// .then(data => {
// products = data;
// });
</script>
{#each products as product}
<Product {product} on:add - to - cart={() => addToCart(product)} />
{/each}
{#if $cartStore.length > 0}
<p>Total items in cart: { $cartStore.reduce((acc, product) => acc + product.quantity, 0) }</p>
{:else}
<p>Your cart is empty</p>
{/if}
派生状态
有时候,我们需要根据现有的状态派生出新的状态。在 Svelte 中,可以使用 derived
函数来实现。例如,在购物车应用中,我们可能需要根据购物车中的商品列表计算总价格。
import { writable, derived } from'svelte/store';
const cartStore = writable([]);
export function addToCart(product) {
cartStore.update((cart) => {
const existingProduct = cart.find((p) => p.id === product.id);
if (existingProduct) {
existingProduct.quantity++;
} else {
product.quantity = 1;
cart.push(product);
}
return cart;
});
}
export function removeFromCart(productId) {
cartStore.update((cart) => cart.filter((product) => product.id!== productId));
}
export function clearCart() {
cartStore.set([]);
}
const totalPriceStore = derived(cartStore, ($cartStore) => {
return $cartStore.reduce((acc, product) => acc + product.price * product.quantity, 0);
});
export { cartStore, totalPriceStore };
在组件中使用派生状态:
<script>
import { cartStore, totalPriceStore } from '../stores/cartStore.js';
</script>
{#if $cartStore.length > 0}
<p>Total price: ${ $totalPriceStore }</p>
{:else}
<p>Your cart is empty</p>
{/if}
样式与代码组织
组件内样式
Svelte 允许在组件内部定义样式,这使得样式与组件紧密关联,提高了代码的可维护性。每个组件的样式只作用于该组件内部的元素,不会影响其他组件。
<script>
let buttonText = 'Click me';
function handleClick() {
console.log('Button clicked');
}
</script>
<button on:click={handleClick}>
{buttonText}
</button>
<style>
button {
background - color: blue;
color: white;
padding: 10px 20px;
border: none;
border - radius: 5px;
}
</style>
这种方式适合于组件特定的样式,比如按钮的样式、卡片的样式等。如果需要复用样式,可以通过提取公共样式到一个单独的 CSS 文件,并在组件中引入。
全局样式
对于整个应用的全局样式,如字体、颜色主题等,可以在 public/assets/styles.css
文件中定义。然后在 index.html
文件中引入这个 CSS 文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF - 8">
<meta name="viewport" content="width=device - width, initial - scale = 1.0">
<link rel="stylesheet" href="assets/styles.css">
<title>My Svelte App</title>
</head>
<body>
<div id="app"></div>
<script defer src="build/bundle.js"></script>
</body>
</html>
styles.css
示例:
body {
font - family: Arial, sans - serif;
background - color: lightgray;
}
h1 {
color: darkblue;
}
动态样式
Svelte 支持动态样式,根据组件的状态来改变样式。例如,在前面的 ToggleButton.svelte
组件中,按钮的背景颜色根据开关状态动态变化。
<script>
let isOn = false;
function toggle() {
isOn =!isOn;
}
</script>
<button on:click={toggle}>
{isOn? 'Turn Off' : 'Turn On'}
</button>
<style>
button {
background - color: {isOn? 'green' : 'gray'};
color: white;
padding: 10px 20px;
border: none;
border - radius: 5px;
}
</style>
还可以通过绑定类名来实现更复杂的动态样式。比如,在一个 Card.svelte
组件中,根据卡片是否被选中来添加不同的类名。
<script>
let isSelected = false;
function toggleSelection() {
isSelected =!isSelected;
}
</script>
<div class={`card ${isSelected? 'card - selected' : ''}`} on:click={toggleSelection}>
<h2>Card Title</h2>
<p>Card content</p>
</div>
<style>
.card {
border: 1px solid gray;
border - radius: 5px;
padding: 10px;
margin: 10px;
}
.card - selected {
background - color: lightblue;
}
</style>
构建和部署相关的代码组织
构建配置
在 Svelte 项目中,通常使用 rollup
进行构建。rollup.config.js
文件用于配置构建过程。例如,可以配置入口文件、输出路径、插件等。
import svelte from '@rollup/plugin - svelte';
import resolve from '@rollup/plugin - resolve';
import commonjs from '@rollup/plugin - commonjs';
import livereload from 'rollup - plugin - livereload';
import { terser } from 'rollup - plugin - terser';
import css from 'rollup - plugin - css - only';
const production =!process.env.ROLLUP_WATCH;
export default {
input:'src/main.js',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js'
},
plugins: [
svelte({
// 配置 Svelte 插件选项
compilerOptions: {
// 生产环境下启用压缩
dev:!production
}
}),
resolve({
browser: true,
dedupe: ['svelte']
}),
commonjs(),
css({ output: 'bundle.css' }),
production && terser(),
!production && livereload('public')
],
watch: {
clearScreen: false
}
};
这里配置了入口文件为 src/main.js
,输出文件为 public/build/bundle.js
,并且在生产环境下启用压缩。
部署相关的组织
在部署 Svelte 应用时,需要考虑静态资源的管理。通常,public
目录下的内容可以直接部署到服务器的静态文件服务器上。例如,使用 Vercel、Netlify 等平台进行部署时,可以直接将项目的根目录上传,它们会自动识别 public
目录并进行部署。
如果需要进行后端集成,可能需要在服务器端配置反向代理,将请求转发到 Svelte 应用的静态资源目录。例如,在 Node.js 中使用 Express 作为服务器,可以这样配置:
const express = require('express');
const app = express();
app.use(express.static('public'));
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
这样,当用户访问服务器时,会直接从 public
目录中获取 Svelte 应用的静态文件,包括 HTML、CSS 和 JavaScript 文件。
在部署过程中,还需要考虑环境变量的配置。例如,在开发环境和生产环境中,API 地址可能不同。可以使用 dotenv
库在开发环境中加载 .env
文件中的环境变量,在生产环境中通过服务器的环境变量配置来设置。
.env
文件示例:
API_URL=http://localhost:3001/api
在 Svelte 组件中使用环境变量:
<script>
const apiUrl = import.meta.env.VITE_API_URL;
async function fetchData() {
const response = await fetch(apiUrl + '/data');
const data = await response.json();
console.log(data);
}
</script>
<button on:click={fetchData}>Fetch Data</button>
通过合理的构建和部署相关的代码组织,可以确保 Svelte 应用在不同环境下稳定运行,并且易于维护和更新。
测试与代码组织
单元测试
在 Svelte 项目中,进行单元测试可以确保每个组件和函数的正确性。通常使用 jest
和 @testing - library/svelte
进行单元测试。
对于一个简单的 Button.svelte
组件,测试其点击功能:
import { render, fireEvent } from '@testing - library/svelte';
import Button from '../src/components/Button.svelte';
describe('Button Component', () => {
it('should call the click handler when clicked', () => {
const handleClick = jest.fn();
const { getByText } = render(Button, { on: { click: handleClick } });
fireEvent.click(getByText('Click me'));
expect(handleClick).toHaveBeenCalled();
});
});
这里使用 render
函数渲染 Button.svelte
组件,并传入一个模拟的点击事件处理函数。然后通过 fireEvent.click
模拟按钮点击,并使用 expect
断言点击处理函数是否被调用。
对于 JavaScript 函数,比如 userStore
中的 login
和 logout
函数,也可以进行单元测试:
import userStore, { login, logout } from '../src/stores/userStore.js';
describe('userStore', () => {
it('should login user correctly', () => {
const user = { name: 'John', email: 'john@example.com' };
login(user);
expect(userStore.get().isLoggedIn).toBe(true);
expect(userStore.get().userInfo).toEqual(user);
});
it('should logout user correctly', () => {
logout();
expect(userStore.get().isLoggedIn).toBe(false);
expect(userStore.get().userInfo).toBe(null);
});
});
集成测试
集成测试用于测试组件之间的交互以及与外部系统(如 API)的交互。在 Svelte 项目中,可以使用 cypress
进行集成测试。
假设我们有一个 ArticleList.svelte
组件,它从 API 获取文章列表并展示。可以编写如下的 Cypress 测试:
describe('ArticleList Component', () => {
it('should display articles from API', () => {
cy.intercept('GET', 'api/articles', {
fixture: 'articles.json'
});
cy.visit('/');
cy.get('.article - card').should('have.length.greaterThan', 0);
});
});
这里使用 cy.intercept
拦截对 api/articles
的 GET 请求,并返回一个本地的 articles.json
数据作为模拟响应。然后访问应用首页,断言页面上的文章卡片数量大于 0。
通过合理的测试代码组织,将单元测试和集成测试分开,可以更全面地保证项目的质量,使得代码在修改和扩展时更加稳定和可靠。同时,将测试代码放在与被测试代码相对应的目录结构下,比如在 src/components/__tests__
目录下存放组件的测试代码,有助于提高代码的可维护性和可读性。
文档与代码组织
组件文档
为每个组件编写文档是提高项目可维护性的重要一环。文档应该包括组件的功能描述、属性说明、事件说明以及使用示例。
对于 Button.svelte
组件,文档可以这样写:
Button Component Documentation
功能描述:该按钮组件用于触发特定的操作,比如提交表单、执行某个功能等。
属性:
- 无
事件:
click
:当按钮被点击时触发,可在父组件中通过on:click
监听并传入处理函数。
使用示例:
<script>
function handleClick() {
console.log('Button clicked');
}
</script>
<Button on:click={handleClick}>Click me</Button>
可以将组件文档放在组件文件同一目录下的 README.md
文件中,这样在使用组件或进行代码审查时,能够方便地查阅文档。
项目级文档
项目级文档应该涵盖项目的整体架构、技术栈、安装和运行步骤、环境变量说明等重要信息。
项目架构:描述项目的目录结构、组件层次结构、状态管理方式等。
技术栈:列出项目所使用的技术,如 Svelte、Rollup、Jest 等,并简要说明为什么选择这些技术。
安装和运行步骤:
- 克隆项目仓库:
git clone <repository - url>
- 进入项目目录:
cd my - svelte - project
- 安装依赖:
npm install
- 启动开发服务器:
npm run dev
环境变量说明:解释 .env
文件中各个环境变量的用途,如 API_URL
用于指定 API 地址等。
项目级文档可以放在项目根目录下的 README.md
文件中,这样新加入项目的开发者能够快速了解项目的整体情况,上手开发工作。同时,良好的文档也有助于项目的长期维护和扩展,当代码发生变化时,及时更新文档能够保证文档与代码的一致性。
通过完善的文档与代码组织相结合,可以使 Svelte 项目更加易于理解、维护和扩展,无论是对于单个开发者还是团队协作开发都具有重要意义。在实际项目中,要养成及时编写和更新文档的习惯,确保文档能够准确反映代码的功能和架构。