前端开发技巧【vue、react、组件化、实用工具】ing

263 阅读26分钟

本文会总结前端开发过程中“神奇”的一些操作,只要还没有被ai干掉,我就会持续更新

[2025年7月14日] 即使是在今天,之前吹的神乎其技的cursor、chatgpt都还是没办法很好的理解需求,同一个问题,越改错的越离谱。大胆预测,ai应该还是只会成为辅助工具,毕竟他现在连取代初级程序员的实力都没有。

一、组件封装、组件库二次封装原则

前端开发,一般都会根据每个项目ui的设计,独立个性化的定制一些“base”组件,在写项目的过程中总结了一些开发技巧

1. 对组件库,常见的比如element-uiant-design-vueant-design进行二次封装的时候,原则应该是最大限度的还原组件库本身的api调用方式,减小接手项目的上手成本。

  • 比如封装一个后管系统的表格base组件,表格数据获取方法应该被统一封装,组件使用的时候在base组件里面统一调用赋值方法,传入对应的接口请求函数,特殊情况下的数据处理方法即可。

  • base组件的核心的prop应该直接通过v-bind,v-listen直接继承组件库本身table组件对应的api和方法,在base组件使用的时候直接在组件本身写下api即可,直观,易上手。

  • 表格列的封装,建议参考ant-design-vue的表格列设置属性,设置一个列配置对象,对象上设置对应的prop,label等参数,直接对应组件库表格列本身的api,易上手,好维护。定义一个特殊的render函数,对需要进行过滤,枚举显示的字段进行相应处理;定义一个插槽属性,对jsx或者h函数不方便处理的情况,直接使用template标签进行处理,需要返回row,index,value属性,方便进行数据操作。基于此,可以做到对每个单元格的精细化操作。

  • 分页组件,直接集成到表格赋值函数的处理中,在base组件上实现当前页,总数,分页操作,避免在使用的时候每个组件又单独定义一个分页控制对象。

  • 搜索组件,base组件底层实现“搜索”,“重置”功能,避免大量的重复代码,不利于后期修改维护。比较理想的情况是在表格列配置对象中直接添加一个search属性来控制当前列对应的字段是否支持搜索(一个合格的产品,有搜索条件,表单项应该有对应的字段显示,有搜索条件但是表格列中没有对应字段的愚蠢需求不在此考虑范围内)。同时,通过计算属性等方法,底层实现搜索项的展开/折叠。搜索字段中会有日期,下拉框等复杂情况,这种情况下建议使用插槽实现,组件库原生的输入组件对每一个人来说都是最容易上手的。

  • 枚举值的显示,应该通过上边提到的column配置项中的render函数,获取前端状态管理库里面的数据进行枚举,如果枚举只有单纯的key value形式,一般直接使用对象,访问的时候会方便很多。obj[key]进行访问即可,复杂情况下就需要定义成数组,使用的时候通过arr.find(item =>item.key === value)?.value函数来进行显示。而不是在每个使用到枚举的页面,都调用一次枚举接口进行数据访问。

        // 使用对象的key-value的形式保存简单枚举
        const NAME_LIST = {
            zhangsan: '张三',
            lise: '李四'
        }
        
        // 使用数组的方式保存复杂枚举
        const ARR_ENUM = [
            {
                label: '张三',
                value: 'zhangsan',
                ...
            },
            {
                label: '李四',
                value: 'lisi',
                ...
            },
        ]
        
        // 定义枚举查询函数,查询出对应的数据
        const useEnum = ARR_ENUM.find(item=> item.value === value)?.label
    
  • 开发过程中,表格对应的表单项会有很多很个性化的需求,比如联动,分列,分组。这种情况下,一般建议单独对表单项组件进行定义即可,直接使用组件库本身的表单输入组件会加快特殊需求的实现以及后期维护成本。

  • 经常使用到下拉框组件,文件上传后给后端提交的信息组件,一般建议针对项目进行单独封装,使用的时候直接v-model字段名,绑定方法接受数据处理结果即可。一般下拉框组件也需要二次封装,方便使用。

  • 项目中如果涉及到表示状态的标签,建议定义一个通用组件,对颜色进行统一管理,使用的时候传入颜色对应的标识符,方便后期对整个项目标签的颜色进行统一修改。

