Djnago-解耦教程-一-

221 阅读37分钟

Djnago 解耦教程(一)

原文:Decoupled Django

协议:CC BY-NC-SA 4.0

一、解耦世界简介

本章简要介绍了:

  • 单片和解耦架构

  • REST 架构

  • GraphQL 查询语言

在这一章中,我们回顾了传统的 web 应用,基于视图、模型和控制器的经典 MVC 模式。

我们开始概述解耦架构的用例、优点和缺点。我们探索 REST 的基础,看看它与 GraphQL 的比较,并了解 REST APIs 毕竟不仅仅是 RESTful 的。

独石和 MVC

至少二十年来,传统的网站和应用都共享一个基于模型-视图-控制器模式的通用设计,缩写为 MVC。

这种模式不是一天建成的。一开始,业务逻辑、HTML 和我们今天所知的 JavaScript 的苍白模仿交织在一起。在典型的 MVC 安排中,当用户请求一个网站的路径时,应用用一些 HTML 来响应。在幕后,一个控制器,通常是一个函数或方法,负责将适当的视图返回给用户。这发生在控制器通过 ORM ( 对象关系映射)用来自数据库层的数据填充视图之后。这样一个系统,作为一个整体为用户服务,所有的组件都在一个地方,被称为 monolith 。在一个单一的 web 应用中,HTML 响应是在将页面返回给用户之前生成的,这个过程被称为传统的服务器端呈现。图 1-1 显示了 MVC 的一个表示。

img/505838_1_En_1_Fig1_HTML.png

图 1-1

MVC 应用用一个由控制器生成的视图来响应用户。模型层提供来自数据库的数据

MVC 有一些变化,比如 Django 使用的模型-视图-模板模式。在 Django 的 MVT 中,数据仍然来自数据库,但是视图的作用就像一个控制器:它通过 ORM 从数据库中获取数据,并将结果注入模板,然后返回给用户。MVC 和它的变体仍然存在:所有最流行的 web 框架都像。NET core、Rails、Laravel 和 Django 本身都成功地采用了这种模式。然而,最近我们看到了基于面向服务架构的解耦应用的传播。

在这种设计中,RESTful 或 GraphQL API 为一个或多个 JavaScript 前端、移动应用或另一台机器公开数据。面向服务和解耦架构是一个更广泛的类别,包含了一系列的微服务系统。在整本书中,我们在 web 应用的上下文中提到解耦架构,主要是指后端有 REST API 或 GraphQL,前端有独立的 JavaScript/HTML 的系统。在关注 REST APIs 之前,让我们首先解开解耦架构背后的东西。

解耦架构是由什么构成的?

一个解耦架构是一个遵守软件工程中最重要的规则之一的系统:关注点分离

在一个解耦的架构中,客户端和服务器之间有一个清晰的分离。这也是 REST 要求的最重要的约束之一。图 1-2 显示了这样一个系统的概况,包括一个后端和一个前端。

img/505838_1_En_1_Fig2_HTML.png

图 1-2

使用 REST API 作为 JavaScript/HTML 前端数据源的解耦应用

正如您将在本书后面看到的,客户机和服务器、视图和控制器之间的这种分离并不总是严格的,并且根据解耦方式的不同,这种区别会变得模糊。例如,我们可以让 REST API 和前端生活在两个完全不同的环境中(单独的域或者不同的起源)。在这种情况下,分工非常明确。在某些情况下,当完整的 JavaScript 前端没有意义时,Django 仍然可以公开 REST 或 GraphQL API,并将 JavaScript 嵌入 Django 模板中,与端点进行对话。

更糟糕的是,像 Angular 这样的框架甚至在构建前端代码时也采用了模型-视图-控制器模式。在单页应用中,我们可以找到相同的 MVC 设计,它复制了后端结构。您可能已经猜到,在某种程度上,纯解耦架构的缺点之一是代码重复。定义了什么是解耦架构之后,现在让我们来谈谈它的用例。

为什么以及何时解耦?

这不是一本关于 JavaScript 淘金热的书。事实上,你应该在考虑完全重写你心爱的 Django monolith 之前权衡你的选择。

不是每个项目都需要单页应用。相反,如果您的应用属于以下类别之一,您可以开始评估解耦架构的优势。这里列出了最常见的使用案例:

  • 机器对机器通信

  • 带有大量 JS 驱动的交互的交互式仪表盘

  • 静态站点生成

  • 移动应用

使用 Django,您可以构建各种各样涉及机器对机器通信的东西。想象一个从传感器收集数据的工业应用,这些数据可以在以后汇总成各种数据报告。这样的仪表板可以有很多 JS 驱动的交互。解耦架构的另一个有趣的应用是内容存储库。像 Django 这样的 Monoliths,Drupal 这样的 CMS,或者 WordPress 这样的博客平台都是静态站点生成器的好伙伴。我们稍后将详细探讨这个主题。

分离架构的另一个好处是能够服务不同类型的客户端:移动应用是最引人注目的用例之一。现在,如果解耦架构听起来太有吸引力,我建议您考虑它们的缺点。专门基于单页面应用的解耦架构对于以下情况并不总是有效的选择:

  • 受约束的团队

  • 很少或没有 JS 驱动交互的网站

  • 受限设备

  • 注重搜索引擎优化的内容密集型网站

Note

正如你将在第七章中看到的,像 Next.js 这样的框架可以通过生成静态 HTML 来帮助搜索引擎优化单页应用。使用这种技术的框架的其他例子有 Gatsby 和 Prerenderer。

现代前端开发很容易让人不知所措,尤其是如果团队很小的话。从零开始设计或构建解耦架构时,最严重的障碍之一是隐藏在 JavaScript 工具背后的巨大复杂性。在接下来的小节中,我们将重点关注 REST 和 GraphQL,这是解耦架构的两大支柱。

超媒体所有的东西

几乎所有解耦前端架构的基础都是 REST 架构风格。

如今,休息几乎不是一个新奇的概念。理论是,通过动词或命令,我们在系统上创建、检索或修改资源。例如,给定后端的一个User模型,由 REST API 作为资源公开,我们可以通过一个GET HTTP 请求获得数据库中所有实例的集合。下面显示了一个典型的GET请求来检索实体列表:

GET https://api.example/api/users/

如您所见,在检索资源时,我们说users,而不是user。按照惯例,资源应该总是复数。为了从 API 中检索单个资源,我们在路径中传递 ID,作为一个路径参数。下面显示了一个GET对单个资源的请求:

GET https://api.example/api/users/1

表 1-1 显示了所有动词(HTTP 方法)的分类及其对资源的影响。

表 1-1

对后端的给定资源产生相应影响的 HTTP 方法

|

方法

|

影响

|

幂等

| | --- | --- | --- | | POST | 创建资源 | 不 | | GET | 检索资源 | 是 | | PUT | 更新资源 | 是 | | DELETE | 删除资源 | 是 | | PATCH | 部分更新资源 | 不 |

为了引用这组 HTTP 方法,我们还使用了术语 CRUD ,它代表创建、读取、更新和删除。从表中可以看出,有些 HTTP 动词是等幂,意思是操作的结果总是稳定的。例如,一个GET请求总是返回相同的数据,无论我们在第一次请求后发出多少次命令。

相反,一个POST请求总是会导致一个副作用,即在后端创建一个新的资源,每个调用有不同的值。当使用GET检索资源时,我们可以在查询字符串中使用搜索参数来指定搜索约束、排序或限制结果的数量。下面显示了一组有限用户的请求:

GET https://api.example/api/users?limit=20

当用POST创建一个新资源时,我们可以发送一个请求体和请求。根据操作类型,API 可以用 HTTP 状态代码和新创建的对象进行响应。HTTP 响应代码的常见例子有200 OK201 Created202 Accepted。当事情进展不顺利时,API 可能会用一个错误代码来响应。HTTP 错误代码的常见例子有500 Internal Server Error403 Forbidden401 Unauthorized

客户机和服务器之间的这种来回通信通过 HTTP 协议传送 JSON 对象。如今,JSON 是交换数据的首选格式,而在过去,您可以在 HTTP 上看到 XML(SOAP 架构现在仍然存在)。REST 为什么遵循这些约定,为什么使用 HTTP?当 Roy Fielding 在 2000 年写他的题为“架构风格和基于网络的软件架构的设计”的论文时,他定义了以下规则:

  • 作为引擎的超媒体:当请求一个资源时,来自 API 的响应也必须包括到相关实体或其他动作的超链接。

  • 客户机-服务器分离:消费者(JavaScript、机器或通用客户机)和 Web API 必须是两个独立的实体。

  • 无状态:客户端和服务器之间的通信不应该使用服务器上存储的任何数据。

  • 可缓存的:API 应该尽可能地利用 HTTP 缓存。

  • 统一接口(Uniform interface):客户端和服务器之间的通信应该使用相关资源的表示,以及标准的通信语言。

为了更深入地了解这些规则,我们有必要走一个捷径。

作为引擎的超媒体

在最初的论文中,这个约束隐藏在统一接口部分,但是它对于理解 REST APIs 的真正本质是至关重要的。

超媒体作为引擎的实际含义是,当与 API 通信时,我们应该能够通过检查响应中的任何链接来了解下一步是什么。Django REST framework 是 Django 中构建 REST APIs 最流行的框架,它使得构建超媒体 API变得很容易。事实上,Django REST 框架序列化器有能力返回超链接资源。例如,对一个List模型的查询可以返回一对多关系的方。清单 1-1 展示了来自 API 的 JSON 响应,其中Card模型通过外键连接到List

{
  "id": 8,
  "title": "Doing",
  "cards": [
      "https://api.example/api/cards/1",
      "https://api.example/api/cards/2",
      "https://api.example/api/cards/3",
      "https://api.example/api/cards/4"
  ]
}

Listing 1-1A JSON Response with Hyperlinked Relationships

超链接资源的其他例子是分页链接。清单 1-2 是对boards资源(Board模型)的 JSON 响应,带有在结果间导航的超链接。

{
  "id": 4,
  "title": "Doing",
  "next": "https://api.example/api/boards/?page=5",
  "previous": "https://api.example/api/boards/?page=3"
}

Listing 1-2A JSON Response with Pagination Links

Django REST 框架的另一个有趣的特性是可浏览 API,这是一个用于与 REST API 交互的 web 接口。所有这些特性使得 Django REST 框架超媒体 API 准备就绪,这是这些系统的正确定义。

客户机-服务器分离

第二个约束,客户机-服务器分离,很容易实现。

REST API 可以公开端点,消费者可以连接到这些端点来检索、更新或删除数据。在我们的例子中,消费者将是 JavaScript 前端。

无国籍的

一个兼容的 REST API 应该是无状态的。

无状态意味着在客户机和服务器通信期间,请求不应该使用存储在服务器上的任何上下文数据。这并不意味着我们不能与 REST APIs 公开的资源进行交互。该约束适用于会话数据,如存储在服务器上的会话 cookies 或其他标识方式。这个严格的规定促使工程师们为 API 认证寻找新的解决方案。JSON Web Token,在本书后面被称为 JWT,就是这种研究的产物,它不一定比其他方法更安全,您将在后面看到。

可缓冲的

一个兼容的 REST API 应该尽可能地利用 HTTP 缓存。

HTTP 缓存通过 HTTP 头进行操作。一个设计良好的 REST API 应该总是给客户端提示一个GET响应的生命周期。为此,后端用一个max-age指令在响应上设置一个Cache-Control头,这决定了响应的生命周期。例如,要缓存一个小时的响应,服务器可以设置以下标头:

Cache-Control: max-age=3600

大多数时候,响应中还有一个ETag头,表示资源版本。清单 1-3 显示了一个带有缓存头的典型 HTTP 响应。

200 OK
Cache-Control: max-age=3600
ETag: "x6ty2xv"

Listing 1-3An HTTP Response with Cache Headers

Note

启用 HTTP 缓存的另一种方法涉及到Last-Modified头。如果服务器设置了这个头,客户端可以依次使用If-Modified-SinceIf-Unmodified-Since来检查资源的新鲜度。

当客户端请求相同的资源并且max-age还没有到期时,从浏览器的缓存中获取响应,而不是从服务器中获取。如果max-age已经过期,客户端通过附加If-None-Match头和来自ETag的值向服务器发出请求。这种机制被称为条件请求。如果资源仍然是新的,服务器用304 Not Modified响应,从而避免不必要的数据交换。相反,如果资源是陈旧的,也就是说,它已经过期,服务器会用一个新的响应进行响应。请记住,浏览器只缓存以下响应代码,这一点很重要:

  • 200 OK

  • 301 Moved Permanently

  • 404 Not Found

  • 206 Partial Content

此外,默认情况下,设置了Authorization头的响应不会被缓存,除非Cache-Control头包含了public指令。此外,正如您稍后将看到的,GraphQL 主要处理POST请求,默认情况下不会缓存这些请求。

统一界面

统一接口是 REST 最重要的规则之一。

它的原则之一,*表示,*规定客户端和服务器之间的通信,例如在后端创建一个新的资源,应该携带资源本身的表示。这意味着,如果我想在后端创建一个新资源,并发出一个POST请求,我应该提供一个包含该资源的有效负载。

假设我有一个 API,它接受一个端点上的命令,但是没有请求体。仅基于针对端点发出的命令创建新资源的 REST API 不是 RESTful 的。如果我们用统一接口和表示来代替,当我们想在服务器上创建一个新的资源时,我们在请求体中发送资源本身。清单 1-4 展示了一个投诉请求,请求主体用于创建一个新用户。

POST https://api.example/api/users/

{
  "name": "Juliana",
  "surname": "Crain",
  "age": 44
}

Listing 1-4A POST Request

这里我们使用 JSON 作为媒体类型,使用资源的表示作为请求体。统一接口也指用于驱动从客户端到服务器的通信的 HTTP 动词。当我们与一个 REST API 对话时,主要使用五种方法:GETPOSTPUTDELETEPATCH。这些方法也是统一接口,即我们用于客户端-服务器通信的通用语言。在回顾了 REST 原则之后,现在让我们把注意力转向它所谓的竞争者 GraphQL。

GraphQL 简介

GraphQL 出现在 2015 年,由脸书提出,并作为 REST 的替代品上市。

GraphQL 是一种数据查询语言,它允许客户端精确地定义从服务器获取什么数据,并在一个请求中组合来自多个资源的数据。在某种意义上,这就是我们对 REST APIs 的一贯做法,但是 GraphQL 更进一步,将更多的控制权推给了客户端。我们看到了如何从 REST API 请求数据。例如,为了获得单个用户,我们可以访问以下虚构的 REST API 的 URL:

https://api.example/api/users/4

作为响应,API 返回给定用户的所有字段。清单 1-5 显示了单个用户的 JSON 响应,这也恰好与Friend模型有一对多的关系。

{
  "id": 4,
  "name": "Juliana",
  "surname": "Crain",
  "age": 44,
  "city": "London",
  "occupation": "Software developer",
  "friends": [
      "https://api.example/api/friend/1",
      "https://api.example/api/friend/2",
      "https://api.example/api/friend/3",
      "https://api.example/api/friend/4"
  ]
}

Listing 1-5A JSON Response from a REST API

这是一个虚构的例子,但是如果您想象一下响应中有一组更大的字段,那么很明显我们正在过量获取,也就是说,我们正在请求比我们需要的更多的数据。如果我们考虑一下同一个 API,这次是用 GraphQL 实现的,我们可以请求一个更小的字段子集。为了从 GraphQL API 请求数据,我们可以进行一个查询。清单 1-6 显示了一个典型的 GraphQL 查询,通过 ID 请求一个用户,只有一个字段子集。

query {
    getUser(userID: 4) {
        surname,
        age
    }
}

Listing 1-6A GraphQL Query

如您所见,客户端控制它可以请求的字段。例如,这里我们跳过了除surnameage之外的所有字段。这个查询还有一个由userID标识的参数,它充当查询的第一个过滤器。为了响应这个查询,GraphQL API 返回请求的字段。清单 1-7 显示了我们查询的 JSON 响应。

{
  "surname": "Crain",
  "age": 44
}

Listing 1-7A JSON Response from the Previous Query

“不再过度提取”是 GraphQL 优于 REST 的主要卖点之一。实际上,这种基于字段的过滤功能并不是 GraphQL APIs 独有的。例如,遵循 JSON API 规范的 REST APIs 可以使用稀疏字段集来请求数据的一个子集。一旦我们对 GraphQL 端点发出一个查询,这个查询就作为请求体通过一个POST请求。清单 1-8 显示了一个对 GraphQL API 的请求。

POST https://api.example/graphql

{
"query" : "query { getUser(userID: 4) { surname, age } }",
"variables": null
}

Listing 1-8A GraphQL Query over a POST Request

您已经注意到,在这个请求中,我们调用了/graphql端点,而不是/api/users/4。同样,我们使用POST而不是GET来检索资源。这与 REST 架构风格有很大的不同。GraphQL 中的查询请求只是故事的一半。REST 分别使用POSTPUTDELETE来创建、更新或删除资源,而 GraphQL 使用突变的概念来改变数据。清单 1-9 显示了创建新用户的一个变种。

mutation {
   createUser(name: "Caty", surname: "Jonson") {
       name,
       surname
   }
}

Listing 1-9A GraphQL Mutation

订阅是 GraphQL 服务的另一个有趣的特性。客户端可以订阅事件。例如,我们可能希望在新用户注册我们的服务时收到来自服务器的通知。在 GraphQL 中,我们为此注册了一个订阅。清单 1-10 说明了一个订阅。

subscription {
   userRegistered {
       name,
       email
   }
}

Listing 1-10A GraphQL Subscription

当 GraphQL 查询到达后端时会发生什么?该流程与 REST API 相比如何?一旦查询到达后端,它将根据包含类型定义模式进行验证。然后一个或多个解析器,连接到模式中每个字段的专用函数,集合并为用户返回适当的数据。说到类型定义,GraphQL 中的一切都是类型:查询、变异、订阅和域实体。每个查询、变异和实体在使用之前都必须在模式中定义,用模式定义语言编写。清单 1-11 显示了一个简单的查询模式。

type User {
      name: String,
      surname: String,
      age: Int,
      email: String
}

type Query {
      getUser(userID: ID): User!
}

type Mutation {
      createUser(name: String, surname: String): User
}

type Subscription {
      userRegistered: User
}

Listing 1-11A Simple GraphQL Schema

通过查看这个模式,您可以立即注意到 GraphQL 是如何实施强类型的,这很像 TypeScript 或 C#之类的类型化语言。这里,StringIntID标量类型,而User是我们的自定义类型。按照 GraphQL 的说法,这些自定义类型属于对象类型的定义。GraphQL 如何适应 Python 生态系统?如今,有许多用于构建 Pythonesque GraphQL APIs 的库。最受欢迎的如下:

  • 石墨烯,以其代码优先的方式构建 GraphQL 服务

  • Ariadne,一个模式优先的 GraphQL 库

  • Strawberry,建立在数据类之上,代码优先,有类型提示

所有这些库都集成了 Django。GraphQL 的代码优先方法和模式优先方法之间的区别在于,前者将 Python 语法提升为编写模式的一等公民。后者使用多行 Python 字符串来表示它。在第 10 和 11 章中,我们与 Django 的 GraphQL、Ariadne 和 Strawberry 进行了广泛的合作。

摘要

本章回顾了传统架构和解耦架构的基础。你学到了:

  • 单片系统是作为一个整体单元向用户提供 HTML 和数据的系统

  • REST APIs 实际上是超媒体 API,因为它们使用 HTTP 作为通信媒介,并使用超链接提供相关资源的路径

  • JavaScript 优先和单页应用并不是所有用例的完美解决方案

  • GraphQL 是 REST 的有力竞争者

在下一章,我们将深入 JavaScript 生态系统,看看它是如何适应 Django 的。

额外资源

二、JavaScript 遇上 Django

本章涵盖:

  • JavaScript 如何融入 Django

  • JavaScript 生态系统和工具

  • JavaScript 前端库和框架

尽管 JavaScript 被认为是一种玩具语言,但近年来它已经成为一种成熟的工具。

不管是好是坏,如今 JavaScript 无处不在。JavaScript 工具也呈指数增长,新的库和技术快速涌入生态系统。在这一章中,我们将介绍现代 JavaScript 场景。您将理解 JavaScript 工具是做什么的,以及它们如何适应 Django。我们还看一下最流行的前端库和框架。

生产中的 JavaScript 和 Django

为了更好地理解现代 JavaScript 如何融入 Django,我们不仅要考虑本地开发环境,而且首先要考虑典型的生产环境。

生产中的 Django 与发展中的 django 完全不同。首先,开发中的 Django 有一个本地服务器,可以提供静态文件,即 JavaScript、图像和 CSS。然而,同一个开发服务器不能处理生产负载,更不用说生产设置的安全性了。因此,在生产环境中,我们通常采用以下组件:

  • Django 项目本身

  • 一个反向代理,比如 NGINX,服务于静态文件,它也充当 SSL 终端

  • 一个 WSGIASGI 服务器,比如 Gunicorn、Uvicorn 或 Hypercorn(下一章将详细介绍 ASGI)

JavaScript 如何融入其中?当我们在生产服务器上部署 Django 时,我们运行python manage.py collectstatic将所有 Django 应用的静态文件分组到一个地方,由STATIC_ROOT配置变量标识。为了便于理解,假设我们有一个 Django 项目,它有一个名为quote的应用和一个位于~/repo-root/quote/static/quote/js/index.js中的 JavaScript 文件。假设我们对STATIC_ROOT进行了如下配置,其中/home/user/static/是生产服务器上的一个现有文件夹:

STATIC_ROOT = "/home/user/static/"

当我们运行python manage.py collectstatic时,静态文件在/home/user/static/中着陆,准备好被任何引用static模板标签的 Django 模板获取。为此,STATIC_URL配置必须指向用于提供静态文件的 URL。在我们的例子中,我们想象一个名为static.decoupled-django.com的子域:

STATIC_URL = "https://static.decoupled-django.com/"

这个 URL 通常由 NGINX 虚拟主机提供服务,有一个location块指向 Django 的STATIC_ROOT中配置的值。清单 2-1 展示了如何从 Django 模板中调用静态文件(这里是 JavaScript)。

{% load static %}
<!DOCTYPE html>
<html lang="en">
<body>
<h1>Hello Django!</h1>
<div id="root"></div>
</body>
<script src="{% static "quote/js/index.js" %}"></script>
</html>

Listing 2-1A Django Template Referencing a Static File

在实际的 HTML 中,URL 变成了:

<script src="https://static.decoupled-django.com/quote/js/index.js"></script>

这是最简单的情况,我们有一个或多个 Django 应用,每个都有自己的 JavaScript 文件。这种方法适用于小型应用,其中应用的入口点的 JavaScript 代码符合 200KB 的限制。这里的入口点指的是浏览器启动整个应用必须下载的第一个 JavaScript 文件。由于这本书是关于“解耦的 Django ”,我们需要考虑更复杂的设置,其中提供给用户的 JavaScript 负载可能超过 200KB。此外,JavaScript 应用越大,我们就越需要以模块化的方式构建代码,这就引出了 JavaScript ES 模块和模块捆绑器

对模块捆扎机的需求

直到 2015 年,JavaScript 在前端还没有一个标准的模块系统。尽管 Node.js 从一开始就有require(),但情况实际上分散在前端,有不同的竞争方法,如 AMD modules、UMD 和 CommonJS。

终于在 2015 年, ES 模块登陆 ECMAScript。ES 模块提供了 JavaScript 中代码重用的标准方法,同时也支持像动态导入这样的强大模式来提高大型应用的性能。现在,一个典型的前端项目的问题是,ES 模块不是开发人员唯一可用的资产。有图像、样式文件(如 CSS 或 SASS)以及不同类型的 JavaScript 模块。我们也不要忘记 ES 模块是一个相当新的产品,传统的模块格式仍然存在。一个 JavaScript 项目可能使用基于 es 模块的新库,但是也需要包含作为 CommonJS 发布的代码。此外,ES 模块不被较旧的浏览器支持。

现代前端开发人员面临的另一个挑战是典型 JavaScript 应用的大小,尤其是当项目需要大量依赖项时。为了克服这些问题,被称为模块捆绑器的专门工具出现了。模块捆绑器的目标是多方面的。该工具可以:

  • 将不同类型的 JavaScript 模块组装到同一个应用中

  • 在 JavaScript 项目中包含不同类型的文件和资源

  • 使用一种称为代码分割的技术来提高应用的性能

简而言之,模块捆绑器提供了一个统一的接口,用于收集前端项目的所有依赖项,组装它们,并生成一个或多个名为 bundles 的 JavaScript 文件,以及最终应用的任何其他资产(CSS 和图像)。最近最流行的模块捆绑器之一是 webpack ,它也被用于 JavaScript 领域最重要的项目搭建 CLI 工具(create-react-app,Vue CLI)。在下一节中,我们将探讨为什么 webpack 对于需要处理大量 JavaScript 的 Django 开发人员来说很重要。

Webpack 对抗 Django(代码拆分的需要)

JavaScript 中的代码分割指的是向客户端提供尽可能少的 JavaScript 代码,同时按需加载其余代码的能力。

对于普通的 Django 开发人员来说,理解代码分割并不是绝对必要的,但是参与需要在前端进行大量交互的中型到大型 Django 项目的 Python 团队必须了解这个概念。在前面的小节中,我提到了 JavaScript 应用入口点的理论极限为 200KB。接近这个数字,我们可能会提供糟糕的导航体验。JavaScript 对于任何设备都是有成本的,但是在低端设备和慢速网络上,性能下降会更加明显(我建议一直关注 Addy Osmani 的“JavaScript 的成本”,参见参考资料中的链接)。由于这个原因,对最终的工件应用一系列的技术是非常重要的。一种这样的技术是代码缩减**,其中最终的 JavaScript 代码去掉了注释、空格和while函数,变量名被破坏。这是一个众所周知的优化,几乎任何工具都可以完成。但是一种更强大的技术,不包括现代的模块捆绑器,叫做代码分割*,可以进一步缩小生成的 JavaScript 文件。JavaScript 应用中的代码拆分适用于各种级别:*

** 在路由级别

  • 在组件级别

  • 关于用户交互(动态导入)

在某种程度上,像 Vue CLI 和 create-react-app 这样的 CLI 工具已经在代码分割方面提供了现成的默认设置。在这些工具中,webpack 已经被配置为产生高效的输出,这要归功于一种被称为供应商分裂的基本形式的代码分裂。在下面的例子中可以看到代码分割对 JavaScript 应用的影响。这是在配置为单页面应用的最小项目上运行npm run build的结果:

js/chunk-vendors.468a5298.js
js/app.24e08b96.js

