最近花了些时间更新了下咱们的模版引擎。就像构建工具同样,模版引擎也基本是你们玩烂的内容,什么运行速度啊,编译速度啊,你们也谈了不少。让咱们讲些不一样的东东^ ^html
项目地址:https://github.com/QQEDU/micro-tplgithub
没需求也不会有更新,首先咱们看看此次咱们有哪些需求:gulp
include
功能的开发,只有interpolation
和evaluation
虽然解决了大部分问题,可是无法优雅的将模版分块复用。诚然若是咱们基于AMD规范
,也能够利用r.js
的打包解决,但毕竟仍是有些项目没有基于AMD规范
的。空间的糖饼的artTemplate使用:app
{{include '../public/header'}}
也就是直接把一个模版插入到另外一个新的模版。和数据没啥关系的模版(好比页面的header)还好,对于有数据的模版就略微蛋疼了。两个模版共享同一个上下文,给模版复用带来较大的困难。咱们先想一想,若是对于使用了AMD规范
的项目中的include是怎样的?函数
<%=require('../public/header')(data, opts)%>
很好,为了对齐体验又便于区分,咱们引入include
方法:工具
<%=include('../public/header')(data, opts)%>
这样咱们就解决了上面提到的问题。测试
在编译构成中,先检查代码,保证代码没有问题。ui
在模版中,html问题是难以分析的,由于咱们不清楚Javascript会输出什么,例如:this
<a href=<%=it.href ? '"' + it.href + '"' : ''%>>
该模版是有问题的,好比当it.href不存在是就变成了<a href=>,但很难分析出来。因此咱们忽略html的错误,把它放在渲染时处理。
模版闭合问题时一般遇到的问题,好比
noclose<%
这是比较好分析的,再好比说:
<% if (true) { %> <a href="javscirpt:void(0);">hello</a> <% { %>
这也较好分析。
变量问题很差分析,由于咱们没法知道传进来的变量可能有什么。
那么咱们换一种思路,好比在Strict Mode时候的变量分析,除去模版内声明的变量和函数以及it和opt外,其余变量引用应当都是不合法的,例如考虑下面例子:
<a href="<%=$.render(it.item.url)%>">hello</>
这个例子是不合法的,由于内部没有明显的$的申明,严格模式下,不该当容许使用这种隐性引用。
因为实现缘由,字面量是没法多行的,考虑下面例子:
<p><%= it.say %></p>
例如:
<p><%=it.say;%></p>
例如:
<p><%={say, ,'error'}%></p>
简单的说,只是利用语法分析找到这些问题。但咱们仍是有点难点的:
- 如何从终端直观的找到代码问题
- Javascirpt AST Parse固然能解决不少问题,可是模版语言她不懂,仍是要处理下再丢给她的
首先咱们先去jscs扒了段代码,只要告诉该函数错误的行和列,以及原文件内容他就能虽然出一个不错的UI:
// Inspired from jscs var colors = require('colors'); function explainError(error, colorize) { // 错误的行号 var lineNumber = error.line - 1 // 原文件的内容,并拆成一行一行 , lines = error.lines // 输出结果 , result = [ renderLine(lineNumber, lines[lineNumber], colorize), renderPointer(error.column, colorize) ] , i = lineNumber - 1 // 显示错误行上下2行,即最多显示5行内容 , linesAround = 2; // 从错误行向前找行数并渲染 while (i >= 0 && i >= (lineNumber - linesAround)) { result.unshift(renderLine(i, lines[i], colorize)); i--; } i = lineNumber + 1; // 从错误行向后找行数并渲染 while (i < lines.length && i <= (lineNumber + linesAround)) { result.push(renderLine(i, lines[i], colorize)); i++; } result.unshift(formatErrorMessage(error.message, error.filename, colorize)); return result.join('\n'); } // 生成错误提示 function formatErrorMessage(message, filename, colorize) { return (colorize ? colors.bold(message) : message) + ' at ' + (colorize ? colors.green(filename) : filename) + ' :'; } // 生成制定空格 function prependSpaces(s, len) { while (s.length < len) { s = ' ' + s; } return s; } // 渲染一行内容 function renderLine(n, line, colorize) { line = line.replace(/\t/g, ' '); var lineNumber = prependSpaces((n + 1).toString(), 5) + ' |'; // 渲染出行号 + 内容,例如: // 1 | alert('hello world'); return ' ' + (colorize ? colors.grey(lineNumber) : lineNumber) + line; } // 渲染出指针,用于指示出错误位置 function renderPointer(column, colorize) { var res = (new Array(column + 9)).join('-') + '^'; return colorize ? colors.grey(res) : res; } module.exports = explainError;
首先咱们先解决<%
和%>
的闭合问题:
analyse: function () { // 找到打开标记 var i = find(this.tmpl, '<%' , this.i); // 若是存在 if (~i) { this.i = i; // 找下一个闭合标记 i = find(this.tmpl, '%>', this.i); // 存在,则辨认是interpolation仍是evaluation if (~i) { this.tmpl.charAt(this.i + 2) === '=' ? // check interpolation this.check(this.tmpl.substring(this.i + 3, i), i + 3) : // prepare for ast builder this.script.push([this.tmpl.substring(this.i + 2, i), this.i + 2]); this.i = i; // 递归遍历 this.analyse(); // 不存在,显然没闭合,抛出错误 } else { throw (explainError(merge({ message: 'it need %> after <%', lines: this.lines, filename: this.filename }, getPos(this.tmpl, this.i)), true)); } // 不然,即闭合问题检查完毕,检查脚本 } else { this.checkEval(); } }
直接将其丢入AST进行分析,可是因为实现缘由,不能使用分号和换行,因此若是使用,则抛错:
check: function (str, i) { // 使用换行,则抛错 if (~str.indexOf('\n')) { throw (explainError(merge({ message: 'Should not use multi-lines in interpolation', lines: this.lines, filename: this.filename }, getPos(this.tmpl, this.i + str.indexOf('\n') + 2)), true)); } // 创建ast,若是有错误,则包装后抛出 try { var ast = acorn.parse(str); } catch (e) { throw (explainError(merge({ message: e.toString().replace(/\(.+?\)/, ''), lines: this.lines, filename: this.filename }, getPos(this.tmpl, this.i + e.pos + 3)), true)); } // 若是发现ast末尾为分号(目前还未实现辨认全非字符串分号,简单处理末尾的分号),则抛错 if (str.charAt(ast.end - 1) === ';') { throw (explainError(merge({ message: 'Should not use ";" in interpolation', lines: this.lines, filename: this.filename }, getPos(this.tmpl, this.i + ast.end + 2)), true)); } }
咱们将前面过程当中获得的代码组装起来,丢进ast,若是找到问题,从新定位并抛错:
checkEval: function () { // 若是有脚本则检查,没有不须要进行 if (this.script.length) { var script = ''; // 将脚本组合起来 this.script.forEach(function (arr) { script += arr[0]; }); // 丢进ast看看有没有错误 try { var ast = acorn.parse(script); } catch (e) { // 有错误则经过错误,回溯源码位置,包装后抛出 var pos = e.pos, num, l; this.script.every(function (arr, i) { l = arr[0].length; if (l < pos) { pos -= l; return true; } else { num = i; return false; } }); pos = this.script[num][1] + pos; throw (explainError(merge({ message: e.toString().replace(/\(.+?\)/, ''), lines: this.lines, filename: this.filename }, getPos(this.tmpl, pos)), true)); } } }
这样咱们就基本解决了编译阶段提早找出错误的需求,将来看看如何改进了^ ^
咱们能够看到对于测试用例noclose.html:
no<%close
模版引擎提示本模版有错误,并指出问题处在哪里,开发者能够很是方便的定位问题。其余用例类推。