标签模板

标签模板 #

模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为标签模板功能(tagged template)。

(function (log, S) {
    log `123`;// [ '123' ]
    // 等同于
    log(123);// 123
})(console.log, String)

标签模板其实不是模板,而是函数调用的一种特殊形式标签指的就是函数,紧跟在后面的模板字符串就是它的参数


基础说明 #

第一个参数raw属性 #

模板处理函数的第一个参数模板字符串数组),还有一个raw属性。

(function (log, S) {
    log `123`;// ["123", raw: Array[1]]
    // 等同于
    log(123);// 123
})(console.log, String)

上面代码中,console.log接受的参数,实际上是一个数组

数组有一个raw属性,保存的是转义后的原字符串

请看下面的例子。

(function (log, S) {
    function tag(strings) {
        const currRaw = strings.raw;
        log(strings);
        log(currRaw);
        log(typeof currRaw);
        for(const item of currRaw){
            log(item);
        }
    }
    tag`First line\nSecond line`;
    // [ 'First line\nSecond line' ]
    // [ 'First line\\nSecond line' ]
    // object
    // strings.raw[0] 为 "First line\\nSecond line"
    // First line
    // Second line

    // tag('First line\nSecond line');
    // undefined
    // undefined
    // TypeError: currRaw is not iterable
})(console.log, String)

上面代码中,tag函数的第一个参数strings,有一个raw属性,也指向一个数组

数组成员strings数组完全一致。

比如,strings数组["First line\nSecond line"],那么strings.raw数组就是["First line\\nSecond line"]

两者唯一的区别,就是字符串里面的斜杠都被转义了。

比如,strings.raw 数组会将\n视为\\n两个字符,而不是换行符

这是为了方便取得转义之前的原始模板设计的。


使用变量 #

但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数

(function (log, S) {
    function tag(strings) {
        const currRaw = strings.raw;
        log(strings);
        log(currRaw);
        log(typeof currRaw);
        for(const item of currRaw){
            log(item);
        }
    }
    const a = 5;
    const b = 10;

    tag`Hello ${ a + b } world ${ a * b }`;
    // [ 'Hello ', ' world ', '' ]
    // object
    // Hello
    //  world
})(console.log, String)

上面代码中,模板字符串前面有一个标识名tag,它是一个函数


其他参数说明 #

tag函数的其他参数,都是模板字符串各个变量被替换后的

const a = 5;
const b = 10;

tag`Hello ${ a + b } world ${ a * b }`;

由于上面案例中,模板字符串含有两个变量,因此tag会接受到value1value2两个参数

tag函数所有参数实际值如下。

参数顺序标识参数值说明
第一个参数['Hello ', ' world ', '']
第二个参数15
第三个参数50

也就是说,tag函数实际上以下面的形式调用。

(function (log, S) {
    function tag(strings) {
        const currRaw = strings.raw;
        log(strings);
        log(currRaw);
        log(typeof currRaw);
        for(const item of currRaw){
            log(item);
        }
    }
    const a = 5;
    const b = 10;

    tag`Hello ${ a + b } world ${ a * b }`;
    // [ 'Hello ', ' world ', '' ]
    // object
    // Hello
    //  world
    // 等同于
    tag(['Hello ', ' world ', ''], 15, 50);
    // [ 'Hello ', ' world ', '' ]
    // undefined
    // undefined
    // TypeError: currRaw is not iterable
})(console.log, String)

返回值说明 #

整个表达式返回值,就是tag函数处理模板字符串后的返回值

函数tag依次会接收到多个参数


function tag(stringArr, value1, value2){
  // ...
}

// 等同于

function tag(stringArr, ...values){
  // ...
}

tag函数的第一个参数是一个数组,该数组成员模板字符串中那些没有变量替换的部分。

也就是说,变量替换只发生在数组的第一个成员与第二个成员之间、第二个成员与第三个成员之间,以此类推。


案例说明 #

自定义使用 #

我们可以按照需要编写tag函数的代码。下面是tag函数的一种写法,以及运行结果。