应用的后续部分,称为,位于不同于主入口点的文件中,并且可以并行加载。你可以看到我们有两个文件,app.24e08b96.jschunk-vendors.468a5298jsapp.24e08b96.js文件是应用的入口点。当应用加载时,入口点需要第二个块,名为chunk-vendors.468a5298.js。当你在一个块名中看到供应商时,这表明 webpack 正在进行最基本的代码拆分:供应商拆分。供应商依赖是像 lodash 和 React 这样的库,它们可能包含在项目的多个地方。为了防止依赖性重复,可以指示 webpack 识别依赖性的消费者之间的共同点,并将共同的依赖性分割成单个块。从这些文件名中您可以注意到的另一件事是散列。例如在app.24e08b96.js中,hash 是24e08b96,由模块捆绑器从文件内容中计算出来。当文件内容改变时,散列也会改变。要记住的重要一点是,入口点和块在脚本标签中出现的顺序对于应用的运行至关重要。清单 2-2 展示了我们的文件应该如何出现在 HTML 标记中。

<-- rest of the document -->
<script src=/js/chunk-vendors.468a5298.js></script>
<script src=/js/app.24e08b96.js></script>
<-- rest of the document -->

Listing 2-2Two Chunks As They Appear in the HTML

这里,chunk-vendors.468a5298.js必须在app.24e08b96.js之前,因为chunk-vendors.468a5298.js包含一个或多个对入口点的依赖。继续关注 Django,您可以想象,为了以相同的顺序注入这些块,我们需要一些系统来将每个文件的出现顺序与模板中的static标签配对。一个名为django-webpack-loader的 Django 库旨在简化 Django 项目中 webpack 的使用,但是当 webpack 4 推出新的代码分割配置时,splitChunksdjango-webpack-loader停止了工作。

这里的要点是 JavaScript 工具比其他任何东西都移动得快,包维护者要跟上最新的变化并不容易。此外,弄乱 webpack 配置是一种奢侈,不是每个人都能负担得起的,还不算配置漂移和破坏性更改的风险。如果有疑问,在使用 webpack 或接触其配置之前,使用这个小启发来决定要做什么:如果应用的 JavaScript 部分超过 200KB,使用适当的 CLI 工具,并将应用作为 Django 模板中的单页应用,或作为解耦的 SPA。我们将在第五章中探讨第一种方法。如果 JavaScript 代码符合 200KB 的限制,并且交互的数量很少,那么使用一个简单的<script>标签来加载您需要的内容,或者如果您想使用现代的 JavaScript,那么至少配置一个简单的 webpack 管道,并进行供应商拆分。概述了模块捆绑器的基础之后,现在让我们继续我们的现代 JavaScript 工具之旅。

Note

JavaScript 工具,尤其是 webpack,是一个太多的移动目标,在一本书中涵盖它会有提供过时指令的风险。出于这个原因,我在这里不讨论 webpack 项目的设置。您可以在参考资料中找到这种设置示例的链接。

现代 JavaScript、Babel 和 Webpack

作为开发人员,我们很幸运,因为大多数时候我们可以使用快速的互联网连接、强大的多核机器、大量的 RAM 和现代浏览器。

如果这个闪亮的新 JavaScript 片段可以在我的机器上运行,那么它应该可以在任何地方运行,对吗?很容易理解编写现代 JavaScript 的吸引力。考虑以下基于 ECMAScript 5 的示例:

var arr = ["a", "b"];
function includes(arr, element) {
  return arr.indexOf(element) !== -1;
}

这个函数检查一个给定的元素是否存在于一个数组中。它基于Array.prototype.indexOf(),这是一个内置的数组函数,如果在目标列表中没有找到给定的元素,它将返回-1。现在转而考虑基于 ECMAScript 2016 的以下片段:

const arr = ["a", "b"];
const result = arr.includes("c");

第二个例子显然更简洁,更容易理解,也更适合开发人员。缺点是老一点的浏览器看不懂Array.prototype.includes()或者const。我们不能按原样发送此代码。

Tip

caniuse.comdeveloper.mozilla.org的兼容性表都是非常宝贵的资源,可以用来了解给定的目标浏览器是否支持现代语法。

幸运的是,越来越少的开发者需要担心可怕的 Internet Explorer 11,但仍然有许多边缘情况需要考虑。到目前为止,最兼容的 JavaScript 版本是 ECMAScript 2009 (ES5),这是一个安全的目标。为了让 JavaScript 开发者和用户都满意,社区提出了一类叫做 transpilers 的工具,其中 Babel 是最受欢迎的化身。有了这样的工具,我们可以编写现代的 JavaScript 代码,将它传递到一个转换/编译管道中,并最终得到兼容的 JavaScript 代码。在典型的设置中,我们配置一个构建管道,其中:

  1. Webpack 吸收了用现代 JavaScript 编写的 ES 模块。

  2. webpack 加载器通过 Babel 传递代码。

  3. 巴贝尔破译了密码。

webpack/Babel duo 现在无处不在,被 create-react-app、Vue CLI 等等使用。

打字稿上的一句话

对于大多数开发人员来说,TypeScript 是房间里的大象。

TypeScript 作为 JavaScript 的静态类型化下降,更类似于 C#或 Java 这样的语言。它在 Angular 世界中广泛存在,并且正在征服越来越多的 JavaScript 库,这些库现在默认带有类型定义。不管你喜不喜欢 TypeScript,它都是一个需要考虑的工具。在第 8 、 11 和 12 章中,我们在 React 中使用打字稿。

JavaScript 前端库和框架

多年来,JavaScript 领域发生了巨大的变化。jQuery 仍然拥有很大的市场份额。

但是当涉及到客户端应用时,这些都是用 React 和 Vue.js 等现代前端库或 Angular 等成熟的框架编写或重写的。Django 主要由 HTML 模板支持,但是一旦时机成熟,它可以与几乎任何 JavaScript 库配对。如今,这一领域由三个竞争者主导:

  • React,来自脸书的 UI 库,它普及了(但不是首创)基于组件的界面编写方法

  • Vue.js,来自前 Angular 开发者尤雨溪的渐进式 UI 库,因其进步性而大放异彩

  • Angular,包含电池的框架,基于 TypeScript

在这三人中,Vue.js 是最进步的。Angular 包含更多的电池(就像 Django 一样),但有一个陡峭的学习曲线。相反,React 是最自由的,因为它没有对开发人员施加任何约束。你可以选择任何你需要的库。不管这是不是一个优势,我把意见留给你。要记住的重要一点是,核心 UI 库只是解决另一组问题的许多依赖项的起点,这些问题是在编写更大的客户端应用时出现的。特别是,您迟早会需要:

  • 国家管理图书馆

  • 路由库

  • 模式验证库

  • 表单验证库

每个 UI 库都有自己的附属子库来处理上述问题。React 依靠 Redux 或 Mobx(最近也依靠反冲. js)进行状态管理,依靠 React Router 进行路由。Vue.js 使用 Vuex 进行状态管理,使用 Vue 路由器进行路由。Angular 有很多不同的状态管理方法,但是 NgRx 是最广泛使用的。最终,所有这些库和框架都可以作为 Django 的外部客户端很好地工作,或者成对出现:

  • 客户端应用从 Django REST/GraphQL API 获取数据

  • 使用 Django 作为内容源的服务器端呈现或静态站点生成器

我们将在本书后面更详细地探讨这两个主题。在下一节中,我们快速看一下传统单页方法的一些替代方法。

轻量级 JavaScript UI 库

除了 Angular、Vue、React 和 Svelte,还有越来越多的轻量级 JavaScript 迷你框架,它们旨在简化前端最普通的任务,并提供足够的 JavaScript 来运行。

在这一类别中,我们可以提及以下工具:

  • (Alpines)人名

  • 热线的

  • 断续器

Hotwire 是由 Ruby on Rails 及其创造者 David Heinemeier Hansson 推广的一套工具和技术。在撰写本文时,有一项名为 turbo-django 的实验性工作,旨在将这些技术移植到 django 中。同样,还有一个新的 Django 框架叫做 django-unicorn 。所有这些工具都提供了一种不太依赖 JavaScript 的方法来构建交互式界面。一旦它们开始在野外获得牵引力,它们将值得一看。

通用 JavaScript 应用

Node.js 是一个在浏览器之外运行 JavaScript 代码的环境。这意味着服务器和 CLI 工具。

我们在本章中提到的大多数工具都是基于 JavaScript 的,因为它们运行在命令行上,所以它们需要一个 JavaScript 环境,Node.js 提供了这个环境。现在,如果您将它与能够在任何 JavaScript 环境中运行的前端库结合起来,而不仅仅是浏览器(如 React 和 Vue.js),您将获得一种特殊的 JavaScript 工具,它通过以 JavaScript 为中心的方法在服务器端呈现中风靡一时。

当谈到 MVC web 框架时,我们已经在第一章提到了服务器端渲染。在传统的服务器端渲染中,HTML 和数据是由 Ruby、Python 或 Java 等服务器端语言生成的,而在以 JavaScript 为中心的服务器端渲染方法中,一切都是由 Node.js 上的 JavaScript 生成的。这对最终用户和开发人员意味着什么?基于 JavaScript 的客户端应用和服务器端呈现的应用之间的主要区别在于,后者在将 HTML 发送给用户或搜索引擎爬虫之前生成 HTML。与纯客户端应用相比,这种方法有许多优点:

  • 它改善了内容密集型网站的搜索引擎优化

  • 它提高了性能,因为主要的渲染工作都推给了服务器

相反,对于开发人员来说,通用 JavaScript 应用是代码重用的圣杯,因为所有东西都可以用一种语言 JavaScript 编写。这些工具背后的推理和使用它们的动机大致如下:

  • 我们已经有了一个大的客户端应用,我们希望改善它的性能,无论是对最终用户还是爬虫

  • 我们在前端和 Node.js 后端之间有很多公共代码,我们希望重用它们

然而,与任何技术一样,通用 JavaScript 应用也有自己的缺点。对于一个专注于 Python 的 Django 商店来说,维护一个基于 Node.js 的并行架构可能会很费力。这些设置需要 Node.js 服务器才能运行,这带来了维护负担和复杂性。像 Vercel 和 Netlifly 这样的平台简化了这些架构的部署,但是仍然需要记住一些事情。目前用于创建通用 JavaScript 应用的最流行的工具有:

  • Next.js for React

  • Nuxt.js 表示 vue . js

  • 角度通用

可能还有一百万种以上的工具。在第七章,我们重点介绍 Next.js。

静态站点生成器

虽然 Next.js 和 Nuxt.js 等工具提供的服务器端呈现方法确实很有趣,但在搜索引擎优化至关重要并且某些页面上很少或没有 JavaScript 驱动的交互的所有情况下(例如,想想博客),静态站点生成应该是首选。

使用 JavaScript 生成静态站点的当前场景包括:

  • 盖茨比(姓)

  • Next.js for React

  • Nuxt.js 表示 vue . js

  • 斯考利代表安格尔

Next.js 和 Nuxt.js 可以工作在两种模式下:服务器端渲染静态站点生成。为了从后端获取数据,这些工具提供了向 REST API 或 GraphQL 发出普通 HTTP 请求的接口。Gatsby 只使用 GraphQL,并不适合每个团队。

测试工具

整个章节 8 致力于测试 Django 和 JavaScript 应用。在这一节中,我们将简要介绍最流行的 JavaScript 测试工具。它们属于测试的传统分类。

单元测试:

  • 玩笑

端到端测试:

  • 柏树

  • 谷歌木偶师

  • 微软剧作家

对于单元测试和多个单元之间的集成测试,Jest 是迄今为止最流行的工具。它可以测试纯 JavaScript 代码,也可以测试 React/Vue.js 组件。对于端到端测试和功能测试,Cypress 是功能最全的测试运行程序,与 Django 配合得也很好,木偶师和剧作家也越来越受欢迎。说实话,Jest 和 Cypress 更像是现有测试库的包装器:Jest 构建在 Jasmine 之上,而 Cypress 构建在 Mocha 之上,因为它们从这些库中借用了大量方法。然而,与更传统的工具相比,它们的流行是由它们提供的流畅的测试 API 引发的。

其他辅助 JavaScript 工具

如果不提辅助的 JavaScript 工具,那将是我的失职,这对现代 JavaScript 开发人员来说是如此重要。

在 Python 和 JavaScript 领域,都有代码短句。对于 JavaScript 来说,ESLint 是最普遍的。然后我们有更漂亮的代码格式化程序。在纯 JavaScript 代码和设计系统的交叉点上,我们找到了 Storybook,一个构建设计系统的强大工具。Storybook 在 React 和 React Native 社区中广泛使用,但与最流行的前端库兼容,如 Vue 和 Svelte。与测试工具一起,linters、formatters 和 UI 工具为每个 JavaScript 和 Django 开发人员提供了一个强大的武器库。

摘要

本章探讨了 Django 和客户端应用的界限。您了解了:

  • 生产中的 JavaScript 和 Django

  • 模块捆绑器和代码分割

  • webpack 如何集成到 Django 中

  • JavaScript 工具整体

  • 通用 JavaScript 应用

在下一章,我们将介绍异步 Django 环境。

额外资源

三、现代 Django 和 Django REST 框架

本章涵盖:

  • Django REST 框架和 Django 并排

  • 异步 Django

我猜所有 Django 开发者都有一个共同的故事。他们构建了许多东西,并尝试了类似 Flask 的迷你框架方法,但最终,他们总是回到 Django,因为它固执己见,并且它提供了用 Python 构建全栈 web 应用的所有工具。Django REST 框架是遵循相同的实用方法的 Django 包。在这一章中,我们比较了 Django REST 框架和 Django,并探索了异步 Django 环境。

什么是 Django REST 框架?

Django REST 框架(简称 DRF)是一个用于构建 Web APIs 的 Django 包。

