聊一聊 ElDialog 封装背后不为人知的故事

57 阅读27分钟

最近用 AI 生成了一篇文章《🚀 这个 ElDialog 封装方案,让我的代码量减少了 80%》发到了知乎和掘金上,截止 2026年01月07日一周的内容数据:

  • 掘金:展现数(2.11万),阅读数据(736),收藏数(22),点赞数(13),评论数(7)
  • 知乎:阅读数(1573),赞同(9),评论(4),收藏(25)

从数据上来看在掘金上完全不入流,我自己前端都刷不到这篇文章,但是我觉得这篇文章很有价值。本来是打算就 BusDialog 组件的封装写一篇背后的实现逻辑以及推广的文章,但是无赖在家里完全没有时间去写,所以着急就用 AI 直接写了一篇分享到了网上。

作为 90 后在 2025 年刚刚迈入 35 就遭遇了人生中第三次被裁员:在银行驻厂遇到同一项目的其他公司一个小人(众人眼中就是项目组的蛀虫,谁背后都会捅刀子那种人,整天瞎晃悠)的投拆再加上之前的一些曲折事情,我最终失业了,而且在没有给 N+1 的情况下(这种情况符合 2N)我就立马闪人了,因为我实在不想在那样的环境下继续待下去了,早离开是一种精神解脱。

每次换工作我都会整二篇文章装饰一下自己,提高一点点竞争力,但是这次是真的有干货分享,所以边复习边开发。一星期后才写这篇文章是因为老婆孩子去外婆家了,我才得已独坐电脑间奋笔疾书和准备面试复习。

关于在银行驻厂的经历后面单独分享一下让大家了解一下里面的小世界多么复杂。

最近开发一个新项目被人投诉的原因是我延迟交付了一二天给到测试人员到 UAT 去测试。这个项目的负责人正是我上面提到投诉我的那个人(985 毕业的,按他的背景在银行驻厂真有点屈才了,但是细究肯定是犯过大错[小道消息:不为人知的秘密]),因为怕项目延期担责任,就不断给测试压力以及我们这边项目经理,但是从来没有正面和我说过一次(我是听别人说在背后搞我了好几次)。

项目延期其实有多方面的原因,当然主要是我自己在搞事情:

  1. 配合的后端真的是我遇到在这边最不主动的一个,什么事情都要我去推进,配合完全没有默契,同时也是一个新手(使用这个项目的框架开发业务)。
  2. 分配任务时写的是简单功能,开发过程中业务方反复变动变成了复杂功能,还好我早有准备。
  3. 来回写 CRUD 真的没有意思,整个超级页面封装是不是更有技术挑战,于是一个疯狂的计划开始了...

从一个 CRU 开启疯狂计划

我们在银行所做的开发多数都是那种一个列表页(查询表单、工具栏和表格),然后一个创建/编辑/详情页面那种。我以前在掘金上分享过二篇 B 端数据表格封装 Vue 3 实践的文章:

这次我直接在项目中安排上了,不同点在于我们公司的组件库(Element Plus 定制化)提供了部分基础组件,我所做的就是通过配置的方式直接把表单部分生成出来。

要实现根据配置渲染出表单,其实很简单,但是也不简单,因为我们表单有很多地方要动态控制:

  • 表单字段的显示与隐藏、动态改变表单控件类型、选项数据异步请求等
  • 页面创建和编辑是同一个路由,不同的入口如何保证数据的一致性
  • 表单分组如何实现
  • 如何设计组件将所有功能完整暴露并保持一个简单的使用方式
  • 上传组件需要重新封装并支持 ElForm 验证

关于这个组件(在银行内部项目中组件名称是 PdCruPage​)我会在项目 vben-business-components 中分享经过优化调整后的版本(在当时项目中虽然在二个业务模块中落地了,但是还是很大的优化空间)。目前在我的开源项目中已经分享了其中的 PdCruHeader​ 和 PdCruGroup​ 两个组件(对应 BusPageHeader​ 和 BusGroup​),后续实现 BusFormLayout​ 和 BusFormRenderer​ 后就会拼出完整的版图。

