C语言 - 学习笔记
Harvard CS50 计算机科学概论
Harvard CS50 计算机科学概论
  • Prologue
  • WEEK 0 Introduction
  • WEEK 1 C
  • WEEK 2 Arrays
  • WEEK 3 Algorithms
  • WEEK 4 Memory
  • WEEK5 Data Structures
  • WEEK6 Python
  • WEEK7 SQL
  • WEEK8 HTML, CSS, JavaScript
  • WEEK9 Flask
  • C语言总结
Powered by GitBook
On this page
  • 1. Web编程
  • 2. Flask
  • 2.1 app.py
  • 2.2 templates/
  • 2.3 运行程序
  • 2.4 Form
  • 2.5 Layouts
  • 3. POST
  • 4. MVC
  • 5. 保存数据
  • 6. 发送邮件
  • 7. Sessions
  • 7.1 Cookie
  • 7.2 flask_session
  • 8. AJAX

WEEK9 Flask

1. Web编程

JavaScript能够让网页实现动态/交互,但是网页背后的数据是静态的(比如之前的apache2或者http-server),网页并不能动态处理用户的输入,返回不同的结果。

因此后端语言在网页开发中必不可少,Python也可以用于网页开发。

一个URL后面可以是Path:

http://www.exmaple.com/path

Path指的是一个主机真实存在的路径。

一个URL后面也可以是Route:

http://www.exmaple.com/route

Route指的是一个虚拟的路径。

An HTTP request for a route with inputs might look like:

GET /search?q=cats HTTP/1.1
Host: www.google.com
...

Now, we need a web server that can parse, or analyze, HTTP request headers and return different pages based on the route.

2. Flask

Flask是一个网络编程的框架,封装了很多方法,可以自动建立TCP/UDP连接,解析并处理HTTP请求,发送HTTP回复报文。

当然也可以自己从头写如何让建立TCP连接,如何Parse HTTP请求,但是显然这样很费时间,也完全没有必要,使用框架可以让编程者专注于业务逻辑,而非底层的网络传输。

Flask程序包含以下几个部分:

  • app.py: Application

  • requirements.txt: Required libraries

  • static/: Static files

  • templates/: HTMLs

2.1 app.py

from flask import Flask, render_template, request

app = Flask(__name__)


@app.route("/")
def index():
    name = request.args.get("name")
    return render_template("index.html", name=name)

2.2 templates/

index.html文件:

<html lang="en">
    <head>
        <meta name="viewport" content="initial-scale=1, width=device-width">
        <title>hello</title>
    </head>
    <body>
        hello, {{ name }}
    </body>
</html>

2.3 运行程序

先设置环境变量:

export FLASK_APP=app.py # 默认值,可以不设置
export FLASK_DEBUG=1 # 默认值,可以不设置

使用flask run运行程序:

ubuntu@c-test-node:~/C/w9/flask_demo$ ls
app.py  tmplates
ubuntu@c-test-node:~/C/w9/flask_demo$ flask run
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

打开网页:

ubuntu@c-test-node:~/C/w9/flask_demo$ curl localhost:5000/?name=Ray
<!DOCTYPE html>
  
<html lang="en">
    <head>
        <meta name="viewport" content="initial-scale=1, width=device-width">
        <title>hello</title>
    </head>
    <body>
        hello, Ray
    </body>
</html>

2.4 Form

使用Form,让用户提交信息,然后跳转到另一个网页。

<body>
  <form action="/greet" method="get">
    <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
    <input type="submit">
  </form>
</body>

后端:

@app.route("/greet")
def greet():
    name = request.args.get("name")
    return render_template("greeting.html", name=name)

需要注意的是,尽管HTML可以设置Tag的限制条件(比如设置非空),但是由于其他用户可以通过修改HTML文件,所以在后端接受数据的时候,必须要重新检查数据的有效性,不能假设数据的Integrity。

任何时候都不要相信用户!

2.5 Layouts

模版语言为Jinja。

HTML模版可以使用继承:一个HTML文档继承一个HTML的结构,然后可以补充某些Tag中的内容。

比如,先已经有layout.html:

<!DOCTYPE html>

<html lang="en">
    <head>
        <title>hello</title>
    </head>
    <body>
        

<div data-gb-custom-block data-tag="block"></div>

    </body>
</html>

可以让index.html继承这个文档:


<div data-gb-custom-block data-tag="extends" data-0='layout.html'></div>

<div data-gb-custom-block data-tag="block">

    <form action="/greet" method="post">
        <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
        <input type="submit">
    </form>

</div>

可以让greeting.html继承这个文档:


<div data-gb-custom-block data-tag="extends" data-0='layout.html'></div>

<div data-gb-custom-block data-tag="block">

    hello, {{ name }}

</div>

还可以使用循环:

from flask import Flask, render_template, request

app = Flask(__name__)

SPORTS = [
    "Basketball"
    "Soccer",
    "Ultimate Frisbee"
]


@app.route("/")
def index():
    return render_template("index.html", sports=SPORTS)

...
...
<select name="sport">
    

<div data-gb-custom-block data-tag="for">

  		<input name="sport" type="checkbox" value="{{ sport }}"> {{ sport }}
  	

</div>

</select>
...

3. POST

使用Post请求,而非GET,可以隐藏提交的数据(而不是显示在URL中)。

POST一般都是配合Form使用。

Form可以支持使用POST提交:

<form action="/greet" method="post">
  ...
</form>

Flask也支持Post:

@app.route("/greet", methods=["POST"])
def greet():
    return render_template("greet.html", name=request.form.get("name", "world"))

注意在解析参数的时候需要使用request.form。

为什么不只使用POST?

GET和POST的区别:

  1. GET请求,用于请求数据,无副作用的,是幂等的,且可缓存,请求的数据会附加在URL之后,以?分割URL和传输数据,多个参数用&连接。URL的编码格式采用的是ASCII编码,而不是Unicode,即是说所有的非ASCII字符都要编码之后再传输。

  2. GET提交有数据大小的限制,一般是不超过1024个字节,而这种说法也不完全准确,HTTP协议并没有设定URL字节长度的上限,而是浏览器做了些处理,所以长度依据浏览器的不同有所不同;POST请求在HTTP协议中也没有做说明,一般来说是没有设置限制的,但是实际上浏览器也有默认值。总体来说,少量的数据使用GET,大量的数据使用POST。

  3. POST请求:用于提交数据,有副作用,非幂等,不可缓存,POST请求会把请求的数据放置在HTTP请求包的包体中。上面的item=bandsaw就是实际的传输数据。 复用形低,很难嵌入到其他应用中。

因此,GET请求的数据会暴露在地址栏中,所以安全性比较低,比如密码是不能暴露的,就不能使用GET请求,而POST请求中,请求参数信息是放在请求头的,所以安全性较高,可以使用。在实际中,涉及到登录操作的时候,尽量使用HTTPS请求,安全性更好。而POST请求则不会。

4. MVC

The Flask framework implements a particular paradigm, or way of thinking and programming. This paradigm, also implemented by other frameworks, is known as MVC, or Model–view–controller:

  • The controller contains our “business logic”, code that manages our application overall, given user input. In Flask, this will be our Python code in app.py.

  • The view includes templates and visuals for the user interface, like the HTML and CSS that the user will see and interact with.

  • The model is our application’s data, such as a SQL database or CSV file, which we haven’t yet used.

5. 保存数据

可以直接储存在内存中(Python程序):

...
REGISTRANTS = {}
...
@app.route("/register", methods=["POST"])
def register():

    # Validate name
    name = request.form.get("name")
    if not name:
        return render_template("error.html", message="Missing name")

    # Validate sport
    sport = request.form.get("sport")
    if not sport:
        return render_template("error.html", message="Missing sport")
    if sport not in SPORTS:
        return render_template("error.html", message="Invalid sport")

    # Remember registrant
    REGISTRANTS[name] = sport

    # Confirm registration
    return redirect("/registrants")

可以直接储存在数据库中:

...
db = SQL("sqlite:///froshims.db")
...
@app.route("/register", methods=["POST"])
def register():

    # Validate submission
    name = request.form.get("name")
    sport = request.form.get("sport")
    if not name or sport not in SPORTS:
        return render_template("failure.html")

    # Remember registrant
    db.execute("INSERT INTO registrants (name, sport) VALUES(?, ?)", name, sport)

    # Confirm registration
    return redirect("/registrants")
  
@app.route("/deregister", methods=["POST"])
def deregister():

    # Forget registrant
    id = request.form.get("id")
    if id:
        db.execute("DELETE FROM registrants WHERE id = ?", id)
    return redirect("/registrants")

