hi

ES6模版方法:Handlebar杀手?

hi · 2016-11-28翻译 · 1310阅读 原文链接

我在FT上最近有和其中一个一流开发者@bjfletcher的讨论。我们正在考虑替换模板语言的可行性,例如Handlebars和ES6模版方法的某些方面,Ben建议把我们的对话发布到网上是个不错的主意 - 带一个有诱惑性的标题链接及所有内容。

所以什么是模版语法呢?它们怎么做到像Handlebars那样复杂的库呢?静下心来,亲爱的读者,让我们找出它...

一个由模板语法引起的崩溃

我以前在这里讨论过ES6 - 我们看一下 Symbols (你应该读一下,我们今天将用到Symbols), Reflect 及我们应该看看Proxies。即使也能收到更多的ES6内容。另一个ES6很大的布局就是被称作 模板语法 的那些事。在ES6领域中他们对字符串很多地方都做一点修正。所以让我们对这些特性做个简要介绍:

一个模板语法

ES6模板语法,如它的命名建议,随之而来是一个新的字面量语法。它们用返回标记()来表示开始和结束(就如字符串用引号(“ ' ' ”或“ " " ”))。在那些返回标记中,模板语法是在运行时被解析(解释执行)的,以便去提供一些新的特性。

支持换行

其中一个模板语法的简洁的特性, 就是它不像字符串语法,它们具有换行这个特点。字符在跨多行时看起来会整洁的多,你不再需要跨行写一长串难过的语法连接符。这看起来像一个小的,也许无关的特性,但是它们对我们的用例非常重要 - 让模板更语言化! 这里有个例子:

var templateString = `
  Hello
  World
`;

// 等同于字符串:
var oldString = '\n' +
'  Hello\n' +
'  World\n';

嵌入表达式

模板语法 最重要的特性,就是它们有一个新的内置字符串语法允许内联。简单的用'${'和'}' 内置会被执行的代码,并且字符添加至对应的地方。如果你以前用过Ruby = 这个概念将会让你感到熟悉(Ruby有一个特殊的#{} 标志特性来替代。)这是JS第一次去这么做。这里有个Ruby的例子:

``puts "1+1 = #{ 1 + 1 }" # Outputs: "1+1 = 2"``

这里有一个同类的例子,用ES5(旧版JS)代码编写:

``console.log('1+1 = ' + (1 + 1)); // Outputs: "1+1 = 2"``

现在这里有个例子是用ES6代码写的 - 这就是我们用到的令人惊艳的新 模板语法 嵌入表达式!

``console.log(`1+1 = ${ 1 + 1 }`); // Outputs: "1+1 = 2"``

在这个字符串替换中,代码 (1 + 1)被执行,并且值(2)被强制转换成字符串("2")然后添加字符串到适当的位置。这里我们也能够使用变量 - 他们在作用域中如所期望的那样运行:

var place = 'World';
console.log(`Hello ${place}`); // Outputs: "Hello World"

function greeting(place) {
  return `Hello ${place}`;
}
console.log(greeting('readers')); // Outputs: "Hello readers"

这个内插法的能力给我们一些很好的去创建函数的能力,它能够获取数据,并去生成一个字符串 - 这就是所有模板语言的基础! 然而,这里有一些我们仍旧需要但错过的细节,就是必须转义坏的代码(任何好模板语言的核心特性)。多么好的一个延续啊...

被标记的模板语法

这是我们讨论模板语法的最后一个重要的特性,被标记的模板语法提供给函数一个暴露模板语法组件部分的方法。模板标签仅是普通的函数,但是他们必须被不同的方式调用才有用。如果你尝试调用一个普通方法去调用模板语法函数你只能从第一个参数得到一个字符串。如果你用特殊的被标记模板语法调用方式,那么你就会得到一个模板语法的组合体。你将会获得所有字符串碎片,并且得到所有每个插入式表达式的结果。

好了,所有以上说的干货都比较瓷实,所以让我们举例说明:

// here is our normal function. It takes two arguments, `strings` and `value1`
function greeting(strings, value1) {
  console.log('Strings:', strings);
  console.log('Value1:', value1);
}

var place = 'World';

// If we try to call our function in the normal way, we're really just passing a string as the first argument
greeting(`Hello ${place}`);
// Log output:
// Strings: 'Hello World'
// Value1: undefined

// invoking our function as a Tagged Template Literal - note the lack of parenthesis
greeting`Hello ${place}`
// Log output:
// Strings: ['Hello ', '']
// Value1: 'World'

注意到当用普通方法调用调用greeting()的时候,是带着模板字符串作为第一个参数的 - 一个已被无安全渲染的参数("Hello World")并且传入到了greeting(),但是当我们移除括号时 - 使其变成一个被标记的模板语法 - 我们得到一个字符串数组(['Hello ', '']) 作为第一个参数,并且第二个参数是我们被插入变量place 的值 ('World')。这些是每一个模板的组件部分 - 我们能把这些都组合在一起并重制一个字符串,正如你所期望的:

function greeting(strings, value1) {
  return strings[0] + value1 + strings[1];
}
var place = 'World';
console.log(greeting`Hello ${place}`)); // Outputs: "Hello World"

