# Web服务

# 启动Web服务

我们使用Corejs内置的ServiceCore创建和管理Web服务。启动Web服务分为两个步骤:

首先,使用Core.ServiceCore创建ServiceCore实例:

const Core = require('node-corejs');
const serviceCore = new Core.ServiceCore();
1
2

创建ServiceCore时可以指定Web服务的配置对象configs

  • configs.idServiceCore的ID,用于标识ServiceCore。非必填项,默认值:ServiceCore_${generateRandomString(6, 'uln')};

  • configs.portWeb服务驻留的端口。非必填项,默认值:3000

  • configs.serverOptWeb服务的构建配置。非必填项,默认值:{}

  • configs.baseRoutePathWeb服务的基础请求路径,用于指定统一的请求路径前缀。非必填项,默认值:'/'

    比如,当此配置指定为'/api',且ServiceCore挂载了请求路径规则为'/Test.do'的Handler时,客户端需要请求'/api/Test.do'才能命中此Handler。

    注意

    ServiceCore将自动校正配置对象中指定的基础请求路径:

    • 当基础请求路径不以'/'开头时,将附加'/'作为前缀。比如:当业务层指定基础请求路径'api'时将自动被校正为'/api'

    • 当基础请求路径以'/'结尾时,将删除位于结尾的所有'/'。比如:当业务层指定基础请求路径'/api//'时将自动被校正为'/api'

  • configs.middlewares:全局中间件列表。非必填项,默认值:[]


然后,使用实例方法start()启动Web服务。此方法接收两个非必填参数:启动配置回调函数

serviceCore.start([[options], callBack]);
1

提示

对于构建配置configs.serverOpt和启动配置options,我们将在构建过程一节中进行详细讨论。

通常,我们在执行start()时使用回调函数处理Web服务的启动结果:

serviceCore.start((error) => {
  // 如果error为null则表示Web服务启动成功
  if (error) {
    // Web服务启动失败
    console.log(`服务启动失败: ${error}`);
    return;
  }
  // Web服务启动成功
  console.log('服务启动成功');
});
1
2
3
4
5
6
7
8
9
10

需要注意的是,start()执行成功后将会变更ServiceCore为启动状态,仅允许在关闭状态执行的操作将被拒绝执行:

# 设置请求路径

处理特定请求路径的客户端请求需要结合Handler进行。Handler有独立于ServiceCore生命周期和处理流程

客户端请求经过全局中间件管道后,将进入与请求路径匹配的Handler中执行后续处理。

我们实现一个Handler至少需要:

  • 指定请求路径规则
  • 指定请求方式(比如:GET、POST)对应的处理逻辑

因此,我们在自定义Handler时至少需要:

  • 实现一个继承自Core.Handler的类
  • 重写getRoutePath静态方法,指定请求路径规则
  • 重写[METHOD]Handler实例方法,指定请求方式对应的处理逻辑

注意

指定请求路径规则时,需要重写Handler中的静态方法,即static getRoutePath();而实现某个请求方式的处理逻辑时重写的[METHOD]Handler是实例方法。

另外,[METHOD]Handler不是实际重写的方法名,只是Handler Method的代称,比如:期望处理客户端POST请求时,需要重写Handler中的实例方法postHandler


接下来,让我们来实现Hello World Handler:

class HelloWorldHandler extends Core.Handler {
  // 指定请求路径规则
  static getRoutePath() {
    return '/HelloWorld.do';
  }
  // 指定get请求处理
  getHandler(req, res, next) {
    next('Hello World');
  }
}
1
2
3
4
5
6
7
8
9
10

需要注意的是,Handler必须绑定至ServiceCore才能生效。

所以,我们还应该使用ServiceCore的实例方法bind()HandlerServiceCore绑定:

// 实现Hello World Handler
class HelloWorldHandler extends Core.Handler { ... }

// 创建ServiceCore
const serviceCore = new Core.ServiceCore();
// 绑定ServiceCore和Handler
serviceCore.bind([HelloWorldHandler]);
// 启动ServiceCore
serviceCore.start();
1
2
3
4
5
6
7
8
9

最后,我们使用浏览器打开http://localhost:3000/HelloWorld.do就可以检查实现成果啦!

注意

ServiceCore实例仅允许在处于关闭状态时执行bind()动作,且多次执行bind()时将只保留最后一次绑定的Handler。

# 构建过程

ServiceCore执行实例方法start()时将触发Web服务的实际构建,我们可以通过修改实例属性createServer对构建过程进行定制。

注意

ServiceCore实例仅允许在处于关闭状态时变更其createServer属性。

构建过程中需要包含构建Server启动Server两个阶段。

ServiceCore的实例属性createServer根据构建过程内实际逻辑的同步或异步使用FunctionAsyncFunction,其参数列表依次为:

  • options:执行start()时指定的启动配置

  • appServiceCore自动构建的Express实例

  • configsServiceCore实例化时指定的构造参数。

    ServiceCore在实例化过程中将自动对构造参数中指定的配置进行校正。因此可能与创建ServiceCore时实际指定的构造参数有差异。

  • callBack:回调函数,参数列表为(error, detail)

    ServiceCore将根据此函数回调的信息对构建结果进行仲裁,当回调的errornull时表示构建成功,此时ServiceCore将被变更为启动状态。

    说明

    我们可以在定制构建过程时,使用期望的errordetail调用callBack,以控制ServiceCore实例方法start()回调启动结果时的附加信息。

    在默认的构建过程下,detail的结构为{ app, server, serverType }

    • appServiceCore自动构建的Express实例
    • server:支撑Web服务的Server实例。
    • serverTypeServer实例的类型,为'http''https'

接下来,我们将结合默认构建过程的实现,来讨论创建ServiceCore时指定的configs.serverOpt和执行start()时指定的options的作用。

默认的构建过程实现如下:

createServer(options, app, configs, callBack) {
  // 参数处理
  const { port, serverOpt } = configs;
  options = Object.assign({}, { port }, options);

  // 创建server
  let server = null;
  let serverType = 'http';
  // 当指定了有效的ssl配置时 - 启动https服务器
  if (serverOpt && serverOpt.cert && serverOpt.key) {
    server = https.createServer(serverOpt, app);
    serverType = 'https';
  }
  // 未指定ssl或配置无效时 - 启动http服务器
  else {
    server = http.createServer(serverOpt, app);
    serverType = 'http';
  }

  // 启动server
  server.on('error', (error) => callBack(error));
  server.on('listening', () => callBack(null, { app, server, serverType }));
  server.listen(options);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

因此,在创建ServiceCore时,如果指定了有效的configs.serverOpt.keyconfigs.serverOpt.cert,则在执行start()时将使用https.createServer()创建Server实例,否则使用http.createServer()

提示

configs.serverOpt将作为创建Server实例的配置参数;执行实例方法start()时指定的启动配置options将作为启动Server实例的配置参数。

具体配置项可以参照NodeJS官方文档https.createServer()http.createServer()server.listen()的相关描述。

# TLS/SSL

如果我们期望启动TLS/SSL模式的Web服务,只需在实例化ServiceCore时指定有效的configs.serverOpt.keyconfigs.serverOpt.cert即可。

我们在构建过程一节已经了解到,默认的构建行为使用https.createServer()实现Web服务的TLS/SSL,因此:

我们可以通过指定证书和密钥文件路径的方式启动TLS/SSL模式的Web服务。

const SSL_KEY_PATH = './ssl.key';
const SSL_CERT_PATH = './cert.pem';

const serviceCore = new Core.ServiceCore({
  serverOpt: {
    key: SSL_KEY_PATH,
    cert: SSL_CERT_PATH
  }
});
1
2
3
4
5
6
7
8
9

通常,直接指定证书和密钥文件的路径具有很大的局限性(比如:打包导致文件路径产生偏移)。

我们可以使用将证书和密钥装入Buffer的形式进行规避:

const fs = require('fs');
const SSL_KEY_PATH = './ssl.key';
const SSL_CERT_PATH = './cert.pem';

const serviceCore = new Core.ServiceCore({
  serverOpt: { 
    key: fs.readFileSync(SSL_KEY_PATH),
    cert: fs.readFileSync(SSL_CERT_PATH)
  }
});
1
2
3
4
5
6
7
8
9
10

# 处理模型

请求处理模型

# 全局拦截器

进入ServiceCore的所有客户端流量将被全局拦截器捕获并处理。ServiceCore将在业务层执行实例方法start()时,将全局拦截器使用app.use()挂载至Express中间件列表首位。

我们可以通过修改ServiceCore的实例属性globalIntercaptor以对全局拦截器行为进行定制。

注意

  • 在全局拦截器逻辑结束后,务必使用next()分发处理流程至后续阶段。

  • ServiceCore实例仅允许在处于关闭状态时变更其globalIntercaptor属性。

  • ServiceCore将自动捕获全局拦截器根函数维度产生的异常,使之进入错误拦截器处理。(因此,推荐使用async/await执行异步动作以保证异常抛出)。

ServiceCore的实例属性globalIntercaptor根据全局拦截器内实际逻辑的同步或异步使用FunctionAsyncFunction,其参数列表依次为:

  • req:客户端请求实例。
  • res:客户端返回实例。
  • next:Express中间件流程控制函数,使用方式可以参考Express官方文档中对于next()的描述。

在全局拦截器中不包含异步逻辑时,我们通常指定为Function类型:

// 指定同步全局拦截器
serviceCore.globalIntercaptor = (req, res, next) => {
  // 执行全局拦截器逻辑
  // ...
  // 拦截器逻辑结束后执行next()分发处理流程至下游链路
  next();
};
1
2
3
4
5
6
7

当全局拦截器中包含异步逻辑时,推荐指定为AsyncFunction类型以使用await指令进行异步操作:

// 指定异步全局拦截器
serviceCore.globalIntercaptor = async (req, res, next) => {
  // 执行全局拦截器逻辑
  // ...
  // 拦截器逻辑结束后执行next()分发处理流程至下游链路
  next();
};
1
2
3
4
5
6
7

接下来,我们将结合默认全局拦截器的实现,来讨论其对后续处理过程产生的影响。

默认全局拦截器的实现如下:

globalInterceptor(req, res, next) {
  const requestPath = req.path;
  const routePathes = Object.keys(this._handlerMap);
  let isHandlerBinded = false;
  for (let i = 0; i < routePathes.length; i++) {
    const handlerRoutePath = routePathes[i];
    if (requestPath.indexOf(this.baseRoutePath) === 0 && requestPath.indexOf(handlerRoutePath) !== -1) {
      (isHandlerBinded = true) && next();
      break;
    }
  }

  !isHandlerBinded && res.status(404).end();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

因此,如果客户端请求路径未能匹配到Handler,则认为请求无效直接返回404状态码,不再进入后续的全局中间件Handler阶段

注意

我们可能在全局中间件中使用泛路径中间件(如:express.static),默认的全局拦截器行为可能导致请求无效。此类场景下,我们可以尝试:

  • 将泛路径中间件变更至Handler维度。
  • 变更全局拦截器默认行为,放行相关请求。

# 全局中间件

我们把在全局拦截器中被放行的客户端请求称为有效请求,作用于有效请求的中间件便是全局中间件

提示

全局中间件依赖于Express中间件系统实现,因此兼容Express生态且不支持动态中间件行为。

对于作用于特定请求路径的中间件,我们可以在自定义Handler时根据客户端请求的实际上下文(比如:请求参数):

  • 动态指定中间件列表
  • 控制中间件执行规则(比如:跳过执行)

全局中间件由创建ServiceCore时的构造参数configs.middlewares配置项指定:

const serviceCore = new Core.ServiceCore({
  middlewares: []
});
1
2
3

ServiceCore在业务层执行实例方法start()时,当挂载全局拦截器完成后,将迭代构造参数configs.middlewares内的中间件,依次使用app.use()挂载至Express中间件列表。

因此,我们可以直接使用Express生态的中间件作为全局中间件

const bodyParser = require('body-parser');
// 创建body-parser中间件
const jsonParser = bodyParser.json({ limit: 2 * 1024 * 1024 });
const urlEncodedParser = bodyParser.urlencoded({ limit: 2 * 1024 * 1024, extended: true });
// 创建ServiceCore并启动
const serviceCore = new Core.ServiceCore({
  middlewares: [jsonParser, urlEncodedParser]
});
serviceCore.start();
1
2
3
4
5
6
7
8
9

需要注意的是,全局中间件不支持动态中间件。在一些全局中间件逻辑控制的场景中,我们可以使用Middleware Wrapper实现。

接下来,我们将实现一个根据body-parser解析结果进行自定义处理的🌰,解析异常时不再向客户端抛出500状态码,而是使用200状态码向客户端返回异常信息:

const bodyParser = require('body-parser');
const jsonParser = bodyParser.json({ limit: 1 });
// 对原始中间件进行包装,修改默认逻辑
const jsonParserWrapper = (req, res, next) => {
  jsonParser(req, res, (error) => { res.status(200).send(error.message) });
}
// 创建ServiceCore并启动
const serviceCore = new Core.ServiceCore({
  middlewares: [jsonParserWrapper],
});
serviceCore.start();
1
2
3
4
5
6
7
8
9
10
11

# 错误拦截器

ServiceCore引入了错误拦截器用于收口请求处理过程中产生的异常。ServiceCore在业务层执行实例方法start()时,当挂载全局拦截器全局中间件完成后,将错误拦截器使用app.use()挂载至Express中间件列表。

提示

错误拦截器本质上是一个位于Express中间件列表的末位的标准错误中间件,将捕获到以下类型的异常:

默认的错误拦截器将直接向客户端返回500状态码(即:执行res.status(500).end()),我们可以通过修改ServiceCore的实例属性errorIntercaptor以对错误拦截器逻辑进行定制。

注意

ServiceCore实例仅允许在处于关闭状态时变更其errorIntercaptor属性。

另外,ServiceCore将自动包装错误拦截器Express标准错误中间件。因此,我们在设置错误拦截器时,函数签名按需指定即可,无需严格保持(error, req, res, next)

ServiceCore的实例属性errorIntercaptor根据错误拦截器内实际逻辑的同步或异步使用FunctionAsyncFunction,其参数列表依次为:

  • error:未被捕获的异常。
  • req:客户端请求实例。
  • res:客户端返回实例。
  • next:Express中间件流程控制函数,使用方式可以参考Express官方文档中对于next()的描述。

在错误拦截器中不包含异步逻辑时,我们通常指定为Function类型:

// 指定同步错误拦截器
serviceCore.errorIntercaptor = (error, req, res, next) => {
  // 执行错误拦截器逻辑
  // ...
};
1
2
3
4
5

当错误拦截器中包含异步逻辑时,推荐指定为AsyncFunction类型以使用await指令进行异步操作:

// 指定异步全局拦截器
serviceCore.globalIntercaptor = async (error, req, res, next) => {
  // 执行全局拦截器逻辑
  // ...
};
1
2
3
4
5

当然,错误拦截器的执行过程中也可能产生异常,默认将触发Express的异常处理逻辑。

对于此类异常,我们通常在自定义构建过程时向Express实例中注入兜底Express错误处理中间件

const serviceCore = new Core.serviceCore();
const nativeCreateServer = serviceCore._createServer;
serviceCore.createServer = (options, app, configs, callBack) => {
  // 注入兜底错误处理中间件(逻辑应保持简单以降低异常产生概率)
  app.use((error, req, res, next) => {
    res.status(500).send(error.message);
  });
  nativeCreateServer(options, app, configs, callBack);
}
1
2
3
4
5
6
7
8
9

# 日志收集

我们可以通过ServiceCore的实例属性logger指定其使用的日志收集工具。在内部实现上,ServiceCore将调用this.logger.log(level, funcName, message)输出其内部运行日志。

通常,我们使用Corejs内置的日期输出器作为ServiceCore日志收集工具

const serviceCore = new Core.ServiceCore();
// 指定ServiceCore使用DateLogger进行日志收集
serviceCore.logger = new Core.DateLogger({ filePrefix: 'ServiceCore' });
1
2
3

提示

Corejs内置的日期输出器将同一日期周期内产生的日志归档至一个文件(组),且支持自动清理和文件分割。


ServiceCore内部运行日志的输出等级、方法名和文案存储在Core.MacrosCore.Messages中,我们可以通过提前修改这些宏变量的方式实现日志内容定制(比如:日志国际化)。

  • level:日志输出等级

    提示

    日志输出等级存储在Core.Macros中。

    宏名称 描述 默认值
    SERVICE_CORE_INFOS_LOG_LEVEL 信息日志等级 'infos'
    SERVICE_CORE_WARNS_LOG_LEVEL 警告日志等级 'warns'
    SERVICE_CORE_ERROR_LOG_LEVEL 错误日志等级 'error'
  • funcName:调用方法名

    提示

    调用方法名存储在Core.Messages中。

    宏名称 默认值
    SERVICE_CORE_FUNCNAME_LOG '服务核心'
  • message:日志文案内容

    提示

    日志文案内容存储在Core.Messages中。

    定制日志文案内容时可以使用${VAR_NAME}的形式引用内置变量名以获取特征信息。

    比如:我们可以使用'当前状态下无法执行操作:[${funcName}]'引用内置变量名为'funcName'中的信息。

    宏名称 描述 内置变量名 输出等级
    SERVICE_CORE_MESSAGE_INVALID_STATE 当前状态下不允许执行操作 funcName:操作方法名 SERVICE_CORE_WARNS_LOG_LEVEL
    SERVICE_CORE_MESSAGE_INVALID_HANDLER 待绑定的Handler类型无效 index:Handler位于绑定列表的索引 SERVICE_CORE_WARNS_LOG_LEVEL
    SERVICE_CORE_MESSAGE_INVALID_ROUTE_PATH 待绑定的Handler请求路径无效 routePath:Handler的请求路径 SERVICE_CORE_WARNS_LOG_LEVEL
    SERVICE_CORE_MESSAGE_INVALID_PARAM_TYPE 设置全局拦截器/错误拦截器/构建过程时参数无效 抛出异常,不产生日志
    SERVICE_CORE_MESSAGE_SUCCESS_BIND_HANDLER 成功绑定Handler routePath:Handler的请求路径 SERVICE_CORE_INFOS_LOG_LEVEL
    SERVICE_CORE_MESSAGE_SUCCESS_START_SERVER ServiceCore启动成功 serverType:ServiceCore的服务类型;baseRoutePath:基础请求路径 SERVICE_CORE_INFOS_LOG_LEVEL
    SERVICE_CORE_MESSAGE_FAILURE_START_SERVER ServiceCore启动失败 error:启动失败的原因 SERVICE_CORE_ERROR_LOG_LEVEL