温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Python自动化开发学习-RESTful API

发布时间:2020-07-30 02:20:51 来源:网络 阅读:11987 作者:骑士救兵 栏目:编程语言

RESTful API

RESTful API 是一种面向资源编程,也叫表征状态转移(英文:Representational State Transfer,简称REST)。
认为网络上所有的东西都是资源,对资源的操作无非就是增删改查。

传统的方法

比如有个资产的页面,URL是 www.example.com/asset。要对它进行增删改查,可能使用不同的url来区分:

  • www.example.com/addAsset :增加资产,一般是POST方法。
  • www.example.com/delAsset :删除资产,一般是POST方法。
  • www.example.com/editAsset :修改资产,一般是POST方法。
  • www.example.com/showAsset :显示资产,一般是GET方法。也可能使用 www.example.com/asset 作为url

这里的url一般使用的都是动词,表示是一个动作。

RESTful API 的规则

RESTful API 用一个url代指一个资源,既然是资源,这个词要用名词。那么这个url就是 www.example.com/asset 。增删改查都是通过这个url实现的,通过不同的method实现不同的方法,常用的是下面几个方法:

  • GET(SELECT):从服务器取出资源(一项或多项)。
  • POST(CREATE):在服务器新建一个资源。
  • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)。
  • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)。
  • DELETE(DELETE):从服务器删除资源。

在django中,推荐使用CBV。当然FBV也不是不行。

RESTful API 设计指南

这篇貌似讲的很好,值得参考:http://www.ruanyifeng.com/blog/2014/05/restful_api.html

JsonResponse

使用API就会有很多序列化数据返回的操作。
之前当我们需要给前端返回序列化后的字符串时,往往都是先调用json.dumps()这个方法,然后再用HttpResponse()把字符串返回给前端。既然每次都要这么搞,于是django给我么封装了一个新方法,直接完成序列化和返回字符串。
JsonResponse这个类是HttpRespon的子类,通过它直接就可以把字典进行序列化并返回给前端。

>>> from django.http import JsonResponse
>>> response = JsonResponse({'foo': 'bar'})
>>> response.content
'{"foo": "bar"}'

默认只能传入一个字典,并且API要返回的数据应该也就是字典。但是如果一定要序列化一个其他的类型,比如列表,可以设置safe参数:

>>> response = JsonResponse([1, 2, 3], safe=False)

如果要自定义编码器,和json方法一样,通过下面的参数指定:

>>> response = JsonResponse(data, encoder=MyJSONEncoder)

这里的 encoder 参数就是原生的 json.dumps 的cls参数。源码里最后也是调用原生的 json.dumps 把 encoder 传给cls 的。
另外,也可以只定义类中的 default 方法,但是 JsonRespons 没有专门的参数来接收,不过调用原生的 json.dumps 时,会把 json_dumps_params 参数传递过去。也就是在 JsonRespons 里,可以把所有的 json.dumps 的参数先传给 json_dumps_params 。调用原生的 json.dumps 方法的源码是这样的:

data = json.dumps(data, cls=encoder, **json_dumps_params)

所以,可以这么用:

return JsonResponse(
        data={'obj': obj},
        json_dumps_params={'default': fn},  # 这个参数是传给原生的 json.dumps 执行的参数
    )

# 上面自然是要先定义好一个fn函数的,比如下面这样
def fn(obj):
    if hasattr(obj, 'isoformat'):
        return obj.strftime("%Y-%m-%d %T")

代码示例

这段代码用来从数据库获取数据,然后在前端动态的生成表格。
完整的代码在最后,前面是一步一步把这个功能给做出来。
处理函数主要负责两件事情:

  • 从数据库获取数据,返回给前端
  • 定制一个存有配置项的字典,定义好前端怎么显示这些数据,也返回给前端

准备(初始化)

在 urls.py 里写好对应关系:

from django.contrib import admin
from django.urls import path

from app01 import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('host/', views.HostView.as_view()),
]

写一个处理函数 views.py,这里用CBV,直接返回页面

from django.views import View

class HostView(View):

    def get(self, request, *args, **kwargs):
        return render(request, 'host.html')

前端的页面先返回一个空的表格,之后再填充表格内容:

<body>
<h2>主机列表</h2>
<table border="1">
    <thead id="thead"></thead>
    <tbody id="tbody"></tbody>
