Hello!
I’ve been working on a little news aggregator that uses EEx templates to render pages, and I’ve found myself reconstructing a lot of Phoenix’s functionality (I’m not actually using Phoenix, just Plug/Cowboy), but haven’t quite found an elegant way to deal with compiling my templates so that I don’t have to read from eex
files in priv/
at runtime.
At the moment, I have a base Template
module with a __using__
macro that injects the following render/3
function into my “views”. The function calls back out to the Template
module, passing in information regarding the caller module, the file we want to render, etc:
In Template
:
defmacro __using__(opts) do
layout = Keyword.get(opts, :layout, "base.html.eex") |> (&(Path.join(@layout_dir, &1))).()
quote do
def render(conn, file, assigns \\ []), do: Template.render(__MODULE__, conn, file, assigns)
def layout, do: unquote(layout)
end
end
def render(module, %{ status: status } = conn, file, assigns) do
rendered_template = render_template(module, file, assigns)
rendered_layout = render_layout(module, [ assigns | [ inner_content: rendered_template ] ])
Plug.Conn.send_resp(conn, (status || 200), rendered_layout)
end
# Both render_layout and render_function are ultimately calls to this next function, render_file
defp render_file(file, assigns, eval_options \\ []) do
quoted = EEx.compile_file(file)
# Note that binding() will return, among others, the "assigns" parameter, allowing us to use the @key EEx convenience in templates.
{result, _binding} = Code.eval_quoted(quoted, binding(), eval_options)
result
end
Of course, this means that I’m constantly compiling and rendering files at runtime, when I could be precompiling them. In trying to figure out how Phoenix handles this, I found the following function in Phoenix.Template
that seems to be at the heart of how templates are compiled in the framework:
defp compile(path, root, engines) do
name = template_path_to_name(path, root)
defp = String.to_atom(name)
ext = Path.extname(path) |> String.trim_leading(".") |> String.to_atom
engine = Map.fetch!(engines, ext)
quoted = engine.compile(path, name)
{name, quote do
@file unquote(path)
@external_resource unquote(path)
defp unquote(defp)(var!(assigns)) do
_ = var!(assigns)
unquote(quoted)
end
defp render_template(unquote(name), assigns) do
unquote(defp)(assigns)
end
end}
end
This gets called from a defmacro __before_compile__(env)
declaration, which in turn unquotes the newly minted functions in whatever view we’re building. I want to clear up my understanding of how this works before running off and trying to re-implement this on my own:
-
From what I gather, all
.eex
template files, regardless of whether or not they’re actually “used”, are compiled according to the namespace of their parent view? It’s my understanding that these get turned into named functions that correspond to their file names? -
When calling
render(conn, <filename>)
in a controller, we’re not actually saying “hey, find this file and render it”, rather what we’re doing is calling a function named according to<filename>
? This function is the already-compiled EEx template injected into the corresponding view? -
Output from an
EEx.compile_file/2
call will generate a “quoted” structure - to evaluate said structure, is it enough to simply unquote it within a function, and ensure that anassigns
binding is present? At present I’ve been evaluating it withCode.eval_quoted
, but it seems that the Phoenix implementation doesn’t use this? -
I’ve been incorporating the caller view-module’s “context” (i.e. functions declared in the “view” module) that I want available in the EEx template by passing in
functions: [{<caller module>, <caller module>.__info__(:functions)}]
into theCode.eval_quoted
call within the parentTemplate
module. Is this made redundant if I’m injecting a function into the view that contains the unquoted result ofEEx.compile_file
? In other words, will the now-unquoted structure generated bycompile_file
have access to the surrounding module’s functions?
It’s been hard developing an intuitive understanding of how this works behind the scenes - it’s so seamless when you’re using these constructs as they’re provided! Any help clarifying these points would be much appreciated.