尽管 GraphQL 迅速普及,并且出现了 Starlette 和 FastAPI 等异步微框架,但 DRF 仍然为数以千计的 web 服务提供支持。DRF 与 Django 无缝集成,以补充其构建 REST APIs 的特性。特别是,它提供了一系列现成的组件:

  • 基于类的 REST 视图

  • viewster

  • 序列化程序

这一章并不打算作为初学者的 DRF 指南,但是花一些时间来浏览一下这个包的主要构件是值得的。在接下来的部分中,我们将探索这些组件,因为它们将是我们的解耦 Django 项目的第一部分的乐高积木。

Django 和 DRF 的阶级观

在构建 web 应用时,处理数据插入和数据列表的一些常见模式会反复出现。

以 HTML 表单为例。需要考虑三个不同的阶段:

  • 显示表单,可以是空的,也可以有初始数据

  • 验证用户输入并显示最终错误

  • 将数据保存到数据库

在我们的项目中反复复制粘贴相同的代码是愚蠢的。出于这个原因,Django 提供了一个方便的 web 开发中常见模式的抽象。这些类被命名为基于类的视图,或者简称为 CBV 。Django 的 CBV 的一些例子是CreateViewListViewDeleteViewUpdateViewDetailView。您可能已经注意到,这些类的命名与 CRUD 模式密切相关,这在 REST APIs 和传统 web 应用中很常见。特别是:

  • CreateViewUpdateView用于POST请求

  • ListViewDetailView用于GET请求

  • DeleteView对于DELETE个请求

Django REST 框架遵循相同的约定,并为 REST API 开发提供了一个广泛的基于类的视图工具箱:

  • CreateAPIView对于POST个请求

  • ListAPIViewRetrieveAPIView用于GET请求

  • DestroyAPIView对于DELETE个请求

  • UpdateAPIView对于PUTPATCH的请求

此外,您可以使用 cbv 的组合进行检索/删除操作,如RetrieveDestroyAPIView,或者检索/更新/销毁,如RetrieveUpdateDestroyAPIView。您将在解耦的 Django 项目中使用许多这样的 cbv 来加速最常见任务的开发,尽管 DRF 在 cbv 之上提供了一个更强大的层,称为视图集

Tip

关于 Django 中基于类的视图的完整列表,请参见ccbv.co.uk。关于 Django REST 框架,请参见cdrf.co

drf 中的 crud 查看器

在第一章中,我们回顾了作为 REST 主要构件之一的资源的概念。

在 MVC 框架中,对资源的操作由公开 CRUD 动词方法的控制器来处理。我们还澄清了 Django 是一个 MVT 框架,而不是 MVC。在 Django 和 DRF 中,我们使用基于类的视图,按照GETPOSTPUT等等来展示常见的 CRUD 操作。然而,Django REST 框架在基于类的视图上提供了一个聪明的抽象,称为视图集,这使得 DRF 看起来比以前更加“足智多谋”。清单 3-1 显示了一个视图集,特别是一个ModelViewSet

from rest_framework import viewsets
from .models import Blog, BlogSerializer

class BlogViewSet(viewsets.ModelViewSet):
   queryset = Blog.objects.all()
   serializer_class = BlogSerializer

Listing 3-1A ModelViewSet in DRF

这样的视图集免费为您提供了处理常见 CRUD 操作的所有方法。表 3-1 总结了视图集方法、HTTP 方法和 CRUD 操作之间的关系。

表 3-1

视图集方法、HTTP 方法和 CRUD 操作之间的关系

|

视图集方法

|

HTTP 方法

|

CRUD 操作

| | --- | --- | --- | | create() | POST | 创建资源 | | list() / retrieve() | GET | 检索资源 | | update() | PUT | U 更新资源 | | destroy() | DELETE | 删除资源 | | update() | PATCH | 部分更新资源 |

一旦有了视图集,就只需要用urlpatterns连接类了。清单 3-2 显示了前一视图集的urls.py

from .views import BlogViewSet
from rest_framework.routers import DefaultRouter

router = DefaultRouter()
router.register(r"blog", BlogViewSet, basename="blog")
urlpatterns = router.urls

Listing 3-2Viewset and Urlpatterns in Django REST

正如您所看到的,用最少的代码,您就拥有了 CRUD 操作的完整集合,以及相应的 URL。

模型、表单和序列化程序

用很少或根本不用代码创建页面和表单的能力让 Django 大放异彩。

例如,由于有了模型表单,从 Django 模型开始创建表单只需要几行代码,完成验证和错误处理,就可以包含在视图中了。当你很急的时候,你甚至可以组装一个CreateView,它正好需要三行代码(至少)来产生一个模型的 HTML 表单,附加到相应的模板上。如果 Django 模型表单是最终用户和数据库之间的桥梁,那么 Django REST 框架中的序列化器就是最终用户、我们的 REST API 和 Django 模型之间的桥梁。序列化器负责 Python 对象的序列化和反序列化,它们可以被认为是 JSON 的模型形式。考虑清单 3-3 中所示的模型。

class Quote(models.Model):
   client = models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
   proposal_text = models.TextField(blank=True)

Listing 3-3A Django Model

从这个模型,我们可以制作一个 DRF 模型序列化器,如清单 3-4 所示。

class QuoteSerializer(serializers.ModelSerializer):
   class Meta:
       model = Quote
       fields = ["client", "proposal_text"]

Listing 3-4A DRF Serializer

当我们到达 DRF 端点时,在任何输出显示给用户之前,序列化程序将底层模型实例转换成 JSON。反之亦然,当我们对 DRF 视图发出一个POST请求时,序列化程序会将我们的 JSON 转换成相应的 Python 对象,而不是在验证输入之前。序列化器也可以表达模型关系。在清单 3-4 中,Quote通过多对一关系连接到定制用户模型。在我们的序列化程序中,我们可以将这种关系公开为超链接,如清单 3-5 所示(还记得超媒体 API 吗?).

class QuoteSerializer(serializers.ModelSerializer):
   client = serializers.HyperlinkedRelatedField(
       read_only=True, view_name="users-detail"
   )

   class Meta:
       model = Quote
       fields = ["client", "proposal_text"]

Listing 3-5A DRF Serializer

这将产生清单 3-6 中所示的 JSON 输出。

[
   {
     "client": "https://api.example/api/users/1",
     "proposal_text": "Django quotation system"
   },
   {
     "client": "https://api.example/api/users/2",
     "proposal_text": "Django school management system"
   }
]

Listing 3-6A JSON Response with Relationships

在第六章中,我们使用序列化器来解耦我们的 Django 项目。概述了 DRF 的构建模块之后,现在让我们来探索异步 Django 的奇妙世界。

从 WSGI 到 ASGI

WSGI 是 web 服务器与 Python 通信的通用语言,也就是说,它是一种能够在 web 服务器(如 Gunicorn)和底层 Python 应用之间来回通信的协议。

正如第二章所预期的,Django 需要一个 web 服务器来在生产中高效运行。通常,像 NGINX 这样的反向代理充当最终用户的主要入口点。Python WSGI 服务器监听 NGINX 背后的请求,并充当 HTTP 请求和 Django 应用之间的桥梁。在 WSGI 中,所有的事情都是同步发生的,没有办法在不引入重大改变的情况下重写协议。这使得社区(对于这项巨大的工作,我们必须感谢 Andrew Godwin)编写了一个新的协议,称为 ASGI,用于在支持 ASGI 的 web 服务器下运行异步 Python 应用。为了异步运行 Django,我们将在下一节看到这意味着什么,我们需要一个支持异步的服务器。你可以选择达芙妮,Hypercorn,或 Uvicorn。在我们的例子中,我们将使用 Uvicorn。

异步 Django 入门

异步代码完全是关于非阻塞执行的。这是 Node.js 等平台背后的魔力,这些平台早在高吞吐量服务领域出现多年。

相反,在 Python 3.5 (2015)中的async/await到来之前,异步 Python 的前景一直是支离破碎的,有许多 pep 和竞争的实现。在 Django 3.0 之前,Django 中的异步是一个梦想,当时对上述 ASGI 的开创性支持进入了核心。异步视图(Django 3.1)是 Django 近年来最令人兴奋的新增功能之一。为了理解异步在 Django 和 Python 中解决了什么问题,考虑一个简单的 Django 视图。当用户到达这个视图时,我们从外部服务获取一个链接列表,如清单 3-7 所示。

from django.http import JsonResponse
import httpx

client = httpx.Client(base_url="https://api.valentinog.com/demo")

def list_links(_request):
    links = client.get("/sleep/").json()
    json_response = {"links": links}
    return JsonResponse(data=json_response)

Listing 3-7A Synchronous View Doing Network Calls

这应该会立即引发一个危险信号。它可以运行得很快,真的很快,或者永远无法完成,让浏览器挂起。由于 Python 解释器的单线程性质,我们的代码按顺序步骤运行。在我们看来,在 API 调用完成之前,我们不能向用户返回响应。事实上,我的链接 https://api.valentinog.com/demo/sleep/ 被配置为在返回结果之前休眠 10 秒钟。换句话说,我们的视线被挡住了。这里的httpx,我用来发出请求的 Python HTTP 客户机,被配置了一个安全超时,几秒钟后会抛出一个异常,但是并不是每个库都有这种安全措施。

任何 IO 绑定的操作都有可能导致资源匮乏或阻塞整个执行过程。传统上,为了在 Django 中解决这个问题,我们会使用一个任务队列,它是一个在后台运行的组件,拾取要执行的任务,并在稍后返回结果。Django 最受欢迎的任务队列是 Celery 和 Django Q。强烈建议将任务队列用于 IO 绑定的操作,如发送电子邮件、运行调度的作业、HTTP 请求,或者需要在多个内核上运行的 CPU 绑定的操作。Django 中的异步视图不能完全取代任务队列,特别是对于 CPU 受限的操作。例如 Django Q 使用 Python multiprocessing。相反,对于非关键的 IO 绑定操作,比如 HTTP 调用或发送电子邮件,Django 异步视图非常好。在最简单的情况下,您可以发送电子邮件或调用外部 API,而不会有阻塞用户界面的风险。那么异步 Django 视图中有什么呢?让我们用异步视图重写前面的例子,让httpx客户机在后台检索数据;见清单 3-8 。

from django.http import HttpResponse
import httpx
import asyncio

async def get_links():
   base_url = "https://api.valentinog.com/demo"
   client = httpx.AsyncClient(base_url=base_url, timeout=15)
   response = await client.get("/sleep")
   json_response = response.json()
   # Do something with the response or with the json
   await client.aclose()

async def list_links(_request):
   asyncio.create_task(get_links())
   response = "<p>Fetching links in background</p>"
   return HttpResponse(response)

Listing 3-8An Asynchronous View Doing Network Calls, This Time Safely

如果您从未使用过异步 Python 和 Django,那么这段代码中有一些新概念值得澄清。首先我们导入asyncio,我们和异步 Python 世界之间的桥梁。然后我们用async def声明第一个异步函数。在第一个函数get_links()中,我们使用异步httpx客户端,超时为 15 秒。因为我们将在后台运行这个调用,所以我们可以安全地增加超时。接下来,我们在client.get()前使用await。最后,我们用client.aclose()关闭客户端。为了避免资源处于开放状态,您还可以将异步客户端与异步上下文管理器一起使用。在这种情况下,我们可以重构到async with,如清单 3-9 所示。

async def get_links():
   base_url = "https://api.valentinog.com/demo"
   async with httpx.AsyncClient(base_url=base_url, timeout=15) as client:
       response = await client.get("/sleep")
       json_response = response.json()
       # Do something with the json ...

Listing 3-9Using an Asynchronous Context Manager

Tip

异步上下文管理器实现了__aenter____aexit__,而不是__enter____exit__

在第二个异步函数list_links(),我们的 Django 视图中,我们使用asyncio.create_task()在后台运行get_links()。这才是真正的新闻。async def在 Django 看来,从开发人员的角度来看,这是最显著的变化。相反,对用户来说,最明显的好处是,如果执行时间比预期的长,他们不必等待查看 HTML。例如,在我们之前设想的场景中,我们可以稍后用电子邮件消息将结果发送给用户。这是 Django 中异步视图最引人注目的用例之一。但这并没有停止。概括一下,现在异步 Django 已经成为一个事物,您可以做的事情有:

  • 在一个视图中高效地并行执行多个 HTTP 请求

  • 计划长期运行的任务

  • 与外部系统安全交互

在 Django 和 DRF 变成 100%异步之前还缺少一些东西 ORM 和 Django REST 视图不是异步的——但是我们将在我们的解耦项目中到处使用异步 Django 功能来实践。

竞争的异步框架和 DRF

在撰写本文时,Django REST 框架还不支持异步视图。

有鉴于此,使用 FastAPI 或 Starlette 之类的东西来构建异步 web 服务不是更好吗?Starlette 是由 DRF 创建者 Tom Christie 构建的 ASGI 框架。相反,FastAPI 构建在 Starlette 之上,为构建异步 Web APIs 提供了一流的开发工具。两者都是绿地项目的绝佳选择,幸运的是你不必选择,因为 FastAPI 可以在 Django 本身内部运行,这要感谢像django-ninja这样的实验项目,而我们在等待异步 DRF。

摘要

本章回顾了 Django REST 框架的基础,并介绍了如何运行一个简单的异步 Django 视图。你学到了:

  • 什么是 DRF 基于类的视图、视图集和序列化程序

  • 如何创建异步 Django 视图

  • 如何在独角兽下经营 django

在下一章,我们将详细分析解耦合 Django 的模式,而在第六章,我们将最终接触到 Django 和 JavaScript 前端。

