珠峰-Web前端架构师培养计划(完结)

珠峰-Web前端架构师培养计划(完结)

获课:97java.xyz/2481/

Web 前端架构师培养:开启技术进阶之路

在当今数字化时代,互联网产品的用户体验至关重要,而 Web 前端作为直接与用户交互的部分,其技术架构的优劣直接影响着产品的质量和用户满意度。Web 前端架构师在其中扮演着关键角色,他们不仅要掌握扎实的前端技术,还需具备系统设计、团队协作等多方面的能力。因此,培养优秀的 Web 前端架构师成为行业发展的迫切需求。

Web 前端架构师是项目前端技术方向的引领者。他们负责设计和搭建合理的前端架构,确保系统的稳定性、可扩展性和性能优化。在一个大型项目中,前端架构师需要综合考虑各种因素,如不同设备的兼容性、复杂业务逻辑的处理、数据的高效传输与展示等。通过精心设计的架构,能够让前端开发团队高效协作,减少代码的冗余和维护成本,同时为用户带来流畅、稳定的使用体验。

  1. 深厚的技术功底:掌握 HTML、CSS、JavaScript 等基础技术的高级应用,熟悉各种前端框架如 Vue、React、Angular 等的原理和应用场景。了解前端构建工具如 Webpack、Gulp 等,能够进行高效的项目构建和优化。
  1. 系统设计能力:具备从整体上规划前端架构的能力,能够根据项目需求设计合理的模块划分、数据流向和通信机制。掌握性能优化的方法和技巧,包括代码压缩、资源加载优化、渲染优化等,提升系统的响应速度和用户体验。
  1. 团队协作与沟通能力:作为技术领导者,需要与后端开发团队、产品团队、测试团队等密切合作。能够清晰地表达自己的技术观点和方案,理解其他团队的需求和反馈,协调各方资源,推动项目顺利进行。
  1. 持续学习与创新意识:前端技术发展迅速,新的框架、工具和理念不断涌现。Web 前端架构师需要保持敏锐的技术洞察力,不断学习和探索新技术,将其应用到实际项目中,推动技术创新和团队的技术进步。
  1. 基础学习阶段
    • 扎实掌握 HTML、CSS、JavaScript 的基础知识,通过大量的实践项目来巩固所学。可以从简单的网页制作开始,逐步深入到复杂的页面布局和交互效果实现。例如,尝试制作个人博客页面,从页面结构搭建到样式美化,再到添加简单的交互功能,如点击按钮显示隐藏内容等。
    • 利用在线学习平台,如慕课网、网易云课堂等,学习相关的基础课程,跟随专业讲师的讲解系统学习。同时,阅读经典的前端书籍,如《JavaScript 高级程序设计》《CSS 揭秘》等,加深对基础知识的理解。
    • 学习过程中,要注重代码的规范性和可维护性,养成良好的编程习惯。可以参考开源项目的代码规范,如 Airbnb 的 JavaScript 代码规范,规范自己的代码风格。
  1. 框架学习与应用
    • 选择一到两个主流前端框架进行深入学习,了解其设计思想、核心原理和常用功能。比如学习 Vue 框架,可以通过官方文档和相关的开源项目进行学习,深入理解 Vue 的组件化开发模式、响应式原理等。
    • 通过实际项目来应用框架,掌握如何进行组件化开发、状态管理、路由配置等。可以参与一些开源的 Vue 项目,或者自己搭建小型的项目,如电商产品展示页面,实践组件的封装、数据的传递以及路由的切换等功能。
    • 同时,对比不同框架的优缺点,根据项目需求选择合适的技术方案。可以阅读一些技术博客和论坛上的对比分析文章,如知乎上关于 Vue 和 React 的对比讨论,结合实际项目需求进行思考。
  1. 项目实践与经验积累
    • 参与实际项目的开发,从项目的需求分析、架构设计到编码实现、测试上线,全程参与。在项目中,积极承担不同难度的任务,如负责一个模块的开发,从无到有实现功能并进行优化。
    • 不断积累解决实际问题的经验,提高自己的技术能力和应变能力。当遇到兼容性问题时,学会通过调试工具,如 Chrome 浏览器的开发者工具,分析问题原因并寻找解决方案。
    • 同时,积极参与团队讨论和技术分享,学习他人的经验和技巧。定期参加团队的技术分享会,分享自己在项目中的收获,也学习其他成员的技术心得。
  1. 技术深入与拓展
    • 深入学习前端性能优化、安全防护、跨端开发等领域的知识。可以阅读专业的技术书籍,如《高性能 JavaScript》《Web 安全深度剖析》等,系统学习相关知识。
    • 了解前端工程化的流程和方法,掌握自动化测试、代码质量监控等工具的使用。例如,使用 Jest 进行单元测试,使用 ESLint 进行代码质量检查,通过配置工具来提高开发效率和代码质量。
    • 拓展自己的技术视野,关注行业动态和新技术的发展趋势。订阅一些知名的前端技术博客,如 InfoQ、开源中国等,及时了解最新的技术资讯和行业动态。
  1. 团队协作与管理能力培养
    • 在项目中逐渐承担更多的团队协作和沟通任务,学会如何协调团队成员的工作,合理分配任务,解决团队内部的技术分歧和问题。可以通过参与项目管理工具的使用,如 Trello、Jira 等,来管理团队任务和进度。
    • 学习项目管理的知识和方法,提升自己的团队管理能力。阅读项目管理相关的书籍,如《项目管理知识体系指南》,学习项目管理的五大过程组和十大知识领域,将理论应用到实际项目中。

