前言
已经超过半年没写过文章了,原因除了肯定会有的懒以外,主要还是因为,
其一,根据过去的经验,写一篇文章要花比较长的时间,要先透彻地明白自己所讲的问题;
其二,部分学习和总结是已经写成代码了,再写成文字似乎就重复了;
其三,只想写一些自觉有新意的内容。
说回这次的要实现的功能。在以前公司做导出excel文件是放在后端做的,所以当数据量和使用人数过多的时候就出现了接口超时的问题。为了减轻后端的压力,所以这次打算后端提供必要的数据,excel文件由前端生成。
说到文件生成,我第一时间就想到了web worker
和webAssembly
的使用,加上之前学了下rust
,正好可以学以致用。
这个功能做完以后,粗略地测试了下,导出一个263KB
文件,webAssembly
和js
的实现的导出速度。
webAssembly|js
———–|—
62ms| 52ms
所以从效果上来说,webAssembly
实现完全是白做了。查了些资料,我觉得原因主要是:
- 生成Excel的计算量不大,数据的传送反而占了比较多的时间
- 数据要传给
webAssembly
,需要先转成JSON
,再encode
为Uint8Array
,这个是与JS
的实现相比额外的消耗。
项目介绍
这个项目是一个fork项目,除去原有的核心的文件构建逻辑,我做的改动主要有:
- 对
verticalAlign
的支持 - 支持数字类型的单元格数据
- 添加作为回退方案的
js
实现
改动1其实只是按照原来的做法,增加对
verticalAlign
的处理。改动2的问题是对既可能是字符串又可能是数字的数值处理,处理的方法是改成枚举类型,然后还要增加两行宏
#[derive(Deserialize)]
,#[serde(untagged)]
,前者是支持serde
库进行反序列化,后者是让serde
自动判断反序列转化的类型。详细可以在这里查阅1
2
3
4
5
6
pub enum Value {
Number(f64),
String(String)
}改动3是因为webAssembly只有在17年后的浏览器有支持,所以需要
js
方案作兼容,此方案用到了exceljs
这个库,由于是运行在worker
环境,不可通过script
标签加载,另一方面第三方库是希望作为外部引用的,所以需要给打包生成的worker
文件头部增加importScript
方法。经过一些资料的查找,只需要设置rollup
的output.banner
即可
对WebAssembly的认识
下面讲述自己阅读WebAssembly
相关的材料后,梳理出的对WebAssembly
的认识。
WebAssembly
从名字中能看出其两个性质,第一是它有着与汇编语言相似的格式,其指令易于机器执行;第二是它是被设计为面向网络应用的,包括客户端和服务端。它有两种格式:
.wat
(WebAssembly text format file),因为它是文本格式,所以我们可以阅读和编辑,但它不能直接被执行,需要转换为.wasm
文件。.wasm
,真正的WebAssembly程序文件,由二进制编码。由于WebAssembly
是类汇编的初级语言,所以它可以被如C++
和rust
等高级语言作为编译对象而生成出来,从这个意义上,它有着不区分开发语言,通用跨平台的特点,就像我们系统中的可执行文件一样。
WebAssembly
的开发方式:- 直接编写
.wat
文件。 - 编写高级语言后进行编译。
由于
.wat
的数据类型目前只有i32 | i64 | f32 | f64
4种,虽然可以定义函数,但运算操作是基于栈式虚拟机,比较初级,相比高级语言,编写代码量过大并且与实际业务逻辑的编写习惯相差甚远,所以生产开发是选择方式2不过为了理解栈式虚拟机和
WebAssembly
的执行机制,下面通过某个网络安全题目提供的.wasm
转译成的.wat
内容进行简单说明。
- 直接编写
1 | (module |
以下为Run
函数的JS
版本1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29function Run($0, $1) {
let $2;
let $3;
let $4;
let $5;
let $6;
let $7;
let delta;
$2 = $0;
$4 = $1 - 1;
{
do {
$3 = $2;
$6 = 0;
$7 = 10;
do {
$5 = $3 % 10;
$3 = Math.floor($3 / 10);
$6 = Math.max($5, $6);
$7 = Math.min($5, $7);
} while ($3 > 0);
$2 += $6 * $7;
$4 -= 1;
} while ($4 != 0);
}
return $2;
}
WebAssembly
和JS
之间的通信JS
和WebAssembly
之间较常见的是互传function
和WebAssembly.Memory
,通过上面说到的imports
(JS
传给WebAssembly
)和WebAssembly.Instance.exports
(WebAssembly
传给JS
) 。限制:函数的传参在这里的限制只能是使用上面提到的
4种
数据类型。那怎么去传递复杂的数据类型呢?方法是通过memory buffer
,其可以JS
端通过WebAssembly.Memory
创建,或者WebAssembly
端通过exports
导出自己的内存,而这里关键是这个Memory
是共享的,两端都可以进行操作。具体做法,通过
wasm-bindgen
生成js glue code
里的工具方法说明:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// 用Uint8Array来表示wasm的buffer
function getUint8Memory0() {
if (cachegetUint8Memory0 === null || cachegetUint8Memory0.buffer !== wasm.memory.buffer) {
cachegetUint8Memory0 = new Uint8Array(wasm.memory.buffer);
}
return cachegetUint8Memory0;
}
//设置字符串到`buffer`(可以把对象`stringify`以后作为字符串传入)
function passStringToWasm0(arg, malloc, realloc) {
// ....
// 通过TextEncoder将字符串编码为UTF-8编码的Uint8Array
const buf = cachedTextEncoder.encode(arg);
// malloc为rust vm exports的方法,为字符串分配空间,分配空间的逻辑由rust完成
const ptr = malloc(buf.length);
// 返回ptr是Uint8Array的整数索引。相当于指针,这里把buf的值设置到wasm内存的这个区间里[ptr, ptr + buf.length]
getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
// ....
}
// 如果是Uint8Array就更简单了,直接把数组的值设置到buffer即可
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1);
getUint8Memory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}上面两个传递数据的工具函数都有两个关键的值,那就是
ptr
和WASM_VECTOR_LEN
,毫无疑问,这两个值都是整数类型,都可以通过函数进行传递,而实际数据就以Memory
为介质,通过这种方法就解决了复杂数据传递的问题了。
后记
以上就是通过开发导出Excel需求后,对开发过程和WebAssembly
学习的总结,可能WebAssembly
在一般的前端开发里,比较少应用场景。但基于知识储备的考虑和兴趣,自然而然地就会去学习这个在2019年12月被W3C
认定为既html
, css
, js
的第四种开发语言。
上面记述的内容主要是面向我自己的总结,可能并不那么详尽,不过至少理解了上面的内容以后,我大概明白了WebAssembly
的工作机制。
下面是每周邮件中推荐的文章,同时亦时本文的参考,感兴趣的可以一读