一种不同的前端架构方法
本文旨在介绍一种易于推理且可维护性高的前端架构(适用于使用 Vue、React、Svelte 等构建的应用程序)。如果您正在构建一个中大型应用程序,并且经常感到困惑,那么本文可能会对您有所帮助。
良好架构的好处
在深入研究任何技术问题之前,让我们先解决一个小问题:
(图片来源: https: //pusher.com/tutorials/clean-architecture-introduction)
在上图中,你能一眼看出如何用胶带代替订书机吗?有些人可能会想出一个有趣的办法,但对于我们大多数人来说,我们无法立即想出如何解决这个问题。它在我们眼中看起来乱糟糟的,而且会让我们大脑混乱。
现在看看这个:
(图片来源: https: //pusher.com/tutorials/clean-architecture-introduction)
你现在能立刻告诉我怎么把订书机换掉吗?我们只需要解开连接订书机的绳子,然后把胶带放回去就行了。几乎不需要任何脑力劳动就能搞定。
想象一下,上图中的所有项目都是软件中的模块或部件。一个好的架构应该更像第二种布局。这种架构的好处是:
- 减少您在执行项目时的认知负荷/脑力劳动。
- 使您的代码更加模块化、松散耦合,从而更易于测试和维护。
- 简化替换架构中特定部分的过程。
通用前端架构
目前分离前端应用程序最基本、最常见的方法可能是这样的:
上面的架构乍一看没什么问题。但是,这种架构中出现了一种常见的模式,即把架构的某些部分紧密耦合在一起。例如,这是一个用 Vue 3 和 Vuex 4 编写的简单计数器应用程序:
<template>
<p>The count is {{ counterValue }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</template>
<script lang="ts">
import { computed } from 'vue';
import { useStore } from 'vuex';
export default {
name: 'Counter',
setup() {
const store = useStore();
const count = computed<number>(() => store.getters.count);
const increment = () => {
store.dispatch('increment');
};
const decrement = () => {
store.dispatch('decrement');
};
return {
count,
increment,
decrement
};
}
}
</script>
你会发现,这在使用 Vue 3 和 Vuex 编写的应用程序中是一种相当常见的模式,因为它在Vuex 4 的指南中有所介绍。实际上,这也是 React 与 Redux 或 Svelte 与 Svelte Stores 的常见模式:
- React 和 Redux 的示例:
import React, { useCallback } from 'react';
import { useSelector, useDispatch } from 'react-redux';
export const CounterComponent = () => {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
const increment = () => {
dispatch({ type: 'increment' });
};
const decrement = () => {
dispatch({ type: 'decrement' });
};
return (
<div>
<p>The count is {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
};
- Svelte 和 Svelte Stores 的示例:
<script>
import { count } from './stores.js';
function increment() {
count.update(n => n + 1);
}
function decrement() {
count.update(n => n - 1);
}
</script>
<p>The count is {$count}</p>
<button on:click={increment}>+</button>
<button on:click={decrement}>-</button>
这些本质上没有什么问题。事实上,大多数中大型应用程序可能都是这样写的。这些是官方指南/教程中推荐的方法。
然而,凡事皆有取舍。那么,这种模式到底有哪些优点和缺点呢?
最明显的好处可能就是简单。
但为了这个,你牺牲了什么?
您已经将 store 与组件紧密耦合。那么,如果有一天您的团队发现 Redux 不再适合该应用程序(可能是因为它过于复杂),并且想要切换到其他版本,该怎么办?您不仅需要重写所有 store,还需要重写与 Redux 紧密耦合的 React 组件的逻辑。
同样的问题也发生在应用程序中的所有其他层。最终,你无法轻易地用其他部分替换应用程序的某个部分,因为所有部分都紧密耦合在一起。最好的办法是保持现状,从头重写所有内容。
但事实并非如此。真正的模块化架构可以让你用 React + MobX(或 Valtio)替换你的 React + Redux 应用程序,甚至更疯狂的是,用 React + Vuex 或 Vue + Redux(无论出于何种原因)替换,而不会影响应用程序的其他部分。
那么,我们如何在不影响其余部分的情况下替换应用程序的一部分,或者换句话说,我们如何将应用程序的每个部分彼此分离?
引入不同的方法
- 表示层:这一层主要由 UI 组件构成。对于 Vue 来说,它们是 Vue SFc。对于 React 来说,它们是 React 组件。对于 Svelte 来说,它们是 Svelte SFC。等等。表示层直接与应用层耦合。
- 应用程序层:此层包含应用程序逻辑。它了解领域层和基础设施层。在此架构中,此层通过 React 中的 React Hooks 或 Vue 3 中的 Vue Hooks 实现。
- 领域层:此层用于领域/业务逻辑。领域层仅包含业务逻辑,因此这里只有纯 JavaScript/TypeScript 代码,没有任何框架/库。
- 基础设施层:此层负责与外界通信(发送请求/接收响应)并存储本地数据。以下是您在实际应用中将使用此层的库的示例:
- HTTP 请求/响应:Axios、Fetch API、Apollo Client 等。
- Store(状态管理):Vuex、Redux、MobX、Valtio 等
应用架构
如果将此架构应用于应用程序,它看起来是这样的:
从上面的架构图可以看出以下特点:
- 当您替换 UI 库/框架时,只有表示层和应用程序层会受到影响。
- 在基础架构层,我们有一个Facade,这样当您替换 store 的实现细节(例如,用 Vuex 替换 Redux)时,只有 store 本身会受到影响。用 Fetch API 替换 Axios 或反之亦然。应用层不知道 store 或 HTTP 客户端的实现细节。换句话说,我们将 React 与 Redux/Vuex/MobX 解耦。store 的逻辑也足够通用,不仅可以与 React 一起使用,还可以与 Vue 或 Svelte 一起使用。
- 如果业务逻辑发生变化,则必须相应地修改领域层,这将影响架构中的其他部分。
这个架构更有趣的是,你甚至可以进一步模块化它:
注意事项
尽管这种架构可以将应用程序的各个部分解耦,但它也带来了代价:复杂性增加。因此,如果你正在开发一个小型应用程序,我不建议使用这种架构。“莫用大锤砸核桃”。
对于更复杂的应用程序,这种架构可能有助于您实现如下目标:
(图片来源: https: //www.simform.com/react-architecture-best-practices)
一个例子
我构建了一个简单的计数器应用,演示了这种架构的优点。您可以在这里查看源代码:https://github.com/itswillta/flexible-counter-app。
在这个应用程序中,我引入了 Vue、React 以及 Vuex、Redux、MobX、Valtio 甚至 localStorage。它们都可以互相替换,互不影响。请按照 README 文件中的简单说明,尝试将应用程序的一部分替换为另一部分。
我知道对于这个计数器应用程序,我正在用大锤砸开一个坚果,但是构建一个复杂的应用程序对我来说现在有点不可能。
非常欢迎提问和讨论😊。
如果您对前端开发和 Web 开发感兴趣,请关注我并查看下面个人资料中我的文章。
文章来源:https://dev.to/itswillt/a- Different-approach-to-frontend-architecture-38d4