注意:暂时项目使用 Vben Admin 作为宿主环境在开发,后续会基于 Turborepo 单独搭建并迁出。

上传组件(PdCruAttachmentUpload​)是在业务组件库的 CubeUploadDialog​ 基础上进行二次封装的,后者又是基于一个公共 Hooks(useUpload​) 封装实现的。这个上传的对话框也是我写的,写得相当的复杂(支持自动上传、进度条、多个文件批量上传、数量总大小校验等功能),以至于行内其他同事使用的时候总是会找我去解决各种场景下的使用问题,同时我自己在使用的过程中也容易迷失方向(属性整太多了,功能都是暗盒,一个配置出错就导致组件行为发生意想不到的诡异)。

​PdCruPage​ 的封装难点在于如何保证灵活性,也就是说除了我们提供的分组(放在 children​ 中就是一个分组)和表单项外,如果有额外的不属于组件内部提供的内容如何暴露出表单配置和模型以及 ElForm​ 相关的实例方法并在分组外部和内部动态插入内容;还有一个就是 PdCruPage​ 内部由二个兄弟组件 PdPageHeader​ 和 PdFormRenderer​ 组成,如何在页面头部暴露出 ElForm​ 相关的实例方法(PdCruPage​ 组件本身没提供任何实例方法)。

因为整个页面都是在一个 Schema 配置下同时结合插槽和 portal-vue 的实现的,我们需要在 Schema 中处理复杂的表单联动操作及异步数据的请求。组件没有显示的给出表单项之间联动的配置,但是会在所有表单控件的事件参数中提供当前的 Schema 配置、表单模型以及一个方法 getSchema(fieldName)​ 来获取指定字段名的配置(项目中并没有适配 a[0].b​ 这种动态的场景)。对于像 ElSelect​ 这样的组件我们提供了一个 options​ 属性,支持数组、函数和异步函数返回一个选项数据,但是需要在配置中使用 effectScope​ 来管理组件的状态:

