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

Svelte 代码复用:高效利用自定义逻辑与组件

2023-01-137.4k 阅读

一、Svelte 中的代码复用概述

在前端开发中,代码复用是提高开发效率、减少重复劳动的关键策略。Svelte 作为一种新兴的前端框架,为代码复用提供了多种强大的机制,主要围绕自定义逻辑和组件展开。

(一)为什么代码复用至关重要

  1. 提高开发效率:避免重复编写相同或相似的代码片段,节省开发时间。例如,在一个电商应用中,商品列表展示部分可能在首页、分类页面等多处出现。如果能够复用这部分代码,开发人员就无需为每个页面重复构建相同的商品展示逻辑和样式。
  2. 便于维护:当需要对某一功能进行修改时,只需在复用的代码处进行改动,而不必在多个重复实现的地方逐一修改。假设商品展示模块需要添加新的信息字段,在复用代码的情况下,只需要在一处修改,所有使用该商品展示模块的地方都会自动更新。
  3. 增强代码可读性:复用代码使得项目结构更加清晰,将复杂的功能模块化,易于理解和管理。开发人员可以更快速地定位和理解各个功能模块的职责。

(二)Svelte 代码复用的主要途径

  1. 组件复用:Svelte 组件是代码复用的基础单元。通过创建可复用的组件,如按钮组件、表单组件等,可以在不同的页面或应用场景中重复使用。
  2. 自定义逻辑复用:Svelte 提供了如 stores、actions 和 directives 等机制来复用自定义逻辑,这些逻辑可以跨组件使用,增强了代码的灵活性和可维护性。

二、Svelte 组件复用

(一)创建可复用组件

  1. 基础组件结构 在 Svelte 中,组件是一个单独的 .svelte 文件,包含 HTML、CSS 和 JavaScript 代码。以下是一个简单的按钮组件示例:
<!-- Button.svelte -->
<button style="padding: 10px 20px; background-color: blue; color: white;">
  {#if label}
    {label}
  {:else}
    Click me
  {/if}
</button>

<script>
  export let label;
</script>

在这个示例中,我们创建了一个 Button.svelte 组件。通过 export let label 导出了一个名为 label 的变量,用于设置按钮的文本。如果没有传递 label,按钮将显示默认文本 “Click me”。

  1. 组件样式隔离 Svelte 组件的 CSS 是作用域内的,即每个组件的样式只影响该组件内部的元素,不会污染全局样式。这使得组件在复用过程中更加独立和安全。例如,在上述 Button.svelte 组件中,按钮的样式仅应用于该按钮组件内部,不会影响其他按钮或元素。

(二)在不同场景中复用组件

  1. 在页面中使用组件 假设我们有一个 Home.svelte 页面,需要使用上述的 Button.svelte 组件:
<!-- Home.svelte -->
<script>
  import Button from './Button.svelte';
</script>

<Button label="Custom Button" />

通过 import 语句导入 Button.svelte 组件,然后在页面中像使用 HTML 标签一样使用 <Button> 组件,并传递 label 属性。

  1. 组件嵌套复用 组件可以相互嵌套,实现更复杂的布局和功能。例如,我们创建一个 Card.svelte 组件,内部包含多个 Button.svelte 组件:
<!-- Card.svelte -->
<div style="border: 1px solid gray; padding: 10px;">
  <h2>Card Title</h2>
  <p>Card content here.</p>
  <Button label="Primary Action" />
  <Button label="Secondary Action" />
</div>

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

这样,Card.svelte 组件复用了 Button.svelte 组件,并且可以根据需求定制按钮的文本和行为。

(三)通过 props 定制复用组件

  1. 传递数据和行为 除了简单的文本属性,组件还可以通过 props 传递更复杂的数据和函数。例如,我们修改 Button.svelte 组件,使其可以接受一个点击事件处理函数:
<!-- Button.svelte -->
<button style="padding: 10px 20px; background-color: blue; color: white;" on:click={handleClick}>
  {#if label}
    {label}
  {:else}
    Click me
  {/if}
</button>

<script>
  export let label;
  export let handleClick;
</script>

Home.svelte 页面中使用时:

<!-- Home.svelte -->
<script>
  import Button from './Button.svelte';
  const handleButtonClick = () => {
    console.log('Button clicked!');
  };
</script>

<Button label="Click me" handleClick={handleButtonClick} />

通过传递 handleClick 函数,我们可以在不同场景下为按钮定制不同的点击行为,进一步增强了组件复用的灵活性。

  1. 默认 props 值 为了使组件在使用时更加方便,我们可以为 props 设置默认值。在 Button.svelte 组件中:
<!-- Button.svelte -->
<button style="padding: 10px 20px; background-color: {backgroundColor}; color: white;" on:click={handleClick}>
  {#if label}
    {label}
  {:else}
    Click me
  {/if}
</button>

<script>
  export let label;
  export let handleClick = () => {};
  export let backgroundColor = 'blue';
</script>

现在,如果在使用 Button.svelte 组件时没有传递 handleClickbackgroundColor,将使用默认值。这使得组件在简单场景下可以直接使用,而在复杂场景下又可以通过传递 props 进行定制。

三、Svelte 自定义逻辑复用

(一)Stores:状态管理与逻辑复用

  1. 什么是 Stores Stores 是 Svelte 中用于管理状态的一种机制,它允许我们在不同组件之间共享状态,实现逻辑复用。Stores 本质上是一个对象,包含一个 subscribe 方法,用于订阅状态的变化。

  2. 创建和使用 Stores 以下是一个简单的计数器 store 示例:

// counterStore.js
import { writable } from'svelte/store';

export const counter = writable(0);

在这个示例中,我们使用 writable 函数创建了一个名为 counter 的 store,初始值为 0。

在组件中使用这个 store:

<!-- CounterComponent.svelte -->
<script>
  import { counter } from './counterStore.js';
  let count;
  const unsubscribe = counter.subscribe((value) => {
    count = value;
  });
  const increment = () => {
    counter.update((n) => n + 1);
  };
  const decrement = () => {
    counter.update((n) => n - 1);
  };
  $: console.log('Count changed:', count);
  onDestroy(() => {
    unsubscribe();
  });
</script>

<div>
  <p>Count: {count}</p>
  <button on:click={increment}>Increment</button>
  <button on:click={decrement}>Decrement</button>
</div>

CounterComponent.svelte 组件中,我们通过 subscribe 方法订阅 counter store 的变化,并在组件销毁时取消订阅。通过 update 方法更新 store 的值,实现了计数器的逻辑复用。多个组件可以同时订阅这个 counter store,共享计数器的状态。

  1. 衍生 Stores Svelte 还提供了衍生 stores 的功能,通过现有 stores 创建新的 stores。例如,我们可以基于上述的 counter store 创建一个 doubleCounter store:
// counterStore.js
import { writable, derived } from'svelte/store';

export const counter = writable(0);
export const doubleCounter = derived(counter, ($counter) => $counter * 2);

在组件中使用 doubleCounter store:

<!-- DoubleCounterComponent.svelte -->
<script>
  import { doubleCounter } from './counterStore.js';
  let doubleCount;
  const unsubscribe = doubleCounter.subscribe((value) => {
    doubleCount = value;
  });
  onDestroy(() => {
    unsubscribe();
  });
</script>

<div>
  <p>Double Count: {doubleCount}</p>
</div>

doubleCounter store 的值会随着 counter store 的变化而自动更新,实现了更复杂的逻辑复用。

(二)Actions:复用 DOM 相关逻辑

  1. Actions 简介 Actions 是 Svelte 中一种可以附加到 DOM 元素上的自定义逻辑。它们允许我们在不同组件中复用与 DOM 操作相关的逻辑,如添加事件监听器、设置样式等。

  2. 创建和使用自定义 Actions 以下是一个简单的 focus action 示例,用于自动聚焦到输入框:

// focusAction.js
export function focus(node) {
  node.focus();
  return {
    destroy() {
      // 可以在这里进行清理操作,如移除事件监听器等
    }
  };
}

在组件中使用这个 action:

<!-- InputComponent.svelte -->
<script>
  import { focus } from './focusAction.js';
</script>

<input type="text" use:focus />

通过 use:focus,我们将 focus action 应用到输入框元素上,使得输入框在组件渲染时自动获得焦点。多个输入框组件可以复用这个 focus action,减少了重复的聚焦逻辑代码。

  1. 带参数的 Actions Actions 也可以接受参数,以实现更灵活的逻辑复用。例如,我们创建一个 highlight action,用于根据传入的颜色参数高亮显示元素:
// highlightAction.js
export function highlight(node, color) {
  node.style.backgroundColor = color;
  return {
    update(newColor) {
      node.style.backgroundColor = newColor;
    },
    destroy() {
      node.style.backgroundColor = '';
    }
  };
}

在组件中使用:

<!-- HighlightComponent.svelte -->
<script>
  import { highlight } from './highlightAction.js';
  let color = 'yellow';
  const changeColor = () => {
    color = 'lightblue';
  };
</script>

<div use:highlight={color}>
  <p>This div is highlighted with {color}</p>
  <button on:click={changeColor}>Change Color</button>
</div>

通过传递不同的颜色参数,highlight action 可以在不同组件中以不同的颜色高亮显示元素,实现了与 DOM 样式相关逻辑的复用。

(三)Directives:复用模板逻辑

  1. Directives 基础 Directives 是 Svelte 中用于复用模板逻辑的机制,它们以 $: 前缀开头,可以在模板中执行自定义逻辑。

  2. 创建和使用自定义 Directives 以下是一个简单的 uppercase directive 示例,用于将文本转换为大写:

// uppercaseDirective.js
export function uppercase(node) {
  const unsubscribe = new MutationObserver((mutations) => {
    for (const mutation of mutations) {
      if (mutation.type === 'childNodes' || mutation.type === 'attributes') {
        node.textContent = node.textContent.toUpperCase();
      }
    }
  }).observe(node, { childList: true, attributes: true });
  return {
    destroy() {
      unsubscribe.disconnect();
    }
  };
}

在组件中使用这个 directive:

<!-- UppercaseComponent.svelte -->
<script>
  import { uppercase } from './uppercaseDirective.js';
</script>

<p use:uppercase>this text will be uppercase</p>

通过 use:uppercase,我们将 uppercase directive 应用到 <p> 元素上,使得元素内的文本自动转换为大写。多个文本元素组件可以复用这个 uppercase directive,实现了模板逻辑的复用。

  1. 指令与响应式更新 Directives 可以与 Svelte 的响应式系统很好地配合。例如,我们可以创建一个 toggleClass directive,根据一个布尔值切换元素的类名:
// toggleClassDirective.js
export function toggleClass(node, { className, condition }) {
  const updateClass = () => {
    if (condition) {
      node.classList.add(className);
    } else {
      node.classList.remove(className);
    }
  };
  updateClass();
  return {
    update(newProps) {
      condition = newProps.condition;
      updateClass();
    }
  };
}

在组件中使用:

<!-- ToggleClassComponent.svelte -->
<script>
  import { toggleClass } from './toggleClassDirective.js';
  let isActive = true;
  const toggle = () => {
    isActive =!isActive;
  };
</script>

<div use:toggleClass={{ className: 'active', condition: isActive }}>
  <p>Toggle class based on {isActive? 'active' : 'inactive'}</p>
  <button on:click={toggle}>Toggle</button>
</div>

<style>
 .active {
    background-color: lightgreen;
  }
</style>

isActive 的值发生变化时,toggleClass directive 会自动更新元素的类名,实现了根据条件复用样式切换逻辑。

四、代码复用中的注意事项

(一)组件耦合度控制

  1. 避免过度耦合 在复用组件时,要注意避免组件之间过度耦合。过度耦合会导致组件的可维护性和复用性降低。例如,如果一个组件直接依赖于另一个组件的内部状态或实现细节,当被依赖组件发生变化时,依赖它的组件也需要进行大量修改。为了避免这种情况,组件之间应该通过 props 和 events 进行通信,保持相对独立。

  2. 保持组件职责单一 每个组件应该有单一的职责,这样可以提高组件的复用性和可维护性。例如,一个按钮组件应该专注于按钮的外观和点击行为,而不应该包含与按钮无关的业务逻辑,如数据获取或复杂的状态管理。如果将过多的职责堆积在一个组件中,当需要复用该组件时,可能会发现很多功能是多余的,而且修改其中一个功能可能会影响到其他功能。

(二)自定义逻辑的边界与清理

  1. 明确逻辑边界 在使用 stores、actions 和 directives 等自定义逻辑时,要明确其作用范围和边界。例如,一个 store 应该只管理与其相关的状态,避免管理过多无关的状态,导致逻辑混乱。对于 actions 和 directives,要确保它们只对目标 DOM 元素或组件起作用,不影响其他不相关的部分。

  2. 资源清理 对于一些需要占用资源的自定义逻辑,如事件监听器、定时器等,要在适当的时候进行清理。例如,在 action 或 directive 的 destroy 方法中,应该移除添加的事件监听器或清除定时器,以避免内存泄漏。在使用 store 时,如果组件不再需要订阅某个 store 的变化,应该及时取消订阅,以减少不必要的计算和资源消耗。

(三)版本兼容性与维护

  1. 关注框架版本 随着 Svelte 框架的不断发展,其 API 和功能可能会发生变化。在复用代码时,要关注 Svelte 的版本兼容性,确保复用的代码在当前项目所使用的 Svelte 版本中能够正常工作。如果项目升级 Svelte 版本,要及时检查复用代码是否需要进行相应的调整。

  2. 定期维护复用代码 复用代码可能会在多个项目或场景中使用,因此需要定期进行维护。及时修复复用代码中的 bug,更新其功能以适应新的需求和技术发展。同时,要保持复用代码的文档化,以便其他开发人员能够快速理解和使用。

五、结合实际项目场景的代码复用案例

(一)电商项目中的组件复用

  1. 商品列表组件 在电商项目中,商品列表是一个常见的复用组件。我们可以创建一个 ProductList.svelte 组件,用于展示商品列表:
<!-- ProductList.svelte -->
<ul style="list-style-type: none; padding: 0;">
  {#each products as product}
    <li style="border: 1px solid gray; padding: 10px; margin-bottom: 10px;">
      <h3>{product.name}</h3>
      <p>{product.description}</p>
      <p>Price: ${product.price}</p>
      <Button label="Add to Cart" on:click={() => addToCart(product)} />
    </li>
  {/each}
</ul>

<script>
  import Button from './Button.svelte';
  export let products = [];
  const addToCart = (product) => {
    // 实际的添加到购物车逻辑
    console.log(`Added ${product.name} to cart`);
  };
</script>

这个 ProductList.svelte 组件复用了之前创建的 Button.svelte 组件,并且通过 products props 接受商品数据列表进行展示。在首页、分类页面等需要展示商品列表的地方,都可以复用这个组件。

  1. 购物车组件 购物车组件也是电商项目中重要的复用组件。我们创建一个 Cart.svelte 组件:
<!-- Cart.svelte -->
<div style="border: 1px solid gray; padding: 10px;">
  <h2>Cart</h2>
  <ul style="list-style-type: none; padding: 0;">
    {#each cartItems as item}
      <li style="padding: 5px;">
        {item.product.name} - ${item.product.price} x {item.quantity}
        <Button label="Remove" on:click={() => removeFromCart(item)} />
      </li>
    {/each}
  </ul>
  <p>Total: ${calculateTotal()}</p>
</div>

<script>
  import Button from './Button.svelte';
  import { cartStore } from './cartStore.js';
  let cartItems;
  const unsubscribe = cartStore.subscribe((value) => {
    cartItems = value;
  });
  const removeFromCart = (item) => {
    // 实际的从购物车移除商品逻辑
    cartStore.update((cart) => cart.filter((cartItem) => cartItem!== item));
  };
  const calculateTotal = () => {
    return cartItems.reduce((total, item) => total + item.product.price * item.quantity, 0);
  };
  onDestroy(() => {
    unsubscribe();
  });
</script>

Cart.svelte 组件复用了 Button.svelte 组件,并且通过订阅 cartStore 来获取购物车中的商品列表。它展示了购物车中的商品信息、数量和总价,并提供了移除商品的功能。在购物车页面以及需要展示购物车摘要的地方,都可以复用这个组件。

(二)自定义逻辑在项目中的应用

  1. 用户登录状态管理 在电商项目中,用户登录状态管理是一个关键的自定义逻辑。我们可以创建一个 authStore.js
// authStore.js
import { writable } from'svelte/store';

export const isLoggedIn = writable(false);
export const user = writable(null);

export const login = (userData) => {
  isLoggedIn.set(true);
  user.set(userData);
};

export const logout = () => {
  isLoggedIn.set(false);
  user.set(null);
};

在各个需要根据用户登录状态进行逻辑控制的组件中,都可以复用这个 authStore。例如,在导航栏组件中:

<!-- Navbar.svelte -->
<script>
  import { isLoggedIn, login, logout } from './authStore.js';
  let loggedIn;
  const unsubscribe = isLoggedIn.subscribe((value) => {
    loggedIn = value;
  });
</script>

<nav style="background-color: lightgray; padding: 10px;">
  {#if loggedIn}
    <p>Welcome, {userData.name}</p>
    <Button label="Logout" on:click={logout} />
  {:else}
    <Button label="Login" on:click={() => {
      // 实际的登录逻辑,调用 login 函数
      const userData = { name: 'John Doe' };
      login(userData);
    }} />
  {/if}
</nav>

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

通过复用 authStore,导航栏组件可以根据用户登录状态显示不同的内容,实现了用户登录状态相关逻辑的复用。

  1. 输入框验证逻辑 在电商项目的注册和登录表单中,输入框验证是一个常见的逻辑。我们可以创建一个 validateInput action:
// validateInputAction.js
export function validateInput(node, { type, minLength = 0 }) {
  const validate = () => {
    let isValid = true;
    if (type === 'email') {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      isValid = emailRegex.test(node.value);
    } else if (type === 'password' && node.value.length < minLength) {
      isValid = false;
    }
    if (!isValid) {
      node.style.borderColor ='red';
    } else {
      node.style.borderColor = 'gray';
    }
    return isValid;
  };
  node.addEventListener('input', validate);
  return {
    destroy() {
      node.removeEventListener('input', validate);
    }
  };
}

在注册表单组件中使用这个 action:

<!-- RegisterForm.svelte -->
<script>
  import { validateInput } from './validateInputAction.js';
  let emailValid = true;
  let passwordValid = true;
  const handleSubmit = () => {
    if (emailValid && passwordValid) {
      // 实际的注册逻辑
      console.log('Form submitted successfully');
    } else {
      console.log('Form has validation errors');
    }
  };
</script>

<form on:submit|preventDefault={handleSubmit}>
  <label>Email:</label>
  <input type="email" use:validateInput={{ type: 'email' }} bind:value={email} on:input={() => {
    emailValid = validateInput(node, { type: 'email' });
  }} />
  <label>Password:</label>
  <input type="password" use:validateInput={{ type: 'password', minLength: 6 }} bind:value={password} on:input={() => {
    passwordValid = validateInput(node, { type: 'password', minLength: 6 });
  }} />
  <Button label="Register" />
</form>

通过复用 validateInput action,注册表单组件中的输入框可以实现通用的验证逻辑,提高了代码的复用性和可维护性。

通过以上在电商项目中的实际案例,我们可以看到 Svelte 的组件复用和自定义逻辑复用机制在实际开发中能够极大地提高开发效率、增强代码的可维护性和复用性。无论是复杂的业务组件还是通用的逻辑模块,都可以通过合理的设计和复用,构建出高效、灵活的前端应用。在实际项目中,不断总结和提炼可复用的代码,将有助于团队更快地开发出高质量的产品。