现在每个在模板语法中的表达式插入法会被传入作为一个新参数到函数内。这意味着如果我们想hold住所有可能的值的话,我们需要去写一个可变的函数(仅是一个花哨的方式去告知它会带来一个无限量的参数域)。当让每次这里有一个表达式插入法,这里将会有两个字符串在字符串数组中 - 一个是左边的插入值,另一个是右边的插入值。所以如果模板语法有一个表达式插入法的话那么标记函数将会有一个包含两个字符创的数组,并且有第二个参数。如果模板语法有两个表达式,那么标记函数将会有一个三个字符串的数组,并且有第二个和第三个参数。

要创建一个可以与模板语法一起使用的函数,可以使用或不使用表达式插入法,我们最好只是循环遍历所有的字符串值,并将它们与所有值连接。 让我们为此写一些代码!

function getFullString(strings) {
  var interpolatedValues = [].slice.call(arguments, 1); // get the "variadic" values from arguments, make them an array
  return strings.reduce(function (total, current, index) { // use `reduce` to iterate over the array, returning a new value
    total += current; // append the string to the total string
    if (interpolatedValues.hasOwnProperty(index)) { // if there is an interpolatedValue with a matching index, append this too
      total += interpolatedValues[index];
    }
    return total;
  }, ''); // the starting value is an empty string (`''`)
}

var place = 'World';
getFullString`Hello ${place}!`; // outputs: "Hello World!"

我们能够用一些ES6技巧去简化如上的函数,让它变得更加紧凑和易读:

function getFullString(strings, ...interpolatedValues) { // `...` essentially slices the arguments for us.
  return strings.reduce((total, current, index) => { // use an arrow function for brevity here
    total += current;
    if (interpolatedValues.hasOwnProperty(index)) {
      total += interpolatedValues[index];
    }
    return total;
  }, '');
}

当然这个getFullString函数不做任何特殊的事情,模板语法还没有做。 我之前提到过,像Handlebars这样的模板语言的核心功能是自动转义输入。 HTML模板语言(如Handlebars)将会转义模板中注入的任何变量,以防止XSS(跨站点脚本)漏洞 - 基本上确保注入的变量不能呈现新的HTML,如`` 标记。 我们可以使用我们的标签函数做同样的转义:

function safeHTML(strings, ...interpolatedValues) { // `...` essentially slices the arguments for us.
  return strings.reduce((total, current, index) => { // use an arrow function for brevity here
    total += current;
    if (interpolatedValues.hasOwnProperty(index)) {
      total += String(interpolatedValues[index]).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
    }
    return total;
  }, '');
}

var badVariable = '`<script>deleteEverything()</script>`';
safeHTML`<div>${badVariable}</div>`; // Outputs: "<div>`<script>deleteEverything()</script>`</div>"

(顺便提一下,不要真的去写这样的html转义代码并把它发到生产环境上去,如 WebReflection指出的)那样,它仍有XSS漏洞,但这里提供了一个简单的解决示例:

用ES6模板字符串替换Handlebars(或Pug/Jade或Dust之类)

好了, 所以我们能跨越多行文本了,用原生ES6插入变量并转义html。这应该足够用ES6模板字符串去替换一个类似Handlebars的库了,对吗?

让我们看一下一个Handlebars模板例子。这是一个怎么使用Handlebars的简单的例子,我们将用这个作为一个衡量标准去对比ES6模板字符串是多么的激动人心:

var Handlebars = require('handlebars');
Handlebars.registerHelper('capitalize', function (options) {
  var string = String(options.fn(this));
  return string[0].toUpperCase() + string.slice(1);
});
var page = Handlebars.compile(`
  <h2>People</h2>
  <ul>
  {\{#each people}}
      <li>
        <span></span>
        {\{#if isAdmin}}
          <button>Delete </button>
        {\{/if}}
      </li>
  {\{/each}}
  </ul>
`);

page({ isAdmin: true, people: ['Keith', 'Dave', 'Amy' ] });

首次通过, ES6模板字符串

