译文:nest 控制器和提供者(二)

学习一个技术最好的方式就是看他的官网,大家好,我是云牧,这次翻译的是 nest 最新官网。

翻译方式是 chatgpt 主力翻译 + 人工每行校对。错误的地方,欢迎大家语雀评论纠正。

语雀文档地址:nest 官网译文 (yuque.com)

过程中我会对一些感觉不恰当内容有少量的增加修改删除。

希望内容会对大家有所帮助。

控制器

控制器负责处理传入的请求 并向客户端返回响应

控制器的目的是接收应用程序的特定请求。路由机制控制哪个控制器接收哪个请求。

通常,每个控制器都有多个路由,不同的路由可以执行不同的操作。

为了创建一个基本的控制器,我们使用类和装饰器。装饰器将类与所需的元数据关联起来,并使 Nest 能够创建一个路由映射(将请求与相应的控制器绑定)。

要快速创建一个带有内置验证的 CRUD 控制器,您可以使用 CLI 的 CRUD 生成器: nest g resource [name] 。

Routing 路由

在下面的示例中,我们将使用 @Controller() 装饰器来定义一个基本的控制器。

我们将指定一个可选的路由路径前缀 cats 。在 @Controller() 装饰器中使用路径前缀可以方便地将一组相关的路由分组,并减少重复的代码。

例如,我们可以选择将一组管理与 cats 实体交互的路由分组在路由 /cats 下。在这种情况下,我们可以在 @Controller() 装饰器中指定路径前缀 cats ,这样我们就不必为文件中的每个路由重复该路径的部分。

typescript 复制代码
import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

使用 CLI 创建控制器,只需执行 $ nest g controller [name] 命令即可。

在 @Get() 方法之前的 HTTP 请求方法装饰器告诉 Nest 为特定的 HTTP 请求端点创建一个处理程序。

端点对应于 HTTP 请求方法(在本例中为 GET)和路由路径。

什么是路由路径?处理程序的路由路径由控制器声明的(可选的)前缀和方法装饰器中指定的任何路径连接而成。

由于我们为每个路由声明了一个前缀( cats ),并且在装饰器中没有添加任何路径信息,Nest 将把 GET /cats 请求映射到这个处理程序。

如前所述,路径包括可选的控制器路径前缀和请求方法装饰器中声明的任何路径字符串。例如,路径前缀为 cats ,与装饰器 @Get('breed') 结合使用,将产生一个用于处理类似 GET /cats/breed 请求的路由映射。

在我们上面的示例中,当向此端点发出 GET 请求时,Nest 将请求路由到我们自定义的 findAll() 方法。请注意,我们在这里选择的方法名是完全任意的。显然,我们必须声明一个方法来绑定路由,但 Nest 对所选择的方法名不附加任何意义。

这个方法将返回一个 200 状态码和相关的响应,这种情况下只是一个字符串。为什么会这样呢?

为了解释这个问题,我们首先介绍一下 Nest 使用两种不同的选项来操作响应的概念:

标准(推荐) 使用这个内置方法时,当请求处理程序返回一个 JavaScript 对象或数组时,它将自动序列化为 JSON。然而,当它返回一个 JavaScript 原始类型(例如, string , number , boolean )时,Nest 将只发送值而不尝试序列化它。这使得响应处理变得简单:只需返回值,Nest 会处理剩下的事情。 此外,默认情况下,响应的状态码始终为200,除了使用 POST 请求的情况下会使用 201。我们可以通过在处理程序级别添加 @HttpCode(...) 装饰器来轻松更改此行为(see Status codes)。
库特定 我们可以使用特定于库的(例如Express)响应对象,可以在方法处理程序签名中使用 @Res() 装饰器进行注入(例如 findAll(@Res() response) )。通过这种方法,您可以使用该对象公开的原生响应处理方法。例如,使用Express,您可以使用类似 response.status(200).send() 的代码构建响应。

警告:Nest检测到处理程序使用 @Res() 或 @Next() ,表示您选择了库特定的选项。 如果在一个处理函数同时使用了两种方法,标准方法在单个路由,并且不再按预期工作。要同时使用两种方法(例如,通过注入响应对象,单独设置 cookie / header,但把其余部分留给框架),您必须在 @Res({ passthrough: true }) 装饰器中将 passthrough 选项设置为 true 。

Request object 请求对象

处理程序通常需要访问客户端请求的详细信息。Nest 提供了对底层平台(默认为 Express)的请求对象的访问。

