WebAssembly初探
WebAssembly?
自从互联网诞生以来,到今天,它已经成为了世界上最大的社区。随着互联网发展起来的还有各类脚本语言,其中JavaScript在几轮斗争中存活下来成为今天当之无愧的“霸主”。全球最大的包管理npm中数亿的包每天被无数人引用,撑起了互联网的半壁江山。
随着互联网的发展,原本简单的应用变得逐渐臃肿。这个时候,JavaScript显得比较力不从心了。带宽的增大和网络费用的下调,网络应用似乎越来越成为现实。从以往的经验来看,带宽已经基本满足网络应用所需,但是更大的问题是:什么样的应用可以运行在互联网上呢?
几乎是共识的一件事儿是:脚本语言的性能与编译型语言的是存在差距的,这一点儿在js上体现得比较明显。Google虽然引入了V8引擎,让js有点儿静态语言的意思,性能上也提升了不少,但是和标准静态语言相比相差甚远。
为了应对高速发展的互联网,解决传统脚本语言性能不足的缺点,互联网标准化组织在接受提议并经过讨论以后,发布了WebAssembly标准。WebAssembly很大程度上提升了性能,力图解决js性能不足以应对大量计算应用的缺点。
解决性能问题的方案有很多,例如asm.js也是个优秀的解决方案。
WebAssembly提出已经相当长一段时间了,也已经有了许多成熟的项目。其中我最为震撼的应该是DOOM3,它的成功移植也让人看到WebAssembly的巨大潜力。
WebAssembluy需要浏览器提供支持,你可以在这里查看各个浏览器的兼容性。令人振奋的是,除了IE以外,几乎所有浏览器都开始陆陆续续添加对WebAssembly的支持!
而且,无法预料的是,就目前看来,WebAssembly有可能替代js成为开发首选。很有可能在不久的将来,WebAssembly将成为js的替代者,这不是危言耸听,Web IDL草案已经开始陆续征集意见,这个草案的完成度极高。Web IDL能够提供浏览器的API接口给其他语言,这意味着操纵DOM不再是js专属,只要符合标准,都可以调用!而且,调用这些API的速度会比JS更快!
所以这车,得上!稳!
这个系列我会写几篇,把我对其的了解逐一通过文章的形式分享出来。注意,这篇文章只是初探,不会过分涉及代码和原理,请不要疑惑为什么这么简单。
WebAssembly暂时无法提供polyfill支持。
怎么coding?
WebAssembly不是一门语言,是一个标准集。经过解析后,它看起来和汇编很像,可以将它看作浏览器上运行的“汇编”。直接书写不太现实,更为常规的做法是将静态语言编译成WebAssembly,就像编译成链接文件一样。
编译就需要编译器。得益于emscripten项目,目前已经有多种语言支持编译成为WebAssembly。理论上,只要符合一定标准,都可以使用它编译编译成WebAssembly。
本教程中,我们会选择一门比较前沿的语言:Rust。
Rust的理念很先进,绝大部分错误都可以通过编译器检测出来,没有GC,这意味着它不需要runtime。同时Rust的抽象是零开销的,并且代码可优化程度很高。在通过LLVM编译优化以后,Rust的性能可以直逼C/C++的运行速度,这也使得它进入T1梯队。有得有失,Rust为了实现这些,加了很多条条框框,使得书写起来不是那么简单。但我认为,Rust应该成为每个有追求的程序员应该学习的语言。
工具准备
rust的安装比较傻瓜化,只需要运行一段命令,接着按照提示弄好环境变量就行了.
安装方法(*unix):
curl https://sh.rustup.rs -sSf | sh
上面方法是用于安装rustup的。运行完上面的脚本之后,通常需要把~/.cargo/bin加入$PATH里面的。运行下面的命令:
echo PATH="$PATH:\$HOME/.cargo/bin" >> you_profile && source your_profile && rustc --version
your profile根据你的shell环境不同而不同,通常大多数人使用的是bash的.bash_profile。我是使用的zsh,因此是.zshrc。
值得注意的是,rust分为多个版本,对于支持WebAssembly的一些特性而言,需要nightly版本支持,因此一般情况下,我们都是在使用nightly。使用下面的命令切换默认配置为nightly:
rustup default nightly
接着我们需要能够将Rust代码编译成WebAssembly的工具。这里推荐wasm-pack,它几乎是现在最佳的WebAssembly的编译器,上手几乎没有难度。而且它为了和npm生态联动,使用起来和一些库很相似,尤其是webpack。它会自动将Rust编译,并且产生js代码,这个js代码是对wasm调用的封装,这样对开发者而言,使用起来就像一个普通的js包一样。另外它还产生了ts的定义文件,方便IDE代码提示。
由于是初探,因此不要把东西弄复杂了。我们先看看官方的模板吧!
首先,我们下载一个cargo-generate。顾名思义,用于根据模板生成项目的工具,类似于create-react-app。
接着,前端开发必备nodejs全家桶。
最后,浏览器,请使用最新版firefox或者chrome。
初探
我们首先下载官方提供的例子**[1]**:
cargo generate --git https://github.com/rustwasm/wasm-pack-template
接着你会得到一个工程,它的目录看起来是这样的:
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── src
│ ├── lib.rs
│ └── utils.rs
└── tests
└── web.rs
这是一个标准的rust工程,我们得稍加改造,使它可以在浏览器运行。
npm init**[2]**运行一下,使该工程同样也成为一个nodejs工程。现在,目录看起来是:
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── package.json
├── src
│ ├── lib.rs
│ └── utils.rs
└── tests
└── web.rs
接着,添加js依赖。我们需要webpack。运行以下命令**[3]**:
npm install webpack webpack-cli webpack-dev-server --save-dev
然后添加配置文件**[4]**,webpack.config.js,并写入以下内容:
const path = require('path');
module.exports = {
entry: "./bootstrap.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bootstrap.js",
},
mode: "development",
};
我们把bootstrap.js作为入口文件,因此在项目跟路径下创建它**[5]**,并在里面写入:
import('./pkg/webassembly')
.then(wasm => {
wasm.greet();
})
.catch(e => console.error(e));
注意,
wasm模块目前只能通过异步调用!因此我们需要bootstrap.js文件来异步引入它,不能直接import导入。
'./pkg/webassembly'现在不存在,那么怎么得到呢?wasm-pack派上用场了,直接build**[6]**即可,他会产生的!
wasm-pack build --out-name webassembly
该过程可能会很慢甚至失败,这是由于众所周知的原因。请搜索中科大源,将rustup和rust的源都替换为中科大提供的镜像源。运行成功以后,项目里面会出现pkg目录,里面有该wasm项目的组成文件,我们通常不会去关注他们。我们只需要调用那个js文件就行了。
然后,创建index.html文件,并引用bootstrap.js文件**[6]**:
<html>
<head>
<script src="./bootstrap.js"></script>
</head>
</html>
接着,我们直接启动webpack服务**[7]**,看看效果:
npx webpack-dev-server
如果一切顺利,那么你打开localhost:8080的时候,应该会出现一个Hello, XXX的弹窗。这意味着,你迈出了WebAssembly的第一步。现在,让我们揭开这个项目的神秘面纱。
代码
关于js的部分,我默认你已经很熟悉js了,因此不再解释。需要值得注意的是,看起来我们只是在bootstrap.js里面只是调用了import('./pkg/webassembly')来引入了wasm代码,但实际上真的这么简单吗?
当然不是。之所以我们能够这么轻松地使用,得归功于wasm-pack和webpack。wasm-pack为你生成了wasm代码的同时,也生成了相应的封装好的js、d.ts文件,这些js文件里为了自动导出了你在原生rust代码里面希望导出给js的一些方法等。
如果你去阅读其中的js代码,你会发现在里面也只是简单的使用import wasm from './webassembly.wasm'来导入WebAssembly代码。但真的是这样简单吗?wasm代码能够像普通js库一样导入吗?答案是否定的,熟悉js的你或许已经有答案了,那就是webpack。webpack将import语句转换成了对应的WebAssembly的API。这些API以后可能会出文介绍。如果你感兴趣,MDN上关于WebAssembly的主题或许可以给你提供帮助。
跳过js,我们来看看rust代码部分。这部分比较困难,因为它涉及的部分比较多。这里不会去帮你解读它的具体原理,也不会教你学Rust。
Rust教程十分的棒!
在src目录下两个文件lib.rs和utils.rs,后者没什么用,至少现在是这样,因为根本没调用。实际上它是用来调试的wasm代码的,因为在wasm的运行环境下,常规的调试条件有点儿难用。
首先是tests目录,顾名思义,是用于测试的,rust自带测试功能。同样,这个目录并没有用,里面也没有测试代码。忽略它是目前最好的做法。
在代码里面,我们使用了一个叫做wasm_bindgen的crate(rust把库/包叫做crate)。这个库是很有魔力的,它让WebAssembly变得更加容易,如同代码中:
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, webassemblu!");
}
它把js环境中的alert绑定到了rust环境中,同时把greet函数导出到js环境中,一切看起来如此简单。导出倒还好,导入就显得比较麻烦了。每次都得像书写头文件定义一样,这样也太烦人了。难道没有人把所有的函数都定义好吗?那当然有了,出自quote作者的另两个crate:web_sys和js_sys,他们分别提供了web环境和js环境的IDL绑定。
emmmmn,事情似乎变得困难了起来,这都是些什么啊?实际上,这些东西是不需要我们关心的,我们只需要学会怎么去用就行了。至于怎么实现的,这个真的有点儿复杂了。不过如果感兴趣,可以去看看源代码。
本文到此结束,或许你会问,这啥啊,什么都没讲清楚。你得明白,WebAssembly这个标准十分庞大,涉及的点儿也十分多。其次,它需要一门静态语言的支持,选择rust意味着这个难度变得更高。如果妄想一篇文章讲清楚,有点儿痴人说梦了。所以正如前文所说,这只是初探,进一步的解析,后面的文章会陆续解释的。