• 我们的前端模版引擎更新总结


    最近花了些时间更新了下我们的模版引擎。就像构建工具一样,模版引擎也基本是大家玩烂的内容,什么运行速度啊,编译速度啊,大家也谈了很多。让我们讲些不同的东东^ ^

    原文地址:https://app.yinxiang.com/shard/s30/sh/83bcf109-aee4-44df-ab01-4990d384d8f7/8fafd8df4dd428bbc6f76fadd6febb59

    项目地址:https://github.com/QQEDU/micro-tpl

    需求

    没需求也不会有更新,首先我们看看这次我们有哪些需求:

    • 随着构建工具的发展,特别是gulp这种基于流(Stream)的构建工具的出现,以前的插件已经很难满足未来的需求,我们需要将模版线下编译核心抽出来,并满足:模版字符串经过核心后编译成Javascript字符串。这样才能使同一份代码在多个构建工具复用。
    • include功能的开发,只有interpolationevaluation虽然解决了大部分问题,但是没法优雅的将模版分块复用。诚然如果我们基于AMD规范,也可以利用r.js的打包解决,但毕竟还是有些项目没有基于AMD规范的。
    • 更早的发现错误,而不是运行时再提示。在编译阶段解决一些问题,那么实际运行过程中问题就少了。

    怎样include比较好?

    空间的糖饼的artTemplate使用:

    {{include '../public/header'}}
    

    也就是直接把一个模版插入到另一个新的模版。和数据没啥关系的模版(比如页面的header)还好,对于有数据的模版就略微蛋疼了。两个模版共享同一个上下文,给模版复用带来较大的困难。我们先想想,如果对于使用了AMD规范的项目中的include是怎样的?

    <%=require('../public/header')(data, opts)%>
    

    很好,为了对齐体验又便于区分,我们引入include方法:

    <%=include('../public/header')(data, opts)%>
    

    这样我们就解决了上面提到的问题。

    如何更早的发现问题

    在编译构成中,先检查代码,保证代码没有问题。

    无法分析的html问题

    在模版中,html问题是难以分析的,因为我们不清楚Javascript会输出什么,例如:

    <a href=<%=it.href ? '"'  + it.href + '"' : ''%>>
    

    该模版是有问题的,比如当it.href不存在是就变成了<a href=>,但很难分析出来。所以我们忽略html的错误,把它放在渲染时处理。

    Javascript问题

    闭合问题

    模版闭合问题时通常遇到的问题,比如

    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>
    

    Javascript解析错误

    例如:

    <p><%={say, ,'error'}%></p>
    

    实现

    简单的说,只是利用语法分析找到这些问题。但我们还是有点难点的:

    • 如何从终端直观的找到代码问题
    • Javascirpt AST Parse当然能解决很多问题,但是模版语言她不懂,还是要处理下再丢给她的

    比较好看的UI

    首先我们先去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('
    ');
    }
    
    // 生成错误提示
    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(/	/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();
      }
    }
    

    解决interpolation错误问题

    直接将其丢入AST进行分析,但是由于实现原因,不能使用分号和换行,所以如果使用,则抛错:

    check: function (str, i) {
      // 使用换行,则抛错
      if (~str.indexOf('
    ')) {
        throw (explainError(merge({
          message: 'Should not use multi-lines in interpolation',
          lines: this.lines,
          filename: this.filename
        }, getPos(this.tmpl, this.i + str.indexOf('
    ') + 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));
      } 
    }
    

    解决evaluation问题

    我们将前面过程中得到的代码组装起来,丢进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));
        }
      }
    }
    

    这样我们就基本解决了编译阶段提前找出错误的需求,未来看看如何改进了^ ^

    效果

    Alt text
     

    我们可以看到对于测试用例noclose.html:

    no<%close
    

    模版引擎提示本模版有错误,并指出问题处在哪里,开发者可以非常方便的定位问题。其他用例类推。

  • 相关阅读:
    键盘弹出与隐藏对TextView的影响
    iOS9 警告框
    计时器的写法
    iOS提交被拒
    新生活
    批量删除wps文档里的回车符的方法!WPS使用技巧分享!
    学习笔记计划
    监控服务器的注册及登陆并邮件通知的代码(go / python)
    Python调用C代码
    导入用户到Discuz论坛
  • 原文地址:https://www.cnblogs.com/justany/p/3999197.html
Copyright © 2020-2023  润新知