Skip to content

Latest commit

 

History

History
263 lines (167 loc) · 20.2 KB

File metadata and controls

263 lines (167 loc) · 20.2 KB

七、构建优化的前端

在这本书中,我们已经走了相当远,同时试图理解如何用 Python 为企业构建应用。到目前为止,我们所涵盖的章节包括如何为我们的企业应用构建一个可伸缩且响应迅速的后端,以满足大量并发用户的需求,从而使我们的企业应用在用户中获得成功。但是,有一个我们一直缺少的主题,在构建企业级应用时通常很少得到关注:应用前端。

当用户与我们的应用交互时,他们几乎不关心后端发生了什么。用户的体验与应用前端如何响应其输入直接相关。这使得应用前端不仅是应用最重要的方面之一,而且也是用户成功应用的主要决定因素之一。

在本章中,我们将了解如何构建应用前端,不仅提供易于使用的体验,而且还提供对其输入的快速响应。

在阅读本章的同时,我们将了解以下主题:

  • 优化应用前端的必要性
  • 优化前端所依赖的资源
  • 利用客户端缓存优化页面加载
  • 利用 web 存储持久化用户数据

技术要求

本书中的代码清单可以在chapter06内置的 bugzot 应用目录下找到 https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

*可以通过运行以下命令克隆代码示例:

git clone https://github.com/PacktPublishing/Hands-On-Enterprise-Application-Development-with-Python

代码的执行不需要任何特定的工具或框架,是一个非常简单的过程。README.md文件指向如何运行本章的代码示例。

优化前端的必要性

应用的 UI 是其最重要的面向用户的组件之一。它决定用户将如何感知应用。平滑流畅的前端在定义应用的用户体验方面大有帮助。

这种对流畅用户体验的需求带来了优化应用前端的需求,因为它提供了易于使用的界面、快速的响应时间和操作的流动性。如果我们继续关注 Web2.0 公司,如谷歌、Facebook、LinkedIn 等,他们只需在优化前端上花费大量资源,就可以节省几毫秒的渲染时间。这就是优化前端的重要性所在。

优化前端的组件

我们正在讨论优化前端。但是优化的前端由什么组成呢?我们如何决定前端是否优化?让我们看一看。

一个优化的前端有几个组件,其中每个组件都需要从前端反射不是强制性的。这些组成部分如下:

  • 快速呈现时间:前端优化的首要重点之一是减少页面呈现时间。虽然没有一组预定义的呈现时间可以被认为是好的或坏的,但是您可以认为,一个好的呈现时间应该是用户不必等待太长的时间就可以将页面加载到一个合适的 internet 连接上。还有,一个。。。

什么导致前端问题

前端问题是用户容易察觉的一类问题,因为它们影响用户与应用交互的方式。为了清楚起见,当我们说企业 web 应用的前端时,我们不仅仅是在谈论它的 UI,我们也在谈论呈现所需 UI 的代码和模板。现在,让我们继续了解前端特定问题的可能原因:

  • 对象数量过多:在大多数负责呈现前端的动态填充模板中,第一个问题是呈现过多的对象。当大量对象被传递到需要呈现的模板时,页面响应时间往往会增加,从而导致该过程即将放缓。

  • 过度包括:软件工程中关注的主要问题之一是如何增加代码库的模块化。模块化的增加有利于提高组件的可重用性。然而,任何过度的行为都可能是可能发生的重大问题的信号。如果前端模板的模块化程度超出了要求,则模板的渲染性能会降低。原因是,对于存在的每个 include,都需要从磁盘加载一个新文件,这是一个异常缓慢的操作。 这里的一个计数器点可能是,一旦模板加载了它包含的所有内容,渲染引擎就可以缓存该模板,并为来自缓存的后续请求提供服务。但是,大多数缓存引擎都有一个限制,即它们可以缓存多少级别的包含深度,超过这个限制,性能将很快受到影响。

  • 不必要的资源集:某些前端可能加载了不必要的大量资源,而这些资源在特定页面的任何位置都没有使用。这包括 JavaScript 文件,其中包含仅在一小部分页面上执行的函数。正在加载的每个额外文件不仅会增加带宽消耗,还会影响前端的加载性能。

  • 强制串行加载代码:大多数现代浏览器现在都经过优化,可以并行加载大量资源,从而有效利用网络带宽,减少页面加载时间。然而,有时,我们用来减少代码量的一些技巧可能会迫使页面按顺序加载,而不是并行加载。可能导致按顺序加载页面资源的最常见示例之一是使用 CSS 导入。尽管 CSS 导入提供了直接在另一个样式表中加载第三方 CSS 文件的灵活性,但它也降低了浏览器并行加载 CSS 文件内容的能力,从而增加了呈现页面所需的时间。

这组原因形成了一个非详尽的问题列表,这些问题可能会导致页面呈现时间减慢,从而给用户带来不愉快的体验。

现在,让我们来看看我们如何优化我们的前端在本质上反应,并提供最好的用户体验。

优化前端

到目前为止,我们了解到了可能影响前端性能的各种问题。现在,我们来看看如何降低对前端的性能影响,使其在企业级环境中快速响应。

优化资源

我们将要关注的第一个也是最重要的优化是在请求特定页面时加载的资源的优化。为此,请在管理面板中的用户数据显示页中考虑下面的代码段,该代码段负责显示数据库中的用户表:

<table>
{% for user in users %}
  <tr>
    <td class="user-data-column">{{ user.username }}</td>
    <td class="user-data-column">{{ user.email }}</td>
    <td class="user-data-column">{{ user.status }}</td>
  </tr>
{% endfor %}
</table>

到目前为止,一切顺利。正如我们所看到的,代码片段只是在用户对象上循环,并根据用户表中存储的记录数呈现表。对于用户记录数量很少(例如,100 左右)的大多数情况,这基本上是好的。但随着应用中用户数量的增加,这段代码将开始出现问题。想象一下,尝试从应用数据库加载 100 万条记录,并使它们显示在 UI 上。这方面存在一些问题:

  • 缓慢的数据库查询:试图同时从数据库加载 100 万条记录将非常缓慢,可能需要相当长的时间,因此会长时间阻止视图的渲染。
  • **解码前端中的对象:**在前端,为了呈现页面,模板引擎必须解码来自所有对象的数据,以便能够在页面上显示数据。这种操作不仅占用 CPU,而且速度很慢。
  • **页面大小大:**想象一下,通过网络将包含数百万条记录的页面从服务器传输到客户端。此过程非常耗时,而且也不利于通过慢速连接加载页面。

那么,我们在这里能做什么?答案很简单:让我们优化将要加载的资源量。为了实现这一点,我们将使用称为分页的概念。

要实现分页,我们需要对负责呈现前端模板以及前端模板的视图进行一些更改。以下代码描述了视图在必须支持分页时的外观:

From bugzot.application import app, db
from bugzot.models import User
from flask.views import MethodView
from flask import render_template, session, request

class UserListView(MethodView):
    """User list view for displaying user data in admin panel.

      The user list view is responsible for rendering the table of users that are registered
      in the application.
    """

    def get(self):
        """HTTP GET handler."""

        page = request.args.get('next_page', 1) # get the page number to be displayed
        users = User.query.paginate(page, 20, False)
        total_records = users.total
        user_records = users.items

        return render_template('admin/user_list.html', users=user_records, next_page=page+1)

我们现在完成了对视图的修改,它现在支持分页。使用 SQLAlchemy 已经提供的工具,使用paginate()方法对数据库表中的结果进行分页,实现这种分页是一项相当简单的任务。此paginate()方法需要三个参数,即页码(应从一开始)、每页上的记录数和error_out负责设置方法的错误报告。此处的False禁止错误显示在stdout上。

开发视图以支持分页,接下来的事情是定义模板,以便它可以利用分页。以下代码显示了利用分页功能修改的模板代码:

<table>
{% for user in users %}
  <tr>
    <td class="user-data-column">{{ user.username }}</td>
    <td class="user-data-column">{{ user.email }}</td>
    <td class="user-data-column">{{ user.status }}</td>
  </tr>
{% endfor %}
</table>
<a href="{{ url_for('admin_user_list', next_page) }}">Next Page</a>

有了这个,我们就有了视图代码。这个视图代码非常简单,因为我们刚刚通过添加一个href扩展了前面的模板,它将加载下一页的数据。

随着我们的资源被发送到现在优化的页面,接下来我们需要关注的是如何使前端加载越来越多的资源更快。

通过避免 CSS 导入并行获取 CSS

CSS 是任何前端的主要部分之一,它有助于向浏览器提供样式信息,说明浏览器应该如何对从服务器收到的页面进行样式设置。通常,前端可能有许多 CSS 文件与之关联。我们可以在这里实现的一个可能的优化是通过并行获取这些 CSS 文件。

因此,让我们假设我们有以下 CSS 文件集,即main.cssreset.cssresponsive.cssgrid.css,我们的前端需要加载这些文件。我们允许浏览器并行加载所有这些文件的方法是使用 HTML 链接标记而不是 CSS 导入将它们链接到前端,这会导致加载 CSS 文件。。。

绑定 JavaScript