上述原则,有一个组件库基本全部违背,那就是avue,这玩意儿用的是真让人难受。

2.自定义组件封装原则

  1. 这就必须提到设计模式中的单例模式,一个自己写的从零到一的组件,应该专注实现一个功能,复杂的功能组件,比如前文提到的baseTable,应该是很多小的单一功能组件集合到一起形成的通用组件,该组件还需要根据ui进行自定义。
  2. 组件名,prop字段名取的时候应该尽量使用语意化,不要使用拼音,不要在某个字段名后边加罗马数字1,2,3区分不同参数。这会让阅读你代码的人觉得你很蠢。
  3. 尽量在prop接收的对象中对每个字段名的含义添加注释,否则后来的人只能靠猜。很复杂的组件还应该提供对应的README文件,里面提供参数说明的使用示例。

基于业务开发过程中不同的需求,将组件封装可以分为业务类组件和功能性组件。

业务类组件功能性组件
目的是实现某一个特定的功能,比如说常用的新增/编辑表单为了使用方便,实现单一功能的功能性组件,类似于工具函数的定位,比如说通用的上传组件、下拉选择组件

2.1 业务类组件

业务类组件封装的要求就是集中于实现一类特定的数据,比如说一个项目里面会用到很多类似的功能模块,这个模块要集中的实现前端数据的增删改查,就可以将这部分的功能单独封装起来,对外只提供数据加工能力,而数据管理能力保留给消费组件,保证组件使用的灵活性以及开发过程中业务需求的个性化定制。

比如说一个表单组件,它应该只提供表单数据的收集功能,数据的提交/表单值的初始化能力暴露给消费组件,保证组件使用的时候的灵活性。

2.2 功能性组件

上文提到的,如果是功能性组件,应该尽量保证一个组件实现单一的一个功能,不要将多个功能封装到同一个功能组件内,比如说项目开发都会使用到的上传功能,每个后台项目对上传接口的封装都不一样,就可以封装一个上传组件,对接口返回数据进行统一格式化,对数据进行统一处理,方便业务使用,vue可以封装v-model绑定数据,react可以查找对应组件库的实现方式,对值进行处理。

二、开发过程中的注意事项

1、取一个正确的字段名

请一定要避免取的参数名是靠在单词后边加1,2,3来进行区分的,比如name1,name2,name3,读到这种代码你能不能告诉我这name分别是什么含义

2、封装正确的工具函数事半功倍

  • 涉及到数学运算的,通过big.js库二次封装加减乘除方法,使用的时候引入工具函数,而不是到处引用big.js库
  • 涉及到日期格式化的,使用日期库date-fns,封装日期处理函数,全局统一定义日期格式,比如YYYY-MM-DD,方便维护的时候进行统一修改
  • 涉及到金额格式化的和上边一样,使用numeral,封装金额格式化函数,全局统一定义金额格式

3、学会读package.json中的依赖列表

上手一个现有项目时,应该关注一下依赖列表,如果里面有自己不熟悉的npm包,请先查阅该组件库的功能,而不是固步自封,一辈子在自己的温柔乡里面写代码,不了解项目特色就开始加代码,你很有可能在人家精装修的项目里面当众出丑,毕竟git记录谁都看得到。

4、良好的组件命名习惯

修改日期:2025年7月8日

4.1 文件夹、组件命名

vue官方推荐的命名方式是VueComponent(大驼峰式),这种命名方式,在单文件组件内部又推荐的是短横线命名法,vue-component。

这是官方的推荐,应该很多人开发还是习惯使用vscode,在vscode中维护项目时,用的最多的应该是ctrl+F,文件内查找,但是使用大驼峰式命名法注册的组件,使用短横线的方式使用的时候,ctrl + F没办法很好的识别,这时候就会导致源码溯源困难,而且,维护过大项目的同学应该都有一种感觉,大驼峰式命名的文件目录看起来就是很拥挤的一整块,文件名的分辨能力不高。所以,我更推荐跟官方反过来。

