如何在vue项目中用Express做中间层

背景:在已有的vue2项目中,需要进行BFF开发,进行数据聚合和接口转发。为便于代码管理,将Express项目和前端放在一个代码仓库内,不进行拆分。

一、在项目根目录下新建server目录,并进入到server目录内

二、初始化项目

1.初始化

npm init
为了避免依赖会有版本冲突,Express单独维护自己的node_modules

2.安装express

npm i express --save

3.安装typescript

安装typescript、ts-node(用来监听ts文件的变化并更新服务),node和express的声明文件@types/node @types/express
npm i typescript ts-node @types/express @types/node -D

4. ts环境配置

新建tsconfig.json文件

{
        // 指定需要编译文件 否则默认当前目录下除了exclude之外的所有.ts, .d.ts,.tsx 文件
        "include":["app.ts"],
        "compilerOptions": {
            // 指定使用的模块 :commonjs amd system cmd  or es2015
            "module": "commonjs",
              /* 注意:如果未指定--lib,则会注入默认的librares列表。注入的默认库为:
                对于 --target ES5: DOM,ES5,ScriptHost
                对于 --target ES6: DOM,ES6,DOM.Iterable,ScriptHost
                TS 绝不会在您的代码中注入polyfill,所以需要你自己制定编译lib */
            "lib": ["ESNext"],
             // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES2015', 'ES2016','ES2017', or 'ESNEXT'
            "target": "ES6",
            // 编译输入文件存放的目录
            "outDir": "build",
            // 是否生成.map文件
            "sourceMap": true,
            "esModuleInterop": true,
            "forceConsistentCasingInFileNames": true,
            "strict": true,
            /* Strict Type-Checking Options */
            // 严格模式将会打开下面的几个选项
            "strict": false, 
            /* 不允许变量或函数参数具有隐式any类型,例如
            function(name) {
                return name;
            } */
            "noImplicitAny": true,
            // null类型检测,const teacher: string = null;会报错
            "strictNullChecks": true,
            // 对函数参数进行严格逆变比较
            "strictFunctionTypes": true,
            // 严格检查bind call apply
            "strictBindCallApply": true,
            // 此规则将验证构造函数内部初始化前后已定义的属性。
            "strictPropertyInitialization": true,
            // 检测this是否隐式指定
            "noImplicitThis": true,
            // 使用js的严格模式,在每一个文件上部声明 use strict
            "alwaysStrict": true,
            /* Additional Checks */
            // 默认false,是否检测定义了但是没使用的变量
            "noUnusedLocals": true,
            // 用于检查是否有在函数体中没有使用的参数
            "noUnusedParameters": true,
            // 用于检查函数是否有返回值,设为true后,如果函数没有返回值则会提示
            "noImplicitReturns": true,
            // 用于检查switch中是否有case没有使用break跳出switch
            "noFallthroughCasesInSwitch": true,
        }
}

5. 新建controller目录,用来管理路由

创建article.ts文件,定义文章相关的路由

import express from 'express'
import {Response, Request} from 'express'

const router = express.Router()

// 文章分类列表
// 路由处理函数一般会有读写数据库的操作,一般会抽离出来,单独在service目录下管理
router.get('/cates/list', function(req:Request, res:Response)=>{
    // do something
    res.send({
        code: 0,
        msg: 'success',
        data:[]
    })
})

export default router

6. server目录下新建入口文件 app.ts

import express from 'express'
import articleRouter from './controller/artcate''
import mw from './middleware'

const app = express()

app.use('/article', articleRouter)

// 错误级别中间件
app.use(mw.errorHandler)

app.listen(3007, () => {
  console.log('api server is running at http://127.0.0.1:3007')
})

7.package.json中新增启动脚本

"script":{
    "dev": "nodemon app.ts"
}

此时运行npm run dev,在postman中就可以正常请求/article/cates/list 接口了

三、公共配置

1. 日志输出

安装pino和dayjs
npm i pino pino-pretty dayjs --save
npm i @types/pino -D

异常监控 Sentry

2. 统一响应参数

新建const目录,用于存放常量。在const目录下新建code.ts用于存放相应参数,包括status、code和message

export enum HTTP_CODE {
  SUCCESS = 0,
  BAD_REQUEST = 400,
  UNAUTHORIZED = 401,
  NOT_FOUND = 404,
  BAD_SERVER = 500,
  NOT_IMPLEMENTED = 501,
  BAD_GATEWAY = 502,
  UNAVAILABLE = 503
}

export enum CODE_MESSAGE {
  SUCCESS = 'success',
  BAD_REQUEST = 'BAD_REQUEST',
  UNAUTHORIZED = '未授权',
  NOT_FOUND = '404',
  BAD_SERVER = '请求异常',
  NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',
  BAD_GATEWAY = 'BAD_GATEWAY',
  UNAVAILABLE = 'UNAVAILABLE'
}

// 状态类型 只能是Code中所枚举的状态
export type CODE_TYPE = keyof typeof HTTP_CODE

utils目录下新建commonRes.ts ,对响应进行统一处理

import { Console } from 'console'
import { Response } from 'express'
import { HTTP_CODE, CODE_MESSAGE, CODE_TYPE } from '../common/const/code'
import logger from './logger'

interface ResOption {
  type?: CODE_TYPE
  message?: string
  status?: number
}

// 默认成功响应
function commonRes(res: Response, data: any, options?: ResOption) {
  options = Object.assign({ type: HTTP_CODE[0] }, options || {}) // 默认success

  const { type, status, message } = options

  let resStatus = status

  if (resStatus === undefined) {
    // 根据状态设置状态码
    resStatus = type === HTTP_CODE[3000] ? 200 : 409
  }

  // 响应参数
  const sendRes: { code: number; data: any; message?: string } = {
    code: HTTP_CODE[type as CODE_TYPE],
    data
  }

  // 响应描述
  message && (sendRes.message = message)

  return res.status(resStatus).send(sendRes)
}

// 错误响应
commonRes.error = function (
  res: Response,
  data: unknown,
  status: number,
  message?: unknown
) {
  const type = HTTP_CODE[status]
  const msg = message || (CODE_MESSAGE[type] as string)

  logger.error(msg)

  this(res, data, {
    type: type,
    message: msg,
    status: status
  })
}

// 无权限响应
commonRes.denied = function (res: Response, data: unknown) {
  this(res, data, {
    type: 'UNAUTHORIZED',
    message: CODE_MESSAGE['UNAUTHORIZED'],
    status: 401
  })
}

export default commonRes