const schemas = reactive([
  {
    label: '供应商名称',
    field: 'companyName',
  },
  effectScope().run(() => {
    const handleOnce = ref(false);

    watch(
      () => formModels.registeredCapitalWanUnit,
      (newVal, oldVal) => {
        // 改变默认值
        if (oldVal === '万元' && newVal === '其他' && !handleOnce.value) {
          handleOnce.value = true;
          const renderItem = schemas[0]?.children.find(
            item => item.field === 'registeredCapitalWan'
          ) as CruRenderItem;
          // 选择其他时,将输入框类型从 `number` 改为 `input`
          renderItem.controlConfig.type = 'input';
        }
      }
    )

    return {
      label: '注册资本',
      field: 'registeredCapitalWan',
      controlType: 'input',
      controlConfig: {
        type: 'number',
        clearable: true
      },
      description: '如果候选币种不支持,请选择【其他】并在输入框中手动输入(示例:12.12万印尼盾)',
      // 在 `el-input` 的 `append` 插槽中渲染 `el-select` 时宽度自适应需要特殊处理
      class: 'select-affix',
      style: '--cru-affix-width:120px;',
      slots: {
        append: () => (
          <el-select
            v-model="formModels.registeredCapitalWanUnit"
            onChange={val => {
              const renderItem = schemas[0]?.children.find(
                item => item.field === 'registeredCapitalWan'
              ) as CruRenderItem;

              if (val === '其他') {
                renderItem.controlConfig.type = 'input';
              } else {
                renderItem.controlConfig.type = 'number';
              }
            }}
          >
            {CURRENCY_WAN.map(item => (
              <el-option label={item.label} value={item.value} />
            ))}
          </el-select>
        )
      }
  })
])

上面的示例代码其实处理的是一种不在 PdFormRenderer​ 中定义的控件的场景。如果币种的选择是在 Schema 中定义的话,事件的处理逻辑就变成:

{
  label: '币种',
  field: 'registeredCapitalWanUnit',
  controlType: 'select',
  options: CURRENCY_WAN,
  events: {
    change: (val: string, { getSchema }) => {
      const renderItem = getSchema('registeredCapitalWan');
      if (val === '其他') {
        renderItem.controlConfig.type = 'input';
      } else {
        renderItem.controlConfig.type = 'number';
    }
  }
}

在实际使用过程中我们需要处理表单模型的初始值以及回显值,在定义 Schema 时务必使用 reactive​ 来保证响应式。如果表单项中有多个异步场景并请求同一个接口,我们就需要特殊处理了:例如用 vue-query,或者进入页面时就通过 idleHttp​(通过 requestIdleCallback​ 封装 axios​)获取数据。

扯得有点远了,后续会参考其他优秀的项目实现方案综合一下,尽量减少属性的数量、提高灵活性、提供控件的注册机制及接口缓存机制等。总之在这个组件不断地完善中我渐渐尝到了甜头,后面就又有了一个把列表页也一次性封装的念头,然而这是一场豪赌,因为我为此一周连续加班到 23 点多,有时一个解决方案困扰好几个小时,夜不能寐。

PdCruIndex 超级组件的诞生

前面介绍了编辑和详情(其实就是表单控件处于禁用状态下的页面展示)页面的封装,现在我们来分享一下列表页的演进过程。我正在开发的采购系统位于一个综合信息服务平台代码下面,这个平台支撑了银行内部众多的功能,代码体量也算是一个中大型项目了。这个综合信息平台又以子应用的方式集成了到了行内的统一工作平台。

系统中的很多模块其实绝大多数列表页都是由查询表单、工具栏(创建、导出)和表格(包含分页)组成,如果有走流程的模块则会以选项卡(通常为全部申请、待办和已办三个选项卡)的方式包含表单、工具栏和表格。

带选项卡的列表页实际上查询表单和表格的字段只存在局部的差异性,但是翻看其他同事的代码会发现他们会复制多份单独去写,好一点的把表单模型和列表的配置用一个 Hooks 集中处理了,但是模板部分仍然会重复去写,当然也有人额外封装了一下做成了一个组件去根据选项卡动态显示。我们所要达到的目标是在使用时只需要配置一份表单模型、一份表单 Schema 和表格配置自动生成多个选项卡并且可以在外部配置每一个选项卡的工具栏,表格中的操作按钮,每个页面的单独导出功能等。

下面是供应商管理模块的目录结构,它实际上有两个路由入口:供应商信息查询和供应商信息维护,区别在于后者包含了流程功能(有选项卡)。

├── views/
│   ├──purchase/
│   │   ├── supplier/
│   │   │   ├── hooks/
│   │   │   │	├── useSupplierConfig.tsx // 创建、编辑和详情页面的配置
│   │   │   │	├── useSupplierQuery.tsx // 列表页的配置
│   │   │   │	├── useSupplierService.ts // 接口相关配置
│   │   │   ├── views/
│   │   │   │	├── apply.vue // 创建和编辑页面
│   │   │   │	├── detail.vue // 详情页面
│   │   │   │	├── index.vue // 列表页面
│   │   │   ├── consts.ts // 常量定义
│   │   │   ├── types.ts // 类型定义
│   │   └── ...
│   └── ...
└── ...

从目录结构上看相当清爽(对比其他同事实现相同的功能少了很多目录和文件),因为页面的而已和处理逻辑全部封装到了 PdPageIndex​ 组件中。下面给出单页面的一个使用示例:

<pd-cru-index
  view-type="single"
  :cru-form-config="{
    model: formModels,
    schema: formConfigs,
    excludeFields: ['applyDate']
  }"
  :cru-table-config="{
    columns,
    rowActions,
    exportConfig: {
      fileName: '供应商信息',
      dataFormatMethod: exportDataFormatHandler
    },
    serverConfig: {
      url: '/api/xx',
      data: {
        applyPage: '0'
      }
    },
    tableConfig: {
      selection: true,
      selectedTotal: true,
      rowKey: 'id'
    }
  }"
  :row-view-handler="viewHandler"
