自定义Web框架

跟着老师一点一点完善代码和文档结构,最终一个Web框架的雏形会显现出来,然后引出之后要学习完善的的Web框架。

Web框架的本质

Web服务端的本质就是一个socket服务端,而我们的浏览器就是socket客户端。浏览器发送的请求就是一次socket请求。一次请求获得一次响应,然后就断开,这是一个短连接。
下面是一个服务端的python代码,直接用socket,实现一个简单的Hello World:

import socket

def handle_request(conn):
    data = conn.recv(1024)  # 接收数据,随便收到啥我们都回复Hello World
    conn.send('HTTP/1.1 200 OK\r\n\r\n'.encode('utf-8'))  # 这是什么暂时不需要知道,客户端浏览器会处理
    conn.send('Hello World'.encode('utf-8'))  # 回复的内容,就是网页的内容

def main():
    # 先起一个socket服务端
    server = socket.socket()
    server.bind(('localhost', 8000))
    server.listen(5)
    # 然后持续监听
    while True:
        conn, addr = server.accept()  # 开启监听
        handle_request(conn)  # 将连接传递给handle_request函数处理
        conn.close()  # 关闭连接

if __name__ == '__main__':
    main()

然后打开本机的浏览器访问我们的Web服务。在地址栏输入 http://127.0.0.1:8000/http://localhost:8000/ 之后,浏览器上就会显示服务端返回的内容了。

WSGI

WSGI(Web Server Gateway Interface),是一种规范。它定义了使用python编写的web app与web server之间接口格式,实现web app与web server间的解耦。
对我们来说,就是我们不用写socket了,直接写Web服务。
python标准库提供的独立WSGI服务的模块称为wsgiref。

from wsgiref.simple_server import make_server

def RunServer(environ, start_response):
    # environ:封装了客户端发来的所有的数据
    # start_response:封装了要返回给用户的数据,响应头和响应状态
    start_response('200 OK', [('Content-Type', 'text/html')])
    # 返回给客户端的内容,用return返回。记得要转成bytes类型
    return ['<h1>Hello, web!</h1>'.encode('utf-8'), ]

if __name__ == '__main__':
    httpd = make_server('', 8000, RunServer)  # 创建服务,不需要我们自己创建socket服务了
    print("Serving HTTP on port 8000...")
    httpd.serve_forever()  # 运行服务

响应不同的url请求

通常我们的请求可能要访问的是网站中的某个页面,比如是这样的形式 http://localhost:8000/data 。这需要在服务端能够区别出不同的请求,然后给客户端返回不同的内容。
environ,封装了客户端发来的所有的数据,现在来找一下请求的数据具体在哪里。
在上面例子中的 RunServer 函数中添加断点,然后Debug模式启动。此时再用上面的url发起请求。
Python自动化开发学习18-Django基础篇

如上图,可以看到返回了两个值。展开 environ 后,可以找到 PATH_INFO 内存放的就是 '/data' ,也就是我们请求的页面。
修改一下上面的函数,在函数内判断一下 environ['PATH_INFO'] 的值,返回给客户端不同的结果:

from wsgiref.simple_server import make_server

def handle_data():
    return ['<h1>Hello, this is data.</h1>'.encode('utf-8'), ]

def handle_default():
    return ['<h1>Hello, web!</h1>'.encode('utf-8'), ]

