TestDriven.io 博客中文翻译(七)

原文:TestDriven.io Blog

协议:CC BY-NC-SA 4.0

用 Flask 和 Vue.js 开发单页应用

原文:https://testdriven.io/blog/developing-a-single-page-app-with-flask-and-vuejs/

以下是如何使用 Vue 和 Flask 设置基本 CRUD 应用程序的分步演练。我们首先用 Vue CLI 搭建一个新的 Vue 应用程序,然后通过 Python 和 Flask 支持的后端 RESTful API 执行基本的 CRUD 操作。

最终应用:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

主要依赖:

  • 视图 v2.6.11
  • CLI 视图 v4.5.11
  • 节点 v15.7.0
  • 国家预防机制 7.4.3 版
  • 烧瓶 v1.1.2
  • python 3 . 9 . 1 版

目标

本教程结束时,您将能够:

  1. 解释什么是烧瓶
  2. 解释什么是 Vue,以及它与其他 UI 库和前端框架(如 React 和 Angular)相比如何
  3. 使用 Vue CLI 搭建 Vue 项目
  4. 在浏览器中创建和渲染 Vue 组件
  5. 使用 Vue 组件创建单页面应用程序(SPA)
  6. 将 Vue 应用程序连接到 Flask 后端
  7. 用 Flask 开发 RESTful API
  8. 使用引导程序设计 Vue 组件
  9. 使用 Vue 路由器创建路线并渲染组件

烧瓶和 Vue

让我们快速看一下每个框架。

烧瓶是什么?

Flask 是一个简单而强大的 Python 微型 web 框架,非常适合构建 RESTful APIs。像 Sinatra (Ruby)和 Express (Node)一样,它是最小和灵活的,所以你可以从小处着手,根据需要构建一个更复杂的应用。

第一次用烧瓶?查看以下两个资源:

  1. flask TDD
  2. 用 Python 和 Flask 开发 Web 应用

Vue 是什么?

Vue 是一个开源的 JavaScript 框架,用于构建用户界面。它采用了 React 和 Angular 的一些最佳实践。也就是说,与 React 和 Angular 相比,它要平易近人得多,因此初学者可以快速上手并运行。它也同样强大,因此它提供了创建现代前端应用程序所需的所有功能。

有关 Vue 的更多信息,以及使用它与 React 和 Angular 相比的优缺点,请查看参考资料:

  1. Vue:与其他框架的比较
  2. 通过构建和部署 CRUD 应用程序学习 Vue
  3. 反应 vs 角度 vs Vue.js

第一次和 Vue 在一起?花点时间通读官方 Vue 指南中的介绍

烧瓶设置

首先创建一个新的项目目录:

`$ mkdir flask-vue-crud
$ cd flask-vue-crud` 

在“flask-vue-crud”中,创建一个名为“server”的新目录。然后,在“服务器”目录中创建并激活虚拟环境:

`$ python3.9 -m venv env
$ source env/bin/activate
(env)$` 

根据您的环境,上述命令可能会有所不同。

将烧瓶连同烧瓶-CORS 延长件一起安装;

`(env)$ pip install Flask==1.1.2 Flask-Cors==3.0.10` 

向新创建的“服务器”目录添加一个 app.py 文件:

`from flask import Flask, jsonify
from flask_cors import CORS

# configuration
DEBUG = True

# instantiate the app
app = Flask(__name__)
app.config.from_object(__name__)

# enable CORS
CORS(app, resources={r'/*': {'origins': '*'}})

# sanity check route
@app.route('/ping', methods=['GET'])
def ping_pong():
    return jsonify('pong!')

if __name__ == '__main__':
    app.run()` 

我们为什么需要弗拉斯克-CORS?为了进行跨来源请求——例如,来自不同协议、IP 地址、域名或端口的请求——您需要启用跨来源资源共享 (CORS)。弗拉斯克-CORS 公司为我们处理此事。

值得注意的是,上面的设置允许来自任何域、协议或端口的所有路由上的跨来源请求。在生产环境中,您应该允许来自托管前端应用程序的域的跨来源请求。参考烧瓶-CORS 文档了解更多信息。

运行应用程序:

要进行测试,将您的浏览器指向http://localhost:5000/ping。您应该看到:

回到终端,按 Ctrl+C 终止服务器,然后导航回项目根目录。现在,让我们把注意力转向前端,设置 Vue。

视图设置

我们将使用强大的 Vue CLI 来生成定制的项目样板文件。

全局安装:

第一次用 npm?查看关于 npm 的官方指南。

然后,在“flask-vue-crud”中,运行以下命令来初始化一个名为client的新 vue 项目:

这需要你回答几个关于这个项目的问题。

`Vue CLI v4.5.11
? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
  Manually select features` 

使用向下箭头键选中“手动选择功能”,然后按 enter 键。接下来,您需要选择想要安装的功能。对于本教程,选择“选择 Vue 版本”、“Babel”、“Router”和“Linter / Formatter”,如下所示:

`Vue CLI v4.5.11
? Please pick a preset: Manually select features
? Check the features needed for your project:
❯◉ Choose Vue version
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing` 

按回车键。

为 Vue 版本选择“2.x”。对路由器使用历史模式。为 linter 选择“ESLint + Airbnb 配置”和“保存时 Lint”。最后,选择“In package.json”选项,以便将配置放在 package.json 文件中,而不是单独的配置文件中。

您应该会看到类似如下的内容:

`Vue CLI v4.5.11
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Router, Linter
? Choose a version of Vue.js that you want to start the project with 2.x
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a linter / formatter config: Airbnb
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In package.json
? Save this as a preset for future projects? (y/N) No` 

再次按 enter 配置项目结构并安装依赖项。

快速查看一下生成的项目结构。这看起来似乎很多,但是我们将处理“src”文件夹中的文件和文件夹,以及在“public”文件夹中找到的index.html文件。

index.html 文件是我们的 Vue 应用程序的起点。

`<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>` 

注意带有appid<div>元素。这是一个占位符,Vue 将使用它来附加生成的 HTML 和 CSS 以生成 UI。

请注意“src”文件夹中的文件夹:

`client/src
├── App.vue
├── assets
│   └── logo.png
├── components
│   └── HelloWorld.vue
├── main.js
├── router
│   └── index.js
└── views
    ├── About.vue
    └── Home.vue` 

情绪完全失控

名字目的
main.jsapp 入口点,它加载并初始化 Vue 以及根组件
app . view根组件,这是渲染所有其他组件的起点
“组件”存储 UI 组件的位置
路由器/index.js其中定义了 URL 并将其映射到组件
“观点”其中存储了与路由器相关联的 UI 组件
“资产”存储静态资产(如图像和字体)的地方

查看client/src/components/hello world . vue文件。这是一个单文件组件,它被分成三个不同的部分:

  1. 模板:针对特定于组件的 HTML
  2. 脚本:组件逻辑通过 JavaScript 实现
  3. 样式:针对 CSS 样式

启动开发服务器:

`$ cd client
$ npm run serve` 

在您选择的浏览器中导航到 http://localhost:8080 。您应该看到以下内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为了简单起见,删除“client/src/views”文件夹。然后,在“client/src/components”文件夹中添加一个名为 Ping.vue 的新组件:

`<template>
  <div>
    <p>{{ msg }}</p>
  </div>
</template>

<script> export  default  { name:  'Ping', data()  { return  { msg:  'Hello!', }; }, }; </script>` 

更新client/src/router/index . js,将“/ping”映射到Ping组件,如下所示:

`import  Vue  from  'vue'; import  Router  from  'vue-router'; import  Ping  from  '../components/Ping.vue'; Vue.use(Router); export  default  new  Router({ mode:  'history', base:  process.env.BASE_URL, routes:  [ { path:  '/ping', name:  'Ping', component:  Ping, }, ], });` 

最后,在 client/src/App.vue 中,移除导航和样式:

`<template>
  <div id="app">
    <router-view/>
  </div>
</template>` 

你现在应该在浏览器的中看到Hello!http://localhost:8080/ping

为了连接客户端 Vue 应用程序和后端 Flask 应用程序,我们可以使用 axios 库来发送 AJAX 请求。

从安装开始:

Ping.vue 中更新组件的script部分,如下所示:

`<script> import  axios  from  'axios'; export  default  { name:  'Ping', data()  { return  { msg:  '', }; }, methods:  { getMessage()  { const  path  =  'http://localhost:5000/ping'; axios.get(path) .then((res)  =>  { this.msg  =  res.data; }) .catch((error)  =>  { // eslint-disable-next-line console.error(error); }); }, }, created()  { this.getMessage(); }, }; </script>` 

在新的终端窗口启动 Flask 应用程序。您应该会在浏览器中看到pong!。本质上,当一个响应从后端返回时,我们将响应对象中的msg设置为data的值。

自举设置

接下来,让我们将 Bootstrap(一个流行的 CSS 框架)添加到应用程序中,这样我们就可以快速添加一些样式。

安装:

忽略jquerypopper.js的警告。不要将这两者添加到项目中。稍后将详细介绍。

将引导样式导入到 client/src/main.js :

`import  Vue  from  'vue'; import  App  from  './App.vue'; import  router  from  './router'; import  'bootstrap/dist/css/bootstrap.css'; Vue.config.productionTip  =  false; new  Vue({ router, render:  (h)  =>  h(App), }).$mount('#app');` 

更新 client/src/App.vue 中的style部分:

`<style> #app  { margin-top:  60px } </style>` 

使用Ping组件中的按钮容器确保自举正确连接;

`<template>
  <div class="container">
    <button type="button" class="btn btn-primary">{{ msg }}</button>
  </div>
</template>` 

运行开发服务器:

您应该看到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

接下来,在名为 Books.vue 的新文件中添加一个名为Books的新组件:

`<template>
  <div class="container">
    <p>books</p>
  </div>
</template>` 

更新路由器:

`import  Vue  from  'vue'; import  Router  from  'vue-router'; import  Books  from  '../components/Books.vue'; import  Ping  from  '../components/Ping.vue'; Vue.use(Router); export  default  new  Router({ mode:  'history', base:  process.env.BASE_URL, routes:  [ { path:  '/', name:  'Books', component:  Books, }, { path:  '/ping', name:  'Ping', component:  Ping, }, ], });` 

测试:

  1. http://localhost:8080
  2. http://localhost:8080/ping

最后,让我们向Books组件添加一个快速的、引导样式的表格:

`<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-10">
        <h1>Books</h1>
        <hr><br><br>
        <button type="button" class="btn btn-success btn-sm">Add Book</button>
        <br><br>
        <table class="table table-hover">
          <thead>
            <tr>
              <th scope="col">Title</th>
              <th scope="col">Author</th>
              <th scope="col">Read?</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>foo</td>
              <td>bar</td>
              <td>foobar</td>
              <td>
                <div class="btn-group" role="group">
                  <button type="button" class="btn btn-warning btn-sm">Update</button>
                  <button type="button" class="btn btn-danger btn-sm">Delete</button>
                </div>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</template>` 

