MVC

Nest 使用 Express 库,因此有关 Express 中的 MVC(模型 - 视图 - 控制器)模式的每个教程都与 Nest 相关。首先,我们来克隆一个 Nest starter 项目:

  1. $ git clone https://github.com/nestjs/typescript-starter.git project
  2. $ cd project
  3. $ npm install
  4. $ npm run start

为了创建一个简单的MVC应用程序,我们必须安装一个模板引擎:

  1. $ npm install --save jade

我选择了,jade因为它是目前最受欢迎的引擎,但个人而言,我更喜欢Mustache。安装过程完成后,我们需要使用以下代码配置快速实例:

main.ts

  1. import * as express from 'express';
  2. import * as path from 'path';
  3. import { NestFactory } from '@nestjs/core';
  4. import { ApplicationModule } from './app.module';
  5. async function bootstrap() {
  6. const app = await NestFactory.create(ApplicationModule);
  7. app.use(express.static(path.join(__dirname, 'public')));
  8. app.set('views', __dirname + '/views');
  9. app.set('view engine', 'jade');
  10. await app.listen(3000);
  11. }
  12. bootstrap();

我们明确表示该 public 目录将用于存储静态文件, views 将包含模板,并且 jade 应使用模板引擎来呈现 HTML 输出。

现在,让我们在该文件夹内创建一个 views 目录和一个 index.jade 模板。在模板内部,我们要打印一张 message 从控制器传来的信息:

index.jade

  1. html
  2. head
  3. body
  4. p= message

然后, 打开 app.controller 文件, 并用下面的代码替换 root() 方法:

app.controller.ts

  1. @Get()
  2. root(@Res() res) {
  3. res.render('index', { message: 'Hello world!' });
  4. }

事实上,当 Nest 检测到 @Res() 装饰器时,它会注入 response 对象。在这里了解更多关于它的能力。

就这样。在应用程序运行时,打开浏览器访问 http://localhost:3000/ 你应该看到这个 Hello world! 消息。

SQL

为了减少开始与数据库进行连接所需的样板, Nest 提供了随时可用的 @nestjs/typeorm 软件包。我们选择了 TypeORM, 因为它绝对是 Node.js 中可用的最成熟的对象关系映射器 (ORM)。由于它是用TypeScript编写的,所以它在Nest框架下运行得非常好。

首先,我们需要安装所有必需的依赖关系:

  1. $ npm install --save @nestjs/typeorm typeorm mysql

?> 在本章中,我们将使用MySQL数据库,但TypeORM提供了许多不同的支持,如PostgreSQL,SQLite甚至MongoDB(NoSQL)。

一旦安装完成,我们可以将其 TypeOrmModule 导入到根目录中 ApplicationModule 。

app.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { TypeOrmModule } from '@nestjs/typeorm';
  3. @Module({
  4. imports: [
  5. TypeOrmModule.forRoot({
  6. type: 'mysql',
  7. host: 'localhost',
  8. port: 3306,
  9. username: 'root',
  10. password: 'root',
  11. database: 'test',
  12. entities: [__dirname + '/../**/*.entity{.ts,.js}'],
  13. synchronize: true,
  14. }),
  15. ],
  16. })
  17. export class ApplicationModule {}

forRoot() 方法接受与 TypeORM 包中的 createConnection() 相同的配置对象。此外, 我们可以在项目根目录中创建一个 ormconfig.json 文件, 而不是将任何内容传递给它。

ormconfig.json

  1. {
  2. "type": "mysql",
  3. "host": "localhost",
  4. "port": 3306,
  5. "username": "root",
  6. "password": "root",
  7. "database": "test",
  8. "entities": ["src/**/**.entity{.ts,.js}"],
  9. "synchronize": true
  10. }

现在我们可以简单地将圆括号留空:

app.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { TypeOrmModule } from '@nestjs/typeorm';
  3. @Module({
  4. imports: [TypeOrmModule.forRoot()],
  5. })
  6. export class ApplicationModule {}

之后,Connection 和 EntityManager 将可用于注入整个项目(无需导入任何其他模块),例如以这种方式:

app.module.ts

  1. import { Connection } from 'typeorm';
  2. @Module({
  3. imports: [TypeOrmModule.forRoot(), PhotoModule],
  4. })
  5. export class ApplicationModule {
  6. constructor(private readonly connection: Connection) {}
  7. }

存储库模式

