Posted in

这一年我优化了一个 46 万行的超级系统_AI阅读总结 — 包阅AI

包阅导读总结

1. 超级系统、前端优化、微服务拆分、业务组件库、性能提升

2. 本文讲述了一位前端工程师对一个 46 万行代码的超级系统进行优化的经历,包括问题分析、目标制定、方案实施和最终成果,重点强调了工作方法和解决思路。

3.

– 超级系统现状

– 构建时间长

– 系统未拆分,维护难度大

– 代码复用率低

– 缺乏统一封装

– 存在大量重复代码和组件

– 页面加载缓慢

– 随意引入插件

– 代码调试难度大

– 优化目标

– 找到问题要害

– 制定目标

– 提供解决方案

– 复盘总结

– 优化方案

– 菜单整理

– 汇总确认,下线废弃菜单

– 标注异常菜单

– 框架优化

– 引入语法检查和格式化

– 规范代码提交

– 优化硬编码等

– 封装配置和权限

– 优化路由

– 接入监控平台

– 业务组件库建设

– 多种封装形式

– 通用部分提取封装

– 微服务搭建

– 基于 pnpm + microApp + module federation 实现

– 解决服务耦合和上线问题

– Rocket-render 接入

– 性能优化

– 资源上 CDN 等

– 优化结果

– 各项指标显著提升

4. 成果指标

– 构建时长大幅缩短

– 代码行数减少

– 服务数量增加

– 业务组件库构建

– 基础框架优化

– 性能评分提高

思维导图:

文章地址:https://juejin.cn/post/7394095950383710247

文章来源:juejin.cn

作者:河畔一角

发布时间:2024/7/22 8:12

语言:中文

总字数:6067字

预计阅读时间:25分钟

评分:88分

标签:系统优化,代码重构,性能提升,软件工程,项目管理


以下为原文内容

本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com

背景

我曾带领团队治理了一个超级工程,是我毕业以来治理的最庞大、最复杂的工程系统,涉及到开发的方方面面。下面我给大家列几个数字,大家感受一下:

指标 数据
菜单数量 250+
代码行数 46
路由数量 300+
业务组件、util 600+
构建时间 6min
关联业务 报表、CRM、订单、车辆、配置、财务…

image.png

这是上一任留给我的烫手山芋,而且从需求迭代频次来看,这个系统占据了这个业务部门50%的需求,也就是说,之前的伙计把几乎所有的业务功能都做进了一个系统中,像屎山一般堆积,构建一次的时间长达6-9min,作为一个合格的前端,简直怒火冲天。

问题

面对这样的超级应用,想要解决,必须先把问题整理出来,才好对症下药。

  • 构建时间过长,影响开发体验。
  • 系统单一,代码量庞大,未做拆分,对于后期维护难度很大,且存在增量上线风险(改一个字,也要整个系统打包上线)。
  • 代码业务组件太少,复用率低,需要整理业务代码,封装高效业务组件库。
  • 工具函数、请求Axios、目录规范、菜单权限、公共能力等未做统一封装和梳理。
  • 存在大量重复的代码、大量重复的组件(有很多都是拷贝了一份,改个名字)。
  • 页面加载极其缓慢,无论是首屏还是菜单加载,很明显存在严重的性能问题。
  • 工程中随意引入各种插件,比如:big.js、xlsx.js、echarts、velocity-animate、lodash、file-saver等。
  • 代码中存在很多mixin写法,导致调试难度很大。

以上是针对46万行的应用做出的问题分析报告,找出这些问题以后,我们就能开启优化之路。

目标

image.png

我觉得不管哪一行,进入这个社会最重要的能力是生存能力,面对生存最重要的技能是解决问题的能力。想要把问题解决好,就要有章法可循,比如:找到问题要害、制定目标、提供解决方案、复盘总结。

方案

  • 250+菜单归类整理、废弃菜单下线
  • 搭建业务组件库
  • 搭建工具函数库
  • 基础框架优化
  • 基于microApp做微服务拆分、引入webpack5的module-federation机制
  • 引入rocket-render插件,对局部页面、基础功能做重构,后续逐步替换。
  • 性能优化

菜单整理

300 多个菜单,业务和产研人员可能已经更换过好几次,通过跟产品的沟通,可以得知,受业务调整影响,很多功能已经废弃,系统未及时下线导致日积月累。然而由于对业务不熟,产研很难判断哪些菜单需要下线,我们也不能随意凭感觉下线某一个菜单或功能,必须采取谨慎的态度,有法可依,因此我们采用如下措施:

菜单汇总