在当前以及未来,我们将不断看到网络带宽的增加,无论是宽带网络还是允许并行更快下载资源的移动网络。但是,对于需要从远程服务器获取的每个资源,仍然存在一些网络延迟,因为每个单独的资源都需要向服务器发出单独的请求。当需要加载大量资源时,以及当用户处于高延迟网络上时,这种延迟会产生影响。

通常,大多数现代 web 应用都大量使用 JavaScript 进行广泛的用途,包括输入验证、动态生成内容等。所有这些功能都被分割成多个文件,其中可能包括一些库、自定义代码等。尽管将所有这些文件拆分为不同的文件有助于并行加载,但有时 JavaScript 文件包含用于在网页上生成动态内容的代码,这可能会阻止网页的呈现,直到成功呈现网页所需的所有必要文件未加载为止。

我们可以减少浏览器加载这些脚本资源所需时间的一种可能方法是将它们捆绑到一个文件中。这允许将所有脚本合并到一个大文件中,浏览器可以在单个请求中获取该文件。虽然这可能会导致用户在第一次访问网站时体验有点慢,但一旦资源被提取和缓存,用户随后加载网页的速度将显著加快。

今天,有很多第三方库可供使用,它们允许我们捆绑这种 JavaScript。让我们以一个名为 Browserify 的简单工具为例,它允许我们绑定 JavaScript 文件。例如,如果我们有多个 JavaScript 文件,例如jquery.jsimage-loader.jsslideshow.jsinput-validator.js,并且我们希望将这些文件与 Browserify 捆绑在一起,那么我们所要做的就是运行以下命令:

browserify jquery.js image-loader.js slideshow.js input-validator.js > bundle.js

此命令将这些 JavaScript 文件创建成一个名为bundle.js的公共文件包,现在可以通过使用以下简单的脚本标记将其包含在我们的 web 应用中:

<script type="text/javascript" src="js/bundle.js"></script>

通过将 JavaScript 捆绑在一个请求中加载,我们可能会看到一些改进,比如在后续页面加载过程中,页面在浏览器中被抓取并显示给用户的速度。现在,让我们来看看另一个有趣的话题,这可能是一个很好的利用,真正影响我们的 Web 应用快速访问网站的速度。

The technique we discussed for bundling of JavaScript can also be a good optimization for the inclusion of CSS files.

利用客户端缓存

缓存长期以来一直被用来加速加载经常使用的资源。例如,大多数现代操作系统都利用缓存来更快地访问最常用的应用。web 浏览器还利用缓存在用户再次访问同一网站时提供对资源的更快访问。这样做是为了避免在没有更改的情况下从远程服务器一次又一次地获取相同的文件,从而减少可能需要的数据传输量,同时还可以缩短页面的呈现时间。

现在,在企业应用的世界中,客户端缓存之类的东西可以证明是非常有用的。这是因为。。。

设置应用范围的缓存控制

因为我们的应用是基于 Flask 的,所以我们可以利用几种简单的机制为我们的应用设置缓存控制。例如,将以下代码添加到我们的bugzot/application.py文件的末尾可以启用站点范围的缓存控制,如下所示:

@app.after_request
def cache_control(response):
  """Implement side wide cache control."""
  response.cache_control.max_age = 300
  response.cache_control.public = True
  return response

在本例中,我们利用 Flask 内置的after_request装饰器钩子,在请求到达 Flask 应用后设置 HTTP 响应头。修饰函数需要一个参数,该参数接受响应类的对象并返回修改后的响应对象。

对于我们的用例,在after_request钩子方法的代码中,我们设置了cache_control.max_age头,它指定了再次从服务器获取内容之前从缓存中提供内容的时间的上限,以及cache_control.public头,它定义了缓存的响应是否可以与多个请求共享。

现在,有时我们可能希望为特定类型的请求设置不同的缓存控制。例如,我们可能不希望为用户配置文件页面设置cache_control.public,以避免向不同的用户显示相同的配置文件数据。我们的应用允许我们非常快地实现这些场景。让我们看一看。

设置请求级缓存控制

在 Flask 中,我们可以在将响应发送回客户端之前修改响应头。这很容易做到。下面的示例显示了一个实现响应特定头控件的简单视图:

from bugzot.application import app, dbfrom bugzot.models import Userfrom flask.views import MethodViewfrom flask import render_template, session, request, make_responseclass UserListView(MethodView):  """User list view for displaying user data in admin panel.  The user list view is responsible for rendering the table of users that are registered  in the application.  """  def get(self):    """HTTP GET handler."""        page = request.args.get('next_page', 1) # get the page number to be displayed users = User.query.paginate(page, ...

