前端功能开发
[一]新增模块(可选)
Dante Cloud 前端工程采用 monorepo
方式,对工程代码进行拆解,划分为多个模块。采用这种方式,类似于后端多模块工程,方便实现代码的重用。
微服务的重要理念之一:大中台、小前台。就在后端以服务的形式沉淀业务,前端承载用户交互。后端接口丰富业务稳定,前端轻量多样简单多变。
也许你会有多个前端系统,使用微服务后,后端接口是固定不变的。那么使用 monorepo
方式将前后端交互重复的内容进行固化,就可以极大地的方便开发和使用。
提示
Monorepo 是一种项目代码管理方式,指单个仓库中管理多个项目,有助于简化代码共享、版本控制、构建和部署等方面的复杂性,并提供更好的可重用性和协作性。和 Java 多模块工程一样,每个模块的代码单独管理,也可以长传至 NPM 仓库,实现代码的复用。
开发前端的第一步,就是考虑好新增的代码是否需要拆分为一个单独的模块。如果不需要一个单独模块,可以考虑将代码添加至现在已有的模块中。
[二]定义类型
Dante Cloud 的前端采用的是纯 Typescript。使用纯 Typescript 看其来确实没有纯 JavaScript 灵活,但是通过其定义能力,可以让编码更加准确,在编码阶段就很多明显错误问题。
提示
Typescript 并没有想象的那么复杂,本质上还是 JavaScript。Typescript 更多的是用来与 IDE 工具交互,方便在编码过程给与更多的提示和校验。
[1]定义实体类型
这里的实体类,与后端 JPA 实体相对应。后端接口返回的数据,就可以与之一一对应,方便前端的代码的开发以及类型的校验。例如:前端的 Table,每一个实体就对应 Table 里的一条数据。
示例代码如下所示:
export interface MgtCertificateEntity extends AbstractSysEntity {
certId: string;
certName: string;
/**
* 证书所有者的公共名称
* <p>
* 简称:CN 字段,对于 SSL 证书,一般为网站域名;而对于代码签名证书则为申请单位名称;而对于客户端证书则为证书申请者的姓名;
*/
commonName: string;
/**
* 组织单元
* <p>
* 简称:OU 字段。组织单位,表示在组织内部负责证书管理的部门或分支
*/
organizationUnit: string;
/**
* 组织
* <p>
* 简称:O 字段,对于 SSL 证书,一般为网站域名;而对于代码签名证书则为申请单位名称;而对于客户端单位证书则为证书申请者所在单位名称;
*/
organization: string;
/**
* 位置或城市
* <p>
* 简称:L 字段,进一步细化了CA的所在地。
*/
locality: string;
/**
* 州或省
* <p>
* 简称:ST 字段,标识证书主体的地理位置。
*/
stateOrProvince: string;
/**
* 国家或地区
* <p>
* 简称:C 字段,只能是国家字母缩写,如中国:CN 。
*/
country: string;
distinguishedName: string;
startTime: Date;
endTime: Date;
password: string;
bucketName: string;
keystoreName: string;
pemName: string;
parentId: string;
keyStoreCategory: string;
certificateCategory: string;
}
注意
在 Dante Cloud 前端工程中,因为有大量的 Typescript 类型定义。为了方便查找和使用,除了特殊的 Typescript 类型定义会放在 types
目录以外,其它所有的 Typescript 定义会被放在 declarations
目录。
[2]定义条件类型
定义完实体之后,还需要定义条件类型。
在 Dante Cloud 中,为了方便常规表格类型的开发,对 Table 基础操作进行了封装。除了常规的 Table 数据展示、添加修改删除操作外,条件搜索也是常见的功能。条件类型定义就是为了指定搜索条件涉及字段的类型。示例代码如下:
export interface MgtCertificateConditions extends Conditions {}
提示
如果 Table 功能不涉及搜索条件,定义一个空条件类型即可。但是不能不定义条件类型类,因为封装的基础必须要用到该类。
[3]定义属性类型
正常情况下,实体和条件类型就已经可以满足开发需求。但在使用基于 Typescript 的 Vue3 组件时,很多参数的值只能使用字符串,这就大大增加了出错的几率。
属性类型是 Dante Cloud 额外需要定义的一种类型,这种类型从实体中直接提取属性字符串作为校验类型,这就规避了手动输入字符串出错的问题。
示例代码如下:
export type MgtCertificateProps = keyof MgtCertificateEntity;
[三]定义服务
这里沿用了后端“服务”(service
)这个说法,本质上就是对后端 REST API 的封装。通过 Service 层的封装,将后端接口的调用和使用进一步简化。
[1]定义服务类
示例代码如下:
import type { MgtCertificateEntity } from "/@/declarations";
import { HttpConfig, BaseService } from "../base";
class MgtCertificateService extends BaseService<MgtCertificateEntity> {
private static instance: MgtCertificateService;
private constructor(config: HttpConfig) {
super(config);
}
public static getInstance(config: HttpConfig): MgtCertificateService {
if (this.instance == null) {
this.instance = new MgtCertificateService(config);
}
return this.instance;
}
public getBaseAddress(): string {
return this.getConfig().getManage() + "/security/permission";
}
}
export { MgtCertificateService };
提示
- 从上面代码可以看出,Typescript 和 Java 很多用法非常像,所以如果您是后端开发出身,没必要对 Typescript 谈虎色变。
- Dante Cloud 前端代码大量采用了“单例”模式,以减少对象的重复创建带来的资源损耗。
[2]编写外部引用接口
将服务类直接导出就可以直接使用。但是随着 Service 不断增多,服务类会越来越多,每次使用就需要查找,增加代码编写繁琐度。
Dante Cloud 定义了一个统一的对外接口,将所有的 Service 封装成对外接口的一个方法,在 IDE 中也会有提示,在使用时就比较方便。
在 API 模块的 main.ts
中,添加具体的 service
,在编写代码时就可以直接调用。示例代码如下:
class ApiResources {
private static instance: ApiResources;
private config = {} as HttpConfig;
private constructor(config: HttpConfig) {
this.config = config;
}
public static getInstance(config: HttpConfig): ApiResources {
if (this.instance == null) {
this.instance = new ApiResources(config);
}
return this.instance;
}
public getConfig(): HttpConfig {
return this.config;
}
......
public mgtCertificate(): MgtCertificateService {
return MgtCertificateService.getInstance(this.config);
}
}
const createApi = (
project: string,
clientId: string,
clientSecret: string,
http: Axios,
oidc: boolean,
): ApiResources => {
const config = new HttpConfig(project, clientId, clientSecret, http, oidc);
return ApiResources.getInstance(config);
};
export { createApi };
重要
以上代码编写完成之后,一定要使用 pnpm shared:build
命令重新编译各个子模块。否则新代码不会生效,在 IDE 中会标红提示出错。
[四]编写页面
[1]定义常量
在前端工程中,很多代码都需要编写一定的字符串作为标识,例如:定义页面或者组件的名字。这类字符串通常会在多个代码中出现,为了减少维护量,定义了专门的常量类,对常量进行统一管理。
找到主工程模块中的文件 packages/ui/src/composables/constants/definition/display.ts
。在其中 ComponentName
添加新增模块的名称。如下所示:
export const ComponentName = {
IOT_PRODUCT_CATEGORY: "IotProductCategory",
IOT_PRODUCT: "IotProduct",
IOT_DEVICE: "IotDevice",
IOT_TSL_FUNCTION: "IotTslFunction",
IOT_TSL_UNIT: "IotTslUnit",
MGT_CERTIFICATE: "MgtCertificate",
};
提示
该常量主要用于定义页面组件的名称。会在多处代码中进行交互使用。也是页面路由的核心元素。
[2]导入类型定义
找到主工程模块中的文件 lib/declaration/base.ts
。在其中导入功能所需的类型定义,即前面定义的 MgtCertificateEntity
、MgtCertificateConditions
和 MgtCertificateProps
提示
之所以在 lib/declaration/base.ts
文件中进行类型的导入,主要是为了构建一个统一的导入入口,修改以及使用的时候会比较方便。当然也可以在各个代码中独自导入,只不过如要修改就会非常麻烦。
[3]实现页面
下面就需要来编写具体的 Vue 页面。建议在主工程模块的 pages
目录下新建目录并创建页面。
Dante Cloud 前端默认的 Vue 名目前有三个:
Index.vue
:功能模块的主页面。如果是 Table 类型,则为 Table 显示页面。Content.vue
:功能模块的新增与编辑页面。新增与编辑类似,所以使用同一个页面。Authorize.vue
:设计授权之类功能的页面。
提示
之所以有这三个相对固定的页面,是因为一方面需要将三级Router、Tab View以及前端存储数据进行关联;另一方面是 Table 类型的功能模块都大同小异,提取出共性内容方便开发。
如果以上三个页面不满足需求,可以自己根据实际需求进行定义。不过这时就需要对 hooks
中的 Table 内容进行扩展,否则页面跳转会出现问题。
[五]页面路由
Dante Cloud 前端支持 静态路由
和 动态路由
两种路由模式:
静态路由
:vue-router
最标注准的路由方式。需要在前端代码中明确写出,但在前端不便于动态调整,即使支持意义也不大。更多的是用于前端开发和调试。动态路由
:由后端提供路由相关数据,前端根据后端数据动态加载相关路由。所以前端菜单的动态变化也是由次来支持。
[1]静态路由
在主工程的 packages/ui/routers/modules
目录下,根据代码需要新建一个 ts
或者在已有 ts
文件中,添加静态路由。示例代码如下:
import type { RouteRecordRaw } from "vue-router";
import { CONSTANTS } from "/@/composables/constants";
const routes: Array<RouteRecordRaw> = [
{
path: "/manage",
component: () => import("/@/views/layouts/Index.vue"),
meta: { title: "系统信息管理", sort: 8, icon: "mdi-leaf" },
redirect: "/manage/certificate",
children: [
{
path: "/manage/certificate",
name: CONSTANTS.ComponentName.MGT_CERTIFICATE,
meta: {
title: "证书管理",
icon: "mdi-certificate",
isHideAllChild: true,
},
component: () => import("/@/views/pages/manage/certificate/Index.vue"),
children: [
{
path: "/manage/certificate/content",
name: "MgtCertificateContent",
meta: {
title: "证书详情",
icon: "mdi-file-certificate",
isDetailContent: true,
},
component: () =>
import("/@/views/pages/manage/certificate/Content.vue"),
},
],
},
],
},
];
export default routes;
[2]动态路由
动态路由已经集成至前端系统之中。通过添加系统菜单就可以实现动态路由的添加。
添加菜单
添加菜单的界面如下面所示:


菜单参数
Vue Router 请求路径
:对应的是 Vue Router 的path
Vue Router Component
:路径对应的组件名。这里的组件应一定要与页面上定义名称保持一致显示标题
:菜单上显示的标题显示图标
:菜单上显示的图标。系统默认使用的 Icon 是 Material Design Icons。只要在输入框输入 Icon 名称就会自动显示可用图标。如下图所示Vue Component 相对路径
:对应的是 Vue Router 的component
。如果是节点菜单,那么对应的就是 Layout 组件;如果是子页面,对应的就是实际页面。可以对照上面静态路由的示例Vue Router 重定向地址
:对应的是 Vue Router 的redirect
。可以对照上面静态路由的示例上级节点
:上级节点用于关联页面和节点菜单,明确上下级关系。即上例中children
关系结构该页面不需要 Keeplive 缓存
:是否要开启当前页面的 Keeplive 缓存该页面不需要权限验证
:系统中默认的页面都需要验证权限,对于特殊的不需要权限的页面可以设置该选项该页面下包含子页面
:该选项用于区分是节点菜单还是页面功能该页面是三级路由页面
:默认的功能页面就是前面所述的Index.vue
。除此以外的Content.vue
和Authorize.vue
页面都属于三级路由排序值
:用于控制菜单功能显示顺序数据状态
:用于控制菜单条目数据的状态是否为保留数据
:如果设置为保留数据,那么在列表中该条数据将不会显示【删除】按钮。是一种保护措施,以避免误删除操作。

