跳转至

直播加餐02 Freewheel前端工程化的演进和最佳实践

本文由编辑整理自宋一玮老师在极客时间直播中的演讲《前端工程化的最佳实践与演进》,详细视频请在b站搜索观看,或直接点击链接观看。以及,PPT获取地址在这里,提取码为:63U8。

上节课我们通过软件开发生命周期,了解了前端开发为什么工程化,那么这节课,我就来相应地介绍一些我所亲身经历的前端工程化的最佳实践,希望能对你有所启发。

FreeWheel是我供职的公司,我们主要是一个视频广告平台,面向的是欧美客户,产品包括视频广告管理平台(这是一套非常复杂的UI)、Ad Serving、报表、预测。

从2014年开始,FreeWheel在前端架构层面就开始做一些与工程化相关的改进,所做的工作量很大,所以我会侧重介绍其中2-3个点,说明当时的痛点是什么,以及我们做工程化改进的初衷是什么。

FreeWheel前端工程化的演进

在2014年时我们就已经开始做一些前后端分离的工作,引入React,并且开始做资源组件库了。因为当时整个应用非常复杂,有300多万行的代码(非常恐怖的代码量),这在当时来说算是比较有远见的一个举措了。

开始做自研组件库,一是市面上没有太多成型的组件库,另一个是我们这个行业,以及行业的这些用户也有一些特别的需求,所以我们当时用了一个构建工具,叫Browserify(如今已经退出历史舞台)。

2016年,我们有了SparkUI 1.x。这是一套React组件库,其中部分组件做了开源,现在主要基于React,还有Redux。

然后打包用的是Webpack,Webpack的版本从1到5,变化还是不小的。后来我们又引入了静态资源的这样一个服务器,也就是Nginx。那么问题来了,打包的结果最后如何能到Nginx上面去呢?

我们当时开发了一系列命令行工具,之后Nginx接cdn,这是肯定要做的,因为我们无论如何都要把Nginx做优化,也就是吞吐量的优化,毕竟它没办法保证我们在世界不同的地方,都能有一个比较能接受的速度去访问这些资源。

后来我们做了一件事情。因为我们说之前的Ruby-on-Rails,整个就是编译一次,然后构建一次,最后到上线一次。可能大家会没法想象,这整个过程要花两三个小时的时间,才能让代码到线上。

也就是说,很有可能你改了一行代码,需要花三个小时的时间才能在线上看到它,从前端开发来讲,这是一个比较难受的速度。所以我们在这一阶段的优化中,已经慢慢地把这个速度提上来了,也不需要完整地部署Ruby-on-Rails的内容了。

在这个时间节点,我们还推出了一个叫FDD(Frontend Delivery Decoupling)这样一个项目(名字是随便起的,不用太纠结)。这个项目实际上保证了我们每个业务模块都会有它自己的构建,并且它们是单独构建、单独上线,这样整个时间就会有很大的一个缩减。然后各个小团队之间也有一个灵活性,自主程度会有很大的提升。

2018年,我们还引入了Backend For Frontend,也就是BFF,主要是做动态的HTML模板,还有前端参数的一个下发。

到了20年,有一些新的前端项目已经开始上手TypeScript了。TS确实很强,对于巨型项目,它的把控力是很强的,让我们不太容易在中间迷失方向。

如果说你是这种弱类型的JS开发 ,很有可能开发的时候发现自己不知道数据的样子是什么,但是TS可以帮助你解决这些问题。

然后编译的工具也是Webpack,少数项目会用Vite。后来我们内部的组件库SparkUI还升级到了4.x。接着就是2021年,我们引入了微前端架构,这个在这节课最后会有比较详细的解释。

以上就是在开发过程中遇到各种痛点时,FreeWheel在前端工程化上所做的事情。

FreeWheel前端工程化的一些最佳实践

接下来我们就具体看看FreeWheel在前端工程化上会有哪些最佳实践。

构建:Wepack耗时过长

首先就是我们React项目,它的Webpack的耗时确实很长。如上图所示,可以看到耗时已经达到153秒,快三分钟了。

