qs 功能强大的查询字符串/数组/对象解析器

发布于 2020-04-27 17:52:29 字数 16076 浏览 1414 评论 0

带有一些附加安全性的查询字符串解析和字符串化库。

使用

var qs = require('qs');
var assert = require('assert');

var obj = qs.parse('a=c');
assert.deepEqual(obj, { a: 'c' });

var str = qs.stringify(obj);
assert.equal(str, 'a=c');

解析对象

qs.parse(string, [options]);

QS 允许您在查询字符串中创建嵌套对象,方法是在子键的名称周围加上方括号 []。例如,字符串 'foo[bar]=baz' 转换:

assert.deepEqual(qs.parse('foo[bar]=baz'), {
    foo: {
        bar: 'baz'
    }
});

当使用 plainObjects 选项解析的值作为空对象返回,该对象是通过 Object.create(null) 因此,您应该知道,它上不存在原型方法,用户可以将这些名称设置为他们喜欢的任何值:

var nullObject = qs.parse('a[hasOwnProperty]=b', { plainObjects: true });
assert.deepEqual(nullObject, { a: { hasOwnProperty: 'b' } });

默认情况下,将忽略覆盖对象原型上属性的参数,如果希望保留这些字段中的数据,请使用 plainObjects 如上文所述,或 allowPrototypes 设置为 true 这将允许用户输入覆盖这些属性。

警告:启用此选项通常是个坏主意,因为它在尝试使用已覆盖的属性时可能会造成问题。这个选项一定要小心。

var protoObject = qs.parse('a[hasOwnProperty]=b', { allowPrototypes: true });
assert.deepEqual(protoObject, { a: { hasOwnProperty: 'b' } });

URI 编码的字符串也可以工作:

assert.deepEqual(qs.parse('a%5Bb%5D=c'), {
    a: { b: 'c' }
});

你也可以嵌套你的对象,比如 'foo[bar][baz]=foobarbaz':

assert.deepEqual(qs.parse('foo[bar][baz]=foobarbaz'), {
    foo: {
        bar: {
            baz: 'foobarbaz'
        }
    }
});

默认情况下,当嵌套对象时 QS 只会解析多达 5 个层级的深度。这意味着,如果您试图解析一个字符串,如 'a[b][c][d][e][f][g][h][i]=j' 您的结果对象将是:

var expected = {
    a: {
        b: {
            c: {
                d: {
                    e: {
                        f: {
                            '[g][h][i]': 'j'
                        }
                    }
                }
            }
        }
    }
};
var string = 'a[b][c][d][e][f][g][h][i]=j';
assert.deepEqual(qs.parse(string), expected);

可以通过传递 depth 选择 qs.parse(string, [options]):

var deep = qs.parse('a[b][c][d][e][f][g][h][i]=j', { depth: 1 });
assert.deepEqual(deep, { a: { b: { '[c][d][e][f][g][h][i]': 'j' } } });

深度限制有助于在下列情况下减少滥用 QS 用于解析用户输入,建议将其保持在相当小的数目。

出于类似的原因,默认情况下 QS 将只解析多达 1000 个参数。可以通过传递 parameterLimit 备选方案:

var limited = qs.parse('a=b&c=d', { parameterLimit: 1 });
assert.deepEqual(limited, { a: 'b' });

若要绕过前面的问号,请使用 ignoreQueryPrefix:

var prefixed = qs.parse('?a=b&c=d', { ignoreQueryPrefix: true });
assert.deepEqual(prefixed, { a: 'b', c: 'd' });

还可以传递一个可选的分隔符:

var delimited = qs.parse('a=b;c=d', { delimiter: ';' });
assert.deepEqual(delimited, { a: 'b', c: 'd' });

分隔符也可以是正则表达式:

var regexed = qs.parse('a=b;c=d,e=f', { delimiter: /[;,]/ });
assert.deepEqual(regexed, { a: 'b', c: 'd', e: 'f' });

参数 allowDots 可用于启用点表示法:

var withDots = qs.parse('a.b=c', { allowDots: true });
assert.deepEqual(withDots, { a: { b: 'c' } });

如果您必须处理遗留浏览器或服务,也支持将百分比编码的octets解码为iso-8859-1:

var oldCharset = qs.parse('a=%A7', { charset: 'iso-8859-1' });
assert.deepEqual(oldCharset, { a: '§' });

