Node.js(下面简称Node)是一个跨平台的JavaScript服务端运行环境,在浏览器之外运行V8 JavaScript
引擎。
Nodejs的核心组成成分:
- V8引擎,它负责把JavaScript代码解释成本地的二进制代码运行
- libuv,它主要负责订阅和处理系统的各种内核消息(跨平台libuv in linux & iocp in windows)
Node其实就是libuv的一个应用。简单的说Node只是把JavaScript解释成C++的回调,并挂在libuv消息循环上,等待处理。这样就实现了非阻塞的异步处理机制。
Node单线程,异步非阻塞的特性使它在执行I/O操作是性能非常高。不过受限于单线程的限制,可能它并不适合用来开发大规模的服务端应用。轻量级的Node做的比较多的是服务中间层,用于服务转发,处理后端数据结构及一些通用的简单的接口。
创建Http Web服务器的示例:
const http = require('http')
const hostname = '127.0.0.1'
const port = 3000
const server = http.createServer((req, res) => {
res.statusCode = 200
res.setHeader('Content-Type', 'text/plain')
res.end('Hello World\n')
})
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`)
})
本文主要讨Node作为BFF(Backend For Frontend)中间层的作用, 以及它在企业级中和Serverless应用的入门教程, 以各个模块内使用比较多的三方框架举例。
1. 轻量级—中间层BFF
Express.js提供了简单而又强大的方式来创建Web服务器,主要概念包括Routing、Middleware等。通常一个项目的目录分布如下:
├── routes // 存放路由的文件夹
├──index.js
├──util.js
├── modules // 存放业务逻辑 或叫services
├── db // DB层
├── index.js // 入口
Express.js创建Web服务器的示例:
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`listening at http://localhost:${port}`)
})
1.1 三方中间件
压缩、cookie解析等
app.use(compression())
app.use(cookieParser())
1.2 登录鉴权
以SSO登录鉴权举例
app.use(ssoauth)
// modules/ssoauth.js
module.exports = function(req, res, next) {
co(function*() {
const token = getCoffeeToken(req)
const userInfo = yield getUserInfo(token)
if (!userInfo || !userInfo.work_code) {
throw new Error('check session fail')
}
req.userInfo = userInfo
next()
}).catch(err => {
const isAjaxRequest = req.xhr || (req.headers.accept || '').indexOf('json') > -1
if (isAjaxRequest) {
res.status(401).send('Unauthorized')
} else {
res.redirect('xxx')
}
})
}
1.3 监控&鉴权
app.use('/:project/api/:api*', [
auth('http'),
auditLog('API'),
etraceMonitor('http'),
apiProxy // 服务转发,详细见1.4
])
以auth鉴权举例
// modules/auth.js
module.exports = type => (req, res, next) => {
const parsedUrl = url.parse(req.originalUrl)
const { project, appId, service } = req.params
co(function*() {
// 请求DB
const authIds = yield db.auth_group_auth.findAll(// ...)
if (authIds) {
return next()
}
return res.status(403).send('Not Allowed')
}).catch(err => res.status(500).send(err.message))
}
1.4 服务转发
示例见1.3的apiProxy
通过路由匹配一定规则,利用express-http-proxy实现转发
// modules/apiProxy.js
const proxy = require('express-http-proxy')
const url = require('url')
const pathToRegexp = require('path-to-regexp')
const restApiPattern = pathToRegexp('/:project/api/:api*')
const apis = {
project1: {
dev: 'https://project1.test.com'
},
project2: {
dev: 'https://project2.test.com'
},
project3: {
dev: 'https://project3.test.com'
},
}
module.exports = function(req) {
const args = arguments
const { ENV = 'alpha', NODE_ENV = 'alpha' } = req.configs
const project = req.params.project
const proxyConfig = {
limit: '500mb',
proxyReqPathResolver: function(request) {
const fn = restApiPattern.exec(request.originalUrl)[2]
return `/${fn}`
},
// modify the proxy's response before sending it to the client
// userResDecorator: function(proxyRes, proxyResData, userReq, userRes) {
// logger.info(proxyRes.statusCode)
// return proxyResData
// },
proxyErrorHandler: function(err, res, next) {
logger.error(`apiProxy error: ${err.message}`)
next(err)
}
}
return proxy(apis[project][ENV], proxyConfig).apply(null, args)
}
1.5 HTTP路由
app.use(routes)
// routes/index.js
router.use('/util', utilRoutes)
router.get('/user', function(req, res, next) {
res.send(Object.assign({}, req.userInfo))
})
另一个轻量级比较流行的库是koa.js:由 Express 背后的同一个团队构建,旨在更简单、更小建立轻量级应用。Koa 的中间件和 Express 不同,Koa 选择了洋葱圈模型。所有的请求经过中间件会执行两次(示例可参考2.3.3)。
中间件洋葱圈模型图:
2. 企业级应用
Express、koa 等轻量级库受限于简单的API和它们的特性使得它很难用到企业级应用。
Egg.js
Egg.js继承于koa, Egg对多进程通信的支持与增强,很高的可扩展性插件机制。它的诞生就是为企业级框架和应用而生的。
Midway.js
Midway.js 是淘宝前端架构团队基于Egg的开发的上层框架。支持了 Web / 全栈 / 微服务 / RPC / Socket / Serverless 等多种场景。结合面向对象和函数式编程,因更详细,更丰富的功能,提供可靠的企业级应用Node.js 服务端研发体验。
Midway.js项目目录
├── src
│ ├── controller
│ │ ├── user.ts
│ │ └── home.ts
│ ├── interface.ts
│ ├── middleware ## 中间件目录
│ │ └── report.ts
│ └── service
│ └── user.ts
├── test
├── package.json
└── tsconfig.json
2.1 控制器(Controller)
Midway.js 创建Web服务器示例
// src/controller/user.ts
import { provide, controller, get, inject, config } from '@ali/midway';
import { IUserService, IUserResult } from '../../interface';
@provide()
@controller('/user')
export class UserController {
@inject('userService')
service: IUserService;
@config('tokenName')
tokenName;
@get('/')
async getSSOUser(ctx): Promise<void> {
const token = ctx.cookies.get(this.tokenName, { signed: false});
const user: IUserResult = await this.service.getUser({token}, ctx);
ctx.body = user;
}
}
Midway 使用 @Controller() 装饰器标注控制器。 @Get、 @Post 表示HTTP请求方法,参数表示其路由。
2.2 服务和注入
除了一个 @Provide 装饰器外,整个服务的结构和普通的 Class 一模一样。
// src/service/user.ts
import { provide, config, Context } from '@ali/midway';
import { IUserOptions, IUserResult, IUserService } from '../../interface';
@provide('userService')
export class UserService implements IUserService {
@config('serverUrl')
serverUrl;
async getUser(options: IUserOptions, ctx: Context): Promise<IUserResult> {
const result = await ctx.curl(`${this.serverUrl}/token/${options.token}/user`, {
dataType: 'json',
rejectUnauthorized: false
})
return result;
}
}
- 使用服务
在需要使用服务的地方"依赖注入(IOC)"进类中
@inject('userService')
service: IUserService;
2.3 中间件
2.3.1 全局中间件
全局中间件就是对所有路由都生效的Web中间件。
// src/config/config.default.ts
export = (appInfo: any) => {
const config: any = exports = {};
config.middleware = [
'ssoauth'
];
return config;
};
2.3.2 路由中间件
在某个路由上生效的中间件
import { Controller, Get, Provide } from '@midwayjs/decorator';
@Provide()
@Controller('/')
export class HomeController {
@Get('/', { middleware: ['reportMiddleware'] })
async home() {}
}
编写中间件——打印Controller执行的时间
根据洋葱圈模型的特性, await next() 后面的代码代表了异步请求结束后要执行的逻辑。
import { Provide } from '@midwayjs/decorator';
import { IWebMiddleware, IMidwayWebNext } from '@midwayjs/web';
import { Context } from 'egg';
@Provide()
export class ReportMiddleware implements IWebMiddleware {
resolve() {
return async (ctx: Context, next: IMidwayWebNext) => {
// 控制器前执行的逻辑
const startTime = Date.now();
// 执行下一个 Web 中间件,最后执行到控制器
await next();
// 控制器之后执行的逻辑
console.log(Date.now() - startTime);
};
}
}
2.3.3 三方中间件
社区有很多三方中间件,Midway 的 Class 写法可以比较方便的接入。本质上, resolve() 方法只需要返回一个符合当前中间件格式的方法即可。
2.4 方法拦截器(切面)
拦截器一般会放在 src/aspect 目录。
// src/controller/home.ts
import { Controller, Get, Provide } from '@midwayjs/decorator';
@Provide()
@Controller('/')
export class HomeController {
@Get('/')
async home() {
return 'Hello Midwayjs!';
}
}
// src/aspect/report.ts
import { Aspect, IMethodAspect, JoinPoint, Provide } from '@midwayjs/decorator';
import { HomeController } from '../controller/home';
@Provide()
@Aspect(HomeController)
export class ReportInfo implements IMethodAspect {
async before(point: JoinPoint) {
console.log('before home router run');
}
}
3. Serverless(无服务器)
无服务器是一种云原生开发模型,可使开发人员专注构建和运行应用,而无需管理服务器。无服务器计算产品通常分为两类:后端即服务(BaaS) 和功能即服务(FaaS)。一般情况下开发人员开发无服务是指FaaS。Faas是一种事件驱动的计算模型。
Faas不适用于一下场景:
- 执行时间超过函数配置下限制的(最好不超过 5s)
- 有状态,在本地存储数据的
- 长链接,比如 ws 等
- 后台任务,有大数据执行的
- 依赖多进程通信的
- 大文件上传(比如网关限制的 2M 以上)
- 自定义环境的,比如 nginx 配置,c++ 库(c++ addon 动态链接库等),python 版本依赖的
- 大量服务端渲染(服务端渲染需要缓存,不是很适合函数场景)
下面以Midway Serverless 和阿里云函数计算平台为例
目录结构
.
├── f.yml # 标准化 spec 文件,函数定义文件
├── package.json # 项目依赖
├── src
│ └── function
│ └── hello.ts ## 函数文件
└── tsconfig.json # TypeScript 配置文件
// f.yml
service:
name: midway-http # 应用名
provider:
name: aliyun # 服务供应商
custom:
customDomain:
domainName: auto # 域名,可指定域名
// funtion/hello.ts
import {
Provide,
Inject,
ServerlessTrigger,
ServerlessTriggerType,
Query,
} from '@midwayjs/decorator';
import { Context } from '@midwayjs/faas';
@Provide()
export class HelloHTTPService {
@Inject()
ctx: Context;
@ServerlessTrigger(ServerlessTriggerType.HTTP, {
path: '/',
method: 'get',
})
async handleHTTPEvent(@Query() name = 'midwayjs') {
return `Hello ${name}`;
}
}
参考文档
- 阿里巴巴前端支持图谱: f2e.tech/
- Node.js简介: nodejs.cn/learn/intro…
- Express: expressjs.com/
- Egg.js: eggjs.org/zh-cn/intro…
- Midway.js: midwayjs.org/
- Midway Serverless: midwayjs.org/docs/server…
- Nodejs应用监控器Pandora.js: github.com/midwayjs/pa…
- cluster包实现多进程:www.jb51.net/article/810…
- 阿里云函数计算FC控制台:fcnext.console.aliyun.com/cn-hangzhou…
- 阿里云函数计算帮助文档:help.aliyun.com/product/509…
- 使用函数计算对日志服务中的数据进行ETL数据处理:bp.aliyun.com/detail/72