def RunServer(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    current_url = environ['PATH_INFO']
    if current_url == '/data':
        # 处理方法单独写一个函数
        return handle_data()
    else:
        # 写一个默认的处理方法,比如也可以是返回404
        return handle_default()

if __name__ == '__main__':
    httpd = make_server('', 8000, RunServer)
    print("Serving HTTP on port 8000...")
    httpd.serve_forever()  # 运行服务

上面的结构还不是很好。如果请求的分支很多,这个if要写的很长。这里可以用字典的形式来存放,看着结构更加清楚。题外话,python没有case语句,这也是一个变通的方式

from wsgiref.simple_server import make_server

# 这里是返回页面的处理函数
def handle_data():
    return ['<h1>Hello, this is data.</h1>'.encode('utf-8'), ]

def handle_home():
    return ['<h1>You are at home.</h1>'.encode('utf-8'), ]

def handle_default():
    return ['<h1>Hello, web!</h1>'.encode('utf-8'), ]

# 写一个字典,里面是url和处理函数的对应关系
URL_DICT = {
    '/data': handle_data,
    '/home': handle_home,
    'default': handle_default
}

def RunServer(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    current_url = environ['PATH_INFO']
    if current_url in URL_DICT:
        func = URL_DICT[current_url]
    else:
        func = URL_DICT['default']
    if func:
        return func()
    else:
        return ['<h1>404</h1>'.encode('utf-8'), ]

if __name__ == '__main__':
    httpd = make_server('', 8000, RunServer)
    print("Serving HTTP on port 8000...")
    httpd.serve_forever()  # 运行服务

上面的字典(URL_DICT)里的key就是客户端请求的url,例子中是明确的对应关系。再复杂一点key值写成正则表达式,if再用正则匹配,就可以把一类请求匹配到一个处理函数中了。这么没继续深入。

分离出html文件

上面的例子中,html的代码是直接写在handle方法里的。实际操作中,这个html代码会很长,并且我们学习Web前端的时候,已经编写了完整的html页面文件。这里就需要把handle函数再改进一下,直接去读取对应的文件然后返回文件内容:

def handle_request():
    with open("index.html", mode='rb') as file:
        data = file.read()
    return [data, ]

既然已经分离出文件了,那么把所有html文件整理一下,统一存在在一个目录下,比如:View。

分离出handle代码

一个站点会有很多的页面,所以对应的handle函数也会有很多个。把所有的handle函数也单独拿出来,写到一个或几个模块中去。把这些模块也放到一个目录下,比如:Controller/account.py。用的时候导入模块,修改一下字典的函数名:

from Contraller import account

# 写一个字典,里面是url和处理函数的对应关系
URL_DICT = {
    '/data': account.handle_data,
    '/home': account.handle_home,
    'default': account.handle_default
}

返回动态数据

之前例子中返回客户端的都是固定的html,现在要动态的返回数据。一般数据是从数据库获取的,这里直接返回系统时间吧。修改html文件,加入一些特殊的标记,比如:“@time”。那么现在我们的 View/index.html 文件内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Hellow Web! It is @time.</h1>
</body>
</html>

然后修改我们的handle方法来处理这个特殊的标记:

import time

def handle_default():
    with open('View/index.html', mode='rb') as file:
        data = file.read()
        data = data.replace(b'@time', time.asctime().encode('utf-8'))
    return [data, ]

这里写死了一个@time的标记,实际使用中多数数据都是是读取数据库的,并且要读的东西也很多。获取动态数据的方法也要单独分离出来写成模块。并且也建一个专门的文件夹存放,比如:Model。

MVC框架

现在我们已经有了这样一个文档结构了:

web_server.py
├─Model
│  └─get_data.py
├─View
│  └─index.html
└─Contraller
    └─account.py

于是一个MVC(Model View Controller 模型-视图-控制器)的Web架构的模式就出来了。

MTV框架

另外还有一种,MTV(Model Templates View 模型-模板-视图)。也就是把MVC中的View分成了视图(展现哪些数据)和模板(如何展现)2个部分,而Contorller这个要素由框架自己来实现了,我们需要做的就是把(带正则表达式的)URL对应到视图就可以了,通过这样的URL配置,系统将一个请求发送到一个合适的视图。
在Python的世界中,基本(除了Pylons)都使用了MTV框架,包括Django。所以后面会重点学习Django。
另外,Flask这种微框架就不是一个MVC模式的,因为它没有提供Model,除非集成了SQLAlchemy之类的ORM进来。

Django基础

Python的WEB框架有 Django、Tornado、Flask 等多种,Django 相较与其他WEB框架其优势为:大而全,框架本身集成了ORM、模型绑定、模板引擎、缓存、Session等诸多功能。

安装

可以用pip3安装:

pip3 install django

或者也可以通过PyCharm来安装:File-->Settings-->Project 。
安装完后,看一下Scripts目录,也就是你的pip3程序所在的目录,多了2个django-admin程序。这个是帮我创建Django程序的。

创建Django项目

切换到你要创建Django项目的目录,执行django-admin程序,就可以完成创建:

django-admin.exe startproject [你的项目名称]

你的确认把Script加到环境变量里去了,否则得输入完整的路径,比如下面这样:

G:\PycharmProjects>D:\Python36\Scripts\django-admin.exe startproject mysite01

现在进入你的项目目录,启动你的项目,命令如下。默认参数是 127.0.0.1:8000 ,可以输入方框号中完整的IP和端口号来指定,比如:0.0.0.0:9001。

python manage.py runserver [127.0.0.1:8000]

然后可以打开浏览器访问你的测试页了,能成功访问则说明上面的步骤都正确了。
上面的步骤也可以通过PyCharm来完成,如果是照着上面用命令行来创建的,现在还需要把我们的项目加到PyCharm里面去。File-->Open 然后选择你项目的目录,如果按上面的例子那么就是 G:\PycharmProjects\mysite01 ,点OK完成。
直接通过PyCharm创建项目:文件-->新建项目,选择Django
Location 里修改一下项目的路径和项目名,默认项目名是untitled,改掉。
Project Interpreter 里选择python环境,可以新建一个环境,或者使用已有的环境。
More Settings 里默认会帮我们创建templates文件夹。如果需要,还可以把 Application name 填上,就是创建项目的同时创建一个app。
templates 和 app 在下面的 “##创建app“ 和 “##写一个登录页面” 里会讲
Python自动化开发学习18-Django基础篇

创建一个页面

在项目下新建一个文件夹存放我们的页面,文件夹下新建一个python程序如下:

from django.shortcuts import HttpResponse

def hello(request):
    return HttpResponse("Hello Web")

现在去修改一下原来urls.py文件,加入上面这个页面的对应关系:

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

from page import mypage  # 导入刚才写的程序

urlpatterns = [
    path('admin/', admin.site.urls),
    path('hello/', mypage.hello),  # 添加我们的对应关系
        path('', views.index),  # 如果要添加一个默认的主页,前面就写一个空字符串
]

添加2行代码,导入py文件,添加对应关系。现在再去重启一个Web服务。直接访问这次页面会返回 Page not found (404) 。在url后加上 hello 能返回程序中 return 的字符串。

创建app

通过创建app,相当于在网站下创建了一个相对独立的功能模块。我们可以创建多个app,为网站建立多个功能模块。创建的方法,是在命令行下输入命令:

python manage.py startapp [你的app的名字]

创建完之后,你的项目里又多了一个文件夹(app的名字),以及一些文件。其中views是给我放置处理函数的,把前面写的处理函数移动到这个文件里。然后再去 urls.py 里修改一下。
创建一个app,比如名字就叫cmdb:

python manage.py startapp cmdb

在 cmdb/views.py文件的后面追加我们的处理函数:

from django.shortcuts import render

# Create your views here.
# 上面是原来文件的内容,在下面添加我们的处理函数
from django.shortcuts import HttpResponse

def hello(request):
    return HttpResponse("Hello Web")

def home(request):
    return HttpResponse("<h1>Hello here is home</h1>")

修改urls.py里的对应关系:

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

from cmdb import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('home/', views.home),
    path('hello/', views.hello),
]

功能文件介绍

templates文件夹 :默认不会创建这个文件夹,不过使用PyCharm创建项目的时候会为我们创建它。存放我们的html文件。
setting.py :配置文件
url.py :url的对应关系
wsgi.py :遵循WSGI规范,默认用的是wsgiref。老师推荐上线的系统用 uwsgi 替换掉默认的,也是这里改。推荐 uwsgi + nginx 。
manage.py :管理Django程序。有很多功能通过命令行参数控制,比如前面用的 runserver 和 startapp。
app下的文件
migrations文件夹 :存放数据库操作的记录,是修改表结构的记录,不包括数据修改的记录。我们可以不用管这个文件夹里的内容
admin.py :Django提供的后台管理
apps.py :配置当前app的
models.py :创建数据库表结构,就是我们的ORM
tests.py :单元测试
views.py :业务代码
文件结构如下:

{project}
├─{app}
│  ├─migrations
│  │  └─__init__.py
│  │
│  ├─__init__.py
│  ├─admin.py
│  ├─apps.py
│  ├─models.py
│  ├─tests.py
│  └─views.py
│
├─{project}
│  ├─__init__.py
│  ├─settings.py
│  ├─urls.py
│  └─wsgi.py
│
├─templates
│  └─index.html  # 这里放我们自己写的页面
│
└─manage.py

{project} 指代项目名,{app} 指代app名。之后写文件路径路径的时候,也可能这么写

写一个登录页面

在app里创建页面,views.py 里不再返回字符串,而是去读取html文件。
创建html页面文件,templates/login.html :

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        label{
            width: 80px;
            text-align: right;
            display: inline-block;
        }
    </style>
</head>
<body>
<!--
这里注意"/login/"结尾的"/"。浏览器中我们不输最后一个"/",但是Django默认会帮我们加上
但是代码里的,不会自动添加。加不加最后的"/",对程序而言就是两个url。
我们在urls.py里写的是带"/"的,写不对会导致找不到url。
不过Django会判断出来是因为你的url最后没加"/",会提示你去settings.py里加一条APPEND_SLASH=False。
人家默认开启是有意义的,我们需要做的就是别忘记在url最后写上各个"/"
-->
<form action="/login/" method="post">
    <p>
        <label for="usernmae">用户名:</label>
        <input id="usernmae" type="text" />
    </p>
    <p>
        <label for="password">密码:</label>
        <input id="password" type="password" />
        <input type="submit" value="提交">
    </p>
</form>
</body>
</html>

{app}/views.py里写一个login方法:

from django.shortcuts import HttpResponse
def login(request):
    with open('templates/login.html', encoding='utf-8') as file:
        data = file.read()
    return HttpResponse(data)

记得修改一下{project}/urls.py添加对应关系,现在可以打开新页面看一下了。
一般都是打开文件返回文件内容,所以django给我们提供了一个 render 方法,直接返回文件内容,login方法改成下面这样:

from django.shortcuts import render  # 这行默认在文件第一行就自动帮我引入了,没有你就加上
 def login(request):
    # 现在可以直接读取文件返回文件内容了,一行搞定
    return render(request, 'login.html')

这里的文件直接写文件名就好了,不过现在测试的话,回显示 TemplateDoesNotExist at /login/ ,因为系统找不到文件。再去设置一下 {project}/settings.py 里的变量 TEMPLATES 。里面有个字典,设置前是这样的:

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],  # 这里加上我们的查找路径
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

