- Published on
聊聊前端工程化-人工版本
- Authors

- Name
- Li WenKang
- https://x.com/liwenkang_space
前端工程化是一个很大的词,就像让你去盖一栋楼,前期的规划设计,方案验证,再到原材料购买,协调人力,工期管理,验收交付等。可以发现前端开发从某种程度上来说,跟你去盖楼没啥本质区别,有时候你可能是需要盖一栋新楼(从 0 到 1 的新需求),而有时候你可能是需要在盖了一半的楼上继续施工(基于已有项目,再开发新功能),你也可能被安排去修修电路,管道啥的(修复一个已有问题)。
那么具体到前端工程化,有哪些我们值得关注的点呢?
从前端发展的角度看,从最开始的原生 html+JavaScript+CSS,到 jQuery 时代,再到 Vue+React,再到如今 Vite 引领的模块化开发。可以发现,前端做了原来越多的事情,也变得越来越复杂,在这个过程中,每当有人发现 "哎呀,这个东西难搞死了,我得想办法处理下",就会出现一个新的工具,背后其实是一种新的思维或者说思想。这篇文章,我不想写成考古文,我想讨论下当前有哪些地方我们可以关注。
在前端工程化中,最基础,也是代价最小的,我想是"规范"。
从微观角度,一个函数的具体实现,一个接口的定义。再到宏观角度,整个项目的开发规范。这些东西如果能在项目一开始就有一个基础的设计,那么性价比将爆炸的高。从实践角度来说,先引入 ESLint/Prettier 保证代码风格的统一,以及配置 commit 阶段和入库阶段的 eslint 检查,都属于一次配置终身受益的必选项。再深入一些,到具体的代码层面,针对有些复杂的函数/接口,有没有一套"规范"来"限制"开发人员,比如函数入参不能太多,组件对外暴漏的参数是否含义明确(是否是必填项?不同参数之间是否有关联效果?)。一个函数或者一个接口,它承载的功能是否太多?或者它承载的功能是否太少?其实我自己并不是每次都要一个最优解,但如果在写代码的时候,稍微多想一下,可能就会写出更优雅的代码。(或者简单粗暴一些,对于你自己拿不准的东西,抽象以后问一下 AI 吧,它总会给你一个不错的答案)
在规范之后,我想到的是模块化
我们先来聊聊模块化,这里又涉及到 JavaScript 模块化,CSS 模块化以及其它资源的模块化。在 JavaScript 中,只要你的项目稍微复杂一些,那必然要面临代码拆分/复用的问题,全局变量更是难题,说不定业务堆着堆着你就发现 A 依赖 B,B 依赖 C,C 又依赖 A,原地爆炸了。那么我们肯定不希望自己一个一个去给模块做区分,而是想有一个工具,让模块变得"唯一"。这时候就需要看下JavaScript模块化的发展了,刚开始是 CommonJS,给 Node 环境用的,同步加载。然后中间出现了 AMD(给浏览器用的,异步加载), CMD(给浏览器用的,异步加载),UMD(在 Node 环境下是同步的,在浏览器环境下是异步的)。到了 ES6 之后,我们可以直接使用 ESM 了,它支持浏览器和 Node 环境,变成了事实意义上的标准。用起来更是方面,import/export 就可以了。有了 ESM,才让 tree shaking 和模块懒加载成为了可能。在 CSS 模块化方面,我的实践不多,最常见的问题可能是你想修改你引入的组件库(elementUI,antdUI,semiUI)中组件的样式。比如把按钮背景色改改之类的,这里我们一般会在一个新增的 .less/.scss 中通过 :global { .xxx {} } 的方式进行样式覆盖。这里有时会埋下大坑,比如你没有限定好修改范围,把全局的样式都改了(尤其你临时修复一个线上样式问题,本来一个小错误,搞成了大问题)。或者由于优先级的问题,导致样式不生效等。解决方法也很简单,写样式的时候,时刻问自己,我写的这段 css 选择器是不是已经是最精准的了?此外其它的资源(图片,字体等),一般我们会通过 webpack loader 之类的工具,处理为可以识别和进一步操作的模块。
接下来聊聊组件化的问题
在使用 Vue、React 框架的时候,我们经常说,我把这个功能封装了一个组件。那么组件化是什么?他又有哪些值得我们关注的地方呢?从大的角度来看,我们在浏览器中看到的是一个完整的页面,而构成这个页面的,往往会包含导航栏,详情内容,底部信息栏等方面。然后任意一个板块,比如详情内容中,可能又包括了图片展示,文字展示的内容。如果说页面的最小单位是一个 DOM,那组件就是一系列关联 DOM 的封装。一个组件可大可小,它的功能可能就是一个鉴权跳转,也可能是整个页面。那么我们在设计一个组件的时候,需要关注哪些东西呢?不管是 UI 组件还是自定义 hook 组件。最最基础的,我们应该找准这个组件的功能定位,它具体要实现什么功能?为了实现这个功能,它需要哪些前置条件?前置条件是通过外层传入的数据就可以了吗?(比如一个纯 UI 组件),还是说它自己也要做一些更复杂的逻辑处理呢?(比如一个表格组件,其中有一列需要前端自己去做计算)它要不要考虑迁移到其它页面/项目后的适配问题?在这些考虑清楚后,我们再看细节一点,比如这个组件的入参要怎么设计?这块又跟上面说的"规范"关联起来了。在组件内部如果有副作用操作,是否需要考虑回退?以及手动的销毁?一个典型但又很烦人的场景,你在一个组件中,需要异步初始化一个实例,然后在初始化完成,组件销毁前,也把相关示例销毁掉。看起来逻辑普普通通,但里面有个大坑--"异步初始化"。当实例的异步初始化还没完成,实例所在的组件就销毁了咋办?那实例将会变成凤凰传奇中曾毅的经典歌词:"留下来!"。当这个操作稍微多起来,那么内存爆涨,CPU 打满,直到页面卡死崩溃,然后我被公司开除(bushi)。还是那句话,我们可能很难写出一个完美的组件,但可以多想想,多试试。
聊完上面这些,我想谈谈性能优化
一说起性能优化,前端翻来覆去就是那些东西,我们只要按照最佳实践,把每一步都做到位了,就没啥问题了。如果真的遇到工程上非常复杂的性能问题,那还是需要单独分析。从我的角度来看,就那几个方面吧。1. 性能检测(接入检测平台,本地性能验证)2. 网络请求阶段优化 3. 静态资源优化 4. 前端代码架构优化 5. 渲染优化 6. 用户体验优化。性能优化的前提是监测,在我的本科生涯中,可能最重要的一个知识点就是在自动控制原理这门课中学到的:没有反馈就没有控制。同样在性能优化上,你得先通过监测结果,知道性能的瓶颈在什么地方,是静态资源太大了?是接口请求太多卡死了?是涉及到很多复杂逻辑计算,让浏览器的单线程扛不住了?等等等等吧,我们先把关键的阻塞点找到,然后再看具体的解决方案。具体来说,在网络请求阶段,前端能做的事情不算太多,大多是后端或者运维去处理的,比如开启 GZIP 压缩,开始 HTTP2 强缓存和协商缓存,针对第三方库提供 CDN 访问服务等。前端可以关注下,有哪些接口可以合并(比如用户进入页面后,是不是需要查询一堆配置项?能合成一个吗?),将支持的三方库换为 CDN 访问方式,以及做服务器的预连接和域名的预解析(主要也是针对三方)。然后在静态资源优化上,生产环境使用压缩后的东西,这里面包括了利用 webpack 的 terser 去压缩代码,图片(尤其是svg)等。这里重点关注下图片和字体的格式,图片里面用 avif->webp->jpeg/png 做优雅降级。字体里面用 woff2->woff->ttf 做优雅降级。有了这些基础,再做一些的 tree shaking 效果就比较明显了。到了具体的代码层面,有一些需要小心的地方,比如不要在循环中操作 DOM,在使用 css 选择器的时候,尽可能的精准(比如你在一个复杂页面汇总,搞几次 querySelectorAll 查一堆元素,页面不卡死算我输),然后尽量避免重绘重排,在组件/定时器/实例/事件监听卸载的地方,尤其注意跟异步组合后,会不会有卸载失败的情况。使用浏览器提供的 localStorage/sessionStorage/Cookie,以及 ES6 提供的单例模式,合理存储和消费数据,避免通过接口重复获取数据。针对复杂计算的场景,我们可以缓存计算结果,比如在 react 中使用 useMemo 缓存计算值,useCallback 缓存组件,如果这样还是不够,那可能需要放在异步中处理,不要阻塞主线程。甚至你可以考虑放 web worker 中进行复杂运算,一个常见场景是计算上传文件的 MD5 值。对了,你使用的三方库,plugin 之类的,也要关注业界方向。比如用 dayjs 替换 momentjs,用体积更小,性能更好的库,跟上社区的步伐,没用的东西趁早删掉,避免屎山形成。接下来在渲染层面,我们常用的优化手段主要就是懒加载(比如树节点的分层懒加载,表格的分页懒加载,图片位于可视区域后懒加载,iframe 懒加载等),虚拟列表(树和表格的虚拟列表)。然后关于重排重绘的一系列注意事项,比如哪些会触发重排(修改 DOM 内容,位置,尺寸,增删DOM,可视窗口变化,手动读取部分布局属性),重绘(修改文本颜色,透明度啥的)。最佳实践有哪些?(比如在重绘重排后,避免立即读取部分布局属性,避免强制同步。避免分多条操作 DOM 样式属性,可以合并成一条,加 class 上去。如果需要基于旧样式算出新样式再变更的话,注意读写分离。可以利用一些特殊的属性,开启合成层的渲染,比如 transform,opacity 等,针对特殊的动画 DOM,可以使用 will-change 属性,以及提前脱离常规文档流,避免影响其它元素布局。针对复杂操作,先把 DOM display:none,等操作完之后,再设置 display: block 就行了,只需要两次重排重绘)。针对某些特殊业务场景,比如发布会需要用到的超大流量宣传页面,可以只关注核心业务,只提供最基础的功能,使用静态页面展示。最后在用户体验上来看,怎么让用户"觉得"系统快,也是需要关注的点,比如全局 loading 效果的设计,骨架屏的设计,都是可以做很多实践。
接下来,我想聊聊自动化的部分(突然发现大学学的自动化专业,仿佛要派上用场了)
程序从自身上来说没有任何价值,是使用这个程序的人,解决了某个问题,从而产生了社会价值,才让这个程序变得有价值了。而我们在构建一个程序的时候,必然是希望我们只关注核心的业务难点,其它地方最好有人帮我兜住。在我们雇得起帮手之前,先考虑下"其它地方"的部分,能不能也通过程序实现"自动化"。从开发流程来说,我们往往会经过 需求评审=>方案设计=>方案评审=>工时预估和任务分配=>实际开发联调=>转测验证=>发布上线 这样一条路线。那我们将重点关注 转测=>发布上线 这块,不知道你有遇到多少次由于刷错包导致的测试环境事故甚至生产环境事故(往事不堪回首,细节不谈)。我始终相信凡是需要人来处理的地方,都必然会出错。那么我们可以把哪些"重复性","事务性"的工作自动化呢?你的代码写好之后,肯定要打包后才能部署。那这里重要的两步"打包"和"部署",就是我们最先处理的部分。打包其实很简单,使用流水线工具,自动执行你已经在 package.json 中定义好的命令,亦或是你自己写好的脚本(尤其需要依靠外部工具传值的时候--例如需要外部指定对应的打包环境)。有了出好的包,那么部署当然也应该自动完成,在 github actions 结合 vercel 就可以轻松实践,到了真实的商业项目中,就要看选用什么平台,总体上没有太明显的差异,如果真的搞不定的话,尽快求助运维同事吧,别一不小心把环境搞崩了。如果从最理想的角度来看,我们希望软件随着时间是逐步进化的,那么如果我们能在解决线上问题/新需求验证完毕的第一时间就可以实时上线的话,那对于用户来说必然是最好的体验。但在实际操作中,这是一个"梦幻模式",先不说让用户"无感升级"的工程复杂性,如何保证频繁迭代上线情况下的工程质量,是一个很大的问题。从前端的角度来说,我写的代码必然没有 bug(bushi),实际上大家只要干过活都知道,bug 其实是无处不在的,它可能来源于开发/产品/测试之间对需求的理解不一致,也可能是在新增/修改/删除代码的过程中,出现了意料之外的问题(尤其是影响到你完全没关注的地方,这是非常危险的)。那么如何在交给测试验收之前,我们就做好自验证呢?从我的实践来说,针对工具类型函数的单元测试,以及需求完成/bug修复后的集成测试是必须完成的事。关于测试,有时候参加测试用例评审,你可能会觉得哎呀耽误我写代码了,但我真的遇到过一个令我"膜拜"的测试人员,她的测试用例评审往往都是耗时最长,也最为详尽,会议争论最多的会议。为什么会这样呢?其实我仔细想来,那位测试同事做的事,我总结为:"从用户角度出发看效果,从开发角度出发看实现,从产品运营角度看价值"。对于一个新的需求,其实测试最直观看到的,就是前端的页面,支持了哪些功能,用起来好不好用?虽说在产品方案设计评审时,大家都已经参与过一波讨论了,但在实践的过程中,大家总会再提出一些新的疑问,观点,目的都是为了有更好的用户体验。而"用户体验"是一个抽象的概念,而一个资深测试,会把它具化为无数的细节,思产品所不曾思,想设计所不曾想,可能他们只需要简单提出几个问题,你就知道他们的功力深厚了。接下来是从开发角度看实现,一个功能的实现可能有无数种方法,那么选取的方法究竟是不是当前的最优解?如果执行的话,会不会对原有系统造成冲击?说起来这些是SE和具体开发关注的点,但测试也会从项目架构以及实际业务的角度,提出建设性意见。最后的看价值环节,最为精彩,我见过多次,由测试或者开发,从功能,方案,投入产出比等角度深入分析,最终告诉所有人:"当前大家别再继续了!有坑!"。我相信最终如果大家真的达成这个结论,那将是一个巨大的价值!有点跑题了,其实我想表达的是,在自动化测试上面,做是一定要做的,不光是为了快速验证功能,保证代码质量,更是为了后续代码的重构,优化奠定一个良好的基础。至于具体是开发主导还是测试主导,就要考虑当前团队成员的背景情况,大家一起合力输出,定好标准,写好用例,使用自动化的手段把代码看护好。
最后,我想聊聊监控平台
还是上文提到的观点:"没有反馈就没有控制"。我们的系统,想要长期稳定运行,就必然要要配备一个"监护人",也就是监控平台。它最基础的功能,就是帮我们统计用户在现网环境下使用时,出现了哪些错误,比如静态资源加载失败了?某个接口调用失败了?某个操作导致页面白屏等等。它需要详细记录用户出错时的所有上下文环境,以及完整的复现步骤,方便我们后续复现排查解决问题。在此基础上,我们还可以深入一些,针对重点关注的页面,它的性能表现如何?加载速度是符合用户预期的,还是低于预期的?性能上的卡点在哪里?是否需要进行深度优化?最后,还可以跟打点日志上报结合起来,看下用户日常最常用哪些功能?尤其是一个新功能上线之后,我们可以从数据里面看到这个功能是否被用户频繁使用,如果没有的话,是不是功能的入口隐藏的太深?页面上没有符合直觉的清晰引导?还是说这个功能就是一个低频功能?亦或者这个功能在用户看来很难用,试过一次之后就再也不想用了。有了数据的支撑,我们就可以明确下一步优化的方向,能做的事情还有很多。
就像文章开头所说的那样,前端工程化是一个很大的话题,我只是把自己在实践过程中的一些想法写在这里,它是一个随着项目发展和团队成长而不断演进的持续过程,所以最好的方法可能是定期回顾流程中的瓶颈,并引入新的实践或工具进行优化,保证我们的"房子"能持久的盖下去!