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)
代碼示例
這段代碼用來從數據庫獲取數據,然後在前端動態的生成表格。
完整的代碼在最後,前面是一步一步把這個功能給做出來。
處理函數主要負責兩件事情:
- 從數據庫獲取數據,返回給前端
- 定製一個存有配置項的字典,定義好前端怎麼顯示這些數據,也返回給前端
準備(初始化)
在 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>
<h1>主機列表</h1>
<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>
測試一下,應該只能看到h1標籤裏的內容。頁面初始化之後會彈一個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>
<h1>主機列表</h1>
<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');
$('h1').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');
$('h1').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>
<h1>主機列表</h1>
<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');
$('h1').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>
<h1>主機列表</h1>
<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');
$('h1').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);