65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 fortjs 框架进行 NodeJs 开发

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2019年6月14日

CPOL

10分钟阅读

viewsIcon

5991

downloadIcon

77

本文介绍如何使用 fortjs 框架进行 Node.js 开发,并演示如何创建 REST API。

引言

Node.js 使您能够使用 JavaScript 编写服务器端代码。实际上,使用 Node.js 创建 Web 服务器非常简单快捷,并且有许多内置框架可以使开发更加轻松快捷。

但是,Node.js 开发中存在一些挑战:

  • Node.js 都是关于回调的,回调越多,您最终会陷入称为“回调地狱”的境地。
  • 编写可读的代码
  • 编写可维护的代码
  • 您没有多少智能感知支持,这使得开发速度很慢。

如果您经验丰富且对 Node.js 有深入了解,则可以使用不同的技术并尝试将这些挑战降至最低。

解决这些问题的最佳方法是使用现代 JavaScript ES6、ES7 或 TypeScript,无论您喜欢哪种。我推荐 TypeScript,因为它为每一行代码都提供了智能感知支持,从而加快了您的开发速度。

于是我发现了一个框架 fortjs - 它非常易于使用,而且它的概念非常酷。它的概念围绕着创建一个堡垒。Fortjs 使您能够编写模块化、安全且非常漂亮且可读的服务器端代码。

背景

FortJs 是一个 MVC 框架,其工作方式类似于真实的堡垒。它以组件的形式提供模块化。有三个组件:

  1. Wall - 这是一个顶级组件,每个 HTTP 请求首先通过 Wall。
  2. Shield - 这个可用于控制器。您可以为控制器分配一个 Shield,它将在调用控制器之前执行。
  3. Guard - 这个可用于控制器内的称为 fort.jsWorker 的方法。Guard 将在调用 Worker 之前被调用。

每个组件都像一个拦截器,如果条件不满足,它们会阻止请求继续进行。就像在真实的堡垒中一样,如果条件不满足,Guard 会阻止任何人通过。

希望您能理解这一点,有关更多信息,请阅读组件文档 - http://fortjs.info/tutorial/components/

Using the Code

在本文中,我将使用 fortjs 和 TypeScript 语言创建 REST API。但您可以使用相同的代码和步骤来实现 JavaScript 版本。

项目设置

FortJs 提供了一个 CLI - fort-creator。这有助于您更快地设置项目和进行开发。我将使用 CLI。

因此,请按顺序执行以下步骤:

  • 打开终端或命令提示符。
  • 全局安装 FortJs - 运行命令 "npm i fort-creator -g"。
  • 创建一个新项目 - 运行命令 "fort-creator new my-app"。其中“my-app”是应用的名称,您可以选择任何名称。它会提示您选择语言,有两个选项:TypeScript 和 JavaScript。使用箭头键选择您的语言,然后按 Enter。由于我将使用 TypeScript,所以我选择了 TypeScript。创建项目需要一些时间。
  • 进入项目目录 - "cd my-app"。
  • 启动开发服务器并进行热重载 - 运行命令 "fort-creator start"。
  • 打开浏览器并键入 URL - https://:4000/

您应该在浏览器中看到类似这样的内容。

fortjs start page

让我们看看这个页面是如何渲染的。

  • 在您喜欢的代码编辑器中打开项目文件夹,我将使用 VS Code。您会在项目根目录下看到许多文件夹,如 _controllers_、_views_ 等。每个文件夹都按其用途进行分组,例如 _controllers_ 文件夹包含所有控制器,_views_ 文件夹包含所有视图。
  • 打开 _controllers_ 文件夹 -> 在 _controllers_ 中,您会看到一个名为 _default_controller_ 的文件,让我们打开它并观察代码。该文件包含一个名为 DefaultController 的类 - 这是一个 控制器 类,它包含返回一些 HTTP 响应的方法。
  • DefaultController 类中 -> 您会看到一个名为 'index' 的方法 - 这是渲染到浏览器输出的方法。该方法在 fortjs 中称为 Worker,因为它们会执行一些工作并以 HTTP 响应的形式返回结果。让我们观察 index 方法的代码。
    try {
        const data= {
            title: 'FortJs'
        };
        const result = await viewResult('default/index.html', data);
        return result;
    } catch (ex) {
        // handle exception and show user a good message.
        // save this ex in a file or db, so that you can know what's the issue and where
        const result = await textResult("Our server is busy right now. Please try later.");
        return result;
    }
    	

    它创建一个数据对象,并将该对象传递给 viewResult 方法。viewResult 方法接受视图位置和视图数据。此方法的工作是渲染视图并返回我们在浏览器中看到的响应。
  • 让我们观察视图代码。打开 _views_ 文件夹 -> 打开 default 文件夹 -> 打开 _index.html_。这是我们的视图代码。它是简单的 HTML 代码,并包含一些 Mustache 语法。Fortjs 的默认视图引擎是 Mustache。

