使用 fortjs 框架进行 NodeJs 开发
本文介绍如何使用 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 框架,其工作方式类似于真实的堡垒。它以组件的形式提供模块化。有三个组件:
- Wall - 这是一个顶级组件,每个 HTTP 请求首先通过 Wall。
- Shield - 这个可用于控制器。您可以为控制器分配一个 Shield,它将在调用控制器之前执行。
- Guard - 这个可用于控制器内的称为 fort.js 中 Worker 的方法。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/。
您应该在浏览器中看到类似这样的内容。
让我们看看这个页面是如何渲染的。
- 在您喜欢的代码编辑器中打开项目文件夹,我将使用 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
- 添加用户 - 应使用 HTTP 方法 "
POST
" - 删除用户 - 应使用 HTTP 方法 "
REMOVE
" - 获取用户 - 应使用 HTTP 方法 "
GET
" - 更新用户 - 应使用 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());
}
}
在这里,我们使用装饰器 DefaultWorker
。DefaultWorker
执行两项操作 - 它添加路由 "/
" 和 HTTP 方法 "GET
"。这是对此场景的快捷方式。在下一部分中,我们将使用其他装饰器来定制路由。
让我们通过调用 URL https://:4000/user 来测试这一点。您可以在浏览器中打开它,或者使用任何 HTTP 客户端工具,如 Postman 或 Advanced Rest Client。我使用的是 Advanced Rest Client。
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 请求创建了端点,但还有一件重要的事情要做,那就是数据验证。验证是服务器端 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。
让我们尝试添加一些无效数据来创建用户。
正如您在图片中看到的,我尝试发送无效的电子邮件,结果是 - "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 并删除用户。让我们测试一下。
将数据放在路由中
您可以通过在路由中使用 "{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
是参数。
让我们测试一下。
因此,我们使用 fortjs 成功创建了 REST API。
关注点
为 fortjs 编写的代码清晰、可读且可维护。组件有助于您将代码模块化。