额外资源

四、解耦架构的优点和缺点

在这一章中,我们概述了分离 Django 项目的各种方法。特别是,我们涵盖:

  • 混合架构

  • 基于 REST 和 GraphQL 的完全解耦架构

  • 两种风格的优缺点

到本章结束时,你应该能够辨别一种或多种解耦风格,并成功地应用到你的下一个 Django 项目中。

伪解耦 Django

伪解耦,或混合解耦 *,*是一种用少量 JavaScript 扩充静态前端的方法;只要能让最终用户觉得事情有互动性和趣味性就够了。

在接下来的两节中,我们将通过研究两种不同的方法来讨论伪解耦设置的优点和缺点:无 REST 和有 REST。

无休止伪解耦

根据您编程的时间长短,您会开始注意到在构建 web 应用时有一类模式反复出现:数据获取和表单处理。

例如,在 Django 应用中可能有一个插入新数据的页面。如何处理数据插入取决于用户需求,但基本上有两种选择:

  • 用 Django 单独处理表单

  • 用 JavaScript 处理表单

Django 表单和模型表单能够很好地为您生成字段,但是大多数时候我们希望拦截表单处理的经典GET / POST /Redirect 模式,尤其是表单的submit事件。为此,我们在 Django 模板中引入了一点 JavaScript。清单 4-1 展示了这样一个例子。

{% block script %}
   <script>
       const form = document.getElementById("reply-create");
       form.addEventListener('submit', function (event) {

           event.preventDefault();
           const formData = new FormData(this);

           fetch("{% url "support:reply-create" %}", {
               method: 'POST',
               body: formData
           }).then(response => {
               if (!response.ok) throw Error(response.statusText);
               return response;
           }).then(() => {
               location.reload();
               window.scrollTo({top:0});
           });
       });
{% endblock %}

Listing 4-1JavaScript Logic for Form Handling

在这个例子中,我们将 JavaScript 绑定到表单上,这样当用户提交数据时,表单的默认事件就会被拦截和停止。接下来,我们构建一个FormData对象,它被发送给 Django CreateView。还要注意我们如何使用 Django 的url模板标签来构建获取的 URL。为了让这个例子工作,表单必须包含一个CSRF标记,如清单 4-2 所示。

<form id="reply-create">
   {% csrf_token %}
   <!-- fields here -->
</form>

Listing 4-2Django’s CSRF Token Template Tag

如果令牌在表单之外,或者对于任何其他不直接来自表单的POST请求,CSRF令牌必须包含在XHR请求头中。这里概述的例子只是 JavaScript 在 Django 模板中的众多用例之一。正如在第二章 ?? 中简要提到的,我们看到了微框架的寒武纪大爆发,它为 Django 模板增加了足够的交互性。本书篇幅有限,无法涵盖所有可能的例子。在这里,我们将重点放在更广泛的架构上,检查每种方法的优缺点。图 4-1 显示了没有 REST 的伪解耦 Django 的表示。

img/505838_1_En_4_Fig1_HTML.png

图 4-1

一个伪解耦的 Django 项目可以有一个或多个应用,每个应用都有自己的模板。JavaScript 融入到模板中,并与常规 Django 视图对话

记住 Django 在开发速度方面所提供的东西,在伪解耦或混合方法的情况下,我们得到了什么,又失去了什么?

  • 认证和 cookie:因为我们在 Django 模板中提供 JavaScript,所以我们不需要担心复杂的认证方法。我们可以使用内置的会话认证。此外,在伪解耦设置中,cookies 可以在同一个域中的每个请求上自由传播。

  • 表单:Django 有一个惊人的表单系统,在开发过程中节省了大量时间。在伪解耦设置中,我们仍然可以使用 Django 表单来构建用于数据插入的 HTML 结构,只需要足够的 JavaScript 来使它们具有交互性。

  • 什么 JS 库?在伪解耦设置中,我们可以使用任何不需要构建管道的轻量级前端库,比如 Vue.js,或者更好的普通 JavaScript。如果我们事先知道我们的目标是什么用户代理,我们就可以提供现代的 JavaScript 语法,而不需要编译步骤。

  • 路由 : Django 负责路由和 URL 构建。无需担心 JavaScript 路由库或浏览器后退按钮的奇怪问题。

  • 搜索引擎优化:对于内容密集型网站,伪解耦设置往往是最安全的选择,只要我们不使用 JavaScript 动态生成关键内容。

  • 开发人员生产力/负担:在混合设置中,JavaScript 的数量希望很低,以至于我们不需要复杂的构建工具。一切仍然以 Django 为中心,开发人员的认知负荷很低。

  • 测试:在 Django 应用的上下文中测试 JavaScript 交互总是很棘手。Selenium for Python 不支持自动等待。有很多工具,主要是 Selenium 的包装器,比如 Splinter,都有这种能力。然而,在没有支持 JavaScript 的测试运行器的情况下测试伪解耦的 Django 前端仍然很麻烦。像 Cypress 这样的工具,我们将在第九章中介绍,与 Django 配合得非常好,减轻了测试 JavaScript 丰富的接口的负担。

与 REST 伪解耦

不是每个应用都必须被设计成单页应用,从本书开始我们就强调了这一点。

按照唐纳德·克努特的说法,过度设计的应用是万恶之源。然而,也有混合的情况,UI 需要大量的 JavaScript 交互性,而不仅仅是简单的表单处理,但是我们仍然不想离开 Django 的保护伞。在这些配置中,你会发现在 Django 项目中引入像 Vue.js 或 React 这样的 JavaScript 库是合理的。虽然 Vue.js 是高度进步的,但它不想控制所有页面。React 迫使开发人员在 React 中做所有的事情。在这些情况下,由模板构成并增加了表单或模型表单的 Django 前端可能会失去重要性,而支持伪解耦设置,从而:

  • 一个或多个 Django 应用的前端完全由 JavaScript 构建

  • 后端公开了一个 REST API

这种设置与前端与 REST API 位于不同域/源的架构之间的区别在于,在伪解耦设置中,我们在同一个 Django 项目中为 SPA 前端和 REST API 提供服务。这有许多积极的副作用。为什么要在这样的设置中引入 REST?Django CreateView和模型在一定程度上工作良好,之后我们不想重新发明轮子,就像模型的 JSON 序列化一样。Django REST 与现代前端库的结合为健壮的解耦项目打下了坚实的基础。图 4-2 显示了一个带有 REST 的伪解耦 Django 的表示。

img/505838_1_En_4_Fig2_HTML.png

图 4-2

一个带有 REST 的伪解耦 Django 项目可以有一个或多个应用,每个应用都有自己的 REST API。JavaScript 作为 Django 项目中的单页应用,与 Django REST 视图对话

在下一章中,我们将看到一个使用 Django REST 框架和 Vue.js 的伪解耦设置的实际例子。在这里,我们将讨论伪解耦配置的优点和缺点,正如我们在上一节中对 REST-less 设置所做的那样。

  • 认证和 cookie:基于会话的认证是伪解耦项目的默认选择,即使是 REST 也是如此。因为我们在同一个 Django 项目中提供单页面应用,所以在从 JavaScript 发出POST请求之前,只需要通过常规的 Django 视图对用户进行身份验证,并获取适当的 cookies。

  • 表单:如果我们决定构建一个或多个 Django 应用作为单页面应用,我们就失去了使用 Django 表单和模型表单的能力。这开始导致代码重复和团队的更多工作,因为 good 'ol Django 表单及其数据验证层必须用选择的 JavaScript 库重新实现。

  • *什么 JS 库?*在 REST 的伪解耦设置中,我们可以使用任何 JavaScript 库或框架。这需要一些额外的步骤来将包包含在 Django 静态系统中,但是这对于任何库都是可能的。

  • 路由:Django 项目中的单页面应用的路由实现起来并不容易。Django 仍然可以为每个应用提供主路径,例如 https://decoupled-django.com/billing/ ,但是每个应用都必须处理其内部路由。与基于历史的路由相比,基于哈希的路由是最简单的路由形式,也最容易实现。

  • 搜索引擎优化:单页面应用(SPAs)不适合内容丰富的网站。在将 SPA 集成到 Django 之前,这是需要考虑的最重要的方面之一。

  • 开发人员的生产力/负担:任何现代的 JavaScript 库都有自己的挑战和工具。在使用 REST 和一个或多个单页面应用的伪解耦设置中,Python 开发人员的开销可能会呈指数级增长。

  • 测试:在使用少量 JavaScript 的伪解耦设置中,考虑到实现自动等待 JavaScript 交互的需要,使用 Selenium 或 Splinter 等工具可能是有意义的。相反,在基于 REST 和 SPA 的伪解耦配置中,以 Python 为中心的工具表现不佳。要测试大量使用 JavaScript 的接口和 JavaScript UI 组件,比如用 Vue.js 或 React 实现的那些,像 Cypress 这样的工具用于功能测试,Jest 用于单元测试是更好的选择。

完全解耦的 Django

与伪解耦设置相反,完全解耦架构,也称为无头,是一种前端和后端完全分离的方法。

在前端,我们可以发现 JavaScript 单页面应用位于与后端不同的域/源上,后端现在充当 REST 或 GraphQL 的数据源。在接下来的两节中,我们将讨论这两种方法。

与 REST 完全解耦

与 REST 完全解耦的 Django 项目是目前最普遍的设置之一。由于其高度的灵活性,REST API 和前端可以部署在不同的域或源上。Django REST 框架是用于在 Django 中构建 REST APIs 的事实上的库,而 JavaScript 以 React、Vue.js 和 Angular 领先于前端。在这些配置中,架构通常安排如下:

  • 一个或多个 Django 应用的前端作为单页 JavaScript 应用存在于 Django 之外

  • 一个或多个 Django 应用公开了一个 REST API

用 REST API 完全解耦配置的 Django 项目可以很好地充当:

  • SPA、移动应用或渐进式 Web 应用的 REST API

  • 静态站点生成工具(SSG)或服务器端呈现的 JavaScript 项目(SSR)的内容存储库

图 4-3 显示了完全解耦的 Django 项目与 REST 的关系。

img/505838_1_En_4_Fig3_HTML.png

图 4-3

带有 REST 的完全解耦的 Django 项目可以有一个或多个应用,每个应用都有自己的 REST API。JavaScript 作为单页应用存在于 Django 项目之外,并通过 JSON 与 Django REST 视图对话

值得注意的是,并不是项目中的每个 Django 应用都必须公开 REST API:人们可以选择分离应用的一个或多个方面,而将其余部分保持在经典的 MVT 安排下。REST 规定的关注点分离为灵活但更复杂的设置开辟了道路。如果我们将 Django 项目与 REST 解耦,会有什么结果呢?

  • 认证和 cookie:完全解耦项目的认证实现起来并不容易。基于会话的认证可以用于 REST 和单页面应用,但是它打破了无状态的限制。有许多不同的方法可以绕过 REST APIs 的基于会话的身份验证的限制,但是在后来的几年中,社区似乎倾向于采用无状态的身份验证机制,比如基于令牌的 JWT 身份验证(JSON web tokens)。然而,由于其安全缺陷和潜在的实施陷阱,JWT 在 Django 社区并不那么受欢迎。

  • 表单:离开 Django 模板和表单意味着我们失去了轻松构建表单的能力。在完全解耦的设置中,表单层通常完全由 JavaScript 构建。数据验证经常在前端重复,现在必须在向后端发送请求之前清理和验证用户输入。

  • 什么 JS 库?在与 REST 完全解耦的设置中,我们可以使用任何 JavaScript 库或框架。将 Django REST 后端与解耦前端配对没有任何特别的限制。

  • *路由:*在完全解耦的设置中,Django 不再处理路由。一切都压在客户肩上。对于单页应用,可以选择实现基于哈希的路由或历史路由。

  • 搜索引擎优化:单页应用不太会玩 SEO。然而,随着诸如 Gatsby、Next.js 和 Nuxt.js 等 JavaScript 静态站点生成器的出现,JavaScript 开发人员可以使用最新的闪亮工具从一个无头 Django 项目中生成静态页面,而没有损害 SEO 的风险。

  • 开发人员生产力/负担:在 REST 和一个或多个单页面应用完全解耦的环境中,Python 开发人员的工作量会成倍增加。出于这个原因,大多数 Django 和 Python web 代理都有一个专门的前端团队,专门处理 JavaScript 及其相关工具。

  • 测试:在完全解耦的项目中,前端和后端是分开测试的。APITestCaseAPISimpleTestCase帮助测试 Django REST APIs,而在前端,我们再次看到 Jest 和 Cypress 用于测试 UI。

与 GraphQL 完全解耦

与使用 REST 的完全解耦的 Django 一样,使用 GraphQL 的完全解耦的 Django 项目提供了高度的灵活性,但也带来了更多的技术挑战。

REST 是一项久经考验的技术。另一方面,GraphQL 是最近才出现的,但似乎比 REST 有一些明显的优势。然而,与任何新技术一样,在生产项目中集成新工具和潜在的新挑战之前,开发人员和 CTO 必须仔细评估优缺点。图 4-4 显示了一个用 GraphQL 和 REST API 解耦的 Django 项目。

img/505838_1_En_4_Fig4_HTML.png

图 4-4

完全解耦的 Django 项目可以公开 REST 和 GraphQL APIs。在同一个项目中使用这两种技术并不罕见

