Python自動化開發學習-RESTful API

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);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章