【第715期】大型前端项目的架构

发布时间:2016-09-27 08:35:01      来源:搜狐

  前言

  上周天傍晚看到这篇的时候发觉分析的还挺仔细的,就花了一点时间把它转翻成简体中文的。早读君也第一次做这个事情,要是在转翻的过程中专业名词用的不对,还请提出来。本文得到原翻作者@benzwjian的授权。

  正文从这开始~

  现在很多前端工程师的招聘都要求有开发过大型项目(Large-Scale Application, LSA)的经验,我很好奇到底LSA的定义是什么呢?

  Addy Osmani刚好有一片文章就在讲他所认为的LSA的定义以其架构和经实现:“Patterns For Large-Scale Java Application Architecture”。Addy Osmani在Google担任前端工程师,是一位很有名的Java开发者。他所描述的LSA应该具备的东西,在我的工作项目里,或多或少有一些影子在,我想我做的东西应该也算符合LSA的定义吧。接下来我就开始翻译这篇文章,分享给前端的开发者们。

  本文翻译取得Addy Osmani的同意,全文如下:

  什么是“Large-Scale” Java Application?

  在开始之前,我们先来定义 Large-Scale Java application 中,“ Large-Scale”所代表的是什么意思?这个问题就算是多年经验的开发者也很难说清楚,而且每个人的见解都相当主观。

  我曾问几个开发者试图从中得到答案:有人说是应用程序的代码超过100,000行,有人说是java文件大小超过1MB。其实都不太正确,程序代码越多不代表应用越复杂,这些100,000行程序可能都是无关紧要的。

  我自己的定义可能不被接受,但我相信他却是反映大型应用应该有的样子:

  In my view, large-scale Java apps are non-trivial applications requiring significant developer effort to maintain, where most heavy lifting of data manipulation and display falls to the browser.

  先检查你目前的架构

  我想强调的一点-我遇到过一些做近似大型应用的开发者说:“在我现在的中型应用项目中将,有一些模式或实现应该可以套用在大型应用项目中吧?”。我觉得不要把他看作理所当然,因为在大型应用的开发里,有些架构设计时需要花时间去想想,我会简单的说明为什么你值得花一点点时间去好好计划你的大型项目。

  大部分的Java开发者可能会使用下面的组件去构建出程序的架构:

  custom widgets

  models

  views

  controllers

  templates

  libraries/toolkits

  an application core

  相关资料

  Rebecca Murphey — Structuring Java Applications

  Peter Michaux — MVC Architecture For Java Applications

  StackOverflow — A discussion on modern MVC frameworks

  Doug Neiner — Stateful Plugins and the Widget Factory

  将应用程序拆分成许多小组件是很好的做法,但存在某些潜在的问题值得思考:

  1.模块可重用性有多少

  一个模块是否可以独立存在?如果在你的项目里随便找一个模块,可否让我直接放在一个新的页面里就可以开始运行了呢?你也许会质疑这样做背后的理由,不过我还是建议你得考虑未来:如果你的公司决定多个应用里都加入某个相同的功能,是否可以很简单完成呢?例如,有人说:“我们的用户很喜欢我们开发的聊天功能,我们把这个功能也放到新的开发的协作应用上吧”。

  2.有多少模块需要依赖其他模块呢?

  他们是很紧密地依赖着吗?从模块的粒度来看,我知道不太可能会有个模块完全不依赖其他的模块。但在功能粒度级别上,一个功能可以是几个模块所构成,而且不需要依赖于任何模块。

  3.如果某模块失效了,你的应用还能运行吗?

  假设你正在开发一个像Gmail的应用,而你的webmail模块突然失去作用,这个时候其他部分的UI展示或其他功能,例如聊天模块,必须还能运行正常。另外,我曾经提过一种动态加载模块机制,例如在Gmail这个例子里,当应用初始化启动后,前提是代码是不包含聊天模块,但如果用户这个时候想到用聊天功能时,应用才会去开始加载这个聊天模块的代码来执行,而且不影响到其他功能。

  4.可以很容易地测试每个模块吗?

  当一个模块处于百万数量级的用户在使用,它很可能就会被重复利用在其他的应用里,这时候我们必须对他做完整的测试。当模块独立存在时,需要被测试,当模块被引用到应用里,更需要被测试,这样才可保证这模块的健壮。

  想远一点

  当在开发你的大型应用架构时,很重要的一点是多想远一点,不知是一个月或一年后,可能是更远。我知道很难猜测未来的发展,但还是有一些空间可以想象。

  开发者常常把DOM操作紧密地绑定在应用里,但就算使用模块化把代码分离封装起来,对长远维护一个应用来说,还是不好,因为有一点必须要考虑的:

  你可能会因为性能、安全或设计的考虑,打算从 Dojo, jQuery, Zepto 或 YUI 换成其他的框架。因为更换框架并不容易,这会导致需要高的转换成本。

  如果你是一名Dojo开发者,也许现在没有更好的转换目标(框架),但谁能预料将来就不会出现呢?

  对小型项目来说,可能不切实际,但对大型项目,自由地使用任何框架或库,从金钱或时间成本来说,是由极大的好处。检查你现在的架构,可能轻易转换框架而不用重写整个应用吗?

  有一些具有影响力的Java开发者都有提到我说的一些观念,在此我列出三个:

  “打造大型应用的诀窍不是在打造一型应用,而是将你的应用拆解成许多小的组件,然后再把他们组成你的应用。” — Justin Meyer, author JavaMVC

  “因为刚开始就无法预知这个应用未来的发展趋势,所以就坦然接受在不知道任合条件的情況下,开始你的设计。这个时候你就会花时间在考虑某些关键模块是否会在将来更变上。例如,当一个应用的某个通讯模块在未来有可能会改变与其他系统通讯的方式时,你就会想要做抽象化来适应设计的变更。” — Nicholas Zakas, author ‘High-performance Java websites’

  最后但并不重要:

  “组件之间的耦合性很高,其可重用性便低,而且很难再设计变更后却不影响到其他组件的运行。” — Rebecca Murphey, author of jQuery Fundamentals.

  这些原则对打造大型应用是很有必要而且经得起时间的考虑,需要谨记在心。

  想想我们新的架构要有什么

  我们想要一个低耦合的架构,它所包含的功能可以被拆解成许多独立的模块,而且不互相依赖着。模块间的通讯靠的是一个中间层来做信息的交换,比如我们有个线上面包店的应用,一条消息从某个模块发出来:“第42批面包已经做好准备开卖”,这时中间层就负责把这个消息转发给有兴趣的模块。

  中间层会解析模块发出来的消息,好让a)模块不能直接访问核心功能,和b)模块不用直接呼叫或调用其他的模块。这避免模块发生错误时而导致整个应用失效,并且提供一个机会可以重新启动失效的模块。

  另一层考虑的是安全性。一般来说,我们很少考虑应用内的安全性,我们认为只要控制好来自应用外部的访问权限就够了。其实不是,如果我不限制一个公开的聊天组件和写资料模块的访问权限,有些人便会利用组件的漏洞进行攻击,所以模块不应该可以访问任何东西,设计一个中间层来处理模块之间的访问权限会增加应用的安全性。

  提倡新的架构

  我们提出的新架构是由三个著名的设计模式组成:module,facade和mediator。

  在传统的模块化架构下,模块之间是直接互相通讯的,但在这新的架构下,它们只会发出事件通知(理想上,不需要知道系统内其他模块的存在)。

  Mediator模式时用来处理模块间的消息发布和订阅工作,而Facade模式是用来加强模块间的访问权限。

  接下来我将针对每个模式做详细说明:

  设计模式

  Module Theory

  Module Pattern

  Object Literan Notation

  CommonJS Modules

  Facade Pattern

  Mediator Pattern

  应用到你的架构中

  The Facade — Abstraction Of The Core

  The Mediator — The Application Core

  Tying It All Together

  Module Theory

  你可能已经在你现有项目架构下使用模块化,如果没有,这小节会做简单的说明。

  模块是组成强健应用的基础,每个模块的运行都只是单一的目的,而且还可以被替换。

  模块的依赖性可以透过自动化的加载来完成,不需要手动,是考虑到扩展性。在粒度上能自行运行的模块就尽量独立出来,就像Gmail的例子,由一个处理表情图片的模块可以被聊天模块或消息模块所引用。

  在架构里,一个模块是不需要知道其他模块在做什么,这个沟通的工作交由mediator来完成。模块只要让mediator知道现在发生了什么事件,剩下的就让mediator去通知其他对这个事件感兴趣的模块,也因此这种低耦合的设计,当某模块失去功能时,无需担心其他模块会受到影响。

  在Java中,有几种方式可以当作模块,像是module pattern 和object literals。如果你知道理解这部分,请直接跳到 CommonJS modules。

  The Module Pattern

  Module pattern 是一种常用的设计模式,它用closures来封装public/private的变量和方法,所有复杂的业务逻辑都留在closure空间里,只暴露public API到全局,并且避免命名空间混乱。

  不过在Java里,并没有任何保留字可以将变量设置成私有的,我们只是利用closure来模拟privacy的概念,让外界无法访问到它,所有私有的变量和方法只能在closure空间内被访问,而被函数返回的变量和方法才能被外部访问。

  下面有个购物车的例子,这个模块在全局里叫basketModule,模块里的basket array是私有的,只能通过public api访问到它,例如:addItem(),getItem()。

  这个模块返回一个组件,并且赋值给basketModule,所以可以想下面访问它:

  从历史回顾下,module pattern原本是在2003年由几个人发展出来的,其中包括Richard Cornford,之後再由 Douglas Crockford 和 Eric Miraglia 发扬光大。

  我们来看看 module pattern 在特定的框架下是如何应用的。

  Dojo

  Dojo通过dojo.declare提供一种近似类(class)的概念来达到module pattern的,例如我们想在命名空间store下新建一个模块basket:

  YUI

  这个例子是用来YUI来实现module pattern。

  jQuery

  相关资料

  Rebecca Murphey — Structuring Java Applications

  Peter Michaux — MVC Architecture For Java Applications

  StackOverflow — A discussion on modern MVC frameworks

  Doug Neiner — Stateful Plugins and the Widget Factory

  Object Literan Notation

  在对象字面量里,将一连串用逗号隔开的name/value组合放到大括号里({}),name 和 value 又以分号隔开,这样就完成了组件的封装,你可以看看下方的案例。

  如果模块沒有任何的私有属性或方法,很合适使用对象字面量。

  相关资料

  Rebecca Murphey — Using Objects To Organize Your Code

  Stoyan Stefanov — 3 Ways To Define A Java Class

  Ben Alman — Clarifications On Object Literals (There’s no such thing as a JSON Object)

  John Resig — Simple Java Inheritance

  CommonJS Modules

  在过去的一两年,我们常常听到 CommonJS,它主要是一个工作团队设计,制作原型和标准化的Java api。目前他们提出CommonJS AMD用来规范模块的设计,这个设计很简洁,我认为它会成为ES6模块组件标准的基础。

  一个CommonJS模块就是一个可重用的Java代码,它只是暴露组件给外部访问。有很多很好的教程来教你如何实现CommonJS模块,简单来说模块基本上包含两个东西:

  一个exports对象,其附加暴露对象

  一个require函数,其用来加载其他模块

  有一些不错的模块加载工具,其中我个人推荐RequireJS,而且这有一篇相关的好文。RequireJS让我们可以很简单的创建模块,并且容易使用非同步方式来加载模块。

  因为上面的例子无法在浏览器中运行,为了能让它也可以在浏览器中执行,我们再举一个简单的例子,我们要把图片转码成ASCII。

  这段代码无法创建一个模块scope,而且require会是undefined,exports对象必须创建,但如果使用XHR加载然后eval()它,就没有问题了。RequireJS都帮我们做好了,只要这样做:

  相关资料

  The CommonJS Module Specifications

  Alex Young — Demystifying CommonJS Modules

  Notes on CommonJS modules with RequireJS

  The Facade Pattern

  Facade pattern在如今的架构中扮演重要的角色。把Facade pattern看作是提供模块间互动的API接口,其背后隐藏着复杂的代码逻辑,让开发者不用去理会其内部的运行。只要API接口不变,你可以再你的模块内随意改用任何的框架,而不需要大心会影响到系统其他的部分。

  下方是一个很简单的例子,我们的模块有几个私有的函数,另外提供一个facade让外界来访问。

  下一段我们将介绍mediator pattern。Facade pattern 和Mediator pattern 的差别在Facade pattern 用来暴露已存在的函数,而Mediator pattern 则可以增加函数。

  The Mediator Pattern

  机场塔台控制中心是mediator pattern 最好的比喻。塔台决定哪架飞机可以升空或降落,所有的消息通讯都必须通过塔台,而不是飞机间自己决定。在一个系统内,模块之间直接互相交换消息,如果模块数量太多,就显得难以维护,这时引入mediator是绝佳时机。Mediator也像个中介,让模块间免于显示地互相调用,来达到解耦的目的。如果你曾应用过Observer pattern来实现事件广播机制,你就会发现mediator相对简单。

  让我们来看看模块如何与mediator互动的。

  把模块当成publisher,把mediator当作publisher和subscriber。模块1广播一个事件去通知mediator做事,mediator收到消息后通知模块2来完成模块1交代的事情,模块2完成工作后再回去通知mediator,在这同时,模块3页收到mediator的要求纪录日志。没有任何模块可以直接互相通讯。如果模块3中途执行失败或中间,mediator可以重启模块3然后继续完成任务,这样的解耦处理可以保证其他模块运行正常。

  Mediator的优势

  通过引入接口的概念,解放模块之间的耦合,让模块可以广播或接收信息而不需要系统其他部分的运行,可以很容易增加或移除功能。

  mediator的弊端

  模块间是间接沟通,所以会稍微降低性能。而且因为是广播与接收的机制,会增加排查问题的难度。

  下面是个例子:

  以下是一个实际案例:

  相关资料

  Stoyan Stefanov — Page 168, Java Patterns

  HB Stone — Java Design Patterns: Mediator

  Vince Huston — The Mediator Pattern (not specific to Java, but a concise)

  实际应用Facade

  我们把Facade当作一个抽象层放在mediator和模块之间,所有的模块只需要注意到Facade抽象层。这抽象层负责提供一致性的接口供模块访问,模块必须通过Facade才能与mediator通讯。

  此外,facade也扮演着安全卫士的作用,检查模块的访问权限。比如有个模块广播:“dataValidationCompletedWriteToDB”事件,这时候facade就会检查模块是否有对资料进行写入的权限,这么做是为了避免不必要的操作。

  实际应用Mediator

  Mediator在我们的架构中很重要,我们已经简单的讨论过它,再次在深入些。

  Mediator的主要职责在管理模块的生命周期,当它收到消息时,它必须决定如何响应,这牵扯到是否启动或停止某个模块。当模块一旦启动,它应该自动完成任务,而什么情况下,模块需要呗停止呢?如果Mediator监听到特定的模块发生异常,要有机制可以让模块重新启动然后继续去完成未完的工作,这么做是为了降低用户的反感。

  另外,从性能方面考虑,Mediator可以动态增加或删除模块,通常应用在当初始页面没有某个特定功能时,但需要在用户需要动态能加载进来时。

  异常管理也可以由Mediator来处理,模块出了广播业务逻辑需要的信息外,也可以广播异常的消息让Mediator来决定接下来的任务,例如停止或调用模块。这种中心式的异常管理,让逻辑处理归于一处,不散落在各模块里,便以维护。

  最后,把所有东西这在一起

  模块代表应用程序的一块块功能,他们负责发布消息通知程序,而且可以耦合一些操作DOM的工具,但最好不要依赖于系统内其他的模块。另外模块并不关心:

  哪些组件或模块的订阅

  执行的环境

  Facade 是一层抽象层,避免让模块与核心直接接触。它也负责模块处理权限的开关。

  Mediator(核心)是消息订阅/发布的管理者,它负责管理,启动和调用模块,适用于加载动态模块。

  这个架构让模块间的耦合度极低,所有可以被单独地测试或维护,并且很容易在其他的新页面或新项目中被重复利用,另外也带来动态加载或删除服务的好处。

  关于本文

  译者:@benzwjian

  繁体译文:https://medium.com/@benzwjian/大型前端專案的架構

  简体译者:@情封

  欢迎投稿到前端早读课

  投稿邮箱:[email protected](据:搜狐 )

相关热词搜索:模块 架构 功能 组件 开发者

免责声明

本站所有展示内容为网友投稿或转载各大媒体,仅为转播更多信息之目的,对于内容的真实性、准确性和合法性请网友自行辨别,贵州网对此不承担任何保证责任。如本站刊载内容有侵犯到您权益的地方,请联系我们[email protected]
关于我们  联系我们  免责声明  友情链接  贵州网LOGO  人员查询  广告刊例  本站域名  百度新闻