├── .eslintignore ├── .coveralls.yml ├── .eslintrc.js ├── screenshots.png ├── test ├── .eslintrc.js ├── test_utils.js ├── test_error.js ├── test_exports.js ├── test_service.js ├── test_promise.js ├── test_manager.js └── test_context.js ├── .travis.yml ├── example ├── logstash.conf ├── package.json ├── tasks.run.js └── test.js ├── lib ├── error.js ├── service.js ├── log_recorder │ ├── stream.js │ └── logger.js ├── manager.js ├── utils.js └── context.js ├── index.js ├── .gitignore ├── LICENSE ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | local-example 2 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: bXqlK0o41Pr4sIKgpgjQhv3V6SDLfYYOJ 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: 'lei', 4 | }; 5 | -------------------------------------------------------------------------------- /screenshots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SuperID/nanoservices/HEAD/screenshots.png -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: 'lei/mocha', 4 | }; 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4.0 4 | - 5.0 5 | - 6.0 6 | - 7.0 7 | -------------------------------------------------------------------------------- /example/logstash.conf: -------------------------------------------------------------------------------- 1 | input { 2 | file { 3 | path => "$dir/logs/**/*.log" 4 | codec => "json" 5 | } 6 | } 7 | 8 | output { 9 | stdout { 10 | codec => "rubydebug" 11 | workers => 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "test.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "cli-color": "^1.1.0", 13 | "lei-stream": "^1.1.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/tasks.run.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global register, path, fs, exec */ 4 | 5 | register('logstash', function () { 6 | const f = path.resolve(__dirname, 'logstash.conf'); 7 | let c = fs.readFileSync(f).toString().trim(); 8 | c = c.replace(/\$dir/g, __dirname); 9 | exec(`logstash -e '${ c }'`); 10 | }); 11 | 12 | register('test', function () { 13 | exec(`node test`); 14 | }); 15 | -------------------------------------------------------------------------------- /lib/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const utils = require('./utils'); 10 | 11 | exports.ServiceNotFoundError = utils.customError('ServiceNotFoundError', { 12 | code: 'SERVICE_NOT_FOUND', 13 | }); 14 | 15 | exports.InvalidLogLineFormatError = utils.customError('InvalidLogLineFormatError', { 16 | code: 'INVALID_LOG_LINE_FORMAT', 17 | }); 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | exports.Context = require('./lib/context'); 10 | const Manager = exports.Manager = require('./lib/manager'); 11 | exports.Service = require('./lib/service'); 12 | exports.utils = require('./lib/utils'); 13 | exports.error = require('./lib/error'); 14 | exports.LoggerRecorder = require('./lib/log_recorder/logger'); 15 | exports.StreamRecorder = require('./lib/log_recorder/stream'); 16 | 17 | const globalManager = new Manager(); 18 | 19 | exports.globalManager = globalManager; 20 | exports.register = globalManager.register.bind(globalManager); 21 | exports.call = globalManager.call.bind(globalManager); 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | local-example 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 SuperID | 免费极速身份验证服务 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const assert = require('assert'); 10 | const isPromise = require('lei-utils').isPromise; 11 | 12 | class Service { 13 | 14 | /** 15 | * Service 16 | * 17 | * @constructor 18 | * @param {String} name 19 | * @param {Function} handler 20 | */ 21 | constructor(name, handler) { 22 | assert(typeof name === 'string', `new Service(name, handler): name必须是字符串类型`); 23 | assert(typeof handler === 'function', 'new Service(name, handler): handler必须为一个函数'); 24 | assert(handler.length === 1, 'new Service(name, handler): handler函数只能有一个参数,比如function (ctx) {}'); 25 | this.name = name; 26 | this.handler = handler; 27 | } 28 | 29 | /** 30 | * 执行服务 31 | * 32 | * @param {Object} ctx 33 | * - {Function} error 34 | */ 35 | call(ctx) { 36 | setImmediate(() => { 37 | let p; 38 | try { 39 | p = this.handler.call(ctx, ctx); 40 | } catch (err) { 41 | ctx.error(err); 42 | } 43 | if (isPromise(p)) { 44 | p.catch(err => ctx.error(err)); 45 | } 46 | }); 47 | } 48 | 49 | } 50 | 51 | module.exports = Service; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nanoservices", 3 | "version": "0.0.11", 4 | "description": "比微服务更小的纳米服务框架", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js", 8 | "lib" 9 | ], 10 | "scripts": { 11 | "test": "npm run lint && mocha -t 10000", 12 | "test-cov": "istanbul cover _mocha --report lcovonly -- -t 10000 -R spec && cat ./coverage/lcov.info | coveralls", 13 | "lint": "eslint lib test example/*.js --fix && echo 'eslint passed'", 14 | "prepublish": "npm test" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/SuperID/nanoservices.git" 19 | }, 20 | "keywords": [ 21 | "microservices" 22 | ], 23 | "author": "Zongmin Lei ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/SuperID/nanoservices/issues" 27 | }, 28 | "homepage": "https://github.com/SuperID/nanoservices#readme", 29 | "dependencies": { 30 | "bluebird": "^3.4.1", 31 | "lei-utils": "^3.0.0", 32 | "mkdirp": "^0.5.1" 33 | }, 34 | "devDependencies": { 35 | "coveralls": "^2.11.11", 36 | "eslint": "^3.8.1", 37 | "eslint-config-lei": "^0.0.16", 38 | "eslint-plugin-promise": "^3.3.0", 39 | "istanbul": "^0.4.4", 40 | "lei-coroutine": "^1.2.3", 41 | "mocha": "^3.1.2" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/test_utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices test 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const assert = require('assert'); 10 | const utils = require('../lib/utils'); 11 | 12 | describe('utils', function () { 13 | 14 | describe('newRequestId(size)', function () { 15 | 16 | it('不指定长度会报错', function () { 17 | assert.throws(() => { 18 | utils.newRequestId(); 19 | }, /AssertionError/); 20 | }); 21 | 22 | it('长度范围不正确会报错', function () { 23 | assert.throws(() => { 24 | utils.newRequestId(10); 25 | }, /AssertionError/); 26 | assert.throws(() => { 27 | utils.newRequestId(60); 28 | }, /AssertionError/); 29 | }); 30 | 31 | it('指定长度', function () { 32 | assert.equal(utils.newRequestId(20).length, 20); 33 | assert.equal(utils.newRequestId(30).length, 30); 34 | }); 35 | 36 | }); 37 | 38 | describe('appendRequestId(id, index)', function () { 39 | 40 | it('生成的ID必须同时包含主ID和后缀', function () { 41 | const id = utils.newRequestId(20); 42 | const suffix = '5:6'; 43 | const newId = utils.appendRequestId(id, suffix); 44 | assert.equal(newId.indexOf(id), 0); 45 | assert.notEqual(newId.indexOf(suffix), -1); 46 | }); 47 | 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /test/test_error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices test 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const assert = require('assert'); 10 | const Manager = require('../lib/manager'); 11 | 12 | describe('Service', function () { 13 | 14 | describe('function', function () { 15 | 16 | it('在 service 内出错被捕捉到', function (done) { 17 | const manager = new Manager(); 18 | manager.register('test', function (ctx) { 19 | if (Date.now() > 0) { 20 | throw new Error('test'); 21 | } 22 | ctx.result({ ok: true }); 23 | }); 24 | manager.call('test', {}, (err, _ret) => { 25 | assert.equal(err && err.message, 'test'); 26 | done(); 27 | }); 28 | }); 29 | 30 | }); 31 | 32 | describe('async function (Promise)', function () { 33 | 34 | it('在 service 内出错被捕捉到', function (done) { 35 | const manager = new Manager(); 36 | manager.register('test', function (ctx) { 37 | return new Promise((resolve, reject) => { 38 | if (Date.now() > 0) { 39 | throw new Error('test'); 40 | } 41 | ctx.result({ ok: true }); 42 | }); 43 | }); 44 | manager.call('test', {}, (err, _ret) => { 45 | assert.equal(err && err.message, 'test'); 46 | done(); 47 | }); 48 | }); 49 | 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /lib/log_recorder/stream.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const assert = require('assert'); 10 | const utils = require('../utils'); 11 | 12 | class StreamRecorder { 13 | 14 | /** 15 | * 基于流的日志记录器 16 | * 17 | * @param {Object} stream 18 | * @param {Object} options 19 | * - {String} format 格式 20 | * - {String} newLine 换行符,为空时不自动加上换行符 21 | */ 22 | constructor(stream, options) { 23 | 24 | assert.ok(stream, `new StreamRecorder(stream): 缺少stream参数`); 25 | assert.equal(typeof stream.write, 'function', `new StreamRecorder(stream): stream.write不是一个函数`); 26 | this.stream = stream; 27 | 28 | const opts = Object.assign({ 29 | format: '$date $time $type: [$id] $content', 30 | newLine: false, 31 | }, options); 32 | 33 | assert.equal(typeof opts.format, 'string', `new LoggerRecorder(logger, { format }): format不是字符串`); 34 | this.format = utils.compileLogFormat(opts.format); 35 | 36 | this.newLine = opts.newLine; 37 | 38 | } 39 | 40 | /** 41 | * 打印日志 42 | * 43 | * @param {Object} ctx 当前的context 44 | * @param {String} type 日志类型 45 | * @param {String} content 内容 46 | */ 47 | write(ctx, type, content) { 48 | let str = this.format(ctx, type, content); 49 | if (this.newLine) str += this.newLine; 50 | this.stream.write(str); 51 | } 52 | 53 | } 54 | 55 | module.exports = StreamRecorder; 56 | -------------------------------------------------------------------------------- /test/test_exports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices test 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const assert = require('assert'); 10 | const Context = require('../lib/context'); 11 | const Manager = require('../lib/manager'); 12 | const Service = require('../lib/service'); 13 | const utils = require('../lib/utils'); 14 | const error = require('../lib/error'); 15 | const microservices = require('../'); 16 | 17 | describe('exports', function () { 18 | 19 | it('Context', function () { 20 | assert.equal(microservices.Context, Context); 21 | }); 22 | 23 | it('Manager', function () { 24 | assert.equal(microservices.Manager, Manager); 25 | }); 26 | 27 | it('Service', function () { 28 | assert.equal(microservices.Service, Service); 29 | }); 30 | 31 | it('utils', function () { 32 | assert.equal(microservices.utils, utils); 33 | }); 34 | 35 | it('error', function () { 36 | assert.equal(microservices.error, error); 37 | }); 38 | 39 | it('globalManager', function () { 40 | assert(microservices.globalManager instanceof Manager); 41 | }); 42 | 43 | it('call()', function () { 44 | assert(typeof microservices.call === 'function'); 45 | }); 46 | 47 | it('register()', function () { 48 | assert(typeof microservices.register === 'function'); 49 | }); 50 | 51 | it('快速使用', function (done) { 52 | microservices.register('hello', function (ctx) { 53 | ctx.result('world'); 54 | }); 55 | microservices.call('hello', {}, (err, ret) => { 56 | assert.equal(err, null); 57 | assert.equal(ret, 'world'); 58 | done(); 59 | }); 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /lib/log_recorder/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const assert = require('assert'); 10 | const utils = require('../utils'); 11 | 12 | class LoggerRecorder { 13 | 14 | /** 15 | * 基于logger接口的日志记录器 16 | * 17 | * @param {Object} logger 18 | * @param {Object} options 19 | * - {String} format 格式 20 | */ 21 | constructor(logger, options) { 22 | 23 | assert.ok(logger, `new LoggerRecorder(logger): 缺少logger参数`); 24 | assert.equal(typeof logger.debug, 'function', `new LoggerRecorder(logger): logger.debug不是一个函数`); 25 | assert.equal(typeof logger.log, 'function', `new LoggerRecorder(logger): logger.log不是一个函数`); 26 | assert.equal(typeof logger.error, 'function', `new LoggerRecorder(logger): logger.error不是一个函数`); 27 | assert.equal(typeof logger.info, 'function', `new LoggerRecorder(logger): logger.info不是一个函数`); 28 | this.logger = logger; 29 | 30 | const opts = Object.assign({ 31 | format: '$type: [$id] $content', 32 | }, options); 33 | 34 | assert.equal(typeof opts.format, 'string', `new LoggerRecorder(logger, { format }): format不是字符串`); 35 | this.format = utils.compileLogFormat(opts.format); 36 | 37 | } 38 | 39 | /** 40 | * 打印日志 41 | * 42 | * @param {Object} ctx 当前的context 43 | * @param {String} type 日志类型 44 | * @param {String} content 内容 45 | */ 46 | write(ctx, type, content) { 47 | const lowerType = type.toLowerCase(); 48 | const method = selectLoggerMethod(this.logger, lowerType); 49 | method.call(this.logger, this.format(ctx, type, content)); 50 | } 51 | 52 | } 53 | 54 | function selectLoggerMethod(logger, type) { 55 | if (type === 'debug') return logger.debug; 56 | if (type === 'log') return logger.log; 57 | if (type === 'error') return logger.error; 58 | return logger.info; 59 | } 60 | 61 | module.exports = LoggerRecorder; 62 | -------------------------------------------------------------------------------- /test/test_service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices test 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const assert = require('assert'); 10 | const Service = require('../lib/service'); 11 | 12 | describe('Service', function () { 13 | 14 | describe('new Service(name, handler)', function () { 15 | 16 | it('name 参数不正确报错', function () { 17 | assert.throws(() => { 18 | new Service(123, function (_) {}); 19 | }, /AssertionError/); 20 | }); 21 | 22 | it('handler 参数不正确报错', function () { 23 | assert.throws(() => { 24 | new Service('test', 123); 25 | }, /AssertionError/); 26 | assert.throws(() => { 27 | new Service('test', function () {}); 28 | }, /AssertionError/); 29 | }); 30 | 31 | it('正确存储了 name 和 handler', function () { 32 | const name = 'test'; 33 | const handler = function (_) {}; 34 | const s = new Service(name, handler); 35 | assert.equal(s.name, name); 36 | assert.equal(s.handler, handler); 37 | }); 38 | 39 | }); 40 | 41 | describe('call()', function () { 42 | 43 | it('必须异步执行', function (done) { 44 | const ctx = { 45 | started: false, 46 | result: ret => { 47 | assert.equal(ret, 123456); 48 | assert.equal(ctx.started, true); 49 | done(); 50 | }, 51 | }; 52 | const s = new Service('test', function (ctx) { 53 | // eslint-disable-next-line 54 | ctx.started = true; 55 | ctx.result(123456); 56 | }); 57 | s.call(ctx); 58 | // service handler 还未执行 59 | assert.equal(ctx.started, false); 60 | }); 61 | 62 | it('执行出错时能捕捉到', function (done) { 63 | const ctx = { 64 | error: err => { 65 | assert.equal(err.message, 'must catch this error'); 66 | done(); 67 | }, 68 | }; 69 | const s = new Service('test', function (_) { 70 | throw new Error('must catch this error'); 71 | }); 72 | s.call(ctx); 73 | }); 74 | 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /lib/manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const assert = require('assert'); 10 | const Service = require('./service'); 11 | const Context = require('./context'); 12 | const utils = require('./utils'); 13 | 14 | // 允许设置的 options 15 | const ALLOWED_OPTIONS = [ 'logRecorder' ]; 16 | 17 | class Manager { 18 | 19 | /** 20 | * Manager 21 | * 22 | * @constructor 23 | * @param {Object} options 24 | * - {Object} logRecorder 25 | */ 26 | constructor(options) { 27 | this._options = Object.assign({}, options || {}); 28 | this._services = new Map(); 29 | this._checkOptions(); 30 | } 31 | 32 | /** 33 | * 检查options是否正确 34 | * 35 | * @param {Object} options 36 | */ 37 | _checkOptions(options) { 38 | const opts = utils.merge(this._options, options || {}); 39 | for (const name in opts) { 40 | assert(ALLOWED_OPTIONS.indexOf(name) !== -1, `${ name } 配置项不存在`); 41 | } 42 | if (opts.logRecorder) { 43 | assert(utils.isValidLogRecorder(opts.logRecorder, `options.logRecorder 必须是一个有效的LogRecorder`)); 44 | } 45 | } 46 | 47 | /** 48 | * 更新配置 49 | * 50 | * @param {String} name 51 | * @param {Mixed} value 52 | * @return {this} 53 | */ 54 | setOption(name, value) { 55 | this._checkOptions({ [name]: value }); 56 | this._options[name] = value; 57 | return this; 58 | } 59 | 60 | /** 61 | * 获取配置 62 | * 63 | * @param {String} name 64 | * @return {Mixed} 65 | */ 66 | getOption(name) { 67 | return this._options[name]; 68 | } 69 | 70 | /** 71 | * 注册服务 72 | * 73 | * @param {String} name 74 | * @param {Function} handler 75 | * @return {this} 76 | */ 77 | register(name, handler) { 78 | this._services.set(name, new Service(name, handler)); 79 | return this; 80 | } 81 | 82 | /** 83 | * 查询服务 84 | * 85 | * @param {String} name 86 | * @return {Object} 87 | */ 88 | getService(name) { 89 | return this._services.get(name) || null; 90 | } 91 | 92 | /** 93 | * 调用服务 94 | * 95 | * @param {String} name 96 | * @param {Object} params 97 | * @param {Function} callback 98 | * @return {Promise} 99 | */ 100 | call(name, params, callback) { 101 | const ctx = this.newContext(); 102 | return ctx.call(name, params, callback); 103 | } 104 | 105 | /** 106 | * 创建新的Context 107 | * 108 | * @param {Object} options 109 | * @return {Object} 110 | */ 111 | newContext(options) { 112 | return new Context(Object.assign({ 113 | manager: this, 114 | logRecorder: this.getOption('logRecorder'), 115 | }, options)); 116 | } 117 | 118 | } 119 | 120 | module.exports = Manager; 121 | -------------------------------------------------------------------------------- /test/test_promise.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices test 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const assert = require('assert'); 10 | const coroutine = require('lei-coroutine'); 11 | const Manager = require('../lib/manager'); 12 | 13 | describe('完全使用 Promise', function () { 14 | 15 | it('正常', function () { 16 | const manager = new Manager(); 17 | 18 | manager.register('test1', coroutine.wrap(function* serviceTest1(ctx) { 19 | yield coroutine.delay(10); 20 | ctx.result({ value: ctx.params.a + ctx.params.b }); 21 | })); 22 | 23 | manager.register('test2', coroutine.wrap(function* serviceTest2(ctx) { 24 | yield coroutine.delay(10); 25 | ctx.result({ value: ctx.params.a * ctx.params.b }); 26 | })); 27 | 28 | return coroutine(function* main() { 29 | const v1 = yield manager.call('test1', { a: 10, b: 22 }); 30 | const v2 = yield manager.call('test2', { a: 11, b: 22 }); 31 | assert.deepEqual(v1, { value: 10 + 22 }); 32 | assert.deepEqual(v2, { value: 11 * 22 }); 33 | }); 34 | 35 | }); 36 | 37 | it('出错情况 - ctx.result() 之后抛出异常不影响结果', function () { 38 | const manager = new Manager(); 39 | 40 | manager.register('test1', coroutine.wrap(function* serviceTest1(ctx) { 41 | yield coroutine.delay(10); 42 | ctx.result({ value: ctx.params.a + ctx.params.b }); 43 | throw new Error('haha test1'); 44 | })); 45 | 46 | manager.register('test2', coroutine.wrap(function* serviceTest2(ctx) { 47 | yield coroutine.delay(10); 48 | ctx.result({ value: ctx.params.a * ctx.params.b }); 49 | throw new Error('haha test2'); 50 | })); 51 | 52 | return coroutine(function* main() { 53 | const v1 = yield manager.call('test1', { a: 10, b: 22 }); 54 | const v2 = yield manager.call('test2', { a: 11, b: 22 }); 55 | assert.deepEqual(v1, { value: 10 + 22 }); 56 | assert.deepEqual(v2, { value: 11 * 22 }); 57 | }); 58 | 59 | }); 60 | 61 | it('出错情况 - ctx.result() 之前抛出异常', function () { 62 | const manager = new Manager(); 63 | 64 | const TRUE = true; 65 | 66 | manager.register('test1', coroutine.wrap(function* serviceTest1(ctx) { 67 | yield coroutine.delay(10); 68 | if (TRUE) throw new Error('haha test1'); 69 | ctx.result({ value: ctx.params.a + ctx.params.b }); 70 | })); 71 | 72 | manager.register('test2', coroutine.wrap(function* serviceTest2(ctx) { 73 | yield coroutine.delay(10); 74 | if (TRUE) throw new Error('haha test2'); 75 | ctx.result({ value: ctx.params.a * ctx.params.b }); 76 | })); 77 | 78 | return coroutine(function* main() { 79 | try { 80 | yield manager.call('test1', { a: 10, b: 22 }); 81 | } catch (err) { 82 | assert.equal(err.message, 'haha test1'); 83 | } 84 | try { 85 | yield manager.call('test2', { a: 11, b: 22 }); 86 | } catch (err) { 87 | assert.equal(err.message, 'haha test2'); 88 | } 89 | }); 90 | 91 | }); 92 | 93 | }); 94 | -------------------------------------------------------------------------------- /test/test_manager.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices test 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const assert = require('assert'); 10 | const Manager = require('../lib/manager'); 11 | const StreamRecorder = require('../lib/log_recorder/stream'); 12 | 13 | describe('Manager', function () { 14 | 15 | describe('new Manager(options)', function () { 16 | 17 | it('不允许设置未知的选项', function () { 18 | assert.throws(() => { 19 | new Manager({ aaaa: 123 }); 20 | }); 21 | assert.throws(() => { 22 | new Manager({ bbbb: 123 }); 23 | }); 24 | }); 25 | 26 | it('logRecorder - 必须是一个LogRecorder', function () { 27 | assert.throws(() => { 28 | new Manager({ logRecorder: 1 }); 29 | }); 30 | new Manager({ 31 | logRecorder: new StreamRecorder({ write() {} }), 32 | }); 33 | }); 34 | 35 | }); 36 | 37 | describe('setOption(name, value)', function () { 38 | 39 | const m = new Manager(); 40 | 41 | it('不允许设置未知的选项', function () { 42 | assert.throws(() => { 43 | m.setOption('cccc', 111); 44 | }); 45 | assert.throws(() => { 46 | m.setOption('dddd', 222); 47 | }); 48 | }); 49 | 50 | it('logRecorder - 必须是一个LogRecorder', function () { 51 | assert.throws(() => { 52 | m.setOption('logRecorder', 'test'); 53 | }); 54 | m.setOption('logRecorder', new StreamRecorder({ write() {} })); 55 | }); 56 | 57 | }); 58 | 59 | describe('getOption(name)', function () { 60 | 61 | const logRecorder = new StreamRecorder({ write(){} }); 62 | const m = new Manager({ logRecorder }); 63 | 64 | it('成功', function () { 65 | assert.equal(m.getOption('logRecorder'), logRecorder); 66 | }); 67 | 68 | it('更新option再获取成功', function () { 69 | const logRecorder2 = new StreamRecorder({ write(){} }); 70 | m.setOption('logRecorder', logRecorder2); 71 | assert.equal(m.getOption('logRecorder'), logRecorder2); 72 | }); 73 | 74 | }); 75 | 76 | describe('newContext(options)', function () { 77 | 78 | const logRecorder = new StreamRecorder({ write(){} }); 79 | const m = new Manager({ logRecorder }); 80 | 81 | it('传递 logRecorder', function () { 82 | const ctx = m.newContext(); 83 | assert.equal(ctx._logRecorder, logRecorder); 84 | }); 85 | 86 | it('传递自定义的 params', function () { 87 | const params = { a: 123, b: 456 }; 88 | const ctx = m.newContext({ params }); 89 | assert.deepEqual(ctx.params, params); 90 | }); 91 | 92 | }); 93 | 94 | describe('register(name, handler) & getService(name)', function () { 95 | 96 | const m = new Manager(); 97 | function handler(_) {} 98 | 99 | it('register(name, handler) - 成功', function () { 100 | m.register('test', handler); 101 | }); 102 | 103 | it('getService(name) - 成功', function () { 104 | const s = m.getService('test'); 105 | assert(s, `无法获取service`); 106 | assert.equal(s.name, 'test'); 107 | assert.equal(s.handler, handler); 108 | }); 109 | 110 | }); 111 | 112 | describe('call(name, params)', function () { 113 | 114 | const m = new Manager(); 115 | m.register('testSuccess', function (ctx) { 116 | setTimeout(() => { 117 | ctx.result(ctx.params.msg); 118 | }, Math.random() * 10); 119 | }); 120 | m.register('testError', function (ctx) { 121 | setTimeout(() => { 122 | ctx.error(new Error(ctx.params.msg)); 123 | }, Math.random() * 10); 124 | }); 125 | 126 | it('callback(null, ret)', function (done) { 127 | m.call('testSuccess', { msg: 'test' }, (err, ret) => { 128 | assert.equal(err, null); 129 | assert.equal(ret, 'test'); 130 | done(); 131 | }); 132 | }); 133 | 134 | it('callback(err)', function (done) { 135 | m.call('testError', { msg: 'test' }, (err, _) => { 136 | assert.notEqual(err, null); 137 | assert.equal(err.message, 'test'); 138 | done(); 139 | }); 140 | }); 141 | 142 | it('Promise.then(ret)', function (done) { 143 | m.call('testSuccess', { msg: 'test' }).then(ret => { 144 | assert.equal(ret, 'test'); 145 | done(); 146 | }).catch(err => { 147 | throw err; 148 | }); 149 | }); 150 | 151 | it('Promise.catch(err)', function (done) { 152 | m.call('testError', { msg: 'test' }).then(_ => { 153 | throw new Error('此处应该报错'); 154 | }).catch(err => { 155 | assert.equal(err.message, 'test'); 156 | done(); 157 | }); 158 | }); 159 | 160 | }); 161 | 162 | }); 163 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const os = require('os'); 10 | const assert = require('assert'); 11 | const Promise = require('bluebird'); 12 | const utils = module.exports = exports = require('lei-utils').extend({}); 13 | const util = require('util'); 14 | const format = util.format; 15 | 16 | exports.format = format; 17 | exports.Promise = Promise; 18 | 19 | /** 20 | * 生成新的requestId 21 | * 22 | * @param {Number} size 长度,默认24,范围 [16...48] 23 | * @return {String} 24 | */ 25 | exports.newRequestId = function (size) { 26 | const num = Number(size); 27 | assert(num >= 16, `utils.newRequestId(size:${ num }): size必须大于或等于16`); 28 | assert(num <= 48, `utils.newRequestId(size:${ num }): size必须小于或等于48`); 29 | return `${ Date.now() }.${ utils.randomString(48) }`.slice(0, num); 30 | }; 31 | 32 | /** 33 | * 根据调用顺序生成新的requestId 34 | * 35 | * @param {String} prefix 36 | * @param {Number} index 37 | * @return {String} 38 | */ 39 | exports.appendRequestId = function (prefix, index) { 40 | return `${ prefix }.${ index }`; 41 | }; 42 | 43 | /** 44 | * 生成一个Promise对象 45 | * 46 | * @param {Function} init 47 | * @return {Promise} 48 | */ 49 | exports.newPromise = function (init) { 50 | return new Promise(init); 51 | }; 52 | 53 | /** 54 | * 使用空格将字符串分隔成多块 55 | * 56 | * @param {String} str 57 | * @param {Number} num 58 | * @return {Array} 59 | */ 60 | exports.whitespaceSeparatedBlocks = function (str, num) { 61 | const blocks = []; 62 | let i = 0; 63 | while (blocks.length < num - 1) { 64 | const j = str.indexOf(' ', i); 65 | if (j === -1) { 66 | blocks.push(str.slice(i).trim()); 67 | i = str.length; 68 | break; 69 | } else { 70 | blocks.push(str.slice(i, j).trim()); 71 | i = j + 1; 72 | } 73 | } 74 | if (i < str.length) { 75 | blocks.push(str.slice(i).trim()); 76 | } 77 | return blocks; 78 | }; 79 | 80 | /** 81 | * 获取requestId的最顶级requestId,没有则返回undefined 82 | * 83 | * @param {String} id 84 | * @return {String} 85 | */ 86 | exports.getParentRequestId = function (id) { 87 | const i = id.lastIndexOf(':'); 88 | if (i === -1) return; 89 | return id.slice(0, i); 90 | }; 91 | 92 | /** 93 | * 生成指定数量的字符 94 | * 95 | * @param {Stirng} char 96 | * @param {Number} num 97 | * @return {String} 98 | */ 99 | exports.takeChars = function (char, num) { 100 | let str = ''; 101 | for (let i = 0; i < num; i++) { 102 | str += char; 103 | } 104 | return str; 105 | }; 106 | 107 | /** 108 | * 检查是否为有效的logRecorder 109 | * 110 | * @param {Object} recorder 111 | * @return {Boolean} 112 | */ 113 | exports.isValidLogRecorder = function (recorder) { 114 | return recorder && typeof recorder.write === 'function'; 115 | }; 116 | 117 | /** 118 | * 编译日志格式模板 119 | * 120 | * @param {String} template 121 | * 可用的变量如下: 122 | * - $id requestId 123 | * - $service 服务名称 124 | * - $uptime 当前context已启动的时间 125 | * - $date 日期,如 2016/08/02 126 | * - $time 时间,如 14:01:37 127 | * - $datetime 日期时间,如 2016/08/02 14:01:37 128 | * - $isotime ISO格式的时间字符串,如 2016-08-12T13:20:27.599Z 129 | * - $timestamp 毫秒级的Unix时间戳,如1470980387892 130 | * - $timestamps 秒级的Unix时间戳,如1470980387 131 | * - $type 日志类型,如 debug, log, error, call, result 132 | * - $content 内容字符串 133 | * - $pid 当前进程PID 134 | * - $hostname 当前主机名 135 | * @return {Function} 136 | */ 137 | exports.compileLogFormat = function (template) { 138 | const vars = {}; 139 | const str = template.replace(/\$([a-z]+)/g, (a, b) => { 140 | vars[b] = true; 141 | return '${' + b + '}'; 142 | }).replace(/`/g, '\\`'); 143 | const lines = Object.keys(vars).map(n => { 144 | if (n === 'id') return `const id = ctx.requestId;`; 145 | if (n === 'service') return `const service = ctx._service && ctx._service.name || null;`; 146 | if (n === 'uptime') return `const uptime = Date.now() - ctx.startTime.getTime();`; 147 | if (n === 'date') return `const date = utils.date('Y/m/d');`; 148 | if (n === 'time') return `const time = utils.date('H:i:s');`; 149 | if (n === 'datetime') return `const datetime = utils.date('Y/m/d H:i:s');`; 150 | if (n === 'isotime') return `const isotime = new Date().toISOString();`; 151 | if (n === 'timestamp') return `const timestamp = Date.now();`; 152 | if (n === 'timestamps') return `const timestamps = parseInt(Date.now() / 1000, 10);`; 153 | if (n === 'type') return ''; 154 | if (n === 'content') return ''; 155 | if (n === 'pid') return `const pid = ${ process.pid };`; 156 | if (n === 'hostname') return `const hostname = '${ os.hostname().replace(/'/g, '\'') }'`; 157 | throw new Error(`不支持的日志模板变量:${ n }`); 158 | }).filter(a => a); 159 | return eval(` 160 | (function format(ctx, type, content) { 161 | 162 | ${ lines.join('\n') } 163 | 164 | return \`${ str }\`; 165 | }) 166 | `.trim()); 167 | }; 168 | -------------------------------------------------------------------------------- /example/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const microservices = require('../'); 6 | const mkdirp = require('mkdirp'); 7 | const clc = require('cli-color'); 8 | const globalManager = microservices.globalManager; 9 | const register = microservices.register; 10 | const utils = microservices.utils; 11 | 12 | // 创建文件流 13 | const logFileName = path.resolve(__dirname, `logs/${ utils.date('Ymd') }/${ utils.date('Ymd-Hi') }.log`); 14 | mkdirp.sync(path.dirname(logFileName)); 15 | const stream = fs.createWriteStream(logFileName, { 16 | flags: 'a', 17 | }); 18 | 19 | // 创建日志记录器 20 | const logger = { 21 | write(str) { 22 | process.stdout.write(str + '\n'); 23 | }, 24 | log(str) { 25 | this.write('LOG:\t' + clc.bold(str)); 26 | }, 27 | info(str) { 28 | this.write('INFO:\t' + clc.blue(str)); 29 | }, 30 | debug(str) { 31 | this.write('DEBUG:\t' + clc.green(str)); 32 | }, 33 | error(str) { 34 | this.write('ERROR:\t' + clc.red(str)); 35 | }, 36 | }; 37 | 38 | // 日志格式 39 | // eslint-disable-next-line 40 | const jsonFormat = '{"time":$isotime,"id":"$id","type":"$type","service":"$service","uptime":$uptime,"content":$content}'; 41 | // eslint-disable-next-line 42 | const textFormat = '$isotime\t$type\t$id\t$service\t$uptime\t$content'; 43 | 44 | // ----------------------------------------------------------------------------- 45 | 46 | // 记录到文件 47 | globalManager.setOption('logRecorder', new microservices.StreamRecorder(stream, { 48 | newLine: '\n', 49 | format: jsonFormat, 50 | })); 51 | 52 | // 直接打印到控制台 53 | globalManager.setOption('logRecorder', new microservices.LoggerRecorder(logger, { 54 | format: textFormat, 55 | })); 56 | 57 | // 打印到控制台,且是JSON格式 58 | // globalManager.setOption('logRecorder', new microservices.LoggerRecorder(logger, { 59 | // format: jsonFormat, 60 | // })); 61 | 62 | // ----------------------------------------------------------------------------- 63 | 64 | function asyncOperate(fn) { 65 | setTimeout(fn, Math.random() * 500); 66 | } 67 | 68 | function randomBoolean() { 69 | return Math.random() >= 0.5; 70 | } 71 | 72 | // ----------------------------------------------------------------------------- 73 | 74 | // 注册新用户 75 | register('api.superid.signup', function (ctx) { 76 | ctx.debug('start signup new user, phone=%s', ctx.params.phone); 77 | ctx.call('user.getOrCreate', { phone: ctx.params.phone }, (err, user) => { 78 | if (err) return ctx.error(err); 79 | 80 | ctx.debug('user id=%s', user.id); 81 | ctx.call('face.compare', { user, face: ctx.params.face }, (err, compareResult) => { 82 | if (err) return ctx.error(err); 83 | 84 | ctx.debug('cool, the last step, generate new access token'); 85 | ctx.call('user.generateNewAccessToken', { user }, (err, token) => { 86 | if (err) return ctx.error(err); 87 | 88 | ctx.result({ 89 | phone: user.phone, 90 | success: true, 91 | score: compareResult.score, 92 | token, 93 | }); 94 | }); 95 | }); 96 | }); 97 | }); 98 | 99 | register('user.get', function (ctx) { 100 | asyncOperate(() => { 101 | if (randomBoolean()) { 102 | ctx.result({ 103 | id: Date.now(), 104 | phone: ctx.params.phone, 105 | name: '老雷', 106 | }); 107 | } else { 108 | ctx.debug('oh no, user does not exists'); 109 | ctx.result(null); 110 | } 111 | }); 112 | }); 113 | register('user.create', function (ctx) { 114 | asyncOperate(() => { 115 | ctx.result({ 116 | id: Date.now(), 117 | phone: ctx.params.phone, 118 | name: '老雷', 119 | }); 120 | }); 121 | }); 122 | register('user.getOrCreate', function (ctx) { 123 | ctx.call('user.get', ctx.params, (err, user) => { 124 | if (err) return ctx.error(err); 125 | if (user) return ctx.result(user); 126 | ctx.debug('ok, let me create a new user'); 127 | ctx.transfer('user.create', ctx.params); 128 | }); 129 | }); 130 | register('user.generateNewAccessToken', function (ctx) { 131 | asyncOperate(() => { 132 | ctx.result(utils.randomString(20)); 133 | }); 134 | }); 135 | 136 | register('face.compare', function (ctx) { 137 | ctx.debug('upload image firstly, it may take a minute'); 138 | ctx.call('face.upload', { face: ctx.params.face }, (err, face) => { 139 | if (err) console.error(err); 140 | if (randomBoolean()) { 141 | ctx.result({ 142 | uuid: face.uuid, 143 | score: Math.random() * 100, 144 | }); 145 | } else { 146 | ctx.debug('compare fail, maybe set a higher minScore to avoid this problem'); 147 | ctx.error('compare fail'); 148 | } 149 | }); 150 | }); 151 | register('face.upload', function (ctx) { 152 | const uuid = utils.randomString(20); 153 | ctx.log('upload image, uuid=%s', uuid); 154 | asyncOperate(() => { 155 | ctx.result({ uuid }); 156 | }); 157 | }); 158 | 159 | // ----------------------------------------------------------------------------- 160 | 161 | function run() { 162 | const ctx = globalManager.newContext(); 163 | ctx.call('api.superid.signup', { phone: 123456, face: utils.randomString(20) + '.jpg' }) 164 | .then(ret => console.log('ok', ret)) 165 | .catch(err => console.log('fail', err)); 166 | ctx.call('api.superid.signup', { phone: 123456, face: utils.randomString(20) + '.jpg' }) 167 | .then(ret => console.log('ok', ret)) 168 | .catch(err => console.log('fail', err)); 169 | } 170 | setInterval(run, 10000); 171 | run(); 172 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const assert = require('assert'); 10 | const utils = require('./utils'); 11 | const error = require('./error'); 12 | const Service = require('./service'); 13 | const ServiceNotFoundError = error.ServiceNotFoundError; 14 | 15 | function formatErrorObject(err) { 16 | if (err instanceof Error) { 17 | const ret = {}; 18 | for (const i in err) { 19 | ret[i] = err[i]; 20 | } 21 | ret.message = err.message; 22 | return ret; 23 | } 24 | return err; 25 | } 26 | 27 | class Context { 28 | 29 | /** 30 | * Context 31 | * 32 | * @constructor 33 | * @param {Object} options 34 | * - {Object} manager 35 | * - {Object} service 36 | * - {String} requestId 37 | * - {Object} params 38 | * - {Function} callback 39 | * - {Object} logRecorder 40 | */ 41 | constructor(options) { 42 | 43 | const opts = Object.assign({}, options || {}); 44 | 45 | // 严格要求输入的params必须是一个对象 46 | if (opts.params) { 47 | const paramsType = typeof opts.params; 48 | assert(paramsType === 'object', `new Context({params}): params只能为一个对象,不能为${ paramsType }类型`); 49 | } 50 | 51 | // manager对象 52 | this._manager = opts.manager || null; 53 | // service对象 54 | this._service = opts.service || null; 55 | // 拼接requestId 56 | this.requestId = opts.requestId || utils.newRequestId(Context.REQUEST_ID_MIN_SIZE); 57 | // 复制并冻结参数对象 58 | this.params = Object.freeze(Object.assign({}, opts.params || {})); 59 | // 回调函数 60 | this._callback = opts.callback || null; 61 | // 打印日志函数 62 | this._logRecorder = opts.logRecorder || null; 63 | 64 | // 检查manager参数 65 | assert(!!this._manager, `new Context({ manager }): manager不能为空`); 66 | assert(typeof this._manager.getService === 'function', `new Context({ manager }): manager.getService()未定义`); 67 | assert(typeof this._manager.newContext === 'function', `new Context({ manager }): manager.newContext()未定义`); 68 | 69 | // 检查其他参数 70 | if (this._callback) assert(typeof this._callback === 'function', `new Context({ callback }): callback必须为一个函数`); 71 | if (this._logRecorder) assert(utils.isValidLogRecorder(this._logRecorder), `new Context({ logRecorder }): logRecorder必须是一个有效的LogRecorder`); 72 | if (this._service) assert(this._service instanceof Service, `new Context(service): service必须为Service的实例`); 73 | 74 | // 执行结束相关的信息 75 | this.startTime = new Date(); 76 | this.stopTime = null; 77 | this.spent = 0; 78 | this._isCallbacked = false; 79 | 80 | this._counter = 0; 81 | 82 | this.writeJSON('call', this.params); 83 | } 84 | 85 | /** 86 | * 返回结果 87 | * 88 | * @param {Object} ret 89 | */ 90 | result(ret) { 91 | this.callback(null, ret); 92 | } 93 | 94 | /** 95 | * 返回错误 96 | * 97 | * @param {Error} err 98 | */ 99 | error(err) { 100 | this.callback(err); 101 | } 102 | 103 | /** 104 | * 执行回调 105 | * 106 | * @param {Error} err 107 | * @param {Object} ret 108 | */ 109 | callback(err, ret) { 110 | setImmediate(() => { 111 | 112 | // 不允许重复执行回调 113 | if (this._isCallbacked) { 114 | return this.debug('context.callback(): callback many times, ignore'); 115 | } 116 | 117 | this.stopTime = new Date(); 118 | this.spent = this.stopTime - this.startTime; 119 | 120 | if (!this._callback) { 121 | return this.debug('context.callback(): has no callback handler'); 122 | } 123 | 124 | // 如果出错,自动加上 requestId 信息 125 | if (err) { 126 | if (!err.$requestId) { 127 | // eslint-disable-next-line 128 | err.$requestId = this.requestId; 129 | } 130 | } 131 | 132 | this._isCallbacked = true; 133 | if (err) { 134 | this.writeJSON('error', formatErrorObject(err)); 135 | } else { 136 | this.writeJSON('result', ret); 137 | } 138 | this._callback(err, ret); 139 | 140 | }); 141 | } 142 | 143 | /** 144 | * 打印日志 145 | * 146 | * @param {String} type 日志类型 147 | * @param {String} text 内容 148 | */ 149 | write(type, text) { 150 | if (this._logRecorder) { 151 | this._logRecorder.write(this, type, text); 152 | } 153 | } 154 | 155 | /** 156 | * 打印日志(JSON对象) 157 | * 158 | * @param {String} type 日志类型 159 | * @param {String} data 内容 160 | */ 161 | writeJSON(type, data) { 162 | this.write(type, utils.jsonStringify(data)); 163 | } 164 | 165 | /** 166 | * 打印调试信息 167 | */ 168 | debug() { 169 | this.writeJSON('debug', utils.format.apply(null, arguments)); 170 | } 171 | 172 | /** 173 | * 打印日志信息 174 | */ 175 | log() { 176 | this.writeJSON('log', utils.format.apply(null, arguments)); 177 | } 178 | 179 | /** 180 | * 调用服务 181 | * 182 | * @param {String} name 183 | * @param {Object} params 184 | * @param {Function} callback 185 | * @return {Promise} 186 | */ 187 | call(name, params, callback) { 188 | if (typeof callback === 'function') { 189 | this._call(name, params, callback); 190 | } else { 191 | return utils.newPromise((resolve, reject) => { 192 | this._call(name, params, (err, ret) => err ? reject(err) : resolve(ret)); 193 | }); 194 | } 195 | } 196 | 197 | _call(name, params, callback) { 198 | // 查询服务 199 | const service = this._manager.getService(name); 200 | if (!service) return callback(new ServiceNotFoundError(`服务"${ name }"未注册`)); 201 | // 生成requestId 202 | this._counter += 1; 203 | const requestId = utils.appendRequestId(this.requestId, this._counter); 204 | // 生成新的Context 205 | const ctx = this._manager.newContext({ service, requestId, params, callback }); 206 | // 调用服务 207 | service.call(ctx); 208 | } 209 | 210 | /** 211 | * 调用服务,并将调用结果作为当前服务的结果返回 212 | * 213 | * @param {String} name 214 | * @param {Function} params 215 | */ 216 | transfer(name, params) { 217 | this.call(name, params, (err, ret) => this.callback(err, ret)); 218 | } 219 | 220 | /** 221 | * 顺序调用多个服务 222 | * 223 | * @param {Array} list 每个元素使用context.prepareCall(name, params)创建 224 | */ 225 | transferSeries(list) { 226 | this.series(list, (err, ret) => this.callback(err, ret)); 227 | } 228 | 229 | /** 230 | * 顺序调用多个服务 231 | * 232 | * @param {Array} list 每个元素使用context.prepareCall(name, params)创建 233 | * @param {Function} callback 234 | * @return {Promise} 235 | */ 236 | series(list, callback) { 237 | if (typeof callback === 'function') { 238 | this._series(list, callback); 239 | } else { 240 | return utils.newPromise((resolve, reject) => { 241 | this._series(list, (err, ret) => err ? reject(err) : resolve(ret)); 242 | }); 243 | } 244 | } 245 | 246 | _series(list, callback) { 247 | const rest = list.slice(); 248 | const next = (err, params) => { 249 | if (err) return callback(err); 250 | if (rest.length < 1) return callback(null, params); 251 | const item = rest.shift(); 252 | item(params, next); 253 | }; 254 | next(); 255 | } 256 | 257 | /** 258 | * 构建预调用服务函数 259 | * 260 | * @param {String} name 261 | * @param {Object} specifyParams 如果指定了此参数,会覆盖在调用时指定的参数 262 | * @return {Function} 263 | */ 264 | prepareCall(name, specifyParams) { 265 | return (params, callback) => { 266 | const newParams = arguments.length > 1 ? specifyParams : params; 267 | this.call(name, newParams, callback); 268 | }; 269 | } 270 | 271 | } 272 | 273 | Context.REQUEST_ID_MIN_SIZE = 24; 274 | 275 | module.exports = Context; 276 | -------------------------------------------------------------------------------- /test/test_context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * super-microservices test 5 | * 6 | * @author Zongmin Lei 7 | */ 8 | 9 | const assert = require('assert'); 10 | const Context = require('../lib/context'); 11 | const Manager = require('../lib/manager'); 12 | const StreamRecorder = require('../lib/log_recorder/stream'); 13 | 14 | describe('Context', function () { 15 | 16 | describe('new Context(options)', function () { 17 | 18 | const manager = new Manager(); 19 | 20 | it('manager - 必须指定', function () { 21 | assert.throws(() => { 22 | new Context(); 23 | }, /AssertionError/); 24 | new Context({ manager }); 25 | }); 26 | 27 | it('requestId - 可自定义', function () { 28 | const requestId = '123456'; 29 | assert.equal(new Context({ manager, requestId }).requestId, requestId); 30 | }); 31 | 32 | it('params - 只能为对象', function () { 33 | assert.throws(() => { 34 | new Context({ manager, params: 123 }); 35 | }, /AssertionError/); 36 | assert.throws(() => { 37 | new Context({ manager, params: 'text' }); 38 | }, /AssertionError/); 39 | const ctx = new Context({ manager, params: { a: 123 }}); 40 | assert.deepEqual(ctx.params, { a: 123 }); 41 | }); 42 | 43 | it('params - 被冻结', function () { 44 | const ctx = new Context({ manager, params: { a: 123 }}); 45 | assert.throws(() => { 46 | ctx.params.a = 456; 47 | }); 48 | assert.throws(() => { 49 | ctx.params.b = 789; 50 | }); 51 | assert.deepEqual(ctx.params, { a: 123 }); 52 | }); 53 | 54 | it('params - 解除原对象的引用', function () { 55 | const params = { a: 123 }; 56 | const ctx = new Context({ manager, params }); 57 | params.a = 456; 58 | params.b = 789; 59 | assert.notEqual(params, ctx.params); 60 | assert.deepEqual(params, { a: 456, b: 789 }); 61 | assert.deepEqual(ctx.params, { a: 123 }); 62 | }); 63 | 64 | it('callback - 如果指定了,必须为一个函数', function () { 65 | assert.throws(() => { 66 | new Context({ manager, callback: 123 }); 67 | }); 68 | new Context({ manager, callback() {} }); 69 | }); 70 | 71 | it('logRecorder - 如果指定了,必须一个有效的LogRecorder', function () { 72 | assert.throws(() => { 73 | new Context({ manager, logRecorder: 123 }); 74 | }); 75 | new Context({ manager, logRecorder: new StreamRecorder({ write() {} }) }); 76 | }); 77 | 78 | }); 79 | 80 | describe('call(name, params)', function () { 81 | 82 | const manager = new Manager(); 83 | manager.register('testSuccess', function (ctx) { 84 | setTimeout(() => { 85 | ctx.result(ctx.params.msg); 86 | }, Math.random() * 10); 87 | }); 88 | manager.register('testError', function (ctx) { 89 | setTimeout(() => { 90 | ctx.error(new Error(ctx.params.msg)); 91 | }, Math.random() * 10); 92 | }); 93 | 94 | it('callback(null, ret)', function (done) { 95 | new Context({ manager }).call('testSuccess', { msg: 'test' }, (err, ret) => { 96 | assert.equal(err, null); 97 | assert.equal(ret, 'test'); 98 | done(); 99 | }); 100 | }); 101 | 102 | it('callback(err)', function (done) { 103 | new Context({ manager }).call('testError', { msg: 'test' }, (err, _) => { 104 | assert.notEqual(err, null); 105 | assert.equal(err.message, 'test'); 106 | done(); 107 | }); 108 | }); 109 | 110 | it('Promise.then(ret)', function (done) { 111 | new Context({ manager }).call('testSuccess', { msg: 'test' }).then(ret => { 112 | assert.equal(ret, 'test'); 113 | done(); 114 | }).catch(err => { 115 | done(err); 116 | }); 117 | }); 118 | 119 | it('Promise.catch(err)', function (done) { 120 | new Context({ manager }).call('testError', { msg: 'test' }).then(_ => { 121 | done(new Error('不应该捕捉到正常结果')); 122 | }).catch(err => { 123 | assert.equal(err.message, 'test'); 124 | done(); 125 | }); 126 | }); 127 | 128 | }); 129 | 130 | describe('transfer(name, params)', function () { 131 | 132 | const manager = new Manager(); 133 | manager.register('bridge', function (ctx) { 134 | ctx.transfer(ctx.params.name, { msg: ctx.params.msg }); 135 | }); 136 | manager.register('testSuccess', function (ctx) { 137 | setTimeout(() => { 138 | ctx.result(ctx.params.msg); 139 | }, Math.random() * 10); 140 | }); 141 | manager.register('testError', function (ctx) { 142 | setTimeout(() => { 143 | ctx.error(new Error(ctx.params.msg)); 144 | }, Math.random() * 10); 145 | }); 146 | 147 | it('callback(null, ret)', function (done) { 148 | new Context({ manager }).call('bridge', { name: 'testSuccess', msg: 'test' }, (err, ret) => { 149 | assert.equal(err, null); 150 | assert.equal(ret, 'test'); 151 | done(); 152 | }); 153 | }); 154 | 155 | it('callback(err)', function (done) { 156 | new Context({ manager }).call('bridge', { name: 'testError', msg: 'test' }, (err, _) => { 157 | assert.notEqual(err, null); 158 | assert.equal(err.message, 'test'); 159 | done(); 160 | }); 161 | }); 162 | 163 | it('Promise.then(ret)', function (done) { 164 | new Context({ manager }).call('bridge', { name: 'testSuccess', msg: 'test' }).then(ret => { 165 | assert.equal(ret, 'test'); 166 | done(); 167 | }).catch(err => { 168 | done(err); 169 | }); 170 | }); 171 | 172 | it('Promise.catch(err)', function (done) { 173 | new Context({ manager }).call('bridge', { name: 'testError', msg: 'test' }).then(_ => { 174 | throw new Error('此处应该报错'); 175 | }).catch(err => { 176 | assert.equal(err.message, 'test'); 177 | done(); 178 | }); 179 | }); 180 | 181 | }); 182 | 183 | describe('prepareCall(name, params?)', function () { 184 | 185 | const manager = new Manager(); 186 | manager.register('testSuccess', function (ctx) { 187 | setTimeout(() => { 188 | ctx.result(ctx.params.msg); 189 | }, Math.random() * 10); 190 | }); 191 | 192 | it('不绑定参数', function (done) { 193 | const call = new Context({ manager }).prepareCall('testSuccess'); 194 | call({ msg: 'ttt' }, (err, ret) => { 195 | assert.equal(err, null); 196 | assert.equal(ret, 'ttt'); 197 | done(); 198 | }); 199 | }); 200 | 201 | it('绑定参数不能覆盖', function (done) { 202 | const call = new Context({ manager }).prepareCall('testSuccess', { msg: 'oooo' }); 203 | call({ msg: 'ttt' }, (err, ret) => { 204 | assert.equal(err, null); 205 | assert.equal(ret, 'oooo'); 206 | done(); 207 | }); 208 | }); 209 | 210 | }); 211 | 212 | describe('series(list, callback)', function () { 213 | 214 | const manager = new Manager(); 215 | manager.register('test1', function (ctx) { 216 | setTimeout(() => { 217 | ctx.result({ value: ctx.params.value + 1 }); 218 | }, Math.random() * 10); 219 | }); 220 | manager.register('test2', function (ctx) { 221 | setTimeout(() => { 222 | ctx.result({ value: ctx.params.value + 10 }); 223 | }, Math.random() * 10); 224 | }); 225 | manager.register('transfer', function (ctx) { 226 | ctx.transferSeries([ 227 | ctx.prepareCall('test1', ctx.params), 228 | ctx.prepareCall('test2'), 229 | ctx.prepareCall('test1'), 230 | ]); 231 | }); 232 | 233 | it('callback', function (done) { 234 | const ctx = new Context({ manager }); 235 | ctx.series([ 236 | ctx.prepareCall('test1', { value: 123 }), 237 | ctx.prepareCall('test2'), 238 | ctx.prepareCall('test1'), 239 | ], (err, ret) => { 240 | assert.equal(err, null); 241 | assert.deepEqual(ret, { value: 135 }); 242 | done(); 243 | }); 244 | }); 245 | 246 | it('promise', function (done) { 247 | const ctx = new Context({ manager }); 248 | ctx.series([ 249 | ctx.prepareCall('test1', { value: 123 }), 250 | ctx.prepareCall('test2'), 251 | ctx.prepareCall('test1'), 252 | ]).then(ret => { 253 | assert.deepEqual(ret, { value: 135 }); 254 | done(); 255 | }).catch(err => { 256 | done(err); 257 | }); 258 | }); 259 | 260 | it('transfer', function (done) { 261 | new Context({ manager }).call('transfer', { value: 110 }, (err, ret) => { 262 | assert.equal(err, null); 263 | assert.deepEqual(ret, { value: 122 }); 264 | done(); 265 | }); 266 | }); 267 | 268 | }); 269 | 270 | }); 271 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM version][npm-image]][npm-url] 2 | [![build status][travis-image]][travis-url] 3 | [![Test coverage][coveralls-image]][coveralls-url] 4 | [![David deps][david-image]][david-url] 5 | [![node version][node-image]][node-url] 6 | [![npm download][download-image]][download-url] 7 | [![npm license][license-image]][download-url] 8 | 9 | [npm-image]: https://img.shields.io/npm/v/nanoservices.svg?style=flat-square 10 | [npm-url]: https://npmjs.org/package/nanoservices 11 | [travis-image]: https://img.shields.io/travis/SuperID/nanoservices.svg?style=flat-square 12 | [travis-url]: https://travis-ci.org/SuperID/nanoservices 13 | [coveralls-image]: https://img.shields.io/coveralls/SuperID/nanoservices.svg?style=flat-square 14 | [coveralls-url]: https://coveralls.io/r/SuperID/nanoservices?branch=master 15 | [david-image]: https://img.shields.io/david/SuperID/nanoservices.svg?style=flat-square 16 | [david-url]: https://david-dm.org/SuperID/nanoservices 17 | [node-image]: https://img.shields.io/badge/node.js-%3E=_4.0-green.svg?style=flat-square 18 | [node-url]: http://nodejs.org/download/ 19 | [download-image]: https://img.shields.io/npm/dm/nanoservices.svg?style=flat-square 20 | [download-url]: https://npmjs.org/package/nanoservices 21 | [license-image]: https://img.shields.io/npm/l/nanoservices.svg 22 | 23 | # nanoservices 24 | 比微服务更小的纳米服务框架 25 | 26 | ## 安装 27 | 28 | ```bash 29 | $ npm install nanoservices --save 30 | ``` 31 | 32 | **要求 Node.js v4.0.0 或更高版本** 33 | 34 | 35 | ## 设计目标 36 | 37 | + [x] 将项目代码服务化,每一个「纳米服务」完成一个小功能 38 | + [x] 通过`requestId`来跟踪记录完整的调用链 39 | + [x] 自动记录日志,结合相应的调试信息方便开发调试 40 | + [ ] 考虑接驳`clouds`系统,可以使得不同主机/进程间的调用也适用 41 | 42 | ![](screenshots.png) 43 | 44 | 45 | ## 使用方法 46 | 47 | ### callback 接口 48 | 49 | ```javascript 50 | 'use strict'; 51 | 52 | const { Manager } = require('nanoservices'); 53 | 54 | // 创建管理器 55 | const services = new Manager(); 56 | 57 | 58 | // 注册服务 59 | services.register('add', function (ctx) { 60 | // ctx.params 为输入的参数,该对象已冻结不能对其更改 61 | // ctx.error(err) 返回出错信息 62 | // ctx.result(ret) 返回调用结果 63 | // ctx.debug(msg) 输出调试信息 64 | 65 | if (isNaN(ctx.params.a)) return ctx.error('参数a不是一个数值'); 66 | if (isNaN(ctx.params.b)) return ctx.error('参数a不是一个数值'); 67 | 68 | ctx.debug('add: a=%s, b=%s', ctx.params.a, ctx.params.b); 69 | 70 | const a = Number(ctx.params.a); 71 | const b = Number(ctx.params.b); 72 | 73 | // 返回结果 74 | ctx.result(a + b); 75 | }); 76 | 77 | 78 | // 调用服务 79 | services.call('add', { a: 123, b: 456 }, (err, ret) => { 80 | if (err) { 81 | console.error(err); 82 | } else { 83 | console.log('result=%s', ret); 84 | } 85 | }); 86 | 87 | // 支持 Promise 88 | services.call('add', { a: 123, b: 456 }) 89 | .then(ret => { 90 | console.log('result=%s', ret); 91 | }) 92 | .catch(err => { 93 | console.error(err); 94 | }); 95 | ``` 96 | 97 | ### promise 接口 98 | 99 | ```javascript 100 | 'use strict'; 101 | 102 | const { Manager } = require('nanoservices'); 103 | 104 | // 创建管理器 105 | const services = new Manager(); 106 | 107 | 108 | // 注册服务 109 | services.register('add', async function (ctx) { 110 | 111 | if (isNaN(ctx.params.a)) return ctx.error('参数a不是一个数值'); 112 | if (isNaN(ctx.params.b)) return ctx.error('参数a不是一个数值'); 113 | 114 | ctx.debug('add: a=%s, b=%s', ctx.params.a, ctx.params.b); 115 | 116 | const a = Number(ctx.params.a); 117 | const b = Number(ctx.params.b); 118 | 119 | // 使用 await 120 | await services.call('service1', { a }); 121 | await services.call('service1', { b }); 122 | 123 | // 返回结果 124 | ctx.result(a + b); 125 | }); 126 | 127 | // 调用服务 128 | services.call('add', { a: 123, b: 456 }) 129 | .then(ret => { 130 | console.log('result=%s', ret); 131 | }) 132 | .catch(err => { 133 | console.error(err); 134 | }); 135 | ``` 136 | 137 | 138 | ## Context对象 139 | 140 | 服务的处理函数只接收一个参数,该参数为一个`Context`对象,通过该对象完成读取参数、返回结果等所有操作。 141 | 142 | `Context`对象结构如下: 143 | 144 | ```typescript 145 | interface Context { 146 | 147 | // 请求ID 148 | requestId: string; 149 | 150 | // 调用开始时间 151 | startTime: Date; 152 | 153 | // 执行结束时间 154 | stopTime: Date; 155 | 156 | // 耗时(毫秒) 157 | spent: number; 158 | 159 | // 参数对象,该对象已被冻结,不能在对象上做修改 160 | params: Object; 161 | 162 | // 返回执行结果 163 | result(ret: any); 164 | 165 | // 返回执行出错 166 | error(err: any); 167 | 168 | // 打印调试信息,支持 debug('msg=%s', msg) 这样的格式 169 | debug(msg: any); 170 | 171 | // 调用其他服务 172 | // 如果没有传递 callback 参数则返回 Promise 173 | call(name: string, params: Object, callback: (err, ret) => void): Promise; 174 | 175 | // 顺序调用一系列的服务,上一个调用的结果作为下一个调用的参数,如果中途出错则直接返回 176 | // 如果没有传递 callback 参数则返回 Promise 177 | series(calls: [CallService], callback: (err, ret) => void): Promise; 178 | 179 | // 调用服务器,该调用的结果作为当前服务的执行结果返回 180 | transfer(name: string, params: Object); 181 | 182 | // 顺序调用一系列的服务,上一个调用的结果作为下一个调用的参数,如果中途出错则直接返回 183 | // 最后一个调用的结果将作为当前服务的执行结果返回 184 | transferSeries(calls: [CallService], callback: (err, ret) => void); 185 | 186 | // 返回一个 CallService 对象,与 series() 结合使用 187 | // params 表示绑定的参数,如果补指定,则使用上一个调用的结果 188 | prepareCall(name: string, params?: Object); 189 | 190 | } 191 | ``` 192 | 193 | 194 | ## 调用链 195 | 196 | 各个服务之间的调用会通过传递`requestId`来记录调用来源以及整个调用链结构(请求参数、返回结果等), 197 | 还可以通过`debug()`方法来打印调试信息,这些信息会根据需要记录到日志文件中, 198 | 只要通过`requestId`即可查询到完整的调用信息。 199 | 200 | ### 在服务外部调用多个服务 201 | 202 | 默认情况下使用`services.call()`会自动生成一个`requestId`并调用服务,但调用方无法获得这个`requestId`, 203 | 我们可以通过`services.newContext()`来获得一个新的`Context`对象: 204 | 205 | ```javascript 206 | // 创建Context 207 | const ctx = services.newContext(); 208 | // 如果要自定义requestId,可以这样: 209 | // const ctx = services.newContext(requestId); 210 | 211 | // 调用服务 212 | ctx.call('add', { a: 1, b: 2 }, (err, ret) => { 213 | if (err) return console.error(err); 214 | console.log(ret); 215 | 216 | // 调用第二个服务 217 | ctx.call('devide', { a: 123, b: 456 }, (err, ret) => { 218 | if (err) return console.error(err); 219 | console.log(ret); 220 | 221 | // ... 222 | }); 223 | }); 224 | ``` 225 | 226 | ### 顺序调用多个服务 227 | 228 | 有时候某个服务实际上是通过顺序调用一系列服务来完成操作的,可以使用`ctx.series()`方法: 229 | 230 | ```javascript 231 | ctx.series([ 232 | 233 | // 第一个服务必须手动绑定调用参数,因为它没有上一个服务调用结果可用 234 | ctx.prepareCall('add', { a: 123, b: 456 }), 235 | 236 | ctx.prepareCall('divide'), 237 | ctx.prepareCall('times'), 238 | 239 | ], (err, ret) => { 240 | if (err) { 241 | ctx.error(err); 242 | } else { 243 | ctx.result(ret); 244 | } 245 | }); 246 | ``` 247 | 248 | 249 | ## 日志 250 | 251 | 默认情况下,服务调用产生以及服务执行期间所产生的日志调试信息是不会被记录的。可以在初始化`Manager`时可以传入一个`logRecorder`参数, 252 | 以便将这些日志信息记录到指定的位置。目前支持`stream`和`logger`两种方式。 253 | 254 | 1、`stream`方式如下: 255 | 256 | ```javascript 257 | const { Manager, StreamRecorder } = require('nanoservices'); 258 | 259 | // 将日志记录到标准输出接口 260 | const stream = process.stdout; 261 | 262 | // 创建LogRecorder 263 | const logRecorder: new StreamRecorder(stream, { 264 | newLine: '\n', 265 | format: '$date $time $type $id $content', 266 | }); 267 | 268 | // 创建Manager 269 | const services = new Manager({ logRecorder }); 270 | ``` 271 | 272 | 在创建`StreamRecorder`时,第一个参数`stream`为一个标准的`Writable Stream`,可以通过`fs.writeWriteStream()`或`TCP`网络的可写流; 273 | 第二个参数为一些选项,比如: 274 | 275 | + `newLine`表示换行符,即每条日志都会自动在末尾加上这个换行符,如果不指定则表示不加换行符 276 | + `format`表示日志格式,其中有以下变量可选: 277 | + `$id` - 当前`requestId` 278 | + `$service` - 当前服务名称,如果没有则为`null` 279 | + `$uptime` - 当前`context`已启动的时间(毫秒) 280 | + `$date` - 日期,如`2016/08/02` 281 | + `$time` - 时间,如`14:01:37` 282 | + `$datetime` - 日期时间,如`2016/08/02 14:01:37` 283 | + `isotime` - ISO格式的时间字符串,如`2016-08-12T13:20:27.599Z` 284 | + `$timestamp` - 毫秒级的Unix时间戳,如`1470980387892` 285 | + `$timestamps` - 秒级的Unix时间戳,如`1470980387` 286 | + `$type` - 日志类型,目前有以下几个:`debug, log, error, call, result` 287 | + `$content` - 内容字符串 288 | + `$pid` - 当前进程PID 289 | + `$hostname` - 当前主机名 290 | 291 | 2、`logger`方式如下: 292 | 293 | ```javascript 294 | const { Manager, LoggerRecorder } = require('nanoservices'); 295 | 296 | // 一个日志记录器 297 | const logger = console; 298 | // 由于console没有debug方法,需要模拟一个 299 | logger.debug = console.log; 300 | 301 | // 创建LogRecorder 302 | const logRecorder: new LoggerRecorder(logger, { 303 | format: '$date $time $type $id $content', 304 | }); 305 | 306 | // 创建Manager 307 | const services = new Manager({ logRecorder }); 308 | ``` 309 | 310 | 在创建`LoggerRecorder`时,第一个参数`logger`为一个包含了`info, log, debug, error`这四个方法的日志记录器; 311 | 第二个参数为一些选项,比如`format`,其使用方法与上文的`StreamRecorder`相同,但默认值与前者不同。 312 | 313 | `ctx.log()`会使用`logger.log()`来记录,`ctx.debug()`使用`logger.debug()`, 314 | `ctx.error()`使用`logger.error()`,其他的均使用`logger.info()`来记录。 315 | 316 | 通过记录服务调用日志等信息,再结合相应的日志分析系统即可实现调试跟踪等功能。 317 | 318 | 319 | ## License 320 | 321 | ``` 322 | The MIT License (MIT) 323 | 324 | Copyright (c) 2016 SuperID | 免费极速身份验证服务 325 | 326 | Permission is hereby granted, free of charge, to any person obtaining a copy 327 | of this software and associated documentation files (the "Software"), to deal 328 | in the Software without restriction, including without limitation the rights 329 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 330 | copies of the Software, and to permit persons to whom the Software is 331 | furnished to do so, subject to the following conditions: 332 | 333 | The above copyright notice and this permission notice shall be included in all 334 | copies or substantial portions of the Software. 335 | 336 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 337 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 338 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 339 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 340 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 341 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 342 | SOFTWARE. 343 | ``` 344 | --------------------------------------------------------------------------------