您现在应该看到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

现在我们可以开始构建 CRUD 应用程序的功能了。

我们在建造什么?

我们的目标是为一个单一的资源书籍设计一个后端 RESTful API,由 Python 和 Flask 提供支持。API 本身应该遵循 RESTful 设计原则,使用基本的 HTTP 动词:GET、POST、PUT 和 DELETE。

我们还将设置一个使用 Vue 的前端应用程序,它使用后端 API:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

本教程只讨论快乐之路。处理错误是一个单独的练习。检查您的理解,并在前端和后端添加适当的错误处理。

获取路线

计算机网络服务器

将图书列表添加到 server/app.py :

`BOOKS = [
    {
        'title': 'On the Road',
        'author': 'Jack Kerouac',
        'read': True
    },
    {
        'title': 'Harry Potter and the Philosopher\'s Stone',
        'author': 'J. K. Rowling',
        'read': False
    },
    {
        'title': 'Green Eggs and Ham',
        'author': 'Dr. Seuss',
        'read': True
    }
]` 

添加路由处理程序:

`@app.route('/books', methods=['GET'])
def all_books():
    return jsonify({
        'status': 'success',
        'books': BOOKS
    })` 

运行 Flask 应用程序,如果它还没有运行的话,然后在http://localhost:5000/books上手动测试路线。

寻找额外的挑战?为此编写一个自动化测试。查看这个资源,了解更多关于测试 Flask 应用的信息。

客户

更新组件:

`<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-10">
        <h1>Books</h1>
        <hr><br><br>
        <button type="button" class="btn btn-success btn-sm">Add Book</button>
        <br><br>
        <table class="table table-hover">
          <thead>
            <tr>
              <th scope="col">Title</th>
              <th scope="col">Author</th>
              <th scope="col">Read?</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(book, index) in books" :key="index">
              <td>{{ book.title }}</td>
              <td>{{ book.author }}</td>
              <td>
                <span v-if="book.read">Yes</span>
                <span v-else>No</span>
              </td>
              <td>
                <div class="btn-group" role="group">
                  <button type="button" class="btn btn-warning btn-sm">Update</button>
                  <button type="button" class="btn btn-danger btn-sm">Delete</button>
                </div>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</template>

<script> import  axios  from  'axios'; export  default  { data()  { return  { books:  [], }; }, methods:  { getBooks()  { const  path  =  'http://localhost:5000/books'; axios.get(path) .then((res)  =>  { this.books  =  res.data.books; }) .catch((error)  =>  { // eslint-disable-next-line console.error(error); }); }, }, created()  { this.getBooks(); }, }; </script>` 

组件初始化后,通过创建的生命周期钩子调用getBooks()方法,从我们刚刚设置的后端端点获取书籍。

查看实例生命周期挂钩以获得更多关于组件生命周期和可用方法的信息。

在模板中,我们通过 v-for 指令遍历图书列表,在每次迭代中创建一个新的表行。索引值被用作。最后, v-if 用于渲染YesNo,表示用户是否读过这本书。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

引导程序视图

在下一节中,我们将使用一个模态来添加一本新书。我们将为此添加 Bootstrap Vue 库,它提供了一组 Vue 组件,使用基于 Bootstrap 的 HTML 和 CSS。

为什么选择 Bootstrap Vue?Bootstrap 的模态组件使用 jQuery ,你应该避免在同一个项目中与 Vue 一起使用,因为 Vue 使用虚拟 Dom 来更新 Dom。换句话说,如果您确实使用 jQuery 来操作 DOM,Vue 不会知道。至少,如果你绝对需要使用 jQuery,不要在同一个 DOM 元素上同时使用 Vue 和 jQuery。

安装:

启用 client/src/main.js 中的 Bootstrap Vue 库:

`import  BootstrapVue  from  'bootstrap-vue'; import  Vue  from  'vue'; import  App  from  './App.vue'; import  router  from  './router'; import  'bootstrap/dist/css/bootstrap.css'; Vue.use(BootstrapVue); Vue.config.productionTip  =  false; new  Vue({ router, render:  (h)  =>  h(App), }).$mount('#app');` 

邮寄路线

计算机网络服务器

更新现有的路由处理程序,以处理添加新书的 POST 请求:

`@app.route('/books', methods=['GET', 'POST'])
def all_books():
    response_object = {'status': 'success'}
    if request.method == 'POST':
        post_data = request.get_json()
        BOOKS.append({
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book added!'
    else:
        response_object['books'] = BOOKS
    return jsonify(response_object)` 

更新导入:

`from flask import Flask, jsonify, request` 

当 Flask 服务器运行时,您可以在新的终端选项卡中测试 POST 路由:

`$ curl -X POST http://localhost:5000/books -d \
  '{"title": "1Q84", "author": "Haruki Murakami", "read": "true"}' \
  -H 'Content-Type: application/json'` 

您应该看到:

`{
  "message": "Book added!",
  "status": "success"
}` 

您还应该在来自http://localhost:5000/books端点的响应中看到这本新书。

标题已经存在怎么办?或者一个书名有多个作者怎么办?通过处理这些案例来检查你的理解。此外,当titleauthor和/或read丢失时,如何处理无效的有效载荷?

客户

在客户端,现在让我们添加一个用于向Books组件添加新书的模型,从 HTML:

`<b-modal ref="addBookModal"
         id="book-modal"
         title="Add a new book"
         hide-footer>
  <b-form @submit="onSubmit" @reset="onReset" class="w-100">
  <b-form-group id="form-title-group"
                label="Title:"
                label-for="form-title-input">
      <b-form-input id="form-title-input"
                    type="text"
                    v-model="addBookForm.title"
                    required
                    placeholder="Enter title">
      </b-form-input>
    </b-form-group>
    <b-form-group id="form-author-group"
                  label="Author:"
                  label-for="form-author-input">
        <b-form-input id="form-author-input"
                      type="text"
                      v-model="addBookForm.author"
                      required
                      placeholder="Enter author">
        </b-form-input>
      </b-form-group>
    <b-form-group id="form-read-group">
      <b-form-checkbox-group v-model="addBookForm.read" id="form-checks">
        <b-form-checkbox value="true">Read?</b-form-checkbox>
      </b-form-checkbox-group>
    </b-form-group>
    <b-button type="submit" variant="primary">Submit</b-button>
    <b-button type="reset" variant="danger">Reset</b-button>
  </b-form>
</b-modal>` 

在结束的div标签前添加这个。快速浏览一下代码。v-model是用于输入值绑定回状态的指令。您将很快看到这一点。

hide-footer是做什么的?在 Bootstrap Vue docs 中自己回顾一下。

更新script部分:

`<script> import  axios  from  'axios'; export  default  { data()  { return  { books:  [], addBookForm:  { title:  '', author:  '', read:  [], }, }; }, methods:  { getBooks()  { const  path  =  'http://localhost:5000/books'; axios.get(path) .then((res)  =>  { this.books  =  res.data.books; }) .catch((error)  =>  { // eslint-disable-next-line console.error(error); }); }, addBook(payload)  { const  path  =  'http://localhost:5000/books'; axios.post(path,  payload) .then(()  =>  { this.getBooks(); }) .catch((error)  =>  { // eslint-disable-next-line console.log(error); this.getBooks(); }); }, initForm()  { this.addBookForm.title  =  ''; this.addBookForm.author  =  ''; this.addBookForm.read  =  []; }, onSubmit(evt)  { evt.preventDefault(); this.$refs.addBookModal.hide(); let  read  =  false; if  (this.addBookForm.read[0])  read  =  true; const  payload  =  { title:  this.addBookForm.title, author:  this.addBookForm.author, read,  // property shorthand }; this.addBook(payload); this.initForm(); }, onReset(evt)  { evt.preventDefault(); this.$refs.addBookModal.hide(); this.initForm(); }, }, created()  { this.getBooks(); }, }; </script>` 

这里发生了什么事?

  1. addBookForm是否通过v-model绑定到表单输入。换句话说,当一个被更新时,另一个也会被更新。这叫做双向绑定。请花点时间阅读一下这里。想想这件事的后果。你认为这使国家管理更容易还是更难?React 和 Angular 如何处理这个问题?在我看来,双向绑定(以及可变性)使得 Vue 比 React 更容易接近。
  2. 当用户成功提交表单时,触发onSubmit。在提交时,我们阻止正常的浏览器行为(evt.preventDefault()),关闭模态(this.$refs.addBookModal.hide()),触发addBook方法,并清除表单(initForm())。
  3. addBook/books发送 POST 请求以添加新书。

根据需要参考 Vue 文档,自行检查其余的变更。

您能想到客户端或服务器上的任何潜在错误吗?自己处理这些以改善用户体验。

最后,更新模板中的“Add Book”按钮,以便在单击按钮时显示模式:

`<button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>` 

该组件现在应该如下所示:

`<template>
  <div class="container">
    <div class="row">
      <div class="col-sm-10">
        <h1>Books</h1>
        <hr><br><br>
        <button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>
        <br><br>
        <table class="table table-hover">
          <thead>
            <tr>
              <th scope="col">Title</th>
              <th scope="col">Author</th>
              <th scope="col">Read?</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(book, index) in books" :key="index">
              <td>{{ book.title }}</td>
              <td>{{ book.author }}</td>
              <td>
                <span v-if="book.read">Yes</span>
                <span v-else>No</span>
              </td>
              <td>
                <div class="btn-group" role="group">
                  <button type="button" class="btn btn-warning btn-sm">Update</button>
                  <button type="button" class="btn btn-danger btn-sm">Delete</button>
                </div>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
    <b-modal ref="addBookModal"
            id="book-modal"
            title="Add a new book"
            hide-footer>
      <b-form @submit="onSubmit" @reset="onReset" class="w-100">
      <b-form-group id="form-title-group"
                    label="Title:"
                    label-for="form-title-input">
          <b-form-input id="form-title-input"
                        type="text"
                        v-model="addBookForm.title"
                        required
                        placeholder="Enter title">
          </b-form-input>
        </b-form-group>
        <b-form-group id="form-author-group"
                      label="Author:"
                      label-for="form-author-input">
            <b-form-input id="form-author-input"
                          type="text"
                          v-model="addBookForm.author"
                          required
                          placeholder="Enter author">
            </b-form-input>
          </b-form-group>
        <b-form-group id="form-read-group">
          <b-form-checkbox-group v-model="addBookForm.read" id="form-checks">
            <b-form-checkbox value="true">Read?</b-form-checkbox>
          </b-form-checkbox-group>
        </b-form-group>
        <b-button-group>
          <b-button type="submit" variant="primary">Submit</b-button>
          <b-button type="reset" variant="danger">Reset</b-button>
        </b-button-group>
      </b-form>
    </b-modal>
  </div>
</template>

<script> import  axios  from  'axios'; export  default  { data()  { return  { books:  [], addBookForm:  { title:  '', author:  '', read:  [], }, }; }, methods:  { getBooks()  { const  path  =  'http://localhost:5000/books'; axios.get(path) .then((res)  =>  { this.books  =  res.data.books; }) .catch((error)  =>  { // eslint-disable-next-line console.error(error); }); }, addBook(payload)  { const  path  =  'http://localhost:5000/books'; axios.post(path,  payload) .then(()  =>  { this.getBooks(); }) .catch((error)  =>  { // eslint-disable-next-line console.log(error); this.getBooks(); }); }, initForm()  { this.addBookForm.title  =  ''; this.addBookForm.author  =  ''; this.addBookForm.read  =  []; }, onSubmit(evt)  { evt.preventDefault(); this.$refs.addBookModal.hide(); let  read  =  false; if  (this.addBookForm.read[0])  read  =  true; const  payload  =  { title:  this.addBookForm.title, author:  this.addBookForm.author, read,  // property shorthand }; this.addBook(payload); this.initForm(); }, onReset(evt)  { evt.preventDefault(); this.$refs.addBookModal.hide(); this.initForm(); }, }, created()  { this.getBooks(); }, }; </script>` 

