89357412 2019-06-30
在浏览器环境下,解释运行JavaScript脚本实现高阶功能早已是家常便饭,然而,Web前端日新月异的需求已逐渐无法完全依赖JavaScript实现。幸运的是,打破瓶颈的新技术已逐渐成熟,它就是WebAssembly。
WebAssembly是一项神奇的技术,简而言之就是一种底层的类汇编语言,其编译后的二进制模块wasm可在浏览器中运行以接近原生的性能运行CC++、C#、Java、GO、PHP、Rust等等语言的代码!自2015年颁布、2017年初正式发布最小功能版本以来,WebAssembly迅速开始盛行,并已得到主流浏览器的广泛支持,详细支持情况可以参见下图或MDN:

(数据采于2019-01-25)
需要强调的是:WebAssembly并不旨在取代JavaScript或任何现有的H5/ES6技术,而是与他们共存 - 我们耳熟能详的WebGL、Web Audio等组件都是WebAssembly模块在浏览器端的运行时,在浏览器端实现所需功能
那么问题来了 - WebAssembly究竟和asm.js、Dart等类似技术有何不同?我们早已可以通过Emscripten编译asm.js在浏览器中跑c/c++了,为什么还需要WebAssembly呢?相比之下,WebAssembly主要具备以下优势:
那么如此神奇的技术究竟如何编译运行?当下最主流编译器可谓就是Emscripten了,广泛应用于原始语言->LLVM中间码->JavaScipt(asm.js)的编译。当然在WebAssembly全面企稳的今天,直接将原始语言编译成WebAssembly(wasm)也不在话下。
较新版本的Emscripten支持跳过LLVM中间码->asm.js->wasm的过度,直接编译wasm,以c语言为例可通过如下命令直接编译:
# `WASM=1`:仅生成wasm模块(默认为LLVM中间码),`SIDE_MODULE=1`:仅编译用户代码,而不包含printf、memalloc等函数 ./emcc hello-world.c -O3 -s WASM=1 -s SIDE_MODULE=1 -o hello-world.html
编译生成的结果包括:
其中编译生成的hello-world.js是帮助我们在页面中调用加载wasm模块的脚本,我们也可结合Fetch API在自己的代码进行加载:
fetch('path/to/wasm')
.then(response => response.arrayBuffer()) //将wasm文件响应转为二进制数组
.then(bits => WebAssembly.compile(bits)) //编译模块
.then(module => { return new WebAssembly.Instance(module) }); //生成模块实例可通过自带的emrun工具在指定浏览器中运行编译结果,或直接托管在Web服务器上:
emrun --browser /path/to/browser/executable hello-world.html
接下来我们就进入今天的实战:将经由Autodesk Forge Model Derivative服务轻量化的模型,通过Forge AR/VR Toolkit导入Unity场景,结合C#/JSLIB脚本与Unity插件,编译为WebAssembly,并集成至我们的前端框架中!
(左为Forge Viewer,右为本例)


发布目标平台为WebGL,并在发布设定中将连接器目标设为WebAssembly,开始Build编译:

编译结果包括:
<项目名>.json(包括运行所需的参数与设置)、UnityLoad.js(浏览器加载wasm所需的脚本)、<项目名>.*.unityweb(发布设定中指定格式的压缩包,包含wasm模块与场景资源等)Build目录导入前端项目的静态资源路径(如./src/assets)npm install react-unity-webgl
import Unity, { UnityContent } from "react-unity-webgl";
class App extends Component {
unityContent:UnityContent = new UnityContent(
"Build/forge_sample.json", \\引用编译结果,将所有编译结果置于相同路径下
"Build/UnityLoader.js" \\并确保浏览器会话可以http协议访问
);
//....
render() {
//...
<Unity unityContent={this.unityContent} />
}
}this.unityContent.send(
"Unity对象名称",
"C#或JSLIB脚本函数名称",
1 //参数值
);[DllImport("__Internal")]
private static extern void EventName (int arg);
public void CallAnEvent (int arg) {
EventName(arg);
}Assets/Plugins/WebGL/forge-sample.jslib)注入事件:mergeInto(LibraryManager.library, {
EventName: function(arg) {
ReactUnityWebGL.EventName(arg);
}
});this.unityContent.on("EventName", arg => {
//...
});public class NewBehaviourScript : MonoBehaviour {
//...
[DllImport("__Internal")]
private static extern void EventName ();
void OnSceneLoaded (Scene scene, LoadSceneMode mode) {
EventName();
}
//...
}npm install vue-unity-webgl
<template>
<div>
...
<unity src="Build/forge_sample.json" unityLoader="Build/UnityLoader.js"></unity>
<!-- 引用编译结果,将所有编译结果置于相同路径下,并确保浏览器会话可以http协议访问 -->
...
</div>
</template>
<script>
import Unity from 'vue-unity-webgl'
//...
export default {
components: { Unity }
//...
}
</script>UnityLoader.js与我们的模块并不兼容(该库不能引用外置Loader),因此我们结合了无框架的引用方式来做示范UnityLoad.js<script language="JavaScript" src="assets/Build/UnityLoader.js"></script>
<!-- app.component.html --> <div id='unityContainer'></div>
//app.component.ts
declare var UnityLoader: any; //声明UnityLoader为任意类
export class AppComponent implements AfterViewInit{
private unityInstance: any;
//...
ngAfterViewInit(){
(<any>window).UnityLoader = UnityLoader; \\将UnityLoader对象暴露为窗体具柄
this.unityInstance = UnityLoader.instantiate('unityContainer', './assets/Build/forge_sample.json'); //引用编译结果,将所有编译结果置于相同路径下,并确保浏览器会话可以http协议访问
}
sendMessage(objectName: string, functionName: any, argumentValue: any) {
this.unityInstance.SendMessage(objectName, functionName, argumentValue); //与Unity对象通讯
}
//...
}
(左为Forge Viewer,右为本例)
编译后的wasm是二进制的,可以通过编译工具(如WABT、Binaryen等)生成或转换为WebAssembly Text (wat) Format - 人类可读的类汇编代码:
(module
(func $i (import "imports" "imported_func") (param i32))
(func (export "exported_func")
i32.const 42
call $i
)
)在浏览器中也可以查看wat,并断点调试