利用 web 存储

任何 web 应用开发人员,只要开发过任何涉及一点用户管理的应用,就一定听说过 web Cookie,它本质上提供了一种在客户端存储某些信息的机制。

利用 Cookie 提供了一种简单的方法,通过这种方法,我们可以在客户端维护少量的用户数据,并可以多次读取这些数据,直到 Cookie 过期。但是,尽管处理 cookie 很容易,但除了在客户端维护少量应用状态之外,还有一些限制限制 cookie 被用于任何有用的用途。其中一些限制如下:

  • Cookie 会随每个请求一起传输,因此会添加到随每个请求一起传输的数据中
  • Cookie 允许存储少量数据,最大存储空间限制为 4KB

现在,问题是,如果我们想存储更多的数据,或者我们想避免每次请求都一次又一次地获取同一组存储的数据,我们该怎么办?

为了处理这种情况,HTML 的最新版本 HTML5 提供了各种允许处理客户端 web 存储的功能。与基于 cookies 的机制相比,此 web 存储提供了许多好处,例如:

  • 由于 web 存储直接在客户端可用,因此不需要在每次请求时由服务器一次又一次地将信息发送到客户端
  • web 存储 API 提供的最大存储空间为 10 MB,是 cookie 存储空间的数倍
  • Web 存储提供了将数据存储在本地存储中的灵活性,例如,即使用户再次关闭和打开浏览器,也可以访问数据,或者在每个会话的基础上,存储在 Web 存储中的数据将在会话失效时立即清除,当负责处理用户注销的应用处理程序销毁用户会话时,或者浏览器关闭时

这使得 web 存储成为一个放置数据并避免反复加载数据的吸引人的地方。

对于我们的企业应用,这可以提供很大的灵活性,只在用户浏览器中存储中间步骤的结果,然后仅在填写完所有必需的输入字段后才将结果提交回服务器。

Bugzot 可能更具体的另一个用例是,我们可以将用户正在归档的 bug 报告存储到 web 存储中,并在 bug 报告完成时将其发送到服务器。在这种情况下,用户可以根据自己的意愿灵活地重新编写 bug 报告,而不用担心从头开始。

现在我们知道了 Web 存储提供的好处,让我们来看看我们如何利用 Web 存储。

使用本地 web 存储

HTML5 很容易使用本地 web 存储,因为它提供了许多 API 来与 web 存储交互。所以,不用浪费太多的时间,让我们来看看一个简单的例子,说明我们如何使用本地 Web 存储。为此,我们将创建一个名为localstore.js的简单 JavaScript 文件,其内容如下:

// check if the localStorage is supported by the browser or notif(localStorage) {  // Put some contents inside the local storagelocalStorage.setItem("username", "joe_henry");  localStorage.setItem("uid", "28372");    // Retrieve some contents from the local storage  var user_email = localStorage.getItem("user_email");} else {  alert("The browser does not support local web storage");}

这是。。。

使用会话存储

使用本地存储非常简单,会话存储也不会增加任何复杂性。例如,让我们看看将我们的示例 To0T0 的例子移植到 T1 T1:

// check if the sessionStorage is supported by the browser or not
if(sessionStorage) {
  // Put some contents inside the local storage
sessionStorage.setItem("username", "joe_henry");
  sessionStorage.setItem("uid", "28372");

  // Retrieve some contents from the session storage
  var user_email = sessionStorage.getItem("user_email");
} else {
  alert("The browser does not support session web storage");
}

从本例可以看出,从本地存储转移到会话存储非常容易,两种存储选项都提供了类似的存储 API,唯一的区别是存储中的数据保留了多长时间。

有了关于如何优化前端以提供完全可扩展且响应迅速的企业 web 应用的知识,现在是我们访问企业应用开发的某些方面的时候了,这些方面可以确保我们正在构建的应用是安全的,能够按照预期工作,并且不会带来随机惊喜。

总结

在本章的整个过程中,我们了解了为什么为企业应用优化前端非常重要,以及前端如何影响企业内部应用的使用。然后,我们继续了解哪些问题通常会影响 web 前端的性能,以及我们可以采取哪些可能的解决方案来改进应用前端。这包括减少前端加载的资源量、允许并行加载 CSS、绑定 JavaScript 等等。然后,考虑到企业 web 应用的用例,我们进一步了解了缓存是如何被证明是有用的。一旦我们理解了缓存的概念,我们就进入了这个领域。。。

问题

  1. CDN 的使用如何提高前端性能?
  2. 我们能做些什么让浏览器利用现有的服务器连接来加载资源吗?
  3. 如何从 web 存储中删除特定密钥或清除 web 存储的内容?*