测试一下!尝试添加一本书:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

警报组件

接下来,让我们添加一个 Alert 组件,在添加新书后向最终用户显示一条消息。我们将为此创建一个新组件,因为您可能会在许多组件中使用该功能。

将名为 Alert.vue 的新文件添加到“客户端/src/组件”中:

`<template>
  <p>It works!</p>
</template>` 

然后,将其导入到Books组件的script部分,并注册该组件:

`<script> import  axios  from  'axios'; import  Alert  from  './Alert.vue'; ... export  default  { data()  { return  { books:  [], addBookForm:  { title:  '', author:  '', read:  [], }, }; }, components:  { alert:  Alert, }, ... }; </script>` 

现在,我们可以在template部分引用新组件:

`<template>
  <b-container>
    <b-row>
      <b-col col sm="10">
        <h1>Books</h1>
        <hr><br><br>
        <alert></alert>
        <button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>

        ...

      </b-col>
    </b-row>
  </b-container>
</template>` 

刷新浏览器。您现在应该看到:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

查看官方 Vue 文档中的 Composing with Components 以获得更多关于在其他组件中使用组件的信息。

接下来,让我们添加实际的 b-alert 组件client/src/components/alert . vue:

`<template>
  <div>
    <b-alert variant="success" show>{{ message }}</b-alert>
    <br>
  </div>
</template>

<script> export  default  { props:  ['message'], }; </script>` 

注意script部分的道具选项。我们可以像这样从父组件(Books)向下传递消息:

`<alert message="hi"></alert>` 

试试这个:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

查看文档了解更多关于道具的信息。

为了使其动态,以便传递自定义消息,在 Books.vue 中使用一个绑定表达式:

`<alert :message="message"></alert>` 

message添加到data选项中,同样在书籍中。vue 中:

`data()  { return  { books:  [], addBookForm:  { title:  '', author:  '', read:  [], }, message:  '', }; },` 

然后,在addBook内,更新消息:

`addBook(payload)  { const  path  =  'http://localhost:5000/books'; axios.post(path,  payload) .then(()  =>  { this.getBooks(); this.message  =  'Book added!'; }) .catch((error)  =>  { // eslint-disable-next-line console.log(error); this.getBooks(); }); },` 

最后,添加一个v-if,这样只有当showMessage为真时才会显示警告:

`<alert :message=message v-if="showMessage"></alert>` 

showMessage添加到data中:

`data()  { return  { books:  [], addBookForm:  { title:  '', author:  '', read:  [], }, message:  '', showMessage:  false, }; },` 

再次更新addBook,将showMessage设置为true:

`addBook(payload)  { const  path  =  'http://localhost:5000/books'; axios.post(path,  payload) .then(()  =>  { this.getBooks(); this.message  =  'Book added!'; this.showMessage  =  true; }) .catch((error)  =>  { // eslint-disable-next-line console.log(error); this.getBooks(); }); },` 

测试一下!

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

挑战:

  1. 想想showMessage应该设置在哪里false。更新您的代码。
  2. 尝试使用警报组件来显示错误。
  3. 将警报重构为可解除

放置路线

计算机网络服务器

对于更新,我们将需要使用一个唯一的标识符,因为我们不能依赖标题是唯一的。我们可以使用来自 Python 标准库uuid

更新 server/app.py 中的BOOKS:

`BOOKS = [
    {
        'id': uuid.uuid4().hex,
        'title': 'On the Road',
        'author': 'Jack Kerouac',
        'read': True
    },
    {
        'id': uuid.uuid4().hex,
        'title': 'Harry Potter and the Philosopher\'s Stone',
        'author': 'J. K. Rowling',
        'read': False
    },
    {
        'id': uuid.uuid4().hex,
        'title': 'Green Eggs and Ham',
        'author': 'Dr. Seuss',
        'read': True
    }
]` 

不要忘记重要的一点:

添加新书时,重构all_books以考虑唯一 id:

`@app.route('/books', methods=['GET', 'POST'])
def all_books():
    response_object = {'status': 'success'}
    if request.method == 'POST':
        post_data = request.get_json()
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book added!'
    else:
        response_object['books'] = BOOKS
    return jsonify(response_object)` 

添加新的路由处理程序:

`@app.route('/books/<book_id>', methods=['PUT'])
def single_book(book_id):
    response_object = {'status': 'success'}
    if request.method == 'PUT':
        post_data = request.get_json()
        remove_book(book_id)
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book updated!'
    return jsonify(response_object)` 

添加助手:

`def remove_book(book_id):
    for book in BOOKS:
        if book['id'] == book_id:
            BOOKS.remove(book)
            return True
    return False` 

花点时间想想你会如何处理一个不存在的id的情况。有效载荷不正确怎么办?在 helper 中重构 for 循环,使它更 Pythonic 化。

客户

步骤:

  1. 添加模态和形式
  2. 处理更新按钮单击
  3. 连接 AJAX 请求
  4. 警告用户
  5. 处理取消按钮单击
(1)添加情态和形式

首先,在模板中添加一个新的模态,就在第一个模态的下面:

`<b-modal ref="editBookModal"
         id="book-update-modal"
         title="Update"
         hide-footer>
  <b-form @submit="onSubmitUpdate" @reset="onResetUpdate" class="w-100">
  <b-form-group id="form-title-edit-group"
                label="Title:"
                label-for="form-title-edit-input">
      <b-form-input id="form-title-edit-input"
                    type="text"
                    v-model="editForm.title"
                    required
                    placeholder="Enter title">
      </b-form-input>
    </b-form-group>
    <b-form-group id="form-author-edit-group"
                  label="Author:"
                  label-for="form-author-edit-input">
        <b-form-input id="form-author-edit-input"
                      type="text"
                      v-model="editForm.author"
                      required
                      placeholder="Enter author">
        </b-form-input>
      </b-form-group>
    <b-form-group id="form-read-edit-group">
      <b-form-checkbox-group v-model="editForm.read" id="form-checks">
        <b-form-checkbox value="true">Read?</b-form-checkbox>
      </b-form-checkbox-group>
    </b-form-group>
    <b-button-group>
      <b-button type="submit" variant="primary">Update</b-button>
      <b-button type="reset" variant="danger">Cancel</b-button>
    </b-button-group>
  </b-form>
</b-modal>` 

将表单状态添加到script部分的data部分:

`editForm:  { id:  '', title:  '', author:  '', read:  [], },` 

挑战:不要使用新的模式,尝试使用相同的模式来处理 POST 和 PUT 请求。

(2)点击处理更新按钮

更新表格中的“更新”按钮:

`<button
        type="button"
        class="btn btn-warning btn-sm"
        v-b-modal.book-update-modal
        @click="editBook(book)">
    Update
</button>` 

添加一个新方法来更新editForm中的值:

`editBook(book)  { this.editForm  =  book; },` 

然后,添加一个方法来处理表单提交:

`onSubmitUpdate(evt)  { evt.preventDefault(); this.$refs.editBookModal.hide(); let  read  =  false; if  (this.editForm.read[0])  read  =  true; const  payload  =  { title:  this.editForm.title, author:  this.editForm.author, read, }; this.updateBook(payload,  this.editForm.id); },` 
(3)连接 AJAX 请求
`updateBook(payload,  bookID)  { const  path  =  `http://localhost:5000/books/${bookID}`; axios.put(path,  payload) .then(()  =>  { this.getBooks(); }) .catch((error)  =>  { // eslint-disable-next-line console.error(error); this.getBooks(); }); },` 
(4)提醒用户

更新updateBook:

`updateBook(payload,  bookID)  { const  path  =  `http://localhost:5000/books/${bookID}`; axios.put(path,  payload) .then(()  =>  { this.getBooks(); this.message  =  'Book updated!'; this.showMessage  =  true; }) .catch((error)  =>  { // eslint-disable-next-line console.error(error); this.getBooks(); }); },` 
(5)点击手柄取消按钮

添加方法:

`onResetUpdate(evt)  { evt.preventDefault(); this.$refs.editBookModal.hide(); this.initForm(); this.getBooks();  // why? },` 

更新initForm:

`initForm()  { this.addBookForm.title  =  ''; this.addBookForm.author  =  ''; this.addBookForm.read  =  []; this.editForm.id  =  ''; this.editForm.title  =  ''; this.editForm.author  =  ''; this.editForm.read  =  []; },` 

请确保在继续之前检查代码。完成后,测试应用程序。确保单击按钮时显示模式,并且正确填充输入值。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

删除路线

计算机网络服务器

更新路由处理程序:

`@app.route('/books/<book_id>', methods=['PUT', 'DELETE'])
def single_book(book_id):
    response_object = {'status': 'success'}
    if request.method == 'PUT':
        post_data = request.get_json()
        remove_book(book_id)
        BOOKS.append({
            'id': uuid.uuid4().hex,
            'title': post_data.get('title'),
            'author': post_data.get('author'),
            'read': post_data.get('read')
        })
        response_object['message'] = 'Book updated!'
    if request.method == 'DELETE':
        remove_book(book_id)
        response_object['message'] = 'Book removed!'
    return jsonify(response_object)` 

客户

像这样更新“删除”按钮:

`<button
        type="button"
        class="btn btn-danger btn-sm"
        @click="onDeleteBook(book)">
    Delete
</button>` 

添加方法来处理按钮点击,然后删除书:

`removeBook(bookID)  { const  path  =  `http://localhost:5000/books/${bookID}`; axios.delete(path) .then(()  =>  { this.getBooks(); this.message  =  'Book removed!'; this.showMessage  =  true; }) .catch((error)  =>  { // eslint-disable-next-line console.error(error); this.getBooks(); }); }, onDeleteBook(book)  { this.removeBook(book.id); },` 

