Análisis de principios del motor de plantillas js

Prefacio

Hoy en día, los marcos de front-end react, vue y angular tienen sus propios motores de plantilla, pero al aprender el motor de plantillas de js nativos, especialmente el procesamiento de varias cadenas y expresiones en la parte inferior, puede ayudar a comprender mejor otros marcos. .Cómo se procesan los datos de la plantilla.

Este artículo se basa en el código fuente de subrayado y utiliza aproximadamente 70 líneas de código para implementar un motor de plantilla simple. Incluye las siguientes funciones principales, como la representación de datos, la representación de expresiones (compatible con declaraciones if y bucles for) y la representación de cadenas html .

El método de llamada en el lado del usuario es el siguiente, se escribe la función de compilación y se espera que se genere el resultado correspondiente.

1. Renderizar los datos

<?= ?>Representa el valor de la variable de salida.

    const data = {
      country: 'China',
    };
    const template = compile(`    //compile生成模板函数
      <div>
         <?= country?>
      </div>
    `);
    console.log(template(data)); //template传入参数data,生成模板字符串

Resultado de salida:

  <div>China</div>

2. Juicio condicional

<? ?>Puede escribir declaraciones js directamente en él, como si un juicio condicional, for loop

    const data = {
      country: 'China',
      gender: 'male',
    };
    const template = compile(`
      <div>
         <? if(gender === 'male'){?>
          <?= country?>
         <?}?>
      </div>
    `);
    console.log(template(data));

Resultado de salida:

  <div>China</div>

3. Declaración de bucle

    const data = {
      country: 'China',
      gender: 'male',
      array: [1, 2, 3],
    };
    
    const template = compile(`
      <div>
         <? for(var i = 0; i< array.length ; i++) {?>
            <span><?= gender + i ?></span>
         <?}?>
      </div>
    `);

    console.log(template(data));

Resultado de salida:

<div><span>male0</span><span>male1</span><span>male2</span></div>



Representación de valor

Inicialmente, se implementa el requisito más simple y los dos valores se representan de la siguiente manera:

    const data = {
      country: 'China',
      gender: 'male',
    };
    const template = compile('<div><?= country?><span><?= gender?></span></div>');
    console.log(template(data));

Resultado Esperado:

 <div>China<span>male</span></div>

En el código de ejecución anterior, se puede ver que la compilación devuelve una nueva función después de pasar la cadena de la plantilla, y el resultado final se puede obtener cuando los datos se pasan a esta función y se ejecutan.

con declaración

Antes de discutir la implementación del motor de plantillas, primero aprenda un punto de conocimiento con la sintaxis. Usando con para evitar llamadas de objetos redundantes, observe el siguiente caso.

function test(data){
    with(data){
         age =  100;
    }
    console.log(data);
}
test({age:1})

resultado:

{age: 100}

Con puede limitar el alcance del objeto de contexto. Dentro del alcance del paquete with, las variables que no se han definido representan los atributos de los datos y pueden manipularse directamente. Por ejemplo, si lo anterior con pasa datos, entonces el atributo de edad se puede manipular directamente en el interior con, sin agregar prefijo de datos.

La función de con ayuda a compilar plantillas. Bajo la acción de con, la cadena de la plantilla se puede escribir directamente como una llamada de atributo sin agregar un prefijo de objeto.

análisis lógico

Compilar devolverá una nueva función después de pasar la cadena de la plantilla, y luego llamará a los datos para devolver el resultado final después de la compilación.Utilizando la función de con, la compilación se puede escribir en la siguiente forma para lograr el objetivo.

  function compile(string){
	    return function(data){
			     var _p = '';
			     with(data){
                   _p +=  '<div>'+country+'<span>'+gender+'</span>'+'</div>';
                }			
			return _p;
	    }    
   }

cadena es ahora '<div><?= country?><span><?= gender?></span></div>'

Si la cadena se convierte para '<div>'+country+'<span>'+gender+'</span>'+'</div>'poder lograr un compilador de plantilla. Pero el problema que ahora enfrentamos es que la cadena, independientemente de cualquier tratamiento, solo puede devolver un total de cadenas, simplemente no se puede hacer como las <div>comillas simples anteriores , pero countrysin las comillas simples .

Por lo tanto, la compilación no puede devolver directamente una función como la anterior.Para hacer que las etiquetas en el interior tengan comillas pero los atributos no estén entrecomillados, puede crear funciones pasando parámetros.

La función de compilación se transforma de la siguiente manera:

  function compile(string){
		var template_str =  `
    			 var _p = '';
			     with(data){
                   _p +=  '<div>'+country+'</div>';
                }			
			   return _p;
        `;
        var fn = new Function('data',template_str );
	    return fn;
   }

