前端文件操作基础知识(二)编码篇

2019-05-14 00:00:00

文件操作编码知识

前言

项目开发中,遇到一个需求:后端返回 json,json中某个字段存有文件的 base64值,然后前端将文件导出下载,即

{
    "file": "文件base64编码值"
}

其实最简单的应该就是将 base64补上前缀,就可以传给 a标签下载了,但是当时就是脑子一抽,非要搞点花里胡哨的,结果就成了这样:

  • 先在前端模拟生成文件的 base64编码值
<input type='file' @change='change'>

change (e) {
    let file = e.target.files[0]
    let reader = new FileReader()
    reader.readAsDataURL(file)
    reader.onload = () => {
        let dataURL = reader.result
        let base64Str = dataURL.split(',')  // , 后面的才是数据对应的 base64码,此处的 base4字符串与后端返回的一致
        ....
    }
}
  • 将 base64含有的实际二进制数据存入 ArrayBuffer
// 先将 base64解码, TextDecoder并不能解码 base64,只能用 atob
let data = atob(base64)
let len = data.length

// 为什么用 Unit8Array? 因为 atob是使用 Latin1编码,即 1字节映射一个字符,将字符编码再传入 Uint8Array中,方能生成相同的二进制数据
let uint8 = new Uint8Array(len)
while(n) {
    uint8[n-1] = String.prototype.charCodeAt.call(data[n-1], 0)
}

// 放入 Blob再生成 url, type代表 blob的数据类型
let blob = new Blob([uint8], {type: 'application/vnd.ms-excel'})
let url = URL.createObjectURL(blob)

// 利用 a标签下载
let a = document.createElement('a')
a.href = url
a.download = '导出.xls'
a.click()

如果你看懂了,那么你可以点击右上角的关闭按钮,咱们拜拜。如果没有看懂,那说明你对其中一些细微知识不甚了解,接下来我们就开始好好聊聊了。

上面代码片段中涉及的主要知识点有:

  • FileReader的用法
  • 二进制数组 ArrayBuffer、TypeArray的使用
  • atob编码原理(包括 Latin1编码、base64编码)

前面两个在《前端文件操作基础知识(一)文件对象篇》有介绍,最后一个将在本篇进行阐述。

javascript中的编码

ES6默认采用 utf-16编码,并且是小端字节序,比如:'a'的 unicode编码为 97, 它的二进制表示为 0110 0001,而在 js中,由于是 utf-16编码且为小端字节序, 那么将存它将被存储为 0110 0001 0000 0000 共两个字节,即

a -> 97 -> 0110 0001 -> 0110 0001 0000 0000

TextEncode和 TextDecode

既然 ES6是采用 utf-16编码的,而我们常用的却是 utf-8编码,那你肯定就会想怎么才能在 ES6中进行 utf-8编码呢,对吧?

这里介绍两个方法:

  • TextEncoder

编码,按指定编码格式(默认 utf-8)对字符进行编码,并返回 uint8Array

let encoder = new TextEncoder() // 参数默认 utf-8,可以为 utf-16, gbk等其它编码格式
let str = '需要编码的字符串'
let uin8 = encoder.encode(str) // 返回 uint8Array
  • TextDecoder

解码,按指定编码格式(默认 utf-8)对 ArrayBuffer/ TypedArray 进行解码

let decoder = new TextDecoder() // 参数默认 utf-8,可以为 utf-16, gbk等其它编码格式
let uint8 = new Uint8Array([97, 98, 99])
let str = decoder.decode(uint8) // 'abc'

总结:借助 TextEncoder和 TextDecoder实现字符串与二进制的互相转化。

base64 编码原理

base64编码一共有64个字符加一个垫字的 '=', 共65个字符,所以只需要 6bit便能刚好映射完 base64字符(2^6 = 64)。

base64编码流程大体如下:

  • 1、将二进制数据按每6位为一组进行分组
  • 2、最后一组数据则补足6位
  • 3、每一组数据前面补两个0
  • 4、按 base64编码映射成字符,最后一组数据不足6位补的0如果为2个,则 base64最后还需加一个'=',如果补了4个,则加两个'='。
