先别吐槽标题,咱们今天讲点 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.stringify 和 JSON.parse 方法的第二个参数吗?
不清楚的小伙伴不妨可以看下 MDN 的文档:
JSON.stringify(value[, replacer [, space]])
replacer 如果该参数是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果该参数是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中;如果该参数为 null 或者未提供,则对象所有的属性都会被序列化。
JSON.parse(text[, reviver])
reviver 转换器, 如果传入该参数(函数),可以用来修改解析生成的原始值,调用时机在 parse 函数返回之前。
简单来讲就是 JSON.parse 和 JSON.stringify 都提供了第二个可选参数:
- 可以
keyvalue的形式遍历当前对象所有可遍历属性。 - 支持返回新的值替换当前属性的值。
然后 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.stringify 和 JSON.parse 的参数细节,但在特定业务场景下却有奇效,下次遇到了需要递归数据操作时不妨试试先 JSON.stringify 和 JSON.parse。
over~!