原文:http://onmr.com/press/getting-started-seneca.htmljavascript
Seneca 是一个能让您快速构建基于消息的微服务系统的工具集,你不须要知道各类服务自己被部署在何处,不须要知道具体有多少服务存在,也不须要知道他们具体作什么,任何你业务逻辑以外的服务(如数据库、缓存或者第三方集成等)都被隐藏在微服务以后。html
这种解耦使您的系统易于连续构建与更新,Seneca 能作到这些,缘由在于它的三大核心功能:java
在 Seneca 中,消息就是一个能够有任何你喜欢的内部结构的 JSON
对象,它们能够经过 HTTP/HTTPS、TCP、消息队列、发布/订阅服务或者任何能传输数据的方式进行传输,而对于做为消息生产者的你来说,你只须要将消息发送出去便可,彻底不须要关心哪些服务来接收它们。node
而后,你又想告诉这个世界,你想要接收一些消息,这也很简单,你只需在 Seneca 中做一点匹配模式配置便可,匹配模式也很简单,只是一个键值对的列表,这些键值对被用于匹配 JSON
消息的极组属性。mysql
在本文接下来的内容中,咱们将一同基于 Seneca 构建一些微服务。git
让咱们从一点特别简单的代码开始,咱们将建立两个微服务,一个会进行数学计算,另外一个去调用它:github
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', left: 1, right: 2 }, (err, result) => { if (err) { return console.error(err); } console.log(result); });
将上面的代码,保存至一个 js
文件中,而后执行它,你可能会在 console
中看到相似下面这样的消息:web
{"kind":"notice","notice":"hello seneca 4y8daxnikuxp/1483577040151/58922/3.2.2/-","level":"info","when":1483577040175} (node:58922) DeprecationWarning: 'root' is deprecated, use 'global' { answer: 3 }
到目前为止,全部这一切都发生在同一个进程中,没有网络流量产生,进程内的函数调用也是基于消息传输。sql
seneca.add
方法,添加了一个新的动做模式(_Action Pattern_)至 Seneca
实例中,它有两个参数:数据库
pattern
:用于匹配 Seneca 实例中 JSON
消息体的模式;action
:当模式被匹配时执行的操做seneca.act
方法一样有两个参数:
msg
:做为纯对象提供的待匹配的入站消息;respond
:用于接收并处理响应信息的回调函数。让咱们再把全部代码从新过一次:
seneca.add('role:math, cmd:sum', (msg, reply) => { reply(null, { answer: ( msg.left + msg.right )}) });
在上面的代码中的 Action
函数,计算了匹配到的消息体中两个属性 left
与 right
的值的和,并非全部的消息都会被建立一个响应,可是在绝大多数状况下,是须要有响应的, Seneca 提供了用于响应消息的回调函数。
在匹配模式中, role:math, cmd:sum
匹配到了下面这个消息体:
{ role: 'math', cmd: 'sum', left: 1, right: 2 }
并获得计自结果:
{ answer: 3 }
关于 role
与 cmd
这两个属性,它们没有什么特别的,只是刚好被你用于匹配模式而已。
接着,seneca.act
方法,发送了一条消息,它有两个参数:
msg
:发送的消息主体response_callback
:若是该消息有任何响应,该回调函数都会被执行。响应的回调函数可接收两个参数: error
与 result
,若是有任何错误发生(好比,发送出去的消息未被任何模式匹配),则第一个参数将是一个 Error
对象,而若是程序按照咱们所预期的方向执行了的话,那么,第二个参数将接收到响应结果,在咱们的示例中,咱们只是简单的将接收到的响应结果打印至了 console
而已。
seneca.act({ role: 'math', cmd: 'sum', left: 1, right: 2 }, (err, result) => { if (err) { return console.error(err); } console.log(result); });
sum.js 示例文件,向你展现了如何定义并建立一个 Action 以及如何呼起一个 Action,但它们都发生在一个进程中,接下来,咱们很快就会展现如何拆分红不一样的代码和多个进程。
模式----而不是网络地址或者会话,让你能够更加容易的扩展或加强您的系统,这样作,让添加新的微服务变得更简单。
如今让咱们给系统再添加一个新的功能----计算两个数字的乘积。
咱们想要发送的消息看起来像下面这样的:
{ role: 'math', cmd: 'product', left: 3, right: 4 }
然后得到的结果看起来像下面这样的:
{ 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', left: 3, right: 4 }, (err, result) => { if (err) { return console.error(err); } console.log(result); });
运行 product.js ,你将获得你想要的结果。
将这两个方法放在一块儿,代码像是下面这样的:
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)
运行 sum-product.js 后,你将获得下面这样的结果:
null { answer: 3 } null { answer: 12 }
在上面合并到一块儿的代码中,咱们发现, seneca.act
是能够进行链式调用的,Seneca
提供了一个链式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,小数部分已经被移除了
代码可在 sum-integer.js 中查看。
如今,你的两个模式都存在于系统中了,并且还存在交叉部分,那么 Seneca
最终会将消息匹配至哪条模式呢?原则是:更多匹配项目被匹配到的优先,被匹配到的属性越多,则优先级越高。
pattern-priority-testing.js 能够给咱们更加直观的测试:
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) }, 100)
输出结果应该像下面这样:
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)
在上面的示例代码中,咱们使用了 this.act
而不是前面的 seneca.act
,那是由于,在 action
函数中,上下文关系变量 this
,引用了当前的 seneca
实例,这样你就能够在任何一个 action
函数中,访问到该 action
调用的整个上下文。
在上面的代码中,咱们使用了 JSON 缩写形式来描述模式与消息, 好比,下面是对象字面量:
{role: 'math', cmd: 'sum', left: 1.5, right: 2.5}
缩写模式为:
'role: math, cmd: sum, left: 1.5, right: 2.5'
jsonic 这种格式,提供了一种以字符串字面量来表达对象的简便方式,这使得咱们能够建立更加简单的模式和消息。
上面的代码保存在了 sum-reuse.js 文件中。
你定义的 Action 模式都是惟一了,它们只能触发一个函数,模式的解析规则以下:
规则被设计得很简单,这使得你能够更加简单的了解到究竟是哪一个模式被匹配了。
下面这些示例可让你更容易理解:
a: 1, b: 2
优先于 a: 1
, 由于它有更多的属性;a: 1, b: 2
优先于 a: 1, c: 3
,由于 b
在 c
字母的前面;a: 1, b: 2, d: 4
优先于 a: 1, c: 3, d:4
,由于 b
在 c
字母的前面;a: 1, b:2, c:3
优先于 a:1, b: 2
,由于它有更多的属性;a: 1, b:2, c:3
优先于 a:1, c:3
,由于它有更多的属性。不少时间,提供一种可让你不须要全盘修改现有 Action 函数的代码便可增长它功能的方法是颇有必要的,好比,你可能想为某一个消息增长更多自定义的属性验证方法,捕获消息统计信息,添加额外的数据库结果中,或者控制消息流速等。
我下面的示例代码中,加法操做指望 left
和 right
属性是有限数,此外,为了调试目的,将原始输入参数附加到输出的结果中也是颇有用的,您可使用如下代码添加验证检查和调试信息:
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 with ,添加额外的功能 .add( '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) result.info = 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' } )
seneca
实例提供了一个名为 prior
的方法,让能够在当前的 action
方法中,调用被其重写的旧操做函数。
prior
函数接受两个参数:
msg
:消息体response_callback
:回调函数在上面的示例代码中,已经演示了如何修改入参与出参,修改这些参数与值是可选的,好比,能够再添加新的重写,以增长日志记录功能。
在上面的示例中,也一样演示了如何更好的进行错误处理,咱们在真正进行操做以前,就验证的数据的正确性,若传入的参数自己就有错误,那么咱们直接就返回错误信息,而不须要等待真正计算的时候由系统去报错了。
错误消息应该只被用于描述错误的输入或者内部失败信息等,好比,若是你执行了一些数据库的查询,返回没有任何数据,这并非一个错误,而仅仅只是数据库的事实的反馈,可是若是链接数据库失败,那就是一个错误了。
上面的代码能够在 sum-valid.js 文件中找到。
一个 seneca
实例,其实就只是多个 Action Patterm
的集合而已,你可使用命名空间的方式来组织操做模式,例如在前面的示例中,咱们都使用了 role: math
,为了帮助日志记录和调试, Seneca
还支持一个简约的插件支持。
一样,Seneca插件只是一组操做模式的集合,它能够有一个名称,用于注释日志记录条目,还能够给插件一组选项来控制它们的行为,插件还提供了以正确的顺序执行初始化函数的机制,例如,您但愿在尝试从数据库读取数据以前创建数据库链接。
简单来讲,Seneca插件就只是一个具备单个参数选项的函数,你将这个插件定义函数传递给 seneca.use
方法,下面这个是最小的Seneca插件(其实它什么也没作!):
function minimal_plugin(options) { console.log(options) } require('seneca')() .use(minimal_plugin, {foo: 'bar'})
seneca.use
方法接受两个参数:
plugin
:插件定义函数或者一个插件名称;options
:插件配置选项上面的示例代码执行后,打印出来的日志看上去是这样的:
{"kind":"notice","notice":"hello seneca 3qk0ij5t2bta/1483584697034/62768/3.2.2/-","level":"info","when":1483584697057} (node:62768) DeprecationWarning: 'root' is deprecated, use 'global' { foo: 'bar' }
Seneca 还提供了详细日志记录功能,能够提供为开发或者生产提供更多的日志信息,一般的,日志级别被设置为 INFO
,它并不会打印太多日志信息,若是想看到全部的日志信息,试试如下面这样的方式启动你的服务:
node minimal-plugin.js --seneca.log.all
会不会被吓一跳?固然,你还能够过滤日志信息:
node minimal-plugin.js --seneca.log.all | grep plugin:define
经过日志咱们能够看到, seneca 加载了不少内置的插件,好比 basic
、transport
、web
以及 mem-store
,这些插件为咱们提供了建立微服务的基础功能,一样,你应该也能够看到 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)
运行 math-plugin.js 文件,获得下面这样的信息:
null { answer: 3 }
看打印出来的一条日志:
{ "actid": "7ubgm65mcnfl/uatuklury90r", "msg": { "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": 1483587274794, "sync": true }, "plugin$": { "name": "math", "tag": "-" }, "tx$": "uatuklury90r" }, "entry": true, "prior": [], "meta": { "plugin_name": "math", "plugin_tag": "-", "plugin_fullname": "math", "raw": { "role": "math", "cmd": "sum" }, "sub": false, "client": false, "args": { "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", "when": 1483587274829 }
全部的该插件的日志都被自动的添加了 plugin
属性。
在 Seneca 的世界中,咱们经过插件组织各类操做模式集合,这让日志与调试变得更简单,而后你还能够将多个插件合并成为各类微服务,在接下来的章节中,咱们将建立一个 math
服务。
插件经过须要进行一些初始化的工做,好比链接数据库等,可是,你并不须要在插件的定义函数中去执行这些初始化,定义函数被设计为同步执行的,由于它的全部操做都是在定义一个插件,事实上,你不该该在定义函数中调用 seneca.act
方法,只调用 seneca.add
方法。
要初始化插件,你须要定义一个特殊的匹配模式 init: <plugin-name>
,对于每个插件,将按顺序调用此操做模式,init
函数必须调用其 callback
函数,而且不能有错误发生,若是插件初始化失败,则 Seneca 会当即退出 Node 进程。因此的插件初始化工做都必须在任何操做执行以前完成。
为了演示初始化,让咱们向 math
插件添加简单的自定义日志记录,当插件启动时,它打开一个日志文件,并将全部操做的日志写入文件,文件须要成功打开而且可写,若是这失败,微服务启动就应该失败。
const fs = require('fs') function math(options) { // 日志记录函数,经过 init 函数建立 var log // 将全部模式放在一块儿会上咱们查找更方便 this.add('role:math,cmd:sum', 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) respond() }) } 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)
在上面这个插件的代码中,匹配模式被组织在插件的顶部,以便它们更容易被看到,函数在这些模式下面一点被定义,您还能够看到如何使用选项提供自定义日志文件的位置(不言而喻,这不是生产日志!)。
初始化函数 init
执行一些异步文件系统工做,所以必须在执行任何操做以前完成。 若是失败,整个服务将没法初始化。要查看失败时的操做,能够尝试将日志文件位置更改成无效的,例如 /math.log
。
以上代码能够在 math-plugin-init.js 文件中找到。
如今让咱们把 math
插件变成一个真正的微服务。首先,你须要组织你的插件。 math
插件的业务逻辑 ---- 即它提供的功能,与它以何种方式与外部世界通讯是分开的,你可能会暴露一个Web服务,也有可能在消息总线上监听。
将业务逻辑(即插件定义)放在其本身的文件中是有意义的。 Node.js 模块便可完美的实现,建立一个名为 math.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) { msg.left = 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
去扩展能够获得同样的效果,它须要两个参数:
pin
:模式匹配模式action
:扩展的 action
函数pin
是一个能够匹配到多个模式的模式,它能够匹配到多个模式,好比 role:math
这个 pin
能够匹配到 role:math, cmd:sum
与 role:math, cmd:product
。
在上面的示例中,咱们在最后面的 wrap
函数中,确保了,任何传递给 role:math
的消息体中 left
与 right
值都是数字,即便咱们传递了字符串,也能够被自动的转换为数字。
有时,查看 Seneca 实例中有哪些操做是被重写了是颇有用的,你能够在启动应用时,加上 --seneca.print.tree
参数便可,咱们先建立一个 math-tree.js 文件,填入如下内容:
require('seneca')() .use('math')
而后再执行它:
❯ node math-tree.js --seneca.print.tree {"kind":"notice","notice":"hello seneca abs0eg4hu04h/1483589278500/65316/3.2.2/-","level":"info","when":1483589278522} (node:65316) DeprecationWarning: 'root' is deprecated, use 'global' Seneca action patterns for instance: abs0eg4hu04h/1483589278500/65316/3.2.2/- ├─┬ cmd:sum │ └─┬ role:math │ └── # math, (15fqzd54pnsp), │ # math, (qqrze3ub5vhl), sum └─┬ cmd:product └─┬ role:math └── # math, (qnh86mgin4r6), # math, (4nrxi5f6sp69), product
从上面你能够看到不少的键/值对,而且以树状结构展现了重写,全部的 Action
函数展现的格式都是 #plugin, (action-id), function-name
。
可是,到如今为止,全部的操做都还存在于同一个进程中,接下来,让咱们先建立一个名为 math-service.js 的文件,填入如下内容:
require('seneca')() .use('math') .listen()
而后启动该脚本,便可启动咱们的微服务,它会启动一个进程,并经过 10101
端口监听HTTP请求,它不是一个 Web 服务器,在此时, HTTP
仅仅做为消息的传输机制。
你如今能够访问 http://localhost:10101/act?ro... 便可看到结果,或者使用 curl
命令:
curl -d '{"role":"math","cmd":"sum","left":1,"right":2}' http://localhost:10101/act
两种方式均可以看到结果:
{"answer":3}
接下来,你须要一个微服务客户端 math-client.js:
require('seneca')() .client() .act('role:math,cmd:sum,left:1,right:2',console.log)
打开一个新的终端,执行该脚本:
null { answer: 3 } { id: '7uuptvpf8iff/9wfb26kbqx55', accept: '043di4pxswq7/1483589685164/65429/3.2.2/-', track: undefined, time: { client_sent: '0', listen_recv: '0', listen_sent: '0', client_recv: 1483589898390 } }
在 Seneca
中,咱们经过 seneca.listen
方法建立微服务,而后经过 seneca.client
去与微服务进行通讯。在上面的示例中,咱们使用的都是 Seneca 的默认配置,好比 HTTP
协议监听 10101
端口,但 seneca.listen
与 seneca.client
方法均可以接受下面这些参数,以达到定抽的功能:
port
:可选的数字,表示端口号;host
:可先的字符串,表示主机名或者IP地址;spec
:可选的对象,完整的定制对象注意:在 Windows 系统中,若是未指定host
, 默认会链接0.0.0.0
,这是没有任何用处的,你能够设置host
为localhost
。
只要 client
与 listen
的端口号与主机一致,它们就能够进行通讯:
Seneca 为你提供的 无依赖传输 特性,让你在进行业务逻辑开发时,不须要知道消息如何传输或哪些服务会获得它们,而是在服务设置代码或配置中指定,好比 math.js
插件中的代码永远不须要改变,咱们就能够任意的改变传输方式。
虽然 HTTP
协议很方便,可是并非全部时间都合适,另外一个经常使用的协议是 TCP
,咱们能够很容易的使用 TCP
协议来进行数据的传输,尝试下面这两个文件:
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
并未指定哪些消息将发送至哪里,只是本地定义了模式的话,会发送至本地的模式中,不然会所有发送至服务器中,咱们能够经过一些配置来定义哪些消息将发送到哪些服务中,你可使用一个 pin
参数来作这件事情。
让咱们来建立一个应用,它将经过 TCP 发送全部 role:math
消息至服务,而把其它的全部消息都在发送至本地:
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...
参数,支持如下配置:
date-time
: log 条目什么时候被建立;seneca-id
: Seneca process ID;level
:DEBUG
、INFO
、WARN
、ERROR
以及 FATAL
中任何一个;type
:条目编码,好比 act
、plugin
等;plugin
:插件名称,不是插件内的操做将表示为 root$
;case
: 条目的事件:IN
、ADD
、OUT
等action-id/transaction-id
:跟踪标识符,_在网络中永远保持一致_;pin
:action
匹配模式;message
:入/出参消息体若是你运行上面的进程,使用了 --seneca.log.all
,则会打印出全部日志,若是你只想看 math
插件打印的日志,能够像下面这样启动服务:
node math-pin-service.js --seneca.log=plugin:math
Seneca不是一个Web框架。 可是,您仍然须要将其链接到您的Web服务API,你永远要记住的是,不要将你的内部行为模式暴露在外面,这不是一个好的安全的实践,相反的,你应该定义一组API模式,好比用属性 role:api
,而后你能够将它们链接到你的内部微服务。
下面是咱们定义 api.js 插件。
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('init:api', function (msg, respond) { this.act('role:web',{routes:{ prefix: '/api', pin: 'role:api,path:*', map: { calculate: { GET:true, suffix:'/{operation}' } } }}, respond) }) }
而后,咱们使用 hapi
做为Web框架,建了 hapi-app.js 应用:
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 => { return { 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) } }); return server; })() }; const seneca = Seneca() .use(SenecaWeb, config) .use('math') .use('api') .ready(() => { const server = seneca.export('web/context')(); server.start(() => { server.log('server started on: ' + server.info.uri); }); });
启动 hapi-app.js
以后,访问 http://localhost:3000/routes,你即可以看到下面这样的信息:
[ { "path": "/routes", "method": "GET", "cors": false }, { "path": "/api/calculate/{operation}", "method": "GET", "cors": false } ]
这表示,咱们已经成功的将模式匹配更新至 hapi
应用的路由中。访问 http://localhost:3000/api/cal... ,将获得结果:
{"answer":3}
在上面的示例中,咱们直接将 math
插件也加载到了 seneca
实例中,其实咱们能够更加合理的进行这种操做,如 hapi-app-client.js 文件所示:
... 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: ' + server.info.uri); }); });
咱们不注册 math
插件,而是使用 client
方法,将 role:math
发送给 math-pin-service.js
的服务,而且使用的是 tcp
链接,没错,你的微服务就是这样成型了。
注意:永远不要使用外部输入建立操做的消息体,永远显示地在内部建立,这能够有效避免注入攻击。
在上面的的初始化函数中,调用了一个 role:web
的模式操做,而且定义了一个 routes
属性,这将定义一个URL地址与操做模式的匹配规则,它有下面这些参数:
prefix
:URL 前缀pin
: 须要映射的模式集map
:要用做 URL Endpoint 的 pin
通配符属性列表你的URL地址将开始于 /api/
。
rol:api, path:*
这个 pin
表示,映射任何有 role="api"
键值对,同时 path
属性被定义了的模式,在本例中,只有 role:api,path:calculate
符合该模式。
map
属性是一个对象,它有一个 calculate
属性,对应的URL地址开始于:/api/calculate
。
按着, calculate
的值是一个对象,它表示了 HTTP
的 GET
方法是被容许的,而且URL应该有参数化的后缀(后缀就类于 hapi
的 route
规则中同样)。
因此,你的完整地址是 /api/calculate/{operation}
。
而后,其它的消息属性都将从 URL query 对象或者 JSON body 中得到,在本示例中,由于使用的是 GET 方法,因此没有 body。
SenecaWeb
将会经过 msg.args
来描述一次请求,它包括:
body
:HTTP 请求的 payload
部分;query
:请求的 querystring
;params
:请求的路径参数。如今,启动前面咱们建立的微服务:
node math-pin-service.js --seneca.log=plugin:math
而后再启动咱们的应用:
node hapi-app.js --seneca.log=plugin:web,plugin:api
访问下面的地址:
{"answer":6}
{"answer":5}
一个真实的系统,确定须要持久化数据,在Seneca中,你能够执行任何您喜欢的操做,使用任何类型的数据库层,可是,为何不使用模式匹配和微服务的力量,使你的开发更轻松?
模式匹配还意味着你能够推迟有关微服务数据的争论,好比服务是否应该"拥有"数据,服务是否应该访问共享数据库等,模式匹配意味着你能够在随后的任什么时候间从新配置你的系统。
seneca-entity 提供了一个简单的数据抽象层(ORM),基于如下操做:
load
:根据实体标识加载一个实体;save
:建立或更新(若是你提供了一个标识的话)一个实体;list
:列出匹配查询条件的全部实体;remove
:删除一个标识指定的实体。它们的匹配模式分别是:
load
: role:entity,cmd:load,name:<entity-name>
save
: role:entity,cmd:save,name:<entity-name>
list
: role:entity,cmd:list,name:<entity-name>
remove
: role:entity,cmd:remove,name:<entity-name>
任何实现了这些模式的插件均可以被用于提供数据库(好比 MySQL)访问。
当数据的持久化与其它的一切都基于相同的机制提供时,微服务的开发将变得更容易,而这种机制,即是模式匹配消息。
因为直接使用数据持久性模式可能变得乏味,因此 seneca
实体还提供了一个更熟悉的 ActiveRecord
风格的接口,要建立记录对象,请调用 seneca.make
方法。 记录对象有方法 load$
、save$
、list$
以及 remove$
(全部方法都带有 $
后缀,以防止与数据字段冲突),数据字段只是对象属性。
经过 npm
安装 seneca-entity
, 而后在你的应用中使用 seneca.use()
方法加载至你的 seneca
实例。
如今让咱们先建立一个简单的数据实体,它保存 book
的详情。
文件 book.js
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-basic,它是 seneca-entity
依赖的插件。
执行上面的代码以后,咱们能够看到下面这样的日志:
❯ node book.js null $-/-/book;id=byo81d;{title:Action in Seneca,price:9.99}
Seneca 内置了 mem-store,这使得咱们在本示例中,不须要使用任何其它数据库的支持也能进行完整的数据库持久操做(虽然,它并非真正的持久化了)。
因为数据的持久化永远都是使用的一样的消息模式集,因此,你能够很是简单的交互数据库,好比,你可能在开发的过程当中使用的是 MongoDB,然后,开发完成以后,在生产环境中使用 Postgres。
下面让我他建立一个简单的线上书店,咱们能够经过它,快速的添加新书、获取书的详细信息以及购买一本书:
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); this .make('purchase') .data$({ 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) { this.log.info('purchase', msg.purchase); respond(); }); };
接下来,咱们能够建立一个简单的单元测试,以验证咱们前面建立的程序:
// 使用 Node 内置的 `assert` 模块 const assert = require('assert') const seneca = require('seneca')() .use('basic') .use('entity') .use('book-store') .error(assert.fail) // 添加一本书 addBook() function addBook() { seneca.act( 'role:store,add:book,data:{title:Action in Seneca,price:9.99}', function(err, savedBook) { this.act( '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":1483607360925,"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(); }) .listen({ 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);
此时,当有新的订单产生时,就会通知到订单监控服务了。
经过上面的全部步骤,咱们如今已经有四个服务了:
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') .listen({ port: 9002, host: 'localhost', pin: 'role:store' }) .client({ 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.js
文件到 api-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) { let id = null; if (msg.args.query.id) id = msg.args.query.id; if (msg.args.body.id) id = msg.args.body.id; const operation = msg.args.params.operation; const storeMsg = { role: 'store', id: id }; 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', { routes: { prefix: '/api', pin: 'role:api,path:*', map: { calculate: { GET: true, suffix: '/{operation}' }, store: { GET: true, POST: true, suffix: '/{operation}' } } } }, respond) }) }
最后, app-all.js:
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 => { return { 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) } }); return server; })() }; const seneca = Seneca() .use(SenecaWeb, config) .use('basic') .use('entity') .use('math') .use('api-all') .client({ type: 'tcp', pin: 'role:math' }) .client({ port: 9002, host: 'localhost', pin: 'role:store' }) .ready(() => { const server = seneca.export('web/context')(); server.start(() => { server.log('server started on: ' + server.info.uri); }); }); // 建立一本示例书籍 seneca.act( 'role:store,add:book', { data: { title: 'Action in Seneca', price: 9.99 } }, console.log )
启动该服务:
node app-all.js --seneca.log.all
从控制台咱们能够看到下面这样的消息:
null $-/-/book;id=0r7mg7;{title:Action in Seneca,price:9.99}
这表示成功建立了一本ID为 0r7mg7
的书籍,如今,咱们访问 http://localhost:3000/api/store/get?id=0r7mg7 便可查看该ID的书籍详情(ID是随机的,因此,你生成的ID可能并非这样的)。
http://localhost:3000/routes 能够查看全部的路由。
而后咱们可建立一个新的购买订单:
curl -d '{"id":"0r7mg7"}' -H "content-type:application/json" http://localhost:3000/api/store/purchase {"when":1483609872715,"bookId":"0r7mg7","title":"Action in Seneca","price":9.99,"id":"8suhf4"}
访问 http://localhost:3000/api/calculate/sum?left=2&right=3 能够获得 {"answer":5}
。
使用执行脚本撰写您的应用程序,不要惧怕为不一样的上下文使用不一样的脚本,它们看上去应该很短,好比像下面这样:
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 启动成功以后的自定义脚本 })