65.9K
CodeProject 正在变化。 阅读更多。
Home

Google App Engine - Python 教程

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.25/5 (3投票s)

2014年11月26日

CC (Attr 3U)

24分钟阅读

viewsIcon

24228

在本教程结束时,您将实现一个可工作的应用程序,一个简单的留言簿,允许用户将消息发布到公共留言板。

引言

欢迎使用 Google App Engine!创建 App Engine 应用程序很容易,只需几分钟。而且它免费开始:立即上传您的应用程序并与用户分享,无需付费,无需承诺。

Google App Engine 应用程序可以用 Python 2.7、Java、Go 或 PHP 编程语言编写。本教程涵盖 Python 2.7。如果您希望使用 Java、Go 或 PHP 构建应用程序,请参阅 JavaGoPHP 指南。

在本教程中,您将学习如何

  • 使用 Python 构建 App Engine 应用程序
  • 使用 webapp2 Web 应用程序框架
  • 将 App Engine 数据存储区与 Python 建模 API 结合使用
  • 将 App Engine 应用程序与 Google 帐户集成以进行用户身份验证
  • Jinja2 模板与您的应用程序结合使用
  • 将您的应用程序上传到 App Engine

在本教程结束时,您将实现一个可工作的应用程序,一个简单的留言簿,允许用户将消息发布到公共留言板。

运行/修改

在学习本教程时,您会看到允许您在浏览器中直接在 Google Cloud Playground 中测试代码示例的按钮。这些按钮标有“运行/修改”。您甚至可以更改代码并实时查看结果!

设置

在我们继续之前,您需要下载 Google App Engine Python SDK,其中包括一个模拟 App Engine 环境的 Web 服务器应用程序,以及用于将您的应用程序部署到 App Engine 生产环境的工具。按照您操作系统的说明进行操作,然后回到这里,以便我们开始!

Hello, World!

让我们从实现一个显示短消息的小应用程序开始。

创建简单的请求处理程序

创建一个名为 helloworld 的目录。此应用程序的所有文件都位于此目录中。

helloworld 目录中,创建一个名为 helloworld.py 的文件,并为其提供以下内容

import webapp2