6. 发送邮件

We can even email users with another library, flask_mail。

# Implements a registration form, confirming registration via email

import os
import re

from flask import Flask, render_template, request
from flask_mail import Mail, Message

app = Flask(__name__)

# Requires that "Less secure app access" be on
# https://support.google.com/accounts/answer/6010255
app.config["MAIL_DEFAULT_SENDER"] = os.environ["MAIL_DEFAULT_SENDER"]
app.config["MAIL_PASSWORD"] = os.environ["MAIL_PASSWORD"]
app.config["MAIL_PORT"] = 587
app.config["MAIL_SERVER"] = "smtp.gmail.com"
app.config["MAIL_USE_TLS"] = True
app.config["MAIL_USERNAME"] = os.environ["MAIL_USERNAME"]
mail = Mail(app)

...

It turns out that we can provide configuration details like a username and password and mail server, in this case Gmail’s, to the Mail variable, which will send mail for us.

In our register route, we send an email to the user with the mail.send() function from the flask_mail library:

@app.route("/register", methods=["POST"])
def register():

    # Validate submission
    name = request.form.get("name")
    email = request.form.get("email")
    sport = request.form.get("sport")
    if not name or not email or sport not in SPORTS:
        return render_template("failure.html")

    # Send email
    message = Message("You are registered!", recipients=[email])
    mail.send(message)

    # Confirm registration
    return render_template("success.html")

To include the libraries we need, we’ll write a requirements.txt file with:

Flask
Flask-Mail

7. Sessions

7.1 Cookie

Sessions are how web servers remembers information about each user, which enables features like allowing users to stay logged in, and saving items to a shopping cart. These features require our server to be stateful, or having access to additional state, or information. HTTP on its own is stateless, since after we make a request and get a response, the interaction is completed.

It turns out that servers can send another header in a response, called Set-Cookie:

HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: session=value
...

Cookies are small pieces of data from a web server that the browser saves for us. In many cases, they are large random numbers or strings used to uniquely identify and track a user between visits.

In this case, the server is asking our browser to set a cookie for that server, called session to a value of value.

Then, when the browser makes another request to the same server, it’ll send back the same cookie that the same server has set before:

GET / HTTP/1.1
Host: gmail.com
Cookie: session=value

通过这个方法能实现长久登陆。

7.2 flask_session

In Flask, we can use the flask_session library to help manage this for us:

app.py:

from flask import Flask, redirect, render_template, request, session
from flask_session import Session

# Configure app
app = Flask(__name__)

# Configure session
app.config["SESSION_PERMANENT"] = False
app.config["SESSION_TYPE"] = "filesystem"
Session(app)


@app.route("/")
def index():
    if not session.get("name"):
        return redirect("/login")
    return render_template("index.html")


@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        session["name"] = request.form.get("name")
        return redirect("/")
    return render_template("login.html")


@app.route("/logout")
def logout():
    session["name"] = None
    return redirect("/")

login.html:


<div data-gb-custom-block data-tag="extends" data-0='layout.html'></div>

<div data-gb-custom-block data-tag="block">

<form action="/login" method="post">
  <input autocomplete="off" autofocus name="name" placeholder="Name" type="text">
  <input type="submit" value="Log In">
</form>

</div>

index.html:


<div data-gb-custom-block data-tag="extends" data-0='layout.html'></div>

<div data-gb-custom-block data-tag="block">

    

<div data-gb-custom-block data-tag="if" data-0='name' data-1='name'>

        You are logged in as {{ session["name"] }}. <a href="/logout">Log out</a>.
    

<div data-gb-custom-block data-tag="else"></div>

        You are not logged in. <a href="/login">Log in</a>.
    

</div>

</div>

We’ll configure the session library to use the server’s filesystem, and use session like a dictionary to store a user’s name. It turns out that Flask will use HTTP cookies for us, to maintain this session variable for each user visiting our web server.

