js模板引擎2 -- 解析模板

模板语法规则:

  • 变量渲染使用 <%=变量名称%> 语法;
  • 条件判断使用 <% if(expr) { %> <% } %>js语法;
  • 列表渲染使用 <% for(var i = 0; i < expr; i++) { %> <% } %> js语法;
  • 模板注释使用 <%# %>;

下面我们一步步的实现一个正则表达式来识别出模板语法规则部分。

正则匹配

语法规则以<%开始,以%>结尾,对应正则为 /<%([\w\W]*?)%>/

关于正则表达式的贪婪模式和非贪婪模式可以参考:正则基础之——贪婪与非贪婪模式

这个正则表达式还不能识别变量名称和注释,扩展一下:
/<%(#?)(=)?([\w\W]*?)%>/

过滤一下语句首尾的空白符:
/<%(#?)(=)?[ \t]*([\w\W]*?)[ \t]*%>/

因为我们要取出所有的捕获组,所以要加上全局标识:
/<%(#?)(=)?[ \t]*([\w\W]*?)[ \t]*%>/g

正则表达式的主体部分已基本完成,下面使用exec()方法测试一下:

假设模板为:

    <% for (var i = 0; i < list.length; i ++) { %>
        <li>索引 <%= i + 1 %> :<%= list[i] %></li>
    <% } %>

测试代码:

var reg = /<%(#?)(=)?[ \t]*([\w\W]*?)[ \t]*%>/g;
var str = `
    <% for (var i = 0; i < list.length; i ++) { %>
        <li>索引 <%= i + 1 %> :<%= list[i] %></li>
    <% } %>
`;
var match;
while(match = reg.exec(str)) {
    console.log(match);
}

输出结果:

[
  '<% for (var i = 0; i < list.length; i ++) { %>',
  '',
  undefined,
  'for (var i = 0; i < list.length; i ++) {',
  index: 5,
  input: '\n' +
    '    <% for (var i = 0; i < list.length; i ++) { %>\n' +
    '        <li>索引 <%= i + 1 %> :<%= list[i] %></li>\n' +
    '    <% } %>\n',
  groups: undefined
]
[
  '<%= i + 1 %>',
  '',
  '=',
  'i + 1',
  index: 67,
  input: '\n' +
    '    <% for (var i = 0; i < list.length; i ++) { %>\n' +
    '        <li>索引 <%= i + 1 %> :<%= list[i] %></li>\n' +
    '    <% } %>\n',
  groups: undefined
]
[
  '<%= list[i] %>',
  '',
  '=',
  'list[i]',
  index: 81,
  input: '\n' +
    '    <% for (var i = 0; i < list.length; i ++) { %>\n' +
    '        <li>索引 <%= i + 1 %> :<%= list[i] %></li>\n' +
    '    <% } %>\n',
  groups: undefined
]
[
  '<% } %>',
  '',
  undefined,
  '}',
  index: 105,
  input: '\n' +
    '    <% for (var i = 0; i < list.length; i ++) { %>\n' +
    '        <li>索引 <%= i + 1 %> :<%= list[i] %></li>\n' +
    '    <% } %>\n',
  groups: undefined
]

匹配到了我们想要的结果。

解析模板

模板字符串的各个部分转换为token对象保存,通过token对象我们需要知道各个部分的类型。

一般有两种类型,一种是模板语法部分,这部分类型标记为express。一种是非模板语法部分,这部分标记为string

后面构造js可执行代码时需要用到这个类型值。

此外还有各个部分的字符串字面量,在原模板字符串中的起始下标,如果是express类型还需要提取出js代码语句。

token对象的构造函数:

function Token(type, value, preToken) {
    this.type = type; // 类型 string / express
    this.value = value; // 字符串值
    this.script = null; // 如果是express 对应的代码
    if(preToken) {
        this.start = preToken.end; // 字符串值在原模板字符串中起始位置
    } else {
        this.start = 0;
    }
    this.end = this.start + value.length;// 字符串值在原模板字符串中结束位置
}

下面是把模板字符串的各个部分转换为token对象的逻辑,根据正则匹配结果的下标区分出string类型部分和express类型部分:

var reg = {
    test: /<%(#?)(=)?[ \t]*([\w\W]*?)[ \t]*%>/g,
    // 对识别出的模板语法语句做进一步处理变量
    use: function (match, comment, output, code) {
        if(output === '=') { // 变量 / 表达式
            output = true;
        } else {
            output = false;
        }
        if(comment) { // 注释
            code = '/*' + code + '*/';
            output = false;
        }
    
        return {
            code: code,
            output: output
        }
    }
};

function tplToToken(str,reg) {
    var tokens = [], 
        match, 
        preToken, 
        index = 0;
    while((match = reg.test.exec(str)) !== null) {
        preToken = tokens[tokens.length - 1];
        // 如果匹配结果的开始下标比上一次匹配结果的结束下标大,说明两个模板语法之间有字符串
        if(match.index > index) {
            preToken = new Token('string', str.slice(index,match.index), preToken);
            tokens.push(preToken);
        }
        preToken = new Token('express', match[0], preToken);
        preToken.script = reg.use.apply(reg,match);
        tokens.push(preToken);
        // 保存本次匹配结果的结束下标
        index = match.index + match[0].length;
    }

    return tokens;
}

调用tplToToken函数会输出以下结果:

 [
  {
    type: 'string',
    value: '\n    ',
    script: null,
    start: 0,
    end: 5
  },
  {
    type: 'express',
    value: '<% for (var i = 0; i < list.length; i ++) { %>',
    script: { code: 'for (var i = 0; i < list.length; i ++) {', output: false },
    start: 5,
    end: 51
  },
  {
    type: 'string',
    value: '\n        <li>索引 ',
    script: null,
    start: 51,
    end: 67
  },
  {
    type: 'express',
    value: '<%= i + 1 %>',
    script: { code: 'i + 1', output: true },
    start: 67,
    end: 79
  },
  {
    type: 'string',
    value: ' :',
    script: null,
    start: 79,
    end: 81
  },
  {
    type: 'express',
    value: '<%= list[i] %>',
    script: { code: 'list[i]', output: true },
    start: 81,
    end: 95
  },
  {
    type: 'string',
    value: '</li>\n    ',
    script: null,
    start: 95,
    end: 105
  },
  {
    type: 'express',
    value: '<% } %>',
    script: { code: '}', output: false },
    start: 105,
    end: 112
  }
]

至此,模板的解析已经完成了,接下来要根据解析结果构造可执行的js代码,也就是模板的编译部分。

posted @ 2023-08-30 17:05  Fogwind  阅读(37)  评论(0编辑  收藏  举报