Introduction看了一些讨论可扩展前端的文章,翻译记录。原文:ScalableFrontend#1—ArchitectureFundamentalsOrigin我的GitHub文本关于软件开发中术语“可扩展性”的两个最常见的含义,与代码库随时间的性能和可维护性有关。您可以同时拥有两者,但专注于良好的可维护性可以更轻松地优化性能而不影响应用程序的其余部分。更重要的是,在前端,与后端有一个重要的区别:本地状态。在本系列文章中,我们将讨论如何使用经过验证的方法开发和维护可扩展的前端应用程序。我们的大多数示例将使用React和Redux,但我们会经常与其他技术堆栈进行比较,以展示如何实现相同的效果。让我们从讨论软件中最重要的部分架构开始本系列。什么是软件架构?什么是软件架构?说它是软件中最重要的部分似乎有点夸张,但让我继续说下去。架构是您如何使软件的各个单元相互协作,以强调必须做出的最重要的决策,并推迟??次要的决策和实现细节。设计软架构意味着将实际应用与其支持的技术解耦。您的实际应用程序不了解数据库、AJAX请求或GUI;相反,它由代表您的软件所涵盖概念的用例和域单元组成,而不管执行用例的参与者或数据保存在何处。关于体系结构,还有另一件重要的事情要说:它并不意味着文件组织,也不意味着您如何命名文件和文件夹。前端开发中的层区分重要和次要的一种方法是使用层,其中每一层都有不同的特定职责。在分层架构中,一种常见的方式是将其分为四层:应用层(application)、领域层(domain)、基础设施层(infrastructure)和输入层(input)。这四个层在NodeJS和良好实践一文中有更好的解释。我们建议您在继续之前阅读本文的第一部分。您不必阅读第二部分,因为它是关于NodeJS的。领域层和应用层在前端和后端之间没有区别,因为它们与技术无关,但我们不能对输入层和基础设施层说同样的话。在网络浏览器中,输入层(视图)通常有一个actor,因此我们甚至可以将其称为视图层。此外,前端无法访问数据库或队列引擎,因此我们在前端基础设施层中找不到它们。我们会发现抽象封装了AJAX请求、浏览器cookie、LocalStorage,甚至是与WebSocket服务器交互的单元。主要区别只是抽象的内容,因此您甚至可以拥有界面完全相同但底层技术不同的前端和后端存储库。你能想象一个好的抽象有多棒吗?使用React、Vue、Angular或其他工具来创建视图并不重要。重要的是要遵循输入层没有任何逻辑的规则,以便将输入参数传递到下一层。对于基于分层架构的前端,还有一个重要的规则:要保持输入/视图层始终与本地状态同步,您应该遵循单向数据流。这个词是不是很耳熟?我们可以通过添加特定的第五层来做到这一点:状态,也称为存储。状态层在遵循单向数据流时,我们从不更改或转换直接从视图接收的数据。相反,我们从视图中发送“动作”。它是这样工作的:一个动作向数据源发送消息,数据源更新自身,然后用新数据重新呈现视图。请注意,从视图到商店从来没有直接路径,因此如果两个子视图使用相同的数据,您可以从其中任何一个分派一个操作,这将导致它们使用新数据重新呈现。看起来我是在专门谈论React和Redux,但事实并非如此;你几乎可以用任何现代前端框架或库实现同样的效果,比如React+contextAPI、Vue+Vuex、Angular+NGXS,甚至使用Ember的data-downaction-up方法(也称为DDAU).您甚至可以使用jQuery的事件系统发送动作!该层负责管理前端的本地和变化状态,例如从后端获取的数据,在前端创建但未持久化的临时数据,或请求状态等临时信息。如果您仍然想知道,这是负责更新状态的动作及其相应处理程序所在的层。虽然直接在actions中可以看到带有业务规则和用例定义的代码库是很常见的,但是如果你仔细阅读其他层的描述,你会发现我们已经有了放置用例和业务规则的地方,它不是状态层。这是否意味着我们的操作现在是用例?不!那么我们应该如何看待他们呢?让我们考虑一下......我们说动作不是用例,我们已经有一个层来放置用例。视图应该调度从视图接收信息的动作,将其交给用例,根据响应调度新的动作,最后更新状态——更新视图并结束单向数据流。动作听起来不像控制器吗?它们不只是一个从视图中获取参数、将它们传递给用例并根据用例的结果做出响应的地方吗?这就是你应该如何对待他们。这里不应该有复杂的逻辑或直接的AJAX调用,因为那些是另一层的责任。状态层应该只知道如何管理本地存储,仅此而已。还有另一个重要因素在起作用。由于状态层管理视图层使用的本地存储,您会注意到这两个存储以某种方式耦合。状态层中只有一些数据仅供视图使用,例如一个布尔标志,表示如果请求仍未决,则视图可以显示加载微调器,这非常好。不要为此自责,你不需要过度概括状态层。依赖注入(Dependencyinjection)好吧,分层很酷,但是它们如何相互通信呢?我们如何使一层不耦合地依赖另一层?是否可以在不执行它委托给的用例的情况下测试一个动作的所有可能输出?是否可以在不触发AJAX调用的情况下测试案例?当然,我们可以通过依赖注入来实现。依赖注入是一种在创建单元期间接收其耦合依赖作为参数的技术。例如,在其构造函数中接收类的依赖项,或使用React/Redux连接组件以存储数据并将所需的数据和操作作为props注入。这个理论并不复杂,对吧?实践应该也不复杂,所以我们以React/Redux应用程序为例。刚才我们说了使用React/Redux连接是一种实现view层和state层依赖注入的方式,简单明了。但是我们之前也说过,action将业务逻辑委托给用例,那么我们如何将用例(应用层)注入到action(状态层)中呢?假设您有一个对象,其中包含应用程序的每个用例的方法。这个对象通常被称为依赖容器。是的,这看起来很奇怪并且不能很好地扩展,但这并不意味着用例的实现就在这个对象内部。这些只是委托给在别处定义的用例的方法。在一个应用程序中,最好将所有用例集中在一个对象中,而不是将它们分散在整个代码库中并且很难找到它们。有了这个对象,我们需要做的就是将它注入到动作中,让它们各自决定触发什么用例,对吧?如果您使用的是redux-thunk,则使用withExtraArgument方法很容易实现,该方法允许您在每个thunk操作的getState之后将容器作为第三个参数注入。如果使用redux-saga,我们将容器作为run方法的第二个参数传递,这种做法应该很简单。如果你使用Ember或者Angular,那么内置的依赖注入机制应该就足够了。这样做会将操作与用例分离,因为您不需要在定义操作时在每个文件中手动导入用例。此外,现在可以非常容易地从用例中单独测试操作:只需注入一个模拟用例即可完全满足您的需求。如果测试用例失败,您是否希望在测试用例失败时派发什么动作?注入一个总是失败的模拟案例,并测试动作如何响应它。无需考虑实际用例如何工作。太好了,我们已经将状态层注入到视图层,并将应用层注入到状态层。剩下的呢?我们如何将依赖项注入用例以构建依赖项容器?这是一个重要的问题,有很多方法可以解决它。首先,不要忘记检查您使用的框架是否内置了依赖注入,例如Angular或Ember。如果它是内置的,您不应该自己制作。如果没有,您可以通过两种方式完成:手动或在包的帮助下。手动执行应该很简单:根据类或闭包定义单元,首先实例化那些没有依赖项的单元,实例化依赖它们的对象,将它们作为参数传递,重复上述步骤,直到实例化所有用例,导出它们。太抽像了?看几个代码示例:importapifrom'./infra/api';//没有依赖项import{validateUser}from'./domain/user';//没有依赖项importmakeUserRepositoryfrom'./infra/user/userRepository';importmakeArticleRepositoryfrom'./infra/article/articleRepository';importmakeCreateUserfrom'./app/user/createUser';importmakeGetArticlefrom'./app/article/getArticle';constuserRepository=makeUserRepository({api});constarticleRepository=makeArticleRepository({api});constcreateUser=makeCreateUser({userRepository,validateUser});constgetArticle=makeGetArticle({userRepository,articleRepository});export{createUser,getArticle};exportdefault({validateUser,userRepository})=>async(userData)=>{if(!validateUser(userData)){thrownewError('无效用户');}try{constuser=awaituserRepository.add(userData);返回用户;}catch(error){抛出错误;}};导出默认值({api})=>({异步cadd(userData){constuser=awaitapi.post('/users',userData);返回用户;}});您会注意到重要的部分-用例,在文件末尾实例化为导出单个对象,因为它们将被注入到操作中,其余代码不需要知道存储库是如何创建的和作品。这并不重要,这只是一个技术细节。存储库是发送AJAX请求还是在LocalStorage中保存某些东西对用例来说并不重要;这些不是用例的责任。如果您想在API仍在开发中时使用LocalStorage,然后切换到对实时API的调用,那么只要与API通信的代码遵循与LocalStorage通信的代码相同的接口,您就不需要无需更改您的用例。你可以像上面描述的那样手动执行注入,即使你有几十个用例、存储库、服务等。如果构建所有依赖项变得太乱,你总是可以使用依赖项注入包,只要它不增加耦合。测试DI包是否足够好的一个经验法则是检查从手动方法切换到使用库是否只需要接触容器代码。如果没有,那么这个包太麻烦了,你应该选择一个不同的包。如果你真的想使用包,我们推荐Awilix。它使用起来非常简单,您需要做的就是从手动方式触摸容器文件。这是软件包作者关于如何以及为什么使用它的一系列很好的文章。好吧,我们已经讨论了架构以及如何以一种很好的方式连接各层!在下一篇文章中,我们将展示一些真实的代码和我们刚才谈到的层的一般模式,状态层除外,这将在另一篇文章中介绍。花一些时间来吸收这些概念;当我们深入理解这些模式时,它们就会有用,一切都会变得更有意义。再见!推荐链接NodeJS和良好实践BobMartin—ArchitecturetheLostYearsRebeccaWirfs-Brock—WhyWeNeedArchitects(andArchitecture)onAgileProjectsDomain-DrivenDesignReferencesScalableFrontend#1—ArchitectureFundamentals
