Python Web Flask源碼解讀(三)——模板渲染過程

  • 2019 年 10 月 3 日
  • 筆記

關於我
一個有思想的程式猿,終身學習實踐者,目前在一個創業團隊任team lead,技術棧涉及Android、Python、Java和Go,這個也是我們團隊的主要技術棧。
Github:https://github.com/hylinux1024
微信公眾號:終身開發者(angrycode)

前面對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)

BaseLoaderFileSystemLoader的基類。這個load方法實現了模板的編譯、載入等邏輯。最後是使用environment.template_class.from_code()方法。其中template_classTemplate類,它代表編譯後的模板對象。
from_codeTemplate類的靜態方法,可以用來創建一個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函數。execPython內置函數,它可以動態的執行Python程式碼。

0x05 總結一下

Flask使用Jinja作為模板引擎。執行路徑為

Flask.render_template => Environment.get_template => Template.render => exec

0x06 學習資料

  • https://palletsprojects.com/p/flask/
  • http://jinja.pocoo.org/docs/2.10/