读源码(七)—— ejs

ejs是一个轻量的模板渲染引擎,通过简单的<%%>标签就能实现简单的模板渲染。本文仅针对<%%>和<%=%>两个常用标签进行分析。

入口代码,api保持了与ejs完全一致

const ejs = require('./ejs')

const html = '<%if(test){%><div><%=user.firstName%></div><%}else{%><%=user.lastName%><%}%>'
const options = {}
const data = {
  test: false,
  user: {
    firstName: 'zhang',
    lastName: 'san'
  }
}

const template = ejs.compile(html, options)
const compiled = template(data)
console.log(compiled)

核心代码,重要代码的讲解写在了注释里,分成了6个step,注意注释的case使用的是

<div><%=user.name%></div>
const _REGEX_STRING = '(<%%|%%>|<%=|<%-|<%_|<%#|<%|%>|-%>|_%>)'
const _DEFAULT_OPEN_DELIMITER = '<'
const _DEFAULT_CLOSE_DELIMITER = '>'
const _DEFAULT_DELIMITER = '%'
const _DEFAULT_LOCALS_NAME = 'locals'

function stripSemi(str){
  return str.replace(/;(\s*$)/, '$1')
}

const _MATCH_HTML = /[&<>'"]/g

const _ENCODE_HTML_RULES = {
  '&': '&amp;',
  '<': '&lt;',
  '>': '&gt;',
  '"': '&#34;',
  "'": '&#39;'
}

const escapeFuncStr =
  'var _ENCODE_HTML_RULES = {\n'
+ '      "&": "&amp;"\n'
+ '    , "<": "&lt;"\n'
+ '    , ">": "&gt;"\n'
+ '    , \'"\': "&#34;"\n'
+ '    , "\'": "&#39;"\n'
+ '    }\n'
+ '  , _MATCH_HTML = /[&<>\'"]/g;\n'
+ 'function encode_char(c) {\n'
+ '  return _ENCODE_HTML_RULES[c] || c;\n'
+ '};\n';

function encode_char(c) {
  return _ENCODE_HTML_RULES[c] || c;
}

function escapeXML(markup) {
  return markup == undefined
    ? ''
    : String(markup)
      .replace(_MATCH_HTML, encode_char);
}

escapeXML.prototype.toString = function() {
  return Function.prototype.toString.call(this) + ';\n' + escapeFuncStr
}

function rethrow(err, str, flnm, lineno, esc) {
  var lines = str.split('\n');
  var start = Math.max(lineno - 3, 0);
  var end = Math.min(lines.length, lineno + 3);
  var filename = esc(flnm);
  // Error context
  var context = lines.slice(start, end).map(function (line, i){
    var curr = i + start + 1;
    return (curr == lineno ? ' >> ' : '    ')
      + curr
      + '| '
      + line;
  }).join('\n');

  // Alter exception message
  err.path = filename;
  err.message = (filename || 'ejs') + ':'
    + lineno + '\n'
    + context + '\n\n'
    + err.message;

  throw err;
}

function Template(text, opts) {
  opts = opts || {}
  const options = {}
  this.templateText = text
  this.mode = null
  this.truncate = false
  this.currentLine = 1
  this.source = ''

  options.client = opts.client || false
  options.context = opts.context
  options.strict = opts.strict || false
  options.escapeFunction = opts.escape || opts.escapeFunction || escapeXML
  options.openDelimiter = opts.openDelimiter || exports.openDelimiter || _DEFAULT_OPEN_DELIMITER
  options.closeDelimiter = opts.closeDelimiter || exports.closeDelimiter || _DEFAULT_CLOSE_DELIMITER
  options.delimiter = opts.delimiter || exports.delimiter || _DEFAULT_DELIMITER
  options.localsName = opts.localsName || exports.localsName || _DEFAULT_LOCALS_NAME

  if (options.strict) {
    options._with = false;
  } else {
    options._with = typeof opts._with != 'undefined' ? opts._with : true;
  }

  this.opts = options

  this.regex = this.createRegex()
}

Template.modes = {
  EVAL: 'eval',
  ESCAPED: 'escaped',
  RAW: 'raw',
  COMMENT: 'comment',
  LITERAL: 'literal'
}

Template.prototype = {
  // 创建解析<% %>标签的模板
  createRegex() {
    const str = _REGEX_STRING
    return new RegExp(str)
  },

  compile() {
    let prepended = ''
    let appended = ''
    let src = ''
    let fn

    const opts = this.opts
    const escapeFn = opts.escapeFunction

    if (!this.source) {
      // step1,赋值this.source,把传入的template转成js代码string
      // <div> => ; __append("<div>")
      // <%=user.name%> => ; __append(escapeFn(user.name))
      this.generateSource()

      // step2,加在上一步生成代码前的代码,实际就是提供了__append方法,然后__output记录最终输出的js string
      prepended +=
        '  var __output = "";\n' +
        '  function __append(s) { if (s !== undefined && s !== null) __output += s }\n'

      // step3,把step1生成的代码放到with语法中,方便生成代码中获取data中的数据
      // 因为生成代码中使用的是user.name,但实际应该是data.user.name,为了不修改原始数据使用with语法
      // 实际官方已经不推荐使用with语法,实际开发中请勿使用
      if (opts._with !== false) {
        prepended +=  '  with (' + opts.localsName + ' || {}) {' + '\n';
        appended += '  }' + '\n';
      }

      // step4,拼接最终返回__output的代码
      appended += '  return __output;' + '\n'
      this.source = prepended + this.source + appended
    }

    src = this.source
    // step5,把js string转成最终的function
    // new Function的用法:const fn = new Function('p1, p2, p3', 'console.log(p1, p2, p3)') // 参数名称列表 & 执行代码的string
    // 执行时fn('1', '2', '3') => 打印1 2 3
    fn = new Function(opts.localsName + ', escapeFn, include, rethrow', src)

    // step6, opts.client为false的情况下,包装一个匿名函数供外部传入最终的data
    const returnedFn = opts.client ? fn : function anonymous(data) {
      // 暂时不实现include方法
      var include = function() {
        console.log('not implemented now')
      }
      return fn.apply(opts.context, [data || {}, escapeFn, include, rethrow])
    }

    return returnedFn
  },

  generateSource() {
    const matches = this.parseTemplateText()
    const d = this.opts.delimiter // %
    const o = this.opts.openDelimiter // <
    const c = this.opts.closeDelimiter // >

    // ['<div>', '<%=', 'user.name', '%>', '</div>']
    if (matches && matches.length) {
      matches.forEach((line, index) => {
        let closing
        if ( line.indexOf(o + d) === 0        // If it is a tag
        && line.indexOf(o + d + d) !== 0) { // and is not escaped
          closing = matches[index + 2]

          // 检查<%等开始标签后 + 2的位置是否是对应的%>的结束标签,如果不是说明语法不符合,直接报错
          if (!(closing == d + c || closing == '-' + d + c || closing == '_' + d + c)) {
            throw new Error('Could not find matching close tag for "' + line + '".');
          }
        }

        this.scanLine(line)
      })
    }
  },

  // 通过正则把string解析成按照<%等标签分割开的数组
  parseTemplateText() {
    let str = this.templateText
    const regex = this.regex
    /**
     * 第一次执行的效果
     * [
          '<%=',
          '<%=',
          index: 5,
          input: '<div><%=user.name%></div>',
          groups: undefined
        ]
     */
    let result = regex.exec(str)
    const arr = []
    let firstPos = 0

    while(result) {
      firstPos = result.index

      if (firstPos !== 0) {
        arr.push(str.substring(0, firstPos))
        str = str.slice(firstPos)
      }

      arr.push(result[0])
      str = str.slice(result[0].length)
      result = regex.exec(str)
    }

    if (str) {
      arr.push(str)
    }

    /**
     * 最终返回['<div>', '<%=', 'user.name', '%>', '</div>']
     */
    return arr
  },

  // 通过状态机,把parseTemplateText生成是数组,解析成对应的js片段
  scanLine(line) {
    const d = this.opts.delimiter;
    const o = this.opts.openDelimiter;
    const c = this.opts.closeDelimiter;
    let newLineCount = 0;

    newLineCount = (line.split('\n').length - 1);

    switch(line) {
      case o + d:
      case o + d + '_':
        this.mode = Template.modes.EVAL;
        break;
      case o + d + '=':
        this.mode = Template.modes.ESCAPED;
        break

      case d + c:
      case '-' + d + c:
      case '_' + d + c:
        if (this.mode == Template.modes.LITERAL) {
          this._addOutput(line)
        }
  
        this.mode = null
        // this.truncate = line.indexOf('-') === 0 || line.indexOf('_') === 0
        break
      
      default: 
        if (this.mode) {
          switch(this.mode) {
            case Template.modes.EVAL:
            case Template.modes.ESCAPED:
            case Template.modes.RAW:
              if (line.lastIndexOf('//') > line.lastIndexOf('\n')) {
                line += '\n'
              }
          }

          switch (this.mode) {
            // Just executing code
            case Template.modes.EVAL:
              this.source += '    ; ' + line + '\n'
              break
              // Exec, esc, and output
            case Template.modes.ESCAPED:
              this.source += '    ; __append(escapeFn(' + stripSemi(line) + '))' + '\n'
              break
            case Template.modes.LITERAL:
              this._addOutput(line)
              break
          }

        } else {
          this._addOutput(line)
        }
    }
  },

  _addOutput(line) {
    this.source += '    ; __append("' + line + '")' + '\n';
  }

}


function compile(template, opts) {
  const templ = new Template(template, opts)
  return templ.compile()
}

module.exports = {
  compile
}

最终动态生成代码如下:

(function anonymous(locals, escapeFn, include, rethrow
) {
  var __output = "";
  function __append(s) { if (s !== undefined && s !== null) __output += s }
  with (locals || {}) {
    ; if(test){
    ; __append("<div>")
    ; __append(escapeFn(user.firstName))
    ; __append("</div>")
    ; }else{
    ; __append(escapeFn(user.lastName))
    ; }
  }
  return __output;

})

执行后就能拿到编译后的模板啦~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值