上次,我们学到了Python,这是一种具有许多特性和库的编程语言。今天,我们将使用Python为网页生存HTML,并看看如何应用关注点分离。
几周前,我们学习了HTTP中的web请求,它可能是这样的:
GET / HTTP/1.1
Host: www.example.com
...
希望服务器的响应是这样的:
HTTP/1.1 200 OK
Content-Type: text/html
...
...
是页面的实际HTML今天,我们将使用一个微框架Flask,或者一组代码,它允许我们构建程序,而不需要一次又一次地编写共享或重复的代码。(例如,Bootstrap就是一个CSS框架。)
Flask是用Python编写的,是一组我们可以用Python编写web服务器的代码库。
一种组织web服务器代码的方法MVC,或模型-视图-控制器(Model-View-Controller):
今天,我们将建立一个网站,学生们可以在那里填写一张表格来注册新生即时通讯,也就是大一校内运动。
我们可以从打开CS50 IDE 开始,写一些Python代码,这是一个简单的web服务器程序,service.py
:
from http.server import BaseHTTPRequestHandler, HTTPServer
class HTTPServer_RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(b"<!DOCTYPE html>")
self.wfile.write(b"<html lang='en'>")
self.wfile.write(b"<head>")
self.wfile.write(b"<title>hello, title</title>")
self.wfile.write(b"</head>")
self.wfile.write(b"<body>")
self.wfile.write(b"hello, body")
self.wfile.write(b"</body>")
self.wfile.write(b"</html>")
port = 8080
server_address = ("0.0.0.0", port)
httpd = HTTPServer(server_address, HTTPServer_RequestHandler)
httpd.serve_forever()
http
库,但我们编写了自己的do_GET
函数,每次收到GET请求时都会调用该函数。像往常一样,我们需要查看库的文档,以了解我们应该编写什么,以及我们可以使用什么。首先,我们发送一个200状态码,并发送HTTP头,表明这是一个HTML页面。然后,逐行向响应中写入一些HTML(以ASCII字节的形式)。python service.py
,我们可以单击 CS50 IDE > Web Server,这将在另一个选项卡中为我们打开IDE的Web服务器,我们将看到刚刚编写的hello,world页面。我们可以看到,即使使用HTTP库,重新实现web服务器的许多通用功能也会变得很乏味,所以像Flask这样的框架在提供我们可以重用的抽象和快捷方式方面提供了很大帮助。
使用Flask,我们可以在application.py
文件中编写以下内容:
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route("/")
def index():
return "hello, world"
app = Flask(_name_)
,我们为application.py
文件初始化一个Flask应用程序。然后,我们使用@app.route("/")
语法来表示下面的函数将响应对/
或我们站点的根页面的任何请求。按照约定,我们将该函数成为index,它将只返回"hello,world"
作为响应,而不包含任何HTML。application.py
相同的文件夹中的终端中调用flask run
,结果URL将显示一个读取"hello,world"的页面(我们的浏览器即使没有HTML也会显示这个页面)。我们可以改变index
函数,以返回一个模板,或一个包含我们编写的HTML的文件,作为视图。
return render_template("index.html")
在templates
文件夹中,我们会有一个index.html
文件,内容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="initial-scale=1, width=device-width">
<title>hello</title>
</head>
<body>
hello,
</body>
</html>
我们看到了一个新特性,'',就像一个占位符。所以我们返回并改变index
的逻辑,我们的控制器,来检查URL中的参数并将它们传递给视图:
return render_template("index.html", name=request.args.get("name", "world"))
request.args.get
从请求的URL中获取一个名为name
的参数。(第二个参数world
将是未设置的默认返回值。)现在,我们可以参照/?name=David
在页面上看到"hello,David"。现在,我们可以生成无限多的页面,即使我们只写了几行代码。在froshims0
中,我们可以编写一个application.py
来接收和响应来自表单的POST请求:
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/register", methods=["POST"])
def register():
if not request.form.get("name") or not request.form.get("dorm"):
return render_template("failure.html")
return render_template("success.html")
对于默认页,我们将返回一个包含表单的index.html
:
{% extends "layout.html" %}
{% block body %}
<h1>Register for Frosh IMs</h1>
<form action="/register" method="post">
<input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
<select name="dorm">
<option disabled selected value="">Dorm</option>
<option value="Apley Court">Apley Court</option>
<option value="Canaday">Canaday</option>
<option value="Grays">Grays</option>
<option value="Greenough">Greenough</option>
<option value="Hollis">Hollis</option>
<option value="Holworthy">Holworthy</option>
<option value="Hurlbut">Hurlbut</option>
<option value="Lionel">Lionel</option>
<option value="Matthews">Matthews</option>
<option value="Mower">Mower</option>
<option value="Pennypacker">Pennypacker</option>
<option value="Stoughton">Stoughton</option>
<option value="Straus">Straus</option>
<option value="Thayer">Thayer</option>
<option value="Weld">Weld</option>
<option value="Wigglesworth">Wigglesworth</option>
</select>
<input type="submit" value="Register">
</form>
{% endblock %}
我们有一个HTML表单,其中一个input
标签供学生输入他们的名字,还有一个select
标签创建一个下拉列表供学生选择宿舍。我们的表单将被提交到我们调用/register
的路由,我们将使用POST方法发送表单的信息。
注意,我们的模板现在使用了一个新特性extends
来定义块,这些块将被替换到另一个文件layout.html
中:
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="initial-scale=1, width=device-width">
<title>froshims0</title>
</head>
<body>
{% block body %}{% endblock %}
</body>
</html>
{% block body %}{% endblock %}
语法是Flask中的占位符块,其他页面,如index.html
,可以提供将替换到该块中的HTML。在我们的register
函数中,我们将表明我们正在侦听一个POST请求,在函数中,确保我们获得了name
和dorm
的值。request.form
是Flask提供的一种抽象,这样我们就可以从请求的POST数据中访问参数。
当我们用flask run
运行应用程序并访问URL时,有时我们可能会看到一个Internal Server Error。如果我们回到运行Flask服务器的终端,我们将看到一条错误消息,它为我们提供了出错的线索。我们可以按Control+C来停止我们的web服务器,做一些有望修复错误的修改,然后重新启动我们的web服务器。即使我们做了改变,但并没有什么损坏,有时候我们也需要离开Flask,重新开始,它才会注意到这些变化。
在templates
目录中,我们还需要一份success.html
和failure.html
,看起来像这样:
{% extends "layout.html" %}
{% block body %}
You are registered! (Well, not really.)
{% endblock %}
register
函数将返回该值,并且模板已经完全呈现。layout.html
,我们不需要复制和粘贴相同的<head>
和其他共享标记,这使得我们可以更容易地一次对所有页面进行更改。失败页面也将共享相同的布局,但发送不同的消息:
{% extends "layout.html" %}
{% block body %}
You must provide your name and dorm!
{% endblock %}
{% %}
语法实际上被称为Jinja,这是一种Flask能够理解并组合在一起的模板语言。所有这些Python代码都位于我们的服务器CS50 IDE中,每次生成一个完整的HTML页面,并将其作为响应发送给浏览器。我们可以通过在Chrome中右键单击页面,点击查看源代码,看到用户将得到一个完整HTML。
现在让我们对提交的表单信息做一些实际的操作。在froshims1/application.py
中,我们将创建一个列表来存储所有注册的学生:
from flask import Flask, redirect, render_template, request
# Configure app
app = Flask(__name__)
# Registered students
students = []
@app.route("/")
def index():
return render_template("index.html")
@app.route("/registrants")
def registrants():
return render_template("registered.html", students=students)
@app.route("/register", methods=["POST"])
def register():
name = request.form.get("name")
dorm = request.form.get("dorm")
if not name or not dorm:
return render_template("failure.html")
students.append(f"{name} from {dorm}")
return redirect("/registrants")
我们创建一个空列表,student = []
,当我们在register
中得到一个name 和 drom 时,我们将使用 students.append(f"{name} from {dorm}")
将带有该name 和 drom 的格式化字符串添加到students
列表中。
在registrants
函数中,我们将把students
列表传递到regiestered.html
的模板中:
{% extends "layout.html" %}
{% block body %}
<ul>
{% for student in students %}
<li>{{ student }}</li>
{% endfor %}
</ul>
{% endblock %}
for
循环,以根据传递给模板的变量生存HTML。(我们需要一个endfor
,因为在HTML中,缩进只用于样式目的,所以我们需要指定循环何时结束。)这里,我们在controller application.py
传入的students
变量中为每个student
创建一个<li>
或字符串。请注意,列表的标记或格式在这个模板或视图中。如果我们停止服务器并重新启动它,我们将丢失我们收集的所有数据,因为只要我们的程序运行,students
变量就会被创建和存储。
在froshims2/application.py
中,我们使用了一个新库:
import os
import smtplib
from flask import Flask, render_template, request
# Configure app
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/register", methods=["POST"])
def register():
name = request.form.get("name")
email = request.form.get("email")
dorm = request.form.get("dorm")
if not name or not email or not dorm:
return render_template("failure.html")
message = "You are registered!"
server = smtplib.SMTP("smtp.gmail.com", 587)
server.starttls()
server.login("[email protected]", os.getenv("PASSWORD"))
server.sendmail("[email protected]", email, message)
return render_template("success.html")
smtplib
和Gmail的文档,我们可以找出以编程方式登录Gmail服务器所需的代码行,并从我们的表单向电子邮件地址发送电子邮件。我们还可以将注册数据保存到我们服务器上的CSV文件中,即使我们的服务器停止后也可以打开它:
from flask import Flask, render_template, request
import csv
app = Flask(__name__)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/register", methods=["POST"])
def register():
if not request.form.get("name") or not request.form.get("dorm"):
return render_template("failure.html")
file = open("registered.csv", "a")
writer = csv.writer(file)
writer.writerow((request.form.get("name"), request.form.get("dorm")))
file.close()
return render_template("success.html")
@app.route("/registered")
def registered():
file = open("registered.csv", "r")
reader = csv.reader(file)
students = list(reader)
return render_template("registered.html", students=students)
我们导入csv库,然后打开一个名为registered.csv
的文件进行附加或者读取。如果我们在register
路径中收到一个表单,我们将打开带有a
的文件以进行附加。然后,我们创建一个csv.writer
(基于库的文档),并使用writerow
函数将名称和宿舍写入文件。最后,我们将关闭文件。
registered
的路由将打开文件进行读取,并根据文件创建一个列表叫lists。然后,在registered.html
中,我们可以遍历列表中的每一个列表(每一行),并打印第一项(名称)和第二项(宿舍):
{% extends "layout.html" %}
{% block body %}
<h1>Registered</h1>
<ul>
{% for student in students %}
<li>{{ student[0] }} from {{ student[1] }}</li>
{% endfor %}
</ul>
{% endblock %}
使用我们下周将研究的语言SQL,我们将能够比使用CSV文件更轻松地处理数据。
在fromshims6/templates/index.html
中,我们在模板中使用JavaScript来立即检查输入:
{% extends "layout.html" %}
{% block body %}
<h1>Register for Frosh IMs</h1>
<form action="/register" method="post">
<input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
<select name="dorm">
<option disabled selected value="">Dorm</option>
<option value="Apley Court">Apley Court</option>
<option value="Canaday">Canaday</option>
<option value="Grays">Grays</option>
<option value="Greenough">Greenough</option>
<option value="Hollis">Hollis</option>
<option value="Holworthy">Holworthy</option>
<option value="Hurlbut">Hurlbut</option>
<option value="Lionel">Lionel</option>
<option value="Matthews">Matthews</option>
<option value="Mower">Mower</option>
<option value="Pennypacker">Pennypacker</option>
<option value="Stoughton">Stoughton</option>
<option value="Straus">Straus</option>
<option value="Thayer">Thayer</option>
<option value="Weld">Weld</option>
<option value="Wigglesworth">Wigglesworth</option>
</select>
<input type="submit" value="Register">
</form>
<script>
document.querySelector('form').onsubmit = function() {
if (!document.querySelector('input').value) {
alert('You must provide your name!');
return false;
}
else if (!document.querySelector('select').value) {
alert('You must provide your dorm!');
return false;
}
return true;
};
</script>
{% endblock %}
form
提交时调用,并检查input
和select
是否都有值。如果其中一个没有值,我们将创建一个警报并return false
以阻止提交表单。否则,如果两者都存在,我们的函数将return true
,允许浏览器提交表单。.js
文件中并包含它,但由于我们还没有很多行代码,我们可以决定将JavaScript代码直接包含在模板中。像React这样的框架会以特定的方式组织视图代码,比如HTML和JavaScript,这样我们就可以在更复杂的web应用程序中保持一致的模式。我么创建一个网站,让人们可以搜索以某个字符串开头的单词,就像我们可能希望自动完成一样。我们需要一个名为large
的文件,它是一个字典单词列表,在words0/application.py
中,我们将拥有:
from flask import Flask, render_template, request
app = Flask(__name__)
WORDS = []
with open("large", "r") as file:
for line in file.readlines():
WORDS.append(line.rstrip())
@app.route("/")
def index():
return render_template("index.html")
@app.route("/search")
def search():
words = [word for word in WORDS if word.startswith(request.args.get("q"))]
return render_template("search.html", words=words)
当我们的服务器启动时,我们将通过读取large
文件的每一行来创建一个WORDS
列表,使用rstrip
删除新行,并将其存储在我们的列表中。
在我们的index
函数中,我们将呈现index.html
,它只是一个表单。
{% extends "layout.html" %}
{% block body %}
<form action="/search" method="get">
<input autocomplete="off" autofocus name="q" placeholder="Query" type="text">
<input type="submit" value="Search">
</form>
{% endblock %}
get
方法,因为我们希望查询位于URL中。在我们的search
路径中,我们创建了一个列表,words
,它是我们的全局WORDS
列表(我们之前阅读过的)中每个以参数q的值开头的word
的列表。它相当于:
words = []
q = request.args.get("q")
for word in WORDS:
if word.startswith(q):
words.append(word)
search.html
,该模板将显示每个带有标记的单词。我们可以使用flask run
运行我们的服务器,当我们访问URL时,我们会看到一个表单,我们可以在其中输入一些东西。如果我们输入字母a
或b
,我们可以点击提交并被带到一个页面,其中包含我们字典中所有以a或b开头的单词。我们注意到我们的路由类似于/search?q=a
,尽管我们可以将q
(用于查询)更改为我们想要的任何内容。我们甚至可以使用q
的其他值更改URL,并查看显示的结果。
在words1
中,我们将使用JavaScript立即获得结果列表。在查看代码之前,我们可以通过在IDE中运行它推断示例的工作原理。我们可以访问该URL,并通过右键单击Chrome中的页面来使用开发者工具中的网络选项卡:
我们可以点击页面上的查看源代码,看到我们的页面在HTML之后又一点JavaScript:
<input autocomplete="off" autofocus placeholder="Query" type="text">
<ul></ul>
<script src="<https://code.jquery.com/jquery-3.3.1.min.js>"></script>
<script>
let input = document.querySelector('input');
input.onkeyup = function() {
$.get('/search?q=' + input.value, function(data) {
document.querySelector('ul').innerHTML = data;
});
};
</script>
input
元素,每次发生keyup
事件时,我们都想更改页面。当我们在输入框中按下一个键并松开时,keyup
事件就会发生。我们使用jQuery的$.get
函数在/search?q=
路由上向我们的服务器发出GET请求,并附加输入框的值。当我们取回一些data
时,$.get
函数将调用一个匿名函数(回调)来将我们页面上的ul
的innerHTML
设置为该data
。