DIRS的暂时是个空列表,加上templates的绝对目录 'DIRS': [os.path.join(BASE_DIR, 'templates')] 。这样就可以直接通过文件名找到 templates 目录下的文件了。

支持引入静态文件

网页中还需要引入css模板,jQuery文件等,现在尝试把这些加上。
建立一个文件夹 “static” 来存放这些静态文件。然后创建一个css文件 “commons.cc” 给body加一个背景色:

body{
    background-color: grey;
}

记得在页面的 head 里引入:

<link rel="stylesheet" href="/static/commons.css">

以默认的配置,系统是找不到我们的静态文件的,还需要在配置文件 settings.py 里条件配置:

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/

STATIC_URL = '/static/'
# 上面都是原来就有的内容,下面的STATICFILES_DIRS是我们要添加的
STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)

现在,就可以支持引入静态文件了,包括jQuery以及前端组件现在都能用了。
课上暂时没讲。上面的引入方式,还是有问题。现在通过Django是可以全部访问了,但是本地打开html文件的时候是找不到引入的文件的。把路径的前面加上 ".." 表示上一级目录,就可以两边兼顾了:

<link rel="stylesheet" href="../static/commons.css">

前端小结

总结一下上面的步骤
PyCharm创建项目:
如果用Pycharm创建,修改一下项目名称,确认是新建python环境还是用已有的。可以选择同时创建app。
命令行创建项目:
创建项目 django-admin.exe startproject [你的项目名称]
创建app python manage.py startapp [你的app的名字]
页面文件html:
把html文件放到templates文件夹下,PyCharm创建的时候会自动创建这个文件
静态文件:
创建一个static文件夹,放置我们的静态文件。html文件中类似这样引入

