针对另一个对象进行JSON递归值替换

4
我有一个包含数组和映射的 JSON 对象,其中包含一些可以通过传递另一个对象来替换的占位符文本。
例如:
data = {
  "name": "Hello ${user.name}",  
  "primary_task": "Task Name: ${user.tasks[0].name}",
  "secondary_tasks": ["Task 2: ${user.tasks[1].name}", "Task 2: ${user.tasks[2].name}"]
}

变量或元数据对象可以
variables = {
  "user": {
    "name": "DJ"
  },
  "tasks": [
    {
      "name": "Task One"
    }
  ]
}

我有一个函数,可以根据一些对象来替换字符串。我不确定如何递归地在 JSON 对象上调用它,以便它可以替换映射和数组中的所有字符串值。
var data = {
  "name": "Hello ${user.name}",  
  "primary_task": "Task Name: Task One",
  "secondary_tasks": ["Task 2: ${user.tasks[1].name}", "Task 2: ${user.tasks[2].name}"]
}

var metadata = {
  "user": {
    "name": "DJ",
    "tasks": [
    {
      "name": "Task One"
    }
  ],
  },  
}

function subString(str) {
  var rxp = /\{([^}]+)\}/g,    
    liveStr = str,
    curMatch;

while( curMatch = rxp.exec( str ) ) {
    var match = curMatch[1];
    liveStr = liveStr.replace("${"+ match + "}", tryEval(match));    
}
return liveStr;
}


function tryEval(evalStr) {
  evalStr = "metadata." + evalStr;
  try {
  return eval(evalStr);
}
catch(error) {
  return "${" + evalStr + "}";
}

}
var str = "user ${user.name} - ${user.tasks[0].name} - ${user.tasks[2].name}";

console.log("Sub " + subString(str));

在上面的示例中,${user.tasks[2].name}在元数据中不存在,因此它不应解析为undefined。如果在元数据对象中找不到该键,它应该保持原样,即${user.tasks[2].name}

我写了一个解析属性函数,你可以扩展它以使其也适用于数组。https://stackblitz.com/edit/typescript-fchtqv - Adrian Brand
这个应用最初为什么要设计成这样? - Pedro Lobito
数据和元数据来自哪里?${}位是如何进入数据的?我猜你不是在硬编码数据吧?是否有定义元数据结构的参数?如果数据具有元数据没有的字段怎么办? - jacob.mccrumb
metadata.user.tasks 中只有一个项目,这就是为什么 "Task 2: ${user.tasks[2].name}" 没有被替换(user.tasks[2] 不存在)。你期望的输出是什么? - CertainPerformance
4个回答

3

仅使用 ES6 的场景:

const data = { "name": "Hello ${user.name} ${user.foo}", "primary_task": "Task Name: ${user.tasks[0].name} ${user.tasks[10].name}", "secondary_tasks": ["Task 2: ${user.tasks[0].name}", "Task 2: ${user.tasks[1].name}", "Task 3: ${user.tasks[11].name}"] }
const meta = { "user": { "name": "DJ", "tasks": [ { "name": "Task One" }, { "name": "Task Two" } ] } }

const getPath = (path, obj) => path.split('.').reduce((r, c) =>
  r ? c.includes('[') ? getPath(c.replace('[', '.').replace(']', ''), r) : r[c] : undefined, obj)

const interpolate = (s, v) =>
  new Function(...Object.keys(v), `return \`${s}\`;`)(...Object.values(v))

const templ = (str, obj) => {
  let r = new RegExp(/\${([\s\S]+?)}/g)
  while (match = r.exec(str)) {
    if (!getPath(match[1], obj))
      str = str.replace(match[0], match[0].replace('${', '__'))
  }
  return interpolate(str, obj).replace('__', '${')
}

const resolve = (d, vars) => {
  if (Array.isArray(d))
    return d.map(x => templ(x, vars))
  else
    return Object.entries(d).reduce((r, [k, v]) =>
      (r[k] = Array.isArray(v) ? resolve(v, vars) : templ(v, vars), r), {})
}

console.log(resolve(data, meta))

该字符串插值的概念受到这个线程的影响。该想法是递归遍历所有对象值,并使用interpolate函数返回实际水化的字符串。使用getPath来遍历路径以及检测不存在的路径。如果字符串中的路径不存在,则使用字符串替换来通过字符串水化获取该字符串,然后再进行替换。 Lodash _.template情景:
在可以利用lodash及其模板机制(通过_.template)的情况下,这就变得更加简单,因为我们已经拥有插值函数。

const data = { "name": "Hello ${user.name} ${user.foo}", "primary_task": "Task Name: ${user.tasks[0].name} ${user.tasks[10].name}", "secondary_tasks": ["Task 2: ${user.tasks[0].name}", "Task 2: ${user.tasks[1].name}", "Task 3: ${user.tasks[11].name}"] }
const meta = { "user": { "name": "DJ", "tasks": [ { "name": "Task One" }, { "name": "Task Two" } ] } }

