Vmo is a data model for the front end. Solve the front-end interface access chaos, server data request method is not unified, data return results are inconsistent micro framework.

Vmo is mainly used for processing data requests, data model management. Vue,React,Angular can be used for data model management with current mainstream front-end frameworks.

Be able to effectively deal with the following issues:

  • Interface request chaos,axios.get...You can see it everywhere.
  • Data management chaos, request to the data results used up immediately throw, get the data directly into theStore.
  • The data reliability is weak, which cannot ensure whether the request data is stable or whether the fields are too many or too few.
  • ActionMethod confusion,ActionThere are synchronization pairs in andStore, and there are asynchronous request modificationStore.
  • Weak code prompt, requested data cannot be usedTypeScriptCode hints can only be definedanyType.
  • Invalid fields increase, personnel change, field meaning information is gradually lost, and new business defines new fields.
  • The project migration is heavy, and the fields are not understood during the project reconstruction, resulting in the loss of function points and data in the reconstruction process.

background

With the booming development of the existing large front-end, frameworks such as Vue and React are becoming increasingly popular, client applications such as RN, Weex and Electron are developing using JS, and new small program frameworks such as Taro, mpVue and CML are constantly innovated. JavaScript will become more popular and diverse, and using JS isomorphic end-projects will no longer be a dream.

JS flexibility in giving you convenient at the same time there are also some problems, the same implementation of a simple operation to obtain data page rendering, there may be a lot of writing. Normally, in Vue, you might write something like this:

const methods = {
  /** * get the classification information */
  async getBarData() {
    try {
      const { data } = await axios.get(url, params);

      return data;
    } catch (e) {
      console.error("something error", e); }}};Copy the code

This is functionally fine, but it becomes very difficult to manage after adding a few other actions.

For example, some associated requests need to be added to the request, and a list of commodity pages need to be obtained. Query parameters include paging parameters (current page, number of queries), category Id, search content, sorting method, and filter items.

When this request is executed, it is found that the category Id also needs to be obtained by another interface. So the code becomes:

const params = {
  sort: -1.search: "".filter: "".page: {
    start: 1.number: 10}};const methods = {
  /** * get the list of items */
  async getGoodsData() {
    try {
      const { data } = await axios.get(url.goodsType); // Get all category ids
      const { id: typeId } = data;
      const res = awaitaxios.get(url.goods, { ... params, typeId });// Get the goods

      return res.data;
    } catch (e) {
      console.error("something error", e); }}};Copy the code

This may appear to complete the business, but in the context of changing business, writing interface requests directly into components like this is very vulnerable.

For example:

  • In the returned result, some fields need to be processed separately before they can be used. For example, an array that the back end might return is.separated
  • There are fields in the returned result that are missing in some cases
  • The interface address changed. Procedure
  • Interface fields need to be changed as services change
  • Other components need to use the same data, but the order in which components are called is not guaranteed
  • Some interface data needs front-end caching
  • The storage mode of the interface is changed. Procedure For example, if there is a network interface, go to LocalStorage if there is no network interface
  • The front-end project framework is migrated and the interface remains unchanged. Vue React? Vue to applets?

To make it easier for readers to understand the pain points I’m talking about, HERE are a few counterexample scenarios to illustrate:

Counterexample Scenario 1

const methods = {
  /** * Obtain information about filter items */
  async getFilterInfo() {
    try {
      const { data: filterInfo } = await axios.get(url.goodsType); // Get all category ids
      2,3,5234,342,412. / / filterInfo ids = > ""
      filterInfo.ids = filterInfo.ids.map(id= > id.split(","));

      return filterInfo;
    } catch (e) {
      console.error("something error", e); }}};Copy the code

In this example, the result information returned from the filter information is assumed to be:

{
  "ids": "2,3,5234,342,412". }Copy the code

In data parsing, you need to deal with arrays that are accepted by the front end, and there are many similar parses.

It may not be interesting to look at this code right now, but if you need to do this every time you call this interface, you’re dealing with similar fields over time. There are even a lot of developers who, when they first get a field, just leave it alone and work on it when it’s needed, every time it’s used.

That would be a very disgusting thing to think about.

If we use Vmo, we will use load() to fit the data at the beginning of the data model, so that the data we get is stable and of the type we define.

Counterexample Scenario 2

// component1
// Goods data is required

const mounted = async() = > {const goods = await this.getGoodsData();
  this.$store.commit("saveGoods", goods); // Store it in store

  this.goods = goods;
};