REST

我们将为实体 user 创建一个 REST 端点 - 该端点将执行用户的 CRUD 操作,例如添加用户、删除用户、获取用户和更新用户。

根据 REST

  1. 添加用户 - 应使用 HTTP 方法 "POST"
  2. 删除用户 - 应使用 HTTP 方法 "REMOVE"
  3. 获取用户 - 应使用 HTTP 方法 "GET"
  4. 更新用户 - 应使用 HTTP 方法 "PUT"

要创建端点,我们需要创建一个与前面解释的默认控制器类似的控制器。

执行命令 - "fort-creator add"。它会询问 "您想添加什么?" 选择 controller 并按 **Enter**。输入控制器名称 "UserController" 并按 **Enter**。

现在我们已经创建了 user 控制器,我们需要将其添加到路由中。由于我们的实体是 user,所以 "/user" 将是一个很好的路由。让我们添加它 - 打开项目根目录下的 _routes.ts_ 文件,并将 UserController 添加到 _routes_ 中。

因此,我们的 _routes.ts_ 看起来是这样的:

import { DefaultController } from "./controllers/default_controller";
import { ParentRoute } from "fortjs";
import { UserController } from "./controllers/user_controller";

export const routes: ParentRoute[] = [{
    path: "/default",
    controller: DefaultController
}, {
    path: "/user",
    controller: UserController
}];

现在 fortjs 知道当调用路由 - "/user" 时,它需要调用 UserController。让我们打开 URL - https://:4000/user

您看到了一个白屏,对吗?

这是因为 - 我们没有从 index 方法返回任何内容。让我们从 index 方法返回文本 "Hello World"。将以下代码添加到 index 方法中并保存。

return textResult('Hello World');

刷新 URL - https://:4000/user

您看到了 Hello World

现在,让我们将 UserController 转换为 REST API。但在编写 REST API 代码之前,让我们创建一个虚拟服务来执行用户 CRUD 操作。

模型

让我们创建一个模型 "User",它将代表实体 user。创建一个 _models_ 文件夹,并在该文件夹内创建一个 _user.ts_ 文件。将以下代码粘贴到文件中。

export class User {
    id?: number;
    name: string;
    gender: string;
    address: string;
    emailId: string;
    password: string;
    
    constructor(user) {
        this.id = Number(user.id);
        this.name = user.name;
        this.gender = user.gender;
        this.address = user.address;
        this.emailId = user.emailId;
        this.password = user.password;
    }
}    
服务

创建一个文件夹“services”,然后在该文件夹内创建一个文件“user_service.ts”。将以下代码粘贴到文件中。

import {
    User
} from "../models/user";

interface IStore {
    users: User[]
};
const store: IStore = {
    users: [{
        id: 1,
        name: "durgesh",
        address: "Bengaluru india",
        emailId: "durgesh@imail.com",
        gender: "male",
        password: "admin"
    }]
}

export class UserService {
    getUsers() {
        return store.users;
    }

    addUser(user: User) {
        const lastUser = store.users[store.users.length - 1];
        user.id = lastUser == null ? 1 : lastUser.id + 1;
        store.users.push(user);
        return user;
    }

    updateUser(user) {
        const existingUser = store.users.find(qry => qry.id === user.id);
        if (existingUser != null) {
            existingUser.name = user.name;
            existingUser.address = user.address;
            existingUser.gender = user.gender;
            existingUser.emailId = user.emailId;
            return true;
        }
        return false;
    }

    getUser(id) {
        return store.users.find(user => user.id === id);
    }

    removeUser(id) {
        const index = store.users.findIndex(user => user.id === id);
        store.users.splice(index, 1);
    }
}

上面的代码包含一个变量 store,其中包含用户集合,服务内的该方法对该 store 执行 add、update、delete 和 get 等操作。

我们将在 REST API 实现中使用此服务。

REST

GET

对于路由 "/user" 和 HTTP 方法 "GET",API 应返回所有用户的列表。为了实现这一点,让我们将 "index" 方法重命名为 "getUsers",使其在语义上更正确,并将以下代码粘贴到方法中。

const service = new UserService();
return jsonResult(service.getUsers());

所以现在,user_controller.ts 看起来是这样的:

import { Controller, DefaultWorker, Worker, textResult, jsonResult } from "fortjs";
import { UserService } from "../services/user_service";

export class UserController extends Controller {

    @DefaultWorker()
    async getUsers() {
        const service = new UserService();
        return jsonResult(service.getUsers());
    }
}

