项目初始化
*# 初始化*
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
函数的解释
T
和U
泛型:
-
T
:表示成功时的返回类型。这个类型可以是任何类型,具体取决于fn
函数的返回值类型。U
:表示错误类型。它的默认值是Error
,表示当发生错误时,silentHandle
捕获的错误类型将是Error
类型。
fn: Function
:
-
- 这个参数是一个函数,它的作用是执行异步操作。我们将
fn
函数传入silentHandle
中,silentHandle
会调用该函数并捕获其可能发生的错误。
- 这个参数是一个函数,它的作用是执行异步操作。我们将
...args: Array<unknown>
:
-
- 这是一个剩余参数,用于传递给
fn
函数的所有额外参数。args
是一个包含所有传入参数的数组。
- 这是一个剩余参数,用于传递给
- 返回类型:
-
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(); // 执行函数
解释:
silentHandle(fetchData, -1)
:
-
silentHandle
被调用并传入fetchData
和参数-1
。因为我们传入的是一个无效的 ID(-1
),fetchData
会抛出一个错误(Error: Invalid ID
)。
- 错误捕获:
-
silentHandle
捕获到错误并返回[Error, null]
,其中Error
对象包含错误信息(Invalid ID
)。
- 处理返回值:
-
- 调用
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… 大部分知识内容 参数校验以及接口使用都为个人进行在原作者的基础上进行了修改和理解,如需学习本文知识内容,可异步原文作者学习,尊重原创作者的知识成果!