概述
平常工作中,经常遇到需要将数据在各种类型之间进行转换的情况。在nodejs环境中,这些转换通常都可以通过buffer来进行。但在浏览器环境中,就没有这么好用的工具了。看了很多程序,它们都可以进行各种操作,但总体而言比较散乱,用到时候想起来要查询,使用不太方便,开发的体验也不一致。
所以笔者借助buffer的思想,自己编写了一个转换工具来使用。这个工具的基本设定如下:
- 基于浏览器环境,不能用buffer或者crypto
- 简单,无第三方依赖
- 支持多种类型的相互转换
- 使用方便和一致,并容易移植和扩展
工具支持的转换类型,特别是对密码学操作相关的,应当包括:
- utf8: 普通文本
- json: json对象,支持utf
- base64: 用字符串表示二进制数据,特别是url版本的
- hex: 常用于密钥
- buf: 底层数据类型arrayBuffer或者Uint8Array
根据以上设定,笔者开发了简单的转换工具,记录如下。
实现
这个实现已经有了新的改进版本。解决了几个在实际应用中遇到的小问题(后详)。现在比较新的代码版本如下:
// sample utf8, obj,b64,hex,buf
const codec = (vinput, ctype = "")=>{
// convert type
let [vfrom,vto] = ctype.split("->");
if (!vfrom && !vto) return vinput;
if (!vto) { // auto mode
vto = vfrom;
if (typeof vinput === "object") {
vfrom = (Array.isArray(vinput)) ? "buf" : "obj";
} else { // string
if (vinput.length % 2 === 0 && /^[0-9a-f]+$/.test(vinput.toLowerCase())) {
vfrom = "hex";
} else if ( /^[0-9a-zA-Z\+\/\-\_]+$/.test(vinput)) {
vfrom = "b64";
} else {
vfrom = "utf8";
}
}
}
let buf ;
switch(vfrom) {
case "obj": case "json": return codec(JSON.stringify(vinput), "utf8->"+vto);
case "buf": buf = vinput; break;
case "utf8": case "str": buf = new TextEncoder().encode(vinput); break;
case "hex": buf = new Uint8Array(vinput.length/2).map((v,i)=> parseInt(vinput.substr(i*2,2), 16)); break;
case "b64":
vinput = vinput.replaceAll("-","+").replaceAll("_","/") + "=".repeat(4 - vinput.length % 4);
buf = new Uint8Array([...atob(vinput)].map(ch => ch.charCodeAt(0)));
break;
}
switch(vto) {
case "buf" : return buf;
case "hex" : return buf.reduce((c,v,i)=>(c += v.toString(16).padStart(2, '0'),c),"");
case "b64" : return btoa(buf.reduce((c,v,i)=>(c += String.fromCharCode(v),c),""))
.replaceAll("+","-").replaceAll("/","_").replaceAll("=","");
case "utf8": case "str" : return new TextDecoder().decode(buf);
case "obj" : case "json": return JSON.parse(new TextDecoder().decode(buf));
}
return vinput; // not convert
}
// 使用示例:
const o = { name: "China@中国" };
const b64 = codec(o, "obj->b64"); // 从json到base64
console.log(b64);
console.log(codec(b64,"b64->utf8"));
// 运行结果
eyJuYW1lIjoiQ2hpbmFA5Lit5Zu9In0
{"name":"China@中国"}
代码其实自己已经说明并表达了主要的设计意思。简单总结和说明一下:
- 两个参数,第一个是任意类型的输入,第二个定义转换来源和方向
- 转换方向,使用字符串表示,并使用 "->" 连接
- 转换类型的关键字是: utf8/str,obj/json,b64,hex,buf
- 转换的核心是先转成buf,然后再转到目标类型,这样非常容易扩展和修改
- 完全没有第三方依赖和高级依赖,都是标准函数
- 后期考虑增加自动类型判断(可能有风险)
- 现在暂时没有错误处理机制
- 性能并非考虑的要素,主要是易用通用,如果关注性能,可以裁剪成特定代码
问题和改进
在初始版本之后,笔者进行了简单的改进,并处理一个一个小问题。
- 增加了类型别名
obj/json, str/utf8, 现在是等效的。
- 增加了自动模式
原始或者完整的版本中,开发者需要明确指定转换来源格式和目标格式。现在不需要了,但可能有一定的风险,在极端情况下可能出现数据错误的问题。
比如原理的调用方式是: codec(vinput, "obj->b64"),现在可以直接这么写: codec(vinput, "b64")。代码会尝试分析和猜测输入的数据类型,并完成相应的配置。
- base64转换的等号裁剪和长度问题
笔者在实际使用原始版本中,偶尔遇到了无法正确的从base64字符串转回的问题。经过研究发现的原因是,笔者使用的算法,会裁剪掉base64的补位符号(=)。还原的时候,如果在nodejs环境中,是可以正确处理的。但在浏览器环境中,其实是无法正确处理的,因为缺失了补位的等号。
由于这个问题笔者在原来简单的测试代码中,刚好没有测试到这个情况,后来是由于其他程序出错才看到的。
所以,处理方式是,人为的增加补位等号。增加的数量是: (4- str.length%4)。
这里有一个示例:
const o = {token: 'P.ekw4.44668.1.b211f630e94e9f3f'};
const b64 = codec(o, "obj->b64");
// 编码后, 原始长度60, 裁剪后为58
eyJ0b2tlbiI6IlAuZWt3NC40NDY2OC4xLmIyMTFmNjMwZTk0ZTlmM2YifQ
// 正常情况,原始代码无法转回,需要补足: 4 - 58 % 4 = 2个等号
eyJ0b2tlbiI6IlAuZWt3NC40NDY2OC4xLmIyMTFmNjMwZTk0ZTlmM2YifQ==
// 正常转回
console.log(JSON.stringify(codec(b64,"json")));
{"token":"P.ekw4.44668.1.b211f630e94e9f3f"}
小结
本文作为工作笔记,记录了笔者编写和使用的一个简单通用的工具函数,可以实现在浏览器环境中,各种和密码学操作相关的数据类型的转换。它使用简单,一致性好,无第三方依赖,而且容易扩展和移植。