欢迎来到MEAN堆栈教程!本教程将教您如何使用MEAN堆栈构建全栈Web应用程序。最终项目将是一个员工管理系统。您可以在GitHub上找到本教程的源代码。
但首先,让我们从基础知识开始。
MEAN是一种用于构建全栈应用程序的技术堆栈。它由以下技术组合而成
使用MEAN堆栈构建的应用程序遵循客户端-服务器架构。客户端,由Angular构建,可以是Web应用程序、原生移动应用程序或桌面应用程序。客户端通过API与服务器通信,该API由Express构建。然后服务器使用MongoDB数据库管理请求。
客户端-服务器架构
在本教程中,我们将构建一个RESTful API,该API实现了员工管理应用程序的CRUD(创建、读取、更新、删除)操作。对于数据持久性,我们将使用MongoDB Atlas集群。
我们将构建一个员工管理Web应用程序。界面将包含以下页面
这是我们的最终应用程序的样貌
您需要Node.js和MongoDB Atlas集群才能遵循本教程。
node --version
让我们首先创建一个目录来托管我们的项目和文件。我们将将其命名为mean-stack-example
。
mkdir mean-stack-example
cd mean-stack-example
在本教程中,我们将使用shell命令来创建目录和文件。但是,您完全可以使用任何其他方法。
现在,让我们创建托管我们服务器端应用程序的目录和文件 — server
和server/src
— 并为其初始化一个package.json
文件。
mkdir server && mkdir server/src
cd server
npm init -y
touch tsconfig.json .env
(cd src && touch database.ts employee.routes.ts employee.ts server.ts)
我们需要一些外部包来构建RESTful API和连接到我们的MongoDB Atlas集群。我们将使用npm install
命令安装它们。
npm install cors dotenv express mongodb
我们还将为我们的服务器应用程序使用TypeScript。我们将使用带有--save-dev
标志的npm install
命令安装TypeScript编译器和相应的@types
包作为开发依赖项。这些包仅在开发期间使用,不应包含在最终的生产应用程序中。
npm install --save-dev typescript @types/cors @types/express @types/node ts-node
最后,我们将以下内容粘贴到TypeScript编译代码所使用的tsconfig.json
配置文件中。
mean-stack-example/server/tsconfig.json
{
"include": ["src/**/*"],
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"outDir": "./dist"
}
}
由于我们正在构建一个员工管理应用程序,主要的数据单元是员工。让我们创建一个Employee
接口,该接口将用于定义员工对象的架构。
mean-stack-example/server/src/employee.ts
import * as mongodb from "mongodb";
export interface Employee {
name: string;
position: string;
level: "junior" | "mid" | "senior";
_id?: mongodb.ObjectId;
}
我们的员工应该有姓名、职位和等级。由于_id
字段由MongoDB生成,因此它是可选的。因此,当我们创建一个新的员工时,我们不需要指定它。然而,当我们从数据库获取员工对象时,它将包含_id
字段。
让我们实现以下函数来连接到我们的数据库
mean-stack-example/server/src/database.ts
import * as mongodb from "mongodb";
import { Employee } from "./employee";
export const collections: {
employees?: mongodb.Collection<Employee>;
} = {};
export async function connectToDatabase(uri: string) {
const client = new mongodb.MongoClient(uri);
await client.connect();
const db = client.db("meanStackExample");
await applySchemaValidation(db);
const employeesCollection = db.collection<Employee>("employees");
collections.employees = employeesCollection;
}
// Update our existing collection with JSON schema validation so we know our documents will always match the shape of our Employee model, even if added elsewhere.
// For more information about schema validation, see this blog series: https://mongodb.ac.cn/blog/post/json-schema-validation--locking-down-your-model-the-smart-way
async function applySchemaValidation(db: mongodb.Db) {
const jsonSchema = {
$jsonSchema: {
bsonType: "object",
required: ["name", "position", "level"],
additionalProperties: false,
properties: {
_id: {},
name: {
bsonType: "string",
description: "'name' is required and is a string",
},
position: {
bsonType: "string",
description: "'position' is required and is a string",
minLength: 5
},
level: {
bsonType: "string",
description: "'level' is required and is one of 'junior', 'mid', or 'senior'",
enum: ["junior", "mid", "senior"],
},
},
},
};
// Try applying the modification to the collection, if the collection doesn't exist, create it
await db.command({
collMod: "employees",
validator: jsonSchema
}).catch(async (error: mongodb.MongoServerError) => {
if (error.codeName === "NamespaceNotFound") {
await db.createCollection("employees", {validator: jsonSchema});
}
});
}
我们使用作为mongodb
NPM包分发的MongoDB Node.js驱动程序。首先,我们使用提供的连接字符串创建一个新的MongoClient
对象。然后,我们连接到数据库。由于connect
是一个异步函数,我们需要使用await
。之后,我们从client
对象中获取db
对象。我们使用此对象来获取员工集合。请注意,变量employees
被转换为Employee
接口。这将为发送到数据库的任何查询提供类型检查。最后,我们将员工集合分配给从该文件导出的collections
对象。这样,我们可以从其他文件(如实现RESTful API的employee.routes.ts
文件)访问员工集合。
我们还使用JSON模式验证来确保所有文档都遵循我们的Employee
接口的形状。这是一种良好的做法,以确保我们不会意外地存储与我们的模型形状不匹配的数据。要了解更多关于JSON模式验证的信息,请参阅JSON Schema Validation - Locking down your model the smart way。
我们将持久化数据到MongoDB Atlas集群。要连接到我们的集群,我们需要设置一个包含连接字符串的ATLAS_URI
环境变量。将其添加到server/.env
文件中。确保用您的凭据替换用户名、密码和集群占位符。
mean-stack-example/server/.env
ATLAS_URI=mongodb+srv://<username>:<password>@<your-cluster>.mongodb.net/meanStackExample?retryWrites=true&w=majority
现在,我们可以使用dotenv
包加载环境变量。我们可以在任何文件中这样做,但最好尽早进行。由于server.ts
文件将是我们的入口点,让我们从它那里加载环境变量,连接到数据库,并启动服务器
mean-stack-example/server/src/server.ts
import * as dotenv from "dotenv";
import express from "express";
import cors from "cors";
import { connectToDatabase } from "./database";
// Load environment variables from the .env file, where the ATLAS_URI is configured
dotenv.config();
const { ATLAS_URI } = process.env;
if (!ATLAS_URI) {
console.error(
"No ATLAS_URI environment variable has been defined in config.env"
);
process.exit(1);
}
connectToDatabase(ATLAS_URI)
.then(() => {
const app = express();
app.use(cors());
// start the Express server
app.listen(5200, () => {
console.log(`Server running at https://127.0.0.1:5200...`);
});
})
.catch((error) => console.error(error));
让我们运行应用程序并查看一切是否正常工作
npx ts-node src/server.ts
你应该看到以下输出
Server running at https://127.0.0.1:5200...
做得好!现在,我们准备好构建员工的RESTful API了。
在本节中,我们将为员工实现GET
、POST
、PUT
和DELETE
端点。正如你所注意到的,这些HTTP方法对应于我们将对数据库中的员工执行的CRUD操作 - 创建、读取、更新和删除。唯一的限制是我们将有两个GET
端点:一个用于获取所有员工,一个用于通过ID获取单个员工。
为了实现端点,我们将使用Express提供的路由器。我们将工作的文件是src/employee.routes.ts
。
让我们先实现GET /employees
端点,该端点允许我们获取数据库中的所有员工。
mean-stack-example/server/src/employee.routes.ts
import * as express from "express";
import { ObjectId } from "mongodb";
import { collections } from "./database";
export const employeeRouter = express.Router();
employeeRouter.use(express.json());
employeeRouter.get("/", async (_req, res) => {
try {
const employees = await collections?.employees?.find({}).toArray();
res.status(200).send(employees);
} catch (error) {
res.status(500).send(error instanceof Error ? error.message : "Unknown error");
}
});
我们正在使用 find()
方法。因为我们传递了一个空对象 {}
,所以我们会得到数据库中所有的员工。然后,我们将使用 toArray()
方法将游标转换为数组。最后,我们将员工数组发送给客户端。
您可能会注意到路由是 /
。这是因为我们将从这个文件中注册所有端点到 /employees
路由下。
接下来,我们将实现 GET /employees/:id
端点,这将允许我们通过 ID 获取单个员工。将以下内容追加到 employee.routes.ts
的底部
mean-stack-example/server/src/employee.routes.ts
employeeRouter.get("/:id", async (req, res) => {
try {
const id = req?.params?.id;
const query = { _id: new ObjectId(id) };
const employee = await collections?.employees?.findOne(query);
if (employee) {
res.status(200).send(employee);
} else {
res.status(404).send(`Failed to find an employee: ID ${id}`);
}
} catch (error) {
res.status(404).send(`Failed to find an employee: ID ${req?.params?.id}`);
}
});
员工的 ID 以参数的形式提供。我们使用 ObjectId
方法将字符串 ID 转换为 MongoDB ObjectId
对象。然后,我们使用 findOne()
方法查找具有给定 ID 的员工。如果找到员工,我们将将其发送给客户端。否则,我们将发送 "404 Not Found" 错误。
如果您想知道上述表达式中的 “?” 符号代表什么,它是一个 可选链操作符。它允许您在不抛出错误的情况下读取嵌套属性。如果属性不存在,表达式将评估为 undefined
,而不是抛出错误。
POST /employees
端点将允许我们创建一个新的员工。将以下内容追加到 employee.routes.ts
的底部
mean-stack-example/server/src/employee.routes.ts
employeeRouter.post("/", async (req, res) => {
try {
const employee = req.body;
const result = await collections?.employees?.insertOne(employee);
if (result?.acknowledged) {
res.status(201).send(`Created a new employee: ID ${result.insertedId}.`);
} else {
res.status(500).send("Failed to create a new employee.");
}
} catch (error) {
console.error(error);
res.status(400).send(error instanceof Error ? error.message : "Unknown error");
}
});
我们从客户端请求体中接收员工对象。我们将使用 insertOne()
方法将员工插入到数据库中。如果插入成功,我们将发送一个包含员工 ID 的 "201 Created" 响应。否则,我们将发送一个 "500 Internal Server Error" 响应。
PUT /employees/:id
端点将允许我们更新现有的员工。将以下内容追加到 employee.routes.ts
的底部
mean-stack-example/server/src/employee.routes.ts
employeeRouter.put("/:id", async (req, res) => {
try {
const id = req?.params?.id;
const employee = req.body;
const query = { _id: new ObjectId(id) };
const result = await collections?.employees?.updateOne(query, { $set: employee });
if (result && result.matchedCount) {
res.status(200).send(`Updated an employee: ID ${id}.`);
} else if (!result?.matchedCount) {
res.status(404).send(`Failed to find an employee: ID ${id}`);
} else {
res.status(304).send(`Failed to update an employee: ID ${id}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error(message);
res.status(400).send(message);
}
});
在这里,员工的 ID 以参数的形式提供,而员工对象在请求体中提供。我们使用 ObjectId
方法将字符串 ID 转换为 MongoDB ObjectId
对象。然后,我们使用 updateOne()
方法更新具有给定 ID 的员工。如果更新成功,我们将发送一个 "200 OK" 响应。否则,我们将发送一个 "304 Not Modified" 响应。
最后,DELETE /employees/:id
端点将允许我们删除现有的员工。将以下内容追加到 employee.routes.ts
的底部
mean-stack-example/server/src/employee.routes.ts
employeeRouter.delete("/:id", async (req, res) => {
try {
const id = req?.params?.id;
const query = { _id: new ObjectId(id) };
const result = await collections?.employees?.deleteOne(query);
if (result && result.deletedCount) {
res.status(202).send(`Removed an employee: ID ${id}`);
} else if (!result) {
res.status(400).send(`Failed to remove an employee: ID ${id}`);
} else if (!result.deletedCount) {
res.status(404).send(`Failed to find an employee: ID ${id}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
console.error(message);
res.status(400).send(message);
}
});
与前面的端点类似,我们根据传递的参数 ID 向数据库发送查询。我们使用 deleteOne()
方法删除员工。如果删除成功,我们将发送一个 "202 Accepted" 响应。否则,我们将发送一个 "400 Bad Request" 响应。如果员工未找到(result.deletedCount
为 0),我们将发送一个 "404 Not Found" 响应。
现在,我们需要指导 Express 服务器使用我们定义的路由。首先,在 src/server.ts
的开始处导入 employeesRouter
。
mean-stack-example/server/src/server.ts
import { employeeRouter } from "./employee.routes";
然后,在 app.listen()
调用之前添加以下内容
mean-stack-example/server/src/server.ts
app.use("/employees", employeeRouter);
最后,通过停止 shell 进程并再次运行它来重启服务器。
npx ts-node src/server.ts
您应该在控制台看到与之前相同的消息。
Server running at https://127.0.0.1:5200...
做得好!我们已经为我们的员工构建了一个简单的 RESTful API。现在,让我们构建一个 Angular 网络应用程序来与之交互!
下一步是构建一个客户端 Angular 网络应用程序,它将与我们的 RESTful API 交互。我们将使用 Angular CLI 来生成应用程序。要安装它,打开一个新的终端标签并运行以下命令
npm install -g @angular/cli
在您正在开发客户端应用程序时,保持服务器运行非常重要。为此,您需要在终端中打开一个新的标签页,在该标签页中执行客户端应用程序的命令。同时,请确保您已导航到 mean-stack-example
目录。
安装完成后,导航到项目的根目录,并运行以下命令以创建一个新的Angular应用程序
ng new client --inline-template --inline-style --minimal --routing --style=css
这将在一个名为 client
的目录中创建一个新的Angular应用程序。 --inline-template
和 --inline-style
标志表示我们将直接在组件文件中包含HTML模板和CSS样式,而不是将它们拆分为单独的文件。 --minimal
标志将跳过任何测试配置,因为我们不会在本教程中涉及此内容。 --routing
标志将生成一个路由模块。 --style=css
标志将启用CSS预处理器。
安装依赖项可能需要一些时间。安装完成后,导航到新应用程序并运行以下命令以启动它
cd client
ng serve -o
应用程序构建完成后,您应该在浏览器窗口中看到一个新标签页,其中运行着应用程序。它应该显示 "欢迎来到客户端!"
最后,对于样式,我们将使用 Angular Material。要从 client
目录中的新终端运行以下命令来安装它。在提示时,选择一个颜色主题以及默认选项
ng add @angular/material
类似于我们的服务器端应用程序,我们将为我们的员工创建一个Angular界面。我们将使用 Employee
接口来定义员工对象的属性。打开一个新的终端窗口并运行以下命令来创建接口
ng generate interface employee
然后,在您的编辑器中打开新创建的 src/app/employee.ts
文件,并添加以下属性
mean-stack-example/client/src/app/employee.ts
export interface Employee {
name: string;
position: string;
level: 'junior' | 'mid' | 'senior';
_id?: string;
}
Angular建议将您的业务逻辑与您的展示逻辑分离。这就是为什么我们将创建一个 服务 来处理与API的 /employee
端点的所有通信。该服务将被应用程序中的组件使用。要生成服务,请运行以下命令
ng generate service employee
ng generate service
命令在 src/app/employee.service.ts
文件中创建一个新的服务。将此文件的内容替换为以下内容
mean-stack-example/client/src/app/employee.service.ts
import { Injectable, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Employee } from './employee';
@Injectable({
providedIn: 'root'
})
export class EmployeeService {
private url = 'https://127.0.0.1:5200';
employees$ = signal<Employee[]>([]);
employee$ = signal<Employee>({} as Employee);
constructor(private httpClient: HttpClient) { }
private refreshEmployees() {
this.httpClient.get<Employee[]>(`${this.url}/employees`)
.subscribe(employees => {
this.employees$.set(employees);
});
}
getEmployees() {
this.refreshEmployees();
return this.employees$();
}
getEmployee(id: string) {
this.httpClient.get<Employee>(`${this.url}/employees/${id}`).subscribe(employee => {
this.employee$.set(employee);
return this.employee$();
});
}
createEmployee(employee: Employee) {
return this.httpClient.post(`${this.url}/employees`, employee, { responseType: 'text' });
}
updateEmployee(id: string, employee: Employee) {
return this.httpClient.put(`${this.url}/employees/${id}`, employee, { responseType: 'text' });
}
deleteEmployee(id: string) {
return this.httpClient.delete(`${this.url}/employees/${id}`, { responseType: 'text' });
}
}
我们使用 HttpClient
服务来向我们的API发出HTTP请求。 refreshEmployees()
方法用于获取完整的员工列表,并将其保存到名为 employees$
的信号中,该信号将可供应用程序的其余部分访问。
HttpClient
服务由Angular通过 provideHttpClient
函数提供。它默认不是应用程序的一部分——我们需要将其导入到 app.config.ts
文件中。首先,将以下导入添加到 app.config.ts
文件的顶部
mean-stack-example/client/src/app/app.config.ts
import { provideHttpClient, withFetch } from '@angular/common/http';
然后,将函数添加到 ApplicationConfig
变量的提供者列表中
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withFetch()),
// ...
],
};
withFetch
函数用于配置 HttpClient
服务以使用 fetch
API。
接下来,我们将创建一个新页面,显示我们的员工表格。
让我们为员工表格创建一个新页面。在Angular中,组件是一个可重用的代码块,可以用来显示视图。我们将创建一个名为 EmployeesList
的新组件,并将其注册为应用程序中的 /employees
路由。
要生成组件,请运行以下命令
ng generate component employees-list
Angular CLI 在 src/app/employees-list.component.ts
文件中生成了一个名为 EmployeesListComponent
的新组件。将 src/app/employees-list.component.ts
文件的内容替换为以下内容
mean-stack-example/client/src/app/employees-list/employees-list.component.ts
import { Component, OnInit, WritableSignal } from '@angular/core';
import { Employee } from '../employee';
import { EmployeeService } from '../employee.service';
import { RouterModule } from '@angular/router';
import { MatTableModule } from '@angular/material/table';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-employees-list',
standalone: true,
imports: [RouterModule, MatTableModule, MatButtonModule, MatCardModule],
styles: [
`
table {
width: 100%;
button:first-of-type {
margin-right: 1rem;
}
}
`,
],
template: `
<mat-card>
<mat-card-header>
<mat-card-title>Employees List</mat-card-title>
</mat-card-header>
<mat-card-content>
<table mat-table [dataSource]="employees$()">
<ng-container matColumnDef="col-name">
<th mat-header-cell *matHeaderCellDef>Name</th>
<td mat-cell *matCellDef="let element">{{ element.name }}</td>
</ng-container>
<ng-container matColumnDef="col-position">
<th mat-header-cell *matHeaderCellDef>Position</th>
<td mat-cell *matCellDef="let element">{{ element.position }}</td>
</ng-container>
<ng-container matColumnDef="col-level">
<th mat-header-cell *matHeaderCellDef>Level</th>
<td mat-cell *matCellDef="let element">{{ element.level }}</td>
</ng-container>
<ng-container matColumnDef="col-action">
<th mat-header-cell *matHeaderCellDef>Action</th>
<td mat-cell *matCellDef="let element">
<button mat-raised-button [routerLink]="['edit/', element._id]">
Edit
</button>
<button
mat-raised-button
color="warn"
(click)="deleteEmployee(element._id || '')"
>
Delete
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" [routerLink]="['new']">
Add a New Employee
</button>
</mat-card-actions>
</mat-card>
`,
})
export class EmployeesListComponent implements OnInit {
employees$ = {} as WritableSignal<Employee[]>;
displayedColumns: string[] = [
'col-name',
'col-position',
'col-level',
'col-action',
];
constructor(private employeesService: EmployeeService) {}
ngOnInit() {
this.fetchEmployees();
}
deleteEmployee(id: string): void {
this.employeesService.deleteEmployee(id).subscribe({
next: () => this.fetchEmployees(),
});
}
private fetchEmployees(): void {
this.employees$ = this.employeesService.employees$;
this.employeesService.getEmployees();
}
}
如您所注意到的,EmployeesListComponent
是一个带有 @Component
装饰器的 TypeScript 类。@Component
装饰器用于指示这个类是一个组件。selector
属性用于指定用于显示此组件的 HTML 标签。剧透:我们不会使用此选择器。相反,我们将组件注册为一个路由。template
属性用于指定用于显示此组件的 HTML 模板。
类的实现包含了组件的逻辑。当组件在页面上渲染时,会调用 ngOnInit()
方法。这是一个获取员工列表的好地方。为此,我们使用我们之前创建的 EmployeeService
。getEmployees()
方法返回包含所有员工的信号。一旦数据可用,它将自动渲染员工列表。我们使用 Angular Material 来渲染显示员工的表格。
模板中还有一些操作——用于编辑、删除和添加新员工。使用 [routerLink]
属性导航到 /employees/edit/:id
路由,我们将在教程的后面实现。使用 (click)
事件调用 deleteEmployee()
方法。此方法在类中实现,使用 EmployeeService
删除员工。
现在我们已经创建了组件,我们需要将其注册为 app.routes.ts
文件中的一个路由。将文件的内容替换为以下内容
mean-stack-example/client/src/app/app.routes.ts
import { Routes } from '@angular/router';
import { EmployeesListComponent } from './employees-list/employees-list.component';
export const routes: Routes = [
{ path: '', component: EmployeesListComponent, title: 'Employees List' },
];
然后,转到 src/app/app.component.ts
文件,并将内容替换为以下内容
mean-stack-example/client/src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { EmployeesListComponent } from './employees-list/employees-list.component';
import { MatToolbarModule } from '@angular/material/toolbar';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, EmployeesListComponent, MatToolbarModule],
styles: [
`
main {
display: flex;
justify-content: center;
padding: 2rem 4rem;
}
`,
],
template: `
<mat-toolbar>
<span>Employees Management System</span>
</mat-toolbar>
<main>
<router-outlet />
</main>
`,
})
export class AppComponent {
title = 'client';
}
让我们刷新浏览器,看看是否一切正常。
我们有一个表格,但还没有看到任何员工。让我们创建一个新页面来添加员工。
我们需要一个表单来填写名称、职位和等级,以创建新员工。对于编辑现有员工,我们还需要一个类似的表单。让我们创建一个 EmployeeForm
组件,并用于添加和编辑。
ng g c employee-form
这里的 g
是 generate
的缩写,c
是 component
的缩写。
现在,我们可以使用 Angular 的 ReactiveFormsModule
和 FormBuilder
来创建一个响应式表单
mean-stack-example/client/src/app/employee-form/employee-form.component.ts
import { Component, effect, EventEmitter, input, Output } from '@angular/core';
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatRadioModule } from '@angular/material/radio';
import { MatButtonModule } from '@angular/material/button';
import { Employee } from '../employee';
@Component({
selector: 'app-employee-form',
standalone: true,
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatRadioModule,
MatButtonModule,
],
styles: `
.employee-form {
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 2rem;
}
.mat-mdc-radio-button ~ .mat-mdc-radio-button {
margin-left: 16px;
}
.mat-mdc-form-field {
width: 100%;
}
`,
template: `
<form
class="employee-form"
autocomplete="off"
[formGroup]="employeeForm"
(submit)="submitForm()"
>
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput placeholder="Name" formControlName="name" required />
@if (name.invalid) {
<mat-error>Name must be at least 3 characters long.</mat-error>
}
</mat-form-field>
<mat-form-field>
<mat-label>Position</mat-label>
<input
matInput
placeholder="Position"
formControlName="position"
required
/>
@if (position.invalid) {
<mat-error>Position must be at least 5 characters long.</mat-error>
}
</mat-form-field>
<mat-radio-group formControlName="level" aria-label="Select an option">
<mat-radio-button name="level" value="junior" required
>Junior</mat-radio-button
>
<mat-radio-button name="level" value="mid"
>Mid</mat-radio-button
>
<mat-radio-button name="level" value="senior"
>Senior</mat-radio-button
>
</mat-radio-group>
<br />
<button
mat-raised-button
color="primary"
type="submit"
[disabled]="employeeForm.invalid"
>
Add
</button>
</form>
`,
})
export class EmployeeFormComponent {
initialState = input<Employee>();
@Output()
formValuesChanged = new EventEmitter<Employee>();
@Output()
formSubmitted = new EventEmitter<Employee>();
employeeForm = this.formBuilder.group({
name: ['', [Validators.required, Validators.minLength(3)]],
position: ['', [Validators.required, Validators.minLength(5)]],
level: ['junior', [Validators.required]],
});
constructor(private formBuilder: FormBuilder) {
effect(() => {
this.employeeForm.setValue({
name: this.initialState()?.name || '',
position: this.initialState()?.position || '',
level: this.initialState()?.level || 'junior',
});
});
}
get name() {
return this.employeeForm.get('name')!;
}
get position() {
return this.employeeForm.get('position')!;
}
get level() {
return this.employeeForm.get('level')!;
}
submitForm() {
this.formSubmitted.emit(this.employeeForm.value as Employee);
}
}
这里有很多代码,但没有什么特别之处。我们只是使用 FormBuilder
创建了一个包含三个字段的响应式表单,并为表单添加了验证。如果表单有验证错误,模板将显示错误消息。我们使用 input()
将父组件的初始表单状态传递给表单。由于我们可能将异步数据传递到表单,因此 input()
的类型为 InputSignal<Employee>
。
例如,父组件可能从 API 获取员工数据并将其传递给表单。子组件将在新数据可用时收到通知。@Output()
是一个事件发射器,将在表单提交时发出表单值。父组件将处理提交并发送 API 调用。
下一步是实现 AddEmployeeComponent
组件。
ng generate component add-employee
将新创建的文件内容替换为以下内容:
mean-stack-example/client/src/app/add-employee/add-employee.component.ts
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { EmployeeFormComponent } from '../employee-form/employee-form.component';
import { Employee } from '../employee';
import { EmployeeService } from '../employee.service';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-add-employee',
standalone: true,
imports: [EmployeeFormComponent, MatCardModule],
template: `
<mat-card>
<mat-card-header>
<mat-card-title>Add a New Employee</mat-card-title>
</mat-card-header>
<mat-card-content>
<app-employee-form
(formSubmitted)="addEmployee($event)"
></app-employee-form>
</mat-card-content>
</mat-card>
`,
styles: ``,
})
export class AddEmployeeComponent {
constructor(
private router: Router,
private employeeService: EmployeeService
) {}
addEmployee(employee: Employee) {
this.employeeService.createEmployee(employee).subscribe({
next: () => {
this.router.navigate(['/']);
},
error: (error) => {
alert('Failed to create employee');
console.error(error);
},
});
this.employeeService.getEmployees();
}
}
我们使用 EmployeeFormComponent
,每当 AddEmployeeComponent
收到表单提交时,它将调用 EmployeeService
来创建员工。当员工创建成功时,EmployeeService
将发出事件,并且 AddEmployeeComponent
将导航回员工列表页面。
同时,让我们实现编辑员工组件
ng generate component edit-employee
将新创建的文件内容替换为以下内容:
mean-stack-example/client/src/app/edit-employee/edit-employee.component.ts
import { Component, OnInit, WritableSignal } from '@angular/core';
import { EmployeeFormComponent } from '../employee-form/employee-form.component';
import { ActivatedRoute, Router } from '@angular/router';
import { Employee } from '../employee';
import { EmployeeService } from '../employee.service';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-edit-employee',
standalone: true,
imports: [EmployeeFormComponent, MatCardModule],
template: `
<mat-card>
<mat-card-header>
<mat-card-title>Edit an Employee</mat-card-title>
</mat-card-header>
<mat-card-content>
<app-employee-form
[initialState]="employee()"
(formSubmitted)="editEmployee($event)"
></app-employee-form>
</mat-card-content>
</mat-card>
`,
styles: ``,
})
export class EditEmployeeComponent implements OnInit {
employee = {} as WritableSignal<Employee>;
constructor(
private router: Router,
private route: ActivatedRoute,
private employeeService: EmployeeService
) {}
ngOnInit() {
const id = this.route.snapshot.paramMap.get('id');
if (!id) {
alert('No id provided');
}
this.employeeService.getEmployee(id!);
this.employee = this.employeeService.employee$;
}
editEmployee(employee: Employee) {
this.employeeService
.updateEmployee(this.employee()._id || '', employee)
.subscribe({
next: () => {
this.router.navigate(['/']);
},
error: (error) => {
alert('Failed to update employee');
console.error(error);
},
});
}
}
与 AddEmployeeComponent
的唯一显著区别是我们从 URL 中获取员工 ID,从 API 中获取员工,然后在 ngOnInit()
方法中将它传递给表单。
最后,让我们添加新页面的导航
mean-stack-example/client/src/app/app-routing.module.ts
import { Routes } from '@angular/router';
import { EmployeesListComponent } from './employees-list/employees-list.component';
import { AddEmployeeComponent } from './add-employee/add-employee.component'; // <-- add this line
import { EditEmployeeComponent } from './edit-employee/edit-employee.component'; // <-- add this line
export const routes: Routes = [
{ path: '', component: EmployeesListComponent, title: 'Employees List' },
{ path: 'new', component: AddEmployeeComponent }, // <-- add this line
{ path: 'edit/:id', component: EditEmployeeComponent }, // <-- add this line
];
好的!让我们来测试一下!尝试创建一个新的员工。填写详细信息后,点击 提交 按钮。你应该能在列表中看到新员工。然后,你可以通过点击 编辑 按钮来尝试编辑它。你应该能看到填写了员工详细信息的表单。提交表单后,员工信息将在表中更新。最后,你可以通过点击 删除 按钮来删除它。
如果有什么不对劲的地方,你可以在 mean-stack-example 仓库中查看完成的项目。
感谢您与我一同经历这次旅程并跟随我的步伐!希望您喜欢它并学到了很多。MEAN 栈使得开始构建可扩展的应用变得容易。在整个技术栈中,您都在使用相同的语言:JavaScript。此外,MongoDB 的文档模型使得将数据映射到对象变得很自然,MongoDB Atlas 的永久免费集群使得无需担心成本即可轻松托管您的数据。