Web 前端架构师的培养是一个长期而系统的过程,需要不断地学习、实践和积累。通过明确的培养目标和科学的培养路径,逐步提升自己的技术能力、系统设计能力、团队协作能力和创新意识,才能成长为一名优秀的 Web 前端架构师,为互联网产品的发展贡献自己的力量。在这个快速发展的时代,保持学习的热情和好奇心,不断挑战自我,才能在前端技术领域始终保持领先地位。

大型 web 前端架构设计-面向抽象编程入门

作者:svenzeng,腾讯 PCG 前端开发工程师

面向抽象编程,是构建一个大型系统非常重要的参考原则。但对于许多前端同学来说,对面向抽象编程的理解说不上很深刻。大部分同学的习惯是 拿到需求单和设计稿之后就开始编写 UI 界面,UI 里哪个按钮需要调哪些方法,接下来再编写这些方法,很少去考虑复用性。当某天发生需求变更时,才发现目前的代码很难适应这些变更,只能重写。日复一日,如此循环。

当第一次看到“将抽象和具体实现分开”这句话的时候,可能很难明白它表达的是什么意思。什么是抽象,什么又是具体实现?为了理解这段话,我们耐下性子,先看一个假想的小例子,回忆下什么是面向具体实现编程。

假设我们正在开发一个类似“模拟人生”的程序,并且创造了小明,为了让他的每一天都有规律的生活下去,于是给他的核心程序里设置了如下逻辑:

过了一个月,小明厌倦了一成不变的重复生活,某天早上起来之后他突然想吃薯片,而不是面包。等到傍晚的时候他想去踢足球,而不是继续打篮球,于是我们只好修改源代码:

又过了一段时间,小明希望周 3 和周 5 踢足球,星期天打羽毛球,这时候为了满足需求,我们的程序里可能会被加进很多 if、else 语句。

为了满足需求的变换,跟现实世界很相似,我们需要深入核心源代码,做大量改动。现在再想想自己的代码里,是不是有很多似曾相识的场景?

这就是一个面向具体实现编程的例子,在这里,吃面包、吃薯片、打篮球、踢足球这些动作都属于具体实现,映射到程序中,它们就是一个模块、一个类,或者一个函数,包含着一些具体的代码,去负责某件具体的事情。

一旦我们想在代码中更改这些实现,必然需要被迫深入和修改核心源代码。当需求发生变更时,一方面,如果核心代码中存在各种各样的大量具体实现,想去全部重写这些具体实现的工作量是巨大的,另一方面,修改代码总是会带来未知的风险,当模块间的联系千丝万缕时,修改任何一个模块都得小心翼翼,否则很可能发生改好 1 个 bug,多出 3 个 bug 的情况。

抽象的意思是:从一些事物中抽取出共同的、本质性的特征。

如果我们总是针对具体实现去编写代码,就像上面的例子,要么写死 9 点吃面包,要么写死 9 点吃薯片。这样一来,在业务发展和系统迭代过程中,系统就会变得僵硬和修改困难。产品需求总是多变的,我们需要在多变的环境里,尽量让核心源代码保持稳定和不用修改。

方法就是需要抽取出“9 点吃面包”和“9 点吃薯片”的通用特性,这里可以用“9 点吃早餐”来表示这个通用特性。同理,我们抽取出“17 点打篮球”和“17 点踢足球”的通用特性,用“17 点做运动”来代替它们。然后让这段核心源代码去依赖这些“抽象出来的通用特性”,而不再是依赖到底是“吃面包”还是“吃早餐”这种“具体实现”。