如果是在服务器上跑CI/CD的话,这个速度其实没什么问题,但如果在本地开发时也是这个速度,那必然是有点痛苦的。所以我们开展了一个优化的过程,把前端交付做解耦。

事实上,我们对于Wepack本身做过很多优化,比如Loader、Plug-in,等等,但发现这样的优化终归是有极限的。所以我们在想,能不能跳出这个盒子(Out of Box),看看是不是真的需要把所有代码一块打包呢?答案是不需要。

原因也比较简单。不同业务模块背后负责的团队,其实并不是完全在一块工作的状态。

比如说,虽然我的开发有公共的部分,但是也可以满足团队的需求。当把整个交付的东西解耦之后,其实单个业务模块的时间就省下来了。当然也没有省去很多时间,当时平均下来是在83秒左右,这个构建时间也不是特别地快。

没关系,我们继续优化。要知道,UI的内容是组件库,组件库要参与到构建里面,这中间可能会有一些转译的工作,或者说一些编译的工作。对于这些工作,我们是不是可以先提前做呢?也就是说,我们通过Wepack里面dll这种方式,先把公共模做一个构建,等到再基于公共模块去构建跨业务的模块的时候,时间就会减少。

当抽完公共模块,一般来说平均下来的构建时间为46秒,这样一来,其他模块用的时间也会相应减少。

公共模块有什么好处呢?对于每个业务模块来说,它的公共模块都是一样的时候,可以帮助我们进行跨业务模块复用,也就是浏览器缓存。

在浏览器上我们会给客户开一年的缓存。也就是说,客户只要在其中任何一个业务模块上访问过了,这个公共模块就会被缓存下来。如果再去跑其他模块的话,就不会再去读这块内容了。这是我们在工程化上的一个改进。

从这个改进的过程中可以看到,我们并不是简单地盯着Wepack里有多少玩法,去挤压它的性能,而是跳出来,看看是否还有改进的空间。这种跳出来的思维我觉得还是比较重要的。

不过后来我们拆分完了之后,觉得还是不够,因为公共的东西也变得越来越重了。那么还能不能再去挤压出一些效率呢?这一次,我们决定从NPM包上入手。

发布NPM包

在JS的整个生态里,最重要的一个中间产物就是NPM包,我们就可以把这些项目发布成NPM。发布NPM包也有几个阶段,一开始是靠人工,这还是很痛苦的。

不仅要确保有权限,然后发布包时还要去给包打Tag,提交到Git上。如果忘了打,到时候还得补上,最后经常出现补着补着补错了的情况。说句题外话,人会出错,这是一定的,因此也没必要去苛责这个人。换句话说,本来就应该是自动化做的事情,就不要让人一直去做,因为他终有一天会出错。

到后来我们改进为在持续集成里去发NPM包,也就是自动化的方式。Conventional commits,意思是说在你的git commit message里,我们可以用一些约定俗成的方式,比如说feat,然后再在后面加一些描述。如果说你有Breaking Change(破坏性更新),你就加一行Breaking Change。

后面还会有一些描述,当这个东西进去以后,比如说如果只有feat,会帮我们去做一个小版本的更新。假设之前是1.2.3,现在变成1.3.0。如果我有Breaking Change,我就告诉Commit说是有Breaking Change,最后就会变成2.0.0发布。

同时,CI也会在Jenkins里面去跑,每次也会探测和判断一下,检查一下我都有哪些Change,是不是需要发包,甚至如果完全没有Change ,就直接跳过。当然,在发包之前肯定要过Lint和Test。

如果我们现在的分支属于main分支,我发的就是正式版本,比如1.1.0。但如果我现在是一个next分支 ,就会发一个pre-release版本,比如1.2.0-alpha.0,类似这样的。

甚至,当我发完包以后 ,lerna会帮我们自动创建一个Git的tag,把这个tag,还有commit都提交上去。提交进去以后,我们还要根据当时的main分支去重新创建一个next分支出来,保证我下次再合并next分支时不会出现太大的冲突。这套机制在我们公司用得还是蛮舒服的,帮助我们解决了很多问题。

这样一来,无论是业务团队还是我们得兄弟团队,当有新版本发布的时候,只要去看新版本,装到自己的项目里就可以了,当然也可以选择不装。

