深入浅出HOOK API及完美应用

本文介绍了HookWin32API的概念、原理及其在Windows系统编程中的应用,包括网络监控、进程防杀、变速控制、屏幕取词等方面的研究。
    Hook Win32 API 是一项有趣而实用的WINDOWS系统编程技术,应用领域十分广泛。虽然已经有不少的文章介绍过 Hook Win32 API 的方法了,我还是来作些简单的介绍,以便大家了解其工作原理。

    Hook Win32 API 是什么意思?就是钩住Win32 API;那又何谓“钩”呢?就是绕弯的意思,让Win32 API函数的调用先绕一个弯路,在它执行实际功能之前,我们可以先做一些“预处理”,这样我们可以监视或定制某个Win32 API的调用,以实现一些特殊的功能。至于具体可以实现些什么样的功能,那就取决于程序设计者的想象力了。

    为什么要Hook Win32 API呢?因为在很多情况下,我们想监视或改变某个应用程序的一些特定的行为,但是那个应用程序却没有提供相应的接口,而我们又几乎不可能得到其源代码,怎么办呢?因为大多数WINDOWS引用程序的行为很大程度上依赖于Win32 API,所以我们可以采用Hook Win32 API的方式来试图监视和改变应用程序的行为。

    如何Hook Win32 API呢?实际上Win32 API是由一组动态链接库实现的,使用动态链接库是为了尽可能的共享内存。由于动态链接库是动态装入的,所以Win32 API函数的入口点也是动态确定的。当WINDOWS应用程序在调用Win32 API的时候,并不是直接调用某个函数地址,而是调用某处所存储的一个动态确定的函数地址来实现间接调用地,该处被命名为Import Address Table(简称IAT)。知道了这一点,接下来要做的就是想办法找到这个存储单元的位置,然后将其内容替换为接管函数的入口地址,不过得事先保存原函数的入口地址,以便执行了接管函数的代码后,可以在适当的地方以适当的方式再调用原函数。最后退出的时候或是不想再钩着它的时候,再将其恢复为原函数的入口地址。这就是Hook Win32 API的基本步骤,具体实现过程这里就不赘述了,可以参阅《WINDOWS 核心编程》(Jeffrer Richter著)

    向大家强烈推荐由微软开发的一个用于Hook Win32 API的库(有源代码):Detours 1.5 - 微软对自家的操作系统当然是最了解的了,相信Detours能在WINDOWS平台上运行得很好。点击这里下载:

Detours 1.5 (538KB)    http://ahzhuo.diy.myrice.com/download/detours.zip

 

 

Hook Win32 API 的应用研究之一:网络监控》
 绝大多数具有网络功能的软件都是基于socket(网络套接字)实现的,或者是使用了更高层的接口(例如:WinInet API)而最底层仍然是基于socket实现的。在大多数操作系统中都实现了socket接口,在WINDOWS操作系统中的实现称为WinSock。WinSock是以DLL的形式实现的,现在WinSock有两个版本的实现:WinSock 1.1(winsock.dll)和WinSock 2(ws2_32.dll),ws2_32.dll既支持WinSock 1.1的函数又支持WinSock 2规范中增加的许多额外的函数,我们可以像Win32 API一样的使用它,只是需要额外链接一个库而已。这里不讨论具体的WinSock编程,只是让大家了解,WinSock是WINDOWS应用程序与网络打交道的接口,是我们实现网络监控这个目的的突破口。

    好了,那我们就开始吧!“网络监控”这个范围有点太泛了,我们先把范围缩小到监控网络连接请求这个具体的操作上面吧,这也就是我的作品:IPGate 网址过滤器 的核心技术。我们先来看看一个TCP/IP连接是如何建立的:

    客户机端               服务器端
    ========               ========
                   监听套接字     连接套接字
                   ==========     ==========
    socket()       socket()
    bind()         bind()
                   listen()
    connect()----->accept()------>创建连接套接字
    send()----------------------->recv()
    recv()<-----------------------send()
         .
         .
         .
    closesocket()  closesocket()  closesocket()

    我们可以看出,是客户机端的connect()执行实际的连接请求动作,我们再来看看connect函数的参数:

int connect(
  SOCKET s, // 指定对哪个套接字进行操作
  const struct sockaddr FAR *name, // 这是一个描述服务器IP地址的结构
  int namelen // 指明上面这个结构的大小
);

