Clover Admin

借助强大的Nest快速、轻巧地构建更优雅的Node服务端应用程序。
Alternatives To Clover Admin
Project NameStarsDownloadsRepos Using ThisPackages Using ThisMost Recent CommitTotal ReleasesLatest ReleaseOpen IssuesLicenseLanguage
Express60,9871,122,97879,5042 days ago299April 29, 2022172mitJavaScript
Fast, unopinionated, minimalist web framework for node.
Socket.io58,335228,9999,6152 days ago144September 02, 2022121mitTypeScript
Realtime application framework (Node.JS server)
Astro30,744151466 hours ago391September 22, 2022169otherTypeScript
The all-in-one web framework designed for speed. ⭐️ Star to support our work!
Parse Server20,2641,140896 hours ago220September 20, 2022394apache-2.0JavaScript
Parse Server for Node.js / Express
Ws19,896393,32912,9539 days ago154September 22, 20225mitJavaScript
Simple to use, blazing fast and thoroughly tested WebSocket client and server for Node.js
Apollo Server13,3675,3261,153a day ago305August 26, 202242mitTypeScript
🌍  Spec-compliant and production ready JavaScript GraphQL server that lets you develop in a schema-first way. Built for Express, Connect, Hapi, Koa, and more.
Volkswagen12,65555a month ago15October 16, 201561mitJavaScript
:see_no_evil: Volkswagen detects when your tests are being run in a CI server, and makes them pass.
Zero5,832736 months ago169May 11, 202084apache-2.0JavaScript
Zero is a web server to simplify web development.
Metrics Server4,740614 hours ago41February 22, 202238apache-2.0Go
Scalable and efficient source of container resource metrics for Kubernetes built-in autoscaling pipelines.
Backendlore4,542
3 years ago
How I write backends
Alternatives To Clover Admin
Select To Compare


Alternative Project Comparisons
Readme

Nest Logo

中文官网

Description

CloverAdmin是一个Nest后台后端解决方案。它借助强大的Nest和社区快速、轻巧地构建更优雅的Node服务端应用程序。目前已集成权限验证、使用Typeorm的MySql与SqlServer方案、全局常量配置、URI多版本控制、数据验证、文件上传、静态服务、日志等功能。它提供了一个完整的RestFul风格的接口示例。

node:14.20.1 pnpm:7.13.3

Installation

如果事先未安装Nest, 则需要先安装Nest

$ pnpm i -g @nestjs/cli

安装项目依赖

$ pnpm install

Running the app

# development
$ pnpm run start

# watch mode
$ pnpm run start:dev

# production mode
$ pnpm run start:prod

Start

建设中。。。

目录结构

跨域

仅需在mian.ts中添加如下配置:

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    cors: true, // 跨域
});

路由

Nest路由由如下几部分组成:

域名(或 IP:端口) /全局路由前缀(如果有)/api版本(如果有)/controller层标识/路由层标识

一个路由的行为,Nest对其进行了划分:

  • controller主要负责接收接口传来的参数和返回数据
  • service主要负责接口逻辑处理,如数据库操作
  • pipe管道则主要负责对接口传来的数据进行验证和转换
  • interceptor拦截器则主要是为接口处理绑定额外的逻辑,比如在接口返回值前进行额外处理(对日期进行格式化等)
  • guard守卫则是为某些需要特殊身份才能访问的接口绑定鉴权行为

全局路由前缀

仅需在main.ts中添加如下配置:

app.setGlobalPrefix('clover-api'); // 全局路由前缀

URI版本控制

在main.ts中添加如下配置:

app.enableVersioning({
    // URI版本控制
    type: VersioningType.URI,
    defaultVersion: '1', // 为所有URI指定默认版本
}); // 默认版本为v1, 即api/v1/xxx

如果需要手动指定控制器版本, 参照如下:

@Controller({
  version: '2',
})
export class CatsControllerV1 {
  @Get('cats')
  findAll(): string {
    return 'This action returns all cats for version 1';
  }
}