(function (log, S) {

    const a = 5;
    const b = 10;

    function tag(s, v1, v2) {
        log(s[0]);
        log(s[1]);
        log(s[2]);
        log(v1);
        log(v2);
        return "OK";
    }

    log(tag `Hello ${ a + b } world ${ a * b}`);
    // "Hello "
    // " world "
    // ""
    // 15
    // 50
    // "OK"
})(console.log, String)

复杂使用 #

下面是一个更复杂的例子。

(function (log, S) {

    // 注意不要使用箭头函数,否则会导致结果会是
    // The total is function String() { [native code] } ( with tax)
    // const passthru = (literals)=>{
    const passthru = function(literals){
        let result = '';
        let i = 0;
        log(...literals);
        while (i < literals.length) {
            result += literals[i++];
            if (i < arguments.length) {
                result += arguments[i];
            }
        }

        return result;
    }
    const total = 30;
    const msg = passthru `The total is ${total} (${total*1.05} with tax)`;


    log(msg);
    // The total is   (  with tax)
    // "The total is 30 (31.5 with tax)"
})(console.log, String)

上面这个例子展示了,如何将各个参数按照原来的位置拼合回去。


结合rest参数 #

passthru函数采用 rest 参数的写法如下。


function passthru(literals, ...values) {
  let output = "";
  let index;
  for (index = 0; index < values.length; index++) {
    output += literals[index] + values[index];
  }

  output += literals[index]
  return output;
}

过滤 HTML 字符串 #

标签模板的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。

const SaferHTML = function (templateData) {
    let s = templateData[0];
    for (let i = 1; i < arguments.length; i++) {
        const arg = String(arguments[i]);

        // Escape special characters in the substitution.
        s += arg.replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");

        // Don't escape special characters in the template.
        s += templateData[i];
    }
    return s;
}
const message =
        SaferHTML `<p>${sender} has sent you a message.</p>`;

上面代码中,sender变量往往是用户提供的,经过SaferHTML函数处理,里面的特殊字符都会被转义。

(function (log, S) {
    const SaferHTML = function (templateData) {
        let s = templateData[0];
        for (let i = 1; i < arguments.length; i++) {
            const arg = String(arguments[i]);

            // Escape special characters in the substitution.
            s += arg.replace(/&/g, "&amp;")
                .replace(/</g, "&lt;")
                .replace(/>/g, "&gt;");

            // Don't escape special characters in the template.
            s += templateData[i];
        }
        return s;
    }
    const sender = '<script>alert("abc")</script>'; // 恶意代码
    const message =
        SaferHTML `<p>${sender} has sent you a message.</p>`;
    log(message);// <p>&lt;script&gt;alert("abc")&lt;/script&gt; has sent you a message.</p>
})(console.log, String)

多语言转换 #

标签模板的另一个应用,就是多语言转换(国际化处理)。


i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!`

// "欢迎访问xxx,您是第xxxx位访问者!"

模板字符串本身并不能取代 Mustache 之类的模板库,因为没有条件判断循环处理功能,但是通过标签函数,你可以自己添加这些功能。


// 下面的hashTemplate函数
// 是一个自定义的模板处理函数
const libraryHtml = hashTemplate`
  <ul>
    #for book in ${myBooks}
      <li><i>#{book.title}</i> by #{book.author}</li>
    #end
  </ul>
`;

嵌入其他语言 #

除此之外,你甚至可以使用标签模板,在 JavaScript 语言之中嵌入其他语言


jsx`
  <div>
    <input
      ref='input'
      onChange='${this.handleChange}'
      defaultValue='${this.state.value}' />
      ${this.state.value}
   </div>
`

上面的代码通过jsx函数,将一个 DOM 字符串转为 React对象。你可以在 GitHub 找到jsx函数的具体实现


运行Java代码 #

下面则是一个假想的例子,通过java函数,在 JavaScript 代码之中运行 Java 代码。

java`
class HelloWorldApp {
  public static void main(String[] args) {
    System.out.println("Hello World!"); // Display the string.
  }
}
`
HelloWorldApp.main();

Build by Loppo 0.6.16