We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
架构终极目标,用最小的人力成本来满足构建和维护该系统的需求。架构始终是我们解决复杂度的一个工具,如果当前系统并不复杂,我们不需要为了所谓的优雅去过分改造与优化它,持续将成本置在一个较低水位,就是软件最好的解决办法。
在大事件活动的开发中,其实是没有严格意义的架构设计的,为什么在长线h5中要做呢? 上图体现了要不要架构设计在时间维度上的开发效率变化。
对于大事件活动来说:
这些属性决定了大事件没有维护的成本,简单业务不抽象复杂的架构设计反而可以带来超高的开发和交互效率,是非常契合业务形态的。
而一个长线迭代的活动,情况就完全不一样了:
生活中,肯定有这样的经历:
1)炎热的夏天,你走在街上,买了一根雪糕。如果找不到垃圾桶,包装纸你会如何处理呢?是否找个有垃圾的角落,悄悄的扔掉?
2)超市买花生,你抓几个尝尝,花生壳一开始你会下意识的拽在手里。如果此时,看到地上有一堆的花生壳,你就会将自己磕的花生壳也扔在地上。
……
于是,同等效应。路上随处可见的垃圾,广告墙上乱七八糟的涂鸦……我们离优雅文明的环境就这样渐行渐远。
环境早就脏了,我扔的这点儿垃圾根本起不到关键性作用”、“反正也不是我先这么做的”,每个人都抱着这样的想法自我暗示。不知不觉,我们就变成让环境恶化的第二双手、第三双手……
这是一种有规律可循的心理现象,在心理学上称为“破窗效应”。指的是一件事情、一个东西一旦稍微被破坏了,人们就会有意无意地任其变得更坏。
随着活动时间的增长,如果没有良好的分层和隔离,那耦合部分会越来越多,越来越重,后续的需求迭代效率也会大大降低,还要花大量的时间回归业务逻辑和寻找bug。
一定会有同学有这个疑问,是不是引入了全局状态管理库redux/pinia,就认为是已经做好了分层?
答案是否定的,架构分层是一种设计思想,redux/pinia只是某一层的具体实现;分层也不止全局状态这一层,根据业务可以扩展出更多的层,因为这里说的前端架构设计本质上是针对业务需求实现最小人力的开发和维护,是从业务角度出发的。
不管是react还是vue,就像他们文档声明的那样,本质上他们都是视图层的编程框架,只不过大家实现思路有差异,解决的问题其实是一致的。
其核心思想是 UI = f(State) 。 视图层只会关心如何处理状态驱动视图的最佳实践,但并不会考虑业务逻辑和业务状态管理。
UI = f(State)
没有状态管理的时候,业务状态是分散在各个组件内部的,这也意味着业务状态和ui组件产生了严重的耦合;简单业务这确实没什么问题,只要复杂度稍微上升或者有所迭代,频繁的组件改动带来了高成本的维护性,可复用性成为了奢望,多人协作无法并行。
在用增的绝大部分场景下,开发者要解决的并不是UI = f(State),用UI = reactive/observable(State)描述也许更合适。
UI = reactive/observable(State)
这里的reactive/observable实际上就是ui层的业务逻辑,比如弹窗队列、动画、交互逻辑、异步请求等等。在视图层处理所有的交互业务逻辑和状态,显然是不合理的。
reactive/observable
全局状态管理在这时候的优势就体现出来了,ui组件可以只专注于视图层的业务渲染,展现什么和何时展现被提升到全局状态管理中,也就是vuex、pinia、redux这样的状态管理库中。
这样做的好处多多:
听起来确实不错,但实现起来仍然有较多问题,比如
后面的具体实现再来解决这些问题。
前端的状态不是无根之水,一般业务场景下,初始状态都是由后端下发,页面生命周期中还会有异步接口请求更新,请求本身可能涉及到重试和缓存,localStorage的存取,端能力获取的某些状态,大量的用户交互带来的状态的改变
这意味着:
思想是一致的,但实现上和后端传统的MVC架构区别非常大。
目前用增的场景,领域数据都是从后端获取的,不存在实质意义上的model层,也无需花大力气维护M层。
ui状态层也并不是对等controller层,这一层是存在状态的,而且提供了业务逻辑的实现以及响应用户交互的真正实现,这和传统M层倒是有点类似。 传统M层也就是领域模型并不关心view层,但ui状态层本身就是为了ui展示衍生出来的,这其实反应了前后端真正处理的业务是有极大差异的。
至于view层,MVC中的v层只存在展示功能,一般没有业务上的逻辑,但现代前端的V层显然比传统产品要复杂的多的多。 不但实现了组件状态和dom的自动绑定和更新,用增场景下的动画、ssr hydration这些都主要在v层实现,他们其实已经是业务逻辑的一部分,而且是和view层深度绑定的,无法彻底剥离。
说了这么多,其实传统的后端MVC真正处理的业务集中在M层和C层,现代前端产品的业务重心集中在V层以及衍生的ui状态层,业务重心不一致自然无法直接套用一样的架构设计。
业务复杂度隔离,避免耦合、复杂度外溢
限制模块的职责范围,不需要关心上中下游模块的实现
一个业务需求往往不一定是完全扁平化的,很有可能在多个维度中跳跃或并行。跳出当前的维度,或在更高阶进行抽象,反而可以让低维需求保持原子化。
响应式编程是一种面向数据流和变化传播的编程范式。 这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。
vue3框架本身就是一种响应式的数据绑定和视图更新,但我们对响应式编程的使用不止于view层。
约定即强约束,降低自由度,开发效率变高和关注点集中 配置极简化,可变的范围非常小,需要操心的配置也足够少
比如现在的css开发还处于裸写less的阶段,实际上和UE、UX一起共建原子化样式规范+业务场景样式规范+交互规范之后,可以极大的降低各方的开发成本,也可以更好的约束不合理的行为。
强约束+分层+规范,很容易形成高质量的稳定输出,进一步形成真正的前端资产沉淀。
有了资产沉淀,可以顺势推出垂直向的低代码平台或相关服务,甚至将开发流程直接搬运到低代码平台构建的服务流程中。
处理外部数据的输入与输出,包含同步注入数据、异步接口请求、端能力获取数据和状态、存取localStorage等等。
作为一个抽象层屏蔽掉内部实现以及相应的缓存、重试、更新等逻辑。
也可以更好的集中管理外部输入与输出。
我们使用pinia来实现全局状态管理,这是整个应用的核心层。
全局状态管理中的状态,绝大部分都来源于或派生自service层的数据获取,同时需要响应用户操作以及页面生命周期的事件。
store根据职责划分为两种:
domainStore存放的是单纯的数据、App及用户状态,可以直接用来渲染和判断的,比如用户名称、当前积分、列表数据以及用户是否登录、页面是否可见等。
后端维护的领域数据和这里的domainStore应该是对应的 通常这里的数据不会频繁更新,即使有状态更新也是自维护的,不需要响应用户交互事件。
如果有需要,可以通过流式的方式来订阅更新service层的数据。
actionStore存放的通常是纯粹的前端业务逻辑或响应用户的交互状态,最常见的就是弹窗的异步交互队列,任务列表上某个任务产生的弹窗的状态控制。
通常情况下,actionStore可能使用domainStore的响应式数据。
组件根据职责划分为两类:
容器型组件是与store直连的组件,为展示型组件或其它容器组件提供数据和行为,尽量避免在其中做一些界面渲染相关的事情。
需要注意的是,ui状态和容器组件并不需要一一对应,这取决于业务需求,多个容器组件依赖同一份或者同多份ui状态是可能的;某一容器下,ui组件可能是嵌套的组件树,但store中存放的状态可能是扁平的。
展示型组件独立于应用的其它部分内容,不关心数据的加载和变更,保持职责单一,仅做视图呈现和最基本交互行为,通过props接收数据和emits回调函数输出结果,保证接收的数据为组件数据依赖的最小集。
一个有成百上千展示型组件的复杂系统,如果展示型组件粒度切分能很好的遵循高内聚低耦合和职责单一原则的话,可以沉淀出很多可复用的通用业务组件。
上面的分层设计主要针对的是简单场景下的数据获取与状态响应。
复杂的交互、请求场景下,页面存在时间轴(Z轴)维度的状态控制。
Z轴维度下,上面的三层都可能存在异步流:
所以,对于异步控制流的处理并不局限在某一层,可以使用工具库简化这些业务场景。
RxJS 是 Reactive Extensions for JavaScript 的缩写,起源于 Reactive Extensions,是一个基于可观测数据流 Stream 结合观察者模式和迭代器模式的一种异步编程的应用库。RxJS 是 Reactive Extensions 在 JavaScript 上的实现。
强烈推荐阅读rxjs文档和示例,使用rxjs简化复杂异步流的开发,可以让这个维度的需求实现非常简单清晰。
The text was updated successfully, but these errors were encountered:
No branches or pull requests
h5前端架构设计
为什么要做架构设计
架构终极目标,用最小的人力成本来满足构建和维护该系统的需求。架构始终是我们解决复杂度的一个工具,如果当前系统并不复杂,我们不需要为了所谓的优雅去过分改造与优化它,持续将成本置在一个较低水位,就是软件最好的解决办法。
大事件为什么不做严格的架构分层
在大事件活动的开发中,其实是没有严格意义的架构设计的,为什么在长线h5中要做呢?
上图体现了要不要架构设计在时间维度上的开发效率变化。
对于大事件活动来说:
这些属性决定了大事件没有维护的成本,简单业务不抽象复杂的架构设计反而可以带来超高的开发和交互效率,是非常契合业务形态的。
长线活动完全不同
而一个长线迭代的活动,情况就完全不一样了:
破窗效应
生活中,肯定有这样的经历:
1)炎热的夏天,你走在街上,买了一根雪糕。如果找不到垃圾桶,包装纸你会如何处理呢?是否找个有垃圾的角落,悄悄的扔掉?
2)超市买花生,你抓几个尝尝,花生壳一开始你会下意识的拽在手里。如果此时,看到地上有一堆的花生壳,你就会将自己磕的花生壳也扔在地上。
……
于是,同等效应。路上随处可见的垃圾,广告墙上乱七八糟的涂鸦……我们离优雅文明的环境就这样渐行渐远。
环境早就脏了,我扔的这点儿垃圾根本起不到关键性作用”、“反正也不是我先这么做的”,每个人都抱着这样的想法自我暗示。不知不觉,我们就变成让环境恶化的第二双手、第三双手……
这是一种有规律可循的心理现象,在心理学上称为“破窗效应”。指的是一件事情、一个东西一旦稍微被破坏了,人们就会有意无意地任其变得更坏。
随着活动时间的增长,如果没有良好的分层和隔离,那耦合部分会越来越多,越来越重,后续的需求迭代效率也会大大降低,还要花大量的时间回归业务逻辑和寻找bug。
前端架构设计 ≠ 全局状态管理
一定会有同学有这个疑问,是不是引入了全局状态管理库redux/pinia,就认为是已经做好了分层?
答案是否定的,架构分层是一种设计思想,redux/pinia只是某一层的具体实现;分层也不止全局状态这一层,根据业务可以扩展出更多的层,因为这里说的前端架构设计本质上是针对业务需求实现最小人力的开发和维护,是从业务角度出发的。
视图层框架不关心业务架构设计
不管是react还是vue,就像他们文档声明的那样,本质上他们都是视图层的编程框架,只不过大家实现思路有差异,解决的问题其实是一致的。
其核心思想是
UI = f(State)
。视图层只会关心如何处理状态驱动视图的最佳实践,但并不会考虑业务逻辑和业务状态管理。
没有状态管理的时候,业务状态是分散在各个组件内部的,这也意味着业务状态和ui组件产生了严重的耦合;简单业务这确实没什么问题,只要复杂度稍微上升或者有所迭代,频繁的组件改动带来了高成本的维护性,可复用性成为了奢望,多人协作无法并行。
在用增的绝大部分场景下,开发者要解决的并不是
UI = f(State)
,用UI = reactive/observable(State)
描述也许更合适。这里的
reactive/observable
实际上就是ui层的业务逻辑,比如弹窗队列、动画、交互逻辑、异步请求等等。在视图层处理所有的交互业务逻辑和状态,显然是不合理的。ui状态和ui组件解耦
全局状态管理在这时候的优势就体现出来了,ui组件可以只专注于视图层的业务渲染,展现什么和何时展现被提升到全局状态管理中,也就是vuex、pinia、redux这样的状态管理库中。
这样做的好处多多:
听起来确实不错,但实现起来仍然有较多问题,比如
后面的具体实现再来解决这些问题。
ui状态和外部数据的解耦
前端的状态不是无根之水,一般业务场景下,初始状态都是由后端下发,页面生命周期中还会有异步接口请求更新,请求本身可能涉及到重试和缓存,localStorage的存取,端能力获取的某些状态,大量的用户交互带来的状态的改变
这意味着:
听起来就是个MVC?
思想是一致的,但实现上和后端传统的MVC架构区别非常大。
目前用增的场景,领域数据都是从后端获取的,不存在实质意义上的model层,也无需花大力气维护M层。
ui状态层也并不是对等controller层,这一层是存在状态的,而且提供了业务逻辑的实现以及响应用户交互的真正实现,这和传统M层倒是有点类似。
传统M层也就是领域模型并不关心view层,但ui状态层本身就是为了ui展示衍生出来的,这其实反应了前后端真正处理的业务是有极大差异的。
至于view层,MVC中的v层只存在展示功能,一般没有业务上的逻辑,但现代前端的V层显然比传统产品要复杂的多的多。
不但实现了组件状态和dom的自动绑定和更新,用增场景下的动画、ssr hydration这些都主要在v层实现,他们其实已经是业务逻辑的一部分,而且是和view层深度绑定的,无法彻底剥离。
说了这么多,其实传统的后端MVC真正处理的业务集中在M层和C层,现代前端产品的业务重心集中在V层以及衍生的ui状态层,业务重心不一致自然无法直接套用一样的架构设计。
基本原则
分层
业务复杂度隔离,避免耦合、复杂度外溢
单一职责/原子化
限制模块的职责范围,不需要关心上中下游模块的实现
高阶抽象、升维思考
一个业务需求往往不一定是完全扁平化的,很有可能在多个维度中跳跃或并行。跳出当前的维度,或在更高阶进行抽象,反而可以让低维需求保持原子化。
响应式编程
响应式编程是一种面向数据流和变化传播的编程范式。 这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。
vue3框架本身就是一种响应式的数据绑定和视图更新,但我们对响应式编程的使用不止于view层。
约定大于配置,垂直场景强约束
约定即强约束,降低自由度,开发效率变高和关注点集中
配置极简化,可变的范围非常小,需要操心的配置也足够少
持续集成最佳实践+协作规范
比如现在的css开发还处于裸写less的阶段,实际上和UE、UX一起共建原子化样式规范+业务场景样式规范+交互规范之后,可以极大的降低各方的开发成本,也可以更好的约束不合理的行为。
前端资产化
强约束+分层+规范,很容易形成高质量的稳定输出,进一步形成真正的前端资产沉淀。
有了资产沉淀,可以顺势推出垂直向的低代码平台或相关服务,甚至将开发流程直接搬运到低代码平台构建的服务流程中。
具体实现
service层
处理外部数据的输入与输出,包含同步注入数据、异步接口请求、端能力获取数据和状态、存取localStorage等等。
作为一个抽象层屏蔽掉内部实现以及相应的缓存、重试、更新等逻辑。
也可以更好的集中管理外部输入与输出。
store层
我们使用pinia来实现全局状态管理,这是整个应用的核心层。
全局状态管理中的状态,绝大部分都来源于或派生自service层的数据获取,同时需要响应用户操作以及页面生命周期的事件。
store根据职责划分为两种:
domainStore
domainStore存放的是单纯的数据、App及用户状态,可以直接用来渲染和判断的,比如用户名称、当前积分、列表数据以及用户是否登录、页面是否可见等。
后端维护的领域数据和这里的domainStore应该是对应的
通常这里的数据不会频繁更新,即使有状态更新也是自维护的,不需要响应用户交互事件。
如果有需要,可以通过流式的方式来订阅更新service层的数据。
actionStore
actionStore存放的通常是纯粹的前端业务逻辑或响应用户的交互状态,最常见的就是弹窗的异步交互队列,任务列表上某个任务产生的弹窗的状态控制。
通常情况下,actionStore可能使用domainStore的响应式数据。
view 视图层
组件根据职责划分为两类:
container 容器型组件
容器型组件是与store直连的组件,为展示型组件或其它容器组件提供数据和行为,尽量避免在其中做一些界面渲染相关的事情。
需要注意的是,ui状态和容器组件并不需要一一对应,这取决于业务需求,多个容器组件依赖同一份或者同多份ui状态是可能的;某一容器下,ui组件可能是嵌套的组件树,但store中存放的状态可能是扁平的。
presentational 展示型组件
展示型组件独立于应用的其它部分内容,不关心数据的加载和变更,保持职责单一,仅做视图呈现和最基本交互行为,通过props接收数据和emits回调函数输出结果,保证接收的数据为组件数据依赖的最小集。
一个有成百上千展示型组件的复杂系统,如果展示型组件粒度切分能很好的遵循高内聚低耦合和职责单一原则的话,可以沉淀出很多可复用的通用业务组件。
异步控制流
上面的分层设计主要针对的是简单场景下的数据获取与状态响应。
复杂的交互、请求场景下,页面存在时间轴(Z轴)维度的状态控制。
Z轴维度下,上面的三层都可能存在异步流:
所以,对于异步控制流的处理并不局限在某一层,可以使用工具库简化这些业务场景。
RxJS 是 Reactive Extensions for JavaScript 的缩写,起源于 Reactive Extensions,是一个基于可观测数据流 Stream 结合观察者模式和迭代器模式的一种异步编程的应用库。RxJS 是 Reactive Extensions 在 JavaScript 上的实现。
强烈推荐阅读rxjs文档和示例,使用rxjs简化复杂异步流的开发,可以让这个维度的需求实现非常简单清晰。
The text was updated successfully, but these errors were encountered: