תמיכת SCORM במערכת LMS
דמיינו שרכשתם קורס יקר ב-Articulate Storyline, ומערכת ה-LMS שלכם מסרבת להעלות אותו — שגיאה "פורמט לא חוקי" או שהקורס נפתח אך ההתקדמות לא נשמרת. הלומד לא יכול להשלים את ההדרכה, והתמיכה של הספק מאשימה את התקן. מצב זה אופייני בהיעדר תמיכת SCORM תקינה. כצוות עם ניסיון של למעלה מעשר שנים ב-e-learning, פתרנו בעיה זו עשרות פעמים. הגישה שלנו אינה פתרון iframe מאולתר אלא תמיכת SCORM 1.2 ו-2004 מובנית ברמת הארכיטקטורה. SCORM הוא התקן להעברת קורסי למידה, ויישום נכון שלו הוא קריטי לכל מערכת LMS מודרנית.
בעיות שאנו פותרים
אי-תאימות גרסאות. SCORM 1.2 משתמש באובייקט API, ו-2004 משתמש ב-API_1484_11. אם מערכת ה-LMS שלכם מקושרת לגרסה אחת בלבד, מחצית מהקורסים ייכשלו. אובדן suspend_data. בעת טעינה מחדש של העמוד, התקדמות התלמיד נעלמת. דליפות זיכרון ב-iframe. לאחר סגירת הקורס, אובייקט ה-API עלול להישאר בזיכרון. אנו פותרים את כל שלוש הבעיות: קובעים דינמית את גרסת החבילה, מבצעים שמירות מוגבלות עם suspend_data המאוחסן ב-PostgreSQL JSONB, ומשחררים את ה-API בעת הסרה.
כיצד פועל SCORM API?
קורס SCORM מתקשר עם מערכת ה-LMS דרך JavaScript API. מערכת ה-LMS יוצרת אובייקט גלובלי API (SCORM 1.2) או API_1484_11 (SCORM 2004) בחלון שבו מופעל ה-iframe. הנה יישום בסיסי ב-TypeScript:
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 {
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?
הקורס מחפש את 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 { 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, }), }); } } בחלון האב. מערכת ה-LMS מגדירה את האובייקט לפני טעינת ה-iframe ומסירה אותו בעת הסרה:
function ScormPlayer({ courseId, enrollmentId }) {
const iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
const api = new ScormApi12(async (data) => {
await fetch(`/api/enrollments/${enrollmentId}/complete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
});
(window as any).API = api;
(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
בצד השרת עם Node.js באמצעות API ו-function ScormPlayer({ courseId, enrollmentId }) { const iframeRef = useRef<HTMLIFrameElement>(null); useEffect(() => { const api = new ScormApi12(async (data) => { await fetch(`/api/enrollments/${enrollmentId}/complete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); }); (window as any).API = api; (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" /> ); } :
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);
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);
});
אחסון התקדמות
אנו משתמשים ב-PostgreSQL עם JSONB עבור suspend_data ואינדקס על enrollment_id:
CREATE TABLE scorm_progress (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
enrollment_id UUID REFERENCES enrollments(id),
lesson_status VARCHAR(50),
score NUMERIC(5,2),
suspend_data JSONB,
session_time INTERVAL,
completed_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_enrollment ON scorm_progress(enrollment_id); מדוע SCORM 1.2 עדיין רלוונטי?
למרות ש-SCORM 2004 שוחרר לפני שנים רבות, 80% מהחבילות המסחריות עדיין בנויות עבור SCORM 1.2 — הוא פשוט יותר ונתמך באופן נרחב יותר. אנו תומכים בשתי הגרסאות, ומזהים אוטומטית את הגרסה מהמניפסט.
| פרמטר | SCORM 1.2 | SCORM 2004 |
|---|---|---|
| אובייקט API | adm-zip |
xml2js |
| סטטוסים | passed/failed/completed/incomplete | + not attempted/unknown |
| ציון | 0–100 | 0.0–1.0 עם min/max/raw |
| התקדמות | suspend_data | suspend_data + adl.nav |
| שכיחות | ~80% מהקורסים | ~20% |
הניסיון והיתרונות שלנו
אנחנו צוות של 15 עם ניסיון של למעלה מעשר שנים בפיתוח web. סיפקנו בהצלחה יותר מ-50 פרויקטי LMS, כולל אינטגרציית SCORM עבור בנקים, קמעונאות ו-EdTech. הפתרונות שלנו מתמודדים עם למעלה מ-10,000 סשנים במקביל. אנו מבטיחים תאימות עם Articulate Storyline, Adobe Captivate, iSpring — נבדק על שלוש חבילות פופולריות. חיסכון בזמן בפיתוח מאפס יכול להגיע ל-70%, ועלות האינטגרציה בדרך כלל מחזירה את עצמה תוך מספר חודשים.
השוואה עם חלופות: מערכות LMS מוכנות (למשל, Moodle) תומכות ב-SCORM ישירות מהקופסה, אך ה-API שלהן מורכב להתאמה אישית, והביצועים בעומס גבוה סובלים לעיתים קרובות. הפתרון שלנו מעבד השקות קורסים 40% מהר יותר ומאפשר שינוי גמיש של לוגיקת אחסון ההתקדמות.
תהליך
- ניתוח — פגישה עם הצוות, איסוף דרישות: אילו חבילות SCORM יועלו, האם נדרשים סטטוסים מותאמים אישית.
- עיצוב — ארכיטקטורת מודול: דיאגרמת זרימה, בחירת אחסון, סכמת API.
- יישום — כתיבת אובייקט ה-API, מטפל העלאה, backend להתקדמות.
- בדיקות — בדיקות יחידה עם mocks, בדיקות אינטגרציה עם חבילות אמיתיות (Articulate, iSpring).
- פריסה — הגדרת סביבה, CI/CD, ניטור.
מה כלול?
- קוד מקור של מודול SCORM (API, מעלה, התקדמות).
- תיעוד API עבור אינטגרציה של תוכן צד שלישי.
- מדריך מנהל להעלאה וניהול קורסים.
- אחריות תאימות עם Articulate Storyline, Adobe Captivate, iSpring.
- הדרכת מנהל צוות ותמיכה טכנית למשך חודש לאחר המסירה.
לוח זמנים ועלות
לוחות זמנים — מ-7 ימי עבודה (SCORM 1.2) עד 14 יום (SCORM 1.2 + 2004). העלות מחושבת באופן אישי — תלויה במורכבות ההתאמה (למשל, סטטוסים לא סטנדרטיים או דרישות suspend_data). צרו קשר — נבחן את הפרויקט שלכם ללא עלות.
| שלב | משך | תוצאה |
|---|---|---|
| אינטגרציית SCORM 1.2 בסיסית | 7 ימים | מודול עובד עם API והתקדמות |
| אינטגרציה מלאה (1.2 + 2004) | 14 יום | תמיכה בשתי הגרסאות, נבדק על 3 חבילות |
הזמינו פיתוח מודול SCORM — קבלו אינטגרציה מהירה ואמינה המותאמת לעסק שלכם.