文件、文件夹的命名使用短横线命名法。

单文件组件内部注册的时候使用大驼峰式命名法。这样不仅方便ctrl + F溯源,代码还会有高亮效果,更加一目了然,而短横线命名法的组件就没有高亮

4.2 单文件组件注册规范

在正常的开发流程中,一般会要求新写组件的时候,name属性和文件命名相同,这样能方便组件的递归使用。

为什么还要单独强调这一点,因为在最近的工作中,由于之前的人留下的糟糕的代码,文件命名和引用不一致,导致出现了vue的文件引入报错,排查了半天才发现是文件循环引用的原因,按照之前的开发习惯,这种问题本不该成为问题,但是却花费了我小半天时间排查。

文件循环引用需要使用动态引入的方式注册组件,否则会一直报错。

4.3 不要使用index默认命名法

学开发流程的时候,肯定有人讲过文件的命名方式,以及前端工程化文件的扫描方式,会告诉你文件夹命名可以用index命名,也能正常查找到

但是,极不推荐这种做法,因为省略具体文件名的路径在vscode里面没办法被jsconfig/tsconfig文件解析,导致按住ctrl键跳转的方式不能使用,对于代码维护来讲极不方便。另外,在同时维护很多个文件的时候,一看导航栏,全是index,极难区分

5、css隔离方案

5.1 vue项目【每个组件都必须加scoped】

所有的单文件组件内的样式,都加上scoped属性,避免页面加载多了之后出现样式错乱

为什么要强调这一点,因为有些人做的是真的很差。写的页面用着用着就发现样式乱跳。

局部样式局部定义,全局样式写到assets文件夹下的全局css文件夹内,方便管理。anther way,全局公共样式的类名不要重合,不要重合,不要重合,有的项目一打开控制台,css样式加载了一大堆,结果排在下边优先级较低的,上面全是横杠,样式又高度重合。

这就说到,类名其实也支持过滤,你可以在控制台输入类名筛选出你的目标类名。

5.2 react项目【推荐styled-components】

react支持的css方式我就不一一赘述了,都介绍下缺点,

  1. css module,类名没办法按照css规范去命名,写起来就很怪,而且使用的时候也很不方便,都得用变量的方式。
  2. tailwind css,外网极度推崇的css使用方式,但是,在有大量css样式的场景下,类名变得很长,而且,难以复用,你只能复制一个个的类名。而且,类名也有很高的上手成本,明明3s可以实现的代码,因为你不知道类名,要去官网现查,可能要5分钟才能实现。
  3. 普通css就更不用说了,没办法实现样式隔离,很容易导致样式错乱。
  4. 内联css,很蠢的一种方式,组件复用的时候根本没办法强制修改样式。

当前推荐的方式:styled-components

运行时的样式实现方式,现在react都是函数式组件,这种方式也是函数式的,还支持动态传入变量实现动态css,很推荐。由于它输出的使用方式是类似于组件,所以也有很高的复用方式。虽然写它失去了css代码提示,但是最起码跟正常的css使用一致,没有额外的上手成本,而且,多手敲还能提升熟练度。进一步,现在都是ai辅助开发,ai代码提示也弥补了没有编辑器自带提示的问题。

另外,涉及到组件复用的话,多说几句,优秀的集成项目应该是有统一的风格,这种情况下你完全可以定义全局类名实现全局统一样式,部分样式的重用你也可以使用复用组件的方式来实现。

6、页面路由和源代码文件夹结构严格对应

如果是从0搭建的项目,一般除了遵守正常的文件夹规范,设置好统一的全局路由规则,axios、规定好工具函数、公共组件的放置规则之外。另一个很重要的默认要求就是,设置页面路由的时候,请严格将页面路由与文件夹路由一一对应,这样做有很多好处。一般要求路由的最后一个地址就对应源代码文件名。

  1. 项目维护的时候不需要再根据路由地址去路由配置文件查询对应的源代码文件地址,有的项目的路由注册还有很复杂的规则,不是严格的路径设置,就导致这个步骤就可能会花费大量时间。
  2. 根据路由地址,使用vscode的 ctrl + p面板,粘贴进路由地址可以快速的跳转源代码文件。过程及其丝滑。如果使用的是webstrom,可以双击shift,可以达到类似的效果。