const methods = {
  /** * get the list of items */
  async getGoodsData() {
    try {
      const { data } = await axios.get(url.goodsType); // Get all category ids
      const { id: typeId } = data;
      const res = awaitaxios.get(url.goods, { ... params, typeId });// Get the goods

      return res.data;
    } catch (e) {
      console.error("something error", e); }}};Copy the code
// component2
// Goods data is also required

const mounted = async() = > {const goods = this.$store.state.goods;

  this.goods = goods;
};
Copy the code

In this example, we briefly describe two component code (which may seem low, but it does exist) that both need to use commodity data. The loading process of a component may be

component1->component2

This sequence of loading, then the above section will work. But if the business requires that suddenly a Component3 be loaded before two components, and also need to use commodity data, then the component changes can be very troublesome (because in real business, your data loading may be much more complicated than here).

Counterexample Scenario 3

Xiao Ming is a front-end developer. He completed a complete H5 SPA application in 3 months with the happy cooperation of back-end staff.

The business grew quickly, and after dozens of iterations, they soon hit 5,000 daily live, but had the common pain points of H5 and low user retention.

The product then decides to use applets to refactor the current project, leaving the UI and back-end interface unchanged.

Xiao Ming said it would take the same 3 months, but he did not understand the product very much. He thought that it had only taken 3 months from scratch, so why did it take so long to simply migrate now?

Xiaoming thinks that although the interface and UI remain unchanged. However, there are grammatical differences between small programs and H5. In order to consider the unification of subsequent H5 and small program multi-terminal iterations, time should be spent on technical construction and the common part should be removed to reduce subsequent maintenance costs.

The product does not understand the development very much. What will happen if we do not pull out? Can you hurry up? Why don’t you just copy it? So Xiao Ming is embarrassed, very dissatisfied that it may be 2 weeks.

Deal! Let’s do that.

2 weeks of development, 1 week of testing, successful launch!

In the fourth week, with demand iteration, the back end modified the returned content of an interface, and found a large area of white screen on the previous H5 page after the linkage of the front and back ends went online.

After locating, it was found that JS anomalies occurred in H5 data parsing due to back-end modification. The project team agreed that the accident was caused by the lack of comprehensive consideration of the previous section of the staff, and Xiao Ming should assume the responsibility.

Five months later, Xiao Ming left…

Counterexample Scenario 4

In a business scenario, suppose an interface returns the following Json:

{
  "c": "0"."m": ""."d": {
    "bannerList": [{"bannerId": "..."."bannerImg": "..."."bannerUrl": "..."."backendColor": null}]."itemList": [{"obsSkuId": "..."."obsItemId": "..."."categoryId": null."itemName": "..."."mainPic": "..."."imgUrlList": null."suggestedPriceInCent": null."priceInCent": null."obsBrandId": "..."."width": null."height": null."length": null."bcsPattern": null."commissionPercent": null."buyLink": "..."."phoneBuyLink": false."storeIdList": null."storeNameList": null."storeNumber": null."cityIdList": null."provinceIdList": null."obsModelId": null."desc": null."shelfImmediately": null."status": 1."brandName": "..."."modelPreviewImg": null."similarModelIdList": null."similarModelImgList": null."relatedModelId": null."relatedModelImg": null."brandAddress": null."promotionActivityVO": null."tagIds": null."tagGroups": []."favored": false}]."newsList": [{"id": "..."."img": "..."."title": "..."."desc": "..."."date": null."order": null}]."activityList": []."itemListOrder": 1."activityOrder": 4."lessonOrder": 3."newsOrder": 1."designerOrder": 2."comboListOrder": 2}}Copy the code

You can see that there are a lot of fields in there, although some companies try to define fields using interface management systems like Yapi.

But with business development, rapid iteration of version, personnel changes and other factors, it is very likely that one day

I ask the front end and the front end says it’s coming from the back end and that’s it, I don’t know.

Ask the back end, the back end says this is what the front end wants, I don’t know.

The field above is one that no one in the company can fully describe.

At this time, if the service of the interface changes, the field adjustment is needed. In order to avoid unknown interface accidents, it is likely to propose a new interface field to realize functions without changing the previous interface content.

In the long run, the interface returned more and more, until the project team spent a great effort to rewrite the interface, the front-end rewrite interface docking.

The debut

Based on the prototype

