我们是否对现代前端开发框架过于崇拜了?
一、前端开发的困境
从我的经历来看,现代前端框架的‘崇拜’导致前端开发变得更加复杂,间接导致代码质量变差,软件的生命周期变短。 当前我们所面临的困境是技术方案有的时候用的特别顺手、高效,而有的时候却很蹩脚、处处碰壁?有的人用着轻松欢快,而有的人用着烦躁不已?有的项目引入了新工具开发效率一下子就上去了,有的项目反而变得越来越复杂难以迭代?二、这是“现代前端开发框架”的锅?
前文我表达了“ 从我的经历来看,现代前端框架的‘崇拜’导致前端开发变得更加复杂,间接导致代码质量变差,软件的生命周期变短。 ” 这里并不是说是现代前端框架造成了这一系列的问题,事实上我非常认可前端框架给前端开发带来的变革,它解决了我们开发界面过程中繁琐的 DOM 操作问题;带来了便捷的组件式的代码复用共享机制;这是一种正向的技术进步。问题的本质是社区带来的 “崇拜” 文化,这是运行机制带来的问题,前端框架只是其中某个重要的节点,我们不能因为工具没有使用好就怪工具本身。三、破除困境的方法?
怎么建立良好的运行机制我还没有思考出好的办法,有想法的同学可以一起交流。虽然还没有找到建立的方式,但我认为这是一条正确的路,值得去探索。
-
再多的信息也不等于知识,经由我们自己的思维转换并以自己的理解方式吸收的才是自己的知识,将知识应用结合到日常工作中的才算是个人的能力
-
人的思考能力是有限的,而现实现象是一个复杂多维度的系统,远超个人所能掌控的极限。正确的做法应该是由小到大,深入挖掘单个维度的现象,最后通过有序的方式组合起来就是一个复杂多维度的系统。
四、我们需要合适的代码架构方案?
我设计过许多底层框架工具,如果要说其中最困难的地方在哪,那就是框架设计者必须考虑到框架本身和使用的开发者间的对立关系(框架和开发者是一个资源争夺的关系,他们争夺的是软件中的计算量,或者简单的说是代码量)。 处理好这种关系,让它达到一个动态平衡的过程是设计者要解决的本质问题。
该方 案目前还是理论设计和验证阶段,还没有在真实业务中落地,大家可以带着辩证的眼光来看待。
若认可该方案,可以互相交流; 若不喜,请勿喷。
-
分层 -
组合 -
单向依赖
- 视图是变化频率比较高的,以视图为中心的代码组织方式就如同以视图作为房子的底座去建房,导致的结果就是每次视图变化了你就需要费很大的劲去调整房子的其它部件以适应底座的变化,所谓牵一发而动全身正是如此。
-
以视图作为代码主体还有一个问题就是视图不能完整的反应业务需求,有部分的业务逻辑是与视图无关的。 随着业务逻辑的比重越大就会出现头重脚轻的现象。
-
数据状态管理工具可以解决业务逻辑变重的问题吗? 我认为是否定的,数据状态管理工具并没有脱离视图框架的本质 ,只是将组件内的局部状态管理模式转换为了全局状态管理的模式。 类似于有了一个中心的仓库放置闲置的物品,并不会降低管理物品的难度,关键的地方应该在于一套科学合理的闲置物品分类管理方案,将杂乱无章的巨型仓库变成物美一样的大型超市。
以如下的 swr 示例代码为例:
function useUser(id) {const { data, error } = useSWR(`/api/user/${id}`, fetcher)return {user: data,isLoading: !error && !data,isError: error,}}
如果与上述场景不匹配在需 要基于 A 方案调整,主要在于每一层内部的有序组合方案问题,以及各层间通信方式的优化。
如果发现数据层逻辑层过大可考虑直接上 DDD。 面对复杂场景,与没有架构指导相比,采用了非理想的架构方案情况也会更好。
4.1. 基于 MVP 模型的演进架构介绍
-
各部分之间的通信,都是双向的。
-
View 与 Model 不发生联系,都通过 Presenter 传递。
-
View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。
-
基于 MVP 的分层架构(理解好分层的边界是分层方案是否有效的重要因素)
-
数据层(Model)- 业务逻辑依赖的数据实体(可参考 DDD 实体的定义)
-
逻辑层(Presenter)- 处理业务逻辑
-
视图层(View)- 处理前端的界面交互
-
每一层基于组合的方式构成完整功能
-
数据层由多个子模型组合而成
-
逻辑层由多个子逻辑模块组合而成(由组合成的主逻辑模块与其它层交互)
-
视图层由多个组件组合而成
-
单一的依赖关系降低耦合度,流程更清晰
4.2. A 方案的一些弊端分析
首先是分层架构带来的判断力需求,真正实践的时候就会发现边界变得模糊,不知道代码该写在哪一层,这是极有可能发生的。对于这种情况,我认为可以按照先求 “完成”,再求“完美” 的方式来走,用实践来积累正确合理的适合自己团队的方式。 其次是过渡抽象,这也是大多数方案存在的,有时候显得明明可以一步到位的事情非要循规蹈矩。把大象装到冰箱需要几步:也可以一步到位,大象自己走进冰箱。
- 打开冰箱
- 大象走进冰箱
- 关闭冰箱
五、总结
- 现代前端开发框架的出现引领了前端界的变革,但我们不能过于“崇拜”它,它主要解决的是复杂交互开发的问题,在用于开发业务逻辑复杂的需求上并不完美,可以将其当做纯视图层的解决方案。
- 没有绝对完美的技术方案,这里面涉及太多的影响因素,开发者是其中的一个大的 “变量”,具备良好架构思维的开发者编写出的代码也许就是好的代码,本身就是一种技术架构方案的反馈。而技术方案的设计产出则是追求的一种普适、易用的方案。
- 设计普适的通用技术方案要由小到大,再由大到小;追求解决核心问题而不是全量的问题,平衡好和开发者间的对立关系;可接受范围内的负面成本并不影响整个方案的决策。
六、示例
下面是一个偏真实的代码架构拆分示例:
export default class PortalPage extends Component {registerModels () {return [{namespace: 'PortalGoodModel',state: {pageNum: 1,pageSize: 10,hasNextPage: true,goodsData: []},reducers: {updateGoodsData (state, payload) {// 更新数据操作}}}]}state (state) {return {...state,// 是否有数据,纯 UI 内部状态noResultPageVisible: state.PortalGoodModel.goodsData.length}}ready () {this.getPageData();}async getInfo () {const locationInfo = await this.dispatch({type: 'PortalLocationModel/updateInfo'})return locationInfo;}async getPageData (type) {const { adCode, userAdCode, longitude, gpsValid } = await this.getInfo();if (!(adCode && userAdCode && longitude) || !gpsValid) {// 定位失败if (type === 'onErrorRetry') {utils.toast('请开启定位');this.dispatch({type: "PortalLocationModel/updateLocationStatus",value: 'error'})}} else {this.dispatch({type: "PortalLocationModel/updateLocationStatus",value: 'success'});const requestParams = utils.getRequestParams({pageData: this.$page.props,location,});const res = await fetch(requestParams);if (res.code !== 1) {// 请求错误处理} else {const result = this.processResult(res);this.dispatch({type: 'PortalGoodModel/updateGoodsData',...result});}}}}
2.1 模型层示例
构建如下的商品模型,模型层与逻辑层的本质区别就是模型层是更原子的数据操作,可类比后端的数据库,或者领域模型中的实体抽象定义。 逻辑层会依赖模型层已构成完整的业务逻辑,而模型层是独立的,不会依赖逻辑层的任何东西 。// 这里使用 dva 做为建模工具,你可以用其它的状态工具或者纯 js 对象都可以export default {namespace: 'PortalGoodModel',state: {pageNum: 1,pageSize: 10,hasNextPage: true,goodsData: []},reducers: {updateGoodsData (state, payload) {// 更新数据操作}}}
2.2 逻辑层示例
逻辑层与视图层的边界判断标准是该状态是否在业务逻辑中需要,若需要则应该在模型层建模,在逻辑层编写依赖逻辑,视图层转换为渲染需要的 UI State-
提供 getPageData 接口给视图层使用
-
拆分出获取地理位置和获取商品数据的子逻辑模块
-
子逻辑模块之间、逻辑层与视图层之间以接口的形式通信
// 该 presenter 会与视图关联,提供给视图使用的接口// PortalPresenter.jsexport default class PortalPresenter {getPageData (obj) {// 获取地理位置信息是 PortalLocationPresenter 做的,而拿到地理信息后// 该怎么去用则是另一个模块的事情this.$PortalLocationPresenter.onLocation({success: () => {this.$PortalGoodsPresenter.fetchData(obj)}})}}
// PortalLocationPresenter.jsexport default class PortalLocationPresenter {async getInfo () {const locationInfo = await this.dispatch({type: 'PortalLocationModel/updateInfo'})return locationInfo;}async onLocation (obj) {const { adCode, userAdCode, longitude, gpsValid } = await this.getInfo();// 定位失败if (!(adCode && userAdCode && longitude) || !gpsValid) {this.dispatch({type: "PortalLocationModel/updateLocationStatus",value: 'error'})obj?.fail();} else {this.dispatch({type: "PortalLocationModel/updateLocationStatus",value: 'success'})obj?.success();}}}
export default class PortalGoodsPresenter {name = 'PortalGoodsPresenter'async fetchData (obj) {// 请求参数处理const requestParams = utils.getRequestParams({pageData: this.$page.props,location,});const res = await fetch(requestParams);if (res.code !== 1) {obj?.fail()} else {const result = this.processResult(res);this.dispatch({type: 'PortalGoodModel/updateGoodsData',...result});obj?.success();}}}
-
做纯 UI 的渲染操作,回归 ui = fn(state) 的本质
-
视图层使用逻辑层提供的获取数据接口去拿到数据
-
获取异步数据过程中涉及的 UI 操作依然在视图层处理
-
如定位失败的 toast 提示 -
如是否显示空数据的 UI (该 UI 逻辑层永远也用不到)
{state (state) {return {...state,// 是否有数据,纯 UI 内部状态noResultPageVisible: state.PortalGoodModel.goodsData.length}}ready () {this.getPageData();},getPageData (info = {}) {this.$presenter.getPageData({type: info.type, // 获取数据类型fail (errorInfo) {if (errorInfo.type === 'locationError') {// from 代表调用类型if (info.from === 'onErrorRetry') {utils.toast('请开启定位');}}}});}}