</table>

<script src="/static/jquery-1.12.4.js"></script>
<script>
    $(function () {
        init();  // 当页面加载完成,执行init()初始化方法。具体的方法写在下面
    });

    function init() {
       alert('初始化')
    }
</script>
</body>

测试一下,应该只能看到h2标签里的内容。页面初始化之后会弹一个alert。

从API接口获取数据

写一下前端的init()方法,发送一个AJAX请求到一个新的url,然后接收到返回的数据后,后台看一下:

<script>
    $(function () {
        init();  // 当页面加载完成,执行init()初始化方法。具体的方法写在下面
    });

    function init() {
       $.ajax({
           url: '/api/host/',
           type: 'GET',
           dataType: 'JSON',
           success: function (arg) {
               console.log(arg)
           }
       })
    }
</script>

在 url.py 里再加一个api接口的对应关系:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('host/', views.HostView.as_view()),
    path('api/host/', views.HostApi.as_view()),
]

处理函数直接返回字典:

class HostApi(View):

    def get(self, request, *args, **kwargs):
        ret = {'status': True,
               'message': None,
               'data': None,
               'error': None,
               }
        ret['message'] = 'API接口测试'
        return JsonResponse(ret)

从API接口获取数据2

这里换个方法来实现上面的处理函数。返回的数据不用字典记录,而是用类来记录。没啥差别,就是原来是用中括号来操作的,现在可以用点来操作。最后返回的时候还是要返回字典的,可以用 .__dict__() 来得到这样的一个字典:

class BaseResponse(object):
    def __init__(self):
        self.status = True
        self.message = None
        self.data = None
        self.error = None

class HostApi(View):

    def get(self, request, *args, **kwargs):
        response = BaseResponse()  # 先实例化
        table_config = [
            {
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
            },
            {
                'title': "端口号",
                'display': 1,
            }
        ]
        response.data = {'table_config': table_config}  # 用点来操作,就是给类的属性赋值
        return JsonResponse(response.__dict__)

前端处理返回的数据

把之前前端页面里AJAX请求的success的回调函数写完整。如果返回status是True,则把参数传递给接下来的处理的函数。否则弹一个alert():

<script>
    $(function () {
        init();  // 当页面加载完成,执行init()初始化方法。具体的方法写在下面
    });

    function init() {
       $.ajax({
           url: '/api/host/',
           type: 'GET',
           dataType: 'JSON',
           success: function (arg) {
               // console.log(arg)
               if (arg.status){
                    createThead(arg.data.table_config)
               }else{
                   alert(arg.error)
               }
           }
       })
    }

    function createThead(config){
        console.log(config)
    }
</script>

如此AJAX请求也完成了:发送了请求,接收了返回结果,然后把返回的结果交给之后的函数进行处理。接下来是就是完善createThead()这个函数了。这里要根据收到的title生成表格的thead的标签:

    function createThead(config){
        // console.log(config)
        var tr = document.createElement('tr');
        $.each(config, function (k, v) {
            if(v.display){
                var th = document.createElement('th');
                th.innerHTML = v.title;
                $(tr).append(th)
            }
        });
        $('#thead').append(tr);
    }

到现在这步,可以在前端看到表格的表头的内容。并且表头是根据后端返回的字典动态生成的。

准备数据库

到这里要后端返回数据了,表结构都还没建,我这里设计了三张表:

class UserInfo(models.Model):
    """用户表"""
    name = models.CharField(max_length=32)
    age = models.IntegerField()

class BusinessUnit(models.Model):
    """业务线"""
    name = models.CharField(max_length=32)

class Host(models.Model):
    """主机列表"""
    host_type_choices = ((1, '服务器'),
                         (2, '防火墙'),
                         (3, '路由器'),
                         (4, '交换机'),
                         )
    host_type = models.IntegerField(choices=host_type_choices)
    hostname = models.CharField(max_length=32)
    port = models.IntegerField()
    business_unit = models.ForeignKey(BusinessUnit, models.CASCADE)
    user = models.ForeignKey(UserInfo, models.CASCADE)

主要用主机列表,其他2张之后可以测试一下对跨表的支持,先一起建好。然后去数据库了随便加几条数据。

后端的处理函数(view),返回更多的数据

