领域模型 模板引擎 dashboard应用列表及配置接口实现
一、概述
本文档旨在详细介绍本项目中模板引擎 dashboard 应用列表及配置接口的实现,包括接口设计、实现逻辑、代码结构以及相关的配置说明。
二、模板引擎API接口实现
2.1 dashboard模板引擎应用列表及配置接口
app/router-schema/project.js
接口参数设计规范
js
module.exports = {
...,
"/api/project/list": {
get: {
query: {
type: "object",
properties: {
proj_key: { type: "string" },
},
},
},
},
"/api/project": {
get: {
query: {
type: "object",
properties: {
proj_key: { type: "string" },
},
required: ["proj_key"],
},
},
},
...
};
app/router/project.js
接口定义
js
module.exports = (app, router) => {
const { project: projectController } = app.controller;
router.get(
"/api/project/list",
projectController.getList.bind(projectController)
);
router.get("/api/project", projectController.get.bind(projectController));
...
};
app/controller/project.js
控制器定义getList
和get
方法
js
module.exports = (app) => {
const BaseController = require("./base")(app);
return class ProjectController extends BaseController {
/**
* 获取当前 projectKey 对应模型下的项目列表 [如果无projectKey 全量获取]
* @param {object} ctx 上下文
*/
getList(ctx) {
const { proj_key: projKey } = ctx.request.query;
const { project: projectService } = app.service;
const projectList = projectService.getList(projKey);
// 构造项目关键数据
const dtoProjectList = projectList.reduce((preList, item) => {
const { modelKey, key, name, desc, homePage } = item;
preList.push({ modelKey, key, name, desc, homePage });
return preList;
}, []);
this.success(ctx, dtoProjectList);
}
/**
* 根据 proj_key 获取项目配置
* @param {object} ctx 上下文
*/
get(ctx) {
const { proj_key: projKey } = ctx.request.query;
const { project: projectService } = app.service;
const projConfig = projectService.get(projKey);
if (!projConfig) {
this.fail(ctx, "项目获取异常", 50000);
return;
}
this.success(ctx, projConfig);
}
...
};
};
app/service/project.js
服务层定义getList
方法 通过model文件获取模型相关数据
js
module.exports = (app) => {
const BaseService = require("./base")(app);
const modelList = require("../../model/index.js")(app);
return class ProjectService extends BaseService {
/**
* 获取统一模型下的项目列表,[如果无 projKey 则全量获取]
* @param {string} projKey 项目 key
* */
getList(projKey) {
return modelList.reduce((projList, modelItem) => {
const { project } = modelItem;
if (projKey && !project[projKey]) return projList;
for (const pKey in project) {
projList.push(project[pKey]);
}
return projList;
}, []);
}
/**
* 根据 projKey 获取项目配置
* @param {string} projKey 项目 key
* */
get(projKey) {
let projConfig;
modelList.forEach((modelItem) => {
if (modelItem.project[projKey]) {
projConfig = modelItem.project[projKey];
}
});
return projConfig;
}
};
};
2.2 Mocha 添加接口测试逻辑
为
/api/project/list
接口添加测试用例/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 modelList;
let projectList = [];
let request;
it("启动测试服务", async () => {
const app = await eplisCore.start();
modelList = require("../../model/index.js")(app);
modelList.forEach((modelItem) => {
const { project } = modelItem;
Object.keys(project).forEach((projectItem) => {
projectList.push(project[projectItem]);
});
});
request = supertest(app.listen());
});
it("GET /api/project/list without proj_key", async () => {
let tmpRes = request.get("/api/project/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 === projectList.length);
resData.forEach((item) => {
assert(item.modelKey);
assert(item.key);
assert(item.desc !== undefined);
assert(item.homePage !== undefined);
});
});
it("GET /api/project/list with proj_key", async () => {
const { key: projKey } =
projectList[Math.floor(Math.random() * projectList.length)];
const { modelKey } = projectList.find((item) => item.key === projKey);
let tmpRes = request.get("/api/project/list");
tmpRes = tmpRes.set("s_t", st);
tmpRes = tmpRes.set("s_sign", md5(`${signKey}_${st}`));
tmpRes = tmpRes.query({ proj_key: projKey });
const res = await tmpRes;
assert(res.body.success === true);
const resData = res.body.data;
assert(
projectList.filter((item) => item.modelKey === modelKey).length ===
resData.length
);
resData.forEach((item) => {
assert(item.modelKey);
assert(item.key);
assert(item.desc !== undefined);
assert(item.homePage !== undefined);
});
});
it("GET /api/project without proj_key", async () => {
let tmpRes = request.get("/api/project");
tmpRes = tmpRes.set("s_t", st);
tmpRes = tmpRes.set("s_sign", md5(`${signKey}_${st}`));
const res = await tmpRes;
const resData = res.body;
assert(resData.success === false);
assert(resData.code === 442);
assert(resData.message.indexOf("request validate fail") > -1);
});
it("GET /api/project without fail", async () => {
let tmpRes = request.get("/api/project");
tmpRes = tmpRes.set("s_t", st);
tmpRes = tmpRes.set("s_sign", md5(`${signKey}_${st}`));
tmpRes = tmpRes.query({ proj_key: "xxxxxxxxxxxx" });
const res = await tmpRes;
const resData = res.body;
assert(resData.success === false);
assert(resData.code === 50000);
assert(resData.message.indexOf("项目获取异常") > -1);
});
it("GET /api/project with proj_key", () => {
projectList.forEach(async (projItem) => {
const { key: projKey } = projItem;
let tmpRes = request.get("/api/project");
tmpRes = tmpRes.set("s_t", st);
tmpRes = tmpRes.set("s_sign", md5(`${signKey}_${st}`));
tmpRes = tmpRes.query({ proj_key: projKey });
const res = await tmpRes;
assert(res.body.success === true);
const resData = res.body.data;
assert(resData.key === projKey);
assert(resData.modelKey);
assert(resData.name);
assert(resData.desc !== undefined);
assert(resData.homePage !== undefined);
const { menu } = resData;
menu.forEach((menuItem) => {
console.log(`-- ${projKey} --`, menuItem.key);
checkMenuItem(menuItem);
});
});
// 递归校验 menu 菜单
const checkMenuItem = (menuItem) => {
assert(menuItem.key);
assert(menuItem.name);
assert(menuItem.menuType);
if (menuItem.menuType === "group") {
assert(menuItem.subMenu != undefined);
menuItem.subMenu.forEach((subMenuItem) => {
assert(subMenuItem.key);
assert(subMenuItem.name);
assert(menuItem.menuType === "module");
checkMenuItem(subMenuItem);
});
}
if (menuItem.menuType === "module") {
checkModule(menuItem);
}
};
//检查 module 菜单配置
const checkModule = (moduleItem) => {
const { moduleType } = moduleItem;
assert(moduleType);
if (moduleType === "sider") {
const { siderConfig } = moduleItem;
assert(siderConfig);
assert(siderConfig.menu);
siderConfig.menu.forEach((siderMenuItem) => {
checkMenuItem(siderMenuItem);
});
}
if (moduleType === "iframe") {
const { iframeConfig } = moduleItem;
assert(iframeConfig);
assert(iframeConfig.path != undefined);
}
if (moduleType === "custom") {
const { customConfig } = moduleItem;
assert(customConfig);
assert(customConfig.path != undefined);
}
if (moduleType === "schema") {
const { schemaConfig } = moduleItem;
assert(schemaConfig);
assert(schemaConfig.api != undefined);
assert(schemaConfig.schema);
}
};
});
});
代码解释
checkMenuItem(menuItem)
: 这个函数用于递归验证菜单项的结构和内容。
- 基本验证 :
assert(menuItem.key)
:验证菜单项的key
字段存在。assert(menuItem.name)
:验证菜单项的name
字段存在。assert(menuItem.menuType)
:验证菜单项的menuType
字段存在。
- 分组菜单验证 :
if (menuItem.menuType === "group") { ... }
:如果菜单项的类型是group
,则验证其子菜单。
- 模块菜单验证 :
if (menuItem.menuType === "module") { ... }
:如果菜单项的类型是module
,则调用checkModule
函数进行验证。
checkModule(moduleItem)
:这个函数用于验证模块菜单的配置。
- 基本验证 :
assert(moduleType)
:验证模块菜单的moduleType
字段存在。
- 侧边栏菜单验证 :
if (moduleType === "sider") { ... }
:如果模块菜单的类型是sider
,则验证其侧边栏配置。
- iframe 菜单验证 :
if (moduleType === "iframe") { ... }
:如果模块菜单的类型是iframe
,则验证其 iframe 配置。
- 自定义菜单验证 :
if (moduleType === "custom") { ... }
:如果模块菜单的类型是custom
,则验证其自定义配置。
- 模式菜单验证 :
if (moduleType === "schema") { ... }
:如果模块菜单的类型是schema
,则验证其模式配置。
三、模板 dashboard 页面实现
3.1 模板页面功能
entry.dashboard.js
入口文件
js
// entry.dashboard.js
import boot from "$pages/boot.js";
import dashboard from "./dashboard.vue";
const routes = [];
// 头部菜单路由
routes.push({
path: "/iframe",
component: () => import("./complex-view/iframe-view/iframe-view.vue"),
});
routes.push({
path: "/schema",
component: () => import("./complex-view/schema-view/schema-view.vue"),
});
// custom 自定义路由
routes.push({
path: "/todo",
component: () => import("./todo/todo-view.vue"),
});
// 侧边栏菜单路由
routes.push({
path: "/sider",
component: () => import("./complex-view/sider-view/sider-view.vue"),
children: [
{
path: "/iframe",
component: () => import("./complex-view/iframe-view/iframe-view.vue"),
},
{
path: "/schema",
component: () => import("./complex-view/schema-view/schema-view.vue"),
},
{
// custom 自定义路由
path: "/todo",
component: () => import("./todo/todo-view.vue"),
},
],
});
// 侧边栏 兜底 策略
routes.push({
path: "/sider/:chapters+",
component: () => import("./complex-view/sider-view/sider-view.vue"),
});
boot(dashboard, { routes });
dashboard.vue
dashboard 页面组件
js
// dashboard.vue
<template>
<el-config-provider :locale="zhCn">
<header-view :proj-name="projName" @menu-select="onMenuSelect">
<template #main-container>
<router-view />
</template>
</header-view>
</el-config-provider>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useRouter, useRoute } from "vue-router";
import zhCn from "element-plus/es/locale/lang/zh-cn";
import headerView from "./complex-view/header-view/header-view.vue";
import $curl from "$common/curl.js";
import { useProjectStore } from "$store/project";
import { useMenuStore } from "$store/menu";
const router = useRouter();
const route = useRoute();
const projectStore = useProjectStore();
const menuStore = useMenuStore();
const projName = ref("");
onMounted(() => {
getProjectList();
getProjectConfig();
});
// 请求 /api/project/list 接口, 存至 project-store
const getProjectList = async () => {
const res = await $curl({
url: "/api/project/list",
method: "GET",
query: { proj_key: route.query?.proj_key },
});
if (!res || !res.success || !res.data) return;
projectStore.setProjectList(res.data);
};
// 请求 /api/project 接口,存至 menu-store
const getProjectConfig = async () => {
const res = await $curl({
url: "/api/project",
method: "GET",
query: { proj_key: route.query?.proj_key },
});
if (!res || !res.success || !res.data) return;
const { name, menu } = res.data;
projName.value = name;
menuStore.setMenuList(menu);
};
// 点击左侧菜单,触发路由跳转
const onMenuSelect = (menuItem) => {
console.log("menuItem", menuItem);
debugger;
const { moduleType, key, customConfig } = menuItem;
if (key === route.query.key) {
return;
}
const pathMap = {
sider: "/sider",
iframe: "/iframe",
schema: "/schema",
custom: customConfig?.path,
};
router.push({
path: pathMap[moduleType],
query: { key, proj_key: route.query.proj_key },
});
};
</script>
<style leng="less" scoped></style>
header-view.vue
模板顶部菜单栏目组件
js
// header-view.vue
<template>
<header-contaier :title="projName">
<template #menu-container>
<!-- 根据 menuStore.menu 渲染菜单 -->
<el-menu
mode="horizontal"
:default-active="activeKey"
:ellipsis="false"
@select="onMenuSelect"
>
<template v-for="item in menuStore.menuList">
<sub-menu
v-if="item.subMenu && item.subMenu.length > 0"
:key="item.key"
:menu-item="item"
/>
<el-menu-item v-else :index="item.key">
{{ item.name }}
</el-menu-item>
</template>
</el-menu>
</template>
<template #setting-container>
<!-- 根据 projectStore.projectList 渲染设置-->
<el-dropdown @command="hanleProjectCommand">
<span class="project-list">
{{ projName }}
<el-icon
v-if="projectStore.projectList.length > 1"
class="el-icon--right"
>
<arrow-down />
</el-icon>
</span>
<template v-if="projectStore.projectList.length > 1" #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="item in projectStore.projectList"
:key="item.key"
:command="item.key"
:disabled="item.key === route.query.proj_key"
>
{{ item.name }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<template #main-container>
<slot name="main-container" />
</template>
</header-contaier>
</template>
<script setup>
import { ref, watch, onMounted } from "vue";
import { useRoute } from "vue-router";
import { ArrowDown } from "@element-plus/icons-vue";
import subMenu from "./complex-view/sub-menu/sub-menu.vue";
import headerContaier from "$widgets/header-container/header-container.vue";
import { useProjectStore } from "$store/project";
import { useMenuStore } from "$store/menu";
const route = useRoute();
const projectStore = useProjectStore();
const menuStore = useMenuStore();
defineProps({
projName: String,
});
const emit = defineEmits(["menu-select"]);
const activeKey = ref("");
watch(
() => route.query.key,
() => {
setActive();
}
);
watch(
() => menuStore.menuList,
() => {
setActive();
}
);
onMounted(() => {
setActive();
});
const setActive = () => {
const menuItem = menuStore.findMenuItem({
key: "key",
value: route.query.key,
});
activeKey.value = menuItem?.key;
};
const onMenuSelect = (menuKey) => {
debugger;
if (!menuKey) return;
const menuItem = menuStore.findMenuItem({
key: "key",
value: menuKey,
});
emit("menu-select", menuItem);
};
const hanleProjectCommand = (projKey) => {
console.log(projKey);
const projectItem = projectStore.projectList.find(
(item) => item.key === projKey
);
if (!projectItem || !projectItem.homePage) return;
const { origin, pathname } = window.location;
window.location.replace(`${origin}${pathname}#${projectItem.homePage}`);
window.location.reload();
};
</script>
<style lang="less" scoped>
.project-list {
margin-right: 20px;
cursor: pointer;
color: var(--el-color-primary);
display: flex;
align-items: center;
outline: none;
}
</style>
五、总结
1. 接口设计与实现
- 接口参数规范 :在
app/router-schema/project.js
中定义了/api/project/list
和/api/project
接口的参数规范,明确了请求参数的类型和要求。 - 接口定义 :
app/router/project.js
完成了接口的定义,将请求映射到相应的控制器方法。 - 控制器实现 :
app/controller/project.js
实现了getList
和get
方法,分别用于获取项目列表和项目配置。根据请求参数调用服务层方法,并处理响应结果。 - 服务层实现 :
app/service/project.js
负责从模型文件中获取相关数据,实现了getList
和get
方法,根据projKey
获取项目列表或配置。
2. 接口测试
使用 Mocha 测试框架在 /test/controller/project.test.js
中为项目相关接口添加了测试用例,确保接口的功能正确性和稳定性。测试用例覆盖了不同参数情况下的请求,包括无 proj_key
、无效 proj_key
和有效 proj_key
的情况,并对响应结果进行了详细验证。
3. 页面实现
- 入口文件 :
entry.dashboard.js
定义了路由配置,包括头部菜单路由、侧边栏菜单路由和兜底策略,为页面导航提供支持。 - dashboard 页面组件 :
dashboard.vue
是核心页面组件,在页面加载时请求/api/project/list
和/api/project
接口,将数据存储到相应的状态管理中,并处理菜单点击事件,实现路由跳转。 - 顶部菜单栏目组件 :
header-view.vue
根据状态管理中的数据渲染顶部菜单和设置选项,处理菜单选择和项目切换事件,实现页面的交互功能。