领域模型应用 API接口以及页面实现
一、概述
本章核心内容是将领域模型从API接口模型模板数据查询到前端页面输出展示。项目主要由服务模块、路由模块以及对应的测试代码组成。服务模块负责处理业务逻辑和数据获取,路由模块负责将客户端的请求准确分发到相应的处理逻辑,而测试代码则用于确保系统的各个接口能够正常工作,保证系统的稳定性和可靠性。
二、服务端API接口实现
2.1 服务端核心文件
app/service/project.js
此文件为服务模块,其主要职责是封装与项目数据相关的业务逻辑,为上层的路由模块和其他业务模块提供统一的数据获取接口。
js
//service/project.js
module.exports = (app) => {
const BaseService = require("./base")(app);
const modelList = require("../../model/index.js")(app);
return class ProjectService extends BaseService {
// 获取所有模型与项目的结构化数据
async getModelList() {
return modelList;
}
};
};
代码解释
-
模块导出 :使用
module.exports
导出一个函数,该函数接收app
作为参数。app
通常是 Koa 应用实例,它包含了整个应用的上下文信息,通过传入app
,可以让服务模块与应用的其他部分进行交互。 -
依赖引入:
BaseService
:从./base
模块引入,并传入app
进行初始化。BaseService
可能是一个基础服务类,包含了一些通用的服务方法和属性,ProjectService
继承自它可以复用这些通用功能,提高代码的复用性。modelList
:从../../model/index.js
模块引入,同样传入app
进行初始化。modelList
代表了所有领域模型与项目的结构化数据。
-
类定义与方法 :定义了
ProjectService
类,它继承自BaseService
。getModelList
方法是一个异步方法,用于获取modelList
领域模型与项目的结构化数据。
/app/router/project.js
该文件为路由模块,其核心功能是定义路由规则,将客户端的请求路径映射到相应的控制器方法,实现请求的分发和处理。
js
//router/project.js
module.exports = (app, router) => {
const { project: projectController } = app.controller;
router.get(
"/api/project/model_list",
projectController.getModelList.bind(projectController)
);
};
代码解释
- 模块导出 :使用
module.exports
导出一个函数,该函数接收app
和router
作为参数。app
是 Koa 应用实例,router
是 koa-router 路由实例,用于定义和管理路由规则。 - 控制器提取 :通过对象解构赋值从
app.controller
中提取project
控制器,并将其赋值给projectController
。这样做的好处是可以更方便地使用project
控制器中的方法。 - 路由定义 :使用
router.get
方法定义一个处理 GET 请求的路由。当客户端发送一个 GET 请求到/api/project/model_list
路径时,会调用projectController
的getModeList
方法,并使用bind
方法将getModeList
方法的this
上下文绑定到projectController
对象上,确保在getModeList
方法内部使用this
时,它指向的是projectController
对象。
2.2 服务端模块的调用关系
路由模块(app/router/project.js
)负责接收客户端的请求,并将请求分发到相应的控制器方法。在这个过程中,控制器方法会调用服务模块(app/service/project.js
)中的方法来处理业务逻辑和获取数据。
具体调用流程如下:
- 客户端发送一个 GET 请求到
/api/project/model_list
路径。 - 路由模块接收到请求后,根据路由规则调用
projectController
的getModeList
方法。 getModeList
方法在控制器中被调用,它会进一步调用服务模块中ProjectService
类的getModelList
方法。getModelList
方法从../../model/index.js
模块中获取modelList
数据,并将其返回给控制器。- 控制器将获取到的数据返回给客户端。
2.3 Mocha 测试文件结构
Mocha测试用例文件
/test/controller/project.test.js
Mocha 是一个功能强大的 JavaScript 测试框架,主要用于 Node.js 和浏览器环境中的单元测试和集成测试,对项目相关的接口进行测试,确保接口的功能正确性和稳定性。
js
// test/controller/project.test.js
const assert = require("assert");
const supertest = require("supertest");
const md5 = require("md5");
const eplisCore = require("../../elpis-core");
const signKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCg";
const st = Date.now();
describe("测试 project 相关接口", function () {
this.timeout(60000);
let request;
it("启动测试服务", async () => {
const app = await eplisCore.start();
request = supertest(app.listen());
});
it("GET /api/project/model_list", async () => {
let tmpRes = request.get("/api/project/model_list");
tmpRes = tmpRes.set("s_t", st);
tmpRes = tmpRes.set("s_sign", md5(`${signKey}_${st}`));
const res = await tmpRes;
assert(res.body.success === true);
const resData = res.body.data;
assert(resData.length > 0);
for (let i = 0; i < resData.length; i++) {
const item = resData[i];
assert(item.model);
assert(item.model.key);
assert(item.model.name);
assert(item.project);
for (const projKey in item.project) {
assert(item.project[projKey].name);
assert(item.project[projKey].key);
}
}
});
});
配置启动脚本 (package.json
)
json
"scripts": {
"test": "set _ENV='local' && mocha 'test/**/*.js'",
}
说明:
set _ENV='local'
设置环境变量_ENV
为'local'
,通常用于指定测试运行的环境(如本地开发环境)。mocha 'test/**/*.js'
使用 Mocha 执行所有位于test
目录下的.js
文件中的测试用例。- 运行
npm test
后,Mocha 会自动找到并执行该测试文件。
测试用例代码解释
-
依赖引入:
assert
:用于进行断言操作,验证测试结果是否符合预期。supertest
:用于模拟 HTTP 请求,方便对接口进行测试。md5
:用于生成签名,模拟请求时的签名验证。eplisCore
:用于启动测试服务,获取 Koa 应用实例。
-
测试用例:
启动测试服务
:调用eplisCore.start()
方法启动测试服务,并使用supertest
创建一个请求对象,用于后续的接口测试。GET /api/project/model_list
:模拟发送一个 GET 请求到/api/project/model_list
接口,设置请求头s_t
和s_sign
,并对响应结果进行断言。确保响应数据的结构和内容符合预期,如success
字段为true
,数据列表不为空,每个数据项包含model
和project
字段,且这些字段包含必要的属性。
-
具体调用流程如下:
- 测试代码调用
eplisCore.start()
方法启动测试服务,获取 Koa 应用实例。 - 使用
supertest
创建一个请求对象,模拟客户端发送请求到/api/project/model_list
接口。 - 路由模块接收到模拟请求后,按照正常的请求处理流程,调用控制器方法,控制器方法再调用服务模块的
getModelList
方法获取数据。 - 服务模块返回数据给控制器,控制器将数据返回给测试代码。
- 测试代码使用
assert
进行断言,验证响应数据是否符合预期。
- 测试代码调用
三、客户端页面实现
3.1 客户端文件及功能
entry.project-list.js
项目入口文件
js
// entry.project-list.js
import boot from "$pages/boot.js";
import projectList from "./project-list.vue";
boot(projectList);
代码解释
-
导入模块:
boot
:从$pages/boot.js
导入,用于初始化和启动组件。projectList
:从./project-list.vue
导入项目列表组件。
-
启动组件 :调用
boot
函数并传入projectList
组件,完成项目列表页面的初始化和启动。
project-list.vue
项目列表组件
模板部分
js
// project-list.vue
<template>
<header-container title="项目列表">
<template #main-content>
<div v-loading="loading">
<div v-for="item in modelList" :key="item.model?.key">
<!-- 展示 model -->
<div class="model-panel">
<el-row type="flex" align="middle">
<div class="title">
{{ item.model?.name }}
</div>
</el-row>
<div class="divider" />
</div>
<!-- 展示 project -->
<el-row flex class="project-list">
<el-card
v-for="projectItem in item.project"
:key="projectItem.key"
class="project-card"
>
<!-- project 头部 -->
<template #header>
<div class="title">
{{ projectItem.name }}
</div>
</template>
<!-- project 主体 -->
<div class="content">
{{ projectItem.desc ?? "----" }}
</div>
<!-- project 底部 -->
<template #footer>
<el-row justify="end">
<el-button link type="primary" @click="onEnter(projectItem)">
进入
</el-button>
</el-row>
</template>
</el-card>
</el-row>
</div>
</div>
</template>
</header-container>
</template>
代码解释
- 使用
header-container
组件:作为页面的整体布局,设置标题为"项目列表"。 main-content
插槽:用于展示项目列表的具体内容。- 加载状态 :使用
v-loading
指令根据loading
状态显示加载动画。 - 循环渲染 :使用
v-for
指令遍历modelList
数组,展示项目模型和具体项目。
脚本部分
js
// project-list.vue
<script setup>
import { ref, onMounted } from "vue";
import $curl from "$common/curl.js";
import headerContainer from "$widgets/header-container/header-container.vue";
const loading = ref(false);
const modelList = ref([]);
const getModelList = async () => {
loading.value = true;
const res = await $curl({
method: "GET",
url: "/api/project/model_list",
errorMessage: "获取项目列表失败",
});
loading.value = false;
if (!res || !res.data || !res.success) {
return;
}
modelList.value = res.data;
};
const onEnter = (projectItem) => {
console.log("on enter project", projectItem);
};
onMounted(() => {
getModelList();
});
</script>
代码解释
-
导入模块:
ref
和onMounted
:从vue
导入,用于响应式数据和生命周期钩子。$curl
:从$common/curl.js
导入,用于发送 HTTP 请求。headerContainer
:从$widgets/header-container/header-container.vue
导入头部容器组件。
-
响应式数据:
loading
:用于控制加载状态。modelList
:用于存储项目模型列表。
-
获取项目列表 :
getModelList
函数通过$curl
发送 GET 请求获取项目列表,并更新modelList
。 -
进入项目事件 :
onEnter
函数处理进入项目的点击事件。 -
生命周期钩子 :
onMounted
钩子在组件挂载后调用getModelList
函数。
样式部分
css
/* project-list.vue */
<style lang="less" scoped>
.model-panel {
margin: 20px 50px;
min-width: 500px;
.title {
font-size: 25px;
font-weight: bold;
color: #6d6c6c;
}
.divider {
margin-top: 10px;
border-bottom: 1px solid #d7d7d7;
width: 200px;
}
}
.project-list {
margin: 0 50px;
.project-card {
margin-right: 30px;
margin-bottom: 20px;
width: 300px;
.title {
font-weight: bold;
font-size: 17px;
color: #47a2ff;
}
.content {
height: 70px;
color: darkgrey;
font-size: 15px;
overflow: auto;
}
}
}
</style>
代码解释
- 使用 Less 预处理器:提高样式代码的可维护性。
scoped
属性:确保样式只作用于当前组件。- 样式定义:定义了项目模型面板和项目卡片的样式。
header-container.vue
页面头部组件
模板部分
js
// header-container.vue
<template>
<el-container class="header-container">
<el-header class="header">
<el-row type="flex" align="middle" class="header-row">
<!-- 左侧 上方 标题区域 -->
<el-row type="flex" align="middle" class="title-panel">
<img src="./asserts//icon.png" class="logo" />
<el-row class="text">
{{ title }}
</el-row>
</el-row>
<!-- 插槽: 菜单区域 -->
<slot name="menu-container" />
<!-- 右侧: 上方 设置| 用户区域 -->
<el-row type="flex" align="middle" justify="end" class="setting-panel">
<!-- 插槽:设置区域 -->
<slot name="setting-container" />
<img src="./asserts//avatar.png" class="avatar" />
<el-dropdown @command="hanleUserCommand">
<span class="user-name">
{{ userName }}
<i class="el-icon-arrow-down el-icon--right" />
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>我的消息</el-dropdown-item>
<el-dropdown-item command="logout"> 退出登录 </el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-row>
</el-row>
</el-header>
<el-main class="main-container">
<!-- 核心内容: 外部拓展填充区域 -->
<slot name="main-content" />
</el-main>
</el-container>
</template>
代码解释
- 使用 ElementPlus 组件 :
el-container
、el-header
、el-row
等,构建头部容器的布局。 - 插槽 :提供
menu-container
、setting-container
和main-content
三个插槽,用于外部拓展内容。 - 用户信息:展示用户头像和用户名,并提供下拉菜单,包含"我的消息"和"退出登录"选项。
脚本部分
js
// header-container.vue
<script setup>
import { ref } from "vue";
defineProps({
title: String,
});
const userName = ref("admin");
const hanleUserCommand = (event) => {
console.log("handle user command");
};
</script>
代码解释
- 导入模块 :
ref
从vue
导入,用于响应式数据。 - 定义属性 :使用
defineProps
定义title
属性,用于设置页面标题。 - 响应式数据 :
userName
用于存储用户名,初始值为"admin"。 - 用户命令处理 :
hanleUserCommand
函数处理用户下拉菜单的命令。
样式部分
css
/* header-container.vue */
<style lang="less" scoped>
.header-container {
height: 100%;
// min-width: 1000px;
overflow: hidden;
.header {
max-height: 120px;
border-bottom: 1px solid #e8e8e8;
.header-row {
height: 60px;
padding: 0 20px;
.title-panel {
width: 180px;
min-width: 180px;
.logo {
margin-right: 10px;
width: 25px;
width: 25px;
border-radius: 50%;
}
.text {
font-size: 15px;
font-weight: 500;
}
}
.setting-panel {
margin-right: auto;
min-width: 180px;
flex: 1;
.avatar {
margin-right: 12px;
width: 25px;
height: 25px;
border-radius: 50%;
}
.user-name {
font-size: 16px;
font-weight: 500;
cursor: pointer;
height: 60px;
line-height: 60px;
outline: none;
}
}
}
}
.main-container {
}
}
:deep(.el-header) {
padding: 0;
}
</style>
代码解释
- 使用 Less 预处理器:提高样式代码的可维护性。
scoped
属性:确保样式只作用于当前组件。- 样式定义:定义了头部容器、标题区域、设置区域和用户信息区域的样式。
五、总结
服务端通过 app/service/project.js
封装业务逻辑,app/router/project.js
定义路由规则,将客户端请求分发到相应处理逻辑,同时使用 Mocha 进行接口测试以确保系统稳定性;客户端以 entry.project-list.js
为入口,project-list.vue
展示项目列表,header-container.vue
构建页面头部,利用 Vue 的响应式特性和组件化开发,结合 ElementPlus 组件库和 Less 样式预处理器,实现页面的渲染和交互。围绕领域模型应用展开,核心思想是实现从 API 接口模型模板数据查询到前端页面输出展示的完整流程。