乐趣区

实战Forge Viewer渐进应用 – 当Xamarin遇上WebAssembly

Xamarin 作为移动端的跨平台原生开发框架的老牌劲旅,一直被视作 Mono Project 寄予厚望的当家花旦之一。近年来,虽然 React Native/Ionic 等后起之秀夺去大半江山,但随着 Xamarin 入驻微软并宣告免费,加之.NET/C# 生态的日益完善与精进,Xamarin 已然重焕青春!
那么,如果我们将它与上期技术分享介绍的业界新贵 WebAssembly 双剑合璧,又会迸发出怎样的化学反应呢?今天我们就将 Autodesk Forge Viewer 离线方案整合到 Xamarin,并发布为基于 WebAssembly 的 Progressive Web App(简称 PWA,渐进式应用)– 即实现浏览器 URL 访问直接安装的神奇效果!
引言彩蛋
首先,让我们 First things first:????Autodesk ADN(开发者社区团队)祝您在新的一年大吉大利!!???? 事顺利!!????
为什么要 WebAssembly 和 PWA?
在原生和 H5 应用如火纯青的今天,WebAssembly 和 PWA 的相对意义与优势在于:

如上期技术分享介绍,WebAssembly 赋能我们在浏览器中以原生的性能运行 CC#、Java、Rust 等及基于它们的框架和技术
编译后的执行包(wasm)为二进制,精简高效的同时保护源代码,各种安全机制防范恶意代码的篡改与攻击,增强应用的安全性
PWA 给予用户非原生应用胜似原生应用的浏览器客户端体验,通过浏览器访问 URL 即可完成 PWA 的安装,无需发布至应用商店

Xamarin 的优势
相比 Cordova/PhoneGap、Ionic、Appgyver 等 H5 混合框架,以及 React Native、Titanium 等原生框架,我们为什么要关注 Xamarin 呢?

相较 Cordova/PhoneGap、Ionic、Appgyver 等混合框架具有原生优势

相较基于 JavaScript 的 React Native、Titanium,.NET/C#在语言某些特性上具备相对优势,也便于熟悉.NET/C# 桌面与后台开发的朋友迅速上手
.NET/C# 生态的相对优势,如可以使用我们的 Forge .NET Client SDK

相较基于状态的 Functional Approach 具有更底层原生 UI 的优势

实战开始!
今天实战的环节为:Forge Viewer 渐进应用 > Xamarin 应用 > 发布为 WebAssembly 的 PWA > 移动端测试
Forge Viewer PWA
往期我们有介绍过利用 ServiceWorker 和 Cache 等 API 实现 Forge Viewer 离线方案,但是悉心的朋友或许已经发现该方案仍有不少瑕疵(将在下期着重阐述),现在我们更一进步: 将整个加载过程离线缓存与客户端! 其成果是独立的 Forge Viewer PWA,满足移动和桌面端的离线使用。

首先定义 Viewer 渐进应用的 ServiceWorker,有关该 API 的详细介绍可以参考往期。不同于往期中介绍的方案,这次我们将要缓存所有 Viewer 脚本、样式和加载模型的请求,且在预设的缓存列表中只有 CSS,其余脚本依赖与模型数据全部在应用首次加载阶段缓存,省去手动适配不同 Viewer 版本与模型资源的麻烦。首先我们来监听请求事件,收集所有需要缓存的请求,并记录从后台获取的 Access Token,以供 Forge API 认证所需。出于性能考虑,待模型加载完成后再统一缓存所有资源:
const urlsToCache = [
‘viewer.html’, //Viewer 页面路径
‘viewer-serviceworker.js’, // 本 ServiceWorker 路径
‘https://developer.api.autodesk.com/modelderivative/v2/viewers/6.*/style.min.css’ //Viewer.js 样式
];

self.addEventListener(‘fetch’, event => {

event.respondWith(
caches.match(event.request)
.then(async response => {
if (response) return response;

if (event.request.url.endsWith(‘/api/token’)) {\\ 判断请求指向获取 Access Token 的后台服务

const response = await fetch(event.request);
fetchOptions.headers = {‘Authorization’: ‘Bearer ‘ + response.access_token} \\ 设定访问 Forge API 请求的 Access Token
return response;

} else fetches.push(event.request.url);
return fetch(event.request)
})
)
});

在 ServiceWorker 中定义缓存操作
self.addEventListener(‘message’, async event => {
switch (event.data.operation) {
case ‘EXECUTE_CACHE’:
await caches.open(CACHE_NAME).then(async cache => await Promise.all(fetches.map(url=>fetch(url, fetchOptions).then(resp => cache.put(url, resp)))));
event.ports[0].postMessage({status: ‘ok’, fetches});
break;
}
});

待 Viewer 触发 GEOMETRY_LOADED_EVENT,即模型加载完毕,一切所需资源已记录在案,再统一触发缓存
navigator.serviceWorker.register(‘/service-worker.js’).then((registration) => {
alert(‘Service worker registered’, registration.scope);
let script = document.createElement(‘script’);
script.onload = function () {
const viewer = new Autodesk.Viewing.Private.GuiViewer3D(myViewerDiv);

Autodesk.Viewing.Initializer(options, () => {

… // 按需以在线或离线模式初始化 Viewer 并加载模型

viewer.addEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, () => {

const channel = new MessageChannel();
channel.port1.onmessage = (event) => console.log(event);

navigator.serviceWorker.controller.postMessage({operation: ‘EXECUTE_CACHE’}, [channel.port2]); // 模型加载完成,该模型所需资源已作记录,遂向 ServiceWorker 发送消息,开始缓存所需资源
})
});
};
script.src = “https://developer.api.autodesk.com/modelderivative/v2/viewers/6.*/viewer3D.min.js”;
document.head.appendChild(script)

});

最后定义后台服务,获取 Access Token,.NET、Node、PHP 的教程可以参看这里

整合 Viewer PWA 至 Xamarin 应用

在 Visual Studio 中创建 Xamarin 项目,注意引用 Xamarin.Forms v2.5 而非 v3.x!

由于 Viewer 采用 WebGL 实现,其 PWA 的整合亦采用 WebView,创建基于 Xamarin XAML 的 Top Banner+WebView 的 UI 界面,其中 WebView 指向我们之前定义的 Viewer PWA 页面
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto” />
<RowDefinition Height=”*” />
</Grid.RowDefinitions>
<StackLayout BackgroundColor=”DimGray” VerticalOptions=”FillAndExpand” HorizontalOptions=”Fill”>
<StackLayout Orientation=”Horizontal” HorizontalOptions=”Center” VerticalOptions=”Center”>
<ContentView Padding=”0,10,0,10″ VerticalOptions=”FillAndExpand”>
<Image Source=”{Binding logo}” VerticalOptions=”Center” HeightRequest=”24″ />
</ContentView>
</StackLayout>
</StackLayout>
<ScrollView Grid.Row=”1″>
<StackLayout Orientation=”Vertical” >
<ContentView VerticalOptions=”FillAndExpand” >
<WebView Source=”URL/TO/YOUR/VIEWER/PWA.html”></WebView>
</ContentView>
</StackLayout>
</ScrollView>
</Grid>

可用所见即所得的设计器(如 GorillaPlayer)设计 XAML,和 Visual Studio 2017 自带的 XAML Previewer 预览 UI 效果(如箭头所示于 XAML 编辑器右上角按钮进入)

编译运行并检查在 UI 和 WebView 加载刚才完成的 Forge Viewer PWA 的效果
发布为基于 WebAssembly 的 PWA!

在 Visual Studio 中创建基于.NET Core 2.x 的 Console(控制台应用)项目,并引用方才创建的 Xamarin 项目

通过 NuGet 安装以下依赖,其中 Ooui 系列用于发布 WebAssembly

Ooui: https://github.com/praeclarum…

Ooui WASM: 发布 WebAssembly
Ooui Forms: Xamarin.Forms 的 Ooui 实现
Xamarin.Forms 2.5 // 重要!一定要使用 2.5 版本以兼容 Ooui

在 Program.cs 中定义发布过程
using OurXamarinApp
static void Main(string[] args)
{
Forms.Init();

var mainPage = new MainPage(); // 以 MainPage 为应用入口为例
UI.Publish(“/”, mainPage.GetOouiElement());
}

运行项目,将在项目的 \bin\Debug\netcoreapp2.1\dist 路径下生成 WebAssembly 和配套的前端页面与脚本:

定义缓存 WebAssembly 的 ServiceWorker:
const urlsToCache = [
‘index.html’,
“https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css”, //Ooui 依赖
“https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css”, //Ooui 依赖
‘service-worker.js’ // 本 ServiceWorker 路径
];

self.addEventListener(‘fetch’, function (event) {
event.respondWith(
caches.match(event.request)
.then(function (response) {
// Cache hit – return response
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});

在发布生成的 index.html 页面中注册该 ServiceWorker
window.addEventListener(‘load’, function () {
navigator.serviceWorker.register(‘service-worker.js’)
})

定义应用清单 (manifest.json),让浏览器识别我们的 PWA 并定义主题颜色、应用图标等元数据
{
“name”: “Forge Viewer PWA”,
“short_name”: “FVPWA”,
“icons”: [
{
“src”: “icons/icon-128×128.png”,
“sizes”: “128×128”,
“type”: “image/png”
},
{
“src”: “icons/icon-144×144.png”,
“sizes”: “144×144”,
“type”: “image/png”
},
{
“src”: “icons/icon-152×152.png”,
“sizes”: “152×152”,
“type”: “image/png”
},
{
“src”: “icons/icon-192×192.png”,
“sizes”: “192×192”,
“type”: “image/png”
},
{
“src”: “icons/icon-512×512.png”,
“sizes”: “512×512”,
“type”: “image/png”
}
],
“start_url”: “index.html”,
“display”: “standalone”,
“background_color”: “#3498DB”,
“theme_color”: “#3498DB”
}

在 index.html 页面中引用清单
<header>

<link rel=”manifest” href=”/manifest.json”>
</header>

移动端测试

用各大 Web 服务器直接静态托管应用所在目录即可,并以支持 ServiceWorker 的浏览器访问,支持情况可参考:https://caniuse.com/#search=s…

在非 HTTPS/SSL 测试环境下,如遇浏览器安全限制:“不安全上下文”错误或无法注册 ServiceWorker (navigator.serviceWorker 为 null),则需给服务器启用 HTTPS/SSL,或使用 ngrok 等云管道服务为本地环境作代理
用访问应用 URL 时浏览器会提示保存快捷方式到桌面,图标由我们的应用清单定义:

保存后进入飞行模式打开应用,测试离线状态的加载:

大功告成!
延伸阅读

Xamarin 开发资源汇总:https://blog.csdn.net/qq_4147…

WebAssembly 遇上 React Native: https://github.com/vincentrie…

PWA 和小程序:https://www.cnblogs.com/softi…

退出移动版