我们将这段代码写成:

这样一来,这段核心源代码就变得相对稳定多了,不管以后小明早上想吃什么,都无需再改动这段代码,只要在后期,由外层程序将“吃早餐”还是“吃薯片”注入进来即可。

刚才是一个虚拟的例子,现在看一段真实的代码,这段代码依然很简单,但可以很好的说明抽象的好处。

在某段核心业务代码里,需要利用 localstorge 储存一些用户的操作信息,代码很快就写好了:

这段代码本来工作的很好,但是有一天,我们发现用户信息相关数据量太大, 超过了 localstorge 的储存容量。这时候我们想到了 indexdb,似乎用 indexdb 来存储会更加合理一些。

现在我们需要将 localstorge 换成 indexdb,于是不得不深入 User 类,将调用 localstorge 的地方修改为调用 indexdb。似乎又回到了熟悉的场景,我们发现程序里,在许多核心业务逻辑深处,不只一个,而是有成百上千个地方调用了 localstorge,这个简单的修改都成了灾难。

所以,我们依然需要提取出 localstorge 和 indexdb 的共同抽象部分,很显然,localstorge 和 indexdb 的共同抽象部分,就是都会向它的消费者提供一个 save 方法。作为它的消费者,也就是业务中的这些核心逻辑代码,并不关心它到底是 localstorge 还是 indexdb,这件事情完全可以等到程序后期再由更外层的其他代码来决定。

我们可以申明一个拥有 save 方法的接口:

然后让核心业务模块 User 仅仅依赖这个接口:

接着让 Localstorge 和 Indexdb 分别实现 DB 接口:

这样一来,User 模块从依赖 Localstorge 或者 Indexdb 这些具体实现,变成了依赖 DB 接口,User 模块成了一个稳定的模块,不管以后我们到底是用 Localstorge 还是用 Indexdb,User 模块都不会被迫随之进行改动。

可能有些同学会有疑问,虽然我们不用再修改 User 模块,但还是需要去选择到底是用 Localstorge 还是用 Indexdb,我们总得在某个地方改动代码把,这和去改动 User 模块的代码有什么区别呢?

实际上,我们说的面向抽象编程,通常是针对核心业务模块而言的。User 模块是属于我们的核心业务逻辑,我们希望它是尽量稳定的。不想仅仅因为选择使用 Localstorge 还是 Indexdb 这种事情就得去改动 User 模块。因为 User 模块这些核心业务逻辑一旦被不小心改坏了,就会影响到千千万万个依赖它的外层模块。

如果 User 模块现在依赖的是 DB 接口,那它被改动的可能性就变小了很多。不管以后的本地存储怎么发展,只要它们还是对外提供的是 save 功能,那 User 模块就不会因为本地存储的变化而发生改变。

相对具体行为而言,接口总是相对稳定的,因为接口一旦要修改,意味着具体实现也要随之修改。而反之当具体行为被修改时,接口通常是不用改动的。

至于选择到底是用 Localstorge 还是用 Indexdb 这件事情放在那里做,有很多种实现方式,通常我们会把它放在更容易被修改的地方,也就是远离核心业务逻辑的外层模块,举几个例子:

将系统分层,就像建筑师会将大厦分为很多层,每层有特有的设计和功能,这是构建大型系统架构的基础。除了过时的 mvc 分层架构方式外,目前常用的分层方式有洋葱架构(整洁架构)、DDD(领域驱动设计)架构、六边形架构(端口-适配器架构)等,这里不会详细介绍每个分层模式,但不管是洋葱架构、DDD 架构、还是六边形架构,它们的层与层之间,都会被相对而动态地区分为外层和内层。

前面我们也提过好几次内层和外层的概念(大部分书里称为高层和低层),那么在实际业务中,哪些模块会对应内层,而哪些模块应该被放在外层,到底由什么规律来决定呢?

先观察下自然界,地球围绕着太阳转,我们认为太阳是内层,地球是外层。眼睛接收光线后通过大脑成像,我们认为大脑是内层,眼睛是外层。当然这里的内层和外层不是由物理位置决定的,而是基于模块的稳定性,即越稳定越难修改的模块应该被放在越内层,而越易变越可能发生修改的模块应该被放在越外层。就像用积木搭建房子时,我们需要把最坚固的积木搭在下面。

