美文网首页大数据 爬虫Python AI Sql
Python Web Flask源码解读!模板渲染过程!

Python Web Flask源码解读!模板渲染过程!

作者: 14e61d025165 | 来源:发表于2019-07-25 14:15 被阅读1次

关于我

编程界的一名小小程序猿,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。

Github: github.com/hylinux1024

微信公众号:angrycode

Python资源共享群:484031800

前面对 Flask 启动流程 和路由原理都进行了源码走读。今天我们看看 模板渲染 的过程。

0x00 使用模板

首先看一个来自官方文档使用模板渲染的例子

from flask import render_template
@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
return render_template('hello.html', name=name)
复制代码
在项目目录下需要有一个 templates 目录,并创建了一个 hello.html 文件

/templates
/hello.html
复制代码
hello.html 的内容为

<!doctype html>
<title>Hello from Flask</title>
{% if name %}
<h1>Hello {{ name }}!</h1>
{% else %}
<h1>Hello, World!</h1>
{% endif %}
复制代码
这个模板中 name 是参数,通过调用 render_template 方法就可以根据参数实现 html 模板文件的渲染。

0x01 Flask.render_template

def render_template(template_name, **context):
"""Renders a template from the template folder with the given
context.
:param template_name: the name of the template to be rendered
:param context: the variables that should be available in the
context of the template.
"""
current_app.update_template_context(context)
return current_app.jinja_env.get_template(template_name).render(context)
复制代码
方法的注释很清楚,从 templates 文件夹中找到名称为 template_name 的文件进行渲染。其中 current_app 是通过以下语句初始化

_request_ctx_stack = LocalStack()
current_app = LocalProxy(lambda: _request_ctx_stack.top.app)
复制代码
LocalStack 就是一个 栈的实现类 。而 _request_ctx_stack 是在 Flask.request_context() 方法中将当前的上下文实例 push 到栈里面的

def request_context(self, environ):
"""Creates a request context from the given environment and binds
it to the current context. This must be used in combination with
the with statement because the request is only bound to the
current context for the duration of the with block.
Example usage::
with app.request_context(environ):
do_something_with(request)
:params environ: a WSGI environment
"""
return _RequestContext(self, environ)
复制代码
_RequestContext 类实现了上下文管理器协议,它可以在 with 语句中使用

class _RequestContext(object):
"""The request context contains all request relevant information. It is
created at the beginning of the request and pushed to the
_request_ctx_stack and removed at the end of it. It will create the
URL adapter and request object for the WSGI environment provided.
"""
def init(self, app, environ):
self.app = app
self.url_adapter = app.url_map.bind_to_environ(environ)
self.request = app.request_class(environ)
self.session = app.open_session(self.request)
self.g = _RequestGlobals()
self.flashes = None
def enter(self):
_request_ctx_stack.push(self)
def exit(self, exc_type, exc_value, tb):
# do not pop the request stack if we are in debug mode and an
# exception happened. This will allow the debugger to still
# access the request object in the interactive shell.
if tb is None or not self.app.debug:
_request_ctx_stack.pop()
复制代码
执行 enter() 时操作 push ,退出 with 语句时就执行 pop 操作。

回到 request_context() 方法,它是在 wsgi_app() 中被调用的

def wsgi_app(self, environ, start_response):
"""The actual WSGI application. This is not implemented in
__call__ so that middlewares can be applied:
app.wsgi_app = MyMiddleware(app.wsgi_app)
:param environ: a WSGI environment
:param start_response: a callable accepting a status code,
a list of headers and an optional
exception context to start the response
"""
with self.request_context(environ):
rv = self.preprocess_request()
if rv is None:
rv = self.dispatch_request()
response = self.make_response(rv)
response = self.process_response(response)
return response(environ, start_response)
复制代码
从路由原理文章的分析知道, wsgi_app() 在服务端接收到客户端请求时就会执行。 所以当请求来临时,就会把当前 Flask 实例的请求上下文实例保存到栈实例 _request_ctx_stack 中;请求处理后,就从栈里面弹出当前请求的上下文实例。

LocalProxy 是一个代理类,它的构造函数传递了一个 lambda 表达式: lambda: _request_ctx_stack.top.app 。 这个操作就把当前的上下文实例通过 LocalProxy 进行了封装,即 current_app 是当前 Flask 实例的上下文的代理。 所以当 current_app.jinja_env 这个语句其实就是访问 Flask 的实例属性 jinja_env ,这个属性是在 Flask 的构造函数中进行初始化的。

class Flask(object):
...
#: 源码太长了省略
#: options that are passed directly to the Jinja2 environment
jinja_options = dict(
autoescape=True,
extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_']
)
def init(self, package_name):
...
#: 源码太长省略部分源码
#: the Jinja2 environment. It is created from the
#: :attr:jinja_options and the loader that is returned
#: by the :meth:create_jinja_loader function.
self.jinja_env = Environment(loader=self.create_jinja_loader(),
**self.jinja_options)
self.jinja_env.globals.update(
url_for=url_for,
get_flashed_messages=get_flashed_messages
)
复制代码
jinja_env 是一个 Environment 实例。 这个是 jinja 模板引擎提供的类, Flask 框架的模板渲染就是通过 jinja 来实现的。 Environment 需要一个 loader ,是通过以下方法获取的

def create_jinja_loader(self):
"""Creates the Jinja loader. By default just a package loader for
the configured package is returned that looks up templates in the
templates folder. To add other loaders it's possible to
override this method.
"""
if pkg_resources is None:
return FileSystemLoader(os.path.join(self.root_path, 'templates'))
return PackageLoader(self.package_name)
复制代码
默认情况下是从 templates 目录下构造一个 FileSystemLoader 的实例,这个类的作用就是从文件系统中加载模板文件的。

0x02 Environment.get_template

@internalcode
def get_template(self, name, parent=None, globals=None):
"""Load a template from the loader. If a loader is configured this
method ask the loader for the template and returns a :class:Template.
If the parent parameter is not None, :meth:join_path is called
to get the real template name before loading.
The globals parameter can be used to provide template wide globals.
These variables are available in the context at render time.
If the template does not exist a :exc:TemplateNotFound exception is
raised.
.. versionchanged:: 2.4
If name is a :class:Template object it is returned from the
function unchanged.
"""
if isinstance(name, Template):
return name
if parent is not None:
name = self.join_path(name, parent)
return self._load_template(name, self.make_globals(globals))
复制代码
get_template() 方法内部调用了 _load_template() 方法

@internalcode
def _load_template(self, name, globals):
if self.loader is None:
raise TypeError('no loader for this environment specified')
if self.cache is not None:
template = self.cache.get(name)
if template is not None and (not self.auto_reload or
template.is_up_to_date):
return template
template = self.loader.load(self, name, globals)
if self.cache is not None:
self.cache[name] = template
return template
复制代码
_load_template() 方法首先会检查是否有缓存,如果缓存可用就使用缓存;缓存不可用就使用 loader 加载模板,这个 loader 就是前面提到的 FileSystemLoader 的实例(默认情况下)。

0x03 BaseLoader.load

@internalcode
def load(self, environment, name, globals=None):
...
# 省略部分源码
return environment.template_class.from_code(environment, code, globals, uptodate)
复制代码
BaseLoader 是 FileSystemLoader 的基类。这个 load 方法实现了模板的编译、加载等逻辑。最后是使用 environment.template_class.from_code() 方法。其中 template_class 是 Template 类,它代表编译后的模板对象。 from_code 是 Template 类的静态方法,可以用来创建一个 Template 实例。当 load 方法返回时,就得到了一个 Template 对象。 最后回到 render_template 方法

def render_template(template_name, **context):
...
return current_app.jinja_env.get_template(template_name).render(context)
复制代码
执行了 Template 对象的 render() 方法。

0x04 Template.render

def render(self, args, kwargs):
"""This function accepts either a dict or some keyword arguments which
will then be the context the template is evaluated in. The return
value will be the rendered template.
:param context: the function accepts the same arguments as the
:class:dict constructor.
:return: the rendered template as string
"""
ns = self.default_context.copy()
if len(args) == 1 and isinstance(args[0], utils.MultiDict):
ns.update(args[0].to_dict(flat=True))
else:
ns.update(dict(
args))
if kwargs:
ns.update(kwargs)
context = Context(ns, self.charset, self.errors)
exec self.code in context.runtime, context
return context.get_value(self.unicode_mode)
复制代码
这个方法接收一个 dict 类型参数,用于给模板传递参数。该方法的
核心是执行 exec **函数。 exec 是 Python 内置函数,它可以动态的执行 Python 代码。

0x05 总结一下

Flask 使用 Jinja 作为模板引擎。执行路径为

Flask.render_template => Environment.get_template => Template.render => exec
复制代码

相关文章

网友评论

    本文标题:Python Web Flask源码解读!模板渲染过程!

    本文链接:https://www.haomeiwen.com/subject/rnkhrctx.html