Timeline
2024-3-14:create article
2024-3-21:add DRF & Serializer
官方文档
https://docs.djangoproject.com/
使用 Django 开发简单前后端分离应用
课程要求
【Part 1】SaaS 后台框架搭建
- 结合需求创建model模型
- 通过DRF提供restful接口,其中标准运维相关接口以mock的形式提供
- 以装饰器的方式mock权限相关代码
【Part 2】前端页面开发
- 使用 lesscode 完成任务列表页面
- 新增部署按钮,刷新按钮,查看任务按钮
- 新建部署弹框(下拉框,文本输入框)
- 事件绑定、计算属性、侦听器等
- 进阶完成 lesscode 暂时无法实现的功能
- 把所有接口对接至后台接口
【Part 3】APIGateway 接入
- 讲解APIGateway对应文档查询
- 把Part3中mock掉的标准运维接口,调整为Gateway调用
【Part 4】异步任务支持
- 把查询标准运维任务状态的功能,通过celery周期任务实现
- 完成任务的消息通知,通过celery异步执行
【Part 5】权限中心接入
- 注册本项目的权限模型到权限中心(任务创建,执行记录查看)
- 对后台提供的接口结合iam sdk进行鉴权
后台框架搭建
流程:创建项目和应用、定义模型、调用 API 存储数据、创建视图(编写 API 并处理数据)、暴露接口
创建 App:host_query
- 在manage.py统计目录下创建host-query目录:
python manage.py startapp host_query
- 在
host_query
中创建urls.py
1 | from django.urls import path |
- 下一步在根URLconf文件中指定我们创建 的
host_query.urls
模块.在/urls.py
文件的urlpatterns
列表里插入一个include()
。
1 | url(r'^host_query/', include('host_query.urls')) |
定义 Models:Business、Host
1 | from django.db import models |
创建 Tasks:
这部分主要是调用各个 api 进行数据读取,之后接入前端进行渲染
封装 api 请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class api_request:
def __init__(self, base_url, bk_app_code, bk_app_secret, access_token):
self.base_url = base_url
self.bk_app_code = bk_app_code
self.bk_app_secret = bk_app_secret
self.access_token = access_token
def get_headers(self):
auth_dict = {
"bk_app_code": self.bk_app_code,
"bk_app_secret": self.bk_app_secret,
"access_token": self.access_token,
}
return {"X-Bkapi-Authorization": json.dumps(auth_dict)}
api_request_instance = api_request(
base_url="<http://api/xxx>",
bk_app_code=os.environ.get('BKPAAS_APP_ID'),
bk_app_secret=os.environ.get('BKPAAS_APP_SECRET'),
access_token='xxx',
)
header = api_request_instance.get_headers调用业务查询接口查询业务并写入数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29#Business model
class Business(models.Model):
bk_biz_id = models.CharField(verbose_name="业务id", primary_key=True, max_length=255)
bk_biz_name = models.CharField(max_length=255, verbose_name="业务名", blank=True, null=True)
def __str__(self):
return self.bk_biz_name
# search all business
def get_business_info() -> list:
plus_url = "api/xxx/search_business/"
url = api_request_instance.base_url + plus_url
response = requests.post(url=url, json={"fields": ["bk_biz_id", "bk_biz_name"]}, headers=header).content
json_data = json.loads(response.decode("utf-8"))
return (json_data['data']['info'])
# add business to database
def add_business_to_database():
all_business = get_business_info()
create_businesses = []
for business in all_business:
create_businesses.append(
Business(bk_biz_id=business.get("bk_biz_id"), bk_biz_name=business.get("bk_biz_name"))
)
# print(create_businesses)
with transaction.atomic():
Business.objects.bulk_create(create_businesses)按照业务将对应的主机写入数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92# Host model
class Host(models.Model):
bk_host_id = models.CharField(verbose_name="主机id", primary_key=True, max_length=255)
bk_cloud_id = models.CharField(verbose_name="云区域", blank=True, max_length=255,null=True)
bk_biz_id = models.CharField(verbose_name="业务id", blank=True, max_length=255,null=True)
bk_os_type = models.CharField(verbose_name="操作系统类型", max_length=255, blank=True, null=True)
bk_host_innerip = models.GenericIPAddressField(verbose_name="内网IP", blank=True, null=True)
bk_mac = models.GenericIPAddressField(verbose_name="内网MAC地址", blank=True, null=True)
bk_set_id = models.CharField(verbose_name="集群ID", max_length=255, blank=True, null=True)
bk_set_name = models.CharField(verbose_name="集群名", max_length=2500, blank=True, null=True)
bk_module_id = models.CharField(verbose_name="模块ID", max_length=255, blank=True, null=True)
bk_module_name = models.CharField(verbose_name="模块名", max_length=255, blank=True, null=True)
def __str__(self):
return self.bk_host_innerip
# get host from business
def get_host_from_business(business_id, start, limit):
plus_url = "api/xxx/list_biz_hosts_topo"
url = api_request_instance.base_url + plus_url
response = requests.post(
url=url,
json={
"page": {"start": start, "limit": limit},
"bk_biz_id": f"{business_id}",
"fields": [
"bk_host_id",
"bk_cloud_id",
"bk_os_type",
"bk_host_innerip",
"bk_mac",
"bk_set_id",
"bk_set_name",
"bk_module_id",
"bk_module_name",
],
},
headers=header,
).content
json_data = json.loads(response.decode("utf-8"))
return json_data["data"]["info"], json_data["data"]["count"]
# add host to database
def add_host_to_database():
all_business_info = get_business_info()
for business_info in all_business_info:
start = 0
limit = 300
all_host_info, count = get_host_from_business(business_info.get('bk_biz_id'), start, limit)
start += limit
while count > start:
next_page, count = get_host_from_business(business_info.get('bk_biz_id'), start, limit)
all_host_info.extend(next_page)
start += 300
creating_hosts = []
bk_host_ids = [host["host"]["bk_host_id"] for host in all_host_info]
existing_host_ids = set(Host.objects.filter(bk_host_id__in=bk_host_ids).values_list("bk_host_id", flat=True))
for host_info in all_host_info:
bk_set_id = host_info["topo"][0]["bk_set_id"]
bk_set_name = host_info["topo"][0]["bk_set_name"]
bk_module_id = host_info["topo"][0]["module"][0]["bk_module_id"]
bk_module_name = host_info["topo"][0]["module"][0]["bk_module_name"]
host_info_host = host_info["host"]
creating_hosts_before = Host(
bk_host_id=host_info_host["bk_host_id"],
bk_cloud_id=host_info_host["bk_cloud_id"],
bk_biz_id=business_info["bk_biz_id"],
bk_os_type=host_info_host["bk_os_type"],
bk_host_innerip=host_info_host["bk_host_innerip"],
bk_mac=host_info_host["bk_mac"],
bk_set_id=bk_set_id,
bk_set_name=bk_set_name,
bk_module_id=bk_module_id,
bk_module_name=bk_module_name,
)
if host_info_host["bk_host_id"] not in existing_host_ids:
creating_hosts.append(creating_hosts_before)
Host.objects.bulk_create(creating_hosts)
编写 VIews,提供前端调用所需 api
需求
- 获取所有 Business
- 根据所选业务的
bk_biz_id
返回集群Set
列表 - 根据所选集群的
bk_set_id
返回模块Module
列表 - 并在下面显示满足三者条件的所有主机的所有字段信息,也许还需要调用一个 API 获得主机详细信息。
获取所有 Business:
/search_business
1
2
3
4
5
6
7
8# search business
def search_business(request):
business_objects = Business.objects.all()
business_list = [
{"bk_biz_id": business.bk_biz_id, "bk_biz_name": business.bk_biz_name} for business in business_objects
]
return(JsonResponse({"business": business_list}))获取对应 Set:
/search_set?bk_biz_id=xxx
1
2
3
4
5
6
7
8
9
10#search set
def search_set(request):
host_objects = Host.objects.all()
bk_biz_id = request.GET.get("bk_biz_id")
set_in_business = host_objects.filter(bk_biz_id=bk_biz_id).values_list("bk_set_name").distinct()
set_list = [
{"bk_set_name": set[0]} for set in set_in_business
]
return(JsonResponse({"set": set_list}))- 注意:最终return的格式是JSON,然后JSON里面蓄意嵌套 list,需要格式转换。
获取对应 Module:
/search_Module?bk_biz_id=xxx&bk_biz_id=xxx
1
2
3
4
5
6
7
8
9
10
11#search module
def search_module(request):
host_objects = Host.objects.all()
bk_biz_id = request.GET.get("bk_biz_id")
bk_set_id = request.GET.get("bk_set_id")
module_in_set = host_objects.filter(bk_biz_id = bk_biz_id, bk_set_id = bk_set_id).values_list("bk_module_name","bk_module_id").distinct()
module_list = [
{"bk_module_name": set[0],"bk_module_id": set[1] } for set in module_in_set
]
return(JsonResponse({"module": module_list}))显示对应主机:
/search_host
1
2
3
4
5
6
7
8
9
10
11def search_host(request):
host_object = Host.object.all()
bk_biz_id = request.GET.get("bk_biz_id")
bk_set_id = request.GET.get("bk_set_id")
bk_module_id = request.GET.get("bk_module_id")
if(bk_biz_id):
host_object = host_object.filter(bk_biz_id = bk_biz_id)
if(bk_set_id):
host_object = host_object.filter(bk_set_id = bk_set_id)
if(bk_module_id):
host_object = host_object.filter(bk_module_id = bk_module_id)
前端 API 调用
使用bk提供的 lesscode 进行前端开发
调用接口实例:调用 api 地址获取 JSON 数据,并赋值给对应 select,代码很简单,注意赋值给对应变量不要单纯只是return就行。
前置知识
Django 入门
安装过程
1 | >pip install djangop |
安装好Django并创建脚手架
跑起来
python manage.py runserver
文件目录
1 | demo01/ |
Django 中,每一个应用都是一个 Python 包,并且遵循着相同的约定。Django 自带一个工具,可以帮你生成应用的基础目录结构,这样你就能专心写代码,而不是创建目录了。
创建投票应用
在manage.py统计目录下创建polls目录
python manage.py startapp polls
出现文件结构
1 | polls // polls 应用目录 |
- 在polls/views.py中添加代码
1 | from django.http import HttpResponse |
- 接下来需要一个URL映射——URLconf
- 在polls中创建urls.py
1 | from django.urls import path |
- 下一步在根URLconf文件中指定我们创建 的polls.urls模块.在demo01/urls.py 文件的urlpatterns列表里插入一个include()。使用include()是为了即插即用
1 | path("polls/", include("polls.urls")), |
- 启动,结束。
设置数据库创建模型
- 打开
deemo01/settings.py
。这是个包含了 Django 项目设置的 Python 模块。
有关数据库代码:
1 | DATABASES = { |
如果需要修改成MySQL等,需要安装合适的 database bindings ,然后改变设置文件中
[DATABASES](<https://docs.djangoproject.com/zh-hans/5.0/ref/settings/#std-setting-DATABASES>)
'default'
项目中的一些键值[migrate](<https://docs.djangoproject.com/zh-hans/5.0/ref/django-admin/#django-admin-migrate>)
命令查看[INSTALLED_APPS](<https://docs.djangoproject.com/zh-hans/5.0/ref/settings/#std-setting-INSTALLED_APPS>)
配置,并根据mysite/settings.py
文件中的数据库配置和随应用提供的数据库迁移文件,创建任何必要的数据库表python manage.py migrate
创建模型
投票系统中需要创建两个模型:问题(描述、发布时间)和选项(选项描述、当前票数)
在polls/models.py中:
1
2
3
4
5
6
7
8
9from django.db import models
class Question(models.Model):
question_text = models.CharField(max_length=200)
pub_date = models.DateTimeField("date published")
class Choice(models.Model):
question = models.ForeignKey(Question,om_delete=models.CASCADE)
choice_text = models.CharField(max_length=200)
votes = models.IntegerField(default=0)
激活模型
- 在setting.py中加上polls的pollsconfig
1 | INSTALLED_APPS = [ |
- 执行:
python manage.py makemigrations polls
- 通过运行
makemigrations
命令,Django 会检测你对模型文件的修改(在这种情况下,你已经取得了新的),并且把修改的部分储存为一次 迁移。 - 使用:
python manage.py sqlmigrate polls 0001
查看具体sql语句 - 使用:
python manage.py migrate
在数据库里创建新定义的模型的数据表
初试API
python manage.py shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17from polls.models import Choice, Question # Import the model classes we just wrote.
all() Question.objects.
<QuerySet []>
from django.utils import timezone
"What's new?", pub_date=timezone.now()) q = Question(question_text=
# Save the object into the database. You have to call save() explicitly.
q.save()
# Now it has an ID.
id q.
1
# Access model field values via Python attributes.
q.question_text
"What's new?"
q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=datetime.timezone.utc)
Django管理页面
- 创建一个管理员账号(能登陆管理页面的账号):
python manage.py createsuperuser
按照流程设置账密 - 启动开发服务器:
python manage.py runserver
- 访问:
http://127.0.0.1:8000/admin/
直接看到后台,使用刚才用的账密登录 - 为问题
Question
设置一个后台接口。打开polls/admin.py
1 | from django.contrib import admin |
- 然后就可以在web端进行操作了!
创建视图
- 在我们的投票应用中,我们需要下列几个视图:
- 问题索引页——展示最近的几个投票问题。
- 问题详情页——展示某个投票的问题和不带结果的选项列表。
- 问题结果页——展示某个投票的结果。
- 投票处理器——用于响应用户为某个问题的特定选项投票的操作。
- 向polls/views.py 中添加更多视图,这些视图都要接收参数
1 | def detail(request, question_id): |
- 把这些新视图添加进
polls.urls
模块里
1 | path("<int:question_id>/", views.detail, name="detail"), |
- 对于视图的要求:Django 只要求返回的是一个
HttpResponse
,或者抛出一个异常。至于这个HttpResponse
可以是模板引擎,可以生成PDF、zip、XML等任何东西。
模板文件
- 创建如下路径:
polls/templates/polls/index.html
并写入数据
1 | {% if latest_question_list %} |
- 然后,更新一下
polls/views.py
里的index
视图来使用模板:
1 | from django.template import loader |
- 作用:载入
polls/index.html
模板文件,并且向它传递一个上下文(context)。这个上下文是一个字典,它将模板内的变量映射为 Python 对象
总结创建应用和数据模型流程
- 创建poll应用
python manage.py startapp polls
- 配置URLconf (路由)
- 定义数据
model
- 如
class Question(models.Model)
- 如
- 创建迁移文件
python manage.py makemigrations
- 执行后就成功创建 迁移脚本
- 进行数据库迁移
python manage.py migrate
- 创建用于登录后台管理的超级用户
python manage.py createsuperuser
- 配置后台管理接口
- 在
admin.py
中import
并regitser
- 在
- 在视图中渲染数据 (添加数据查询)
- 在视图(index)中添加:
context ={ "latest_question_list": latest_question_list,}
return HttpResponse(template.render(context, request))
- 在视图(index)中添加:
Serialize
前置准备
- 创建应用
django-admin.py startproject test_app
- 将新建的
snippets
app和rest_framework
app添加到settings.py
中的INSTALLED_APPS
- 创建
module
- 为
module
创建初始迁移并同步数据库
创建序列化类
开发我们的 Web API 的第一件事是为我们的 Web API 提供一种将代码片段实例序列化和反序列化为诸如json
之类的表示形式的方式。可以通过声明与 Django forms 非常相似的序列化器(serializers)来实现。 在test_app
的目录下创建一个名为serializers.py
文件,并添加内容:
1 | from rest_framework import serializers |
- 序列化器类的第一部分定义了序列化/反序列化的字段。
create()
和update()
方法定义了在调用serializer.save()
时如何创建和修改完整的实例。
使用ModelSerializer
- 先进入 Django shell 来熟悉一下
Serializer
类:python manage.py shell
1 | >from app_test.models import Snippet |
- REST framework包括
Serializer
类和ModelSerializer
类。我们来看看使用ModelSerializer
类重构我们的序列化类。
1 | # 重构 serializers.py 文件 |
ModelSerializer
类并不会做任何特别神奇的事情,它们只是创建序列化器类的快捷方式:
- 一组自动确定的字段。
- 默认简单实现的
create()
和update()
方法。
编写 Django 视图
基于视图
在
views.py
中添加代码,基于类的视图重构 API :1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23from app_test.models import Snippet
from app_test.serializers import SnippetSerializer
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
# 基于类的视图
class SnippetList(APIView):
"""
列出所有的snippets或者创建一个新的snippet。
"""
def get(self, request, format=None):
snippets = Snippet.objects.all()
serializer = SnippetSerializer(snippets, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = SnippetSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)根URL配置
/urls.py
文件中,添加我们的 app_test 应用的URL。1
2
3
4from django.conf.urls import url, include
urlpatterns = [
url(r'^', include('app_test.urls')),
]
基于混合(mixins)
使用基于类视图的最大优势之一是它可以轻松地创建可复用的行为。这些都是在REST框架的mixin类中实现的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16from app_test.models import Snippet
from app_test.serializers import SnippetSerializer
from rest_framework import mixins
from rest_framework import generics
class SnippetList(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
通过使用 mixin 类,我们使用更少的代码重写了这些视图,但我们还可以再进一步。REST框架提供了一组已经混合好(mixed-in)的通用视图。
1
2
3
4
5
6
7from app_test.models import Snippet
from app_test.serializers import SnippetSerializer
from rest_framework import generics
class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer这也就是我们在快速开始那里使用的视图了。
ModelSerializer
Serializer1
2
3
与常规的1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
相同,但提供了:
- 基于模型类⾃动⽣成⼀系列字段
- 基于模型类⾃动为 `Serializer` ⽣成 `validators`,⽐如 `unique_together`
- 包含默认的`create()`和`update()`的实现
------
### **Django REST Framework(DRF)**
#### 文档
> 中文文档:
>
> https://q1mi.github.io/Django-REST-framework-documentation/
#### RESTful API
> REST全称是Representational State Transfer,中⽂意思是表述(编者注:通常译为表征)性状态转移。 ⾸次出 现在2000年Roy Fielding的博⼠论⽂中。这种⻛格的理念认为后端开发任务就是提供数据的,对外提供的是数据资源的访问接⼝,所以在定义接⼝时,客户端访问的URL路径就表示这种要操作的数据资源。
- RESTful 是⼀种专⻔为Web 开发⽽定义API接⼝的设计⻛格,尤其适⽤于前后端分离的应⽤模式中。
- ⽽对于数据资源分别使⽤ POST、DELETE、GET、UPDATE 等请求动作来表达对数据的增删查改。
#### 快速入门
##### Serialize
首先需要定义一些序列化程序。在对应 app 下创建名为:`app/serializres.py` 的文件,用作我们的数据表示。也可以理解为⽤于保存该应⽤的序列化器
```python
# serializers.py
# 引入序列化模块
from rest_framework import serializers
# 引入自建模型
from .models import Host, Business
# 创建序列化类
class HostModelSerializer(serializers.ModelSerializer):
class Meta:
model = Host
fields = [
"bk_host_id",
···
"operator",
]
# 也可以直接:fields = "__all__"
class BusinessModelSerializer(serializers.ModelSerializer):
class Meta:
model = Business
fields = ["bk_biz_id", "bk_biz_name"]
model
指明该序列化器处理的数据字段从模型类Student参考⽣成fields
指明该序列化器包含模型类中的哪些字段,’all‘指明包含所有字段
Views
接下来创建在 views 中的视图,使用我们序列化的类创建 RESTful API
1 | # views.py |
queryset
指明该视图集在查询数据时使⽤的查询集serializer_class
指明该视图在进⾏序列化或反序列化时使⽤的序列化器
URLs
在app/urls.py
中开始定义路由 。
1 | from django.conf.urls import url, include |
因为我们使用的是viewsets而不是views,所以可以通过简单地使用路由器类注册视图来自动生成API的URL conf。
再次,如果我们需要对API URL进行更多的控制,我们可以简单地将其拉出来使用常规基于类的视图,并明确地编写URL conf。
最后,我们将包括用于支持浏览器浏览的API的默认登录和注销视图。这是可选的,但如果您的API需要身份验证,并且你想要使用支持浏览器浏览的API,那么它们很有用。
Setting
注册我们的应用
1 | INSTALLED_APPS = ( |
回显
API
调用 API
- 应用认证 - 用户认证 - 申请访问组件 API 的权限 - 调用组件 API
- 前面都不用管,直接看调用部分
- 使用组件SDK:
pip install bkapi-component-open
,也可以直接写进requirements.txt
生成 API
- 在 view 中编写接口页面
- 并正确配置路由
- 配置用户组和映射
- 前端调用封装的 api 获取数据渲染