本文做者:刘观宇,360奇舞团高级前端工程师、技术经理,曾参与360导航、360影视、360金融、360游戏等多个大型前端项目。关注W3C标准、IOT、机器学习的最新进展,现为W3C CSS工做组成员。javascript
Web应用的蓬勃发展,使得JavaScript、Web前端,乃至整个互联网都发生了深入的变化。前端开始承担起了更多的职责,因而对于执行效率的诉求也就更为急迫。除了在语言自己的进化,Web从业者以及各大浏览器厂商,也在不停地进行探索。2012年Mozillia的工程师提出了Asm.js和Emscripten,使得C/C++以及多种编程语言编写的高效程序转译为JavaScript并在浏览器运行成为可能。html
更进一步地,WebAssembly(简称wasm)技术被提出,并迅速成立了各类研发组织,各类周边工具链的不断完善,相关实验数据也有力地佐证了这条优化和加速路线的可行性。前端
特别是2018年,W3C的WebAssembly工做组发布了第一个工做草案,包含了核心标准、JavaScript API以及Web API。另外,除了C/C++和Rust以外,Golang语言也正式支持了wasm的编译。咱们罕见的看到,各大主流浏览器一致表示支持这一新的技术,也许一个崭新的Web时代即将到来。java
打开wasm的官网,咱们能够看到其宏伟的技术目标。除了定义一个可移植、精悍、载入迅捷的二进制格式以外,还有对移动设备、非浏览器乃至IoT设备支持的规划,而且还会逐步创建一系列工具链。感兴趣的读者,能够从这里看到wasm官方的阐述。node
简单的说,wasm并非一种编程语言,而是一种新的字节码格式,目前,主流浏览器都已经支持 wasm。与 JavaScript 须要解释执行不一样的是,wasm字节码和底层机器码很类似可快速装载运行,所以性能相对于 JavaScript 解释执行有了很大的提高。webpack
下面这张图,展现了目前(2018年7月)主流浏览器对于wasm的支持状况。git
除了在浏览器上能够运行外,目前wasm已经能够在包括NodeJS等命令行环境下运行。github
按照最初的设想,各类高级语言经过本身的前端编译工具,将本身的源代码编译成为底层虚拟机(LLVM)可识别的中间语言表示(LLVM IR)。此时,底层的LLVM能够将LLVM IR根据不一样的CPU架构生成不一样的机器码,同时能够对这些机器码进行编译时的空间与性能的优化。大多数的高级语言都是按照这样的结构来支持wasm的。上述提到的两个步骤,也依次被成为编译器前端和编译器后端。web
编译到wasm的代码,是最终进行实际工做的程序。对此,有一种名为S-表达式的文本格式,扩展名为.wast,以方便程序猿阅读。借助wabt工具链能够实现wasm和wast的互转。一个S-表达式形如:typescript
(module
(type $iii (func (param i32 i32) (result i32)))
(memory $0 0)
(export "memory" (memory $0))
(export "add" (func $assembly/module/add))
(func $assembly/module/add (; 0 ;) (type $iii) (param $0 i32) (param $1 i32) (result i32)
;;@ assembly/module.ts:2:13
(i32.add
;;@ assembly/module.ts:2:9
(get_local $0)
;;@ assembly/module.ts:2:13
(get_local $1)
)
)
)
复制代码
目前已经有多种高级语言支持对wasm的编译,特别是AssemblyScript,这种以TypeScript为基础语言,经过AssemblyScript的工具链支持,能够完成最终到wasm的转换。
根据上述架构,浏览器以及各类运行环境提供者,各自经过提供不一样的运行支持以抹平各个CPU架构不一样形成的差别,使得须要支持wasm高级语言,只须要支持编译到中间语言表示层。能够预见的是,随着开发环境的温馨度逐步提升,愈来愈多的高级语言也会加入支持wasm的阵营。
下面的实践,咱们须要借助AssemblyScript来完成,AssemblyScript定义了一个TypeScript的子集,意在帮助TS背景的同窗,经过标准的JavaScript API来完成到wasm的编译,从而消除语言的差别,让程序猿能够快乐的编码。
AssemblyScript项目主要分为三个子项目:
这里须要说明的是,目前工具链还在开发过程当中,个别步骤可能还不太稳定。咱们尽可能保证安装配置过程的严谨,若是遇到有变更,请以官方描述为准。
为了支持编译,咱们首先须要安装AssemblyScript的支持。为了编译的顺利进行,首先须要保证你的Node版本在8.0以上。同时,你须要安装好TypeScript运行环境。
下面让咱们开始吧:
为了不后面依赖的问题,咱们首先安装AssemblyScript支持
git clone https://github.com/AssemblyScript/assemblyscript.git
cd assemblyscript
npm install
npm link
复制代码
执行上述命令后,你可使用命令asc
来断定是否安装正确。若是正常安装,命令行会显示asc命令的使用说明。
接下来,咱们新建一个NPM项目,如:wasmExample。若是须要,能够加入ts-node和typescript的devDependencies,并安装好依赖。 而后,在项目根目录下,咱们新建一个目录:assembly。 咱们进入assembly目录,同时咱们在这里加入tsconfig.json,内容以下:
{
"extends": "../node_modules/assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
]
}
复制代码
下面,咱们在这个目录下加入简单的ts代码,以下:
export function add(a: i32, b: i32): i32 {
return a + b;
}
复制代码
咱们把上面这段TypeScript代码存储为:module.ts。 那么,如今从项目根目录来看,咱们的文件结构以下图:
为了后面运行简便,咱们把build步骤加入到npm scripts里面,方法是打开项目根目录的package.json,更新scripts字段为:
"scripts": {
"build": "npm run build:untouched && npm run build:optimized",
"build:untouched": "asc assembly/module.ts -t dist/module.untouched.wat -b dist/module.untouched.wasm --validate --sourceMap --measure",
"build:optimized": "asc assembly/module.ts -t dist/module.optimized.wat -b dist/module.optimized.wasm --validate --sourceMap --measure --optimize"
}
复制代码
为了项目整洁,咱们把编译目标放到项目根目录的dist文件夹,此时,咱们须要在项目根目录下新建dist目录。
如今,在项目根目录下,咱们来运行:npm run build
若是没有报错的话,你会看到,在dist目录下生成了6个文件。
咱们先没必要深究文件的具体内容。此时,咱们的编译工做已经作好。细心的读者可能看到了,在上面的编译命令里面使用了不一样的参数。这些参数,咱们能够直接在命令行下键入asc
来查询命令以及参数的使用细节。
如今咱们有了编译的结果。目前,因为wasm还只能由JavaScript引入,所以咱们还须要将编译出的wasm引入到JavaScript程序中。
咱们在项目根目录加入一个module引入代码:module.js,以下:
const fs = require("fs");
const wasm = new WebAssembly.Module(
fs.readFileSync(__dirname + "/dist/module.optimized.wasm"), {}
);
module.exports = new WebAssembly.Instance(wasm).exports;
复制代码
同时,咱们须要一个使用module的代码。如:index.js,以下:
var myModule = require("./module.js");
console.log(myModule.add(1, 2));
复制代码
激动人心的时刻到了,咱们在项目根目录下运行node index.js
,看看结果是否正如咱们所期待。读者自行能够修改index.js里面的调用数据,来测试模块的正确性。须要注意的是,由于是wasm是有数据类型概念的,并且数据类型比TypeScript 更为精确。因此,上面的例子中,若是输入的不是整数(上例指wasm定义的i32),会和传统的JavaScript结果不一致,好比你的调用是myModule.add(2.5, 2)
,结果多是4。所以,咱们须要在调用wasm程序时候,严格关注数据类型。
上面的段落,咱们展现了如何与NodeJS整合,其实对于效率提高更为显著的,当属在浏览器中。那么如何在浏览器中使用咱们编译好的代码呢?
对于JavaScript调用wasm,通常采用以下步骤:
完整的步骤,也能够参见下面的流程图:
这里提供一个异步代码的例子,咱们将其命名为async_module.js:
// 异步引入例子
const fs = require("fs");
const readFile = require("util").promisify(fs.readFile);
const getInstance = async (wasm, importObject={}) => {
let buffer = new Uint8Array(wasm)
return await WebAssembly.instantiate(wasm, importObject)
}
let ins;
const noop = () => {};
const exportFun = (obj, funName) => {
return (typeof obj[funName] === "function")
? obj[funName] : noop;
}
async function getModuleFun(filePath, funName ,importObject={}) {
if (ins){
return exportFun(ins, funName)
}
const wasmText = await readFile(filePath);
const mod = await getInstance(wasmText, importObject);
return exportFun(mod.instance.exports, funName)
}
module.exports = getModuleFun;
复制代码
调用时候,咱们只需以下代码,便可愉快地利用wasm对象进行编码了:
var myModule = require("./async_module.js");
// 调用代码
(async () => {
const fun = await myModule(__dirname + "/dist/module.optimized.wasm", "add")
console.log(fun(1, 2))
console.log(fun(4, 10000))
})()
复制代码
这里是目前所有的JavaScript中与wasm协做的API说明
从webpack4开始,官方提供了默认的wasm的加载方案。若是你的webpack是webpack4之前的版本,可能须要安装诸如assemblyscript-typescript-loader
等开发包。
笔者目前所使用的webpack版本为:4.16.2,对于wasm的原生支持已经比较完善。根据官方的信息,以后的webpack5,会对wasm进行更为稳健的支持。
以下代码便可简单的引入wasm模块,运行npx webpack
能够将代码自动编译:
import("./module.optimized.wasm").then(module => {
const container = document.createElement("div");
container.innerText = "Hello, WebAssembly.";
container.innerText += " add(1, 2) is " + module.add(1, 2);
document.body.appendChild(container);
});
复制代码
因为wasm目前不能直接操做Dom,若是须要这种操做,可能须要借助JavaScript的能力,这种状况下,咱们须要在wasm中调用JavaScript。
WebAssembly.instance 和 WebAssembly.instantiate 函数均支持第二个参数 importObject,这个importObject 参数的做用就是 JavaScript 向 wasm 传入须要调用的JavaScript模块。
做为演示,咱们把上面的module.js代码修改一下,把相加的结果,用“*”的个数来表示。这里咱们为了演示方便,依然使用同步代码,实际上,异步代码更为经常使用。
const fs = require("fs");
const wasm = new WebAssembly.Module(
fs.readFileSync(__dirname + "/dist/module.optimized.wasm"), {}
);
module.exports = new WebAssembly.Instance(wasm, {
window:{
show: function (num){
console.log(Array(num).fill("*").join(""))
}
}
}).exports;
复制代码
调用方index.js
修改成:
var myModule = require("./module.js");
myModule.add(1, 2);
复制代码
同时,咱们须要修改TypeScript源码:
// 声明从外部导入的模块类型
declare namespace window {
export function show(v: number): void;
}
export function add(a: i32, b: i32): void {
window.show(a + b);
}
复制代码
咱们回到项目根目录,从新运行npm run build
。
以后,运行node index.js
咱们看到,原来的结果,改成用*的个数来表示了。说明WebAssembly调用JavaScript代码成功。
对于wasm技术,咱们总结以下:
尽管如此,笔者仍然很是看好wasm的前景,在性能要求很高的如游戏、影音应用等领域,或许会有不错的发展。
本文选题过程,参考了安佳、李松峰、刘宇晨等同事的建议。成文后,李松峰老师和刘宇晨给出了不少中肯的修订意见,在此一并表示诚挚的谢意。
《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送连接到后台便可给咱们投稿。