举个例子,"Ma"的 base64编码:
-> 在 utf-8编码下,其二进制数据为:0100 1101  0110 0001
-> 分组:010011 010110 000100   (注意,最后一组补了2个0)
-> 前面补0:00010011 00010110 00000100
-> 按 base64映射:19=>T  22=>W  4=>E
-> 最后为:TWE=

通过 window.btoa('Ma') 测试成功!

escape、encodeURI和 encodeURIComponent

  • escape (已不提倡使用)

除了 ASCII字母、数字和符号"@+-*/._" 以外,其它将编码为 unicode值(16进制)。unicode编码在1字节内,格式为%xx,超过1字节,格式为%uxxxx

escape('春节')    // %u6625%u8282

对应的解码:
unescape('%u6625%u8282')    // 春节
  • encodeURI

真正用于编码 URL的函数。除了一些常见的符号外,对其它一些网址中有特殊含义的符号 "; / ? : @ & = + $ , #" 也不进行编码。编码方式为:unicode值 -> utf-8编码成二进制 -> 16进制表示 -> 前面再加上%

// "春节"的 utf-8编码下16进制表示为 E6 98 A5 E8 8A 82
encodeURI('春节@')   // "%E6%98%A5%E8%8A%82@"

对应的解码:
decodeURI('%E6%98%A5%E8%8A%82') // '春节'
  • encodeURIComponent

编码方式与 encodeURI大体一致,只是该方式会对 "; / ? : @ & = + $ , #"这些特殊符号也进行编码

encodeURIComponent('春节@') // "%E6%98%A5%E8%8A%82%40"

对应的解码:
decodeURIComponent("%E6%98%A5%E8%8A%82%40") // '春节@'

总结:encodeURI和 encodeURIComponent将字符编码为 utf-8编码的16进制表示,即转化后所有都是 ascii码。

ascii字符 特殊符号 其他符号
encodeURI × ×
encodeURIComponent x

encodeURI设计是为了对整个 uri进行编码,而 encodeURIComponent设计是对 uri查询参数进行编码,而前端使用场景更多的是对 url的查询参数进行编码,所以主用 encodeURIComponent。

Latin1编码(别名:ISO-8859-1)

Latin1字符集是 ascii字符集的扩展,是 8bit大小的字符集,字符编码在 0 - 255之间。

ascii、Latin1和 utf-8之间的关系:

ascii是 Latin1、utf-8的子集,ascii编码的文件可以被 Latin1或 utf-8解码;
Latin1后面的字符被 ascii支持,所以 ascii解码 Latin1可能会报错
Latin1后面的字符在 utf-8中是以2字节存储的,所以如果用 utf-8解码 Latin1会乱码

atob和 btoa原理

atob和 btoa均是基于 Latin1编码实现的,所以字符编码超过 255将会报错

  • atob

用于将 base64解码为字符串, 内部流程是:ascii字符 -> base64解码成二进制 -> Latin1解码为字符

atob("YXBwbGU=")    // 'apple'
  • btoa

用于将字符编码为 base64, 内部流程是:Latin1编码字符为二进制 -> base64编码 -> ascii字符

// 使用 ascii字符串时,可正常编码
btoa('apple')   // "YXBwbGU="

// 使用中文时,由于超出 Latin1编码范围,则报错
btoa('中文')    // 报错 The string to be encoded contains characters outside of the Latin1 range.

由上面可以看出,如果通过 btoa编码中文将会报错,那应该怎么将中文编码为 base64呢,需要满足如下两点:

  • 1、能够将中文编码为 Latin1所允许的字符
  • 2、转化之后可以再转换回中文

那么,有什么方法满足上面的要求呢?对的,就是 encodeURIComponent/ encodeURI。通过这两个方法编码中文后,编码的格式为%xx,均属于 Latin1允许字符内,并且有个 % 可以作为标识,解码回中文。故而含有中文的 base64编码应该为:

let str = '中文abc'
let baseData = btoa(encodeURIComponent(str)) // 编码成 base64
let decodeStr = decodeURIComponent(atob(baseData)) // 解码成字符串 '中文abc'

charCodeAt和 fromCharCode

  • charCodeAt

获取字符在字符编码

let code = 'abc'.charCodeAt(0)  // 97
  • fromCharCode

由字符编码返回字符

let str = String.fromCharCode.call(97)    // 'a'

Gitalking ...

Markdown is supported

Be the first guy leaving a comment!