序文
現在、フロントエンドフレームワークは、react、vue、angularのすべてに独自のテンプレートエンジンを備えていますが、ネイティブjsのテンプレートエンジン、特に下部にあるさまざまな文字列や式の処理を学習することで、他のフレームワークをよりよく理解するのに役立ちます。 。テンプレートデータはどのようにレンダリングされますか。
この記事では、アンダースコアのソースコードを利用し、約70行のコードを使用して、単純なテンプレートエンジンを実装します。これには、データレンダリング、式レンダリング(ifステートメントおよびforループと互換性があります)、html文字列レンダリングなどのコア関数が含まれます。 。
ユーザー側の呼び出しメソッドは次のとおりで、コンパイル関数を記述し、対応する結果を出力する予定です。
1.データをレンダリングします
<?= ?>
出力変数の値を表します
const data = {
country: 'China',
};
const template = compile(` //compile生成模板函数
<div>
<?= country?>
</div>
`);
console.log(template(data)); //template传入参数data,生成模板字符串
出力結果:
<div>China</div>
2.条件付き判断
<? ?>
条件付き判定の場合など、jsステートメントを直接書き込むことができます。forループ
const data = {
country: 'China',
gender: 'male',
};
const template = compile(`
<div>
<? if(gender === 'male'){?>
<?= country?>
<?}?>
</div>
`);
console.log(template(data));
出力結果:
<div>China</div>
3.ループステートメント
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));
出力結果:
<div><span>male0</span><span>male1</span><span>male2</span></div>
値のレンダリング
最初に、最も単純な要件が実装され、2つの値は次のようにレンダリングされます:
const data = {
country: 'China',
gender: 'male',
};
const template = compile('<div><?= country?><span><?= gender?></span></div>');
console.log(template(data));
期待される結果:
<div>China<span>male</span></div>
上記の実行コードから、コンパイルはテンプレート文字列を渡した後に新しい関数を返し、データがこの関数に渡されて実行されると最終結果が得られることがわかります。
ステートメント付き
テンプレートエンジンの実装について説明する前に、まず構文の知識ポイントを学びます。withを使用して冗長なオブジェクト呼び出しを回避するには、次のケースを見てください。
function test(data){
with(data){
age = 100;
}
console.log(data);
}
test({age:1})
結果:
{age: 100}
withは、コンテキストオブジェクトのスコープを制限できます。withパッケージのスコープ内では、定義されていない変数はデータの属性を表し、直接操作できます。たとえば、上記のwithがデータを渡す場合、age属性データプレフィックスを追加せずに、を使用して内部を直接操作できます。
withの機能は、テンプレートのコンパイルに役立ちます。withのアクションでは、オブジェクトプレフィックスを追加せずに、テンプレート文字列を属性呼び出しとして直接書き込むことができます。
論理分析
コンパイルは、テンプレート文字列を渡した後に新しい関数を返し、データを呼び出してコンパイル後に最終結果を返します。withの機能を使用すると、コンパイルを次の形式で記述して目的を達成できます。
function compile(string){
return function(data){
var _p = '';
with(data){
_p += '<div>'+country+'<span>'+gender+'</span>'+'</div>';
}
return _p;
}
}
文字列は今です '<div><?= country?><span><?= gender?></span></div>'
文字列が'<div>'+country+'<span>'+gender+'</span>'+'</div>'
テンプレートコンパイラを実現できるように変換された場合。しかし、現在直面している問題は、処理に関係なく文字列が返されるのは文字列の合計のみであり、上記の<div>
一重引用符のようにはできませんがcountry
、一重引用符がないことです。 。
したがって、コンパイルは上記のような関数を直接返すことはできません。内部に引用符を付けてタグを作成し、属性を引用符で囲まないようにするには、パラメーターを渡して関数を作成できます。
コンパイル関数は次のように変換されます。
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;
}
これで、完了したようstring
にtemplate_str
見えるように変換するだけで済みます。
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;
}
出力結果:
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>
式の処理
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>
論理分析
最初のアイデアは、テンプレート文字列を次の形式に変換することですが、実際には、if式もfor式も文字列に直接追加できないことがわかり、結果はエラーになります。
function render(data){
var _p += '';
with(data){
_p += '<div>' + if(country === "China"){ return '<span>'+gender+'</span>'; } + '</div>';
}
return _p;
}
式を文字列に直接追加できないため、式のロジックは文字列からのみ分離できます。変換は次のようになり、各式の前にセミコロンを追加し、前の文字列を最後に追加するためのコード。式のコンテンツは直接レンダリングされますが、式にラップされたコンテンツは_pを使用して合計する必要があります。
function render(data){
var _p += '';
with(data){
_p += '<div>';
if(country === "China")
{
_p+='<span>'+gender+'</span>';
}
_p += '</div>';
}
return _p;
}
式と値のレンダリングは異なります.if構文だけでなく、if else、if else if、およびforループステートメントもあります。
ただし、式の種類に関係なく、上記で必要なレンダリング構造からいくつかのルールを要約できます。1。式の前にセミコロンを追加して、前のコードを論理的に分離します。2。式自体は引用符なしで直接選択されます。レンダリング3.コンテンツ式を_pと加算し、_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;
}
レンダーの最後にコンパイルされた関数本体
var _p = '';with(data){_p +='<div> '; if(country === "China"){ _p+=' <span>'+ gender+'</span> ';}_p+=' </div>';}return _p;
最終結果:
<div> <span>male</span> </div>
HTMLコードをレンダリングする
const data = {
code: '<div style="color:red">name:张三</div>',
};
const template = compile('<div><?- country?></div>');
console.log(template(data));
結果を楽しみにしています:
<div><div style="color:red">name:张三</div></div>
<?- ?>
出力html文字列をマークするために使用されます
htmlコードのレンダリングは非常に簡単です。templateParse関数に新しいレギュラーを追加し、条件付き判定でhtml文字列をつなぎ合わせます。
変更点は以下のとおりです。
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;
}
ただし、html文字列を単にスプライスすることは安全ではありません.xss攻撃を防ぐために、html文字列の特殊文字をエスケープする必要があります。
特殊文字をエンコードする目的を達成するには、次のようにコードを変更します。
//将html字符串传递给 esacper 函数处理一遍
else if ($2) {
//渲染html字符串
str += '+ esacper(' + $2 + ') +';
}
//处理html字符串的特殊符号,预防xss攻击
function esacper(str) {
const keyMap = {
//需要转译的队列
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '&hx27;',
'`': '٠',
};
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;
}
出力結果:
<div><div style="color:red">name:张三</div></div>
最終コード
最終的なコードは次のとおりです。約70行のコードで、値レンダリング、式レンダリング、HTML文字列レンダリングを含む単純なテンプレートエンジンを実装できます。他の関数が必要な場合は、自分で拡張および拡張できます。
function compile(string) {
string = string.replace(/\n|\r\n/g, ''); //为了调用时兼容es6模板字符串
/**
* 将html字符串的特殊字符转义,预防xss攻击
*/
function esacper(str) {
const keyMap = {
//需要转译的队列
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '&hx27;',
'`': '٠',
};
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;
}