jeecgboot TS + Vue 模板化 03

jeecgboot

TS对比JS

维度 JavaScript (JS) TypeScript (TS)
语言类型 动态类型,弱类型。变量类型在运行时才确定。 静态类型,强类型。编译时可检查类型,支持类型推断、接口和泛型。
语法 ECMAScript 标准,支持最新 JS 语法(ES6+)。 基于 JS 超集,兼容 JS 所有语法,同时增加类型、接口、枚举、泛型、命名空间等。
类型系统 无类型系统,类型检查依赖运行时。 完整类型系统,编译器可提前发现类型错误,减少运行时 bug。
编译/执行 直接被浏览器或 Node.js 执行,无需编译。 需先编译成 JS(tsc 或 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 会自动推断为 Promise<LoginResultModel>
export function loginApi(params: LoginParams, mode: ErrorMessageMode = 'modal') {
  return defHttp.post<LoginResultModel>(
    {
      url: Api.Login,
      params,
    },
    {
      errorMessageMode: mode,
    }
  );
}

async login(

  params: LoginParams & { captcha: string; rememberMe: boolean }, //属性合并

  mode: ErrorMessageMode = 'modal'

): Promise<GetUserInfoModel | null> {
  • params: LoginParams & { captcha: string; rememberMe: boolean } ------ 用 & 做"交叉类型",在现有接口基础上临时扩字段

  • async 函数的返回值类型永远是 Promise<真实返回类型>

  • const data = await loginApi(...) ------ TS 会自动推断 data 为 LoginResultModel

    export interface LoginParams {
    username: string;
    password: string;
    captcha?: string;
    checkKey?: string;
    remember_me?: boolean;
    }

  • LoginParams ------ 你"发出去"给后端的结构

  • LoginResultModel ------ 后端"返回给你"的结构

  • 两者分开写,避免"一个 model 既要当入参又当出参"的模糊语义

  • ? 表示"可选字段",调用方可以不传

java 复制代码
const loading = ref<boolean>(false);
const loginForm = reactive<LoginParams>({
  username: '',
  password: '',
  captcha: '',
});

async function handleLogin() {
  loading.value = true;
  try {
    // userStore.login 返回 Promise<GetUserInfoModel | 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;
  }
}
特性 ref reactive
响应式对象类型 基本类型 / 对象 / 数组 对象 / 数组(深度)
访问方式 .value 直接访问属性
深度响应 对象内部属性需访问 .value,否则浅响应 深度响应,内部属性也是响应式
使用场景 单个值(bool、number、string)或希望手动解包对象 表单对象、复杂数据结构、数组
java 复制代码
loginForm.username = 'admin';
loginForm.password = '123456';
  • ref<boolean>(false) ------ 显式给泛型,防止 TS 推断出 never 或 undefined

  • reactive<LoginParams>({...}) ------ reactive 的类型建议写在尖括号里

  • (e as Error) ------ catch 里的变量默认是 unknown ,需要断言才能读 .message

captcha: formModelRef.value?.captcha || '',

formModelRef.value 中取出 captcha 的值,如果取不到或者值为空,则使用空字符串 '' 作为默认值。

|| '' 如果左边的值不存在(或为空值),就使用空字符串作为默认值。

这里的 问号 ?.可选链运算符(Optional Chaining) ,作用就是安全地访问对象属性 ,避免对象为 nullundefined 时报错。

formModelRef.value == null ? undefined : formModelRef.value.captcha

  • 如果 formModelRef.valuenullundefined,访问 .captcha 不会报错,而是返回 undefined
  • 如果 formModelRef.value 有值,则返回它的 captcha 属性。
java 复制代码
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> 可以理解为"类型的参数"。就像函数的参数是"值的参数":

java 复制代码
// 调用处
const result = await defHttp.post<LoginResultModel>({
  url: '/login',
  params,
});

post<T = any>(
  config: AxiosRequestConfig,
  options?: RequestOptions
): Promise<T>

实际

post(
  config,
  options
): Promise<LoginResultModel>

return this.request<LoginResultModel>(...)

读这段代码的方式 :从调用方反着往回看 ------ "你在调用方写了 <LoginResultModel> ,这个 T 就顺着 defHttp.post<T> → request<T> → Promise<T> 一路贯穿到底"。

例如:

复制代码
interface LoginResultModel {
  token: string;
  userId: number;
}

那么:

复制代码
const result = await defHttp.post<LoginResultModel>(...)

IDE立刻知道:

复制代码
result.token
result.userId

有提示。

而:

复制代码
result.username

会直接报错:

复制代码
Property 'username' does not exist on type 'LoginResultModel'

这就是泛型最大的价值:

src/api/model/baseModel.ts 里有两个你会反复复制的泛型模板:

java 复制代码
// 所有"分页查询"都可以用这个作为基础参数
export interface BasicPageParams {
  page: number;
  pageSize: number;
}

// 所有"分页返回"都应该是这种结构:
// items 是数据项数组,total 是总数
export interface BasicFetchResult<T> {
  items: T[];
  total: number;
}
java 复制代码
// 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 = BasicFetchResult<DepartItem>;
// ↑ 等价于:{ items: DepartItem[]; total: number }
使用场景 推荐 原因
描述对象结构(API返回、表单数据) interface 结构语义明确,支持 extends,适合对象建模
类型别名(简单替换) type 更轻量,用于起别名更直观
联合类型 / 交叉类型 type 支持 `A
函数类型定义 type 写法更简洁 (a: number) => string
class 实现约束 interface implements 只能接 interface
复杂类型组合 type 可组合、可嵌套,表达能力更强
  • interface:更偏"对象结构设计"
  • type:更偏"类型运算和组合"
  • 对象结构 → interface
  • 组合/运算 → type
  • 不确定 → 默认 interface(更安全)
特性 interface type
可重复声明 ✅ 支持合并 ❌ 不支持
扩展已有类型 extends 或合并 ✅ 交叉类型 &
适合场景 库设计、模块扩展、API对象建模 联合类型、复杂类型组合、函数类型
java 复制代码
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":

java 复制代码
// 描述对象 → interface
export interface LoginParams {
  username: string;
  password: string;
}

// 描述"一个较长的类型的别名" → type
export type ID = string | number;
export type DepartListParams = BasicPageParams & { departName?: string };
//                                       ↑ 交叉类型只能用 type

interface:专门描述"对象结构"

type:做"类型组合 / 运算"

  • 给一个复杂或常用类型起别名

  • 本质是"类型重命名 + 联合类型"

    export type ID = string | number;

export type DepartListParams = BasicPageParams & { departName?: string };

👉 把两个类型"合并成一个新类型"

工具 本质
interface "定义一个对象长什么样"
type "拼装 / 组合 / 变换类型"

artial<T> ------ 把 T 的所有字段变成可选

源码位置: src/utils/http/axios/index.ts (createAxios 的签名)

java 复制代码
function createAxios(opt?: Partial<CreateAxiosOptions>) {
  return new VAxios(deepMerge({ transform, requestOptions, ... }, opt || {}));
}
  • CreateAxiosOptions 有一堆字段( timeout 、 headers 、 transform 、 requestOptions ...)

  • 用 Partial<CreateAxiosOptions> 表示"传进来的 opt 可以只包含其中一部分字段"

  • 里面再 deepMerge(默认, opt) 把缺的字段用默认值补全

Pick<T, K> ------ 从 T 中挑出某些字段

场景:一个 User 有 10 个字段,但某个接口只需要 id 和 username 。

java 复制代码
interface User {
  id: string;
  username: string;
  email: string;
  realname: string;
  avatar: string;
}

type LoginUser = Pick<User, 'id' | 'username'>;
// 等价于:{ id: string; username: string }

Omit<T, K> ------ 从 T 中去掉某些字段

场景:"编辑接口"的参数和"新增接口"很像,只是多了一个 id 。

java 复制代码
interface DepartAddParams {
  departName: string;
  orgCode: string;
  parentId?: string;
}

// 编辑比新增多了一个 id 必填
type DepartEditParams = DepartAddParams & { id: string };
// 或者反过来:从"完整版"里拿掉 id
type DepartAddParams2 = Omit<DepartFullParams, 'id'>;

Record<string, T> ------ 描述一个"键值对"对象

场景: useModal / useDrawer 的 attrs 传参,或动态表单配置。

java 复制代码
// 一个字典:key 是字符串,value 是任意值
const dict: Record<string, any> = {
  name: 'Jeecg',
  version: 3,
};

// 更具体的约束:某些固定 key
type UserFieldMap = Record<'username' | 'password', FormFieldConfig>;

何时允许 any ?何时该封掉?

动态 JSON 对象 (后端返回的 userInfo 就是典型的"结构没定死的东西")

java 复制代码
export interface LoginResultModel {
  userId: string | number;
  token: string;
  role: RoleInfo;
  userInfo?: any;  // ← 后端 userInfo 的字段会变动,标 any 合理
}
  • 封装层的顶层 ( defHttp.get<any>(...) ------ 你自己调用的时候会把 any 替换成具体类型,所以底层留 any 没大碍)

  • 第三方库不提供 d.ts ------ 临时声明 declare module 'foo'; 或 const lib: any = require('bar')

应该避免 any 的场景(3 个)

  1. 业务数据模型 ( LoginParams.username 写成 any ,那和没写 TS 没区别)

  2. reactive / ref 状态 ( const loginForm = reactive<any>({}) ------ 你会失去所有编辑器提示)

  3. 函数返回值 ( async function login(): Promise<any> ------ 调用方无法知道拿到的是什么)

最佳实践 : any 只在"确实无法确定结构"的地方用,其他地方都换成具体类型或泛型。你项目里的 LoginParams 、 LoginResultModel 就是好例子。

一个完整的"从零到页面"的 TS 套路

Step 1. 写 model(数据契约)

java 复制代码
// 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 = BasicFetchResult<DepartItem>;
export interface DepartSaveParams {
  id?: string;
  departName: string;
  orgCode?: string;
  parentId?: string;
}

Step 2. 写 API(带泛型返回值)

java 复制代码
// 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.get<DepartListResult>({ 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 类型推到状态层)

java 复制代码
// 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) 返回一个 Promise<DepartItem[]>
      // 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. 写页面(享受自动提示)

java 复制代码
// 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 的核心价值。

相关推荐
小林ixn1 小时前
揭秘JavaScript面向对象:从栈模拟队列到原型链的深度剖析
javascript
下北沢美食家1 小时前
SSE 入门
前端
云计算磊哥@1 小时前
运维开发宝典023-WEB网站服务
运维·前端·运维开发
FlyWIHTSKY1 小时前
React 19 + Next.js 16(App Router)项目中集成 MSW
开发语言·javascript·vue.js
冰暮流星1 小时前
javascript之对象的建立-使用Object
开发语言·javascript·ecmascript
加点油。。。。1 小时前
【1.Obsidian渲染html文件】
前端·html·obsidian
ZFSS1 小时前
BYOK(自带密钥)使用指南
运维·服务器·前端·人工智能·midjourney
AI_零食1 小时前
呼吸灯 - 通过鸿蒙PC Electron框架技术完成-在焦虑时代守护每一次呼吸的数字禅修
前端·javascript·华为·electron·前端框架·鸿蒙
佛山个人技术开发1 小时前
高端旅游风景区酒店民宿网站模板 自适应宽屏文旅酒店源码
前端·html5·旅游