在浏览器环境下,解释运行JavaScript脚本实现高阶功能早已是家常便饭,然而,Web前端日新月异的需求已逐渐无法完全依赖JavaScript实现。幸运的是,打破瓶颈的新技术已逐渐成熟,它就是WebAssembly。什么是WebAssemblyWebAssembly是一项神奇的技术,简而言之就是一种底层的类汇编语言,其编译后的二进制模块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主要具备以下优势:WebAssembly模块不论在加载速度和性能上都有明显优势 - 它以二进制码的形式在浏览器中原生运行,无需像asm.js那样将原始语言编译成JavaScript,远超JavaScript引擎解释脚本的运行速度,即便首屈一指的Chrome V8有JIT加持也无济于事。WebAssembly并不是基于现有组建的扩展,而是一个Web开发新特性/标准,它有独立的路线图,不断有新特性加入进来。不受asm.js等技术在AOT等层面的限制,特性拓展潜力极大,应用场景广泛,详见底部延伸阅读部分的介绍。编译与运行那么如此神奇的技术究竟如何编译运行?当下最主流编译器可谓就是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.wasm: wasm模块二进制码hello-world.html: 展示页面hello-world.js: 读取wasm模块的JavaScript其中编译生成的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实战Unity+WebAssembly接下来我们就进入今天的实战:将经由Autodesk Forge Model Derivative服务轻量化的模型,通过Forge AR/VR Toolkit导入Unity场景,结合C#/JSLIB脚本与Unity插件,编译为WebAssembly,并集成至我们的前端框架中!图片描述参考该教程,为Unity3D项目配置好Forge AR/VR Toolkit将轻量化后的模型导入至Unity场景,如图所示填入模型的URN和从Forge服务端获取的Access Token图片描述需注意将.NET运行时版本调整为4.5以便支持TLS 1.2图片描述完成场景的建模与开发,本例结合Cinemachine的Freelook Camera插件与简单的c#脚本实现由键鼠操控的场景漫游。Cinemachine是一套强大的Unity相机管理工具,可利用其路径(Path)路点(Waypoint)等特性(并结合Timeline)轻松制作强大的预制路线漫游等效果。通过较新Unity3D(2017/5.6+)直接将场景编译为WebAssembly,设定发布目标平台为WebGL,并在发布设定中将连接器目标设为WebAssembly,开始Build编译:图片描述图片描述编译结果包括:html:展示页面Build目录:<项目名>.json(包括运行所需的参数与设置)、UnityLoad.js(浏览器加载wasm所需的脚本)、<项目名>..unityweb(发布设定中指定格式的压缩包,包含wasm模块与场景资源等)Template:展示页面依赖与前端框架整合浏览展示页面确认实际效果无误后,将Build目录导入前端项目的静态资源路径(如./src/assets)接下来将分别介绍针对Vue、React、Angular框架与无框架的整合React推荐使用react-unity-webgl加载Unity WebAssembly:npm install vue-unity-webgl在React组建中调用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} /> }}与Unity中的对象通讯 this.unityContent.send( “Unity对象名称”, “C#或JSLIB脚本函数名称”, 1 \参数值 );在Unity脚本中与JavaScript通讯: [DllImport("__Internal")] private static extern void EventName (int arg); public void CallAnEvent (int arg) { EventName(arg); }在Unity中创建JSLIB脚本(如Assets/Plugins/WebGL/forge-sample.jslib)注入事件:mergeInto(LibraryManager.library, { EventName: function(arg) { ReactUnityWebGL.EventName(arg); }});在前端监听该事件 this.unityContent.on(“EventName”, arg => { \…- 同理我们可以在前端监听Unity的生命周期事件:public class NewBehaviourScript : MonoBehaviour {... [DllImport("__Internal")] private static extern void EventName (); void OnSceneLoaded (Scene scene, LoadSceneMode mode) { EventName();}... }##Vue- 推荐使用vue-unity-webgl组件加载Unity WebAssembly:npm install vue-unity-webgl- 在Vue组件中调用<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>##Angular与无框架- 对于适用于Angular的Unity组件库,我们只找到了ng-unity,但在实测中出现报错,似乎是由于其内置的UnityLoader.js
与我们的模块并不兼容(该库不能引用外置Loader),因此我们结合了无框架的引用方式来做示范- 在页面中引用编译生成的UnityLoad.js
<script language=“JavaScript” src=“assets/Build/UnityLoader.js”></script>- 组件页面中加入容器元素 / app.component.html/ <div id=‘unityContainer’></div>- 在组件中载入模块\app.component.tsdeclare 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对象通讯} ...}- 运行结果如下![图片描述][9]#调试与优化编译后的wasm是二进制的,可以通过编译工具(如WABT、Binaryen等)生成或转换为WebAssembly Text (wat) Format
- 人类可读的类汇编代码:(module (func $i (import “imports” “imported_func”) (param i32)) (func (export “exported_func”)i32.const 42call $i))在浏览器中也可以查看wat
,并断点调试![图片描述][10]##优化考量- 将wasm等依赖在服务器端压缩,加速网络传输,以Node后台为例: https://blog.csdn.net/github_38140984/article/details/83011150- 编译时的优化(如使用WebGL2.0,.NET4.5等),参考: https://docs.unity3d.com/Manual/class-PlayerSettingsWebGL.html#Optimization- 使用WebAssembly JIT:https://webassembly.org/docs/jit-library/#延伸阅读- Forge AR/VR介绍:https://segmentfault.com/a/1190000013672044- Unity Cinemachine插件学习笔记: https://blog.csdn.net/l773575310/article/details/78070808- Emscripten-WebAssembly专栏:https://segmentfault.com/blog/yunhuangbeiqing- WebAssembly入门: https://www.jianshu.com/p/bff8aa23fe4d- WebAssembly入门到入门:https://blog.csdn.net/m549393829/article/details/81839822- WebAssembly应用案例: https://blog.csdn.net/frf0lw4/article/details/79267457 [1]: /img/bVbnQqb [2]: /img/bVbnQqh [3]: /img/bVbnQqi [4]: /img/bVbnQqp [5]: /img/bVbnQqw [6]: /img/bVbnQqA [7]: /img/bVbnQqs [8]: /img/bVbnQqt [9]: /img/bVbnQqu