Можно ли создать строку шаблона в виде обычной строки
let a="b:${b}";
a затем преобразовать его в строку шаблона
let b=10;
console.log(a.template());//b:10
без eval
, new Function
и других способов генерации динамического кода?
Можно ли создать строку шаблона в виде обычной строки
let a="b:${b}";
a затем преобразовать его в строку шаблона
let b=10;
console.log(a.template());//b:10
без eval
, new Function
и других способов генерации динамического кода?
Поскольку ваша строка шаблона должна динамически ссылаться на переменную b
(во время выполнения), так что ответ: НЕТ, невозможно обойтись без генерации динамического кода.
Но с eval
это довольно просто:
let tpl = eval('`'+a+'`');
Что вы просите здесь:
//non working code quoted from the question let b=10; console.log(a.template());//b:10
в точности эквивалентен (по мощности и, er, безопасности) до eval
: возможность взять строку, содержащую код, и выполнить этот код; а также способность исполняемого кода видеть локальные переменные в среде вызывающего абонента.
В JS нет способа для функции видеть локальные переменные в своем вызывающем объекте, если только эта функция не является eval()
. Даже Function()
не может этого сделать.
Когда вы слышите что-то, называемое "строками шаблонов", идущее на JavaScript, естественно предположить, что это встроенная библиотека шаблонов, например Mustache. Это не так. Это в основном просто интерполяция строк и многострочные строки для JS. Я думаю, что это будет распространенное заблуждение на некоторое время.: (
Нет, нет способа сделать это без генерации динамического кода.
Однако я создал функцию, которая превратит обычную строку в функцию, которая может быть снабжена картой значений, используя внутренние строки шаблонов.
Сгенерировать шаблон String Gist
/**
* 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 not ${map.expressions}' (etc) with a blank string.
.replace(/(\$\{(?!map\.)[^}]+\})/g, '');
fn = Function('map', `return \`${sanitized}\``);
}
return fn;
}
return generateTemplate;
})();
Использование:
var kingMaker = generateTemplateString('${name} is king!');
console.log(kingMaker({name: 'Bryan'}));
// Logs 'Bryan is king!' to the console.
Надеюсь, это поможет кому-то. Если вы обнаружите проблему с кодом, пожалуйста, будьте любезны, чтобы обновить Gist.
В моем проекте я создал что-то вроде этого с ES6:
String.prototype.interpolate = function(params) {
const names = _.keys(params);
const vals = _.values(params);
return new Function(...names, `return \`${this}\`;`)(...vals);
}
const template = 'Example text: ${text}';
const result = template.interpolate({
text: 'Foo Boo'
});
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.2/lodash.min.js"></script>
TL;DR: https://jsfiddle.net/w3jx07vt/
Кажется, что все беспокоятся о доступе к переменным, почему бы просто не передать их? Я уверен, что не будет слишком сложно получить переменный контекст в вызывающем абоненте и передать его. Используйте fooobar.com/questions/4053/..., чтобы получить реквизит из obj. Я не могу проверить вас прямо сейчас, но это должно сработать.
function renderString(str,obj){
return str.replace(/\$\{(.+?)\}/g,(match,p1)=>{return index(obj,p1)})
}
Испытано. Вот полный код.
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
Проблема заключается в том, чтобы иметь функцию, которая имеет доступ к переменным своего вызывающего. Вот почему мы видим, что прямая eval
используется для обработки шаблонов. Возможным решением могло бы стать создание функции с формальными параметрами, названными свойствами словаря, и вызовом ее с соответствующими значениями в том же порядке. Альтернативным способом было бы иметь что-то простое:
var name = "John Smith";
var message = "Hello, my name is ${name}";
console.log(new Function('return `' + message + '`;')());
И для тех, кто использует компилятор Babel, нам нужно создать закрытие, которое запоминает среду, в которой она была создана:
console.log(new Function('name', 'return `' + message + '`;')(name));
Вы можете использовать прототип строки, например
String.prototype.toTemplate=function(){
return eval('`'+this+'`');
}
//...
var a="b:${b}";
var b=10;
console.log(a.toTemplate());//b:10
Но ответ на исходный вопрос никоим образом.
В настоящее время я не могу комментировать существующие ответы, поэтому я не могу напрямую комментировать Bryan Raynor отличный ответ. Таким образом, этот ответ будет обновлять свой ответ с небольшой коррекцией.
Короче говоря, его функция не может фактически кэшировать созданную функцию, поэтому она всегда будет воссоздавать, независимо от того, видел ли она шаблон раньше. Вот скорректированный код:
/**
* 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 not ${map.expressions}' (etc) with a blank string.
.replace(/(\$\{(?!map\.)[^}]+\})/g, '');
fn = cache[template] = Function('map', `return \`${sanitized}\``);
}
return fn;
};
return generateTemplate;
})();
Мне нужен этот метод с поддержкой Internet Explorer. Оказалось, что обратные тики не поддерживаются даже IE11. Также; используя eval
или эквивалент Function
не чувствует себя хорошо.
Для тех, кто уведомляет; Я также использую backticks, но они удаляются компиляторами, такими как babel. Методы, предложенные другими, зависят от них во время выполнения. Как сказано ранее; это проблема в IE11 и ниже.
Итак, вот что я придумал:
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);
});
}
Пример вывода:
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 -
@Mateusz Moska, решение отлично работает, но когда я использовал его в React Native (режим сборки), он выдает ошибку: Недопустимый символ `` ', хотя он работает, когда я запускаю его в режим отладки.
Итак, я записал свое собственное решение, используя регулярное выражение.
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)
Демо: https://es6console.com/j31pqx1p/
ПРИМЕЧАНИЕ.. Поскольку я не знаю первопричины проблемы, я поднял билет в режиме реакции-native repo, https://github.com/facebook/react-native/issues/14107, так что как только они смогут исправить/направить меня примерно одинаково:)
По-прежнему динамический, но кажется более контролируемым, чем просто использование голого eval:
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))
Подобно Daniel K answer (и s.meijer gist), но более читаемым:
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
const getObjPath = (path, obj, fallback = '') => {
return path.split('.').reduce((res, key) => res[key] || fallback, obj);
}
Примечание. Это немного улучшает оригинал s.meijer, поскольку он не будет соответствовать таким вещам, как ${foo{bar}
(регулярное выражение допускает только не фигурные фигурные скобки внутри ${
и }
).
UPDATE: меня попросили использовать пример, поэтому здесь вы:
const replacements = {
name: 'Bob',
age: 37
}
interpolate('My name is ${name}, and I am ${age}.`, replacements)
Это решение работает без 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"
Примечание: конструктор Function
всегда создается в глобальной области видимости, что потенциально может привести к тому, что глобальные переменные будут перезаписаны шаблоном, например. render("hello ${ someGlobalVar = 'some new value' }", {name:'mo'});
Мне понравился ответ s.meijer и написал мою собственную версию, основанную на его:
function parseTemplate(template, map, fallback) {
return template.replace(/\$\{[^}]+\}/g, (match) =>
match
.slice(2, -1)
.trim()
.split(".")
.reduce(
(searchObject, key) => searchObject[key] || fallback || match,
map
)
);
}
Поскольку мы изобретаем колесо на чем-то, что было бы прекрасной функцией в javascript.
Я использую eval()
, который не является безопасным, но javascript не защищен. Я с готовностью признаю, что я не превосходна с javascript, но у меня была необходимость, и мне нужен был ответ, поэтому я сделал один.
Я решил стилизовать свои переменные с помощью @
, а не $
, особенно потому, что я хочу использовать многострочную функцию литералов, не оценивая ее до готовности. Таким образом, переменный синтаксис @{OptionalObject.OptionalObjectN.VARIABLE_NAME}
Я не эксперт по javascript, поэтому я с радостью принимаю советы по улучшению, но...
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);
}
Очень простая реализация следует
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);
}