hav-cs50-merge-10

哈佛 CS50 中文官方笔记(十一)

第三讲

原文:cs50.harvard.edu/web/notes/3/

  • 介绍

  • Web 应用程序

  • HTTP

  • Django

  • 路由

  • 模板

    • 条件语句:

    • 样式

  • 任务

  • 表单

    • Django 表单
  • 会话

介绍

  • 到目前为止,我们已经讨论了如何使用 HTML 和 CSS 构建简单的网页,以及如何使用 Git 和 GitHub 来跟踪代码更改并与他人协作。我们还熟悉了 Python 编程语言。

  • 今天,我们将使用 Python 的Django框架来创建动态应用程序。

Web 应用程序

到目前为止,我们编写的所有 Web 应用程序都是静态的。这意味着每次我们打开那个网页时,它看起来都完全一样。然而,我们每天访问的许多网站在每次访问时都会发生变化。例如,如果你访问了《纽约时报》(https://www.nytimes.com/)或 Facebook(https://www.facebook.com/),你今天看到的内容很可能与明天不同。对于像这些大型网站,员工每次更改时手动编辑大型 HTML 文件是不合理的,这就是动态网站非常有用的地方。动态网站是利用编程语言(如 Python)动态生成 HTML 和 CSS 文件的网站。在本讲中,我们将学习如何创建我们的第一个动态应用程序。

HTTP

HTTP,或超文本传输协议,是一种广泛接受的协议,用于在互联网上传输消息。通常,在线信息是在客户端(用户)和服务器之间传递的。客户端和服务器

在此协议中,客户端将向服务器发送一个请求,可能看起来像下面的示例。在下面的示例中,GET只是一个请求类型,我们将在本课程中讨论的三种类型之一。/通常表示我们正在寻找网站的首页,而三个点表示我们还可以传递更多信息。请求

在收到请求后,服务器将发送一个 HTTP 响应,可能看起来像下面的示例。这样的响应将包括 HTTP 版本、状态码(200 表示 OK)、内容描述以及一些附加信息。响应

200 只是许多状态码中的一个,其中一些你可能以前见过:代码

Django

Django 是一个基于 Python 的 Web 框架,它将允许我们编写动态生成 HTML 和 CSS 的 Python 代码。使用像 Django 这样的框架的优势在于,已经为我们编写了很多代码,我们可以利用这些代码。

  • 要开始,我们必须安装 Django,这意味着如果您还没有这样做,您还必须安装 pip

  • 一旦您安装了 Pip,您可以在终端中运行 pip3 install Django 来安装 Django。

在安装 Django 后,我们可以通过以下步骤创建一个新的 Django 项目:

  1. 运行 django-admin startproject PROJECT_NAME 以创建我们项目的一些起始文件。

  2. 运行 cd PROJECT_NAME 以进入您的新项目目录。

  3. 在您选择的文本编辑器中打开该目录。您会注意到已经为您创建了某些文件。现在我们不需要查看这些文件中的大多数,但有三件从开始起就非常重要:

    • manage.py 是我们在终端上执行命令时使用的。我们不需要编辑它,但我们会经常使用它。

    • settings.py 包含了我们新项目的一些重要配置设置。有一些默认设置,但我们可能希望不时地更改其中的一些。

    • urls.py 包含了用户在导航到特定 URL 后应被路由到的指示。

  4. 通过运行 python manage.py runserver 启动项目。这将打开一个开发服务器,您可以通过访问提供的 URL 来访问它。这个开发服务器是在您的机器上本地运行的,这意味着其他人无法访问您的网站。这应该会带您到一个默认的着陆页:着陆页

  5. 接下来,我们必须创建一个应用。Django 项目分为一个或多个应用。我们的大多数项目只需要一个应用,但较大的网站可以利用这种将网站拆分为多个应用的能力。要创建一个应用,我们运行 python manage.py startapp APP_NAME。这将创建一些额外的目录和文件,这些文件将很快变得有用,包括 views.py

  6. 现在,我们必须安装我们的新应用。为此,我们进入 settings.py,向下滚动到 INSTALLED_APPS 列表,并将我们新应用的名称添加到该列表中。已安装应用

路由

现在,为了开始我们的应用:

  1. 接下来,我们将导航到 views.py。这个文件将包含多个不同的视图,我们可以将视图现在视为用户可能希望看到的一页。为了创建我们的第一个视图,我们将编写一个接受 request 的函数。现在,我们将简单地返回一个 HttpResponse(一个非常简单的响应,包括一个 200 的响应代码和一个可以在网页浏览器中显示的文本字符串)。为了做到这一点,我们包含了 from django.http import HttpResponse。我们的文件现在看起来像:

     from django.shortcuts import render
     from django.http import HttpResponse
    
     # Create your views here. 
     def index(request):
         return HttpResponse("Hello, world!") 
    
  2. 现在,我们需要以某种方式将我们刚刚创建的视图与一个特定的 URL 关联起来。为此,我们将在与 views.py 相同的目录中创建另一个名为 urls.py 的文件。我们已经有了一个整个项目的 urls.py 文件,但最好为每个单独的应用程序都保留一个。

  3. 在我们的新 urls.py 中,我们将创建一个用户在使用我们的网站时可能会访问的 URL 模式列表。为了做到这一点:

    1. 我们必须做一些导入:from django.urls import path 将给我们重定向 URL 的能力,而 from . import views 将导入我们在 views.py 中创建的任何函数。

    2. 创建一个名为 urlpatterns 的列表

    3. 对于每个期望的 URL,向 urlpatterns 列表中添加一个项目,该项目包含对 path 函数的调用,该函数有两个或三个参数:一个表示 URL 路径的字符串,一个在访问该 URL 时希望调用的 views.py 中的函数,以及(可选的)该路径的名称,格式为 name="something"。例如,这就是我们简单的应用程序现在看起来像:

     from django.urls import path
     from . import views
    
     urlpatterns = [
         path("", views.index, name="index")
     ] 
    
  4. 现在,我们已经为这个特定应用程序创建了一个 urls.py 文件,并且是时候编辑为我们整个项目创建的 urls.py 文件了。当你打开这个文件时,你应该会看到已经有一个名为 admin 的路径,我们将在后面的课程中讲解。我们想要为我们的新应用程序添加另一个路径,所以我们将向 urlpatterns 列表中添加一个项目。这遵循了我们之前路径相同的模式,除了我们不想将 views.py 中的函数作为第二个参数添加,而是希望能够包含我们应用程序中 urls.py 文件内的所有路径。为此,我们写下:include("APP_NAME.urls"),其中 include 是我们通过从 django.urls 中也导入 include 获得的函数,如下面的 urls.py 所示:

    from django.contrib import admin
    from django.urls import path, include
    
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('hello/', include("hello.urls"))
    ] 
    
  5. 通过这样做,我们指定了当用户访问我们的网站,然后在搜索栏中添加 /hello 到 URL 时,他们将被重定向到我们新应用程序中的路径。

现在,当我使用python manage.py runserver启动应用程序并访问提供的 URL 时,我遇到了这个屏幕:错误的 URL 但这是因为我们只定义了 URL localhost:8000/hello,但没有定义末尾没有任何内容的 URL localhost:8000。所以,当我在搜索栏中的 URL 中添加/hello时:Hello, world 现在我们已经取得了一些成功,让我们回顾一下我们是如何到达这个点的:

  1. 当我们访问 URL localhost:8000/hello/时,Django 查看基本 URL(localhost:8000/)之后的内容,然后前往我们的项目urls.py文件并搜索与hello匹配的模式。

  2. 它之所以发现扩展,是因为我们定义了它,并且看到当遇到这种扩展时,它应该包含我们应用程序内的urls.py文件。

  3. 然后,Django 在重定向时忽略了它已经使用的 URL 部分(localhost:8000/hello/,或者全部),并在我们的其他urls.py文件中寻找与 URL 剩余部分匹配的模式。

  4. 它发现我们迄今为止的唯一路径("")与 URL 剩余部分匹配,因此它将我们导向与该路径关联的views.py中的函数。

  5. 最后,Django 在views.py中运行该函数,并将结果(HttpResponse("Hello, world!"))返回到我们的网页浏览器。

现在,如果我们想的话,我们可以将views.py中的index函数更改为返回我们想要的任何内容!我们甚至可以在函数中跟踪变量并进行计算,然后再返回某些内容。

现在,让我们看看我们如何将多个视图添加到我们的应用程序中。我们可以在应用程序内部遵循许多相同的步骤来创建向布莱恩和大卫打招呼的页面。

views.py内部:

from django.shortcuts import render
from django.http import HttpResponse

# Create your views here. 
def index(request):
    return HttpResponse("Hello, world!")

def brian(request):
    return HttpResponse("Hello, Brian!")

def david(request):
    return HttpResponse("Hello, David!") 

urls.py(在我们的应用程序内部)

from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("brian", views.brian, name="brian"),
    path("david", views.david, name="david")
] 

现在,当我们访问localhost:8000/hello时,我们的网站保持不变,但当我们向 URL 中添加briandavid时,我们会得到不同的页面:布莱恩 大卫

许多网站通过 URL 中包含的项目进行参数化。例如,访问www.twitter.com/cs50将显示 CS50 的所有推文,而访问www.github.com/cs50将带您到 CS50 的 GitHub 页面。您甚至可以通过导航到www.github.com/YOUR_USERNAME找到您自己的公共 GitHub 仓库!

在考虑如何实现这一点时,似乎不可能 GitHub 和 Twitter 这样的网站为每个用户都有一个单独的 URL 路径,因此让我们看看我们如何创建一个更灵活的路径。我们将从向views.py添加一个更通用的函数greet开始:

def greet(request, name):
    return HttpResponse(f"Hello, {name}!") 

这个函数不仅接受一个请求,还接受一个额外的参数,即用户的名称,然后根据该名称返回一个自定义的 HTTP 响应。接下来,我们必须在 urls.py 中创建一个更灵活的路径,这可能看起来像这样:

path("<str:name>", views.greet, name="greet") 

这是一种新的语法,但本质上这里发生的事情是我们不再寻找 URL 中的特定单词或名称,而是任何用户可能输入的字符串。现在,我们可以尝试使用几个其他的 URL 来测试网站:harry connor

我甚至可以通过增强 greet 函数来利用 Python 的 capitalize 函数,使其字符串首字母大写,使这些看起来更美观一些:

def greet(request, name):
    return HttpResponse(f"Hello, {name.capitalize()}!") 

Harry Connor

这很好地说明了我们如何在 Python 中拥有的任何功能在返回之前都可以在 Django 中使用。

模板

到目前为止,我们的 HTTP 响应只是文本,但我们可以包含我们想要的任何 HTML 元素!例如,我可以在 index 函数中决定返回一个蓝色标题而不是纯文本:

def index(request):
    return HttpResponse("<h1 style=\"color:blue\">Hello, world!</h1>") 

蓝色

views.py 中编写整个 HTML 页面会非常繁琐。这也会构成不良设计,因为我们希望在可能的情况下将项目的不同部分保存在不同的文件中。

这就是为什么我们现在要介绍 Django 的模板,它将允许我们在单独的文件中编写 HTML 和 CSS,并使用 Django 渲染这些文件。我们将用于渲染模板的语法看起来是这样的:

def index(request):
    return render(request, "hello/index.html") 

现在,我们需要创建这个模板。为此,我们将在我们的应用中创建一个名为 templates 的文件夹,然后在其中创建一个名为 hello(或我们应用的名称)的文件夹,最后添加一个名为 index.html 的文件。

文件

接下来,我们将添加我们想要添加到新文件中的内容:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Hello</title>
    </head>
    <body>
        <h1>Hello, World!</h1>
    </body>
</html> 

现在,当我们访问我们应用程序的主页时,我们可以看到标题和标题已经更新了:template0

除了编写一些静态的 HTML 页面外,我们还可以使用 Django 的模板语言 来根据访问的 URL 改变我们 HTML 文件的内容。让我们通过更改之前的 greet 函数来试一试:

def greet(request, name):
    return render(request, "hello/greet.html", {
        "name": name.capitalize()
    }) 

注意,我们在 render 函数中传递了第三个参数,这个参数被称为 上下文。在这个上下文中,我们可以提供我们希望在 HTML 文件中可用的信息。这个上下文以 Python 字典的形式存在。现在,我们可以创建一个 greet.html 文件:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Hello</title>
    </head>
    <body>
        <h1>Hello, {{ name }}!</h1>
    </body>
</html> 

您会注意到我们使用了一些新的语法:双大括号。这种语法允许我们访问在 context 参数中提供的变量。现在,当我们尝试它时:模板 1 模板 2

现在,我们已经看到了如何根据我们提供的上下文修改我们的 HTML 模板。然而,Django 模板语言比这更强大,所以让我们看看它还有哪些其他方式可以帮助我们:

条件语句:

我们可能希望根据某些条件更改我们网站上显示的内容。例如,如果您访问网站 www.isitchristmas.com,您可能会看到一个看起来像这样的页面:无 但这个网站在圣诞节那天会改变,届时网站会说 YES。为了创建类似的东西,让我们尝试创建一个类似的应用程序,其中我们检查是否是新年第一天。让我们创建一个新的应用程序来完成这个任务,回顾我们创建新应用程序的过程:

  1. 在终端中运行 python manage.py startapp newyear

  2. 编辑 settings.py,将“newyear”添加为我们的 INSTALLED_APPS 之一

  3. 编辑我们项目的 urls.py 文件,并包含一个类似于为 hello 应用程序创建的路径:

path('newyear/', include("newyear.urls")) 
  1. 在我们新应用程序的目录中创建另一个 urls.py 文件,并更新它以包含类似于 hello 中索引路径的路径:
from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
] 
  1. views.py 中创建一个索引函数。

现在我们已经设置好了我们的新应用程序,让我们弄清楚如何检查是否是新年第一天。为此,我们可以导入 Python 的 datetime 模块。为了了解这个模块的工作方式,我们可以查看 文档,然后使用 Python 解释器在 Django 之外测试它。

  • Python 解释器 是一个我们可以用来测试小块 Python 代码的工具。要使用它,请在您的终端中运行 python,然后您将能够在终端中输入并运行 Python 代码。当您完成使用解释器后,运行 exit() 退出。解释器

  • 我们可以使用这个知识来构建一个布尔表达式,该表达式仅在今天是新年第一天时评估为 True:now.day == 1 and now.month == 1

  • 现在我们有一个可以用来评估是否是新年第一天的表达式,我们可以更新 views.py 中的索引函数:

def index(request):
    now = datetime.datetime.now()
    return render(request, "newyear/index.html", {
        "newyear": now.month == 1 and now.day == 1
    }) 

现在,让我们创建我们的 index.html 模板。我们再次需要创建一个名为 templates 的新文件夹,该文件夹位于其中,然后是一个名为 newyear 的文件夹,以及一个名为 index.html 的文件。在该文件中,我们将编写如下内容:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Is it New Year's?</title>
    </head>
    <body>
        {% if newyear -%}
            <h1>YES</h1>
        {%- else -%}
            <h1>NO</h1>
        {%- endif %}
    </body>
</html> 

在上面的代码中,请注意,当我们希望在 HTML 文件中包含逻辑时,我们使用 {%%} 作为逻辑语句的开启和关闭标签。此外,请注意 Django 的格式化语言要求你包含一个结束标签,表示我们已完成 if-else 块。现在,我们可以打开我们的页面来查看:

No

现在,为了更好地了解幕后发生的事情,让我们检查这个页面的元素:

Source

注意,实际上发送到你的网页浏览器的 HTML 只包括 NO 标题,这意味着 Django 正在使用我们编写的 HTML 模板来创建一个新的 HTML 文件,并将其发送到我们的网页浏览器。如果我们稍微作弊一下,确保我们的条件始终为真,我们会看到相反的情况被填充:

def index(request):
    now = datetime.datetime.now()
    return render(request, "newyear/index.html", {
        "newyear": True
    }) 

Yes Source 0

样式

如果我们想添加一个 CSS 文件,它是一个 静态 文件,因为它不会改变,我们首先创建一个名为 static 的文件夹,然后在其中创建一个 newyear 文件夹,最后在该文件夹中创建一个 styles.css 文件。在这个文件中,我们可以添加任何我们想要的样式,就像我们在第一节课中做的那样:

h1 {
    font-family: sans-serif;
    font-size: 90px;
    text-align: center;
} 

现在,为了在 HTML 文件中包含这个样式,我们在 HTML 模板顶部添加一行 {% load static %},这向 Django 信号我们希望访问 static 文件夹中的文件。然后,而不是像之前那样硬编码样式表的链接,我们将使用一些 Django 特定的语法:

<link rel="stylesheet" href="{% static 'newyear/styles.css' %}"> 

现在,如果我们重新启动服务器,我们可以看到样式更改确实已经应用:big no

任务

现在,让我们将我们迄今为止学到的知识应用到一个小型项目中:创建一个 TODO 列表。让我们再次创建一个新的应用:

  1. 在终端中运行 python manage.py startapp tasks

  2. 编辑 settings.py,将“tasks”添加为我们的 INSTALLED_APPS 之一

  3. 编辑我们项目的 urls.py 文件,并包含一个类似于为 hello 应用创建的路径:

     path('tasks/', include("tasks.urls")) 
    
  4. 在我们新应用的目录中创建另一个 urls.py 文件,并将其更新为包含一个类似于 hello 中的索引路径:

     from django.urls import path
     from . import views
    
     urlpatterns = [
         path("", views.index, name="index"),
     ] 
    
  5. views.py 中创建一个索引函数。

现在,让我们先尝试简单地创建一个任务列表,并将其显示在页面上。让我们在 views.py 的顶部创建一个 Python 列表,我们将在这里存储我们的任务。然后,我们可以更新我们的 index 函数以渲染一个模板,并提供我们新创建的列表作为上下文。

from django.shortcuts import render

tasks = ["foo", "bar", "baz"]

# Create your views here. def index(request):
    return render(request, "tasks/index.html", {
        "tasks": tasks
    }) 

现在,让我们着手创建我们的模板 HTML 文件:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Tasks</title>
    </head>
    <body>
        <ul>
            {% for task in tasks %}
                <li>{{ task }}</li>
            {% endfor %}
        </ul>
    </body>
</html> 

注意这里,我们能够使用类似于我们之前条件语句的语法,以及类似于第二部分课中 Python 循环的语法来遍历我们的任务。当我们现在访问任务页面时,我们可以看到我们的列表被渲染:tasks0

表单

现在我们可以看到所有当前任务作为一个列表,我们可能想要能够添加一些新任务。为此,我们将开始查看如何使用表单来更新网页。让我们首先向 views.py 添加另一个函数,该函数将渲染一个带有添加新任务表单的页面:

# Add a new task: def add(request):
    return render(request, "tasks/add.html") 

接下来,确保向 urls.py 添加另一个路径:

path("add", views.add, name="add") 

现在,我们将创建我们的 add.html 文件,它与 index.html 非常相似,只是在主体中我们将包含一个表单而不是列表:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Tasks</title>
    </head>
    <body>
        <h1>Add Task:</h1>
        <form action="">
            <input type="text" name="task">
            <input type="submit">
        </form>
    </body>
</html> 

然而,我们刚刚所做的不一定是最佳设计,因为我们已经在两个不同的文件中重复了大部分 HTML。Django 的模板语言为我们提供了一种消除这种糟糕设计的方法:模板继承。这允许我们创建一个包含我们页面通用结构的 layout.html 文件:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Tasks</title>
    </head>
    <body>
        {% block body %}
        {% endblock %}
    </body>
</html> 

注意我们再次使用了 {%...%} 来表示某种非 HTML 逻辑,在这种情况下,我们告诉 Django 用来自另一个文件的一些文本填充这个“块”。现在,我们可以修改我们其他两个 HTML 文件,使其看起来像这样:

index.html:

{% extends "tasks/layout.html" %}

{% block body %}
    <h1>Tasks:</h1>
    <ul>
        {% for task in tasks %}
            <li>{{ task }}</li>
        {% endfor %}
    </ul>
{% endblock %} 

add.html:

{% extends "tasks/layout.html" %}

{% block body %}
    <h1>Add Task:</h1>
    <form action="">
        <input type="text" name="task">
        <input type="submit">
    </form>
{% endblock %} 

注意我们现在可以通过 扩展 我们的布局文件来删除大部分重复的代码。现在,我们的索引页面保持不变,我们现在还有一个添加页面:

添加

接下来,每次我们想要添加一个新任务时,在 URL 中输入“/add”并不是很理想,所以我们可能想要在页面之间添加一些链接。但是,我们不是硬编码链接,现在我们可以使用在 urls.py 中为每个路径分配的 name 变量,创建一个看起来像这样的链接:

<a href="{% url 'add' %}">Add a New Task</a> 

其中 'add' 是该路径的名称。我们可以在 add.html 中做类似的事情:

<a href="{% url 'index' %}">View Tasks</a> 

这可能会产生问题,因为我们有多个名为 index 的路由分布在不同的应用中。我们可以通过进入每个应用的 urls.py 文件,并添加一个 app_name 变量来解决此问题,这样文件现在看起来就像这样:

from django.urls import path
from . import views

app_name = "tasks"
urlpatterns = [
    path("", views.index, name="index"),
    path("add", views.add, name="add")
] 

然后,我们可以将链接从简单的 indexadd 改为 tasks:indextasks:add

<a href="{% url 'tasks:index' %}">View Tasks</a>

<a href="{% url 'tasks:add' %}">Add a New Task</a> 

现在,让我们确保当用户提交表单时表单实际上会做一些事情。我们可以通过向 add.html 中创建的表单添加一个 action 来做到这一点:

<form action="{% url 'tasks:add' %}" method="post"> 

这意味着一旦表单提交,我们将被路由回 add URL。在这里,我们指定我们将使用 post 方法而不是 get 方法,这通常是我们在表单可能改变该网页状态时使用的方法。

现在,我们需要对这个表单添加更多内容,因为 Django 需要一个令牌来防止跨站请求伪造(CSRF)攻击。这种攻击是指恶意用户试图从你的网站之外发送请求到你的服务器。这对某些网站来说可能是一个大问题。比如说,一个银行网站有一个表单,允许一个用户向另一个用户转账。如果有人能够从银行网站之外提交转账,那将是一场灾难!

为了解决这个问题,当 Django 发送响应渲染模板时,它还会提供一个CSRF 令牌,该令牌在每个新的会话中都是唯一的。然后,当提交请求时,Django 会检查请求关联的 CSRF 令牌是否与它最近提供的令牌匹配。因此,如果另一个网站上的恶意用户试图提交请求,他们将会因为无效的 CSRF 令牌而被阻止。这种 CSRF 验证是内置在Django 中间件框架中的,它可以干预 Django 应用的请求-响应处理。我们在这门课程中不会进一步讨论中间件,但如果感兴趣,请查看文档

要将这项技术整合到我们的代码中,我们必须在add.html表单中添加一行代码。

<form action="{% url 'tasks:add' %}" method="post">
    {% csrf_token %}
    <input type="text" name="task">
    <input type="submit">
</form> 

这行代码添加了一个由 Django 提供的 CSRF 令牌的隐藏输入字段,这样当我们重新加载页面时,看起来好像没有变化。然而,如果我们检查元素,我们会注意到添加了一个新的输入字段:CSRF

Django 表单

尽管我们可以像刚才那样通过编写原始 HTML 来创建表单,但 Django 提供了一个更简单的方法来收集用户信息:Django 表单。为了使用这种方法,我们需要在views.py的顶部添加以下内容以导入forms模块:

from django import forms 

现在,我们可以在views.py中创建一个新的表单,通过创建一个名为NewTaskForm的 Python 类来实现:

class NewTaskForm(forms.Form):
    task = forms.CharField(label="New Task") 

现在,让我们来看看这个类中发生了什么:

  • NewTaskForm括号后面,我们看到我们使用了forms.Form。这是因为我们的新表单继承自一个名为Form的类,该类包含在forms模块中。我们已经看到了如何在 Django 的模板语言和 Sass 样式中使用继承。这是继承如何被用来从一个更通用的描述(forms.Form类)缩小到我们想要的(我们的新表单)的另一个例子。继承是面向对象编程的关键部分,我们在这门课程中不会详细讨论,但关于这个主题有许多在线资源可供学习!

  • 在这个类内部,我们可以指定我们希望从用户那里收集哪些信息,在这种情况下是任务的名称。

  • 我们通过编写forms.CharField来指定这是一个文本输入,但 Django 的表单模块中包含了许多其他输入字段,我们可以从中选择。

  • 在这个CharField中,我们指定一个label,当用户加载页面时会显示出来。label只是我们可以传递给表单字段的许多参数之一。

现在我们已经创建了NewTaskForm类,我们可以在渲染add页面时将其包含在上下文中:

# Add a new task: def add(request):
    return render(request, "tasks/add.html", {
        "form": NewTaskForm()
    }) 

现在,在add.html中,我们可以用我们刚刚创建的表单替换我们的输入字段:

{% extends "tasks/layout.html" %}

{% block body %}
    <h1>Add Task:</h1>
    <form action="{% url 'tasks:add' %}" method="post">
        {% csrf_token %}
        {{ form }}
        <input type="submit">
    </form>
    <a href="{% url 'tasks:index' %}">View Tasks</a>
{% endblock %} 

使用forms模块而不是手动编写 HTML 表单有几个优点:

  • 如果我们想在表单中添加新字段,我们可以在views.py中简单地添加它们,而无需编写额外的 HTML。

  • Django 自动执行客户端验证,或用户机器本地的验证。这意味着它不会允许用户提交不完整的表单。

  • Django 提供了简单的服务器端验证,或验证在表单数据到达服务器后发生。

  • 在下一讲中,我们将开始使用模型来存储信息,Django 使得根据模型创建表单变得非常简单。

现在我们已经设置好了表单,让我们来处理用户点击提交按钮时会发生什么。当用户通过点击链接或输入 URL 导航到添加页面时,他们向服务器发送一个GET请求,我们已经在add函数中处理了它。但是,当用户提交表单时,他们向服务器发送一个POST请求,目前这个请求在add函数中没有被处理。我们可以通过在函数接收的request参数上添加条件来处理POST方法。下面代码中的注释解释了每行的目的:

# Add a new task: def add(request):

    # Check if method is POST
    if request.method == "POST":

        # Take in the data the user submitted and save it as form
        form = NewTaskForm(request.POST)

        # Check if form data is valid (server-side)
        if form.is_valid():

            # Isolate the task from the 'cleaned' version of form data
            task = form.cleaned_data["task"]

            # Add the new task to our list of tasks
            tasks.append(task)

            # Redirect user to list of tasks
            return HttpResponseRedirect(reverse("tasks:index"))

        else:

            # If the form is invalid, re-render the page with existing information.
            return render(request, "tasks/add.html", {
                "form": form
            })

    return render(request, "tasks/add.html", {
        "form": NewTaskForm()
    }) 

简要说明:为了在成功提交后重定向用户,我们需要导入一些额外的模块:

from django.urls import reverse
from django.http import HttpResponseRedirect 

会话

到目前为止,我们已经成功构建了一个应用程序,允许我们向不断增长的任务列表中添加任务。然而,将任务存储为全局变量可能是一个问题,因为它意味着所有访问页面的用户都会看到完全相同的列表。为了解决这个问题,我们将使用一个称为sessions的工具。

会话是存储在服务器端为每个新访问网站的唯一数据的方式。

要在我们的应用程序中使用会话,我们首先会删除全局tasks变量,然后修改我们的index函数,最后确保在之前任何使用变量tasks的地方,我们都将其替换为request.session["tasks"]

def index(request):

    # Check if there already exists a "tasks" key in our session 
    if "tasks" not in request.session:

        # If not, create a new list
        request.session["tasks"] = []

    return render(request, "tasks/index.html", {
        "tasks": request.session["tasks"]
    })

# Add a new task: def add(request):
    if request.method == "POST":

        # Take in the data the user submitted and save it as form
        form = NewTaskForm(request.POST)

        # Check if form data is valid (server-side)
        if form.is_valid():

            # Isolate the task from the 'cleaned' version of form data
            task = form.cleaned_data["task"]

            # Add the new task to our list of tasks
            request.session["tasks"] += [task]

            # Redirect user to list of tasks
            return HttpResponseRedirect(reverse("tasks:index"))
        else:

            # If the form is invalid, re-render the page with existing information.
            return render(request, "tasks/add.html", {
                "form": form
            })

    return render(request, "tasks/add.html", {
        "form": NewTaskForm()
    }) 

最后,在 Django 能够存储这些数据之前,我们必须在终端中运行python manage.py migrate。下周我们将更详细地讨论迁移是什么,但现阶段只需知道上述命令允许我们存储会话。

这节课的内容就到这里!下次我们将讨论如何使用 Django 来存储、访问和操作数据。

第四讲

原文:cs50.harvard.edu/web/notes/4/

  • 介绍

  • SQL

    • 数据库

    • 列类型

  • 表格

  • 选择

    • 在终端中使用 SQL

    • 函数

    • 更新

    • 删除

    • 其他子句

  • 连接表

    • JOIN 查询

    • 索引

    • SQL 漏洞

  • Django 模型

  • 迁移

  • Shell

    • 启动我们的应用程序
  • Django 管理员

  • 多对多关系

  • 用户

介绍

  • 到目前为止,我们已经讨论了如何使用 HTML 和 CSS 构建简单的网页,以及如何使用 Git 和 GitHub 来跟踪代码更改并与他人协作。我们还熟悉了 Python 编程语言,并开始使用 Django 来创建网络应用程序。

  • 今天,我们将学习如何使用 SQL 和 Django 模型高效地存储和访问数据。

SQL

SQL,或结构化查询语言,是一种编程语言,允许我们更新和查询数据库。

SQL 标志

数据库

在我们深入探讨如何使用 SQL 语言之前,我们应该讨论我们的数据是如何存储的。当使用 SQL 时,我们将与一个 关系数据库 一起工作,在那里我们可以找到所有数据存储在多个 表格 中。这些表格中的每一个都由一定数量的列和灵活数量的行组成。

为了说明如何使用 SQL,我们将使用一个航空公司的网站示例,该网站用于跟踪航班和乘客。在下面的表格中,我们可以看到我们正在跟踪多个航班,每个航班都有一个 origin(起点)、一个 destination(目的地)和一个 duration(时长)。

航班 0

有几种不同的关系数据库管理系统被广泛用于存储信息,并且可以轻松与 SQL 命令交互:

前两个,MySQL 和 PostgreSQL,是更重型的数据库管理系统,通常在运行网站的独立服务器上运行。另一方面,SQLite 是一个更轻量级的系统,可以将所有数据存储在一个单独的文件中。在本课程中,我们将使用 SQLite,因为它是 Django 默认使用的系统。

列类型

正如我们在 Python 中使用了几种不同的变量类型一样,SQLite 有 类型 代表不同形式的信息。其他管理系统可能有不同的数据类型,但它们都与 SQLite 的类型相当相似:

  • TEXT: 用于文本字符串(例如,一个人的名字)

  • NUMERIC: 一种更通用的数值数据形式(例如,日期或布尔值)

  • INTEGER: 任何非十进制数字(例如,一个人的年龄)

  • REAL: 任何实数(例如,一个人的体重)

  • BLOB (二进制大对象): 我们可能想要存储在数据库中的任何其他二进制数据(例如,一张图片)

现在,为了真正开始使用 SQL 与数据库交互,让我们首先创建一个新表。创建新表的 命令 大概是这样的:

CREATE TABLE flights(
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    origin TEXT NOT NULL,
    destination TEXT NOT NULL,
    duration INTEGER NOT NULL
); 