<link rel="stylesheet" href="../static/commons.css">

添加templates的文件夹路径:
PyCharm创建的项目会自动给我们加上,如果是用命令行创建的话,需要自己修改。
settings.py 文件的 TEMPLATES 变量 'DIRS': [os.path.join(BASE_DIR, 'templates')]
添加static的文件夹路径:
settings.py 文件的最后加上下面这些

STATICFILES_DIRS = (
    os.path.join(BASE_DIR, 'static'),
)

编辑处理函数 {app}/views.py :

from django.shortcuts import render  # 这行默认就在文件第一行

def login(request):
    return render(request, 'index.html')

添加对应关系:
修改urls.py的内容,添加对应关系

Django请求

下面以一个表单的示例来说明前端和后端之间的通信,提交和获取。

提交表单

现在页面已经可以打开了。然后看一下我们的form表单,form标签内的内容如下:

<form action="/login/" method="post">

action :是我们要提交表单的地址,这里要注意最后的"/",平时浏览器上我们有没有无所谓,系统会自动补上的,代码里就不能省了
method :是提交表单的方式,有post和get两种。我们浏览器输入地址访问也是get
现在用post提交会有问题,可以先换成get,甚至把action也改一下,看下效果:

<form action="/admin/" method="get">

但是换回post就是有问题,提示CSRF(跨站请求伪造)。先去settings.py里把这个设置关掉,如下:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