到这里,已经可以通过后端返回的字段名在前端动态的生成表头了。接下来把表的内容也显示出来,接着完善后端的处理函数,给前端返回更多的数据。下面是处理函数,根据table_config的配置,去数据库里去对应的字段,然后返回给前端。下面是目前处理函数完整的代码:

class HostApi(View):

    def get(self, request, *args, **kwargs):
        response = BaseResponse()  # 先实例化
        table_config = [
            {
                'field': 'hostname',  # 表中对应的字段名,必须要和字段名一致,下面要用作查询条件
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
            },
            {
                'field': 'id',
                'title': "ID",
                'display': 0,  # 这一列不用显示,但是前端能接收到数据
            },
            {
                'field': 'port',
                'title': "端口号",
                'display': 1,
            },
            {
                'field': None,  # 允许添加额外的列,这个列的内容没有对应的字段
                'title': "操作",
                'display': 1,
            }
        ]
        field_list = []
        for item in table_config:
            if item['field']:
                field_list.append(item['field'])

        # 写一个try,也可以把上面的内容都放进来,
        try:
            result = models.Host.objects.values(*field_list)
            result = list(result)
            response.data = {'table_config': table_config,
                             'data_list': result,
                             }
        except Exception as e:
            response.status = False
            # response.error = str(e)  # 错误信息,用下面的模块可以看到错误产生的位置
            import traceback
            response.error = traceback.format_exc()  # 返回详细的错误信息,包括哪个文件的哪一行
            print(response.error)
        return JsonResponse(response.__dict__)

这里主要就是去数据库里获取数据,然后把获取的QuerySet转成列表也放到response对象里,方便最后返回。
这里注意table_config的配置里有2种特殊的情况:

  • display为0,前端不显示的列。但是依然要把数据传给前端,之后会用到这里的数据
  • field为None,前端要显示,但是数据不是数据库里数据的列,之后会提供填充其中内容的方法

错误信息的优化
处理函数里加了个try,可以把处理函数的全部过程都写到try里进行捕获。如果捕获到异常,就会返回异常信息给前端。前端已经用arg.status来确认是否有异常返回了,下面会再优化一下前端异常显示的效果。
另外这里用了一个traceback模块,traceback对象中包含出错的行数、位置等数据,貌似也很有用。用例子中的方法就可以拿到了。等下面的小节把前端显示优化之后,可以随便哪句语句添加或者删除个字符搞个语法错误,测试效果。

前端显示效果

这里加了一个createTbody()方法,作用是把数据填充到表格里去。另外还有一个showError()方法,作用是如果收到的是后端捕获的异常信息,在标题下面显示出来。下面也是目前前端的完整代码:

<body>
<h2>主机列表</h2>
<table border="1">
    <thead id="thead"></thead>
    <tbody id="tbody"></tbody>
</table>

<script src="/static/jquery-1.12.4.js"></script>
<script>
    $(function () {
        init();  // 当页面加载完成,执行init()初始化方法。具体的方法写在下面
    });

    function init() {
        $.ajax({
            url: '/api/host/',
            type: 'GET',
            dataType: 'JSON',
            success: function (arg) {
                // console.log(arg)
                if (arg.status){
                    createThead(arg.data.table_config);
                    createTbody(arg.data.table_config, arg.data.data_list)
                }else{
                    //alert(arg.error);
                    showError(arg.error);
                }
            }
        })
    }

    function showError(msg) {
        // 插入错误信息
        var tag = document.createElement('p');
        $(tag).html(msg).css('color', 'red');
        $('h2').after(tag);
    }

    function createThead(config){
        // console.log(config)
        var tr = document.createElement('tr');
        $.each(config, function (k, v) {
            if(v.display){
                var th = document.createElement('th');
                th.innerHTML = v.title;
                $(tr).append(th)
            }
        });
        $('#thead').append(tr);
    }

    function createTbody(config, list) {
        // 循环数据,每条数据有一行
        $.each(list, function (k1, row) {
            var tr = document.createElement('tr');
            // 循环配置config,每条配置就是一个字段,即一列
            $.each(config, function (k2, configItem) {
                if (configItem.display){
                    var td = document.createElement('td');
                    td.innerHTML = row[configItem.field];
                    $(tr).append(td)
                }
            });
            $('#tbody').append(tr)
        })
    }
</script>
</body>