Tips: 现在vite搭建的最新项目里面有个控制面板,打开这个功能可以快速的实现从浏览器的开发者工具中的vue devtool插件里面直接跳转进vscode源代码位置。但是,这个插件目前【2025年7月12日】有很多的bug,可能会与业务需求中的某个插件起冲突,而且用了这个插件之后,感觉页面整体运行会变慢,它还要求必须安装devtool。对于某些不太方便安装依赖的项目不怎么友好。

研究过相关的技术文章之后,发现实现原理其实很简单,vscode的唤起可以使用浏览器的地址栏直接操作,而开发环境的vue单文件组件编译,里面本身就吧包含了每行代码的源文件信息,两者一结合,写一个简单的工具函数就可以0依赖的实现类似的功能,甚至更方便,比如说,直接与ctrl键绑定,按住ctrl + 鼠标左键生成一个a标签,点击之后直接跳转。

7、正确设置你的.gitignore

  1. 很多脚手架的默认设置的gitignore文件中会忽略.vscode文件,但是这个文件对统一开发风格有很大的作用,它不该被忽略
  2. 依赖锁定文件也不应该被忽略,package-lock.json、pnpm-lock.yaml都不应该被忽略,固定好项目的版本对后续项目维护意义极大
  3. 应该被忽略的只有那些临时性文件/打包构建产物。一切与开发相关的配置都不应该被忽略。

三、代码格式化,设置好项目的eslint和prettier

  • 项目开始之前,设置好ctrl s保存的时候代码进行格式化,能让你写代码有一种写诗的感觉,像是在做艺术,而不是搬砖
  • 对于项目中使用单引号还是双引号,个人喜欢单引号,因为,它看着干净一点
  • 语句结尾是否使用分号,建议不要,理由也是,没有分号会干净一点
  • 每个对象key value后边要不要加逗号,建议加,因为在对对象属性进行添加的时候可以一气呵成的在对象结尾回车,按回车的时候会有一种畅快感,想加在哪加在哪,而不是加到最后一行之后因为没有逗号而没有编辑提示和出现烦人的红线
  • 数组里面放对象时,数组的左括号和第一个对象的左括号要不要单独成行?建议要,因为在对对象进行复制的时候不用小心翼翼的注意不要选到数组的左括号

上述不应该仅仅是个建议,应该成为规范并在项目中严格执行。可以设置git钩子强制这个流程

四、开发过程中可能会使用到的实用工具

1、完全卸载软件[windows], geek

如果是windows用户,使用windows自带的控制面板卸载软件很多时候会有大量的注册表信息残留,下一次安装的时候还会有痕迹

2、磁盘空间分析大师[windows], spacesniffer

github开源的软件,可以用于分析磁盘空间的占用情况,看到每个文件在磁盘上使用的空间占比

3、github访问神器, Watt Toolkit

上架了微软商店,可以直接在微软商店下载,使用免费,可以用于访问github,steam等常用软件

4、greenhub

上网助手,下载软件,访问国外网站请求缓慢时可以试一下,免费

5、node版本管理工具,nvm-windows

在实际工作中,会遇到不同时期的项目,node版本要求不一样的情况,和nvm desktop相比,nvm-windos是一个命令行工具,这就意味着,可以在任何地方打开cmd窗口,直接进行node版本切换,而不用像nvm desktop一样还要去找到对应的软件,打开面板,点击版本进行切换

// 常用的nvm操作工具
nvm list  // 查看本机安装的所有node版本
nvm list available // 查看互联网上的node版本列表
nvm install [版本号]  // 安装对应版本号
nvm uninstall [版本号] // 卸载对应版本
nvm use [版本号] // 切换到对应的node版本