>
  <template #default="{ uid }">
    <!-- 工具栏 -->
    <portal :to="`${uid}-apply-toolbar`" :order="1">
      <el-button type="primary" @click="handleNew">
        <el-icon class="h-4 w-4">
          <Plus />
        </el-icon>
        供应商入库申请
      </el-button>
    </portal>
  </template>
</pd-cru-index>

在组件 PdCruIndex​ 内部我们根据属性 viewType​ 视图类型来调用不同的子组件(Single.vue​ 和 Shared.vue​),在 Shared.vue​ 内部我们对传入的表单模型、表格配置等都做了特殊处理:将一份配置根据选项卡数量拆分出多个副本并共享同一个 ElForm​ 和 ElTable​(公司项目中使用的是 PdProTable​)。这样做的目的在于保证每一个选项卡都有自己的状态信息,每一个表格的分页信息不会错乱。

组件默认提供了一个工具栏操作按钮【导出列表】,具体功能由组件 PdCruTable​ 提供,同时还和组件 PdCruQuery​ 组件关联,因为导出时需要根据查询参数来请求接口数据。这里有一个问题:表格和表单组件之间其实是都是独立的组件,我们没有办法直接关联上,所以最终只能依赖 mitt 这个库将两者以一个 uid​ 关联起来,细节我就不展开了。

在行内 UI 规范中要求工具栏第一个按钮必须是 primary​ 类型的,后续全部为 default​ 类型,我们组件内部提供的是 primary​ 类型的按钮,但是又需要考虑到在导出按钮前后动态插入别的按钮的场景,内部需要动态控制按钮的类型。

在处理带选项卡的列表页时,需要为每一个选项卡提供一个接口配置,表格列配置以及操作配置。封装组件时需要暴露出当前选项卡的值、表单的配置、表单模型、表单实例方法以及表格的配置(row​, column​, $index​),这对于组件的设计上有点难度,因为有些信息是需要层层透传才能暴露到外部。

这里有很多实现细节,暂时就不展开说了,如果有封装过类似超级页面的读者或许能从只言片语中体会到其中的一些难点。

注:后续会给出相关的实现源码和使用文档,静待 N 个月后出新组件。

现在列表页和详情页面都实现了封装并且效果还不错,只不过连续爆肝了几个周。做项目任务的同时还要封装组件,遇到问题 AI 有时也无能为力时只能静坐思考(有时困扰大半天没有解决思路),还有一个问题时在银行内是桌面云环境开发,纯手工一个一个字母敲出了近万行代码,真的挺佩服自己的。

PdCruDialog 主角来了

在 PdCruPage​ 组件中其实还有两个流程相关的组件是我一直未敢涉足的黑洞:有一个流程相关的组件曾经被我改出过生产问题。这个流程组件的作用是根据不同的角色显示不同的流程处理操作按钮,如:【处理】、【退回】、【撤回】等。流程组件是根据接口返回的 JSON 动态渲染出操作表单以对话框的方式呈现,但是这个组件封装得十分难用,又在很多地方使用到,而且没有任何类型说明与文档,源码更是难以阅读。

这个组件说实话我早在2025年年初的时候就想给它重写了,但是我去找以前写它的后端(也就是前面提到阴我那哥们),想收集一相关的资源(需求文档、设计稿和后端接口文档),结果死活不肯配合,这事只能作罢。