修改table_config的内容,调整前端显示的数据
前端的表格都是通过后端传递来的数据动态生成的。在上面模板的基础上,现在要修改表格显示的内容,只需要去后端调整table_config就可以了,比如改成这样,这里有跨表操作:

        table_config = [
            {
                'field': 'hostname',  # 表中对应的字段名,必须要和字段名一致,下面要用作查询条件
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
            },
            {
                'field': 'id',
                'title': "ID",
                'display': 0,  # 这一列不用显示,但是前端能接收到数据
            },
            {
                'field': 'port',
                'title': "端口号",
                'display': 1,
            },
            {
                'field': 'business_unit__name',
                'title': "业务线",
                'display': 1,
            },
            {
                'field': 'host_type',
                'title': "主机类型",
                'display': 1,
            },
            {
                'field': None,  # 允许添加额外的列,这个列的内容没有对应的字段
                'title': "操作",
                'display': 1,
            }
        ]

主机类型暂时没有办法,因为数据库里记录的值只是数值。而这个数值具体表示的内容是在内存里的。要显示内容首先要获得 models.Host.host_type_choices 然后通过数值拿到对应的文本内容。后面继续优化后应该会有解决的办法。

封装

先暂时写到这里,现在要把前端的js代码做一个封装,做成一个通用的组件。封装的知识点在之前学习jQuery的最后讲过,这里就用上了。封装好的代码如下:

(function ($) {

    var requestURL;
    function init() {
        $.ajax({
            url: requestURL,
            type: 'GET',
            dataType: 'JSON',
            success: function (arg) {
                // console.log(arg)
                if (arg.status){
                    createThead(arg.data.table_config);
                    createTbody(arg.data.table_config, arg.data.data_list)
                }else{
                    //alert(arg.error);
                    showError(arg.error);
                }
            }
        })
    }

    function showError(msg) {
        // 插入错误信息
        var tag = document.createElement('p');
        $(tag).html(msg).css('color', 'red');
        $('h2').after(tag);
    }

    function createThead(config){
        // console.log(config)
        var tr = document.createElement('tr');
        $.each(config, function (k, v) {
            if(v.display){
                var th = document.createElement('th');
                th.innerHTML = v.title;
                $(tr).append(th)
            }
        });
        $('#thead').append(tr);
    }

    function createTbody(config, list) {
        // 循环数据,每条数据有一行
        $.each(list, function (k1, row) {
            var tr = document.createElement('tr');
            // 循环配置config,每条配置就是一个字段,一列
            $.each(config, function (k2, configItem) {
                if (configItem.display){
                    var td = document.createElement('td');
                    td.innerHTML = row[configItem.field];
                    $(tr).append(td)
                }
            });
            $('#tbody').append(tr)
        })
    }

    $.extend({
        'show_table': function (url) {
            requestURL = url;
            init();
        }
    })
})(jQuery);

现在前端页面只要先引用这个js文件,然后调用一下extend里的show_table方法就和之前一样了:

<body>
<h2>主机列表</h2>
<table border="1">
    <thead id="thead"></thead>
    <tbody id="tbody"></tbody>
</table>

<script src="/static/jquery-1.12.4.js"></script>
<script src="/static/show-table.js"></script>
<script>
    $(function () {
        $.show_table('/api/host/');
    });
</script>
</body>

封装之后的js文件,其实就是一个插件了,可以灵活的运用到其他要生成表格的场景里。

输出字符串格式化