// 常用的node命令
node -v // 查看当前node版本
npm -v  // 查看当前npm版本
npm init -y  // 初始化一个npm管理的项目
npm install [npm包名]  /  npm i [npm包名]  // 安装npm包,默认是安装到dependices,运行依赖
                                          // 如果想仅安装开发依赖,比如  npm i less-loader -D 
                                          // 该依赖在npm run build的时候不会被打包
                                          // npm i yarn -g 全局安装一个叫yarn的npm包,可以在任意位
                                          // 置运行
npm i [npm包]@版本号  // 安装指定版本的npm包
npm view [npm包] versions  // 查看所有的npm包版本
npm uninstall [npm包名]  // 卸载npm 包
npm config set registry [https://registry.npmmirror.com](https://registry.npmmirror.com/) // 设置阿里云代理
npm list -g // 查看当前全局安装的npm包列表

// 其它包管理工具
yarn  // facebook推出的包管理工具,但是,它的缓存会占用大量的磁盘空间,不推荐
pnpm  // 自带国内代理的包管理工具, vue官方也在使用,安装速度快,支持monorepo,推荐

五、Corepack【node官方管理包管理器的管理器】

corepack是一个实验性工具。从Node.js v16.13版本开始引入,它默认支持pnpm和yarn,不用再单独安装,还可以指定只使用对应的包管理工具

1、由于是实验性工具,默认是关闭的,需要手动开启

corepack enable  // 开启corepack
corepack disable  // 禁用corepack

开启之后,可以直接pnpm -vyarn -v查看当前版本号

2、配合package.json可以指定包管理工具

{
  "name": "vite-project",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "packageManager": "pnpm@8.15.6",  // 关键的一行,指定包管理工具及版本号
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "preinstall": "npx only-allow pnpm"  // 设定项目的时候运行一次,只允许使用pnpm
  },
}

3、pnpm、yarn版本的更新时间 Node.js引用的是corepack仓库的corepack/config.json文件,这个文件是每周星期天的00:05同步,所以可以说是最新版

4、如果我想用6.11.0版本替代当前的pnpm版本,可以使用

corepack prepare pnpm@6.11.0 --active

缺点

不能离线使用,电脑没有网络的时候用不了。

六、开发过程中奇技淫巧

1、vue页面刷新

在使用i18n国际化插件,特定操作后需要刷新页面的场景中,可能需要对页面进行局部/全局刷新。此时就可以利用v-if状态切换组件会重新挂载的特性,很快速的切换v-if的值,使当前/全局组件进行重新挂载。在特定的场景下这种思路有奇效。能省略一大部分代码。

2、在ts项目中。对storage对象重新封装,利用enum统一管理storage的key

利用本地化存储进行数据保存,很方便,但是会有一个弊端,由于localStorage对象可能会在项目的任何一个角落访问,就会造成key管理困难,而对storage对象进行统一封装且约定set函数的key值为ts约束的enum的时候,这种情况就会好很多。

3、实现比16像素更小的字体

a、使用css的zoom属性,缺点是会触发浏览器的回流和重绘,性能不太好; b、使用transform的scale属性,兼容性好,不会触发回流,推荐。

4、vue2数据声明避免无用的响应式

在vue2开发的时候,应该会遇到需要定义一个数组/配置项供template访问的问题,很多人会直接将数据定义到data里面,这样能实现业务,同时由于js的优化,页面也不会很慢。但是如果对代码有追求的同学,应该都会想有没有别的解决方案?

4.1 使用computed计算属性

<template>
  <ul>
    <li v-for="(item, index) in listArr" :key="index">
      <p>姓名:{{ item.name }}, 年龄:{{ item.age }}</p>
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {}
  },
  computed: {
    listArr() {
      return [
        { name: 'zhangsan', age: 18 },
        { name: 'lisi', age: 20 },
      ]
    },
  },
}
</script>

4.2 使用Object.freeze()冻结对象

