上一章介绍了前端项目的搭建,那么这一章我们基于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配置完成,根据自己需要新增模块
完成
至此,后台项目基本框架就搭建完成了,后续根据自己需要的功能扩展即可
以上是结合自身开发经历,以及阅读过的其他大神的优秀代码搭建的,有误的地方,还请多多指正,谢谢!!
后续可能会记录一些持续集成相关的内容,有兴趣的童鞋可以关注。