Поддержка 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,
}),
});
}
}
Инъекция 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 дней.