现在,当用户单击删除按钮时,onDeleteBook方法被触发,这又触发了removeBook方法。该方法将删除请求发送到后端。当响应返回时,显示警告信息并运行getBooks

挑战:

  1. 添加一个确认警告,而不是单击按钮删除。
  2. 显示一条信息,比如“没有书!请加一个。”,没有书的时候。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

结论

这篇文章讲述了用 Vue 和 Flask 设置 CRUD 应用程序的基础知识。

通过回顾这篇文章开头的目标和经历每个挑战来检查你的理解。

你可以在 flask-vue-crud repo 中找到源代码。感谢阅读。

寻找更多?

  1. 查看用 Stripe、Vue.js 和 Flask 接受付款的博文,该博文从本文停止的地方开始。
  2. 想了解如何将此应用部署到 Heroku?查看使用 Docker 和 Gitlab CI 将 Flask 和 Vue 应用程序部署到 Heroku。

用 Python 开发异步任务队列

原文:https://testdriven.io/blog/developing-an-asynchronous-task-queue-in-python/

本教程着眼于如何使用 Python 的多重处理库和 Redis 实现几个异步任务队列。

队列数据结构

一个队列是一个先进先出 ( FIFO )的数据结构。

  1. 在尾部添加一个项目(入队)
  2. 在头部移除一个项目(出列)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

当您编写本教程中的示例时,您会在实践中看到这一点。

工作

让我们从创建一个基本任务开始:

`# tasks.py

import collections
import json
import os
import sys
import uuid
from pathlib import Path

from nltk.corpus import stopwords

COMMON_WORDS = set(stopwords.words("english"))
BASE_DIR = Path(__file__).resolve(strict=True).parent
DATA_DIR = Path(BASE_DIR).joinpath("data")
OUTPUT_DIR = Path(BASE_DIR).joinpath("output")

def save_file(filename, data):
    random_str = uuid.uuid4().hex
    outfile = f"{filename}_{random_str}.txt"
    with open(Path(OUTPUT_DIR).joinpath(outfile), "w") as outfile:
        outfile.write(data)

def get_word_counts(filename):
    wordcount = collections.Counter()
    # get counts
    with open(Path(DATA_DIR).joinpath(filename), "r") as f:
        for line in f:
            wordcount.update(line.split())
    for word in set(COMMON_WORDS):
        del wordcount[word]

    # save file
    save_file(filename, json.dumps(dict(wordcount.most_common(20))))

    proc = os.getpid()

    print(f"Processed {filename} with process id: {proc}")

if __name__ == "__main__":
    get_word_counts(sys.argv[1])` 

因此,get_word_counts从给定的文本文件中找到 20 个最常用的单词,并将它们保存到输出文件中。它还使用 Python 的 os 库打印当前进程标识符(或 pid)。

跟着一起走?

创建一个项目目录和一个虚拟环境。然后,使用 pip 安装 NLTK :

`(env)$ pip install nltk==3.6.5` 

安装完成后,调用 Python shell 并下载stopwords 文集:

`>>> import nltk
>>> nltk.download("stopwords")

[nltk_data] Downloading package stopwords to
[nltk_data]     /Users/michael/nltk_data...
[nltk_data]   Unzipping corpora/stopwords.zip.
True` 

如果您遇到 SSL 错误,请参考这篇文章。

示例修复:

>>> import nltk
>>> nltk.download('stopwords')
[nltk_data] Error loading stopwords: <urlopen error [SSL:
[nltk_data]     CERTIFICATE_VERIFY_FAILED] certificate verify failed:
[nltk_data]     unable to get local issuer certificate (_ssl.c:1056)>
False
>>> import ssl
>>> try:
...     _create_unverified_https_context = ssl._create_unverified_context
... except AttributeError:
...     pass
... else:
...     ssl._create_default_https_context = _create_unverified_https_context
...
>>> nltk.download('stopwords')
[nltk_data] Downloading package stopwords to
[nltk_data]     /Users/michael.herman/nltk_data...
[nltk_data]   Unzipping corpora/stopwords.zip.
True 

将上面的 tasks.py 文件添加到您的项目目录中,但是不要运行它。

多重处理池

我们可以使用多处理库并行运行这个任务:

`# simple_pool.py

import multiprocessing
import time

from tasks import get_word_counts

PROCESSES = multiprocessing.cpu_count() - 1

def run():
    print(f"Running with {PROCESSES} processes!")

    start = time.time()
    with multiprocessing.Pool(PROCESSES) as p:
        p.map_async(
            get_word_counts,
            [
                "pride-and-prejudice.txt",
                "heart-of-darkness.txt",
                "frankenstein.txt",
                "dracula.txt",
            ],
        )
        # clean up
        p.close()
        p.join()

    print(f"Time taken = {time.time() - start:.10f}")

if __name__ == "__main__":
    run()` 

这里,使用类,我们用两个进程处理了四个任务。

你注意到map_async方法了吗?将任务映射到流程基本上有四种不同的方法。当选择一个时,您必须考虑多参数、并发性、阻塞和排序:

方法多参数并发阻塞有序结果
map
map_async
apply
apply_async

没有closejoin,垃圾收集可能不会发生,这可能导致内存泄漏。

  1. close告知池不接受任何新任务
  2. join告知池在所有任务完成后退出

跟着一起走?从简单任务队列 repo 中的“数据”目录中抓取项目古腾堡样本文本文件,然后添加一个“输出”目录。

您的项目目录应该如下所示:

├── data
│   ├── dracula.txt
│   ├── frankenstein.txt
│   ├── heart-of-darkness.txt
│   └── pride-and-prejudice.txt
├── output
├── simple_pool.py
└── tasks.py 

运行时间应该不到一秒钟:

`(env)$ python simple_pool.py

Running with 15 processes!
Processed heart-of-darkness.txt with process id: 50510
Processed frankenstein.txt with process id: 50515
Processed pride-and-prejudice.txt with process id: 50511
Processed dracula.txt with process id: 50512

Time taken = 0.6383581161` 

这个脚本运行在 16 核的 i9 Macbook Pro 上。

因此,多重处理Pool类为我们处理排队逻辑。它非常适合运行 CPU 密集型任务或任何可以独立分解和分配的任务。如果您需要对队列进行更多的控制,或者需要在多个进程之间共享数据,您可能想看看Queue类。

关于这一点以及并行性(多处理)和并发性(多线程)之间的区别的更多信息,请回顾文章用并发性、并行性和异步性加速 Python。

多重处理队列

让我们看一个简单的例子:

`# simple_queue.py

import multiprocessing

def run():
    books = [
        "pride-and-prejudice.txt",
        "heart-of-darkness.txt",
        "frankenstein.txt",
        "dracula.txt",
    ]
    queue = multiprocessing.Queue()

    print("Enqueuing...")
    for book in books:
        print(book)
        queue.put(book)

    print("\nDequeuing...")
    while not queue.empty():
        print(queue.get())

if __name__ == "__main__":
    run()` 

同样来自多处理库的队列类是一个基本的 FIFO(先进先出)数据结构。它类似于队列。Queue 类,但是是为进程间通信设计的。我们使用put将一个项目加入队列,使用get将一个项目出队。

查看Queue 源代码可以更好地理解这个类的机制。

现在,让我们看看更高级的例子:

`# simple_task_queue.py

import multiprocessing
import time

from tasks import get_word_counts

PROCESSES = multiprocessing.cpu_count() - 1
NUMBER_OF_TASKS = 10

def process_tasks(task_queue):
    while not task_queue.empty():
        book = task_queue.get()
        get_word_counts(book)
    return True

def add_tasks(task_queue, number_of_tasks):
    for num in range(number_of_tasks):
        task_queue.put("pride-and-prejudice.txt")
        task_queue.put("heart-of-darkness.txt")
        task_queue.put("frankenstein.txt")
        task_queue.put("dracula.txt")
    return task_queue

def run():
    empty_task_queue = multiprocessing.Queue()
    full_task_queue = add_tasks(empty_task_queue, NUMBER_OF_TASKS)
    processes = []
    print(f"Running with {PROCESSES} processes!")
    start = time.time()
    for n in range(PROCESSES):
        p = multiprocessing.Process(target=process_tasks, args=(full_task_queue,))
        processes.append(p)
        p.start()
    for p in processes:
        p.join()
    print(f"Time taken = {time.time() - start:.10f}")

if __name__ == "__main__":
    run()` 

这里,我们将 40 个任务(每个文本文件 10 个)放入队列,通过Process类创建单独的进程,使用start开始运行进程,最后,使用join完成进程。

运行时间应该不到一秒钟。

挑战:通过添加另一个队列来保存已完成的任务,检查您的理解。您可以在process_tasks函数中对它们进行排队。

记录

多处理库也支持日志记录:

`# simple_task_queue_logging.py

import logging
import multiprocessing
import os
import time

from tasks import get_word_counts

PROCESSES = multiprocessing.cpu_count() - 1
NUMBER_OF_TASKS = 10

def process_tasks(task_queue):
    logger = multiprocessing.get_logger()
    proc = os.getpid()
    while not task_queue.empty():
        try:
            book = task_queue.get()
            get_word_counts(book)
        except Exception as e:
            logger.error(e)
        logger.info(f"Process {proc} completed successfully")
    return True

def add_tasks(task_queue, number_of_tasks):
    for num in range(number_of_tasks):
        task_queue.put("pride-and-prejudice.txt")
        task_queue.put("heart-of-darkness.txt")
        task_queue.put("frankenstein.txt")
        task_queue.put("dracula.txt")
    return task_queue

def run():
    empty_task_queue = multiprocessing.Queue()
    full_task_queue = add_tasks(empty_task_queue, NUMBER_OF_TASKS)
    processes = []
    print(f"Running with {PROCESSES} processes!")
    start = time.time()
    for w in range(PROCESSES):
        p = multiprocessing.Process(target=process_tasks, args=(full_task_queue,))
        processes.append(p)
        p.start()
    for p in processes:
        p.join()
    print(f"Time taken = {time.time() - start:.10f}")

if __name__ == "__main__":
    multiprocessing.log_to_stderr(logging.ERROR)
    run()` 

要进行测试,请将task_queue.put("dracula.txt")更改为task_queue.put("drakula.txt")。您应该会在终端中看到以下错误输出十次:

`[ERROR/Process-4] [Errno 2] No such file or directory:
'simple-task-queue/data/drakula.txt'` 

想要记录到光盘吗?

`# simple_task_queue_logging.py

import logging
import multiprocessing
import os
import time

from tasks import get_word_counts

PROCESSES = multiprocessing.cpu_count() - 1
NUMBER_OF_TASKS = 10

def create_logger():
    logger = multiprocessing.get_logger()
    logger.setLevel(logging.INFO)
    fh = logging.FileHandler("process.log")
    fmt = "%(asctime)s - %(levelname)s - %(message)s"
    formatter = logging.Formatter(fmt)
    fh.setFormatter(formatter)
    logger.addHandler(fh)
    return logger

def process_tasks(task_queue):
    logger = create_logger()
    proc = os.getpid()
    while not task_queue.empty():
        try:
            book = task_queue.get()
            get_word_counts(book)
        except Exception as e:
            logger.error(e)
        logger.info(f"Process {proc} completed successfully")
    return True

def add_tasks(task_queue, number_of_tasks):
    for num in range(number_of_tasks):
        task_queue.put("pride-and-prejudice.txt")
        task_queue.put("heart-of-darkness.txt")
        task_queue.put("frankenstein.txt")
        task_queue.put("dracula.txt")
    return task_queue

def run():
    empty_task_queue = multiprocessing.Queue()
    full_task_queue = add_tasks(empty_task_queue, NUMBER_OF_TASKS)
    processes = []
    print(f"Running with {PROCESSES} processes!")
    start = time.time()
    for w in range(PROCESSES):
        p = multiprocessing.Process(target=process_tasks, args=(full_task_queue,))
        processes.append(p)
        p.start()
    for p in processes:
        p.join()
    print(f"Time taken = {time.time() - start:.10f}")

if __name__ == "__main__":
    run()` 

同样,通过更改其中一个文件名来导致错误,然后运行它。看一下 process.log 。因为 Python 日志库不使用进程间的共享锁,所以它并不像它应该的那样有组织。为了解决这个问题,我们让每个进程写入自己的文件。为了保持有序,请在项目文件夹中添加一个日志目录:

`#  simple_task_queue_logging_separate_files.py

import logging
import multiprocessing
import os
import time

from tasks import get_word_counts

PROCESSES = multiprocessing.cpu_count() - 1
NUMBER_OF_TASKS = 10

def create_logger(pid):
    logger = multiprocessing.get_logger()
    logger.setLevel(logging.INFO)
    fh = logging.FileHandler(f"logs/process_{pid}.log")
    fmt = "%(asctime)s - %(levelname)s - %(message)s"
    formatter = logging.Formatter(fmt)
    fh.setFormatter(formatter)
    logger.addHandler(fh)
    return logger

def process_tasks(task_queue):
    proc = os.getpid()
    logger = create_logger(proc)
    while not task_queue.empty():
        try:
            book = task_queue.get()
            get_word_counts(book)
        except Exception as e:
            logger.error(e)
        logger.info(f"Process {proc} completed successfully")
    return True

def add_tasks(task_queue, number_of_tasks):
    for num in range(number_of_tasks):
        task_queue.put("pride-and-prejudice.txt")
        task_queue.put("heart-of-darkness.txt")
        task_queue.put("frankenstein.txt")
        task_queue.put("dracula.txt")
    return task_queue

def run():
    empty_task_queue = multiprocessing.Queue()
    full_task_queue = add_tasks(empty_task_queue, NUMBER_OF_TASKS)
    processes = []
    print(f"Running with {PROCESSES} processes!")
    start = time.time()
    for w in range(PROCESSES):
        p = multiprocessing.Process(target=process_tasks, args=(full_task_queue,))
        processes.append(p)
        p.start()
    for p in processes:
        p.join()
    print(f"Time taken = {time.time() - start:.10f}")

if __name__ == "__main__":
    run()` 

雷迪斯

接下来,我们不使用内存队列,而是将 Redis 添加到混合队列中。

跟着一起走? 下载并安装 Redis,如果你还没有安装的话。然后,安装 Python 接口:

(env)$ pip install redis==4.0.2 

我们将把逻辑分成四个文件:

  1. redis_queue.py 分别通过SimpleQueueSimpleTask类创建新的队列和任务。
  2. redis_queue_client 调查新任务。
  3. redis_queue_worker 出队并处理任务。
  4. redis_queue_server 产生工作进程。
`# redis_queue.py

import pickle
import uuid

class SimpleQueue(object):
    def __init__(self, conn, name):
        self.conn = conn
        self.name = name

    def enqueue(self, func, *args):
        task = SimpleTask(func, *args)
        serialized_task = pickle.dumps(task, protocol=pickle.HIGHEST_PROTOCOL)
        self.conn.lpush(self.name, serialized_task)
        return task.id

    def dequeue(self):
        _, serialized_task = self.conn.brpop(self.name)
        task = pickle.loads(serialized_task)
        task.process_task()
        return task

    def get_length(self):
        return self.conn.llen(self.name)

class SimpleTask(object):
    def __init__(self, func, *args):
        self.id = str(uuid.uuid4())
        self.func = func
        self.args = args

    def process_task(self):
        self.func(*self.args)` 

这里,我们定义了两个类,SimpleQueueSimpleTask:

  1. 创建一个新队列,入队,出队,并获取队列的长度。
  2. SimpleTask创建新任务,由SimpleQueue类的实例用来将新任务排队,并处理新任务。

好奇lpush()brpop()llen()?参见命令参考页。(The brpop()函数特别酷,因为它阻塞连接,直到有值存在才弹出!)

`# redis_queue_client.py

import redis

from redis_queue import SimpleQueue
from tasks import get_word_counts

NUMBER_OF_TASKS = 10

if __name__ == "__main__":
    r = redis.Redis()
    queue = SimpleQueue(r, "sample")
    count = 0
    for num in range(NUMBER_OF_TASKS):
        queue.enqueue(get_word_counts, "pride-and-prejudice.txt")
        queue.enqueue(get_word_counts, "heart-of-darkness.txt")
        queue.enqueue(get_word_counts, "frankenstein.txt")
        queue.enqueue(get_word_counts, "dracula.txt")
        count += 4
    print(f"Enqueued {count} tasks!")` 

这个模块将创建一个 Redis 和SimpleQueue类的新实例。然后,它将 40 个任务排队。

`# redis_queue_worker.py

import redis

from redis_queue import SimpleQueue

def worker():
    r = redis.Redis()
    queue = SimpleQueue(r, "sample")
    if queue.get_length() > 0:
        queue.dequeue()
    else:
        print("No tasks in the queue")

if __name__ == "__main__":
    worker()` 

如果任务可用,则调用dequeue方法,然后反序列化任务并调用process_task方法(在 redis_queue.py 中)。

`# redis_queue_server.py

import multiprocessing

from redis_queue_worker import worker

PROCESSES = 4

def run():
    processes = []
    print(f"Running with {PROCESSES} processes!")
    while True:
        for w in range(PROCESSES):
            p = multiprocessing.Process(target=worker)
            processes.append(p)
            p.start()
        for p in processes:
            p.join()

if __name__ == "__main__":
    run()` 

run方法产生了四个新的工作进程。

您可能不希望四个进程一直同时运行,但有时您可能需要四个或更多的进程。考虑如何根据需求有计划地增加和减少额外的工作人员。

要进行测试,请在单独的终端窗口中运行 redis_queue_server.pyredis_queue_client.py :

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过将日志添加到上面的应用程序中,再次检查您的理解。

结论

在本教程中,我们研究了 Python 中的许多异步任务队列实现。如果需求足够简单,以这种方式开发队列可能会更容易。也就是说,如果您正在寻找更高级的功能——如任务调度、批处理、作业优先级和失败任务的重试——您应该寻找一个成熟的解决方案。看看芹菜RQ ,或者 Huey

简单任务队列 repo 中获取最终代码。

DevOps 教程

原文:https://testdriven.io/blog/topics/devops/

描述

DevOps 是一种结合了应用程序开发和操作的软件开发策略,有助于在软件开发人员、质量保证(QA)工程师和系统管理员之间架起一座桥梁。虽然焦点倾向于工具,但是 DevOps 与文化(关于人和过程)以及工具和技术一样重要。

TestDriven.io 上的教程和文章的重点是利用 Docker 和 Kubernetes 等工具来简化开发、测试和部署,以缩短软件开发的生命周期。

将 Django 应用程序部署到 Azure App Service。

通过创建一个静态站点并将其部署到 Netlify,利用 Python 和 Flask 的 JAMstack。

将 Django 应用程序部署到 Google App Engine。

配置 Django,通过一个亚马逊 S3 桶加载和提供公共和私有的静态和媒体文件。

在 Python 应用程序中启用多区域支持。

部署一个 Django 应用程序来呈现。

将 Django 应用程序部署到 Fly.io。

了解什么是最好的 Heroku 替代方案(及其利弊)。

部署一个带有 PostgreSQL 的 Flask 应用程序进行渲染。

在 DigitalOcean droplet 上将 Django 应用程序部署到 Dokku。

配置 GitHub 动作,以持续地将 Django 和 Docker 应用程序部署到 Linode。

用 Docker 在 Nginx 后面运行基本认证配置 Flower。

用 PyPI server 和 Docker 设置自己的私有 PyPI 服务器。

本文着眼于如何配置 GitHub 操作来将 Python 包分发到 PyPI 并阅读文档。

将基于 Python 和 Selenium 的 web scraper 与 Selenium Grid 和 Docker Swarm 并行运行。

将 FastAPI 应用程序部署到 AWS Elastic Beanstalk。

将一个 Flask 应用程序部署到 AWS Elastic Beanstalk。

将 Django 应用程序部署到 AWS Elastic Beanstalk。

如何将 Django 应用程序部署到 DigitalOcean 的 App 平台。

本文着眼于如何使用 Bazel 来创建可重复的、密封的构建。

使用 Terraform 将 Django 应用程序部署到 AWS ECS。

将节点微服务部署到 Google Kubernetes 引擎上的 Kubernetes 集群。

本教程演示了如何在 Kubernetes 集群上部署 Spark。

使用 Hashicorp 的 Vault 和 Consul 为 Flask web 应用程序创建动态 Postgres 凭据的真实示例。

向 Flask、Redis Queue 和 Amazon SES 的新注册用户发送确认电子邮件。

使用 Docker 将自托管 GitLab CI/CD 运行程序部署到数字海洋。

使用 Docker 和 Docker Swarm 将自托管 GitHub Actions runners 部署到数字海洋。

本教程演示了如何在 DigitalOcean 上使用 Python 和 Fabric 自动建立 Kubernetes 集群。

配置一个在 EC2 实例上运行的容器化 Django 应用程序,将日志发送到 CloudWatch。

使用 Docker 层缓存和 BuildKit,在 CircleCI、GitLab CI 和 GitHub 操作上加速基于 Docker 的构建。

本教程展示了如何使用 Docker Swarm 部署 Vault 和 Consul。

设置和使用 Hashicorp 的保险库和 Consul 来安全地存储和管理机密。

配置 Django,通过 DigitalOcean Spaces 加载和提供公共和私有的静态和媒体文件。

使用加密 SSL 证书来保护运行在 HTTPS Nginx 代理后面的容器化 Django 应用程序。

用 Docker 部署一个 Django app 到 AWS EC2,让我们加密。

配置 GitLab CI 以持续地将 Django 和 Docker 应用程序部署到 AWS EC2。