同样, 路由也是如此:

@Controller()
export class CatsController {
  @Version('1')
  @Get('cats')
  findAllV1(): string {
    return 'This action returns all cats for version 1';
  }

  @Version('2')
  @Get('cats')
  findAllV2(): string {
    return 'This action returns all cats for version 2';
  }
}

version也可以是数组, 比如: ['1', '2'],因为有些特殊路由需要在多个版本间保持一致行为,这样做可以减少重复代码。

更多内容见: API多版本

配置

全局常量配置存放在src/config目录下, 分为三种环境(开发环境、生产环境、测试环境),依据环境不同加载不同env文件(不建议创建多环境公用的env文件!!)

使用示例(以development环境为例):

  1. 在development.env中添加配置:

    APP_PORT=8888
    
  2. 在config下创建app.config.ts(后续根据不同模块,创建不同的config.ts)

    import { registerAs } from '@nestjs/config';
    
    export default registerAs('app', () => ({
      port: parseInt(process.env.APP_PORT, 10) || 3000,
    }));
    

    推荐使用这种"带命名空间"的配置对象, 不仅增强可读性, 也为环境变量划分了模块, 而且由于是ts, 可以为访问环境变量添加逻辑处理(例如本例就为port添加了int类型转换和默认值)。

  3. app.module.ts中引入appConfig:

import appConfig from './config/app.config';
@Module({
  imports: [
    /** 导入config模块 */
    ConfigModule.forRoot({
      envFilePath: `./src/config/${ConfigEnum[process.env?.NODE_ENV ?? 'development']}.env`, // 自定义env文件路径
      load: [appConfig], // 自定义配置文件
      isGlobal: true, // 设为全局模块
    }),
  ]
})
  1. app.controller.ts中访问变量

    @Controller()
    export class AppController {
      constructor(
        private configService: ConfigService,
      ) {}
    
      @Get()
      getHello(): string {
        console.log(this.configService.get<string>('app.port')); // 读取env变量示例
      }
    }
    

鉴权

登录注册相关接口在src/core/auth模块下,

CloverAdmin将验证phone和password字段并检验是否与数据库记录匹配来生成token, 如果需要使用其他字段则在src/core/auth/strategies/local.strategy.ts配置:

usernameField: 'phone', // 改为你的字段
passwordField: 'password', // 改为你的字段

其中验证数据库记录的逻辑是auth.service.ts的validateUser方法, 此方法将依据phone查询数据库记录, 并检查密码是否正确, 最终返回去除password字段的user实体。

jwt中存储的user信息可自行在src/core/auth/strategies/kwt.strategy.ts中配置:

async validate(payload: any) {
    console.log('jwt', payload);
    // 此处可以执行其他逻辑操作: 如查询数据以获取完整的user对象
    return { id: payload.sub, phone: payload.phone, roles: payload.role 	}; // 即user的id, phone, role
}

jwt密钥在src/core/auth/constants.ts中。

jwt生效时间在src/core/auth/auth.module.ts中配置:

@Module({
    imports: [
        JwtModule.register({
      		secret: jwtConstants.secret,
      		signOptions: { expiresIn: `${60 * 60}s` }, // 过期时间
    	}),
    ]
})

为登录路由使用管道: @UseGuards(LocalAuthGuard), 其他需要验证token的路由使用管道: @UseGuards(JwtAuthGuard),在此之后可直接获取到jwt中的user信息:

@Post()
@UseGuards(AuthGuard('jwt'))
async createPost(
    @Req() req: any,
): Promise<Result> {
    createInput.user = req.user; // 获取user
}

验证token身份之后, 验证用户权限, 权限Role在src/common/enums/role.enum.ts下

首先为路由或控制器使用管道: @UseGuards(RolesGuard), 再为路由或控制器配置权限@Roles(Role.ADMIN)