<template>
  <ul>
    <li v-for="(item, index) in listArr" :key="index">
      <p>姓名:{{ item.name }}, 年龄:{{ item.age }}</p>
    </li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      listArr: Object.freeze([
        { name: 'zhangsan', age: 18 },
        { name: 'lisi', age: 20 },
      ]),
    }
  },
}
</script>

效果相同 使用前

image.png 使用后 image.png

前后对比可以看到,使用Object.freeze()冻结后声明在data里面的数据,vue2会直接跳过响应式设置这一块,由于少了一个步骤,内存占用会稍微低一点,这对于那种特别大的数据项尤其有用

但是经过实测,组件挂载的时间没有提升多少,而且,由于其本身就是毫秒级,这种优化在运行效率上的作用可能有限,但是,在控制台输出干净信息这件事上,还是有明显效果。

5、vue3的开发技巧

对比vue2,vue3则更深入一些,不需要响应式的数据不要使用响应函数进行包裹,如果是配置项/子组件传入的标签之类的数据,使用shallowRef,shallowReactive包裹,仅有真正需要交互的数据才使用ref/reactive进行处理

七、你真的会用vue吗?

题主的vue是在b站学的。那时候学,讲响应式、父子组件通信的时候,有些知识点一直都记忆犹新,比如:

1、后期对对象属性进行新增,vue不会有响应式

2、vue是MVVM模型,保证单向数据流,子组件不应该直接修改父组件的传值,为此vue还开发出了emit函数,.sync修饰符,组件的v-model,还有特护语法,比如prop传函数,进行父子组件通信。

【2025年7月28日】 但是,最近使用react开发复杂表单项目,体验到了antd中表单组件的自动装配之后,发现react的灵活性确实很强,可以随意的进行组件设计。这也就引发了对vue的思考,想验证下vue是否有类似的能力。在这个过程中,又有了一些新的发现,直接就感觉,好像从来没学会过vue。

1、react中表单组件的灵活度

基于这种组件设计,你可以随意对表单进行复杂逻辑编辑以及高效的表单复用。同时,表现层和逻辑层分离,你可以为每种业务场景定制不同的逻辑。

image.png

// seperate-form.tsx
import { Tabs } from 'antd'
import type { TabsProps } from 'antd'
import OriginForm from './components/origin-form'
import ViewMode from './components/view-mode'
import EditMode from './components/edit-mode'
import BusinessWithoutName from './components/business-without-item'
import BusinessDisableItem from './components/bussiness-disable-item'
import styled from 'styled-components'

const PageTitle = styled.h2`
  margin-bottom: 16px;
  background-color: red;
  padding: 8px 16px;
  color: #fff;
`

const items: TabsProps['items'] = [
  {
    key: '1',
    label: '公共基础表单显示所有的字段',
    children: <OriginForm />,
  },
  {
    key: '2',
    label: '查看模式,禁用所有表单项',
    children: <ViewMode />,
  },
  {
    key: '3',
    label: '编辑模式,对表单字段进行赋值',
    children: <EditMode />,
  },
  {
    key: '4',
    label: '业务场景1,不需要姓名,最高学历',
    children: <BusinessWithoutName />,
  },
  {
    key: '5',
    label: '业务场景2,禁用年龄和最高学历',
    children: <BusinessDisableItem />,
  },
]

const SeperateForm: React.FC = () => {
  return (
    <>
      <PageTitle>
        基于这种管理模式,你可以对以下表单实现不同的提交逻辑,任意字段的显示/禁用
      </PageTitle>
      <Tabs defaultActiveKey="1" items={items} />
    </>
  )
}

export default SeperateForm
import { Button, Form } from 'antd'
import BaseFormItemList from './base-form-item-list'

