Skip to content

在 Vue.js 中以 RESTful 方式操作数据 #3

@laoshu133

Description

@laoshu133

在 Vue.js 中以 RESTful 方式操作数据

近几年“网站即软件”的概念约来越流行,同时使得能简单有效支撑这种高延时、高并发“软件”的 RESTful 架构也越来越流行。

市面上支持 RESTful 方式操作的类库也越来越多,但在 Vue.js 社区内真正能用、好用的确很少,现在我们用了全新的选择: V-Model。

什么是 V-Model

V-Model 是一个 Vue.js 插件,实现了 RESTful 架构的 Client 端 Model 层,基于 bluebirdaxios 实现;其提供了 MVC 设计中 model 层绝大部分功能,例如:创建、更新、删除、模型继承、模型静态属性等。

熟悉 ng-resource 的同学也能很快上手 V-Model,因为它们的理念、语法基本保持一致,不同的地方在于 ng-resource 内所操作的都是资源(resource),在 V-Model 中操作的是模型(model),更接近于 MVC 设计。

为什么会有 V-Model

在我们处理后台管理页面时有大量的接口请求,且大部分接口还是联动的,按照以往直接 http.get(...) 的方式我们需要在各个业务的 Controller 内写大量接口请求相关的代码,而且后期维护相当痛苦,我们迫切需要一个类似于后端中 Model 层的实现,来抽象出数据 I/O 的细节;同时我们也参照了市面上已有的类似实现,但均有些不尽人意,故再造一个轮子。

解决了哪些痛点

  • 支持模型继承
  • 支持模型静态属性
  • 内建支持数据分页结构
  • 支持请求中取消(基于 bluebird cancellation 特性)
  • 支持 $resolved 标记,方便的控制加载中状态(loading)
  • 支持拦截器 (基于 axios interceptors)

基本玩法

注入插件,并配置 API 基础地址:

import Model from 'v-model';

// set baseURL
Model.http.defaults.baseURL = '//api.huanleguang.com';

// install
Vue.use(Model);

声明一个 Post 模型,并为其指定两个状态(静态属性):

const PostModel = Model.extend('/posts/:id', null, {
    EDITING: 0,
    PUBLISHED: 1
});

创建一个 Vue 实例,绑定一个 post 模型,并为其设置 loadsave 方法:

const app = new Vue({
    el: '#app',
    data: {
        post: new PostModel({
            status: PostModel.EDITING,
            content: '',
            title: ''
        })
    },
    methods: {
        load(id) {
            this.post = PostModel.get({ id });

            return this.post.$promise;
        },
        save(data) {
            return this.post.$save(data);
        }
    }
});

以上即为一个简单的模型示例,我们可以调用 app.load(1) 来实现 post 的加载,可以调用 app.save({}) 来保存 post 数据。

细心的同学可能会发现在 load 方法中,我们直接使用了 this.post = PostModel.get(...); 赋值,而不是回调方式来对其进行赋值,那么这是如何实现的?

这里的灵感也来自于 ng-resource,但由于 Angular.js 和 Vue.js 的双向绑定实现原理,差异较大,V-Model 内部的实现也和 ng-resource 不一样,其核心原来在于,在实例一个 model 时我们已经将对象或者数组(取决于 Model action 定义时的 isArray 标记)创建好了,其后续的操作均只是操作已经创建好的对象或数组,另外 V-Model 内部也利用 Vue.set 实现了当 model 新增字段时能使双向绑定及时生效,具体实现见源代码:

https://github.com/huanleguang/v-model/blob/master/src/install.js#L12

数据分页

我们知道在 RESTful 架构中,分页这类 meta data 不应该直接出现在 response body 中,而应该通过 response headers 返回,而对于在我们的业务 controller 内则是需要能够快速访问到分页数据,但如果数据出现在 response headers 内无疑会加重我们业务代码的复杂度;

为此我们为 Model 的 action 设计了一个 hasPagination 的配置标记,如果设置 hasPagination 标记为 true 那么 V-Model 在请求数据返回时会自动读取 response headers 内的 X-Pagination 字段,并将数据转换为如下格式:

{
    pagination: { total: 0, size: 20, num: 1 },
    items: []
}

items 内的没一项均会实例成一个完整的 model,具备完整 action 操作;

举例:

定一个 PostModel ,并复写 query action 使其具有 hasPagination 标记,当请求完成时得到的 data 即为格式化后的数据,data.items 内的每一项也均为 PostModel 的实例

const PostModel = Model.extend('/posts/:id', {
    query: { method: 'get', hasPagination: true }
});

let postsData = PostModel.query({
    page_size: 20,
    page_num: 1
});

postsData.$promise.then(data => {
    console.log(data === postsData); // true
    console.log(data); // { "pagination":{"num":1,"size":20,"total":44}, "items": [...]}
    console.log(typeof data.items[0].$save); // function
});

取消已经发出的请求

当我们在请求一个数据量较大的分页列表时,有时会因为每页数据量差异较大导致每次请求所花费的时间也有较大差异,这时如果用户快速切换页码时就很有可能导致页面错乱(请求加载时序错乱);

当然解决这个办法也很简单,就是当用户切换页码时如果上一次的请求还未结束就先取消上一次的请求,如果我们使用的 lastXhr = http.get(...) 这样的方式去操作接口,那么实现起来就非常简单,在每次请求发起前调用 lastXhr.abort() 即可,但对于基于 Promise 设计的 V-Model 来说显然是行不通的,好在 Promise 最新规范的草案内有了一个提案,而且 axios 实现了, 另外 V-Model 内部的 Promise 均是基于 bluebird,因此我们也可以使用 bluebird 的一些高级特性;

具体实例:

import Model from 'v-model';
import Pormise from 'bluebird';

// enable bluebird cancellation
Promise.config({
    cancellation: true
});

const PostModel = Model.extend('/posts/:id', {
    query: { method: 'get', hasPagination: true }
});

const app = new Vue({
    data() {
        return {
            query: {
                page_num: 1
            },
            itemsData: {
                pagination: { num: 1, size: 20, total: 0 },
                items: []
            }
        }
    },
    methods: {
        load() {
            // Cancel the last request
            // If it has not responded yet
            let promise = this.itemsData.$promise;
            if(promise) {
                promise.cancel();
            }

            // make a new request
            this.itemsData = PostModel.query(query);
        }
    },
    watch: {
        query() {
            this.load();
        }
    },
    created() {
        this.load();
    }
});

PS. 代码中我们开启了 blurbird 的 cancellation 特性,这是个较为高级的特性,默认不开启,且开启后可能影响 bluebird 本身的性能。

解决小菊花(加载提示)

我们在做一些列表页的加载,编辑页的保存时多多少少会设计一些加载状态(loading, 俗称小菊花),以往我们实现往往是添加一个 loading 的标记,然后在请求完成后将其至为 false ,例如:

<template>
    <div class="app">
        <div class="loading" v-if="loading">Loading...</div>
        <div class="detail" v-if="post"><h1>{{post.title}}</h1></div>
    </div>
</template>
<script>
export default {
    data() {
        return {
            loading: false,
            post: null
        };
    },
    methods: {
        load() {
            this.loading = true;

            return http.get().then({
                // ...
            })
            .finally(() => {
                this.loading = false;
            });
        }
    },
    created() {
        this.load();
    }
};
</script>

但这样实现是非常不优雅,而且会产生许多垃圾代码去控制其状态;

当然对于 V-Model 来说自然有更好的解决方案,我们每个 model 都内置 $resolved 标记:

<template>
    <div class="app">
        <div class="loading" v-if="!post.$resolved">Loading...</div>
        <div class="detail" v-if="post.$resolved"><h1>{{post.title}}</h1></div>
    </div>
</template>
<script>
import Model from 'v-model';

const PostModel = Model.extend('/posts/:id');

export default {
    data() {
        return {
            loading: false,
            post: new PostModel({
                title: ''
            })
        };
    },
    methods: {
        load() {
            return this.post.$get();
        }
    },
    created() {
        this.load();
    }
};
</script>

更多高级玩法,参见 Github 主页:https://github.com/huanleguang/v-model

参考资料

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions