资讯专栏INFORMATION COLUMN

Viewer模型加载本地离线缓存实战

oogh / 1502人阅读

摘要:本文将介绍来自顾问团队的国际同事原创的缓存范例,利用广泛用于开发的典型接口实现。因而在缓存模型时,可以调用该接口缓存所有相关的,无需用到。代码示例我们制作了让用户选择模型作离线缓存的例子,查看代码请访问,在线演示请访问。

演示视频:http://www.bilibili.com/video...

由于Autodesk Forge是完全基于RESTful API框架的云平台,且暂时没有本地部署方案,特别是Viewer.js暂不支持本地搭建,必须外部引用位于服务器端的脚本,如何满足离线应用的需求一直是广大开发者关注的问题。本文将介绍来自Forge顾问团队的国际同事Petr原创的Viewer缓存范例,利用HTML5广泛用于PWA(Progressive Web App)开发的典型接口实现。

时至今日,要把来自网络应用或服务的数据缓存至本地设备,我们有几种不同的技术与方式可选,本文将示范使用Service Worker,Cache和Channel Messaging等API实现,它们都是开发Progrssive Web App的常客。虽然这些API相对较为新锐,但已获得新版浏览器的广发支持,详细支持情况可以参考(详见“浏览器兼容性”部分):

Service Worker: https://developer.mozilla.org...

Channel Messaing:https://developer.mozilla.org...

Cache: https://developer.mozilla.org...

Worker: https://developer.mozilla.org...

当我们在JavaScript中注册了Service Worker之后,该Worker会拦截浏览器所有页面对于指定网络源或域的请求,返回缓存中的内容,Service Worker亦可以调用IndexDB、Channel Messaging、Push等API。Service Worker在特殊的Worker上下文(ServiceWorkerGlobalScope)中执行,无法直接对DOM进行操作,可以独立于页面的形式控制页面的加载。一个Service Worker可以控制多个页面,每当指定范围内的页面加载时,Service Worker便于会对其进行安装并操作,所以请小心使用全局变量,每个页面并没有自身独立的Worker。

作为一种特殊的Web Worker,Service Worker的生命周期如下:

在JavaScript中注册Service Worker

浏览器下载并执行Worker脚本

Worker收到“install”(安装)事件,一次性配置所需资源

等待其他正在执行的Service Worker结束(页面关闭)

Worker收到“activate”(激活)事件,清除Worker的旧cache,并按需接管Worker

Worker开始接受“fetch”(拦截网络请求并返回缓存中的资源)和“message”(与前端代码通讯)事件:

Cache是一个存储API,与LocalStorage及IndexDB类似,每个网络源或域都有自己对应的存储空间,其中包括不重名的cache对象,用于存储HTTP请求与应答内容。

Channel Messaging是脚本之间通讯API,支持包括主页面、iframe、Web Worker、Service Worker之间的双向通讯。

缓存策略

缓存诸如静态资源和API端口返回的数据并不复杂,可在Service Worker安装时缓存即可。然后,当页面向API端口发送请求时,Service Worker会当即返回缓存的内容,且可按需在后台拉取资源并更新缓存内容。

缓存模型就稍许繁琐,一个模型通常会转换生成数个资源,生成的资源也时常引用其他素材,所以需要找出所有这些依赖并按需将其缓存。在本文的代码示例中,我们在后台写有接口,可根据模型的URN查询并返回所需资源的URL列表。因而在缓存模型时,Service Worker可以调用该接口缓存所有相关的URL,无需用到Viewer。

代码示例

我们制作了让用户选择模型作离线缓存的例子,查看代码请访问:https://github.com/petrbroz/f...,在线演示请访问:https://forge-offline.herokua...。接下来我们讲解一些具体的代码片段。

例子的后台基于Express,public目录的内容作静态托管,其它服务端口位于以下三个路径:

GET /api/token - 返回验证Token

GET /api/models - 返回可浏览的模型列表

GET /api/models/:urn/files - 根据模型的URN查询并返回所需资源的URL列表

