Ts + express 后端项目搭建学习记录

121 阅读7分钟

项目初始化

*# 初始化*

pnpm init

*# 初始化git仓库*

pnpm add express

*# 安装typescript*

*# ts-node-dev 监听文件变化重启服务,并共享ts的结果,白话就是node运行并监听ts文件*

*# 安装express、node的声明文件*

pnpm add typescript ts-node-dev @types/express @types/node --save-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的创建

import express from 'express'
import logger from './utils/logger'
import initMiddleware from './middleware'
const app = express()


const PORT = 3000
app.listen(PORT, () =>
  console.log(`App is running at http://127.0.0.1:${PORT}`)
)

路由初始化

创建路由文件夹

import { Express, Router, Request, Response } from 'express'
import userRouter from './user'
import videoRouter from './video'

// 路由配置类型接口
interface RouterConf {
  path: string
  router: Router
  meta?: Record<string, any>
}

// 定义路由前缀
const API_PREFIX = '/api'

// 定义路由配置 数组
const routerConf = [
  { path: '/users', router: userRouter },
  { path: '/video', router: videoRouter }
] as RouterConf[]

function routes(app: Express) {
  // 根目录请求
  app.get('/', (req: Request, res: Response) => {
    res.status(200).send('hello workaholic ~')
  })

  // 将 路由配置表进行遍历 注册成为 路由中间件
  routerConf.forEach((router) =>
    app.use(`${API_PREFIX}${router.path}`, router.router)
  )
}

export default routes

注册使用

创建两个测试的路由文件

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

const userRouter = Router()

userRouter.get('/', (req: Request, res: Response) => {
  res.send('/api/users/')
})

export default userRouter
import { Request, Response, Router } from 'express'

const videoRouter = Router()

videoRouter.get('/', (req: Request, res: Response) => {
  res.send('/api/video/')
})

export default videoRouter

发送请求

打印信息美化

安装插件 进行打印控制台信息 美化

# 安装
pnpm add pino pino-pretty dayjs

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

创建对应文件夹目录

// logger.ts

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

const log = pino({
  transport: {
    target: 'pino-pretty'
  },
  base: {
    pid: false
  },
  timestamp: () => `,"time":"${dayjs().format()}"`
})

export default log

使用

代码格式化插件

pnpm add prettier -D

根目录 创建文件 .prettierrc

{
  "arrowParens": "always",
  "bracketSameLine": true,
  "bracketSpacing": true,
  "embeddedLanguageFormatting": "auto",
  "htmlWhitespaceSensitivity": "css",
  "insertPragma": false,
  "jsxSingleQuote": false,
  "printWidth": 120,
  "proseWrap": "never",
  "quoteProps": "as-needed",
  "requirePragma": false,
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "useTabs": false,
  "vueIndentScriptAndStyle": false,
  "singleAttributePerLine": false,
  "endOfLine": "auto"
}

全局配置 config

pnpm add config

pnpm add @types/config --save-dev

再根目录创建文件目录

export default {
  port: 3000,
  api_url: '/api'
}

使用

配置统一状态码

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

// 枚举状态码
enum Code {
  success = 3000,
  denied,
  error
}

enum CodeMessage {
  success = '请求成功',
  denied = '无权限',
  error = '请求失败'
}

// 定义字面量类型 遍历Code 得出
type codeType = keyof typeof Code

export { Code, CodeMessage, codeType }

utils下创建commonRes.ts

// utils/commonRes.ts

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

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

// 默认成功响应
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?: unknown } = {
    code: Code[type as codeType],
    data
  }
  // 响应描述
  message && (sendRes.message = message)
  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

状态码 封装的使用方式

成功 想要返回message

静态错误捕获

// 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

代码解释如下:

这段代码定义了一个 silentHandle 异步函数,用于捕获执行过程中可能发生的错误,并以特定格式返回结果。我们将逐步解释每个部分的含义,并通过一个实际例子来帮助理解。

silentHandle 函数的解释

  1. T U 泛型
    • T:表示成功时的返回类型。这个类型可以是任何类型,具体取决于 fn 函数的返回值类型。
    • U:表示错误类型。它的默认值是 Error,表示当发生错误时,silentHandle 捕获的错误类型将是 Error 类型。
  1. fn: Function
    • 这个参数是一个函数,它的作用是执行异步操作。我们将 fn 函数传入 silentHandle 中,silentHandle 会调用该函数并捕获其可能发生的错误。
  1. ...args: Array<unknown>
    • 这是一个剩余参数,用于传递给 fn 函数的所有额外参数。args 是一个包含所有传入参数的数组。
  1. 返回类型
    • Promise<[U, null] | [null, T]>:表示 silentHandle 函数返回一个 Promise,该 Promise 将解析为一个元组。这个元组有两个元素:
      • 第一个元素是错误信息(类型是 U),如果发生错误,则该元素会包含错误对象。
      • 第二个元素是成功结果(类型是 T),如果操作成功,则该元素会包含返回的结果。
    • 如果操作失败,则返回一个元组 [U, null],如果成功,则返回 [null, T]

异步函数 fetchData 示例:

typescript


复制代码
// 模拟的异步函数,可能会抛出错误
const fetchData = async (id: number): Promise<string> => {
  if (id < 0) {
    throw new Error("Invalid ID"); // 模拟错误
  }
  return `Data for ID ${id}`;
}