对于name参数,由于sockaddr结构内容依赖于具体的协议,所以对于TCP/IP协议,我们传递sockaddr_in这个结构,再来看看这个结构:

struct sockaddr_in{
  short           sin_family; // 必须为AF_INET
  unsigned short  sin_port; // IP端口号
  struct in_addr  sin_addr; // 标识IP地址的一个结构体
  char            sin_zero[8]; // 为了兼容sockaddr而设置的占位空间
};

    到这儿,我们可以看出,对于一次连接请求的目的地信息,已经全部在传入的参数中描述清楚了,接下来要做的就设置一个全局API钩子,钩住所有程序的connect()调用,在进行实际的connect()操作之前,我们先分析传入的参数,如果发现连接目的地是我们不允许访问的,就不进行连接操作,仅返回一个错误码就可以了。就这么简单,就能实现一夫当关,万夫莫开的效果。

    同样的道理,也可以Hook其它函数而实现监控整个网络通讯各方面的内容,比如说截取发送和接收的数据包进行分析等等,这就取决于设计者的意图了,大家不妨动手试试看,感受一下Hook API的魅力。

 

 

 


Hook Win32 API 的应用研究之二:进程防杀
在WINDOWS操作系统下,当我们无法结束或者不知道怎样结束一个程序的时候,或者是懒得去找“退出”按钮的时候,通常会按“CTRL+ALT+DEL”呼出任务管理器,找到想结束的程序,点一下“结束任务”就了事了,呵呵,虽然有点粗鲁,但大多数情况下都很有效,不是吗?

    设想一下,如果有这么一种软件,它所要做的工作就是对某个使用者在某台电脑上的活动作一定的限制,而又不能被使用者通过“结束任务”这种方式轻易地解除限制,那该怎么做?无非有这么三种方法:1.屏蔽“CTRL+ALT+DEL”这个热键的组合;2.让程序不出现在任务管理器的列表之中;3.让任务管理器无法杀掉这个任务。对于第一种方法,这样未免也太残酷了,用惯了“结束任务”这种方法的人会很不习惯的;对于第二址椒ǎ赪INDOWS 9X下可以很轻易地使用注册服务进程的方法实现,但是对于WINDOWS NT架构的操作系统没有这个方法了,进程很难藏身,虽然仍然可以实现隐藏,但实现机制较为复杂;对于第三种方法,实现起来比较简单,我的作品:IPGate 网址过滤器 就是采用的这种方式防杀的,接下来我就来介绍这种方法。

    任务管理器的“结束任务”实际上就是强制终止进程,它所使用的杀手锏是一个叫做TerminateProcess()的Win32 API函数,我们来看看它的定义:

BOOL TerminateProcess(
  HANDLE      hProcess; // 将被结束进程的句柄
  UINT        uExitCode; // 指定进程的退出码
);

    看到这里,是不是觉得不必往下看都知道接下来要做什么:Hook TerminateProcess()函数,每次TerminateProcess()被调用的时候先判断企图结束的进程是否是我的进程,如果是的话就简单地返回一个错误码就可以了。真的是这么简单吗?先提出一个问题,如何根据hProcess判断它是否是我的进程的句柄?答案是:在我的进程当中先获得我的进程的句柄,然后通过进程间通讯机制传递给钩子函数,与hProcess进行比较不就行了?错!因为句柄是一个进程相关的值,不同进程中得到的我的进程的句柄的值在进程间进行比较是无意义的。

    怎么办?我们来考察一下我的hProcess它是如何得到的。一个进程只有它的进程ID是独一无二的,操作系统通过进程ID来标识一个进程,当某个程序要对这个进程进行访问的话,它首先得用OpenProcess这个函数并传入要访问的进程ID来获得进程的句柄,来看看它的参数:

HANDLE OpenProcess(
DWORD      dwDesiredAccess, // 希望获得的访问权限
BOOL       bInheritHandle, // 指明是否希望所获得的句柄可以继承
DWORD      dwProcessId // 要访问的进程ID
);

    脉络渐渐显现:在调用TerminateProcess()之前,必先调用OpenProcess(),而OpenProcess()的参数表中的dwProcessId是在系统范围内唯一确定的。得出结论:要Hook的函数不是TerminateProcess()而是OpenProcess(),在每次调用OpenProcess()的时候,我们先检查dwProcessId是否为我的进程的ID(利用进程间通讯机制),如果是的话就简单地返回一个错误码就可以了,任务管理器拿不到我的进程的句柄,它如何结束我的进程呢?

    至此,疑团全部揭开了。由Hook TerminateProcess()到Hook OpenProcess()的这个过程,体现了一个逆向思维的思想。其实我当初钻进了TerminateProcess()的死胡同里半天出也不来,但最终还是蹦出了灵感的火花,注意力转移到了OpenProcess()上面,实现了进程防杀。喜悦之余,将这心得体会拿出来与大家分享。

 

 

Hook Win32 API 的应用研究之三:变速控制 <<< 
    这是Hook Win32 API的一个比较另类和有趣的应用方面。

    这里所指的变速控制,并不是说可以改变任何程序的运行速度,只能改变符合这些条件的程序的运行速度:程序的运行速度依赖于定时控制,也就是说,程序的执行单元执行的频率是人为的依靠定时机制控制的,不是依赖于CPU的快慢。比如说,某个程序每隔1秒钟发出“滴答”声,它在快的电脑上和慢的电脑上所表现出来的行为是一致的。这样的依赖于定时控制的程序才是我们的研究“变速”对象。

    一个WINDOWS应用程序的定时机制有很多。像上面提到的例子程序可以采用WM_TIMER消息来实现,通过函数SetTimer()可以设定产生WM_TIMER消息的时间间隔。其它的方法还有通过GetTickCount()和timeGetTime()等函数得到系统时间,然后通过比较时间间隔来定时,还有timerSetEvent()设置时钟事件等等方式。先来看看这些函数的定义:

UINT_PTR SetTimer(
  HWND      hWnd, // 接收WM_TIMER消息的窗口句柄
  UINT_PTR  nIDEvent, // 定时器的ID号
  UINT      uElapse, // 发生WM_TIMER消息的时间间隔
  TIMERPROC lpTimerProc // 处理定时发生时的回调函数入口地址
);

MMRESULT timeSetEvent(
  UINT               uDelay, // 时钟事件发生的时间间隔
  UINT               uResolution, // 设置时钟事件的分辨率
  LPTIMERCALLBACK    lpTimerProc, // 处理时钟事件发生时的回调函数入口地址
  DWORD              dwUser, // 没峁┑幕氐魇?br />  UINT               fuEvent // 设置事件的类型
);

DWORD GetTickCount(VOID) // 返回系统启动以来经过了多少毫秒了

DWORD timeGetTime(VOID) // 类似于GetTickCount(),但分辨率更高

    那么我们来看,如果能控制SetTimer()的uElapse参数、timeSetEvent()的uDelay参数、GetTickCount()和timeGetTime()的返回值,就能实现变速控制,除非应用程序使用的是其它的定时机制,不过大多数应用程序采用的定时机制不外乎都是这些。

    该轮到Hook大法出场了。因为我们一般只想改变某个程序的速度,比如是说某个游戏程序,所以我们不设置全局钩子。又因为我们不清楚那个应用程序到底使用的是那种定时机制,所以上述几个函数我们全部都要接管,然后把关于定时参数或返回值按比例缩放就可以了。

    顺藤摸瓜,是不是很简单?
 

 

 

 

 

Hook Win32 API 的应用研究之四:屏幕取词 <<< 
    用过金山词霸吧?用过的人一定对它的屏幕取词功能印象很深刻,因为这种功能使翻译过程更加简便快捷,屏幕取词是金山词霸的核心技术之一。

    大家有没有想过这样神奇的功能是如何实现的呢?经历过DOS年代系统编程的人可能知道,屏幕上显示的字符是存放在显存里的,每个坐标的字符对应显存的一个特定的现存单元存储的字符,直接操作显存,就可以进行字符的显示和读取,若WINDOWS是这样就好了,可惜事实上相去甚远。那WINDOWS的字符是怎样显示的呢?WINDOWS是图形界面,显示的最小单位是像素(Pixel),上面的所有东西都是“画”上去的,当然也包括了字符,也就没有什么字符显存的概念了。没有了直接操作显存而获得屏幕上字符内容的办法了,那还有什么方法呢?

    让我们来设身处地地想想看,假如我们要在自己的程序中显示一个字符串,我们会怎样做呢?不要回答是MessageBox(),我们不是指的这种“显示”方法,我指的是最低阶的方法,也就是直接操作DC的方法,我想一般就是调用上面提到过的Win32 API函数TextOut()了,当然,还有类似的一些其它函数,例如:ExtTextOut()、DrawText()、DrawTextEx()等等。好了,找到点眉目了,我们来看看这些函数的参数能提供哪些信息,这里只列出TextOut()函数的定义,其它的函数基本都包含这些参数,另外提供了更多的附加选项而已,请查阅MSDN相关文档:

BOOL TextOut(
  HDC       hdc, // 设备上下文句柄
  int       nXStart, // 开始绘制字符串的位置的x坐标
  int       nYStart, // 开始绘制字符串的位置的y坐标
  LPCTSTR   lpString, // 指向字符串的指针
  int       cbString // 指明要绘制多少个字符
);

    我们看到,坐标和内容都有了,这不正是我们想要的信息吗?只要Hook住这个函数,这些信息不都唾手可得了吗?于是祭出Hook大法来做个实验:先随便用VC的向导开辟一个单文档应用程序,在OnDraw()函数里调用TextOut()在某个位置随便输出一个字符串(不论是调用pDC->TextOut(...)或者是::TextOut(...)都一样,CDC类只不过把TextOut()封装了一下而已),然后在OnInitialUpdate()里设置Hook(用现成的库),钩住TextOut(),截获TextOut之后,让TextOut()输出另外一个字符串而不输出原来的字符串。还要记住在OnDestroy()里解除Hook。最后编译连接,测试程序。你会发现不仅是你调用TextOut()输出的地方的字符串被替换了,而且连才旦、对话框等等有字的地方也变了,在实验成功之余,是不是个意外的收获?其实WINDOWS内部的大多数文字输出也是调用了TextOut()函数来实现的。现在水落石出了,我们只要Hook住文字输出函数,包括我上面提到的和没有提到的函数,就能截获屏幕上文字输出的坐标和内容等等信息,只要我们一一作记录,并加以分析转换,跟鼠标的位置进行比较,我们就能得到屏幕上某个位置的文字内容是什么了,要翻译怎么的,就看你的了,这就是屏幕取词,虽然实际上实现的过程并不像说得那么简单。

    出了词霸的屏幕取词,还有一些动态汉化、外挂中文平台之类的软件,也是基于这种技术的,现在看来,它们是不是已经不再神秘了? 

