https://github.com/xiaobebe/Vmo 是一个用于前端的数据模型. 解决前端接口访问混乱, 服务端数据请求方式不统一, 数据返回结果不一致的微型框架.
https://github.com/xiaobebe/Vmo 主要用于处理数据请求, 数据模型管理. 可配合当前主流前端框架进行数据模型管理 vue,React,Angular.
能够有效处理以下问题:
接口请求混乱, axios.get... 随处可见.
数据管理混乱, 请求到的数据结果用完即丢, 拿到的数据直接放进 Store.
数据可靠性弱, 不能保证请求数据是否稳定, 字段是否多, 是否少.
Action 方法混乱, Action 中及存在同步对 Store 的修改, 又存在异步请求修改 Store.
代码提示弱, 请求到的数据无法使用 TypeScript 进行代码提示, 只能定义 any 类型.
无效字段增多, 人员变动, 字段含义信息逐步丢失, 新业务定义新字段.
项目迁移繁重, 项目重构时, 对字段不理解, 重构过程功能点, 数据丢失.
背景介绍
随着现有大前端的蓬勃发展, Vue,React 等框架不断流行, RN,Weex,Electron 等使用 JS 开发客户端应用的不断发展, Taro,mpVue,CML 等新型小程序框架的不断创新. JavaScript 将变得更加流行与多样, 使用 JS 同构各端项目将不再是梦.
JS 的灵活在赋予大家方便的同时也同样存在着一些问题, 同样实现一个数据获取到页面渲染的简单操作, 可能就会有非常多的写法. 正常的, 在 Vue 中, 可能会直接这样写:
- const methods = {
- /**
- * 获得分类信息
- */
- async getBarData() {
- try {
- const { data } = await axios.get(url, params);
- return data;
- } catch (e) {
- console.error("something error", e);
- }
- }
- };
这样的做法在功能上讲没什么问题, 但在新增一些其他动作后, 这样的做法就变得非常难以管理.
比如, 需要在请求中加入一些关联请求, 需要获取一个商品页的列表, 查询参数包含, 分页参数 (当前页, 查询数), 分类 Id, 搜索内容, 排序方式, 筛选项.
在执行该请求时, 发现分类 Id 也需要另外一个接口去获取. 于是代码成了:
- const params = {
- sort: -1,
- search: "",
- filter: "",
- page: {
- start: 1,
- number: 10
- }
- };
- const methods = {
- /**
- * 获得商品列表
- */
- async getGoodsData() {
- try {
- const { data } = await axios.get(url.goodsType); // 获取所有分类 Id
- const { id: typeId } = data;
- const res = await axios.get(url.goods, { ...params, typeId }); // 获取商品
- return res.data;
- } catch (e) {
- console.error("something error", e);
- }
- }
- };
这样看上去貌似是完成了这个业务, 但其实在业务不断变化的环境下, 这样直接在组件中书写接口请求是非常脆弱的.
比如以下问题:
返回结果中, 有字段需要单独处理后才能使用. 比如: 后端可能返回的一个数组是, 隔开
返回结果中, 有字段在某种情况下缺失
接口地址发生变动
随着业务变动, 接口字段需要改动
其他组件需要使用同样这份数据, 但不能保证组件调用顺序
部分接口数据需要前端缓存
接口存储方式发生变化. 比如: 有网络走接口, 没网络走 LocalStorage
前端项目框架迁移, 接口不变. Vue 转 React?Vue 转小程序?
为了让读者更容易理解我所说的痛点, 我写了几个反例场景来说明:
反例场景 1
- const methods = {
- /**
- * 获取过滤项信息
- */
- async getFilterInfo() {
- try {
- const { data: filterInfo } = await axios.get(url.goodsType); // 获取所有分类 Id
- // filterInfo.ids => "2,3,5234,342,412"
- filterInfo.ids = filterInfo.ids.map(id => id.split(","));
- return filterInfo;
- } catch (e) {
- console.error("something error", e);
- }
- }
- };
在这个例子中, 获取过滤项信息中返回的结果信息假设为:
- {
- "ids": "2,3,5234,342,412",
- ...
- }
在数据解析中, 就需要处理为前端接受的数组, 类似的解析还有非常多.
也许现在看这段代码无关痛痒, 但若每次调用这个接口都需要这样处理, 长期处理类似字段. 甚至有很多开发者在一开始拿到这个字段都会暂时不去处理, 到用到的地方再处理, 每用一次处理一次.
那想想该是多么非常恶心的一件事情.
如果使用 Vmo 会在数据模型开始时, 就使用 load() 来对数据做适配, 拿到的数据能够稳定保证是我们所定义的那种类型.
反例场景 2
- // component1
- // 需要使用 Goods 数据
- const mounted = async () => {
- const goods = await this.getGoodsData();
- this.$store.commit("saveGoods", goods); // 在 store 中存储
- this.goods = goods;
- };
- const methods = {
- /**
- * 获得商品列表
- */
- async getGoodsData() {
- try {
- const { data } = await axios.get(url.goodsType); // 获取所有分类 Id
- const { id: typeId } = data;
- const res = await axios.get(url.goods, { ...params, typeId }); // 获取商品
- return res.data;
- } catch (e) {
- console.error("something error", e);
- }
- }
- };
- // component2
- // 也需要使用 Goods 数据
- const mounted = async () => {
- const goods = this.$store.state.goods;
- this.goods = goods;
- };
在这个例子中, 简单描述了两个组件代码 (也许看上去很 low, 但这种代码缺失存在), 他们都会需要使用到商品数据. 按照正常流程组件组件的加载流程可能是
component1->component2
这样的顺序加载, 那么上面这段是可以正常运行的. 但假若业务要求, 突然有一个 component3 要在两个组件之前加载, 并且也需要使用商品数据, 那么对于组件的改动是非常头疼的 (因为实际业务中, 可能你的数据加载要比这里复杂的多).
反例场景 3
小明是一位前端开发人员, 他与后端人员愉快的配合 3 个月完成了一款完整的 H5 SPA 应用.
业务发展的很快, 又经过数十次迭代, 他们的日活量很快达到了 5000, 但存在 H5 的普遍痛点, 用户留存率不高.
于是产品决定使用小程序重构当前项目, UI, 后端接口不用改变.
小明排期却说要同样 3 个月, 对此产品非常不理解, 认为当初从无到有才用了 3 个月, 现在简单迁移为什么也需要这么久.
小明认为, 虽然接口, UI 不变. 但小程序与 H5 之间存在语法差异, 为了考虑后续 H5, 小程序多端迭代保持统一, 需要花时间在技术建设上, 抽离出公共部分, 以减轻后续维护成本.
产品非常不理解问开发, 如果不抽离会怎么样, 能快点吗? 就简单的复制过来呢? 于是小明为难之下, 非常不满的说那可能 2 周.
Deal! 就这么办.
2 周开发, 1 周测试, 成功上线!
第 4 周, 随着需求迭代, 后端修改了一个接口的返回内容, 前后端联动上线后发现之前的 H5 页面出现大面积白屏.
事后定位发现, 由于后端修改导致 H5 数据解析出现 JS 异常. 项目组一致认为是由于前段人员考虑不够全面造成的本次事故, 应该由小明承担责任.
5 个月后, 小明离职...
闪亮登场
基础原型
先来看一段 Vmo 的代码:
- import { Vmo, Field } from "@vmojs/base";
- interface IFilterValue {
- name: string;
- value: string;
- }
- export default class FilterModel extends Vmo {
- @Field
- public key: string;
- @Field
- public name: string;
- @Field
- public filters: IFilterValue[];
- public get firstFilter(): IFilterValue {
- return this.filters[0];
- }
- /**
- * 将数据适配 \ 转换为模型字段
- * @param data
- */
- protected load(data: any): this {
- data.filters = data.values;
- return super.load(data);
- }
- }
- const data = {
- key: "styles",
- name: "风格",
- values: [
- { name: "现代简约", value: "1" },
- { name: "中式现代", value: "3" },
- { name: "欧式豪华", value: "4" }
- ]
- };
- const filterModel = new FilterModel(data); // Vmo 通过 load 方法对数据做适配
通过以上方式就成功的将一组 JSON 数据实例化为一个 FilterModel 的数据模型. 这将会为你带来什么好处呢?
适配来源数据, 处理需要改变的字段类型, 如 string => array
可靠的字段定义, 即使接口字段变动, 数据模型字段也不会变
TypeScript 书写提示, 一路回车不用说了, 爽
计算属性, 如 firstFilter
一次定义, 终生受益. 不认识 \ 未使用的字段 say GoodBye
如果项目需要迁移, 后端同构, 拿来即用.
派生能力
在 Vmo 的设计中, 数据模型只是基类, 你同样可以为数据模型赋予一些 "特殊能力" , 比如数据获取.
AxiosVmo 是基于 Vmo 派生的一个使用 axios 作为 Driver(驱动器) 实现数据获取, 存储能力的简单子类.
你同样可以封装自己的 Driver , 通过相同接口, 实现多态方法, 来做到在不同介质上存储和获取数据. 比如 IndexDB,LocalStorage.
- import { AxiosVmo } from "@vmojs/axios";
- import { Field, mapValue } from "@vmojs/base";
- import { USER_URL } from "../constants/Urls";
- import FilterModel from "./FilterModel";
- // 商品查询参数
- interface IGoodsQuery {
- id: number;
- search?: string;
- filter?: any;
- }
- interface IGoodsCollection {
- goods: GoodsModel[];
- goodsRows: number;
- filters: FilterModel[];
- }
- export default class GoodsModel extends AxiosVmo {
- protected static requestUrl: string = USER_URL;
- @Field
- public id: number;
- @Field
- public catId: number;
- @Field
- public aliasName: string;
- @Field
- public uid: number;
- @Field
- public userId: number;
- @Field
- public size: { x: number; y: number };
- /**
- * 返回 GoodsModel 集合
- * @param query
- */
- public static async list(query: IGoodsQuery): Promise<GoodsModel[]> {
- const { items } = await this.fetch(query);
- return mapValue(items, GoodsModel);
- }
- /**
- * 返回 GoodsModel 集合 及附属信息
- * @param query
- */
- public static async listWithDetail(
- query: IGoodsQuery
- ): Promise<IGoodsCollection> {
- const { items, allRows, aggr } = await this.fetch(query);
- const goods = mapValue(items, GoodsModel);
- const filters = mapValue(aggr, FilterModel);
- return { goods, goodsRows: allRows, filters };
- }
- public static async fetch(query: IGoodsQuery): Promise<any> {
- const result = await this.driver.get(this.requestUrl, query);
- return result;
- }
- /**
- * 将请求的数据适配转换为 Model
- * @param data
- */
- protected load(data: any): this {
- data.catId = data.cat_id;
- data.aliasName = data.aliasname;
- data.userId = data.user_id;
- return super.load(data);
- }
- }
- (async () => {
- // 通过静态方法创建 GoodsModel 集合
- const goods = await GoodsModel.listWithDetail({ id: 1 });
- })();
像上面这样的一个 GoodsModel 中, 即定义了数据模型, 又定义了接口地址, 请求方式与适配方法. 在返回结果中会创建出 GoodsModel 的数据模型集合.
Action 与 Store
与以往前端思维不同, 我大费周章的折腾这么一套出来. 到底与原来一些常用框架思维中的 action 完成一切到底有什么不同呢?
请大家思考一个问题, action 的定义到底是什么呢?
最初 Flex 设计中, action 的设计就是为了改变 Store 中的 state, 来达到状态可控, 流向明确的目的.
Redux 中的 action 甚至都是不支持异步操作的, 后来有一些变相的方式实现异步 action, 后来又有了 Redux-thunk,Redux-saga 这类异步中间件实现.
所以, 最开始 action 的设计初衷是为了管理 Store 中状态, 后来因为需要, 开发者们赋予了 action 异步调用接口并改变 Store 状态的能力.
所以很多项目中, 看到 action 经常会类似这样的方法, getUsers() 调用接口获取用户数据, addUser() 添加用户, removeUser() 删除用户.
那么哪个方法会有异步请求呢? 哪个方法是直接操作 Store 而不会发生接口请求呢?
Vmo 希望能够提供一种设计思路, 将数据模型, 异步获取与页面状态 分开管理维护.
将数据获取, 适配处理, 关联处理等复杂的数据操作, 交给 Vmo.
将 Vmo 处理后的数据模型, 交给 Store. 作为最终的页面状态.
Mobx
Vmo 还可以配合 Mobx 使用, 完成数据模型与数据响应结合使用.
- import { Vmo, Field } from "@vmojs/base";
- import { observable } from "mobx";
- interface IFilterValue {
- name: string;
- value: string;
- }
- export default class FilterModel extends Vmo {
- @Field
- @observable
- public key: string;
- @Field
- @observable
- public name: string;
- @Field
- @observable
- public filters: IFilterValue[];
- /**
- * 将数据适配 \ 转换为模型字段
- * @param data
- */
- protected load(data: any): this {
- data.filters = data.values;
- return super.load(data);
- }
- }
总结
Vmo 强调的是一种设计
Vmo 是我的第一个个人开源项目, 凝聚了我对目前大前端数据处理的思考沉淀, 源码实现并不复杂, 主要是想提供一种设计思路.
GitHub 中有完整的 Example, 感兴趣的读者可以移步至项目地址查看.
项目地址 https://github.com/xiaobebe/Vmo
让各位观众老爷见笑了, 欢迎指点讨论~
个人邮箱: wyy.xb@qq.com
来源: https://juejin.im/post/5c793a10e51d4506ce5b0918