Hello Node.js -- 一文通览Node.js在轻量级BFF、企业级和Serverless应用的入门教程

3,423 阅读2分钟

Node.js(下面简称Node)是一个跨平台的JavaScript服务端运行环境,在浏览器之外运行V8 JavaScript
引擎。
Nodejs的核心组成成分

  1. V8引擎,它负责把JavaScript代码解释成本地的二进制代码运行
  2. 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服务器,主要概念包括RoutingMiddleware等。通常一个项目的目录分布如下:

├── 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.3apiProxy
通过路由匹配一定规则,利用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)。
中间件洋葱圈模型图:

1636011134098-b510fd79-6869-4e04-8e86-5a4d031b569a.png

2. 企业级应用

Expresskoa 等轻量级库受限于简单的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}`;
  }
}

参考文档