ps:在nest中, 不仅可以为路由设置管道, 也可以为controller、模块、甚至全局设置管道。

数据库

采用Typeorm 文档地址: https://typeorm.bootcss.com/

Nest文档:https://docs.nestjs.cn/8/techniques?id=%e6%95%b0%e6%8d%ae%e5%ba%93

1. 配置连接

ormconfig.json一般放在项目根目录下, 且mysql与sqlserver配置略有不同, 如下

mysql:

// ormconfig.json
{
  "type": "mysql",
  "host": "数据库ip地址",
  "port": 数据库端口号,
  "username": "xxxx",
  "password": "xxxxxx",
  "database": "数据库名",
  "entities": ["dist/feature/**/entities/*.entity{.ts,.js}"],
  "synchronize": true,
  "autoLoadEntities": true
}

sqlserver:

// ormconfig.json
{
  "type": "mssql",
  "host": "数据库ip地址",
  "port": 数据库端口号,
  "username": "xxxx",
  "password": "xxxxxx",
  "database": "数据库名",
  "entities": ["dist/feature/**/entities/*.entity{.ts,.js}"],
  "synchronize": true,
  "autoLoadEntities": true,
  "options": { # mssql如果出现ssl问题, 则需要添加此options
    "encrypt": false,
    "trustServerCertificate": true,
    "cryptoCredentialsDetails": {
      "minVersion": "TLSv1"
    }
  }
}

注意:

  1. Typeorm生成的sql语句可能不支持SqlServer2008,因此可能需要使用QueryBuilder来拼接sql。
  2. 同时使用relations, skip,take可能会出现TypeError: Cannot read property 'databaseName' of undefined", 只需再加一个order即可以解决。

2. 在app.module.ts中读取配置

@Module({
    imports: [
        TypeOrmModule.forRoot()
    ],
    // ...
})
export class AppModule {
  // 注入TypeORM 的Connection对象
  constructor(private readonly connection: Connection) {}
}

3. 在其他模块中注册模块中实体

@Module({
    imports: [
        TypeOrmModule.forFeature([User]) // 注册User实体
    ],
    // ...
    exports: [TypeOrmModule], // 导出模块, 在导入TypeOrmModule.forFeature 的模块之外使用存储库
})

接着在user.service.ts中就可以使用User存储库(Repository)以及Connection对象,存储库可以胜任所有数据库操作,而Connection对象用于事务。

export class UserService {
    constructor(
    	@InjectRepository(User)
    	private usersRepository: Repository<User>,
    	private connection: Connection,
  	) {}
}

使用示例:

// 查询所有user
findAll(): Promise<Array<User>> {
    return this.usersRepository.find();
}

更多typeorm操作见typeorm部分, 或typeorm官方文档。

校验

class-validator

Nest文档:https://docs.nestjs.cn/8/techniques?id=%e9%aa%8c%e8%af%81

使用示例:

export class User {
    @IsNotEmpty() // 不为空
  	firstName: string;
    @IsString() // 字符串
  	firstName: string;
    @IsMobilePhone('zh-CN', { // 大陆手机号
    	message: '手机号格式不正确', // 自定义验证失败信息
  	})
  	phone: string;
    @IsEnum(Role) // 验证枚举
  	@ValidateIf((o) => o?.role) // 条件验证, 只有当role存在时才验证
  	role?: Role;
}

注意:

CloverAdmin在main.ts中启用了全局验证, 配置如下:

app.useGlobalPipes(
    new ValidationPipe({
      disableErrorMessages: false, // 禁用详细错误信息(开启后不会再返回详细的错误信息)
      whitelist: true, // 自动去除非白名单内的属性(即验证类中没有声明的属性)
      forbidNonWhitelisted: false, // 开启后, 如果出现非白名单内的属性, 将不再是自动去除而是抛出错误
      transform: false, // 全局开启转换器(默认关闭, 多数情况是在controller层开启)---开启后, '14' => 14
    }),
 ); // 全局启用验证