Let’s take a look at the Vmo code:

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];
  }

  /** * ADAPTS data to model field *@param data* /
  protected load(data: any) :this {
    data.filters = data.values;
    return super.load(data); }}const data = {
  key: "styles".name: "Style".values: [{name: "Modern simplicity".value: "1" },
    { name: "Modern Chinese style".value: "3" },
    { name: "European luxury".value: "4"}};const filterModel = new FilterModel(data); // Vmo ADAPTS data using the load method
Copy the code

In this way, we successfully instantiate a set of JSON data into a FilterModel data model. What’s in it for you?

  • ADAPTS source data to handle field types that need to be changed, such asstring => array
  • Reliable field definitions that do not change data model fields even if interface fields change
  • TypeScriptWriting prompts, driving back to the car no need to say, cool
  • Compute properties, such asfirstFilter
  • Define once, benefit for life. Unrecognized \ unused field say GoodBye
  • If the project needs to migrate, the backend isomorphism, use it.

The derived capacity

In the Vmo design, the data model is just the base class, and you can also give the data model “special capabilities”, such as data retrieval.

AxiosVmo is a simple vMO-based subclass that uses AXIOS as a Driver for data retrieval and storage capabilities.

You can also encapsulate your own Driver and implement polymorphic methods for storing and retrieving data on different media through the same interface. Such as IndexDB, LocalStorage.

import { AxiosVmo } from "@vmojs/axios";
import { Field, mapValue } from "@vmojs/base";
import { USER_URL } from ".. /constants/Urls";
import FilterModel from "./FilterModel";

// Commodity query parameters
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 };

  /** * returns the GoodsModel collection *@param query* /
  public static async list(query: IGoodsQuery): Promise<GoodsModel[]> {
    const { items } = await this.fetch(query);
    return items.map(item= > new GoodsModel(item));
  }

  /** * returns the GoodsModel collection and its dependencies *@param query* /
  public static async listWithDetail(
    query: IGoodsQuery
  ): Promise<IGoodsCollection> {
    const { items, allRows, aggr } = await this.fetch(query);
    const goods = items.map(item= > new GoodsModel(item));
    const filters = aggr.map(item= > new FilterModel(item));
    return { goods, goodsRows: allRows, filters };
  }

  public static async fetch(query: IGoodsQuery): Promise<any> {
    const result = await this.driver.get(this.requestUrl, query);
    return result;
  }

  /** * ADAPTS the requested data to 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() = > {// Create the GoodsModel collection statically
  const goods = await GoodsModel.listWithDetail({ id: 1}); }) ();Copy the code

In a GoodsModel like the one above, the data model is defined, as well as the interface address, request mode, and adaptation method. A collection of data models for goodsModels is created in the return result.

Final print:

The Action and Store

Unlike previous front-end thinking, I went to a lot of trouble to toss out such a set. What is the difference between action and the original common framework thinking?

Please consider a question, what is the definition of action?

In the initial Flux design, action was designed to change the state in Store to achieve the purpose of controllable state and clear flow.

The Actions in Redux didn’t even support asynchronous operations. Later, there were variations of asynchronous actions and asynchronous middleware implementations such as Redux-Thunk and Redux-Saga.

As a result, actions were originally designed to manage Store state, but as needed, they were given the ability to call the interface asynchronously and change Store state.

GetUsers () calls the interface to get user data, addUser() adds the user, and removeUser() removes the user.

So which method has an asynchronous request? Which method would operate directly on the Store without an interface request?

Vmo wants to provide a design approach that manages and maintains data models, asynchronous fetching, and page state separately.

The complex data operations such as data acquisition, adaptive processing and association processing are handed over to Vmo.

Give the Vmo processed data model to Store. As the final page state.

Mobx

Vmo can also be used with Mobx to combine data models with data responses.

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[];

  /** * ADAPTS data to model field *@param data* /
  protected load(data: any) :this {
    data.filters = data.values;
    return super.load(data); }}Copy the code

conclusion

Vmo is about design

Through Vmo, we hope to help front-end personnel to establish the importance of data and the cognition of data model. The operation and processing of data are handed over to Model, restoring Store’s original intention of front-end state design.

Vmo is my first personal open source project, condensed my current big front-end data processing thinking precipitation, source code implementation is not complex, mainly want to provide a design idea.

GitHub has a complete Example for those interested in the project.

The project address

Let the audience master laugh, welcome to show discussion ~

Personal email: [email protected]

Personal wechat: Wangyinye (please specify your purpose and gold digging)