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

Svelte 组件复用:如何构建可复用的 UI 组件

2024-02-166.0k 阅读

Svelte 组件复用:基础概念与优势

什么是组件复用

在前端开发中,组件复用指的是将已有的 UI 组件应用到多个不同的场景中,以减少重复代码的编写。例如,一个通用的按钮组件,在网站的导航栏、表单提交区域等多个地方都可能会用到。通过复用这个按钮组件,开发者无需为每个使用场景单独编写按钮的 HTML、CSS 和 JavaScript 代码。

在 Svelte 中,组件复用遵循相同的理念。Svelte 组件是独立的、可复用的代码块,包含 HTML、CSS 和 JavaScript 逻辑。通过将相关的 UI 功能封装到组件中,可以方便地在不同的页面或应用程序部分重复使用。

组件复用的优势

  1. 提高开发效率:复用现有的组件意味着减少编写新代码的工作量。当开发一个大型应用程序时,可能会有许多重复的 UI 元素,如导航栏、卡片等。复用这些组件可以显著加快开发速度,使开发者能够将更多的时间和精力放在实现业务逻辑上。
  2. 保持一致性:使用相同的组件确保了整个应用程序 UI 的一致性。所有的按钮、菜单等看起来和行为都一致,这有助于提升用户体验。如果需要对某个组件进行样式或功能的更改,只需要在组件的定义处修改一次,所有使用该组件的地方都会自动更新。
  3. 易于维护:由于组件的逻辑和样式都封装在一个地方,维护起来更加容易。如果某个组件出现问题,开发者可以直接在组件内部进行调试和修复,而不会影响到其他不相关的代码部分。

创建基础可复用组件

简单按钮组件示例

  1. 创建 Svelte 组件文件:在 Svelte 项目中,通常为每个组件创建一个单独的 .svelte 文件。例如,创建一个名为 Button.svelte 的文件。
<script>
    let buttonText = 'Click me';
    let 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>

在这个 Button.svelte 组件中:

  • 首先在 <script> 标签内定义了一个变量 buttonText 作为按钮显示的文本,以及一个点击处理函数 handleClick,当按钮被点击时,会在控制台打印一条消息。
  • 在 HTML 部分,创建了一个 <button> 元素,绑定了点击事件到 handleClick 函数,并显示 buttonText 的值。
  • <style> 标签内定义了按钮的样式,包括背景颜色、文本颜色、内边距和边框样式。
  1. 在其他组件中使用按钮组件:假设我们有一个 App.svelte 文件,要在其中使用刚刚创建的 Button.svelte 组件。
<script>
    import Button from './Button.svelte';
</script>

<Button />

<style>
    body {
        font - family: Arial, sans - serif;
    }
</style>

App.svelte 中,首先使用 import 语句引入了 Button.svelte 组件,然后直接在 HTML 部分使用 <Button /> 标签将按钮组件插入到页面中。

带有属性的可复用组件

  1. 传递文本属性:为了使按钮组件更加通用,我们可以让外部传入按钮的文本。修改 Button.svelte 文件如下:
<script>
    export let text = 'Click me';
    let handleClick = () => {
        console.log('Button clicked!');
    };
</script>

<button on:click={handleClick}>
    {text}
</button>

<style>
    button {
        background-color: blue;
        color: white;
        padding: 10px 20px;
        border: none;
        border - radius: 5px;
    }
</style>

这里使用 export let 声明了一个名为 text 的属性,默认值为 'Click me'。现在在 App.svelte 中使用时可以这样传递不同的文本:

<script>
    import Button from './Button.svelte';
</script>

<Button text="Submit" />

<style>
    body {
        font - family: Arial, sans - serif;
    }
</style>

这样按钮上显示的文本就变为了 Submit

  1. 传递颜色属性:我们还可以传递按钮的颜色属性,进一步增强组件的复用性。继续修改 Button.svelte 文件:
<script>
    export let text = 'Click me';
    export let color = 'blue';
    let handleClick = () => {
        console.log('Button clicked!');
    };
</script>

<button on:click={handleClick} style="background - color: {color}; color: white; padding: 10px 20px; border: none; border - radius: 5px;">
    {text}
</button>

现在在 App.svelte 中可以这样使用:

<script>
    import Button from './Button.svelte';
</script>

<Button text="Cancel" color="red" />

<style>
    body {
        font - family: Arial, sans - serif;
    }
</style>

这样就创建了一个红色背景的 Cancel 按钮,通过传递不同的属性,同一个按钮组件可以满足多种不同的需求。

组件复用中的插槽(Slots)

插槽基础概念

