Підтримка SCORM у LMS
SCORM (Sharable Content Object Reference Model) — стандарт упаковки e-learning контенту. Більшість корпоративних курсів, виконаних у Articulate Storyline, Adobe Captivate, iSpring — це SCORM-пакети. LMS повинна вміти їх завантажувати, запускати в iframe та отримувати дані про прогрес.
Що таке SCORM-пакет
SCORM-пакет — ZIP-архів з файлом imsmanifest.xml і HTML/JS/медіа-файлами курсу. Версії: SCORM 1.2 (найпоширеніша) та SCORM 2004 (4-та редакція).
Структура пакета:
course.zip
├── imsmanifest.xml # метаданні, структура
├── index.html # точка входу курсу
├── scorm_api.js # реалізація SCORM API
└── content/
├── slide1.html
├── media/
└── ...
SCORM API — міст між курсом і LMS
Курс комунікує з LMS через JavaScript API. LMS створює глобальний об'єкт API (SCORM 1.2) або API_1484_11 (SCORM 2004) у вікні, де запущено iframe:
// SCORM 1.2 API об'єкт — створюється на сторінці LMS
class ScormApi12 {
private lessonStatus = 'not attempted';
private suspendData = '';
private score = 0;
private sessionTime = '';
private dataStore = new Map<string, string>();
private onComplete: (data: ScormData) => void;
constructor(onComplete: (data: ScormData) => void) {
this.onComplete = onComplete;
}
LMSInitialize(_: string): string {
this.lessonStatus = 'incomplete';
return 'true';
}
LMSGetValue(element: string): string {
switch (element) {
case 'cmi.core.lesson_status': return this.lessonStatus;
case 'cmi.suspend_data': return this.suspendData;
case 'cmi.core.score.raw': return String(this.score);
case 'cmi.core.lesson_location': return this.dataStore.get('lesson_location') ?? '';
default: return this.dataStore.get(element) ?? '';
}
}
LMSSetValue(element: string, value: string): string {
switch (element) {
case 'cmi.core.lesson_status':
this.lessonStatus = value;
break;
case 'cmi.suspend_data':
this.suspendData = value;
break;
case 'cmi.core.score.raw':
this.score = Number(value);
break;
case 'cmi.core.session_time':
this.sessionTime = value;
break;
default:
this.dataStore.set(element, value);
}
return 'true';
}
LMSCommit(_: string): string {
// Відправити дані на сервер (дроселювання — не частіше разу на 5 секунд)
this.saveProgress();
return 'true';
}
LMSFinish(_: string): string {
this.onComplete({
status: this.lessonStatus,
score: this.score,
suspendData: this.suspendData,
sessionTime: this.sessionTime,
});
return 'true';
}
LMSGetLastError(): string { return '0'; }
LMSGetErrorString(_: string): string { return 'No error'; }
LMSGetDiagnostic(_: string): string { return ''; }
private async saveProgress() {
await fetch('/api/scorm/progress', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status: this.lessonStatus,
score: this.score,
suspendData: this.suspendData,
}),
});
}
}
Інjecção API у iframe
Курс шукає API у батьківських вікнах (window.parent.parent...). LMS встановлює об'єкт перед завантаженням iframe:
function ScormPlayer({ courseId, enrollmentId }) {
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
// Встановити SCORM API на поточне вікно — iframe знайде його через parent
const api = new ScormApi12(async (data) => {
await fetch(`/api/enrollments/${enrollmentId}/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
});
// SCORM 1.2
(window as any).API = api;
// SCORM 2004
(window as any).API_1484_11 = api;
return () => {
delete (window as any).API;
delete (window as any).API_1484_11;
};
}, [enrollmentId]);
return (
<iframe
ref={iframeRef}
src={`/api/courses/${courseId}/launch`}
className="w-full border-0"
style={{ height: 'calc(100vh - 64px)' }}
allow="camera; microphone; fullscreen"
title="SCORM Course"
/>
);
}
Завантаження та розпакування SCORM-пакета
import AdmZip from 'adm-zip';
import { parseStringPromise } from 'xml2js';
app.post('/api/courses/upload', authenticate, upload.single('scorm'), async (req, res) => {
const zipBuffer = req.file!.buffer;
const zip = new AdmZip(zipBuffer);
// Розпакувати до сховища (S3 або локально)
const courseId = crypto.randomUUID();
const extractPath = `/courses/${courseId}`;
zip.extractAllTo(path.join(process.env.STORAGE_PATH!, extractPath), true);
// Розпарсити маніфест
const manifestEntry = zip.getEntry('imsmanifest.xml');
if (!manifestEntry) throw new Error('Invalid SCORM package: no imsmanifest.xml');
const manifest = await parseStringPromise(manifestEntry.getData().toString());
const title = manifest.manifest.organizations[0].organization[0].title[0];
const launchUrl = manifest.manifest.resources[0].resource[0]['$']['href'];
const scormVersion = manifest.manifest['$']['version']?.includes('1.2') ? '1.2' : '2004';
const course = await db.courses.create({
id: courseId,
title,
launchUrl: `${extractPath}/${launchUrl}`,
scormVersion,
uploadedBy: req.user.id,
});
res.json(course);
});
Зберігання прогресу
CREATE TABLE scorm_progress (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
enrollment_id UUID REFERENCES enrollments(id),
lesson_status VARCHAR(50), -- 'passed' | 'failed' | 'completed' | 'incomplete'
score NUMERIC(5,2),
suspend_data TEXT, -- для закладок та стану курсу
session_time INTERVAL,
completed_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ DEFAULT now()
);
SCORM 1.2 vs SCORM 2004
| Параметр | SCORM 1.2 | SCORM 2004 |
|---|---|---|
| API об'єкт | window.API |
window.API_1484_11 |
| Статусів | passed/failed/completed/incomplete | passed/failed/completed/incomplete/not attempted/unknown |
| Оцінка | 0–100 | 0.0–1.0 (min/max/raw) |
| Прогрес | suspend_data | suspend_data + adl.nav |
| Поширеність | Широка | Менше |
Строки виконання
Підтримка SCORM 1.2 з завантаженням пакетів, API-об'єктом та зберіганням прогресу — 1–1.5 тижні. З підтримкою SCORM 2004 — ще 3–5 днів.







