Skip to content

Handling SCORM files on the Frontend Using a Service Worker

Posted on:February 25, 2025 at 04:12 PM (6 min read)

Introduction

SCORM (Sharable Content Object Reference Model) is a widely used e-learning standard that enables interoperability between different Learning Management Systems (LMS) and educational content. Typically, SCORM packages are handled server-side, requiring backend services to unpack and serve the necessary files. However, handling SCORM files directly on the frontend presents challenges such as file extraction, CORS issues, and efficient content delivery.

This article explores how to use a service worker to manage SCORM files on the frontend, enabling seamless content delivery and improved performance. We’ll also discuss how the React SCORM player interacts with the service worker and the role of Rollup in bundling the package.

Understanding SCORM and Its Challenges

SCORM content is packaged in ZIP files containing HTML, JavaScript, multimedia assets, and an imsmanifest.xml file that defines the structure of the learning module. The three common SCORM formats are:

Challenges of Frontend-Based SCORM Handling

Handling SCORM files on the frontend involves overcoming several obstacles:

  1. Extracting SCORM ZIP Files – Browsers don’t natively support ZIP extraction, requiring the use of libraries like JSZip.

  2. CORS Restrictions – Fetching SCORM content from different origins may lead to cross-origin issues.

  3. Dynamic File Serving – The browser cannot natively serve extracted ZIP files, requiring an alternative approach.

  4. Offline Access – SCORM content should be accessible even if the user loses internet connectivity.

Using a Service Worker for SCORM Management

A service worker is a script that runs in the background of a web page, intercepting and handling network requests. We use it to:

How it works

  1. The React app registers a service worker.

  2. The service worker intercepts requests for SCORM content.

  3. When a SCORM ZIP file is loaded, the service worker extracts it using JSZip.

  4. The extracted files are stored in memory, allowing the worker to serve them dynamically.

  5. The service worker communicates with the React app to initialize SCORM tracking and API interactions.

Registering the Service Worker in React

To begin, we need to register the service worker in our React app:

const registerServiceWorker =
  async (): Promise<ServiceWorkerRegistration | null> => {
    if (!("serviceWorker" in navigator)) {
      console.warn("Service Worker not supported in this browser.");
      return null;
    }
    try {
      const registration = await navigator.serviceWorker.register(
        "/service-worker-scorm.js",
        { scope: "/" },
      );
      return registration;
    } catch (err) {
      console.error("Service Worker registration failed:", err);
      return null;
    }
  };

useEffect(() => {
  registerServiceWorker();
}, []);

This function ensures that the service worker is properly registered and ready to handle SCORM files.

Implementing the Service Worker

The service worker is responsible for intercepting requests and handling SCORM ZIP extraction. Below is an essential part of the service worker:

self.addEventListener("install", (event) => {
  console.log("Service Worker installing...");
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  console.log("Service Worker activated.");
  event.waitUntil(self.clients.claim());
});

self.addEventListener("fetch", async (event) => {
  const url = event.request.url;
  if (url.includes("__scorm__")) {
    event.respondWith(handleScormRequest(event.request));
  }
});

This service worker listens for fetch events and routes requests for SCORM files through handleScormRequest, which will be responsible for serving extracted content.

Extracting and Serving SCORM Files

Once the SCORM ZIP file is received by the service worker, it needs to be extracted and its contents served dynamically. We use JSZip for this purpose.

Extracting SCORM Files

The service worker listens for messages containing the SCORM ZIP file URL and extracts its contents:

self.addEventListener("message", async (event) => {
  const url = event.data;
  const response = await fetch(url);
  const blob = await response.blob();
  const zip = new JSZip();

  await zip.loadAsync(blob);
  const xml = await zip.file("imsmanifest.xml")?.async("string");
  if (!xml) {
    console.error("SCORM manifest file not found.");
    return;
  }

  const scormObj = getSCORMDetails(xml);
  resolvers[url] = zip;

  const clients = await self.clients.matchAll();
  clients.forEach((client) => client.postMessage({ scormObj }));
});

This function extracts the SCORM package and parses the imsmanifest.xml file to determine the SCORM version and structure.

Serving Extracted Files

Once extracted, the service worker intercepts fetch requests and serves the appropriate files from the extracted ZIP:

self.addEventListener("fetch", (event) => {
  const requestUrl = new URL(event.request.url);
  if (requestUrl.pathname.includes("__scorm__")) {
    event.respondWith(
      (async () => {
        const zip = resolvers[requestUrl.origin];
        if (!zip)
          return new Response("SCORM package not loaded", { status: 404 });

        const filePath = requestUrl.pathname.split("__scorm__/")[1];
        const fileBlob = await zip.file(filePath)?.async("blob");
        if (!fileBlob)
          return new Response("File not found in SCORM ZIP", { status: 404 });

        return new Response(fileBlob, {
          headers: { "Content-Type": "application/octet-stream" },
        });
      })(),
    );
  }
});

This method ensures that requests for SCORM files are intercepted and served directly from memory without needing backend intervention.

Initializing SCORM Tracking in React

The React component is responsible for setting up the SCORM API, communicating with the service worker, and tracking user progress. The initialization varies depending on the SCORM version.

Handling SCORM API Initialization

const initializeScorm12 = (settings: ScormSettings["data"]) => {
  window.API = new Scorm12API(settings as any);

  if (comunitate && onScormPost && onScormGet) {
    window.API.on("LMSSetValue.cmi.*", (CMIElement: string, value: any) => {
      onScormPost({ cmi: { [CMIElement]: value } });
    });

    window.API.on("LMSGetValue.cmi.*", async (CMIElement: string) => {
      const value = await onScormGet(CMIElement);
      window.API.LMSSetValue(CMIElement, value);
    });

    window.API.on("LMSCommit", () => {
      onScormPost({ cmi: window.API.cmi });
    });
  }
};

This function initializes the SCORM 1.2 API, handling data storage and retrieval through onScormPost and onScormGet callbacks.

Registering SCORM 2004

const initializeScorm2004 = (settings: ScormSettings["data"]) => {
  window.API_1484_11 = new Scorm2004API(settings as any);

  if (comunitate && onScormPost && onScormGet) {
    window.API_1484_11.on(
      "SetValue.cmi.*",
      (CMIElement: string, value: any) => {
        onScormPost({ cmi: { [CMIElement]: value } });
      },
    );

    window.API_1484_11.on("GetValue.cmi.*", async (CMIElement: string) => {
      const value = await onScormGet(CMIElement);
      window.API_1484_11.SetValue(CMIElement, value);
    });

    window.API_1484_11.on("Commit", () => {
      onScormPost({ cmi: window.API_1484_11.cmi });
    });
  }
};

Loading SCORM Content

After the SCORM API is initialized, the React component loads SCORM content into an iframe:

const [iframeUrl, setIframeUrl] = useState<string | null>(null);

const loadScormSCO = async (registration: ServiceWorkerRegistration) => {
  try {
    const res = await fetch(`${apiUrl}/url-to-scorm-source/${uuid}`);
    if (!res.ok) throw new Error("Failed to fetch SCORM settings");
    const { data } = (await res.json()) as ScormSettings;

    registration.active?.postMessage(data.entry_url_zip);

    navigator.serviceWorker.addEventListener(
      "message",
      (event: MessageEvent) => {
        const { scormObj } = event.data;
        if (!scormObj) return;

        switch (scormObj.version) {
          case "2004":
            initializeScorm2004(data);
            break;
          default:
            initializeScorm12(data);
        }

        setIframeUrl(`${scormObj.PREFIX}/${data.entry_url}`);
      },
      { once: true },
    );
  } catch (err) {
    console.error("Failed to load SCORM SCO", err);
  }
};

Conclusion

Using a service worker to handle SCORM files on the frontend provides a significant advantage in terms of performance, flexibility, and offline support. By dynamically extracting SCORM ZIP files, intercepting fetch requests, and serving content directly from memory, we can reduce the dependency on backend services and improve user experience.

Integrating this approach with a React-based SCORM player enables seamless communication between the service worker and the SCORM API, ensuring proper tracking and data persistence. The combination of JSZip, scorm-again, and service worker capabilities offers an efficient way to manage SCORM content in modern web applications.

Future enhancements could include improved caching strategies, IndexedDB support for persistent storage, and additional optimizations to further streamline SCORM delivery on the frontend.

Summary

In this article, we explored how to handle SCORM files on the frontend using a service worker. We discussed the challenges of SCORM content handling, the role of service workers in extracting and serving SCORM files dynamically, and how the React component interacts with the service worker for seamless integration.