实战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:

:heartpulse:Autodesk ADN(开发者社区团队)祝您在新的一年大吉大利!!:pig2:事顺利!!:heartpulse:

为什么要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-128x128.png",
        "sizes": "128x128",
        "type": "image/png"
      },
      {
        "src": "icons/icon-144x144.png",
        "sizes": "144x144",
        "type": "image/png"
      },
      {
        "src": "icons/icon-152x152.png",
        "sizes": "152x152",
        "type": "image/png"
      },
      {
        "src": "icons/icon-192x192.png",
        "sizes": "192x192",
        "type": "image/png"
      },
      {
        "src": "icons/icon-512x512.png",
        "sizes": "512x512",
        "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时浏览器会提示保存快捷方式到桌面,图标由我们的应用清单定义:

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

  • 大功告成!

延伸阅读

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章