Czy jest możliwe utworzenie ciągu szablonu jako zwykłego ciągu?
let a="b:${b}";
a następnie przekonwertuj go na łańcuch szablonu
let b=10;
console.log(a.template());//b:10
bez eval, new Functioni inne środki dynamicznego generowania kodu?
Czy jest możliwe utworzenie ciągu szablonu jako zwykłego ciągu?
let a="b:${b}";
a następnie przekonwertuj go na łańcuch szablonu
let b=10;
console.log(a.template());//b:10
bez eval, new Functioni inne środki dynamicznego generowania kodu?
Odpowiedzi:
Ponieważ ciąg szablonu musi pobierać odwołanie do bzmiennej dynamicznie (w czasie wykonywania), więc odpowiedź brzmi: NIE, nie da się obejść bez dynamicznego generowania kodu.
Ale evalto całkiem proste:
let tpl = eval('`'+a+'`');
aciągu i będzie znacznie mniej niebezpieczny: let tpl = eval('`'+a.replace(/`/g,'\\`')+'`');. Myślę, że ważniejsze jest to, aby evaluniemożliwić kompilatorowi optymalizację kodu. Ale myślę, że nie ma to związku z tym pytaniem.
eval. Należy jednak pamiętać, że literał szablonu sam w sobie jest formą eval. Dwa przykłady: var test = Result: ${alert('hello')}; var test = Result: ${b=4}; Oba kończą się wykonaniem dowolnego kodu w kontekście skryptu. Jeśli chcesz zezwolić na dowolne ciągi znaków, równie dobrze możesz zezwolić eval.
W moim projekcie stworzyłem coś takiego w 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);
UPDATE Usunąłem zależność lodash, ES6 ma równoważne metody pobierania kluczy i wartości.
ReferenceError: _ is not defined. Czy jest to kod nie ES6, ale lodashkonkretny, czy ...?
O co tu prosisz:
//non working code quoted from the question let b=10; console.log(a.template());//b:10
jest dokładnie równoważne (pod względem mocy i, hm, bezpieczeństwa) do eval: możliwości pobrania łańcucha zawierającego kod i wykonania tego kodu; a także możliwość zobaczenia przez wykonywany kod zmiennych lokalnych w środowisku wywołującego.
W JS nie ma możliwości, aby funkcja mogła zobaczyć zmienne lokalne w swoim programie wywołującym, chyba że ta funkcja jest eval(). Nawet Function()nie mogę tego zrobić.
Kiedy słyszysz, że w JavaScript pojawia się coś, co nazywa się „ciągami szablonów”, naturalne jest założenie, że jest to wbudowana biblioteka szablonów, taka jak Mustache. Tak nie jest. To głównie interpolacja ciągów znaków i ciągi wielowierszowe dla JS. Myślę jednak, że przez jakiś czas będzie to powszechne nieporozumienie. :(
template is not a function.
Nie, nie da się tego zrobić bez dynamicznego generowania kodu.
Jednak stworzyłem funkcję, która zamieni zwykły ciąg znaków w funkcję, do której można dostarczyć mapę wartości, używając wewnętrznie ciągów szablonów.
Wygeneruj streszczenie ciągu szablonów
/**
* 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;
})();
Stosowanie:
var kingMaker = generateTemplateString('${name} is king!');
console.log(kingMaker({name: 'Bryan'}));
// Logs 'Bryan is king!' to the console.
Mam nadzieję, że to komuś pomoże. Jeśli znajdziesz problem z kodem, prosimy o aktualizację Gist.
var test = generateTemplateString('/api/${param1}/${param2}/') console.log(test({param1: 'bar', param2: 'foo'}))zwrotu szablonów/api/bar//
TLDR: https://jsfiddle.net/w3jx07vt/
Wydaje się, że wszyscy martwią się o dostęp do zmiennych, dlaczego po prostu ich nie przekazać? Jestem pewien, że nie będzie zbyt trudno uzyskać kontekst zmiennej w programie wywołującym i przekazać go. Użyj tego https://stackoverflow.com/a/6394168/6563504, aby pobrać props z obj. Nie mogę teraz testować dla ciebie, ale to powinno działać.
function renderString(str,obj){
return str.replace(/\$\{(.+?)\}/g,(match,p1)=>{return index(obj,p1)})
}
Przetestowany. Oto pełny kod.
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
${}znaków. Spróbuj:/(?!\${)([^{}]*)(?=})/g
Problem polega na tym, aby mieć funkcję, która ma dostęp do zmiennych swojego wywołującego. Właśnie dlatego widzimy bezpośrednie evalwykorzystanie do przetwarzania szablonów. Możliwym rozwiązaniem byłoby wygenerowanie funkcji pobierającej parametry formalne nazwane przez właściwości słownika i wywołującej ją z odpowiednimi wartościami w tej samej kolejności. Alternatywnym sposobem byłoby posiadanie czegoś prostego:
var name = "John Smith";
var message = "Hello, my name is ${name}";
console.log(new Function('return `' + message + '`;')());
A dla każdego używającego kompilatora Babel musimy stworzyć zamknięcie, które zapamięta środowisko, w którym zostało utworzone:
console.log(new Function('name', 'return `' + message + '`;')(name));
evaldlatego, że działa tylko ze namezmienną globalną
var template = function() { var name = "John Smith"; var message = "Hello, my name is ${name}"; this.local = new Function('return ;')();}
new Functionnie ma dostępu do var namew templatefunkcji.
Istnieje wiele dobrych rozwiązań, ale jeszcze żadne nie wykorzystywałoby metody ES6 String.raw . Oto moja inicjatywa. Ma ważne ograniczenie polegające na tym, że akceptuje tylko właściwości z przekazanego obiektu, co oznacza, że żadne wykonanie kodu w szablonie nie zadziała.
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"]objwedług nazwy właściwości. Rozwiązanie jest ograniczone przez płytkie mapowanie jednopoziomowe. Niezdefiniowane wartości są zastępowane pustym łańcuchem, ale akceptowane są inne fałszywe wartości.parameters: ["John Doe", 18]String.raw(...)i zwróć wynik..replace()wielokrotnego dzwonienia ?
.replace():) Uważam, że czytelność jest ważna, więc sam używając wyrażeń regularnych staram się je nazywać, żeby wszystko to
Podobna do odpowiedzi Daniela (i s.meijer za GIST ), ale bardziej czytelny:
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);
}
Uwaga: To nieznacznie poprawia oryginał s.meijer, ponieważ nie będzie pasował do rzeczy takich jak ${foo{bar}(wyrażenie regularne dopuszcza tylko znaki niekręcone w nawiasach klamrowych wewnątrz ${i }).
AKTUALIZACJA: Poproszono mnie o przykład za pomocą tego, więc proszę:
const replacements = {
name: 'Bob',
age: 37
}
interpolate('My name is ${name}, and I am ${age}.', replacements)
/\$\{(.*?)(?!\$\{)\}/g(do obsługi nawiasów klamrowych). Mam działające rozwiązanie, ale nie jestem pewien, czy jest tak przenośne, jak Twoje, więc chciałbym zobaczyć, jak należy to zaimplementować na stronie. Mój też używa eval().
evalpozostawia cię bardziej otwartym na możliwe błędy, które mogłyby spowodować problemy z bezpieczeństwem, podczas gdy moja wersja szuka właściwości obiektu ze ścieżki oddzielonej kropkami, która powinna być bezpieczna.
Spodobała mi się odpowiedź s.meijera i na jego podstawie napisałem własną wersję:
function parseTemplate(template, map, fallback) {
return template.replace(/\$\{[^}]+\}/g, (match) =>
match
.slice(2, -1)
.trim()
.split(".")
.reduce(
(searchObject, key) => searchObject[key] || fallback || match,
map
)
);
}
Wymagałem tej metody z obsługą Internet Explorera. Okazało się, że tylne tiki nie są obsługiwane nawet przez IE11. Również; używanie evallub odpowiednik Functionnie wydaje się właściwe.
Dla tego, który zauważy; Używam również znaków odwrotnych, ale te są usuwane przez kompilatory, takie jak babel. Metody sugerowane przez inne metody zależą od nich w czasie wykonywania. Jak powiedziano wcześniej; jest to problem w IE11 i niższych.
Oto co wymyśliłem:
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);
});
}
Przykładowe dane wyjściowe:
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 + '`')po prostu nie działa.
eval. Odnośnie do literałów szablonu: dzięki za ponowne zwrócenie uwagi. Używam Babel do transpozycji mojego kodu, ale moja funkcja najwyraźniej nadal nie działa 😐
Obecnie nie mogę komentować istniejących odpowiedzi, więc nie mogę bezpośrednio skomentować doskonałej odpowiedzi Bryana Raynora. Tak więc ta odpowiedź będzie aktualizować jego odpowiedź, wprowadzając niewielką korektę.
Krótko mówiąc, jego funkcja nie potrafi w rzeczywistości buforować utworzonej funkcji, więc zawsze zostanie odtworzona, niezależnie od tego, czy wcześniej widziała szablon. Oto poprawiony kod:
/**
* 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, rozwiązanie działa świetnie, ale gdy użyłem go w React Native (tryb budowania), wyświetla błąd: Nieprawidłowy znak '' ' , chociaż działa, gdy uruchamiam go w trybie debugowania.
Zapisałem więc własne rozwiązanie za pomocą wyrażenia regularnego.
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)
Demo: https://es6console.com/j31pqx1p/
UWAGA: Ponieważ nie znam głównej przyczyny problemu, zgłosiłem zgłoszenie w repozytorium react-native, https://github.com/facebook/react-native/issues/14107 , aby gdy mogli napraw / poprowadź mnie o tym samym :)
Wciąż dynamiczny, ale wydaje się bardziej kontrolowany niż zwykłe używanie nagiego evala:
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))
To rozwiązanie działa bez 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"
Uwaga: Functionkonstruktor jest zawsze tworzony w zasięgu globalnym, co może potencjalnie spowodować nadpisanie zmiennych globalnych przez szablon, np.render("hello ${ someGlobalVar = 'some new value' }", {name:'mo'});
Ponieważ wymyślamy na nowo koło w czymś, co byłoby uroczą funkcją w javascript.
Używam eval(), co nie jest bezpieczne, ale javascript nie jest bezpieczny. Chętnie przyznaję, że nie jestem doskonały z javascriptem, ale miałem potrzebę i potrzebowałem odpowiedzi, więc ją zrobiłem.
Zdecydowałem się stylizować moje zmienne na @raczej niż an $, szczególnie dlatego, że chcę używać funkcji multilinii w literałach bez oceniania, aż będzie gotowa. Tak więc składnia zmiennej to@{OptionalObject.OptionalObjectN.VARIABLE_NAME}
Nie jestem ekspertem od javascript, więc chętnie skorzystam z porad dotyczących ulepszeń, ale ...
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);
}
Następuje bardzo prosta implementacja
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);
}
W mojej rzeczywistej realizacji decyduję się na użycie @{{variable}}. Jeszcze jeden zestaw szelek. Absurdalnie mało prawdopodobne, aby spotkał się z tym niespodziewanie. Wyglądałoby to następująco/\@\{\{(.*?)(?!\@\{\{)\}\}/g
Aby było to łatwiejsze do odczytania
\@\{\{ # 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.
Jeśli nie masz doświadczenia z wyrażeniem regularnym, całkiem bezpieczną zasadą jest unikanie każdego znaku niealfanumerycznego i nie zawsze niepotrzebnie unikaj litery, ponieważ wiele takich znaków ma specjalne znaczenie dla praktycznie wszystkich odmian wyrażenia regularnego.
Powinieneś wypróbować ten mały moduł JS autorstwa Andrei Giammarchi z 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 (wszystkie poniższe testy zwracają prawdę):
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});
Stworzyłem własne rozwiązanie, wykonując typ z opisem jako funkcją
export class Foo {
...
description?: Object;
...
}
let myFoo:Foo = {
...
description: (a,b) => `Welcome ${a}, glad to see you like the ${b} section`.
...
}
i tak robiąc:
let myDescription = myFoo.description('Bar', 'bar');
Zamiast używać eval, lepiej jest użyć wyrażenia regularnego
Eval nie jest zalecane i bardzo odradzane, więc nie używaj go ( mdn eval ).
let b = 10;
let a="b:${b}";
let response = a.replace(/\${\w+}/ ,b);
conssole.log(response);