创建一个应用

从头开始

可以使用 Dashboard 或者直接从文件系统创建应用。在这里,我们将手动创建,因为 Dashboard 的使用已经在其自己的章节中描述过了。

记住,一个应用(app)是一个 Python 模块;因此,它只需要一个文件夹和一个该文件夹中的 __init__.py 文件。

备注

从 Python 3.3 开始,空的 __init__.py 文件不是必须要有的,但它在以后会很有用。

打开命令提示符并切换到到你的 py4web Apps 的主文件夹。输入以下简单的命令来创建一个新的空 myapp 应用:

mkdir apps/myapp
echo '' > apps/myapp/__init__.py

小技巧

对于Windows,你必须使用反斜杠(即 \ ),而不是斜杠。

如果你现在重新启动 py4web 或者在 Dashboard 中按下 “Reload Apps”,py4web 将会找到这个模块并导入它,并且将它识别为一个应用,这仅仅是因为它的位置。默认情况下,py4web 以 lazy watch 模式运行(参见 run 命令行选项),当应用改变时,会自动重新加载应用,这在开发环境中非常有用。在生成环境或调试环境中,最好使用以下命令来运行 py4web:

py4web run apps --watch off

py4web 应用不需要做任何事情。它可能只是一个静态文件或任意代码的容器,其他应用可能想导入和访问。然而,通常大多数应用都是设计为公开静态或动态网页。

静态网页

要公开静态网页,你只需要简单地创建一个 static 子文件夹,那里面的任何文件都会自动发布:

mkdir apps/myapp/static
echo 'Hello World' > apps/myapp/static/hello.txt

新创建的文件将可以在以下地址访问(如果不能访问,请重新加载应用):

http://localhost:8000/myapp/static/hello.txt

注意, static 是 py4web 的一个特殊路径,只有 static 文件夹下的文件才会被提供静态服务(访问)。

重要

py4web 在内部使用了 ombott (One More BOTTle) web server -- 这是 bottlepy 的一个最小且快速的衍生产品。它支持流媒体、部分内容、范围请求,以及 “if-modified-since”(若自指定时间后有修改)功能。这一切都是基于HTTP请求头自动处理的。

动态网页

要创建一个动态页面,你必须创建一个返回页面内容的函数。例如,像下面那样编辑 myapp/__init__.py 地内容

import datetime
from py4web import action

@action('index')
def page():
    return "hello, now is %s" % datetime.datetime.now()

重新加载应用,然后在以下地址访问这个页面:

http://localhost:8000/myapp/index

或者

http://localhost:8000/myapp

(注意,index 是可选的)

不像其他框架,我们不需要导入 myapp 应用的代码,或重新启动包含它的服务器。那是因为 py4web 已经在运行中,并且它可以为多个应用同时提供服务。 py4web 导入我们的代码,然后公开用 @action() 声明的函数。 还要注意,py4web 会在 @action 中声明的 url 路径前自动添加 /myapp (即应用程序的名称)。 这是因为这里会有多个应用,它们可能定义了冲突的路由。在前面加上应用的名称可以消除歧义。但是有一个例外:如果你把应用命名为 _default ,或者你创建了一个从 _defaultmyapp 的符号链接,那么 py4web 不会在前面加上任何前缀。

返回值

py4web 的 actions 应该返回一个字符串或一个字典。如果你返回一个字典,你必须告诉 py4web 如何处理它。默认情况下,py4web 会将它序列化成 json。例如,像下面那样编辑 __init__.py 并在最后添加:

@action('colors')
def colors():
    return {'colors': ['red', 'blue', 'green']}

用下面的 URL 访问这个页面:

http://localhost:8000/myapp/colors

然后返回一个 JSON 对象 {"colors": ["red", "blue", "green"]} 。请注意,我们选择将函数命名为与路由相同的名称。这不是必需的,但这是一个我们经常会遵循的约定。

你可以使用任何模板语言将你的数据转换为字符串。PY4WEB 自带 yatl,稍后将专门用一整章详细介绍。一会儿,下面将提供一个简短的示例。

路由

可以将 URL 中的模式映射到函数的参数中。例如:

@action('color/<name>')
def color(name):
    if name in ['red', 'blue', 'green']:
        return 'You picked color %s' % name
    return 'Unknown color %s' % name

用下面的 URL 访问这个页面:

http://localhost:8000/myapp/color/red

路由模式的语法与 Bottle routes 相同。可以像这样定义一个路由通配符:

  • <name> 或者

  • <name:filter> 或者

  • <name:filter:config>

下面是可用的过滤器(只有 :re 有配置项)

  • :int 匹配(有符号)数字并将其转换为整数。

  • :float 与 :int 类似,但用于十进制小数。

  • :path 以非贪婪方式匹配所有字符,包括斜杠字符,可以用来匹配多个路径段。

  • :re[:exp] 允许你在配置字段中指定一个自定义的正则表达式。匹配的值不会被修改。

与通配符匹配的模式会通过指定的变量名 name 传递给函数。

需要注意的是,ombott 中的路由功能是通过基数树混合路由器实现的。它不受声明顺序影响,并且优先处理静态路由片段而非动态路由片段,因为这是最符合预期的行为

这会带来一些限制,例如,不能存在多个在同一位置包含不同类的型动态片段(如 int、path 类型)的路由。因此,像下面这样的写法是错误的,会导致报错:

@action('color/<code:int>')
def color(code):
    return f'Color code: {code}'

@action('color/<name:path>')
def color(name):
    return f'Color name: {name}'

相反,要实现类似的结果,需要在一个动作中处理所有逻辑:

@action('color/<color_identifier:path>')
def color(color_identifier):
   try:
      msg = f'Color code: {int(color_identifier)}'
   except:
      msg = f'Color name: {color_identifier}'
   return msg

此外,动作装饰器接受一个可选的 method 参数,该参数可以是 HTTP 方法或方法列表:

@action('index', method=['GET','POST','DELETE'])

您可以使用多个装饰器在多个路由下公开相同的函数。

request 对象

你可以从 py4web 中导入 request

from py4web import request

@action('paint')
def paint():
    if 'color' in request.query:
       return 'Painting in %s' % request.query.get('color')
    return 'You did not specify a color'

这个操作( action ) 可以在下面的 URL 中访问:

http://localhost:8000/myapp/paint?color=red

注意,请求对象等同于一个 Bottle request object 。 还有一个附加属性:

request.app_name

你可以使用那个代码来识别应用程序使用的名称和文件夹。

模板

为了使用 yatl 模板,你必须声明它。例如,创建一个文件 apps/myapp/templates/paint.html ,其内容包含:

<html>
 <head>
    <style>
      body {background:[[=color]]}
    </style>
 </head>
 <body>
    <h1>Color [[=color]]</h1>
 </body>
</html>

然后修改 paint 的 action 以使用模板并默认使用绿色。

@action('paint')
@action.uses('paint.html')
def paint():
    return dict(color = request.query.get('color', 'green'))

页面现在将显示颜色名称,它是页面背景的相应颜色。

这里的关键要素是装饰器 @action.uses(...)action.uses 的参数被称为夹具( fixtures )。您可以在一个装饰器中指定多个夹具,也可以改用多个装饰器。夹具( fixtures )是修改动作(action)行为的对象,可能需要根据请求进行初始化,可以过滤动作的输入和输出,并且可能相互依赖(它们的作用范围与 Bottle 插件相似,但它们是按动作声明的,并且它们有一个依赖树,稍后将对此进行解释)

类型最简单的夹具是模板。您简单地指定要用作模板的文件的名称即可。该文件必须遵循 yatl 语法,必须位于应用程序的 templates 文件夹中。action 返回的对象将由模板处理并转换为字符串。

您可以轻松地为其他模板语言定义夹具。这将在后面描述。

一些内置地夹具是:

  • DAL 对象(告诉 py4web 在每次请求时从池中获取数据库连接,并在成功时提交或在失败时回滚)

  • Session 对象(告诉 py4web 在每次请求时解析 cookie 并检索会话,并在更改时保存它)

  • Translator 对象(告诉 py4web 处理 accept-language 标头并确定最佳国际化/多元化规则)

  • Auth 对象(告诉 py4web 应用程序需要访问用户信息)

它们可能相互依赖。例如,Session 可能需要 DAL(数据库连接),而 Auth 可能需要两者。依赖关系将自动处理。

_scaffold 应用程序

大多数时候,你不想从头开始写代码。您还需要遵循这里介绍的一些理智的约定,比如不要将所有代码都放在 __init__.py 中。PY4WEB 提供了一个脚手架(_scaffold)应用程序,其中文件被正确组织,许多有用的对象被预定义。此外,它还向您展示了如何管理用户及其注册。就像建筑工地上的真正脚手架一样,脚手架应用程序可以为您的项目提供某种快速简洁的结构,您可以依靠它来构建真正的项目。

_images/_scaffold.png

通常,您会在 apps 下找到脚手架应用程序,但您可以使用 Dashboard 或手动轻松地创建它的一个新副本。

这里是 _scaffold 应用程序的树形结构:

_images/scaffold_tree.png

脚手架应用程序包含一个更复杂的动作(action)的示例:

from py4web import action, request, response, abort, redirect, URL
from yatl.helpers import A
from . common import db, session, T, cache, auth


@action('welcome', method='GET')
@action.uses('generic.html', session, db, T, auth.user)
def index():
    user = auth.get_user()
    message = T('Hello {first_name}'.format(**user))
    return dict(message=message, user=user)

注意以下内容:

  • requestresponseabort , 由 ombott 定义, 它是 bottlepy 框架的一个小且快速的衍生产品。

  • redirectURL ,与 web2py 中的功能类似。

  • 帮助程序( ADIVSPANIMG 以及其他)必须从 yatl.helpers 导入。 它们的工作方式与在 web2py 中的方式非常相似。

  • dbsessionTcacheauth 是夹具。它们必须在 common.py 中定义。

  • @action.uses(auth.user) 表示此 action 期望通过 auth.get_user() 检索到有效的登录用户。如果不是那种情况,此 action 将重定向到登录页面(也在 common.py 中定义,并使用 Vue.js 的 auth.html 组件)。

当您从脚手架开始时,您可能希望编辑 settings.pytemplatesmodels.pycontrollers.py ,但最好不要在 common.py 中进行任何更改。

在您的 html 中,您可以使用任何您想要的 JS 库,因为 py4web 对您选择的 JS 和 CSS 是无感知的,但有一些例外。 auth.html 使用 vue.js 组件处理用户的注册、登录以及其它事情。因此,如果您想使用相关功能,您不应该删除 vue.js 。

复制 _scaffold 应用

脚手架应用程序非常有用,您很可能会大量使用它作为测试和甚至开发功能齐全的新应用的起点。

最好不要直接在上面工作:始终复制它来创建新应用。您可以通过两种方式来实现:

  • 使用命令行:将整个 apps/_scaffold 文件夹复制到另一个文件夹中(例如 apps/my_app )。然后重新加载 py4web ,它将自动被加载。

  • 使用 Dashboard:在 “Installed Applications” 的上方部分,选择按钮 Create/Upload App 。只需给新应用一个名称,并将 “Scaffold” 作为源进行选择。最后按 Create 按钮, Dashboard 和新应用,都会被重新加。

    _images/dashboard_new_app.png

监视文件的变化

正如在 run 命令行选项 中所描述的, 当其 Python 源文件发生变化时(默认情况下) Py4web 会通过自动重新加载应用程序来简化开发服务器的设置。但实际上,应用程序内部的任何其他文件都可以通过设置 @app_watch_handler 装饰器来被监视。

现在展示两个使用示例。如果您不明白它们,不要担心:这里的关键要点是,即使是非 Python 代码也可以在使用 @app_watch_handler 装饰器时被自动重新加载。

监视 SASS 文件并在编辑时进行编译:

from py4web.core import app_watch_handler
import sass # https://github.com/sass/libsass-python

@app_watch_handler(
    ["static_dev/sass/all.sass",
     "static_dev/sass/main.sass",
     "static_dev/sass/overrides.sass"])
def sass_compile(changed_files):
    print(changed_files) # for info, files that changed, from a list of watched files above
    ## ...
    compiled_css = sass.compile(filename=filep, include_paths=includes, output_style="compressed")
    dest = os.path.join(app, "static/css/all.css")
    with open(dest, "w") as file:
        file.write(compiled)