在设置whitelist: true后,前端传递的数据如果没有在entity或dto中设置验证, 那么这个数据将被忽略。

序列化

对接口出入参作转换 官方文档: https://docs.nestjs.cn/9/techniques?id=%e5%ba%8f%e5%88%97%e5%8c%96%ef%bc%88serialization%ef%bc%89

typestack/class-transformer

排除属性

  • 在Entity中为要排除的属性添加注解@Exclude()
    import { Exclude } from 'class-transformer';
    export class UserEntity {
      @Exclude()
      password: string;
    }
    
  • 在controller中使用@UseInterceptors(ClassSerializerInterceptor)
    @UseInterceptors(ClassSerializerInterceptor)
    @Get()
    findOne(): UserEntity{}
    

公开属性

这类似于计算属性(或者说, getter函数)

  • 使用注解@Expose()
    @Expose()
    get fullName(): string {
      return `${this.firstName} ${this.lastName}`;
    }
    

转换

对数据进行转换, 常常需要为接口出入参做数据转换

  • 使用注解@Transform()
    // 比如在查询时希望展示role的name而不是整个对象
    @Transform(role => role.name)
    role: RoleEntity;
    

传递选项

ClassSerializerInterceptor的默认行为可以被覆盖,只需要传入一个options对象

// 自动排除了所有以_前缀开头的属性
@SerializeOptions({
  excludePrefixes: ['_'],
})
@Get()
findOne(): UserEntity {
  return {};
}

静态服务

在app.module.ts中添加配置:

@Module({
    imports: [
        ServeStaticModule.forRoot({
      		// 配置静态服务目录---访问: http://localhost:3000/client目录内/xxx.png
      		rootPath: join(__dirname, '..', 'client'),
    	}),
    ]
})

配置完成后, 所有需要静态访问的静态资源文件均可放在根目录的client(没有则需创建)目录下。

文件上传

文件上传后将会保存在client/fileUpload目录下, 也可以在file.module.ts中更改destination进行重新设置。

文件上传有一个专门的模块, 在src/feature/file下, 此功能基于multer实现, 相关的所有配置均在file.module.ts下。

如果在上传文件同时, 希望一同传递其他参数, 请在src/feature/file/dto中进行设置。

定时任务

官方文档: https://docs.nestjs.cn/9/techniques?id=%e5%ae%9a%e6%97%b6%e4%bb%bb%e5%8a%a1

定时任务模块位于: src/core/task

创建一个定时任务需要使用注解@Cron()

@Cron('45 * * * * *')代表每分钟执行一次,在第45秒执行, 参数遵从如下规则:

* * * * * *
| | | | | |
| | | | | day of week
| | | | month
| | | day of month
| | hour
| minute
second (optional)

一些常用的计时模式示例如下:

* * * * * * 每秒

45 * * * * * 每分钟第 45 秒

_ 10 _ * * * 每小时,从第 10 分钟开始

0 _/30 9-17 _ * * 上午 9 点到下午 5 点之间每 30 分钟

0 30 11 * * 1-5 周一至周五上午 11:30

@nestjs/schedule包的内置CronExpression对象也提供一些常用枚举, 如CronExpression.EVERY_30_SECONDS 每分钟执行一次,在第30秒执行

使用注解@Interval(10000)声明像js中setInterval()一样的任务

使用注解@Timeout(5000)声明延时任务, 就像js中 setTimeout()一样

日志

CloverAdmin在Nest默认日志系统基础上进行一些扩展(你可以在core/logger模块看到详细代码),并提供了两种日志记录实现,分别是中间件日志监控(只记录请求信息)和拦截器日志监控(记录请求信息和部分响应信息,默认已启用此方案)。

MyLogger模块会在项目根目录下创建logs文件夹,并会将日志输出到logs/info.log和logs/err.log文件中。