这里要进一步定制输出的内容。之前只能输出数据库里的内容。现在是把数据库的内容作为原始数据,但是输出到页面的内容可以通过format方法格式化后再最终展示出来。table_config里再加一个text属性。text内部有content属性,这个是最终要输出的内容,可以像format那样使用{}把需要格式化的内容标记出来。然后再在text内部的kwargs里,指定前面的这些占位符所对应的具体内容,这里面又用了@来标记这不是一个字符串,而是要取对应的字段的值。
所有的{}和@标记都是等到前端再处理的,后端只是进行设置,现在的table_config如下:

        table_config = [
            {
                'field': 'hostname',  # 表中对应的字段名,必须要和字段名一致,下面要用作查询条件
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
            },
            {
                'field': 'id',
                'title': "ID",
                'display': 0,  # 这一列不用显示,但是前端能接收到数据
                'text': None,  # 上面不显示,所以这里text有没有都没关系
            },
            {
                'field': 'port',
                'title': "端口号",
                'display': 1,
                'text': {'content': '端口:{port}', 'kwargs': {'port': '@port'}}
            },
            {
                'field': 'business_unit__id',
                'title': "业务线ID",
                'display': 0,
            },
            {
                'field': 'business_unit__name',
                'title': "业务线",
                'display': 1,
                'text': {'content': '{n}(id:{id})', 'kwargs': {'n': '@business_unit__name', 'id': '@business_unit__id'}}
            },
            {
                'field': 'host_type',
                'title': "主机类型",
                'display': 1,
                'text': {'content': '{type}', 'kwargs': {'type': '@host_type'}}
            },
            {
                'field': None,  # 允许添加额外的列,这个列的内容没有对应的字段
                'title': "操作",
                'display': 1,
                'text': {'content': '<a href="/api/host/{id}">查看详细</a>', 'kwargs': {'id': '@id'}}
            },
        ]

不显示的字段,display设置为0,那么就不显示了,所以text属性是用不到的。但是其他字段里可以通过@取到这个字段的值了。
有的显示的字段,我也没设置text,那么等下前端处理的时候,还是按照之前的方法来进行展示
最后的操作字段,现在可以加上任意内容了。这里写了一个a标签,并且href里加上了主机id。

前端代码
之前已经完成了封装,所以这里就是修改js文件里的内容。
之前是通过 td.innerHTML = row[configItem.field] 显示内容的。现在这个方法保留,在没有text属性的时候继续按这个来显示。否则,显示content的内容并且根据kwargs的内容进行格式化。前端是没有格式化方法的,这里自己写了一个(下一节展开),完整的代码如下:

(function ($) {

    var requestURL;
    function init() {
        $.ajax({
            url: requestURL,
            type: 'GET',
            dataType: 'JSON',
            success: function (arg) {
                // console.log(arg)
                if (arg.status){
                    createThead(arg.data.table_config);
                    createTbody(arg.data.table_config, arg.data.data_list)
                }else{
                    //alert(arg.error);
                    showError(arg.error);
                }
            }
        })
    }

    function showError(msg) {
        // 插入错误信息
        var tag = document.createElement('p');
        $(tag).html(msg).css('color', 'red');
        $('h2').after(tag);
    }

    function createThead(config){
        // console.log(config)
        var tr = document.createElement('tr');
        $.each(config, function (k, v) {
            if(v.display){
                var th = document.createElement('th');
                th.innerHTML = v.title;
                $(tr).append(th)
            }
        });
        $('#thead').append(tr);
    }

    function createTbody(config, list) {
        // 循环数据,每条数据有一行
        $.each(list, function (k1, row) {
            var tr = document.createElement('tr');
            // 循环配置config,每条配置就是一个字段,一列
            $.each(config, function (k2, configItem) {
                if (configItem.display){
                    var td = document.createElement('td');
                    if (!configItem.text){
                        td.innerHTML = row[configItem.field];
                    }else{
                        var kwargs = {};
                        // 把configItem.text.kwargs的内容存到上面的kwargs里
                        // 没有@开头的原样放过去,以@开头的做特殊处理
                        $.each(configItem.text.kwargs, function (key, value) {
                            if(value.startsWith('@')){
                                // 如果是以@开头,需要做特殊处理
                                var _value = value.substring(1, value.length);  // 把第一个字符截掉,即去掉@
                                kwargs[key] = row[_value]
                            }else{
                                kwargs[key] = value
                            }
                        });
                        td.innerHTML = configItem.text.content.format(kwargs);
                    }

                    $(tr).append(td)
                }
            });
            $('#tbody').append(tr)
        })
    }

    // 为字符串创建format方法,用于字符串格式化
    String.prototype.format = function (args) {
        return this.replace(/\{(\w+)\}/g, function (substring, args2) {
            return args[args2];
        })
    };

    $.extend({
        'show_table': function (url) {
            requestURL = url;
            init();
        }
    })
})(jQuery);

在前端增加format方法

这里要在Sting对象的原型里添加一个format()方法,让前端的字符串也可以像python那样,对字符串进行格式化输出。代码就下面简单的几行,正则匹配然后用replace做替换。不过替换的内容又是一个function,逻辑有点复杂了,总之先拿着现成的用把,稍微改改大概也行。暂时没有完全理解:

    // 为字符串创建format方法,用于字符串格式化
    String.prototype.format = function (args) {
        return this.replace(/\{(\w+)\}/g, function (substring, args2) {
            return args[args2];
        })
    };

