jeecgboot TS + Vue 模板化 03

jeecgboot TS + Vue 模板化 03 jeecgbootTS对比JS维度JavaScript (JS)TypeScript (TS)语言类型动态类型弱类型。变量类型在运行时才确定。静态类型强类型。编译时可检查类型支持类型推断、接口和泛型。语法ECMAScript 标准支持最新 JS 语法ES6。基于 JS 超集兼容 JS 所有语法同时增加类型、接口、枚举、泛型、命名空间等。类型系统无类型系统类型检查依赖运行时。完整类型系统编译器可提前发现类型错误减少运行时 bug。编译/执行直接被浏览器或 Node.js 执行无需编译。需先编译成 JStsc 或 Babel再由浏览器/Node.js 执行。IDE 支持基础智能提示和自动补全但类型推断有限。强大的智能提示、自动补全、重构、类型检查提高开发效率和可维护性。错误检测多数错误在运行时才发现。编译时捕获类型错误和潜在逻辑错误提高代码安全性。面向对象支持类、继承、ES6 模块但类型不强制。完整的类、接口、抽象类、访问修饰符(private/protected/public)支持更适合大规模项目。函数特性支持高阶函数、回调、箭头函数、默认参数等。除 JS 所有特性外可为函数参数、返回值、this 指定类型支持函数重载。模块化ES Module、CommonJS、AMD 等标准。完全兼容 JS 模块化支持命名空间增强模块管理。泛型支持无原生泛型。原生泛型支持可实现类型安全的复用函数和类。生态和库丰富的库和框架生态直接使用。完全兼容 JS 生态且可通过 DefinitelyTyped 获取类型声明文件.d.ts。学习成本低适合初学者快速上手。较高需要理解类型系统和编译概念但对大型项目更安全可维护。调试直接在浏览器或 Node.js 调试。调试需要先编译为 JS但 VS Code 等 IDE 可直接映射 TS 源码调试。适用场景小型项目、快速开发、脚本任务。中大型项目、团队协作、需要严格类型和可维护性的系统。社区和支持JS 社区庞大、资料丰富。TS 社区快速成长Angular、NestJS、Vue3、React 等框架广泛采用 TS。未来发展稳定成熟随着 ES 发展持续更新。越来越受大型项目和企业青睐TypeScript 已成为 JS 的标准补充。- 参数类型写在括号里 params: LoginParams - 返回值类型写在 defHttp.post**T** 的泛型参数里 - 不写返回值的显式类型 —— TS 会自动推断为 PromiseLoginResultModel export function loginApi(params: LoginParams, mode: ErrorMessageMode modal) { return defHttp.postLoginResultModel( { url: Api.Login, params, }, { errorMessageMode: mode, } ); }async login( params: LoginParams { captcha: string; rememberMe: boolean }, //属性合并 mode: ErrorMessageMode modal ): PromiseGetUserInfoModel | null {- params: LoginParams { captcha: string; rememberMe: boolean } —— 用 做交叉类型在现有接口基础上临时扩字段- async 函数的返回值类型永远是 Promise真实返回类型- const data await loginApi(...) —— TS 会自动推断 data 为 LoginResultModelexport interface LoginParams { username: string; password: string; captcha?: string; checkKey?: string; remember_me?: boolean; }- LoginParams —— 你发出去给后端的结构- LoginResultModel —— 后端返回给你的结构- 两者分开写避免一个 model 既要当入参又当出参的模糊语义- ? 表示可选字段调用方可以不传const loading refboolean(false); const loginForm reactiveLoginParams({ username: , password: , captcha: , }); async function handleLogin() { loading.value true; try { // userStore.login 返回 PromiseGetUserInfoModel | null const userInfo await userStore.login({ ...loginForm, /// captcha: formModelRef.value?.captcha || , rememberMe: formModelRef.value?.rememberMe || false, }); notification.success({ message: 登录成功, description: 欢迎回来${userInfo?.realname} }); router.push(/home); } catch (e) { notification.error({ message: 登录失败, description: (e as Error).message }); } finally { loading.value false; } }特性refreactive响应式对象类型基本类型 / 对象 / 数组对象 / 数组深度访问方式.value直接访问属性深度响应对象内部属性需访问.value否则浅响应深度响应内部属性也是响应式使用场景单个值bool、number、string或希望手动解包对象表单对象、复杂数据结构、数组loginForm.username admin; loginForm.password 123456;- refboolean(false) —— 显式给泛型防止 TS 推断出 never 或 undefined- reactiveLoginParams({...}) —— reactive 的类型建议写在尖括号里- (e as Error) —— catch 里的变量默认是 unknown 需要断言才能读 .messagecaptcha: formModelRef.value?.captcha || ,从formModelRef.value中取出captcha的值如果取不到或者值为空则使用空字符串作为默认值。|| 如果左边的值不存在或为空值就使用空字符串作为默认值。这里的问号?.是可选链运算符Optional Chaining作用就是安全地访问对象属性避免对象为null或undefined时报错。formModelRef.value null ? undefined : formModelRef.value.captcha如果formModelRef.value是null或undefined访问.captcha不会报错而是返回undefined。如果formModelRef.value有值则返回它的captcha属性。let captcha; if (formModelRef.value?.captcha) { captcha formModelRef.value.captcha; } else { captcha ; }rememberMe: formModelRef.value?.rememberMe || false,尝试取rememberMe的值如果取不到value是 null/undefined 或rememberMe本身是 falsy就使用默认值false泛型 T 你最常打交道的东西泛型 T 可以理解为类型的参数。就像函数的参数是值的参数// 调用处 const result await defHttp.postLoginResultModel({ url: /login, params, }); postT any( config: AxiosRequestConfig, options?: RequestOptions ): PromiseT 实际 post( config, options ): PromiseLoginResultModel return this.requestLoginResultModel(...)读这段代码的方式 从调用方反着往回看 —— 你在调用方写了 LoginResultModel 这个 T 就顺着 defHttp.postT → requestT → PromiseT 一路贯穿到底。例如interface LoginResultModel { token: string; userId: number; }那么const result await defHttp.postLoginResultModel(...)IDE立刻知道result.token result.userId有提示。而result.username会直接报错Property username does not exist on type LoginResultModel这就是泛型最大的价值src/api/model/baseModel.ts 里有两个你会反复复制的泛型模板// 所有分页查询都可以用这个作为基础参数 export interface BasicPageParams { page: number; pageSize: number; } // 所有分页返回都应该是这种结构 // items 是数据项数组total 是总数 export interface BasicFetchResultT { items: T[]; total: number; }// src/api/sys/model/departModel.ts import { BasicPageParams, BasicFetchResult } from //api/model/baseModel; export interface DepartItem { id: string; departName: string; orgCode: string; createTime: string; } export type DepartListParams BasicPageParams { departName?: string }; // 是 交叉类型Intersection Type 表示把 BasicPageParams 类型和 { departName?: string } 类型 合并 结果类型拥有两边的所有属性 //? 表示 可选属性 可以有也可以没有 类型是 string export type DepartListResult BasicFetchResultDepartItem; // ↑ 等价于{ items: DepartItem[]; total: number }使用场景推荐原因描述对象结构API返回、表单数据interface结构语义明确支持extends适合对象建模类型别名简单替换type更轻量用于起别名更直观联合类型 / 交叉类型type支持A | B和A Binterface不支持函数类型定义type写法更简洁(a: number) stringclass 实现约束interfaceimplements只能接interface复杂类型组合type可组合、可嵌套表达能力更强interface更偏“对象结构设计”type更偏“类型运算和组合”写对象结构 → interface做组合/运算 → type不确定 → 默认 interface更安全特性interfacetype可重复声明✅ 支持合并❌ 不支持扩展已有类型✅extends或合并✅ 交叉类型适合场景库设计、模块扩展、API对象建模联合类型、复杂类型组合、函数类型type User { name: string; }; type User { age: number; }; // ❌ 会报错Cannot redeclare block-scoped variable User. type User { name: string }; type UserWithAge User { age: number }; const u: UserWithAge { name: Alice, age: 18 };在 JeecgBoot 里最典型的是 API model 用 interface组合/别名用 type// 描述对象 → interface export interface LoginParams { username: string; password: string; } // 描述一个较长的类型的别名 → type export type ID string | number; export type DepartListParams BasicPageParams { departName?: string }; // ↑ 交叉类型只能用 typeinterface专门描述“对象结构”type做“类型组合 / 运算”给一个复杂或常用类型起别名本质是“类型重命名 联合类型”export type ID string | number;export type DepartListParams BasicPageParams { departName?: string }; 把两个类型“合并成一个新类型”工具本质interface“定义一个对象长什么样”type“拼装 / 组合 / 变换类型”### artialT —— 把 T 的所有字段变成可选源码位置 src/utils/http/axios/index.ts createAxios 的签名function createAxios(opt?: PartialCreateAxiosOptions) { return new VAxios(deepMerge({ transform, requestOptions, ... }, opt || {})); }- CreateAxiosOptions 有一堆字段 timeout 、 headers 、 transform 、 requestOptions ...- 用 PartialCreateAxiosOptions 表示传进来的 opt 可以只包含其中一部分字段- 里面再 deepMerge(默认, opt) 把缺的字段用默认值补全PickT, K —— 从 T 中挑出某些字段场景一个 User 有 10 个字段但某个接口只需要 id 和 username 。interface User { id: string; username: string; email: string; realname: string; avatar: string; } type LoginUser PickUser, id | username; // 等价于{ id: string; username: string }### OmitT, K —— 从 T 中去掉某些字段场景编辑接口的参数和新增接口很像只是多了一个 id 。interface DepartAddParams { departName: string; orgCode: string; parentId?: string; } // 编辑比新增多了一个 id 必填 type DepartEditParams DepartAddParams { id: string }; // 或者反过来从完整版里拿掉 id type DepartAddParams2 OmitDepartFullParams, id;Recordstring, T —— 描述一个键值对对象场景 useModal / useDrawer 的 attrs 传参或动态表单配置。// 一个字典key 是字符串value 是任意值 const dict: Recordstring, any { name: Jeecg, version: 3, }; // 更具体的约束某些固定 key type UserFieldMap Recordusername | password, FormFieldConfig;何时允许 any 何时该封掉动态 JSON 对象 后端返回的 userInfo 就是典型的结构没定死的东西export interface LoginResultModel { userId: string | number; token: string; role: RoleInfo; userInfo?: any; // ← 后端 userInfo 的字段会变动标 any 合理 }- 封装层的顶层 defHttp.getany(...) —— 你自己调用的时候会把 any 替换成具体类型所以底层留 any 没大碍- 第三方库不提供 d.ts —— 临时声明 declare module foo; 或 const lib: any require(bar)### 应该避免 any 的场景3 个1. 业务数据模型 LoginParams.username 写成 any 那和没写 TS 没区别2. reactive / ref 状态 const loginForm reactiveany({}) —— 你会失去所有编辑器提示3. 函数返回值 async function login(): Promiseany —— 调用方无法知道拿到的是什么最佳实践 any 只在确实无法确定结构的地方用其他地方都换成具体类型或泛型。你项目里的 LoginParams 、 LoginResultModel 就是好例子。一个完整的从零到页面的 TS 套路Step 1. 写 model数据契约// src/api/sys/model/departModel.ts export interface DepartItem { id: string; departName: string; orgCode: string; createTime: string; } export type DepartListParams BasicPageParams { departName?: string }; export type DepartListResult BasicFetchResultDepartItem; export interface DepartSaveParams { id?: string; departName: string; orgCode?: string; parentId?: string; }Step 2. 写 API带泛型返回值// src/api/sys/depart.ts import { defHttp } from //utils/http/axios; import { DepartListParams, DepartListResult, DepartItem, DepartSaveParams } from ./model/departModel; enum Api { LIST /sys/sysDepart/list, ADD /sys/sysDepart/add, EDIT /sys/sysDepart/edit, DELETE /sys/sysDepart/delete, } export const departListApi (params: DepartListParams) defHttp.getDepartListResult({ url: Api.LIST, params }); export const departAddApi (params: DepartSaveParams) defHttp.post({ url: Api.ADD, params }); export const departEditApi (params: DepartSaveParams) defHttp.put({ url: Api.EDIT, params }); export const departDeleteApi (id: string) defHttp.delete({ url: Api.DELETE, params: { id } });Step 3. 写 store把 TS 类型推到状态层// src/store/modules/depart.ts // 定义并导出一个 Pinia store 模块 // useDepartStore 是一个函数调用它可以在组件中拿到 store 实例 export const useDepartStore defineStore({ // store 的唯一 ID用于区分不同模块 id: depart, // state 是一个函数返回当前模块的响应式数据对象 state: () ({ // 当前选中的部门初始值为 null // TS 类型断言可能是 DepartItem 类型或者 null currentDepart: null as DepartItem | null, // 部门列表初始值为空数组 // TS 类型断言数组中每一项都是 DepartItem 类型 departList: [] as DepartItem[], }), // actions 用来定义方法可以同步或异步操作 state actions: { // 异步加载部门列表 // params: DepartListParams 类型通常包含分页或搜索条件 async loadList(params: DepartListParams) { // 调用 API 获取部门列表 // departListApi(params) 返回一个 PromiseDepartItem[] // await 等待异步结果 const list await departListApi(params); // 把获取到的部门列表保存到 store 的 state // this.departList 直接访问 store 中的 departList this.departList list; }, }, });departList 的类型是DepartItem[]空数组[]本身 TS 会推断类型为any[]如果不加类型断言写法如下会报错this.departList await departListApi(params); // TS 可能报类型不匹配加上as DepartItem[]后departList明确类型TS 能检查赋值正确性保证数组中元素都是DepartItem避免后续访问报错写法面向对象作用TS 类型检查departList: [] as DepartItem[]变量/字段明确字段类型检查字段赋值类型params: DepartListParams函数参数指定函数调用接口检查调用者传参类型Step 4. 写页面享受自动提示// src/views/system/depart/index.vue const { departList, total } await departListApi({ page: 1, pageSize: 10, departName: formState.departName, // ← 这里敲 departName. 会出字段提示 });写完 Step 1 → Step 4 之后 只要后端的响应结构有变动 你改一下 departModel.ts 所有用到它的地方 TS 都会帮你标出来。这就是 TS 的核心价值。