【问题标题】:JSON recursive value substitution against another object针对另一个对象的 JSON 递归值替换
【发布时间】:2019-04-14 02:43:12
【问题描述】:

我有一个 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} 在元中不存在,因此它不应解析为未定义。如果在元对象中找不到密钥,它应该保持原样,即${user.tasks[2].name}

【问题讨论】:

  • 我写了一个解析属性函数,你可以扩展它来处理数组。 stackblitz.com/edit/typescript-fchtqv
  • 为什么第一名的应用是这样设计的?
  • 数据和元数据从何而来? ${} 位如何进入数据?我假设您没有对数据进行硬编码?是否有任何参数定义元数据的结构?如果数据有字段,元数据没有怎么办?
  • metadata.user.tasks 中只有一项,这就是为什么 "Task 2: ${user.tasks[2].name}" 没有被替换(user.tasks[2] 不存在)。您的预期输出是什么?

标签: javascript node.js


【解决方案1】:

你可以试试这个。它基本上是一个函数 (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));

【讨论】:

    【解决方案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函数完成水合字符串时,我们将它替换回来。

    【讨论】:

    • 我喜欢您的解决方案,但如果元对象中不存在密钥,它将解析为未定义。例如:如果元对象中不存在 user.tasks[1].name,则应保持原样,而不是设置为未定义。
    • @ed1t 两种解决方案都已更新以支持您的要求。
    【解决方案3】:

    使用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);
    &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"&gt;&lt;/script&gt;

    注意事项:

    1. Codepen.io Demo
    2. Lodash Documentation

    【讨论】:

    • 为什么使用eval?它不是必需的,并且可能有害
    • 正如注释代码中所述,我找不到更好的解决方案来解决这个问题。如果您有 eval 的替代品,请随时展示。
    • 确保您可以根据匹配的键遍历元数据结构。匹配的键是基于对象的,即param.param2`(在. 上拆分)和/或基于数组的即param1[index2],您在[] 上拆分并匹配索引。它比 eval 工作更多,但使用起来肯定更好
    • 首先将基于数组的索引转换为基于对象的索引,使用像matchKey.replace(/\[([^\[\]])\]/, ".$1"); 这样的正则表达式,然后修剪前导/尾随点matchedKey = matchKey.replace(/^\.|\.$/, "");,然后根据每个子点拆分并遍历元数据结构-key,如果不存在键则返回错误,否则返回最终值。这应该工作
    • lodash 函数实现了我在评论中描述的内容,但最好将代码与 lodash 隔离开来,并将其用作自治函数,以避免仅包含用于函数的 3rd-pary 库
    【解决方案4】:

    这里有一个纯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));

    【讨论】:

    • 如果键在元对象中不存在,它将解析为未定义。例如:如果元对象中不存在 user.tasks[1].name,则应将其保留为未定义,而不是设置为未定义。我原来的功能是 ${user.tasks[1].name}
    猜你喜欢
    • 2015-03-13
    • 2023-03-24
    • 2017-02-18
    • 2017-03-26
    • 2012-12-05
    • 2018-04-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多