class MainPage(webapp2.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.write('Hello, World!')

application = webapp2.WSGIApplication([
    ('/', MainPage),
], debug=True)

此 Python 脚本通过描述内容和消息 Hello, World! 的 HTTP 标头响应请求。

注意: 确保将您创建的文件保存为纯文本。否则可能会遇到错误。

创建配置文件

App Engine 应用程序有一个名为 app.yaml 的配置文件。除其他外,此文件描述了哪些处理程序脚本应用于哪些 URL。

helloworld 目录中,创建一个名为 app.yaml 的文件,其内容如下

import webapp2

class MainPage(webapp2.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.write('Hello, World!')

application = webapp2.WSGIApplication([
    ('/', MainPage),
], debug=True)

从上到下,此配置文件描述了此应用程序的以下内容

  • 应用程序标识符是 your-app-id。App Engine 上的每个新应用程序都有一个唯一的应用程序标识符。您将在下一步注册应用程序时选择应用程序的标识符。在此之前,您可以将此处的值设置为 your-app-id,因为此值在本地开发时并不重要。
  • 这是此应用程序代码的第 1 版本号。如果您在上传新版本的应用程序软件之前调整此版本号,App Engine 将保留以前的版本,并允许您使用管理控制台回滚到以前的版本。
  • 此代码在 python27 运行时环境,API 版本 1 中运行。未来可能会支持其他运行时环境和语言。
  • 此应用程序是 threadsafe 的,因此同一个实例可以处理多个并发请求。线程安全是一项高级功能,如果您的应用程序没有专门设计为线程安全的,可能会导致不稳定行为。
  • 所有路径与正则表达式 /.*(所有 URL)匹配的 URL 请求都应由 helloworld 模块中的 application 对象处理。

此文件的语法是 YAML。有关配置选项的完整列表,请参阅 app.yaml 参考

测试应用程序

有了处理程序脚本和将所有 URL 映射到处理程序的配置文件,应用程序就完成了。您现在可以使用 App Engine SDK 中包含的 Web 服务器对其进行测试。

如果您正在使用 Google App Engine Launcher,您可以通过选择 文件 菜单,添加现有应用程序...,然后选择 helloworld 目录来设置应用程序。在应用程序列表中选择该应用程序,单击“运行”按钮启动应用程序,然后单击“浏览”按钮查看它。单击“浏览”只是在您的默认 Web 浏览器中加载(或重新加载)https://:8080/

如果您没有使用 Google App Engine Launcher,请使用以下命令启动 Web 服务器,并为其提供 helloworld 目录的路径

<path-to-Python-SDK>/dev_appserver.py helloworld/

Web 服务器现在正在运行,监听 8080 端口的请求。您可以通过在 Web 浏览器中访问以下 URL 来测试应用程序

有关运行开发 Web 服务器的更多信息,包括如何更改其使用的端口,请参阅 开发 Web 服务器参考,或使用选项 --help 运行命令。

迭代开发

在开发应用程序时,您可以让 Web 服务器保持运行。Web 服务器知道监视源文件中的更改并在必要时重新加载它们。

现在尝试一下:让 Web 服务器保持运行,然后编辑 helloworld.pyHello, World! 更改为其他内容。重新加载 https://:8080/ 或在 Google App Engine Launcher 中单击“浏览”以查看更改。

要关闭 Web 服务器,请确保终端窗口处于活动状态,然后按 Control-C(或控制台的相应“中断”键),或在 Google App Engine Launcher 中单击“停止”。

您可以让 Web 服务器在教程的其余部分保持运行。如果您需要停止它,可以通过运行上述命令再次重新启动它。

Web Server Gateway Interface (WSGI) 标准很简单,但手动编写所有使用它的代码会很麻烦。Web 应用程序框架为您处理这些细节,因此您可以将开发精力集中在应用程序的功能上。Google App Engine 支持任何用纯 Python 编写并支持 WSGI 的框架,包括 DjangoCherryPyPylonsweb.pyweb2py。您可以通过将框架代码复制到应用程序目录中,将其与应用程序代码捆绑在一起。

App Engine 包含一个简单的 Web 应用程序框架,称为 webapp2webapp2 框架已经安装在 App Engine 环境和 SDK 中,因此您无需将其与应用程序代码捆绑即可使用它。我们将在本教程的其余部分使用 webapp2

解释 webapp2 框架

Web Server Gateway Interface (WSGI) 标准很简单,但手动编写所有使用它的代码会很麻烦。Web 应用程序框架为您处理这些细节,因此您可以将开发精力集中在应用程序的功能上。Google App Engine 支持任何用纯 Python 编写并支持 WSGI 的框架,包括 DjangoCherryPyPylonsweb.pyweb2py。您可以通过将框架代码复制到应用程序目录中,将其与应用程序代码捆绑在一起。

App Engine 包含一个简单的 Web 应用程序框架,称为 webapp2webapp2 框架已经安装在 App Engine 环境和 SDK 中,因此您无需将其与应用程序代码捆绑即可使用它。我们将在本教程的其余部分使用 webapp2

你好,webapp2!

webapp2 应用程序有两个部分

  • 一个或多个 RequestHandler 类,它们处理请求并构建响应
  • 一个 WSGIApplication 实例,它根据 URL 将传入请求路由到处理程序

让我们再看看我们的友好问候应用程序

import webapp2

class MainPage(webapp2.RequestHandler):
    def get(self):
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.write('Hello, World!')

application = webapp2.WSGIApplication([
    ('/', MainPage),
], debug=True)

webapp2 的作用

此代码定义了一个请求处理程序 MainPage,映射到根 URL (/)。当 webapp2 收到对 URL / 的 HTTP GET 请求时,它会实例化 MainPage 类并调用实例的 get 方法。在方法内部,可以使用 self.request 获取有关请求的信息。通常,该方法会设置 self.response 上的属性以准备响应,然后退出。webapp2 根据 MainPage 实例的最终状态发送响应。

应用程序本身由 webapp2.WSGIApplication 实例表示。传递给其构造函数的参数 debug=true 告诉 webapp2 如果处理程序遇到错误或引发未捕获的异常,则将堆栈跟踪打印到浏览器输出。您可能希望从应用程序的最终版本中删除此选项。

我们将在本教程后面使用 webapp2 的更多功能。有关 webapp2 的更多信息,请参阅 webapp2 文档

使用用户服务

Google App Engine 提供了几种基于 Google 基础架构的有用服务,应用程序可以使用 SDK 中包含的库访问这些服务。其中一项服务是用户服务,它允许您的应用程序与 Google 用户帐户集成。通过用户服务,您的用户可以使用他们已有的 Google 帐户登录您的应用程序。

让我们使用用户服务来个性化此应用程序的问候语。

使用用户

再次编辑 helloworld/helloworld.py,并将其内容替换为以下内容

from google.appengine.api import users

import webapp2


class MainPage(webapp2.RequestHandler):

    def get(self):
        # Checks for active Google account session
        user = users.get_current_user()

        if user:
            self.response.headers['Content-Type'] = 'text/plain'
            self.response.write('Hello, ' + user.nickname())
        else:
            self.redirect(users.create_login_url(self.request.uri))


application = webapp2.WSGIApplication([
    ('/', MainPage),
], debug=True)

在浏览器中重新加载页面。您的应用程序会将您重定向到 Google 登录页面的本地版本,适合测试您的应用程序。您可以在此屏幕中输入任何您喜欢的用户名,您的应用程序将根据该用户名看到一个伪造的 User 对象。

当您的应用程序在 App Engine 上运行时,用户将被定向到 Google 帐户登录页面,然后在成功登录或创建帐户后重定向回您的应用程序。

用户 API

让我们仔细看看新部分

# Checks for active Google account session
user = users.get_current_user()

如果用户已登录您的应用程序,get_current_user() 返回该用户的 User 对象。否则,它返回 None

if user:
    self.response.headers['Content-Type'] = 'text/plain'
    self.response.write('Hello, ' + user.nickname())

如果用户已登录,则使用与用户帐户关联的昵称显示个性化消息。

else:
    self.redirect(users.create_login_url(self.request.uri))

如果用户尚未登录,请告诉 webapp2 将用户的浏览器重定向到 Google 帐户登录屏幕。重定向包含此页面的 URL (self.request.uri),因此 Google 帐户登录机制将在用户登录或注册新帐户后将用户发送回此处。

有关用户 API 的更多信息,请参阅 用户参考

使用 webapp2 处理表单

如果我们希望用户能够发布自己的问候语,我们需要一种方法来处理用户通过 Web 表单提交的信息。webapp2 框架使处理表单数据变得容易。

从 Hello World 到留言簿

为了准备我们目前创建的 Hello World 应用程序,请进行以下更改

  • 将顶级 helloworld 目录重命名为 guestbook
  • helloworld.py 重命名为 guestbook.py
  • app.yamlhandlers 部分替换为
handlers:
- url: /.*
  script: guestbook.application

使用新的 guestbook 目录重新启动开发服务器。

使用 webapp2 处理 Web 表单

通过将此 libraries 部分添加到您的 app.yaml 中来声明您正在使用 webapp2

libraries:
- name: webapp2
  version: latest

guestbook/guestbook.py 的内容替换为以下内容

import cgi
from google.appengine.api import users
import webapp2

MAIN_PAGE_HTML = """\
<html>
  <body>
    <form action="/sign" method="post">
      <div><textarea name="content" rows="3" cols="60"></textarea></div>
      <div><input type="submit" value="Sign Guestbook"></div>
    </form>
  </body>
</html>
"""

class MainPage(webapp2.RequestHandler):
    def get(self):
        self.response.write(MAIN_PAGE_HTML)

class Guestbook(webapp2.RequestHandler):
    def post(self):
        self.response.write('<html><body>You wrote:<pre>')
        self.response.write(cgi.escape(self.request.get('content')))
        self.response.write('</pre></body></html>')

application = webapp2.WSGIApplication([
    ('/', MainPage),
    ('/sign', Guestbook),
], debug=True)

重新加载页面以查看表单,然后尝试提交消息。

此版本有两个处理程序:MainPage 映射到 URL /,显示一个 Web 表单。Guestbook 映射到 URL /sign,显示 Web 表单提交的数据。

Guestbook 处理程序有一个 post() 方法而不是 get() 方法。这是因为 MainPage 显示的表单使用 HTTP POST 方法 (method="post") 提交表单数据。如果由于某种原因您需要单个处理程序来处理对相同 URL 的 GET 和 POST 操作,您可以在同一个类中为每个操作定义一个方法。

post() 方法的代码从 self.request 获取表单数据。在将其显示回用户之前,它使用 cgi.escape() 将 HTML 特殊字符转义为其字符实体等效项。cgi 是标准 Python 库中的一个模块;有关更多信息,请参阅 cgi 的文档

使用数据存储区

在可伸缩的 Web 应用程序中存储数据可能很棘手。用户在给定时间可能与数十个 Web 服务器中的任何一个进行交互,并且用户的下一个请求可能发送到与上一个请求不同的 Web 服务器。所有 Web 服务器都需要与分布在数十台机器上的数据进行交互,这些机器可能位于世界各地的不同位置。

使用 Google App Engine,您无需担心所有这些。App Engine 的基础架构通过简单的 API 处理所有数据的分发、复制和负载平衡,您还可以获得强大的查询引擎和事务。

App Engine 的数据存储库,即 高可用性数据存储区 (HRD), 使用 Paxos 算法 将数据复制到多个数据中心。数据以称为 实体 的对象写入数据存储区。每个实体都有一个唯一标识它的 。实体可以选择将另一个实体指定为其 父实体; 第一个实体是父实体的 子实体。数据存储区中的实体因此形成一个层次结构空间,类似于文件系统的目录结构。实体的父实体、父实体的父实体等等递归地是其 祖先; 其子实体、子实体的子实体等等是其 后代。 没有父实体的实体是 根实体。

数据存储区在面对灾难性故障时具有极高的弹性,但其一致性保证可能与您所熟悉的有所不同。派生自共同祖先的实体属于同一个 实体组; 共同祖先的键是该组的 父键, 用于标识整个组。对单个实体组的查询,称为 祖先查询 引用父键而不是特定实体的键。实体组是 一致性和事务性 的一个单位:虽然对多个实体组的查询可能会返回陈旧的、最终一致的结果,但那些仅限于单个实体组的查询始终返回最新的、强一致的结果。

本指南中的示例应用程序将相关实体组织到实体组中,并对这些实体组使用祖先查询以返回强一致性结果。在示例代码注释中,我们强调了这种方法可能影响应用程序设计的一些方式。有关更多详细信息,请参阅 为强一致性构建数据

使用数据存储区的完整示例

这是 guestbook/guestbook.py 的新版本,它创建了一个页面页脚,将问候语存储在数据存储区中。本页面的其余部分讨论了此较大示例的摘录,组织在存储问候语和检索它们的主题下。

import cgi
import urllib

from google.appengine.api import users
from google.appengine.ext import ndb

import webapp2

MAIN_PAGE_FOOTER_TEMPLATE = """\
    <form action="/sign?%s" method="post">
      <div><textarea name="content" rows="3" cols="60"></textarea></div>
      <div><input type="submit" value="Sign Guestbook"></div>
    </form>
    <hr>
    <form>Guestbook name:
      <input value="%s" name="guestbook_name">
      <input type="submit" value="switch">
    </form>
    <a href="%s">%s</a>
  </body>
</html>
"""

DEFAULT_GUESTBOOK_NAME = 'default_guestbook'

# We set a parent key on the 'Greetings' to ensure that they are all in the same
# entity group. Queries across the single entity group will be consistent.
# However, the write rate should be limited to ~1/second.

def guestbook_key(guestbook_name=DEFAULT_GUESTBOOK_NAME):
    """Constructs a Datastore key for a Guestbook entity with guestbook_name."""
    return ndb.Key('Guestbook', guestbook_name)

class Greeting(ndb.Model):
    """Models an individual Guestbook entry."""
    author = ndb.UserProperty()
    content = ndb.StringProperty(indexed=False)
    date = ndb.DateTimeProperty(auto_now_add=True)

class MainPage(webapp2.RequestHandler):
    def get(self):
        self.response.write('<html><body>')
        guestbook_name = self.request.get('guestbook_name',
                                          DEFAULT_GUESTBOOK_NAME)

        # Ancestor Queries, as shown here, are strongly consistent with the High
        # Replication Datastore. Queries that span entity groups are eventually
        # consistent. If we omitted the ancestor from this query there would be
        # a slight chance that Greeting that had just been written would not
        # show up in a query.
        greetings_query = Greeting.query(
            ancestor=guestbook_key(guestbook_name)).order(-Greeting.date)
        greetings = greetings_query.fetch(10)

        for greeting in greetings:
            if greeting.author:
                self.response.write(
                        '<b>%s</b> wrote:' % greeting.author.nickname())
            else:
                self.response.write('An anonymous person wrote:')
            self.response.write('<blockquote>%s</blockquote>' %
                                cgi.escape(greeting.content))

        if users.get_current_user():
            url = users.create_logout_url(self.request.uri)
            url_linktext = 'Logout'
        else:
            url = users.create_login_url(self.request.uri)
            url_linktext = 'Login'

        # Write the submission form and the footer of the page
        sign_query_params = urllib.urlencode({'guestbook_name': guestbook_name})
        self.response.write(MAIN_PAGE_FOOTER_TEMPLATE %
                            (sign_query_params, cgi.escape(guestbook_name),
                             url, url_linktext))

class Guestbook(webapp2.RequestHandler):
    def post(self):
        # We set the same parent key on the 'Greeting' to ensure each Greeting
        # is in the same entity group. Queries across the single entity group
        # will be consistent. However, the write rate to a single entity group
        # should be limited to ~1/second.
        guestbook_name = self.request.get('guestbook_name',
                                          DEFAULT_GUESTBOOK_NAME)
        greeting = Greeting(parent=guestbook_key(guestbook_name))

        if users.get_current_user():
            greeting.author = users.get_current_user()

        greeting.content = self.request.get('content')
        greeting.put()

        query_params = {'guestbook_name': guestbook_name}
        self.redirect('/?' + urllib.urlencode(query_params))

application = webapp2.WSGIApplication([
    ('/', MainPage),
    ('/sign', Guestbook),
], debug=True)

guestbook/guestbook.py 替换为以下内容,然后在浏览器中重新加载 https://:8080/。发布几条消息以验证消息是否正确存储和显示。

警告! 在本地运行应用程序中的查询会导致 App Engine 创建或更新 index.yaml。如果 index.yaml 缺失或不完整,当您上传的应用程序执行尚未指定必要索引的查询时,您将看到索引错误。为避免在生产环境中出现索引缺失错误,请始终在上传应用程序之前至少在本地测试一次新查询。有关更多信息,请参阅 Python 数据存储区索引配置

存储提交的问候语

App Engine 包含一个用于 Python 的数据建模 API。它类似于 Django 的数据建模 API,但在幕后使用 App Engine 的可伸缩数据存储区。

要使用数据建模 API,我们的示例导入 google.appengine.ext.ndb 模块

# Imports the NDB data modeling API
from google.appengine.ext import ndb

对于留言簿应用程序,我们希望存储用户发布的问候语。每个问候语都包含作者姓名、消息内容以及消息发布日期和时间,以便我们可以按时间顺序显示消息。以下代码定义了我们的数据模型

class Greeting(ndb.Model):
    """Models an individual Guestbook entry."""
    author = ndb.UserProperty()
    content = ndb.StringProperty(indexed=False)
    date = ndb.DateTimeProperty(auto_now_add=True)

这定义了一个具有三个属性的 Greeting 模型:author 其值是 google.appengine.api.user 对象,content 其值是字符串,以及 date 其值是 datetime.datetime

某些属性构造函数接受参数以进一步配置其行为。为 ndb.StringProperty 构造函数提供 indexed=False 参数表示此属性的值将不被索引。这节省了我们不需要的写入,因为我们从不在查询中使用该属性。为 ndb.DateTimeProperty 构造函数提供 auto_now_add=True 参数将模型配置为自动为新对象分配对象创建时间的 datetime 时间戳,如果应用程序没有另外提供值。有关属性类型及其选项的完整列表,请参阅 NDB 属性

现在我们有了问候语的数据模型,应用程序可以使用该模型创建新的 Greeting 对象并将它们放入数据存储区。Guestbook 处理程序创建新的问候语并将其保存到数据存储区

class Guestbook(webapp2.RequestHandler):
    def post(self):
        # We set the same parent key on the 'Greeting' to ensure each Greeting
        # is in the same entity group. Queries across the single entity group
        # will be consistent. However, the write rate to a single entity group
        # should be limited to ~1/second.
        guestbook_name = self.request.get('guestbook_name',
                                          DEFAULT_GUESTBOOK_NAME)
        greeting = Greeting(parent=guestbook_key(guestbook_name))

        if users.get_current_user():
            greeting.author = users.get_current_user()

        greeting.content = self.request.get('content')
        greeting.put()

        query_params = {'guestbook_name': guestbook_name}
        self.redirect('/?' + urllib.urlencode(query_params))

Guestbook 处理程序创建一个新的 Greeting 对象,然后使用用户发布的数据设置其 authorcontent 属性。Greeting 的父实体是一个 Guestbook 实体。在将 Guestbook 实体设置为另一个实体的父实体之前,无需创建 Guestbook 实体。在此示例中,父实体用作事务和一致性目的的占位符。有关更多信息,请参阅 事务 页面。共享共同 祖先 的对象属于同一个实体组。它不设置 date 属性,因此 date 使用我们上面配置的 auto_now_add=True 自动设置为当前时间。

最后,greeting.put() 将我们的新对象保存到数据存储区。如果我们从查询中获取了此对象,put() 将更新现有对象。由于我们使用模型构造函数创建了此对象,put() 将新对象添加到数据存储区。

因为在高可用性数据存储区中查询仅在实体组内具有强一致性,所以在此示例中,我们通过为每个问候语设置相同的父实体,将一本书的所有问候语分配到同一个实体组。这意味着用户将在写入后立即看到问候语。但是,写入同一实体组的速率限制为每秒写入一次。在设计实际应用程序时,您需要记住这一点。请注意,通过使用 Memcache 等服务,您可以减轻用户在写入后立即跨实体组查询时看不到最新结果的可能性。

检索提交的问候语

App Engine 数据存储区具有用于数据模型的复杂查询引擎。由于 App Engine 数据存储区不是传统的关系数据库,因此查询不使用 SQL 指定。相反,数据通过两种方式查询:通过 数据存储区查询,或使用名为 GQL 的类似 SQL 的查询语言。要访问数据存储区查询的全部功能,我们建议使用数据存储区查询而不是 GQL。

MainPage 处理程序检索并显示以前提交的问候语。数据存储区查询发生在此处

greetings_query = Greeting.query(
    ancestor=guestbook_key(guestbook_name)).order(-Greeting.date)
greetings = greetings_query.fetch(10)

关于数据存储区索引的说明

App Engine 数据存储区中的每个查询都通过一个或多个 索引(将有序属性值映射到实体键的表)进行计算。这就是 App Engine 能够快速提供结果的原因,无论您的应用程序数据存储区的大小如何。许多查询可以从内置索引计算,但对于更复杂的查询,数据存储区需要 自定义索引。如果没有自定义索引,数据存储区就无法高效执行这些查询。

例如,我们上面的留言簿应用程序通过留言簿进行过滤并按日期排序,使用祖先查询和排序顺序。这需要您的应用程序的 index.yaml 文件中指定自定义索引。您可以手动编辑此文件,或者,如本页前面警告框中所述,您可以通过在本地运行应用程序中的查询来自动处理它。一旦索引在 index.yaml 中定义,上传您的应用程序也将上传您的自定义索引信息。

您的 index.yaml 文件中查询的定义如下

indexes:
- kind: Greeting
  ancestor: yes
  properties:
  - name: date
    direction: desc

您可以在 数据存储区索引页面 中阅读有关数据存储区索引的所有信息。您可以在 Python 数据存储区索引配置 中阅读有关 index.yaml 文件的正确规范。

我们现在有一个可用的留言簿应用程序,它使用 Google 帐户验证用户,让他们提交消息,并显示其他用户留下的消息。由于 App Engine 会自动处理缩放,因此随着我们的应用程序变得流行,我们无需重新访问此代码。

此最新版本将 HTML 内容与 MainPage 处理程序的代码混合在一起。这将使更改应用程序的外观变得困难,尤其是当我们的应用程序变得更大更复杂时。让我们使用模板来管理外观,并为 CSS 样式表引入静态文件。

使用模板

嵌入在代码中的 HTML 凌乱且难以维护。最好使用模板系统,其中 HTML 保存在一个单独的文件中,其中包含特殊的语法,用于指示应用程序中的数据显示在哪里。Python 有许多模板系统:EZTCheetahClearSilverQuixoteDjangoJinja2 只是其中几个。您可以通过将您选择的模板引擎与您的应用程序代码捆绑在一起来使用它。

为了您的方便,App Engine 包含 Django 和 Jinja2 模板引擎。

使用 Jinja2 模板

首先修改 guestbook/app.yaml 底部的 libraries 部分

libraries:
- name: webapp2
  version: latest
- name: jinja2
  version: latest

此配置使最新支持的 Jinja2 版本可用于您的应用程序。为避免可能的兼容性问题,严肃的应用程序应使用 实际版本号 而不是 latest

现在修改 guestbook/guestbook.py 顶部的语句

import os
import urllib

from google.appengine.api import users
from google.appengine.ext import ndb

import jinja2
import webapp2


JINJA_ENVIRONMENT = jinja2.Environment(
    loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
    extensions=['jinja2.ext.autoescape'],
    autoescape=True)

MainPage 处理程序替换为以下代码

class MainPage(webapp2.RequestHandler):

    def get(self):
        guestbook_name = self.request.get('guestbook_name',
                                          DEFAULT_GUESTBOOK_NAME)
        greetings_query = Greeting.query(
            ancestor=guestbook_key(guestbook_name)).order(-Greeting.date)
        greetings = greetings_query.fetch(10)

        if users.get_current_user():
            url = users.create_logout_url(self.request.uri)
            url_linktext = 'Logout'
        else:
            url = users.create_login_url(self.request.uri)
            url_linktext = 'Login'

        template_values = {
            'greetings': greetings,
            'guestbook_name': urllib.quote_plus(guestbook_name),
            'url': url,
            'url_linktext': url_linktext,
        }

        template = JINJA_ENVIRONMENT.get_template('index.html')
        self.response.write(template.render(template_values))

最后,在 guestbook 目录中创建一个名为 index.html 的新文件,其内容如下

<!DOCTYPE html>
{% autoescape true %}
<html>
  <body>
    {% for greeting in greetings %}
      {% if greeting.author %}
        <b>{{ greeting.author.nickname() }}</b> wrote:
      {% else %}
       An anonymous person wrote:
      {% endif %}
      <blockquote>{{ greeting.content }}</blockquote>
    {% endfor %}

    <form action="/sign?guestbook_name={{ guestbook_name }}" method="post">
      <div><textarea name="content" rows="3" cols="60"></textarea></div>
      <div><input type="submit" value="Sign Guestbook"></div>
    </form>

    <hr>

    <form>Guestbook name:
      <input value="{{ guestbook_name }}" name="guestbook_name">
      <input type="submit" value="switch">
    </form>

    <a href="{{ url|safe }}">{{ url_linktext }}</a>

  </body>
</html>
{% endautoescape %}

重新加载页面,然后尝试一下。

JINJA_ENVIRONMENT.get_template(name) 接受模板文件的名称,并返回一个模板对象。template.render(template_values) 接受一个值字典,并返回渲染的文本。模板使用 Jinja2 模板语法访问和迭代值,并且可以引用这些值的属性。在许多情况下,您可以直接将数据存储区模型对象作为值传递,并从模板访问其属性。

提示: App Engine 应用程序对项目上传的所有文件、库模块以及其他文件具有只读访问权限。当前工作目录是应用程序根目录,因此 index.html 的路径只是 "index.html"

其他模板语言

此示例使用 Jinja2 完成,但我们在 Google Developers Console 中也有 使用 Flask 和 Bottle 的 App Engine 入门项目

每个 Web 应用程序都通过模板或其他机制从应用程序代码返回动态生成的 HTML。大多数 Web 应用程序还需要提供静态内容,例如图像、CSS 样式表或 JavaScript 文件。为了提高效率,App Engine 对静态文件的处理方式与应用程序源文件和数据文件不同。您可以使用 App Engine 的静态文件功能为此应用程序提供 CSS 样式表。

使用静态文件

与传统的 Web 托管环境不同,Google App Engine 不会直接从应用程序的源目录提供文件,除非配置为这样做。我们将模板文件命名为 index.html,但这并不会自动使该文件在 URL /index.html 处可用。

但在许多情况下,您希望将静态文件直接提供给 Web 浏览器。图像、CSS 样式表、JavaScript 代码、电影和 Flash 动画通常都与 Web 应用程序一起存储并直接提供给浏览器。App Engine 可以直接提供特定文件,而无需您编写自己的处理程序。

使用静态文件

编辑 guestbook/app.yaml 并将其内容替换为以下内容

application: your-app-id
version: 1
runtime: python27
api_version: 1
threadsafe: true

handlers:
- url: /stylesheets
  static_dir: stylesheets

- url: /.*
  script: guestbook.application

libraries:
- name: webapp2
  version: latest
- name: jinja2
  version: latest

新的 handlers 部分为 URL 定义了两个处理程序。当 App Engine 收到以 /stylesheets 开头的 URL 请求时,它会将路径的其余部分映射到 stylesheets 目录中的文件,如果找到适当的文件,则将文件内容返回给客户端。所有其他 URL 都与 / 路径匹配,并由 guestbook 模块中的 application 对象处理。

默认情况下,App Engine 使用基于文件名扩展名的 MIME 类型提供静态文件。例如,文件名以 .css 结尾的文件将以 text/css 的 MIME 类型提供。您可以在 app.yaml配置处理程序 时,通过使用 mime_type 设置来配置显式 MIME 类型。

URL 处理程序路径模式按照它们在 app.yaml 中出现的顺序,从上到下进行测试。在这种情况下,/stylesheets 模式将在适当路径的 /.* 模式之前匹配。有关 URL 映射和您可以在 app.yaml 中指定的其他选项的更多信息,请参阅 app.yaml 参考

注意: 您可以在静态目录处理程序中指定 http_headers 设置,以在处理程序返回的响应中提供自定义标头。例如,这对于包含支持 CORS 所需的 `Access-Control-Allow-Origin` 标头很有用。有关更多信息,请参阅 静态文件处理程序 下的 http_headers 文档。

创建目录 guestbook/stylesheets/。在此新目录中,创建一个名为 main.css 的新文件,其内容如下

body {
  font-family: Verdana, Helvetica, sans-serif;
  background-color: #DDDDDD;
}

最后,编辑 guestbook/index.html 并在顶部的 <html><body> 标签之间插入以下几行

<head>
  <link type="text/css" rel="stylesheet" href="https://cloud.google.com/stylesheets/main.css" />
</head>

在浏览器中重新加载页面。新版本使用样式表。

是时候向全世界展示您完成的应用程序了。

上传您的应用程序

您使用 Google Developers Console 创建和管理 App Engine 应用程序。为您的应用程序注册应用程序 ID 后,您可以使用 SDK 中提供的命令行工具 appcfg.py 将其上传到您的网站。或者,如果您使用 Google App Engine Launcher,您可以通过单击“部署”按钮上传您的应用程序。

注意: 应用程序 ID 必须以字母开头。一旦您注册了应用程序 ID,您可以将其删除,但删除后不能重新注册相同的应用程序 ID。如果您目前不想注册 ID,可以跳过这些后续步骤。

注意: 如果您拥有 App Engine Premier 帐户,您可以指定您的新应用程序应位于欧盟而不是美国。没有 Premier 帐户的开发人员需要 填写此表格启用计费,以便应用程序应位于欧盟。

在欧盟托管应用程序特别有用,如果您的用户离欧洲比美国更近。网络延迟更低,并且最终用户内容将静止存储在欧盟。您必须在注册应用程序时通过单击“位置选项”部分中的“编辑”链接来指定此位置;您以后无法更改。

注册应用程序

您可以通过以下 URL 从 Developers Console 创建和管理 App Engine 应用程序

https://console.developers.google.com/

Google App Engine Launcher 用户可以通过单击“仪表板”按钮到达此 URL。

使用您的 Google 帐户登录 App Engine。如果您没有 Google 帐户,可以 创建一个 Google 帐户,其中包含电子邮件地址和密码。

注意: 您可能已经使用 Google Developers Console 创建了一个项目。如果是这种情况,您无需创建新应用程序。您的项目有一个标题和一个 ID。在以下说明中,项目 标题和 ID 可以在任何提到 应用程序 标题和 ID 的地方使用。它们是相同的。

要创建新应用程序,请单击“创建应用程序”按钮。按照说明注册应用程序 ID,该名称在此应用程序中是唯一的。如果您选择使用免费的 appspot.com 域名,则应用程序的完整 URL 将是 http://your-app-id.appspot.com/。您还可以为您的应用程序购买顶级域名,或使用您已注册的域名。

注意: 必须使用高可用性数据存储区才能使用 Python 2.7 运行时。这是创建新应用程序时的默认设置。

如果您拥有 App Engine Premier 帐户,您可以指定您的新应用程序应位于欧盟而不是美国。如果您的应用程序用户离欧洲比美国更近,这尤其有用。网络延迟更低,并且最终用户内容将静止存储在欧盟。您必须在注册应用程序时指定此位置;您以后无法更改。单击“位置选项”部分中的“编辑”链接;选择一个位置选项,美国或欧盟。

编辑 app.yaml 文件,然后将 application: 设置的值从 your-app-id 更改为您的注册应用程序 ID。

上传应用程序

要将您完成的应用程序上传到 Google App Engine,请运行以下命令

    appcfg.py update guestbook/

或在 Google App Engine Launcher 中单击 部署,然后在提示符处输入您的 Google 用户名和密码。

如果您使用 Git 版本控制系统,您可以在 Google 云中创建远程存储库,并配置您的开发环境,以便在每次将代码推送到该存储库时部署最新版本。请参阅 使用 Git 推送和部署

http://your-app-id.appspot.com

注意: 数据存储区索引 可能需要一些时间才能生成,然后您的应用程序才能使用。如果索引仍在生成过程中,您在访问应用程序时将收到 NeedIndexError。对于此示例,这是一个暂时性错误,因此如果您最初收到此异常,请稍后尝试。

恭喜!

您已完成本教程。有关此处涵盖主题的更多信息,请参阅 App Engine 文档 的其余部分。

除非另有说明,本页的代码示例根据 Apache 2.0 许可证 授权。

© . All rights reserved.