我们可以通过在处理程序的签名中添加 @Req() 装饰器来指示 Nest 注入请求对象以访问它。

typescript 复制代码
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request: Request): string {
    return 'This action returns all cats';
  }
}

提示:为了更好利用 express 类型(如上面的 request: Request 参数示例),请安装 @types/express 软件包。

Request 对象表示 HTTP 请求,具有请求查询字符串、参数、HTTP headers 和 body (read more here)。

在大多数情况下,不需要手动获取这些属性。我们可以使用专用的装饰器,例如 @Body() 或 @Query() ,它们可以直接使用。

下面是提供的装饰器列表和它们所代表的纯平台特定对象。

@Request(), @Req() req
@Response(), @Res()***** res
@Next() next
@Session() req.session
@Param(key?: string) req.params / req.params[key]
@Body(key?: string) req.body / req.body[key]
@Query(key?: string) req.query / req.query[key]
@Headers(name?: string) req.headers / req.headers[name]
@Ip() req.ip
@HostParam() req.hosts

为了与底层的 HTTP 平台(例如 Express 和 Fastify)兼容,Nest提供了 @Res() 和 @Response() 装饰器。

其中 @Res() 只是 @Response() 的别名。它们都直接暴露了底层的原生平台 response 对象接口。

在使用它们时,您还应该导入底层库的类型(例如 @types/express )以充分发挥优势。

请注意,当您在方法处理程序中注入 @Res() 或 @Response() 时,您将使 Nest 进入特定于库的模式,并且您将负责管理响应。

在这样做时,您必须通过对 response 对象进行调用(例如 res.json(...) 或 res.send(...) )来发出某种响应,否则 HTTP 服务器将挂起。

要学习如何创建自定义装饰器,visit this chapter.

Resources 资源

之前,我们定义了一个端点来获取 cats 资源(GET 路由)。

通常,我们还希望提供一个端点来创建新的记录。为此,让我们创建一个 POST 处理程序:

typescript 复制代码
import { Controller, Get, Post } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Post()
  create(): string {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

就是这么简单。Nest 为所有标准的 HTTP 方法提供了装饰器: @Get() , @Post() , @Put() , @Delete() , @Patch() , @Options() ,和 @Head() 。此外, @All() 定义了一个处理它们的端点。

Route wildcards 路由通配符

还支持基于模式的路由。例如,星号被用作通配符,可以匹配任意字符的组合。

typescript 复制代码
@Get('ab*cd')
findAll() {
  return 'This route uses a wildcard';
}

路由路径将匹配 'ab*cd' , abcd , ab_cd , abecd 等等。字符 ? , + , * ,和 () 可以在路由路径中使用,并且是它们正则表达式对应项的子集。连字符( - )和点号( . )在基于字符串的路径中被直接解释。

**警告:**只有 Express 模式支持在路由中间使用通配符。

Status code 状态码

如前所述,默认情况下,响应状态码始终为 200,除了 POST 请求为 201。我们可以通过在处理程序级别添加 @HttpCode(...) 装饰器来轻松更改此行。

typescript 复制代码
@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

**提示:**从 HttpCode 包中导入 @nestjs/common 。

通常情况下,你的状态码并不是静态的,而是取决于各种因素。在这种情况下,你可以使用特定于库的响应(使用 @Res() 注入)对象(或者在出现错误时抛出异常)。

Headers 标题

要指定自定义响应头,您可以使用 @header() 装饰器或特定库的响应对象(并直接调用 res.header() )。

typescript 复制代码
@Post()
@Header('Cache-Control', 'none')
create() {
  return 'This action adds a new cat';
}

**提示:**从 Header 包中导入 @nestjs/common 。

Redirection 重定向

要将响应重定向到特定的 URL,您可以使用 @Redirect() 装饰器或特定于库的响应对象(并直接调用 res.redirect())。

@Redirect() 接受两个参数, url 和 statusCode ,两者都是可选的。如果省略, statusCode 的默认值为 302 ( Found )。

typescript 复制代码
@Get()
@Redirect('https://nestjs.com', 301)

**提示:**有时候你可能想要动态确定 HTTP 状态码或重定向的 URL。通过返回一个遵循 HttpRedirectResponse 接口的对象来实现这一点(来自 @nestjs/common )。

类似这种:

typescript 复制代码
{
  "url": string,
  "statusCode": number
}

返回值将覆盖传递给 @Redirect() 装饰器的任何参数。例如:

typescript 复制代码
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version) {
  if (version && version === '5') {
    return { url: 'https://docs.nestjs.com/v5/' };
  }
}