这样的规则设置是很有意义的,因为一个成熟的分层系统都会严格遵守单向依赖关系。

我们看下面这个图:

mark

假设系统中被分为了 A、B、C、D 这 4 层,那么 A 是相对的最内层,外层依次是 B、C、D。在一个严格单向依赖的系统中,依赖关系总是只能从外层指向内层。

这是因为,如果最内层的 A 模块被修改,则依赖 A 模块的 B、C、D 模块都会分别受到牵连。在静态类型语言中,这些模块因为 A 模块的改动都要重新进行编译,而如果它们引用了 A 模块的某个变量或者调用了 A 模块中的某个方法,那么它们很可能因为 A 模块的修改而需要随之修改。所以我们希望 A 模块是最稳定的,它最好永远不要发生修改。

但如果外层的模块被修改呢?比如 D 模块被修改之后,因为它处在最外层,没有其他模块依赖它,它影响的仅仅是自己而已,A、B、C 模块都不需要担心它们受到任何影响,所以,当外层模块被修改时,对系统产生的破坏性相对是比较小的。

如果从一开始就把容易变化,经常跟着产品需求变更的模块放在靠近内层,那意味着我们经常会因为这些模块的改动,不得不去跟着调整或者测试系统中依赖它的其他模块。

可以设想一下,造物者也许也是基于单向依赖原则来设置宇宙和自然界的,比如行星依赖恒星,没有地球并不会对太阳造成太大影响,而如果失去了太阳,地球自然也不存在。眼睛依赖大脑,大脑坏了眼睛自然失去了作用,但眼睛坏了大脑的其他功能还能使用。看起来地球只是太阳的一个插件,而眼睛只是大脑的一个插件。

回到具体的业务开发,核心业务逻辑一般是相对稳定的,而越接近用户输入输出的地方(越接近产品经理和设计师,比如 UI 界面),则越不稳定。比如开发一个股票交易软件,股票交易的核心规则是很少发生变化的,但系统的界面长成什么样子很容易发生变化。所以我们通常会把核心业务逻辑放在内层,而把接近用户输入输出的模块放在外层。

在腾讯文档业务中,核心业务逻辑指的就是将用户输入数据通过一定的规则进行计算,转换成文档数据。这些转换规则和具体计算过程是腾讯文档的核心业务逻辑,它们是非常稳定的,从微软 office 到谷歌文档到腾讯文档,30 多年了也没有太多变化,它们理应被放在系统的内层。另一方面,不管这些核心业务逻辑跑在浏览器、终端或者是 node 端,它们也都不应该变化。而网络层、存储层,离线层、用户界面这些是易变的,在终端环境里,终端用户界面层和 web 层的实现就完全不一样。在 node 端,存储层或许可以直接从系统中剔除掉,因为在 node 端,我们只需要利用核心业务逻辑模块对函数进行一些计算。同理,在单元测试或者集成测试的时候,离线层和存储层可能都是不需要的。在这些易变的情况下,我们需要把非核心业务逻辑都放在外层,方便它们被随时修改或替换。

所以,遵守单向依赖原则能极大提高系统稳定性,减少需求变更时对系统的破坏性。我们在设计各个模块的时候,要将相当多的时间花在设计层级、模块的切分,以及层级、模块之间的依赖关系上,我们常说“分而治之”, “分”就是指层级、模块、类等如何切分,“治”就是指如何将分好的层级、模块、类合理的联系起来。这些设计比具体的编码细节工作要更加重要。

依赖反转原则的核心思想是:内层模块不应该依赖外层模块,它们都应该依赖于抽象。

尽管我们会花很多时间去考虑哪些模块分别放到内层和外层,尽量保证它们处于单向依赖关系。但在实际开发中,总还是有不少内层模块需要依赖外层模块的场景。

比如在 Localstorge 和 Indexdb 的例子里,User 模块作为内层的核心业务逻辑,却依赖了外层易变的 Localstorge 和 Indexdb 模块,导致 User 模块变得不稳定。

缺图

为了解决 User 模块的稳定性问题,我们引入了 DB 抽象接口,这个接口是相对稳定的,User 模块改为去依赖 DB 抽象接口,从而让 User 变成一个稳定的模块。

然后让核心业务模块 User 仅仅依赖这个接口:

接着让 Localstorge 和 Indexdb 分别实现 DB 接口:

依赖关系变成:缺图

User -> DB <- Localstorge