一些服务添加了一个初始 utf8=✓ 值到 URL 中,以便旧的 InternetExplorer 版本更有可能以 utf-8 的形式提交表单。此外,服务器还可以根据校验字符的错误编码检查值,并检测查询字符串或 application/x-www-form-urlencoded 身体不作为 utf-8 发送,如果表单有一个 accept-charset 参数或包含的页具有不同的字符集。

QS 通过 charsetSentinel 选择。如果指定,则 utf8 参数将从返回的对象中省略。它将用于切换到 iso-8859-1 / utf-8 模式,取决于复选标记的编码方式。

重要当您指定两个 charset 选项和 charsetSentinel 选项,charset 当请求包含 utf8 参数,从中可以推导出实际的字符集。从这个意义上讲,charset 将表现为默认字符集,而不是权威字符集。

var detectedAsUtf8 = qs.parse('utf8=%E2%9C%93&a=%C3%B8', {
    charset: 'iso-8859-1',
    charsetSentinel: true
});
assert.deepEqual(detectedAsUtf8, { a: 'ø' });

// Browsers encode the checkmark as ✓ when submitting as iso-8859-1:
var detectedAsIso8859_1 = qs.parse('utf8=%26%2310003%3B&a=%F8', {
    charset: 'utf-8',
    charsetSentinel: true
});
assert.deepEqual(detectedAsIso8859_1, { a: 'ø' });

如果您想解码 &#...; 语法到实际字符,可以指定 interpretNumericEntities 备选方案还包括:

var detectedAsIso8859_1 = qs.parse('a=%26%239786%3B', {
    charset: 'iso-8859-1',
    interpretNumericEntities: true
});
assert.deepEqual(detectedAsIso8859_1, { a: '☺' });

中检测到字符集时,它也可以工作。charsetSentinel 模式。

解析数组

QS也可以使用类似的 [] 符号:

var withArray = qs.parse('a[]=b&a[]=c');
assert.deepEqual(withArray, { a: ['b', 'c'] });

您还可以指定一个索引:

var withIndexes = qs.parse('a[1]=c&a[0]=b');
assert.deepEqual(withIndexes, { a: ['b', 'c'] });

请注意,数组中的索引与对象中的键之间唯一的区别是括号之间的值必须是创建数组的数字。当创建具有特定索引的数组时,QS将稀疏数组压缩到只保留其顺序的现有值:

var noSparse = qs.parse('a[1]=b&a[15]=c');
assert.deepEqual(noSparse, { a: ['b', 'c'] });

请注意,空字符串也是一个值,将保留如下:

var withEmptyString = qs.parse('a[]=&a[]=b');
assert.deepEqual(withEmptyString, { a: ['', 'b'] });

var withIndexedEmptyString = qs.parse('a[0]=b&a[1]=&a[2]=c');
assert.deepEqual(withIndexedEmptyString, { a: ['b', '', 'c'] });

QS还将限制在数组中指定索引的最大索引为20。索引大于20将被转换为以索引为键的对象。这是处理案件所需要的,例如,当有人发送,例如,a[999999999] 迭代这个巨大的数组需要花费大量的时间。

var withMaxIndex = qs.parse('a[100]=b');
assert.deepEqual(withMaxIndex, { a: { '100': 'b' } });

可以通过传递 arrayLimit 备选方案:

var withArrayLimit = qs.parse('a[1]=b', { arrayLimit: 0 });
assert.deepEqual(withArrayLimit, { a: { '1': 'b' } });

若要完全禁用数组解析,请设置parseArraysfalse.

var noParsingArrays = qs.parse('a[]=b', { parseArrays: false });
assert.deepEqual(noParsingArrays, { a: { '0': 'b' } });

如果你把符号混在一起,QS将这两个项合并为一个对象:

var mixedNotation = qs.parse('a[0]=b&a[b]=c');
assert.deepEqual(mixedNotation, { a: { '0': 'b', b: 'c' } });

还可以创建对象数组:

var arraysOfObjects = qs.parse('a[][b]=c');
assert.deepEqual(arraysOfObjects, { a: [{ b: 'c' }] });

有些人用逗号连接数组,QS可以解析它:

var arraysOfObjects = qs.parse('a=b,c', { comma: true })
assert.deepEqual(arraysOfObjects, { a: ['b', 'c'] })

(这不能转换嵌套对象,例如a={b:1},{c:d})

Stringifying

qs.stringify(object, [options]);