配置 GitHub 操作,以持续将 Django 和 Docker 应用程序部署到 DigitalOcean。

配置 GitLab CI 以持续将 Django 和 Docker 应用程序部署到 DigitalOcean。

在下面的教程中,我们将带你了解如何在 Kubernetes 上设置 Hashicorp 的金库和领事。

如何将基于 Flask 的微服务(以及 Postgres 和 Vue.js)部署到 Kubernetes 集群的分步演练。

这篇文章详细介绍了如何将 Apache Spark 部署到 DigitalOcean 上的 Docker Swarm 集群。

这篇文章介绍了如何在 Docker Swarm 上运行 Flask 应用程序。

使用 Gitlab CI 将 Flask 和 Vue 支持的全栈 web 应用打包并部署到 Heroku。

简化在 Heroku 上部署、维护和扩展生产级 Django 应用的流程。

什么是连续交货?为什么说是竞争优势?流程是什么样的?

在本帖中,我们将了解如何配置 Travis CI 向 Telegram messenger 发送构建通知。

使用 Selenium Grid 和 Docker 进行分布式测试

原文:https://testdriven.io/blog/distributed-testing-with-selenium-grid/

对于希望实现频繁交付方法(比如持续集成和交付)或者总体上加速开发周期的软件开发团队来说,减少测试执行时间是关键。在频繁构建和测试是常态的环境中,开发人员根本无法承受连续几个小时等待测试完成。将测试分布在许多机器上是这个问题的一个解决方案。

本文着眼于如何使用 Selenium GridDocker Swarm 将自动化测试分布到多个机器上。

我们还将了解如何在多种浏览器上运行测试,并自动配置和取消配置机器以降低成本。

目标

完成本教程后,您将能够:

  1. 用 Docker 将硒网格装箱
  2. 在 Selenium Grid 上运行自动化测试
  3. 描述分布式计算和并行计算的区别
  4. 通过 Docker Compose 和 Machine 将 Selenium 网格部署到数字海洋
  5. 自动供应和取消供应数字海洋上的资源

项目设置

让我们从 Python 中的基本 Selenium 测试开始:

`import time
import unittest

from selenium import webdriver
from selenium.webdriver.common.keys import Keys

class HackerNewsSearchTest(unittest.TestCase):

    def setUp(self):
        self.browser = webdriver.Chrome()

    def test_hackernews_search_for_testdrivenio(self):
        browser = self.browser
        browser.get('https://news.ycombinator.com')
        search_box = browser.find_element_by_name('q')
        search_box.send_keys('testdriven.io')
        search_box.send_keys(Keys.RETURN)
        time.sleep(3)  # simulate long running test
        self.assertIn('testdriven.io', browser.page_source)

    def test_hackernews_search_for_selenium(self):
        browser = self.browser
        browser.get('https://news.ycombinator.com')
        search_box = browser.find_element_by_name('q')
        search_box.send_keys('selenium')
        search_box.send_keys(Keys.RETURN)
        time.sleep(3)  # simulate long running test
        self.assertIn('selenium', browser.page_source)

    def test_hackernews_search_for_testdriven(self):
        browser = self.browser
        browser.get('https://news.ycombinator.com')
        search_box = browser.find_element_by_name('q')
        search_box.send_keys('testdriven')
        search_box.send_keys(Keys.RETURN)
        time.sleep(3)  # simulate long running test
        self.assertIn('testdriven', browser.page_source)

    def test_hackernews_search_with_no_results(self):
        browser = self.browser
        browser.get('https://news.ycombinator.com')
        search_box = browser.find_element_by_name('q')
        search_box.send_keys('?*^^%')
        search_box.send_keys(Keys.RETURN)
        time.sleep(3)  # simulate long running test
        self.assertNotIn('<em>', browser.page_source)

    def tearDown(self):
        self.browser.quit()  # quit vs close?

if __name__ == '__main__':
    unittest.main()` 

跟着一起走?

  1. 创建新的项目目录。
  2. 将上述代码保存在一个名为 test.py 的新文件中。
  3. 创建并激活虚拟环境。
  4. 装硒:pip install selenium==3.141.0
  5. 全局安装 ChromeDriver 。(我们正在使用版本 88.0.4324.96 。)
  6. 确保它工作:python test.py

在这个测试中,我们导航到https://news.ycombinator.com,执行四次搜索,然后断言搜索结果页面被适当地呈现。没什么特别的,但是足够用了。请随意使用你自己的硒测试来代替这个测试。

执行时间:约 25 秒

`$ python test.py

....
----------------------------------------------------------------------
Ran 4 tests in 24.533s

OK` 

硒栅

说到分布式测试, Selenium Grid 是最强大和最流行的开源工具之一。有了它,我们可以将测试负载分散到多台机器上,并跨浏览器运行它们。

假设你有一套 90 个测试,在你的笔记本电脑上针对一个版本的 Chrome 运行。也许运行这些测试需要六分钟。使用 Selenium Grid,您可以旋转三台不同的机器来运行它们,这将减少(大约)三分之一的测试执行时间。您也可以在不同的浏览器和平台上运行相同的测试。因此,您不仅节省了时间,而且还有助于确保您的 web 应用程序在不同的浏览器和环境中呈现时表现和外观都是一样的。

Selenium Grid 使用客户机-服务器模型,包括一个中心和多个节点(运行测试的浏览器)。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

例如,您可以将三个节点连接到集线器,每个节点运行不同的浏览器。然后,当您使用特定的远程 WebDriver 运行您的测试时,WebDriver 请求被发送到中心 hub,它搜索与指定标准匹配的可用节点(例如,像浏览器版本)。一旦找到一个节点,就发送脚本并运行测试。

我们可以使用来自 Selenium Docker 的官方映像来启动一个中心和几个节点,而不是处理手动配置和安装 Selenium Grid 的麻烦。

要启动并运行,请将以下代码添加到项目根目录下名为 docker-compose.yml 的新文件中:

`version:  '3.8' services: hub: image:  selenium/hub:3.141.59 ports: -  4444:4444 chrome: image:  selenium/node-chrome:3.141.59 depends_on: -  hub environment: -  HUB_HOST=hub firefox: image:  selenium/node-firefox:3.141.59 depends_on: -  hub environment: -  HUB_HOST=hub` 

我们使用了3.141.59标签,它与以下版本的 Selenium、WebDriver、Chrome 和 Firefox 相关联:

`Selenium:  3.141.59 Chrome:  88.0.4324.96 ChromeDriver:  88.0.4324.96 Firefox:  85.0 GeckoDriver:  0.29.0` 

如果你想使用不同版本的 Chrome 或 Firefox,请参考发布版页面

提取并运行图像:

本指南使用 Docker 版本 18.09.2。