在图 1 和图 2 看来,User 模块不再显式的依赖 Localstorge,而是依赖稳定的 DB 接口,DB 到底是什么,会在程序后期,由其他外层模块将 Localstorge 或者 Indexdb 注入进来,这里的依赖关系看起来被反转了,这种方式被称为“依赖反转”。

我们的主题“面向抽象编程”,很多时候其实就是指的“面向接口编程”,面向抽象编程站在系统设计的更宏观角度,指导我们如何构建一个松散的低耦合系统,而面向接口编程则告诉我们具体实现方法。依赖倒置原则告诉我们如何通过“面向接口编程”,让依赖关系总是从外到内,指向系统中更稳定的模块。

知易行难,面向抽象编程虽然概念上不难理解,但在真实实施中却总是不太容易。哪些模块应该被抽象,哪些依赖应该被倒转,系统中引入多少抽象层是合理的,这些问题都没有标准答案。

我们在接到一个需求,对其进行模块设计时,要先分析这个模块以后有没有可能随着需求变更被替换,或是被大范围修改重构?当我们发现可能会存在变化之后,就需要将这些变化封装起来,让依赖它的模块去依赖这些抽象。

比如上面例子中的 Localstorge 和 indexdb,有经验的程序会很容易想到它们是有可能需要被互相替换的,所以它们最好一开始就被设计为抽象的。

同理,我们的数据库也可能产生变化,也许今天使用的是 mysql,但明年可能会替换为 oracle,那么我们的应用程序里就不应该强依赖 mysql 或者 oracle,而是要让它们依赖 mysql 和 oracle 的公共抽象。

再比如,我们经常会在程序中使用 ajax 来传输用户输入数据,但有一天可能会想将 ajax 替换为 websocket 的请求,那么核心业务逻辑也应该去依赖 ajax 和 websocket 的公共抽象。

实际上常见的 23 种设计模块,都是从封装变化的角度被总结出来的。拿创建型模式来说,要创建一个对象,是一种抽象行为,而具体创建什么对象则是可以变化的,创建型模式的目的就是封装创建对象的变化。而结构型模式封装的是对象之间的组合关系。行为型模式封装的是对象的行为变化。

比如工厂模式,通过将创建对象的变化封装在工厂里,让核心业务不需要依赖具体的实现类,也不需要了解过多的实现细节。当创建的对象有变化的时候,我们只需改动工厂的实现就可以,对核心业务逻辑没有造成影响。

比如模块方法模式,封装的是执行流程顺序,子类会继承父类的模版函数,并按照父类设置好的流程规则执行下去,具体的函数实现细节,则由子类自己来负责实现。

通过封装变化的方式,可以把系统中稳定不变的部分和容易变化的部分隔离开来。在系统的演变过程中,只需要替换或者修改那些容易变化的部分,如果这些部分是已经封装好的,替换起来也相对容易。这可以最大程度地保证程序的稳定性。

虽然抽象提高了程序的扩展性和灵活性,但抽象也引入了额外的间接层,带来了额外的复杂度。本来一个模块依赖另外一个模块,这种依赖关系是最简单直接的,但我们在中间每增加了一个抽象层,就意味着需要一直关注和维护这个抽象层。这些抽象层被加入系统中,必然会增加系统的层次和复杂度。

如果我们判断某些模块相对稳定,很长时间内都不会发生变化,那么没必要一开始就让它们成为抽象。

比如 java 中的 String 类,它非常稳定,所以并没有对 String 做什么抽象。

比如一些工具方法,类似 utils.getCookie(),我很难想象 5 年内有什么东西会代替 cookie,所以我更喜欢直接写 getCookie。

比如腾讯文档 excel 的数据 model,它属于内核中的内核,像整个身体中的骨骼和经脉,已经融入到了各个应用逻辑中,它被替换的可能性非常小,难度也非常大,不亚于重写一个腾讯文档 excel,所以也没有必要对 model 做过度抽象。

面向抽象编程有 2 个最大好处。

一方面,面向抽象编程可以将系统中经常变化的部分封装在抽象里,保持核心模块的稳定。

另一方面,面向抽象编程可以让核心模块开发者从非核心模块的实现细节中解放出来,将这些非核心模块的实现细节留在后期或者留给其他人。

这篇文章讨论的实际主要偏重第一点,即封装变化。封装变化是构建一个低耦合松散系统的关键。

这篇文章,作为面向抽象编程的入门,希望能帮助一些同学认识面向抽象编程的好处,以及掌握一些基础的面向抽象编程的方法。

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。