给出Handlebars的例子,如果我们去除Handlebars,并且仅尝试模板语法加上一个包装好的函数,我们用如下方法归结:

function template(strings, ...interpolatedValues) {
  return strings.reduce((total, current, index) => {
    total += current;
    if (interpolatedValues.hasOwnProperty(index)) {
      total += String(interpolatedValues[index]).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
    }
    return total;
  }, '');
}
function capitalize(string) {
  return string[0].toUpperCase() + string.slice(1);
}
var page = ({ people, isAdmin }) => template`
  <h2>People</h2>
  <ul>
  ${people.map(person => `
      <li>
        <span>${capitalize(person)}</span>
        ${isAdmin ?
          `<button>Delete ${capitalize(person)}</button>`
        : ''}
      </li>
  `)}
  </ul>
`;

page({ isAdmin: true, people: ['Keith', 'Dave', 'Amy' ] });

这就非常整洁了!好的,我猜我们已经完成这里了...

但没那么快。这里有一些非常怪异的函数碎片。首先,这里的模板标签在模板标签内部,意味着到处散落着很多小的恼人的语法。其次,我们使用一个三元的(${isAdmin})强制我们包含了“标签(elsey)”值(那些怪异的: ''}碎片)。不仅是这些,但Handlebars给了我们其它协助模块的加载,例如unless, withlookup。Handlebars也提供了一个机械化的方法来注册自定义协助模块组件,我们确定能那么做!所以该怎么修正这两者的那些问题呢?

附加的ES6模板helpers

幸运的是,用了ES6模板字符串 - 我们能轻松的制造一个新协助模块, 它们只是些函数!让我们制造一个helpers(协助模块)对象,同时也能创建新的helpers:

function template(strings, ...interpolatedValues) {
  return strings.reduce((total, current, index) => {
    total += current;
    if (interpolatedValues.hasOwnProperty(index)) {
      total += String(interpolatedValues[index]).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
    }
    return total;
  }, '');
}
var helpers = {
  if: (condition, thenTemplate, elseTemplate = '') => {
    return condition ? thenTemplate : elseTemplate;
  },
  unless: (condition, thenTemplate, elseTemplate) => {
    return !condition ? thenTemplate : elseTemplate;
  },
  registerHelper(name, fn) => {
    helpers[name] = fn;
  }
};
helpers.registerHelper('capitalize', (string) => {
  return string[0].toUpperCase() + string.slice(1).toUpperCase();
});

var page = ({ people, isAdmin }) => template`
  <h2>People</h2>
  <ul>
  ${people.map(person => `
      <li>
        <span>${helpers.capitalize(person)}</span>
        ${helpers.if(isAdmin,
          `<button>Delete ${helpers.capitalize(person)}</button>`
        )}
      </li>
  `)}
  </ul>
`;

page({ isAdmin: true, people: ['Keith', 'Dave', 'Amy' ] });

好的,这变得更整洁一些了。这里用了更少轻量级的语法,并且我们有一个更好的方法去添加有用的helpers。这里仍然到处有很多返回标记,有办法除掉他们吗?

带符号(Symbol)的es6模板字符串

所以,如我们能够解析模板那样,并得到所有的值 - 我们能利用如哨兵值那样符号(Symbol)的唯一性。哨兵值仅是唯一的值,我们能通过触发具体的代码路径检测到代码。这里我们能用哨兵值去触发我们的模板特性,好比一个if条件判断。让我们看一下我们能用符号(Symbol)哨兵值做什么,通过替换if并增加一个友好可读的endif

var startBlockSentinel = Symbol('blockSentinel');
var ignoreBlockSentinel = Symbol('ignoreBlockSentinel');
var endBlockSentinel = Symbol('endBlockSentinel');
var helpers = {
  if: (condition, thenTemplate, elseTemplate = '') => {
    return condition ? startBlockSentinel : ignoreBlockSentinel;
  },
  end: () => {
    return endBlockSentinel;
  },
  unless: (condition, thenTemplate, elseTemplate) => {
    return !condition ? startBlockSentinel : ignoreBlockSentinel;
  },
  registerHelper(name, fn) => {
    helpers[name] = fn;
  },
};

function template(strings, ...interpolatedValues) {
  const blockNest = [];
  return strings.reduce((total, current, index) => {
    if (blockNest.includes(ignoreBlockSentinel)) { // If at any point we chose to ignore this block, skip this render pass
      return total;
    }

    total += current;
    if (interpolatedValues.hasOwnProperty(index)) {
      var value = interpolatedValues[index];
      if (value === startBlockSentinel || value === ignoreBlockSentinel) {
        blockNest.push(value);
      }
      if (value === endBlockSentinel) {
        blockNest.pop();
      }
      total += String(interpolatedValues[index]).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
    }
    return total;
  }, '');
}

helpers.registerHelper('capitalize', (string) => {
  return string[0].toUpperCase() + string.slice(1).toUpperCase();
});

var page = ({ people, isAdmin }) => template`
  <h2>People</h2>
  <ul>
  ${people.map(person) => `
      <li>
        <span>${helpers.capitalize(person)}</span>
        ${helpers.if(isAdmin)}
        <button>Delete ${helpers.capitalize(person)}</button>
        ${helpers.end()}
      </li>
  `)}
  </ul>
`;

page({ isAdmin: true, people: ['Keith', 'Dave', 'Amy' ] });

嗨,来看看吧!我们增加一个销量的代码到我们的模板函数内,并且我们计划从所有易于混淆的语法中简洁化我们的模板,我们不用再大量的括号和返回标记中迷失了,取而代之的是友好可读的函数调用。

等等... 这些代码真的更好了吗?

这是一个很好地问题。我们总是问自己相同的问题,就表达语法而言,总是质问如果能做的比Handlebars已经提供给我们的更好。我们真的不再需要使我们的代码更简洁,也许他们不需要比曾经的Handlebars更快(当完全编译后),所以问题来了,这样做值得吗?

最后,如你现在期望的那样 - 我认为答案不完全是否定的。这是一个有趣的体验,并展示了一些ES6很酷的特性, 但是有Handlebars的存在 - 有很多很棒的特性及开发者已经花了大量时间去改bug和做相关前沿情况的调研。而我们所有ES6的这些解决方案,正是做这些巨大承诺的时刻,就是让ES6底层达到Handlebars之类曾经有的那样精致的程度。

肯定这曾是一个完全浪费时间的事情吗?我们曾从中学到了什么?我们带走了什么?好的,我很高兴你这么问,亲爱的读者(你曾这么问过对吗?)。

微模板是最好的解决方案

真实的答案是,我认为,我们的解决方案没有比Handlebars之类的模板更有表现力,这是一个“整体模板”的基础概念,但在它已有的表现中被突破了。

如果你花费了很多时间去做React,或任何函数式编程,那么你已经相当熟悉模板的拆分概念 - 在复杂的情况和条件下 - 在一系列更小的模板中更容易去管理等。我相信这就是我们能用 模板语法 的先进性。我们已有的模板做了很多复杂的事情!让我们把它们拆分成更小巧的模板吧:

var capitalize = (string) => {
  return string[0].toUpperCase() + string.slice(1);
};

var adminDelete = ({ person }) => {
  return `<button>Delete ${capitalize(person)}</button>`;
}
var personRecord = ({ person, deleteButton }) => {
  return `
  <li>
    <span>${capitalize(person)}</span>
    ${deleteButton}
  </li>
`;
}

var peopleList = ({ people, isAdmin }) => {
  return `
  <ul>
    ${people.map(person => personRecord({
      person,
      deleteButton: isAdmin ? adminDelete(person) : ''
    })).join('')}
  </ul>
`;
}

var page = ({ people, isAdmin }) => {
  return `
  <h2>People</h2>
  ${peopleList({ people, isAdmin })}
`;
}

通过这些更小的函数,我们制造了更加利于管理的代码基础,因为:

  1. 小函数更易于测试,它们用了更小的数据集合,并有更小范围的输出。

  2. 小函数是可组合的!你可以轻松地交换其中的部分,或者用很少的工作去重排或重构它们。

  3. 小函数是可复用的!例如personRecord能轻松被应用列表联系人使用,在朋友列表或模块列表使用等。

##结论

所以Handlebars(或Dust或Jade之类你正在或想用的模板语言)在做一个好的工作。ES6模板语法也许不能很快的替换那些库,但可以从那些正在做的库里得到点什么。如果你用了一个已存在的模板库,你能有信心那里不再有什么新的好事去撼动这一切了。

说到那里,也许这些模板语法给我们一个机会去退一步,并看到那些模板语言确实不能真的用正确的方法去修复问题?也许能解决很小的一部分, 但组成的模板碎片实际上更可取,且也许能被它们(模板碎片)拆分的更小巧 - 那么我们可以去除所有对模板库的需求了吗?

这里你是怎么认为的?我想听到你的想法,无论主题是娱乐化还是带有阴谋论的。随时可以发带有这些怨念的邮件到/dev/null里,否则跟往常一样,在下面的评论里说出来或者在Twitter上回复,账号是 @keithamus

译者hi尚未开通打赏功能

相关文章