ES6专题之从 Babel 编译分析字符串模板(三)
Opened this issue · 0 comments
前言
在字符串没出现之前,我们大多运用的方法是字符串拼接,和 lodash 等函数库的模板引擎。下面让我们看看字符串模板给我们带来什么开发优势。
ES5 字符串拼接与 ES6 字符串模板的对比:
// ES5
let name = "Chris";
// ES6
let name = `Chris`; // Chris
// ES5
let world = ' World';
let message = "Hello" + World; // Hello World
// ES6 变量嵌套
let world = ' World';
let message = `Hello${World}`; // Hello World
我们发现字符串模板的一个强大功能就是 嵌套变量。
变量嵌套
模板字符串支持嵌入变量,只需要将变量名写在 ${} 之中,当然任意的 JavaScript 表达式都是可以的:
let x = 1, y = 2;
let z = () => {return 3};
let message = `<ul><li>${x}</li><li>${x + y}</li><li>${z()}</li></ul>`
console.log(message); // <ul><li>1</li><li>3</li><li>3</li></ul>
// 编译后
var x = 1,
y = 2;
var z = function z() {
return 3;
};
var message = "<ul><li>".concat(x, "</li><li>").concat(x + y, "</li><li>").concat(z(), "</li></ul>");
console.log(message); // <ul><li>1</li><li>3</li><li>3</li></ul>
从编译的结果发现,${}
内部是运用字符串的 concat
方法进行字符串拼接。
模板字符串也支持多层嵌套:
let arr = [{value: 1}, {value: 2}];
let message = `
<ul>
${arr.map((item) => {
return `
<li>${item.value}</li>
`
})}
</ul>
`;
console.log(message);
// 编译后
var arr = [{
value: 1
}, {
value: 2
}];
var message = "\n\t<ul>\n\t\t".concat(arr.map(function (item) {
return "\n\t\t\t\t<li>".concat(item.value, "</li>\n\t\t\t");
}), "\n\t</ul>\n");
console.log(message);
注意,在 li
标签中间多了一个逗号,这是因为当大括号中的值是一个数组,而不是字符串,最终其转为字符串,比如一个数组 [1, 2, 3] 就会被转为 "1,2,3"。
let arr = [1,2,3]
let message = `${arr}` // "1,2,3"
// 编译后
var arr = [1, 2, 3];
var message = "".concat(arr); // "1,2,3"
用数组的 join 方法去除逗号:
let arr = [{value: 1}, {value: 2}];
let message = `
<ul>
${arr.map((item) => {
return `
<li>${item.value}</li>
`
}).join('')}
</ul>
`;
console.log(message);
这样我们开发的话会运用这些的话就可以了。但是字符串模板还有一个功能,就是 模板标签
模板标签
模板标签是一个非常重要的能力,模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。
举个例子:
alert`123`
// 等同于
alert(123)
// 编译后
function _templateObject() {
var data = _taggedTemplateLiteral(["123"]);
_templateObject = function _templateObject() {
return data;
};
return data;
}
function _taggedTemplateLiteral(strings, raw) { if (!raw) { raw = strings.slice(0); } return Object.freeze(Object.defineProperties(strings, { raw: { value: Object.freeze(raw) } })); }
alert(_templateObject());
我们发现,会有一个 _taggedTemplateLiteral
负责处理字符串的内容。
// literals 文字
// 注意在这个例子中 literals 的第一个元素和最后一个元素都是空字符串
function message(literals, value1, value2) {
console.log(literals); // [ "", ", I am ", "" ]
console.log(value1); // Hi
console.log(value2); // Chris
}
let x = 'Hi', y = 'Chris';
var res = message`${x}, I am ${y}`;
console.log(res);
直接调用函数的话,函数的第一个参数是一个数组,该数组的成员是模板字符串中那些没有变量替换的部分,再看看我们之前经过 Babel 编译的得到 _templateObject
函数,功能就是对变量和字符串的分离操作。
我们也可以将字符串拼合回去
let x = 'Hi', y = 'Chris';
let msg = message`${x}, I am ${y}`;
function message(literals) {
let result = '';
let i = 0;
while (i < literals.length) {
result += literals[i++];
if (i < arguments.length) {
result += arguments[i];
}
}
return result;
}
msg // "Hi, I am Chris"
标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。
let message =
SaferHTML`<p>${sender} has sent you a message.</p>`;
function SaferHTML(templateData) {
let s = templateData[0];
for (let i = 1; i < arguments.length; i++) {
let arg = String(arguments[i]);
// 替换中的转义特殊字符.
s += arg.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
// 不转义的模板中的特殊字符.
s += templateData[i];
}
return s;
}
上面代码中,sender
变量往往是用户提供的,经过SaferHTML
函数处理,里面的特殊字符都会被转义。
let sender = '<script>alert("abc")</script>'; // 恶意代码
let message = SaferHTML`<p>${sender} has sent you a message.</p>`;
message
// <p><script>alert("abc")</script> has sent you a message.</p>
实现一个字符串模板
传统的 lodash
、underscore
等函数库都有对应的模板引擎,下面我们从模板字符串的功能出发,实现一个简易的模板引擎。
// lodash模板引擎实例
let name = "Chris"
let tpl = "hello: <%= name %>";
let compiled = _.template(tpl);
compiled({ name: 'Kevin' }); // "hello: Chris"
在 HTML 文件中:
<ul id="name_list"></ul>
<script type="text/html" id="user_tmpl">
<%for ( var i = 0; i < users.length; i++ ) { %>
<li>
<a href="<%=users[i].url%>">
<%=users[i].name%>
</a>
</li>
<% } %>
</script>
<script>
var container = document.getElementById("name_list");
var data = {
users: [
{ "name": "Chris", "url": "http://localhost" },
{ "name": "James", "url": "http://localhost" },
{ "name": "Kobe", "url": "http://localhost" }
]
}
var precompile = _.template(document.getElementById("user_tmpl").innerHTML);
var html = precompile(data);
container.innerHTML = html;
</script>
模板引擎处理后
实现思路
依然是以这段模板字符串为例:
<%for ( var i = 0; i < users.length; i++ ) { %>
<li>
<a href="<%=users[i].url%>">
<%=users[i].name%>
</a>
</li>
<% } %>
将这段代码转换为这样一段程序:
// 模拟数据
var users = [{"name": "Kevin", "url": "http://localhost"}];
var p = [];
for (var i = 0; i < users.length; i++) {
p.push('<li><a href="');
p.push(users[i].url);
p.push('">');
p.push(users[i].name);
p.push('</a></li>');
}
// 最后 join 一下就可以得到最终拼接好的模板字符串
console.log(p.join('')) // <li><a href="http://localhost">Kevin</a></li>
我们注意,模板其实是一段字符串,我们怎么根据一段字符串生成一段代码呢?很容易就想到用 eval。
然后我们会发现,为了转换成这样一段代码,我们需要将<%xxx%>
转换为 xxx
,其实就是去掉包裹的符号。
下面我们用正则实现:
- 将
%>
替换成p.push('
- 将
<%
替换成');
- 将
<%=xxx%>
替换成');p.push(xxx);p.push('
我们来举个例子:
<%for ( var i = 0; i < users.length; i++ ) { %>
<li>
<a href="<%=users[i].url%>">
<%=users[i].name%>
</a>
</li>
<% } %>
按照这个替换规则会被替换为:
');for ( var i = 0; i < users.length; i++ ) { p.push('
<li>
<a href="');p.push(users[i].url);p.push('">
');p.push(users[i].name);p.push('
</a>
</li>
'); } p.push('
这样肯定会报错,毕竟代码都没有写全,我们在首和尾加上部分代码,
变成:
// 添加的首部代码
var p = []; p.push('
');for ( var i = 0; i < users.length; i++ ) { p.push('
<li>
<a href="');p.push(users[i].url);p.push('">
');p.push(users[i].name);p.push('
</a>
</li>
'); } p.push('
// 添加的尾部代码
');
我们整理下这段代码:
var p = []; p.push('');
for ( var i = 0; i < users.length; i++ ) {
p.push('<li><a href="');
p.push(users[i].url);
p.push('">');
p.push(users[i].name);
p.push('</a></li>');
}
p.push('');
恰好可以实现这个功能,不过还要注意一点,要将换行符替换成空格,防止解析成代码的时候报错,不过在这里为了方便理解原理,就只在代码里实现。
完整代码:
function tmpl(str, data) {
var str = document.getElementById(str).innerHTML;
var string = "var p = []; p.push('" +
str
.replace(/[\r\t\n]/g, "")
.replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
.replace(/<%/g, "');")
.replace(/%>/g,"p.push('")
+ "');"
eval(string)
return p.join('');
};
在 HTML 文件中:
<ul id="name_list"></ul>
<script type="text/html" id="user_tmpl">
<%for ( var i = 0; i < users.length; i++ ) { %>
<li>
<a href="<%=users[i].url%>">
<%=users[i].name%>
</a>
</li>
<% } %>
</script>
<script>
function tmpl(str, data) {
var str = document.getElementById(str).innerHTML;
var string = "var p = []; p.push('" +
str
.replace(/[\r\t\n]/g, "")
.replace(/<%=(.*?)%>/g, "');p.push($1);p.push('")
.replace(/<%/g, "');")
.replace(/%>/g, "p.push('")
+ "');"
eval(string)
return p.join('');
};
var users = [
{ "name": "Chris", "url": "http://localhost" },
{ "name": "James", "url": "http://localhost" },
{ "name": "Kobe", "url": "http://localhost" }
]
var results = document.getElementById("name_list");
results.innerHTML = tmpl("user_tmpl", users);
</script>
结果:
这样我们就实现了一个支持文本和嵌套变量的简易模板引擎
模板字符串的限制
值得注意的是,模板字符串内部对敏感标签进行了转义,来避免 XSS
攻击,比如恶意注入 script
标签。
var results = document.getElementById("name_list");
results.innerHTML = ` <script>alert(1)</script1>` // 报错