该TypeORM支持库的设计模式,使每个实体都有自己的仓库。这些存储库可以从数据库连接中获取。

首先,我们至少需要一个实体。我们将重用 Photo 官方文档中的实体。

photo/photo.entity.ts

  1. import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
  2. @Entity()
  3. export class Photo {
  4. @PrimaryGeneratedColumn()
  5. id: number;
  6. @Column({ length: 500 })
  7. name: string;
  8. @Column('text')
  9. description: string;
  10. @Column()
  11. filename: string;
  12. @Column('int')
  13. views: number;
  14. @Column()
  15. isPublished: boolean;
  16. }

该 Photo 实体属于该 photo 目录。这个目录代表了 PhotoModule。这是你决定在哪里保留你的模型文件。从我的观点来看,最好的方法是将它们放在他们的域中, 放在相应的模块目录中。

让我们看看 PhotoModule:

  1. import { Module } from '@nestjs/common';
  2. import { TypeOrmModule } from '@nestjs/typeorm';
  3. import { PhotoService } from './photo.service';
  4. import { PhotoController } from './photo.controller';
  5. import { Photo } from './photo.entity';
  6. @Module({
  7. imports: [TypeOrmModule.forFeature([Photo])],
  8. components: [PhotoService],
  9. controllers: [PhotoController],
  10. })
  11. export class PhotoModule {}

此模块使用 forFeature() 方法定义应在当前范围内注册的存储库。

现在, 我们可以使用 @InjectRepository() 修饰器向 PhotoService 注入 PhotoRepository:

photo/photo.service.ts

  1. import { Component, Inject } from '@nestjs/common';
  2. import { InjectRepository } from '@nestjs/typeorm';
  3. import { Repository } from 'typeorm';
  4. import { Photo } from './photo.entity';
  5. @Component()
  6. export class PhotoService {
  7. constructor(
  8. @InjectRepository(Photo)
  9. private readonly photoRepository: Repository<Photo>,
  10. ) {}
  11. async findAll(): Promise<Photo[]> {
  12. return await this.photoRepository.find();
  13. }
  14. }

就这样。完整的源代码在这里可用。

?> 不要忘记将 PhotoModule 导入根 ApplicationModule。

MongoDB

有2种处理 MongoDB 的方法。您可以使用 TypeORM 提供 MongoDB 支持 或者 Mongoose 这是最流行的 MongoDB 对象建模工具。如果你想留在 TypeORM, 你可以按照这些步骤。否则, 我们将使用一个专门的 @nestjs/mongoose 包。

首先, 我们需要安装所有必需的依赖项:

  1. $ npm install --save @nestjs/mongoose mongoose

安装过程完成后, 我们可以将 MongooseModule 导入到根 ApplicationModule 中。

app.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { MongooseModule } from '@nestjs/mongoose';
  3. @Module({
  4. imports: [MongooseModule.forRoot('mongodb://localhost/nest')],
  5. })
  6. export class ApplicationModule {}

forRoot() 方法接受与来自 Mongoose 包的 mongoose.connect() 相同的配置对象。

模块注入

在Mongoose, 一切都是从一个模式派生。让我们定义 CatSchema:

cats/schemas/cat.schema.ts

  1. import * as mongoose from 'mongoose';
  2. export const CatSchema = new mongoose.Schema({
  3. name: String,
  4. age: Number,
  5. breed: String,
  6. });

CatsSchema 属于 cats 名录。此目录表示 CatsModule。这是您决定将您的架构文件保存到的位置。从我的角度来看, 最好的方法是将它们几乎放在他们的域中, 放在相应的模块目录中。

让我们看看 CatsModule:

cats/cats.module.ts

  1. import { Module } from '@nestjs/common';
  2. import { MongooseModule } from '@nestjs/mongoose';
  3. import { CatsController } from './cats.controller';
  4. import { CatsService } from './cats.service';
  5. import { CatSchema } from './schemas/cat.schema';
  6. @Module({
  7. imports: [MongooseModule.forFeature([{ name: 'Cat', schema: CatSchema }])],
  8. controllers: [CatsController],
  9. components: [CatsService],
  10. })
  11. export class CatsModule {}

此模块使用 forFeature () 方法定义应在当前范围内注册的模型。

现在, 我们可以使用 @InjectModel() 修饰器向 CatsService 注入 CatModel:

cats/cats.service.ts

  1. import { Model } from 'mongoose';
  2. import { Component } from '@nestjs/common';
  3. import { InjectModel } from '@nestjs/mongoose';
  4. import { Cat } from './interfaces/cat.interface';
  5. import { CreateCatDto } from './dto/create-cat.dto';
  6. import { CatSchema } from './schemas/cat.schema';
  7. @Component()
  8. export class CatsService {
  9. constructor(@InjectModel(CatSchema) private readonly catModel: Model<Cat>) {}
  10. async create(createCatDto: CreateCatDto): Promise<Cat> {
  11. const createdCat = new this.catModel(createCatDto);
  12. return await createdCat.save();
  13. }
  14. async findAll(): Promise<Cat[]> {
  15. return await this.catModel.find().exec();
  16. }
  17. }

就这样。完整的源代码在这里可用。

?> 不要忘记将 CatsModule 导入根 ApplicationModule。

身份验证(Passport)

Passport 是最受欢迎的认证库,几乎全球所有的 node.js 开发人员都知道,并且已经在很多生产应用程序中使用。将此工具与 Nest 框架集成非常简单。为了演示目的,我将设置 Passport-jwt 策略。

要使用这个库,我们必须安装所有必需的依赖关系:

  1. $ npm install --save passport passport-jwt jsonwebtoken

首先,我们要创建一个 AuthService。该类将包含2种方法,(1)使用假用户创建令牌,(2)从解码的 JWT(硬编码true)中验证签名用户。

auth.service.ts

  1. import * as jwt from 'jsonwebtoken';
  2. import { Component } from '@nestjs/common';
  3. @Component()
  4. export class AuthService {
  5. async createToken() {
  6. const expiresIn = 60 * 60, secretOrKey = 'secret';
  7. const user = { email: 'thisis@example.com' };
  8. const token = jwt.sign(user, secretOrKey, { expiresIn });
  9. return {
  10. expires_in: expiresIn,
  11. access_token: token,
  12. };
  13. }
  14. async validateUser(signedUser): Promise<boolean> {
  15. // put some validation logic here
  16. // for example query user by id / email / username
  17. return true;
  18. }
  19. }

?> 在最佳情况下,jwt 对象和令牌配置(密钥和过期时间)应作为自定义组件提供并通过构造函数注入。

Passport 使用策略的概念来认证请求。在本章中,我们将扩展 passport-jwt 包提供的策略 JwtStrategy:

jwt.strategy.ts

  1. import * as passport from 'passport';
  2. import { ExtractJwt, Strategy } from 'passport-jwt';
  3. import { Component, Inject } from '@nestjs/common';
  4. import { AuthService } from '../auth.service';
  5. @Component()
  6. export class JwtStrategy extends Strategy {
  7. constructor(private readonly authService: AuthService) {
  8. super(
  9. {
  10. jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  11. passReqToCallback: true,
  12. secretOrKey: 'secret',
  13. },
  14. async (req, payload, next) => await this.verify(req, payload, next)
  15. );
  16. passport.use(this);
  17. }
  18. public async verify(req, payload, done) {
  19. const isValid = await this.authService.validateUser(payload);
  20. if (!isValid) {
  21. return done('Unauthorized', false);
  22. }
  23. done(null, payload);
  24. }
  25. }

JwtStrategy 使用 AuthService 来验证有效负载 (已签名的用户)。当有效载荷有效时, 该请求可以由路由处理程序处理。否则, 用户将收到 401 Unauthorized 响应。

最后一步是创建一个 AuthModule:

auth.module.ts

  1. import * as passport from 'passport';
  2. import {
  3. Module,
  4. NestModule,
  5. MiddlewaresConsumer,
  6. RequestMethod,
  7. } from '@nestjs/common';
  8. import { AuthService } from './auth.service';
  9. import { JwtStrategy } from './passport/jwt.strategy';
  10. import { AuthController } from './auth.controller';
  11. @Module({
  12. components: [AuthService, JwtStrategy],
  13. controllers: [AuthController],
  14. })
  15. export class AuthModule implements NestModule {
  16. public configure(consumer: MiddlewaresConsumer) {
  17. consumer
  18. .apply(passport.authenticate('jwt', { session: false }))
  19. .forRoutes({ path: '/auth/authorized', method: RequestMethod.ALL });
  20. }
  21. }

技巧是提供一个 JwtStrategy 作为组件, 并在实例创建后立即设置策略 (在构造函数内)。此外, 我们还将功能中间件绑定到/auth/authorized 的路由 (仅用于测试目的)。

完整的源代码在这里可用。