产品牵头,汇总系统所有菜单,按环境进行逐个确认。前端会根据确认结果依次删除路由配置、菜单、关联组件、调用 API 等相关代码。

  1. 同业务方确认后,直接下线。
  2. 线上访问异常菜单,进行标注。
  3. 数据异常菜单,进行标注。
  4. 对于无法确认的,通过监控查看页面访问量,通过1-3个月的观察,最终决定是否下线。

通过1个月的菜单整理,下线了大概50+的菜单、删除了近100+页面,以及不计其数的业务组件代码,整体代码量减少近8万,同时,我们将250多个菜单进行归类,拆分了6个大类,供后续微服务做准备。

框架优化

一个稳定的系统,必然有一个稳定的框架做支撑。我们在梳理的过程中发现系统极其脆弱,无 ESLint 语法检查规范、没有 Prettier 格式化、Hard Code、随处可见的自定义组件、到处报错的控制台…

  • 引入 ESLint 做语法检查.
  • 引入 Pettier 做代码格式化。
  • 引入 Husky lint-staged 规范代码提交。
  • 对于硬编码部分跟产品沟通做成可视化配置或者json配置形式。
  • Axios的二次封装:常规请求、全局Loading、文件下载、登录失效拦截、报错拦截提示、状态码统一适配。
  • 插件删减:big.js 剔除改为手动计算、lodash 替换为 lodash-es、删除动画插件、文件导出统一由后端返回二进制,前端通过Axios封装下载函数进行文件下载等等。
  • 配置文件封装:多环境配置、常量定义。
  • 菜单权限封装。
  • 按钮权限指令封装:v-has="'create'"
  • router路由提取优化,针对路由守卫处理一些特殊跳转问题。
  • 接入前端监控平台,对于资源请求失败、接口请求超时、接口请求异常等做异常监控。
  1. 对于eslintprettier大家自行参考其他文章,此处不再赘述。

  2. 对于Axios二次封装,大家可能会比较奇怪,为什么要封装文件下载?因为文件导出本身也是一个请求,只是跟常规get/post有差异,所以我们为了方便调用,把它放在一个文件里面定义,比如:

export default {  get(url, params = {}, options = { showLoading: true, showError: true }) {    return instance.get(url, { params, ...options })  },  post(url, params = {}, options = { showLoading: true, showError: true }) {    return instance.post(url, params, options)  },  download(url, data, fileName = 'fileName.xlsx') {    instance({      url,      data,      method: 'post',      responseType: 'blob'    }).then(response => {      const blob = new Blob([response.data], {        type: response.data.type      })      const name = (response.headers['file-name'] as string) || fileName      const link = document.createElement('a')      link.download = decodeURIComponent(name)      link.href = URL.createObjectURL(blob)      document.body.append(link)      link.click()      document.body.removeChild(link)      window.URL.revokeObjectURL(link.href)    })  }}

我们在api.js中调用的时候,就会变的很简单,如:api.get('/user/list')api.download('/export',{ id: 123 })。当然可能每个人有自己的开发习惯,这些都是我个人的经验,也不一定适合大家。对于上面的参数有一个options变量,用来做参数扩展的,比如展示loadingerror,为了做全局Loading和全局错误提示用的。

  1. 如果你担心页面并发请求,导致重复loading问题,可以通过计数的方式来控制,避免多次重复Loading

  2. 插件删除部分大家可能有争议,这个纯属于个人行为,因为我们不是金融行业,不需要高精度,如果出现计算我们直接四舍五入即可,而lodash-es主要为了做tree-shaking,还有很多插件根据自身情况考虑要不要引入。

  3. 指令封装,对于按钮权限非常管用,举个例子:

封装指令

import { getPageButtons } from '@hll/css-oms-utils'Vue.directive('has', {  inserted: (el, binding) => {    let pageButtons = getPageButtons().map;    if (!pageButtons[binding.value]) {      el.parentNode && el.parentNode.removeChild(el)    }  }})

权限判断

// 1. 通过v-if判断<el-button type="primary" v-if="pageButtons.create">创建</el-button>// 2. 通过v-has指令判断<el-button type="primary" v-has="'create'">创建</el-button>

getPageButtons 其实是为了兼容历史代码而封装的函数。

整个系统权限极其混乱,代码替换,耗时近 2 周,覆盖近 500 个文件才完成。

  1. 状态码适配

这个我觉得有必要提一下,我相信很多前端可能都遇到这个问题,一个系统对应n个后台服务,不同的后台服务有不同的格式,比如:A系统返回code=0,B系统返回result=0,C系统返回res=0,那前端就要做不同的适配,其实也有不同的方法可以做:

  • 让后端接入网关,统一在网关做适配。
  • 前端在拦截器中开发adapter函数,针对响应码做适配。
  • 针对分页、返回码、错误信息等都可以在适配函数里面做几种适配,或者提供单一函数在不同的页面单独调用,类似于requestto模块。