插槽是 Svelte 中一种强大的机制,用于在组件复用过程中自定义组件内部的部分内容。当我们创建一个可复用组件时,可能无法完全预知组件内部每个部分的具体内容。例如,一个卡片组件,卡片的标题和内容可能因使用场景而异。插槽允许我们在使用组件时插入自定义的 HTML 或其他 Svelte 组件。

匿名插槽示例

  1. 创建带有匿名插槽的卡片组件:创建一个 Card.svelte 文件:
<script>
    export let title = 'Default Title';
</script>

<div class="card">
    <h2>{title}</h2>
    <slot></slot>
</div>

<style>
   .card {
        border: 1px solid gray;
        border - radius: 5px;
        padding: 10px;
    }
</style>

在这个 Card.svelte 组件中,定义了一个 title 属性作为卡片的标题,然后在 HTML 部分使用 <slot> 标签。这个 <slot> 就是匿名插槽,它作为一个占位符,在使用 Card.svelte 组件时,插入到 <Card> 标签内的内容会显示在插槽的位置。

  1. 使用带有匿名插槽的卡片组件:在 App.svelte 中使用:
<script>
    import Card from './Card.svelte';
</script>

<Card title="My Card">
    <p>This is the content of the card.</p>
</Card>

<style>
    body {
        font - family: Arial, sans - serif;
    }
</style>

这里在 <Card> 标签内插入了一个 <p> 元素,这个 <p> 元素的内容会显示在 Card.svelte 组件中 <slot> 的位置,同时卡片的标题会显示为 My Card

具名插槽示例

  1. 创建带有具名插槽的卡片组件:有时我们可能需要在组件内部有多个不同的插槽位置,这就需要用到具名插槽。修改 Card.svelte 文件如下:
<script>
    export let title = 'Default Title';
</script>

<div class="card">
    <h2>{title}</h2>
    <slot name="header"></slot>
    <slot></slot>
    <slot name="footer"></slot>
</div>

<style>
   .card {
        border: 1px solid gray;
        border - radius: 5px;
        padding: 10px;
    }
</style>

这里创建了三个插槽,一个匿名插槽和两个具名插槽 headerfooter

  1. 使用带有具名插槽的卡片组件:在 App.svelte 中使用:
<script>
    import Card from './Card.svelte';
</script>

<Card title="My Card">
    <div slot="header">
        <p>This is the header content.</p>
    </div>
    <p>This is the main content of the card.</p>
    <div slot="footer">
        <p>This is the footer content.</p>
    </div>
</Card>

<style>
    body {
        font - family: Arial, sans - serif;
    }
</style>

在使用 Card 组件时,通过 slot 属性指定了不同部分的内容应该插入到哪个具名插槽中。<div slot="header"> 中的内容会插入到 Card.sveltename="header" 的插槽位置,同理,footer 部分也会插入到对应的插槽位置。

组件复用与逻辑抽象

抽象通用逻辑到组件

  1. 以表单验证组件为例:假设我们有一个通用的表单验证需求,比如验证输入是否为有效的电子邮件地址。我们可以创建一个 EmailValidator.svelte 组件。
<script>
    export let value = '';
    let isValid = () => {
        const emailRegex = /^[a-zA - Z0 - 9_.+-]+@[a-zA - Z0 - 9 -]+\.[a-zA - Z0 - 9-.]+$/;
        return emailRegex.test(value);
    };
</script>

