我有一个使用CherryPy,Mako HTML模板和JavaScript编写的应用程序。我想允许它安装到任何 URL - 也就是说,我希望有人能够将其安装到 http://example.com、http://example.com/app 或 http://this.is.an.example.com/some/application/whatever。我也希望它像Apache后面的WSGI应用程序一样工作,或者使用CherryPy内置的网络服务器。但是,我在处理我的模板和 JavaScript 必须使用的 URL 时遇到了问题。
也就是说,如果我想访问相对于我的应用程序根目录的资源,例如api/something
或static/application.js
,如何确保无论应用程序的基本 URL 是什么,引用都有效?
我最初的天真解决方案是使用相对 URL,但当我想在多个端点返回相同的 Mako 模板时,这停止了工作。也就是说,如果我有一个用于显示项目的模板,也许我会想在页面的根目录(/
)的某个地方显示该项目,也许在其他地方(比如/item
,也许/username/items
也是)。如果模板中有一个相对 URL,则该 URL 是相对于该位置的- 这意味着,由于我的静态 CSS/JS/图像资源仅相对于 root,因此模板在/item
和/username/items
位置的链接将被断开。
真子模板
我发现我可以将我的 URL 包装在我的 Mako 模板中的cherrypy.url()
中,这解决了这个问题。例如,无论如何,此示例模板都将正确引用/link
URL:
<%!
import cherrypy
%>
<a href="${cherrypy.url('/link')}">Click here!</a>
这很好,但它有点麻烦,在我的模板中import cherrypy
似乎很奇怪。这是正确的方法吗?有没有更好的方法?如果它是自动的,那就太好了,所以我不必记得自己包装 URL。
JavaScript
我正在使用 CherryPy 的tools.staticdir
选项提供静态.js文件。这工作正常,但我正在进行几次 AJAX 调用,就像我的 Mako 模板中的静态资源一样,URL 是相对于我的应用程序根目录的。例如,如果 CherryPy 公开了一个/api/something
URL,而我想从 JavaScript 访问它,我该如何编写我的 JS,以便无论我的 CherryPy 应用程序挂载在哪里仍然可以访问它?
我的第一个想法是,我可以向我的模板添加某种隐藏的 HTML 元素或注释,其中包含没有参数的调用cherrypy.url()
的值,这将导致我的应用程序的根 URL,并且我可以遍历 DOM,获取该值,并在我尝试发出 HTTP 请求之前将其附加到我想要的任何 URL。好处是这在 JavaScript 中非常透明;缺点是,当我向应用程序添加越来越多的模板时,很容易忘记包含神奇的隐藏 HTML 元素。我想我可以通过使所有模板都依赖于根模板来解决此问题,但这似乎仍然是一个黑客。
我的第二个想法是将我的JavaScript文件本身转换为Mako模板,而不是使用tools.staticdir
来提供它们,并使用与我在现有Mako HTML模板中使用的相同的cherrypy.url()
方法。这具有一致性的吸引力,并且不需要我现有模板中的魔术HTML元素,但这意味着文件必须经过整个模板渲染过程才能以理论上的速度损失提供,而且感觉也有点不对劲。
有没有更好的选择?
其他问题
虽然我目前没有这个问题,但我想将来我可能也想在我的静态 CSS 文件中使用应用程序相对 URL。
代码异味问题?
我花了一些时间在谷歌和SO以及CherryPy文档中试图找到这个问题的解决方案,但我没有发现任何东西。这是否表明我正在做一些奇怪的事情,并且有一些模式或最佳实践可以避免我只是不遵循这个问题?
我最终将我的裸JavaScript文件转换为JavaScript文件的Mako模板,并传入一个设置为cherrypy.url('/')
值的baseurl
变量。由于我的应用程序的现有结构,我能够为每个呈现的模板自动执行此操作,这主要满足了我的需求。
我如何使用真子
首先,请注意,我使用的是 CherryPy wiki 上 Mako 页面中的MakoHandler
和MakoLoader
类。使用这些类如下所示(为简洁起见,略作编辑):
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__()
传递给self.template.render
的env
变量来向所有模板添加变量替换。考虑到这一点,我们可以将MakoHandler
类更改为如下所示:
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
变量将设置为所有模板中应用程序的根目录 - 这正是我想要的。它甚至可以与我在另一个模板中<%include.../>
的模板一起使用(见下文)。
附带好处
以前,在我的HTML中,我有一个<script>
标签,指向CherryPy提供的静态JS文件,浏览器发出了单独的HTTP请求来获取该文件。但是当我转向使用 Mako 来模板化我的 JavaScript 以添加baseurl
变量时,我意识到我可以直接在我的 HTML 中<%include.../>
它,从而消除了一次往返。