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 用一个url代指一个资源,既然是资源,这个词要用名词。那么这个url就是 www.example.com/asset
。增删改查都是通过这个url实现的,通过不同的method实现不同的方法,常用的是下面几个方法:
在django中,推荐使用CBV。当然FBV也不是不行。
这篇貌似讲的很好,值得参考:http://www.ruanyifeng.com/blog/2014/05/restful_api.html
使用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。
写一下前端的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)
这里换个方法来实现上面的处理函数。返回的数据不用字典记录,而是用类来记录。没啥差别,就是原来是用中括号来操作的,现在可以用点来操作。最后返回的时候还是要返回字典的,可以用 .__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张之后可以测试一下对跨表的支持,先一起建好。然后去数据库了随便加几条数据。
到这里,已经可以通过后端返回的字段名在前端动态的生成表头了。接下来把表的内容也显示出来,接着完善后端的处理函数,给前端返回更多的数据。下面是处理函数,根据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种特殊的情况:
错误信息的优化
处理函数里加了个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);
这里要在Sting对象的原型里添加一个format()方法,让前端的字符串也可以像python那样,对字符串进行格式化输出。代码就下面简单的几行,正则匹配然后用replace做替换。不过替换的内容又是一个function,逻辑有点复杂了,总之先拿着现成的用把,稍微改改大概也行。暂时没有完全理解:
// 为字符串创建format方法,用于字符串格式化
String.prototype.format = function (args) {
return this.replace(/\{(\w+)\}/g, function (substring, args2) {
return args[args2];
})
};
首先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);
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。