前端应用架构设计中的简单模式和复杂模式

3,639 阅读5分钟

前端应用是个很宽泛的概念, 本文聚焦的是单纯的 web 应用, 通俗的讲就是 spa 应用. 大部分应用研发的框架, 入早期的 Angular Backbone 都会以一个 todomvc 来说明框架如何满足应用架构设计. 在这篇文章中, 我同样以 todomvc 为切入点, 结合我们自研的应用研发框架 Rdeco 来分享.

关于前端应用架构的概念, 在我之前的文章中有提到, 经过一段时间的讨论和沉淀, 我们目前倾向于将前端应用架构分为三种不同的模式, 分别对应现在前端应用研发上的三种复杂度.

这三种模式分别是

  1. 单体应用下的简单模式

  2. 复杂应用下模块分层的复杂模式

  3. 多应用集成下的微模块模式

关于第三种后面单开文章来讲, 就目前大多数的前端应用研发场景而言, 主要还是前 2 种模式居多.

简单模式

前端应用最初的形态称不上应用, 只是一段 JavaScript 脚本, 前端应用这个概念是随着前端工程化发展和应用研发模式逐步成熟而演变过来的.

现有的前端应用架构模式通常指 mvc flux 或者 mmvm 等但本质上其核心都是 mvc, 唯一的区别是 m 的多寡, 比如中心化的 single store 又或者多 store 等等.这些模式我们都将其归类为复杂模式, 即应用模块分布在不同的分层中, 层与层之间依赖框架提供的方式互相调用与集成.

复杂模式的实现方式非常多. 让我们后面在讨论, 回到简单模式上, 我们对简单模式的定义是所有的分层都只在单一应用内实现, 对外并不暴露应用的内部分层.

以 todomvc 为例, todomvc 包含了 curd 的基本操作, 如果将 todomvc 看做一个内聚的单一应用或者组件, 那么代码大致是这样的

import React from 'react'
import { createComponent } from 'rdeco'

export default createComponent({
  name: 'todomvc',
  state: {
    todolist: [
      { text: '起床', check: false },
      { text: '吃饭', check: false },
      { text: '睡觉', check: false },
    ],
    newTodoValue: '',
  },
  controller: {
    onChange(e, index) {
      this.state.todolist[index].check = e.target.checked
      this.setter.todolist(this.state.todolist)
    },
    onNewTodoChange(e) {
      this.setter.newTodoValue(e.target.value)
    },
    onDeleteClick(index) {
      this.setter.todolist(
        this.state.todolist.filter((v, i) => {
          return i !== index
        })
      )
    },
    onClick() {
      this.state.todolist.push({
        check: false,
        text: this.state.newTodoValue,
      })
      this.setter.todolist(this.state.todolist)
      this.setter.newTodoValue('')
    },
  },
  view: {
    render() {
      return (
        <div className="App">
          <ul>
            {this.state.todolist.map((todo, index) => {
              return (
                <li key={todo.text}>
                  <input
                    onChange={(e) => this.controller.onChange(e, index)}
                    checked={todo.check}
                    type="checkbox"
                  />
                  {todo.text}
                  <button onClick={() => this.controller.onDeleteClick(index)}>
                    删除
                  </button>
                </li>
              )
            })}
          </ul>
          <input
            type="text"
            onChange={this.controller.onNewTodoChange}
            value={this.state.newTodoValue}
          />
          <br />
          <br />
          <button onClick={this.controller.onClick}>添加待办事项</button>
        </div>
      )
    },
  },
})

在线示例: codesandbox.io/s/romantic-…

简单模式的好处是组件内部的分层同样清晰, 数据层 → state, 响应交互的控制层 → controller, 以及用于渲染的视图层 → view

同时因为各层之间共享组件上下文 this, 这让各层之间的互操作变得非常方便. 简单模式对于不需要暴露内部分层细节的场景是非常适用的.

但真实应用场景下, 很少有业务应用用简单模式就可以满足, 主要原始因为完全内聚的前端组件/应用的场景是非常少的.

通常我们会要求数据能够被持久化到服务端, 服务端的接口设计通常并不考虑前端的场景, 这导致数据接口往往会被不同的视图共享, 当多个视图共享同一块服务端数据的时候, 简单模式会让数据处理的逻辑变得冗余, 同时组件和组件的直接联系让组件本身的逻辑也变得不那么纯粹.

但最致命的问题往往是, 组件直接依赖另一个组件的内部数据, 会导致一些意想不到的 bug, 例如这样的场景

截屏2021-11-26 下午2.57.28.png

