在不知道应用程序根 URL 的情况下,我应该如何处理与应用程序根相关的 URL 引用?

How should I handle URL references that are relative to the root of my application, without knowing the application's root URL?

我有一个正在使用 CherryPy、Mako HTML 模板和 JavaScript 编写的应用程序。我想让它安装到任何 URL - 也就是说,我希望有人能够将它安装到 http://example.com, or http://example.com/app, or http://this.is.an.example.com/some/application/whatever。我还希望它能像 Apache 后面的 WSGI 应用程序或使用 CherryPy 的内置网络服务器一样工作。但是,我在处理我的模板和 JavaScript 必须使用的 URL 时遇到了问题。

也就是说,如果我想访问与我的应用程序根目录相关的资源,如 api/somethingstatic/application.js,我如何确保无论我做什么,引用都有效应用的基础 URL 是?

我最初天真的解决方案是使用相对 URLs,但是当我想在多个端点 return 相同的 Mako 模板时,它就停止工作了。也就是说,如果我有一个用于显示项目的模板,也许我会想在页面的根目录 (/) 的某处显示该项目,也可能在其他地方显示该项目(例如 /item ,也许还有 /username/items)。如果模板中有一个相对的 URL,那么 URL 是相对于那个位置 - 这意味着,因为我的静态 CSS/JS/image 资源是仅相对于 root,/item/username/items 位置的模板链接将断开。

Mako 模板

我发现我可以在我的 Mako 模板中将我的 URL 包装在 cherrypy.url() 中,这解决了这个问题。例如,此示例模板无论如何都会正确引用 /link URL:

<%!
    import cherrypy
%>
<a href="${cherrypy.url('/link')}">Click here!</a>

这很好,但是有点麻烦,而且 import cherrypy 在我的模板中看起来很奇怪。这是正确的方法吗?有没有更好的办法?如果它是自动的就好了,所以我不必记得自己包装 URLs。

JavaScript

我使用 CherryPy 的 tools.staticdir 选项提供静态 .js 文件。这工作正常,但我进行了几次 AJAX 调用,并且像我的 Mako 模板中的静态资源一样,URLs 是相对于我的应用程序根目录的。例如,如果 CherryPy 公开了一个 /api/something URL,而我想从 JavaScript 访问它,我该如何编写我的 JS,以便无论我的 CherryPy 应用程序安装在哪里,它仍然可以访问?

我的第一个想法是,我可以向我的模板添加某种隐藏的 HTML 元素或注释,其中包含不带参数调用的 cherrypy.url() 的值,这将导致根 URL 我的应用程序,并且我可以遍历 DOM,获取该值,并在我尝试发出 HTTP 请求之前将其添加到我想要的任何 URL 之前。好处是这在 JavaScript 中是非常透明的;缺点是当我向应用程序添加越来越多的模板时,很容易忘记包含神奇的隐藏 HTML 元素。我想我可以通过使所有模板都依赖于根模板来解决这个问题,但它仍然看起来像是一个 hack。

我的第二个想法是将我的 JavaScript 文件本身变成 Mako 模板,而不是使用 tools.staticdir 来为它们提供服务,并使用我现有的 cherrypy.url() 方法Mako HTML 模板。这具有一致性的吸引力,并且在我现有的模板中不需要神奇的 HTML 元素,但这意味着文件必须经过整个模板渲染过程才能提供,理论上会损失一些速度, 感觉也有点不对。

还有更好的选择吗?

其他问题

虽然我目前没有这个问题,但我想将来我可能也想在我的静态 CSS 文件中使用应用程序相关的 URLs。

代码异味问题?

我花了一些时间在 Google 和 SO 上以及在 CherryPy 文档中试图找到解决这个问题的方法,但我没有找到任何东西。这是否表明我正在做一些奇怪的事情,并且有一些模式或最佳实践可以避免我只是没有遵循的这个问题?

我最终将裸 JavaScript 文件转换为 JavaScript 文件的 Mako 模板,并传入一个 baseurl 变量,设置为 cherrypy.url('/') 的值。由于我的应用程序的现有结构,我能够为每个呈现的模板自动执行此操作,这基本上满足了我的需求。

我如何使用 Mako

首先,请注意我使用的是 the Mako page on the CherryPy wiki 中的 MakoHandlerMakoLoader class。使用那些 classes 看起来像这样(为简洁起见略作编辑):

import cherrypy
from mako.lookup import TemplateLookup

class MakoHandler(cherrypy.dispatch.LateParamPageHandler):
    def __init__(self, template, next_handler):
        self.template = template
        self.next_handler = next_handler
    def __call__(self):
        env = globals().copy()
        env.update(self.next_handler())
        try:
            return self.template.render(**env)
        except:
            cherrypy.response.status = "500"
            return exceptions.html_error_template().render()

class MakoLoader(object):
    def __init__(self):
        self.lookups = {}
    def __call__(self, filename, directories, module_directory=None,
                 collection_size=-1):
        key = (tuple(directories), module_directory)
        try:
            lookup = self.lookups[key]
        except KeyError:
            lookup = TemplateLookup(directories=directories,
                                    module_directory=module_directory,
                                    collection_size=collection_size)
            self.lookups[key] = lookup
        cherrypy.request.lookup = lookup
        cherrypy.request.template = t = lookup.get_template(filename)
        cherrypy.request.handler = MakoHandler(t, cherrypy.request.handler)

main = MakoLoader()
cherrypy.tools.mako = cherrypy.Tool('on_start_resource', main)

这样您就可以像这样在 CherryPy 中引用模板:

@cherrypy.expose
@cherrypy.tools.mako(filename="index.html")
def index(name=None):
    return {'username': name}

添加一组默认的变量替换

现在,使用此代码,您可以通过修改 MakoHandler.__call__() 传递给 [=19= 的 env 变量来向 所有 模板添加变量替换].考虑到这一点,我们可以将 MakoHandler class 更改为如下所示:

class MakoHandler(cherrypy.dispatch.LateParamPageHandler):
    def __init__(self, template, next_handler):
        self.template = template
        self.next_handler = next_handler
    def __call__(self):
        env = globals().copy()
        env.update(self.next_handler())
        env.update({'baseurl': cherrypy.url('/')})
        try:
            return self.template.render(**env)
        except:
            cherrypy.response.status = "500"
            return exceptions.html_error_template().render()

由此,baseurl 变量被设置为 all 模板中应用程序的根目录——这正是我想要的。它甚至可以与我在另一个模板中 <%include.../> 的模板一起使用(见下文)。

附带好处

之前,在我的 HTML 中,我有一个 <script> 标记指向由 CherryPy 提供的静态 JS 文件,浏览器发出单独的 HTTP 请求来获取该文件。但是当我转而使用 Mako 来模板我的 JavaScript 以添加 baseurl 变量时,我意识到我可以直接 <%include.../> 它在我的 HTML 中,省去一轮旅行。