用Development Tool查看网络过程:

  1. 默认回到Login界面。这里会提交一个表格。

  2. 登陆

    • 浏览器提交表格,发送POST请求。

    • Flask生成一个Cookie,在这个Cookie对应的session中会记录<"name", "Ray">这个KV对,这个KV对会写进硬盘(因为之前规定了app.config["SESSION_TYPE"] = "filesystem"),并返回这个Cookie。

      302 Found代表临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI。

    • 浏览器根据302回复,发送GET请求,希望跳转到/。这个请求会包含之前的Cookie信息。

    • 由于这个Cookie对应的session中已经存在name这个K,Flask会返回index.html。

  3. Every user will be logged in with a different session, and we can see them in the flask_session directory:

    ubuntu@c-test-node:~/C/w9/login$ ls -l flask_session/
    total 8
    -rw------- 1 ubuntu ubuntu  9 Sep  2 02:25 2029240f6d1128be89ddc32729463129
    -rw------- 1 ubuntu ubuntu 32 Sep  2 02:25 ae9980280505093c6e25b56b355faa61

由于Flask和浏览器都保存了这个Cookie在硬盘,因此,就算浏览器重启或者Flask重启,浏览器发送数据的时候会携带Cookie,Flask也会根据这个Cookie寻找对应的Session文件,发现文件里已经有<"name", "Ray">这个KV对,用户便不需要再登陆了。

总结:

  • 每当服务端使用Session的时候,如果浏览器没有发送Cookie,或者Cookie不存在,客户端都会生成一个新的Cookie,然后添加到Set-Cookie中,返回给浏览器。之后浏览器发送请求时候,会携带最新的Cookie。

  • 服务端会根据Cookie查找对应的Session文件,Session可以理解为一系列KV对,浏览器的某些请求信息会储存在Session文件中。

8. AJAX

Up until now, our interaction with JavaScript has been mostly limited to: push a button, something happens.

We still don't have to entirely reload our page, but there is still some degree of user interaction.

Ajax (formerly Asynchronous JavaScript and XML) allows us to dynamically update a webpage even more dynamically.

Central to our ability to asynchronously update our pages is to make use of a special JavaScript object called an XMLHttpRequest.

var xhttp = new XMLHttpRequest();

After obtaining your new object, you need to define its onreadystatechange behavior.

  • This is a function (typically an anonymous function) that will be called when the asynchronous HTTP request has completed, and thus typically defines what is expected to change on your site.

XMLHttpRequests have two additional properties that are used to detect when the page finishes loading.

  • The readyState property will change from from 0 (request not yet initialized) to 1, 2, 3, and finally 4 (request finished, response ready).

  • The status property will (hopefully!) be 200 (OK).

Then just make your asynchronous request using the open() method to define the request and the send() method to actually send it.

例子:

function ajax_request(argument){
  var aj = new XMLHttpRequest();
	aj.onreadystatechange = function (){
		if (aj.readyState ==4 && aj.status ==200){
      // TODO
    }
  };
aj.open("GET", /* url */, true);  
aj.send();00
};

使用JavaScript可以在后台和服务端进行交互(比如一个网页没有刷新,但是Network监控页面限制一直有文件传输),这些交互可以是基于时间,也可以于基于事件的。

例如,当用户在输入框里输入的时候,自动完成补全。

JS代码如下:

let input = document.querySelector('input');
input.addEventListener('input', async function() {
  let response = await fetch('/search?q=' + input.value);
  let shows = await response.json();
  let html = '';
  for (let id in shows) {
    let title = shows[id].title.replace('<', '&lt;').replace('&', '&amp;');
    html += '<li>' + title + '</li>';
  }
  document.querySelector('ul').innerHTML = html;
});

fetch('/search?q=' + input.value)会在后台发送GET请求。

Flask设置了这个路由:

@app.route("/search")
def search():
  shows = db.execute("SELECT * FROM shows WHERE title LIKE ?", "%" + request.args.get("q") + "%")
  return render_template("search.html", shows=shows)

会从数据库里查找数据,然后生成模版:


<div data-gb-custom-block data-tag="extends" data-0='layout.html'></div>

<div data-gb-custom-block data-tag="block">

    <ul>
        

<div data-gb-custom-block data-tag="for">

            <li>{{ show["title"] }}</li>
        

</div>

    </ul>

</div>

或者直接生成JSON(不使用模版):

def search():
  q = request.args.get("q")
  if q:
    shows = db.execute("SELECT * FROM shows WHERE title LIKE ? LIMIT 50", "%" + q + "%")
  else:
    shows = []
  return jsonify(shows)

JS收到请求后,更新当前现实的HTML文档。

PreviousWEEK8 HTML, CSS, JavaScriptNextC语言总结

Last updated 2 years ago