现在再提交就不会报CSRF了,由于提交的地址就是当前页面,所以目前能看到的效果只是页面的刷新。
通过F12进入开发者模式,看网络标签,提交页面后会刷出请求的服务器资源。右边可以展开显示详细的信息。可以看到请求URL、请求方法。如果你是get方法,请求方法就是GET,如果你是post方法,请求方法就是POST。下面还有请求头、响应头。
请求头里有个内容知道一下,你的系统和浏览器信息会在这里,如果是手机,这里也能看到手机的系统

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36 Edge/16.16299

修改一下我们的views.py文件中的方法,也能获取到用户请求的方法:

def login(request):
    # request 包含用户提交的所有信息
    # request.method 获取用户提交的方法
    print(request.method)
    return render(request, 'login.html')

request :包含用户提交的所有信息
request.method : 获取用户提交的方式 get 或 post
现在我们可以通过request.method判断出收到的是get请求还是post请求。如果是get就正常返回页面,如果是post就处理用户表单。
为了能获取到post的信息,还需要为input加上name属性。之前的input未设置name,现在需要加上:

<form action="/login/" method="post">
    <p>
        <label for="usernmae">用户名:</label>
        <input id="usernmae" name="user" type="text" />
    </p>
    <p>
        <label for="password">密码:</label>
        <input id="password" name="pwd" type="password" />
        <input type="submit" value="提交">
    </p>
</form>

解决了上面的问题,终于可以着手处理浏览器提交的信息了。
request.POST 就是用户提交的表单信息。可以把这个当成是一个字典来操作。上面已经为input加上的name属性,这个就是字典的key。推荐用字典的get方法来获取值,因为如果获取不到,默认是返回None不会报错。获取到用户名和密码后简单验证一下,然后如果正确就跳转到另外一个页面。页面跳转要用到 redirect 函数,还得在开头导入模块 from django.shortcuts import redirect ,然后login处理函数如下:

from django.shortcuts import render
from django.shortcuts import redirect

def login(request):
    # request 包含用户提交的所有信息
    # request.method 获取用户提交的方法
    # print(request.method)
    if request.method == 'POST':
        # 这里用字典的操作方法,用get方法获取值
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        # print(user, pwd)
        if user == 'steed' and pwd == '123':
            return redirect('http://blog.51cto.com/steed')
    return render(request, 'login.html')

redirect() 里面只能填url,就上例子中那样。如果需要跳转本站的地址,也可以,必须以 / 开头,这里开头的 / 就代指本机的地址,如:/login 就是一个本机的url。

返回数据

通过Django给客户端返回动态的数据,需要用到模板自己的语言,该语言可以实现数据展示。这里要用到模板语言中的变量
变量如下所示:{{ item }}。 当模板引擎遇到变量时,它将评估该变量并将其替换为结果。
这样在html中遇到用两个大括号包起来的形式,Django会把它替换成我们后端指定的内容。现在我们来输出错误信息,继续修改我们的login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="../static/commons.css">
    <style>
        label{
            width: 80px;
            text-align: right;
            display: inline-block;
        }
    </style>
</head>
<body>
<form action="/login/" method="post">
    <p>
        <label for="usernmae">用户名:</label>
        <input id="usernmae" name="user" type="text" />
    </p>
    <p>
        <label for="password">密码:</label>
        <input id="password" name="pwd" type="password" />
        <input type="submit" value="提交">
        <span style="color: red">{{ error_msg }}</span>
    </p>
</form>
</body>
</html>

上面是完整的文件内容,这次是在最后加了一个span标签,里面用来返回错误信息,错误信息用{{ error_msg }}代替。
然后继续去编辑之前的login处理函数,之前还没写认证失败的逻辑。现在加上并且替换到页面里的错误信息。这里还是用到之前的render方法。这个方法还有第三个参数,传入一个字典,key就是页面中需要替换的内容,value是我们要替换上去的内容。具体如下:

from django.shortcuts import render
from django.shortcuts import redirect

def login(request):
    # request 包含用户提交的所有信息
    # request.method 获取用户提交的方法
    # print(request.method)
    if request.method == 'POST':
        # 这里用字典的操作方法,用get方法获取值
        user = request.POST.get('user')
        pwd = request.POST.get('pwd')
        # print(user, pwd)
        if user == 'steed' and pwd == '123':
            return redirect('http://blog.51cto.com/steed')
        if user and user != 'steed':
            return render(request, 'login.html', {'error_msg': '用户名%s不存在' % user})
    return render(request, 'login.html')

get方法提交的时候,返回的值是没有写第三个参数的,页面里也看不到错误信息。这里Django也帮我么把要替换的内容默认设为空了。如果输入一个错误的用户名,这里也动态的把这个用户名返回到页面上了。
使用点(.)来访问变量的属性:
从技术上讲,当模板系统遇到点时,会按以下顺序尝试以下查找:

  • 字典查找
  • 属性或方法查找
  • 数字索引查找

所以还可以用点来访问到字典或列表中的元素。

返回表格数据-for标签

Django的模板语言还支持循环,这样我们可以方便的获取一系列的数据,比如字典和列表。模板语句中叫做标签
标签看起来像这样:{% tag %}。 一些标签需要开始和结束标签(即 {% tag %} ... 标签 内容 ... {% endtag %})。现在先重点来看一下for标签,循环数组中的每个项目。 例如,要显示athlete_list中提供的运动员列表:

<ul>
{% for item in item_list %}
    <li>{{ item.key }}</li>
{% endfor %}
</ul>