这一次采购开发,好在共同开发这个系统的另一个前端在我们组另一个前端封装(实际上是那哥们版本的拷贝,只不过换了新的接口地址)的基础上又搞了一个新的组件并添加了一些属性,我这边由于项目赶时间就参考他的先完成了功能并交付。这个同事值得夸奖的是他给出了流程组件的类型注释,这为我后面重新封装出 PdCruFlow​ 组件提供了支持,少走了很多弯路。

另一个直接展示流程明细的组件(使用抽屉侧面显示)虽然封装上谈不上好,但是使用上比较简单,暂时没有必要去动它,所以就不提了(实际上在我离开前我已经重新实现了部分功能,现在夭折了)。

在创建或者编辑草稿后点击【提交】按钮首选会校验表单,没有什么问题后会弹出一个对话框选择要复核的人员或者部门领导人审核。这个提示弹出框其实在所有走流程的模块都会用到,我也曾经在做账户开户时做了一个封装,但是灵活性不够,在使用时每次都要引入对话框并定义打开的变量。虽然可以直接使用 ElMessageBox 来承载里面的表单,但是项目中有很多用对话框封装的组件,有的需要灵活地自定义对话框头部和底部。

因为在项目中使用 portal-vue 为组件封装提供了强大的灵活性支撑,因此在思考如何封装对话框组件时自然就考虑到了如何利用上,此外 Vueuse 的 createTemplatePromise​ 为对话框提供了命令式弹窗,二者结合下可以提供强大的底层能力支撑。ElDialog 的使用想必大家都门清,我们既然提供命令式的方式打开弹窗那对话框实例得挂载在某一个地方,但是我们又不能一直放在那儿,得动态控制其显示,因此通过传送门技术在需要的时候传送到指定的位置是一个最佳解。

在当时的综合信息平台中是在本地和微前端布局的地方添加了 ​,在组件内部使用 Wormhole.open()​ 来将对话框主体传送到指定的位置并显示。

因为 portal-vue 的目标位置是全局的,我们只需要确保其名字唯一就可以在任意位置向同一个目标传送任意组件并带出想要的任何参数。基于 portal-vue 的特性,结合传统的组件封装思路,我们可以为对话框内容提供字符串、渲染函数和模板三种指定方式。

对话框的关闭上我们需要区分是取消还是主动关闭,不同的关闭方式在业务处理时需要对应的处理逻辑,这一点我们通过 Promise 的方式来提供。在对话框关闭时我们调用 createTemplatePromise​ 提供的 reject()​ 方法,并提供一个关闭类型和参数,在【确认】时调用 resolve()​ 并提供同样的参数。

很多时候如果用户不点击【取消】但是点击了右上角的关闭,对话框的内容被改变了,这时如果我们想要做一个二次确认,这就需要保留 ElDialog 的 beforeClose​ 函数,在必要的时候调用。

这里我就不过多展开了,因为源码也就 200 多行。

在对话框封装就绪后,紧接着我就在此基础上封闭了上面说的流程第一个节点的弹窗组件 PdCruFlowAssignDialog​,然后开始对流程组件下手...

流程组件的封装完全是对着另外一个同事开发的采购申请来做的,我根据接口返回的参数以及提交参数,并参考他的界面保证数据一致,并一点点死啃和梳理源码,前前后后加上后续的问题修复写了一千多行代码。

其实这个流程组件就是动态渲染后端返回的表单配置数据,但是很多场景下又需要添加自己的表单项,但是又依赖动态内容。原始的组件封装在内部写了很多插槽,如果需要动态改变表单文本或内容插槽满足不了就不断地添加属性,导致组件越来越臃肿,存在很多潜在的问题,指不定哪天就影响到了别人使用了这个组件的业务逻辑。