转换对象的时候,QS 默认情况下,URI 对输出进行编码。对象将如您所期望的那样被压缩:

assert.equal(qs.stringify({ a: 'b' }), 'a=b');
assert.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');

可以通过设置 encode选择 false:

var unencoded = qs.stringify({ a: { b: 'c' } }, { encode: false });
assert.equal(unencoded, 'a[b]=c');

可以通过设置 encodeValuesOnly 选择 true:

var encodedValues = qs.stringify(
    { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
    { encodeValuesOnly: true }
);
assert.equal(encodedValues,'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h');

此编码还可以由自定义编码方法(设置为 encoder 备选方案:

var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str) {
    // Passed in values `a`, `b`, `c`
    return // Return encoded string
}})

(注:encoder 选项不适用于 encode 是 false)

模拟encoder有一个decoder可供选择的parse若要重写属性和值的解码:

var decoded = qs.parse('x=z', { decoder: function (str) {
    // Passed in values `x`, `z`
    return // Return decoded string
}})

通过使用提供给编码器的类型参数,可以使用不同的逻辑对键和值进行编码:

var encoded = qs.stringify({ a: { b: 'c' } }, { encoder: function (str, defaultEncoder, charset, type) {
    if (type === 'key') {
        return // Encoded key
    } else if (type === 'value') {
        return // Encoded value
    }
}})

类型参数还提供给解码器:

var decoded = qs.parse('x=z', { decoder: function (str, defaultEncoder, charset, type) {
    if (type === 'key') {
        return // Decoded key
    } else if (type === 'value') {
        return // Decoded value
    }
}})

超出此点的示例将被显示为输出不是为清晰性而编码的URI。请注意,在这些情况下返回值将要在实际使用期间编码URI。

当数组被字符串化时,默认情况下会给它们显式索引:

qs.stringify({ a: ['b', 'c', 'd'] });
// 'a[0]=b&a[1]=c&a[2]=d'

您可以通过设置indices选择false:

qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false });
// 'a=b&a=c&a=d'

您可以使用arrayFormat选项指定输出数组的格式:

qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })
// 'a[0]=b&a[1]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })
// 'a[]=b&a[]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })
// 'a=b&a=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' })
// 'a=b,c'

当对象被字符串化时,默认情况下它们使用括号表示法:

qs.stringify({ a: { b: { c: 'd', e: 'f' } } });
// 'a[b][c]=d&a[b][e]=f'

您可以通过设置allowDots选择true:

qs.stringify({ a: { b: { c: 'd', e: 'f' } } }, { allowDots: true });
// 'a.b.c=d&a.b.e=f'

空字符串和空值将省略该值,但等于符号(=)仍然有效:

assert.equal(qs.stringify({ a: '' }), 'a=');

没有值的键(如空对象或数组)将不返回任何内容:

assert.equal(qs.stringify({ a: [] }), '');
assert.equal(qs.stringify({ a: {} }), '');
assert.equal(qs.stringify({ a: [{}] }), '');
assert.equal(qs.stringify({ a: { b: []} }), '');
assert.equal(qs.stringify({ a: { b: {}} }), '');

属性设置为undefined将完全省略:

assert.equal(qs.stringify({ a: null, b: undefined }), 'a=');

查询字符串可以选择性地加上问号:

assert.equal(qs.stringify({ a: 'b', c: 'd' }, { addQueryPrefix: true }), '?a=b&c=d');

分隔符也可以被字符串覆盖:

assert.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');

如果您只想重写Date对象时,可以提供serializeDate备选方案:

var date = new Date(7);
assert.equal(qs.stringify({ a: date }), 'a=1970-01-01T00:00:00.007Z'.replace(/:/g, '%3A'));
assert.equal(
    qs.stringify({ a: date }, { serializeDate: function (d) { return d.getTime(); } }),
    'a=7'
);

您可以使用sort选项影响参数键的顺序:

function alphabeticalSort(a, b) {
    return a.localeCompare(b);
}
assert.equal(qs.stringify({ a: 'c', z: 'y', b : 'f' }, { sort: alphabeticalSort }), 'a=c&b=f&z=y');

最后,您可以使用filter选项来限制将哪些键包含在字符串化输出中。如果传递一个函数,将为每个键调用它以获得替换值。否则,如果传递一个数组,它将用于选择属性和数组索引以进行字符串化:

function filterFunc(prefix, value) {
    if (prefix == 'b') {
        // Return an `undefined` value to omit a property.
        return;
    }
    if (prefix == 'e[f]') {
        return value.getTime();
    }
    if (prefix == 'e[g][0]') {
        return value * 2;
    }
    return value;
}
qs.stringify({ a: 'b', c: 'd', e: { f: new Date(123), g: [2] } }, { filter: filterFunc });
// 'a=b&c=d&e[f]=123&e[g][0]=4'
qs.stringify({ a: 'b', c: 'd', e: 'f' }, { filter: ['a', 'e'] });
// 'a=b&e=f'
qs.stringify({ a: ['b', 'c', 'd'], e: 'f' }, { filter: ['a', 0, 2] });
// 'a[0]=b&a[2]=d'

处理 null

默认情况下,null 值被视为空字符串:

var withNull = qs.stringify({ a: null, b: '' });
assert.equal(withNull, 'a=&b=');

解析不能区分有和没有相同符号的参数。两者都转换为空字符串。

var equalsInsensitive = qs.parse('a&b=');
assert.deepEqual(equalsInsensitive, { a: '', b: '' });

区分 null 值和空字符串使用 strictNullHandling 选项。在结果字符串中,null价值没有=标志:

var strictNull = qs.stringify({ a: null, b: '' }, { strictNullHandling: true });
assert.equal(strictNull, 'a&b=');

若要解析值,请不要 = 回到 null 使用 strictNullHandling 选项:

var parsedStrictNull = qs.parse('a&b=', { strictNullHandling: true });
assert.deepEqual(parsedStrictNull, { a: null, b: '' });

完全跳过呈现键的步骤。null 值,使用 skipNulls 选项:

var nullsSkipped = qs.stringify({ a: 'b', c: null}, { skipNulls: true });
assert.equal(nullsSkipped, 'a=b');

如果您正在与遗留系统通信,则可以切换到iso-8859-1 使用 charset 备选方案:

var iso = qs.stringify({ æ: 'æ' }, { charset: 'iso-8859-1' });
assert.equal(iso, '%E6=%E6');

中不存在的字符 iso-8859-1 将转换为数值实体,类似于浏览器所做的工作:

var numeric = qs.stringify({ a: '☺' }, { charset: 'iso-8859-1' });
assert.equal(numeric, 'a=%26%239786%3B');

您可以使用 charsetSentinel 选项通过包括 utf8=✓ 参数,如果选中标记,则使用适当的编码,类似于RubyonRails和其他人在提交表单时所做的工作。

var sentinel = qs.stringify({ a: '☺' }, { charsetSentinel: true });
assert.equal(sentinel, 'utf8=%E2%9C%93&a=%E2%98%BA');

var isoSentinel = qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' });
assert.equal(isoSentinel, 'utf8=%26%2310003%3B&a=%E6');

处理特殊字符集

默认情况下,字符的编码和解码是在 utf-8,和 iso-8859-1 支持也是通过 charset 参数。

如果希望将查询字符串编码到不同的字符集,例如 Shift JIS,您可以使用 qs-iconv 库:

var encoder = require('qs-iconv/encoder')('shift_jis');
var shiftJISEncoded = qs.stringify({ a: 'こんにちは!' }, { encoder: encoder });
assert.equal(shiftJISEncoded, 'a=%82%B1%82%F1%82%C9%82%BF%82%CD%81I');

这也适用于查询字符串的解码:

var decoder = require('qs-iconv/decoder')('shift_jis');
var obj = qs.parse('a=%82%B1%82%F1%82%C9%82%BF%82%CD%81I', { decoder: decoder });
assert.deepEqual(obj, { a: 'こんにちは!' });

RFC 3986 和 RFC 1738 空格编码

RFC 3986作为默认选项并编码“”到%20这是向后兼容的。同时,输出可以按照RFC 1738与“等于‘+’进行串制。

assert.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
assert.equal(qs.stringify({ a: 'b c' }, { format : 'RFC3986' }), 'a=b%20c');
assert.equal(qs.stringify({ a: 'b c' }, { format : 'RFC1738' }), 'a=b+c');

相关链接

Github 地址:https://github.com/ljharb/qs

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84960 人气
更多

推荐作者

漫雪独思

文章 0 评论 0

垂暮老矣

文章 0 评论 0

鹊巢

文章 0 评论 0

萌酱

文章 0 评论 0

雨说

文章 0 评论 0

冰葑

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文