{#if isValid()}
    <p style="color: green;">Valid email</p>
{:else}
    <p style="color: red;">Invalid email</p>
{/if}

在这个组件中,定义了一个 value 属性用于接收输入的值,以及一个 isValid 函数来验证输入是否为有效的电子邮件地址。根据验证结果显示不同的提示信息。

  1. 在表单组件中复用验证组件:创建一个 Form.svelte 组件来使用 EmailValidator.svelte 组件。
<script>
    import EmailValidator from './EmailValidator.svelte';
    let emailValue = '';
</script>

<label for="email">Email:</label>
<input type="email" bind:value={emailValue} />
<EmailValidator value={emailValue} />

<style>
    label {
        display: block;
        margin - top: 10px;
    }
    input {
        width: 200px;
        padding: 5px;
        margin - top: 5px;
    }
</style>

Form.svelte 中,引入了 EmailValidator.svelte 组件,并将 input 元素的值绑定到 emailValue 变量,同时将 emailValue 作为 value 属性传递给 EmailValidator.svelte 组件,实现了表单输入的电子邮件验证功能。

使用 Svelte 动作(Actions)复用行为

  1. 创建一个防抖(Debounce)动作:防抖是一种常见的行为,用于防止频繁触发某个事件。例如,在搜索框中,我们不希望用户每次输入一个字符都触发搜索请求,而是在用户停止输入一段时间后再触发。创建一个 debounce.js 文件来定义防抖动作:
export function debounce(node, delay = 300) {
    let timer;
    node.addEventListener('input', () => {
        clearTimeout(timer);
        timer = setTimeout(() => {
            node.dispatchEvent(new CustomEvent('debouncedInput'));
        }, delay);
    });
    return {
        destroy() {
            clearTimeout(timer);
        }
    };
}

这个防抖动作接收一个 DOM 节点 node 和一个延迟时间 delay(默认 300 毫秒)。它在 input 事件触发时清除之前的定时器,并设置一个新的定时器,在延迟时间后触发一个自定义的 debouncedInput 事件。

  1. 在输入框组件中使用防抖动作:创建一个 SearchInput.svelte 组件来使用这个防抖动作。
<script>
    import { debounce } from './debounce.js';
    let searchValue = '';
    let handleSearch = () => {
        console.log('Searching with value:', searchValue);
    };
</script>

<input type="text" bind:value={searchValue} use:debounce on:debouncedInput={handleSearch} />

<style>
    input {
        width: 200px;
        padding: 5px;
    }
</style>

SearchInput.svelte 组件中,通过 use:debounce 将防抖动作应用到 input 元素上,并在 debouncedInput 事件触发时调用 handleSearch 函数,这样就实现了在输入框中输入时的防抖功能,避免了频繁触发搜索逻辑。

组件库的构建与复用

构建本地组件库

  1. 目录结构规划:假设我们要构建一个本地的 Svelte 组件库,首先规划好目录结构。创建一个 components 目录作为组件库的根目录,在其中可以根据组件类型再创建子目录,例如 buttonscards 等。
components/
    buttons/
        Button.svelte
    cards/
        Card.svelte
  1. 导出组件:在 components 目录下创建一个 index.js 文件,用于导出所有的组件,方便在其他项目中引入。
export { default as Button } from './buttons/Button.svelte';
export { default as Card } from './cards/Card.svelte';
  1. 在项目中使用本地组件库:在其他 Svelte 项目中,可以通过相对路径引入这个本地组件库。例如,在 App.svelte 中:
<script>
    import { Button, Card } from '../components';
</script>

<Button text="Local Button" />
<Card title="Local Card">
    <p>This is local card content.</p>
</Card>

<style>
    body {
        font - family: Arial, sans - serif;
    }
</style>

这样就可以方便地在项目中复用本地组件库中的组件。

发布和使用开源组件库

  1. 发布到 npm:要将 Svelte 组件库发布到 npm,首先需要在组件库项目根目录下初始化 package.json 文件。
npm init -y

然后在 package.json 文件中配置相关信息,如 nameversionmain 等。main 字段通常指向 components/index.js 文件。

{
    "name": "my - svelte - components",
    "version": "1.0.0",
    "main": "components/index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [
        "svelte",
        "components"
    ],
    "author": "Your Name",
    "license": "MIT"
}

接着,确保组件库代码没有语法错误,然后使用以下命令发布到 npm:

npm publish
  1. 在项目中安装和使用开源组件库:在其他 Svelte 项目中,可以通过 npm install 安装已发布的组件库。
npm install my - svelte - components

然后在 App.svelte 中引入并使用:

<script>
    import { Button, Card } from'my - svelte - components';
</script>

<Button text="Open - source Button" />
<Card title="Open - source Card">
    <p>This is open - source card content.</p>
</Card>

<style>
    body {
        font - family: Arial, sans - serif;
    }
</style>

通过这种方式,可以在不同的项目中方便地复用开源的 Svelte 组件库。

组件复用中的注意事项

样式隔离与冲突

  1. Svelte 的样式隔离:Svelte 组件的样式默认是隔离的,即每个组件的 <style> 标签内定义的样式只作用于该组件内部的元素。例如,在 Button.svelte 组件中定义的按钮样式不会影响到其他组件中的按钮。
<script>
    export let text = 'Click me';
    let handleClick = () => {
        console.log('Button clicked!');
    };
</script>

<button on:click={handleClick}>
    {text}
</button>

<style>
    button {
        background-color: blue;
        color: white;
        padding: 10px 20px;
        border: none;
        border - radius: 5px;
    }
</style>

这里按钮的样式只会应用到 Button.svelte 组件中的 <button> 元素,不会影响到其他地方的按钮。

  1. 避免样式冲突:然而,在复用组件时仍需注意潜在的样式冲突。如果在不同的组件中使用了相同的类名,可能会导致意外的样式覆盖。例如,如果在 Card.svelte 组件中也定义了一个 .button 类:
<script>
    export let title = 'Default Title';
</script>

<div class="card">
    <h2>{title}</h2>
    <button class="button">Card Button</button>
</div>

<style>
   .card {
        border: 1px solid gray;
        border - radius: 5px;
        padding: 10px;
    }
   .button {
        background-color: green;
        color: white;
        padding: 5px 10px;
        border: none;
        border - radius: 3px;
    }
