当前位置: 首页 > 后端技术 > Node.js

模式体系与最简单的Node.jsMVCWebServer设计

时间:2023-04-03 16:02:45 Node.js

学了这么久的设计模式。最近在看Node.js的设计模式。我一直在想为什么会有模式这样的东西。那么模式到底是什么呢?后来看了《面向模式的软件架构》,才渐渐明白自己有了一些系统的概念。什么是模式?当面对一个特定的问题时,专家们很少寻找与现有解决方案截然不同的新解决方案,而是通常会回忆以前已经解决过的类似问题,并将他们解决方案的精髓应用到这个新问题上。模式是通过从特定问题解决方案中提取共同因素获得的:这些问题解决方案通常是一系列熟悉的问题和解决方案,其中每一对问题解决方案都表现出相同的模式。Model-View-Controller模式MVC模式在现代软件开发过程中被广泛使用。为什么会存在MVC模式?让我们看一下这个例子:开发具有人机界面的软件。用户界面要求可能会发生变化。例如,当添加应用程序功能时,必须修改菜单以允许访问新功能,并且可能需要为特定客户定制用户界面。该系统可能需要移植到具有完全不同的“外观和感觉”标准的另一个平台。即使升级到新的窗口系统版本也可能需要修改代码。总之,如果系统的寿命很长,可能需要经常修改用户界面。在设计灵活的系统时,将用户界面与功能核心紧密交织在一起成本高昂且容易出错。这样做的结果是可能需要开发和维护几个非常不同的软件系统——每个用户界面实现一个,修改将涉及许多不同的模块。总之,在开发这样的交互式软件系统时,必须考虑以下两个方面:应该可以很容易地修改用户界面,可以在运行时完成;在调整或移植用户界面时,不应影响应用功能的核心代码。为了解决这个问题,一个交互式应用程序应该分为三个部分:处理、输出和输入。模型组件封装核心数据和功能,独立于输出表示和输入行为。视图(view)组件向用户显示信息。视图从模型中获取它显示的信息,一个模型可以有多个视图。每个视图都有一个关联的控制器组件。控制器接受输入,通常是表示鼠标移动、鼠标按钮激活或键盘输入的事件。事件被翻译成服务请求,服务请求被发送到模型或视图。用户仅通过控制器与系统交互。通过将模型与视图和控制器组件分开,允许同一模型的多个视图。如果用户通过视图的控制器修改模型,则此更改应反映在依赖相关数据的所有其他视图中。为此,每当模型的数据发生变化时,它都会通知所有视图,并且视图会从模型中检索新数据并更新显示的信息。该解决方案确保对应用程序的一个子系统的修改不会严重影响其他子系统。例如,可以在不修改模型子系统的情况下将非图形用户界面更改为图形用户界面,支持新的输入设备而不影响信息的显示和功能核心。所有的软件版本都可以依赖于相同的模型子系统,这与“外观”无关。使用模型-视图-控制器模式实现身份验证服务我们从下图所示的结构开始:上图显示了模型-视图-控制器模式的典型示例;它描述了一个简单的认证服务的结构。AuthController接受来自客户端的输入,从请求中提取登录信息,并执行一些初步验证。然后AuthService检查客户端提供的凭据是否与数据库中存储的信息相匹配;这最终是通过使用db模块执行一些特定查询作为与数据库通信的方式来完成的。这三个组件如何连接在一起将决定应用程序的可重用性、可测试性和可维护性。这里:模型(Model)指的是db模块,控制器(Controller)指的是AuthController和AuthService,视图是前端的用户界面,即HTML文档。将这些组件连接在一起的最自然的方式是通过AuthService请求db模块,然后从AuthController请求AuthService。让我们通过实际实施刚刚描述的系统来证明这一点。那么我们来设计一个简单的认证服务器,它会有以下两个HTTPAPI:POST'/login':接收一个包含用于认证的用户名和密码对的JSON对象。成功后,它会返回一个JSONWeb令牌(JWT),该令牌在后续请求中用于对用户进行身份验证。GET'/checkToken':检查用户是否有权限。对于这个例子,我们将使用几种技术;这些对我们来说都不是新的。我们使用express来实现WebAPI,使用levelup来存储用户数据。db模块我们从底层开始构建应用程序;首先实现levelUp数据库实例的模块。让我们创建一个名为lib/db.js的新文件,内容如下:constlevel=require('level');constsublevel=require('level-sublevel');module.exports=sublevel(level('example-db',{valueEncoding:'json'}));前面的模块是连接存放在./example-db目录下的LevelDB数据库,然后使用sublevel修改实例,通过它实现增删改查数据库。模块导出的对象是数据库对象本身。authService模块现在我们有了一个db单例,我们可以用它来实现lib/authService.js模块,它负责查询数据库,并根据用户凭据检查用户是否有权限。代码如下(只显示相关部分):"usestrict";constjwt=require('jwt-simple');constbcrypt=require('bcrypt');constdb=require('./db');constusers=db.sublevel('users');consttokenSecret='SHHH!';exports.login=(username,password,callback)=>{users.get(username,(err,user)=>{if(err)returncallback(err);bcrypt.compare(password,user.hash,(err,res)=>{if(err)returncallback(err);if(!res)returncallback(newError('无效密码'));lettoken=jwt.encode({username:username,expire:Date.now()+(1000*60*60)//1小时},tokenSecret);callback(null,token);});});};exports.checkToken=(token,callback)=>{让userData;try{//如果令牌无效,jwt.decode将抛出userData=jwt.decode(token,tokenSecret);if(userData.expire<=Date.now()){thrownewError('令牌过期');}}catch(err){返回process.nextTick(callback.bind(空,错误));}users.get(userData.username,(err,user)=>{if(err)returncallback(err);callback(null,{username:userData.username});});};authService模块实现了login()服务,负责查询数据库,检查用户名和密码信息,checkToken()服务接受token作为参数并验证其有效性。authController模块继续在应用程序级别,我们现在看一下lib/authController.js模块。该模块负责处理HTTP请求,本质上是Express路由的集合;该模块的代码如下:"usestrict";constauthService=require('./authService');exports.login=(req,res,next)=>{authService.login(req.body.username,req.body.password,(err,result)=>{if(err){returnres.status(401).send({ok:false,error:'无效的用户名/密码'});}res.status(200).send({ok:true,token:result});});};exports.checkToken=(req,res,next)=>{authService.checkToken(req.query.token,(err,result)=>{if(err){returnres.status(401).send({ok:false,error:'令牌无效或过期'});}res.status(200).send({ok:'true',user:result});});};authController模块实现了两条Express路由:login()用于执行登录操作并返回相应的token,checkToken()用于检查token的有效性。这两个路由将它们的大部分逻辑委托给authService,因此它们唯一的工作就是处理HTTP请求和响应。应用程序模块最后,在应用程序的入口点,我们调用我们的控制器。按照约定,我们将把这个逻辑放在项目根目录下一个名为app.js的模块中,如下所示:“usestrict”;constExpress=require('快递');constbodyParser=require('body-parser');consterrorHandler=require('errorhandler');consthttp=require('http');constauthController=require('./lib/authController');letapp=module.exports=newExpress();app.use(bodyParser.json());app.post('/login',authController.login);app.get('/checkToken',authController.checkToken);app.use(errorHandler());http.createServer(app).listen(3000,()=>{console.log('Expressserverstarted');});如我们所见,我们的应用程序模块非常基础。它由一个简单的Express服务器组成,该服务器注册了一些中间件和两个由authController导出的路由。这是一个简单的Web服务,包括一个控制器和一个模型。添加一个前端HTML页面,实现了MVC架构的分离模式。特征模式描述了特定设计情况下反复出现的问题并提供了解决方案。模式记录了有据可查的先前设计经验。模式描述了超越类、实例和组件的抽象。模式提供了一种通用语言,并允许每个人就设计原则达成一致。模式是记录软件架构的一种方式。模式有助于创建具有指定特征的软件。模式有助于塑造复杂和异构的软件架构。模式有助于控制软件的复杂性。为什么叫图案呢?每个模式由三部分组成:上下文(Context)问题的背景;问题(Problem)在此上下文中反复出现的问题;背景背景描述了问题的情况,使原本平淡无奇的问题解决方案变得更加丰富。模式的上下文可以非常笼统,例如“开发具有人机界面的软件”,也可以将特定模式联系在一起,例如“在模型、视图和控制器之间实现变更传播机制”。问题模式描述大纲的这一部分解决了给定上下文中反复出现的问题。它以阐明问题性质的一般性问题陈述开始:必须解决的具体设计问题是什么?例如,Model-View-Controller模式解决了用户界面频繁变化的问题。一个模式代表了解决一个问题时需要考虑的所有方面:解决方案必须满足的要求,比如进程之间的对等通信必须是高效的;必须考虑的约束,例如进程间通信必须遵守特定的协议;解决方案必须具备的特性,例如应该能够轻松修改软件。模型-视图-控制器模式说明了两种力量:修改用户界面应该很容易,而且这样的修改不应该影响软件的核心功能。解决方案模式的解决方案部分指示如何解决重复出现的问题,更准确地说,如何平衡相关力量。在软件架构中,这样的解决方案包括两个方面:每个模式指定一个特定的结构,元素的空间配置。例如,模型-视图-控制器模式的描述中有这样一句话:“将交互式应用程序分解为三个部分——处理、输出和输入。”每个模式都描述了运行阶段的行为。例如,在模型-视图-控制器模式的“解决方案”部分有这样一句话:“控制器接受输入,通常是表示鼠标移动、鼠标按钮激活或键盘输入的事件。事件被翻译成服务请求和服务请求被发送到模型或视图。”模式的类型模式一般分为三类:架构模式:用于特定软件架构的模板,它表征了应用程序的系统级结构并将影响架构。例如,模型-视图-控制器模式设计模式:是一种媒介尺寸模式,比架构模式小,但通常独立于编程语言和编程范式。应用设计模式不会影响软件系统的基本架构,但可能会严重影响子系统的架构。例如:观察者模式。示例:如何解决特定的设计问题。特定于语言的模式。例如,C++语言的CountedBody模式。摘要模式为开发具有特定特征的软件提供了一种有前途的方法。它们记录了先前的设计知识并帮助找到适当的解决方案设计问题。模式在大小和抽象级别上各不相同,涵盖了软件开发的许多重要领域。模式是相互交织的,我们可以使用一个模式来改进另一个更大的模式,或者组合模式来解决复杂的问题。模式解决了软件架构的一些重要方面,并补充了现有的技术和方法。模式可以与任何编程范式一起使用,并且几乎可以用任何编程语言实现。