业务组件库建设

这一部分工作非常重要,不管你在哪个公司,做任何行业,我都建议封装业务组件库,当然封装有三种形式:

  1. 基于公司自建的npm平台开发业务组件库,通过npm方式引入。
  2. 对于小体量项目,直接把业务组件库放在components中进行维护,但是无法跨项目使用。
  3. 基于webpack5module federation能力开发公共组件,跨项目提供服务。

MF文档参考:www.webpackjs.com/concepts/mo… 我觉得在某些场景下,这个能力非常好用,它不需要像npm一样,发布以后还需要给项目做升级,只要你改了组件源码,发布以后,其他引用的项目就会立即生效。想要快速了解这个能力,我建议大家先看一下插件文档,然后了解两个概念exposesremotes,如果还是看不懂,就去找个掘进入门文章再结合本地实践一次基本就算入门了。

我是基于上面三种方式来搭建系统的业务组件库,对于通用部分全部提取封装,基于rollupvite搭建一套npm包,最终发布到公司私有npm平台。对于一些频繁改动,链路较长部分通过module federation进行封装和暴露。

梳理业务后,其实封装了很多业务组件,上面只是给大家提供了思路,没有一一列举具体组件,下面给大家简单列一个大纲图:

image.png

业务组件库建设对于这种大体量的超级系统收益非常大,而且是长期可持续的。

微服务搭建

前面第一部分梳理完菜单以后,其实已经将250+的菜单进行了归类,归类的目的其实就是为了服务拆分。拆分的好处非常明显:

  • 服务解耦,便于维护。
  • 局部需求可单独上线,不需要整包上传,减小线上风险。
  • 缩小每个服务模块的构建时间,提升开发体验。

本次基于pnpm + microApp + module federation来实现的微服务拆分,为什么?

  • 微服务拆分以后,创建了 7 个仓库,非常不利于代码维护。pnpm天然具备monorepo能力,可以把多个仓库合并为一个仓库,而且可以继续按7个项目的开发构建方式去工作。
  • 微服务使用的是京东的microApp框架,集成非常简单,上手成本很低,文档友好,我觉得比阿里的乾坤简单太多了(个人主观看法)。
  • 对于难于抽取的组件,直接通过module federation对外暴露组件服务。

上面在搭建业务组件库的时候,其实遇到了一个非常棘手的问题,有些组件跨了很多项目,链路很长,在抽取的过程中难度非常大,大家看一个图:

image.png

服务之间耦合太严重,从而导致组件抽取难度很大,那如何解决? 答案就是module federation,抽取不了,就不抽取了,直接通过exposes对外暴露组件服务,在其它子服务中调用即可。

下面给大家举一个接入microApp的例子:

基座服务(主应用)

import microApp from '@micro-zoe/micro-app';microApp.start()

添加组件容器(主应用)

<template>  <micro-app name="vms" :url="url" baseroute="/" @datachange='handleDataChange'></micro-app></template><script>export default {  name: 'ChildApp',  data() {    return {      activeMenu: '',      url: 'http://xxxx',    };  },  methods:{    handleDataChange({ detail: { data } }) {          },  }};</script>

分配菜单(主应用)

{    path: '/child',    name: 'child',    component: () => import('./../layout/microLayout.vue'),    meta: {      appName: 'vms',      title: '子服务A'    }}

就这样,一个主服务就搭建好了,等子服务上线以后,点击/child菜单,就能打开对应的子服务了。 当然因为我们是一个超级应用,所以我们在做的过程中其实遇到了很多问题,比如:跨域问题、变量问题、包共享问题、本地开发调试问题、远程加载问题等等,不过大家可以参考官方文档,都是可以解决的。

Rocket-render接入

这是我个人开源的一套基于Vue2的渲染引擎,通过json定义就可以快速搭建各种简单或复杂的表单、表格场景,跟formly这一类非常相似。

给大家举一个简单的例子:

  1. 安装插件
yarn add rocket-render -S
  1. 组件注册
import RocketRender from 'rocket-render';import 'rocket-render/lib/rocket-render.css';Vue.use(RocketRender);Vue.use(RocketRender, {  size: 'small',  empty: '-',  inline: 'flex',  toolbar: true,  align: 'center',  stripe: true,  border: true,  pager: true,  pageSize: 20,  emptyText: '暂无数据',});

插件支持自定义属性,建议默认即可,我们已经给大家调好,如果确实想改,就通过自定义属性的方式进行修改。

  1. 页面应用