编辑时验证 javascript 语法:

import esprima # Python implementation of Esprima from Node.js

@app_watch_handler(
    ["static/js/index.js",
     "static/js/utils.js",
     "static/js/dbadmin.js"])
def validate_js(changed_files):
    for cf in changed_files:
        print("JS syntax validation: ", cf)
        with open(os.path.abspath(cf)) as code:
            esprima.parseModule(code.read())

传递给 @app_watch_handler 装饰器的文件路径必须是相对于应用程序的。传递给装饰器的列表中的 Python 文件(即 "*.py" )被忽略,因为它们是默认被监视的。处理程序函数的参数是已更改的文件路径的一个列表。处理程序内部的所有异常都将在终端中打印显示。

域映射应用程序

在生产环境中,通常需要一个 py4web 服务器为多个应用程序提供服务,其中不同的应用程序需要被映射到不同的域。

py4web 可以轻松处理运行多个应用程序,但没有内置机制将域映射到特定应用程序。这种映射需要在 py4web 外部完成,例如使用 nginx 等 web 反向代理。

虽然 nginx 或其他反向代理在生产环境中也可用于处理 SSL termination、缓存和其他用途,但我们在这里只介绍域到 py4web 应用程序的映射。

映射到域 myapp.example.com 的应用程序 myapp 的 nginx 配置示例可能如下:

server {
   listen 80;
   server_name myapp.example.com;
   proxy_http_version 1.1;
   proxy_set_header Host $host;
   proxy_set_header X-PY4WEB-APPNAME /myapp;
   location / {
      proxy_pass http://127.0.0.1:8000/myapp$request_uri;
   }
}

这个示例是 nginx 配置中的 server 块的内容。必须为 py4web 服务器提供服务的 每个应用程序/每个域 创建一个单独的此类块。注意一些重要方面:

  • server_name 定义了映射到应用 myapp 的域,

  • proxy_http_version 1.1; 指令是可选的,但强烈建议使用(否则 nginx 使用 HTTP 1.0 方式和

    后端服务器 —— 这里是 py4web —— 进行通信,它会在缓冲和其他方面产生各种问题),

  • proxy_set_header Host $host; 指令确保将正确的 Host 传递给py4web —— 这里是 myapp.example.com

  • proxy_set_header X-PY4WEB-APPNAME /myapp; 指令确保 py4web(和ombott)知道要服务哪个应用程序

    此外,该应用程序是域映射的——特别注意 myapp 名称前的斜线( / )—— 需要 确保在 ombott 级别正确解析 URL,

  • 最后, proxy_pass http://127.0.0.1:8000/myapp$request_uri; 确保请求完整传递( $request_uri

    到 py4web 服务器(此处: 127.0.0.1:8000 )和正确的应用程序( /myapp )。

这种配置确保了 ombott 和 py4web 中的所有 URL 操作,特别是在 AuthFormGrid 等模块中,都是使用应用程序映射到的域正确完成的。

自定义错误页面

py4web 提供默认错误页面。例如,如果应用程序中的所有路由都与请求不匹配,则将显示默认的 404 错误页面。默认情况下,所有 HTTP 错误代码都由 py4web 自动处理。

然而,可以覆盖此行为。它可以针对某个 HTTP 错误代码,甚至可以针对所有错误完成。

以下是重写 HTTP 代码 404 (未找到)的示例:

from py4web.core import ERROR_PAGES
ERROR_PAGES[404] = f"Page not found!"

如果要替换 _all_ 对应的默认错误页面,应使用特殊的限定符 "*" 。此外,返回的值也可能包含 HTML 代码:

from py4web import URL
from py4web.core import ERROR_PAGES
from yatl.helpers import A

ERROR_PAGES["*"] = f"We have encountered an error! (try: {A('Main Page', _href=URL("/",scheme=True))})"

请注意,此设置是 全局 的。这意味着对于特定 py4web 实例上的所有应用,它只需定义一次。原因是,当出现错误时,可能是由于请求未匹配到任何应用。因此,这种配置 只应在其中一个应用中进行