实践笔记: 解析前端框架中的设计模式 |青训营

56 阅读6分钟

一. 设计模式概览

前端框架中广泛应用的设计模式有许多种,它们有助于在开发过程中提高代码的可维护性、可扩展性和可重用性。以下是一些常见的前端设计模式,以及它们的优缺点和使用案例。
  1. MVC (Model-View-Controller) 模式:

    • 优点: 分离关注点,使代码更加清晰、可维护。模型用于管理数据和业务逻辑,视图负责展示数据,控制器处理用户输入和逻辑流程。
    • 缺点: 在复杂应用中,可能导致模块之间的紧密耦合,难以维护。
    • 案例: Angular.js 1.x 使用了经典的 MVC 模式。
  2. MVVM (Model-View-ViewModel) 模式:

    • 优点: 将视图和模型分离,ViewModel 处理数据绑定,实现了双向绑定,使视图自动响应模型的变化。
    • 缺点: 对于小型应用,引入双向绑定可能会增加复杂性。
    • 案例: Vue.js 和 Knockout.js 使用了 MVVM 模式。
  3. 单例模式:

    • 优点: 确保一个类只有一个实例,全局共享访问点,适用于状态管理、全局配置等场景。
    • 缺点: 可能会导致代码紧密耦合,滥用会导致代码难以维护。
    • 案例: 在 Vue.js 中,Vuex 使用单例模式来管理应用的状态。
  4. 观察者模式:

    • 优点: 解耦主题(被观察者)和观察者,当主题状态改变时,观察者会自动更新,用于事件处理和订阅/发布模型。
    • 缺点: 大量观察者可能会导致性能问题。
    • 案例: 在 React 中,组件之间通过 props 和状态来实现观察者模式。
  5. 策略模式:

    • 优点: 将算法和使用算法的代码解耦,易于扩展和维护。
    • 缺点: 需要针对不同的策略创建多个类,可能增加类的数量。
    • 案例: 在前端表单验证中,可以使用策略模式来实现不同的验证策略。
  6. 装饰者模式:

    • 优点: 动态地为对象添加新的功能,不改变其结构,避免了使用继承的复杂性。
    • 缺点: 可能导致类的数量增加,代码复杂化。
    • 案例: 在 React 中,高阶组件(HOC)就是一种装饰者模式的应用,用于给组件添加额外的功能。
  7. 工厂模式:

    • 优点: 将对象的创建和使用分离,隐藏了具体的创建逻辑。
    • 缺点: 可能会增加代码的复杂性,特别是在简单场景下使用时。
    • 案例: 在 Angular 中,依赖注入(DI)就是一种工厂模式的实现,用于创建和管理依赖。

不同的设计模式适用于不同的场景,选择适合项目需求的模式可以提高代码的可维护性和可扩展性。然而,滥用设计模式可能导致不必要的复杂性。

二. 一些设计模式的使用实例

以MVC和MVVM这两个设计模式为例

MVC 实例 :Web 应用程序:

比如开发一个简单的待办事项列表 Web 应用程序,要使用 MVC 模式来组织代码。

  1. Model(模型): 模型负责管理数据和业务逻辑。可以创建一个包含待办事项的数组,并在模型中实现添加、编辑和删除待办事项的方法。

  2. View(视图): 视图负责展示数据,并与用户交互。可以创建一个页面来显示待办事项列表,并在界面上添加按钮和输入框供用户操作。

  3. Controller(控制器): 控制器处理用户输入和逻辑流程。比如当用户点击添加按钮时,控制器会调用模型的添加方法来更新数据,然后通知视图更新显示。

MVVM 实例 :用户信息编辑:

假设使用 MVVM 模式开发一个用户信息编辑页面,用户可以在表单中编辑他们的个人信息。

  1. Model: 模型包含用户的个人信息数据,如姓名、电子邮件和地址。

  2. View: 视图展示用户信息的表单,每个表单字段都绑定到 ViewModel 中的对应属性。

  3. ViewModel: ViewModel 作为数据绑定的中间层,包含了与视图相关的逻辑。例如你可以在 ViewModel 中创建属性来与视图中的表单字段进行双向绑定,以便在用户编辑信息时保持同步。

这些实例展示了如何在不同领域中应用 MVC 和 MVVM 设计模式。这些模式有助于将代码分解成易于管理和扩展的部分,提高了代码的可维护性、可重用性。

三.设计模式代码示例(基于js和vue)