要使用MyLogger模块首先需要全局启用MyLogger模块,在app.module.ts中执行导入:

@Module({
    imports: [
        LoggerModule
    ]
})

接着在main.ts中全局启用MyLogger:

const app = await NestFactory.create(AppModule, {
    bufferLogs: true,
  });
app.useLogger(new MyLogger());

现在你可以在任何地方注入MyLogger并调用其中的日志方法来输出日志,像这样:

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  /** 注入日志服务 */
  constructor(
    private readonly myLogger: MyLogger,
  ) {
    // 设置日志上下文
    this.myLogger.setContext(LoggerMiddleware.name); 
  }
  use(req: Request, res: Response, next: NextFunction) {
     // 记录请求日志
    this.myLogger.log(`Request... ${req.url} ${req.method}
    body: ${JSON.stringify(req.body)}
    query: ${JSON.stringify(req.query)}
    user-agent: ${req.headers['user-agent']}`);
    next();
  }
}

这个示例中,我们在中间件中注入了MyLogger,并依据其log方法输出日志。

在每一个controller中注入MyLogger输出请求信息十分繁琐,所以我们借助中间件和拦截器提供了两种记录请求日志的方案:

如果你希望使用中间件日志监控,则需要在app.module.ts中增加如下配置:

@Module({...})
export class AppModule implements NestModule {
  // ...

  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware) // 启用日志中间件
      // .exclude({ path: 'cats', method: RequestMethod.GET }) // 排除指定路由, 可指定多个规则
      .forRoutes({ path: '*', method: RequestMethod.ALL }); // 匹配所有路由下的所有请求方法, 参数亦可是多个Controller
  }
}

如果你希望使用拦截器日志监控,我们建议你将日志拦截器设为全局拦截器,在main.ts中增加:

app.useGlobalInterceptors(new LoggerInterceptor(app.get(MyLogger)));

如果你仅仅希望在控制台输出请求的简单信息,那么仅需直接在main.ts使用函数式日志中间件:

app.use(logger);

另外,我们也可以控制Nest默认日志在控制台输出哪些级别的日志,在main.ts中:

const app = await NestFactory.create(AppModule, {
    logger: ['error', 'warn', 'debug', 'log', 'verbose'], // 输出的日志级别
  });