在图 4-4 中,我们想象了一个完全解耦的 Django 项目,它公开了两个不同的应用:一个使用 REST,另一个使用 GraphQL。事实上,GraphQL 可以与 REST 共存,以支持从遗留 REST API 到 GraphQL 端点的渐进重构。这对于在从 REST 切换之前评估 GraphQL 或者为 Gatsby 之类的工具公开 GraphQL API 非常有用。拥抱 GraphQL 要付出什么代价?让我想想。

  • 认证和 cookie:在完全解耦的设置中,GraphQL 的认证主要通过基于令牌的认证来处理。在后端,GraphQL 需要实现突变来处理登录、注销、注册和所有相关的极端情况。

  • *什么 JS 库?*在 GraphQL 的完全解耦设置中,我们可以使用任何 JavaScript 库或框架。将 Django GraphQL 后端与解耦前端配对没有任何特别的限制。GraphQL 查询甚至可以用Fetch或者XMLHttpRequest来完成。

  • 搜索引擎优化:前端的 GraphQL 多与 React 这样的客户端库配合使用。这意味着我们不能按原样发布客户端生成的页面,否则我们将冒 SEO 受损的风险。Gatsby、Next.js 和 Nuxt.js 等工具可以在 SSG(静态站点生成)模式下运行,从 GraphQL API 生成静态页面。

  • 开发人员生产力/负担 : GraphQL 是一种新颖的技术,尤其是在前端,有十几种方法可以实现数据提取层。GraphQL 似乎提高了开发人员的工作效率,但同时也引入了新的学习内容和新的模式。

由于 GraphQL 是一个数据提取层,所以对表单、路由和测试的考虑与解耦 REST 项目没有什么不同。

摘要

在这一章中,我们概述了分离 Django 项目的各种方法:

  • 带和不带 REST 的伪解耦

  • 与 REST 或 GraphQL 完全解耦

希望你现在已经准备好为你的下一个 Django 项目做出明智的选择。在下一章,我们在转移到 JavaScript 和 Vue.js 之前准备 Django 项目。

五、建立 Django 项目

本章涵盖:

  • 建立 Django 项目

在接下来的部分中,我们开始奠定 Django 项目的结构。

这个项目将伴随我们这本书的剩余部分。它将在第六章中用 REST API 扩展,稍后用 GraphQL API 扩展。

设置项目

首先,为项目创建一个新文件夹,并移入其中:

mkdir decoupled-dj && cd $_

Note

用 Git 将项目置于源代码控制之下是一个好主意。创建项目文件夹后,建议您立即用git init初始化 repo。

进入文件夹后,创建一个新的 Python 虚拟环境:

python3.9 -m venv venv

对于虚拟环境,你可以使用任何高于 3 的 Python 版本;版本越高越好。当环境准备就绪时,激活它:

source venv/bin/activate

要确认虚拟环境是活动的,请在命令提示符下查找(venv)。如果一切就绪,安装 Django:

pip install django

Note

最好安装 Django 3.1 以上的版本,支持异步视图。

接下来,创建您的 Django 项目:

django-admin startproject decoupled_dj .

关于项目文件夹结构的说明:

  • decoupled-dj是回购根

  • decoupled_dj是实际的 Django 项目

  • Django 应用位于decoupled-dj

准备好之后,创建两个 Django 应用。第一款应用命名为billing:

python manage.py startapp billing

第二个应用是博客:

python manage.py startapp blog

这些应用的简要说明。billing将是一个 Django 应用,公开一个用于创建客户发票的 REST API。blog会先公开一个 REST API,再公开一个 GraphQL API。现在检查项目根目录中的所有内容是否都已就位。运行ls -1,您应该会看到如下输出:

blog
billing
decoupled_dj
manage.py
venv

在下一节中,我们将继续项目定制,引入一个定制的 Django 用户。

自定义用户

尽管我们的项目并不严格要求,但是如果您决定将项目投入生产,那么从长远来看,自定义的 Django 用户可以节省您的时间。让我们创建一个。首先,创建一个新的应用:

python manage.py startapp user

打开users/models.py并创建自定义用户,如清单 5-1 所示。

from django.contrib.auth.models import AbstractUser
from django.db.models import CharField

class User(AbstractUser):
   name = CharField(blank=True, max_length=100)

   def __str__(self):
       return self.email

Listing 5-1A Custom Django User

我们保持自定义用户精简和简单,只有一个额外的字段,以允许未来进一步定制。下一步是将AUTH_USER_MODEL添加到我们的设置文件中,但是在此之前,我们需要根据环境来划分我们的设置。

Tip

在费德里科·马拉尼(Federico Marani)的名为《Django 2 和 2 频道的实用书籍(第四章中的“用户模型”一节)中,您将找到 Django 中自定义用户的另一个广泛示例。

插曲:选择正确的数据库

这一步本质上与我们的解耦 Django 项目无关,但是在任何 web 框架中,使用正确的数据库是您可以做的最重要的事情之一。在整本书中,我将使用 Postgres 作为数据库的选择。如果你也想这样做,下面是在你的机器上安装 Postgres 的方法:

  • MacOS 的 Postgres.app

  • 码头下的邮局

  • 通过软件包管理器直接在系统上安装 Postgres

如果您想使用 SQLite,请查看下一节中的说明。

拆分设置文件

在生产环境中部署时,分割设置特别有用,它是根据环境对 Django 设置进行分区的一种方式。在典型的项目中,您可能有:

  • 所有场景通用的基础环境

  • 开发环境,带有开发设置

  • 测试环境,其设置仅适用于测试

  • 登台环境

  • 生产环境

理论是,根据环境的不同,Django 从一个.env文件中加载它的设置。这种方法被称为十二因素应用,由 Heroku 于 2011 年首次推广。在 Django 有很多十二要素的图书馆。一些开发人员更喜欢使用os.environ来完全避免额外的依赖。我最喜欢的图书馆是django-environ。对于我们的项目,我们设置了三个环境:基础、开发和后期生产。让我们安装django-environpsycopg2:

pip install django-environ pyscopg2-binary

(psycopg2只有在使用 Postgres 时才是必需的。)接下来,我们在decoupled_dj.中创建一个名为settings的新 Python 包。在这个文件中,我们导入了django-environ,并放置了 Django 需要运行的所有东西,而不考虑具体的环境。这些设置包括:

  • SECRET_KEY

  • DEBUG

  • INSTALLED_APPS

  • MIDDLEWARE

  • AUTH_USER_MODEL

记住,在上一节中,我们配置了一个定制的 Django 用户。在基本设置中,我们需要在INSTALLED_APPS中包含自定义用户应用,最重要的是,配置AUTH_USER_MODEL。我们的基本设置文件应该类似于清单 5-2 。

import environ
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent
env = environ.Env()
environ.Env.read_env()
SECRET_KEY = env("SECRET_KEY")
DEBUG = env.bool("DEBUG", False)

