引用抖音哲玄前端
框架名称 - elpis
创建一个项目的时候往往都是很随意的命名,因为项目的命名随意,导致后面的上线部署,项目管理的时候都会很奇怪的感觉,看到项目都不知道干嘛的,所以在系统建立起来之前要给系统建立一个名字。
根据前言、问题分析、需求推导可以得出一个结论,这套系统做的是多网站建设平台,通过配置化解放重复性工作,通过这个项目达到几个目的:
1. 可以作为系统框架运用到日常开发中,解放重复性体力工作。
2. 通过做这套系统可以去提升编程思想,可以去解放全栈开发能力。
3. 培养系统架构能力,可以去体系化的思考。
4. 可以培养从零到一上线的思维方式。
发现问题、分析问题、提出需求、业界方案调研、自己的方案落地、复盘、未来规划这套方案一定是贯穿做事体系的,最终为此项目取名为elpis,寓意是希望、期待。
业界方案调研
做每件事强调的都不是能做成怎样,而是在成怎么样之前的思考,是否对比过其他解决方案而得到当前解决方案,只有这样才会将每件事情做起来靠谱,或者说给到对方的技术广度是更明显的。
在未来toB的产业物联网时代中,多方交付系统,我们的人力和单兵作战的能力是有限的,从而我们要实现出这个目标,企业级应用框架!往往现在的程序员都是单一工具,大家的能力都参差不齐,而又都在做提炼抽象这个动作,就会导致内容、质量都参差不齐,本质上还是会导致重复的工作,还是会重复开发组件》
方案A:大而全的触达系统,标准化流程(行业参考:神策,易观)
方案A问题:
1. 不适合多客户交付场景
2. 交付时冗余过多无用能力
3. 定制化拓展能力弱,往往牺牲客户需求
4. 跟随迭代,增删明显
方案B:多个子系统配合,灵活配置各个运营场景
方案B问题:
1. 不适合外部客户私有化交付的场景
2. 通用建站能力不适用于领域较强的场景
3. 过分灵活、搭建复杂,并无实质性提效
4. 未能体系化解决触达领域问题
折中解决方案
1. 粒度:算子服务
2. AOP领域模型
3. 面向对象建站
项目架构设计
再重新聊一下之前推导过的问题和目标,首先要全栈全流程的支持一个多网站的系统平台或者应用框架,这个框架或者平台可以配置化去沉淀百分之八十的重复性工作,而且提供各种各样定制化能力,可以灵活支持剩余百分之二十定制化需求。
要想做出这样一个框架或平台,就要结合业界方案调研去对比,我们会发现传统的建站系统运作在toB的场景下会各有利弊,我们需要一个沉淀能力、支持拓展的系统框架,基于这个框架上面,我们再去孵化各种各样的中后台管理系统,交付客户。
这个就是我们系统目标的架构图了,分为三层:展示层、BFF层、数据层
数据层:
是整个系统的能源供给,上层的所有工作,本质上都是在消费上面层的所有工具,包括但不限于数据库、缓存、文件存储、日志、外部服务。
BFF层:
本质上是一个后端,可以理解为前端的后端,不仅会做一个转发的动作,还会做一个数据层的消费和前端的交互,都会在这一层去发生,在这个项目中我们会用node去实现.
很多程序员都停留在只写页面的工作上,通过这个项目可以让前端更灵活,可以在这里面做接口转发,数据库表操作,读写缓存,接口合并,第三方服务的一些安全保障,签名等等,都可以在这一层去发生,跨域问题也不会再有了,这一层也是渲染web页面的一个服务器,外部服务都是通过这一层去请求的,因为一旦这样的话我们会把页面到API的请求变成页面到BFF层再由BFF转发到各个目标服务器,那服务对服务,就不会受到同源策略的限制,同时跨域问题都解决掉了
在BFF层里还细分服务架构的三层
接入层: 路由定义、参数校验、各种中间件,当请求通过接入层之后就会进入业务层,从ABC的角度看就各种的controller控制器
业务层: controller就会通过不同接口处理的具体业务逻辑,包括一些参数的整合,返回结果的处理啊,逻辑计算啊,还有一些失败结果的处理,都在这一层去处理,再过程中会掉起服务层各种各样的service,去配合完成这个业务逻辑
服务层: 服务层就是service,每个service都包含着有多种原子性的方法,每一个方法都是针对数据层一个原子性操作,尽可能不涉及业务,具体的由业务层调取不同的service去完成,要完全隔离业务是不太可能的,有时候涉及特殊的业务逻辑是要放在服务层去做。
这么来看BFF层可以做到什么事情?
第一点可以解耦,展示成后端接口,后端程序员只需要关注接口的业务逻辑,不需要关注前端如何展示,然后具体的展示逻辑,比如展示成什么字段,增加什么接口都由前端同学BFF层里去完成,前端只需要做前端,后端只关注原子性的接口逻辑即可。
第二 点就是对比接口的请求都可以收拢到这里面去做,所有的对外请求BFF来汇聚处理。
比如某个页面需要依赖多个接口的,可以由BFF层统一去对外分发,这样外网到内网的接口请求交互时长,无形中变短了,有效的提升接口的所有性能,当所有的接口都是由页面端去发生的,那每一个接口要做的是从页面,解析域名再到创建链接,DNS的节点寻找,寻找完了就要去具体服务端的网关,负载均衡,防火墙,再到具体的服务节点。从外网到内网经历的步骤就会长很多很多的节点多很多的,而我们把这个长节点的这个行为,从我们的页面端BFF层,剩余的我们BFF层跟后端的具体服务,也许我们在同一个集群下,我们剩余的服务对服务,IP指到端口去访问,那我们的效率会极大的变大。
换个角度来说,本来一个页面有十个接口,十个接口我们去请求到目标服务器,而我们中间就会经历十次外网到内网的请求,那我们把十个接口放在BFF层,我们就只需要经历一次从外网到内网,另外的目标对服务器都是我们服务对服务的集群去完成,性能会得到一个提升,同时也会绕开浏览器对同一域名下请求的并发上限,这一层就是达到这么个好处,
第三 点就是一些后端的签名,这些密钥就可以放在BFF层去管理,当我发起请求时从BFF层里用这个密钥去签名,签名后再把这个后端,服务对服务的请求到后端,这样对于一个目标API服务器才能起到一个保护效果,比如后端有一个API,这个API是一个用户的一个分页查询列表,那一旦这个接口暴露给前端的话,一些爬虫黑客拿到这个链接,就会不断改这个数据,把page从1到n一直改参数,一直遍历就会把后端所有数据都提取出来了,那我们怎么做这个内容的防范呢,我们就要加一个失效,让一个接口,及时提取了这个链接也是有时效的,加一个时间密钥,签上一个signKey,而用后端解析这个signKey,看看这个signKey是否过期,如果过期了,这个接口就不会生效了,反扒还有一个很大的学问,这个只是其中一个,这样去做的话,就会让接口API有一个时效性了,中间没有BFF的话,密钥就要暴露在页面端,再页面端架密钥的话他的效果就不会太明显了,当然我们也可以这样做,但是对方要去识别去破解还是可以拿到的,所以要把这个密钥放在后端,放在BFF层就一定不会对外暴露的,对后端服务器起到一个保护作用。
第四点因为由BFF层,这个项目就具有ssr的能力。没有这一层也是可以的,做ssr的时候要与后端有许多沟通,往往会把页面布置在上,而不是让后端去ssr,这样去做的话理论上可以,但真做起来还是有很多阻力的,沟通的成本、对方的不愿意等等,前端没有这么灵活,最后就缓存,表查询啊,日志等等,其实我们都可以在这一层前端自己完成,比如说对菜单列表做成一个动态的,那总不可能在业务后端去写好一个API专门去查菜单的,将来架一个菜单就让后端改一改录一下这个库,这对后端来说也是没有发挥到后端真正的价值,而我们现在有了BFF层,有了权限的校验,一些登录的控制,或者动态菜单,我们就可以在BFF层由前端自己去处理,后端不用管这些的,就关于BFF层有了这层就让前端有了更多的可能性,性能方案、安全方案,业务方案都有了更多的选择空间
展示层:
第一,这里的请求只对BFF服务的,就所有的请求发出去都是请求到后端BFF层,不会请求到像java、python那些对外的API,所有外部API都是由BFF层转发。
第二,这个项目的框架层由我们熟悉的MVVM框架组成的包括ui框架、组件库等等通过这一个个部件组装成我们前端框架的一个框架层,前面也提到了系统也具备一个ssr的能力,所以我们的系统也是支持多页面入口,是一个MPA的应用,但每一个页面单独进去又是一个SPA应用。不同的view展示不同展示逻辑,像现在前端程序员,也都只在这一个SPA小框框里去写的,那我们的路会越走越窄,但通过这个项目会适应这个时代的发展,这就是系统的架构层面的事情
技术选型(vue3 + nodejs)
回到这个图,我们针对这三个不同的层级选出对应比较好的也比较匹配的技术栈,通过分析对比选出整一套的架构里面所涉及到的技术
我们首先从数据层开始,数据层选了两个点作为项目里面用的,数据库和日志系统。首先看数据库对比,MySql和MongoDB,
单看这些八股文的概念很难选择,因为都是成熟的数据库,从来不会因为这些而去比较,只会看适合与不适合,后端接触Mysql比较多,网上对mysql的生态支持是更好的,在像阿里这样的大公司,node所接触的数据库也逐渐从MongoDB转为Mysql了,考虑到背景、生态,况且后端也未必是node,虽然我们选择的是node是因为更适合大家去用node,做后端的一个迁移,或者是技术栈的一个转型,所以将来也有可能去迁移到java、go、python,那这样的话mysql的生态会更加完善一点的,而且对于从来没有了解过数据库的人,了解mysql对于后面的竞争力、通用性其实也会更好,所以在后面的项目中,考虑到这些因素,所以会在elpis中选用mysql。
日志工具,有了BFF,持久化存放在磁盘一定是不可或缺的,我们平常直接请求到后端连接,后端记日志,前段是对日志没有太大感知的,最多就看看错误代码接口返回请求参数,对日志感知度不是很强,但是有了BFF层就不一样了,因为我的数据接口,先请求到BFF,由BFF转出去给java,那可能BFF请求过去那一层里面出了问题,那这些在前端未必能感知得到,所以我们必须要BFF层上打日志,日志工具就很重要了
通过比较来确定日志选型,首先就要排除掉Bunyan、Pino,生态支持太少了,就意味着他的更新没有跟上就会出现很不稳定的风险,这个项目是企业级不是单一练手项目,所以这个log4js是更加适合的,关于日志系统就选择log4js来作为日志打印工具。
在BFF层中,后端就建议用node了,因为对于前端来说直接去用java、python、go什么的是陌生的,对后端没有太多了解的更适合node,node的运行和浏览器是相同的,等到node熟悉好了后端,那就可以去平移到java、python、go了,学习路线就没有这么陡峭了,平滑过渡了。
对于node也是有很多服务框架的,因为对Koa比较熟悉,踩过的坑比较多能解决很多问题,所以BFF层就选用了Koa,也会用一些egg.js的思路去搭建一个简易版的Egg内在核心引擎。
展示层就会选用生态环境好的,主流的,熟悉的Vue3+Element-plus+Webpack5,作为前端,工程化的能力很重要,是怎么样的核心体现我们的前端能力,为什么说不用Vite呢?因为没有什么本质的区别,本质都是在做一个工程化的事情,对于这个项目的出发点是,掌握工程化的大理念,Webpack5是更能体现出这方面的,等了解熟悉了,就可以转到Vite中,所以这也是选用Webpack5的原因。
看到这里是否有掌握到一个点?因为数据层mysql生态更好,以及各种各样的语言的适配性更高,所以选mysql,去规划这样的选型,log4js考虑到是一个企业级应用,不是一个快速开发所以选log4js,node18考虑到的是对于前端工程师对数据层两个东西都是陌生的,我们先让语言的陌生感消除到,所以选择node18,为什么选择koa?考虑到经验选择koa,像vue3那些前端的工具是因为经验多、生态好,主流,webpack5是对工程化的理解更全面,所以选择这些,要明白在选择某个技术选型的时候,一定是有个理由而选择个这个技术,可能考虑到生态、经验、未来发展、总会有个理由去推动选择技术栈,这就是技术选型核心思路。
服务端BFF方案设计
我们要怎么样才能把这些东西运作起来呢?我们需要有这么一套规则,
根据这个思路去定义规则
我们定好了文件目录的规则,再通过解析器,这也是egg.js的一个简版,在服务启动时,根据预定好的目录规则,将相应的代码,相应的内容,解析加载到内存中,我们将这套解析器叫elpis-koa,在运行时,通过外部的请求进入到我们的服务端,这些服务端进入到接口层,由router做接口分发,渠道router-schema做接口的参数校验,过了校验进入到先进后出的洋葱圈模型,通过不断的包裹住中间件,每一个中间件都有一个进入的时期,和离开的时期,中间包住的东西处理其他逻辑,中间件可以理解为各种各样的拦截器,在业务处理之前和之后做相应的一个处理,可以全局去配置也可以特定的API进行配置,过了中间件就来到了业务层,controller处理具体的业务逻辑,由controller掉起各种的环境配置读取拓展能力的读取,通用方法的处理,反正在这一层controller处理各种各样的业务逻辑,最核心的就会调起service,来进行业务处理,原子化能力,最后返回请求给回前端,这样就能看清整个请求的流向,从路由到中间件的迁至处理,到业务controller的处理,到serivce的处理,再到中间件的后置处理,再到返回结果,无论是业务请求,API的请求,这套流程是同样适用的。
整个elpis-koa也是服务内核,是整个BFF层的底层设计,也是最关键的部分,elpis解析器是非常非常关键的,后续整个服务拓展都是基于这个内核的能力来编写的,这也是小型的egg.js及其简单版的
前端-领域模型-架构设计
我们现在往往面临的需求是往往是很多的增删改查、运营系统,将来的虚拟团队可能会有标品交付客户这么些场景,每一个运营系统之间,可能会有很多的重复性板块,但是同时每一个运营系统又会有很多特异性的需求,就又有很大量的重复性代码,但是又不能完全重用,可能中间又有些特异性在里面,就会重复性的再做一些增删改查的体力活工作。
因此就需要打造这么一套elpis系统,沉淀大量重复性功能,也会有足够的定制化能力开发,接下来的逻辑会贯穿到后续开发所有逻辑上的,做这个系统的核心就是要解决重复性的工作,特异性的功能就针对性的做,这是最理想的状态,这样的目的有个需求,他跟领域模型设计理念是不谋而合的,他具体指的是通过一套统一的DSL描述出整个系统的具体结构,这样的话就可以把大量体力工作通过领域模型数据结构描述出来,再通过解析器把这个描述出来的语言解析生成具体的系统
但这样不太够,有了这个领域模型,数据结构,我们能够解析这个系统,但这样的话我们没有满足定制化能力,所以我们要结合面向对象设计思路,面向对象的核心思路:封装、继承、多态,基类派生多个子类又实例化成各种各样的实例,所以我们可以基于一套封装好的领域模型的基类在针对不同的项目继承出不同的项目配置,基于一套标准的流程,一套标准的结构,可以派生出子项目结构,在这个过程中我们新增,或者重载领域模型中的配置,再通过解析器把这份具体的派生出来的子配置实例化成各个具体的系统,这样我们就需要做两件核心的事情:
定义出具体DSL的用法,用于描述出具体领域模型的项目,
还有就是根据DSL动态生成系统的解析引擎。大概思路就是,在前端里面有一套DSL一个数据结构,实现解析引擎,同时DSL可以派生出各种各样的子类,每一个子类都可以做定制化的东西,定制化的配置,定制化的开发,这就是我们的诉求,两个大方向的事情
从配置上我们就需要继承同时要支持新属性的新增,重载领域模型,或者各种各样的配置能力,那这样一来的话我们领域模型中的配置,可以沉淀出大量的重复性的配置,或者我们重复性的代码通过领域模型可以沉淀下来,而不同系统之间通过定制化的能力,可以通过拓展出来的这些配置来进行后续继承还有封装重载,这方面的能力,当然数据的话就看两眼就好了,不用很具体的去看
第二件的事就是要去实现模板框架也是解析引擎,去响应DSL的配置,首先要有个工具库,存放各种各样的工具,比如网络请求、公共方法,也可以根据需求添加更多的工具,组件库里面的组件可以根据schema的配置DSL生成的,组件库里面的组件都可以通过DSL描述出来的,具体长什么样子,要发什么请求,要什么样的参数,怎么样去渲染,包括了一些table、sche-bar、form这些各种各样的组件,同样的根据我们实时再开发的需求,这个组件库也是个在拓展的,接下来就是能相应各种DSL的模板页,进入了模板页就是SPA的应用,通过DSL的解析,还有vue-Router把页面分发到不同的schema-view、ifame-view、custom-view上,我们能响应DSL的页面,同时能跳转第三方服务的页面,还有就是我们自定义的开发,如果有特殊性需求可以通过custom-view去拓展,关于schema-view中就能用header-container、sider-container、schema-searhc-bar、schema-table、schema-form去组成,这些组件都可以通过schema去渲染出来,同时这些组件也会流出足够的拓展空间,这个组件的本身是可以去拓展的,就比如说header-container、sider-contanier就流出足够的插槽,chema-searhc-bar、schema-table、schema-form可以动态去拓展新的组件,而table就可以设计成根据schema去渲染出各种各样的操作按钮,这些操作按钮又可以去动态响应,调取各种各样的组件,让这些组件可以根据需求动态添加,这就是足够的拓展性。
系统的设计本身是一个多页面的设计,模板页其实是其中一个入口,在这个项目中实现一个模板页的入口,如果后面有更多的页面需求,我们也可以支持新增其他模板的模板页,再就事论事去开发新的页面,这样一下来,我们这模板页,蓝色和黄色的的部分都是沉淀好的,都是可以复用的,基本可以解决百分之八十的业务场景,针对各个项目中的定制化能力,我们都可以通过绿色这些部分去进行拓展,我们经过研发的工作重心,去不断地开发这种管理后台的重复性工作,把他转化到直接针对性的开发,绿色部分的一个工作项目,就我们的时间,后面的工作,就这样有针对性了,我们做的是,维护可复用的那些组件,同时每次过来新需求的时候,我们的时间都投入到只开发新增的或者定制化的需求上,我们这样就可以很好的去减免重复性的工作,这样的话我们除了工作提效以外,我们的个人成长也是有很大帮助,整个项目的可维护性熵增也会相对少了很多,缓慢很多,这上面是关于前端的整体的设计