在上述命令中,我们创建了一个新表,我们决定将其命名为 flights,并且我们向这个表添加了四个列:

  1. id: 有一个允许我们唯一标识表中每一行的数字通常很有帮助。在这里,我们指定 id 是一个整数,并且它也是我们的 主键,这意味着它是我们的唯一标识符。我们还指定了它将 AUTOINCREMENT,这意味着我们每次向表中添加时不需要提供 id,因为它会自动完成。

  2. origin: 在这里我们指定这是一个文本字段,并且通过写入 NOT NULL 我们要求它必须有一个值。

  3. destination: 同样,我们指定这将是一个文本字段,并阻止它为空。

  4. duration: 同样,这个值不能为空,但这次它是一个整数而不是文本。

我们在创建列时刚刚看到了 NOT NULLPRIMARY KEY 约束,但还有其他几个 约束 可供我们使用:

  • CHECK: 确保在允许添加/修改行之前满足某些约束

  • DEFAULT: 如果没有给出值,则提供默认值

  • NOT NULL: 确保提供值

  • PRIMARY KEY: 表示这是在数据库中搜索行的主要方式

  • UNIQUE: 确保该列中不会有两个行具有相同的值。

现在我们已经看到了如何创建表,让我们看看我们如何可以向其中添加行。在 SQL 中,我们使用 INSERT 命令来完成这个操作:

INSERT INTO flights
    (origin, destination, duration)
    VALUES ("New York", "London", 415); 

在上述命令中,我们指定了我们要插入的表名,然后提供了一列列名列表,我们将提供有关这些列的信息,然后指定我们想要填充表中该行的 VALUES,确保 VALUES 的顺序与我们的列名列表相对应。请注意,我们不需要为 id 提供值,因为它会自动递增。

SELECT

一旦表格被填充了一些行,我们可能希望有一种方法来访问该表中的数据。我们通过使用 SQL 的SELECT查询来实现这一点。最简单的SELECT查询到我们的航班表可能看起来像这样:

SELECT * FROM flights; 

上述命令(*)检索了我们航班表中的所有数据

all

然而,可能我们并不真的需要数据库中的所有列,只需要起点和目的地。为了只访问这些列,我们可以用我们想要访问的列名替换*。以下查询返回所有起点和目的地。

SELECT origin, destination FROM flights; 

Just two cols

随着我们的表格越来越大,我们可能还想缩小查询返回的行数。我们通过添加一个WHERE并跟上一个条件来实现这一点。例如,以下命令只选择id3的行:

SELECT * FROM flights WHERE id = 3; 

only one row

我们可以按任何列过滤,而不仅仅是id

SELECT * FROM flights WHERE origin = "New York"; 

Origin is New York

在终端中使用 SQL

现在我们已经了解了一些基本的 SQL 命令,让我们在终端中测试它们!为了在您的计算机上使用 SQLite,您必须首先下载 SQLite。(我们不会在讲座中使用它,但您也可以下载 DB Browser以更用户友好的方式运行 SQL 查询。)

我们可以通过手动创建一个新文件或在终端中运行touch flights.sql来为我们的数据库创建一个文件。现在,如果我们通过终端运行sqlite3 flights.sql,我们将进入一个 SQLite 提示符,在那里我们可以运行 SQL 命令:

 # Entering into the SQLite Prompt
(base) % sqlite3 flights.sql
SQLite version 3.26.0 2018-12-01 12:34:55
Enter ".help" for usage hints.

# Creating a new Table
sqlite> CREATE TABLE flights(
   ...>     id INTEGER PRIMARY KEY AUTOINCREMENT,
   ...>     origin TEXT NOT NULL,
   ...>     destination TEXT NOT NULL,
   ...>     duration INTEGER NOT NULL
   ...> );

# Listing all current tables (Just flights for now)
sqlite> .tables
flights

# Querying for everything within flights (Which is now empty)
sqlite> SELECT * FROM flights;

# Adding one flight
sqlite> INSERT INTO flights
   ...>     (origin, destination, duration)
   ...>     VALUES ("New York", "London", 415);

# Checking for new information, which we can now see
sqlite> SELECT * FROM flights;
1|New York|London|415

# Adding some more flights
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("Shanghai", "Paris", 760);
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("Istanbul", "Tokyo", 700);
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("New York", "Paris", 435);
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("Moscow", "Paris", 245);
sqlite> INSERT INTO flights (origin, destination, duration) VALUES ("Lima", "New York", 455);

# Querying this new information
sqlite> SELECT * FROM flights;
1|New York|London|415
2|Shanghai|Paris|760
3|Istanbul|Tokyo|700
4|New York|Paris|435
5|Moscow|Paris|245
6|Lima|New York|455

# Changing the settings to make output more readable
sqlite> .mode columns
sqlite> .headers yes

# Querying all information again
sqlite> SELECT * FROM flights;
id origin      destination  duration
----------  ----------  -----------  ----------
1           New York    London       415
2           Shanghai    Paris        760
3           Istanbul    Tokyo        700
4           New York    Paris        435
5           Moscow      Paris        245
6           Lima        New York     455

# Searching for just those flights originating in New York
sqlite> SELECT * FROM flights WHERE origin = "New York";
id origin      destination  duration
----------  ----------  -----------  ----------
1           New York    London       415
4           New York    Paris        435 

我们还可以使用不仅仅是等于来过滤我们的航班。对于整数和实数值,我们可以使用大于或小于:

SELECT * FROM flights WHERE duration > 500; 

> 500

我们还可以使用其他逻辑(AND, OR)如 Python 中的逻辑:

SELECT * FROM flights WHERE duration > 500 AND destination = "Paris"; 

> 500 and paris

SELECT * FROM flights WHERE duration > 500 OR destination = "Paris"; 

> 500 or paris

我们还可以使用关键字IN来查看数据是否是几个选项之一:

SELECT * FROM flights WHERE origin IN ("New York", "Lima"); 

in

我们甚至可以使用正则表达式通过使用LIKE关键字更广泛地搜索单词。以下查询通过使用%作为通配符字符,找到所有在起点中有a的结果。

SELECT * FROM flights WHERE origin LIKE "%a%"; 

Origin has an 'a'

函数

此外,我们还可以将一些 SQL 函数应用于查询结果。如果我们不需要查询返回的所有数据,而只需要一些数据的摘要统计信息,这些函数可能很有用。

UPDATE

我们现在已经看到了如何添加和搜索表,但我们可能还希望能够更新已存在的表的行。我们可以使用UPDATE命令来完成这个操作,如下所示。正如你可能通过大声读出来所猜到的,该命令找到所有从纽约飞往伦敦的航班,并将它们的持续时间设置为 430。

UPDATE flights
    SET duration = 430
    WHERE origin = "New York"
    AND destination = "London"; 

DELETE

我们还可能想要有从我们的数据库中删除行的能力,我们可以使用DELETE命令来完成这个操作。以下代码将删除所有飞往东京的航班:

DELETE FROM flights WHERE destination = "Tokyo"; 

其他子句

我们可以使用许多额外的子句来控制返回给我们的查询

  • LIMIT:限制查询返回的结果数量

  • ORDER BY:根据指定的列对结果进行排序

  • GROUP BY:根据指定的列对结果进行分组

  • HAVING:允许基于结果数量进行额外的约束

表的连接

到目前为止,我们一直是在一次处理一个表,但实践中许多数据库都是由多个相互关联的表组成的。在我们的航班示例中,让我们想象我们还想添加一个机场代码与城市一起。按照我们目前表的结构,我们可能需要为每一行添加两个额外的列。我们也会重复信息,因为我们必须在多个地方写上城市 X 与代码 Y 相关联。

我们可以解决这个问题的方法之一是决定有一个表来跟踪航班,然后另一个表来跟踪机场。第二个表可能看起来像这样

机场表

现在我们有一个与代码和城市相关的表,而不是在我们的航班表中存储整个城市名称,如果我们能够只保存那个机场的id,那么这将节省存储空间。因此,我们应该相应地重写航班表。由于我们正在使用机场表的id列来填充origin_iddestination_id,我们称这些值为外键

新航班

除了航班和机场,航空公司可能还想要存储有关其乘客的数据,比如每个乘客将乘坐哪班航班。利用关系型数据库的力量,我们可以添加另一个表来存储姓名和代表他们所乘坐航班的键

简单的乘客表

尽管如此,我们还能做得更好,因为同一个人可能乘坐多趟航班。为了解决这个问题,我们可以创建一个people表来存储姓名,以及一个passengers表来将人与航班配对

人物

乘客

由于在这种情况下,一个人可以乘坐多趟航班,而一趟航班可以有很多人,我们称flightspeople之间的关系为多对多关系。连接这两个表的passengers表被称为关联表

JOIN 查询

虽然我们的数据现在存储得更高效,但似乎查询数据可能更困难。幸运的是,SQL 有一个JOIN查询,我们可以用它来合并两个表以进行另一个查询。

例如,假设我们想找到乘客正在乘坐的每次旅行的出发地、目的地和姓名。为了简化这个表,我们将使用包含航班 ID、名和姓的非优化passengers表。这个查询的第一部分看起来相当熟悉:

SELECT first, origin, destination
FROM ... 

但在这里我们遇到了一个问题,因为first存储在passengers表中,而origindestination存储在flights表中。我们通过使用passengers表中的flight_idflights表中的id相对应的事实来连接这两个表:

SELECT first, origin, destination
FROM flights JOIN passengers
ON passengers.flight_id = flights.id; 

连接模糊

我们刚刚使用了叫做INNER JOIN的东西,这意味着我们正在忽略两个表之间没有匹配的行,但还有其他类型的连接,包括LEFT JOINRIGHT JOINFULL OUTER JOIN,我们在这里不会详细讨论。

索引

当处理大型表时,我们可以通过创建一个类似于教科书背面的索引来使我们的查询更高效。例如,如果我们知道我们经常通过姓氏查找乘客,我们可以使用以下命令创建一个从姓氏到 ID 的索引:

CREATE INDEX name_index ON passengers (last); 

SQL 漏洞

现在我们已经了解了使用 SQL 处理数据的基础知识,重要的是要指出与使用 SQL 相关的主要漏洞。我们将从SQL 注入开始。

SQL 注入攻击是指恶意用户在网站上输入 SQL 代码作为输入,以绕过网站的安全措施。例如,假设我们有一个存储用户名和密码的表,然后在页面的主页上有一个登录表单。我们可能使用以下查询来搜索用户:

SELECT * FROM users
WHERE username = username AND password = password; 

一个名为 Harry 的用户可能会访问这个网站,并输入harry作为用户名,12345作为密码,在这种情况下,查询看起来会是这样:

SELECT * FROM users
WHERE username = "harry" AND password = "12345"; 

另一方面,一个黑客可能会输入harry" --作为用户名,密码为空。结果是--在 SQL 中表示注释,这意味着查询看起来会是这样:

SELECT * FROM users
WHERE username = "harry"--" AND password = "12345"; 

因为在这个查询中,密码检查已经被注释掉了,黑客可以在不知道密码的情况下登录 Harry 的账户。为了解决这个问题,我们可以使用:

  • 转义字符以确保 SQL 将输入视为纯文本而不是 SQL 代码。

  • 在 SQL 之上有一个抽象层,它包括自己的转义序列,所以我们不需要自己编写 SQL 查询。

当涉及到 SQL 时,另一个主要漏洞被称为竞态条件.

竞态条件是在同时对数据库进行多个查询时发生的情况。当这些查询没有得到妥善处理时,数据库更新时的精确时间可能会出现问题。例如,假设我银行账户中有 150 美元。如果我在手机和笔记本电脑上同时登录我的银行账户,并在每个设备上尝试提取 100 美元,就可能发生竞态条件。如果银行的软件开发者没有正确处理竞态条件,那么我可能能够从只有 150 美元的账户中提取 200 美元。解决这个问题的一个潜在方案是锁定数据库。我们不允许在完成一个事务之前与数据库进行任何其他交互。在银行示例中,在我电脑上点击导航到“提取”页面后,银行可能不允许我在手机上导航到该页面。

Django Models

Django 模型是在 SQL 之上的一个抽象层,它允许我们使用 Python 类和对象而不是直接 SQL 查询来与数据库交互。

让我们开始使用模型,为我们的航空公司创建一个 Django 项目,并在该项目中创建一个应用程序。

django-admin startproject airline
cd airline
python manage.py startapp flights 

现在我们将像通常添加应用程序一样进行添加应用程序的过程:

  1. settings.py中将flights添加到INSTALLED_APPS列表中

  2. urls.py中添加flights的路由:

     path("flights/", include("flights.urls")), 
    
  3. flights应用程序中创建一个urls.py文件。并填充标准的urls.py导入和列表。

现在,我们不再创建实际的路径,而是从views.py开始,我们将在models.py文件中创建一些模型。在这个文件中,我们将概述我们希望在应用程序中存储的数据。然后,Django 将确定存储我们每个模型所需的信息的 SQL 语法。让我们看看单个航班模型的例子:

class Flight(models.Model):
    origin = models.CharField(max_length=64)
    destination = models.CharField(max_length=64)
    duration = models.IntegerField() 

让我们看看这个模型定义中发生了什么:

  • 在第一行中,我们创建了一个新的模型,该模型扩展了 Django 的模型类。

  • 下面,我们添加了起点、终点和持续时间的字段。前两个是Char Fields,意味着它们存储字符串,第三个是Integer Field。这些只是许多内置 Django 字段类中的两个

  • 我们为两个字符字段指定了最大长度为 64。你可以通过检查文档来查看给定字段的可用规格。

Migrations

现在,尽管我们已经创建了一个模型,但我们还没有数据库来存储这些信息。要从我们的模型创建数据库,我们导航到项目的根目录并运行以下命令。

python manage.py makemigrations 

此命令创建了一些 Python 文件,这些文件将创建或编辑我们的数据库,以便能够存储我们在模型中的内容。你应该得到一个类似于下面的输出,如果你导航到你的migrations目录,你会注意到为我们创建了一个新文件

迁移输出 0

接下来,要应用这些迁移到我们的数据库,我们运行以下命令

python manage.py migrate 

现在,你会看到一些默认迁移已经应用,并且你也会注意到我们现在在项目的目录中有一个名为db.sqlite3的文件

迁移输出

Shell

现在,为了开始向数据库添加信息并对其进行操作,我们可以进入 Django 的 shell,在那里我们可以在项目中运行 Python 命令。

python manage.py shell
Python 3.7.2 (default, Dec 29 2018, 00:00:04)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.5.0 -- An enhanced Interactive Python. Type '?' for help. 
# Import our flight model In [1]: from flights.models import Flight

# Create a new flight In [2]: f = Flight(origin="New York", destination="London", duration=415)

# Instert that flight into our database In [3]: f.save()

# Query for all flights stored in the database In [4]: Flight.objects.all()
Out[4]: <QuerySet [<Flight: Flight object (1)>]> 

当我们查询数据库时,我们看到我们只得到一个名为Flight object (1)的航班。这个名字不是很 informative,但我们可以修复它。在models.py中,我们将定义一个__str__函数,该函数提供将 Flight 对象转换为字符串的指令:

class Flight(models.Model):
    origin = models.CharField(max_length=64)
    destination = models.CharField(max_length=64)
    duration = models.IntegerField()

    def __str__(self):
        return f"{self.id}: {self.origin} to {self.destination}" 

现在,当我们回到 shell 时,我们的输出更易于阅读:

# Create a variable called flights to store the results of a query In [7]: flights = Flight.objects.all()

# Displaying all flights In [8]: flights
Out[8]: <QuerySet [<Flight: 1: New York to London>]>

# Isolating just the first flight In [9]: flight = flights.first()

# Printing flight information In [10]: flight
Out[10]: <Flight: 1: New York to London>

# Display flight id In [11]: flight.id
Out[11]: 1

# Display flight origin In [12]: flight.origin
Out[12]: 'New York'

# Display flight destination In [13]: flight.destination
Out[13]: 'London'

# Display flight duration In [14]: flight.duration
Out[14]: 415 

这是一个好的开始,但回想一下之前,我们不想为每个航班存储城市名称作为起点和终点,所以我们可能需要一个与航班模型相关联的机场模型:

class Airport(models.Model):
    code = models.CharField(max_length=3)
    city = models.CharField(max_length=64)

    def __str__(self):
        return f"{self.city} ({self.code})"

class Flight(models.Model):
    origin = models.ForeignKey(Airport, on_delete=models.CASCADE, related_name="departures")
    destination = models.ForeignKey(Airport, on_delete=models.CASCADE, related_name="arrivals")
    duration = models.IntegerField()

    def __str__(self):
        return f"{self.id}: {self.origin} to {self.destination}" 

我们在新的Airport类中已经看到了所有内容,但Flight类中origindestination字段的变化对我们来说是新的:

  • 我们指定origindestination字段是每个外键,这意味着它们引用另一个对象。

  • 通过将Airport作为我们的第一个参数输入,我们指定了这个字段所引用的对象的类型。

  • 下一个参数 on_delete=models.CASCADE 指示了如果删除机场时应该发生什么。在这种情况下,我们指定当删除机场时,与其关联的所有航班也应该被删除。除了 CASCADE 之外,还有 其他几个选项

  • 我们提供了一个 相关名称,这为我们提供了一种通过给定机场作为起点或终点来搜索所有航班的途径。

每次我们在 models.py 中进行更改时,我们必须进行迁移然后迁移。请注意,您可能需要删除现有的从纽约到伦敦的航班,因为它不符合新的数据库结构。

# Create New Migrations
python manage.py makemigrations

# Migrate
python manage.py migrate 

现在,让我们在 Django 命令行中尝试这些新的模型:

# Import all models In [1]: from flights.models import *

# Create some new airports In [2]: jfk = Airport(code="JFK", city="New York")
In [4]: lhr = Airport(code="LHR", city="London")
In [6]: cdg = Airport(code="CDG", city="Paris")
In [9]: nrt = Airport(code="NRT", city="Tokyo")

# Save the airports to the database In [3]: jfk.save()
In [5]: lhr.save()
In [8]: cdg.save()
In [10]: nrt.save()

# Add a flight and save it to the database f = Flight(origin=jfk, destination=lhr, duration=414)
f.save()

# Display some info about the flight In [14]: f
Out[14]: <Flight: 1: New York (JFK) to London (LHR)>
In [15]: f.origin
Out[15]: <Airport: New York (JFK)>

# Using the related name to query by airport of arrival: In [17]: lhr.arrivals.all()
Out[17]: <QuerySet [<Flight: 1: New York (JFK) to London (LHR)>]> 

启动我们的应用程序

我们现在可以开始构建一个应用程序,该应用程序围绕使用模型与数据库交互的过程。让我们首先为我们的航空公司创建一个索引路由。在 urls.py 中:

urlpatterns = [
    path('', views.index, name="index"),
] 

views.py 文件中:

from django.shortcuts import render
from .models import Flight, Airport

# Create your views here. 
def index(request):
    return render(request, "flights/index.html", {
        "flights": Flight.objects.all()
    }) 

在我们的新 layout.html 文件中:

 <!DOCTYPE html>
<html lang="en">
    <head>
        <title>Flights</title>
    </head>
    <body>
        {% block body %}
        {% endblock %}
    </body>
</html> 

在新的 index.html 文件中:

 {% extends "flights/layout.html" %}

{% block body %}
    <h1>Flights:</h1>
    <ul>
        {% for flight in flights %}
            <li>Flight {{ flight.id }}: {{ flight.origin }} to {{ flight.destination }}</li>
        {% endfor %}
    </ul>
{% endblock %} 

我们在这里所做的是创建了一个默认页面,其中列出了我们迄今为止创建的所有航班。当我们现在打开这个页面时,它看起来像这样

列表上只有一个

现在,让我们通过返回 Django 命令行来为我们的应用程序添加更多航班:

# Using the filter command to find all airports based in New York In [3]: Airport.objects.filter(city="New York")
Out[3]: <QuerySet [<Airport: New York (JFK)>]>

# Using the get command to get only one airport in New York In [5]: Airport.objects.get(city="New York")
Out[5]: <Airport: New York (JFK)>

# Assigning some airports to variable names: In [6]: jfk = Airport.objects.get(city="New York")
In [7]: cdg = Airport.objects.get(city="Paris")

# Creating and saving a new flight: In [8]: f = Flight(origin=jfk, destination=cdg, duration=435)
In [9]: f.save() 

现在,当我们再次访问我们的网站时

两架航班

Django 管理员

由于开发者经常需要创建新对象,就像我们在 shell 中所做的那样,Django 提供了一个 默认管理界面,这使得我们可以更容易地完成这项工作。要开始使用这个工具,我们必须首先创建一个管理用户:

(base) cleggett@Connors-MacBook-Pro airline % python manage.py createsuperuser
Username: user_a
Email address: a@a.com
Password:
Password (again):
Superuser created successfully. 

现在,我们必须通过在我们的应用中进入 admin.py 文件,并导入和注册我们的模型,将我们的模型添加到管理应用程序中。这告诉 Django 我们希望在管理应用程序中访问哪些模型。

from django.contrib import admin
from .models import Flight, Airport

# Register your models here. admin.site.register(Flight)
admin.site.register(Airport) 

现在,当我们访问我们的网站并将 /admin 添加到 URL 中时,我们可以登录到一个看起来像这样的页面

登录

