ES6模板文字可以在运行时替换(或重用)吗?


128

tl; dr:是否可以制作可重用的模板文字?

我一直在尝试使用模板字面量,但我想我只是不明白,而现在却感到沮丧。我的意思是,我认为我明白了,但是“它”不应该是它的工作方式或应该如何得到。它应该变得不同。

我看到的所有示例(甚至是带标签的模板)都要求“替换”必须在声明时而不是在运行时完成,这对我来说对于模板似乎完全没有用。也许我疯了,但是对我来说,“模板”是一个包含令牌的文档,这些令牌在您使用时(而不是在创建时)会被替换,否则只是一个文档(即字符串)。模板与令牌一起存储为令牌,并且在您评估模板时会评估这些令牌。

每个人都举一个可怕的例子,类似于:

var a = 'asd';
return `Worthless ${a}!`

很好,但是如果我已经知道了a,我会return 'Worthless asd'return 'Worthless '+a。重点是什么?说真的 好吧,关键是懒惰。更少的优点,更多的可读性。大。但这不是模板!不是恕我直言。MHO至关重要!恕我直言,问题是模板在声明时就进行了评估,因此,如果这样做,恕我直言:

var tpl = `My ${expletive} template`;
function go() { return tpl; }
go(); // SPACE-TIME ENDS!

由于expletive未声明,因此输出类似的信息My undefined template。超。实际上,至少在Chrome中,我什至无法声明模板;由于expletive未定义,因此将引发错误。我需要的是能够在声明模板后进行替换:

var tpl = `My ${expletive} template`;
function go() { return tpl; }
var expletive = 'great';
go(); // My great template

但是我不知道这怎么可能,因为这些并不是真正的模板。即使您说我应该使用标签,不,它们也不起作用:

> explete = function(a,b) { console.log(a); console.log(b); }
< function (a,b) { console.log(a); console.log(b); }
> var tpl = explete`My ${expletive} template`
< VM2323:2 Uncaught ReferenceError: expletive is not defined...

所有这些使我相信模板文字被严重地错误命名,应该被称为它们的真实名称:heredocs。我猜“文学”部分应该已经使我脱颖而出(例如,一成不变)?

我想念什么吗?有没有(好的)方法来制作可重用的模板文字?


我给你,可重用的模板文字

> function out(t) { console.log(eval(t)); }
  var template = `\`This is
  my \${expletive} reusable
  template!\``;
  out(template);
  var expletive = 'curious';
  out(template);
  var expletive = 'AMAZING';
  out(template);
< This is
  my undefined reusable
  template!
  This is
  my curious reusable
  template!
  This is
  my AMAZING reusable
  template!

这是一个幼稚的“助手”功能...

function t(t) { return '`'+t.replace('{','${')+'`'; }
var template = t(`This is
my {expletive} reusable
template!`);

...使其“更好”。

我倾向于称它们为模板Guteral,因为它们产生扭曲的感觉的区域。


1
它确实支持删除线(但不支持这种注释)。将您的文本放在<strike>标签中。
尖尖的2015年

ES6模板文字常用于老式的字符串插值。如果您想使用动态模板,请使用Handlebars等,或Pointy的带标签模板解决方案。
2015年

1
尽管有名称,模板字符串却没有模板。另请参见延迟执行ES6模板字符串
Bergi,2015年

8
您能否让您的帖子少一些保证?另外,您似乎打算以问答形式编写教程,如果这样做,请从问题中删除“ 我给您… ”部分,并将其发布为答案
Bergi 2015年

我注意到这里有很多好的答案。也许接受一个。
abalter

Answers:


86

为了使这些文字像其他模板引擎一样工作,需要一种中间形式。

最好的方法是使用Function构造函数。

const templateString = "Hello ${this.name}!";
const templateVars = {
    name: "world"    
}

const fillTemplate = function(templateString, templateVars){
    return new Function("return `"+templateString +"`;").call(templateVars);
}

console.log(fillTemplate(templateString, templateVars));

与其他模板引擎一样,您可以从文件等其他地方获取该字符串。

使用此方法可能会遇到一些问题,例如模板标记难以使用,但是如果您很聪明,可以添加这些标记。由于后期插值的原因,您也无法使用内联JavaScript逻辑。也可以通过一些思考来补救。


8
真好!您甚至可以使用new Function(`return \`${template}\`;`)
Ruben Stolk

通过调用方法或传入另一个模板的编译结果,可以通过参数来组合或“包含”这些模板。
Quentin Engles'9

Quentin“没有模板标签”是什么意思?谢谢!
mikemaccana

10
请注意,此模板字符串有点“隐藏”到了代码转换(即webpack)中,因此不会在客户端转换为足够兼容的东西(即IE11) ...!
Frank Nocke '18


64

您可以将模板字符串放入函数中:

function reusable(a, b) {
  return `a is ${a} and b is ${b}`;
}

您可以使用带标签的模板执行相同的操作:

function reusable(strings) {
  return function(... vals) {
    return strings.map(function(s, i) {
      return `${s}${vals[i] || ""}`;
    }).join("");
  };
}

var tagged = reusable`a is ${0} and b is ${1}`; // dummy "parameters"
console.log(tagged("hello", "world"));
// prints "a is hello b is world"
console.log(tagged("mars", "jupiter"));
// prints "a is mars b is jupiter"

这个想法是让模板解析器从变量“ slots”中分离出常量字符串,然后返回一个函数,该函数每次基于一组新的值将它们全部修补在一起。


3
@FelixKling可能是;我会检查并修复。编辑是的,看起来我遗漏了示例的重要部分,它是“可重用”的功能:)
Pointy

@FelixKling我不确定该怎么做,因为我根本无法回忆起当时的想法!
尖尖的

1
是的,既不是tbh也没有多大意思;)您可以随时将其删除.....但是reusable可以将其实现以使其返回函数,并且您可以在文字中使用${0}and ${1}而不是${a}and ${b}。然后,您可以使用该值来引用该函数的参数,类似于Bergi在他的上一个示例中所做的操作:stackoverflow.com/a/22619256/218196(或者我想它基本上是相同的)。
Felix Kling

1
@FelixKling好吧,我想我已经提出了至少在OP方面含糊其词的东西。
尖尖的2016年

3
如果结果实际上不是字符串,则标记的模板可能非常强大。例如,在我的一个项目中,我用它来进行AST节点插值。例如,可以expression`a + ${node}`使用现有的AST节点构建BinaryExpression节点node。在内部,我们插入一个占位符以生成有效代码,将其解析为AST并将占位符替换为传入的值。
菲利克斯·克林

45

可能最干净的方法是使用箭头功能(因为此时,我们已经在使用ES6)

var reusable = () => `This ${object} was created by ${creator}`;

var object = "template string", creator = "a function";
console.log (reusable()); // "This template string was created by a function"

object = "example", creator = "me";
console.log (reusable()); // "This example was created by me"

...对于标记的模板文字:

reusable = () => myTag`The ${noun} go ${verb} and `;

var noun = "wheels on the bus", verb = "round";
var myTag = function (strings, noun, verb) {
    return strings[0] + noun + strings[1] + verb + strings[2] + verb;
};
console.log (reusable()); // "The wheels on the bus go round and round"

noun = "racecars", verb = "fast";
myTag = function (strings, noun, verb) {
    return strings[0] + noun + strings[1] + verb;
};
console.log (reusable()); // "The racecars go fast"

这也避免了使用eval()Function()会导致编译器出现问题并导致很多速度下降。


我认为这是最好的方法之一,因为您可以在函数内注入一些代码myTag来做一些事情。例如,使用输入参数作为键来缓存输出。
WOW 2013年

我认为这是最好的答案。您还可以将参数添加到arrow函数中,我认为它使其更加简洁:var reusable = (value: string) => `Value is ${value}`
haggisandchips

13

2019年答案

注意:库最初希望用户清理字符串以避免XSS。库的版本2不再需要清理用户字符串(Web开发人员无论如何都应该这样做),因为它可以eval完全避免。

es6-dynamic-templatenpm上的模块可以执行此操作。

const fillTemplate = require('es6-dynamic-template');

与当前答案不同:

  • 它使用ES6模板字符串,而不是类似的格式。更新版本2使用类似的格式,而不是ES6模板字符串,以防止用户使用未经过滤的输入字符串。
  • 不需要this模板字符串
  • 您可以在单个函数中指定模板字符串和变量
  • 这是一个可维护的可更新模块,而不是StackOverflow中的copypasta

用法很简单。使用单引号作为模板字符串,稍后将解决!

const greeting = fillTemplate('Hi ${firstName}', {firstName: 'Joe'});

如果您将其与React Native一起使用,它将在Android上特别中断。Android节点运行时不支持动态模板,仅支持预填充模板
Oliver Dixon,

1
这是我在个人项目中使用的解决方案,它可以完美地工作。我实际上认为使用太多的库是个坏主意,尤其是对于像这样的小型实用程序。
奥利弗·迪克森


1
@kamil如果您(a)使用户能够创建b)不清理输入字符串,则只有XSS。我将添加一条警告,指出人们应该清理用户输入。
mikemaccana

1
这不会远程使用es6模板文字。尝试使用10 * 20 = ${10 * 20}它,虽然它可能是类似的格式,但它甚至不是远程的es6模板文字
gman

12

是的,您可以通过Function(或eval)将模板的字符串解析为JS来做到这一点-但这并不建议这样做,它会导致XSS攻击

相反,您可以按照以下方式以动态方式将对象字段安全地插入obj模板str

let inject = (str, obj) => str.replace(/\${(.*?)}/g, (x,g)=> obj[g]);


这是我使用的方法,效果很好。好的例子!是吗?在正则表达式中的*之后有帮助吗?我不是RegEx专家,但是我猜测是因为*表示零或更多(在这种情况下,您确实希望“更多”),所以不需要贪婪的限制吗?
Gen1-1

@ Gen1-1 .*?表示非贪婪方式 -如果删除,"?"
摘要

你是对的,我的错。我没有在模板中使用$,而是使用RegEx:/ {(\ w *)} / g,因为标记中没有空格,但是。*?也可以。我使用:function taggedTemplate(template, data, matcher) { if (!template || !data) { return template; } matcher = matcher || /{(\w*)}/g; // {one or more alphanumeric characters with no spaces} return template.replace(matcher, function (match, key) { var value; try { value = data[key] } catch (e) { // } return value || ""; }); }
Gen1-1

@ Gen1-1是否还有嵌套数据的可能?喜欢data = { a: 1, b: { c:2, d:3 } }-> b.c吗?
muescha

1
@muescha您将更改以下行:value = data [key],以使用递归并搜索整个数据对象和嵌套对象,直到找到该属性。示例:codereview.stackexchange.com/questions/73714/…mikedoesweb.com/2016/es6-depth-first-object-tree-search
Gen1-1

9

简化@metamorphasi提供的答案;

const fillTemplate = function(templateString, templateVars){
  var func = new Function(...Object.keys(templateVars),  "return `"+templateString +"`;")
  return func(...Object.values(templateVars));
}

// Sample
var hosting = "overview/id/d:${Id}";
var domain = {Id:1234, User:22};
var result = fillTemplate(hosting, domain);

console.log(result);


该代码比主要答案更容易解释。得到了我的投票:)
ymz

这应该允许您将变量或外部文件(在NodeJS中)用作模板或在运行时动态构建它们。不使用eval
B01

XSS漏洞?小提琴与恶意代码(变量var hostingHERE
卡米尔·基列夫斯基(KamilKiełczewski),

7

如果你不希望使用命令参数或上下文/命名空间来引用您的模板中,变量例如${0}${this.something}或者${data.something},你可以有一个模板函数,负责划定范围的为您服务。

如何调用此类模板的示例

const tempGreet = Template(() => `
  <span>Hello, ${name}!</span>
`);
tempGreet({name: 'Brian'}); // returns "<span>Hello, Brian!</span>"

模板功能:

function Template(cb) {
  return function(data) {
    const dataKeys = [];
    const dataVals = [];
    for (let key in data) {
      dataKeys.push(key);
      dataVals.push(data[key]);
    }
    let func = new Function(...dataKeys, 'return (' + cb + ')();');
    return func(...dataVals);
  }
}

在这种情况下,古怪的地方是您只需要传递一个返回ES6模板文字的函数(在示例中,我使用了箭头函数)。我认为这是我们需要的那种可重用插值的一个小折衷。

它在GitHub上:https : //github.com/Adelphos/ES6-Reuseable-Template


3
这是好事,但缩小(丘壑,FUNC等)是unnecessaery,“CB”是不是回调(这是完全同步的代码),你可以只使用Object.values()Object.keys()
mikemaccana

3

简短的回答是只使用_.template在lodash

// Use the ES template literal delimiter as an "interpolate" delimiter.
// Disable support by replacing the "interpolate" delimiter.
var compiled = _.template('hello ${ user }!');
compiled({ 'user': 'pebbles' });
// => 'hello pebbles!'

3

我想念什么吗?有没有[好的]方法来制作可重用的模板文字?

也许错过了一些东西,因为我对这个问题的解决方案对我来说是如此明显,以至于我很惊讶没有人在如此古老的问题中写下这一点。

我为此几乎一线:

function defer([first, ...rest]) {
  return (...values) => rest.reduce((acc, str, i) => acc + values[i] + str, first);
}

就这样。当我想重用模板并推迟替换的分辨率时,我只是这样做:

> t = defer`My template is: ${null} and ${null}`;
> t('simple', 'reusable');          // 'My template is: simple and reusable'
> t('obvious', 'late to the party'; // 'My template is: obvious and late to the party'
> t(null);                          // 'My template is: null and undefined'
>
> defer`Choose: ${'ignore'} / ${undefined}`(true, false); // 'Choose: true / false'

应用此标记将返回'function'(而不是'string')忽略所有传递给文字的参数。然后可以使用新参数调用它。如果参数没有相应的替换,它将变为'undefined'


扩展答案

这个简单的代码是有功能的,但是如果您需要更多详细的行为,则可以应用相同的逻辑,并且存在无穷的可能性。你可以:

  1. 利用原始参数:

您可以将传递给文字的原始值存储在构造中,并在应用模板时以创造性的方式使用它们。它们可以成为标志,类型验证器,函数等。这是一个使用它们作为默认值的示例:

    function deferWithDefaults([first, ...rest], ...defaults) {
      return (...values) => rest.reduce((acc, curr, i) => {
        return acc + (i < values.length ? values[i] : defaults[i]) + curr;
      }, first);
    }

然后:

    > t = deferWithDefaults`My template is: ${'extendable'} and ${'versatile'}`;
    > t('awesome');                 // 'My template is: awesome and versatile' 
  1. 写一个模板工厂:

通过将此逻辑包装在一个函数中来做到这一点,该函数需要一个自定义函数作为参数,该自定义函数可以在归约中应用(当加入模板文字片段时),并返回具有自定义行为的新模板。

    const createTemplate = fn => function (strings, ...defaults) {
      const [first, ...rest] = strings;
      return (...values) => rest.reduce((acc, curr, i) => {
        return acc + fn(values[i], defaults[i]) + curr;
      }, first);
    };

然后,例如,您可以编写在编写嵌入式html,css,sql,bash时自动转义或清理参数的模板...

    function sqlSanitize(token, tag) {
      // this is a gross simplification, don't use in production.
      const quoteName = name => (!/^[a-z_][a-z0-9_$]*$/.test(name) ? `"${name.replace(/"/g, '""')}"` : name);
      const quoteValue = value => (typeof value == 'string' ? `'${value.replace(/'/g, "''")}'` : value);
      switch (tag) {
        case 'table':
          return quoteName(token);
        case 'columns':
          return token.map(quoteName);
        case 'row':
          return token.map(quoteValue);
        default:
          return token;
      }
    }

    const sql = createTemplate(sqlSanitize);

有了这个天真的(我再说一遍,天真的!)sql模板,我们可以构建如下查询:

    > q  = sql`INSERT INTO ${'table'} (${'columns'})
    ... VALUES (${'row'});`
    > q('user', ['id', 'user name', 'is"Staff"?'], [1, "O'neil", true])
    // `INSERT INTO user (id,"user name","is""Staff""?")
    // VALUES (1,'O''neil',true);`
  1. 接受命名参数进行替换:一项不太困难的练习,基于已经给出的内容。在另一个答案中有一个实现。

  2. 使返回对象的行为类似于'string':嗯,这是有争议的,但可能导致有趣的结果。显示在另一个答案中

  3. 在呼叫站点的全局名称空间中解析参数:

我给你,可重用的模板文字:

嗯,这就是OP用命令evil我的意思是显示了他的附录eval。无需这样做eval,只需将传递的变量名称搜索到全局(或窗口)对象中即可。我不会展示如何做,因为我不喜欢它。封闭是正确的选择。


2

这是我的最佳尝试:

var s = (item, price) => {return `item: ${item}, price: $${price}`}
s('pants', 10) // 'item: pants, price: $10'
s('shirts', 15) // 'item: shirts, price: $15'

概括如下:

var s = (<variable names you want>) => {return `<template with those variables>`}

如果您没有运行E6,也可以执行以下操作:

var s = function(<variable names you want>){return `<template with those variables>`}

这似乎比以前的答案更简洁。

https://repl.it/@abalter/reusable-JS-template-literal


2

通常,我反对使用邪恶eval(),但在这种情况下,这是有道理的:

var template = "`${a}.${b}`";
var a = 1, b = 2;
var populated = eval(template);

console.log(populated);         // shows 1.2

然后,如果您更改值并再次调用eval(),则会得到新结果:

a = 3; b = 4;
populated = eval(template);

console.log(populated);         // shows 3.4

如果要在函数中使用它,则可以这样编写:

function populate(a, b){
  return `${a}.${b}`;
}

如果要编写包含模板的函数,则绝对不应使用eval
Bergi'7

@Bergi为什么?与您的实施有何不同?
isapir '17

2
我“似乎知道”的原因适用于任何动态构建的代码。编写该函数以使它在不eval()显式调用的情况下生成结果与完全相同eval(),因此它没有任何好处,因为它只会使代码更难阅读。
isapir'7

1
究竟。并且由于您的populate函数不会动态生成代码,因此不应使用eval它的所有缺点。
Bergi'7

6
您的功能可能只是function populate(a,b) { return `${a}.${b}`; }评估而不会增加任何内容
Vitim.us

1

更新:以下答案仅限于单个变量名,因此,像这样的模板'Result ${a+b}'在这种情况下无效。但是,您始终可以使用模板值:

format("This is a test: ${a_b}", {a_b: a+b});

原始答案:

基于先前的答案,但创建了一个更“友好”的实用程序功能:

var format = (template, params) => {
    let tpl = template.replace(/\${(?!this\.)/g, "${this.");
    let tpl_func = new Function(`return \`${tpl}\``);

    return tpl_func.call(params);
}

您可以像这样发票:

format("This is a test: ${hola}, second param: ${hello}", {hola: 'Hola', hello: 'Hi'});

结果字符串应为:

'This is a test: Hola, second param: Hi'

这样的模板呢?`Result: ${a+b}`
Atiris'3

1
嗨@Atiris,您是对的,这是一个限制,我已经更新了答案。
罗伯托

1

如果您正在寻找比较简单的东西(只是固定变量字段,没有计算,有条件的…),但是在没有模板字符串支持的浏览器(如IE 8,9,10,11)上也可以在客户端使用

开始了:

fillTemplate = function (templateString, templateVars) {
    var parsed = templateString;
    Object.keys(templateVars).forEach(
        (key) => {
            const value = templateVars[key]
            parsed = parsed.replace('${'+key+'}',value)
        }
    )
    return parsed
}

这将对每个变量进行查找。还有一种方法可以一次替换所有我在此模块中实现的操作:safe-es6-template
Aalex Gabi

1

this.每次键入都需要额外的冗余使我很烦恼,因此我还添加了regex来扩展变量,如.ato this.a

解:

const interp = template => _thisObj =>
function() {
    return template.replace(/\${([^}]*)}/g, (_, k) =>
        eval(
            k.replace(/([.a-zA-Z0-9$_]*)([a-zA-Z0-9$_]+)/, (r, ...args) =>
                args[0].charAt(0) == '.' ? 'this' + args[0] + args[1] : r
            )
        )
    );
}.call(_thisObj);

这样使用:

console.log(interp('Hello ${.a}${.b}')({ a: 'World', b: '!' }));
// outputs: Hello World!

1

我只是发布了一个可以简单地完成这项工作的npm软件包。深受这个答案的启发。

const Template = require('dynamic-template-string');

var tpl = new Template('hello ${name}');

tpl.fill({name: 'world'}); // ==> 'hello world';
tpl.fill({name: 'china'}); // ==> 'hello china';

它的工具非常简单。希望你会喜欢。


module.exports = class Template {
  constructor(str) {
    this._func = new Function(`with(this) { return \`${str}\`; }`);
  }

  fill(data) {
    return this._func.call(data);
  }
}

1

您可以使用以下嵌入式箭头功能,定义:

const template = (substitute: string) => `[^.?!]*(?<=[.?\s!])${substitute}(?=[\s.?!])[^.?!]*[.?!]`;

用法:

console.log(template('my replaced string'));

1

运行时模板字符串

var templateString = (template, values) => {
    let output = template;
    Object.keys(values)
        .forEach(key => {
        output = output.replace(new RegExp('\\$' + `{${key}}`, 'g'), values[key]);
    });
    return output;
};

测试

console.debug(templateString('hello ${word} world', {word: 'wonderful'}));

0

const fillTemplate = (template, values) => {
  template = template.replace(/(?<=\${)\w+(?=})/g, v=>"this."+v);
  return Function.apply(this, ["", "return `"+template+"`;"]).call(values);
};

console.log(fillTemplate("The man ${man} is brother of ${brother}", {man: "John", brother:"Peter"}));
//The man John is brother of Peter

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.