为td定制属性

首先table_config里再加一个属性attr,用来定制td标签的属性:

        table_config = [
            {
                'field': 'hostname',  # 表中对应的字段名,必须要和字段名一致,下面要用作查询条件
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
                'attr': {'k1': 'v1', 'k2': 'v2'}
            },
            {
                'field': 'port',
                'title': "端口号",
                'display': 1,
                'text': {'content': '端口:{port}', 'kwargs': {'port': '@port'}},
                'attr': {'original': '@port'}
            },
        ]

然后在js插件里,td.innerHTML赋值之后,添加到tr标签里之前,插入下面这段,为td标签设置属性:

                    // 为td添加属性
                    if (configItem.attr){
                        $.each(configItem.attr, function (name, value) {
                            if(value.startsWith('@')){
                                // 如果是以@开头,需要做特殊处理
                                var _value = value.substring(1, value.length);  // 把第一个字符截掉,即去掉@
                                td.setAttribute(name, row[_value]);
                            }else{
                                td.setAttribute(name, value);
                            }
                        })
                    }

                    $(tr).append(td)

这里添加属性的时候,也支持@符号。
把单元格的原始数据保留一份在td的某个属性里,这样做的好处是,如果你支持在表格里做数据修改。当你要保存修改的时候,先通过js代码检查单元格里现在的内容和之前留在td属性里的原始内容是否一致。不一致才提交给后台进行更新,如果一致,那么这个单元格不需要更新。

双@标记

用什么表情都无所谓,但是这里需要一个新的标记,标记一个新的数据显示的方法。
这里解决之前显示 models.Host.host_type_choices 的问题了。后端返回的response.data里开辟一个key(global_dict),用来存放这类数据

            # 获取global_dict
            global_dict = {
                'business_unit': list(models.BusinessUnit.objects.values_list('id', 'name')),
                'host_type': models.Host.host_type_choices,
            }

            response.data = {'table_config': table_config,
                             'data_list': result,
                             'global_dict': global_dict,
                             }

这样的数据格式不但放在内存里的choices可以用,ForeignKey使用 .values_list()方法也能生成一样的数据,所以也能用。这种方法是不跨表的,适合条目比较少的情况。如果表里行数很多的话就不适合了,一方面所有的条目都会传递给客户端,另一方面前端是遍历查找。
这里需要一个新的标记,标记是去global_dict里去查找对应的内容。所以用了两个@。那么table_config现在要这么写:

        table_config = [
            {
                'field': 'hostname',  # 表中对应的字段名,必须要和字段名一致,下面要用作查询条件
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
                'attr': {'k1': 'v1', 'k2': 'v2'}
            },
            {
                'field': 'business_unit',
                'title': "业务线_不跨表",
                'display': 1,
                'text': {'content': '{n}', 'kwargs': {'n': '@@business_unit'}}
            },
            {
                'field': 'host_type',
                'title': "主机类型",
                'display': 1,
                'text': {'content': '{type}', 'kwargs': {'type': '@@host_type'}}
            },
        ]

前端的实现
先处理response.data.global_dict数据的接收。所有的数据都是在AJAX的success方法里在参数arg里,原先已经有2个方法了,这里再增加一个方法,保存global_dict数据:

                    initGlobal(arg.data.global_dict);  // AJAX的success函数里新加这个方法
                    createThead(arg.data.table_config);
                    createTbody(arg.data.table_config, arg.data.data_list)

调用的方法,就是把这个数据暂存到一个在插件内部是全局有效的变量GLOBAL_DICT里,这样做应该是方便在插件内部的其他方法里调用:

    // 用户保存当前作用域内的“全局变量”
    var GLOBAL_DICT = {};

    function initGlobal(globalDict) {
        $.each(globalDict, function (k, v) {
            GLOBAL_DICT[k] = v;
        })
    }