search-form 自带背景色和内填充,如果你有特殊需要,可以添加 class 进行覆盖,另外建议给 model 添加 sync 修饰符。

<template>  <search-form    :json="form"    :model.sync="queryForm"    @handleQuery="getTableList"  /></template><script>  export default {    data() {      return {        queryForm: {},        form: [          {            type: 'text',            model: 'user_name',            label: '用户',            placeholder: '请输入用户名称',          },        ],      };    },  };</script>

我们针对需要自定义部分提供了slot插槽,所以不用担心用了以后,对于复杂需求支持不了,导致返工的现象,完全不存在,除了json以外,我们还内置了非常多的开发技巧,保证你爱不释手,继续看几个例子:

  1. 日期范围组件,通过export直接暴露两个字段。
{    type: 'daterange',    model: 'login_time',    label: '日期范围',        export: ['startTime', 'endTime'],        valueFormat: 'timestamp',     defaultTime: ['00:00:00', '23:59:59'], }

前端是一个组件对应一个字段,但是接口往往需要让你传开始和结束日期,通过export可以直接拆分,太好用了。

  1. 下拉组件支持一步请求
{    type: 'select',    model: 'userStatus',    label: '用户状态',    multiple: true,     filterable: true,     clearable: true,        fetchOptions: async () => {      return [        { name: '全部', id: 0 },        { name: '已注销', id: 1 },        { name: '老用户', id: 2 },        { name: '新用户', id: 3 },      ];    },        field: {      label: 'name',      value: 'id',    },    options: [],        change: this.getSelectList,}

通过fetchOptions可以直接在里面调用接口来返回下拉的值。如果是静态的,直接定义options即可。 如果接口返回的类别字段不是label和value结构,可以通过field来做映射。我觉得用的太爽了。

还有很多,我就不举例子了,大家去看文档就好了,文档写的非常清楚。

性能优化

前面几部分做完以后,我们的代码总量基本已经降到了 30 万行左右了,而且整个构建时长从9min降到了 30s ,有了业务组件库的加持,简直不要太爽,不过你看到的这些,看起来容易,其实我们花费了近1年的时间才完成。 剩下就是提升系统性能了:

  1. 资源全部上cdn,不仅上cdn,还要再阿里云针对图片开启webp(需要做兼容处理),cdn记得添加Cache-Control缓存。
  2. 服务器全部支持gzip压缩。
  3. 添加external配置,我在npm开发了一个vite-plugin-external-new插件,可以帮你解决。
  • 这个大家一定要做,因为可以有效降低vender体积,你通过LightHouse做性能分析,就能知道原因,对于体积较大的js会给你提示的。
  • 通过external,我们可以直接让vuevue-routervuexelement-ui等等全部通过defer加载。
  1. 建议在根html<div id="app"></div>中加一个Loading标签
<div id="app">    <div class="loading">加载中...</div></div>

这样做的好处是,如果vue.js还没有加载完成之前,可以让页面先loading,等new Vue({ el: '#app' }) 执行以后,才会覆盖#app里面的内容,这样可以提升FCP指标。5. 对于比较大的插件,建议按需

export const loadScript = (src: string) => {  return new Promise((resolve, reject) => {    const script = document.createElement('script');    script.type = 'text/javascript';    script.defer = true;    script.onload = resolve;    script.onerror = reject;    script.src = src;    document.head.append(script);  });};

某一个页面如果使用较大的插件,比如:xlsx、html2Canvas等,可以单独在某一个页面内做按需加载。

  1. 有些页面也可以针对vue组件或者大图片做按需加载。
  2. 其它性能优化,我觉得可做可不做,针对LightHouse分析报告针对性下手即可,可能很多文章将性能优化会非常细致,比如chunk分包等等,我倒觉得不是很重要。 只要把上面的做完,理论上前端的性能已经会有巨大的提升了。

结果指标

指标 优化前 优化后
构建时长 6-9min 30-45s
代码行数 46万 30 万
服务 1个 7个
业务组件库 乱七八糟 基于rollup开发构建
基础框架 乱七八糟 高逼格
性能评分 30分 92分

以上就是我面对一个 46 万行代码的超级系统,耗时大概一年的时间所做的一些成果,很多技术细节也只是泛泛而谈,但是我希望给大家提供的是工作方法、解决思路等偏软实力方面,技术功底和技术视野还是要靠自己一步一个脚印。

这一年我过的很充实,面对突如其来的问题,我心情很复杂,这一年,我感觉也老了很多。最后,感谢你耐心的倾听,我是河畔一角,一个普通的前端工程师。

最近刚写了一篇低代码文章:juejin.cn/post/739207… 有兴趣的不妨一阅。