客户端包括两个核心脚本:public/javascript/main.jspublic/service-worker.js,其中public/javascript/main.js主要用于配置Viewer和UI逻辑,有两个重要的函数在脚本底部:initServiceWorkersubmitWorkerTask,前者触发Service Worker的注册,后者向其发送消息:

async function initServiceWorker() {
    try {
        const registration = await navigator.serviceWorker.register("/service-worker.js");
        console.log("Service worker registered", registration.scope);
    } catch (err) {
        console.error("Could not register service worker", err);
    }
}

在“activate”事件中,本例并无清理旧Worker的需要,于是直接接管并控制所有Service Worker:

async function activateAsync() {
    const clients = await self.clients.matchAll({ includeUncontrolled: true });
    console.log("Claiming clients", clients.map(client => client.url).join(","));
    await self.clients.claim();
}

拦截请求时,我们优选比对并返回缓存中的内容,除了GET /api/token将优先尝试获取新的Token,因为Token是有时效性的,只有获取新Token失败时我们才使用缓存:

async function fetchAsync(event) {
    // 优先获取新Token而非使用缓存
    if (event.request.url.endsWith("/api/token")) {
        try {
            const response = await fetch(event.request);
            return response;
        } catch(err) {
            console.log("Could not fetch new token, falling back to cache.", err);
        }
    }

    // 如缓存匹配成功,直接返回其内容
    const match = await caches.match(event.request.url, { ignoreSearch: true });
    if (match) {
        // 如请求指向静态资源或我们制定范围内的API,同时后台更新缓存
        if (STATIC_URLS.includes(event.request.url) || API_URLS.includes(event.request.url)) {
            caches.open(CACHE_NAME)
                .then((cache) => cache.add(event.request))
                .catch((err) => console.log("Cache not updated, but that"s ok...", err));
        }
        return match;
    }

    return fetch(event.request);
}

最后,使用Channel Messaging API执行从页面脚本发起的任务:

async function messageAsync(event) {
    switch (event.data.operation) {
        case "CACHE_URN":
            try {
                const urls = await cacheUrn(event.data.urn, event.data.access_token);
                event.ports[0].postMessage({ status: "ok", urls });
            } catch(err) {
                event.ports[0].postMessage({ error: err.toString() });
            }
            break;
        case "CLEAR_URN":
            try {
                const urls = await clearUrn(event.data.urn);
                event.ports[0].postMessage({ status: "ok", urls });
            } catch(err) {
                event.ports[0].postMessage({ error: err.toString() });
            }
            break;
        case "LIST_CACHES":
            try {
                const urls = await listCached();
                event.ports[0].postMessage({ status: "ok", urls });
            } catch(err) {
                event.ports[0].postMessage({ error: err.toString() });
            }
            break;
    }
}

async function cacheUrn(urn, access_token) {
    console.log("Caching", urn);
    // 首先从后台获取URN所需资源的URL列表
    const baseUrl = "https://developer.api.autodesk.com/derivativeservice/v2";
    const res = await fetch(`/api/models/${urn}/files`);
    const derivatives = await res.json();
    // 初始化拉取请求以便缓存资源
    const cache = await caches.open(CACHE_NAME);
    const options = { headers: { "Authorization": "Bearer " + access_token } };
    const fetches = [];
    const manifestUrl = `${baseUrl}/manifest/${urn}`;
    fetches.push(fetch(manifestUrl, options).then(resp => cache.put(manifestUrl, resp)).then(() => manifestUrl));
    for (const derivative of derivatives) {
        const derivUrl = baseUrl + "/derivatives/" + encodeURIComponent(derivative.urn);
        fetches.push(fetch(derivUrl, options).then(resp => cache.put(derivUrl, resp)).then(() => derivUrl));
        for (const file of derivative.files) {
            const fileUrl = baseUrl + "/derivatives/" + encodeURIComponent(derivative.basePath + file);
            fetches.push(fetch(fileUrl, options).then(resp => cache.put(fileUrl, resp)).then(() => fileUrl));
        }
    }
    // 并发执行拉取请求并缓存资源
    const urls = await Promise.all(fetches);
    return urls;
}

async function clearUrn(urn) {
    console.log("Clearing cache", urn);
    const cache = await caches.open(CACHE_NAME);
    const requests = (await cache.keys()).filter(req => req.url.includes(urn));
    await Promise.all(requests.map(req => cache.delete(req)));
    return requests.map(req => req.url);
}

async function listCached() {
    console.log("Listing caches");
    const cache = await caches.open(CACHE_NAME);
    const requests = await cache.keys();
    return requests.map(req => req.url);
}

以上。在线演示请访问: https://forge-offline.herokua...,点击左上角模型旁边的“☆”触发缓存, 请参考本文开头所示使用支持所需API的浏览器访问。

延伸阅读:

一文读懂Service Worker

Web Worker、Service Worker与Shared Worker分不清楚?

Service Worker、Web Worker和Websocket

PWA速成

Cache和Application Cache

关于消息通道(Channel Messaging)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101177.html

相关文章

  • Viewer模型加载本地离线缓存实战

    摘要:本文将介绍来自顾问团队的国际同事原创的缓存范例,利用广泛用于开发的典型接口实现。因而在缓存模型时,可以调用该接口缓存所有相关的,无需用到。代码示例我们制作了让用户选择模型作离线缓存的例子,查看代码请访问,在线演示请访问。 演示视频:http://www.bilibili.com/video... 由于Autodesk Forge是完全基于RESTful API框架的云平台,且暂时没有本...

    刘厚水 评论0 收藏0
  • Autodesk Forge Viewer 信息本地化技术分析

    摘要:默认情况下,是英文环境,调取的是的资源其实无需翻译。但是,如前面提到的,语言包只是包含了部分常规字串的翻译,如果遇到没有包含的常规字串怎么办呢例如,本例中的语言包并没有对,进行翻译,所以即使切换了语言,它们仍旧是英文。 注:本文是个人调试分析所得,非官方文档,请酌情选用参考。文中分析的数据由https://extract.autodesk.io转换下载而来。 谈到信息本地化,个人觉得包...

    littleGrow 评论0 收藏0
  • 在 Android 上离线导览模型

    摘要:但有些朋友偏好将镶嵌到移动端应用里,所以我们提供了这一个示例但在上实作离线导览模型会有什么问题主要是我们的的代码是使用通信协议实作的,并不支持这种档案协议,所以他没办法透过这个协议从本地存储上载入模型文档。 这篇文章的原作者是 Autodesk ADN 的 Adam Nagy,以下以我简称。 对 Forge Viewer 熟悉的朋友都知道可以透过 Viewer 在任何已支持的浏览器上观...

    curried 评论0 收藏0
  • 腾讯祭出大招VasSonic,让你的H5页面首屏秒开

    摘要:经过一系列优化后,在平台上,点击到页面首屏展示的耗时从平均多降低为,优化以上。而现在页面为了更好地为用户推荐喜欢的内容,我们后台引入机器学习和随机算法来做智能个性化推荐。另外还有部分的内容是随机算法推荐的。 VasSonic成长历程 前言 2017.8.8 14时,SNG增值产品部Vas团队研发的轻量级高性能Hybrid框架VasSonic通过了公司最终审核,作为腾讯开源组件分享给大...

    xzavier 评论0 收藏0
  • 腾讯祭出大招VasSonic,让你的H5页面首屏秒开

    摘要:经过一系列优化后,在平台上,点击到页面首屏展示的耗时从平均多降低为,优化以上。而现在页面为了更好地为用户推荐喜欢的内容,我们后台引入机器学习和随机算法来做智能个性化推荐。另外还有部分的内容是随机算法推荐的。 VasSonic成长历程 前言 2017.8.8 14时,SNG增值产品部Vas团队研发的轻量级高性能Hybrid框架VasSonic通过了公司最终审核,作为腾讯开源组件分享给大...

    doodlewind 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<