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

时间:2018-11-10 22:50:24

标签: javascript node.js

我有一个由数组组成的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对象上调用它,以便它可以替换map和array中的所有字符串值

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} 在元数据中不存在,因此它不应解析为未定义。如果未在元对象中找到密钥,则应将其保留不变,即${user.tasks[2].name}

4 个答案:

答案 0 :(得分:2)

仅使用 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))

字符串插值的思想受到this thread的影响。这个主意 是递归地遍历所有对象值,并使用interpolate函数返回实际的水合字符串。遍历路径getPath以及发现不存在的路径。如果字符串中的路径不存在,则使用字符串替换来使该字符串通过字符串水合,然后我们将其替换。

Lodash _.template 方案:

在可以利用lodash及其templating mechanism(通过_.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 :(得分:1)

使用loadash实用程序库的方法:

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 Demo
  2. Lodash Documentation

答案 2 :(得分:1)

这里是使用纯JS的解决方案,只需使用JSON.stringify,regex,matchreplacereduce和其他方法即可。

确实引起问题的原因是您的“变量”变量似乎格式不正确。 “任务”应该是“用户”的子级。如果确实是您想要的,我已为您修复了该问题。

这绝对是可以优化的,所以让我知道是否有什么可以简化的。

解决方案:

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

如何做到这一点的逻辑如下:

获取数组中所有str变量名称,然后使用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));

答案 3 :(得分:0)

您可以尝试一下。从本质上讲,它是一个函数(获取),它为您提供路径后的值(如果有),并且一个给定值的函数(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));