第一个工作场所或如何开始在Node.js上开发API

引言


在本文中,我想与大家分享我从头开始使用TypeScript在Node.js上开发第一个REST API的情感和所学技能。 这个故事很平庸: “我毕业于大学,获得了文凭。 在哪里上班?” 您可能已经猜到我了,即使我不必考虑太多,问题也无法幸免。 开发人员(同一专业的毕业生)要求实习。 我相信这是相当普遍的做法,并且有许多类似的故事。 我三思而后行,决定试着去...

图片

第一天 介绍Node.js


我来到了后端开发。 该IT公司使用我完全不熟悉的Node.js平台。 我向前跑了一下,忘记告诉读者我从未用JavaScript开发过任何东西(除了几个带有复制代码的脚本之外)。 自从我用Java,Python和Clojure开发CRUD以来,我就整体上了解了工作算法和Web应用程序的体系结构,但这还不够。 因此,在我完全致力于研究Node.js的第一天,这个截屏视频确实有所帮助。

在研究Express Web框架, npm软件包管理器以及package.json和tsconfig.json之类的文件时,我的头脑只是绕过了很多信息。 另一个教训是,同时掌握所有材料几乎是不可能完成的任务。 到最后,我仍然设法配置了环境,并且能够运行Express Web服务器! 但是现在高兴还为时过早,因为他带着一种完全的误解回到家。 我被淹没在JS的广阔世界中的感觉并没有让我离开一分钟,因此需要重新启动。

第二天 介绍TypeScript


当天又进行了相同的重启。 至此,我完全意识到了我的问题,我们将继续进行讨论。 知道不必用纯JavaScipt编写,从Node.js进行的培训就顺利地流向TypeScript语言,即其功能和语法。 在这里,我看到了期待已久的类型 ,没有类型的编程实际上是两天前的, 而不是使用函数式编程语言。 这是我最大的误解,这使我在第一天就无法理解和学习用JavaScript编写的代码。