我写的以下代码演示了一个待办事项列表 Web 应用程序,使用 MVC 模式来组织代码。Model 负责管理数据和业务逻辑,View 负责展示数据和处理用户交互,Controller 负责处理用户输入和控制逻辑流程。

```// Model
class TodoModel {
  constructor() {
    this.todos = [];
  }

  addTodo(todoText) {
    const newTodo = { text: todoText, completed: false };
    this.todos.push(newTodo);
  }

  editTodo(index, newText) {
    this.todos[index].text = newText;
  }

  deleteTodo(index) {
    this.todos.splice(index, 1);
  }
}

// View
class TodoView {
  constructor(controller) {
    this.controller = controller;
    this.todoList = document.getElementById('todo-list');
    this.addTodoButton = document.getElementById('add-todo-button');
    this.todoInput = document.getElementById('todo-input');

    this.addTodoButton.addEventListener('click', () => {
      const todoText = this.todoInput.value;
      if (todoText) {
        this.controller.addTodoItem(todoText);
        this.renderTodoList();
      }
    });
  }

  renderTodoList() {
    this.todoList.innerHTML = '';
    this.controller.getTodos().forEach((todo, index) => {
      const todoItem = document.createElement('li');
      todoItem.textContent = todo.text;

      const editButton = document.createElement('button');
      editButton.textContent = 'Edit';
      editButton.addEventListener('click', () => {
        const newText = prompt('Edit todo:', todo.text);
        if (newText !== null) {
          this.controller.editTodoItem(index, newText);
          this.renderTodoList();
        }
      });

      const deleteButton = document.createElement('button');
      deleteButton.textContent = 'Delete';
      deleteButton.addEventListener('click', () => {
        this.controller.deleteTodoItem(index);
        this.renderTodoList();
      });

      todoItem.appendChild(editButton);
      todoItem.appendChild(deleteButton);
      this.todoList.appendChild(todoItem);
    });
  }
}

// Controller
class TodoController {
  constructor() {
    this.model = new TodoModel();
    this.view = new TodoView(this);
  }

  addTodoItem(todoText) {
    this.model.addTodo(todoText);
  }

  editTodoItem(index, newText) {
    this.model.editTodo(index, newText);
  }

  deleteTodoItem(index) {
    this.model.deleteTodo(index);
  }

  getTodos() {
    return this.model.todos;
  }
}

// Initialize the application
const todoApp = new TodoController();

```js

用vue实现:MVVM 实例:用户信息编辑: 使用 MVVM 模式开发一个用户信息编辑页面,其中用户可以在表单中编辑他们的个人信息。

```<template>
  <div>
    <form>
      <label for="name">Name:</label>
      <input type="text" id="name" v-model="viewModel.name" />

      <label for="email">Email:</label>
      <input type="email" id="email" v-model="viewModel.email" />

      <label for="address">Address:</label>
      <input type="text" id="address" v-model="viewModel.address" />

      <button @click="saveChanges">Save</button>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      viewModel: {
        name: '',
        email: '',
        address: '',
      },
    };
  },
  methods: {
    saveChanges() {
      // Perform logic to save changes using this.viewModel
    },
  },
};
</script>

```vue

在这个实例中Vue 组件的模板部分包含一个表单,表单中的输入字段通过 v-model 指令与组件的 viewModel 对象属性进行双向绑定。这样当用户在表单中编辑输入时,viewModel 中的属性会自动更新,反之亦然。

感想:

我的感想是,了解这些设计模式的核心思想可以帮助开发者更好地选择适合项目需求的模式,从而提高代码的可维护性、可重用性等等。

例如:

  • MVC 模式的核心思想: 分离关注点,将应用程序分为模型、视图和控制器。模型管理数据和业务逻辑,视图负责展示数据,控制器处理用户输入和逻辑流程。这种分离有助于减少代码的耦合性,使开发者能够更容易地修改和扩展各个部分。

  • MVVM 模式的核心思想: 将视图和模型分离,引入视图模型来处理数据绑定。视图模型负责管理视图所需的数据,并与视图进行双向绑定,使数据的变化自动更新视图。MVVM 的核心是数据驱动视图的概念,它在开发响应式和交互性强的应用程序中非常有用。

每个模式都有其适用的场景和优点,但在实际开发中,不必过于教条地遵循一个模式。最重要的是,了解不同的设计模式背后的核心思想,有助于我们更好地组织代码和维护项目。在不断学习实践的过程中,我们可以逐渐培养出更加灵活的编程思维。