</style>

如果在 App.svelte 中同时使用了 Button.svelteCard.svelte 组件,并且不小心将 Button.svelte 中的按钮也添加了 .button 类,就可能导致样式冲突。为了避免这种情况,可以使用更具特异性的类名,或者使用 CSS 预处理器的命名空间功能。

组件状态管理

  1. 组件内状态与共享状态:在复用组件时,需要明确组件内部状态和共享状态的管理。组件内部状态是指仅与该组件自身相关的状态,例如 Button.svelte 组件中的点击计数。
<script>
    export let text = 'Click me';
    let clickCount = 0;
    let handleClick = () => {
        clickCount++;
        console.log('Button clicked', clickCount, 'times');
    };
</script>

<button on:click={handleClick}>
    {text} ({clickCount})
</button>

<style>
    button {
        background-color: blue;
        color: white;
        padding: 10px 20px;
        border: none;
        border - radius: 5px;
    }
</style>

这里的 clickCount 就是 Button.svelte 组件的内部状态,它只影响该按钮组件自身的显示。

而共享状态是指多个组件可能需要访问和修改的状态。例如,在一个多页面应用中,用户登录状态可能需要在导航栏组件、用户资料组件等多个组件中共享。在这种情况下,通常需要使用状态管理库,如 Svelte 的官方状态管理库 svelte/store

  1. 使用 svelte/store 管理共享状态:假设我们有一个 UserStore.js 文件来管理用户登录状态:
import { writable } from'svelte/store';

export const userLoggedIn = writable(false);

Nav.svelte 组件中可以这样使用:

<script>
    import { userLoggedIn } from './UserStore.js';
</script>

{#if $userLoggedIn}
    <p>Welcome, user!</p>
{:else}
    <p>Please log in.</p>
{/if}

UserProfile.svelte 组件中也可以同样引入并使用这个共享状态,确保各个组件对于用户登录状态的显示和行为一致。

性能优化

  1. 避免不必要的重新渲染:Svelte 会自动跟踪组件的状态变化并进行重新渲染,但在复用组件时,我们仍需注意避免不必要的重新渲染。例如,如果一个组件接收的属性没有变化,却因为其父组件的重新渲染而导致自身重新渲染,这可能会影响性能。

假设我们有一个 ListItem.svelte 组件,用于显示列表项:

<script>
    export let itemText = '';
</script>

<li>{itemText}</li>

<style>
    li {
        list - style - type: none;
        padding: 5px;
    }
</style>

在一个 List.svelte 组件中使用:

<script>
    import ListItem from './ListItem.svelte';
    let items = ['Item 1', 'Item 2', 'Item 3'];
    let count = 0;
    let incrementCount = () => {
        count++;
    };
</script>

<button on:click={incrementCount}>Increment</button>
<ul>
    {#each items as item}
        <ListItem itemText={item} />
    {/each}
</ul>

<style>
    button {
        margin - bottom: 10px;
    }
</style>

在这个例子中,点击按钮 Increment 会导致 List.svelte 组件重新渲染,但实际上 ListItem.svelte 组件的 itemText 属性并没有变化,这种情况下可以使用 Svelte 的 {#key} 指令来优化。

修改 List.svelte 如下:

<script>
    import ListItem from './ListItem.svelte';
    let items = ['Item 1', 'Item 2', 'Item 3'];
    let count = 0;
    let incrementCount = () => {
        count++;
    };
</script>

<button on:click={incrementCount}>Increment</button>
<ul>
    {#each items as item (item)}
        <ListItem itemText={item} />
    {/each}
</ul>

<style>
    button {
        margin - bottom: 10px;
    }
</style>

这里通过 (item) 作为 #each 指令的 key,Svelte 会更智能地判断哪些 ListItem.svelte 组件需要重新渲染,从而提高性能。

  1. 懒加载组件:对于一些不常用或加载成本较高的组件,可以采用懒加载的方式。在 Svelte 中,可以使用动态导入(Dynamic Imports)来实现懒加载。

假设我们有一个 BigChart.svelte 组件,用于显示复杂的图表,加载成本较高。在 App.svelte 中可以这样懒加载:

<script>
    let showChart = false;
    let ChartComponent;
    const loadChart = async () => {
        ChartComponent = (await import('./BigChart.svelte')).default;
    };
</script>

<button on:click={() => {
    showChart = true;
    loadChart();
}}>Show Chart</button>

{#if showChart && ChartComponent}
    <ChartComponent />
{/if}

<style>
    button {
        margin - top: 10px;
    }
</style>

这里通过点击按钮触发 loadChart 函数,动态导入 BigChart.svelte 组件,只有在用户需要显示图表时才会加载该组件,从而提升应用的初始加载性能。