Analyse de principe du moteur de template js

Préface

De nos jours, les frameworks front-end react, vue et angular ont tous leurs propres moteurs de template, mais en apprenant le moteur de template de js natif, en particulier le traitement de diverses chaînes et expressions en bas, cela peut aider à mieux comprendre d'autres frameworks Comment les données du modèle sont-elles rendues?

Cet article s'appuie sur le code source du trait de soulignement et utilise environ 70 lignes de code pour implémenter un moteur de modèle simple. Il comprend les fonctions de base suivantes, telles que le rendu des données, le rendu des expressions (compatible avec les instructions if et les boucles for) et le rendu de chaînes html .

La méthode d'appel côté utilisateur est la suivante, la fonction de compilation est écrite et le résultat correspondant devrait être produit.

1. Rendre les données

<?= ?>Représente la valeur de la variable de sortie

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

Résultat de sortie:

  <div>China</div>

2. Jugement conditionnel

<? ?>Vous pouvez y écrire directement des instructions js, comme si jugement conditionnel, pour boucle

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

Résultat de sortie:

  <div>China</div>

3. Instruction de boucle

    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));

Résultat de sortie:

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



Rendu de valeur

Au départ, l'exigence la plus simple est implémentée et les deux valeurs sont rendues comme suit:

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

Résultat attendu:

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

À partir du code d'exécution ci-dessus, on peut voir que compile retourne une nouvelle fonction après avoir passé la chaîne de modèle, et le résultat final peut être obtenu lorsque les données sont passées à cette fonction et exécutées.

avec déclaration

Avant de discuter de l'implémentation du moteur de modèle, commencez par apprendre un point de connaissance avec la syntaxe. En utilisant avec pour éviter les appels d'objets redondants, examinez le cas suivant.

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

résultat:

{age: 100}

Avec peut limiter la portée de l'objet de contexte. Dans le cadre du package with, les variables qui n'ont pas été définies représentent les attributs sur les données et peuvent être directement manipulées. Par exemple, si ce qui précède avec transmet des données, l'attribut age peut être directement manipulé à l'intérieur avec, Sans ajouter de préfixe de données.

La fonctionnalité de with permet de compiler des modèles. Sous l'action de with, la chaîne de modèle peut être directement écrite en tant qu'appel d'attribut sans ajouter de préfixe d'objet.

analyse logique

Compile renverra une nouvelle fonction après avoir transmis la chaîne de modèle, puis appellera data pour renvoyer le résultat final après la compilation. En utilisant la fonctionnalité de with, compile peut être écrite sous la forme suivante pour atteindre l'objectif.

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

la chaîne est maintenant '<div><?= country?><span><?= gender?></span></div>'

Si la chaîne est convertie pour '<div>'+country+'<span>'+gender+'</span>'+'</div>'pouvoir obtenir un compilateur de modèles. Mais le problème maintenant est que la chaîne, quel que soit le traitement, ne peut renvoyer qu'un total de chaînes, ne peut tout simplement pas faire comme les <div>guillemets simples ci-dessus , mais countrysans les guillemets simples .

Par conséquent, compile ne peut pas renvoyer directement une fonction comme ci-dessus. Pour que les balises à l'intérieur soient entre guillemets mais que les attributs ne soient pas entre guillemets, vous pouvez créer des fonctions en passant des paramètres.

La fonction de compilation est transformée comme suit:

  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;
   }

Il suffit maintenant d' stringêtre converti pour donner l' template_strimpression que vous avez terminé.

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;
}

Résultat de sortie:

 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>



Traitement des expressions

 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>

analyse logique

La première idée est de convertir la chaîne de modèle dans la forme suivante, mais dans la pratique, on constate que ni si ni pour les expressions ne peuvent être directement ajoutées à la chaîne, et le résultat sera une erreur.

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

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

      }
      return _p;
    }

Comme l'expression ne peut pas être ajoutée directement à la chaîne, la logique de l'expression ne peut être séparée que de la chaîne. La transformation est la suivante, en ajoutant un point-virgule devant chaque expression et le code pour ajouter la chaîne précédente à la fin Le contenu de l'expression est alors directement rendu, mais le contenu encapsulé dans l'expression doit être additionné à l'aide de _p.

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

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

        _p += '</div>';

      }
      return _p;
    }

Le rendu des expressions et des valeurs est différent. Il a non seulement la syntaxe if, mais aussi les instructions if else, if else if et for loop.

Mais quel que soit le type d'expression, nous pouvons résumer certaines règles de la structure de rendu requise ci-dessus. 1. Ajoutez un point-virgule avant l'expression pour isoler logiquement le code précédent 2. L'expression elle-même est directement sélectionnée sans guillemets. Rendu 3. Le contenu après l'expression doit être additionnée avec _p et assignée à _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;
}

Le dernier corps de fonction compilé de render

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

Résultats finaux:

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



Rendre le code HTML

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

Dans l'attente du résultat:

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

<?- ?>Utilisé pour marquer la chaîne html de sortie

Le rendu du code html est très simple, il suffit d'ajouter un nouveau standard dans la fonction templateParse et de raccorder la chaîne html dans le jugement conditionnel.

Les changements sont les suivants

 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;
  }

Cependant, il n'est pas sûr de simplement épisser la chaîne html.Afin d'éviter les attaques xss, nous devons échapper les caractères spéciaux dans la chaîne html.

Modifiez le code comme suit pour atteindre l'objectif d'encodage des caractères spéciaux.

   
    //将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;
}

Résultat de sortie:

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



Code final

Le code final est le suivant, environ 70 lignes de code peuvent implémenter un moteur de modèle simple qui inclut le rendu de valeur, le rendu d'expression et le rendu de chaîne html. Si vous avez besoin d'autres fonctions, vous pouvez le développer et l'améliorer vous-même.

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;
}

Je suppose que tu aimes

Origine blog.csdn.net/brokenkay/article/details/110533559
conseillé
Classement