function OriginForm() {
  const [form] = Form.useForm()

  const onFinish = (values: Record<string, unknown>) => {
    console.log('看看最终提交的表单的样子', values)
  }

  return (
    <Form
      name="basic"
      form={form}
      labelCol={{ span: 8 }}
      wrapperCol={{ span: 16 }}
      onFinish={onFinish}
    >
      <BaseFormItemList form={form} />

      <Form.Item>
        <Button type="primary" htmlType="submit">
          提交
        </Button>
      </Form.Item>

      <h2>基于这种表单管理模式,你可以使用同一个表单,实现不同的提交逻辑:</h2>
      <ul>
        <li>1、针对不同的业务场景,处理不同的交互逻辑</li>
        <li>2、查看模式,将所有表单禁用</li>
        <li>3、编辑模式,在业务组件对表单进行赋值</li>
        <li>4、根据不同的业务场景,对不同的字段进行显示/禁用,同时处理不同的提交逻辑</li>
      </ul>
    </Form>
  )
}

export default OriginForm
import { Form } from 'antd'
import BaseFormItemList from './base-form-item-list'

function ViewMode() {
  const [form] = Form.useForm()

  return (
    <Form name="basic" form={form} labelCol={{ span: 8 }} wrapperCol={{ span: 16 }} disabled>
      <BaseFormItemList form={form} />
    </Form>
  )
}

export default ViewMode
import { Button, Form } from 'antd'
import BaseFormItemList from './base-form-item-list'
import { useEffect } from 'react'

function EditMode() {
  const [form] = Form.useForm()

  useEffect(() => {
    form.setFieldsValue({
      name: 'John Doe',
      age: 20,
    })
  }, [])

  const onFinish = (values: Record<string, unknown>) => {
    console.log('看看最终提交的表单的样子', values)
  }

  return (
    <Form
      name="basic"
      form={form}
      labelCol={{ span: 8 }}
      wrapperCol={{ span: 16 }}
      onFinish={onFinish}
    >
      <BaseFormItemList form={form} />

      <Form.Item>
        <Button type="primary" htmlType="submit">
          提交
        </Button>
      </Form.Item>
    </Form>
  )
}

export default EditMode
import { Button, Form } from 'antd'
import BaseFormItemList from './base-form-item-list'

function BusinessWithoutName() {
  const [form] = Form.useForm()

  const onFinish = (values: Record<string, unknown>) => {
    console.log('看看最终提交的表单的样子', values)
  }

  return (
    <Form
      name="basic"
      form={form}
      labelCol={{ span: 8 }}
      wrapperCol={{ span: 16 }}
      onFinish={onFinish}
    >
      <BaseFormItemList form={form} hiddenArr={['name', 'highestEducation']} />

      <Form.Item>
        <Button type="primary" htmlType="submit">
          提交
        </Button>
      </Form.Item>
    </Form>
  )
}

export default BusinessWithoutName
import { Button, Form } from 'antd'
import BaseFormItemList from './base-form-item-list'
import { useEffect } from 'react'

function BusinessDisableItem() {
  const [form] = Form.useForm()

  useEffect(() => {
    form.setFieldsValue({
      name: 'John Doe',
      age: 20,
    })
  }, [])

  const onFinish = (values: Record<string, unknown>) => {
    console.log('看看最终提交的表单的样子', values)
  }

  return (
    <Form
      name="basic"
      form={form}
      labelCol={{ span: 8 }}
      wrapperCol={{ span: 16 }}
      onFinish={onFinish}
    >
      <BaseFormItemList form={form} disableArr={['age', 'highestEducation']} />

      <Form.Item>
        <Button type="primary" htmlType="submit">
          提交
        </Button>
      </Form.Item>
    </Form>
  )
}

export default BusinessDisableItem

2、在vue里面如何实现类似的组件设计?

在vue里面实现类似的功能,经历了几个阶段===========>

2.1 【defineModel】现在vue支持的都是vue3版本,基于最新的api设计,发现可以利用defineModel实现类似的功能

<template>
  <BaseFormList v-model:form="form" />
</template>

<script setup lang="ts">
import { reactive, type Reactive } from 'vue'
import BaseFormList from './components/base-form-list.vue'

export type FormData = {
  name: string
  age: number
}

const form = reactive({}) as Reactive<FormData>
</script>
<template>
  <section>
    <label for="name">姓名:</label>
    <input type="text" id="name" v-model="form!.name" />
  </section>

  <section>
    <label for="name">年龄:</label>
    <input type="text" id="age" v-model="form!.age" />
  </section>