然后来处理@@的解析,在原来的@的解析的if里再增加一个分支:

                        var kwargs = {};
                        // 把configItem.text.kwargs的内容存到上面的kwargs里
                        // 没有@开头的原样放过去,以@开头的做特殊处理
                        $.each(configItem.text.kwargs, function (key, value) {
                            if(value.startsWith('@@')){
                                var global_name = value.substring(2, value.length);
                                // console.log(GLOBAL_DICT[global_name]);
                                $.each(GLOBAL_DICT[global_name], function (index, arr) {
                                    if (arr[0] === row[global_name]){
                                        kwargs[key] = arr[1];
                                        return false;  // 匹配到一个,就退出遍历
                                    }
                                });
                            } else if(value.startsWith('@')){
                                // 如果是以@开头,需要做特殊处理
                                var _value = value.substring(1, value.length);  // 把第一个字符截掉,即去掉@
                                kwargs[key] = row[_value]
                            }else{
                                kwargs[key] = value
                            }
                        });

这里用的是遍历的方式来查找的,所以如果列表太长就不太适合了。放在内存中的choices应该都不会很长。如果是ForeignKey,现在有2个方法可以显示了。这个方法不跨表,但是数据太多就不适合了。

完整的代码:

路由的对应关系,urls.py

urlpatterns = [
    path('admin/', admin.site.urls),
    path('host/', views.HostView.as_view()),
    path('api/host/', views.HostApi.as_view()),
]

表结构,models.py

class UserInfo(models.Model):
    """用户表"""
    name = models.CharField(max_length=32)
    age = models.IntegerField()

class BusinessUnit(models.Model):
    """业务线"""
    name = models.CharField(max_length=32)

class Host(models.Model):
    """主机列表"""
    host_type_choices = ((1, '服务器'),
                         (2, '防火墙'),
                         (3, '路由器'),
                         (4, '交换机'),
                         )
    host_type = models.IntegerField(choices=host_type_choices)
    hostname = models.CharField(max_length=32)
    port = models.IntegerField()
    business_unit = models.ForeignKey(BusinessUnit, models.CASCADE)
    user = models.ForeignKey(UserInfo, models.CASCADE)

处理函数,views.py

class BaseResponse(object):
    def __init__(self):
        self.status = True
        self.message = None
        self.data = None
        self.error = None

class HostView(View):

    def get(self, request, *args, **kwargs):
        return render(request, 'host.html')

class HostApi(View):

    def get(self, request, *args, **kwargs):
        response = BaseResponse()  # 先实例化
        table_config = [
            {
                'field': 'hostname',  # 表中对应的字段名,必须要和字段名一致,下面要用作查询条件
                'title': "主机名",  # 表格的列名
                'display': 1,  # 是否显示该列,1是显示,0是不显示
                'attr': {'k1': 'v1', 'k2': 'v2'}
            },
            {
                'field': 'id',
                'title': "ID",
                'display': 0,  # 这一列不用显示,但是前端能接收到数据
                'text': None,  # 上面不显示,所以这里text有没有都没关系
            },
            {
                'field': 'port',
                'title': "端口号",
                'display': 1,
                'text': {'content': '端口:{port}', 'kwargs': {'port': '@port'}},
                'attr': {'original': '@port'}
            },
            {
                'field': 'business_unit__id',
                'title': "业务线ID",
                'display': 0,
            },
            {
                'field': 'business_unit__name',
                'title': "业务线",
                'display': 1,
                'text': {'content': '{n}(id:{id})', 'kwargs': {'n': '@business_unit__name', 'id': '@business_unit__id'}}
            },
            {
                'field': 'business_unit',
                'title': "业务线_不跨表",
                'display': 1,
                'text': {'content': '{n}', 'kwargs': {'n': '@@business_unit'}}
            },
            {
                'field': 'host_type',
                'title': "主机类型",
                'display': 1,
                'text': {'content': '{type}', 'kwargs': {'type': '@@host_type'}}
            },
            {
                'field': None,  # 允许添加额外的列,这个列的内容没有对应的字段
                'title': "操作",
                'display': 1,
                'text': {'content': '<a href="/api/host/{id}">查看详细</a>', 'kwargs': {'id': '@id'}}
            },
        ]
        field_list = []
        for item in table_config:
            if item['field']:
                field_list.append(item['field'])

        # 写一个try,也可以把上面的内容都放进来,
        try:
            result = models.Host.objects.values(*field_list)
            result = list(result)

            # 获取global_dict
            global_dict = {
                'business_unit': list(models.BusinessUnit.objects.values_list('id', 'name')),
                'host_type': models.Host.host_type_choices,
            }

            response.data = {'table_config': table_config,
                             'data_list': result,
                             'global_dict': global_dict,
                             }
        except Exception as e:
            response.status = False
            # response.error = str(e)  # 错误信息,用下面的模块可以看到错误产生的位置
            import traceback
            response.error = traceback.format_exc()  # 返回详细的错误信息,包括哪个文件的哪一行
            print(response.error)
        return JsonResponse(response.__dict__)

