31 KiB
使用 Flask 的 Python Web 应用程序–第二部分
原文:https://realpython.com/python-web-applications-with-flask-part-ii/
请注意:这是来自 Real Python 的 Michael Herman 和来自 De Deo Designs 的 Python 开发者 Sean Vieira 的合作作品。
本系列文章:
- 第一部分:应用程序设置
- 第二部分:设置用户账号、模板、静态文件← 本期文章
- 第三部分:测试(单元和集成)、调试和错误处理
欢迎回到烧瓶跟踪开发系列!对于那些刚刚加入我们的人来说,我们正在实现一个符合这个餐巾纸规范的网络分析应用。对于所有在家的人来说,你可以查看今天的代码:
$ git checkout v0.2
或者,你可以从 Github 的发布页面下载。那些刚刚加入我们的人可能也希望读一读关于存储库结构的注释。
家务管理
快速回顾一下,在我们的上一篇文章中,我们设置了一个基本的应用程序,它可以通过简单的 web 界面或 HTTP 添加站点并记录访问。
今天,我们将添加用户,访问控制,并使用户能够使用跟踪信标从他们自己的网站添加访问。我们还将深入探讨编写模板、保持模型和表单同步以及处理静态文件的一些最佳实践。
从单包装到多包装
上次我们离开应用程序时,目录结构看起来像这样:
flask-tracking/
flask_tracking/
templates/ # Holds Jinja templates
__init__.py # General application setup
forms.py # User data to domain data mappers and validators
models.py # Domain models
views.py # well ... controllers, really.
config.py # Configuration, just like it says on the cover
README.md
requirements.txt
run.py # `python run.py` to bring the application up locally.
为了清楚起见,让我们将现有的forms、models和views移到一个tracking子包中,并为我们的User特定功能创建另一个子包,我们称之为users:
flask_tracking/
templates/
tracking/ # This is the code from Part 1
__init__.py # Create this file - it should be empty.
forms.py
models.py
views.py
users/ # Where we are working today
__init__.py
__init__.py # This is also code from Part 1
这意味着我们需要将flask_tracking/__init__.py的进口从from .views import tracking改为from .tracking.views import tracking。
然后是tracking.models中的数据库设置。我们将把它移到父包(flask_tracking)中,因为数据库管理器将在包之间共享。让我们称那个模块为data:
# flask_tracking/data.py
from flask.ext.sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def query_to_list(query, include_field_names=True):
"""Turns a SQLAlchemy query into a list of data values."""
column_names = []
for i, obj in enumerate(query.all()):
if i == 0:
column_names = [c.name for c in obj.__table__.columns]
if include_field_names:
yield column_names
yield obj_to_list(obj, column_names)
def obj_to_list(sa_obj, field_order):
"""Takes a SQLAlchemy object - returns a list of all its data"""
return [getattr(sa_obj, field_name, None) for field_name in field_order]
然后我们可以更新tracking.models来使用from flask_tracking.data import db和tracking.views来使用from flask_tracking.data import db, query_to_list,我们现在应该有一个工作的多包应用程序。
用户
既然我们已经将我们的应用程序分成了相关功能的独立包,让我们开始处理users包。用户需要能够注册一个帐户,管理他们的帐户,登录和退出。可能会有更多与用户相关的功能(尤其是围绕权限),但为了清楚起见,我们将坚持这些基本功能。
寻求帮助
我们有一个规则来处理依赖——我们添加的每一个依赖都必须很好地解决至少一个难题。维护用户会话有几个有趣的边缘情况,这使它成为依赖关系的绝佳候选。幸运的是,对于这个用例,有一个现成的工具- Flask-Login 。然而,有一件事 Flask-Login 根本不处理——认证。我们可以使用任何我们想要的身份验证方案——从“只需提供用户名”到像 Persona 这样的分布式身份验证方案。让我们保持简单,使用用户名和密码。这意味着我们需要存储一个用户的密码,我们希望对其进行哈希运算。由于正确地散列密码是一个困难的问题,我们将采用另一个依赖项来确保我们的密码被安全地散列。(我们选择 pbdkdf2 是因为在撰写本文时它被认为是安全的,并且包含在 Python 3.3 中——我们只在运行 Python 2 时需要它。)
让我们继续添加:
Flask-Login==0.2.7
backports.pbkdf2==0.1
到我们的requirements.txt文件,然后(确保我们的虚拟环境被激活)我们可以再次运行pip install -r requirements.txt来安装它们。(编译 pbkdf2 的 C 加速时可能会出现一些错误——可以忽略它们)。我们稍后将把它与我们的应用程序集成在一起——首先我们需要设置我们的用户,这样 Flask-Login 就可以使用了。
型号
我们将在users.models中建立我们的User SQLAlchemy 类。我们将只存储用户名、电子邮件地址和密码:
from random import SystemRandom
from backports.pbkdf2 import pbkdf2_hmac, compare_digest
from flask.ext.login import UserMixin
from sqlalchemy.ext.hybrid import hybrid_property
from flask_tracking.data import db
class User(UserMixin, db.Model):
__tablename__ = 'users_user'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
email = db.Column(db.String(120), unique=True)
_password = db.Column(db.LargeBinary(120))
_salt = db.Column(db.String(120))
sites = db.relationship('Site', backref='owner', lazy='dynamic')
@hybrid_property
def password(self):
return self._password
# In order to ensure that passwords are always stored
# hashed and salted in our database we use a descriptor
# here which will automatically hash our password
# when we provide it (i. e. user.password = "12345")
@password.setter
def password(self, value):
# When a user is first created, give them a salt
if self._salt is None:
self._salt = bytes(SystemRandom().getrandbits(128))
self._password = self._hash_password(value)
def is_valid_password(self, password):
"""Ensure that the provided password is valid.
We are using this instead of a ``sqlalchemy.types.TypeDecorator``
(which would let us write ``User.password == password`` and have the incoming
``password`` be automatically hashed in a SQLAlchemy query)
because ``compare_digest`` properly compares **all***
the characters of the hash even when they do not match in order to
avoid timing oracle side-channel attacks."""
new_hash = self._hash_password(password)
return compare_digest(new_hash, self._password)
def _hash_password(self, password):
pwd = password.encode("utf-8")
salt = bytes(self._salt)
buff = pbkdf2_hmac("sha512", pwd, salt, iterations=100000)
return bytes(buff)
def __repr__(self):
return "<User #{:d}>".format(self.id)
唷——这段代码几乎有一半是密码!更糟糕的是,当你读到这篇文章时,我们的_hash_password实现很可能被认为是不完美的(这是加密技术不断变化的本质),但它确实涵盖了所有基本的最佳实践:
- 始终使用每个用户独特的盐。
- 使用带有可调工作单元的键拉伸算法。
- 使用常数时间算法比较哈希。
在与密码无关的注释中,我们在User s 和Site s ( sites = db.relationship('Site', backref='owner', lazy='dynamic'))之间建立了一对多关系,这样我们就可以让用户管理多个站点。
此外,我们正在子类化 Flask-Login 的UserMixin类。Flask-Login 要求User类实现某些方法 ( get_id、is_authenticated等)。)以便它能完成它的工作。UserMixin提供了这些方法的缺省版本,对于我们的目的来说非常好。
积分瓶-登录
现在我们有了一个User,我们可以与 Flask-Login 集成。为了避免循环导入,我们将在名为auth的顶级模块中设置扩展:
# flask_tracking/auth.py
from flask.ext.login import LoginManager
from flask_tracking.users.models import User
login_manager = LoginManager()
login_manager.login_view = "users.login"
# We have not created the users.login view yet
# but that is the name that we will use for our
# login view, so we will set it now.
@login_manager.user_loader
def load_user(user_id):
return User.query.get(user_id)
@login_manager.user_loader向 Flask-Login 注册我们的load_user函数,以便当用户登录 Flask-Login 后返回时,可以从存储在 Flask 的session中的 user_id 加载用户。
最后,我们将login_manager导入到flask_tracking/__init__.py中,并向我们的应用程序对象注册它:
from .auth import login_manager
# ...
login_manager.init_app(app)
视图
接下来,让我们为用户设置视图和控制器功能,以启用注册/登录/注销功能。首先,我们将设置我们的表单:
# flask_tracking/users/forms.py
from flask.ext.wtf import Form
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
from wtforms import fields
from wtforms.validators import Email, InputRequired, ValidationError
from .models import User
class LoginForm(Form):
email = fields.StringField(validators=[InputRequired(), Email()])
password = fields.StringField(validators=[InputRequired()])
# WTForms supports "inline" validators
# which are methods of our `Form` subclass
# with names in the form `validate_[fieldname]`.
# This validator will run after all the
# other validators have passed.
def validate_password(form, field):
try:
user = User.query.filter(User.email == form.email.data).one()
except (MultipleResultsFound, NoResultFound):
raise ValidationError("Invalid user")
if user is None:
raise ValidationError("Invalid user")
if not user.is_valid_password(form.password.data):
raise ValidationError("Invalid password")
# Make the current user available
# to calling code.
form.user = user
class RegistrationForm(Form):
name = fields.StringField("Display Name")
email = fields.StringField(validators=[InputRequired(), Email()])
password = fields.StringField(validators=[InputRequired()])
def validate_email(form, field):
user = User.query.filter(User.email == field.data).first()
if user is not None:
raise ValidationError("A user with that email already exists")
同样,大量的代码,这一次主要是验证用户输入。需要注意的一点是,对于我们的登录表单,当用户通过身份验证时,我们将表单上的User实例公开为form.user(因此我们不必在两个地方进行相同的查询——尽管 SQLAlchemy 在这里会做正确的事情,并且只访问数据库一次)。
最后,我们可以建立我们的观点:
# flask_tracking/users/views.py
from flask import Blueprint, flash, redirect, render_template, request, url_for
from flask.ext.login import login_required, login_user, logout_user
from flask_tracking.data import db
from .forms import LoginForm, RegistrationForm
from .models import User
users = Blueprint('users', __name__)
@users.route('/login/', methods=('GET', 'POST'))
def login():
form = LoginForm()
if form.validate_on_submit():
# Let Flask-Login know that this user
# has been authenticated and should be
# associated with the current session.
login_user(form.user)
flash("Logged in successfully.")
return redirect(request.args.get("next") or url_for("tracking.index"))
return render_template('users/login.html', form=form)
@users.route('/register/', methods=('GET', 'POST'))
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User()
form.populate_obj(user)
db.session.add(user)
db.session.commit()
login_user(user)
return redirect(url_for('tracking.index'))
return render_template('users/register.html', form=form)
@users.route('/logout/')
@login_required
def logout():
# Tell Flask-Login to destroy the
# session->User connection for this session.
logout_user()
return redirect(url_for('tracking.index'))
并将它们导入并注册到我们的应用程序对象:
# flask_tracking/__init__.py
from .users.views import users
# ...
app.register_blueprint(users)
注意在我们的login视图中对load_user的调用。Flask-Login要求我们调用这个函数来激活我们用户的会话(它将为我们管理)。
最后要看的是我们的users/login.html模板:
{% extends "layout.html" %}
{% import "helpers/forms.html" as forms %}
{% block title %}Log into Flask Tracking!{% endblock %}
{% block content %}
{{super()}}
<form action="{{ url_for('users.login', ext=request.args.get('next', '')) }}" method="POST">
{{ forms.render(form) }}
<p><input type="Submit" value="Sign In"></p>
</form>
{% endblock content %}
我们一会儿将讨论layout.html和forms宏——需要注意的关键是,对于表单的action,我们显式地传入了next参数的值:
url_for('users.login', next=request.args.get('next', ''))
这确保了当用户将表单提交给users.login时,next参数可用于我们的重定向代码:
login_user(form.user)
flash("Logged in successfully.")
return redirect(request.args.get("next") or url_for("tracking.index"))
这段代码中有一个微妙的安全漏洞,我们将在下一篇文章中修复它(但是如果您已经发现了它,它会指出来)。
战斗复制
但是等等!你看到我们刚刚重复第三次的模式了吗?(我们实际上至少重复了和两种模式,但是我们今天只打算删除其中一种模式的重复部分)。这部分的register代码:
user = User()
form.populate_obj(user)
db.session.add(user)
db.session.commit()
也在tracking代码中重复多次。让我们使用一个自定义 mixin 来提取数据库会话行为,我们可以从 Flask-Kit 中借用这个自定义 mixin。打开flask_tracking/data并添加以下代码:
class CRUDMixin(object):
__table_args__ = {'extend_existing': True}
id = db.Column(db.Integer, primary_key=True)
@classmethod
def create(cls, commit=True, **kwargs):
instance = cls(**kwargs)
return instance.save(commit=commit)
@classmethod
def get(cls, id):
return cls.query.get(id)
# We will also proxy Flask-SqlAlchemy's get_or_44
# for symmetry
@classmethod
def get_or_404(cls, id):
return cls.query.get_or_404(id)
def update(self, commit=True, **kwargs):
for attr, value in kwargs.iteritems():
setattr(self, attr, value)
return commit and self.save() or self
def save(self, commit=True):
db.session.add(self)
if commit:
db.session.commit()
return self
def delete(self, commit=True):
db.session.delete(self)
return commit and db.session.commit()
CRUDMixin为我们提供了一种更简单的方式来处理四种最常见的模型操作(创建、读取、更新和删除):
def create(cls, commit=True, **kwargs):
pass
def get(cls, id):
pass
def update(self, commit=True, **kwargs):
pass
def delete(self, commit=True):
pass
现在,如果我们将我们的User类更新为子类CRUDMixin:
from flask_tracking.data import CRUDMixin, db
class User(UserMixin, CRUDMixin, db.Model):
然后我们可以使用更清晰的:
user = User.create(**form.data)
征求我们的意见。这使得推理我们的代码在做什么变得更容易,也使得重构变得更容易(因为每段代码处理的关注点更少)。我们也可以更新我们的tracking包的代码来使用相同的方法。
模板
在第一部分中,为了节省时间,我们跳过了对模板的审查。现在让我们花几分钟时间来回顾一下我们用来呈现 HTML 的更有趣的部分。
稍后,我们可能会将这些都分解成一个 RESTful 接口。我们可以使用 JavaScript MVC 框架来处理前端,并向后端发出请求以获取必要的数据,而不是让 Python/Flask/Jinja 提供预先格式化的页面。然后,客户端将向服务器发送请求以创建/注册新站点,并负责在创建新站点和访问时更新视图。视图将负责 REST 接口。
也就是说,由于我们关注的是 Flask,我们现在将使用 Jinja 来提供页面。
布局
首先,看看 layout.html (为了节省空间,我将大部分代码从本文中去掉,但我提供了完整代码的链接):
<title>{% block title %}{{ title }}{% endblock %}</title>
<!-- ... snip ... -->
<h1>{{ self.title() }}</h1>
这个片段展示了我最喜欢的两个技巧——首先,我们有一个包含变量的块(title),这样我们就可以从我们的render_template调用中设置这个值(这样我们就不需要仅仅为了更改标题而创建一个全新的模板)。第二,我们用特殊的self变量重用我们的头块的内容。这意味着,当我们设置title(在子模板中或者通过关键字参数设置为render_template)时,我们提供的文本将在浏览器的标题栏和h1标签中显示*。*
表单管理
值得一看的模板结构的另一部分是我们的宏。对于那些来自 Django 背景的人来说,Jinja 的宏是 Django 的类固醇。例如,我们的form.render宏使得向我们的一个模板添加表单变得非常容易:
{% macro render(form) %}
<dl>
{% for field in form if field.type not in ["HiddenField", "CSRFTokenField"] %}
<dt>{{ field.label }}</dt>
<dd>{{ field }}
{% if field.errors %}
<ul class="errors">
{% for error in field.errors %}
<li>{{error}}</li>
{% endfor %}
</ul>
{% endif %}</dd>
{% endfor %}
</dl>
{{ form.hidden_tag() }}
{% endmacro %}
使用它非常简单:
{% import "helpers/forms.html" as forms %}
<!-- ... snip ... -->
<form action="{{url_for('users.register')}}" method="POST">
{{ forms.render(form) }}
<p><input type="Submit" value="Learn more about your visitors"></p>
</form>
我们可以使用form.render为表单中的每个字段自动生成样本 HTML,而不是一遍又一遍地编写相同的表单 HTML。这样,我们所有的形式看起来和功能都是一样的,如果我们不得不改变它们,我们只需要在一个地方做一次。不要重复自己可以产生非常干净的代码。
重构跟踪应用程序
现在我们已经正确地设置好了所有这些,让我们回过头来重构应用程序的主体:请求跟踪。
在第一部分中,我们构建了请求跟踪器的框架。网站被创建在索引页面上,任何人都可以查看所有可用的网站。只要最终用户自己发送所有信息,Flask-Tracking 就会愉快地存储这些信息。现在,我们有了用户,所以我们想过滤网站的列表。此外,如果我们的应用程序可以从访问者那里获取一些数据,而不是要求应用程序的最终用户自己获取所有数据,那就更好了。
过滤网站
让我们从网站列表开始:
# flask_tracking/tracking/views.py
@tracking.route("/sites", methods=("GET", "POST"))
@login_required
def view_sites():
form = SiteForm()
if form.validate_on_submit():
Site.create(owner=current_user, **form.data)
flash("Added site")
return redirect(url_for(".view_sites"))
query = Site.query.filter(Site.user_id == current_user.id)
data = query_to_list(query)
results = []
try:
# The header row should not be linked
results = [next(data)]
for row in data:
row = [_make_link(cell) if i == 0 else cell
for i, cell in enumerate(row)]
results.append(row)
except StopIteration:
# This happens when a user has no sites registered yet
# Since it is expected, we ignore it and carry on.
pass
return render_template("tracking/sites.html", sites=results, form=form)
_LINK = Markup('<a href="{url}">{name}</a>')
def _make_link(site_id):
url = url_for(".view_site_visits", site_id=site_id)
return _LINK.format(url=url, name=site_id)
从顶部开始,@login_required装饰器由Flask-Login提供。任何试图访问/sites/的未登录用户都将被重定向到登录页面。接下来,我们检查用户是否正在添加一个新站点(form.validate_on_submit检查request.method是否是 POST 并验证表单——如果任何一个先决条件失败,该方法返回False,否则返回True)。如果用户正在创建一个新的站点,我们创建一个新的站点(使用我们的CRUDMixin定义的方法,所以如果您自己对代码进行更改,您将希望确保Site和Visit都从CRUDMixin继承)并重定向回同一个页面。我们在保存新站点后重定向回我们自己,以防止页面刷新导致用户试图添加站点两次。(这称为 Post-Redirect-Get 模式)。
如果你不确定我的意思,试着注释掉return redirect(url_for(".view_sites")),然后提交“添加一个站点”表单,当页面重新加载时,按下F5刷新你的浏览器。恢复重定向后,尝试同样的练习。(当重定向被删除时,浏览器将询问您是否真的要再次提交表单数据——浏览器发出的最后一个请求是创建新站点的帖子。通过重定向,浏览器发出的最后一个请求是 GET 请求,该请求重新加载了view_sites页面。
接下来,如果用户没有创建新的站点(或者如果提供的数据有错误),我们将查询我们的数据库,以查找当前登录用户创建的所有站点。然后我们稍微转换一下我们的列表,将数据库 ID 转换成每个非标题行的 HTML 链接。当您还不知道模板模式是否值得“宏化”时,使用“内联”模板有利于快速原型开发。在我们的例子中,这是带有动作链接的表格的唯一视图,所以我们使用内联模板技术来演示另一种方法。
值得注意的是,我们已经选择使用sites_view来显示站点及其访问和注册站点。如何拆分应用程序完全取决于您。拥有一个view_sites和一个add_site视图,其中前者仅可用于获取请求,后者用于发布也是一种有效的技术。无论哪种技术对你来说更清晰,你都应该更喜欢——只要确保你始终如一。
从访问者那里获取数据
与此同时,add_visit现在有点复杂(尽管它主要是映射代码):
from flask import request
from .geodata import get_geodata
# ... snip ...
@tracking.route("/sites/<int:site_id>/visit", methods=("GET", "POST"))
def add_visit(site_id=None):
site = Site.get_or_404(site_id)
browser = request.headers.get("User-Agent")
url = request.values.get("url") or request.headers.get("Referer")
event = request.values.get("event")
ip_address = request.access_route[0] or request.remote_addr
geodata = get_geodata(ip_address)
location = "{}, {}".format(geodata.get("city"),
geodata.get("zipcode"))
# WTForms does not coerce obj or keyword arguments
# (otherwise, we could just pass in `site=site_id`)
# CSRF is disabled in this case because we will *want*
# users to be able to hit the /sites/{id} endpoint from other sites.
form = VisitForm(csrf_enabled=False,
site=site,
browser=browser,
url=url,
ip_address=ip_address,
latitude=geodata.get("latitude"),
longitude=geodata.get("longitude"),
location=location,
event=event)
if form.validate():
Visit.create(**form.data)
# No need to send anything back to the client
# Just indicate success with the response code
# (204 is "Your request succeeded; I have nothing else to say.")
return '', 204
return jsonify(errors=form.errors), 400
我们已经移除了用户通过表单从我们的网站手动添加访问的能力(因此我们也移除了add_visit上的第二条路线)。我们现在对可以在服务器上导出的数据(浏览器、IP 地址)进行显式映射,然后构造我们的VisitForm,直接传入这些映射值。我们从access_route获取的 IP 地址以防我们在代理之后,此后remote_addr将包含最后一个代理的 IP 地址,这根本不是我们想要的。我们禁用 CSRF 保护,因为我们实际上希望用户能够从其他地方向该端点发出请求。最后,我们知道这个请求是针对哪个站点的,因为我们已经为 URL 设置了<int:site_id>参数。
这并不是这个想法的完美实现。我们没有任何方法来验证该请求是来自我们的跟踪信标的合法请求。有人可以修改 JavaScript 代码或者从另一台服务器提交修改过的请求,我们会很乐意保存它。这很简单,也很容易实现。但是您可能不应该在生产环境中使用这些代码。
get_geodata(ip_address)查询http://freegeoip.net/这样我们可以大致了解请求来自哪里:
from json import loads
from re import compile, VERBOSE
from urllib import urlopen
FREE_GEOIP_URL = "http://freegeoip.net/json/{}"
VALID_IP = compile(r"""
\b
(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
\b
""", VERBOSE)
def get_geodata(ip):
"""
Search for geolocation information using http://freegeoip.net/
"""
if not VALID_IP.match(ip):
raise ValueError('Invalid IPv4 format')
url = FREE_GEOIP_URL.format(ip)
data = {}
try:
response = urlopen(url).read()
data = loads(response)
except Exception:
pass
return data
将此作为geodata.py保存在tracking目录中。
返回到视图,这个视图所做的就是从请求中复制信息并将其存储在数据库中。它用 HTTP 204(无内容)响应来响应请求。这告诉浏览器请求成功了,但是我们不需要花费额外的时间来生成终端用户看不到的内容。
查看访问
我们还为每个站点的访问视图添加了身份验证:
@tracking.route("/sites/<int:site_id>")
@login_required
def view_site_visits(site_id=None):
site = Site.get_or_404(site_id)
if not site.user_id == current_user.id:
abort(401)
query = Visit.query.filter(Visit.site_id == site_id)
data = query_to_list(query)
return render_template("tracking/site.html", visits=data, site=site)
这里唯一真正的变化是,如果用户登录,但不拥有该网站,他们将看到一个授权错误页面,而不是能够查看该网站的访问。
提供追踪访客的方法
最后,我们希望为用户提供一段代码,他们可以将这段代码放在自己的网站上,自动记录访问次数:
{# flask_tracking/templates/tracking/site.html #}
{% block content %}
{{ super() }}
<p>To track visits to this site, simple add the following snippet to the pages that you wish to track:</p>
<code><pre>
<script>
(function() {
var img = new Image();
img.src = "{{ url_for('tracking.add_visit', site_id=site.id, event='PageLoad', _external=true) }}";
})();
</script>
<noscript>
<img src="{{ url_for('tracking.add_visit', site_id=site.id, event='PageLoad', _external=true) }}" width="1" height="1" />
</noscript>
</pre></code>
<h2>Visits for {{ site.base_url }}</h2>
<table>
{{ tables.render(visits) }}
</table>
{% endblock content %}
我们的代码片段非常简单——当页面加载时,我们创建一个新的图像,并将其来源设置为我们的跟踪 URL。浏览器将立即加载指定的图像(这将是什么都没有),我们将在我们的应用程序中记录一个跟踪点击。我们也有一个<noscript>块给那些没有启用 JavaScript 的人。(如果我们真的想跟上时代,我们也可以更新我们的服务器端代码来检查Do Not Track头,只有当用户选择跟踪时才记录访问。)
总结
这个帖子到此为止。我们现在有了用户帐户,并且开始有了易于使用的客户端跟踪 API。我们仍然需要最终确定我们的客户端 API,应用程序的风格和添加报告。
应用程序的代码可以在这里找到。
您的应用程序现在应该如下所示:
展望未来:
- 在第三部分中,我们将探索为我们的应用程序编写测试,记录和调试错误。
- 在第四部分中,我们将进行一些测试驱动的开发,使我们的应用程序能够接受付款并显示简单的报告。
- 在第五部分中,我们将编写一个 RESTful JSON API 供其他人使用。
- 在第六部分中,我们将介绍使用 Fabric 和基本 A/B 特性测试的自动化部署(在 Heroku 上)。
- 最后,在第七部分中,我们将介绍如何用文档、代码覆盖率和质量度量工具来保护您的应用程序。*****