登录后,您将被带到下面的页面,您可以在其中创建、编辑和删除存储在数据库中的对象

管理员页面

现在,让我们为我们的网站添加更多页面。我们将首先添加点击航班以获取更多航班信息的功能。为此,让我们创建一个包含航班 id 的 URL 路径:

path("<int:flight_id>", views.flight, name="flight") 

然后,在 views.py 中,我们将创建一个 flight 函数,它接受一个航班 ID 并渲染一个新的 HTML 页面:

def flight(request, flight_id):
    flight = Flight.objects.get(id=flight_id)
    return render(request, "flights/flight.html", {
        "flight": flight
    }) 

现在,我们将创建一个模板来显示这些航班信息,并包含一个链接回到主页

 {% extends "flights/layout.html" %}

{% block body %}
    <h1>Flight {{ flight.id }}</h1>
    <ul>
        <li>Origin: {{ flight.origin }}</li>
        <li>Destination: {{ flight.destination }}</li>
        <li>Duration: {{ flight.duration }} minutes</li>
    </ul>
    <a href="{% url 'index' %}">All Flights</a>
{% endblock %} 

最后,我们需要添加从一页链接到另一页的能力,因此我们将修改我们的索引页面以包含链接:

 {% extends "flights/layout.html" %}

{% block body %}
    <h1>Flights:</h1>
    <ul>
        {% for flight in flights %}
            <li><a href="{% url 'flight' flight.id %}">Flight {{ flight.id }}</a>: {{ flight.origin }} to {{ flight.destination }}</li>
        {% endfor %}
    </ul>
{% endblock %} 

现在主页看起来是这样的

新家

例如,当我们点击航班 5 时,我们会来到这个页面

单程航班

多对多关系

现在,让我们将乘客集成到我们的模型中。我们将首先创建一个乘客模型:

class Passenger(models.Model):
    first = models.CharField(max_length=64)
    last = models.CharField(max_length=64)
    flights = models.ManyToManyField(Flight, blank=True, related_name="passengers")

    def __str__(self):
        return f"{self.first}  {self.last}" 
  • 正如我们讨论的那样,乘客与航班有 多对多 的关系,我们在 Django 中使用 ManyToManyField 来描述这种关系。

  • 此字段中的第一个参数是与该对象相关联的类的类型。

  • 我们提供了 blank=True 参数,这意味着乘客可以没有航班

  • 我们添加了一个 related_name,它具有与之前相同的作用:它将允许我们找到给定航班上的所有乘客。

要实际应用这些更改,我们必须进行迁移并执行迁移。然后我们可以在 admin.py 中注册 Passenger 模型,并访问管理页面来创建一些乘客!

现在我们已经添加了一些乘客,让我们更新我们的航班页面,以便它显示航班上的所有乘客。我们首先访问 views.py 并更新我们的航班视图,以提供乘客列表作为上下文。我们使用之前定义的相关名称来访问列表。

def flight(request, flight_id):
    flight = Flight.objects.get(id=flight_id)
    passengers = flight.passengers.all()
    return render(request, "flights/flight.html", {
        "flight": flight,
        "passengers": passengers
    }) 

现在,将乘客列表添加到 flight.html

 <h2>Passengers:</h2>
<ul>
    {% for passenger in passengers %}
        <li>{{ passenger }}</li>
    {% empty %}
        <li>No Passengers.</li>
    {% endfor %}
</ul> 

在这一点上,当我们点击航班 5 时,我们看到

新航班 5

现在,让我们来为网站访客提供预订航班的能力。我们将通过在 urls.py 中添加一个预订路由来实现这一点:

path("<int:flight_id>/book", views.book, name="book") 

现在,我们将在 views.py 中添加一个名为 book 的函数,该函数将乘客添加到航班中:

def book(request, flight_id):

    # For a post request, add a new flight
    if request.method == "POST":

        # Accessing the flight
        flight = Flight.objects.get(pk=flight_id)

        # Finding the passenger id from the submitted form data
        passenger_id = int(request.POST["passenger"])

        # Finding the passenger based on the id
        passenger = Passenger.objects.get(pk=passenger_id)

        # Add passenger to the flight
        passenger.flights.add(flight)

        # Redirect user to flight page
        return HttpResponseRedirect(reverse("flight", args=(flight.id,))) 

接下来,我们将向我们的航班模板添加一些上下文,以便页面可以通过 Django 的能力从查询中排除某些对象来访问当前不是航班乘客的每个人:

def flight(request, flight_id):
    flight = Flight.objects.get(id=flight_id)
    passengers = flight.passengers.all()
    non_passengers = Passenger.objects.exclude(flights=flight).all()
    return render(request, "flights/flight.html", {
        "flight": flight,
        "passengers": passengers,
        "non_passengers": non_passengers
    }) 

现在,我们将向我们的航班页面 HTML 添加一个表单,使用选择输入字段:

 <form action="{% url 'book' flight.id %}" method="post">
    {% csrf_token %}
    <select name="passenger" id="">
        {% for passenger in non_passengers %}
            <option value="{{ passenger.id }}">{{ passenger }}</option>
        {% endfor %}
    </select>
    <input type="submit">
</form> 

现在,让我们看看我访问航班页面并添加乘客后网站看起来像什么

表单

已提交

使用 Django 管理应用的一个优点是它可以自定义。例如,如果我们希望在管理界面中看到航班的各个方面,我们可以在 admin.py 中创建一个新的类,并在注册 Flight 模型时将其作为参数添加:

class FlightAdmin(admin.ModelAdmin):
    list_display = ("id", "origin", "destination", "duration")

# Register your models here. admin.site.register(Flight, FlightAdmin) 

现在,当我们访问航班的管理页面时,我们还可以看到 id

管理表

查阅 Django 的管理文档 以找到更多自定义管理应用的方法。

用户

今天讲座的最后我们将讨论认证的概念,即允许用户登录和退出网站。幸运的是,Django 为我们简化了这一过程,让我们通过一个示例来看看我们如何实现。我们首先创建一个名为users的新应用。在这里,我们将完成创建新应用的所有常规步骤,但在我们的新urls.py文件中,我们将添加一些额外的路由:

urlpatterns = [
    path('', views.index, name="index"),
    path("login", views.login_view, name="login"),
    path("logout", views.logout_view, name="logout")
] 

让我们从创建一个用户可以登录的表单开始。我们将像往常一样创建一个layout.html文件,然后创建一个login.html文件,该文件包含一个表单,并在存在消息时显示该消息。

 {% extends "users/layout.html" %}

{% block body %}
    {% if message -%}
        <div>{{ message }}</div>
    {%- endif %}

    <form action="{% url 'login' %}" method="post">
        {% csrf_token %}
        <input type="text", name="username", placeholder="Username">
        <input type="password", name="password", placeholder="Password">
        <input type="submit", value="Login">
    </form>
{% endblock %} 

现在,在views.py中,我们将添加三个函数:

def index(request):
    # If no user is signed in, return to login page:
    if not request.user.is_authenticated:
        return HttpResponseRedirect(reverse("login"))
    return render(request, "users/user.html")

def login_view(request):
    return render(request, "users/login.html")

def logout_view(request):
    # Pass is a simple way to tell python to do nothing.
    pass 

接下来,我们可以前往管理站点并添加一些用户。完成之后,我们将回到views.py并更新我们的login_view函数以处理带有用户名和密码的POST请求:

# Additional imports we'll need: from django.contrib.auth import authenticate, login, logout

def login_view(request):
    if request.method == "POST":
        # Accessing username and password from form data
        username = request.POST["username"]
        password = request.POST["password"]

        # Check if username and password are correct, returning User object if so
        user = authenticate(request, username=username, password=password)

        # If user object is returned, log in and route to index page:
        if user:
            login(request, user)
            return HttpResponseRedirect(reverse("index"))
        # Otherwise, return login page again with new context
        else:
            return render(request, "users/login.html", {
                "message": "Invalid Credentials"
            })
    return render(request, "users/login.html") 

现在,我们将创建一个user.html文件,当用户认证时,index函数将渲染此文件:

 {% extends "users/layout.html" %}

{% block body %}
    <h1>Welcome, {{ request.user.first_name }}</h1>
    <ul>
        <li>Username: {{ request.user.username }}</li>
        <li>Email: {{ request.user.email }}</li>
    </ul>

    <a href="{% url 'logout' %}">Log Out</a>
{% endblock %} 

最后,为了允许用户登出,我们将更新logout_view函数,使其使用 Django 的内置logout函数:

def logout_view(request):
    logout(request)
    return render(request, "users/login.html", {
                "message": "Logged Out"
            }) 

现在我们已经完成,这是一个网站的演示

演示

这节课就到这里!下次,我们将学习课程中的第二种编程语言:JavaScript。

第五讲

原文:cs50.harvard.edu/web/notes/5/

  • 简介

  • JavaScript

  • 事件

  • 变量

  • querySelector

  • DOM 操作

    • JavaScript 控制台

    • 箭头函数

    • 待办事项列表

  • 间隔

  • 本地存储

  • APIs

    • JavaScript 对象

    • 货币兑换

简介

  • 到目前为止,我们已经讨论了如何使用 HTML 和 CSS 构建简单的网页,以及如何使用 Git 和 GitHub 来跟踪代码更改并与他人协作。我们还熟悉了 Python 编程语言,开始使用 Django 创建网络应用程序,并学习了如何使用 Django 模型在我们的网站上存储信息。

  • 今天,我们将介绍一种新的编程语言:JavaScript。

JavaScript

让我们从几节课前的一个图表开始回顾:

客户端/服务器图

回想一下,在大多数在线交互中,我们有一个客户端/用户向服务器发送 HTTP 请求,服务器发送 HTTP 响应。我们迄今为止使用 Django 编写的所有 Python 代码都在服务器上运行。JavaScript 将允许我们在客户端运行代码,这意味着在运行时不需要与服务器交互,使我们的网站变得更加互动。

为了在我们的页面上添加一些 JavaScript,我们可以在 HTML 页面的某个位置添加一对 <script> 标签。我们使用 <script> 标签来通知浏览器,在两个标签之间写入的任何内容都是我们希望在用户访问我们的网站时执行的 JavaScript 代码。我们的第一个程序可能看起来像这样:

alert('Hello, world!'); 

JavaScript 中的 alert 函数向用户显示一条消息,然后他们可以将其关闭。为了展示这在实际 HTML 文档中的位置,这里有一个包含一些 JavaScript 的简单页面的示例:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Hello</title>
        <script>
            alert('Hello, world!');
        </script>
    </head>
    <body>
        <h1>Hello!</h1>
    </body> </html> 

alert

事件

JavaScript 的一个特性是它支持 事件驱动编程,这使得它在网页编程中非常有用。