重写的流程组件 PdCruFlow​ 参考了另一个同事的类型注释并基于 PdCruDialog​ 和 PdFormRenderer​进行了二次封装,提供了更加语义化的属性名:

  • ​uid​: 唯一标识组件
  • ​taskId​: 任务 ID,默认从查询参数 route.query.taskId​ 中获取
  • ​instanceId​: 流程实例 ID, 默认从查询参数 route.query.instanceId​ 中获取
  • ​commentLabel​: 审批意见文本标签
  • ​revokeHandler​: 【撤回】处理器
  • ​getIsEditable​: 当前流程是否需要外部表单可编辑并提交表单数据
  • ​formDataProvider​: 表单数据提供器
  • ​beforeRequestValidate​: 数据提交前的校验函数
  • ​beforeRequestParams​: 数据请求前的参数调整函数
  • ​schemaInterceptor​: 动态调整流程的表单结构
  • ​customRequest​: 自定义请求函数

流程组件封装有一个难点在于动态表单部分,一开始调用流程接口会返回部分配置数据,后面又需要根据操作后的 radio 请求接口并返回另外一部分数据并组合在一起。一种场景是选择 “是” 动态加载了二个表单项,当选择 “否” 时我们要清除表单项和表单模型,我们的表单项还有可能是动态插入的,而且位置有可能还有明确的要求,甚至有时业务需求还要求改变默认的标签和提示文本,这给组件的封装带来了极大的挑战,也是为什么原始的组件越来越臃肿和每个人可能都会弄一个副本自己改的原因。

上述问题我最终想到了一种解决办法:我们根据第一次接口返回的表单内容转换成 PdFormRenderer​ 的 Schema,然后在每个节点前后插入足够的占位表单项,并为每个配置项指定一个默认的 field​,以 field${index}__​ 的方式,并额外添加了一个记录索引的字段 __index__​,这样在代码中就可以根据精确处理每一个表单项,下面是将 Schema 进行扩展的源码(注释部分去掉了):

async function expandChildren(
    children,
    model: Reactive<Record<string, any>>,
    lineItem: CruLineItem,
    placeholderFactory?: (index: number) => Record<string, any>,
  ) {
    const total = 30; // 指定 30 个节点,足够支撑业务需求了
    const step = 10; // 分组步长

    if (!Array.isArray(children)) {
      return Array.from({ length: total })
        .fill(null)
        .map((_, index) => ({
          visible: false,
          field: `__field__${index}__`,
          __index__: index,
        }));
    }

    const result = Array.from({ length: total })
      .fill(null)
      .map((_, index) =>
        placeholderFactory
          ? placeholderFactory(index)
          : {
              visible: false,
              field: `__field__${index}__`,
              __index__: index,
            },
      );

    children.forEach((child) => {
      const position = (i + 1) * step; // 基于 1 的索引,5, 10, 15...
      if (position < 1 || position > total) return;

      const targetIndex = position - 1;
      result[targetIndex] = {
        ...child,
        __index__: targetIndex,
        visible: child.visible ?? true,
      };
    });

    if (props.schemaInterceptor) {
      if (isAsyncFunc(props.schemaInterceptor)) {
        return await props.schemaInterceptor(
          lineItem,
          flowConfig,
          reactive(result as CruRenderItem[]),
          model
        );
      }

      return props.schemaInterceptor(
        lineItem,
        flowConfig,
        reactive(result as CruRenderItem[]),
        model
      );
    }

    return reactive(result);
  }
}

流程组件就到此为止,这个组件不一定具有普适性,所以没有必要继续下去了。

我被人投诉,朋友劝我跟他配合可以保住饭碗,我婉言谢绝了

虽然我在我们项目组的地位比较尴尬,从入职后没多久就拉去开发统合信息平台的各种任务,自己所在的统一工作平台的事情做得不多,除了早期在主应用中弄了一个收藏夹功能(后续没有再维护),然后就是做业务组件库的开发,搞了二个广泛应用的文件上传和部门人员选择器组件后,业务组件体现不出银行负责人这边的价值(不好汇报和考核),所以我后面就在做各种业务系统的活儿。