const templ = (str, obj) => {
  let r = new RegExp(/\${([\s\S]+?)}/g)
  while (match = r.exec(str)) {
    if (!_.get(obj, match[1]))
      str = str.replace(match[0], match[0].replace('${', '__'))
  }
  return _.template(str)(obj).replace('__', '${')
}

const resolve = (d, vars) => {
  if (_.isArray(d))
    return _.map(d, x => templ(x, vars))
  else
    return _.entries(d).reduce((r, [k, v]) =>
      (r[k] = _.isArray(v) ? resolve(v, vars) : templ(v, vars), r), {})
}

console.log(resolve(data, meta))
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>

这个想法是递归遍历对象树,并通过lodash的_.template将任何简单属性转换为字符串。使用Array.map_.map遍历数组,使用Array.reduce_.reduce遍历对象以将它们转换为模板字符串。
唯一的问题是需要保留不存在的paths。为了实现这一点,我们检查哪些路径不存在,用${替换它的__,当_.template函数完成字符串的填充后,我们再将其替换回来。

我喜欢你的解决方案,但是如果元对象中不存在该键,则它将解析为未定义。例如:如果用户任务[1].名称在元对象中不存在,则应将其保留为原样,而不是设置为未定义。 - ed1t
@ed1t 两个解决方案都已更新以支持您的要求。 - Akrion

1

使用lodash实用程序库的一种方法:

var _ = require('lodash'); // use for node
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/0.10.0/lodash.min.js"></script>; // use for browser 

var data = {
"name": "Hello ${user.name}",
"primary_task": "Task Name: Task One",
"secondary_tasks": ["Task 2: ${user.tasks[0].name}", "Task 2: ${user.tasks[1].name}"]
}

var metadata = {
"user": {
    "name": "DJ",
    "tasks": [{
            "name": "Task One"
        },
        {
            "name": "Task Two"
        }
    ],
},
}
var text = JSON.stringify(data); // stringify data object
var myregexp = /\${([\[\]a-z\d.]+)}/i // regex to match the content to be replaced in data
while (match = myregexp.exec(text)) { // loop all matches
try {
    // Example: [0]=${user.name} / [1]=user.name
    new_data = text.replace(match[0], _.get(metadata, match[1])); // replace values using _ library
    text = new_data;
} catch (err) {
    console.log("Requested element doesn't exist", err.message);
}
match = myregexp.exec(text);
}
var new_data = JSON.parse(new_data); // convert new_data to object
console.log(new_data);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>


注释:

  1. Codepen.io演示
  2. Lodash文档

为什么要使用 eval?它是不必要的,而且可能会有害。 - Nikos M.
我自己不是JS专家,对于基于数组的项目感到困惑,但其他值很容易实现而无需使用eval,例如:[metadata][user.name] - Pedro Lobito
1
首先,您需要使用类似于 matchKey.replace(/\[([^\[\]])\]/, ".$1"); 的正则表达式将基于数组的索引转换为基于对象的索引,然后修剪前导/尾随点 matchedKey = matchKey.replace(/^\.|\.$/, "");,然后在点上拆分并根据每个子键遍历元数据结构,如果不存在键,则返回错误,否则返回最终值。 这应该可以工作。 - Nikos M.
我找到了一个很好的替代方案来代替eval,那就是_lodash库,它非常适合这种情况。回答已更新,感谢您的反馈。 - Pedro Lobito
1
那个 lodash 函数实现了我在评论中描述的内容,但更好的方法是将代码从 lodash 中隔离出来,并将其用作自主函数,以避免仅为一个函数而包含第三方库。 - Nikos M.
显示剩余4条评论

1

这里有一个仅使用JSON.stringify、正则表达式、matchreplacereduce和其他一些方法的纯JS解决方案。

导致问题的某些内容是,您的“variables”变量似乎没有正确的格式。“tasks”应该是“user”的子级。如果那确实是您的意图,我已为您修复了此问题。

这绝对可以进行优化,因此如果有任何简化的内容,请告诉我。

解决方案:

data = {
  "name": "Hello ${user.name}",
  "primary_task": "Task Name: ${user.tasks[0].name}",
  "secondary_tasks": ["Task 2: ${user.tasks[1].name}", "Task 2: ${user.tasks[2].name}"]
}

variables = {
  "user": {
    "name": "DJ",
    "tasks": [{
        "name": "Task Primary"
      },
      {
        "name": "Task One"
      },
      {
        "name": "Task Two"
      }
    ]
  }
}

const str = JSON.stringify(data);
const reg = /\$\{([a-z]|\[\d\]|\.)+\}/gi

const res = str.match(reg).reduce((acc, cur) => {
  //slice to remove ${ and }
  const val = cur.slice(2, -1).split(".").reduce((acc2, cur2) => {
    //check to see if it's like for example: tasks[1]
    if (cur2.indexOf("[") > -1) {
      const s = cur2.split("[");
      //Ex: acc2["tasks"][0]
      //slice to remove trailing "]"
      return acc2[s[0]][s[1].slice(0,-1)];
    }
    //Ex acc2["user"]
    return acc2[cur2];
  }, variables);
  //val contains the value used to replace the variable string name
  return acc.replace(cur, val);
}, str);

console.log(JSON.parse(res))

制作背后的逻辑如下:

将所有字符串变量名存储在一个数组中,然后使用reduce循环遍历。

const str = '{"name":"Hello ${user.name}","primary_task":"Task Name: ${user.tasks[0].name}","secondary_tasks":["Task 2: ${user.tasks[1].name}","Task 2: ${user.tasks[2].name}"]}'

console.log(str.match(/\$\{([a-z]|\[\d\]|\.)+\}/gi))

在str.match()数组中的每个值中找到相应的值。

const arrayOfStringProperties = "${user.tasks[2].name}".slice(2,-1).split(".")

console.log(arrayOfStringProperties);

//tasks[2] is the one a bit more complicated to handle

const task = "tasks[2]".split("[");
console.log(task[0], task[1].slice(0,-1));

//this allows us to get the value: metadata["user"]["tasks"]["2"]
variables = {"user": {"name": "DJ","tasks": [{"name": "Task Primary"},{"name": "Task One"},{"name": "Task Two"}]}}

console.log(variables["user"]["tasks"]["2"]["name"]);

错误处理和异步:

下面是一个包装在异步函数中的解决方案,最终可以捕获任何错误。

dataNoError = {
  "name": "Hello ${user.name}",
  "primary_task": "Task Name: ${user.tasks[0].name}",
  "secondary_tasks": ["Task 2: ${user.tasks[1].name}", "Task 2: ${user.tasks[2].name}"]
}

dataWithError = {
  "name": "Hello ${user.name}",
  "primary_task": "Task Name: ${usera.tasks[0].name}",
  "secondary_tasks": ["Task 2: ${user.tasks[1].name}", "Task 2: ${user.tasks[2].name}"]
}

variables = {
  "user": {
    "name": "DJ",
    "tasks": [{
        "name": "Task Primary"
      },
      {
        "name": "Task One"
      },
      {
        "name": "Task Two"
      }
    ]
  }
}

async function substitution(metadata, data) {
  const str = JSON.stringify(data);
  const reg = /\$\{([a-z]|\[\d\]|\.)+\}/gi

  const res = str.match(reg).reduce((acc, cur) => {
    //slice to remove ${ and }
    const val = cur.slice(2, -1).split(".").reduce((acc2, cur2) => {
      //check to see if it's like for example: tasks[1]
      if (cur2.indexOf("[") > -1) {
        const s = cur2.split("[");
        //Ex: acc2["tasks"][0]
        //slice to remove trailing "]"
        return acc2[s[0]][s[1].slice(0, -1)];
      }
      //Ex acc2["user"]
      return acc2[cur2];
    }, metadata);
    //val contains the value used to replace the variable string name
    return acc.replace(cur, val);
  }, str);

  return JSON.parse(res);
}

substitution(variables, dataNoError).then(res => console.log(res)).catch(err => console.warn(err.message));

substitution(variables, dataWithError).then(res => console.log(res)).catch(err => console.warn(err.message));


如果元对象中不存在该键,则它将解析为未定义。例如:如果元对象中不存在user.tasks[1].name,则应该保留它而不是将其设置为未定义。我的原始函数将其保留为${user.tasks[1].name}。 - ed1t

0
你可以尝试这个。它基本上是一个函数(get),它获取沿着路径的值(如果有的话),以及一个函数(replaceWhereDefined),它会在你的数据中替换给定的值。希望这能帮到你。

const data = {"name": "Hello ${user.name}", "primary_task": "Task Name: Task One", "secondary_tasks": ["Task 2: ${user.tasks[0].name}", "Task 2: ${user.tasks[2].name}"]};

const meta = {"user": {"name": "DJ", "tasks": [{"name": "Task One"}] } };

const get = (s, meta) => {

    const parts = s.replace(/(\${)|}/g, '').split('.');

    const value = parts.reduce((acc, val) => {

        const isArray = val.match(/\[\d+]/g);

        if (isArray) {

            const arr = val.match(/[a-zA-Z]+/g).toString();

            const position = isArray.toString().replace(/[\[\]]/g, '');

            acc = acc[arr];

            if (acc) acc = acc[position];
        }

        else acc = acc[val];

        return acc || {};

    }, meta);

    return typeof value === 'string' ? value : null;
};

const replaceWhereDefined = (data, meta) =>

    Object.keys(data).reduce((acc, key) => {

        const toReplace = data[key].toString().match(/\${.*?}/g);

        if (toReplace) {

            toReplace.forEach((path) => {

                const value = get(path, meta);

                if (Array.isArray(acc[key]) && value) {

                    acc[key] = acc[key].map((d) => d.replace(path, value));
                }

                else if (value) acc[key] = acc[key].replace(path, value);
            });
        }

        return acc;

    }, data);

console.log(replaceWhereDefined(data, meta));


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接