事件驱动编程是一种编程范式,它围绕事件的检测以及检测到事件时应采取的操作展开。

事件可以是几乎任何东西,包括按钮被点击、光标移动、输入响应或页面加载。几乎用户与网页交互的每一件事都可以被视为一个事件。在 JavaScript 中,我们使用 事件监听器 来等待某些事件的发生,然后执行一些代码。

让我们从将上面的 JavaScript 转换为名为hello函数开始:

function hello() {
    alert('Hello, world!')
} 

现在,让我们工作在每次点击按钮时运行这个函数。为此,我们将在页面上创建一个带有onclick属性的 HTML 按钮,该属性为浏览器提供了当按钮被点击时应执行的操作的指令:

<button onclick="hello()">Click Here</button> 

这些更改允许我们在某些事件发生之前等待运行 JavaScript 代码的某些部分。

变量

JavaScript 是一种编程语言,就像 Python、C 或你之前工作过的任何其他语言一样,这意味着它具有与其他语言相同的许多功能,包括变量。在 JavaScript 中,我们可以使用以下三个关键字来分配值:

  • var:用于在全局范围内定义变量
var age = 20; 
  • let:用于在当前块(如函数或循环)中定义作用域有限的变量
let counter = 1; 
  • const:用于定义不会改变的值
const PI = 3.14; 

为了说明我们可以如何使用变量,让我们看看一个跟踪计数器的页面:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Count</title>
        <script>
            let counter = 0;
            function count() {
                counter++;
                alert(counter);
            }
        </script>
    </head>
    <body>
        <h1>Hello!</h1>
        <button onclick="count()">Count</button>
    </body>
</html> 

计数

querySelector

除了允许我们通过弹窗显示消息外,JavaScript 还允许我们更改页面上的元素。为了做到这一点,我们首先需要介绍一个名为document.querySelector的函数。这个函数会搜索并返回 DOM 中的元素。例如,我们会使用:

let heading = document.querySelector('h1'); 

以提取一个标题。然后,为了操作我们最近找到的元素,我们可以更改其innerHTML属性:

heading.innerHTML = `Goodbye!`; 

就像在 Python 中一样,我们也可以在 JavaScript 中利用条件。例如,让我们说,如果我们不想总是将标题更改为Goodbye!,我们希望在不同之间切换Hello!Goodbye!。我们的页面可能看起来像下面这样。注意,在 JavaScript 中,我们使用===作为两个项目之间更强的比较,它还会检查对象是否属于同一类型。我们通常尽可能使用===

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Count</title>
        <script>
            function hello() {
                const header = document.querySelector('h1');
                if (header.innerHTML === 'Hello!') {
                    header.innerHTML = 'Goodbye!';
                }
                else {
                    header.innerHTML = 'Hello!';
                }
            }
        </script>
    </head>
    <body>
        <h1>Hello!</h1>
        <button onclick="hello()">Click Here</button>
    </body>
</html> 

切换

DOM 操作

让我们利用 DOM 操作这个想法来改进我们的计数页面:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Count</title>
        <script>
            let counter = 0;
            function count() {
                counter++;
                document.querySelector('h1').innerHTML = counter;
            }
        </script>
    </head>
    <body>
        <h1>0</h1>
        <button onclick="count()">Count</button>
    </body>
</html> 

计数 2

