技术探索与实践的一些思考:从产品思维到工程落地的综合权衡

长安码客
2025-12-14 20:49
阅读 624

入职新公司快两个月了,今天难得在周五下午五点半准时关掉 IDE,泡了杯手冲(没错,我就是那个每天 8 点就坐在工位上敲代码的早起型选手),突然想写点东西。不是为了卷,而是这两个月踩的坑太多,不吐不快。

以前做产品经理时,总觉得技术同学“矫情”——为什么一个接口不能今天提需求明天上线?现在自己转码了才明白,原来每个“不行”背后,都藏着一堆技术债、兼容性问题和凌晨三点的告警短信。身份转换后最大的感悟是:技术选型从来不是单纯的技术问题,而是一场关于时间、人力、业务目标和未来可维护性的综合博弈


起因:一个“简单”的需求,差点让我连夜跑路

事情得从上周说起。我们团队负责一个内部运营平台,原本用的是 Vue 2 + Element UI 的老架构。产品同事(对,就是曾经的我自己会干的事)提了个“小需求”:要支持动态表单配置,字段类型包括富文本、级联选择、上传组件等,而且要求能拖拽排序、实时预览。

听起来很常规对吧?但当我打开代码库那一刻,心凉了半截——项目里还混着 jQuery 和原生 JS 写的模块,Webpack 配置文件比我的简历还长,CI/CD 流程跑一次要 12 分钟。更致命的是,没人敢动核心模块,因为去年双11前改了个按钮样式,结果导致订单导出功能挂了,运维半夜打电话骂街。

领导看我一脸苦相,拍拍肩膀说:“你不是做过 PM 吗?这次你来定技术方案,既要快又要稳。”
我:???这不就是传说中的“既要马儿跑,又要马儿不吃草”?


选型困境:框架之争,本质是风险与效率的拉扯

面对这种“历史包袱重 + 迭期紧”的场景,技术选型就成了生死抉择。我翻遍了掘金、知乎、GitHub Trending,甚至把床头那本《软件架构模式》翻到卷边(别笑,转码人真的会靠书籍续命),最后锁定了三个方向:

  1. 继续用 Vue 2 + 自研动态表单组件
  2. 升级到 Vue 3 + 使用 Formily / FormKit 等成熟方案
  3. 彻底重构,上 React + Ant Design Pro

乍一看第三个选项最“先进”,但结合现实,立马被我否了。为什么?产品迭代节奏不允许。我们下个月就要支撑一场大型营销活动,没时间搞大换血。而且团队里 React 经验几乎为零,贸然切换等于自爆。

于是重点对比前两个方案。我列了个表格,把关键维度都拉出来晒一晒:

维度 Vue 2 自研 Vue 3 + Formily
学习成本 低(团队熟悉) 中(需学新生态)
开发速度 慢(重复造轮子) 快(开箱即用)
可维护性 差(无标准规范) 好(社区方案)
兼容风险 高(老旧依赖冲突) 中(需处理 Vue 2/3 混用)
长期收益 几乎无 可平滑迁移

看到“可维护性”这一栏,我果断倒向了 Vue 3 + Formily。原因很简单:我太懂“临时方案变永久债务”有多痛了。当年做 PM 时,为了赶上线让开发写了个“临时 hack”,结果三年都没人敢碰,成了系统里的幽灵代码。

但现实哪有这么理想?Vue 3 在我们项目里根本跑不起来——因为底层依赖的某个 UI 库只支持 Vue 2。这就尴尬了。


曲线救国:微前端 + 渐进式迁移

被逼到墙角的时候,人总能想出骚操作。我灵机一动:为什么不把新功能做成一个独立的微应用?

具体思路是:

  • 主应用保持 Vue 2 不动
  • 新的动态表单模块用 Vue 3 + Vite 单独构建
  • 通过 Webpack Module Federation 实现模块共享
  • 通信通过 CustomEvent + localStorage(别喷,过渡期够用就行)

说干就干。周一下午我就拉着前端组长开了个“技术可行性评审会”。他一开始满脸怀疑:“Module Federation?我们连 Webpack 5 都没升,你确定能跑通?”
我说:“跑不通就改配置呗,反正 CI 流水线慢成狗,多试几次也不差这点时间。”

结果周三晚上真的跑通了!虽然过程堪称灾难:

  • 因为 @vue/compiler-sfc 版本冲突,Vite 构建直接报错 Cannot find module 'vue'
  • 微应用加载时主应用的 CSS 样式污染了新组件
  • 本地开发热更新失效,每次改代码都要手动刷新

但好在,核心逻辑跑起来了。我把关键配置贴出来,给后来人避坑:

// vue3-form-app/vite.config.js
export default defineConfig({
  build: {
    // 关键:暴露模块供主应用消费
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue'
        }
      }
    }
  },
  plugins: [
    vue(),
    federation({
      name: 'vue3FormApp',
      filename: 'remoteEntry.js',
      exposes: {
        './DynamicForm': './src/components/DynamicForm.vue'
      },
      shared: {
        vue: { 
          singleton: true, 
          requiredVersion: '^3.2.0' 
        }
      }
    })
  ]
})

主应用那边也要加一段加载逻辑:

// main-app/src/utils/loadMicroApp.js
export async function loadDynamicForm() {
  const container = document.getElementById('form-container');
  if (!container) return;

  // 动态加载 remoteEntry
  await import('http://localhost:5173/assets/remoteEntry.js');
  const factory = await __webpack_require__.e('DynamicForm');
  const Module = await factory();
  
  // 挂载到指定 DOM
  createApp(Module).mount(container);
}

虽然丑了点,但它 work 了!周五 demo 时,产品同事眼睛都亮了:“哇,这个拖拽体验好流畅!” 我表面微笑,内心狂喜:终于不用背锅了!


可读性与可维护性:我的强迫症救了团队

作为前 PM,我现在写代码有个执念:任何接手的人都能在 10 分钟内看懂我在干嘛

所以我在新模块里强制做了几件事:

  1. 组件命名语义化DynamicFormRenderer.vue 而不是 Form.vue
  2. 关键逻辑加注释:不是“这里是个循环”,而是“此处处理级联字段的联动逻辑,参考 PRD 第 3.2 节”
  3. 配置抽离:把字段类型映射关系单独放到 fieldTypeMap.js
  4. 错误边界兜底:每个异步操作都有 try-catch + 用户友好提示

举个例子,富文本上传图片的逻辑,我这么写的:

// src/utils/uploadHandler.js
/**
 * 处理富文本编辑器图片上传
 * @param {File} file - 用户选择的图片文件
 * @param {Function} onProgress - 上传进度回调(用于显示 loading)
 * @returns {Promise<string>} - 返回 CDN 图片 URL
 * 注意:此接口需走内部鉴权网关,不可直连 OSS
 */
export async function uploadRichTextImage(file, onProgress) {
  // 1. 校验文件类型 & 大小
  if (!['image/jpeg', 'image/png'].includes(file.type)) {
    throw new Error('仅支持 JPG/PNG 格式');
  }
  if (file.size > 5 * 1024 * 1024) {
    throw new Error('图片大小不能超过 5MB');
  }

  // 2. 调用内部上传服务(带 token)
  try {
    const formData = new FormData();
    formData.append('file', file);
    
    const res = await axios.post('/api/internal/upload', formData, {
      headers: { 'Authorization': getAuthToken() },
      onUploadProgress: (progressEvent) => {
        const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
        onProgress?.(percent);
      }
    });
    
    return res.data.url; // CDN 地址
  } catch (error) {
    // 3. 错误分类处理
    if (error.response?.status === 401) {
      throw new Error('登录已过期,请刷新页面');
    }
    throw new Error(`上传失败:${error.message}`);
  }
}

可能有人觉得啰嗦,但上周测试同学发现一个上传 403 的 bug,直接根据注释定位到是 token 过期,省了半小时排查时间。可读性不是写给机器看的,是写给人看的


教训与反思:技术决策不能只看“酷不酷”

回过头看,这次技术探索给我几个血泪教训:

  • 不要迷信新技术:Vue 3 很香,但在遗留系统里硬上就是找死。微前端看似复杂,却是平衡之选。
  • 产品思维是优势,不是负担:正因为我知道这个需求背后是运营 KPI,才敢砍掉“完美方案”,选择“够用就好”。
  • 文档即代码:我花了一整天写 README,包括如何本地联调、如何模拟错误场景。结果新来的实习生第一天就跑起来了。
  • Deadline 是第一生产力:如果不是下周就要演示,我可能还在纠结要不要用 Svelte……

最讽刺的是,昨天运维大哥跑来问我:“你们那个新表单,能不能顺便把日志格式标准化一下?现在查问题全靠 grep。”
我:……(默默打开代码,加上了 structured logging)


写在最后:技术人的成长,是不断在妥协中寻找最优解

从 PM 转码两个月,最大的变化不是学会了写 Promise,而是理解了所有技术决策都是 trade-off。没有银弹,只有更适合当下场景的选择。

现在的我,依然会在凌晨三点被报警电话吵醒,依然会对着满屏 red error 抓狂,但至少——
我知道自己在为什么而妥协,又在坚持什么

比如坚持代码可读性,坚持写单元测试(哪怕只覆盖核心路径),坚持在 PR 里写清楚背景和影响范围。这些事看起来“没那么 urgent”,但长期来看,它们才是系统不崩的真正护城河。

对了,如果你也在经历技术选型的煎熬,不妨翻翻手边的书,或者问问自己:

“三个月后,接手这段代码的人会骂我还是感谢我?”

答案,往往就在问题里。

(完)

P.S. 刚才提交 PR 的时候,CI 又跑了 11 分 47 秒……运维兄弟,求求你优化下流水线吧!🙏

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