在这里,我们使用装饰器 DefaultWorkerDefaultWorker 执行两项操作 - 它添加路由 "/" 和 HTTP 方法 "GET"。这是对此场景的快捷方式。在下一部分中,我们将使用其他装饰器来定制路由。

让我们通过调用 URL https://:4000/user 来测试这一点。您可以在浏览器中打开它,或者使用任何 HTTP 客户端工具,如 Postman 或 Advanced Rest Client。我使用的是 Advanced Rest Client。

get method screenshot

POST

让我们添加一个名为 "addUser" 的方法,该方法将从请求体中提取数据并添加用户。

async addUser() {
    const user: User = {
        name: this.body.name,
        gender: this.body.gender,
        address: this.body.address,
        emailId: this.body.emailId,
        password: this.body.password
    };
    const service = new UserService();
    const newUser = service.addUser(user);
    return jsonResult(newUser, HTTP_STATUS_CODE.Created);
}

为了使此方法对 HTTP 请求可见,我们需要将其标记为 worker。方法通过添加装饰器 "Worker" 来标记为 worker。Worker 装饰器接受 HTTP 方法列表,并使该方法仅可用于那些 HTTP 方法。所以让我们添加装饰器。

@Worker([HTTP_METHOD.Post])
async addUser() {
    const user: User = {
        name: this.body.name,
        gender: this.body.gender,
        address: this.body.address,
        emailId: this.body.emailId,
        password: this.body.password
    };
    const service = new UserService();
    const newUser = service.addUser(user);
    return jsonResult(newUser, HTTP_STATUS_CODE.Created);
}   

现在此方法的路由与方法名称相同,即 "addUser"。您可以通过向 https://:4000/user/addUser 发送一个带有用户数据在 body 中的 POST 请求来检查这一点。

但是我们希望路由是 "/",这样它就是一个 REST API。Worker 的路由是通过使用装饰器 "Route" 配置的。现在让我们更改路由。

@Worker([HTTP_METHOD.Post])
@Route("/")
async addUser() {
    const user: User = {
        name: this.body.name,
        gender: this.body.gender,
        address: this.body.address,
        emailId: this.body.emailId,
        password: this.body.password
    };
    const service = new UserService();
    const newUser = service.addUser(user);
    return jsonResult(newUser, HTTP_STATUS_CODE.Created);
}   

现在我们的端点已配置为 POST 请求。让我们通过向 https://:4000/user/ 发送一个带有用户数据在 body 中的 POST 请求来测试这一点。

post request

我们已经为 POST 请求创建了端点,但还有一件重要的事情要做,那就是数据验证。验证是服务器端 Web 应用程序的一个非常重要的组成部分。

FortJs 提供了一个名为 Guard 的组件来完成此类工作。根据 fortjs 文档:

引用

Guard 是 Worker 之上的安全层。它控制是否允许请求调用 Worker。

因此,我们将使用 Guard 进行数据验证。让我们使用 fort-creator 创建 Guard。执行命令 - "fort-creator add" 并选择 Guard。输入文件名 "UserValidatorGuard"。将在 _guards_ 文件夹中创建一个名为 "user_validator_guard.ts" 的文件,请打开该文件。

Guard 可以访问 body,因此您可以在其中验证数据。在 check 方法中返回 null 表示允许调用 Worker,返回其他任何内容表示阻止调用。

让我们通过编写代码进行验证来更深入地理解这一点。将以下代码粘贴到 "user_validator_guard.ts" 文件中。

import { Guard, HTTP_STATUS_CODE, textResult } from "fortjs";
import { User } from "../models/user";

export class UserValidatorGuard extends Guard {

