# QidianJS **Repository Path**: qishu-software/qidianjs-template ## Basic Information - **Project Name**: QidianJS - **Description**: QidianJS 是一个基于 nodejs/express 的后端轻量级开发框架。遵循MVC与IoC模式。基于它,您可以快速构建一个轻量级、高性能的服务。 - **Primary Language**: TypeScript - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 1 - **Created**: 2024-05-26 - **Last Updated**: 2024-08-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # QidianJS QidianJS 是一个基于 nodejs/express 的轻量级后端开发框架。遵循MVC与IoC模式。基于它,您可以快速构建一个轻量级、高性能的服务。 > 观前提示:如果您对本文中提到的概念(诸如 DTO、依赖注入、ORM 等)以及使用的依赖包缺乏认知,除了查阅对应的官方文档以外,推荐您浏览编程博主“小满zs”的文章或视频,以便于更加直观与高效的建立初步知识体系: > > - [小满zs【稀土掘金主页】](https://juejin.cn/user/2463384809252397) > - [小满zs【bilibili】node.js教程视频](https://www.bilibili.com/video/BV1cV4y1B7P4/?share_source=copy_web&vd_source=91f64d6c5a76e839c74474e86184bbc0) ## 项目构建 您可以通过 QidianJS 脚手架快速构建您的项目。 - 运行命令 `npm i qidian-cli -g` 安装脚手架 - [脚手架 - 使用手册](https://gitee.com/qishu-software/qidianjs-cli/blob/master/docs/DEVELOP.md) - [脚手架 - 开源地址](https://gitee.com/qishu-software/qidianjs-cli.git) 如过您已经安装 QidianJS 脚手架,则可以通过 `qi-cli create ` 命令快速创建项目。 ## 基本概述 本章将为您讲述 QidianJS 的基础知识,方便您理解与使用本框架。 ### 语言与开发环境 目前,QidianJS 仅支持使用 TypeScript 开发(JavaScript版本处于计划阶段,敬请期待)。 正式启动项目前,请确保您的开发环境符合以下要求: - **PC 端操作系统**: - windows - MacOS - Linux - **Node.js** (建议版本 > 18.16.0,低版本可能存在兼容性问题,请自行评估) - **Git** (最低版本 > 1.7.0,低版本可能无法使用) ### 模块(Module) QidianJS 的模块由[控制器](#控制器controller)(controller)、[服务层](#服务层services)(services)、[对象层](#对象层dto)(dto)三个部分组成,分别对应模块的 API 管理、数据传输对象管理以及接口对应请求的处理与响应逻辑。 如果您已经安装了 [qidian cli](https://gitee.com/qishu-software/qidianjs-cli.git),可通过 `qi-cli add:module ` 命令快速创建模块。 #### 控制器(controller) 基于 [inversify-express-utils](https://www.npmjs.com/package/inversify-express-utils)与 [inversify(依赖注入)](https://doc.inversify.cloud/zh_cn/installation)实现,使用 `@controller()` 装饰器创建模块控制器,并使用 `@httpPost()` / `@httpGet` / `@httpPut` / `@httpDelete` 等装饰器为模块提供请求 API 管理。下面是一个简易的示例: ```typescript // demo.controller.ts import { controller, httpGet as get, httpPost as post } from "inversify-express-utils"; import { DemoServices } from "./demo.services"; import { inject } from 'inversify' import { Request, Response } from "express"; /** * ## Demo 模块控制器 * - 使用 inversify-express-utils 中的 controller 装饰器创建模块 * - 根路径:/demo */ @controller('/demo') export class Demo { constructor( // 此处使用 inversify 中的 inject 装饰器注入了模块的 services 服务层,详情请查看本节第三部分“服务层(services)” @inject(PostServices) private readonly demo: DemoServices ) {} /** * ## 请求示例 * - 请求方式:GET * - 请求路径:/demo/get */ @get('/get') public async get(req: Request, res: Response) { let result = this.demo.get() res.send(result) } /** * ## 请求示例 * - 请求方式:POST * - 请求路径:/demo/post */ @post('/post') public async post(req: Request, res: Response) { let result = await this.demo.post(req.body) res.send(result) } } ``` #### 对象层(dto) 基于 [class-validator](https://www.npmjs.com/package/class-validator) 实现,用于设计与管理请求时携带的数据对象、验证请求数据结构的合法性、以及项目开发过程中的便利性(代码提示)。 有关DTO概念详情,可参考百度百科:[DTO(数据传输对象)](https://baike.baidu.com/item/dto/16016821) 下面是一个简易的示例: ```typescript // demo.dto.ts import { IsEmail, IsNotEmpty, IsInt } from 'class-validator' /** * ## DTO层(数据对象) * - 创建后,可在服务层(services)验证请求数据的合法性 */ export class DemoDto { @IsNotEmpty({ message: '邮箱不能为空' }) @IsEmail({}, { message: '邮箱格式不正确' }) email: string @IsNotEmpty({ message: '密码不能为空' }) password: string @IsNotEmpty({ message: '创建时间不能为空' }) @IsInt({ message: '创建时间格式不正确' }) createdTime: number } ``` #### 服务层(services) 基于 [injectable](https://doc.inversify.cloud/zh_cn/installation)(依赖注入)、[class-transformer](https://www.npmjs.com/package/class-transformer)(对象与类的实例转换)、 [class-validator](https://www.npmjs.com/package/class-validator)(数据验证)实现,是接口对应请求的处理与响应逻辑处理层。简而言之就是接口相关的**业务逻辑代码**。下面是一个简易的示例: ```typescript // demo.services.ts import { injectable } from 'inversify' import { plainToClass } from 'class-transformer' import { validate } from 'class-validator' import { DemoDto } from './demo.dto' /** * ## Demo 逻辑层 */ @injectable() export class DemoServices { constructor() { } // 对应上文控制器中的 get 请求接口 public get() { return 'get success' } /** * ### 对应上文控制器中的 post 请求接口 * - 使用 class-transformer 中的 plainToClass 方法将客户端请求携带的数据转换为数据类实例 * - 使用 class-validato 中的 validate 验证数据合法性 */ public async post(demoData: DemoDto) { let demoDto = plainToClass(DemoDto, demoData) const errors = await validate(demoDto) console.log(errors); return 'post success' } } ``` ### 配置文件(app.config) 通过脚手架创建项目后自带的文件,用于管理项目中注册的[中间件](#中间件middleware)以及关联 container 实例。下面是一个简易的示例: ```typescript // app.config.ts import { InversifyExpressServer } from 'inversify-express-utils' import { Container } from 'inversify' import express from 'express' import cors from 'cors' import { JWT } from './jwt' /** * ## App 配置文件 * @param container 容器,在 main.ts 中引用类时传入 * @param server InversifyExpressServer 实例,在 main.ts 中引用类时传入 * * @middList 放置中间件,该类会自动进行注册 * @relevancyContainer 配置需要关联至 container 实例的对象(暂无自动注册,需要手动配置) */ export class AppConfig { container: Container server: InversifyExpressServer /** 中间件列表 */ middList: any[] = [ express.json(), // json 数据解析 express.urlencoded({ extended: false }), // url-encoded 数据解析 cors(), // 跨域 // 更多中间件... ] constructor(container: Container, server: InversifyExpressServer) { this.container = container this.server = server this.server.setConfig((app: express.Application) => { this.middleware(this.middList, app) this.relevancyContainer(app) }) } /** * ### 注册中间件 * - 根据中间件列表自动循环注册 */ private middleware(middList: any[], app: express.Application) { middList.forEach(item => { app.use(item) }) } /** * ### 关联 container 实例 * @param app container 实例 * - 通过 app.use() 进行对象关联 */ relevancyContainer(app: express.Application) { app.use(this.container.get(JWT).init()) } } ``` ### 容器注入(app.containerInj) 通过脚手架创建项目后自带的文件,用于管理项目中的依赖注入对象。下面是一个简易的示例: ```typescript // app.containerInj.ts import { Container } from 'inversify' import { User } from './modules/user/user.controller' import { UserServices } from './modules/user/user.services' import { Post } from './modules/post/post.controller' import { PostServices } from './modules/post/post.services' import { PrismaClient } from '@prisma/client' import { DB } from './db' import { JWT } from './jwt' /** * ## App 容器注入 * @param container 容器,在 main.ts 中引用类时传入 * @injObjList 中放置需要注入的对象,该类会自动进行注入 * @injPlantList 中放置需要工厂注入的对象,该类会自动进行注入 */ export class AppContainerInj { container: Container /** 注入对象列表 */ injObjList: any[] = [User, UserServices, Post, PostServices, DB, JWT] /** * ### 注入工厂对象列表 * - 元素格式为: {obj: [注入对象], name: 名称 } */ injPlantList: any[] = [ { obj: PrismaClient, name: 'prisma' } ] constructor(container: Container) { this.container = container // 执行注入 this.injection(this.injObjList) this.injectPlant(this.injPlantList) } /** * ### 普通注入 * @param InjObjList 注入对象列表 * - 根据注入对象列表自动循环注入 */ private injection(InjObjList: any[]) { InjObjList.forEach(item => this.container.bind(item).toSelf()) } /** * ### 注入工厂 * @param InjPlantList 注入对象列表 * - 根据注入对象列表自动循环注入 */ private injectPlant(InjPlantList: any[]) { InjPlantList.forEach(item => this.container.bind(item.name).toFactory(() => { return () => { return new item.obj() } })) } } ``` ### 中间件(Middleware) QidianJS 提供了默认中间件对客户端请求进行默认处理,您可以基于他们进行一定程度的修改与完善,让它们更加符合您的业务需求。当然,您也可以根据自己的需求[自定义中间件](#自定义中间件),以实现更多复杂的请求处理。 #### 全局请求处理器(GlobalReqProc) 接收到客户端请求时触发,并清晰的打印出请求日志。中间件源码如下: ```typescript // globalReq.proc.ts import colors from 'colors' import { time } from "../utils/day.utc"; import { Request, Response } from "express"; /** * ### 全局请求处理器 */ export const globalReqProcMidd = (req: Request, res: Response, next: any) => { // 打印请求日志 console.log(colors.bold(`\n>>>>>>> ${req.ip} 接口请求:`).bgBlue); console.log(`\n======${time().format('YYYY-MM-DD HH:mm:ss')}======\n`); console.log(`请求接口: ${req.path}`); console.log(`请求方式: ${req.method}`); console.log(`\nQuery 参数:`); console.log(req.query); console.log(`\nBody 参数:`); console.log(req.body); console.log(`\nParams 参数:`); console.log(req.params); console.log(`\n======${time().format('YYYY-MM-DD HH:mm:ss')}======\n`); next() } ``` #### 全局响应处理器(GlobalResProc) 接收到客户端请求时,在 res(Response)中添加了 cc 函数,可在请求成功/失败响应时调用,自动向客户端响应对应数据并清晰的打印响应日志。中间件源码如下: ```typescript // globalRes.proc.ts import colors from 'colors' import { time } from "../utils/day.utc"; import { Request, Response } from "express"; /** * ### 全局响应处理器 * 用于快捷处理请求成功/失败时的响应 * - 使用方式:res.cc(message, data, status) * * @param message : 响应信息 * @param data : 响应数据 * @param status : 响应状态码 * @returns */ export const globalResProcMidd = (req: Request, res: Response, next: any) => { res.cc = (message: object | string, data: object | string, status: number = 400) => { // 打印响应日志 console.log(status < 202 ? colors.bold(`\n>>>>>>> 响应请求${status}:`).bgGreen : status < 210 ? colors.bold(`\n>>>>>>> 响应请求${status}:`).bgYellow : colors.bold(`\n 响应请求${status}:`).bgRed); console.log(`\n====== ${time().format('YYYY-MM-DD HH:mm:ss')} ======\n`); console.log(`状态码 (status): ${status}`); console.log(`信息 (message): ${message}`); console.log(`\n响应数据 (data):`); console.log(data); console.log(`\n====== ${time().format('YYYY-MM-DD HH:mm:ss')} ======\n`); // 向前端返回成功/失败的值 res.send({ status, message: message, data: data, }) } next() } ``` ##### 使用示例 ```typescript // xxx.controller.ts @get('/get') public async get(req: Request, res: Response) { let result = this.myPost.get() res.cc('ok', {data: result}, 200) } ``` #### 全局错误处理器(GlobalErrProc) 用于拦截与处理请求过程中产生的意料中/意料外的报错并响应给客户端,以确保服务不会崩溃。每次拦截,后台都会清晰的打印错误日志,便于排查错误。中间件源码如下: ```typescript // globalErr.proc.ts import colors from 'colors' import { Request, Response } from "express"; import { time } from '../utils/day.utc'; /** * ### 全局错误处理器 * - 用于处理错误情况,防止请求错误带来的服务器崩溃 */ export const globalErrProcMidd = (err: Error, req: Request, res: Response, next: any) => { // 已知错误 if (err.message === 'jwt expired') return cc(res, { err: '登录已过期,请重新登录', message: err.message, stack: err.stack }, 401, 1) // 如上所示,您可以添加更多已知错误... // 统一捕获未知错误 cc(res, { err: '请求发生错误,请稍后重试: ', message: err.message, stack: err.stack }) } const cc = (res: Response, err: any, status: number = 402, prompt: number = 0) => { // 打印响应日志 console.log(colors.bold(`\n 全局错误处理器${status}:`).bgRed); console.log(`\n====== ${time().format('YYYY-MM-DD HH:mm:ss')} ======\n`); console.log(`状态码 (status): ${status}`); console.log(`信息 (message): ${err.err}`); console.log(`\n响应数据 (data):`); console.log(err.message); console.log(`\n====== ${time().format('YYYY-MM-DD HH:mm:ss')} ======\n`); return res.send({ status: status, prompt: prompt, message: err instanceof Error ? err.message : err }) } ``` #### 自定义中间件 功能实现中,敬请期待... ## 跨域支持(Cors) 功能实现中,敬请期待... ## ORM 数据库对象关系映射支持(Prisma) Prisma 是一个现代化的数据库工具套件,用于简化和改进应用程序与数据库之间的交互。它提供了一个类型安全的查询构建器和一个强大的 ORM(对象关系映射)层,使开发人员能够以声明性的方式操作数据库(支持多种主流数据库,包括 PostgreSQL、MySQL 和 SQLite)。[Prisma 官网:https://www.prisma.io/](https://www.prisma.io/) QidianJS 内置了对 Prisma 的支持,您可以在创建项目时选择添加 Prisma 模块,也可以在已有的项目中通过脚手架 `qi-cli add:prisma` 命令创建 Prisma 模块。随后可按照如下步骤使用: ### 配置数据库基础信息 1. 在项目的根目录中打开 `.env` 文件,对 `DATABASE_URL` 参数进行如下操作 ``` DATABASE_URL="数据库名称://账号:密码@主机地址:端口号/数据库名称" # 示例如下 DATABASE_URL="mysql://root:admin@localhost:3306/database_url" ``` 2. 完成配置后,输入 `prisma init --datasource-provider <数据库名称,如:mysql>` 即可完成初始化并连接数据库。 3. 在项目根目录中找到 `/prisma/schema.prisma` 文件,完成数据表模型设计,示例如下: ```typescript model Post { id Int @id @default(autoincrement()) //id 整数 自增 title String // 字符串类型 publish Boolean @default(false) // 布尔值默认 false author User @relation(fields: [authorId], references: [id]) // 创建关联关系 authorId Int ... } model User { id Int @id @default(autoincrement()) name String email String @unique posts Post[] ... } ``` 4. 运行 `prisma migrate dev` 命令,自动创建数据库对应的表结构(请注意是否覆盖当前库里面的所有内容) 5. 更多操作请查阅官网文档:[Prisma Documentation](https://www.prisma.io/docs) ### 在项目中使用 进入任意一个模块的 xx.services.ts 文件。 1. 注入 PrismaClient ```typescript // xx.services.ts ... import { PrismaClient } from '@prisma/client' @injectable() export class XxServices { prisma: PrismaClient constructor( ... @inject('prisma') PrismaClient: () => PrismaClient ) { this.prisma = PrismaClient() } ... } ``` 2. 增删改查操作 ```typescript // xx.services.ts // 1.新增 const { name, email } = {name: '张三', email: '123456@qq.com'} await this.prisma.user.create({ data: { name, email, // 同步新增关联表的数据 posts: { create: { title: '标题', publish: true }, } } }) // 2.修改 const { id, name, email } = {id: 123, name: '李四', email: '234567@qq.com'} await prisma.user.update({ where: { id: id }, data: { name, email } }) // 3.单个查找 await prisma.user.findMany({ include: { posts: true } }) res.send(data) }) // 4.关联查找 await prisma.user.findMany({ where: { id: Number(req.params.id) } }) // 5.删除 const id = 123 const data = await prisma.user.delete({ where: { id: id, }, }) // 更多操作请查阅官网文档:https://www.prisma.io/docs ``` ## 鉴权支持(Jwt) JWT(JSON Web Token)是一种开放的标准(RFC 7519),用于在网络应用间传递信息的一种方式。它是一种基于JSON的安全令牌,用于在客户端和服务器之间传输信息。 [jwt.io](https://link.juejin.cn/?target=https%3A%2F%2Fjwt.io%2F) QidianJS 内置了对 Jwt 的支持,您可以在创建项目时选择添加 Jwt 模块,也可以在已有的项目中通过脚手架 `qi-cli add:jwt` 命令创建 Jwt模块。随后可按照如下步骤使用: ### 设置密钥 进入 `/src/jwt/index.ts` 中,将 `secret` 属性替换为自己的密钥(请勿将密钥泄露给他人) ```typescript @injectable() export class JWT { // 将此处字符串替换为自己的密钥 private secret: string = 'you key'; private jwtOpiotns = { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: this.secret } constructor() { this.strategy() } ... } ``` ### 生成加密 Token 进入任意一个模块的 xx.services.ts 文件。 1. 注入 Jwt ```typescript // xx.services.ts ... import { JWT } from '../../jwt' @injectable() export class XxServices { constructor( ... @inject(JWT) private readonly JWT: JWT, ) { } ... } ``` 2. 生成加密 token ```typescript // 括号内是写入密文的用户信息(用于身份识别) this.JWT.createToken({name: '张三', email: '123456@qq.com', ...}) ``` ### 为接口添加身份验证 进入任意一个模块的 xx.controller.ts 文件。随后进行如下操作: ```typescript // xx.controller.ts ... import { JWT } from "../../jwt"; @controller('/user') export class User { ... // 在此处添加 JWT.middleware(),即可自动对该接口请求的 token 进行验证,并拦截验证失败的请求 @get('/test', JWT.middleware()) public async get(req: Request, res: Response) { ... } ... } ``` ## 代码规范(EsLint/Prettier) 功能实现中,敬请期待...