我们是否对现代前端开发框架过于崇拜了?
一、前端开发的困境
二、这是“现代前端开发框架”的锅?
1. 为什么好的东西会变“坏”?
2. 框架的快速发展和开发人员缓慢吸收矛盾
三、破除困境的方法?
1. 好的东西可能会变 “坏”,坏的东西也可能会变 “好”
2. 建立拥有正向反馈能力的运行机制?
怎么建立良好的运行机制我还没有思考出好的办法,有想法的同学可以一起交流。虽然还没有找到建立的方式,但我认为这是一条正确的路,值得去探索。
3. 有效训练,代码是“写”出来的?
再多的信息也不等于知识,经由我们自己的思维转换并以自己的理解方式吸收的才是自己的知识,将知识应用结合到日常工作中的才算是个人的能力
人的思考能力是有限的,而现实现象是一个复杂多维度的系统,远超个人所能掌控的极限。正确的做法应该是由小到大,深入挖掘单个维度的现象,最后通过有序的方式组合起来就是一个复杂多维度的系统。
四、我们需要合适的代码架构方案?
我设计过许多底层框架工具,如果要说其中最困难的地方在哪,那就是框架设计者必须考虑到框架本身和使用的开发者间的对立关系(框架和开发者是一个资源争夺的关系,他们争夺的是软件中的计算量,或者简单的说是代码量)。处理好这种关系,让它达到一个动态平衡的过程是设计者要解决的本质问题。
该方案目前还是理论设计和验证阶段,还没有在真实业务中落地,大家可以带着辩证的眼光来看待。
若认可该方案,可以互相交流;若不喜,请勿喷。
分层 组合 单向依赖
1. 什么原因会导致前端代码变差?
视图是变化频率比较高的,以视图为中心的代码组织方式就如同以视图作为房子的底座去建房,导致的结果就是每次视图变化了你就需要费很大的劲去调整房子的其它部件以适应底座的变化,所谓牵一发而动全身正是如此。
以视图作为代码主体还有一个问题就是视图不能完整的反应业务需求,有部分的业务逻辑是与视图无关的。随着业务逻辑的比重越大就会出现头重脚轻的现象。
数据状态管理工具可以解决业务逻辑变重的问题吗?我认为是否定的,数据状态管理工具并没有脱离视图框架的本质,只是将组件内的局部状态管理模式转换为了全局状态管理的模式。类似于有了一个中心的仓库放置闲置的物品,并不会降低管理物品的难度,关键的地方应该在于一套科学合理的闲置物品分类管理方案,将杂乱无章的巨型仓库变成物美一样的大型超市。
2. 前端的主要关注点是什么?
3. React Hook/Vue Composition API 带来的开发思维转变?
以如下的 swr 示例代码为例:
function useUser(id) {
const { data, error } = useSWR(`/api/user/${id}`, fetcher)
return {
user: data,
isLoading: !error && !data,
isError: error,
}
}
4. 以业务逻辑为核心的架构方案 - (A)
该方案暂以 A 命名,A 方案的适用场景是中型的页面为主,拆分出来的子模型、子逻辑模块在不超过 20 个,看起来不多,其实已经满足了大部分页面的开发需求。
如果与上述场景不匹配在需要基于 A 方案调整,主要在于每一层内部的有序组合方案问题,以及各层间通信方式的优化。 如果发现数据层逻辑层过大可考虑直接上 DDD。面对复杂场景,与没有架构指导相比,采用了非理想的架构方案情况也会更好。
4.1. 基于 MVP 模型的演进架构介绍
各部分之间的通信,都是双向的。
View 与 Model 不发生联系,都通过 Presenter 传递。
View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。
基于 MVP 的分层架构(理解好分层的边界是分层方案是否有效的重要因素)
数据层(Model)- 业务逻辑依赖的数据实体(可参考 DDD 实体的定义)
逻辑层(Presenter)- 处理业务逻辑
视图层(View)- 处理前端的界面交互
每一层基于组合的方式构成完整功能
数据层由多个子模型组合而成
逻辑层由多个子逻辑模块组合而成(由组合成的主逻辑模块与其它层交互)
视图层由多个组件组合而成
单一的依赖关系降低耦合度,流程更清晰
4.2. A 方案的一些弊端分析
其次是过渡抽象,这也是大多数方案存在的,有时候显得明明可以一步到位的事情非要循规蹈矩。
把大象装到冰箱需要几步:
打开冰箱 大象走进冰箱 关闭冰箱 也可以一步到位,大象自己走进冰箱。
五、总结
现代前端开发框架的出现引领了前端界的变革,但我们不能过于“崇拜”它,它主要解决的是复杂交互开发的问题,在用于开发业务逻辑复杂的需求上并不完美,可以将其当做纯视图层的解决方案。 没有绝对完美的技术方案,这里面涉及太多的影响因素,开发者是其中的一个大的 “变量”,具备良好架构思维的开发者编写出的代码也许就是好的代码,本身就是一种技术架构方案的反馈。而技术方案的设计产出则是追求的一种普适、易用的方案。 设计普适的通用技术方案要由小到大,再由大到小;追求解决核心问题而不是全量的问题,平衡好和开发者间的对立关系;可接受范围内的负面成本并不影响整个方案的决策。
六、示例
1. 常规写法伪代码:
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. A 架构拆分伪代码
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.js
export default class PortalPresenter {
getPageData (obj) {
// 获取地理位置信息是 PortalLocationPresenter 做的,而拿到地理信息后
// 该怎么去用则是另一个模块的事情
this.$PortalLocationPresenter.onLocation({
success: () => {
this.$PortalGoodsPresenter.fetchData(obj)
}
})
}
}
// PortalLocationPresenter.js
export 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('请开启定位');
}
}
}
});
}
}
微信扫码关注该文公众号作者