É possível criar uma sequência de modelos como uma sequência usual
let a="b:${b}";
e depois converta-o em uma string de modelo
let b=10;
console.log(a.template());//b:10
sem eval, new Functione outros meios de geração de código dinâmico?
É possível criar uma sequência de modelos como uma sequência usual
let a="b:${b}";
e depois converta-o em uma string de modelo
let b=10;
console.log(a.template());//b:10
sem eval, new Functione outros meios de geração de código dinâmico?
Respostas:
Como a sequência do modelo deve obter referência à bvariável dinamicamente (em tempo de execução), a resposta é: NÃO, é impossível ficar sem a geração dinâmica de código.
Mas com evalisso é bem simples:
let tpl = eval('`'+a+'`');
acorda e será muito menos inseguros: let tpl = eval('`'+a.replace(/`/g,'\\`')+'`');. Eu acho que o mais importante é que evalevite que o compilador otimize seu código. Mas acho irrelevante para esta pergunta.
eval. No entanto, lembre-se de que um literal de modelo é uma forma de eval. Dois exemplos: var test = Result: ${alert('hello')}; teste var = Result: ${b=4}; Ambos acabarão executando código arbitrário no contexto do script. Se você deseja permitir seqüências arbitrárias, também pode permitir eval.
No meu projeto, criei algo assim com o ES6:
String.prototype.interpolate = function(params) {
const names = Object.keys(params);
const vals = Object.values(params);
return new Function(...names, `return \`${this}\`;`)(...vals);
}
const template = 'Example text: ${text}';
const result = template.interpolate({
text: 'Foo Boo'
});
console.log(result);
ATUALIZAÇÃO Removi a dependência do lodash, o ES6 possui métodos equivalentes para obter chaves e valores.
ReferenceError: _ is not defined. O código não é ES6, mas lodashespecífico ou ...?
O que você está pedindo aqui:
//non working code quoted from the question let b=10; console.log(a.template());//b:10
é exatamente equivalente (em termos de poder e, er, segurança) à eval: capacidade de pegar uma string que contém código e executar esse código; e também a capacidade do código executado ver variáveis locais no ambiente do chamador.
Na JS, não há como uma função ver variáveis locais em seu chamador, a menos que essa função seja eval(). Mesmo Function()não pode fazer isso.
Quando você ouve que algo chamado "strings de modelo" está chegando ao JavaScript, é natural assumir que é uma biblioteca de modelos embutida, como o Bigode. Não é. É principalmente apenas interpolação de cadeias e cadeias de linhas múltiplas para JS. Eu acho que isso vai ser um equívoco comum por um tempo, no entanto. :(
template is not a function.
Não, não há uma maneira de fazer isso sem a geração dinâmica de código.
No entanto, criei uma função que transformará uma sequência regular em uma função que pode ser fornecida com um mapa de valores, usando seqüências de caracteres de modelo internamente.
Gerar essência da sequência de modelos
/**
* Produces a function which uses template strings to do simple interpolation from objects.
*
* Usage:
* var makeMeKing = generateTemplateString('${name} is now the king of ${country}!');
*
* console.log(makeMeKing({ name: 'Bryan', country: 'Scotland'}));
* // Logs 'Bryan is now the king of Scotland!'
*/
var generateTemplateString = (function(){
var cache = {};
function generateTemplate(template){
var fn = cache[template];
if (!fn){
// Replace ${expressions} (etc) with ${map.expressions}.
var sanitized = template
.replace(/\$\{([\s]*[^;\s\{]+[\s]*)\}/g, function(_, match){
return `\$\{map.${match.trim()}\}`;
})
// Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string.
.replace(/(\$\{(?!map\.)[^}]+\})/g, '');
fn = Function('map', `return \`${sanitized}\``);
}
return fn;
}
return generateTemplate;
})();
Uso:
var kingMaker = generateTemplateString('${name} is king!');
console.log(kingMaker({name: 'Bryan'}));
// Logs 'Bryan is king!' to the console.
Espero que isso ajude alguém. Se você encontrar um problema com o código, por favor, atualize o Gist.
var test = generateTemplateString('/api/${param1}/${param2}/') console.log(test({param1: 'bar', param2: 'foo'}))retornados/api/bar//
TLDR: https://jsfiddle.net/w3jx07vt/
Todo mundo parece estar preocupado em acessar variáveis, por que não apenas passá-las? Tenho certeza de que não será muito difícil obter o contexto variável no chamador e transmiti-lo. Use este https://stackoverflow.com/a/6394168/6563504 para obter os adereços da obj. Não posso testar para você agora, mas isso deve funcionar.
function renderString(str,obj){
return str.replace(/\$\{(.+?)\}/g,(match,p1)=>{return index(obj,p1)})
}
Testado. Aqui está o código completo.
function index(obj,is,value) {
if (typeof is == 'string')
is=is.split('.');
if (is.length==1 && value!==undefined)
return obj[is[0]] = value;
else if (is.length==0)
return obj;
else
return index(obj[is[0]],is.slice(1), value);
}
function renderString(str,obj){
return str.replace(/\$\{.+?\}/g,(match)=>{return index(obj,match)})
}
renderString('abc${a}asdas',{a:23,b:44}) //abc23asdas
renderString('abc${a.c}asdas',{a:{c:22,d:55},b:44}) //abc22asdas
${}caracteres. Tente:/(?!\${)([^{}]*)(?=})/g
O problema aqui é ter uma função que tenha acesso às variáveis de seu chamador. É por isso que vemos o uso direto evalno processamento de modelos. Uma solução possível seria gerar uma função usando parâmetros formais nomeados pelas propriedades de um dicionário e chamando-a com os valores correspondentes na mesma ordem. Uma maneira alternativa seria ter algo simples como este:
var name = "John Smith";
var message = "Hello, my name is ${name}";
console.log(new Function('return `' + message + '`;')());
E para quem usa o compilador Babel, precisamos criar um fechamento que se lembre do ambiente em que foi criado:
console.log(new Function('name', 'return `' + message + '`;')(name));
evalporque ele funciona apenas com a global namevariável
var template = function() { var name = "John Smith"; var message = "Hello, my name is ${name}"; this.local = new Function('return '+ message +';')();}
new Functionnão tem acesso ao var namena templatefunção.
Há muitas boas soluções publicadas aqui, mas nenhuma ainda utiliza o método ES6 String.raw . Aqui está minha contriubuição. Ele tem uma limitação importante, pois aceita apenas propriedades de um objeto passado, o que significa que nenhuma execução de código no modelo funcionará.
function parseStringTemplate(str, obj) {
let parts = str.split(/\$\{(?!\d)[\wæøåÆØÅ]*\}/);
let args = str.match(/[^{\}]+(?=})/g) || [];
let parameters = args.map(argument => obj[argument] || (obj[argument] === undefined ? "" : obj[argument]));
return String.raw({ raw: parts }, ...parameters);
}
let template = "Hello, ${name}! Are you ${age} years old?";
let values = { name: "John Doe", age: 18 };
parseStringTemplate(template, values);
// output: Hello, John Doe! Are you 18 years old?
parts: ["Hello, ", "! Are you ", " years old?"]args: ["name", "age"]objdo nome da propriedade. A solução é limitada pelo mapeamento superficial de um nível. Valores indefinidos são substituídos por uma sequência vazia, mas outros valores falsos são aceitos.parameters: ["John Doe", 18]String.raw(...)e retorne o resultado..replace()repetidamente?
.replace(), no entanto :) Eu acho que a legibilidade é importante, então, enquanto eu mesmo uso expressões regulares, tento nomeá-las para ajudar a entender tudo ...
Semelhante à resposta de Daniel (e à essência do s.meijer ), mas mais legível:
const regex = /\${[^{]+}/g;
export default function interpolate(template, variables, fallback) {
return template.replace(regex, (match) => {
const path = match.slice(2, -1).trim();
return getObjPath(path, variables, fallback);
});
}
//get the specified property or nested property of an object
function getObjPath(path, obj, fallback = '') {
return path.split('.').reduce((res, key) => res[key] || fallback, obj);
}
Nota: Isso melhora um pouco o original do s.meijer, já que não corresponde a coisas como ${foo{bar}(a regex permite apenas caracteres entre chaves não encaracolados dentro ${e }).
UPDATE: Me pediram um exemplo usando isso, então aqui está:
const replacements = {
name: 'Bob',
age: 37
}
interpolate('My name is ${name}, and I am ${age}.', replacements)
/\$\{(.*?)(?!\$\{)\}/g(para manipular chaves de ninho). Eu tenho uma solução funcional, mas não tenho certeza de que seja tão portátil quanto a sua, então adoraria ver como isso deve ser implementado em uma página. O meu também usa eval().
evaldeixa você muito mais aberto a possíveis erros que causariam problemas de segurança, enquanto tudo que minha versão está fazendo é procurar uma propriedade em um objeto a partir de um caminho separado por pontos, o que deve ser seguro.
Gostei da resposta do s.meijer e escrevi minha própria versão com base na dele:
function parseTemplate(template, map, fallback) {
return template.replace(/\$\{[^}]+\}/g, (match) =>
match
.slice(2, -1)
.trim()
.split(".")
.reduce(
(searchObject, key) => searchObject[key] || fallback || match,
map
)
);
}
Eu exigi esse método com suporte para o Internet Explorer. Acontece que os ticks anteriores não são suportados pelo IE11. Além disso; usando evalou equivalente Functionnão parece certo.
Para aquele que percebe; Eu também uso backticks, mas esses são removidos por compiladores como o babel. Os métodos sugeridos por outros dependem deles no tempo de execução. Como dito antes; esse é um problema no IE11 e inferior.
Então é isso que eu criei:
function get(path, obj, fb = `$\{${path}}`) {
return path.split('.').reduce((res, key) => res[key] || fb, obj);
}
function parseTpl(template, map, fallback) {
return template.replace(/\$\{.+?}/g, (match) => {
const path = match.substr(2, match.length - 3).trim();
return get(path, map, fallback);
});
}
Exemplo de saída:
const data = { person: { name: 'John', age: 18 } };
parseTpl('Hi ${person.name} (${person.age})', data);
// output: Hi John (18)
parseTpl('Hello ${person.name} from ${person.city}', data);
// output: Hello John from ${person.city}
parseTpl('Hello ${person.name} from ${person.city}', data, '-');
// output: Hello John from -
eval('`' + taggedURL + '`')simplesmente não funciona.
eval. Em relação aos literais do modelo: obrigado por apontar isso novamente. Eu estou usando Babel para transpilar meu código, mas minha função ainda não funcionará aparentemente #
Atualmente, não posso comentar sobre as respostas existentes, por isso não posso comentar diretamente sobre a excelente resposta de Bryan Raynor. Portanto, essa resposta atualizará sua resposta com uma leve correção.
Em resumo, sua função falha ao armazenar em cache a função criada; portanto, ela sempre será recriada, independentemente de ter visto o modelo antes. Aqui está o código corrigido:
/**
* Produces a function which uses template strings to do simple interpolation from objects.
*
* Usage:
* var makeMeKing = generateTemplateString('${name} is now the king of ${country}!');
*
* console.log(makeMeKing({ name: 'Bryan', country: 'Scotland'}));
* // Logs 'Bryan is now the king of Scotland!'
*/
var generateTemplateString = (function(){
var cache = {};
function generateTemplate(template){
var fn = cache[template];
if (!fn){
// Replace ${expressions} (etc) with ${map.expressions}.
var sanitized = template
.replace(/\$\{([\s]*[^;\s\{]+[\s]*)\}/g, function(_, match){
return `\$\{map.${match.trim()}\}`;
})
// Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string.
.replace(/(\$\{(?!map\.)[^}]+\})/g, '');
fn = cache[template] = Function('map', `return \`${sanitized}\``);
}
return fn;
};
return generateTemplate;
})();
@Mateusz Moska, a solução funciona muito bem, mas quando a usei no React Native (modo de compilação), gera um erro: Caractere inválido '' ' , embora funcione quando a executo no modo de depuração.
Então, eu escrevi minha própria solução usando regex.
String.prototype.interpolate = function(params) {
let template = this
for (let key in params) {
template = template.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), params[key])
}
return template
}
const template = 'Example text: ${text}',
result = template.interpolate({
text: 'Foo Boo'
})
console.log(result)
Demonstração: https://es6console.com/j31pqx1p/
NOTA: Como não sei a causa raiz de um problema, aumentei um ticket no repositório react-native, https://github.com/facebook/react-native/issues/14107 , para que assim que possível corrigir / orientar-me sobre o mesmo :)
Ainda dinâmico, mas parece mais controlado do que apenas usar uma avaliação nua:
const vm = require('vm')
const moment = require('moment')
let template = '### ${context.hours_worked[0].value} \n Hours worked \n #### ${Math.abs(context.hours_worked_avg_diff[0].value)}% ${fns.gt0(context.hours_worked_avg_diff[0].value, "more", "less")} than usual on ${fns.getDOW(new Date())}'
let context = {
hours_worked:[{value:10}],
hours_worked_avg_diff:[{value:10}],
}
function getDOW(now) {
return moment(now).locale('es').format('dddd')
}
function gt0(_in, tVal, fVal) {
return _in >0 ? tVal: fVal
}
function templateIt(context, template) {
const script = new vm.Script('`'+template+'`')
return script.runInNewContext({context, fns:{getDOW, gt0 }})
}
console.log(templateIt(context, template))
Esta solução funciona sem o ES6:
function render(template, opts) {
return new Function(
'return new Function (' + Object.keys(opts).reduce((args, arg) => args += '\'' + arg + '\',', '') + '\'return `' + template.replace(/(^|[^\\])'/g, '$1\\\'') + '`;\'' +
').apply(null, ' + JSON.stringify(Object.keys(opts).reduce((vals, key) => vals.push(opts[key]) && vals, [])) + ');'
)();
}
render("hello ${ name }", {name:'mo'}); // "hello mo"
Nota: o Functionconstrutor é sempre criado no escopo global, o que pode potencialmente fazer com que variáveis globais sejam substituídas pelo modelo, por exemplo,render("hello ${ someGlobalVar = 'some new value' }", {name:'mo'});
Como estamos reinventando a roda em algo que seria um recurso adorável em javascript.
Eu uso eval(), o que não é seguro, mas o javascript não é seguro. Eu admito prontamente que não sou excelente com javascript, mas tinha uma necessidade e precisava de uma resposta, então fiz uma.
Eu escolhi estilizar minhas variáveis com um @e não um $, principalmente porque eu quero usar o recurso de múltiplas linhas de literais sem avaliar até que esteja pronto. Portanto, a sintaxe variável é@{OptionalObject.OptionalObjectN.VARIABLE_NAME}
Como não sou especialista em javascript, gostaria de receber conselhos sobre melhorias, mas ...
var prsLiteral, prsRegex = /\@\{(.*?)(?!\@\{)\}/g
for(i = 0; i < myResultSet.length; i++) {
prsLiteral = rt.replace(prsRegex,function (match,varname) {
return eval(varname + "[" + i + "]");
// you could instead use return eval(varname) if you're not looping.
})
console.log(prsLiteral);
}
Uma implementação muito simples segue
myResultSet = {totalrecords: 2,
Name: ["Bob", "Stephanie"],
Age: [37,22]};
rt = `My name is @{myResultSet.Name}, and I am @{myResultSet.Age}.`
var prsLiteral, prsRegex = /\@\{(.*?)(?!\@\{)\}/g
for(i = 0; i < myResultSet.totalrecords; i++) {
prsLiteral = rt.replace(prsRegex,function (match,varname) {
return eval(varname + "[" + i + "]");
// you could instead use return eval(varname) if you're not looping.
})
console.log(prsLiteral);
}
Na minha implementação real, eu escolho usar @{{variable}}. Mais um conjunto de chaves. É absurdamente improvável encontrar isso inesperadamente. O regex para isso seria semelhante/\@\{\{(.*?)(?!\@\{\{)\}\}/g
Para facilitar a leitura
\@\{\{ # opening sequence, @{{ literally.
(.*?) # capturing the variable name
# ^ captures only until it reaches the closing sequence
(?! # negative lookahead, making sure the following
# ^ pattern is not found ahead of the current character
\@\{\{ # same as opening sequence, if you change that, change this
)
\}\} # closing sequence.
Se você não tem experiência com regex, uma regra bastante segura é escapar a todos os caracteres não alfanuméricos e nunca escapar desnecessariamente de letras, pois muitas letras escapadas têm um significado especial para praticamente todos os tipos de regex.
Você deve experimentar este pequeno módulo JS, de Andrea Giammarchi, do github: https://github.com/WebReflection/backtick-template
/*! (C) 2017 Andrea Giammarchi - MIT Style License */
function template(fn, $str, $object) {'use strict';
var
stringify = JSON.stringify,
hasTransformer = typeof fn === 'function',
str = hasTransformer ? $str : fn,
object = hasTransformer ? $object : $str,
i = 0, length = str.length,
strings = i < length ? [] : ['""'],
values = hasTransformer ? [] : strings,
open, close, counter
;
while (i < length) {
open = str.indexOf('${', i);
if (-1 < open) {
strings.push(stringify(str.slice(i, open)));
open += 2;
close = open;
counter = 1;
while (close < length) {
switch (str.charAt(close++)) {
case '}': counter -= 1; break;
case '{': counter += 1; break;
}
if (counter < 1) {
values.push('(' + str.slice(open, close - 1) + ')');
break;
}
}
i = close;
} else {
strings.push(stringify(str.slice(i)));
i = length;
}
}
if (hasTransformer) {
str = 'function' + (Math.random() * 1e5 | 0);
if (strings.length === values.length) strings.push('""');
strings = [
str,
'with(this)return ' + str + '([' + strings + ']' + (
values.length ? (',' + values.join(',')) : ''
) + ')'
];
} else {
strings = ['with(this)return ' + strings.join('+')];
}
return Function.apply(null, strings).apply(
object,
hasTransformer ? [fn] : []
);
}
template.asMethod = function (fn, object) {'use strict';
return typeof fn === 'function' ?
template(fn, this, object) :
template(this, fn);
};
Demo (todos os seguintes testes retornam true):
const info = 'template';
// just string
`some ${info}` === template('some ${info}', {info});
// passing through a transformer
transform `some ${info}` === template(transform, 'some ${info}', {info});
// using it as String method
String.prototype.template = template.asMethod;
`some ${info}` === 'some ${info}'.template({info});
transform `some ${info}` === 'some ${info}'.template(transform, {info});
Eu fiz minha própria solução fazendo um tipo com uma descrição em função
export class Foo {
...
description?: Object;
...
}
let myFoo:Foo = {
...
description: (a,b) => `Welcome ${a}, glad to see you like the ${b} section`.
...
}
e assim fazendo:
let myDescription = myFoo.description('Bar', 'bar');