Ahora solo necesitas stringconvertirlo para que template_strparezca que has terminado.

function compile(string) {
  var template_str = `
    			 var _p = '';
			     with(data){
                   _p +=  '<div>'+country+'</div>';
                }			
			   return _p;
        `;

  function render() {
    var str = '';
    str += "var _p = ''";
    str += 'with(data){';
    str += '_p +=';
    str += templateParse();
    str += ';}return _p;';
  }

  function templateParse() {
    var reg = /<\?=([\s\S]+?)\?>/g;
    string.replace(reg, function (matches, $1, offset) {
      console.log($1,offset);
    });
  }

  var template_str = render();
  var fn = new Function('data', template_str);
  return fn;
}

Resultado de salida:

 country 5
 gender 24
function compile(string) {
  function render() {
    var str = '';
    str += "var _p = '';";
    str += 'with(data){';
    str += '_p +=';
    str = templateParse(str);
    str += ';}return _p;';
    console.log(str);
    return str;
  }

  function templateParse(str) {
    var reg = /<\?=([\s\S]+?)\?>/g;
    var index = 0;
    string.replace(reg, function (matches, $1, offset) {
      str += "'" + string.slice(index, offset) + "'";
      str += '+';
      str += $1;
      str += '+';
      index = offset + matches.length;
    });
    str += "'" + string.slice(index) + "'";
    return str;
  }

  var template_str = render();
  var fn = new Function('data', template_str);
  return fn;
}

var _p = '';with(data){_p +='<div>'+ country+'<span>'+ gender+'</span></div>';}return _p;
<div>China<span>male</span></div>



Procesamiento de expresiones

 const data = {
      country: 'China',
      gender: 'male',
    };
    const template = compile(
      '<div> <? if(country === "China"){ ?> <span><?= gender?></span> <?}?> </div>'
    );
    console.log(template(data));
<div><span>male</span></div>

análisis lógico

La primera idea es convertir la cadena de plantilla en la siguiente forma, pero en la práctica, se encuentra que ni si ni para expresiones se pueden agregar directamente a la cadena, y el resultado será un error.

 function render(data){
      var _p += '';
      with(data){

        _p += '<div>' + if(country === "China"){ return '<span>'+gender+'</span>'; } + '</div>';

      }
      return _p;
    }

Dado que la expresión no se puede agregar directamente a la cadena, la lógica de la expresión solo se puede separar de la cadena. La transformación es la siguiente, agregando un punto y coma delante de cada expresión y el código para agregar la cadena anterior al final Luego, el contenido de la expresión se representa directamente, pero el contenido envuelto en la expresión debe sumarse usando _p.

    function render(data){
      var _p += '';
      with(data){
        _p += '<div>';

        if(country === "China")
        { 
          _p+='<span>'+gender+'</span>'; 
        }

        _p += '</div>';

      }
      return _p;
    }

La representación de expresiones y valores es diferente, no solo tiene sintaxis if, sino también if else, if else if y for loop.

Pero no importa qué tipo de expresión, podemos resumir algunas reglas de la estructura de representación requerida anteriormente. 1. Agregue un punto y coma antes de la expresión para aislar lógicamente el código anterior 2. La expresión en sí se selecciona directamente sin comillas Representación 3. El contenido después de que la expresión debe sumarse con _p y asignarse a _p

function compile(string) {
  function render() {
    var str = '';
    str += "var _p = '';";
    str += 'with(data){';
    str += '_p +=';
    str = templateParse(str);
    str += ';}return _p;';
    console.log(str);
    return str;
  }

  function templateParse(str) {
    var reg = /<\?=([\s\S]+?)\?>|<\?([\s\S]+?)\?>/g;
    var index = 0;
    string.replace(reg, function (matches, $1, $2, offset) {
      str += "'" + string.slice(index, offset) + "'";
      if ($1) {
        //渲染值
        str += '+';
        str += $1;
        str += '+';
      } else if ($2) {
        //渲染表达式
        str += ';'; //第一步加个分号将前面的逻辑终止
        str += $2; //第二步直接拼接表达式
        str += '_p+='; //第三步要将表达式包裹的内容与_p相加并赋值给_p
      }
      index = offset + matches.length;
    });
    str += "'" + string.slice(index) + "'";
    return str;
  }

  var template_str = render();
  var fn = new Function('data', template_str);
  return fn;
}

El último cuerpo de función compilado de render

var _p = '';with(data){_p +='<div> '; if(country === "China"){ _p+=' <span>'+ gender+'</span> ';}_p+=' </div>';}return _p;

Resultados finales:

<div>  <span>male</span>  </div>



