Viewer模型加載本地離線緩存實戰

演示視頻:http://www.bilibili.com/video...javascript

因爲Autodesk Forge是徹底基於RESTful API框架的雲平臺,且暫時沒有本地部署方案,特別是Viewer.js暫不支持本地搭建,必須外部引用位於服務器端的腳本,如何知足離線應用的需求一直是廣大開發者關注的問題。本文將介紹來自Forge顧問團隊的國際同事Petr原創的Viewer緩存範例,利用HTML5普遍用於PWA(Progressive Web App)開發的典型接口實現。html

時至今日,要把來自網絡應用或服務的數據緩存至本地設備,咱們有幾種不一樣的技術與方式可選,本文將示範使用Service Worker,Cache和Channel Messaging等API實現,它們都是開發Progrssive Web App的常客。雖然這些API相對較爲新銳,但已得到新版瀏覽器的廣發支持,詳細支持狀況能夠參考(詳見「瀏覽器兼容性」部分):前端

當咱們在JavaScript中註冊了Service Worker以後,該Worker會攔截瀏覽器全部頁面對於指定網絡源或域的請求,返回緩存中的內容,Service Worker亦能夠調用IndexDBChannel MessagingPush等API。Service Worker在特殊的Worker上下文(ServiceWorkerGlobalScope)中執行,沒法直接對DOM進行操做,能夠獨立於頁面的形式控制頁面的加載。一個Service Worker能夠控制多個頁面,每當指定範圍內的頁面加載時,Service Worker便於會對其進行安裝並操做,因此請當心使用全局變量,每一個頁面並無自身獨立的Worker。java

做爲一種特殊的Web Worker,Service Worker的生命週期以下:git

  • 在JavaScript中註冊Service Worker
  • 瀏覽器下載並執行Worker腳本
  • Worker收到「install」(安裝)事件,一次性配置所需資源
  • 等待其餘正在執行的Service Worker結束(頁面關閉)
  • Worker收到「activate」(激活)事件,清除Worker的舊cache,並按需接管Worker
  • Worker開始接受「fetch」(攔截網絡請求並返回緩存中的資源)和「message」(與前端代碼通信)事件:

圖片描述

Cache是一個存儲API,與LocalStorageIndexDB相似,每一個網絡源或域都有本身對應的存儲空間,其中包括不重名的cache對象,用於存儲HTTP請求與應答內容。
圖片描述github

Channel Messaging是腳本之間通信API,支持包括主頁面、iframe、Web Worker、Service Worker之間的雙向通信。web

緩存策略

緩存諸如靜態資源和API端口返回的數據並不複雜,可在Service Worker安裝時緩存便可。而後,當頁面向API端口發送請求時,Service Worker會立即返回緩存的內容,且可按需在後臺拉取資源並更新緩存內容。json

緩存模型就稍許繁瑣,一個模型一般會轉換生成數個資源,生成的資源也時常引用其餘素材,因此須要找出全部這些依賴並按需將其緩存。在本文的代碼示例中,咱們在後臺寫有接口,可根據模型的URN查詢並返回所需資源的URL列表。於是在緩存模型時,Service Worker能夠調用該接口緩存全部相關的URL,無需用到Viewer。api

代碼示例

咱們製做了讓用戶選擇模型做離線緩存的例子,查看代碼請訪問: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的瀏覽器訪問。
clipboard.png

延伸閱讀:

相關文章
相關標籤/搜索