其实我们在这边的地位很尴尬,公司口口声声一再声明我们不是外包,但我们要为两边服务,一个业务组件既要按照公司的规范写各种需求文档、代码做走查、写单元测试,一个组件的开发周期无比的漫长,代码不符合预期不给合主分支,银行这边业务又急需投入使用。一个组件从综合信息平台中抽离出来,本来就当是一个迁移,但是按照这一套走下来,期间又提出各种功能需求,复杂度一下子拉高了一个档次。

在开发时公司要求保留一份代码,我们每次在外面开发后还需要同步到桌面云环境,这种操作占用了不少时间。

其实最开始日子过得还可以,后面因为接手了一个烂尾项目,各种问题再加上我们那个时间都在尝试 AI 来辅助编码,结果整个项目交给 AI 改,没有去认真审核每一行改动代码(业务涉及电子表格,业务也不熟悉),结果埋下了祸根,后面整出各种问题,虽然加班加点修复但是还是有不少小问题,最大的问题是性能很差无法接受,也因这个被这边一个女生天天向我们这边行方负责人投诉。

这个项目像个皮球最终还是回到了最初拉手项目的另一位前端的手上,代码回退到一个月前,并且部分原来要合并的历史分支功能也去掉了(这部分功能其实还不少),后面还找来了公司另一个开发这个的前端进行了培训。

正是因为这个项目我在这边的处境一下子艰难起来(无法独档一面),后面开始谨慎行事,做风险统一平台。这个项目说实话在我们手上成了一个试验 AI 编码的练手项目,通过它我测试了 Trae, Windsurf, Cursor, Copilot 和 Augment。我们将内网后端提供的 API 文档(电子表格提供)、业务需求文档以及外网的 UI 设计稿给到 AI 进行页面的生成,整体使用下来还是 Augment 最给力。

后期不在外面用 AI 来修改代码后,在行内桌机云环境继续维护时发现代码就是一坨屎。

我发现我们项目组的同事都是这方面的小白,同一个工具下生成的效果差距很大,不过后面大家慢慢学会调整提示词,并结合 MCP 可以高效地产出了。

银行这边项目有一个通病,一开始特别的急,天天开会过进度,后面就烂尾,不闻不问了,这个风险后面也是,需求确认不了,数据源确认不了,导致我开始有点闲了,在公司看了几天 Go 语言,然后就被这边银行负责人发现了(实际上他每天都会盯着大家),然后被借调去开发采购系统。这个采购系统负责人或者称乎项目经理就是那个阴我的人。

我所负责的模块设计稿都没有,我整了一个页面出来,然后那个阴我的后端就当着银行负责人的面各种挑毛病,我当时也是无语了,再后来就是在开发过程中进行我的疯狂组件计划,过程中确实遇到了一些问题导致提 UAT 晚了一点,但是还是按交付日期上线了。我后面听好几个别的同事说有人在背后打小报告投诉我,一个朋友就说另一个前端要被优化,你就赶紧接下他的活跟我做后绪的优化任务。我当时正处于组件的攻关期,其价值比苟延残喘在这边压抑着工作更具有挑战。因为我现在的位置本来就很尴尬,行方年年砍预算,又没有多少活儿可以做了,统一工作台还得绞尽脑汁为每个人提供工作量,公司这个项目又赚不到钱,后面我就没有活儿干了,所以这次被那个人投诉促使我提前离开了。

虽然在这个时间点上离开不太好,但是我也不想待下去了,就爽快地答应了,主要是公司其实也不想留我了,认为我不能胜认(有了孩子后学习时间就没有了,在银行驻厂又各种压榨),但是公司这边就以被投诉为由不给 N + 1(按网上的说话我其实是可以 2N 的,但是这种驻场开发情况谁说得准呢),还威胁说仲裁又不是种经历过,甚至 N 都不给,最后算上各种没有休的假赔偿不到 2 月的工资,年终奖也没我什么关系了。

离开的最后一二天我还在写代码

在我接到公司人事通知后,下一周我就选择了离开,此时我手上的项目刚提测,于是接下来的时间我一边修复问题,一边还在想着写一个关于这一次弄出来的十几个组件的简单使用文档。