有两个 Table 依赖同一个服务端接口的源数据, 为了节省接口请求, Table B 可能直接从 Table A 上获取数据, 但如果 Table A 意外修改了源数据, 可能导致 Table B 发生 bug.

但如果 Table B 也从服务端获取数据, 那么就会造成请求的冗余, 性能和效率都会被影响. 因此基于组件内聚的简单模式在这种场景下是不满足的. 于是就需要复杂模式

复杂模式

复杂模式的核心是将分层从单一的模块扩大到整个应用级, 在应用内实现分层模块的有序的互操作.

截屏2021-11-26 下午3.03.10.png

对于上面的示例中的 todomvc 来说, 就是将围绕 todolist 的操作剥离出来重构成一个独立的 model, 然后利用 localStorage 来模拟 service 层, 让 model 和 localStorage 互操作, 将服务端和视图层之间隔离开来, 先来看代码

import React from "react";
import { create, createComponent } from "rdeco";

const todomvcService = new Promise((resolve, reject) => {
  setTimeout(() => {
    if (localStorage.getItem("todolist")) {
      resolve(JSON.parse(localStorage.getItem("todolist")));
    } else {
      resolve([
        { text: "起床", check: false },
        { text: "吃饭", check: false },
        { text: "睡觉", check: false }
      ]);
    }
  }, 100);
});

const todomvcModel = create({
  name: "todomvc-model",
  state: {
    todolist: []
  },
  subscribe: {
    "todomvc-model": {
      state: {
        todolist({ nextState }) {
          localStorage.setItem("todolist", JSON.stringify(nextState));
        }
      }
    }
  },
  controller: {
    onMount() {
      todomvcService.then((data) => {
        this.setter.todolist(data);
      });
    },
    onCompleteTodo(index, checked) {
      this.state.todolist[index].check = checked;
      this.setter.todolist(this.state.todolist);
    },
    onAddTodo(text) {
      this.state.todolist.push({
        text,
        check: false
      });
      this.setter.todolist(this.state.todolist);
    },
    onDeletTodo(index) {
      this.setter.todolist(
        this.state.todolist.filter((v, i) => {
          return i !== index;
        })
      );
    }
  }
});

export default createComponent({
  name: "todolist",
  subscribe: {
    "todomvc-model": {
      state: {
        todolist({ nextState }) {
          this.setter.todolist(nextState);
        }
      }
    }
  },
  state: {
    todolist: todomvcModel.state.todolist,
    newTodoValue: ""
  },
  controller: {
    onChange(e, index) {
      todomvcModel.controller.onCompleteTodo(index, e.target.checked);
    },
    onNewTodoChange(e) {
      this.setter.newTodoValue(e.target.value);
    },
    onDeleteClick(index) {
      todomvcModel.controller.onDeletTodo(index);
    },
    onClick() {
      todomvcModel.controller.onAddTodo(this.state.newTodoValue);
      this.setter.newTodoValue("");
    }
  },
  view: {
    render() {
      return (
        <div className="App">
          <ul>
            {this.state.todolist.map((todo, index) => {
              return (
                <li key={todo.text}>
                  <input
                    onChange={(e) => this.controller.onChange(e, index)}
                    checked={todo.check}
                    type="checkbox"
                  />
                  {todo.text}
                  <button onClick={() => this.controller.onDeleteClick(index)}>
                    删除
                  </button>
                </li>
              );
            })}
          </ul>
          <input
            type="text"
            onChange={this.controller.onNewTodoChange}
            value={this.state.newTodoValue}
          />
          <br />
          <br />
          <button onClick={this.controller.onClick}>添加待办事项</button>
        </div>
      );
    }
  }
});


在线示例: codesandbox.io/s/romantic-…

重构之后 todomvc 的分层就从内聚的内部分层变成了外部分层

截屏2021-11-26 下午3.05.05.png

以 React 为例, 目前能够实现复杂模式的框架有很多, 经典的 redux mobx 又或者官方的自己的, 或者通过 useContext 结合 useReducer 都可以实现. 不过这些框架有一个问题需要注意的是, 应用架构的设计往往是从简单模式到复杂模式逐步演变过来的.

也就是我们通常无法在应用架构的初期就按照复杂模式来设计, 那样如果被设计的应用后期业务上没有足够的变化, 会导致额外的设计成本, 大概率会变成过度设计. 对于这一点, 我们在研发 Rdeco 的时候就考虑到了应用架构的演进问题, 因此引入了对象化的描述结构, 你可以对比前后两个版本的代码, todomvc 从简单模式到复杂模式的演进不是破坏式的, 而是一种软性过渡.

如果你对此感兴趣可以关注我们的项目 github.com/kinop112365… 👏🏻 start