我们可以通过在计数器达到十的倍数时显示一个弹窗来使这个页面更有趣。在这个弹窗中,我们想要格式化一个字符串以自定义消息,在 JavaScript 中我们可以使用模板字符串来完成。模板字符串要求整个表达式周围有反引号(`),任何替换项周围有美元符号和花括号。例如,让我们改变我们的计数函数

function count() {
    counter++;
    document.querySelector('h1').innerHTML = counter;

    if (counter % 10 === 0) {
        alert(`Count is now ${counter}`)
    }
} 

使用弹窗计数

现在,让我们看看我们可以如何改进这个页面的设计。首先,就像我们试图避免使用 CSS 的内联样式一样,我们希望尽可能避免内联 JavaScript。在我们的计数器示例中,我们可以通过添加一行脚本,改变页面按钮的 onclick 属性,并从 button 标签内部移除 onclick 属性来实现这一点。

document.querySelector('button').onclick = count; 

关于我们刚刚所做的一件事需要注意的一点是,我们不是通过在后面添加括号来调用 count 函数,而是仅仅命名这个函数。这指定了我们只希望在按钮被点击时调用这个函数。这之所以可行,是因为,像 Python 一样,JavaScript 支持函数式编程,因此函数可以被当作值本身来处理。

仅通过上述更改是不够的,正如我们通过检查页面和查看浏览器控制台所看到的那样:

错误控制台

这个错误出现是因为当 JavaScript 使用 document.querySelector('button') 搜索元素时,它没有找到任何东西。这是因为页面加载需要一点时间,而我们的 JavaScript 代码在按钮被渲染之前就运行了。为了解决这个问题,我们可以指定代码只有在页面加载后才会运行,使用 addEventListener 函数。这个函数接受两个参数:

  1. 要监听的事件(例如:'click'

  2. 当检测到事件时运行的函数(例如:上面的 hello

我们可以使用这个函数来确保代码只在所有内容加载完毕后运行:

document.addEventListener('DOMContentLoaded', function() {
    // Some code here
}); 

在上面的例子中,我们使用了一个 匿名函数,这是一个从未被赋予名称的函数。将这些放在一起,我们的 JavaScript 现在看起来是这样的:

let counter = 0;

function count() {
    counter++;
    document.querySelector('h1').innerHTML = counter;

    if (counter % 10 === 0) {
        alert(`Count is now ${counter}`)
    }
}

document.addEventListener('DOMContentLoaded', function() {
    document.querySelector('button').onclick = count;
}); 

我们可以通过将 JavaScript 移入一个单独的文件来改进我们的设计。我们这样做的方式与我们为样式将 CSS 放入单独文件的方式非常相似:

  1. 将所有的 JavaScript 代码都写入一个以 .js 结尾的单独文件中,比如 index.js

  2. <script> 标签添加一个 src 属性,指向这个新文件。

对于我们的计数器页面,我们可以有一个名为 counter.html 的文件,其内容如下:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Count</title>
        <script src="counter.js"></script>
    </head>
    <body>
        <h1>0</h1>
        <button>Count</button>
    </body>
</html> 

以及一个名为 counter.js 的文件,其内容如下:

let counter = 0;

function count() {
    counter++;
    document.querySelector('h1').innerHTML = counter;

    if (counter % 10 === 0) {
        alert(`Count is now ${counter}`)
    }
}

document.addEventListener('DOMContentLoaded', function() {
    document.querySelector('button').onclick = count;
}); 

将 JavaScript 放在单独的文件中有几个原因:

  • 视觉吸引力:我们的单个 HTML 和 JavaScript 文件变得更加易读。

  • HTML 文件之间的访问:现在我们可以有多个 HTML 文件,它们共享相同的 JavaScript。

  • 协作:现在,我们可以轻松地让一个人处理 JavaScript,而另一个人处理 HTML。

  • 导入:我们可以导入其他人已经编写的 JavaScript 库。例如 Bootstrap 有自己的 JavaScript 库,你可以包含它来使你的网站更具交互性。

让我们开始另一个示例页面,这个页面可以更加互动。下面,我们将创建一个页面,用户可以在其中输入他们的名字以获取自定义问候。

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Hello</title>
    <script>
        document.addEventListener('DOMContentLoaded', function() {
            document.querySelector('form').onsubmit = function() {
                const name = document.querySelector('#name').value;
                alert(`Hello, ${name}`);
            };
        });
    </script>
</head>
<body>
    <form>
        <input autofocus id="name" placeholder="Name" type="text">
        <input type="submit">
    </form>
</body>
</html> 

问候演示

关于上面页面的几点说明:

  • 我们在name输入的autofocus字段中使用了data-SOMETHING属性来指示光标应在页面加载时立即设置在该输入内。

  • 我们在document.querySelector内部使用#name来查找具有idname的元素。在这个函数中,我们可以使用与 CSS 中相同的所有选择器。

  • 我们使用输入字段的value属性来查找当前输入的内容。

我们可以使用 JavaScript 不仅向页面添加 HTML,还可以更改页面的样式!在下面的页面中,我们使用按钮来更改标题的颜色。

<!DOCTYPE html>
<html lang="en">
<head>
     <title>Colors</title>
     <script>
         document.addEventListener('DOMContentLoaded', function() {
            document.querySelectorAll('button').forEach(function(button) {
                button.onclick = function() {
                    document.querySelector("#hello").style.color = button.dataset.color;
                }
            });
         });
     </script>
</head>
<body>
    <h1 id="hello">Hello</h1>
    <button data-color="red">Red</button>
    <button data-color="blue">Blue</button>
    <button data-color="green">Green</button>
</body>
</html> 

颜色演示

关于上面页面的几点说明:

  • 我们使用style.SOMETHING属性来更改元素的风格。

  • 我们使用data-SOMETHING属性将数据分配给 HTML 元素。我们可以在 JavaScript 中使用元素的dataset属性稍后访问该数据。

  • 我们使用querySelectorAll函数来获取一个包含所有匹配查询的元素的Node List(类似于 Python 列表或 JavaScript 数组)。

  • JavaScript 中的forEach函数接受另一个函数,并将该函数应用于列表或数组中的每个元素。

JavaScript 控制台

控制台是一个有用的工具,可以用来测试小块代码和调试。你可以在控制台中编写和运行 JavaScript 代码,这可以通过在网页浏览器中检查元素然后点击console来实现。(具体过程可能因浏览器而异。)调试的一个有用工具是向控制台打印,你可以使用console.log函数来完成。例如,在上面的colors.html页面中,我可以添加以下行:

console.log(document.querySelectorAll('button')); 

这在控制台给出了以下结果:

节点列表

箭头函数

除了我们之前已经看到的传统函数表示法之外,JavaScript 现在还允许我们使用箭头函数,其中有一个输入(或当没有输入时括号)后跟=>,然后是执行一些代码。例如,我们可以修改上面的脚本以使用匿名箭头函数:

document.addEventListener('DOMContentLoaded', () => {
    document.querySelectorAll('button').forEach(button => {
        button.onclick = () => {
            document.querySelector("#hello").style.color = button.dataset.color;
        }
    });
}); 

我们也可以有命名函数,使用箭头,就像对count函数的这种重写:

count = () => {
    counter++;
    document.querySelector('h1').innerHTML = counter;

    if (counter % 10 === 0) {
        alert(`Count is now ${counter}`)
    }
} 

要了解我们可以使用的一些其他事件,让我们看看如何使用下拉菜单而不是三个单独的按钮来实现我们的颜色切换器。我们可以使用onchange属性检测select元素的变化。在 JavaScript 中,this是一个根据其使用上下文而变化的关键字。在事件处理程序的情况下,this指的是触发事件的那个对象。

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Colors</title>
        <script>
            document.addEventListener('DOMContentLoaded', function() {
                document.querySelector('select').onchange = function() {
                    document.querySelector('#hello').style.color = this.value;
                }
            });
        </script>
    </head>
    <body>
        <h1 id="hello">Hello</h1>
        <select>
            <option value="black">Black</option>
            <option value="red">Red</option>
            <option value="blue">Blue</option>
            <option value="green">Green</option>
        </select>

    </body>
</html> 

带有下拉菜单的颜色

在 JavaScript 中,我们可以检测许多其他事件,包括以下常见的:

  • onclick

  • onmouseover

  • onkeydown

  • onkeyup

  • onload

  • onblur

TODO 列表

为了将本节课学到的几个知识点结合起来,让我们尝试使用 JavaScript 制作一个完全基于 JavaScript 的 TODO 列表。我们将从编写页面的 HTML 布局开始。注意以下内容,我们为无序列表留出了空间,但我们还没有添加任何内容。同时注意,我们在tasks.js中添加了一个链接,我们将在这里编写 JavaScript。

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Tasks</title>
        <script src="tasks.js"></script>
    </head>
    <body>
        <h1>Tasks</h1>
        <ul id="tasks"></ul>
        <form>
            <input id="task" placeholder = "New Task" type="text">
            <input id="submit" type="submit">
        </form>
    </body>
</html> 

现在,这是我们的代码,我们可以将其保存在tasks.js中。以下是一些关于您将看到的内容的说明:

  • 这段代码与讲座中的代码略有不同。在这里,我们只在开始时查询我们的提交按钮和输入任务字段一次,并将这两个值存储在变量submitnewTask中。

  • 我们可以通过设置其disabled属性为false/true来启用/禁用按钮。

  • 在 JavaScript 中,我们使用.length来查找字符串和数组等对象的长短。

  • 在脚本的末尾,我们添加一行return false。这防止了表单的默认提交,这可能涉及重新加载当前页面或重定向到新页面。

  • 在 JavaScript 中,我们可以使用createElement函数创建 HTML 元素。然后我们可以使用append函数将这些元素添加到 DOM 中。

// Wait for page to load
document.addEventListener('DOMContentLoaded', function() {

    // Select the submit button and input to be used later
    const submit = document.querySelector('#submit');
    const newTask = document.querySelector('#task');

    // Disable submit button by default:
    submit.disabled = true;

    // Listen for input to be typed into the input field
    newTask.onkeyup = () => {
        if (newTask.value.length > 0) {
            submit.disabled = false;
        }
        else {
            submit.disabled = true;
        }
    }

    // Listen for submission of form
    document.querySelector('form').onsubmit = () => {

        // Find the task the user just submitted
        const task = newTask.value;

        // Create a list item for the new task and add the task to it
        const li = document.createElement('li');
        li.innerHTML = task;

        // Add new element to our unordered list:
        document.querySelector('#tasks').append(li);

        // Clear out input field:
        newTask.value = '';

        // Disable the submit button again:
        submit.disabled = true;

        // Stop form from submitting
        return false;
    }
}); 

任务演示

间隔

除了指定在事件触发时运行函数外,我们还可以设置函数在设定的时间后运行。例如,让我们回到我们的计数器页面的脚本,并添加一个间隔,即使用户没有点击任何东西,计数器也会每秒增加。为此,我们使用setInterval函数,该函数接受一个要运行的函数和一个函数运行之间的时间(以毫秒为单位)作为参数。

let counter = 0;

function count() {
    counter++;
    document.querySelector('h1').innerHTML = counter;
}

document.addEventListener('DOMContentLoaded', function() {
    document.querySelector('button').onclick = count;

    setInterval(count, 1000);
}); 

计数自动

本地存储

到目前为止,我们所有的网站都有一个需要注意的事情,那就是每次我们重新加载页面时,我们所有的信息都会丢失。标题颜色会变回黑色,计数器会回到 0,所有的任务都会被清除。有时这是我们想要的,但有时我们希望能够存储信息,以便用户返回网站时可以使用。

我们可以这样做的一种方式是使用本地存储,或者将信息存储在用户的网页浏览器中,我们可以在以后访问它。这些信息以一组键值对的形式存储,几乎就像 Python 字典一样。为了使用本地存储,我们将使用两个关键函数:

  • localStorage.getItem(key): 这个函数在本地存储中搜索具有给定键的条目,并返回与该键关联的值。

  • localStorage.setItem(key, value): 这个函数在本地存储中设置一个条目,将键与一个新值关联。

让我们看看如何使用这些新功能来更新我们的计数器!在下面的代码中,

// Check if there is already a value in local storage
if (!localStorage.getItem('counter')) {

    // If not, set the counter to 0 in local storage
    localStorage.setItem('counter', 0);
}

function count() {
    // Retrieve counter value from local storage
    let counter = localStorage.getItem('counter');

    // update counter
    counter++;
    document.querySelector('h1').innerHTML = counter;

    // Store counter in local storage
    localStorage.setItem('counter', counter);
}

document.addEventListener('DOMContentLoaded', function() {
    // Set heading to the current value inside local storage
    document.querySelector('h1').innerHTML = localStorage.getItem('counter');
    document.querySelector('button').onclick = count;
}); 

APIs

JavaScript 对象

一个JavaScript 对象与 Python 字典非常相似,因为它允许我们存储键值对。例如,我可以创建一个代表哈利·波特的 JavaScript 对象:

let person = {
    first: 'Harry',
    last: 'Potter'
}; 

我可以使用括号或点符号来访问或更改该对象的部分:

哈利·波特

JavaScript 对象的一个非常有用的用途是在一个网站和另一个网站之间传输数据,尤其是在使用APIs

API,或应用程序编程接口,是两个不同应用程序之间结构化通信的形式。

例如,我们可能希望我们的应用程序从谷歌地图、亚马逊或某些天气预报服务中获取信息。我们可以通过调用服务的 API 来实现这一点,它将返回结构化数据给我们,通常以JSON(JavaScript 对象表示法)的形式。例如,一个航班在 JSON 形式中可能看起来像这样:

{  "origin":  "New York",  "destination":  "London",  "duration":  415  } 

JSON 中的值不必仅仅是字符串和数字,如上面的例子所示。我们还可以存储列表,甚至其他 JavaScript 对象:

{  "origin":  {  "city":  "New York",  "code":  "JFK"  },  "destination":  {  "city":  "London",  "code":  "LHR"  },  "duration":  415  } 

货币兑换

为了展示我们如何在应用程序中使用 API,让我们构建一个应用程序,我们可以找到两种货币之间的汇率。在整个练习中,我们将使用欧洲中央银行的汇率 API。通过访问他们的网站,你会看到 API 的文档,这通常是当你想使用 API 时开始的好地方。我们可以通过访问 URL 来测试这个 API:api.exchangeratesapi.io/latest?base=USD。当你访问这个页面时,你会看到美元与其他许多货币之间的汇率,以 JSON 形式呈现。你还可以通过将 URL 中的 GET 参数从USD更改为任何其他货币代码来更改你得到的汇率。

让我们通过创建一个名为currency.html的新 HTML 文件并将其链接到一个 JavaScript 文件来实现将此 API 集成到应用程序中,但保持主体为空:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Currency Exchange</title>
        <script src="currency.js"></script>
    </head>
    <body></body>
</html> 

现在,我们将使用一种叫做 AJAX 的东西,或者称为异步 JavaScript 和 XML,它允许我们在页面加载后访问外部页面的信息。为了做到这一点,我们将使用 fetch 函数,这将允许我们发送 HTTP 请求。fetch 函数返回一个 promise。我们在这里不会详细讨论 promise 的细节,但我们可以将其视为某个时刻会传递过来的值,但不一定是立即。我们通过给它们一个 .then 属性来处理 promise,该属性描述了在接收到 response 时应该执行的操作。下面的代码片段将把我们的响应记录到控制台。

document.addEventListener('DOMContentLoaded', function() {
    // Send a GET request to the URL
    fetch('https://api.exchangeratesapi.io/latest?base=USD')
    // Put response into json form
    .then(response => response.json())
    .then(data => {
        // Log data to the console
        console.log(data);
    });
}); 

货币日志

关于上述代码的一个重要观点是,.then 的参数始终是一个函数。尽管看起来我们正在创建 responsedata 这两个变量,但这些变量仅仅是两个匿名函数的参数。

而不是简单地记录这些数据,我们可以使用 JavaScript 在屏幕上显示一条消息,如下面的代码所示:

document.addEventListener('DOMContentLoaded', function() {
    // Send a GET request to the URL
    fetch('https://api.exchangeratesapi.io/latest?base=USD')
    // Put response into json form
    .then(response => response.json())
    .then(data => {

        // Get rate from data
        const rate = data.rates.EUR;

        // Display message on the screen
        document.querySelector('body').innerHTML = `1 USD is equal to ${rate.toFixed(3)} EUR.`;
    });
}); 

货币

现在,让我们通过允许用户选择他们想看到的货币来使网站更加互动。我们将首先修改我们的 HTML,以便用户可以输入货币:

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Currency Exchange</title>
        <script src="currency.js"></script>
    </head>
    <body>
        <form>
            <input id="currency" placeholder="Currency" type="text">
            <input type="submit" value="Convert">
        </form>
        <div id="result"></div>
    </body>
</html> 

现在,我们将对 JavaScript 进行一些修改,使其仅在表单提交时才改变,并考虑到用户的输入。我们还将在这里添加一些错误检查:

document.addEventListener('DOMContentLoaded', function() {
    document.querySelector('form').onsubmit = function() {

        // Send a GET request to the URL
        fetch('https://api.exchangeratesapi.io/latest?base=USD')
        // Put response into json form
        .then(response => response.json())
        .then(data => {
            // Get currency from user input and convert to upper case
            const currency = document.querySelector('#currency').value.toUpperCase();

            // Get rate from data
            const rate = data.rates[currency];

            // Check if currency is valid:
            if (rate !== undefined) {
                // Display exchange on the screen
                document.querySelector('#result').innerHTML = `1 USD is equal to ${rate.toFixed(3)}  ${currency}.`;
            }
            else {
                // Display error on the screen
                document.querySelector('#result').innerHTML = 'Invalid Currency.';
            }
        })
        // Catch any errors and log them to the console
        .catch(error => {
            console.log('Error:', error);
        });
        // Prevent default submission
        return false;
    }
}); 

交换演示

这节课的内容就到这里!下次,我们将探讨如何使用 JavaScript 创建更加吸引人的用户界面!

posted @ 2025-11-08 11:25  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报