Ts + express 后端项目搭建记录

6,126 阅读6分钟

上一章介绍了前端项目的搭建,那么这一章我们基于express来搭建后台项目

技术栈

express typescript mongoose

初始化项目

项目初始化

# 初始化
yarn init -y

# 初始化git仓库
git init

# 安装express
yarn add express

# 安装typescript
# ts-node-dev 监听文件变化重启服务,并共享ts的结果,白话就是node运行并监听ts文件
# 安装express、node的声明文件
yarn add typescript ts-node-dev @types/express @types/node --dev

package.json新增启动脚本

"scripts": {
    "dev": "ts-node-dev --respawn --transpile-only src/app.ts"
},

根目录新建.gitignore

node_modules
build

ts环境配置,这在上一章有过介绍 新建tsconfig.json

{
  "compilerOptions": {
    "outDir": "build",
    "target": "es5",
    "module": "commonjs",
    "sourceMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

入口文件app.ts 新建src目录,在目录下创建app.ts

// src/app.ts

import express from 'express'
import routes from './routes' // 路由

const app = express()

app.use(express.json())

const PORT = 1337

// 启动
app.listen(PORT, async () => {
  console.log(`App is running at http://localhost:${PORT}`)
  routes(app)
})

路由 创建routes目录,目录下创建index.ts

// src/routes/index.ts

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

// 路由配置接口
interface RouterConf {
  path: string,
  router: Router,
  meta?: unknow
}

// 路由配置
const routerConf: Array<RouterConf> = []

function routes(app: Express) {
  // 根目录
  app.get('/', (req: Request, res: Response) => res.status(200).send('Hello Shinp!!!'))

  routerConf.forEach((conf) => app.use(conf.path, conf.router))
}

export default routes

启动项目

yarn dev

# 浏览器访问localhost:1337
# 可以看到浏览器输出Hello Shinp!!!

代码规范(上一章介绍过了,基本相同)

参考配置

// .eslintrc.js

module.exports = {
  root: true,
  // 扩展规则
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
    'prettier',
  ],
  parserOptions: {
    ecmaVersion: 12,
    parser: '@typescript-eslint/parser',
    sourceType: 'module',
  },
  // 注册插件
  plugins: ['@typescript-eslint', 'prettier'],
  // 规则 根据自己需要增加
  rules: {
    'no-var': 'error',
    'no-undef': 0,
    '@typescript-eslint/consistent-type-definitions': ['error', 'interface'],
  },
}
// .prettierrc.js

module.exports = {
  tabWidth: 2,
  semi: false,
  singleQuote: true,
}

项目公共配置

日志输出

pino 高效的Node.js 记录器,美化输出。

dayjs 极简的日期操作JS库。

# 安装
yarn add pino pino-pretty dayjs

# 安装声明文件
yarn add @types/pino --dev

根目录下创建utils目录,目录下创建logger.ts

// logger.ts

import pino from 'pino'
import dayjs from 'dayjs'

const log = pino({
  transport: { // pino 7.x的写法有所不同
    target: 'pino-pretty',
  },
  base: {
    pid: false,
  },
  timestamp: () => `,"time":"${dayjs().format()}"`,
})

export default log

app.ts中使用

// app.ts 有删减
import logger from './utils/logger'

app.listen(POST, async () => {
  logger.info(`App is running at http://localhost:${POST}`)
  routes(app)
})

全局配置

config 轻量、简单的nodejs配置部署库

使用非常简单,在根目录创建config,就可以在任何地方使用API操作

根目录下创建config目录,目录下创建default.json

/* config/default.json */
{
  "port": 1337
}

也可以是ts文件

// default.ts

export default {
  port: 1337,
}

app.ts中使用

// app.ts 有删减

import config from 'config'

const PORT = config.get<number>('port')

app.listen(PORT, async () => {
  logger.info(`App is running at http://localhost:${PORT}`)
})

统一响应参数

根目录创建constants目录,用于存放常量,目录下创建code.ts

// constants/code.ts

// 枚举状态码 根据自己需要定义
enum Code {
  success = 3000,
  denied,
  error
}

enum CodeMessage {
  success = '请求成功',
  denied = '无权限',
  error = '系统异常'
}

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

export { Code, codeType, CodeMessage }

utils下创建commonRes.ts

// utils/commonRes.ts
// 按自己需要删改

import { Response } from 'express'
import { Code, codeType, CodeMessage } from '../constants/code'

interface ResOption {
  type?: codeType
  status?: number
  message?: unknow
}

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

  const { type, status, message } = options
  let resStatus = status

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

  // 响应参数
  const sendRes: { code: number; data: unknown; message?: unknow } = {
    code: Code[type as codeType],
    data,
  }
  // 响应描述
  message && (sendRes.message = message)

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

// 错误响应
commonRes.error = function (res: Response, data: unknown, message?: unknown, status?: number) {
  logger.error(message || CodeMessage['error'])
  this(res, data, { type: 'error', message: message || CodeMessage['error'], status: status || 409 })
}

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

export default commonRes

routes中使用

// routes/index.ts 有删减

app.get('/', (req: Request, res: Response) => {
    // commonRes(res, { word: 'Hello Shinp!!!' }, { type: 'success', message: '请求成功' }) 成功
    // commonRes.denied(res, null) 无权限
    // commonRes.error(res, null) 错误
    commonRes(res, { word: 'Hello Shinp!!!' }) // 成功
})

浏览器打印

{
    "code": 3000,
    "data": {
        "word": "Hello Shinp!!!"
    }
}

静态错误捕捉

// utils/silentHandle.ts

// 如果执行过程有错误,则捕捉并赋值给返回数组的第一个元素
async function silentHandle<T, U = Error>(fn: Function, ...args: Array<unknown>): Promise<[U, null] | [null, T]> {
  let result: [U, null] | [null, T]

  try {
    result = [null, await fn(...args)]
  } catch (e: any) {
    result = [e, null]
  }

  return result
}

export default silentHandle

使用

// routes/index.ts  有删减

import silentHandle from '../utils/silentHandle'

const getInfo = function () {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      Math.random() > .5 ? resolve('info...') : reject('error...')
    }, 500)
  })
}

app.get('/', async (req: Request, res: Response) => {
    const [e, result] = await silentHandle(getInfo)
    e ? commonRes.error(res, null) : commonRes(res, { result })
})

中间件 - 响应头信息

根目录创建middleware,用于存放中间件,目录下创建responseHeader.ts

// middleware/responseHeader.ts

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

const responseHeader = (req: Request, res: Response, next: NextFunction) => {
  const { origin, Origin, referer, Referer } = req.headers
  
  // 若没有手动设置,则为通配符
  const allowOrigin = origin || Origin || referer || Referer || '*'
    
  // 允许请求源
  res.header('Access-Control-Allow-Origin', allowOrigin)
  // 允许头部字段
  res.header('Access-Control-Allow-Headers', 'Content-Type')
  // 允许公开的头部字段
  res.header('Access-Control-Expose-Headers', 'Content-Disposition')
  // 允许的请求方式
  res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS')
  // 允许携带cookie
  res.header('Access-Control-Allow-Credentials', 'true')

  // 预检返回204
  if (req.method == 'OPTIONS') {
    res.sendStatus(204)
  } else {
    next()
  }
}

export default responseHeader

创建index.ts

import { Express } from 'express'
import express from 'express'
import responseHeader from './responseHeader'

function initMiddleware(app: Express) {
  app.use(express.json())
  app.use(responseHeader)
}

export default initMiddleware

app.ts中

import initMiddleware from './middleware'

const app = express()

// 挂载中间件
initMiddleware(app)

mongodb

安装

安装就不赘述啦,大家根据自己的系统本地安装mongodb,配合compass使用即可

连接mongodb

# mongoose 操作mongodb的库
# zod 类型定义的库

yarn add mongoose zod
yarn @types/mongoose --dev

配置

// config/default.ts

// 新增 mongodb连接配置
dbUri: 'mongodb://localhost:27017/shinp',
dbUser: 'root',
dbPassword: '******',
dbAuthSource: 'admin',
// utils/dbConnect.ts

// 连接db

import mongoose from 'mongoose'
import config from 'config'
import logger from './logger'

async function dbConnect() {
  const dbUri = config.get<string>('dbUri')
  const dbUser = config.get<string>('dbUser')
  const dbPassword = config.get<string>('dbPassword')
  const dbAuthSource = config.get<string>('dbAuthSource')

  try {
    const connection = await mongoose.connect(dbUri, {
      user: dbUser,
      pass: dbPassword,
      authSource: dbAuthSource,
    })

    logger.info('DB connected')

    return connection
  } catch (error) {
    logger.error('Could not connect to db')
    process.exit(1)
  }
}

export default dbConnect

app.ts中使用

// app.ts

import dbConnect from './utils/dbConnect'

app.listen(PORT, async () => {
  logger.info(`App is running at http://localhost:${PORT}`)

  await dbConnect()

  routes(app)
})

启动,控制台出现DB connected,则说明连接成功

定义数据模块

以用户信息为例

根目录下创建models/user.model.ts

// user.model.ts

import mongoose from 'mongoose'
import config from 'config'

// 模板接口
export interface UserDocument extends mongoose.Document {
  name: string
  account: string
  password: string
  createdAt: Date
  updatedAt: Date
  deletedAt: Date
}

// 模板校验规则
const userSchema = new mongoose.Schema(
  {
    name: { type: String, required: true },
    account: { type: String, required: true },
    password: { type: String, required: true },
  },
  {
    timestamps: true,
  }
)

// 唯一
userSchema.index({ account: 1, deletedAt: 1 }, { unique: true })

// 创建模板 执行之后会自动在mongodb中创建相应的模板
const UserModel = mongoose.model<UserDocument>('User', userSchema)

export default UserModel

参数校验

根目录下创建schema/user.schema.ts

// user.schema.ts
// 接口参数校验 主要使用zod,具体使用可查看文档

import { object, string, TypeOf } from 'zod'

// 创建接口
export const createUserSchema = object({
  body: object({
    account: string({ required_error: '缺少账号名称' }).nonempty(),
    name: string({ required_error: '缺少用户姓名' }).nonempty(),
    password: string({ required_error: '缺少用户密码' }).min(6, '密码太短 - 至少6个字符'),
    passwordConfirmation: string({ required_error: '缺少确认密码' }),
  }).refine((data) => data.password === data.passwordConfirmation, {
    message: '两次密码不匹配',
    path: ['passwordConfirmation'],
  }),
})

export type CreateUserInput = Omit<TypeOf<typeof createUserSchema>, 'body.passwordConfirmation'>

新增一个用于接口参数校验的中间件

// middleware/validate.ts

import { Request, Response, NextFunction } from 'express'
import { AnyZodObject } from 'zod'
import { commonRes } from '../utils'

const validate = (schema: AnyZodObject) => (req: Request, res: Response, next: NextFunction) => {
  try {
    schema.parse({
      body: req.body,
      query: req.query,
      params: req.params,
    })

    next()
  } catch (e: any) {
    return commonRes.error(res, null, e.errors)
  }
}

export default validate

通用数据库操作函数

提供一套常用的mongodb 增删改查操作方法

// utils/crudProvider.ts

// 根据自己的需要增加

import { FilterQuery, UpdateQuery, DocumentDefinition, QueryOptions, Model, InsertManyOptions } from 'mongoose'

class BaseCrudProviderCls<document, Cdocument> {
  private DBModel: Model<any>

  constructor(DBModel: Model<any>) {
    this.DBModel = DBModel
  }

  async create(input: DocumentDefinition<Cdocument>) {
    const data = await this.DBModel.create(input)

    return data.toJSON()
  }

  async update(query: FilterQuery<document>, update: UpdateQuery<document>, options?: QueryOptions) {
    return this.DBModel.updateOne(query, update, options)
  }

  async find(query: FilterQuery<document>, projection?: any, options?: QueryOptions) {
    const result = await this.DBModel.find(query, projection, options)
    return result && result.map((d) => d.toJSON())
  }
}

const BaseCrudProvider = function <document, Cdocument>(DBModel: Model<any>) {
  const CRUD = new BaseCrudProviderCls<document, Cdocument>(DBModel)

  return {
    create: CRUD.create.bind(CRUD),
    update: CRUD.update.bind(CRUD),
    find: CRUD.find.bind(CRUD),
  }
}

export { BaseCrudProvider }

数据库操作

根目录下创建service/user.service.ts

// user.service.ts
// mongodb操作

import { BaseCrudProvider } from '../utils/crudProvider'
import UserModel, { UserDocument } from '../models/user.model'

const CRUD = BaseCrudProvider<UserDocument, Omit<UserDocument, 'createdAt'>>(UserModel)

export default CRUD

接口处理

根目录下创建controller/user.controller.ts

// user.controller.ts

import { Request, Response } from 'express'
import { commonRes, silentHandle } from '../utils'

import { CreateUserInput } from '../schema/user.schema'
import USER_CRUD from '../service/user.service'

export async function createUserHandler(req: Request<{}, {}, CreateUserInput['body']>, res: Response) {
  const [e, user] = await silentHandle(USER_CRUD.create, req.body)

  return e ? commonRes.error(res, null, e.message) : commonRes(res, user)
}

路由定义

最后一步,新增routes/user.routes.ts

import { Router } from 'express'
import { createUserHandler } from '../controller/user.controller'
import validate from '../middleware/validate'
import { createUserSchema } from '../schema/user.schema'

const router = Router()

// 需要校验接口参数的,加上校验中间件
router.post('/create', validate(createUserSchema), createUserHandler)

export default router

routes/index.ts中引入

import User from './user.routes'

const routerConf: Array<RouterConf> = [{ path: '/user', router: User }]

启动项目 调试接口

启动项目,刷新compass中的数据库,可以看到users模块

postman调用接口,新增一条记录,可以传入不同的参数测试参数校验

至此mongodb配置完成,根据自己需要新增模块

完成

至此,后台项目基本框架就搭建完成了,后续根据自己需要的功能扩展即可

以上是结合自身开发经历,以及阅读过的其他大神的优秀代码搭建的,有误的地方,还请多多指正,谢谢!!

后续可能会记录一些持续集成相关的内容,有兴趣的童鞋可以关注。