如何使用nodejs 前端工程构建.js构建微服务

966,690 四月 独立访问用户
语言 & 开发
架构 & 设计
文化 & 方法
您目前处于:
使用API网关构建微服务
使用API网关构建微服务
日. 估计阅读时间:
不到一分钟
相关厂商内容
相关赞助商
ArchSummit深圳-8日,深圳&华侨城洲际酒店,
例如,下图展示了在Amazon Android移动应用中滚动产品详情时看到的内容。
虽然这是个智能手机应用,产品详情页面也显示了大量的信息。例如,该页面不仅包含基本的产品信息(如名称、描述、价格),而且还显示了如下内容:
购物车中的件数
低库存预警
各种推荐,包括经常与该产品一起购买的其它产品,购买该产品的客户购买的其它产品,购买该产品的客户看过的其它产品。
可选的购买选项。
当使用单体应用程序架构时,移动客户端将通过向应用程序发起一次REST调用(/productdetails/&productId&)来获取这些数据。负载均衡器将请求路由给N个相同的应用程序实例中的一个。然后,应用程序会查询各种数据库表,并将响应返回给客户端。
相比之下,当使用微服务架构时,产品详情页面显示的数据归多个微服务所有。下面是部分可能的微服务,它们拥有要显示在示例中产品详情页面上的数据:
购物车服务&&购物车中的件数
订单服务&&订单历史
目录服务&&产品基本信息,如名称、图片和价格
评论服务&&客户的评论
库存服务&&低库存预警
送货服务&&送货选项、期限和费用,这些单独从送货方的API获取
推荐服务&&建议的产品
我们需要决定移动客户端如何访问这些服务。让我们看看都有哪些选项。
客户端与微服务直接通信
从理论上讲,客户端可以直接向每个微服务发送请求。每个微服务都有一个公开的端点(https ://&serviceName&.pany.name)。该URL将映射到微服务的负载均衡器,由它负责在可用实例之间分发请求。为了获取产品详情,移动客户端将逐一向上面列出的N个服务发送请求。
遗憾的是,这种方法存在挑战和局限。一个问题是客户端需求和每个微服务暴露的细粒度API不匹配。在这个例子中,客户端需要发送7个独立请求。在更复杂的应用程序中,可能要发送更多的请求。例如,按照Amazon的说法,他们在显示他们的产品页面时就调用了数百个服务。然而,客户端通过LAN发送许多请求,这在公网上可能会很低效,而在移动网络上就根本不可行。这种方法还使得客户端代码非常复杂。
客户端直接调用微服务的另一个问题是,部分服务使用的协议不是Web友好协议。一个服务可能使用Thrift二进制RPC,而另一个服务可能使用AMQP消息传递协议。不管哪种协议都不是浏览器友好或防火墙友好的,最好是内部使用。在防火墙之外,应用程序应该使用诸如HTTP和WebSocket之类的协议。
这种方法的另一个缺点是,它会使得微服务难以重构。随着时间推移,我们可能想要更改系统划分成服务的方式。例如,我们可能合并两个服务,或者将一个服务拆分成两个或更多服务。然而,如果客户端与微服务直接通信,那么执行这类重构就非常困难了。
由于这些问题的存在,客户端与微服务直接通信很少是合理的。
使用API网关
通常,一个更好的方法是使用所谓的。API网关是一个服务器,是系统的唯一入口。从面向对象设计的角度看,它与类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、&请求整形(request shaping)&与管理、静态响应处理。
下图展示了API网关通常如何融入架构:
API网关负责服务请求路由、组合及协议转换。客户端的所有请求都首先经过API网关,然后由它将请求路由到合适的微服务。API网管经常会通过调用多个微服务并合并结果来处理一个请求。它可以在Web协议(如HTTP与WebSocket)与内部使用的非Web友好协议之间转换。
API网关还能为每个客户端提供一个定制的API。通常,它会向移动客户端暴露一个粗粒度的API。例如,考虑下产品详情的场景。API网关可以提供一个端点(/productdetails?productid=xxx),使移动客户端可以通过一个请求获取所有的产品详情。API网关通过调用各个服务(产品信息、推荐、评论等等)并合并结果来处理请求。
是一个很好的API网关实例。Netflix流服务提供给数以百计的不同类型的设备使用,包括电视、机顶盒、智能手机、游戏系统、平板电脑等等。最初,Netflix试图为他们的流服务提供一个的API。然而他们发现,由于各种各样的设备都有自己独特的需求,这种方式并不能很好地工作。如今,他们使用一个API网关,通过运行特定于设备的适配器代码来为每个设备提供一个定制的API。通常,一个适配器通过调用平均6到7个后端服务来处理每个请求。Netflix API网关每天处理数十亿请求。
API网关的优点和不足
如你所料,使用API网关有优点也有不足。使用API网关的最大优点是,它封装了应用程序的内部结构。客户端只需要同网关交互,而不必调用特定的服务。API网关为每一类客户端提供了特定的API。这减少了客户端与应用程序间的交互次数,还简化了客户端代码。
API网关也有一些不足。它增加了一个我们必须开发、部署和维护的高可用组件。还有一个风险是,API网关变成了开发瓶颈。为了暴露每个微服务的端点,开发人员必须更新API网关。API网关的更新过程要尽可能地简单,这很重要。否则,为了更新网关,开发人员将不得不排队等待。不过,虽然有这些不足,但对于大多数现实世界的应用程序而言,使用API网关是合理的。
实现API网关
到目前为止,我们已经探讨了使用API网关的动机及其优缺点。下面让我们看一下需要考虑的各种设计问题。
性能和可扩展性
只有少数公司有Netflix的规模,每天需要处理数十亿请求。不管怎样,对于大多数应用程序而言,API网关的性能和可扩展性通常都非常重要。因此,将API网关构建在一个支持异步、I/O非阻塞的平台上是合理的。有多种不同的技术可以用于实现一个可扩展的API网关。在JVM上,可以使用一种基于NIO的框架,比如Netty、Vertx、Spring Reactor或JBoss Undertow中的一种。一个非常流行的非JVM选项是Node.js,它是一个以Chrome JavaScript引擎为基础构建的平台。另一个选项是使用。NGINX Plus提供了一个成熟的、可扩展的、高性能Web服务器和一个易于部署的、可配置可编程的反向代理。NGINX Plus可以管理身份验证、访问控制、负载均衡请求、缓存响应,并提供应用程序可感知的健康检查和监控。
使用响应式编程模型
API网关通过简单地将请求路由给合适的后端服务来处理部分请求,而通过调用多个后端服务并合并结果来处理其它请求。对于部分请求,比如产品详情相关的多个请求,它们对后端服务的请求是独立于其它请求的。为了最小化响应时间,API网关应该并发执行独立请求。然而,有时候,请求之间存在依赖。在将请求路由到后端服务之前,API网关可能首先需要调用身份验证服务验证请求的合法性。类似地,为了获取客户意愿清单中的产品信息,API网关必须首先获取包含那些信息的客户资料,然后再获取每个产品的信息。关于API组合,另一个有趣的例子是。
使用传统的异步回调方法编写API组合代码会让你迅速坠入回调地狱。代码会变得混乱、难以理解且容易出错。一个更好的方法是使用响应式方法以一种声明式样式编写API网关代码。响应式抽象概念的例子有Scala中的、Java 8中的和JavaScript中的,还有最初是微软为.NET平台开发的。Netflix创建了RxJava for JVM,专门用于他们的API网关。此外,还有RxJS for JavaScript,它既可以在浏览器中运行,也可以在Node.js中运行。使用响应式方法将使你可以编写简单但高效的API网关代码。
基于微服务的应用程序是一个分布式系统,必须使用一种进程间通信机制。有两种类型的进程间通信机制可供选择。一种是使用异步的、基于消息传递的机制。有些实现使用诸如JMS或AMQP那样的消息代理,而其它的实现(如Zeromq)则没有代理,服务间直接通信。另一种进程间通信类型是诸如HTTP或Thrift那样的同步机制。通常,一个系统会同时使用异步和同步两种类型。它甚至还可能使用同一类型的多种实现。总之,API网关需要支持多种通信机制。
API网关需要知道它与之通信的每个微服务的位置(IP地址和端口)。在传统的应用程序中,或许可以硬连线这个位置,但在现代的、基于云的微服务应用程序中,这并不是一个容易解决的问题。基础设施服务(如消息代理)通常会有一个静态位置,可以通过OS环境变量指定。但是,确定一个应用程序服务的位置没有这么简单。应用程序服务的位置是动态分配的。而且,单个服务的一组实例也会随着自动扩展或升级而动态变化。总之,像系统中的其它服务客户端一样,API网关需要使用系统的服务发现机制,可以是,也可以是。下一篇文章将更详细地描述服务发现。现在,需要注意的是,如果系统使用客户端发现,那么API网关必须能够查询,这是一个包含所有微服务实例及其位置的数据库。
处理局部失败
在实现API网关时,还有一个问题需要处理,就是局部失败的问题。该问题在所有的分布式系统中都会出现,无论什么时候,当一个服务调用另一个响应慢或不可用的服务,就会出现这个问题。API网关永远不能因为无限期地等待下游服务而阻塞。不过,如何处理失败取决于特定的场景以及哪个服务失败。例如,在产品详情场景下,如果推荐服务无响应,那么API网关应该向客户端返回产品详情的其它内容,因为它们对用户依然有用。推荐内容可以为空,也可以,比如说,用一个固定的TOP 10列表取代。不过,如果产品信息服务无响应,那么API网关应该向客户端返回一个错误信息。
如果缓存数据可用,那么API网关还可以返回缓存数据。例如,由于产品价格不经常变化,所以如果价格服务不可用,API网关可以返回缓存的价格数据。数据可以由API网关自己缓存,也可以存储在像Redis或Memcached那样的外部缓存中。通过返回默认数据或者缓存数据,API网关可以确保系统故障不影响用户的体验。
在编写代码调用远程服务方面,是一个异常有用的库。Hystrix会将超出设定阀值的调用超时。它实现了一个&断路器(circuit breaker)&模式,可以防止客户端对无响应的服务进行不必要的等待。如果服务的错误率超出了设定的阀值,那么Hystrix会切断断路器,在一个指定的时间范围内,所有请求都会立即失败。Hystrix允许用户定义一个请求失败后的后援操作,比如从缓存读取数据,或者返回一个默认值。如果你正在使用JVM,那么你绝对应该考虑使用Hystrix。而如果你正在使用一个非JVM环境,那么你应该使用一个等效的库。
对于大多数基于微服务的应用程序而言,实现一个API网关是有意义的,它可以作为系统的唯一入口。API网关负责服务请求路由、组合及协议转换。它为每个应用程序客户端提供一个定制的API。API网关还可以通过返回缓存数据或默认数据屏蔽后端服务失败。在本系列的下一篇文章中,我们将探讨服务间通信。
感谢对本文的审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至。也欢迎大家通过新浪微博(,),微信(微信号:)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群)。
Author Contacted
告诉我们您的想法
允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p
当有人回复此评论时请E-mail通知我
在本系列的下一篇文章中,我们将探讨服务间通信。
Re: 微服务太多的话,事务如何控制?
Re: 在本系列的下一篇文章中,我们将探讨服务间通信。
感人的截图
Re: 微服务太多的话,事务如何控制?
允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p
当有人回复此评论时请E-mail通知我
允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p
当有人回复此评论时请E-mail通知我
赞助商链接
InfoQ每周精要
订阅InfoQ每周精要,加入拥有25万多名资深开发者的庞大技术社区。
架构 & 设计
文化 & 方法
<及所有内容,版权所有 &#169;
C4Media Inc.
服务器由 提供, 我们最信赖的ISP伙伴。
北京创新网媒广告有限公司
京ICP备号-7
找回密码....
InfoQ账号使用的E-mail
关注你最喜爱的话题和作者
快速浏览网站内你所感兴趣话题的精选内容。
内容自由定制
选择想要阅读的主题和喜爱的作者定制自己的新闻源。
不再错过InfoQ编辑特稿
“当你不知道某件事情的时候,你很难意识到。”想要改变?看看InfoQ编辑们的推荐内容吧。
注意:如果要修改您的邮箱,我们将会发送确认邮件到您原来的邮箱。
使用现有的公司名称
修改公司名称为:
公司性质:
使用现有的公司性质
修改公司性质为:
使用现有的公司规模
修改公司规模为:
使用现在的国家
使用现在的省份
Subscribe to our newsletter?
Subscribe to our industry email notices?
我们发现您在使用ad blocker。
我们理解您使用ad blocker的初衷,但为了保证InfoQ能够继续以免费方式为您服务,我们需要您的支持。InfoQ绝不会在未经您许可的情况下将您的数据提供给第三方。我们仅将其用于向读者发送相关广告内容。请您将InfoQ添加至白名单,感谢您的理解与支持。微服务架构
我的图书馆
微服务架构
当我听到这个名词的时候还是三天以前,做为一个初入者,我觉得对于这些对于我来说还是需要去好好理解一番。有种趋势似乎是大型系统架构,越来越往这边靠拢。原因不仅仅在于系统过于臃肿,还在于如何更好的似乎小团队开始。微内核这只是由微服务与传统架构之间对比而引发的一个思考,让我引一些资料来当参考吧.单内核:也称为宏内核。将内核从整体上作为一个大过程实现,并同时运行在一个单独的地址空间。所有的内核服务都在一个地址空间运行,相互之间直接调用函数,简单高效。微内核:功能被划分成独立的过程,过程间通过IPC进行通信。模块化程度高,一个服务失效不会影响另外一个服务。Linux是一个单内核结构,同时又吸收了微内核的优点:模块化设计,支持动态装载内核模块。Linux还避免了微内核设计上的缺陷,让一切都运行在内核态,直接调用函数,无需消息传递。对就的微内核便是:微内核――在微内核中,大部分内核都作为单独的进程在特权状态下运行,他们通过消息传递进行通讯。在典型情况下,每个概念模块都有一个进程。因此,假如在设计中有一个系统调用模块,那么就必然有一个相应的进程来接收系统调用,并和能够执行系统调用的其他进程(或模块)通讯以完成所需任务。如果读过《操作系统原理》及其相关书籍的人应该很了解这些,对就的我们就可以一目了然地解决我们当前是的微服务的问题。微服务文章的来源是James Lewis与Martin Fowler写的。对就于上面的monolithic kernelmicrokernel与文中的monolithic servicesmicroservices我们还是将其翻译成微服务与宏服务。引起原文中对于微服务的解释:简短地说,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,通过轻量的通讯机制联系,经常是基于HTTP资源API,这些服务基于业务能力构建,能够通过自动化部署方式独立部署,这些服务自己有一些小型集中化管理,可以是使用不同的编程语言编写,正如不同的数据存储技术一样。原文是:In short, the microservice architectural style [1] is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare mininum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.而关于微服务的提出是早在2011年的5月份The term "microservice" was discussed at a workshop of software architects near Venice in May, 2011 to describe what the participants saw as a common architectural style that many of them had been recently exploring.微服务思考简单地与微内核作一些对比。微内核,微内核部分经常只但是是个消息转发站,而微服务从某种意义上也是如此,他们都有着下面的优点。有助于实现模块间的隔离在不影响系统其他部分的情况下,用更高效的实现代替现有文档系统模块的工作将会更加容易。对于微服务来说每个服务本身都是很简单的对于每个服务,我们可以选择最好和最合适的工具来开发系统本质上是松耦合的不同的团队可以工作在不同的服务中可以持续发布,而其他部分还是稳定的从某种意义上来说微服务更适合于大型企业架构,而不是一般的应用,对于一般的应用来说他们的都在同一台主机上。无力于支付更多的系统开销,于是如微服务不是免费的午餐一文所说微服务带来很多的开销操作大量的DevOps技能要求隐式接口重复努力分布式系统的复杂性异步性是困难的!可测试性挑战因而不得不再后面补充一些所知的额外的东西。微服务与持续集成针对于同样的话题,开始了解其中的一些问题。当敏捷的思想贯穿于开发过程时,我们不得不面对持续集成与发布这样的问题。我们确实可以在不同的服务下工作,然而当我们需要修改API时,就对我们的集成带来很多的问题。我们需要同时修改两个API!我们也需要同时部署他们!微服务与测试相比较的来说,这也是另外的一个挑战。测试对于项目开发来说是不可缺少的,而当我们的服务一个个隔离的时候,我们的测试不得不去mock一个又一个的服务。在有些时候修复这些测试可能比添加这个功能花费的时间还多。.不过他更适合那些喜欢不同技术栈的程序员。参考围观我的墙, 也许,你会遇到心仪的项目或许您还需要:
TA的最新馆藏Seneca :NodeJS 微服务框架入门指南 - 推酷
Seneca :NodeJS 微服务框架入门指南
是一个能让您快速构建基于消息的微服务系统的工具集,你不需要知道各种服务本身被部署在何处,不需要知道具体有多少服务存在,也不需要知道他们具体做什么,任何你业务逻辑之外的服务(如数据库、缓存或者第三方集成等)都被隐藏在微服务之后。
这种解耦使您的系统易于连续构建与更新,Seneca 能做到这些,原因在于它的三大核心功能:
模式匹配:不同于脆弱的服务发现,模式匹配旨在告诉这个世界你真正关心的消息是什么;
无依赖传输:你可以以多种方式在服务之间发送消息,所有这些都隐藏至你的业务逻辑之后;
组件化:功能被表示为一组可以一起组成微服务的插件。
在 Seneca 中,消息就是一个可以有任何你喜欢的内部结构的
对象,它们可以通过 HTTP/HTTPS、TCP、消息队列、发布/订阅服务或者任何能传输数据的方式进行传输,而对于作为消息生产者的你来讲,你只需要将消息发送出去即可,完全不需要关心哪些服务来接收它们。
然后,你又想告诉这个世界,你想要接收一些消息,这也很简单,你只需在 Seneca 中作一点匹配模式配置即可,匹配模式也很简单,只是一个键值对的列表,这些键值对被用于匹配
消息的极组属性。
在本文接下来的内容中,我们将一同基于 Seneca 构建一些微服务。
让我们从一点特别简单的代码开始,我们将创建两个微服务,一个会进行数学计算,另一个去调用它:
const seneca = require('seneca')();
seneca.add('role:math, cmd:sum', (msg, reply) =& {
reply(null, { answer: ( msg.left + msg.right )})
seneca.act({
role: 'math',
cmd: 'sum',
}, (err, result) =& {
if (err) {
return console.error(err);
console.log(result);
将上面的代码,保存至一个
文件中,然后执行它,你可能会在
中看到类似下面这样的消息:
{&kind&:&notice&,&notice&:&hello seneca 4y8daxnikuxp/1/.2/-&,&level&:&info&,&when&:5}
(node:58922) DeprecationWarning: 'root' is deprecated, use 'global'
{ answer: 3 }
到目前为止,所有这一切都发生在同一个进程中,没有网络流量产生,进程内的函数调用也是基于消息传输。
seneca.add
方法,添加了一个新的动作模式(_Action Pattern_)至
实例中,它有两个参数:
:用于匹配 Seneca 实例中
消息体的模式;
:当模式被匹配时执行的操作
seneca.act
方法同样有两个参数:
让我们再把所有代码重新过一次:
seneca.add('role:math, cmd:sum', (msg, reply) =& {
reply(null, { answer: ( msg.left + msg.right )})
在上面的代码中的
函数,计算了匹配到的消息体中两个属性
的值的和,并不是所有的消息都会被创建一个响应,但是在绝大多数情况下,是需要有响应的, Seneca 提供了用于响应消息的回调函数。
在匹配模式中,
role:math, cmd:sum
匹配到了下面这个消息体:
role: 'math',
cmd: 'sum',
并得到计自结果:
这两个属性,它们没有什么特别的,只是恰好被你用于匹配模式而已。
seneca.act
方法,发送了一条消息,它有两个参数:
response_callback
响应的回调函数可接收两个参数:
,如果有任何错误发生(比如,发送出去的消息未被任何模式匹配),则第一个参数将是一个
对象,而如果程序按照我们所预期的方向执行了的话,那么,第二个参数将接收到响应结果,在我们的示例中,我们只是简单的将接收到的响应结果打印至了
seneca.act({
role: 'math',
cmd: 'sum',
}, (err, result) =& {
if (err) {
return console.error(err);
console.log(result);
示例文件,向你展示了如何定义并创建一个 Action 以及如何呼起一个 Action,但它们都发生在一个进程中,接下来,我们很快就会展示如何拆分成不同的代码和多个进程。
匹配模式如何工作?
模式----而不是网络地址或者会话,让你可以更加容易的扩展或增强您的系统,这样做,让添加新的微服务变得更简单。
现在让我们给系统再添加一个新的功能----计算两个数字的乘积。
我们想要发送的消息看起来像下面这样的:
role: 'math',
cmd: 'product',
而后获得的结果看起来像下面这样的:
answer: 12
知道怎么做了吧?你可以像
role: math, cmd: sum
模式这样,创建一个
role: math, cmd: product
seneca.add('role:math, cmd:product', (msg, reply) =& {
reply(null, { answer: ( msg.left * msg.right )})
然后,调用该操作:
seneca.act({
role: 'math',
cmd: 'product',
}, (err, result) =& {
if (err) {
return console.error(err);
console.log(result);
,你将得到你想要的结果。
将这两个方法放在一起,代码像是下面这样的:
const seneca = require('seneca')();
seneca.add('role:math, cmd:sum', (msg, reply) =& {
reply(null, { answer: ( msg.left + msg.right )})
seneca.add('role:math, cmd:product', (msg, reply) =& {
reply(null, { answer: ( msg.left * msg.right )})
seneca.act({role: 'math', cmd: 'sum', left: 1, right: 2}, console.log)
.act({role: 'math', cmd: 'product', left: 3, right: 4}, console.log)
后,你将得到下面这样的结果:
null { answer: 3 }
null { answer: 12 }
在上面合并到一起的代码中,我们发现,
seneca.act
是可以进行链式调用的,
提供了一个链式API,调式调用是顺序执行的,但是不是串行,所以,返回的结果的顺序可能与调用顺序并不一样。
扩展模式以增加新功能
模式让你可以更加容易的扩展程序的功能,与
if...else...
语法不同的是,你可以通过增加更多的匹配模式以达到同样的功能。
下面让我们扩展一下
role: math, cmd: sum
操作,它只接收整型数字,那么,怎么做?
seneca.add({role: 'math', cmd: 'sum', integer: true}, function (msg, respond) {
var sum = Math.floor(msg.left) + Math.floor(msg.right)
respond(null, {answer: sum})
现在,下面这条消息:
{role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true}
将得到下面这样的结果:
{answer: 3}
// == 1 + 2,小数部分已经被移除了
现在,你的两个模式都存在于系统中了,而且还存在交叉部分,那么
最终会将消息匹配至哪条模式呢?原则是:更多匹配项目被匹配到的优先,被匹配到的属性越多,则优先级越高。
可以给我们更加直观的测试:
const seneca = require('seneca')()
seneca.add({role: 'math', cmd: 'sum'}, function (msg, respond) {
var sum = msg.left + msg.right
respond(null, {answer: sum})
// 下面两条消息都匹配 role: math, cmd: sum
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5}, console.log)
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true}, console.log)
setTimeout(() =& {
seneca.add({role: 'math', cmd: 'sum', integer: true}, function (msg, respond) {
var sum = Math.floor(msg.left) + Math.floor(msg.right)
respond(null, { answer: sum })
// 下面这条消息同样匹配 role: math, cmd: sum
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5}, console.log)
// 但是,也匹配 role:math,cmd:sum,integer:true
// 但是因为更多属性被匹配到,所以,它的优先级更高
seneca.act({role: 'math', cmd: 'sum', left: 1.5, right: 2.5, integer: true}, console.log)
输出结果应该像下面这样:
null { answer: 4 }
null { answer: 4 }
null { answer: 4 }
null { answer: 3 }
在上面的代码中,因为系统中只存在
role: math, cmd: sum
模式,所以,都匹配到它,但是当 100ms 后,我们给系统中添加了一个
role: math, cmd: sum, integer: true
模式之后,结果就不一样了,匹配到更多的操作将有更高的优先级。
这种设计,可以让我们的系统可以更加简单的添加新的功能,不管是在开发环境还是在生产环境中,你都可以在不需要修改现有代码的前提下即可更新新的服务,你只需要先好新的服务,然后启动新服务即可。
基于模式的代码复用
模式操作还可以调用其它的操作,所以,这样我们可以达到代码复用的需求:
const seneca = require('seneca')()
seneca.add('role: math, cmd: sum', function (msg, respond) {
var sum = msg.left + msg.right
respond(null, {answer: sum})
seneca.add('role: math, cmd: sum, integer: true', function (msg, respond) {
// 复用 role:math, cmd:sum
this.act({
role: 'math',
cmd: 'sum',
left: Math.floor(msg.left),
right: Math.floor(msg.right)
}, respond)
// 匹配 role:math,cmd:sum
seneca.act('role: math, cmd: sum, left: 1.5, right: 2.5',console.log)
// 匹配 role:math,cmd:sum,integer:true
seneca.act('role: math, cmd: sum, left: 1.5, right: 2.5, integer: true', console.log)
在上面的示例代码中,我们使用了
而不是前面的
seneca.act
,那是因为,在
函数中,上下文关系变量
,引用了当前的
实例,这样你就可以在任何一个
函数中,访问到该
调用的整个上下文。
在上面的代码中,我们使用了 JSON 缩写形式来描述模式与消息, 比如,下面是对象字面量:
{role: 'math', cmd: 'sum', left: 1.5, right: 2.5}
缩写模式为:
'role: math, cmd: sum, left: 1.5, right: 2.5'
这种格式,提供了一种以字符串字面量来表达对象的简便方式,这使得我们可以创建更加简单的模式和消息。
上面的代码保存在了
模式是唯一的
你定义的 Action 模式都是唯一了,它们只能触发一个函数,模式的解析规则如下:
更多我属性优先级更高
若模式具有相同的数量的属性,则按字母顺序匹配
规则被设计得很简单,这使得你可以更加简单的了解到到底是哪个模式被匹配了。
下面这些示例可以让你更容易理解:
a: 1, b: 2
, 因为它有更多的属性;
a: 1, b: 2
a: 1, c: 3
字母的前面;
a: 1, b: 2, d: 4
a: 1, c: 3, d:4
字母的前面;
a: 1, b:2, c:3
,因为它有更多的属性;
a: 1, b:2, c:3
,因为它有更多的属性。
很多时间,提供一种可以让你不需要全盘修改现有 Action 函数的代码即可增加它功能的方法是很有必要的,比如,你可能想为某一个消息增加更多自定义的属性验证方法,捕获消息统计信息,添加额外的数据库结果中,或者控制消息流速等。
我下面的示例代码中,加法操作期望
属性是有限数,此外,为了调试目的,将原始输入参数附加到输出的结果中也是很有用的,您可以使用以下代码添加验证检查和调试信息:
const seneca = require('seneca')()
'role:math,cmd:sum',
function(msg, respond) {
var sum = msg.left + msg.right
respond(null, {
answer: sum
// 重写 role:math,cmd:sum with ,添加额外的功能
'role:math,cmd:sum',
function(msg, respond) {
// bail out early if there's a problem
if (!Number.isFinite(msg.left) ||
!Number.isFinite(msg.right)) {
return respond(new Error(&left 与 right 值必须为数字。&))
// 调用上一个操作函数 role:math,cmd:sum
this.prior({
role: 'math',
cmd: 'sum',
left: msg.left,
right: msg.right,
}, function(err, result) {
if (err) return respond(err)
= msg.left + '+' + msg.right
respond(null, result)
// 增加了的 role:math,cmd:sum
.act('role:math,cmd:sum,left:1.5,right:2.5',
console.log // 打印 { answer: 4, info: '1.5+2.5' }
实例提供了一个名为
的方法,让可以在当前的
方法中,调用被其重写的旧操作函数。
函数接受两个参数:
response_callback
在上面的示例代码中,已经演示了如何修改入参与出参,修改这些参数与值是可选的,比如,可以再添加新的重写,以增加日志记录功能。
在上面的示例中,也同样演示了如何更好的进行错误处理,我们在真正进行操作之前,就验证的数据的正确性,若传入的参数本身就有错误,那么我们直接就返回错误信息,而不需要等待真正计算的时候由系统去报错了。
错误消息应该只被用于描述错误的输入或者内部失败信息等,比如,如果你执行了一些数据库的查询,返回没有任何数据,这并不是一个错误,而仅仅只是数据库的事实的反馈,但是如果连接数据库失败,那就是一个错误了。
上面的代码可以在
文件中找到。
使用插件组织模式
实例,其实就只是多个
Action Patterm
的集合而已,你可以使用命名空间的方式来组织操作模式,例如在前面的示例中,我们都使用了
role: math
,为了帮助日志记录和调试,
还支持一个简约的插件支持。
同样,Seneca插件只是一组操作模式的集合,它可以有一个名称,用于注释日志记录条目,还可以给插件一组选项来控制它们的行为,插件还提供了以正确的顺序执行初始化函数的机制,例如,您希望在尝试从数据库读取数据之前建立数据库连接。
简单来说,Seneca插件就只是一个具有单个参数选项的函数,你将这个插件定义函数传递给
seneca.use
方法,下面这个是最小的Seneca插件(其实它什么也没做!):
function minimal_plugin(options) {
console.log(options)
require('seneca')()
.use(minimal_plugin, {foo: 'bar'})
seneca.use
方法接受两个参数:
上面的示例代码执行后,打印出来的日志看上去是这样的:
{&kind&:&notice&,&notice&:&hello seneca 3qk0ij5t2bta/4/.2/-&,&level&:&info&,&when&:7}
(node:62768) DeprecationWarning: 'root' is deprecated, use 'global'
{ foo: 'bar' }
Seneca 还提供了详细日志记录功能,可以提供为开发或者生产提供更多的日志信息,通常的,日志级别被设置为
,它并不会打印太多日志信息,如果想看到所有的日志信息,试试以下面这样的方式启动你的服务:
node minimal-plugin.js --seneca.log.all
会不会被吓一跳?当然,你还可以过滤日志信息:
node minimal-plugin.js --seneca.log.all | grep plugin:define
通过日志我们可以看到, seneca 加载了很多内置的插件,比如
,这些插件为我们提供了创建微服务的基础功能,同样,你应该也可以看到
minimal_plugin
现在,让我们为这个插件添加一些操作模式:
function math(options) {
this.add('role:math,cmd:sum', function (msg, respond) {
respond(null, { answer: msg.left + msg.right })
this.add('role:math,cmd:product', function (msg, respond) {
respond(null, { answer: msg.left * msg.right })
require('seneca')()
.use(math)
.act('role:math,cmd:sum,left:1,right:2', console.log)
文件,得到下面这样的信息:
null { answer: 3 }
看打印出来的一条日志:
&actid&: &7ubgm65mcnfl/uatuklury90r&,
&role&: &math&,
&cmd&: &sum&,
&left&: 1,
&right&: 2,
&meta$&: {
&id&: &7ubgm65mcnfl/uatuklury90r&,
&tx&: &uatuklury90r&,
&pattern&: &cmd:sum,role:math&,
&action&: &(bjx5u38uwyse)&,
&plugin_name&: &math&,
&plugin_tag&: &-&,
&prior&: {
&chain&: [],
&entry&: true,
&depth&: 0
&start&: 4,
&sync&: true
&plugin$&: {
&name&: &math&,
&tag&: &-&
&tx$&: &uatuklury90r&
&entry&: true,
&prior&: [],
&plugin_name&: &math&,
&plugin_tag&: &-&,
&plugin_fullname&: &math&,
&role&: &math&,
&cmd&: &sum&
&sub&: false,
&client&: false,
&role&: &math&,
&cmd&: &sum&
&rules&: {},
&id&: &(bjx5u38uwyse)&,
&pattern&: &cmd:sum,role:math&,
&msgcanon&: {
&cmd&: &sum&,
&role&: &math&
&priorpath&: &&
&client&: false,
&listen&: false,
&transport&: {},
&kind&: &act&,
&case&: &OUT&,
&duration&: 35,
&result&: {
&answer&: 3
&level&: &debug&,
&plugin_name&: &math&,
&plugin_tag&: &-&,
&pattern&: &cmd:sum,role:math&,
所有的该插件的日志都被自动的添加了
在 Seneca 的世界中,我们通过插件组织各种操作模式集合,这让日志与调试变得更简单,然后你还可以将多个插件合并成为各种微服务,在接下来的章节中,我们将创建一个
插件通过需要进行一些初始化的工作,比如连接数据库等,但是,你并不需要在插件的定义函数中去执行这些初始化,定义函数被设计为同步执行的,因为它的所有操作都是在定义一个插件,事实上,你不应该在定义函数中调用
seneca.act
方法,只调用
seneca.add
要初始化插件,你需要定义一个特殊的匹配模式
init: &plugin-name&
,对于每一个插件,将按顺序调用此操作模式,
函数必须调用其
函数,并且不能有错误发生,如果插件初始化失败,则 Seneca 会立即退出 Node 进程。所以的插件初始化工作都必须在任何操作执行之前完成。
为了演示初始化,让我们向
插件添加简单的自定义日志记录,当插件启动时,它打开一个日志文件,并将所有操作的日志写入文件,文件需要成功打开并且可写,如果这失败,微服务启动就应该失败。
const fs = require('fs')
function math(options) {
// 日志记录函数,通过 init 函数创建
// 将所有模式放在一起会上我们查找更方便
this.add('role:math,cmd:sum',
this.add('role:math,cmd:product', product)
// 这就是那个特殊的初始化操作
this.add('init:math', init)
function init(msg, respond) {
// 将日志记录至一个特写的文件中
fs.open(options.logfile, 'a', function (err, fd) {
// 如果不能读取或者写入该文件,则返回错误,这会导致 Seneca 启动失败
if (err) return respond(err)
log = makeLog(fd)
function sum(msg, respond) {
var out = { answer: msg.left + msg.right }
log('sum '+msg.left+'+'+msg.right+'='+out.answer+'\n')
respond(null, out)
function product(msg, respond) {
var out = { answer: msg.left * msg.right }
log('product '+msg.left+'*'+msg.right+'='+out.answer+'\n')
respond(null, out)
function makeLog(fd) {
return function (entry) {
fs.write(fd, new Date().toISOString()+' '+entry, null, 'utf8', function (err) {
if (err) return console.log(err)
// 确保日志条目已刷新
fs.fsync(fd, function (err) {
if (err) return console.log(err)
require('seneca')()
.use(math, {logfile:'./math.log'})
.act('role:math,cmd:sum,left:1,right:2', console.log)
在上面这个插件的代码中,匹配模式被组织在插件的顶部,以便它们更容易被看到,函数在这些模式下面一点被定义,您还可以看到如何使用选项提供自定义日志文件的位置(不言而喻,这不是生产日志!)。
初始化函数
执行一些异步文件系统工作,因此必须在执行任何操作之前完成。 如果失败,整个服务将无法初始化。要查看失败时的操作,可以尝试将日志文件位置更改为无效的,例如
以上代码可以在
文件中找到。
创建微服务
现在让我们把
插件变成一个真正的微服务。首先,你需要组织你的插件。
插件的业务逻辑 ---- 即它提供的功能,与它以何种方式与外部世界通信是分开的,你可能会暴露一个Web服务,也有可能在消息总线上监听。
将业务逻辑(即插件定义)放在其自己的文件中是有意义的。 Node.js 模块即可完美的实现,创建一个名为
的文件,内容如下:
module.exports = function math(options) {
this.add('role:math,cmd:sum', function sum(msg, respond) {
respond(null, { answer: msg.left + msg.right })
this.add('role:math,cmd:product', function product(msg, respond) {
respond(null, { answer: msg.left * msg.right })
this.wrap('role:math', function (msg, respond) {
= Number(msg.left).valueOf()
msg.right = Number(msg.right).valueOf()
this.prior(msg, respond)
然后,我们可以在需要引用它的文件中像下面这样添加到我们的微服务系统中:
// 下面这两种方式都是等价的(还记得我们前面讲过的 `seneca.use` 方法的两个参数吗?)
require('seneca')()
.use(require('./math.js'))
.act('role:math,cmd:sum,left:1,right:2', console.log)
require('seneca')()
.use('math') // 在当前目录下找到 `./math.js`
.act('role:math,cmd:sum,left:1,right:2', console.log)
seneca.wrap
方法可以匹配一组模式,同使用相同的动作扩展函数覆盖至所有被匹配的模式,这与为每一个组模式手动调用
seneca.add
去扩展可以得到一样的效果,它需要两个参数:
:模式匹配模式
是一个可以匹配到多个模式的模式,它可以匹配到多个模式,比如
可以匹配到
role:math, cmd:sum
role:math, cmd:product
在上面的示例中,我们在最后面的
函数中,确保了,任何传递给
的消息体中
值都是数字,即使我们传递了字符串,也可以被自动的转换为数字。
有时,查看 Seneca 实例中有哪些操作是被重写了是很有用的,你可以在启动应用时,加上
--seneca.print.tree
参数即可,我们先创建一个
文件,填入以下内容:
require('seneca')()
.use('math')
然后再执行它:
? node math-tree.js --seneca.print.tree
{&kind&:&notice&,&notice&:&hello seneca abs0eg4hu04h/0/.2/-&,&level&:&info&,&when&:2}
(node:65316) DeprecationWarning: 'root' is deprecated, use 'global'
Seneca action patterns for instance: abs0eg4hu04h/0/.2/-
├─┬ cmd:sum
│ └─┬ role:math
└── # math, (15fqzd54pnsp),
# math, (qqrze3ub5vhl), sum
└─┬ cmd:product
└─┬ role:math
└── # math, (qnh86mgin4r6),
# math, (4nrxi5f6sp69), product
从上面你可以看到很多的键/值对,并且以树状结构展示了重写,所有的
函数展示的格式都是
#plugin, (action-id), function-name
但是,到现在为止,所有的操作都还存在于同一个进程中,接下来,让我们先创建一个名为
的文件,填入以下内容:
require('seneca')()
.use('math')
然后启动该脚本,即可启动我们的微服务,它会启动一个进程,并通过
端口监听HTTP请求,它不是一个 Web 服务器,在此时,
仅仅作为消息的传输机制。
你现在可以访问
即可看到结果,或者使用
curl -d '{&role&:&math&,&cmd&:&sum&,&left&:1,&right&:2}' http://localhost:10101/act
两种方式都可以看到结果:
{&answer&:3}
接下来,你需要一个微服务客户端
require('seneca')()
.act('role:math,cmd:sum,left:1,right:2',console.log)
打开一个新的终端,执行该脚本:
null { answer: 3 } { id: '7uuptvpf8iff/9wfb26kbqx55',
accept: '043di4pxswq7/4/.2/-',
track: undefined,
{ client_sent: '0',
listen_recv: '0',
listen_sent: '0',
client_recv: 0 } }
中,我们通过
seneca.listen
方法创建微服务,然后通过
seneca.client
去与微服务进行通信。在上面的示例中,我们使用的都是 Seneca 的默认配置,比如
seneca.listen
seneca.client
方法都可以接受下面这些参数,以达到定抽的功能:
:可选的数字,表示端口号;
:可先的字符串,表示主机名或者IP地址;
:可选的对象,完整的定制对象
注意:在 Windows 系统中,如果未指定
, 默认会连接
,这是没有任何用处的,你可以设置
的端口号与主机一致,它们就可以进行通信:
seneca.client(8080) → seneca.listen(8080)
seneca.client(8080, '192.168.0.2') → seneca.listen(8080, '192.168.0.2')
seneca.client({ port: 8080, host: '192.168.0.2' }) → seneca.listen({ port: 8080, host: '192.168.0.2' })
Seneca 为你提供的
无依赖传输
特性,让你在进行业务逻辑开发时,不需要知道消息如何传输或哪些服务会得到它们,而是在服务设置代码或配置中指定,比如
插件中的代码永远不需要改变,我们就可以任意的改变传输方式。
协议很方便,但是并不是所有时间都合适,另一个常用的协议是
,我们可以很容易的使用
协议来进行数据的传输,尝试下面这两个文件:
require('seneca')()
.use('math')
.listen({type: 'tcp'})
require('seneca')()
.client({type: 'tcp'})
.act('role:math,cmd:sum,left:1,right:2',console.log)
默认情况下,
client/listen
并未指定哪些消息将发送至哪里,只是本地定义了模式的话,会发送至本地的模式中,否则会全部发送至服务器中,我们可以通过一些配置来定义哪些消息将发送到哪些服务中,你可以使用一个
参数来做这件事情。
让我们来创建一个应用,它将通过 TCP 发送所有
消息至服务,而把其它的所有消息都在发送至本地:
require('seneca')()
.use('math')
// 监听 role:math 消息
// 重要:必须匹配客户端
.listen({ type: 'tcp', pin: 'role:math' })
require('seneca')()
// 本地模式
.add('say:hello', function (msg, respond){ respond(null, {text: &Hi!&}) })
// 发送 role:math 模式至服务
// 注意:必须匹配服务端
.client({ type: 'tcp', pin: 'role:math' })
// 远程操作
.act('role:math,cmd:sum,left:1,right:2',console.log)
// 本地操作
.act('say:hello',console.log)
你可以通过各种过滤器来自定义日志的打印,以跟踪消息的流动,使用
--seneca...
参数,支持以下配置:
: log 条目何时被创建;
: Seneca process ID;
中任何一个;
:条目编码,比如
:插件名称,不是插件内的操作将表示为
: 条目的事件:
action-id/transaction-id
:跟踪标识符,_在网络中永远保持一致_;
匹配模式;
:入/出参消息体
如果你运行上面的进程,使用了
--seneca.log.all
,则会打印出所有日志,如果你只想看
插件打印的日志,可以像下面这样启动服务:
node math-pin-service.js --seneca.log=plugin:math
Web 服务集成
Seneca不是一个Web框架。 但是,您仍然需要将其连接到您的Web服务API,你永远要记住的是,不要将你的内部行为模式暴露在外面,这不是一个好的安全的实践,相反的,你应该定义一组API模式,比如用属性
,然后你可以将它们连接到你的内部微服务。
下面是我们定义
module.exports = function api(options) {
var validOps = { sum:'sum', product:'product' }
this.add('role:api,path:calculate', function (msg, respond) {
var operation = msg.args.params.operation
var left = msg.args.query.left
var right = msg.args.query.right
this.act('role:math', {
validOps[operation],
right: right,
}, respond)
this.add('init:api', function (msg, respond) {
this.act('role:web',{routes:{
prefix: '/api',
pin: 'role:api,path:*',
calculate: { GET:true, suffix:'/{operation}' }
}}, respond)
然后,我们使用
作为Web框架,建了
const Hapi = require('hapi');
const Seneca = require('seneca');
const SenecaWeb = require('seneca-web');
const config = {
adapter: require('seneca-web-adapter-hapi'),
context: (() =& {
const server = new Hapi.Server();
server.connection({
port: 3000
server.route({
path: '/routes',
method: 'get',
handler: (request, reply) =& {
const routes = server.table()[0].table.map(route =& {
path: route.path,
method: route.method.toUpperCase(),
description: route.settings.description,
tags: route.settings.tags,
vhost: route.settings.vhost,
cors: route.settings.cors,
jsonp: route.settings.jsonp,
reply(routes)
const seneca = Seneca()
.use(SenecaWeb, config)
.use('math')
.use('api')
.ready(() =& {
const server = seneca.export('web/context')();
server.start(() =& {
server.log('server started on: ' + .uri);
hapi-app.js
之后,访问
,你便可以看到下面这样的信息:
&path&: &/routes&,
&method&: &GET&,
&cors&: false
&path&: &/api/calculate/{operation}&,
&method&: &GET&,
&cors&: false
这表示,我们已经成功的将模式匹配更新至
应用的路由中。访问
,将得到结果:
{&answer&:3}
在上面的示例中,我们直接将
插件也加载到了
实例中,其实我们可以更加合理的进行这种操作,如
文件所示:
const seneca = Seneca()
.use(SenecaWeb, config)
.use('api')
.client({type: 'tcp', pin: 'role:math'})
.ready(() =& {
const server = seneca.export('web/context')();
server.start(() =& {
server.log('server started on: ' + .uri);
我们不注册
插件,而是使用
math-pin-service.js
的服务,并且使用的是
连接,没错,你的微服务就是这样成型了。
注意:永远不要使用外部输入创建操作的消息体,永远显示地在内部创建,这可以有效避免注入攻击。
在上面的的初始化函数中,调用了一个
的模式操作,并且定义了一个
属性,这将定义一个URL地址与操作模式的匹配规则,它有下面这些参数:
:URL 前缀
: 需要映射的模式集
:要用作 URL Endpoint 的
通配符属性列表
你的URL地址将开始于
rol:api, path:*
表示,映射任何有
role=&api&
键值对,同时
属性被定义了的模式,在本例中,只有
role:api,path:calculate
符合该模式。
属性是一个对象,它有一个
属性,对应的URL地址开始于:
/api/calculate
的值是一个对象,它表示了
方法是被允许的,并且URL应该有参数化的后缀(后缀就类于
规则中一样)。
所以,你的完整地址是
/api/calculate/{operation}
然后,其它的消息属性都将从 URL query 对象或者 JSON body 中获得,在本示例中,因为使用的是 GET 方法,所以没有 body。
来描述一次请求,它包括:
:HTTP 请求的
querystring
:请求的路径参数。
现在,启动前面我们创建的微服务:
node math-pin-service.js --seneca.log=plugin:math
然后再启动我们的应用:
node hapi-app.js --seneca.log=plugin:web,plugin:api
访问下面的地址:
{&answer&:6}
{&answer&:5}
数据持久化
一个真实的系统,肯定需要持久化数据,在Seneca中,你可以执行任何您喜欢的操作,使用任何类型的数据库层,但是,为什么不使用模式匹配和微服务的力量,使你的开发更轻松?
模式匹配还意味着你可以推迟有关微服务数据的争论,比如服务是否应该&拥有&数据,服务是否应该访问共享数据库等,模式匹配意味着你可以在随后的任何时间重新配置你的系统。
提供了一个简单的数据抽象层(ORM),基于以下操作:
:根据实体标识加载一个实体;
:创建或更新(如果你提供了一个标识的话)一个实体;
:列出匹配查询条件的所有实体;
:删除一个标识指定的实体。
它们的匹配模式分别是:
role:entity,cmd:load,name:&entity-name&
role:entity,cmd:save,name:&entity-name&
role:entity,cmd:list,name:&entity-name&
role:entity,cmd:remove,name:&entity-name&
任何实现了这些模式的插件都可以被用于提供数据库(比如
当数据的持久化与其它的一切都基于相同的机制提供时,微服务的开发将变得更容易,而这种机制,便是模式匹配消息。
由于直接使用数据持久性模式可能变得乏味,所以
实体还提供了一个更熟悉的
ActiveRecord
风格的接口,要创建记录对象,请调用
seneca.make
方法。 记录对象有方法
(所有方法都带有
后缀,以防止与数据字段冲突),数据字段只是对象属性。
seneca-entity
, 然后在你的应用中使用
seneca.use()
方法加载至你的
现在让我们先创建一个简单的数据实体,它保存
const seneca = require('seneca')();
seneca.use('basic').use('entity');
const book = seneca.make('book');
book.title = 'Action in Seneca';
book.price = 9.99;
// 发送 role:entity,cmd:save,name:book 消息
book.save$( console.log );
在上面的示例中,我们还使用了
seneca-entity
依赖的插件。
执行上面的代码之后,我们可以看到下面这样的日志:
? node book.js
null $-/-/id=byo81d;{title:Action in Seneca,price:9.99}
Seneca 内置了
,这使得我们在本示例中,不需要使用任何其它数据库的支持也能进行完整的数据库持久操作(虽然,它并不是真正的持久化了)。
由于数据的持久化永远都是使用的同样的消息模式集,所以,你可以非常简单的交互数据库,比如,你可能在开发的过程中使用的是
,而后,开发完成之后,在生产环境中使用
下面让我他创建一个简单的线上书店,我们可以通过它,快速的添加新书、获取书的详细信息以及购买一本书:
module.exports = function(options) {
// 从数据库中,查询一本ID为 `msg.id` 的书,我们使用了 `load$` 方法
this.add('role:store, get:book', function(msg, respond) {
this.make('book').load$(msg.id, respond);
// 向数据库中添加一本书,书的数据为 `msg.data`,我们使用了 `data$` 方法
this.add('role:store, add:book', function(msg, respond) {
this.make('book').data$(msg.data).save$(respond);
// 创建一条新的支付订单(在真实的系统中,经常是由商品详情布中的 *购买* 按钮触
// 发的事件),先是查询出ID为 `msg.id` 的书本,若查询出错,则直接返回错误,
// 否则,将书本的信息复制给 `purchase` 实体,并保存该订单,然后,我们发送了
// 一条 `role:store,info:purchase` 消息(但是,我们并不接收任何响应),
// 这条消息只是通知整个系统,我们现在有一条新的订单产生了,但是我并不关心谁会
// 需要它。
this.add('role:store, cmd:purchase', function(msg, respond) {
this.make('book').load$(msg.id, function(err, book) {
if (err) return respond(err);
.make('purchase')
when: Date.now(),
bookId: book.id,
title: book.title,
price: book.price,
.save$(function(err, purchase) {
if (err) return respond(err);
this.act('role:store,info:purchase', {
purchase: purchase
respond(null, purchase);
// 最后,我们实现了 `role:store, info:purchase` 模式,就只是简单的将信息
// 打印出来, `seneca.log` 对象提供了 `debug`、`info`、`warn`、`error`、
// `fatal` 方法用于打印相应级别的日志。
this.add('role:store, info:purchase', function(msg, respond) {
('purchase', msg.purchase);
respond();
接下来,我们可以创建一个简单的单元测试,以验证我们前面创建的程序:
// 使用 Node 内置的 `assert` 模块
const assert = require('assert')
const seneca = require('seneca')()
.use('basic')
.use('entity')
.use('book-store')
.error(assert.fail)
// 添加一本书
function addBook() {
seneca.act(
'role:store,add:book,data:{title:Action in Seneca,price:9.99}',
function(err, savedBook) {
'role:store,get:book', {
id: savedBook.id
function(err, loadedBook) {
assert.equal(loadedBook.title, savedBook.title)
purchase(loadedBook);
function purchase(book) {
seneca.act(
'role:store,cmd:purchase', {
id: book.id
function(err, purchase) {
assert.equal(purchase.bookId, book.id)
执行该测试:
? node book-store-test.js
[&purchase&,{&entity$&:&-/-/purchase&,&when&:5,&bookId&:&a2mlev&,&title&:&Action in Seneca&,&price&:9.99,&id&:&i28xoc&}]
在一个生产应用中,我们对于上面的订单数据,可能会有单独的服务进行监控,而不是像上面这样,只是打印一条日志出来,那么,我们现在来创建一个新的服务,用于收集订单数据:
const stats = {};
require('seneca')()
.add('role:store,info:purchase', function(msg, respond) {
const id = msg.purchase.bookId;
stats[id] = stats[id] || 0;
stats[id]++;
console.log(stats);
respond();
port: 9003,
host: 'localhost',
pin: 'role:store,info:purchase'
然后,更新
book-store-test.js
const seneca = require('seneca')()
.use('basic')
.use('entity')
.use('book-store')
.client({port:9003,host: 'localhost', pin:'role:store,info:purchase'})
.error(assert.fail);
此时,当有新的订单产生时,就会通知到订单监控服务了。
将所有服务集成到一起
通过上面的所有步骤,我们现在已经有四个服务了:
: 用于收集书店的订单信息;
:提供书店相关的功能;
:提供一些数学相关的服务;
:Web 服务
book-store-stats
math-pin-service
我们已经有了,所以,直接启动即可:
node math-pin-service.js --seneca.log.all
node book-store-stats.js --seneca.log.all
现在,我们需要一个
book-store-service
require('seneca')()
.use('basic')
.use('entity')
.use('book-store')
port: 9002,
host: 'localhost',
pin: 'role:store'
port: 9003,
host: 'localhost',
pin: 'role:store,info:purchase'
该服务接收任何
role:store
消息,但同时又将任何
role:store,info:purchase
消息发送至网络,
永远都要记住, client 与 listen 的 pin 配置必须完全一致
现在,我们可以启动该服务:
node book-store-service.js --seneca.log.all
然后,创建我们的
app-all.js
,首选,复制
,这是我们的API。
module.exports = function api(options) {
var validOps = {
sum: 'sum',
product: 'product'
this.add('role:api,path:calculate', function(msg, respond) {
var operation = msg.args.params.operation
var left = msg.args.query.left
var right = msg.args.query.right
this.act('role:math', {
cmd: validOps[operation],
left: left,
right: right,
}, respond)
this.add('role:api,path:store', function(msg, respond) {
if (msg.args.query.id) id = msg.args.query.
if (msg.args.body.id) id = msg.args.body.
const operation = msg.args.params.
const storeMsg = {
role: 'store',
if ('get' === operation) storeMsg.get = 'book';
if ('purchase' === operation) storeMsg.cmd = 'purchase';
this.act(storeMsg, respond);
this.add('init:api', function(msg, respond) {
this.act('role:web', {
prefix: '/api',
pin: 'role:api,path:*',
calculate: {
GET: true,
suffix: '/{operation}'
GET: true,
POST: true,
suffix: '/{operation}'
}, respond)
const Hapi = require('hapi');
const Seneca = require('seneca');
const SenecaWeb = require('seneca-web');
const config = {
adapter: require('seneca-web-adapter-hapi'),
context: (() =& {
const server = new Hapi.Server();
server.connection({
port: 3000
server.route({
path: '/routes',
method: 'get',
handler: (request, reply) =& {
const routes = server.table()[0].table.map(route =& {
path: route.path,
method: route.method.toUpperCase(),
description: route.settings.description,
tags: route.settings.tags,
vhost: route.settings.vhost,
cors: route.settings.cors,
jsonp: route.settings.jsonp,
reply(routes)
const seneca = Seneca()
.use(SenecaWeb, config)
.use('basic')
.use('entity')
.use('math')
.use('api-all')
type: 'tcp',
pin: 'role:math'
port: 9002,
host: 'localhost',
pin: 'role:store'
.ready(() =& {
const server = seneca.export('web/context')();
server.start(() =& {
server.log('server started on: ' + .uri);
// 创建一本示例书籍
seneca.act(
'role:store,add:book', {
title: 'Action in Seneca',
price: 9.99
console.log
启动该服务:
node app-all.js --seneca.log.all
从控制台我们可以看到下面这样的消息:
null $-/-/id=0r7mg7;{title:Action in Seneca,price:9.99}
这表示成功创建了一本ID为
的书籍,现在,我们访问
即可查看该ID的书籍详情(ID是随机的,所以,你生成的ID可能并不是这样的)。
可以查看所有的路由。
然后我们可创建一个新的购买订单:
curl -d '{&id&:&0r7mg7&}' -H &content-type:application/json& http://localhost:3000/api/store/purchase
{&when&:5,&bookId&:&0r7mg7&,&title&:&Action in Seneca&,&price&:9.99,&id&:&8suhf4&}
{&answer&:5}
最佳 Seneca 应用结构实践
推荐你这样做
将业务逻辑与执行分开,放在单独的插件中,比如不同的Node模块、不同的项目甚至同一个项目下不同的文件都是可以的;
使用执行脚本撰写您的应用程序,不要害怕为不同的上下文使用不同的脚本,它们看上去应该很短,比如像下面这样:
var SOME_CONFIG = process.env.SOME_CONFIG || 'some-default-value'
require('seneca')({ some_options: 123 })
// 已存在的 Seneca 插件
.use('community-plugin-0')
.use('community-plugin-1', {some_config: SOME_CONFIG})
.use('community-plugin-2')
// 业务逻辑插件
.use('project-plugin-module')
.use('../plugin-repository')
.use('./lib/local-plugin')
.listen( ... )
.client( ... )
.ready( function() {
// 当 Seneca 启动成功之后的自定义脚本
插件加载顺序很重要,这当然是一件好事,可以主上你对消息的成有绝对的控制权。
不推荐你这样做
将 Seneca 应用的启动与初始化同其它框架的启动与初始化放在一起了,永远记住,保持事务的简单;
将 Seneca 实例当做变量到处传递。
已发表评论数()
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
主题不准确
没有分页内容
图片无法显示
视频无法显示
与原文不一致

我要回帖

更多关于 node.js 构建工具 的文章

 

随机推荐