前端主页,host.html

<body>
<h2>主机列表</h2>
<table border="1">
    <thead id="thead"></thead>
    <tbody id="tbody"></tbody>
</table>

<script src="/static/jquery-1.12.4.js"></script>
<script src="/static/show-table.js"></script>
<script>
    $(function () {
        $.show_table('/api/host/');
    });
</script>
</body>

前端插件,show-table.js

(function ($) {

    // 用户保存当前作用域内的“全局变量”
    var GLOBAL_DICT = {};

    var requestURL;
    function init() {
        $.ajax({
            url: requestURL,
            type: 'GET',
            dataType: 'JSON',
            success: function (arg) {
                // console.log(arg)
                if (arg.status){
                    initGlobal(arg.data.global_dict);
                    createThead(arg.data.table_config);
                    createTbody(arg.data.table_config, arg.data.data_list)
                }else{
                    //alert(arg.error);
                    showError(arg.error);
                }
            }
        })
    }

    function showError(msg) {
        // 插入错误信息
        var tag = document.createElement('p');
        $(tag).html(msg).css('color', 'red');
        $('h2').after(tag);
    }

    function initGlobal(globalDict) {
        $.each(globalDict, function (k, v) {
            GLOBAL_DICT[k] = v;
        })
    }

    function createThead(config){
        // console.log(config)
        var tr = document.createElement('tr');
        $.each(config, function (k, v) {
            if(v.display){
                var th = document.createElement('th');
                th.innerHTML = v.title;
                $(tr).append(th)
            }
        });
        $('#thead').append(tr);
    }

    function createTbody(config, list) {
        // 循环数据,每条数据有一行
        $.each(list, function (k1, row) {
            var tr = document.createElement('tr');
            // 循环配置config,每条配置就是一个字段,一列
            $.each(config, function (k2, configItem) {
                if (configItem.display){
                    var td = document.createElement('td');
                    if (!configItem.text){
                        td.innerHTML = row[configItem.field];
                    }else{
                        var kwargs = {};
                        // 把configItem.text.kwargs的内容存到上面的kwargs里
                        // 没有@开头的原样放过去,以@开头的做特殊处理
                        $.each(configItem.text.kwargs, function (key, value) {
                            if(value.startsWith('@@')){
                                var global_name = value.substring(2, value.length);
                                // console.log(GLOBAL_DICT[global_name]);
                                $.each(GLOBAL_DICT[global_name], function (index, arr) {
                                    if (arr[0] === row[global_name]){
                                        kwargs[key] = arr[1];
                                        return false;  // 匹配到一个,就退出遍历
                                    }
                                });
                            } else if(value.startsWith('@')){
                                // 如果是以@开头,需要做特殊处理
                                var _value = value.substring(1, value.length);  // 把第一个字符截掉,即去掉@
                                kwargs[key] = row[_value]
                            }else{
                                kwargs[key] = value
                            }
                        });
                        td.innerHTML = configItem.text.content.format(kwargs);
                    }

                    // 为td添加属性
                    if (configItem.attr){
                        $.each(configItem.attr, function (name, value) {
                            if(value.startsWith('@')){
                                // 如果是以@开头,需要做特殊处理
                                var _value = value.substring(1, value.length);  // 把第一个字符截掉,即去掉@
                                td.setAttribute(name, row[_value]);
                            }else{
                                td.setAttribute(name, value);
                            }
                        })
                    }

                    $(tr).append(td)
                }
            });
            $('#tbody').append(tr)
        })
    }

    // 为字符串创建format方法,用于字符串格式化
    String.prototype.format = function (args) {
        return this.replace(/\{(\w+)\}/g, function (substring, args2) {
            return args[args2];
        })
    };

    $.extend({
        'show_table': function (url) {
            requestURL = url;
            init();
        }
    })
})(jQuery);
向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI