使用过前端模板的同学们,尤其是使用过nodejs写后台服务的同学们,应该对ejs模板和jade模板都不陌生。对与ejs模板和jade模板孰强孰弱,载各大论坛中一直争论不休,有说ejs更直观的,也有说jade更优雅、更强大的。我今天不讨论谁好谁坏,而是记录一下这几天发现的一个特殊的使用场景——递归树形结构渲染。
什么是递归树形结构渲染?
递归树形结构其实是特指那些父子结构中子级展开后和父级结构相同或类似,并有可能继续展开不断延伸,有点像树形结构中的枝干,每一级的枝干即是上一级枝干的子级,又是下一级枝干的父级,单独从局部上看又是一样的结构,整体又构成了一个层次分明的树。这种结构其实很常见,比如树形文件夹-文件管理、家里的族谱、公司的部门划分、网站分类的菜单等。但是为什么又觉得在开发中感到很陌生,是因为我们平时很少写递归的代码,前端的产品形态也很少使用递归和树形结构,如果有就会去掉用别人的三方js库,如果是app或者桌面应用就不免要遍历文件夹来展示,还有就是前端的交互特点更多倾向于多级联动,即点开某一级的时候再去拉数据加载或计算当前的下一级,比如常见的省份组件和日期选择器。
我遇到的问题——ejs没有递归语法?
中所周知ejs模板更像是html的扩展,在html的基础上加入了变量和逻辑(条件和循环)以及片段引用。因此常规的情况下是很适合使用,可读性强,前后端的同学都能轻松上手。但是要实现递归的树形结构就要满足一个条件——自己调用自己。ejs并不支持定义一个函数。但是jade就不同,jade有mixin的语法,这从某种程度上就满足了要求。
但是我现有的项目都是ejs模板实现的,局部使用jade又会使项目的模板引擎管理显得很乱。加上另一个原因就是我的项目是前后端复用同一套模板,而且这个需要递归的功能是在前端模板来实现的,webpack中使用ejs-loader会有另一个问题就是不支持include语法。面前有一个方法就是在前端再加一个jade-loader来递归处理树形的数据。
ejs实现递归树形结构渲染的猜想
经过一番斗争我找到了在ejs中实现递归树形结构渲染的方法。要记住两个限制:1、尽量不使用include语法,来保证前端工程中ejs-loader的兼容性;2、ejs模板本身不支持函数定义。
但是这两个不就是递归实现的基本途径吗?是也不是,ejs只是不允许函数的定义,但是却允许函数的调用,即 <%= data %> 中data可以是变量值或表达式,既然是表达式,就说明data可以是某个函数的调用表达式即 <%= data.fn(option) %> 。甚至来说函数的返回值可以是一个html或者另一个模板。
那么函数的定义在哪里完成?
既然是data.fn 那么fn就是data的一部分,即函数是数据中的一个字段,我们知道webpack 使用ejs-loader时require进来就是个函数。
1 var $$tmpl = require('./tmpl.ejs'); 2 // $$tmpl 是个直接可以渲染数据的函数 3 $(body).append($$tmpl(data));
我们直接将这个函数传进去,即:
1 var $$tmpl = require(' ./tmpl.ejs '); // 一级模板 2 var $$recursiveTmpl = require('./recursive.ejs'); // 二级模板:局部递归部分的模板,此时$$tmpl 和 $$recursiveTmpl 都是函数 3 // $$tmpl 是个直接可以渲染数据的函数 4 $.get('/api/getlist', function (res) { 5 // 一级模板渲染使用的数据由数据和二级模板函数组成 6 var data = { 7 'list': res.list, 8 'tmplFn': $$recursiveTmpl 9 } 10 11 // 将数据传入,一级模板 12 $(body).append($$tmpl(data)); 13 })
js调用的部分就算是完成了,那么模板改怎么写呢?我们看看一级模板—— ./tmpl.ejs
<p>递归list</p> <div> <%= tmplFn({"list": list, "tmplFn": tmplFn}) %> </div>
我们再看看二级递归模板—— ./recursive.ejs
1 <ul> 2 <% list.forEach(function(item){ %> 3 <li> 4 <span><%= item.title %></span> 5 <% if (item.subList && item.subList.length > 0) { %> 6 <span>有下一级</span> 7 <%= tmplFn ({"list": item.subList , "tmplFn": tmplFn})%> 8 <% } else { %> 9 <span>没有下一级了</span> 10 <% } %> 11 </li> 12 <% } %> 13 </ul>
还记得递归调用的特点吗?1、调用自身;2、由特定条件的出口
可以看出二级递归模板是哥不断延展的ul > li > ul > li 的树形结构,再想象递归调用的特点。不难看出
<%= tmplFn ({"list": item.subList , "tmplFn": tmplFn})%>
就是不断调用自身的保障,tmplFn就是从最外层的js开始传入的,每次作为属性字段传入,供下一级再次使用。而 if (item.subList && item.subList.length > 0) 就是跳出结束使用的边界条件。
就这样,我们就实现了ejs的递归调用,而且是在ejs功能不全的前端ejs-loader中兼容的递归调用。