完成后,打开浏览器并导航到 Selenium Grid 控制台(http://localhost:4444/Grid/console),确保一切正常:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过更新setUp方法在测试文件中配置远程驱动程序:

`def setUp(self):
    caps = {'browserName': os.getenv('BROWSER', 'chrome')}
    self.browser = webdriver.Remote(
        command_executor='http://localhost:4444/wd/hub',
        desired_capabilities=caps
    )` 

确保也添加导入:

通过 Chrome 节点上的 Selenium Grid 运行测试:

`$ export BROWSER=chrome && python test.py

....
----------------------------------------------------------------------
Ran 4 tests in 21.054s

OK` 

也试试 Firefox:

`$ export BROWSER=firefox && python test.py

....
----------------------------------------------------------------------
Ran 4 tests in 25.058s

OK` 

为了模拟更长的测试运行,让我们连续运行 20 次相同的测试——10 次在 Chrome 上,10 次在 Firefox 上。

将名为 sequential_test_run.py 的新文件添加到项目根:

`from subprocess import check_call

for counter in range(10):
    chrome_cmd = 'export BROWSER=chrome && python test.py'
    firefox_cmd = 'export BROWSER=firefox && python test.py'
    check_call(chrome_cmd, shell=True)
    check_call(firefox_cmd, shell=True)` 

运行测试:

`$ python sequential_test_run.py` 

执行时间:约 8 分钟

分布式与并行

这很好,但是测试仍然没有并行运行。

这可能会引起混淆,因为“并行”和“分布式”经常被测试人员和开发人员互换使用。查看分布式与并行计算了解更多信息。

到目前为止,我们只处理了在多台机器上分发测试,这是由 Selenium Grid 处理的。测试运行器或框架,如 pytestnose ,负责并行运行测试。为了简单起见,我们将使用子流程模块,而不是完整的框架。值得注意的是,如果你正在使用 pytest 或 nose,请分别查看 pytest-xdist 插件或使用 nose 的并行测试指南以获得并行执行的帮助。

并行运行

将名为 parallel_test_run.py 的新文件添加到项目根:

`from subprocess import Popen

processes = []

for counter in range(10):
    chrome_cmd = 'export BROWSER=chrome && python test.py'
    firefox_cmd = 'export BROWSER=firefox && python test.py'
    processes.append(Popen(chrome_cmd, shell=True))
    processes.append(Popen(firefox_cmd, shell=True))

for counter in range(10):
    processes[counter].wait()` 

这将同时运行测试文件二十次,每次都使用单独的进程。

`$ python parallel_test_run.py` 

执行时间:约 4 分钟

这将花费不到四分钟的时间来运行,与顺序运行所有二十个测试相比,将执行时间减少了一半。我们可以通过注册更多的节点来进一步加快速度。

数字海洋

让我们旋转一个数字海洋液滴,这样我们就有更多的核心来工作。

如果你还没有账户,先从注册开始,然后生成一个访问令牌,这样我们就可以使用数字海洋 API

将令牌添加到您的环境中:

`$ export DIGITAL_OCEAN_ACCESS_TOKEN=[your_digital_ocean_token]` 

使用 Docker Machine 供应新的 droplet:

`$ docker-machine create \
    --driver digitalocean \
    --digitalocean-access-token $DIGITAL_OCEAN_ACCESS_TOKEN \
    --engine-install-url "https://releases.rancher.com/install-docker/19.03.9.sh" \
    selenium-grid;` 

需要--engine-install-url,因为在撰写本文时,Docker v20.10.0 无法与 Docker Machine 一起使用。

完成后,将 Docker 守护进程指向该机器,并将其设置为活动机器:

`$ docker-machine env selenium-grid
$ eval $(docker-machine env selenium-grid)` 

旋转 droplet 上的三个容器——中心和两个节点:

抓取水滴的 IP:

`$ docker-machine ip selenium-grid` 

确保 Selenium Grid 在http://YOUR _ IP:4444/Grid/console启动并运行,然后更新测试文件中的 IP 地址:

`command_executor='http://YOUR_IP:4444/wd/hub',` 

再次并行运行测试:

`$ python parallel_test_run.py` 

刷新网格控制台。两个测试应该正在运行,而剩余的 18 个测试正在排队:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

同样,这应该需要大约四分钟来运行。

码头工人群体模式

继续前进,我们应该旋转更多的节点来运行测试。然而,由于 droplet 上的资源有限,让我们添加一些 droplet 供节点驻留。这就是 Docker Swarm 发挥作用的地方。

为了创建 Swarm 集群,让我们从头开始,首先旋转旧的液滴:

`$ docker-machine rm selenium-grid` 

然后,旋转五个新的液滴:

`$ for i in 1 2 3 4 5; do
    docker-machine create \
      --driver digitalocean \
      --digitalocean-access-token $DIGITAL_OCEAN_ACCESS_TOKEN \
      --engine-install-url "https://releases.rancher.com/install-docker/19.03.9.sh" \
      node-$i;
done` 

node-1初始化群模式:

`$ docker-machine ssh node-1 -- docker swarm init --advertise-addr $(docker-machine ip node-1)` 

您应该会看到类似如下的内容:

`Swarm initialized: current node (ae0iz7lqwz6g9p0oso4f5g6sd) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-54ca6zbkpya4mw15mctnmnkp7uzqmtcj8hm354ym2qqr8n5iyq-2v63f4ztawazzzitiibgpnh39 134.209.115.249:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.` 

请注意 join 命令,因为它包含一个令牌,我们需要这个令牌来将节点 workers 添加到群中。

如果忘记了,随时可以跑docker-machine ssh node-1 -- docker swarm join-token worker

将剩余的四个节点作为工人添加到群中:

`$ for i in 2 3 4 5; do
    docker-machine ssh node-$i \
      -- docker swarm join --token YOUR_JOIN_TOKEN;
done` 

更新 docker-compose.yml 文件,以 Swarm 模式部署 Selenium 网格:

`version:  '3.8' services: hub: image:  selenium/hub:3.141.59 ports: -  4444:4444 deploy: mode:  replicated replicas:  1 placement: constraints: -  node.role == worker chrome: image:  selenium/node-chrome:3.141.59 volumes: -  /dev/urandom:/dev/random depends_on: -  hub environment: -  HUB_PORT_4444_TCP_ADDR=hub -  HUB_PORT_4444_TCP_PORT=4444 -  NODE_MAX_SESSION=1 entrypoint:  bash -c 'SE_OPTS="-host $$HOSTNAME -port 5555" /opt/bin/entry_point.sh' ports: -  5555:5555 deploy: replicas:  1 placement: constraints: -  node.role == worker firefox: image:  selenium/node-firefox:3.141.59 volumes: -  /dev/urandom:/dev/random depends_on: -  hub environment: -  HUB_PORT_4444_TCP_ADDR=hub -  HUB_PORT_4444_TCP_PORT=4444 -  NODE_MAX_SESSION=1 entrypoint:  bash -c 'SE_OPTS="-host $$HOSTNAME -port 5556" /opt/bin/entry_point.sh' ports: -  5556:5556 deploy: replicas:  1 placement: constraints: -  node.role == worker` 

主要变化:

  1. 布局约束:我们设置了node.role == worker布局约束,这样所有的任务都将在 worker 节点上运行。通常最好让管理器节点远离 CPU 和/或内存密集型任务。
  2. Entrypoint :这里,我们更新了 entry_point.sh 脚本中的SE_OPTS中的主机集,这样运行在不同主机上的节点将能够成功链接回 hub。

这样,将 Docker 守护进程指向node-1并部署堆栈:

`$ eval $(docker-machine env node-1)
$ docker stack deploy --compose-file=docker-compose.yml selenium` 

再添加几个节点:

`$ docker service scale selenium_chrome=4 selenium_firefox=4` 

查看堆栈:

`$ docker stack ps selenium` 

您应该会看到类似这样的内容:

`ID             NAME                 IMAGE                            NODE      DESIRED STATE   CURRENT STATE
99filw99x8bc   selenium_chrome.1    selenium/node-chrome:3.141.59    node-3    Running         Running 41 seconds ago
9ff9cwx1dmqw   selenium_chrome.2    selenium/node-chrome:3.141.59    node-4    Running         Running about a minute ago
ige7rlnj1e03   selenium_chrome.3    selenium/node-chrome:3.141.59    node-5    Running         Running 59 seconds ago
ewsg5mxiy9eg   selenium_chrome.4    selenium/node-chrome:3.141.59    node-2    Running         Running 56 seconds ago
y3ud4iojz8u0   selenium_firefox.1   selenium/node-firefox:3.141.59   node-4    Running         Running about a minute ago
bvpizrfdhlq0   selenium_firefox.2   selenium/node-firefox:3.141.59   node-5    Running         Running about a minute ago
0jdw3sr7ld62   selenium_firefox.3   selenium/node-firefox:3.141.59   node-3    Running         Running 50 seconds ago
4esw9a2wvcf3   selenium_firefox.4   selenium/node-firefox:3.141.59   node-2    Running         Running about a minute ago
3dd04mt1t7n8   selenium_hub.1       selenium/hub:3.141.59            node-5    Running         Running about a minute ago` 

然后,获取运行集线器的节点的名称和 IP 地址,并将其设置为环境变量:

`$ NODE=$(docker service ps --format "{{.Node}}" selenium_hub)
$ export NODE_HUB_ADDRESS=$(docker-machine ip $NODE)` 

再次更新setUp方法:

`def setUp(self):
    caps = {'browserName': os.getenv('BROWSER', 'chrome')}
    address = os.getenv('NODE_HUB_ADDRESS')
    self.browser = webdriver.Remote(
        command_executor=f'http://{address}:4444/wd/hub',
        desired_capabilities=caps
    )` 

测试!

`$ python parallel_test_run.py` 

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

执行时间:约 1.5 分钟

去除水滴:

`$ docker-machine rm node-1 node-2 node-3 node-4 node-5 -y` 

概括地说,为了创建一个群体,我们:

  1. 旋转出新的水滴
  2. 在其中一个液滴上初始化群体模式(本例中为node-1)
  3. 将节点作为工人添加到群体中

自动化脚本

因为让 droplet 闲置,等待客户端运行测试是不划算的,所以我们应该在测试运行之前自动供应 droplet,然后在运行之后取消供应。

让我们编写一个脚本:

  • 用 Docker 机器提供液滴
  • 配置 Docker 群组模式
  • 向群集添加节点
  • 部署硒网格
  • 运行测试
  • 旋转水滴

create.sh :

`#!/bin/bash

echo "Spinning up five droplets..."

for i in 1 2 3 4 5; do
  docker-machine create \
    --driver digitalocean \
    --digitalocean-access-token $DIGITAL_OCEAN_ACCESS_TOKEN \
    --engine-install-url "https://releases.rancher.com/install-docker/19.03.9.sh" \
    node-$i;
done

echo "Initializing Swarm mode..."

docker-machine ssh node-1 -- docker swarm init --advertise-addr $(docker-machine ip node-1)

docker-machine ssh node-1 -- docker node update --availability drain node-1

echo "Adding the nodes to the Swarm..."

TOKEN=`docker-machine ssh node-1 docker swarm join-token worker | grep token | awk '{ print $5 }'`

docker-machine ssh node-2 "docker swarm join --token ${TOKEN}  $(docker-machine ip node-1):2377"
docker-machine ssh node-3 "docker swarm join --token ${TOKEN}  $(docker-machine ip node-1):2377"
docker-machine ssh node-4 "docker swarm join --token ${TOKEN}  $(docker-machine ip node-1):2377"
docker-machine ssh node-5 "docker swarm join --token ${TOKEN}  $(docker-machine ip node-1):2377"

echo "Deploying Selenium Grid to http://$(docker-machine ip node-1):4444..."

eval $(docker-machine env node-1)
docker stack deploy --compose-file=docker-compose.yml selenium
docker service scale selenium_chrome=2 selenium_firefox=2` 

destroy.sh :

`#!/bin/bash

docker-machine rm node-1 node-2 node-3 node-4 node-5 -y` 

测试!

`$ sh create.sh

$ eval $(docker-machine env node-1)
$ NODE=$(docker service ps --format "{{.Node}}" selenium_hub)
$ export NODE_HUB_ADDRESS=$(docker-machine ip $NODE)

$ python parallel_test_run.py

$ sh destroy.sh` 

结论

本文研究了如何使用 Docker 和 Docker Swarm 配置 Selenium Grid,以便在多台机器上分布测试。

完整的代码可以在selenium-grid-docker-swarm-test库中找到。

寻找一些挑战?

  1. 通过在不同的 Selenium 网格节点上并行运行所有测试方法,尝试进一步减少测试执行时间。
  2. 在 Travis 或 Jenkins(或其他 CI 工具)上配置测试的运行,使它们成为持续集成过程的一部分。

在 Django 中使用 AJAX

原文:https://testdriven.io/blog/django-ajax-xhr/

AJAX ,代表异步 JavaScript 和 XML,是一组在客户端使用的技术,用于异步发送和从服务器检索数据。

AJAX 允许我们对网页内容进行修改,而不需要用户重新加载整个页面。例如,这对于搜索栏中的自动完成或表单验证非常有用。如果使用得当,您可以提高网站的性能,减少服务器负载,并改善整体用户体验。

在本文中,我们将看看如何在 Django 中执行 GET、POST、PUT 和 DELETE AJAX 请求的例子。虽然重点将放在获取 API 上,我们也将展示 jQuery 的例子。

什么是 AJAX?

AJAX 是一种编程实践,它使用 XMLHttpRequest (XHR)对象与服务器异步通信并构建动态网页。尽管 AJAX 和 XMLHttpRequest 经常互换使用,但它们是不同的事物。

为了向 web 服务器发送数据和从 web 服务器接收数据,AJAX 使用以下步骤:

  1. 创建一个 XMLHttpRequest 对象。
  2. 使用 XMLHttpRequest 对象在客户端和服务器之间异步交换数据。
  3. 使用 JavaScript 和 DOM 来处理数据。

通过使用 ajax 方法,AJAX 可以与 jQuery 一起使用,但是本机 Fetch API 要好得多,因为它有一个干净的接口,不需要第三方库。

获取 API 的一般结构如下所示:

`fetch('http://some_url.com') .then(response  =>  response.json())  // converts the response to JSON .then(data  =>  { console.log(data); // do something (like update the DOM with the data) });` 

参考 MDN 文档中的使用 Fetchwindoworworkerglobalscope . Fetch()获取更多示例以及fetch方法可用的完整选项。

什么时候应该使用 AJAX?

同样,AJAX 有助于提高站点的性能,同时降低服务器负载,改善整体用户体验。也就是说,它增加了应用程序的复杂性。正因为如此,除非你使用的是一个单页面应用(SPA)——比如 React、Angular 或 Vue——你应该只在绝对必要的时候使用 AJAX。

您可能会考虑使用 AJAX 的一些例子:

  1. 搜索自动完成
  2. 表单验证
  3. 表格排序和过滤
  4. 验证码
  5. 调查和民意测验

一般来说,如果内容需要根据用户交互进行大量更新,您可能希望使用 AJAX 来管理 web 页面的更新部分,而不是通过页面刷新来管理整个页面。

CRUD 资源

本文中的例子可以应用于任何 CRUD 资源。示例 Django 项目使用 todos 作为它的资源:

方法统一资源定位器描述
得到/todos/返回所有待办事项
邮政/todos/添加待办事项
/todos/<todo-id>/更新待办事项
删除/todos/<todo-id>/删除待办事项

示例项目可以在 GitHub 上找到:

  1. 获取版本:https://github.com/testdrivenio/django-ajax-xhr
  2. jQuery 版本:https://github.com/testdrivenio/django-ajax-xhr/tree/jquery

获取请求

让我们从简单的获取数据的 GET 请求开始。

获取 API

示例:

`fetch(url,  { method:  "GET", headers:  { "X-Requested-With":  "XMLHttpRequest", } }) .then(response  =>  response.json()) .then(data  =>  { console.log(data); });` 

唯一必需的参数是您希望从中获取数据的资源的 URL。如果 URL 需要关键字参数或查询字符串,可以使用 Django 的{% url %}标签。

你注意到标题了吗?这是通知服务器您正在发送一个 AJAX 请求所必需的。

fetch返回包含 HTTP 响应的承诺。我们使用.then方法首先从响应中提取 JSON 格式的数据(通过response.json()),然后访问返回的数据。在上面的例子中,我们只是在控制台中输出数据。

https://github . com/testdrivenio/django-Ajax-xhr/blob/main/static/main . js # L19-L39

jQuery AJAX

等效的 jQuery 代码:

`$.ajax({ url:  url, type:  "GET", dataType:  "json", success:  (data)  =>  { console.log(data); }, error:  (error)  =>  { console.log(error); } });` 

https://github . com/test rivieno/django-Ajax-xhr/blob/jquery/static/main . js # l19-l41

姜戈观点

在 Django 方面,虽然有几种方法可以处理视图中的 AJAX 请求,但最简单的方法是使用基于函数的视图:

`from django.http import HttpResponseBadRequest, JsonResponse

from todos.models import Todo

def todos(request):
    # request.is_ajax() is deprecated since django 3.1
    is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'

    if is_ajax:
        if request.method == 'GET':
            todos = list(Todo.objects.all().values())
            return JsonResponse({'context': todos})
        return JsonResponse({'status': 'Invalid request'}, status=400)
    else:
        return HttpResponseBadRequest('Invalid request')` 

在本例中,我们的资源是 todos。因此,在从数据库获取 todos 之前,我们验证了我们正在处理一个 AJAX 请求,并且请求方法是 GET。如果两者都为真,我们序列化数据并使用JsonResponse类发送响应。由于QuerySet对象不是 JSON 可序列化的(在本例中是 todos),我们使用values方法将 QuerySet 作为一个字典返回,然后将其包装在list中。最终结果是一个字典列表。

https://github . com/testdrivenio/django-Ajax-xhr/blob/main/todos/views . py # L13-L28

发布请求

接下来,让我们看看如何处理 POST 请求。

获取 API

示例:

`fetch(url,  { method:  "POST", credentials:  "same-origin", headers:  { "X-Requested-With":  "XMLHttpRequest", "X-CSRFToken":  getCookie("csrftoken"), }, body:  JSON.stringify({payload:  "data to send"}) }) .then(response  =>  response.json()) .then(data  =>  { console.log(data); });` 

我们需要指定如何在请求中发送凭证。

在上面的代码中,我们使用了"same-origin"(默认值)的值来指示浏览器,如果所请求的 URL 与 fetch 调用在同一个源上,就发送凭证。

在前端和后端托管在不同服务器上的情况下,您必须将credentials设置为"include"(它总是随每个请求发送凭证),并在后端启用跨源资源共享。您可以使用 django-cors-headers 包将 cors 报头添加到 django 应用程序的响应中。

想进一步了解如何处理同一域和跨域的 AJAX 请求吗?查看 Django 基于会话的单页面应用认证文章。

这次我们在请求的body中向服务器发送数据。

注意X-CSRFToken标题。如果没有它,您将在终端中从服务器得到 403 禁止响应:

`Forbidden (CSRF token missing or incorrect.): /todos/` 

这是因为在发布请求时必须包含 CSRF 令牌,以防止跨站请求伪造攻击。

我们可以通过将每个XMLHttpRequest上的X-CSRFToken头设置为 CSRF 令牌的值来包含 CSRF 令牌。

Django 文档为我们提供了一个很好的函数,允许我们获取令牌,从而简化了我们的生活:

`function  getCookie(name)  { let  cookieValue  =  null; if  (document.cookie  &&  document.cookie  !==  "")  { const  cookies  =  document.cookie.split(";"); for  (let  i  =  0;  i  <  cookies.length;  i++)  { const  cookie  =  cookies[i].trim(); // Does this cookie string begin with the name we want? if  (cookie.substring(0,  name.length  +  1)  ===  (name  +  "="))  { cookieValue  =  decodeURIComponent(cookie.substring(name.length  +  1)); break; } } } return  cookieValue; }` 

https://github . com/testdrivenio/django-Ajax-xhr/blob/main/static/main . js # L42-L56

jQuery AJAX

带有 jQuery 的 AJAX POST 请求与 GET 请求非常相似:

`$.ajax({ url:  url, type:  "POST", dataType:  "json", data:  JSON.stringify({payload:  payload,}), headers:  { "X-Requested-With":  "XMLHttpRequest", "X-CSRFToken":  getCookie("csrftoken"),  // don't forget to include the 'getCookie' function }, success:  (data)  =>  { console.log(data); }, error:  (error)  =>  { console.log(error); } });` 

https://github . com/test rivieno/django-Ajax-xhr/blob/jquery/static/main . js # l44-l61

姜戈观点

在服务器端,视图需要从请求中获取 JSON 格式的数据,所以您需要使用json模块来加载它。

`import json

from django.http import HttpResponseBadRequest, JsonResponse

from todos.models import Todo

def todos(request):
    # request.is_ajax() is deprecated since django 3.1
    is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'

    if is_ajax:
        if request.method == 'POST':
            data = json.load(request)
            todo = data.get('payload')
            Todo.objects.create(task=todo['task'], completed=todo['completed'])
            return JsonResponse({'status': 'Todo added!'})
        return JsonResponse({'status': 'Invalid request'}, status=400)
    else:
        return HttpResponseBadRequest('Invalid request')` 

在验证我们正在处理一个 AJAX 请求并且请求方法是 POST 之后,我们反序列化请求对象并提取有效负载对象。然后,我们创建了一个新的 todo 并发回了适当的响应。

https://github . com/testdrivenio/django-Ajax-xhr/blob/main/todos/views . py # L13-L28

上传请求

获取 API

示例:

`fetch(url,  { method:  "PUT", credentials:  "same-origin", headers:  { "X-Requested-With":  "XMLHttpRequest", "X-CSRFToken":  getCookie("csrftoken"),  // don't forget to include the 'getCookie' function }, body:  JSON.stringify({payload:  "data to send"}) }) .then(response  =>  response.json()) .then(data  =>  { console.log(data); });` 

这应该类似于 POST 请求。唯一的区别是 URL 的形状:

  1. 后- /todos/
  2. 放- /todos/<todo-id>/

https://github . com/testdrivenio/django-Ajax-xhr/blob/main/static/main . js # L59-L73

jQuery AJAX

等效 jQuery:

`$.ajax({ url:  url, type:  "PUT", dataType:  "json", data:  JSON.stringify({payload:  payload,}), headers:  { "X-Requested-With":  "XMLHttpRequest", "X-CSRFToken":  getCookie("csrftoken"),  // don't forget to include the 'getCookie' function }, success:  (data)  =>  { console.log(data); }, error:  (error)  =>  { console.log(error); } });` 

https://github . com/test rivieno/django-Ajax-xhr/blob/jquery/static/main . js # l64-l81

姜戈观点

示例:

`import json

from django.http import HttpResponseBadRequest, JsonResponse
from django.shortcuts import get_object_or_404

from todos.models import Todo

def todo(request, todoId):
    # request.is_ajax() is deprecated since django 3.1
    is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'

    if is_ajax:
        todo = get_object_or_404(Todo, id=todoId)

        if request.method == 'PUT':
            data = json.load(request)
            updated_values = data.get('payload')

            todo.task = updated_values['task']
            todo.completed = updated_values['completed']
            todo.save()

            return JsonResponse({'status': 'Todo updated!'})
        return JsonResponse({'status': 'Invalid request'}, status=400)
    else:
        return HttpResponseBadRequest('Invalid request')` 

https://github . com/testdrivenio/django-Ajax-xhr/blob/main/todos/views . py # L31-L53

删除请求

获取 API

示例:

`fetch(url,  { method:  "DELETE", credentials:  "same-origin", headers:  { "X-Requested-With":  "XMLHttpRequest", "X-CSRFToken":  getCookie("csrftoken"),  // don't forget to include the 'getCookie' function } }) .then(response  =>  response.json()) .then(data  =>  { console.log(data); });` 

https://github . com/testdrivenio/django-Ajax-xhr/blob/main/static/main . js # L76-L89

jQuery AJAX

jQuery 代码:

`$.ajax({ url:  url, type:  "DELETE", dataType:  "json", headers:  { "X-Requested-With":  "XMLHttpRequest", "X-CSRFToken":  getCookie("csrftoken"), }, success:  (data)  =>  { console.log(data); }, error:  (error)  =>  { console.log(error); } });` 

https://github . com/test rivieno/django-Ajax-xhr/blob/jquery/static/main . js # l84-l100

姜戈观点

查看:

`from django.http import HttpResponseBadRequest, JsonResponse
from django.shortcuts import get_object_or_404

from todos.models import Todo

def todo(request, todoId):
    # request.is_ajax() is deprecated since django 3.1
    is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest'

    if is_ajax:
        todo = get_object_or_404(Todo, id=todoId)

        if request.method == 'DELETE':
            todo.delete()
            return JsonResponse({'status': 'Todo deleted!'})
        return JsonResponse({'status': 'Invalid request'}, status=400)
    else:
        return HttpResponseBadRequest('Invalid request')` 

https://github . com/testdrivenio/django-Ajax-xhr/blob/main/todos/views . py # L31-L53

摘要

AJAX 允许我们执行异步请求来更改页面的某些部分,而不必重新加载整个页面。

在本文中,您看到了如何使用 Fetch API 和 jQuery 在 Django 中执行 GET、POST、PUT 和 DELETE AJAX 请求的详细示例。

示例项目可以在 GitHub 上找到:

  1. 获取版本:https://github.com/testdrivenio/django-ajax-xhr
  2. jQuery 版本:https://github.com/testdrivenio/django-ajax-xhr/tree/jquery
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值