原文: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 啓動成功以後的自定義腳本 })