יישום תכונות Local-First ב-Electron עם SQLite
תארו לעצמכם משתמש ברכבת התחתית פותח אפליקציית דסקטופ ועורך מסמך חשוב. החיבור נופל, אבל השינויים חייבים להישמר ללא אובדן ולהסתנכרן כשהרשת חוזרת. ללא פעולה מנותקת, כל הפרעת רשת מובילה לתסכול ולאובדן נתונים. יישמנו את המכניקה הזו ביותר מ-20 פרויקטים—בואו נדבר על איך זה עובד ועל הפתרונות הטכניים שבהם אנו משתמשים.
מדריך זה מכסה מצב לא מקוון באפליקציות דסקטופ, מצב לא מקוון ב-Electron, וסנכרון נתונים לא מקוון באמצעות SQLite better-sqlite3. הוא כולל פתרון קונפליקטים בסנכרון, סנכרון תור פעולות, וטכניקות מסד נתונים מקומי לדסקטופ לפיתוח אפליקציות לא מקוונות. כמו כן, אנו מכסים ניטור רשת ב-Electron ופיתוח פונקציונליות לא מקוונת. יכולת לא מקוונת אינה מציין מקום עם הודעת "אין אינטרנט". זהו פתרון ארכיטקטוני מלא: אחסון מקומי (SQLite דרך better-sqlite3), תור פעולות, פתרון קונפליקטים, והצגת סטטוס שקופה. מחסנית: Electron, React, TypeScript. פעולה מנותקת דורשת ארכיטקטורה מעוצבת היטב: מסד נתונים מקומי, תור, זיהוי רשת, ופתרון קונפליקטים. ללא הגישה הנכונה, נוצרות בעיות שלמות נתונים וביצועים.
מקרה מהשטח: עבור לקוח בתחום הלוגיסטיקה, יישמנו מצב לא מקוון לאפליקציית דסקטופ על Electron. האפליקציה המקורית איבדה נתונים בעת נפילות חיבור. לאחר הכנסת SQLite ותור סנכרון, אובדן הנתונים בוטל, וזמן ההשבתה ירד ב-90%. זה הביא לחיסכון מוערך של $20,000 בשנה. מקרה זה מתואר בפירוט בתיעוד שלנו.
זיהוי מצב רשת ב-Electron
המאפיין המובנה net.online מציין רק נוכחות של ממשק רשת, לא גישה לאינטרנט. שיטה אמינה יותר היא ping קבוע לנקודת קצה HTTPS אמינה עם פסק זמן של 5 שניות ומרווח של 15 שניות. כאשר הסטטוס משתנה, אנו שולחים אירוע ל-renderer דרך IPC. שיטות חלופיות—WebSocket keep-alive או בדיקות service worker—פחות אמינות בסביבת דסקטופ.
כדי ליישם זיהוי רשת אמין, בצעו את השלבים הבאים:
- צרו מופע
NetworkMonitor. - הגדירו מרווח בדיקה של 15 שניות.
- הירשמו לאירועים בתהליך ה-renderer דרך
ipcRenderer.on('network:change').
// main/network-monitor.js
const { net } = require('electron');
class NetworkMonitor {
constructor() {
this.isOnline = true;
this.listeners = new Set();
this.checkInterval = null;
}
start(mainWindow) {
this.window = mainWindow;
this.checkInterval = setInterval(() => this.checkConnectivity(), 15000);
this.checkConnectivity();
}
async checkConnectivity() {
const wasOnline = this.isOnline;
try {
const response = await Promise.race([
fetch('https://connectivity-check.your-api.com/ping', { method: 'HEAD' }),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000))
]);
this.isOnline = response.ok;
} catch {
this.isOnline = false;
}
if (wasOnline !== this.isOnline) {
this.window?.webContents.send('network:change', { isOnline: this.isOnline });
this.emit('change', this.isOnline);
}
}
on(event, listener) {
this.listeners.add({ event, listener });
}
emit(event, data) {
this.listeners.forEach(l => {
if (l.event === event) l.listener(data);
});
}
stop() {
clearInterval(this.checkInterval);
}
}
module.exports = new NetworkMonitor();
מסד נתונים מקומי: SQLite דרך better-sqlite3
הבסיס לפונקציונליות local-first הוא אחסון מקומי. SQLite הוא הבחירה הטובה ביותר לנתונים מובנים: עסקאות ACID, גודל קטן (1–5 MB לאפליקציה טיפוסית), מהירות גבוהה. השוו לחלופות:
| פרמטר | SQLite (better-sqlite3) | IndexedDB (דרך LokiJS) | קבצי JSON |
|---|---|---|---|
| סוג נתונים | מובנה, רלציוני | מסמכים (NoSQL) | כל סוג, אבל ללא אינדקסים |
| עסקאות | ACID | לא | לא |
| ביצועים (10,000 רשומות) | <100 ms הכנסה | ~500 ms הכנסה | ~2 s הכנסה |
| גודל מסד נתונים | 1–5 MB | 10–50 MB | תלוי בנפח |
| אינדקסים | כן | כן (מוגבל) | לא |
SQLite מהיר פי 5 מ-IndexedDB ופי 20 מקבצי JSON להכנסות בכמות גדולה. לביצועים מיטביים, אנו משתמשים במצב WAL, המאפשר קריאות וכתיבות במקביל ללא נעילות. זה חשוב במיוחד בעבודה עם תור הפעולות.
הגדרת SQLite לגישה במקביל
אנו מפעילים מצב WAL ומפתחות זרים, כפי שמוצג בקוד למטה. זה מאפשר ביצוע שאילתות ללא נעילה, דבר קריטי לכתיבות וקריאות בו-זמנית מהתור.
// main/db.js
const Database = require('better-sqlite3');
const path = require('path');
const { app } = require('electron');
const dbPath = path.join(app.getPath('userData'), 'app.db');
const db = new Database(dbPath);
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
updated_at INTEGER NOT NULL,
server_updated_at INTEGER,
sync_status TEXT NOT NULL DEFAULT 'synced'
);
CREATE TABLE IF NOT EXISTS sync_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
operation TEXT NOT NULL,
entity_type TEXT NOT NULL,
entity_id TEXT NOT NULL,
payload TEXT NOT NULL,
created_at INTEGER NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
last_error TEXT
);
`);
module.exports = db;
איך תור הפעולות ועדכון אופטימי עובדים
תבנית ה"עדכון האופטימי": כתבו מקומית מיד, סנכרנו מאוחר יותר. זהו העיקרון המרכזי: המשתמש לא מחכה לתגובת השרת. כל פעולה (יצירה, עדכון, מחיקה) מוחלת תחילה על מסד הנתונים המקומי, ולאחר מכן נכתבת ל-sync_queue. כאשר הרשת חוזרת, התור מעובד ברצף. להלן דוגמה למסמכים.
// main/documents.js
const db = require('./db');
function createDocument(doc) {
const id = doc.id || crypto.randomUUID();
const now = Date.now();
db.prepare(`
INSERT INTO documents (id, title, content, updated_at, sync_status)
VALUES (@id, @title, @content, @updated_at, @sync_status)
`).run({
id,
title: doc.title,
content: doc.content,
updated_at: now,
sync_status: 'pending'
});
db.prepare(`
INSERT INTO sync_queue (operation, entity_type, entity_id, payload, created_at)
VALUES (@operation, @entity_type, @entity_id, @payload, @created_at)
`).run({
operation: 'create',
entity_type: 'document',
entity_id: id,
payload: JSON.stringify({ id, title: doc.title, content: doc.content }),
created_at: now
});
return { id, title: doc.title, content: doc.content, sync_status: 'pending' };
}
function updateDocument(id, changes) {
const now = Date.now();
db.prepare(`
UPDATE documents
SET title = COALESCE(@title, title),
content = COALESCE(@content, content),
updated_at = @updated_at,
sync_status = 'pending'
WHERE id = @id
`).run({
...changes,
id,
updated_at: now
});
db.prepare(`
INSERT INTO sync_queue (operation, entity_type, entity_id, payload, created_at)
VALUES ('update', 'document', @id, @payload, @created_at)
`).run({
id,
payload: JSON.stringify({ id, ...changes }),
created_at: now
});
}
module.exports = { createDocument, updateDocument };
פתרון קונפליקטים בסנכרון
קונפליקט מתרחש כאשר אותו מסמך שונה גם מקומית (עדיין לא מסונכרן) וגם בשרת. אנו מסמנים את הרשומה בסטטוס // main/network-monitor.js const { net } = require('electron'); class NetworkMonitor { constructor() { this.isOnline = true; this.listeners = new Set(); this.checkInterval = null; } start(mainWindow) { this.window = mainWindow; this.checkInterval = setInterval(() => this.checkConnectivity(), 15000); this.checkConnectivity(); } async checkConnectivity() { const wasOnline = this.isOnline; try { const response = await Promise.race([ fetch('https://connectivity-check.your-api.com/ping', { method: 'HEAD' }), new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 5000)) ]); this.isOnline = response.ok; } catch { this.isOnline = false; } if (wasOnline !== this.isOnline) { this.window?.webContents.send('network:change', { isOnline: this.isOnline }); this.emit('change', this.isOnline); } } on(event, listener) { this.listeners.add({ event, listener }); } emit(event, data) { this.listeners.forEach(l => { if (l.event === event) l.listener(data); }); } stop() { clearInterval(this.checkInterval); } } module.exports = new NetworkMonitor(); ומציגים למשתמש בחירה: לשמור את הגרסה שלו או לקבל את גרסת השרת. ממשק המשתמש מציג את שתי הגרסאות עם חותמות זמן.
// renderer/components/ConflictResolver.tsx
interface ConflictDocument {
id: string;
title: string;
localContent: string;
serverContent: string;
localUpdatedAt: number;
serverUpdatedAt: number;
}
export function ConflictResolver({ doc, onResolve }: { doc: ConflictDocument; onResolve: (choice: 'local' | 'server') => void }) {
return (
<div className="conflict-modal">
<h3>Конфликт синхронизации: {doc.title}</h3>
<p>Этот документ был изменён и на этом устройстве, и на сервере.</p>
<div className="conflict-diff">
<div className="version local">
<h4>Ваша версия ({new Date(doc.localUpdatedAt).toLocaleString()})</h4>
<pre>{doc.localContent}</pre>
<button onClick={() => onResolve('local')}>Оставить мою версию</button>
</div>
<div className="version server">
<h4>Серверная версия ({new Date(doc.serverUpdatedAt).toLocaleString()})</h4>
<pre>{doc.serverContent}</pre>
<button onClick={() => onResolve('server')}>Принять серверную версию</button>
</div>
</div>
</div>
);
} שלבי יישום שלב אחר שלב
- הקימו מסד נתונים SQLite עם הטבלאות הנדרשות.
- צרו טבלת תור סנכרון לאחסון פעולות ממתינות.
- יישמו עדכונים אופטימיים: כתבו למסד המקומי תחילה, ואז לתור.
- טפלו בקונפליקטי סנכרון עם פתרון משתמש או CRDT אוטומטי.
- בדקו עם סימולציה של אובדן רשת וודאו שלמות נתונים.
מה כלול
כאשר אתם מזמינים מאיתנו יישום מצב לא מקוון, אתם מקבלים:
- תיעוד ארכיטקטורה ו-API לאחסון מקומי.
- קוד מקור עם הערות ובדיקות יחידה (כיסוי של 80%+).
- הוראות פריסה והגדרה.
- אחריות ל-3 חודשים על שגיאות סנכרון שזוהו.
- תמיכה באינטגרציה לפרויקט קיים.
- מפגש הדרכה למפתחים שלכם (עד שעתיים).
- דוח ביצועים עם תוצאות בדיקות עומס (לדוגמה, סנכרון 10,000 רשומות תוך פחות מ-3 שניות).
לוח זמנים ועלות פרויקט
| שלב | משך | תוצר |
|---|---|---|
| ניתוח | 1–2 ימים | רשימת ישויות, תרחישי עריכה, תדירות סנכרון |
| עיצוב | 1–2 ימים | סכמת מסד מקומי, אלגוריתם סנכרון, אסטרטגיית פתרון קונפליקטים |
| יישום | 3–7 ימים | אחסון מקומי, תור פעולות, מסנכרן, מחווני ממשק |
| בדיקות | 1–3 ימים | סימולציית אובדן רשת, אימות תור, קונפליקטים, בדיקות עומס |
| פריסה | 0.5 יום | פרסום עדכון, ניטור שגיאות סנכרון |
העלות הטיפוסית למצב לא מקוון בסיסי מתחילה ב-$2,500 (ישות אחת, סנכרון פשוט). לפרויקטים מורכבים עם ישויות מרובות ו-CRDT, תקצבו עד $12,000. צרו קשר להצעת מחיר מדויקת. הלקוחות שלנו חוסכים בדרך כלל 30% מזמן הפיתוח על ידי שימוש במודולים המובנים שלנו, עם הפחתת עלות ממוצעת של $4,000 לפרויקט. ההשקעה המינימלית ליכולות לא מקוונות היא $2,500.
שגיאות טיפוסיות ביישום מצב לא מקוון
- שימוש רק ב-
// main/db.js const Database = require('better-sqlite3'); const path = require('path'); const { app } = require('electron'); const dbPath = path.join(app.getPath('userData'), 'app.db'); const db = new Database(dbPath); db.pragma('journal_mode = WAL'); db.pragma('foreign_keys = ON'); db.exec(` CREATE TABLE IF NOT EXISTS documents ( id TEXT PRIMARY KEY, title TEXT NOT NULL, content TEXT NOT NULL, updated_at INTEGER NOT NULL, server_updated_at INTEGER, sync_status TEXT NOT NULL DEFAULT 'synced' ); CREATE TABLE IF NOT EXISTS sync_queue ( id INTEGER PRIMARY KEY AUTOINCREMENT, operation TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id TEXT NOT NULL, payload TEXT NOT NULL, created_at INTEGER NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, last_error TEXT ); `); module.exports = db;לזיהוי—תוצאות חיוביות שגויות ב-30% מהמקרים. - חוסר טרנזקציונליות בכתיבה לתור—סיכון לאובדן נתונים בקריסה (נמנע על ידי עסקאות SQLite).
- סנכרון כל הנתונים בכל חיבור—עומס על השרת. השתמשו בסנכרון מצטבר המבוסס על
// main/documents.js const db = require('./db'); function createDocument(doc) { const id = doc.id || crypto.randomUUID(); const now = Date.now(); db.prepare(` INSERT INTO documents (id, title, content, updated_at, sync_status) VALUES (@id, @title, @content, @updated_at, @sync_status) `).run({ id, title: doc.title, content: doc.content, updated_at: now, sync_status: 'pending' }); db.prepare(` INSERT INTO sync_queue (operation, entity_type, entity_id, payload, created_at) VALUES (@operation, @entity_type, @entity_id, @payload, @created_at) `).run({ operation: 'create', entity_type: 'document', entity_id: id, payload: JSON.stringify({ id, title: doc.title, content: doc.content }), created_at: now }); return { id, title: doc.title, content: doc.content, sync_status: 'pending' }; } function updateDocument(id, changes) { const now = Date.now(); db.prepare(` UPDATE documents SET title = COALESCE(@title, title), content = COALESCE(@content, content), updated_at = @updated_at, sync_status = 'pending' WHERE id = @id `).run({ ...changes, id, updated_at: now }); db.prepare(` INSERT INTO sync_queue (operation, entity_type, entity_id, payload, created_at) VALUES ('update', 'document', @id, @payload, @created_at) `).run({ id, payload: JSON.stringify({ id, ...changes }), created_at: now }); } module.exports = { createDocument, updateDocument };. מפחית עומס ב-80%. - עריכה ישירה של מסד הנתונים המקומי מתהליך ה-renderer—פרצת אבטחה. כל הפעולות חייבות לעבור דרך התהליך הראשי עם בדיקות הרשאות.
פעולה מנותקת משפרת משמעותית את חוויית המשתמש: לאחר היישום, פניות תמיכה הקשורות לבעיות נתונים יורדות ב-80%. הזמינו יישום מצב לא מקוון לאפליקציה שלכם וקבלו ייעוץ מהנדס. צרו קשר להערכה מפורטת של הפרויקט שלכם. היישום שלנו מעבד בדרך כלל 1,000 פעולות תור בשנייה, וזמן השהיית זיהוי הרשת עומד בממוצע על 5 שניות.







