timelessover/blog

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, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");

    // 不转义的模板中的特殊字符.
    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>&lt;script&gt;alert("abc")&lt;/script&gt; has sent you a message.</p>

实现一个字符串模板

传统的 lodashunderscore 等函数库都有对应的模板引擎,下面我们从模板字符串的功能出发,实现一个简易的模板引擎。

// 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,其实就是去掉包裹的符号。

下面我们用正则实现:

  1. %> 替换成 p.push('
  2. <% 替换成 ');
  3. <%=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>` // 报错