实际上留给我写文档的时候并不多,只剩下一天多,期间更多的是处理提测问题,还有就是业务组件库的别人遇到的问题。在我花费了二天时间在项目中弄出了几个演示页面后,在周四通知项目组几个前端(7个人)并作演示和交接时,实际只来了二个人,其中一个是要接手采购后续,另外一个感谢支持吧,最终在催促下简单演示完了,至于用不用我就不费心了。

在银行驻厂开发确实能积累项目经验,但是一旦在里面久了就会慢慢被石化,失去了创新精神,代码能 CP 的绝不造轮子,稍有一点风险的事绝对不去改代码,这就是他们的真实写造,项目屎山代码不断地散发着新鲜的味道。一个列表页布局的 Layout 都能在 components 中搞出 5-6 个副本,后端永远开发时随手甩给你一个 Word,你去对着看吧,OpenAPI 完全推不动,谁也不想去为之改变,唯一变化的是近年来行情不好,甲方岗位减少,更多的优质人才慢慢流入提高混了整体水平,把那些不求改变,混日子的人逐渐淘汰掉了。

为什么要分享到社区

说实话在这一行干了这么多年,我是一点儿核心竞争你都没有,很惭愧过着颠沛流离的职业生活,唯一让我有点谈资的就是在银行这边探索出一套基于 portal-vue 的新开发模式,目前 AI 还不会这么想,所以我它以后会被我的数据所喂养。

选择基于 Vben Admin 来承载这部分功能是因为它确实做得很优秀,界面美观、功能强大,其提供的几个基础组件也让我受益,后期在实现 BusFormRenderer​ 时可以借鉴一下。

毕竟不是自己一手弄出来的项目,Vben Admin Components 中保留着很原始 Vben Admin 的内容,此外它的 Admin 部分我没能成功部署到 Vercel 上去,只有文档部分成功了。后面我会基于 Terborepo 并参考部分相关项目并结合统一工作台在微前端(基于 QianKun)方面的积累以及公司内部基础组件库和业务组件库分享一些有用的组件供学习交流。

关于近期找工作

相较于上二份工作(一点都不忙,但是并没有提升)而言,虽然我觉得目前这一份工作中确实积累了一些经验,但是毕竟在年尾这个阶段,同时步入了一个年龄段比较敏感的阶段,也比较艰难。

整个 Boss 直聘上成了外包、驻厂的聚集地,各种相关的公司来回给你信息轰炸。现在就一个驻厂开发还要求学历、要求工作变更不能太频换等等,甲方公司投简历更是石沉大海,因为应聘竞争的人太多了。

个人的履历上又有太多了空白,如微信小程序开发和H5开发经验都积累太少,Uniapp 无经验,可视化就会 Echarts,React 也没有什么经验。看着招聘上面的要求发现能投递的岗位真的很少。

家里有小棉袄天天要陪她玩,刚离职几天又恰巧老婆甲流了,完全没有时间去复习了,唯一产出物就是熬夜弄了 Vben Admin Components 上几个组件,这篇长文章能够写玩,还是因为老婆孩子送去外婆家了,有 2 个月的自由时间可以好好复习和学习一下。

其实刚离职(周五)后二三天就有一家公司(好买财富)面试,只可惜我裸面的,错失了一个甲方的好机会。其实面试问的问题真的非常简单,如果在我上一次求职期间回答一点都没有问题,只不过现在确实忘记了,后面有一个字节OD的页面我没有时间准备也推掉了,然后就在家带娃做饭直到送回外婆家。

现在我也想明白了,简历海投是没有意义的,好好复习,机会总是留给有准备的人(机会来了要有能力抓住),放平心态,一边复习一边想想怎么把要分享的组件库平台搭建好,把公司这边的一些积累转化为自身的实力一部分。

最后

文章中涉及的相关信息如果有侵权或涉密请告之,特此声明。