Flask蓝图模板文件夹

58 投票
7 回答
51636 浏览
提问于 2025-04-17 05:27

我的Flask应用的结构是:

myapp/
    run.py
    admin/
        __init__.py
        views.py
        pages/
            index.html
    main/
        __init__.py
        views.py
        pages/
            index.html

_init_.py文件是空的。admin/views.py的内容是:

from flask import Blueprint, render_template
admin = Blueprint('admin', __name__, template_folder='pages')

@admin.route('/')
def index():
    return render_template('index.html')

main/views.py的内容和admin/views.py差不多:

from flask import Blueprint, render_template
main = Blueprint('main', __name__, template_folder='pages')

@main.route('/')
def index():
    return render_template('index.html')

run.py的内容是:

from flask import Flask
from admin.views import admin
from main.views import main

app = Flask(__name__)
app.register_blueprint(admin, url_prefix='/admin')
app.register_blueprint(main, url_prefix='/main')

print app.url_map

app.run()

现在,如果我访问 http://127.0.0.1:5000/admin/,它能正确显示 admin/index.html。但是,访问 http://127.0.0.1:5000/main/ 时却还是显示 admin/index.html,而不是 main/index.html。我检查了 app.url_map:

<Rule 'admin' (HEAD, OPTIONS, GET) -> admin.index,
<Rule 'main' (HEAD, OPTIONS, GET) -> main.index,

我还确认了 main/views.py 中的 index 函数按预期被调用。如果我把 main/index.html 改个名字,那就能正常工作。那么,不改名字的情况下,如何才能让 http://127.0.0.1:5000/main/ 显示 main/index.html 呢?

7 个回答

16

twooster的回答很有意思,但还有一个问题就是,Jinja默认会根据模板的名字来缓存模板。因为这两个模板都叫“index.html”,所以加载器在后续的蓝图中就不会再运行了。

除了linqq的两个建议,还有第三个选择,就是完全忽略蓝图的templates_folder选项,把模板放在应用程序的模板目录下各自的文件夹里。

也就是说:

myapp/templates/admin/index.html
myapp/templates/main/index.html
24

除了linqq上面提到的好建议外,如果需要的话,你还可以覆盖默认的功能。有几种方法可以做到这一点:

你可以在一个子类化的Flask应用中覆盖 create_global_jinja_loader(这个方法会返回一个在flask/templating.py中定义的 DispatchingJinjaLoader)。虽然这样做是可行的,但并不推荐。之所以不推荐,是因为 DispatchingJinjaLoader 本身已经足够灵活,可以支持自定义加载器的注入。如果你自己搞坏了加载器,它还可以依赖默认的、正常的功能。

所以,推荐的做法是“覆盖 jinja_loader 函数”。不过,这里就涉及到文档不足的问题。修改Flask的加载策略需要一些似乎没有文档说明的知识,以及对Jinja2的良好理解。

你需要了解两个组成部分:

  • Jinja2环境
  • Jinja2模板加载器

这些都是Flask自动创建的,默认设置也很合理。(顺便提一下,你可以通过覆盖 app.jinja_options 来指定自己的 Jinja2选项,但要注意,如果你不自己指定,你将失去Flask默认包含的两个扩展—— autoescapewith。可以查看flask/app.py,看看它们是如何引用这些的。)

环境中包含了所有的上下文处理器(例如,你可以在模板中使用 var|tojson),辅助函数(url_for等)和变量(gsessionapp)。它还包含了对模板加载器的引用,在这里就是前面提到的自动实例化的 DispatchingJinjaLoader。所以,当你在应用中调用 render_template 时,它会找到或创建Jinja2环境,设置好所有这些内容,然后在这个环境上调用 get_template,这又会在 DispatchingJinjaLoader 内部调用 get_source,尝试几种后面会描述的策略。

如果一切顺利,这个过程会找到一个文件并返回它的内容(以及一些 其他数据)。另外,请注意,这也是 {% extend 'foo.htm' %} 的执行路径。

DispatchingJinjaLoader 做了两件事:首先,它检查应用的全局加载器,也就是 app.jinja_loader 是否能找到文件。如果找不到,它会检查所有的应用蓝图(根据注册顺序,尽我所知)中的 blueprint.jinja_loader,试图找到文件。追溯到最后,这里是jinja_loader的定义(在flask/helpers.py中,_PackageBoundObject,这是Flask应用和蓝图的基类):

def jinja_loader(self):
    """The Jinja loader for this package bound object.

    .. versionadded:: 0.5
    """
    if self.template_folder is not None:
        return FileSystemLoader(os.path.join(self.root_path,
                                             self.template_folder))

啊!现在我们明白了。显然,这两个的命名空间会在同一个目录名上发生冲突。由于全局加载器首先被调用,它总是会胜出。(FileSystemLoader 是多个标准Jinja2加载器之一。)不过,这意味着没有真正简单的方法可以重新排序蓝图和应用范围的模板加载器。

因此,我们需要修改 DispatchingJinjaLoader 的行为。起初,我认为没有好的、不被反对且高效的方法来实现这一点。然而,显然如果你覆盖 app.jinja_options['loader'] 本身,我们就能得到想要的行为。所以,如果我们子类化 DispatchingJinjaLoader,并修改一个小函数(我想完全重新实现可能更好,但目前这样也可以),我们就能得到想要的行为。总体来说,一个合理的策略如下(未经测试,但应该适用于现代Flask应用):

from flask.templating import DispatchingJinjaLoader
from flask.globals import _request_ctx_stack

class ModifiedLoader(DispatchingJinjaLoader):
    def _iter_loaders(self, template):
        bp = _request_ctx_stack.top.request.blueprint
        if bp is not None and bp in self.app.blueprints:
            loader = self.app.blueprints[bp].jinja_loader
            if loader is not None:
                yield loader, template

        loader = self.app.jinja_loader
        if loader is not None:
            yield loader, template

这在两个方面修改了原始加载器的策略:首先尝试从蓝图加载(并且仅从当前执行的蓝图,而不是所有蓝图),如果失败,再从应用加载。如果你喜欢所有蓝图的行为,可以从flask/templating.py中复制一些代码。

最后,你需要在Flask对象上设置 jinja_options

app = Flask(__name__)
# jinja_options is an ImmutableDict, so we have to do this song and dance
app.jinja_options = Flask.jinja_options.copy() 
app.jinja_options['loader'] = ModifiedLoader(app)

第一次需要模板环境(因此被实例化),也就是第一次调用render_template时,你的加载器应该被使用。

78

从Flask 0.8开始,蓝图(blueprints)会把指定的模板文件夹添加到应用的搜索路径中,而不是把每个目录当作独立的部分。这意味着如果你有两个同名的模板文件,搜索路径中第一个找到的那个会被使用。这个行为确实让人困惑,而且目前文档也写得不太清楚(可以查看这个问题)。看来并不是只有你对这种行为感到困惑。

这样设计的原因是为了让蓝图中的模板能够轻松被主应用的模板覆盖,而主应用的模板在Flask的模板搜索路径中是优先的。

这里有两个解决方案。

  • 把每个index.html文件重命名为独一无二的名字(比如admin.htmlmain.html)。
  • 在每个模板文件夹中,把每个模板放在蓝图文件夹的子目录里,然后通过这个子目录来调用模板。例如,你的管理员模板可以是yourapp/admin/pages/admin/index.html,然后在蓝图中用render_template('admin/index.html')来调用。

撰写回答