总结来说,通过CI/CD的方式提高了我们工程实践上的效率,也是比较革命性的一个事情了。当然,我们还是借助了外面的工具,也发现无论是Webpack还是后来的NBA包,它们都是有极限的。

构建:Vite

当团队变多了,项目也需要越来越快的响应的时候,其实Wepack就没那么够用了,这时我们看到了Vite2.0这个工具,这确实是非常赞的一个版本。

Vite2.0 并不是纯粹的从零产生的一个工具,它的基础是开发服务器,实际上依赖了esbuild这样一个比较成熟的基于Go语言的工具。源文件无需打包,依赖项使用esbulid预构建,直接以原生ES Modules形式即可加载到浏览器中。

在生产构建上,是基于Rollup这个工具,针对生产环境的打包命令。说句题外话,Rollup其实在打NPM包的时候用得非常广泛,所以这是两种工作模式,后来共同组成了 Vite2.0。

Vite2.0其实利用了浏览器的特性,现在最新浏览器已经支持ES Modules了。

既然支持了,那么在这种开发模式下,我们也不太关心它是不是一定要有特别好的性能,因为我们可以让浏览器来去一些不需要Bundle的JS文件。

这个时候其实又省了一个步骤,也就是不需要Bundle了,可以说,Vite在整个设计层面就已经很快了。

我放一下这个片 ,其实也是从里面拿过来的一个片,左面是一个在线的Create React application架子,然后创建一个项目,右面是创建一个Vite React的项目。

其实我在说这句话的期间,右边已经创建完,并且可以用了,而左边这部分还在跑。从开发的角度来讲,Vite这种开发性能的提升是指数级的,可以为我们的项目带来很多提升。但同时里面也有坑 ,鉴于不是咱们这节课的重点,我就先略过。

微前端

在工程化的实践改进上,我们刚才大都是在构建这方面做优化、提高效率,现在让我们回到业务上,从各个团队的工作方式来看,是不是还有其他方式可以提高我们的灵活性,然后解放生产力呢?这个时候我们看到了微前端架构。

最大的改进就是微前端容器。前端开发者都知道,在公共的部分,比如说页头页面上,我们主要关心公共导航、错误处理、用户认证逻辑等。对此,我们没有必要在每个业务模块里都写一份,或者是都通过NPM包的方式包装一次。

那么我们可以通过微前端的方式做一个容器,相当于把公共的东西就固定在那里,不会再变。然后对于每个业务模块 ,我们可以把它独立打包部署成子应用,不限制React版本,不限制构建技术,而是根据路由按需加载子应用。

而且微前端有这样一个好处,它可以保证这些子应用样式是有一个隔离的。比如JS有一定的隔离,然后是子应用。可能一个子应用是用TypeScript加上React 17来写的,另一个子应用可能是JS再加上React v.18来写。

甚至可能有一些老的应用,干脆就不更新了,就是React 15加Webpack来写的,我们都做了这样的改进,让它们可以成为这种子应用。

这样得话,我们在整个Container里面就可以跑一些异构的子应用,从而解放生产力。

互动时刻

对于前端工程化,你有经历过一些实践吗?成功还是失败了呢?可以分享一下你的经验吗?

欢迎通过留言告诉我,我们一起交流进步,下节课将进入一个新的模块!

精选留言(4)
  • 墨白™ 👍(6) 💬(1)

    做过微前端+BFF,两者做完的感受就是,技术不是最难的,最难的是想清楚这些技术引入能不能为当前业务有一些增益部分。

    2022-11-29

  • 01 👍(2) 💬(1)

    老师您觉得微前端具体解决什么问题呢? 是应该奔着使用微前端为前提来开发应用, 还是从前期通过更好的约定或者工程来解决呢

    2022-11-16

  • Jy 👍(0) 💬(1)

    我认为微前端最大的优势是能解决不同组织带来的协作成本。如果说引用了微前端,没有带来分工上的优势,那么就要跟引入的成本权衡下了。

    2023-08-05

  • InfoQ_3906e8b6c95f 👍(3) 💬(0)

    现在web工程的复杂度已经和App工程有得一拼了

    2022-10-23