他以前大部分时间是使用面向对象的编程语言(例如Java,C ++,C#)编写的。 意识到TypeScript的可能性,我感到很轻松。 这种编程语言从字面上使我感受到了当时那个复杂环境的生命。 在一天结束之前,我完全设置了环境,启动了服务器(已经在TypeScript上使用),连接了必要的库,我将在下面进行讨论。 底线:准备开发API。 我们直接通过发展...

API开发


我们将离开对工作原理的解释以及对REST API的其他解释,因为该论坛上有很多与此相关的文章,包括示例和各种编程语言的开发。
图片

任务如下:

使用REST API进行服务。 通过承载令牌(/信息,/延迟,/注销)进行授权。 配置为可从任何域访问的CORS。 DB-MongoDB。 在每次通话时创建一个令牌。

API说明:

  1. /登录[POST]-通过ID和密码请求令牌承载// //接收JSON中的数据
  2. / signup [POST]-注册新用户://接收json中的数据
  3. / info [GET]-返回用户ID和ID类型,要求在身份验证中由承载者发行的令牌
  4. /延迟[GET]-返回延迟(ping),要求承载者在身份验证中发出的令牌
  5. /注销[GET]-使用参数all:true-删除所有用户承载令牌,或者false-仅删除当前承载令牌

我立即注意到,对于Web应用程序开发人员而言,该任务看起来非常简单。 但是该任务必须用一种编程语言来实现,大约三天前它根本什么都不知道! 即使是对我来说,它在纸上看起来也是完全透明的,在Python中,该实现花费了一些时间,但我没有这种选择。 开发堆栈预示着麻烦。

实施手段


因此,我提到第二天我已经研究了几个库(框架),我们将从此开始。 对于路由,我选择了路由控制器 ,并与Spring框架(Java)的装饰器进行了许多相似的选择。 作为ORM,我选择了typeorm ,尽管以实验模式与MongoDB一起工作,但足以完成这样的任务。 我使用uuid生成令牌,使用dotenv加载变量。

Web服务器启动


通常,express是以其纯形式使用的,但是我提到了路由控制器框架,该框架使我们可以如下创建一个express服务器:

//  Express const app = createExpressServer({ // routePrefix: process.env.SERVER_PREFIX, //  defaults: { nullResultCode: Number(process.env.ERROR_NULL_RESULT_CODE), undefinedResultCode: Number(process.env.ERROR_NULL_UNDEFINED_RESULT_CODE), paramOptions: { required: true } }, //   authorizationChecker: authorizationChecker, // controllers: [UserController] }); //  app.listen(process.env.SERVER_PORT, () => { console.log(process.env.SERVER_MASSAGE); }); 


如您所见,没有什么复杂的。 实际上,该框架具有更多功能,但不需要它们。
  • routePrefix只是服务器地址后面URL中的前缀,例如: localhost :3000 / prefix
  • 默认值-没什么有趣的,只需初始化错误代码
  • authorizationChecker-框架检查用户授权的绝佳机会,然后我们将更详细地考虑
  • 控制器是我们指定应用程序中使用的控制器的主要字段之一


数据库连接


之前,我们已经启动了Web服务器,因此我们将继续连接到MongoDB数据库,之前已将其部署在本地服务器上。 官方文档中详细描述了安装和配置。 我们将直接使用typeorm考虑连接:

 //  createConnection({ type: 'mongodb', host: process.env.DB_HOST, database: process.env.DB_NAME_DATABASE, entities: [ User ], synchronize: true, logging: false }).catch(error => console.log(error)); 


一切都非常简单,您需要指定几个参数:

  • 类型-DB
  • 主机-部署数据库的IP地址
  • database-先前在mongodb中创建的数据库的名称
  • 同步-与数据库自动同步(注意:当时很难掌握迁移情况)
  • 实体-在这里,我们指示执行同步的实体


现在,我们将服务器启动并连接到数据库。 我注意到资源的导入与Node.js中使用的经典资源不同。 结果,我们得到以下可执行文件,在我的情况下为main.ts:

 import 'reflect-metadata'; import * as dotenv from 'dotenv'; import { createExpressServer } from 'routing-controllers'; import { createConnection } from 'typeorm'; import { authorizationChecker } from './auth/authorizationChecker'; import { UserController } from './controllers/UserController'; import { User } from './models/User'; dotenv.config(); //  createConnection({ type: 'mongodb', host: process.env.DB_HOST, database: process.env.DB_NAME_DATABASE, entities: [ User ], synchronize: true, logging: false }).catch(error => console.log(error)); //  Express const app = createExpressServer({ // routePrefix: process.env.SERVER_PREFIX, //  defaults: { nullResultCode: Number(process.env.ERROR_NULL_RESULT_CODE), undefinedResultCode: Number(process.env.ERROR_NULL_UNDEFINED_RESULT_CODE), paramOptions: { required: true } }, //   authorizationChecker: authorizationChecker, // controllers: [UserController] }); //  app.listen(process.env.SERVER_PORT, () => { console.log(process.env.SERVER_MASSAGE); }); 

实体


让我提醒您,任务是分别对用户进行身份验证和授权,我们需要一个实体:User。 但这还不是全部,因为每个用户都有一个令牌而不是一个令牌! 因此,有必要创建一个令牌实体。

用户名

 import { ObjectID } from 'bson'; import { IsEmail, MinLength } from 'class-validator'; import { Column, Entity, ObjectIdColumn } from 'typeorm'; import { Token } from './Token'; //  @Entity() export class User { //  @ObjectIdColumn() id: ObjectID; //Email    @Column() @IsEmail() email: string; //  @Column({ length: 100 }) @MinLength(2) password: string; //  @Column() token: Token; } 

在“用户”表中,我们创建一个字段-用户的非常标记的数组。 我们还启用了calss-validator ,因为用户必须通过电子邮件登录。

代币

 import { Column, Entity } from 'typeorm'; //   @Entity() export class Token { @Column() accessToken: string; @Column() refreshToken: string; @Column() timeKill: number; } 

基数如下:

图片

用户授权


对于授权,我们使用authorizationChecker (创建服务器时的参数之一,请参见上文),为方便起见,我们将其放在单独的文件中:

 import { Action, UnauthorizedError } from 'routing-controllers'; import { getMongoRepository } from 'typeorm'; import { User } from '../models/User'; export async function authorizationChecker(action: Action): Promise<boolean> { let token: string; if (action.request.headers.authorization) { //   token = action.request.headers.authorization.split(" ", 2); const repository = getMongoRepository(User); const allUsers = await repository.find(); for (let i = 0; i < allUsers.length; i++) { if (allUsers[i].token.accessToken.toString() === token[1]) { return true; } } } else { throw new UnauthorizedError('This user has not token.'); } return false; } 

经过身份验证后,每个用户都有自己的令牌,因此我们可以从响应的标头中获取必要的令牌,如下所示: Bearer 046a5f60-c55e-11e9-af71-c75526de439e 。 现在我们可以检查此令牌是否存在,此后函数将返回授权信息:true-用户已授权,false-用户未授权。 在应用程序中,我们可以在控制器中使用非常方便的装饰器:@Authorized()。 此时,将调用authorizationChecker函数,该函数将返回响应。

逻辑学


首先,我要描述业务逻辑,因为控制器是所提供类之下的一行方法调用。 另外,在控制器中,我们将接受所有数据,在本例中为JSON和Query。 我们将考虑用于各个任务的方法,最后我们将形成最终文件,称为UserService.ts。 我注意到,那时根本没有足够的知识来消除依赖关系。 如果您还没有满足依赖注入一词,我强烈建议您阅读一下。 目前,我使用DI框架,即使用容器,即通过构造函数注入。 我认为这是一篇很好的评论文章 。 我们回到任务。

  • /登录[POST] -注册用户的身份验证。 一切都非常简单和透明。 我们只需要在数据库中找到该用户并发出新令牌即可。 对于读写,使用MongoRepository。

     async userSignin(user: User): Promise<string> { // Mongo repository const repo = getMongoRepository(User); //       let userEmail = await repo.findOne({ email: user.email, password: user.password }); if (userEmail) { //  userEmail = await this.setToken(userEmail); //    repo.save(userEmail); return userEmail.token.accessToken; } return process.env.USER_SERVICE_RESPONSE; } 
  • /注册[POST] -注册新用户。 一种非常相似的方法,因为起初我们也在寻找用户,因此我们没有使用一封电子邮件注册用户。 接下来,在发出令牌之后,我们将新用户写入数据库。

     async userSignup(newUser: User): Promise<string> { // Mongo repository const repo = getMongoRepository(User); //   email (   2    email) const userRepeat = await repo.findOne({ email: newUser.email }); if (!userRepeat) { //  newUser = await this.setToken(newUser); //   const addUser = getMongoManager(); await addUser.save(newUser); return newUser.token.accessToken; } else { return process.env.USER_SERVICE_RESPONSE; } } 
  • / info [GET] -返回用户ID和ID类型,要求在身份验证中由承载者发行的令牌。 图片也是透明的:首先我们从请求标头中获得用户的当前令牌,然后在数据库中查找它并确定它位于谁,然后返回找到的用户。

     async getUserInfo(req: express.Request): Promise<User> { // Mongo repository const repository = getMongoRepository(User); //    const user = await this.findUser(req, repository); return user; } private async findUser(req: express.Request, repository: MongoRepository<User>): Promise<User> { if (req.get(process.env.HEADER_AUTH)) { //  const token = req.get(process.env.HEADER_AUTH).split(' ', 2); //    const usersAll = await repository.find(); //  for (let i = 0; i < usersAll.length; i++) { if (usersAll[i].token.accessToken.toString() === token[1]) { return usersAll[i]; } } } } 

  • /延迟[GET] -返回延迟(ping),要求承载中的令牌在身份验证中发出。 不过,这篇文章完全没有意思。 在这里,我仅使用一个现成的库来检查tcp-ping延迟。

     getLatency(): Promise<IPingResult> { function update(progress: number, total: number): void { console.log(progress, '/', total); } const latency = ping({ address: process.env.PING_ADRESS, attempts: Number(process.env.PING_ATTEMPTS), port: Number(process.env.PING_PORT), timeout: Number(process.env.PING_TIMEOUT) }, update).then(result => { console.log('ping result:', result); return result; }); return latency; } 
  • /注销[GET] -使用参数all:true-删除所有用户承载令牌,或者false-仅删除当前承载令牌。 我们只需要找到用户,检查查询参数并删除令牌即可。 我认为一切都应该清楚。

     async userLogout(all: boolean, req: express.Request): Promise<void> { // Mongo repository const repository = getMongoRepository(User); //    const user = await this.findUser(req, repository); if (all) { // true    user.token.accessToken = process.env.GET_LOGOUT_TOKEN; user.token.refreshToken = process.env.GET_LOGOUT_TOKEN; //  repository.save(user); } else { // false    user.token.accessToken = process.env.GET_LOGOUT_TOKEN; //  repository.save(user); } } 


控制者


许多人不需要解释什么以及在MVC模式中如何使用控制器,但是我仍然会讲两个词。 简而言之,控制器是用户和应用程序之间的链接,用于在用户和应用程序之间重定向数据。 上面已经对逻辑进行了完整的描述,其逻辑根据路由来调用,路由由URI和ip服务器组成(例如:localhost:3000 / signin) 。 我之前提到控制器中的装饰器: GetPOST ,@Authorized,其中最重要的是@JsonController。 该框架的另一个非常重要的功能是,如果我们要发送和接收JSON,则可以使用此装饰器而不是Controller

 import * as express from 'express'; import { Authorized, Body, Get, Header, JsonController, NotFoundError, Post, QueryParam, Req, UnauthorizedError } from 'routing-controllers'; import { IPingResult } from '@network-utils/tcp-ping'; import { User } from '../models/User'; import { UserService } from '../services/UserService'; //    JSON @JsonController() export class UserController { userService: UserService //  constructor() { this.userService = new UserService(); } //  @Post('/signin') async login(@Body() user: User): Promise<string> { const responseSignin = await this.userService.userSignin(user); if (responseSignin !== process.env.USER_SERVICE_RESPONSE) { return responseSignin; } else { throw new NotFoundError(process.env.POST_SIGNIN_MASSAGE); } } //  @Post('/signup') async registrateUser(@Body() newUser: User): Promise<string> { const responseSignup = await this.userService.userSignup(newUser); if (responseSignup !== process.env.USER_SERVICE_RESPONSE) { return responseSignup; } else { throw new UnauthorizedError(process.env.POST_SIGNUP_MASSAGE); } } //   @Get('/info') @Authorized() async getId(@Req() req: express.Request): Promise<User> { return this.userService.getUserInfo(req); } //   @Authorized() @Get('/latency') getPing(): Promise<IPingResult> { return this.userService.getLatency(); } @Get('/logout') async deleteToken(@QueryParam("all") all: boolean, @Req() req: express.Request): Promise<void> { this.userService.userLogout(all, req); } } 

结论


在本文中,我不想再反映正确代码或类似内容的技术组成部分,而只是分享一个事实,即一个人可以使用数据库构建一个Web应用程序,并在五天内至少包含一些绝对零值的逻辑。 只需考虑一下,没有一种乐器是您熟悉的,记住自己或将它放在我的位置。 这种情况绝不会说:“我是最好的,你永远都做不到。” 相反,这是一个人的灵魂的呐喊,该人目前对Node.js的世界完全满意并与您分享。 事实上,没有什么是不可能的,您只需要采取行动即可!

当然,不能否认作者一无所知,坐下来第一次编写代码。 不,OOP的知识,REST API的原理,ORM和数据库的数量足够多。 这只能说,取得成果的方法绝对不会发挥任何作用,并以这种方式说:“我不会去做这份工作,有一种我没有学过的编程语言”,对我来说,这只是一个人的表现,不是弱点,而是保护免受陌生的外部环境。 但是隐藏的地方是,恐惧与我同在。

总结一下。 我想建议尚未开始从事IT事业的学生和人们,不要害怕开发工具和未知技术。 高级同志一定会帮助您(如果您和我一样幸运的话),他们将详细解释并回答问题,因为他们每个人都处于这个位置。 但不要忘记,您的愿望是最重要的方面!

链接到项目

Source: https://habr.com/ru/post/zh-CN466745/


All Articles