1. 概览
无服务器迁移站系列 Codelab(自定进度的动手教程)和相关视频旨在帮助 Google Cloud 无服务器开发者通过一次或多次迁移(主要是从旧版服务迁移)来指导他们的应用现代化。这样做可让您的应用更易于移植,并为您提供更多选择和灵活性,使您能够与更多 Cloud 产品集成并访问这些产品,还能更轻松地升级到新的语言版本。虽然最初侧重于最早的 Cloud 用户(主要是 App Engine [标准环境] 开发者),但本系列文章的范围足够广,可涵盖其他无服务器平台(例如 Cloud Functions 和 Cloud Run),或者其他平台(如果适用)。
此模块 15 的 Codelab 介绍了如何将 App Engine blobstore 用法添加到模块 0 中的示例应用。然后,您就可以在模块 16 中将该使用情况迁移到 Cloud Storage 了。
在接下来的实验中
- 添加了对 App Engine Blobstore API/库的使用
- 将用户上传的内容存储到
blobstore服务 - 准备下一步,迁移到 Cloud Storage
所需条件
- 具有有效的 GCP 结算账号的 Google Cloud Platform 项目
- 基本 Python 技能
- 常用 Linux 命令的实践知识
- 具备开发和部署 App Engine 应用的基础知识
- 有效的模块 0 App Engine 应用(从代码库获取)
调查问卷
您将如何使用本教程?
您如何评价使用 Python 的体验?
您如何评价自己在使用 Google Cloud 服务方面的经验水平?
2. 背景
为了从 App Engine Blobstore API 进行迁移,请将该 API 的使用添加到模块 0 中的现有基准 App Engine ndb 应用。示例应用会向用户显示最近的 10 次访问。我们正在修改应用,以提示最终用户上传与其“访问”对应的制品(文件)。如果用户不想这样做,可以选择“跳过”。无论用户做出何种决定,下一页都会呈现与模块 0 中的应用(以及本系列中的许多其他模块)相同的输出。实现了此 App Engine blobstore 集成后,我们可以在下一个(模块 16)Codelab 中将其迁移到 Cloud Storage。
App Engine 提供对 Django 和 Jinja2 模板系统的访问权限,此示例与众不同的一点(除了添加 blobstore 访问权限之外)是,它从在模块 0 中使用 Django 切换到在模块 15 中使用 Jinja2。对 App Engine 应用进行现代化改造的关键一步是将 Web 框架从 webapp2 迁移到 Flask。后者使用 Jinja2 作为其默认模板系统,因此我们开始朝着这个方向发展,在保持 webapp2 以便访问 Blobstore 的同时实现 Jinja2。由于 Flask 默认使用 Jinja2,这意味着在模块 16 中,无需对模板进行任何更改。
3. 设置/准备工作
在开始学习本教程的主要部分之前,请设置项目、获取代码并部署基准应用,以便从工作代码开始。
1. 设置项目
如果您已部署模块 0 应用,我们建议您重复使用同一项目(和代码)。或者,您可以创建一个全新的项目或重复使用另一个现有项目。确保该项目具有有效的结算账号,并且已启用 App Engine。
2. 获取基准示例应用
此 Codelab 的前提条件是拥有有效的工作模块 0 示例应用。如果您没有此模块,可以从模块 0 的“START”文件夹(下面的链接)中获取。本 Codelab 将引导您完成每个步骤,最终获得类似于模块 15“FINISH”文件夹中的代码。
模块 0 启动文件的目录应如下所示:
$ ls README.md index.html app.yaml main.py
3. (重新)部署基准应用
您现在需要执行的剩余预处理步骤:
- 重新熟悉
gcloud命令行工具 - 使用
gcloud app deploy重新部署示例应用 - 确认应用在 App Engine 上运行没有任何问题
成功执行完这些步骤并看到您的 Web 应用正常运行(输出类似于下方所示)后,您就可以为应用添加缓存使用功能了。