</template>

<script setup lang="ts">
import type { FormData } from '../seperate-form.vue'

const form = defineModel<FormData>('form')
</script>

2.2 尝试在vue2里面也实现类似的功能,发现,有的事情跟一直以来的观念不一样,就使用组件的v-model进行值绑定

<template>
  <div>
    <el-form :model="form" :rules="rules" ref="formRef">
      <FormItemList :disabled-arr="['name']" v-model="form" />
    </el-form>

    <h2>看看绑定的表单项</h2>
    <pre>{{ JSON.stringify(form, null, 2) }}</pre>
  </div>
</template>

<script>
import FormItemList from "./form-item-list.vue"

export default {
  name: "DisableName",
  components: {
    FormItemList,
  },
  data() {
    return {
      form: {
        hobbies: []
      },
      rules: {
        age: [{ required: true, message: "请输入年龄" }],
      },
    }
  },
}
</script>
<template>
  <div>
    <el-form-item v-if="!hiddenArr.includes('name')" prop="name">
      <el-input
        v-model="value.name"
        :disabled="disabledArr.includes('name')"
        placeholder="请输入名称"
      />
    </el-form-item>

    <el-form-item v-if="!hiddenArr.includes('age')" prop="age">
      <el-input-number
        v-model="value.age"
        :disabled="disabledArr.includes('age')"
        placeholder="请输入年龄"
      />
    </el-form-item>

    <el-form-item v-if="!hiddenArr.includes('gender')" prop="gender">
      <el-select
        v-model="value.gender"
        :disabled="disabledArr.includes('gender')"
        placeholder="请选择性别"
      >
        <el-option label="男" value="male" />
        <el-option label="女" value="female" />
      </el-select>
    </el-form-item>

    <el-form-item v-if="!hiddenArr.includes('hobbies')" prop="hobbies">
      <el-checkbox-group
        v-model="value.hobbies"
        :disabled="disabledArr.includes('hobbies')"
      >
        <el-checkbox label="读书" name="hobbies" />
        <el-checkbox label="运动" name="hobbies" />
        <el-checkbox label="旅行" name="hobbies" />
      </el-checkbox-group>
    </el-form-item>

    <section>
      <input type="text" v-model="value.test" />
    </section>
  </div>
</template>

<script>
export default {
  name: "FormItemList",
  props: {
    // 禁用的表单项
    disabledArr: {
      type: Array,
      default: () => [],
    },
    // 隐藏的表单项
    hiddenArr: {
      type: Array,
      default: () => [],
    },
    // 表单数据
    value: {
      type: Object,
      default: () => {},
    },
  },
}
</script>

2.3 在vue3里面尝试类似的写法

<template>
  <BaseFormList v-model:form="form" />

  <pre>{{ JSON.stringify(form, null, 2) }}</pre>
</template>

<script setup lang="ts">
import { reactive, type Reactive } from 'vue'
import BaseFormList from './components/base-form-list.vue'

export type FormData = {
  name: string
  age: number
}

const form = reactive({}) as Reactive<FormData>
</script>
<template>
  <section>
    <label for="name">姓名:</label>
    <input type="text" id="name" v-model="form!.name" />
  </section>

  <section>
    <label for="name">年龄:</label>
    <input type="text" id="age" v-model="form!.age" />
  </section>
</template>

<script setup lang="ts">
import type { PropType } from 'vue'
import type { FormData } from '../seperate-form.vue'

defineProps({
  form: {
    type: Object as PropType<FormData>,
    required: true,
    default: () => {},
  },
})
</script>

不使用defineModel,同样可以达到效果

总结起来,发现了什么?表单绑定的时候,绑定的数据源对象,并没有声明所有的属性,但是表单响应式依然起作用。按照正常思维,子组件v-model直接绑定的是父组件的props,它不但没有报错,还运行的很好

引用数据类型的参数,貌似可以直接在子组件进行修改而无需通过vue的官方api。依然能够获得响应性,包括数组的增加,删除。对象属性值的修改。