按照上面的格式写好我们的html文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <table border="1">
        <thead>
        <tr>
            <th>NAME</th>
            <th>AGE</th>
            <th>DEPT.</th>
        </tr>
        </thead>
        <tbody>
        <!--
        <tr>
            <td>Adam</td>
            <td>22</td>
            <td>IT</td>
        </tr>
        <tr>
            <td>Bob</td>
            <td>32</td>
            <td>IT</td>
        </tr>
        <tr>
            <td>Carmen</td>
            <td>30</td>
            <td>Sales</td>
        </tr>
        <tr>
            <td>David</td>
            <td>40</td>
            <td>HR</td>
        </tr>
        <tr>
            <td>Edda</td>
            <td>26</td>
            <td>HR</td>
        </tr>
        -->
        {% for row in members %}
        <tr>
            <td>{{ row.name }}</td>
            <td>{{ row.age }}</td>
            <td>{{ row.dept }}</td>
        </tr>
        {% endfor %}

        </tbody>
    </table>
</div>
</body>
</html>

表格的内容用html也写了一份,但是注释掉了。这里的标签使用的变量是 members ,然后标签内部是对members做遍历,一次取出name、age、dept。这是类似字典的操作。members是个列表,列表的成员是字典并且有nage、age、dept这些key。
现在要去写我们的处理函数,把表格的数据写进去。首先我们要有一份表格的数据,一般是去数据库里读取的。暂时先不做数据库操作,写一个列表(TAB_MEMBER)存放表格数据。然后同样是通过render来返回。这次把整个字典传进去。views.py 文件:

# 现在也不搞数据库,先创建这么一个列表来存数据
TAB_MEMBER = [
    {'name': 'Adam', 'age': 22, 'dept': 'IT'},
    {'name': 'Bob', 'age': 32, 'dept': 'IT'},
    {'name': 'Carmen', 'age': 30, 'dept': 'Sales'},
    {'name': 'David', 'age': 40, 'dept': 'HR'},
    {'name': 'Edda', 'age': 26, 'dept': 'HR'},
]

def table(request):
    return render(request, 'table.html', {'members': TAB_MEMBER})

现在可以去重启Web服务,打开新的页面,检查是否可以获取到表格的数据显示出来。

为表格添加数据

接下来为上面的表格添加数据,修改html文件的内容,在表格上面加上输入框填写数据以及一个添加按钮:

<div>
    <form action="/table/" method="post">
        <input type="text" name="name" placeholder="NAME" />
        <input type="text" name="age" placeholder="AGE" />
        <input type="text" name="dept" placeholder="DEPT." />
        <input type="submit" value="添加" />
    </form>
</div>
<!-- 在表格的上面添加这个div,下面是原来表格的代码 -->

通过上面的添加按钮,会把添加的数据通过post请求发送到Web服务端,最后会返回一个更新后的表格。
现在要修改处理函数,添加对post请求的处理:

def table(request):
    if request.method == 'POST':
        name = request.POST.get('name')
        age = request.POST.get('age')
        dept = request.POST.get('dept')
        tmp = {'name': name, 'age': age, 'dept': dept}
        TAB_MEMBER.append(tmp)
    return render(request, 'table.html', {'members': TAB_MEMBER})

最后都是返回列表的数据来生成表格,上面的做法是把收到的添加请求的内容生成一个新列表成员(字典)添加到 TAB_MEMBER 列表的末尾。
使用GET请求,上面用的是POST请求,也可以改成GET请求。把html中的form标签的methon的值改成get,就是发送get请求了。而处理函数可以用 request.method == 'GET' 做条件判断,取值也和POST的处理方法一样,使用 request.GET.get() 来获取请求的数据。对处理函数修改一下可以同时处理 get 和 post 请求:

def table(request):
    if request.method == 'POST':
        name = request.POST.get('name')
        age = request.POST.get('age')
        dept = request.POST.get('dept')
        tmp = {'name': name, 'age': age, 'dept': dept}
        TAB_MEMBER.append(tmp)
    elif request.method == 'GET'  and request.GET:
        name = request.GET.get('name')
        age = request.GET.get('age')
        dept = request.GET.get('dept')
        tmp = {'name': name, 'age': age, 'dept': dept}
        TAB_MEMBER.append(tmp)
    return render(request, 'table.html', {'members': TAB_MEMBER})

