赵走x博客
网站访问量:151521
首页
书籍
软件
工具
古诗词
搜索
登录
4、全局变量
3、模板渲染过程
2、路由原理
1、整体运行梳理
3、模板渲染过程
资源编号:76590
Flask源码解读
Python Web
热度:88
来源:https://juejin.im/post/5d38fc1af265da1bb31c7b31
前面对Flask启动流程和路由原理都进行了源码走读。今天我们看看模板渲染的过程。 # 0x00 使用模板 首先看一个来自官方文档使用模板渲染的例子 ```python from flask import render_template @app.route('/hello/') @app.route('/hello/
') def hello(name=None): return render_template('hello.html', name=name) ``` 在项目目录下需要有一个templates目录,并创建了一个hello.html文件 ```python /templates /hello.html ``` hello.html的内容为 ```python
Hello from Flask
{% if name %}
Hello {{ name }}!
{% else %}
Hello, World!
{% endif %} ``` 这个模板中name是参数,通过调用render_template方法就可以根据参数实现html模板文件的渲染。 # 0x01 Flask.render_template ```python 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是通过以下语句初始化 ```python _request_ctx_stack = LocalStack() current_app = LocalProxy(lambda: _request_ctx_stack.top.app) ``` LocalStack就是一个栈的实现类。而_request_ctx_stack是在Flask.request_context()方法中将当前的上下文实例push到栈里面的 ```python 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语句中使用 ```python 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()中被调用的 ```python 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的构造函数中进行初始化的。 ```python 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,是通过以下方法获取的 ```python 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 ```python @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()方法 ```python @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 ```python @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方法 ```python def render_template(template_name, **context): ... return current_app.jinja_env.get_template(template_name).render(context) ``` 执行了Template对象的render()方法。 # 0x04 Template.render ```python 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 ``` # 0x06 学习资料 [palletsprojects.com/p/flask/](https://palletsprojects.com/p/flask/) [jinja.pocoo.org/docs/2.10/](http://jinja.pocoo.org/docs/2.10/)