INSTALLED_APPS = [
   "django.contrib.admin",
   "django.contrib.auth",
   "django.contrib.contenttypes",
   "django.contrib.sessions",
   "django.contrib.messages",
   "django.contrib.staticfiles",
   "users.apps.UsersConfig",
]
MIDDLEWARE = [ # OMITTED FOR BREVITY ]
ROOT_URLCONF = "decoupled_dj.urls"
TEMPLATES = [ # OMITTED FOR BREVITY ]
WSGI_APPLICATION = "decoupled_dj.wsgi.application
DATABASES = {"default": env.db()
AUTH_PASSWORD_VALIDATORS = [  # OMITTED FOR BREVITY  ]
LANGUAGE_CODE = "en-GB"
TIME_ZONE = "UTC"
USE_I18N = True
USE_L10N = True
USE_TZ = Tru
STATIC_URL = env("STATIC_URL")
AUTH_USER_MODEL = "users.User"

Listing 5-2Base Settings for Our Project

Note

为了简洁起见,我省略了以下配置的完整代码:MIDDLEWARETEMPLATESAUTH_PASSWORD_VALIDATORS。这些应该有来自 stock Django 的默认值。

接下来,我们在decoupled_dj/settings文件夹中创建一个.env文件。根据环境的不同,该文件将具有不同的值。对于开发,我们使用清单 5-3 中的值。

DEBUG=yes
SECRET_KEY=!changethis!
DATABASE_URL=psql://decoupleddjango:localpassword@127.0.0.1/decoupleddjango
STATIC_URL=/static/

Listing 5-3Environment File for Development

如果您想用 SQLite 代替 Postgres,将DATABASE_URL改为:

DATABASE_URL=sqlite:/decoupleddjango.sqlite3

要完成设置,创建一个名为decoupled_dj/settings/development.py的新文件,并从基本设置中导入所有内容。此外,我们还定制配置。这里我们将启用django-extensions,这是 Django 开发中的一个方便的库(清单 5-4 )。

from .base import *  # noqa
INSTALLED_APPS = INSTALLED_APPS + ["django_extensions"]

Listing 5-4decoupled_dj/settings/development.py – The Settings File for Development

让我们也安装库:

pip install django-extensions

我们不要忘记导出DJANGO_SETTINGS_MODULE环境变量:

export DJANGO_SETTINGS_MODULE=decoupled_dj.settings.development

现在,您可以进行迁移了:

python manage.py makemigrations

最后,您可以将它们应用到数据库:

python manage.py migrate

稍后,我们将测试我们的设置。

额外收获:在 ASGI 领导下管理 Django

为了异步运行 Django,我们需要一个 ASGI 服务器。在生产中,您可以将 Uvicorn 与 Gunicorn 一起使用。在开发中,您可能希望单独使用 Uvicorn。安装它:

pip install uvicorn

同样,如果您还没有导出DJANGO_SETTINGS_MODULE环境变量,请不要忘记这样做:

export DJANGO_SETTINGS_MODULE=decoupled_dj.settings.development

接下来,使用以下命令运行服务器:

uvicorn decoupled_dj.asgi:application

如果一切顺利,您应该会看到以下输出:

INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

如果你点击链接,你应该会看到熟悉的 Django 火箭!在继续之前还有一件事:我们需要分割需求文件。

拆分需求文件

正如我们对设置文件所做的那样,将 Django 应用的需求分开是一个很好的实践。我们将在接下来的大部分章节中从事开发工作,现在我们可以将需求分成两个文件:基础和开发。稍后,我们还将为测试和生产添加依赖项。创建一个名为requirements的新文件夹,将base.txtdevelopment.txt文件放入其中。在base文件中,我们放置了项目最重要的依赖项:

  • Django

  • django-environ用于处理.env文件

  • pyscopg2-binary用于连接 Postgres(如果您决定使用 SQLite,则不需要)

  • 在 ASGI 手下管理 Django 的 Uvicorn

您的requirements/base.txt文件应该如下所示:

Django==3.1.3
django-environ==0.4.5
psycopg2-binary==2.8.6
uvicorn==0.12.2

您的requirements/development.txt文件应该如下所示:

-r ./base.txt
django-extensions==3.0.9

Note

当你读到这本书的时候,你的版本很可能和我的不同。

从现在开始,要安装项目的依赖项,您将运行以下命令,其中要求文件将根据您所处的环境而有所不同:

pip install -r requirements/development.txt

Note

这是提交到目前为止所做的更改并将工作推送到 Git repo 的好时机。你可以在 https://github.com/valentinogagliardi/decoupled-dj/tree/chapter_05_setting_up_project 找到本章的源代码。

摘要

本章准备了 Django 项目,并解释了如何使用异步 ASGI 服务器运行 Django。你学到了:

  • 如何拆分设置和需求

  • 如何在独角兽下经营 django

在下一章,我们终于接触到了 Django 和 JavaScript 前端。

六、使用 Django REST 框架解耦 Django

本章涵盖:

  • 使用 Vue.js 的 Django REST 框架

  • Django 模板中的单页应用

  • 嵌套 DRF 序列化程序

在本章中,您将学习如何使用 Django REST 框架在您的 Django 项目中公开 REST API,以及如何在 Django 中提供单页应用。

构建计费应用

在前一章中,我们在 Django 项目中创建了一个计费应用。如果你还没有这样做,这里有一个快速回顾。首先,配置DJANGO_SETTINGS_MODULE:

export DJANGO_SETTINGS_MODULE=decoupled_dj.settings.development

然后,运行startapp命令来创建应用:

python manage.py startapp billing

如果您喜欢更大的灵活性和更多的输入,您可以将设置文件直接传递给manage.py:

python manage.py command_name --settings=decoupled_dj.settings.development

这里,command_name是您想要运行的命令的名称。您还可以用这个命令创建一个 shell 函数,以避免重复输入。

Note

本章的其余部分假设您在 repo root decoupled-dj中,Python 虚拟环境是活动的。

构建模型

对于这个应用,我们需要两个 Django 模型:Invoice用于实际的发票,ItemLine表示发票中的一行。让我们概括一下这些模型之间的关系:

  • 每个Invoice可以有一个或多个ItemLine

  • 一个ItemLine恰好属于一个Invoice

这是一个多对一(或一对多)关系,这意味着ItemLine将有一个Invoice的外键(如果您需要复习这个主题,请查看本章末尾的参考资料部分)。此外,每个Invoice都与一个User(我们在第三章中构建的自定义 Django 用户)相关联。这意味着:

  • 一个User可以有多个Invoice

  • 每个Invoice属于一个User

为了帮助理解这一点,图 6-1 显示了 ER 图,我们将在其上构建这些 Django 模型。

img/505838_1_En_6_Fig1_HTML.png

图 6-1

计费应用的 ER 图

定义了实体之后,现在让我们构建适当的 Django 模型。打开billing/models.py并如清单 6-1 所示定义模型。

from django.db import models
from django.conf import settings

class Invoice(models.Model):
   class State(models.TextChoices):
       PAID = "PAID"
       UNPAID = "UNPAID"
       CANCELLED = "CANCELLED"

   user = models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
   date = models.DateField()
   due_date = models.DateField()
   state = models.CharField(max_length=15, choices=State.choices, default=State.UNPAID)

class ItemLine(models.Model):
   invoice = models.ForeignKey(to=Invoice, on_delete=models.PROTECT)
   quantity = models.IntegerField()
   description = models.CharField(max_length=500)
   price = models.DecimalField(max_digits=8, decimal_places=2)
   taxed = models.BooleanField()

Listing 6-1billing/models.py – The Models for the Billing App

这里我们利用了 Django 3.0 附带的特性models.TextChoices。至于其他的,它们是标准的 Django 字段,所有的关系都是根据 ER 图建立的。为了增加一点保护,因为我们不想意外删除发票或项目行,我们在它们上面使用了PROTECT

启用应用

当模型准备好时,在decoupled_dj/settings/base.py中启用计费应用,如清单 6-2 所示。

INSTALLED_APPS = [
   ...
   "billing.apps.BillingConfig",
]

Listing 6-2decoupled_dj/settings/base.py - Enabling the Billing App

最后,您可以进行并应用迁移(这是两个独立的命令):

python manage.py makemigrations
python manage.py migrate

有了应用,我们现在准备勾勒界面,然后是后端代码使其工作。

计费应用线框化

在谈论任何前端库之前,让我们先看看我们要构建什么。图 6-2 显示了计费应用的线框,具体来说,就是创建新发票的界面。

img/505838_1_En_6_Fig2_HTML.jpg

图 6-2

计费应用的线框

在编写任何代码之前,记住 UI 是很重要的;这种方法被称为由外向内开发。通过查看接口,我们可以开始考虑我们需要公开什么 API 端点。从前端到后端应该做哪些 HTTP 调用?首先,我们需要获取一个客户端列表来填充表示“选择一个客户端”的select。这是一个对像/billing/api/clients/这样的端点的GET呼叫。至于其他的,一旦我们编译了发票中的所有字段,几乎所有的动态数据都必须通过POST请求发送。这可能是对/billing/api/invoices/的请求。还有一个发送电子邮件按钮,它应该会触发一封电子邮件。概括来说,我们需要进行以下调用:

  • GET来自/billing/api/clients/的所有或部分用户

  • POST新发票的数据到/billing/api/invoices/

  • 用于向客户发送电子邮件(你将在第十一章中学习)

对于任何熟悉 JavaScript 的开发人员来说,这些交互听起来可能微不足道。然而,它们将有助于理解一个典型的解耦项目的架构。在接下来的部分中,我们将 JavaScript 前端与 DRF API 配对。请记住,我们关注的是所有活动部分之间的交互和架构,而不是努力实现完美的代码。

Note

在这个项目中,我们没有专门的客户端模型。客户只是 Django 的用户。为了方便起见,我们在前端使用术语客户端,而对于 Django,每个人都是User

与 Django REST 框架伪解耦

我们在前一章讨论了伪解耦,作为一种用 JavaScript 增强应用前端或者用单页应用完全取代静态前端的方法。

我们还没有深入讨论认证,但是简单地说,伪解耦方法的一个优点是我们可以使用基于会话的内置 Django 认证。在接下来的几节中,我们将实际使用构建交互式前端的最流行的 JavaScript 库之一——vue . js——来看看它是如何融入 Django 的。由于其高度可配置性,Vue.js 是伪解耦 Django 项目的完美匹配。如果你想知道,我们将在本书的后面讨论 React。

vista . js 和 Django

让我们从 Vue 开始。我们希望从 Django 模板中为我们的单页面应用提供服务。

为此,我们必须建立一个 Vue 项目。首先,安装 Vue CLI:

npm install -g @vue/cli

现在我们需要在某个地方创建 Vue 项目。Vue 高度可配置;在大多数情况下,由你来决定把应用放在哪里。为了保持一致性,我们在billing文件夹中创建 Vue 应用,Django 应用已经在这个文件夹中。进入文件夹并运行 Vue CLI:

cd billing
vue create vue_spa

安装程序将询问我们是要手动选择功能还是使用默认预设。对于我们的项目,我们选择以下配置:

  • 视图 2.x

  • 巴比伦式的城市

  • 没有路由

  • Linter/formatter (ESLint 和更漂亮)

  • 专用配置文件中的配置

按 Enter 键,让安装程序配置项目。当包管理器完成所有的依赖项时,花一点时间来探索 Vue 项目结构。一旦完成,您就可以探索在 Django 中使用单页面应用的所有步骤了。

为了使事情易于管理,我们现在只针对开发环境(我们将在下一章讨论生产)。正如第二章所预期的,Django 可以通过集成服务器提供开发中的静态文件。当我们运行python manage.py runserver时,Django 收集所有的静态资产,只要我们配置STATIC_URL。在第三章中,我们拆分了项目的所有设置,我们将STATIC_URL配置为/static/用于开发。开箱即用,Django 可以从每个应用文件夹中收集静态文件,对于我们的计费应用,这意味着我们需要将静态资产放在billing/static中。

有了一堆简单的 JavaScript 文件,这很容易。您只需将它们放在适当的文件夹中。使用像 Vue CLI 或 create-react-app 这样的 CLI 工具,JavaScript 包和所有其他静态资产的目标文件夹已经由工具决定了。对于 Vue CLI 来说,这个文件夹被命名为dist,它应该位于单页应用的同一个项目文件夹中。这对 Django 来说很糟糕,因为它将无法获取这些静态文件。幸运的是,由于 Vue 的可配置性,我们可以将 JavaScript 构建和模板放在 Django 需要的地方。我们可以通过vue.config.js来决定静态文件和index.html应该在哪里结束。由于 Vue CLI 有一个带热重装的集成开发服务器,我们在开发的这个阶段有两个选项:

  • 我们用npm run serve提供应用

  • 我们通过 Django 的开发服务器提供应用

通过第一个选项,我们可以在http://localhost:8081/运行并访问应用,实时查看变化。有了第二种选择,就方便获得更真实的感觉:例如我们可以使用内置的认证系统。为了使用第二个选项,我们需要配置 Vue CLI。

首先,在 Vue 项目文件夹billing/vue_spa中,创建一个名为。env.staging有以下内容:

VUE_APP_STATIC_URL=/static/billing/

注意,这是 Django 的STATIC_URL和 Django 的应用文件夹billing的组合,我们在decoupled_dj/settings/.env中恰当地配置了 ??。接下来,在同一个 Vue 项目文件夹中创建vue.config.js,内容如清单 6-3 所示。

const path = require("path");

module.exports = {
 publicPath: process.env.VUE_APP_STATIC_URL,
 outputDir: path.resolve(__dirname, "../static", "billing"),
 indexPath: path.resolve(__dirname, "../templates/", "billing", "index.html")
};

Listing 6-3billing/vue_spa/vue.config.js – Vue’s Custom Configuration

在这种配置下,我们告诉 Vue:

  • 使用在.env.staging指定的路径作为publicPath

  • 将静态资产放入billing/static/billing内的outputDir

  • index.htmlindexPath放在billing/templates/billing里面

这种设置尊重 Django 关于在哪里找到静态文件和主模板的期望。publicPath是 Vue 应用的预期部署路径。在开发/登台阶段,我们可以指向/static/billing/,Django 将在那里提供文件。在生产中,我们提供了不同的途径。

Note

Django 在静态文件和模板结构方面是高度可配置的。你可以自由地尝试不同的设置。在整本书中,我们将坚持 Django 的股票结构。

现在,您可以在“staging”模式下构建您的 Vue 项目(您应该从 Vue 项目文件夹中运行此命令):

npm run build -- --mode staging

运行构建后,您应该看到 Vue 文件到达预期的文件夹:

  • 静态资产进入billing/static/billing

  • index.html进去billing/templates/billing

为了进行测试,我们需要在 Django 中连接一个视图和 URL。首先,在billing/views.py中创建一个TemplateView的子类来服务 Vue 的index.html,如清单 6-4 所示。

from django.views.generic import TemplateView

class Index(TemplateView):
      template_name = "billing/index.html"

Listing 6-4billing/views.py - Template View for Serving the App Entry Point

Note

如果你更喜欢函数视图,你可以在函数视图中使用render()快捷键来代替TemplateView

接下来,在billing/urls.py中配置主路由,如清单 6-5 所示。

from django.urls import path
from .views import Index

app_name = "billing"

urlpatterns = [
      path("", Index.as_view(), name="index")
]

Listing 6-5billing/urls.py - URL Configuration

最后,在decoupled_dj/urls.py中包含计费应用的 URL,如清单 6-6 所示。

from django.urls import path, include

urlpatterns = [
   path(
       "billing/",
       include("billing.urls", namespace="billing")
   ),
]

Listing 6-6decoupled_dj/urls.py - Project URL Configuration

现在可以在另一个终端中运行 Django development server 了:

python manage.py runserver

如果你访问http://127.0.0.1:8000/billing/,你应该会看到你的 Vue 应用启动并运行,如图 6-3 所示。

img/505838_1_En_6_Fig3_HTML.jpg

图 6-3

我们的 Vue 应用由 Django 的开发服务器提供服务

你可能想知道为什么我们使用术语筹备,而不是开发来进行这个设置。你从这个配置中得到的,实际上,更像是一个“预准备”环境,你可以在 Django 中测试 Vue 应用。这种配置的缺点是,要看到反映的变化,我们每次都需要重新构建 Vue 应用。当然,没有什么能阻止你运行npm run serve来启动带有集成 webpack 服务器的 Vue 应用。在接下来的部分中,我们将完成账单应用的 UI,最后是 REST 后端。

构建 Vue 应用

现在让我们来构建我们的 Vue 应用。首先,清除vue_spa/src/App.vue中的样板文件,从清单 6-7 中显示的代码开始。

<template>
  <div id="app">
      <InvoiceCreate />
  </div>
</template>

<script>
import InvoiceCreate from "@/components/InvoiceCreate";

export default {
  name: "App",
  components: {
      InvoiceCreate
  }
};
</script>

Listing 6-7Main Vue Component

这里我们包括了InvoiceCreate组件。现在,在名为vue_spa/src/components/InvoiceCreate.vue的新文件中创建这个组件(您也可以删除HelloWorld.vue)。清单 6-8 首先显示模板部分。

<template>
 <div class="container">
   <h2>Create a new invoice</h2>
   <form @submit.prevent="handleSubmit">
     <div class="form">
       <div class="form__aside">
         <div class="form__field">
           <label for="user">Select a client</label>
           <select id="user" name="user" required>
             <option value="--">--</option>
             <option v-for="user in users" :key="user.email" :value="user.id">
               {{ user.name }} - {{ user.email }}
             </option>
           </select>
         </div>
         <div class="form__field">
           <label for="date">Date</label>
           <input id="date" name="date" type="date" required />
         </div>
         <div class="form__field">
           <label for="due_date">Due date</label>
           <input id="due_date" name="due_date" type="date" required />
         </div>
       </div>
       <div class="form__main">
         <div class="form__field">
           <label for="quantity">Qty</label>
           <input
             id="quantity"
             name="quantity"
             type="number"
             min="0"
             max="10"
             required
           />
         </div>
         <div class="form__field">
           <label for="description">Description</label>
           <input id="description" name="description" type="text" required />
         </div>
         <div class="form__field">
           <label for="price">Price</label>
           <input
             id="price"
             name="price"
             type="number"
             min="0"
             step="0.01"
             required
           />
         </div>
         <div class="form__field">
           <label for="taxed">Taxed</label>
           <input id="taxed" name="taxed" type="checkbox" />
         </div>
       </div>
     </div>
     <div class="form__buttons">
       <button type="submit">Create invoice</button>
       <button disabled>Send email</button>
     </div>
   </form>
 </div>
</template>

Listing 6-8Template Section of the Vue Form Component

在这个标记中,我们有:

  • 用于选择客户的select

  • 两个日期输入

  • 数量、描述和价格的输入

  • 已征税的复选框

  • 两个按钮

接下来是逻辑部分,带有典型的表单处理,如清单 6-9 所示。

<script>
export default {
 name: "InvoiceCreate",
 data: function() {
   return {
     users: [
       { id: 1, name: "xadrg", email: "xadrg@acme.io" },
       { id: 2, name: "olcmf", email: "olcmf@zyx.dev" }
     ]
   };
 },
 methods: {
   handleSubmit: function(event) {
     // eslint-disable-next-line no-unused-vars
     const formData = new FormData(event.target);

     // TODO - build the request body
     const data = {};

     fetch("/billing/api/invoices/", {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify(data)
     })
       .then(response => {
         if (!response.ok) throw Error(response.statusText);
         return response.json();
       })
       .then(json => {
         console.log(json);
       })
       .catch(err => console.log(err));
   }
 },
 mounted() {
   fetch("/billing/api/clients/")
     .then(response => {
       if (!response.ok) throw Error(response.statusText);
       return response.json();
     })
     .then(json => {
       this.users = json;
     });
 }
};
</script>

Listing 6-9JavaScript Section of the Vue Form Component

在这段代码中,我们有:

  • Vue 组件状态中的一个users属性

  • 一种处理表单提交的方法

  • 一种在装载时获取数据的mounted生命周期方法

此外,我们以 API 端点(尚未实现)为目标:/billing/api/clients//billing/api/invoices/。你可以在users里注意到一些假数据;这是为了在我们等待构建 REST API 时,我们有一个最小的可用接口。

Tip

可以不用后端开发前端,用 Mirage JS 之类的工具,可以拦截和响应 HTTP 调用。

为了让代码工作,记得在vue_spa/src/components/InvoiceCreate.vue中把模板和脚本部分按顺序放好。有了这个最小的实现,您现在就可以用两种方式开始这个项目了。要构建应用并使用 Django 提供它,请在 Vue 项目文件夹中运行以下命令:

npm run build -- --mode staging

然后,启动 Django 开发服务器,前往http://localhost:8000/billing/。要使用其开发服务器运行 Vue,请在 Vue 文件夹中运行以下命令:

npm run serve

该应用将在http://localhost:8081/开始,但由于我们还没有后端,没有什么将为最终用户工作。同时,我们可以设置应用,以便:

  • 当在 Django 的保护伞下启动时,它调用/billing/api/clients//billing/api/invoices/

  • 当使用集成的 webpack 服务器调用时,它调用http://localhost:8000/billing/api/clients/http://localhost:8000/billing/api/invoices/,这是 DRF 将监听的端点

为此,打开vue.config.js并在配置中添加清单 6-10 中的行。

// omitted
module.exports = {
  // omitted
  devServer: {
      proxy: "http://localhost:8000"
  }
};

Listing 6-10Development Server Configuration for Vue CLI

这确保了该项目在使用伪解耦设置的试运行/生产中以及作为独立应用的开发中运行良好。很快,我们将最终构建 REST 后端。

vue . js、Django 和 CSS

在这一点上,你可能想知道 CSS 在大局中的位置。我们的 Vue 组件确实有一些类,但是在前面的部分中我们没有显示任何 CSS 管道。

原因是在我们正在构建的项目中,至少有两种使用 CSS 的方法。具体来说,您可以:

  • 在基本 Django 模板中包含 CSS

  • 包括来自每个单页应用的 CSS

在撰写本文时,Tailwind 是 Django 场景中最流行的 CSS 库之一。在伪解耦设置中,您可以在主 Django 项目中配置 Tailwind,在基本模板中包含 CSS 包,并让单页 Vue 应用扩展基本模板。如果每个单页应用都是独立的,每个都有自己的风格,你可以单独配置 Tailwind 和 friends。请注意,从长远来看,第二种方法的可维护性可能有点困难。

Note

你可以在本章的源代码 https://github.com/valentinogagliardi/decoupled-dj/tree/chapter_06_decoupled_with_drf 中找到组件的最小 CSS 实现。

构建 REST 后端

我们在 Vue 组件中留了一个说明,写着// TODO - build the request body。这是因为使用我们构建的表单,我们不能将请求原样发送到 Django REST 框架。你马上就会明白原因。同时,有了 UI,我们可以将后端与 DRF 连接起来。基于我们从 UI 调用的端点,我们需要公开以下来源:

  • /billing/api/clients/

  • /billing/api/invoices/

让我们回顾一下所有实体之间的关系:

  • 每个Invoice可以有一个或多个ItemLine

  • 一个ItemLine恰好属于一个Invoice

  • 一个User可以有多个Invoice

  • 每个Invoice属于一个User

这是什么意思?当过帐到后端以创建新发票时,Django 希望:

  • 与发票相关联的用户 ID

  • 与发票关联的一个或多个物料行

用户不是问题,因为我们从对/billing/api/clients/的第一次 API 调用中就抓住了它。每个项目和相关的发票不能作为一个整体从前端发送。我们需要:

  • 在前端构建正确的对象

  • 调整 DRF 中的 ORM 逻辑以保存相关对象

构建序列化程序

首先,我们需要在 DRF 中创建以下组件:

  • 用于User的串行器

  • 用于Invoice的串行器

  • 用于ItemLine的串行器

首先,让我们安装 Django REST 框架:

pip install djangorestframework

安装完成后,更新requirements/base.txt以包含 DRF:

Django==3.1.3
django-environ==0.4.5
psycopg2-binary==2.8.6
uvicorn==0.12.2
djangorestframework==3.12.2

接下来,启用decoupled_dj/settings/base.py中的 DRF,如清单 6-11 所示。

INSTALLED_APPS = [
   ...
   "users.apps.UsersConfig",
   "billing.apps.BillingConfig",
   "rest_framework", # enables DRF
]

Listing 6-11decoupled_dj/settings/base.py - Django Installed Apps with the DRF Enabled

现在在billing中创建一个名为api的新 Python 包,这样我们就有了一个billing/api文件夹。在这个包中,我们放置了 REST API 的所有逻辑。现在让我们构建序列化程序。创建一个名为billing/api/serializers.py的新文件,内容如清单 6-12 所示。

from users.models import User
from billing.models import Invoice, ItemLine
from rest_framework import serializers

class UserSerializer(serializers.ModelSerializer):
   class Meta:
       model = User
       fields = ["id", "name", "email"]

class ItemLineSerializer(serializers.ModelSerializer):
   class Meta:
       model = ItemLine
       fields = ["quantity", "description", "price", "taxed"]

class InvoiceSerializer(serializers.ModelSerializer):
   items = ItemLineSerializer(many=True, read_only=True)

   class Meta:
       model = Invoice
       fields = ["user", "date", "due_date", "items"]

Listing 6-12billing/api/serializers.py – The DRF Serializers

这里我们有三个串行化器。UserSerializer会连载我们的User型号。ItemLineSerializerItemLine的序列化器。最后,InvoiceSerializer将连载我们的Invoice车型。每个串行化器都子类化 DRF 的ModelSerializer,我们在第三章中遇到过,并且有适当的字段映射到相应的模型。列表中的最后一个序列化器InvoiceSerializer很有趣,因为它包含一个嵌套的ItemLineSerializer。正是这个序列化程序需要一些工作来符合我们的前端。要了解原因,让我们构建视图。

构建视图和 URL

创建一个名为billing/api/views .py的新文件,代码如清单 6-13 所示。

from .serializers import InvoiceSerializer, UserSerializer, User
from rest_framework.generics import CreateAPIView, ListAPIView

class ClientList(ListAPIView):
   serializer_class = UserSerializer
   queryset = User.objects.all()

class InvoiceCreate(CreateAPIView):
   serializer_class = InvoiceSerializer

Listing 6-13billing/api/views.py - DRF Views

这些视图将分别响应/billing/api/clients//billing/api/invoices/。在这里,ClientList是通用 DRF 列表视图的子类。InvoiceCreate改为子类化 DRF 的通用创建视图。我们现在准备好为我们的应用连接 URL。打开billing/urls.py并定义您的路线,如清单 6-14 所示。

from django.urls import path
from .views import Index
from .api.views import ClientList, InvoiceCreate

app_name = "billing"

urlpatterns = [
   path("", Index.as_view(), name="index"),
   path(
       "api/clients/",
       ClientList.as_view(),
       name="client-list"),
   path(
       "api/invoices/",
       InvoiceCreate.as_view(),
       name="invoice-create"),
]

Listing 6-14billing/urls.py - URL Patterns for the Billing API

这里,app_name与主项目 URL 中的名称空间配对将允许我们用reverse()调用billing:client-listbilling:invoice-create,这在测试中特别有用。最后一步,您应该在decoupled_dj/urls.py中配置好 URL,如清单 6-15 所示。

from django.urls import path, include

urlpatterns = [
   path(
       "billing/",
       include("billing.urls", namespace="billing")
   ),
]

Listing 6-15decoupled_dj/urls.py - The Main Project URL Configuration

我们已经做好了测试的准备。为了在数据库中创建几个模型,您可以启动一个增强的 shell(这来自于django-extensions):

python manage.py shell_plus

要创建模型,运行以下查询(>>>是 shell 提示符):

>>> User.objects.create_user(username="jul81", name="Juliana", email="juliana@acme.io")
>>> User.objects.create_user(username="john89", name="John", email="john@zyx.dev")

退出 shell 并启动 Django:

python manage.py runserver

在另一个终端中,运行下面的curl命令,看看会发生什么:

curl -X POST --location "http://127.0.0.1:8000/billing/api/invoices/" \
   -H "Accept: */*" \
   -H "Content-Type: application/json" \
   -d "{
         \"user\": 1,
         \"date\": \"2020-12-01\",
         \"due_date\": \"2020-12-30\"
       }"

作为响应,您应该会看到以下输出:

{"user":1,"date":"2020-12-01","due_date":"2020-12-30"}

这是 Django REST 框架告诉我们它在数据库中创建了一个新的发票。目前为止一切顺利。现在在发票上加一些项目怎么样?为此,我们需要使序列化程序可写。在billing/api/serializers.py中,将read_only=Trueitems字段中移除,使其看起来像列表 6-16 。

class InvoiceSerializer(serializers.ModelSerializer):
   items = ItemLineSerializer(many=True)

   class Meta:
       model = Invoice
       fields = ["user", "date", "due_date", "items"]

Listing 6-16billing/api/serializers.py - The Serializer for an Invoice, Now with a Writable Relationship

您可以使用 curl 再次进行测试,这一次也传递两个项目:

curl -X POST --location "http://127.0.0.1:8000/billing/api/invoices/" \
   -H "Accept: application/json" \
   -H "Content-Type: application/json" \
   -d "{
         \"user\": 1,
         \"date\": \"2020-12-01\",
         \"due_date\": \"2020-12-30\",
         \"items\": [
           {
             \"quantity\": 2,
             \"description\": \"JS consulting\",
             \"price\": 9800.00,
             \"taxed\": false
           },
           {
             \"quantity\": 1,
             \"description\": \"Backend consulting\",
             \"price\": 12000.00,
             \"taxed\": true
           }
         ]
       }"

此时,所有东西都应该爆炸,您应该会看到以下异常:

TypeError: Invoice() got an unexpected keyword argument 'items'
Exception Value: Got a TypeError when calling Invoice.objects.create().

这可能是因为您在 serializer 类上有一个可写字段,它不是Invoice.objects.create()的有效参数。您可能需要将该字段设为只读,或者覆盖InvoiceSerializer.create()方法来正确处理这个问题。

Django REST 要求我们调整InvoiceSerializer中的create(),以便它可以接受发票旁边的项目。

使用嵌套序列化程序

打开billing/api/serializers.py并修改串行器,如清单 6-17 所示。

class InvoiceSerializer(serializers.ModelSerializer):
   items = ItemLineSerializer(many=True)

   class Meta:
       model = Invoice
       fields = ["user", "date", "due_date", "items"]

   def create(self, validated_data):
       items = validated_data.pop("items")
       invoice = Invoice.objects.create(**validated_data)
       for item in items:
           ItemLine.objects.create(invoice=invoice, **item)
       return invoice

Listing 6-17billing/api/serializers.py - The Serializer for an Invoice, Now with a Customized create()

这也是调整ItemLine模型的好时机。正如您从序列化程序中看到的,我们正在使用items字段来设置给定发票上的相关项目。问题是,Invoice模型中没有这样的字段。这是因为 Django 模型上的反向关系可以作为modelname _set访问,除非有不同的配置。要修复该字段,打开billing/models.py并将related_name属性添加到发票行,如清单 6-18 所示。

class ItemLine(models.Model):
   invoice = models.ForeignKey(
     to=Invoice, on_delete=models.PROTECT, related_name="items"
   )
   ...

Listing 6-18billing/models.py - The ItemLine Model with a related_name

保存文件后,按如下方式运行迁移:

python manage.py makemigrations billing
python manage.py migrate

启动 Django 后,您现在应该能够重复相同的curl请求,这次成功了。在这个阶段,我们也可以修复前端。

固定 Vue 前端

vue_spa/src/components/InvoiceCreate.vue中,找到显示// TODO - build the request body的行,并调整代码,如清单 6-19 所示。

 methods: {
   handleSubmit: function(event) {
     const formData = new FormData(event.target);

     const data = Object.fromEntries(formData);
     data.items = [
       {
         quantity: formData.get("quantity"),
         description: formData.get("description"),
         price: formData.get("price"),
         taxed: Boolean(formData.get("taxed"))
       }
     ];

     fetch("/billing/api/invoices/", {
       method: "POST",
       headers: { "Content-Type": "application/json" },
       body: JSON.stringify(data)
     })
       // omitted;
   }
 }

Listing 6-19The handleSubmit Method from the Vue Component

为了简洁起见,我只展示了相关的部分。这里,我们使用Object.fromEntries() (ECMAScript 2019)从我们的表单构建一个对象。然后,我们继续向对象添加一个项目数组(目前只有一个项目)。我们最后发送对象作为fetch的主体负载。您可以使用集成服务器运行 Vue(从 Vue 项目文件夹中):

npm run serve

您应该看到一个在http://localhost:8080/创建发票的表单。尝试填写表格并点击创建发票。在浏览器控制台中,您应该看到来自 Django REST 框架的响应,发票被成功保存到数据库中。干得好!我们完成了这个解耦的 Django 项目的第一个真正的特性。

Note

这是提交到目前为止所做的更改并将工作推送到 Git repo 的好时机。你可以在 https://github.com/valentinogagliardi/decoupled-dj/tree/chapter_06_decoupled_with_drf 找到本章的源代码。

Exercise 6-1: Handling Multiple Items

扩展 Vue 组件来处理发票的多个项目。用户应该能够单击加号(+)按钮向表单添加更多项目,这些项目应该与请求一起发送。

摘要

本章将 Vue.js 前端与 Django REST 框架 API 配对,Vue.js 在与主 Django 项目相同的上下文中提供服务。

通过这样做,您学会了如何:

  • 将 Vue.js 集成到 Django 中

  • 通过 JavaScript 与 DRF API 进行交互

  • 在 Django REST 框架中使用嵌套的序列化程序

在下一章中,我们将探讨一个更真实的场景。在再次转向 JavaScript 领域之前,我们将在第八章的 Next.js 中讨论安全性和部署。

额外资源