一种简单的视图层数据查询模块数据流--视图、路由、API
优化版本链接
https://www.cnblogs.com/cjc-0313/p/16837624.html
背景简介
实现前台项目的搜索页时,通常会将查询参数直接传给路由并显示在顶部的地址。
params 参数可以路径的一部分,因此可以理解;不过保留 query 的目的暂不清楚,对 SSR 模式有用,但 SPA 模式大多还是通过 AJAX 发送请求。一个可能的猜测是页面刷新时,路径不会改变,因此可以保留传递的参数。此外,路由传参也是全局通信方式的一种,可以按需使用。
数据分层
此类场景下,数据通常分为 3 层(实际的变量定义可能不会分得这么清楚):
- 视图:由框架维护的数据,通常用于存储需要显示到视图上的输出类数据或从视图获取的用户输入类数据,任何开发模式下都会有该类数据
- 路由:路由参数,包括 params 和 query
- API:访问 API 时传入的参数,也就是与后端交互所需的数据
一般地,维护视图数据和 API 参数就够用了。
数量流向维护过程
以下过程按照数据的流向并以时间先后分为:
- 初始化阶段:页面第一次加载时,初始化查询参数,由业务组件向服务端发送第一次请求;拿到数据后,渲染到视图中
- 数据交互阶段:基于用户操作,修改本地数据
- 查询阶段:基于修改后的本地数据,在用户只需特定操作时,修改当前路由地址(修改查询参数);同时,修改 API 参数并重新发送请求;之后,重新将服务端响应的数据渲染到视图中
第二、三阶段可能在用户的同一个交互行为下依次触发,比如:用户点击分类选择器组件,修改本地数据后,立刻修改 API 参数并发送请求,并不需要用户点击查询按钮。
根据以上过程,可画出以下的数据流向图:
对于一般的请求,从数据层到 API 层直接走上图中的虚线部分;而当需要维护路由参数时,则走实线部分。
以下是根据上图整理的具体执行过程:
发送请求:
- 视图层到数据层:由于 vue 的双向绑定机制的存在,两层间的部分数据通信会自动执行,如表单输入等;而对于点击、选择等交互行为,可能需要绑定专门的事件处理器。
- 数据层到路由层:从数据层拿到数据,整理成
params
和query
对象,并通过push()
方法推入路由器对象(router
)。 - 路由层到 API 层:通过
watch
侦听路由对象(route
)变化,自动将params
和query
对象整理成 API 所需的参数,并调用相应 API 。
接收响应(与一般的请求处理过程相同):
- API 层到数据层:接收数据后传给本地数据变量。此处不经过路由层。
- 数据层到视图层:双向绑定或单向绑定到视图
代码示例
以下过程实际是通过函数/方法处理的,视图层、数据层和路由层本身只负责存储或展示数据,API 层由组件的函数/方法调用。
示例根据上述数据流向和过程,分为几大部分:
- 在请求的回调中将返回的数据保存为本地数据。这部分比较常规,不过多赘述。
- 通过事件绑定,将用户输入转为本地数据,并调用路由参数处理函数/方法
handleQueryChanged(newQuery)
。同上,不多赘述。 - 根据最新输入,转换为路由对象的
params
和query
属性(可以为空),并通过push()
方法传给router
对象 watch
侦听$route
,自动调用 API 参数处理函数/方法,完成后调用相应的 API
刷新后,由于 watch 的存在,保存 API 参数信息的变量会自动更新,若这些变量被绑定到视图,则视图也会同步更新,也就达到了刷新后维持之前状态的效果。
绑定事件处理器
新增或修改数据
handleDataUpdated(newData) {
// 更新本地参数,并调用路由参数处理方法
this.someData = newData; // 对象或数组在 vue2 中需要通过 this.$set 赋值来保留响应性
handleQueryChanged(newQuery);
// 若将作为 API 参数的变量绑定到视图,则可以直接将新的数据作为 query 的属性
// 后续在 handleQueryChanged 方法内部,调用 API 参数处理方法后,视图也会自动更新
handleQueryChanged({ queryKey: newData });
}
删除数据
handleDataUpdated(newData) {
// 一般类型的参数直接置空
this.data = ''; // 或者 0 undefined 等
// 或者已将作为 API 参数的变量绑定到视图,则可以直接给 query 一个空值属性,跳过本地数据的置空动作
handleQueryChanged({ queryKey: '' });
// 对象或数组通过 this.$set ,将特定位置或属性的值置空
let props = [].concat(this.$route.query.props);
props.splice(attrId, 1);
handleQueryChanged({ props: props.length > 0 ? props : null });
}
值为空的 query 参数不会显示在路径中。当然,也可以选择将需要置空的属性直接删除。
处理路由参数
// 此处仅处理了 query 的变化;params 同理,将特殊处理添加到合并语句之前即可
handleQueryChanged(newQuery) {
let location = { name: 'RouteComponentName' };
// 获取路由更新前的参数副本
let oldParams = this.$route.params,
// query 为空对象表示清空所有 query 参数
if (!isEmpty(newQuery)) { // lodash 的 isEmpty 方法
let oldQuery = Object.assign({}, this.$route.query);
... // 其他特殊处理
// 新的 query 如果提供空的属性值,则将 oldQuery 对象中的该属性置空,如:query = { keyword: '' }
for (const key in newQuery) {
if (Object.hasOwnProperty.call(newQuery, key) && !newQuery[key]) {
oldQuery[key] = undefined;
}
}
... // 其他特殊处理
// 将新 query 对象中新增的属性合并到原 query 对象;同名属性会被覆盖为新值
newQuery = Object.assign(oldQuery, newQuery);
if (!isEmpty(newQuery)) location.query = newQuery;
}
// 重新添加路由参数
if (!isEmpty(oldParams)) location.params = oldParams;
this.$router.push(location);
},
处理 API 参数
setupSearchParams(route) {
const params = route.params,
query = route.query;
if (this.isParamsEmpty(params)) {
if (isEmpty(query)) {
this.searchParams = Object.assign(this.defaultParams);
} else {
for (const key in query) {
// 将 params 中的同名参数重置为初始值
if (
Object.hasOwnProperty.call(this.searchParams, key) &&
!Object.hasOwnProperty.call(query, key)
) {
this.$set(this.searchParams, key, this.defaultParams[key]);
}
}
this.searchParams = Object.assign(
this.searchParams,
{
// 特定参数初始化
},
query
);
}
} else {
if (isEmpty(query)) {
for (const key in params) {
// 将 query 中的同名参数重置为初始值
if (
Object.hasOwnProperty.call(this.searchParams, key) &&
!Object.hasOwnProperty.call(params, key)
) {
this.$set(this.searchParams, key, this.defaultParams[key]);
}
}
this.searchParams = Object.assign(this.searchParams, params);
} else {
this.searchParams = Object.assign(
this.searchParams,
{
// 特定参数初始化
},
params,
query
);
}
}
},
// 有时候 params 对象有属性,但值为空,所以还需要额外判断
isParamsEmpty(params) {
if (isEmpty(params)) return true;
for (const key in params) {
if (Object.hasOwnProperty.call(params, key)) {
if (params[key]) return true;
}
}
return false;
},
侦听路由对象
watch: {
$route: {
immediate: true,
handler(newRoute) {
this.setupSearchParams(newRoute);
this.fetchSearchResult(this.searchParams);
},
},
},