有用的 JSON.stringify

581 阅读4分钟

先别吐槽标题,咱们今天讲点 JSON.stringify 不常用的操作。

看个例子

const data = {
    global: {
        preview: 'https://bucket.oss-cn-hangzhou.aliyuncs.com/images/p1.png'
    },
    layouts: [
        {
            elements: [
                {
                    imageUrl: 'https://bucket.oss-cn-hangzhou.aliyuncs.com/images/p2.png',
                    elements: [
                        {
                            imageUrl: 'https://bucket.oss-cn-hangzhou.aliyuncs.com/images/p3.png'
                        }
                    ]
                },
                {
                    url: 'https://bucket2.oss-cn-hangzhou.aliyuncs.com/images/p2.png'
                }
            ],
            backgroundImage: 'https://bucket.oss-cn-hangzhou.aliyuncs.com/images/p4.png'
        }
    ]
};

你同往常一样从接口拿到的模板数据将图片展示到页面上,某天后端同学为 oss 资源加了个 CDN 域名后需要让原本走 bucket.oss-cn-hangzhou.aliyuncs.com 的图片统一走 some.oss-cdn.com 加速图片资源的访问,这时你需要将数据中的所有图片资源的域名 bucket.oss-cn-hangzhou.aliyuncs.com 改成 cdn 域名 some.oss-cdn.com

知道数据结构情况我们当然可以直接遍历整个对象:

function replace(url) {
    return url.replace('bucket.oss-cn-hangzhou.aliyuncs.com', 'some.oss-cdn.com');
}
function replaceOssHost(data) {
    data.global.preview = replace(data.global.preview);
    data.layouts.forEach(layout => {
        layout.backgroundImage = replace(layout.backgroundImage);
        layout.elements.forEach(element => {
            element.url = replace(element.url);
            element.imageUrl = replace(element.imageUrl);
            if (element.elements) {
                // ...
            }
        });
    });
    return data;
}

嗯,一看就很不靠谱!随便加一个新的 image 资源字段,遍历的规则就得改了,所以我们需要一个与具体解构无关的替换方法,大概如下:

function replaceOssHost(data) {
    Object.entries(data).forEach(([k, v]) => {
        if (v && typeof v === 'object') {
            return replaceOssHost(v);
        }
        if (typeof v === 'string') {
            data[k] = replace(v);
        }
    });
    return data;
}

这不就是递归遍历一个对象么?所以和 JSON.stringify 有啥关系?

常被忽略 JSON.stringify 参数

问题:你知道 JSON.stringifyJSON.parse 方法的第二个参数吗?

不清楚的小伙伴不妨可以看下 MDN 的文档:

JSON.stringify(value[, replacer [, space]])

replacer 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。

JSON.parse(text[, reviver])

reviver 转换器, 如果传入该参数(函数),可以用来修改解析生成的原始值,调用时机在 parse 函数返回之前。

简单来讲就是 JSON.parse 和 JSON.stringify 都提供了第二个可选参数:

  • 可以 key value 的形式遍历当前对象所有可遍历属性。
  • 支持返回新的值替换当前属性的值。

然后 JSON.parse 和 JSON.stringify 遍历顺序有些区别:

const data = {
    "1": 1,
    "2": 2,
    "3": {
        "4": 4,
        "5": {
            "6": 6
        }
    }
};

const dataStr = JSON.stringify(data);

JSON.stringify(data, function (k, v) {
    console.log(`${k}:`, v);
    return v;
});
// 输出:根对象 k 为空,遍历从外层往内层遍历
// : { '1': 1, '2': 2, '3': { '4': 4, '5': { '6': 6 } } } 对象自身时的 key 会是空的
// 1: 1
// 2: 2
// 3: { '4': 4, '5': { '6': 6 } }
// 4: 4
// 5: { '6': 6 }
// 6: 6

JSON.parse(dataStr, function (k, v) {
    console.log(`${k}:`, v);
    return v;
});
// 输出:根对象 k 为空,从最最里层的属性开始,一级级往外,最终到达顶层,也就是解析值本身
// 1: 1
// 2: 2
// 4: 4
// 6: 6
// 5: { '6': 6 }
// 3: { '4': 4, '5': { '6': 6 } }
// : { '1': 1, '2': 2, '3': { '4': 4, '5': { '6': 6 } } } 对象自身时的 key 会是空的

JSON.stringify 的遍历会更符合直觉一点,JSON.parse 遍历属性为对象时会优先从对象的子级开始遍历,大多时候我们并不太关心遍历顺序,但需要注意一下遍历对象自身时的 key 会是空的

看回原来的例子,使用 JSON.stringify 可以十分方便实现 host 的替换:

使用 JSON.stringify 进行对象遍历

简单几行就可以实现替换的操作:

function replaceOssHost(data) {
    return JSON.parse(JSON.stringify(data, function (k, v) {
        if (v && typeof v === 'string') {
            return replace(v);
        }
        return v;
    }));
}

当然这是比较简单的例子,下面看个稍微复杂的例子:

const data = {
    global: {
        preview: 'data:image/png;base64,iVBORw0KGgoAAAANSUhE...'
    },
    layouts: [
        {
            elements: [
                {
                    imageUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhE...',
                    elements: [
                        {
                            imageUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhE...'
                        }
                    ]
                },
                {
                    url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhE...'
                }
            ],
            backgroundImage: 'data:image/png;base64,iVBORw0KGgoAAAANSUhE...'
        }
    ]
};

同样是图片资源,但是资源变成了 base64 data,而且现在要求在保存模板数据数据时需要将 base64 资源上传到 oss 并替换成对应的链接。

base64 上传是个异步操作,JSON.stringify 的 replacer 是个同步函数,我们无法直接在 replacer 中进行上传和图片替换,改如何处理呢?

实现其实很简单,将上传和替换分离就好了,直接上代码了:

const data = {
    global: {
        preview: 'data:image/png;base64,iVBORw0KGgoAAAANSUhE...'
    }
};

// 遍历返回资源 map
function getResources(data) {
    const resources = new Map();
    const content = JSON.stringify(data, function (_, v) {
        if (v && typeof v === 'string' && v.includes('base64,')) {
            resources.set(v, v);
        }
        return v;
    });
    return { content, resources };
}

// base64 资源上传
function uploadBase64(base64) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('https://some.oss-cdn.com/images/xxx.png');
        }, 200);
    });
}

async function uploadResources(data) {
    const { content, resources } = getResources(data);
    // 上传替换 base64 为 url
    for (let [_, value] of resources) {
        const url = await uploadBase64(value);
        resources.set(value, url);
    }
    // 在根据 resources value 和 url 映射替换 base64 为 url
    return JSON.parse(content, function(_, v) {
        if (!v || typeof v !== 'string' || !v.includes('base64,')) {
            return v;
        }
        return resources.get(v) || v;
    });
}

之前很少关注 JSON.stringifyJSON.parse 的参数细节,但在特定业务场景下却有奇效,下次遇到了需要递归数据操作时不妨试试先 JSON.stringifyJSON.parse

over~!