get请求也可以直接通过浏览器的地址栏发出。在url的后面加问号(?),然后拼接上请求的内容:

http://127.0.0.1:8000/table/?name=Frank&age=37&dept=Marketing

url和请求之间用 ? 分隔,字段之间用 & 分隔,字段左边是 key 右边是 value 中间是 = 。

选择性的返回数据-if标签

if标签的语法如下:

{% if 条件1 %}
    当条件1为 true 时的代码
{% elif 条件2 %}
    当条件2为 true 时的代码
{% else %}
    所有条件都不为 true 时的代码
{% endif %}

在上面的代码的基础上,修改一下html中的内容,在上面for循环里的加一层if,比如只显示age>30的条目:

<div>
    <table border="1">
        <thead>
        <tr>
            <th>NAME</th>
            <th>AGE</th>
            <th>DEPT.</th>
        </tr>
        </thead>
        <tbody>
        {% for row in members %}
        <tr>
            {% if row.age > 30 %}
            <td>{{ row.name }}</td>
            <td>{{ row.age }}</td>
            <td>{{ row.dept }}</td>
            {% endif %}
        </tr>
        {% endfor %}
        </tbody>
    </table>
</div>

Djang请求的生命周期

下面是一个请求的整个流程,括号里是我们的代码存放的位置
请求 <--> 路由系统(urls.py) <--> 视图函数(views.py) <--> DB、html(templates)、静态文件(static)

补充-locals()小技巧

locals()返回一个包含当前作用域里面的所有变量和它们的值的字典。

TAB_MEMBER = [
    {'name': 'Adam', 'age': 22, 'dept': 'IT'},
    {'name': 'Bob', 'age': 32, 'dept': 'IT'},
    {'name': 'Carmen', 'age': 30, 'dept': 'Sales'},
    {'name': 'David', 'age': 40, 'dept': 'HR'},
    {'name': 'Edda', 'age': 26, 'dept': 'HR'},
]

def table(request):
    members = TAB_MEMAER  # 首先,得有局部变量,所以这里这面赋值一下
    # return render(request, 'table.html', {'members': TAB_MEMBER})
     return render(request, 'table.html', locals())  # 使用locals的写法

好处就是,如果要返回的变量很多,不用一个一个写了。
坏处就是不管前端用不用,所有的局部变量都会返回。这个或许还有个进阶用法可以改善?

课后练习

主机管理:

  • 后台数据使用数据库,PyMySql 或者 SQLAlchemy 操作。
  • 一张主机管理数据表,至少8列。比如:主机名、IP地址、端口号、描述、状态等
  • 一张用户登录认证表,有用户名和密码。不要求操作和管理,就是用来登录验证的
  • 一个登录页面,验证通过跳转到下一个页面
  • 一个主机管理页面,可以用模板也可以自己写。至少3部分:头部内容、左侧菜单、中间显示我们的数据表格
    • 查看所有的主机信息,信息只需要显示部分数据,4列数据即可。其余的不用显示出来,用下面的查看详细显示出来
    • 可以添加主机,就是添加一行数据,用模态对话框
    • 查看详细,点击打开一个新页面,显示当前主机的所有信息。tips:写一个a标签
      <a href="/detail?hid={{ row.id }}">详细</a>

      这个就是发起一个get请求了,然后去写一个detail的页面和处理函数。

    • 删除主机,就是删除一行。tips:还是写一个a标签
      <a class="delete" href="#" hid="{{ row.id }}">删除</a>

      上面的href也可以不写,这里内容是#同样是不跳转,但是会自动有一个超链接的样式(鼠标变小手,字体变蓝色有下划线),如果不写href就和一个span标签一样了。
      目前只会通过form表单来提交POST数据,所以删除得做一个模态框,里面写一个form表单。通过js获取到id填到表单里。最后通过submit提交到一个delete的处理函数,完成后返回(redirect)回来。