使用 silentHandle 捕获错误:

typescript


复制代码
// 调用 silentHandle 捕获 fetchData 的错误
const handleDataFetch = async () => {
  const [error, data] = await silentHandle(fetchData, -1); // 传入一个无效的 ID

  if (error) {
    console.error("Error occurred:", error.message); // 打印错误信息
  } else {
    console.log("Fetched data:", data); // 打印成功返回的数据
  }
}

handleDataFetch(); // 执行函数

解释:

  1. silentHandle(fetchData, -1)
    • silentHandle 被调用并传入 fetchData 和参数 -1。因为我们传入的是一个无效的 ID(-1),fetchData 会抛出一个错误(Error: Invalid ID)。
  1. 错误捕获
    • silentHandle 捕获到错误并返回 [Error, null],其中 Error 对象包含错误信息(Invalid ID)。
  1. 处理返回值
    • 调用 silentHandle 后,[error, data] 会接收到返回值:
      • 如果 error 存在(即发生错误),会进入 if (error) 分支,打印错误信息。
      • 如果没有发生错误,data 会包含成功的结果。

结果输出:

plaintext


复制代码
Error occurred: Invalid ID

中间件挂载封装

创建文件夹目录

import { Express } from 'express'
import express from 'express'

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

  
}

export default initMiddleware

使用

我们的要求就是尽量的让 app的主文件目录 简单干净

中间件的安装

跨域中间件

pnpm add cors

pnpm add @types/cors --save-dev

日志记录插件

pnpm add morgan

pnpm add @types/morgan --save-dev

import { Express } from 'express'
import express from 'express'
import cors from 'cors'
import morgan from 'morgan'
import router from '../router'

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

  // 跨域 以及 日志信息
  app.use(cors())
  app.use(morgan('dev'))
}

export default initMiddleware

控制器 路由操作

也就是将 请求的方法进行抽离出来

创建controller文件目录

userController.ts

mongodb 数据库

mongoose 操作mongodb的库

zod 类型定义的库

pnpm add mongoose zod

pnpm add @types/mongoose --save-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 '../utils/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,
    })
    **/

    const connection = await mongoose.connect(dbUri)

    logger.info('mongodb connect success ~ ')

    return connection
  } catch (error) {
    logger.error('Could not connect to db')
    process.exit(1) // 表示异常连接 强制结束进程
  }
}

export default dbConnect

定义数据模块

import mongoose from 'mongoose'

// mongoose.Document 是 Mongoose 中用于表示文档的基本类型 
// 他包含 操作数据库的一些方法和数据 例如 id save() remove()
export interface UserDocument extends mongoose.Document {
  username: string
  email: string
  password: string
  phone: string
  image: string | null
  createdAt: Date
  updatedAt: Date
  deletedAt: Date
}

const userSchema = new mongoose.Schema(
  {
    username: { type: String, required: true },
    email: { type: String, required: true },
    password: { type: String, required: true },
    phone: { type: String, required: true },
    image: { type: String, default: null },
  },
  {
    timestamps: true, // 添加这个参数 就会 为我们自动生成 createdAt 和 updatedAt 字段
  },
)

// 表示唯一索引 就是不能同是出现 具有相同 account 和 deletedAt 值的两条记录
// 通过复合唯一索引,能够确保 未删除的用户(deletedAt: null)的 account 是唯一的,
// 但允许软删除后的用户使用相同的 account 创建新记录。
userSchema.index({ account: 1, deletedAt: 1 }, { unique: true })

const UserModel = mongoose.model<UserDocument>('User', userSchema)

export default UserModel

唯一性解释:

封装后对数据库的基本操作

参数校验

我们不使用 zod 进行验证

pnpm add express-validator

创建validator中间件 对参数校验 继续 封装

error.back.ts 封装中间件

import express from 'express'
import { ValidationChain, validationResult } from 'express-validator'
import commonRes from '../../utils/commonRes'

export default (validator: ValidationChain[]) => {
  return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
    await Promise.all(validator.map((validate) => validate.run(req)))
    const errors = validationResult(req)
    if (!errors.isEmpty()) {
      return commonRes.error(res, null, errors.array(), 401)
    }

    next()
  }
}

参考校验数据的书写

import { body } from 'express-validator'
import validate from './error.back'

const register = validate([
  body('account')
    .notEmpty()
    .withMessage('用户名不能为空 ~')
    .bail()
    .isLength({ min: 3 })
    .withMessage('用户们长度不能小于3个字符 ~')
    .bail(),
  body('password')
    .notEmpty()
    .withMessage('密码不能为空 ~')
    .bail()
    .isLength({ min: 3, max: 20 })
    .withMessage('密码长度应大于6小于20个字符 ~')
    .bail(),
])

export default {
  register: register,
}

使用

接口定义以及代码调试

这边我们为了 进一步规范数据的格式 我们给 body定义一下类型

(req: Request<{}, {}, RegisterFields['body']>, res: Response)

用户名重复 被异常捕获进行返回

成功代码

完成

以上学习记录笔记为个人整理学习所用,为自用笔记!!!

本文借鉴学习了 juejin.cn/post/706977… 大部分知识内容 参数校验以及接口使用都为个人进行在原作者的基础上进行了修改和理解,如需学习本文知识内容,可异步原文作者学习,尊重原创作者的知识成果!