Renderizar código HTML

    const data = {
      code: '<div style="color:red">name:张三</div>',
    };
    const template = compile('<div><?- country?></div>');
    console.log(template(data));

Esperando el resultado:

<div><div style="color:red">name:张三</div></div>

<?- ?>Se usa para marcar la cadena html de salida

Representar el código html es muy simple, simplemente agregue un nuevo regular en la función templateParse y empalme la cadena html en el juicio condicional.

Los cambios son los siguientes

 function templateParse(str) {
    var reg = /<\?=([\s\S]+?)\?>|<\?-([\s\S]+?)\?>|<\?([\s\S]+?)\?>/g;
    var index = 0;
    string.replace(reg, function (matches, $1, $2, $3, offset) {
      str += "'" + string.slice(index, offset) + "'";
      if ($1) {
        //渲染值
        str += '+';
        str += $1;
        str += '+';
      } else if ($2) {
        //渲染html字符串
        str += '+' + $2 + '+';
      } else if ($3) {
        //渲染表达式
        str += ';'; //第一步加个分号将前面的逻辑终止
        str += $3; //第二步直接拼接表达式
        str += '_p+='; //第三步要将表达式包裹的内容与_p相加并赋值给_p
      }
      index = offset + matches.length;
    });
    str += "'" + string.slice(index) + "'";
    return str;
  }

Sin embargo, no es seguro simplemente empalmar la cadena html. Para evitar ataques xss, necesitamos escapar de los caracteres especiales en la cadena html.

Modifique el código de la siguiente manera para lograr el propósito de codificar caracteres especiales.

   
    //将html字符串传递给 esacper 函数处理一遍
    else if ($2) {
     //渲染html字符串
     str += '+ esacper(' + $2 + ') +';
   }
	
	//处理html字符串的特殊符号,预防xss攻击
	function esacper(str) {
	  const keyMap = {
	    //需要转译的队列
	    '&': '&amp;',
	    '<': '&lt;',
	    '>': '&gt;',
	    '"': '&quot;',
	    "'": '&hx27;',
	    '`': '&#x660;',
	  };
	
	  const keys = Object.keys(keyMap);
	
	  const reg = new RegExp(`(?:${keys.join('|')})`, 'g');
	
	  const replace = (value) => {
	    return keyMap[value];
	  };
   return reg.test(str) ? str.replace(reg, replace) : str;
}

Resultado de salida:

<div>&lt;div style=&quot;color:red&quot;&gt;name:张三&lt;/div&gt;</div>



Código final

El código final es el siguiente, alrededor de 70 líneas de código pueden implementar un motor de plantilla simple que incluye representación de valores, representación de expresiones y representación de cadenas html. Si necesita otras funciones, puede expandirlo y mejorarlo usted mismo.

function compile(string) {
  string = string.replace(/\n|\r\n/g, ''); //为了调用时兼容es6模板字符串

  /**
   * 将html字符串的特殊字符转义,预防xss攻击
   */
  function esacper(str) {
    const keyMap = {
      //需要转译的队列
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&hx27;',
      '`': '&#x660;',
    };

    const keys = Object.keys(keyMap);

    const reg = new RegExp(`(?:${keys.join('|')})`, 'g');

    const replace = (value) => {
      return keyMap[value];
    };

    return reg.test(str) ? str.replace(reg, replace) : str;
  }

  function render() {
    var str = '';
    str += esacper.toString();
    str += "var _p = '';";
    str += 'with(data){';
    str += '_p +=';
    str = templateParse(str);
    str += ';}return _p;';
    return str;
  }

  function templateParse(str) {
    var reg = /<\?=([\s\S]+?)\?>|<\?-([\s\S]+?)\?>|<\?([\s\S]+?)\?>/g;
    var index = 0;
    string.replace(reg, function (matches, $1, $2, $3, offset) {
      str += "'" + string.slice(index, offset) + "'";
      if ($1) {
        //渲染值
        str += '+';
        str += $1;
        str += '+';
      } else if ($2) {
        //渲染html字符串
        str += '+ esacper(' + $2 + ') +';
      } else if ($3) {
        //渲染表达式
        str += ';'; //第一步加个分号将前面的逻辑终止
        str += $3; //第二步直接拼接表达式
        str += '_p+='; //第三步要将表达式包裹的内容与_p相加并赋值给_p
      }
      index = offset + matches.length;
    });
    str += "'" + string.slice(index) + "'";
    return str;
  }

  var template_str = render();

  var fn = new Function('data', template_str);

  return fn;
}

Supongo que te gusta

Origin blog.csdn.net/brokenkay/article/details/110533559
Recomendado
Clasificación