简单的基于CRUD的模块,是任何企业的共同要求,应该是简单的构建和维护。Remult是一个全面的框架,它允许开发人员只使用TypeScript代码来构建全栈式、类型安全的应用程序。
本文将介绍Remult的基本概念,并将展示如何使用Remult来简化和加快你的网络应用程序开发过程!
在本指南中,我们将创建一个简单的预订表单,我们将把表单提交存储在MongoDB集合中。我们将使用React构建UI,然后用Spectre.css添加样式。
了解Remult框架
Remult是一个CRUD框架,使用TypeScript实体进行CRUD操作。它还提供了一个类型安全的API客户端和一个用于后端数据库操作的ORM。
这个框架抽象化并减少了你的应用程序中的模板代码。它使使用TypeScript构建全栈应用程序变得容易,也使开发人员能够与其他框架(如Express.js和Angular)集成。
Remult是一个中间地带。它不强迫你以某种方式工作;相反,它为你的项目提供了许多选项。
用Remult设置React项目
让我们先用Create React App创建一个React项目,并选择TypeScript模板:
> npx create-react-app remult-react-booking-app --template typescript
> cd remult-react-booking-app
接下来,我们将安装所需的依赖项:
> npm i axios express remult dotenv
> npm i -D @types/express ts-node-dev concurrently
在上面的代码中,我们使用的是concurrently 包。这个包是必需的,因为我们将从React项目的根部同时提供客户端和服务器代码。
现在,为服务器创建一个tsconfig 文件,像这样:
// tsconfig.server.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"emitDecoratorMetadata": true
}
}
然后,在主tsconfig.json 文件中,添加experimentalDecorators 选项以启用装饰器的使用:
// tsconfig.json
{
"compilerOptions": {
// ...
"experimentalDecorators": true
},
}
更新package.json 文件,像这样:
// package.json
{
"proxy": "http://localhost:3002",
// ...
"scripts": {
// ...
"start:dev": "concurrently -k -n \"SERVER,WEB\" -c \"bgBlue.bold,bgGreen.bold\" \"ts-node-dev -P tsconfig.server.json src/server/\" \"react-scripts start\""
},
}
在这里,我们添加了proxy 选项,让webpack开发服务器知道当应用程序在本地环境中运行时要代理3000到3002端口的API请求。我们还添加了一个npm脚本来同时启动前端和API开发服务器。
初始化remultExpress 中间件
现在,让我们在Create React App创建的src 文件夹内创建一个server 文件夹,并创建一个api.ts 文件,将初始化remultExpress 中间件:
// src/server/api.ts
import { remultExpress } from "remult/remult-express";
export const api = remultExpress();
接下来,为服务器创建一个.env 文件,并指定API端口号:
// src/server/.env
API_PORT=3002
接下来,创建一个index.ts 文件,作为服务器的根文件,初始化express ,加载环境变量,并注册remultExpress 中间件:
// src/server/index.ts
import { config } from "dotenv";
config({ path: __dirname + "/.env" });
import express from "express";
import { api } from "./api";
const app = express();
app.use(api);
app.listen(process.env.API_PORT || 3002, () => console.log("Server started"));
在前端初始化Remult
我们将使用React应用程序中的全局Remult 对象,通过axios HTTP客户端与API服务器通信:
// src/common.ts
import axios from "axios";
import { Remult } from "remult";
export const remult = new Remult(axios);
在这一点上,主要的项目设置已经完成,准备在本地服务器上运行。
使用下面的命令:
> npm run start:dev
添加数据库连接
在本指南中,我们将使用MongoDB来存储我们的表单提交。要为Remult设置MongoDB连接池,请使用remultExpress 中间件的dataProvider 选项。
首先,你必须在你的项目中安装mongodb ,作为一个依赖项,像这样:
> npm i mongodb
dataProvider 选项可以接受一个async() 函数,该函数连接到 MongoDB 并返回MongoDataProvider 对象,该对象充当 Remult 的连接器:
// src/server/api.ts
import { MongoDataProvider } from "remult/remult-mongo";
export const api = remultExpress({
dataProvider: async () => {
const client = new MongoClient(process.env.MONGO_URL || "");
await client.connect();
console.log("Database connected");
return new MongoDataProvider(client.db("remult-booking"), client);
},
});
用Remult实体生成API端点
实体被Remult用于生成API端点、API查询和数据库命令。entity ,作为模型类用于前端和后端代码。
我们将需要两个实体,以便定义预订对象和每天的可用时段。
在src 内创建一个shared 文件夹,它将包括前端和后端共享的代码。然后,在shared 文件夹中创建另一个子文件夹用于存储实体,并创建实体类文件:Booking.entity.ts 和Slot.entity.ts 。
要创建一个实体,定义一个具有所需属性的类,并使用@Entity 装饰器。@Entity 装饰器接受一个用于确定API路线的基本参数,默认的数据库集合或表名,以及一个用于定义实体相关属性和操作的选项参数。
在本指南中,Slot 实体可以被定义如下:
// src/shared/entities/Slot.entity.ts
import { Entity, Fields, IdEntity } from "remult";
@Entity("slots")
export class Slot extends IdEntity {
@Fields.string()
startTime: String;
@Fields.string()
endTime: String;
}
@Fields.string 装饰器定义了一个类型为String 的实体数据字段。这个装饰器也被用来描述字段相关的属性,如验证规则和操作:
// src/shared/entities/Booking.entity.ts
import { Entity, Fields, IdEntity, Validators } from "remult";
@Entity("bookings", {
allowApiCrud: true
})
export class Booking extends IdEntity {
@Fields.string({
validate: Validators.required,
})
name: String;
@Fields.string({
validate: Validators.required,
})
email: String;
@Fields.string({ validate: Validators.required })
description: String;
@Fields.string({
validate: Validators.required,
})
date: String;
@Fields.string({
validate: Validators.required,
})
slotId: string;
}
现在这两个实体都被定义了,让我们把它们添加到remultExpress 中间件的entities 属性。我们还可以使用initApi 属性将初始数据种到槽集合中:
// src/server/api.ts
import { Slot } from "../shared/entities/Slot.entity";
import { Booking } from "../shared/entities/Booking.entity";
export const api = remultExpress({
entities: [Slot, Booking],
initApi: async (remult) => {
const slotRepo = remult.repo(Slot);
const shouldAddAvailablSlots = (await slotRepo.count()) === 0;
if (shouldAddAvailablSlots) {
const availableSlots = [10, 11, 12, 13, 14, 15, 16, 17].map((time) => ({
startTime: `${time}:00`,
endTime: `${time}:45`,
}));
await slotRepo.insert(availableSlots);
}
},
dataProvider: async () => {
// ...
},
});
构建和样式化前台
让我们开始在应用程序的前端工作,建立表单用户界面:

首先,用以下代码替换src/App.tsx 文件中的默认模板代码:
// src/App.tsx
import "./App.css";
import { BookingForm } from "./components/BookingForm";
function App() {
return (
<div className="App">
<header className="hero hero-sm bg-primary ">
<div className="hero-body text-center">
<div className="container grid-md">
<h1>Book an appointment</h1>
</div>
</div>
</header>
<BookingForm />
</div>
);
}
export default App;
现在,让我们添加Spectre.css库,以使用户界面看起来更美观:
> npm i spectre.css
你可以参考下面的代码,了解BookingForm 组件:
// src/components/BookingForm.tsx
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { remult } from "../common";
import { Booking } from "../shared/entities/Booking.entity";
import { Slot } from "../shared/entities/Slot.entity";
const bookingRepo = remult.repo(Booking);
export const BookingForm = () => {
const {
register,
handleSubmit,
setValue,
watch,
setError,
clearErrors,
reset,
formState: { errors },
} = useForm();
const [availableDates, setAvailableDates] = useState<string[]>([]);
const [availableSlots, setAvailableSlots] = useState<Slot[]>([]);
const [isSubmitting, setSubmitting] = useState<boolean>(false);
const bookingDate = watch("date");
const onSubmit = async (values: Record<string, any>) => {
try {
setSubmitting(true);
const data = await bookingRepo.save(values);
console.log({ data });
reset();
} catch (error: any) {
setError("formError", {
message: error?.message,
});
} finally {
setSubmitting(false);
}
};
// JSX code
return (
<form onSubmit={handleSubmit(onSubmit)}>
<>...</>
</form>
);
};
这里,我们使用react-hook-form 库来管理表单状态和输入值。
为了在bookings 集合中保存提交的值,我们需要为Booking 实体创建一个资源库对象:
const bookingRepo = remult.repo(Booking);
Remult资源库对象提供了对实体进行CRUD操作的方法。在这种情况下,我们使用save() 存储库方法将数据插入集合中:
await bookingRepo.save(values);
添加仅有后台的方法
有时,你可能想创建带有额外逻辑的自定义API,比如发送电子邮件、执行多个数据库操作或完成其他连续的任务。
多个数据库操作必须只在后端执行,因为在前端有各种实体级功能可能会影响应用程序的性能。
在Remult中实现只在后端的方法的一种方法是创建一个控制器类,并使用@BackendMethod 装饰器。
对于我们项目的预订表格,让我们创建两个后端方法。第一个方法,getAvailableDates() ,将获得未来五个可用的工作日。第二个方法,getAvailableSlots() ,将按日期获得可用的预订时段:
// src/shared/controllers/Booking.controller.ts
import { BackendMethod, Remult } from "remult";
import { Booking } from "../entities/Booking.entity";
import { Slot } from "../entities/Slot.entity";
import { addWeekDays, formattedDate } from "../utils/date";
export class BookingsController {
@BackendMethod({ allowed: true })
static async getAvailableDates() {
const addDates = (date: Date, count = 0) =>
formattedDate(addWeekDays(date, count));
return Array.from({ length: 5 }).map((v, idx) => addDates(new Date(), idx));
}
@BackendMethod({ allowed: true })
static async getAvailableSlots(date: string, remult?: Remult) {
if (!remult) return [];
const unavailableSlotIds = (
await remult.repo(Booking).find({ where: { date } })
).map((booking) => booking.slotId);
const availableSlots = await remult
.repo(Slot)
.find({ where: { id: { $ne: unavailableSlotIds } } });
return availableSlots;
}
}
@BackendMethod 装饰器中的allowed 属性定义了请求的用户是否可以访问API。在这种情况下,它是真的,因为我们希望API是公开的。
你可以有授权规则来控制allowed 属性的值。后端方法也可以访问remult 对象,以便执行DB操作。
要使用后端方法,你不需要手动调用任何API。只要在你的前台代码中导入控制器,然后像其他模块一样直接调用这些方法。
在内部,Remult使用你初始化Remult时在前端代码中定义的HTTP客户端为你进行API调用。这样一来,你就可以保证API的类型安全,而且更容易维护:
// src/components/BookingForm.tsx
import { BookingsController } from "../shared/controllers/Booking.controller";
export const BookingForm = () => {
// ...
useEffect(() => {
BookingsController.getAvailableDates().then(setAvailableDates);
}, []);
useEffect(() => {
if (!availableDates.length) return;
setValue("date", availableDates[0]);
BookingsController.getAvailableSlots(availableDates[0]).then(
setAvailableSlots
);
}, [availableDates]);
useEffect(() => {
BookingsController.getAvailableSlots(bookingDate).then(setAvailableSlots);
}, [bookingDate]);
useEffect(() => {
setValue("slotId", availableSlots[0]?.id);
}, [availableSlots]);
// ...
}
如下图所示,日期和可用的下拉表单字段现在是默认预填的:


如果我们试图用不完整的值提交表单,在Booking 实体中添加的验证规则将失败并返回一个错误:

要查看本文的完整代码,请看GitHub repo。
总结
Remult是一个伟大的框架,它允许你快速而轻松地构建类型安全的全栈应用程序。它简单明了的语法使Remult成为任何想要开始类型安全编程的开发者的完美工具。你可以查看官方文档,了解本指南中所涉及的方法的更深入的解释。
那么你还在等什么呢?今天就来试试Remult吧