Route parameters 路由参数

当您需要接受请求中的动态数据时,静态路径的路由将无法工作(例如,获取 ID 为 1 的 cats)。

为了定义带有参数的路由,我们可以在路由的路径中添加路由参数标记,以捕获请求 URL 中该位置的动态值。

下面的 @Get() 装饰器示例中的路由参数标记演示了这种用法。以这种方式声明的路由参数可以使用 @Param() 装饰器进行访问,该装饰器应添加到方法签名中。

**提示:**带有参数的路由应该在任何静态路径之后声明。这样可以防止参数化路径拦截静态路径的流量。

typescript 复制代码
@Get(':id')
findOne(@Param() params: any): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

@Param() 用于装饰方法参数(如上例中的 params ),并使路由参数在方法体内作为该装饰方法参数的属性可用。如上面的代码所示,我们可以通过引用 params.id 来访问 id 参数。

您还可以将特定的参数标记传递给装饰器,然后在方法体中直接按名称引用路由参数。

** 提示:**从 Param 包中导入 @nestjs/common 。

typescript 复制代码
@Get(':id')
findOne(@Param('id') id: string): string {
  return `This action returns a #${id} cat`;
}

Sub-Domain Routing 子域路由

装饰器 @Controller 可以使用 host 选项,要求传入请求的 HTTP 主机与某个特定值匹配。

typescript 复制代码
@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}

**警告:**由于 Fastify 不支持嵌套路由器,因此在使用子域路由时,应使用(默认的)Express 适配器。

类似于路由 path , hosts 选项可以使用参数标识来捕获主机名中该位置的动态值。

下面的 @Controller() 装饰器示例中的主机参数标记展示了这种用法。以这种方式声明的主机参数可以通过 @HostParam() 装饰器进行访问,该装饰器应添加到方法签名中。

typescript 复制代码
@Controller({ host: ':account.example.com' })
export class AccountController {
  @Get()
  getInfo(@HostParam('account') account: string) {
    return account;
  }
}

scopes 范围

对于来自不同编程语言背景的人来说,可能会意外地发现在 Nest 中,几乎所有的东西都是在传入请求之间共享的。

我们有一个连接池用于数据库,具有全局状态的单例服务等等。

请记住,Node.js 不遵循请求/响应多线程无状态模型,其中每个请求都由单独的线程处理。

在 Nest 中,每个请求都由主线程处理。因此,对于我们的应用程序来说,使用单例实例是完全安全的。

然而,在某些特殊情况下,控制器的基于请求的生命周期可能是期望的行为,例如 GraphQL 应用程序中的请求缓存、请求跟踪或多租户。 Learn how to control scopes here.

Asynchronicity 异步性

我们喜欢现代JavaScript,并且我们知道数据获取大多是异步的。这就是为什么 Nest 支持并且与 async 函数很好地配合使用。

**提示:**Learn more about async / await feature here

每个异步函数都必须返回一个 Promise 。这意味着您可以返回一个延迟值,Nest 将能够自行解析。让我们看一个例子:

typescript 复制代码
@Get()
async findAll(): Promise<any[]> {
  return [];
}

上述代码是完全有效的。

此外,Nest 路由处理程序更强大,因为它们能够返回 RxJS observable 流。Nest 将自动订阅底层源并获取最后发出的值(一旦流完成)。

typescript 复制代码
@Get()
findAll(): Observable<any[]> {
  return of([]);
}

上面的两种方法都有效,您可以根据自己的需求选择使用哪种方法。

Request payloads 请求负载

我们之前的 POST 路由处理程序没有接受任何客户端参数。让我们通过在这里添加 @Body() 装饰器来修复这个问题。

但是首先(如果您使用 TypeScript),我们需要确定 DTO(数据传输对象)的模式。

DTO 是一个对象,它定义了如何通过网络发送数据。我们可以通过使用 TypeScript 接口或简单的类来确定 DTO 的模式。

有趣的是,我们建议在这里使用类。为什么呢?类是 JavaScript ES6 标准的一部分,因此它们在编译后的 JavaScript 中被保留为实际实体。

另一方面,由于 TypeScript 接口在转译过程中被移除,Nest 无法在运行时引用它们。这一点很重要,因为诸如 **Pipes **之类的特性在运行时具有变量的元类型时会有额外的可能性。