    isValidEmail(email: string) {
        var re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|
                 (".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|
                 (([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
        return re.test(String(email).toLowerCase());
    }

    validate(user: User) {
        let errMessage;
        if (user.name == null || user.name.length < 5) {
            errMessage = "name should be minimum 5 characters"
        } else if (user.password == null || user.password.length < 5) {
            errMessage = "password should be minimum 5 characters";
        } else if (user.gender == null || ["male", "female"].indexOf(user.gender) < 0) {
            errMessage = "gender should be either male or female";
        } else if (user.emailId == null || !this.isValidEmail(user.emailId)) {
            errMessage = "email not valid";
        } else if (user.address == null || user.address.length < 10) {
            errMessage = "address length should be greater than 10";
        }
        return errMessage;
    }

    async check() {
        const user = new User(this.body);
        const errMsg = this.validate(user);
        if (errMsg == null) {
            // pass user to worker method, so that they dont need to parse again  
            this.data.user = user;
            // returning null means - this guard allows request to pass  
            return null;
        } else {
            return textResult(errMsg, HTTP_STATUS_CODE.BadRequest);
        }
    }
}  

在上面的代码中:

  • 我们创建了一个名为 validate 的方法,该方法接受 user 作为参数。它会验证 user,如果存在验证错误则返回错误消息,否则返回 null
  • 我们在 check 方法中编写代码,该方法是 Guard 生命周期的一部分。我们通过调用 validate 方法在其中进行用户验证。
  • 如果 user 有效,那么我们将通过 "data" 属性传递 user 值并返回 null。返回 null 表示 Guard 已允许此请求,并且应该调用 Worker。
  • 如果 user 无效,我们将返回一个文本响应和一个 HTTP 代码 - "badrequest" 作为错误消息。在这种情况下,执行将在此处停止,Worker 将不会被调用。

为了激活此 Guard,我们需要将其添加到 addUser 方法中。Guard 是通过使用装饰器 "Guards" 添加的。所以让我们添加 Guard。

@Guards([UserValidatorGuard])
@Worker([HTTP_METHOD.Post])
@Route("/")
async addUser() {
    const user: User = this.data.user;
    const service = new UserService();
    const newUser = service.addUser(user);
    return jsonResult(newUser, HTTP_STATUS_CODE.Created);
}

在上面的代码中:

  • 我已使用 Guards 装饰器添加了 "UserValidatorGuard"。
  • 有了 Guard 在流程中,我们不再需要在 Worker 中解析 body 中的数据,我们是从 "ModelUserGuard" 中传递的 this.data 读取它的。
  • 只有当 Guard 允许时(即所有数据都有效)才会调用 "addUser" 方法。

需要注意的是 - 使用组件后,"addUser" 方法看起来非常简洁,并且它还执行验证。您可以向 Worker 添加多个 Guard,这使您能够将代码模块化为多个 Guard。

让我们尝试添加一些无效数据来创建用户。

post request with invalid data

正如您在图片中看到的,我尝试发送无效的电子邮件,结果是 - "email not valid"。这意味着 Guard 已激活并正常工作。

PUT

让我们添加另一个方法 - "updateUser",路由为 "/",Guard - "UserValidatorGuard"(用于用户验证),最重要的是 - Worker 的 HTTP 方法为 "PUT"。

@Worker([HTTP_METHOD.Put])
@Guards([UserValidatorGuard])
@Route("/")
async updateUser() {
    const user: User = this.data.user;
    const service = new UserService();
    const userUpdated = service.updateUser(user);
    if (userUpdated === true) {
        return textResult("user updated");
    }
    else {
        return textResult("invalid user");
    }
}

更新代码与插入代码类似,除了功能方面,即数据更新。在这里,我们重新利用 UserValidatorGuard 来验证数据。

删除

为了删除数据,用户需要传递用户的 id。这可以通过三种方式传递:

  • 将数据放在 body 中,就像我们为 add 和 update 所做的那样。
  • 将数据放在查询字符串中。
  • 将数据放在路由中 - 为此,我们需要自定义路由。

我们已经实现了从 body 获取数据。所以让我们看看其他两种方式。

将数据放在查询字符串中

让我们创建方法 "removeByQueryString" 并粘贴以下代码。

@Worker([HTTP_METHOD.Delete])
@Route("/")
async removeByQueryString() {
    // taking id from query string
    const userId = Number(this.query.id);

    const service = new UserService();
    const user = service.getUser(userId);
    if (user != null) {
        service.removeUser(userId);
        return textResult("user deleted");
    }
    else {
        return textResult("invalid user");
    }
}   

上面的代码非常简单。它从控制器中的 query 属性获取 id 并删除用户。让我们测试一下。

delete user using query string

将数据放在路由中

您可以通过在路由中使用 "{var}" 来参数化路由。让我们看看如何?

让我们创建另一个方法 "removeByRoute" 并粘贴以下代码。

@Worker([HTTP_METHOD.Delete])
@Route("/{id}")
async removeByRoute() {
    // taking id from route
    const userId = Number(this.param.id);

    const service = new UserService();
    const user = service.getUser(userId);
    if (user != null) {
        service.removeUser(userId);
        return textResult("user deleted");
    }
    else {
        return textResult("invalid user");
    }
}

上面的代码与 removeByQueryString 完全相同,除了它从路由中提取 id 并使用路由中的参数,即 "/{id}",其中 id 是参数。

让我们测试一下。

delete user using route

因此,我们使用 fortjs 成功创建了 REST API。

关注点

为 fortjs 编写的代码清晰、可读且可维护。组件有助于您将代码模块化。

参考文献

  1. http://fortjs.info/
  2. https://medium.com/fortjs/rest-api-using-typescript-94004d9ae5e6
© . All rights reserved.