IBM全栈开发【13·上】:全栈应用程序开发者毕业设计
近期在学习IBM全栈应用开发微学士课程,故此记录学习笔记。
恭喜您成功完成 IBM 全栈软件开发人员专业证书的所有前面课程!现在是通过完成期末考试来检验您的新技能的时候了。
您将通过考试,了解自己在 PC 各门课程中学到的知识。现在,您应该已经熟练掌握了以下主题:
- 云计算的核心概念
- 网页开发语言,包括 HTML、CSS 和 JavaScript
- Git 和 GitHub
- Node.js、Express 和 React
- 容器
- Python
- 数据库概念和相关技术,如 SQL 和 Django
- 微服务和无服务器计算
这是 IBM 全栈软件开发人员专业证书的最后一门课程。它将测试您迄今为止所掌握的知识和技能。本课程包含分级期末考试,涵盖 PC 中各种课程的内容。
您将就以下主题接受评估:核心云计算概念;HTML、CSS、JavaScript 和 Python 等语言;Node.js、Express 和 React 等框架;以及 Docker、Kubernetes、OpenShift、SQL、Django、微服务和 Serverless 等后端技术。
在学习本课程之前,请确保您已完成 IBM 全栈开发人员专业证书中的所有先前课程。
通过附加功能丰富汽车经销商门户网站
该课程从模块开始:通过三个实验丰富汽车经销商门户网站的附加功能。
在“全栈应用程序开发项目”(可见文章)中,您创建了一个汽车经销商应用程序,需要开发前端页面、用户管理、构建数据库操作动作、创建后端服务以及配置 CI/CD 管道。前端使用的技术包括 HTML、CSS、JavaScript 和 React,后端使用的技术包括 Django、Node.js、NoSQL(Mongo)、容器化、IBM 代码引擎、Python 和 Kubernetes。
通过汽车经销商应用程序,我们可以根据经销商的 ID 查看所有经销商的详细信息和评论,还可以注册、登录为用户,并在注册或登录后为特定经销商添加新的评论,方法是将 About
和 Contact Us
等静态页面整合到一起。
用户注册和登录功能也是在为端点和 Django 视图配置了 Express-Mongo 后端微服务后实现的。还集成了一个情感分析微服务来分析评论。也实现了各种功能,如显示经销商列表、详细信息和评论,在 Django 应用程序中添加新的经销商评论。最后,将 CI/CD Linting 服务集成到应用程序中,并部署到 Kubernetes 上。
在本课程中,您将从增强同一个汽车经销商应用程序开始。改进的重点是前端方面,然后通过添加新的微服务转移到后端,最后在后续实验中将其与前端集成。
强烈建议先完成全栈应用程序开发项目课程,然后再学习本课程,因为本模块的实验是增强已建应用程序的一部分。虽然这些内容不属于评分标准的一部分,但它们对于增强你的理解和技能仍然很有价值。
概述
作为毕业设计项目的一部分,您已经成功创建并测试了 Car Dealerships website
,确保其符合预期功能。这包括允许用户查看所有经销商的详细信息、查看对特定经销商的现有评论,以及在作为注册用户登录后发布对特定经销商的新评论。
完成上述工作后,下一步就是增强应用程序的功能。这涉及到应用程序的前端和后端方面。如下所述,您将分三部分实施这些增强功能:
-
前端改进
- 将
Dealerships
页面上的States
下拉菜单转换为可搜索文本框,使用户能够通过输入搜索字符串来筛选经销商。 - 改进应用程序主页上导航栏和经销商按钮的配色方案,以及
Dealerships Review
页面上审查面板和审查图标的颜色。 - 微调经销商审查面板的视觉元素,对字体大小和字体对齐方式等方面进行调整。
- 将
-
汽车库存后端服务
- 使用 MongoDB 和 Node.js 服务器建立一个新的后端微型服务,专门用于获取与汽车库存相关的各种详细信息。
- 将新创建的微服务与 Django 应用程序的后端集成,并验证后端服务器的成功启动。
-
汽车库存服务的前端开发
- 开发前端组件,并将其与第 2 部分:汽车库存后端服务中开发的后端汽车库存微服务集成。
- 创建一个按品牌、型号、年份、里程和价格选择汽车的选项。
- 对 Django 应用程序与集成的汽车库存服务生成的输出进行全面测试。
前端改进
将 Dealerships
页面上的 States
下拉菜单转换为可搜索文本框
- 访问
frontend/src/components/Dealers/Dealers.jsx
文件。 - 您会注意到,经销商下拉菜单的代码是在
<select>
下拉菜单元素中显示的:1
2<select name="state" id="state" onChange={(e) => filterDealers(e.target.value)}>
</select> - 用包含以下属性的
<input>
字段取代现有的<select>
元素:- 用户可在文本框中输入搜索州。
- 根据输入的搜索查询过滤显示的经销商,并与州匹配。
- 当输入框失去焦点时,将显示的经销商重置为原始列表。
1
<input type="text" placeholder="Search states..." onChange={handleInputChange} onBlur={handleLostFocus} value={searchQuery} />
观察输入元素中的函数。现在,让我们创建它们。
- 创建一个名为
handleInputChange
的新函数,用于管理输入更改,并根据输入的状态查询过滤经销商。1
2
3
4
5
6
7
8const handleInputChange = (event) => {
const query = event.target.value;
setSearchQuery(query);
const filtered = originalDealers.filter(dealer =>
dealer.state.toLowerCase().includes(query.toLowerCase())
);
setDealersList(filtered);
};- 每次输入框内的值发生变化时,都会触发该函数。
- 它会检索用户在输入框中输入的当前值,并将其作为查询存储在
setSearchQuery
变量中,用于筛选经销商。 - 系统会生成一个新数组,其中只包含状态与输入的查询相匹配的经销商。
- 通过将查询和经销商状态转换为小写,使其大小写不敏感,从而确保用户获得更友好的搜索体验。
- 然后,该函数将显示与输入的状态查询相匹配的经销商。
- 总之,
handleInputChange
函数可根据用户在搜索栏中的输入动态过滤经销商列表。它实时更新显示的经销商列表,为用户提供响应式搜索功能。 - 创建
handleLostFocus
函数,以确保当用户将搜索输入留空并点击或滑动标签离开时,经销商列表会重置为原始列表。1
2
3
4
5const handleLostFocus = () => {
if (!searchQuery) {
setDealersList(originalDealers);
}
}- 该函数在执行时验证
searchQuery
状态是否为空。 - 如果
searchQuery
确实为空,它就会将经销商列表恢复到开始搜索前的原始状态。 handleLostFocus
函数在用户点击输入框外或标签页离开输入框时调用,由onBlur
事件触发。
- 该函数在执行时验证
- 将此代码与之前定义的其他状态变量一起添加:
1
const [searchQuery, setSearchQuery] = useState('');
- 这将利用
useState
钩子创建一个名为searchQuery
的状态变量和一个相应的函数setSearchQuery
来更新其值。 searchQuery
状态变量实时保存用户在搜索输入框中输入的值。当用户在搜索栏中输入时,该状态变量会通过调用setSearchQuery
进行更新,并触发组件的重新渲染,以反映对搜索查询所做的任何更改。
- 这将利用
- 初始化和设置状态变量,用于管理原始经销商列表。
- 添加以下代码,初始化
dealersList
、searchQuery
和states
变量及其设置函数:在这段代码中,状态变量1
const [originalDealers, setOriginalDealers] = useState([]);
originalDealers
及其设置函数setOriginalDealers
用于跟踪和更新原始经销商列表。 - 将下面的代码放在
getDealers
函数中更新其他状态(setStates
和setDealersList
)的地方。这一行用从应用程序接口获取的所有经销商数组设置状态变量1
setOriginalDealers(all_dealers);
originalDealers
,用于存储过滤前的原始经销商列表。
- 添加以下代码,初始化
- 确保保存所有更改。
- 运行以下命令来构建应用程序的客户端:
1
2cd /home/project/xrwvm-fullstack_developer_capstone/server/frontend
npm run build - 访问经销商详细信息页面,测试应用程序的输出。
- 请观察用于经销商搜索的搜索框的外观,它取代了之前的下拉框。
- 输入搜索查询,例如
Texas
,搜索该州来进行测试。
更改应用程序的配色方案
在本节中,您将了解在以下区域更改应用程序配色方案的步骤:
-
与应用程序主页有关的方面,包括:
- 更改导航栏的背景颜色。
- 修改
View Dealerships
按钮的背景颜色。
-
与
Dealership Review
页面有关的方面包括:- 调整与
Review
面板相关的背景颜色。 - 自定义与
Review
图标相关的悬浮边框。
- 调整与
-
更改导航栏的背景颜色
- 打开文件
frontend/static/Home.html
。 - 您将看到以下代码片段,它将当前的深绿色背景添加到应用程序中的导航栏:
1
<nav class="navbar navbar-expand-lg navbar-light" style={{backgroundColor:"darkturquoise",height:"1in"}}>
- 用您喜欢的颜色(如
mediumspringgreen
)代替它,以修改页眉的背景颜色。1
<nav class="navbar navbar-expand-lg navbar-light" style="background-color:mediumspringgreen; height: 1in;">
- 保存更改。
- 运行提供的命令构建客户端并显示上述更改:
1
npm run build
- 刷新应用页面。
- 观察导航栏背景的变化。
- 打开文件
-
修改
View Dealerships
按钮的背景颜色- 以下代码表示应用程序主页上
View Dealerships
按钮的背景颜色:1
<a href="/dealers" class="btn" style="background-color: aqua;margin:10px">View Dealerships</a>
- 将其调整为您喜欢的颜色(例如,
plum
,一种紫色)。1
<a href="/dealers" class="btn" style="background-color: plum; margin:10px">View Dealerships</a>
- 请观察
View Dealerships
按钮的最新配色方案。
- 以下代码表示应用程序主页上
-
调整与
Review
面板相关的背景颜色Dealers.css
文件中的样式是为应用程序中的Dealerships
和Reviews
面板定制的。review_panel
类包含用于设计经销商审查面板样式的代码。- 请注意,它的边框是纯灰色的。
1
border: solid grey;
- 调整代码,使其具有纯紫色边框:
1
border: solid purple;
- 你会发现
Review
面板的边框变成了纯紫色,外观也发生了变化。
-
自定义与
Review
图标相连的悬浮边框- 当用户将鼠标悬停在
Review
图标上时,.review_icon:hover
类将为其应用样式。 - 请注意,悬停时它的背景是纯浅灰色。
1
border: solid lightgray;
- 将颜色调整为黑色色调,边框变细(如 2 像素),以便在悬停时呈现出鲜明而纤细的背景外观。
1
border: 2px solid #080808;
- 观察修改后的图标,它带有细细的黑色边框,悬停时会显示出明显的存在感。
- 当用户将鼠标悬停在
更改应用程序的外观和感觉
在本节中,您将学习如何改进应用程序中 Dealer Review
面板的视觉效果。
其中包括:
- 调整字体大小
- 确保字体对齐正确,以改善整体外观
-
调整字体大小
.reviewer
类定义了用户评论的样式。- 当前字体大小设置为小。
- 将字体大小调整到特定值(例如 18 像素),以放大
Review
文本,提高可读性。1
font-size: 18px;
-
确保字体对齐正确,改善整体外观
- 文本对齐属性并未被指定。
- 因此,浏览器的默认文本对齐方式(通常为左对齐)会被应用,从而产生左对齐的
Review
文本。 - 将文本居中,以确保
User reviews
显示在Review
窗格的中心。1
text-align: center;
汽车库存后端服务
使用 MongoDB 和 Node.js 开发新的后端汽车库存微服务
-
打开一个新的终端窗口,导航到
xrwvm-fullstack_developer_capstone/server
目录。
生成一个名为carsInventory
的新目录。1
mkdir carsInventory
-
执行以下命令,初始化一个新的 Node.js 项目,并在
carsInvent
目录下创建package.json
文件:1
npm init
将以下应用程序依赖项添加到
package.json
中:1
2
3
4"cors": "^2.8.5",
"express": "^4.18.2",
"mongodb": "^6.3.0",
"mongoose": "^8.0.1"这些依赖项对于启用 CORS(跨源资源共享)、处理网络应用程序路由和中间件(Express)、与 MongoDB 交互(MongoDB 驱动程序)以及在使用 MongoDB 的 Node.js 应用程序中提供便捷的数据建模方式(Mongoose)至关重要。
每个依赖关系中版本号前的
^
符号允许在运行npm install
命令时安装兼容的未来更新。将
name
设置为carsInventory
,将main
的值设置为app.js
。这样,package.json
文件看起来应该与下面相似:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17{
"name": "carsInventory",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"mongodb": "^6.3.0",
"mongoose": "^8.0.1"
}
}执行命令安装这些依赖项:
1
npm install
-
在名为
inventory.js
的文件中设置 MongoDB schema。现在,您将使用mongoose
库为名为cars
的集合建立 MongoDB schema。
这将用于创建一个与 MongoDB 数据库交互的mongoose
模型,使应用程序能够以更有条理、更有组织的方式对汽车文档执行 CRUD 操作。
schema 应定义汽车文件的结构,其中应包括以下字段及其数据类型:dealer_id
:Number
make
:String
model
:String
bodyType
:String
year
:Number
mileage
:Number
price
:Number
模型将被命名为
cars
,与所连接的 MongoDB 数据库中的cars
集合相对应。MongoDB 模型将以此名称导出。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37const { Int32 } = require('mongodb');
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const cars = new Schema({
dealer_id: {
type: Number,
required: true
},
make: {
type: String,
required: true
},
model: {
type: String,
required: true
},
bodyType: {
type: String,
required: true
},
year: {
type: Number,
required: true
},
mileage: {
type: Number,
required: true
},
price: {
type: Number,
required: true
}
});
module.exports = mongoose.model('cars', cars); -
获取包含汽车库存和相关详细信息的 JSON 数据集。
首先创建一个名为data
的文件夹并导航进入:1
2mkdir data
cd data下载汽车库存数据集:
1
wget https://cf-courses-data.s3.us.cloud-object-storage.appdomain.cloud/IBM-CD0321EN-SkillsNetwork/labs/v2/m6/car_records.json
检查文件,发现每个汽车对象都有以下字段:
1
make, model, bodyType, year, dealer_id, mileage, price
-
返回
carsInvent
目录,创建更多文件。建立具有后端端点功能的 Node.JS 服务器。首先创建一个名为
app.js
的文件:1
touch app.js
它应具备以下功能:
- 设置与 MongoDB 集成的 Express 服务器
- 从文件
car_records.json
中读取数据 - 建立与 MongoDB 的连接
- 定义根 API 端点
/
,当访问根 API 时,它会响应一条欢迎访问 Mongoose API 消息 - 定义以下六个端点,用于根据各种条件查询汽车:
cars/:id
端点,可根据指定的经销商 ID 从 MongoDB 集合中检索并返回汽车文档/carsbymake/:id/:make
端点,可根据经销商 ID 和汽车品牌检索并返回汽车文档/carsbymake/:id/:model
端点,可根据经销商 ID 和车型检索并返回汽车文档/carsbymaxmileage/:id/:mileage
端点,可根据经销商 ID 和里程限制检索并返回汽车文件,如下所示:- 里程数:
- 小于或等于 50000
- 50000 至 100000
- 100000 至 150000
- 150000 至 200000
- 大于 200000
- 里程数:
/carsbyprice/:id/:price
端点,可根据经销商ID和价格约束检索并返回汽车文件,如下所示:- 价格:
- 小于或等于 20000
- 20000 至 40000
- 40000 至 60000
- 60000 至 80000
- 大于 80000
- 价格:
/carsbymake/:id/:year
端点,可根据经销商ID和最低年份约束检索并返回汽车文件
- 在
3050
端口启动服务器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116/*jshint esversion: 8 */
const express = require('express');
const mongoose = require('mongoose');
const fs = require('fs');
const cors = require('cors');
const app = express();
const port = 3050;
app.use(cors());
app.use(express.urlencoded({ extended: false }));
const carsData = JSON.parse(fs.readFileSync('car_records.json', 'utf8'));
mongoose.connect('mongodb://mongo_db:27017/', { dbName: 'dealershipsDB' });
const Cars = require('./inventory');
try {
Cars.deleteMany({}).then(() => {
Cars.insertMany(carsData.cars);
});
} catch (error) {
console.error(error);
// Handle errors properly here
}
app.get('/', async (req, res) => {
res.send('Welcome to the Mongoose API');
});
app.get('/cars/:id', async (req, res) => {
try {
const documents = await Cars.find({dealer_id: req.params.id});
res.json(documents);
} catch (error) {
res.status(500).json({ error: 'Error fetching reviews' });
}
});
app.get('/carsbymake/:id/:make', async (req, res) => {
try {
const documents = await Cars.find({dealer_id: req.params.id, make: req.params.make});
res.json(documents);
} catch (error) {
res.status(500).json({ error: 'Error fetching reviews by car make and model' });
}
});
app.get('/carsbymodel/:id/:model', async (req, res) => {
try {
const documents = await Cars.find({ dealer_id: req.params.id, model: req.params.model });
res.json(documents);
} catch (error) {
res.status(500).json({ error: 'Error fetching dealers by ID' });
}
});
app.get('/carsbymaxmileage/:id/:mileage', async (req, res) => {
try {
let mileage = parseInt(req.params.mileage)
let condition = {}
if(mileage === 50000) {
condition = { $lte : mileage}
} else if (mileage === 100000){
condition = { $lte : mileage, $gt : 50000}
} else if (mileage === 150000){
condition = { $lte : mileage, $gt : 100000}
} else if (mileage === 200000){
condition = { $lte : mileage, $gt : 150000}
} else {
condition = { $gt : 200000}
}
const documents = await Cars.find({ dealer_id: req.params.id, mileage : condition });
res.json(documents);
} catch (error) {
res.status(500).json({ error: 'Error fetching dealers by ID' });
}
});
app.get('/carsbyprice/:id/:price', async (req, res) => {
try {
let price = parseInt(req.params.price)
let condition = {}
if(price === 20000) {
condition = { $lte : price}
} else if (price=== 40000){
console.log("\n \n \n "+ price)
condition = { $lte : price, $gt : 20000}
} else if (price === 60000){
condition = { $lte : price, $gt : 40000}
} else if (price === 80000){
condition = { $lte : price, $gt : 60000}
} else {
condition = { $gt : 80000}
}
const documents = await Cars.find({ dealer_id: req.params.id, price : condition });
res.json(documents);
} catch (error) {
res.status(500).json({ error: 'Error fetching dealers by ID' });
}
});
app.get('/carsbyyear/:id/:year', async (req, res) => {
try {
const documents = await Cars.find({ dealer_id: req.params.id, year : { $gte :req.params.year }});
res.json(documents);
} catch (error) {
res.status(500).json({ error: 'Error fetching dealers by ID' });
}
});
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
}); -
为 Node.js 应用程序创建 Docker 镜像。首先创建
Dockerfile
:1
touch Dockerfile
添加以下内容:
- 基础 Docker 镜像:
node:18.12.1-bullseye-slim
- 安装
9.1.3
版本的npm
- 从当前目录内添加
package.json
、app.js
、car_records.json
到 Docker 镜像的根目录 - 复制当前目录的所有文件到 Docker 镜像
- 让容器监听 3050 端口
- 将容器启动时要运行的默认命令指定为
node app.js
1
2
3
4
5
6
7
8
9
10
11
12
13FROM node:18.12.1-bullseye-slim
RUN npm install -g npm@9.1.3
ADD package.json .
ADD app.js .
ADD data/car_records.json .
COPY . .
RUN npm install
EXPOSE 3050
CMD [ "node", "app.js" ] - 基础 Docker 镜像:
-
为运行两个服务(MongoDB 容器和 Node.js 应用程序)设置 Docker Compose 配置文件。
创建 Docker Compose 配置 YAML 文件(docker-compose.yml
):1
touch docker-compose.yml
添加内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23version: '3.9'
services:
# Mongodb service
mongo_db:
container_name: carsInventory_container
image: mongo:latest
ports:
- 27018:27017
restart: always
volumes:
- mongo_data:/data/db
# Node api service
api:
image: nodeapp
ports:
- 3050:3050
depends_on:
- mongo_db
volumes:
mongo_data: {}构建 Docker 应用:
1
docker build . -t nodeapp
执行以下命令启动服务器:
1
docker-compose up
启动服务器并验证汽车库存服务的根端点是否显示了
Welcome to the Mongoose API
的消息。
将微服务与 Django 应用程序后端集成,并成功启动后端服务器
-
导航至
xrwvm-fullstack_developer_capstone/server
目录。如果 Django 服务器已在运行,则停止该服务器。 -
在
.env
文件中插入汽车库存服务端点的 URL(上一节已复制)。1
searchcars_url='your end'
不要添加尾端的
\
。 -
在
restapis.py
中加入执行以下功能的代码:-
从
.env
文件中获取 URL。1
2
3
4searchcars_url = os.getenv(
'searchcars_url',
default="http://localhost:3050/"
) -
执行一个名为
searchcars_request
的方法,该方法具有以下功能:- 接受端点和变量关键字参数
- 利用提供的端点、查询参数和基本 URL 构建一个完整的请求 URL
- 执行 GET 请求并返回响应的 JSON 内容
- 处理错误(包括网络异常)并提供成功完成消息
代码结构将与您之前在 Capstone 主项目中创建的
get_request
方法非常相似:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18def searchcars_request(endpoint, **kwargs):
params = ""
if (kwargs):
for key, value in kwargs.items():
params = params+key + "=" + value + "&"
request_url = searchcars_url+endpoint+"?"+params
print("GET from {} ".format(request_url))
try:
# Call get method of requests library with URL and parameters
response = requests.get(request_url)
return response.json()
except:
# If any error occurs
print("Network exception occurred")
finally:
print("GET request call complete!")
-
-
在
djangoapp/views.py
文件中添加名为get_inventory
的视图,以获取汽车库存。- 该视图应处理 HTTP 请求,提取查询参数和经销商 ID。
- 它应使用提供的参数构建一个 API 端点,并调用
restapis.py
中定义的searchcars_request
函数来检索汽车数据。确保为searchcars_request
函数加入模块导入。 - 它应返回状态为
200
的 JSON 响应,如果提供了经销商 ID,则返回获取的汽车。 - 如果没有经销商 ID,则应返回状态为
400
的 JSON 响应和Bad Request
消息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25# Module import
from .restapis import get_request, analyze_review_sentiments, post_review, searchcars_request
# Code for the view
def get_inventory(request, dealer_id):
data = request.GET
if (dealer_id):
if 'year' in data:
endpoint = "/carsbyyear/"+str(dealer_id)+"/"+data['year']
elif 'make' in data:
endpoint = "/carsbymake/"+str(dealer_id)+"/"+data['make']
elif 'model' in data:
endpoint = "/carsbymodel/"+str(dealer_id)+"/"+data['model']
elif 'mileage' in data:
endpoint = "/carsbymaxmileage/"+str(dealer_id)+"/"+data['mileage']
elif 'price' in data:
endpoint = "/carsbyprice/"+str(dealer_id)+"/"+data['price']
else:
endpoint = "/cars/"+str(dealer_id)
cars = searchcars_request(endpoint)
return JsonResponse({"status": 200, "cars": cars})
else:
return JsonResponse({"status": 400, "message": "Bad Request"})
return JsonResponse({"status": 400, "message": "Bad Request"}) -
在
djangoapp/urls.py
文件中包含该视图的路由。1
path(route='get_inventory/<int:dealer_id>', view=views.get_inventory, name='get_inventory'),
-
导航到
xrwvm-fullstack_developer_capstone/server
目录。
确保 Docker Compose 服务器正在运行。如果没有,请使用docker-compose up
命令启动它。 -
执行模型迁移并启动服务器。
1
2
3python3 manage.py makemigrations
python3 manage.py migrate
python3 manage.py runserver -
确保服务器启动成功且无错误。如果出现任何错误日志,请检查并根据需要处理您的代码。
汽车库存服务的前端开发
开发并集成与后端汽车库存微服务相对应的前端服务
这需要创建一个 React 组件,专门用于搜索和显示汽车信息,并为该组件整合相应的路线。
-
创建一个用于搜索和显示汽车信息的 React 组件。
- 导航至
xrwvm-fullstack_developer_capstone/server/frontend/src/components/Dealers
目录,并添加名为SearchCars.jsx
的新文件。 - 在该文件中加入以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import Header from '../Header/Header';
const SearchCars = () => {
const [cars, setCars] = useState([]);
const [makes, setMakes] = useState([]);
const [models, setModels] = useState([]);
const [dealer, setDealer] = useState({"full_name":""});
const [message, setMessage] = useState("Loading Cars....");
const { id } = useParams();
let dealer_url = `/djangoapp/get_inventory/${id}`;
let fetch_url = `/djangoapp/dealer/${id}`;
const fetchDealer = async ()=>{
const res = await fetch(fetch_url, {
method: "GET"
});
const retobj = await res.json();
if(retobj.status === 200) {
let dealer = retobj.dealer;
setDealer({"full_name":dealer[0].full_name})
}
}
const populateMakesAndModels = (cars)=>{
let tmpmakes = []
let tmpmodels = []
cars.forEach((car)=>{
tmpmakes.push(car.make)
tmpmodels.push(car.model)
})
setMakes(Array.from(new Set(tmpmakes)));
setModels(Array.from(new Set(tmpmodels)));
}
const fetchCars = async ()=>{
const res = await fetch(dealer_url, {
method: "GET"
});
const retobj = await res.json();
if(retobj.status === 200) {
let cars = Array.from(retobj.cars)
setCars(cars);
populateMakesAndModels(cars);
}
}
const setCarsmatchingCriteria = async(matching_cars)=>{
let cars = Array.from(matching_cars)
console.log("Number of matching cars "+cars.length);
let makeIdx = document.getElementById('make').selectedIndex;
let modelIdx = document.getElementById('model').selectedIndex;
let yearIdx = document.getElementById('year').selectedIndex;
let mileageIdx = document.getElementById('mileage').selectedIndex;
let priceIdx = document.getElementById('price').selectedIndex;
if(makeIdx !== 0) {
let currmake = document.getElementById('make').value;
cars = cars.filter(car => car.make === currmake);
}
if(modelIdx !== 0) {
let currmodel = document.getElementById('model').value;
cars = cars.filter(car => car.model === currmodel);
if(cars.length !== 0) {
document.getElementById('make').value = cars[0].make;
}
}
if(yearIdx !== 0) {
let curryear = document.getElementById('year').value;
cars = cars.filter(car => car.year >= curryear);
if(cars.length !== 0) {
document.getElementById('make').value = cars[0].make;
}
}
if(mileageIdx !== 0) {
let currmileage = parseInt(document.getElementById('mileage').value);
if(currmileage === 50000) {
cars = cars.filter(car => car.mileage <= currmileage);
} else if (currmileage === 100000){
cars = cars.filter(car => car.mileage <= currmileage && car.mileage > 50000);
} else if (currmileage === 150000){
cars = cars.filter(car => car.mileage <= currmileage && car.mileage > 100000);
} else if (currmileage === 200000){
cars = cars.filter(car => car.mileage <= currmileage && car.mileage > 150000);
} else {
cars = cars.filter(car => car.mileage > 200000);
}
}
if(priceIdx !== 0) {
let currprice = parseInt(document.getElementById('price').value);
if(currprice === 20000) {
cars = cars.filter(car => car.price <= currprice);
} else if (currprice === 40000){
cars = cars.filter(car => car.price <= currprice && car.price > 20000);
} else if (currprice === 60000){
cars = cars.filter(car => car.price <= currprice && car.price > 40000);
} else if (currprice === 80000){
cars = cars.filter(car => car.price <= currprice && car.price > 60000);
} else {
cars = cars.filter(car => car.price > 80000);
}
}
if(cars.length === 0) {
setMessage("No cars found matching criteria");
}
setCars(cars);
}
let SearchCarsByMake = async ()=> {
let make = document.getElementById("make").value;
dealer_url = dealer_url + "?make="+make;
const res = await fetch(dealer_url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}})
const retobj = await res.json();
if(retobj.status === 200) {
setCarsmatchingCriteria(retobj.cars);
}
}
let SearchCarsByModel = async ()=> {
let model = document.getElementById("model").value;
dealer_url = dealer_url + "?model="+model;
const res = await fetch(dealer_url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}})
const retobj = await res.json();
if(retobj.status === 200) {
setCarsmatchingCriteria(retobj.cars);
}
}
let SearchCarsByYear = async ()=> {
let year = document.getElementById("year").value;
if (year !== "all") {
dealer_url = dealer_url + "?year="+year;
}
const res = await fetch(dealer_url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}})
const retobj = await res.json();
if(retobj.status === 200) {
setCarsmatchingCriteria(retobj.cars);
}
}
let SearchCarsByMileage = async ()=> {
let mileage = document.getElementById("mileage").value;
if (mileage !== "all") {
dealer_url = dealer_url + "?mileage="+mileage;
}
const res = await fetch(dealer_url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}})
const retobj = await res.json();
if(retobj.status === 200) {
setCarsmatchingCriteria(retobj.cars);
}
}
let SearchCarsByPrice = async ()=> {
let price = document.getElementById("price").value;
if(price !== "all") {
dealer_url = dealer_url + "?price="+price;
}
const res = await fetch(dealer_url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}})
const retobj = await res.json();
if(retobj.status === 200) {
setCarsmatchingCriteria(retobj.cars);
}
}
const reset = ()=>{
const selectElements = document.querySelectorAll('select');
selectElements.forEach((select) => {
select.selectedIndex = 0;
});
fetchCars();
}
useEffect(() => {
fetchCars();
fetchDealer();
},[]);
return (
<div>
<Header />
<h1 style={{ marginBottom: '20px'}}>Cars at {dealer.full_name}</h1>
<div>
<span style={{ marginLeft: '10px', paddingLeft: '10px'}}>Make</span>
<select style={{ marginLeft: '10px', marginRight: '10px' ,paddingLeft: '10px', borderRadius :'10px'}} name="make" id="make" onChange={SearchCarsByMake}>
{makes.length === 0 ? (
<option value=''>No data found</option>
):(
<>
<option disabled defaultValue> -- All -- </option>
{makes.map((make, index) => (
<option key={index} value={make}>
{make}
</option>
))}
</>
)
}
</select>
<span style={{ marginLeft: '10px', paddingLeft: '10px'}}>Model</span>
<select style={{ marginLeft: '10px', marginRight: '10px' ,paddingLeft: '10px', borderRadius :'10px'}} name="model" id="model" onChange={SearchCarsByModel}>
{models.length === 0 ? (
<option value=''>No data found</option>
) : (
<>
<option disabled defaultValue> -- All -- </option>
{models.map((model, index) => (
<option key={index} value={model}>
{model}
</option>
))}
</>
)}
</select>
<span style={{ marginLeft: '10px', paddingLeft: '10px'}}>Year</span>
<select style={{ marginLeft: '10px', marginRight: '10px' ,paddingLeft: '10px', borderRadius :'10px'}} name="year" id="year" onChange={SearchCarsByYear}>
<option selected value='all'> -- All -- </option>
<option value='2024'>2024 or newer</option>
<option value='2023'>2023 or newer</option>
<option value='2022'>2022 or newer</option>
<option value='2021'>2021 or newer</option>
<option value='2020'>2020 or newer</option>
</select>
<span style={{ marginLeft: '10px', paddingLeft: '10px'}}>Mileage</span>
<select style={{ marginLeft: '10px', marginRight: '10px' ,paddingLeft: '10px', borderRadius :'10px'}} name="mileage" id="mileage" onChange={SearchCarsByMileage}>
<option selected value='all'> -- All -- </option>
<option value='50000'>Under 50000</option>
<option value='100000'>50000 - 100000</option>
<option value='150000'>100000 - 150000</option>
<option value='200000'>150000 - 200000</option>
<option value='200001'>Over 200000</option>
</select>
<span style={{ marginLeft: '10px', paddingLeft: '10px'}}>Price</span>
<select style={{ marginLeft: '10px', marginRight: '10px' ,paddingLeft: '10px', borderRadius :'10px'}} name="price" id="price" onChange={SearchCarsByPrice}>
<option selected value='all'> -- All -- </option>
<option value='20000'>Under 20000</option>
<option value='40000'>20000 - 40000</option>
<option value='60000'>40000 - 60000</option>
<option value='80000'>60000 - 80000</option>
<option value='80001'>Over 80000</option>
</select>
<button style={{marginLeft: '10px', paddingLeft: '10px'}} onClick={reset}>Reset</button>
</div>
<div style={{ marginLeft: '10px', marginRight: '10px' , marginTop: '20px'}} >
{cars.length === 0 ? (
<p style={{ marginLeft: '10px', marginRight: '10px', marginTop: '20px' }}>{message}</p>
) : (
<div>
<hr/>
{cars.map((car) => (
<div>
<div key={car._id}>
<h3>{car.make} {car.model}</h3>
<p>Year: {car.year}</p>
<p>Mileage: {car.mileage}</p>
<p>Price: {car.price}</p>
</div>
<hr/>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default SearchCars;
- 导航至
-
为该组件添加路由并构建前端。
- 在
frontend/src/App.js
中为该组件添加导入语句和路由,将搜索路径设为/searchcars/:id
。1
import SearchCars from "./components/Dealers/SearchCars";
1
<Route path="/searchcars/:id" element={<SearchCars />} />
- 整合锚链接,将用户从特定经销商的评论页面重定向到
Search Cars
页面。- 转到
frontend/src/components/Dealers/Dealer.jsx
,插入一个锚元素。 - 将其标记为
Search Cars
,并设置为点击后导航至Search Cars
页面。
1
<a href={`/searchcars/${id}`}>SearchCars</a>
- 转到
- 构建应用程序的前端,以便部署。
1
npm run build
- 在
djangoproj/urls.py
中添加路径为searchcars/<int:dealer_id>
的动态 URL 模式。1
path('searchcars/<int:dealer_id>',TemplateView.as_view(template_name="index.html")),
- 在