JavaScript 中将内存中的简单 JSON 对象转储为 YAML
为了简单, 手写了一个简单的 JSON 对象转储为 JSON 的代码.

日前笔者在 Cloudflare Workers 上搭建了一个在线分发配置信息的服务. 笔者本人在用的应用程序使用的均为 JSON 格式的配置, 使用 JavaScript 自带的库就能解决. 不过前日需要分享配置信息给朋友, 而对方那边需要 YAML 格式的配置文件. 由于服务并不复杂, 而引入外部库就需要建立项目并使用 Wrangler, 因此笔者倾向于使用单文件, 这样仅在面板上就能完成对 Cloudflare Workers 的调试与维护.

简单的 YAML 语法并不复杂, 它和 JSON 很类似, 以缩进表示层级, 主要特性有列表, 多行字符串字面量等. 将内存中现有的 JSON 对象写出为符合 YAML 的格式也很简单, 递归就能很好解决.

先来一个简单的 JSON 对象:

let obj = {
  "name": "Henry Black",
  "nickname": ["Henry", "O Black"],
  "optionals": {
    "married": false,
    "age": 23,
    "hobbies": [
      {"name": "music", "weight": 0.8},
      {"name": "reading", "weight": 0.3},
      {"name": "sports", "weight": 0.7},
    ],
  },
};

其对应的一种合法的 YAML 如下所示:

name: Henry Black
description: |
  Hi, there.
  Nice to meet you!  
nickname:
  - Henry
  - O Black
optionals:
  married: false
  age: 23
  hobbies:
    -
      name: music
      weight: 0.8
    -
      name: reading
      weight: 0.3
    -
      name: sports
      weight: 0.7

直接动手写个 Javascript 中的递归函数:

// 递归函数, 遍历某对象的 attributes
// obj:   待处理对象
// depth: 记录递归的层数
function dumpToYaml(obj, depth=0) {
  const indent = '  ';  // 用于缩进的单位字符串
  let s = '';  // 存储 YAML 转储结果的字符串
  const isArray = Array.isArray(obj);  // 判断对象是不是普通数组
  for (const i in obj) {  // 遍历 "对象 (关联数组) 的各个属性" 或者 "数组中每个元素"
    // 对象如果是数组, 输出 `-`; 否则, 则输出属性名
    s += indent.repeat(depth) + (isArray ? '- ' : `${i}: `);
    const val = obj[i];  // 当前所遍历到的属性对应的值 / 数组中的元素
    switch (typeof val) {  // 根据元素类型做出行为
      case 'string':  // 字符串
        const l = val.split('\n');  // 将字符串按换行断开
        const d = '\n' + indent.repeat(depth + 1);  // 多行间的间隔: 换行 + 缩进量
        // 判断是否是多行字符串, 若是则采用多行标记法, 否则直接输出
        s += l.length > 1 ? ('|' + d + l.join(d)) : val;
        s += '\n';
        break;
      case 'number':  // 数值类型
      case 'boolean':  // 布尔值类型
        s += `${val}\n`;  // 均直接 toString
        break;
      default:  // 除此之外则应为对象, 则递归处理
        s += '\n';
        s += dumpToYaml(val, depth + 1);
    }
  }
  return s;
}

测试通过. 不过在测试中, 发现还有另外一些没有注意到的细节: 比如多行字符串首行行首若有空格, 需要额外的标记; 有些特殊符号需要 quote 起来. 示范 YAML 如下所示:

name: Henry Black
description: |2-
   Hi, there.
  Nice to meet you!
optionals:
  hobbies:
    - name: "~"
      weight: 0.5

修改后的对应部分代码如下:

    switch (typeof val) {
      case 'string':
        const l = val.split('\n');  // 将字符串按换行断开
        const d = '\n' + indent.repeat(depth + 1);  // 多行间的间隔: 换行 + 缩进量
        s += l.length > 1 // 判断是否是多行字符串
          ? ('|' + (l[0][0] === ' ' ? `${indent.length* (depth + 1)}-` : '') + d + l.join(d))
          : '`~!@#%&:,?\'"{}[]|-'.includes(val[0]) ? `"${ val.replaceAll('"', '\\"')}" ` : val;
        s += '\n';
        break;
      case 'number':
      case 'boolean':
      	/* ... */
      default:
        /* ... */
    }

完整代码如下

let obj = {
  "name": "Henry Black",
  "description": " Hi, there.\nNice to meet you!",
  "nickname": ["Henry", "O Black"],
  "optionals": {
    "married": false,
    "age": 23,
    "hobbies": [
      [{"name": "music", "weight": 0.8},
      {"name": "reading", "weight": 0.3},],
      [{"name": "sports", "weight": 0.7},
      {"name": "~", "weight": 0.5},]
    ],
  },
};

function dumpToYaml(obj, depth=0, inArray) {
  const indent = '  ';
  let s = '';
  const isArray = Array.isArray(obj);
  for (const i in obj) {
    s += (inArray ? '' : indent.repeat(depth)) + (isArray ? '- ' : `${i}: `);
    inArray = false;
    const val = obj[i];
    switch (typeof val) {
      case 'string':
        const l = val.split('\n');
        const d = '\n' + indent.repeat(depth + 1);
        s += l.length > 1 
          ? `|${l[0][0] === ' ' ? `${indent.length * (depth + 1)}-` : ''}${d}${l.join(d)}`
          : '`~!@#%&:,?\'"{}[]|-'.includes(val[0]) ? `"${val.replaceAll('"', '\\"')}"` : val;
        s += '\n';
        break;
      case 'number':
      case 'boolean':
        s += `${val}`;
        s += '\n';
        break;
      default:
        s += isArray ? '' : '\n';
        s += dumpToYaml(val, depth + 1, isArray);
    }
  }
  return s;
}

console.log(dumpToYaml(obj))

在线 Demo: /2023/02/dump-to-yaml.html


Last modified on 2023-02-17