TypeORM

  • 实体

    实体推荐在相应模块目录下, 创建entities目录, 并创建实体, 命名方式推荐为: xxx.entity.ts

    实体需加@entity注解

    @Entity({
      name: 'questions', // 存储进数据库时的表名
      orderBy: { // find等查询方法返回数据时的默认排序方式
        id: 'ASC', // 依据id正序排序
      },
    })
    export class Question {}
    

    建议为实体创建构造函数

    export class User {
      // 构造函数, 用户构建实体
      constructor(partial: Partial<User>) {
        Object.assign(this, partial);
      }
    }
    

    实体成员在运行时将被创建为数据库表的列, 相关注解如下:

    • @PrimaryGeneratedColumn() 自增主键
    • @Column 普通列
    • @CreateDateColumn 特殊列,自动为实体插入日期。无需设置此列,该值将自动设置
    • @UpdateDateColumn 特殊列,在每次调用实体管理器或存储库的save时,自动更新实体日期。无需设置此列,该值将自动设置。
    • @VersionColumn 特殊列,在每次调用实体管理器或存储库的save时自动增长实体版本(增量编号)。无需设置此列,该值将自动设置。

    每个Column都有额外的options可配置, 常用如下:

    @Column({
        type: 'varchar', // 列类型
        name: 'phone', // 数据库表中的列名(默认为属性名)
        length: 16, // 列长度
        nullable: false, // 列可否为空(默认false)
        select: true, // 查询时是否隐藏此列,设为false则列数据不会显示标准查询(默认true)
        primary: false, // 标记为主列(作用等同于@PrimaryColumn)
        unique: false, // 将列标记为唯一列(创建唯一约束)
        comment: '手机号', // 数据库列备注
    })
    

    实体间的关系也是依据注解实现, 见关系 | TypeORM 中文文档 | TypeORM 中文网 (bootcss.com)

  • 查询所有用户

    this.usersRepository.find();
    
  • 查询指定id的用户

    this.usersRepository.findOneBy({
          id,
    });
    
  • 查询指定学生某张试卷的成绩

    this.gradeRecordRepository.findOneBy({
          examPaper: { id: submitScore.examPaperId },
          user: { id: user.id },
    });
    
  • 查询指定学生的所有错题(这里错题记录和题目是两个实体, 他们之间关系为多对一)

    this.wrongQuestionRecordRepository.find({
          where: { user: { id: user.id } },
          relations: ['question'], // 开启查询级联, 同时将对应题目信息也返回
    });
    
  • 查询指定id的题目信息(题目信息需要包含一个关键字实体, 他们之间关系为一对多)

    this.questionsRepository.findOne({
          where: {
            id,
            actionRecords: {
              mark: Mark.NORMAL,
            },
          },
          relations: [
            'words',
          ],
    });
    
  • 高级查询

    this.examPaperRepository
          .createQueryBuilder('exam_paper') // 创建queryBuilder
          .leftJoinAndSelect('exam_paper.scorePapers', 'score_paper') // 左连表, 第一个参数为要加载的关系, 第二个字段是为此关系的表分配的别名
          .leftJoinAndSelect('exam_paper.creator', 'creator') // 可以左连任意多个表
          .leftJoinAndSelect('exam_paper.modifier', 'modifier')
          .leftJoinAndSelect('score_paper.question', 'question')
          .leftJoinAndSelect('question.selectQuestion', 'select_question')
          .leftJoinAndSelect('question.judgmentQuestion', 'judgement_question')
          .leftJoinAndSelect('question.narrateQuestion', 'narrate_question')
          .where('exam_paper.id = :id', { id }) // 查询条件
          .andWhere('exam_paper.actionRecords.mark = :mark', { mark: Mark.NORMAL }) // and 查询条件, 另一种是 or
          .select([
            // 自定义返回字段
            'exam_paper.id',
            'exam_paper.name',
            'exam_paper.description',
            'exam_paper.type',
            'exam_paper.status',
            'exam_paper.actionRecords.updateTime',
            'score_paper.id',
            'score_paper.score',
            'question.id',
            'select_question.id',
            'judgement_question.id',
            'narrate_question.id',
            'creator.id',
            'creator.username',
            'modifier.id',
            'modifier.username',
          ])
          .getOne();
    
  • 在创建实体Entity时, 如果为password字段设置了select:false

    @Column({select:false})
    password: string;
    

    那么一般的查询则不会获取到password字段, 需要使用addSelect:

    this.usersRepository
          .createQueryBuilder('user')
          .addSelect('user.password') // 增加查询字段(密码)
          .where('user.phone = :phone', { phone })
          .getOne();
    
  • 更新指定id的用户新

    this.usersRepository.update(id, user);
    
  • 创建一个新用户

    this.usersRepository.save(user);
    
  • 移除指定id的用户

    this.usersRepository.delete(id);
    
  • 执行一个事务

    await this.connection.transaction(async (manager) => {
          // 想要在事务运行的所有操作都必须在这个回调函数里
          /**
           * 我们不建议在循环中使用await,而是使用Promise.all(),让异步并发执行以提高效率
           */
          const createTasks = [];
          for (const user of users) {
            createTasks.push(manager.save(new User(user)));
          }
          await Promise.all(createTasks);
    });
    

License

Nest is MIT licensed.

Popular Nodejs Projects
Popular Server Projects
Popular Runtime Environments Categories
Related Searches

Get A Weekly Email With Trending Projects For These Categories
No Spam. Unsubscribe easily at any time.
Typescript
Node
Server
Mysql
Restful
Admin
Nest
Mssql