分配权限
前端菜单是单独的权限管理,主要的权限管控是基于 RBAC 中的角色。分别权限就是为菜单分配角色,如下图所示:

[六]注意事项
再次重申,不管是从 大中台、小前台
的理念还是从技术复杂度出发,微服务的核心都是后端而不是前端。所以,在使用本项目前端时请注意以下事项:
- 本系统前端并不是一套通用的管理模版,所以通用性肯定不足以满足所有 Web 前端的需求。如果你是想找一套管理模版,本系统前端未必合适
- 本系统前端也是一套完整的系统,但是更多定位的是作为微服务系统的管理端。本章内容均是基于扩展相关的功能进行编写,并未覆盖所有实现点。
- 之所以选择 Quasar 作为本系统首选组件,是因为经过多方对比,觉得 Quasar 更适合后端人员,无需编写大量 CSS 就可以写出还看得过去的开发前端
- 如果本系统的样式风格不满足你的需要,那么完全可以使用自己的前端框架,把本系统权当是前端与后端交互各类功能的示例即可。(微服务系统的前端,本来就应该“轻量”和易于修改以适应多变的需求)。
- 因为本人并不是专注于前端,所以如果有哪些代码实现的不够理想,有更好、更优的实现或者编码方式,可以提出 ISSUE,在个人能力范围内会尽量实现。