原文地址: http://blog.youkuaiyun.com/geagle/article/details/1598572
wow64_hook 源码历史更新 --------------------------------------------------------------- 2021/4/16  模块源码1.8.7 更新: 1:重新架构了穿插汇编指令,优化了一些代码和流程 2:在 远程hook64指令_安装()时新增可回调的3个自定义参数值,这些值在回调接口的[寄存器64.自定义值1;2;3]里可获取到该值 3:修复 寻找无效8字节指令地址()中一个重要BUG,此BUG极大可能导致之前版本在 远程hook64指令_安装()时即导致目标程序崩溃的现象 本次更新比较重要 建议使用者更新到此版本使用............ --------------------------------------------------------------- 2021/4/15  模块源码1.8.6 更新: 1:新增3组函数:X64_取模块代码区起始地址(),X64_取模块入口地址(),X64_取模块代码执行段大小() 2:自定义类型:模块信息64,的成员构成新增改动为 以下,在枚举模块中亦可直接取得 成员 模块基址, 长整数型, , , 模块映像的内存地址 也称为句柄 成员 模块长度, 整数型, , , 整个模块文件长度 成员 模块入口, 长整数型, , , 模块入口函数地址 如 Mian/DllMain 成员 模块代码入口, 长整数型, , , 模块代码执行区起始地址 成员 模块代码长度, 整数型, , , 模块代码执行区的长度 成员 模块名称, 文本型, , , 文件名称 成员 模块路径, 文本型, , , 完整的路径地址 3:新消息接口()远程返回_调用回调子程序()优化了代码严谨性,减少hook目标崩溃的可能性 4:寻找无效8字节指令地址()由之前的全模块查找 改动为 在模块代码执行区查找 5:改写模块实列为 一对多的模式 6:模块实列操作控件的方式由变量改为堆内存,避免引起多线程自身崩溃 7:模块实列 对 recv,recvfrom两个函数hook方式由原先 在回调内 暂停recv-->recv_call-->恢复recv,的方式改为经过特殊改造的 recv_call,这个call经过特殊处理,在recv回调函数调用,用来取得真实长度,这个调用会绕过hook位置,所以不会触发 recv回调,详见源码 8:修改了一些已知可能出现的问题 --------------------------------------------------------------- 2021/4/12 模块源码 v1.8.2更新 1:修复 x64_远调用函数()在 易语言 主线程调用时造成消息无法回调,导致易语言主线程窗口卡死的问题。      感谢楼下易友发现的BUG,已经第一时间更新 --------------------------------------------------------------- 2021/4/12 模块源码 v1.8.1更新 1:修复 hook全部卸载时的流程写法的一个错误,由于句柄的提前关闭导致多个hook点卸载不干净的问题 2:改写了消息回调时线程传参的代码优化,优化了其他一些小问题 3:  鉴于很多朋友需要,改写了模块自带实列,对TCP,UDP的两组封包函数做了hook实列写法 4:列子中同样增加对x64_远调用函数()的应用写了几个列子,如使用套接字取得本地或远端IP端口API调用的的应用实列 5:本hook模块不支持非模块内存区hook,如申请的动态分配页等,不是不能支持,只是觉得没有任何意义,对这方面有需求的,自行改写模块源码使用 提醒:hook回调函数中尽量减少耗时代码,时间越长返回越慢,回调中谨慎操作控件,如必须要用到可参考源码中实列写法采用线程操作 --------------------------------------------------------------- 2021/3/1   模块源码v1.6更新: 1:修复  x64_远程调用函数()命令,在没有提供 寄存器 参数时,没有返回值的BUG。 --------------------------------------------------------------- 2021/2/28 模块源码v1.5更新: 一:修复win7 64位系统下枚举模块 出现部分模块长度出现负数的问题,从而导致部分win7用户不能使用 二:强化 远程hook64指令_安装 的稳定性:        1,穿插代码中增加对标志位的保护,避免hook位置长度下一条指令为跳转时产生跳转错乱的问题,强化了hook任意位置的定位        2,因为穿插代码中会调用AP
<think>好的,我需要帮助用户优化关于动态添加删除表单校验的说明。用户已经按照STAR模型(背景、任务、实现、结果、反思)组织内容,现在希望进一步优化,尤其是使用ES6新方法,比如扩展运算符。 首先,我需要检查用户提供的现有实现部分。用户提到使用扩展运算符的特性来合并校验规则,例如: ```js this.rules = { ...this.rules, xhfLxdh: this.xhfLxdh, ghfLxdh: this.ghfLxdh }; // 添加校验 this.rules = { ...this.rules, xhfLxdh: [], ghfLxdh: [] } // 清除校验 ``` 这里的逻辑是通过对象展开,后面的属性覆盖前面的,从而实现动态修改规则。用户还提到在修改规则后需要清除校验结果,否则可能触发旧规则的校验。 接下来,用户的优化需求包括: 1. **更清晰的代码结构**:可能需要将动态更新规则的部分封装成函数,提高可维护性。 2. **ES6方法的深入应用**:除了扩展运算符,是否还有其他ES6+特性可用?比如箭头函数、解构赋值、可选链等。 3. **项目反思部分需要体现深度**:用户建议多考虑使用ES6新方法,可能需要讨论如何结合现代框架(如Vue/React)的最佳实践,或如何处理深层对象的问题(如嵌套规则)。 4. **注意事项补充**:比如浅拷贝问题、性能优化(避免频繁合并大对象)、不可变数据的重要性等。 在优化时,我需要确保: - **保持STAR结构**:优化后的内容仍需符合背景、任务、实现等部分,但每个部分更详细或更规范。 - **代码示例更规范**:比如添加注释,使用更通用的变量名,或展示如何在框架中结合使用(如Vue的响应式注意事项)。 - **强调ES6+的优势**:比如使用可选链操作符?.处理深层对象,避免错误;使用箭头函数简化回调。 - **可能的改进点**:比如使用Map结构管理动态规则键,或使用自定义Hook/Composable函数封装逻辑。 现在需要组织这些思考,形成结构化的优化版本,确保语言口语化,但内容专业,符合用户给出的格式要求(如代码块、强调重点)。</think>--- ### 🌟 **优化后的动态表单校验方案(STAR模型进阶版)** --- #### **一、背景(Situation)** 复杂表单常存在**动态依赖关系**: - 表单项的必填性由其他字段的值决定(如选择“企业”需填写税号,个人则不需要) - 传统方案需手动操作校验规则,代码冗余且易遗漏边界条件 --- #### **二、任务(Task)** 实现**动态、精准**的表单校验规则切换,要求: 1. 规则变更时自动覆盖旧配置,避免残留 2. 清除旧校验结果,防止错误提示残留 3. 代码简洁易维护,符合现代框架最佳实践 --- #### **三、实现(Action)** **👉 核心策略**:ES6扩展运算符 `...` + **不可变数据模式** ##### **1. 动态规则管理(ES6优化版)** ```javascript // ✅ 封装动态更新方法(通用函数) const updateValidationRules = (field, rules, required) => { return { ...rules, // 保留其他字段规则 [field]: required ? [/* 必填规则 */] : [] // 动态覆盖目标字段 }; }; // ✅ 使用示例(Vue场景) // 当发票类型切换时触发 handleInvoiceTypeChange(type) { const isCompany = type === 'company'; this.rules = updateValidationRules('taxNumber', this.rules, isCompany); this.$nextTick(() => this.$refs.form.clearValidate('taxNumber')); // 清除旧校验 } ``` ##### **2. 高级技巧:结合现代框架特性** ```javascript // 🌰 Vue3 Composition API 封装(深度优化) import { ref, watch } from 'vue'; export function useDynamicForm() { const rules = ref({}); const setRule = (field, ruleConfig) => { rules.value = { ...rules.value, [field]: ruleConfig }; // 不可变更新 }; // 监听依赖项自动更新规则(如监听发票类型) watch(invoiceType, (newVal) => { setRule('taxNumber', newVal === 'company' ? [{ required: true }] : []); }); return { rules, setRule }; } ``` ##### **3. 优化关键点** - **扩展运算符特性**:利用 `{ ...old, [key]: newValue }` 确保规则覆盖 - **不可变数据**:避免直接修改原对象,防止副作用 - **自动清理校验**:更新规则后调用框架的 `clearValidate()` 方法(如Element UI/Vant) --- #### **四、结果(Result)** 1. **精准控制**:字段级规则动态切换,无冗余校验 2. **代码简洁**:逻辑集中管理,减少 `if-else` 分支 3. **框架友好**:适配 Vue/React 响应式系统,性能优化 --- #### **五、深度反思与扩展** ##### **1. 进阶优化方向** - **规则工厂模式**:用 `Map` 或 `Object.entries` 管理规则配置表 ```javascript const ruleConfigs = { taxNumber: (isCompany) => isCompany ? [{ required: true }] : [], phone: (hasPhone) => hasPhone ? [{ pattern: /^1\d{10}$/ }] : [] }; ``` - **异步规则支持**:结合 `async-validator` 处理服务端校验 - **TypeScript强化**:定义 `RuleSet` 类型,确保类型安全 ##### **2. 踩坑警示** - **浅拷贝陷阱**:嵌套规则需使用深拷贝(如 `JSON.parse(JSON.stringify())` 或 `lodash.cloneDeep`) - **性能优化**:避免频繁合并大对象,推荐按需更新 - **框架差异**:Vue中需用 `this.$set` 或响应式API,React依赖 `setState` 不可变更新 ##### **3. ES6+ 特性综合应用** ```javascript // ✅ 解构 + 动态键名(Computed Property Names) const dynamicField = 'email'; const newRules = { ...rules, [dynamicField]: [/* 规则 */] }; // ✅ 可选链(Optional Chaining)防止空值 const currentRule = this.rules?.taxNumber?.[0] || {}; // ✅ 箭头函数简化回调 fields.forEach(field => this.clearValidation(field)); ``` --- ### **总结** 通过 **扩展运算符** 的覆盖特性 + **现代框架响应式机制**,动态表单校验可达到**高内聚、低耦合**的效果。核心在于: 1. **声明式规则管理**:而非命令式操作 2. **不可变数据流**:确保可预测性 3. **结合框架生态**:如 Vue 的 `$refs.form.validate` 或 React 的 `formik` 库 > **扩展思考**:在超大型表单中,可引入 **状态管理工具**(如 Vuex/Pinia/Redux)集中管理校验规则,进一步解耦 UI 与业务逻辑。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值