MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Svelte 代码组织:构建可维护的项目结构

2023-02-267.8k 阅读

项目目录结构基础

在 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.svelteArticleList.svelteFooter.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 中,组件之间的通信有多种方式。

  1. 父子组件通信:父组件可以通过属性(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}
  1. 非父子组件通信:对于非父子组件通信,可以使用 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 中的 loginlogout 函数,也可以进行单元测试:

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 等,并简要说明为什么选择这些技术。

安装和运行步骤

  1. 克隆项目仓库:git clone <repository - url>
  2. 进入项目目录:cd my - svelte - project
  3. 安装依赖:npm install
  4. 启动开发服务器:npm run dev

环境变量说明:解释 .env 文件中各个环境变量的用途,如 API_URL 用于指定 API 地址等。

项目级文档可以放在项目根目录下的 README.md 文件中,这样新加入项目的开发者能够快速了解项目的整体情况,上手开发工作。同时,良好的文档也有助于项目的长期维护和扩展,当代码发生变化时,及时更新文档能够保证文档与代码的一致性。

通过完善的文档与代码组织相结合,可以使 Svelte 项目更加易于理解、维护和扩展,无论是对于单个开发者还是团队协作开发都具有重要意义。在实际项目中,要养成及时编写和更新文档的习惯,确保文档能够准确反映代码的功能和架构。