4. 更新配置文件
app.yaml
应用配置没有重大变化,但如前所述,我们正在从 Django 模板(默认)迁移到 Jinja2,因此为了进行切换,用户应指定 App Engine 服务器上可用的最新版 Jinja2,方法是将它添加到 app.yaml 的内置第三方库部分。
之前:
runtime: python27
threadsafe: yes
api_version: 1
handlers:
- url: /.*
script: main.app
通过添加新的 libraries 部分(如您在此处看到的),修改 app.yaml 文件:
升级后:
runtime: python27
threadsafe: yes
api_version: 1
handlers:
- url: /.*
script: main.app
libraries:
- name: jinja2
version: latest
无需更新其他配置文件,接下来我们来更新应用文件。
5. 修改应用文件
导入和 Jinja2 支持
main.py 的第一组更改包括添加 Blobstore API 的使用,以及将 Django 模板替换为 Jinja2。具体变化如下:
os模块的用途是创建指向 Django 模板的文件路径名。由于我们改用 Jinja2 来处理此问题,因此不再需要使用os以及 Django 模板渲染器google.appengine.ext.webapp.template,所以我们移除了它们。- 导入 Blobstore API:
google.appengine.ext.blobstore - 导入原始
webapp框架中的 Blobstore 处理程序,这些处理程序在webapp2中不可用:google.appengine.ext.webapp.blobstore_handlers - 从
webapp2_extras软件包导入 Jinja2 支持
之前:
import os
import webapp2
from google.appengine.ext import ndb
from google.appengine.ext.webapp import template
通过将 main.py 中的当前导入部分替换为以下代码段,实现上述列表中的更改。
升级后:
import webapp2
from webapp2_extras import jinja2
from google.appengine.ext import blobstore, ndb
from google.appengine.ext.webapp import blobstore_handlers
在导入项之后,添加一些样板代码以支持使用 Jinja2,如 webapp2_extras 文档中所定义。以下代码段使用 Jinja2 功能封装了标准 webapp2 请求处理程序类,因此请将此代码块添加到 main.py 中的导入项之后:
class BaseHandler(webapp2.RequestHandler):
'Derived request handler mixing-in Jinja2 support'
@webapp2.cached_property
def jinja2(self):
return jinja2.get_jinja2(app=self.app)
def render_response(self, _template, **context):
self.response.write(self.jinja2.render_template(_template, **context))
添加 Blobstore 支持
与本系列中的其他迁移不同,在此示例中,我们对用户体验进行了大幅更改,而其他迁移则在不(大幅)更改用户体验的情况下,保持示例应用的功能或输出完全相同(或几乎相同)。我们不会立即注册新访问记录,然后显示最近的 10 条访问记录,而是会更新应用,要求用户提供文件制品来注册其访问记录。然后,最终用户可以上传相应的文件,也可以选择“跳过”而不上传任何内容。完成此步骤后,系统会显示“最近访问过的网站”页面。
此更改允许我们的应用使用 Blobstore 服务在“最近访问过的网页”页面上存储(并可能在稍后呈现)该图片或其他文件类型。
更新数据模型并实现其使用
我们正在存储更多数据,具体来说,就是更新数据模型以存储上传到 Blobstore 的文件的 ID(称为“BlobKey”),并添加一个引用以将该 ID 保存到 store_visit() 中。由于此额外数据会在查询时与其他所有数据一起返回,因此 fetch_visits() 保持不变。
以下是更新前后的对比,其中包含 file_blob(一种 ndb.BlobKeyProperty):
之前:
class Visit(ndb.Model):
'Visit entity registers visitor IP address & timestamp'
visitor = ndb.StringProperty()
timestamp = ndb.DateTimeProperty(auto_now_add=True)
def store_visit(remote_addr, user_agent):
'create new Visit entity in Datastore'
Visit(visitor='{}: {}'.format(remote_addr, user_agent)).put()
def fetch_visits(limit):
'get most recent visits'
return Visit.query().order(-Visit.timestamp).fetch(limit)
升级后:
class Visit(ndb.Model):
'Visit entity registers visitor IP address & timestamp'
visitor = ndb.StringProperty()
timestamp = ndb.DateTimeProperty(auto_now_add=True)
file_blob = ndb.BlobKeyProperty()
def store_visit(remote_addr, user_agent, upload_key):
'create new Visit entity in Datastore'
Visit(visitor='{}: {}'.format(remote_addr, user_agent),
file_blob=upload_key).put()
def fetch_visits(limit):
'get most recent visits'
return Visit.query().order(-Visit.timestamp).fetch(limit)
下图直观地展示了迄今为止所做的更改:

支持文件上传
功能方面最显著的变化是支持文件上传,无论是提示用户上传文件、支持“跳过”功能,还是呈现与访问对应的文件。所有这些都是画面的一部分。以下是支持文件上传所需的更改:
- 主处理程序
GET请求不再提取最近的访问记录以供显示。而是提示用户进行上传。 - 当最终用户提交要上传的文件或跳过该流程时,表单中的
POST会将控制权传递给从google.appengine.ext.webapp.blobstore_handlers.BlobstoreUploadHandler派生的新UploadHandler。 UploadHandler的POST方法执行上传操作,调用store_visit()注册访问,并触发 HTTP 307 重定向,将用户发送回“/”,其中...- 主处理程序的
POST方法会查询(通过fetch_visits())并显示最近的访问记录。如果用户选择“跳过”,系统不会上传任何文件,但仍会注册相应访问,然后进行相同的重定向。 - 最近访问记录显示内容中包含一个新字段,该字段会向用户显示一个超链接的“查看”(如果上传文件可用)或“无”(否则)。这些更改将在 HTML 模板中实现,同时还会添加上传表单(更多相关信息即将推出)。
- 如果最终用户点击上传了视频的任何访问会话的“查看”链接,系统会向从
google.appengine.ext.webapp.blobstore_handlers.BlobstoreDownloadHandler派生的新ViewBlobHandler发出GET请求,从而呈现文件(如果文件是图片,则在浏览器中呈现,如果不是,则提示下载),或者在找不到文件时返回 HTTP 404 错误。 - 除了新的处理程序类对以及用于向其发送流量的新路由对之外,主处理程序还需要一个新的
POST方法来接收上述 307 重定向。
在这些更新之前,模块 0 应用仅包含一个具有 GET 方法的主处理程序和一个路由:
之前:
class MainHandler(webapp2.RequestHandler):
'main application (GET) handler'
def get(self):
store_visit(self.request.remote_addr, self.request.user_agent)
visits = fetch_visits(10)
tmpl = os.path.join(os.path.dirname(__file__), 'index.html')
self.response.out.write(template.render(tmpl, {'visits': visits}))
app = webapp2.WSGIApplication([
('/', MainHandler),
], debug=True)
实现这些更新后,现在有三个处理程序:1) 具有 POST 方法的上传处理程序;2) 具有 GET 方法的“查看 blob”下载处理程序;3) 具有 GET 和 POST 方法的主处理程序。进行这些更改后,应用的其余部分现在应如下所示。
升级后:
class UploadHandler(blobstore_handlers.BlobstoreUploadHandler):
'Upload blob (POST) handler'
def post(self):
uploads = self.get_uploads()
blob_id = uploads[0].key() if uploads else None
store_visit(self.request.remote_addr, self.request.user_agent, blob_id)
self.redirect('/', code=307)
class ViewBlobHandler(blobstore_handlers.BlobstoreDownloadHandler):
'view uploaded blob (GET) handler'
def get(self, blob_key):
self.send_blob(blob_key) if blobstore.get(blob_key) else self.error(404)
class MainHandler(BaseHandler):
'main application (GET/POST) handler'
def get(self):
self.render_response('index.html',
upload_url=blobstore.create_upload_url('/upload'))
def post(self):
visits = fetch_visits(10)
self.render_response('index.html', visits=visits)
app = webapp2.WSGIApplication([
('/', MainHandler),
('/upload', UploadHandler),
('/view/([^/]+)?', ViewBlobHandler),
], debug=True)
我们刚刚添加的这段代码中有几个关键调用:
- 在
MainHandler.get中,有一个对blobstore.create_upload_url的调用。此调用会生成表单要POST到的网址,并调用上传处理程序将文件发送到 Blobstore。 - 在
UploadHandler.post中,有一个对blobstore_handlers.BlobstoreUploadHandler.get_uploads的调用。这才是真正的神奇之处,它会将文件放入 Blobstore 中,并返回该文件的唯一且持久的 ID,即BlobKey。 - 在
ViewBlobHandler.get中,使用文件的BlobKey调用blobstore_handlers.BlobstoreDownloadHandler.send会导致系统提取该文件并将其转发给最终用户的浏览器
这些调用代表了对添加到应用中的功能的大部分访问。下图直观地展示了对 main.py 的第二组也是最后一组更改:

更新 HTML 模板
对主应用的一些更新会影响应用的用户界面 (UI),因此需要在 Web 模板中进行相应的更改,实际上是两项更改:
- 需要一个包含 3 个输入元素的文件上传表单:一个文件和一对分别用于文件上传和跳过的提交按钮。
- 更新了最近访问的输出,为有相应文件上传的访问添加了“查看”链接,否则添加“无”。
之前:
<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<body>
<h1>VisitMe example</h1>
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
<li>{{ visit.timestamp.ctime }} from {{ visit.visitor }}</li>
{% endfor %}
</ul>
</body>
</html>
实现上述列表中的更改,以构成更新后的模板:
升级后:
<!doctype html>
<html>
<head>
<title>VisitMe Example</title>
<body>
<h1>VisitMe example</h1>
{% if upload_url %}
<h3>Welcome... upload a file? (optional)</h3>
<form action="{{ upload_url }}" method="POST" enctype="multipart/form-data">
<input type="file" name="file"><p></p>
<input type="submit"> <input type="submit" value="Skip">
</form>
{% else %}
<h3>Last 10 visits</h3>
<ul>
{% for visit in visits %}
<li>{{ visit.timestamp.ctime() }}
<i><code>
{% if visit.file_blob %}
(<a href="/view/{{ visit.file_blob }}" target="_blank">view</a>)
{% else %}
(none)
{% endif %}
</code></i>
from {{ visit.visitor }}
</li>
{% endfor %}
</ul>
{% endif %}
</body>
</html>
此图片说明了对 index.html 的必要更新:

最后,Jinja2 倾向于将模板放在 templates 文件夹中,因此请创建该文件夹并将 index.html 移到其中。完成此最后一步后,您就完成了向模块 0 示例应用添加 Blobstore 使用所需的所有更改。
(可选)Cloud Storage“增强”
Blobstore 存储最终演变为 Cloud Storage 本身。这意味着 Blobstore 上传内容会显示在 Cloud 控制台中,特别是 Cloud Storage 浏览器中。问题在于“在哪里”。答案是您的 App Engine 应用的默认 Cloud Storage 存储分区。其名称是 App Engine 应用的完整域名,即 PROJECT_ID.appspot.com。这非常方便,因为所有项目 ID 都是唯一的,对吧?
对示例应用所做的更新会将上传的文件放入该存储分区中,但开发者可以选择更具体的位置。默认存储分区可通过 google.appengine.api.app_identity.get_default_gcs_bucket_name() 以编程方式访问,如果您想访问此值(例如,将其用作整理上传文件的文件名前缀),则需要进行新的导入。例如,按文件类型排序:

例如,如需为图片实现类似功能,您需要编写如下代码,并添加一些用于检查文件类型以选择所需存储分区名称的代码:
ROOT_BUCKET = app_identity.get_default_gcs_bucket_name()
IMAGE_BUCKET = '%s/%s' % (ROOT_BUCKET, 'images')
您还可以使用 Python 标准库 imghdr 模块等工具来验证上传的图片,以确认图片类型。最后,您可能需要限制上传文件的大小,以防出现恶意行为者。
假设所有这些都已完成。如何更新应用以支持指定上传文件的存储位置?关键在于调整 MainHandler.get 中对 blobstore.create_upload_url 的调用,通过添加 gs_bucket_name 参数来指定上传到 Cloud Storage 的所需位置,如下所示:
blobstore.create_upload_url('/upload', gs_bucket_name=IMAGE_BUCKET))
由于这是一项可选更新(如果您想指定上传文件的存储位置),因此它不属于代码库中的 main.py 文件。不过,代码库中提供了一个名为 main-gcs.py 的替代方案供您查看。main-gcs.py 中的代码会将上传内容存储在“根”存储分区 (PROJECT_ID.appspot.com) 中,而不是使用单独的存储分区“文件夹”,这与 main.py 类似,但如果您要将示例派生为更复杂的内容(如本部分中所述),则它会提供您所需的基架。下图展示了 main.py 和 main-gcs.py 之间的“差异”。

6. 总结/清理
本部分将通过部署应用来结束此 Codelab,并验证应用是否按预期运行以及是否在任何反映的输出中正常运行。应用验证完成后,执行任何清理步骤,并考虑后续步骤。
部署并验证应用
使用 gcloud app deploy 重新部署应用,并确认应用按预期运行,但用户体验 (UX) 与模块 0 应用不同。现在,您的应用中有两个不同的界面,第一个是访问文件上传表单提示:
然后,最终用户可以上传文件并点击“提交”,也可以点击“跳过”而不上传任何内容。无论哪种情况,结果都是最新的访问屏幕,现在在访问时间戳和访问者信息之间添加了“查看”链接或“无”:

恭喜您完成本 Codelab,为模块 0 示例应用添加了对 App Engine Blobstore 的使用。您的代码现在应与 FINISH(模块 15)文件夹中的内容一致。该文件夹中还存在替代 main-gcs.py。
清理
常规
如果您暂时不想继续操作,建议您停用 App Engine 应用,以免产生结算费用。不过,如果您想进一步测试或实验,App Engine 平台有免费配额,因此只要您不超过该使用层级,就不会产生费用。这是计算费用,但相关 App Engine 服务也可能会产生费用,因此请查看其价格页面了解详情。如果此迁移涉及其他 Cloud 服务,则这些服务会单独计费。在任何一种情况下,如果适用,请参阅下文中的“本 Codelab 特有的问题”部分。
为了完全公开透明,我们在此说明,部署到 Google Cloud 无服务器计算平台(例如 App Engine)会产生少量 build 和存储费用。Cloud Build 和 Cloud Storage 都有各自的免费配额。存储该图片会占用部分配额。不过,您可能居住在没有此类免费层的地区,因此请注意存储空间用量,以尽可能减少潜在费用。您应查看的特定 Cloud Storage“文件夹”包括:
console.cloud.google.com/storage/browser/LOC.artifacts.PROJECT_ID.appspot.com/containers/imagesconsole.cloud.google.com/storage/browser/staging.PROJECT_ID.appspot.com- 上述存储链接取决于您的
PROJECT_ID和 *LOC*ation,例如,如果您的应用托管在美国,则为“us”。
另一方面,如果您不打算继续学习此应用或其他相关迁移 Codelab,并且想要彻底删除所有内容,请关闭您的项目。
此 Codelab 特有的
以下列出的服务是此 Codelab 特有的。如需了解详情,请参阅各个产品的文档:
- App Engine Blobstore 服务属于存储的数据配额和限制,因此请查看该页面以及旧版捆绑服务的价格页面。
- App Engine Datastore 服务由 Cloud Datastore(Datastore 模式的 Cloud Firestore)提供,后者也提供免费层级;如需了解详情,请参阅其价格页面。
后续步骤
下一个需要考虑的逻辑迁移在模块 16 中介绍,该模块向开发者展示了如何从 App Engine Blobstore 服务迁移到使用 Cloud Storage 客户端库。升级的好处包括能够访问更多 Cloud Storage 功能,熟悉适用于 App Engine 以外的应用(无论是在 Google Cloud、其他云还是本地)的客户端库。如果您认为自己不需要 Cloud Storage 提供的所有功能,或者担心其对费用的影响,可以继续使用 App Engine Blobstore。
除了模块 16 之外,还有许多其他可能的迁移,例如 Cloud NDB 和 Cloud Datastore、Cloud Tasks 或 Cloud Memorystore。此外,还有跨产品迁移到 Cloud Run 和 Cloud Functions 的功能。迁移代码库包含所有代码示例,可将您链接到所有可用的 Codelab 和视频,还提供有关应考虑哪些迁移以及任何相关迁移“顺序”的指南。
7. 其他资源
Codelab 问题/反馈
如果您发现本 Codelab 存在任何问题,请先搜索您的问题,然后再提交。用于搜索和创建新问题的链接:
迁移时可参考的资源
下表中提供了指向模块 0(开始)和模块 15(完成)的 Repo 文件夹的链接。您还可以从所有 App Engine Codelab 迁移的代码库中访问这些示例,您可以克隆该代码库或下载 ZIP 文件。
Codelab | Python 2 | Python 3 |
模块 0 | 不适用 | |
第 15 模块(本 Codelab) | 不适用 |
在线资源
以下是一些可能与本教程相关的在线资源:
App Engine
- App Engine Blobstore 服务
- App Engine 存储的数据的配额和限制
- App Engine 文档
- Python 2 App Engine(标准环境)运行时
- 在 Python 2 App Engine 上使用 App Engine 内置库
- App Engine 价格和配额信息
- 第二代 App Engine 平台发布 (2018)
- 比较第一代和第二代平台
- 对旧版运行时的长期支持
- 文档迁移示例代码库
- 社区提供的迁移示例代码库
Google Cloud
- 在 Google Cloud Platform 上使用 Python 应用
- Google Cloud Python 客户端库
- Google Cloud“始终免费”层级
- Google Cloud SDK(gcloud 命令行工具)
- 所有 Google Cloud 文档
Python
视频
许可
此作品已获得 Creative Commons Attribution 2.0 通用许可授权。