让我们创建 CreateCatDto 类:

typescript 复制代码
export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

它只有三个基本属性。之后,我们可以在 CatsController 中使用新创建的 DTO:

typescript 复制代码
@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

**提示:**我们的 ValidationPipe 可以过滤掉方法处理程序不应接收的属性。在这种情况下,我们可以列出可接受的属性,任何未包含在白名单中的属性都会自动从结果对象中剥离。在 CreateCatDto 示例中,我们的白名单是 name 、 age 和 breed 属性。Learn more here.

Handling errors 处理错误

关于处理错误有一个单独的章节,你可以在 here 了解更多信息。

Full resource sample 完整资源示例

下面是一个示例,利用了几个可用的装饰器来创建一个基本的控制器。这个控制器公开了一些方法,用于访问和操作内部数据。

typescript 复制代码
import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(@Query() query: ListAllEntities) {
    return `This action returns all cats (limit: ${query.limit} items)`;
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns a #${id} cat`;
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
    return `This action updates a #${id} cat`;
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return `This action removes a #${id} cat`;
  }
}

提示:Nest CLI 提供了一个生成器(原理图),它会自动生成所有样板代码 ,帮助我们避免手动编写这些代码,从而使开发者的体验更加简单。Read more about this feature here.

Getting up and running 开始运行

在上面完全定义了控制器之后,Nest 仍然不知道 CatsController 的存在,因此不会创建该类的实例。

控制器总是属于一个模块,这就是为什么我们在装饰器 @Module() 中包含了 controllers 数组的原因。由于除了根模块 AppModule 之外,我们还没有定义其他模块,所以我们将使用根模块来引入 CatsController :

typescript 复制代码
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule {}

我们使用 @Module() 装饰器将元数据附加到模块类上,Nest 现在可以轻松反映哪些控制器需要挂载。

Library-specific approach 特定库的方法

到目前为止,我们已经讨论了 Nest 处理响应的标准方式。第二种处理响应的方式是使用特定库的 response object。为了注入特定的响应对象,我们需要使用 @Res() 装饰器。为了展示区别,让我们将 CatsController 重写为以下内容:

typescript 复制代码
import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Res() res: Response) {
    res.status(HttpStatus.CREATED).send();
  }

  @Get()
  findAll(@Res() res: Response) {
     res.status(HttpStatus.OK).json([]);
  }
}

虽然这种方法可以工作,并且实际上在某些方面提供了更大的灵活性(如完全控制响应对象的头部操作、特定库的功能等),但是它应该谨慎使用。

总的来说,这种方法不太清晰,并且有一些缺点。主要的缺点是你的代码变得依赖于平台(因为底层库可能在响应对象上有不同的API),并且更难进行测试(你需要模拟响应对象等)。

此外,在上面的示例中,你将失去与依赖于 Nest 标准响应处理的 Nest 功能的兼容性,例如拦截器和 @HttpCode() / @Header() 装饰器。为了解决这个问题,你可以将 passthrough 选项设置为 true ,如下所示:

typescript 复制代码
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}

现在您可以与原生响应对象进行交互(例如,根据特定条件设置 Cookie 或 HTTP 头),但将其余部分交给框架处理。

提供者

Providers 提供者是 Nest 中的一个基本概念。许多基本的 Nest 类可以被视为 provider - service, repository, factory, helper 等等。

提供者的主要思想是它可以作为依赖注入 ;这意味着对象可以相互创建各种关系,并且将这些对象的"连接"功能在很大程度上委托给 Nest 运行时系统。

在前一章中,我们构建了一个简单的 CatsController 。控制器应该处理 HTTP 请求,并将更复杂的任务委托给提供者。提供者是声明为模块中的 providers 的普通 JavaScript 类。

**提示:**由于 Nest 使得以更面向对象的方式设计和组织依赖成为可能,我们强烈建议遵循 SOLID 原则。

Services 服务

让我们从创建一个简单的 CatsService 开始。这个服务将负责数据存储和检索,并且被设计为被 CatsController 使用。因此,我们用 @Injectable() 来装饰这个类 。

typescript 复制代码
import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}

**提示:**要使用 CLI 创建一个服务,只需执行 $ nest g service cats 命令。

我们的 CatsService 是一个基本类,有一个属性和两个方法。唯一的新特性是它使用了 @Injectable() 装饰器。这个 @Injectable() 装饰器附加了元数据,声明 CatsService 是一个可以由 Nest IoC 容器管理的类。顺便说一下,这个例子还使用了一个 Cat 接口,可能长这样:

typescript 复制代码
export interface Cat {
  name: string;
  age: number;
  breed: string;
}

现在我们有了一个用于获取 cats 的服务类,让我们在 CatsController 内部使用它:

typescript 复制代码
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

CatsService 通过类构造函数进行注入。注意 private 语法的使用。这个简写允许我们在同一位置同时声明和初始化 catsService 成员。

Dependency injection 依赖注入

Nest 是围绕着一种被广泛称为依赖注入的强大设计模式构建的。我们建议阅读官方 Angular 文档中关于这个概念的一篇很棒的文章。

在 Nest 中,由于 TypeScript 的能力,管理依赖非常容易,因为它们仅通过类型来解析。

在下面的示例中,Nest 将通过创建并返回一个 catsService 的实例(或者在单例的正常情况下,如果已经在其他地方请求过,则返回现有实例)来解析这个依赖项。这个依赖项将被解析并传递给您的控制器的构造函数(或者分配给指定的属性):

typescript 复制代码
constructor(private catsService: CatsService) {}

Scopes 作用域

Provider 通常具有与应用程序生命周期同步的生命周期("作用域")。

当应用程序启动时,必须解析每个依赖项,因此必须实例化每个提供者。同样地,当应用程序关闭时,每个 provider 都将被销毁。然而,也有方法可以使您的 provider 的生命周期变为请求范围 。您可以在这里阅读更多关于这些 techniques here.

Custom providers 自定义提供者

Nest 内置了一个控制反转("IoC")容器,用于解决提供者之间的关系。这个特性是上面描述的依赖注入特性的基础,但实际上比我们迄今为止描述的要强大得多。有几种定义提供者的方式:您可以使用普通值、类,以及异步或同步工厂。More examples are provided here.

Optional providers 可选提供者

有时候,您可能有一些不一定需要解决的依赖关系。例如,您的类可能依赖于一个配置对象,但如果没有传递任何配置对象,应该使用默认值。在这种情况下,依赖关系变得可选,因为缺少配置提供者不会导致错误。

要指示提供者是可选的,请在构造函数的签名中使用 @Optional() 装饰器。

typescript 复制代码
import { Injectable, Optional, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}

请注意,在上面的示例中,我们使用了自定义提供者,这是我们包含 HTTP_OPTIONS 自定义 **token **的原因。之前的示例展示了基于构造函数的注入,通过构造函数中的类来指示依赖关系。在此处(here)阅读有关自定义提供者及其相关令牌的更多信息。

Property-based injection 基于属性的注入

到目前为止,我们使用的技术称为基于构造函数的注入,因为提供者是通过构造方法注入的。

在某些非常特殊的情况下,基于属性的注入可能会很有用。例如,如果顶级类依赖于一个或多个 providers,那么通过从构造函数中调用子类中的 super() 来传递它们就会非常烦人了。因此,为了避免出现这种情况,可以在属性上使用 @Inject() 装饰器。

typescript 复制代码
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class HttpService<T> {
  @Inject('HTTP_OPTIONS')
  private readonly httpClient: T;
}

警告:如果你的类没有继承其他类,你应该始终优先使用基于构造函数的注入。

Provider registration 提供者注册

现在我们已经定义了一个提供者( CatsService ),并且我们有一个使用该服务的消费者( CatsController ),我们需要将该服务注册到 Nest 中,以便它可以进行注入。

我们通过编辑我们的模块文件( app.module.ts )并将服务添加到 @Module() 装饰器的 providers 数组中来实现这一点。

javascript 复制代码
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}

Nes t现在将能够解决 CatsController 类的依赖关系。

这是我们目录结构现在应该看起来的样子:

Manual instantiation 手动实例化

到目前为止,我们已经讨论了 Nest 如何自动处理大部分解决依赖的细节。

在某些情况下,您可能需要跳出内置的依赖注入系统,手动获取或实例化提供者。我们以下简要讨论了两个相关主题。

要获取现有实例或动态实例化提供程序,您可以使用 Module reference.

要在 bootstrap() 函数中获取提供程序(例如用于没有控制器的独立应用程序,或在引导过程中使用配置服务), see Standalone applications.

相关推荐
哎呦没28 分钟前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch1 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光1 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   1 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   1 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web1 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常1 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇2 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr2 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
杨哥带你写代码2 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端