برنامه نویسی

Storage Wars: Web Edition – یا اینکه چگونه یاد گرفتیم که داده های باینری را به طور موثر ذخیره کنیم

Summarize this content to 400 words in Persian Lang

پیش درآمد

همه مرورگرهای مدرن راه هایی برای ذخیره داده ها به صورت محلی ارائه می دهند. اگر آنها این کار را نمی کردند، دنیای ما نسبتاً متفاوت به نظر می رسید. از آنجایی که شما در حال خواندن این مطلب هستید، مطمئن هستم که از قبل اطلاعاتی در مورد فضای ذخیره سازی محلی در مرورگر دارید. با این حال، اجازه دهید به سرعت به اصول اولیه بپردازیم.

من این را در Google Docs می نویسم. فکر می کنم قبل از همگام سازی با سرورهای Google، داده های من را به صورت محلی ذخیره می کند. این چیز خوبی است، زیرا اطمینان حاصل می کند که هر تغییری که من ایجاد می کنم بلافاصله در دستگاه من ذخیره می شود و Docs به طور کامل به در دسترس بودن سرور خارجی وابسته نیست.

کوکی‌ها قدیمی‌ترین راه برای ذخیره داده‌ها به صورت محلی هستند – با این حال، آنها برای سوء استفاده توسط اشخاص ثالث آماده هستند. آنها همچنین برای ارتباط با سرورها به هر حال در نظر گرفته شده اند، بنابراین آنها یک داستان برای زمان دیگری هستند.

سپس ذخیره سازی جلسه و ذخیره سازی محلی مناسب داریم. آنها تقریباً یکسان هستند، به جز دامنه و طول عمرشان.

ذخیره سازی محلی داده ها را در هر مبدأ ذخیره می کند، به این معنی که هر زمان و از هر برگه ای که از همان URL بازدید می کنید، همان داده ها را به صورت محلی ذخیره می کنید.

ذخیره سازی جلسهاز سوی دیگر، در هر نمونه کار می کند، به این معنی که چرخه عمر آن به یک برگه مرورگر محدود می شود. در نظر گرفته شده است که به نمونه های جداگانه ای از یک برنامه وب اجازه دهد تا در ویندوزهای مختلف بدون تداخل با یکدیگر اجرا شوند، که این یک ویژگی بسیار خوب است. با این حال، دقیقاً مطابق با نیازهای ما نیست.

صبر کنید، کدام الزامات؟ دقیقا دنبال چی هستیم؟

ما اینجا هستیم تا گزینه های ذخیره سازی Scanbot Web SDK را بررسی کنیم.

این SDK دارای یک API پیچیده است که با انواع مختلف داده ها سروکار دارد. وقتی یک سند را با آن اسکن می کنید، فقط یک دسته رشته و اعداد دریافت نمی کنید. برای یک، داده های نتیجه همچنین شامل یک آرایه با مختصات مکان سند روی تصویر اسکن شده است. برای دیگری، شما همچنین خود تصویر را به عنوان داده باینری دریافت می کنید (در ادامه در این مورد بیشتر توضیح خواهیم داد). همه اینها باید به درستی ذخیره شوند و تا حد امکان به راحتی برای کاربر مدیریت شوند.

در اینجا نمونه‌ای از شی داده‌ای است که باید ذخیره کنیم (من به‌خاطر اختصار، حاشیه‌نویسی‌ها و انواع نامربوط را حذف کرده‌ام):

class CroppedDetectionResult {
croppedImage: RawImage | null;
originalImage: Image;
status: DetectionStatus = “NOT_ACQUIRED”;
detectionScores: DetectionScores;
points: Point[];
horizontalLines: LineSegment[];
verticalLines: LineSegment[];
aspectRatio: number;
averageBrightness: number = 0;
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که می بینید، خیلی پیچیده نیست. برخی از اعداد و رشته ها، برخی از کلاس ها به عنوان انواع.

تنها دو بخش وجود دارد که در واقع مشکل ساز هستند: RawImage، که حاوی Uint8Array داده ها، و Image، که نوعی است که می تواند به هر یک از اینترفیس ها اشاره کند ImageData، ArrayBuffer، Uint8Array و RawImage خود

پس چگونه می توانیم همه اینها را ذخیره کنیم؟

ذخیره سازی محلی

بیایید با اصول اولیه، OG ذخیره سازی شروع کنیم: localStorage. از قبل از اینکه بتوانم خودم را توسعه‌دهنده نرم‌افزار بنامم، از سال 2009 در دسترس بوده است. آه، زمان‌های ساده‌تر. localStorage هنوز در انواع برنامه ها استفاده می شود و API در طول اعصار نسبتاً پایدار باقی مانده است، بنابراین من مستقیماً وارد پیاده سازی می شوم.

بیایید یک کلاس ذخیره سازی جداگانه برای شی داده خود ایجاد کنیم. فقط استخوان های خالی برای ذخیره یک مورد و بازیابی آن بدون نگرانی در مورد کلیدهای منحصر به فرد یا چیز دیگری.

تجزیه JSON ضروری است زیرا localStorage فقط رشته‌ها را به‌عنوان مقادیر می‌پذیرد، که نشانه دیگری است که نشان می‌دهد این برای مجموعه‌های داده بزرگ‌تر نیست. و اینجاست:

export default class SBStorage {

static storeDocument(source: CroppedDetectionResult): void {
localStorage.setItem(“document”, JSON.stringify(source));
}

static getDocument(): CroppedDetectionResult {
const item = localStorage.getItem(“document”);
return item ? JSON.parse(item) : null;
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

محدودیت ها

در این مرحله، من فقط می‌خواستم به شما نشان دهم که یک سند بازیابی شده چگونه به نظر می‌رسد، اما پروژه شروع React من بلافاصله یک استثنا را ایجاد کرد:

sb-storage.ts:7 Uncaught (in promise) DOMException: Failed to execute ‘setItem’ on ‘Storage’: Setting the value of ‘document’ exceeded the quota.
at SBStorage.storeDocument (

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این ابزار آنلاین زیبا وجود دارد که به شما نشان می دهد چند کاراکتر را می توان در آن نگه داشت localStorage: https://arty.name/localstorage.html. از زمان نوشتن این، به نظر می رسد محدودیت من 5،200،000 باشد. این عدد بر اساس مرورگر و نسخه ای که استفاده می کنید متفاوت خواهد بود.

نویسنده سایت حتی توضیح می دهد:

Chrome 6.0.472.36 بتا به من اجازه می دهد 2600-2700 هزار کاراکتر را ذخیره کنم، فایرفاکس 3.6.8 – 5200-5300k، Explorer 8 – 4900-5000k، و Opera 10.70 build 9013 یک دیالوگ ظاهر شد که به من اجازه داد تا فضای ذخیره سازی نامحدود را ارائه دهم. Spec خودسرانه 5 مگابایت فضای ذخیره سازی را توصیه می کند، اما چیزی در مورد کاراکترهای واقعی نمی گوید، بنابراین در UTF-16 شما دو برابر کمتر دریافت می کنید.

بنابراین، بیایید بررسی کنیم که چه تعداد کاراکتر از داده‌ها را می‌خواهیم ذخیره کنیم. یک تغییر سریع در قطعه موجود…

const value = JSON.stringify(source);
console.log(“Characters:”, value.length);
localStorage.setItem(“document”, value);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

… و نتیجه ای که می گیریم این است:

Characters: 68046772

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که معلوم است، localStorage حتی نمی توان یک دهم آن را ذخیره کرد.

اگرچه ما قبلاً ثابت کرده ایم که در اینجا گزینه مناسبی نیست، به طور خلاصه می خواهم به این واقعیت اشاره کنم که localStorage داده های باینری را پشتیبانی نمی کند. این همان چیزی بود که من در وهله اول قرار بود به آن برسم، اما در عوض بلافاصله با موضوع سهمیه مواجه شدیم.

من فقط می خواهم به سرعت به شما نشان دهم که سریال سازی با فرمت داده باینری چه می کند. برای شروع، قالب ما croppedImage داده یک است Uint8Array. کم و بیش شبیه چیزی است که شما انتظار دارید (این آرایه در واقع 1,504,272 کاراکتر طول دارد، اما شروع آن اینجاست):

[169, 195, 195, 169, 195, 195, 168, 194, 194, 168, …

Enter fullscreen mode

Exit fullscreen mode

After serialization, the same array looks like the following:

{“0″:169,”1″:195,”2″:195,”3″:169,”4″:195,”5″:195,”6″:168,”7″:194,”8″:194,”9”:168, …

Enter fullscreen mode

Exit fullscreen mode

We’ll get into why this is in a later section, but what we need to know right now is that localStorage only accepts strings and therefore doesn’t support binary formats out of the box.

There are ways to get around this – you can probably already imagine an algorithm to turn this map of keys and values back into an array of integers. It’s always going to be a hack, though. You just shouldn’t have to manipulate binary data manually.

Base64

The correct way to do this would be to convert the image data to a Base64 image first, which we can then store properly.

Base64 is a family of binary-to-text encoding schemes that represent binary data in an ASCII string format by transforming it into a radix-64 representation.

That may sound complicated, but it’s really a straightforward process. You simply create a reader object, pass your array as a blob, and read it in as a data URL using the native JavaScript function. If necessary, also remove the descriptor part of the string. Here’s the convenience function we use:

public static async toBase64(data: Uint8Array): Promise<string> {
// See https://stackoverflow.com/a/39452119
const base64url = await new Promise<string>(resolve => {
const reader = new FileReader()
reader.onload = () => resolve(reader.result as string)
reader.readAsDataURL(new Blob([data]))
})؛
// قسمت 'data:…;base64' را از ابتدا حذف کنید
بازگشت base64url.برش(base64url.indexOf('،') + 1)
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و سپس موارد زیر، اگر خیلی طولانی نباشد، می توانند به طور موثر در حافظه محلی نگهداری شوند:

const base64 = await ImageUtils.toBase64(source.croppedImage.data);
// Base64: wsLQw8LQw8LQw8TRxMXSxMXTxcfUxcjVxMjVxcnXx8vaztHh1dbm19np2dvr2Nv…
console.log(“Base64: “, base64);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با این حال، هر نوع عملیاتی با این بزرگی از نظر محاسباتی سنگین است، چه دستکاری مستقیم نقشه کلید-مقدار یا نسخه Base64، و ما در اینجا سعی می‌کنیم بهینه باشیم. بنابراین همه اینها آن را قطع نمی کند. ما باید بتوانیم مستقیماً با داده های باینری کار کنیم.

و این ما را به …

داده های باینری

در توسعه وب، ما با داده های باینری بیشتر در هنگام کار با فایل ها (ایجاد، آپلود، دانلود) مواجه می شویم. یکی دیگر از موارد استفاده معمول، پردازش تصویر است – کاری که ما در اینجا انجام می دهیم.

کلاس های زیادی برای پیاده سازی های مختلف داده های باینری در جاوا اسکریپت وجود دارد، مانند ArrayBuffer، Uint8Array، DataView، Blob و File، اما این تفاوت های ظریف مختلف در حال حاضر مهم نیستند.

به هر حال، هدف اصلی است ArrayBuffer – ریشه همه چیز، داده های باینری خام. و این همان چیزی است که ما با آن کار خواهیم کرد. از هر یک از کلاس‌های wrapper فوق‌الذکر که استفاده می‌کنید، دارای تنوعی از یک است ArrayBuffer.

همه چیز یک شی است (تقریبا)

در حال حاضر، توضیح به عنوان دلیل رشته ArrayBuffer یا Uint8Array تبدیل آن به کمی کابوس در واقع بسیار جالب است. ببینید، همه چیز در جاوا اسکریپت در واقع یک شی است، به جز رشته ها. رشته ها هنوز هم رشته هستند. اما با آرایه ها، به این معنی است که می توانید کارهای زیر را انجام دهید:

const a = [1, 2, 3]; a.foo = “bar”console.log(a); // [ 1, 2, 3, foo: ‘bar’]

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

دلیلش این است که به طور بومی، یک آرایه هنوز یک نمونه اولیه شی است، اما در آن رشته می شود نگاه کردن مانند یک آرایه ساده فقط به این دلیل toString() تابع برای راحتی توسعه دهندگان بازنویسی شده است. زیرا اغلب مورد نیاز است.

و دقیقاً به همین دلیل است که یک بند ناف Uint8Array آن را به یک شی از جفت های کلید-مقدار تبدیل می کند (به یاد داشته باشید {“0″:169,”1″:195,”2″:195,”3”:169 …} مثال قبلی) و اینکه چرا ما به مراحل اضافی برای سریال سازی صحیح آن نیاز داریم.

سریال سازی

در این مرحله، همچنین ارزش آن را دارد که در مورد نکته ای که قبلاً به آن اشاره کردم توضیح دهیم: اینکه داده های باینری قابل سریال سازی نیستند. در حالی که این هنوز اساساً درست است، جاوا اسکریپت این روزها از پشتیبانی بومی برای سریال سازی برخوردار است، با آخرین و بهترین ابزار TextDecoder. این مثال را در نظر بگیرید:

const buffer = new TextEncoder().encode(‘banana’);
const testObj = { encodedText: buffer.toString() };

const serializedTestObj = JSON.stringify(testObj);
console.log(serializedTestObj); // {“encodedText”:”98,97,110,97,110,97″}

const deserializedTestObj = JSON.parse(serializedTestObj);
const deserializedBuffer = deserializedTestObj.encodedText.split(‘,’);
const newBuffer = Uint8Array.from(deserializedBuffer);
const str = new TextDecoder().decode(newBuffer);
console.log(str); // banana

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با تشکر از ویل مانسلو از dev.to برای این قطعه!

در حالی که هست نسبتا به سادگی، بلافاصله متوجه خواهید شد که این روش رمزگذاری به موارد زیر نیاز دارد:

برای اینکه داده‌ها در ابتدا رشته‌ای باشند که با کلاس کدگذار سریال‌سازی می‌شوند، که یک مورد استفاده نسبتاً غیر معمول است (در بیشتر موارد شما با داده‌های باینری شروع می‌کنید).
شکافتن رشته، که یک عمل گران قیمت است.
در پایان، هم سریال‌زدایی بافر و هم رمزگشایی متن دوباره.

حالا بیایید به روش بعدی ذخیره سازی وب برویم.

IndexedDB

بنابراین، IndexedDB چیست؟ من با کمال میل می‌توانستم نمای کوتاهی از مفهوم ارائه دهم، اما MDN به بهترین وجه می‌گوید:

IndexedDB یک API سطح پایین برای ذخیره سازی در سمت مشتری مقادیر قابل توجهی از داده های ساختاریافته، از جمله فایل ها/حباب ها است. این API از شاخص ها برای فعال کردن جستجوهای با کارایی بالا در این داده ها استفاده می کند.

آنچه که در واقع برای ما معنی دارد، یک راه مناسب برای ذخیره داده های باینری فوق الذکر است ArrayBuffer قالب بله، دیگر از این منطق رمزگشایی و رمزگشایی پیچیده، مزخرف و تشنه منابع خبری نیست!

برای تکرار سریع، مشکلات اصلی با localStorage عبارتند از:

فقط می تواند رشته ها را ذخیره کند.
دارای یک درپوش داده بسیار کوچک و ناپایدار است.

IndexedDB هر دوی این مسائل را به راحتی حل می کند. علاوه بر آن، مزایای دیگری نیز دارد، مانند این که می‌توان از آن در وب‌کارگرها استفاده کرد (در ادامه در این مورد بیشتر توضیح خواهیم داد). همچنین ناهمزمان است، به این معنی که چرخه رندر هنگام ذخیره/بارگذاری مسدود نمی شود.

تنها چیز localStorage به دلیل سادگی API آن است. فقط همین است localStorage.setItem و localStorage.getItem. چند کارکرد راحتی اضافی وجود دارد، اما تقریباً همین است. انجام شد!

API و راه حل های آشفته

IndexedDB نیز کامل نیست: API معروف است که یک آشفتگی شگفت انگیز است. در واقع، این چنین آشفتگی است که یک اکوسیستم کامل از کتابخانه ها در اطراف آن ایجاد شده است تا توسعه دهنده معمولی شما مجبور نباشد با مزخرفات آن دست و پنجه نرم کند.

بنابراین، قبل از اینکه در نهایت به اجرای واقعی خود بپردازیم، بیایید نگاهی به آنچه در حال حاضر وجود دارد بیندازیم تا در این مورد به شما کمک کند.

use-indexeddb وجود دارد که قول می‌دهد سبک‌ترین مورد باشد، اما این به این دلیل است که فقط تعدادی از اصول اولیه را در بر می‌گیرد. استفاده از آن چندان راحت نیست، و همچنین دارای برخی گزارش‌های نفوذی است که غیرفعال نمی‌شوند.

بعدی Dexie است، یک چارچوب ذخیره سازی کامل تر که بسیار امیدوارکننده به نظر می رسد. API آن بسیار ساده است. ایجاد یک پایگاه، نوشتن پرس و جو و افزودن مدخل ها فوق العاده بصری است:

const db = new Dexie(‘MyDatabase’);

// Declare tables, IDs and indexes
db.version(1).stores({ friends: ‘++id, name, age’ });

// Find some old friends
const oldFriends = await db.friends.where(‘age’).above(75).toArray();

// or make a new one
await db.friends.add({
name: ‘Camilla’,
age: 25,
street: ‘East 13:th Street’,
picture: await getBlob(‘camilla.png’)
});

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در نگاه اول، به نظر می رسد که از دستورات LINQ C# الهام گرفته شده است، و همچنین از پرس و جوهای زنده پشتیبانی می کند. این بدان معناست که شما می توانید آن را با مثلاً Redux ترکیب کنید و به طور کامل مدیریت دولت را از طریق آن مدیریت کنید. در حالی که این یک ویژگی خوب است، Dexie در واقع برای مورد استفاده ما کمی پیچیده است.

یکی دیگر idb است که ادعا می کند بیشتر API IndexedDB را منعکس می کند، اما با برخی بهبودهای قابل استفاده قابل توجه است. اما در واقع، با نگاهی به پیاده‌سازی، به نظر می‌رسد که API را در داخل می‌پیچد Promise کلاس و نه چیزهای دیگر. قطعاً برای چنین افزایش جزئی در راحتی، ارزش افزودن وابستگی شخص ثالث را ندارد.

فریم ورک محبوب نهایی JsStore نام دارد که IndexedDB API را در یک API شبیه به SQL قرار می دهد.

var insertCount = await connection.insert({ into: ‘Product’, values: [value] });

var results = await connection.select({ from: ‘Product’, where: { id: 5 } });

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

که به نظر حقیر من پیشرفت چندانی ندارد. SQL هنوز بسیار پیچیده است و ایجاد تراکنش های کمتر از حد بهینه آسان است. این SQL خام نیست، بنابراین شاید خیلی بد نباشد، اما من بارها توسط SQL سوزانده شده‌ام که نمی‌توانم دوباره به چنین چیزی اعتماد کنم.

به علاوه، مانند Dexie، به نظر می رسد JsStore برای موارد استفاده ما نیز بیش از حد است.

صادقانه بگویم، از قبل می دانستم که تحت تأثیر هیچ یک از این کتابخانه ها قرار نخواهم گرفت. برنامه همیشه این بود که دستمان را به روش اسپارتی کثیف کنیم و دیگر خود را به هیچ وابستگی بند ندهیم. من فقط می خواستم یک نمای کلی از گزینه های موجود ارائه دهم. اگر راه حل ما پیچیده تر بود، من با دکسی همراه می شدم.

رویکرد اسپارتی

با این حال، ما در حال ساخت یک SDK برای مشتریان هستیم، بنابراین نمی‌خواهیم به هیچ کتابخانه خارجی تکیه کنیم. ممکن است، و ما از آنها استفاده می کنیم، اما آنها را به سادگی اضافه نمی کنیم، زیرا:

آنها همیشه با مجوز می آیند که ما باید به آن پایبند باشیم و افشا کنیم.
آنها سربار را به توسعه و تعمیرات آتی اضافه می کنند (مطمئنم که درد ارتقاء وابستگی ها را به خوبی می دانید).
آنها نقاط شکست اضافی را به پروژه ما اضافه می کنند.

در صورت امکان و در حد معقول، می‌خواهیم بر آنچه در داخل SDK ما اتفاق می‌افتد، کنترل داشته باشیم.

بنابراین، برای منطق ذخیره سازی ما، بیایید خودمان را رول کنیم!

از نظر موارد استفاده، مورد ما نسبتاً ساده است. مدل داده به خودی خود پیچیده است، اما تراکنش های پایگاه داده اینطور نیست، بنابراین من انتظار دردسر زیادی را نداشتم. راه اندازی اولیه به اندازه کافی ساده است:

const request = indexedDB.open(“SBSDKBase”, 1);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ما درخواست را ایجاد می کنیم، نامی برای پایگاه داده در نظر می گیریم و شماره نسخه پایگاه داده ای را که از آن درخواست می کنیم اضافه می کنیم. تعیین فوری شماره نسخه ممکن است غیر ضروری به نظر برسد، اما مستندات دلیل آن را کاملاً روشن می‌کند: مدل‌های پایگاه داده همیشه تغییر می‌کنند و داشتن یک گزینه آسان برای پیگیری تاریخچه نسخه به اندازه کافی بصری به نظر می‌رسد.

شی درخواست دارای تماس های مختلف است: onerror، onupgradeneeded، و onsuccess. در حال حاضر، می‌توانیم خطاها را نادیده بگیریم، زیرا فقط سعی می‌کنیم برخی از اسناد اولیه را به پایگاه اضافه کنیم و آنها را بازیابی کنیم. هیچ تغییری در مدل وجود ندارد، هیچ نمایه‌سازی وجود ندارد، ما فقط از حالت نوشتن خواندن و افزایش خودکار استفاده می‌کنیم (امیدواریم که این به ما نخورد).

تنها چیزی که در حال حاضر به آن علاقه مندیم، پاسخ به تماس موفقیت آمیز است.

request.onsuccess = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
db.createObjectStore(“documents”, { keyPath: “id”, autoIncrement: true });

const transaction = db.transaction(“documents”, “readwrite”);
const store = transaction.objectStore(“documents”);
store.add(source);

console.log(“Added item to store”);

const getRequest = store.get(1);
getRequest.onsuccess = (event) => {
const result = (event.target as IDBRequest).result;
console.log(“Got item from store”, result);
};
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این بی‌سابقه‌ترین کد کدی است که فکر کردم باید سندی را به فضای ذخیره‌سازی اضافه کنم. پایگاه داده را باز کنید، یک جدول سند ایجاد کنید، یک شی را در آن قرار دهید. من مدت زیادی است که این کار را انجام می‌دهم، روی پایگاه‌های اطلاعاتی مختلفی کار کرده‌ام و فکر می‌کنم یک دسته روی آن دارم. فکر می کردم این کار می کند.

نه من همون موقع گاز گرفتم خطا:

Uncaught DOMException: Failed to execute ‘createObjectStore’ on ‘IDBDatabase’: The database is not running a version change transaction.

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

چرا هنگام باز کردن پایگاه داده نمی توانم جدول ایجاد کنم؟ من به سختی شروع کرده ام و از قبل این احساس را دارم که نوشتن SQL خام شهودی تر از این است. در آنجا، حداقل می توانید پس از باز کردن پایگاه داده، کوئری ها را اجرا کنید.

بیایید امیدوار باشیم از اینجا به بعد بهتر شود! حداقل خود پیام خطا رمزآلود نیست. با اجرای دوباره تلاش خواهیم کرد onupgradeneeded (فراموش نکنید نسخه db خود را ارتقا دهید):

request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
db.createObjectStore(“documents”, { keyPath: “id”, autoIncrement: true });
console.log(“IndexedDB upgraded”, event);
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون می توانیم آن را نیز حذف کنیم create ما یک پایگاه داده ایجاد کردیم، اولین مورد خود را به آن اضافه کردیم و همچنین آن را بازیابی کردیم:

request.onsuccess = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
const transaction = db.transaction(“documents”, “readwrite”);
const store = transaction.objectStore(“documents”);
store.add(source);
console.log(“Added item to store”);

const getRequest = store.get(1);
getRequest.onsuccess = (event) => {
const result = (event.target as IDBRequest).result;
console.log(“Got item from store”, result);
};
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و شی بازیابی شده همچنان حاوی داده های تصویر ما به درستی است Uint8Array قالب آبدار!

Got item from store {
status: ‘OK’,
detectionScores: {…},
points: Array(4),
horizontalLines: Array(5),
verticalLines: Array(5), …}
aspectRatio: 0.698217078300424averageBrightness: 183
croppedImage: {…, format: ‘BGR’, data: Uint8Array(1859760)

}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بهبودهای ناهمزمان

زمان آن است که کل فرآیند کمی راحت تر شود. بیایید باز شدن پایگاه داده را در بلوک ناهمزمان خودش بپیچانیم:

private static openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {

const DB_NAME = “SBSDKBase”;
const DB_VERSION = 4;
const request = indexedDB.open(DB_NAME, DB_VERSION);

request.onsuccess = (event) => {
resolve((event.target as IDBOpenDBRequest).result);
};

request.onerror = (event) => { reject(event); };
request.onupgradeneeded = (event) => {
// Update object stores when necessary
};
});
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و یک بلوک دیگر برای دریافت خود فروشگاه، بنابراین دیگر لازم نیست آن را تکرار کنیم:

private static async getDocumentStore(): Promise<IDBObjectStore> {
const db = await SBStorage.openDB();
const transaction = db.transaction(“documents”, “readwrite”);
return transaction.objectStore(“documents”);
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و اکنون ما دو عملکرد عمومی فوق العاده بصری برای سرویس ذخیره سازی خود برای ذخیره یک سند و بازیابی همه آنها داریم:

static async storeDocument(source: CroppedDetectionResult): Promise<void> {
const store = await SBStorage.getDocumentStore();
store.add(source);
}

static async getDocuments(): Promise<CroppedDetectionResult[]> {
const store = await SBStorage.getDocumentStore();
return new Promise((resolve, reject) => {
store.getAll().onsuccess = (event) => {
const result = (event.target as IDBRequest).result;
resolve(result);
};
});
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در نهایت باید مدیریت صحیح خطا را اضافه کنیم، اما همانطور که می‌بینید، با چند عملکرد wrapper، پیاده‌سازی IndexedDB به تنهایی، بدون نیاز به کتابخانه‌های شخص ثالث، نسبتاً آسان است.

این رویکرد ناهمزمان با Promise در حال حاضر از تماس های برگشتی استفاده می کند و رشته اصلی را مسدود نمی کند. می بینید که جاوا اسکریپت خیلی خوب است. ما فقط برخی از قندهای نحوی اولیه را پیاده سازی کردیم تا کد زیباتر به نظر برسد، و به هر حال این رویکرد بهینه بود.

زمان بندی معاملات

امکان بهینه سازی این مورد حتی بیشتر وجود دارد و ما در عرض یک ثانیه به آن خواهیم رسید. اما ابتدا، بیایید بررسی کنیم که تمام این تراکنش ها چقدر زمان می برد. شما همیشه باید دقیقا بدانید که چه چیزی را بهینه می کنید و چرا. معیارهای عملکرد کلیدی هستند.

قوی ترین راه برای انجام این کار این است که فقط به پیاده سازی اولیه خود برگردیم و اندازه گیری کنیم که چند میلی ثانیه پس از هر عملیات معنی دار گذشته است. به عنوان مثال:

const currentMS = new Date().getTime();
const request = indexedDB.open(“SBSDKBase”, 4);
console.log(“Opening indexedDB”, new Date().getTime() – currentMS);

request.onsuccess = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
const transaction = db.transaction(“documents”, “readwrite”);
const store = transaction.objectStore(“documents”);
store.add(source);
console.log(“Added item to store”, new Date().getTime() – currentMS);

const getRequest = store.get(1);
getRequest.onsuccess = (event) => {
const result = (event.target as IDBRequest).result;
console.log(“Got item from store”, new Date().getTime() – currentMS);
};
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

Opening indexedDB 0
Added item to store 17
Got item from store 30

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این خیلی بد نیست، اما هیچ چیز هم نیست. با توجه به اینکه من در حال حاضر روی یک مک بوک پرو رده بالاتر با تراشه M2 و SSD کار می کنم، این عملیات به راحتی می تواند 20 تا 50 برابر بیشتر در یک دستگاه اندروید یا iOS رده پایین تر طول بکشد.

ما قبلاً استفاده اولیه IndexedDB ناهمزمان را با Promises پیاده‌سازی کرده‌ایم، و در برخی موارد استفاده به اندازه کافی خوب است. اما از آنجایی که ما با تکه های بزرگ داده سر و کار داریم، ارزش آن را دارد که روی موازی سازی واقعی سرمایه گذاری کنیم و همه چیز را از موضوع اصلی خارج کنیم.

و این ما را به …

کارگران وب

کارگران وب ابزار ساده ای برای محتوای وب برای اجرای اسکریپت ها در رشته های پس زمینه هستند. Worker Thread می تواند وظایف را بدون تداخل با رابط کاربری انجام دهد. (MDN)

اینکه چگونه کارگران وب با وعده‌ها/تقاضاها متفاوت هستند بسیار اساسی است.

من خیلی عمیق در threading غوطه ور نمی شوم، اما به طور خلاصه، یک محیط جاوا اسکریپت اساساً تک رشته ای است. در ساده‌ترین شکل، ناهمزمان فقط قند نحوی برای زنجیره‌های وعده است که اجازه می‌دهد رابط کاربری قبل از سایر عملیات طولانی‌مدت ارائه شود.

همچنین مهم است که توجه داشته باشید که دو شغل هرگز نمی توانند به صورت موازی در این محیط اجرا شوند. یک عملیات ناهمگام ممکن است بین دو کار از یکی دیگر اجرا شود، بنابراین از این نظر می‌توانند «همزمان» اجرا شوند. برای موازی سازی واقعی، به عنوان مثال رندر همزمان رابط کاربری و ذخیره داده ها، ما به رشته ها نیاز داریم.

خوشبختانه، همه مرورگرهای مدرن به ما رشته‌های کارگری می‌دهند که امکان انجام این نوع عملیات را فراهم می‌کنند.

پیاده سازی اساسی و مشکلات

بیایید ببینیم چگونه می‌توانیم موضوعات کارگری را برای مورد خاص خود، با پیروی از همان کد مثالی که قبلاً استفاده شده بود، تنظیم کنیم.

تخم ریزی یک کارگر یک کار نسبتاً پیش پا افتاده است. از نمونه‌های MDN، فقط باید یک فایل جاوا اسکریپت کارگر ایجاد کنید:

onmessage = function(e) {
console.log(‘Worker: Message received from main script’);
const result = e.data[0] * e.data[1];
if (isNaN(result)) {
postMessage(‘Please write two numbers’);
} else {
const workerResult = ‘Result: ‘ + result;
console.log(‘Worker: Posting message back to main script’);
postMessage(workerResult);
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و سپس می توانید آن را در فایل اصلی جاوا اسکریپت خود وارد کنید و داده ها را به صورت زیر به آن ارسال کنید:

if (window.Worker) {
const myWorker = new Worker(“worker.js”);

[first, second].forEach(input => {
input.onchange = function() {
myWorker.postMessage([first.value, second.value]);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که می بینید، اجرای یک کد جاوا اسکریپت بر روی یک رشته دیگر به اندازه اجرای ناهمزمان آن بی اهمیت نیست. شما باید یک کارگر اختصاصی را از یک فایل جداگانه تشکیل دهید. در حالی که این کار نسبتاً ساده به نظر می رسد، هنگامی که سیستم ساخت شما پیچیده تر می شود، این کار نیز انجام می شود.

فایل Worker خود را کجا در محیط Vite قرار می دهید؟ وقتی از Webpack استفاده می کنید کجا می رود؟ آیا محیط شما از Webpack بدون استخوان استفاده می کند که قابل تنظیم است یا پشت سیستم ساخت دیگری پیچیده شده است؟ اگر تست‌های خودکاری هم در حال اجرا داشته باشید چه می‌شود – آیا آنها به راحتی می‌توانند فایل کارگر شما را دریافت کنند؟

یا، برای ارائه مثالی دقیق تر: ما از Webpack برای انتقال SDK استفاده می کنیم، و دارای یک افزونه worker-loader جداگانه است که می تواند برای اطمینان از بسته شدن کارگران در محصول نهایی استفاده شود. این خیلی ساده است. با این حال، محیط توسعه ما مستقیماً از کد منبع SDK استفاده می کند، بنابراین لازم است گزینه دیگری برای اطمینان از اینکه این فایل کارگر به درستی در آن محیط کامپایل شده است، داشته باشیم.

بارگذاری یک فایل به عنوان داده

بنابراین، ساده‌ترین راه‌حل این است که به سادگی فایل را همانطور که معمولاً می‌کنید، در کد منبع خود ایجاد کنید، و سپس به سادگی از خود جاوا اسکریپت برای بسته‌بندی آن در یک حباب استفاده کنید. سپس آن لکه در worker بارگذاری می‌شود، دقیقاً مانند یک فایل جاوا اسکریپت خارجی (و از Martins Olumide در dev.to برای این راهنمایی تشکر می‌کنیم).

ممکن است پیچیده به نظر برسد، اما راه حلی کاملاً محکم برای شروع است. شرط می بندم که برای تولید به اندازه کافی خوب است، اما قبل از ارسال، ما قطعاً به معیارهای عملکرد اضافی می پردازیم تا ببینیم آیا استفاده از Webpack برای پیش کامپایل آن در بسته بهتر است یا خیر.

ابتدا فایل worker پایه به شکل زیر خواهد بود:

const workerFunction = function () {
// Perform every operation we want in this function
self.onmessage = (event: MessageEvent) => {
postMessage(‘Message received!’);
};
};

// This stringifies the whole function
let codeToString = workerFunction.toString();
// This brings out the code in the bracket in string
let mainCode = codeToString.substring(codeToString.indexOf(‘{‘) + 1, codeToString.LastIndexOf(‘}’));
// Convert the code into a raw data
let blob = new Blob([mainCode], { type: ‘application/javascript’ });
// A url is made out of the blob object and we’re good to go
let worker_script = URL.createObjectURL(blob);

export default worker_script;

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نظرات در بلوک بالا باید کاملاً واضح باشد که ما قصد انجام آن را داریم: ما اساساً بارگذاری یک فایل worker جداگانه را به سادگی با stringify کردن کل worker و برگرداندن آن به عنوان یک حباب مسخره می‌کنیم.

همه را کنار هم گذاشتن

پس از آن، ما فقط باید کد قبلی خود را کمی تغییر دهیم تا با آن تابع جاوا اسکریپت مطابقت داشته باشد و کارگر خود را فعال کنیم. ما توابع wrapper تعریف شده قبلی را به داخل آن تابع منتقل می کنیم (چون، خوشبختانه، در جاوا اسکریپت می توانید توابع داخلی داشته باشید، در غیر این صورت به هم می خورد) و دقیقاً همان کاری را انجام می دهیم که قبلا انجام می دادیم، فقط اکنون باید به جای برگرداندن پیام، پیام را ارسال کنیم. داده ها:

const workerFunction = function () {

function openDB(): Promise<IDBDatabase> {

}

async function getDocumentStore(): Promise<IDBObjectStore> {

}

self.onmessage = (event: MessageEvent) => {
console.log(“Received in worker thread:”, event.data);
if (event.data.type === ‘storeDocument’) {
console.log(“Storing document in worker thread”);
getDocumentStore().then((store) => {
store.add(event.data.data);
self.postMessage(true);
});
} else if (event.data.type === ‘getDocuments’) {
console.log(“Getting documents in worker thread”);
getDocumentStore().then((store) => {
const request = store.getAll();
request.onsuccess = (event) => {
self.postMessage((event.target as IDBRequest).result);
};
});
}
};
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون که همه پیاده‌سازی IndexedDB API را به worker منتقل کرده‌ایم، فایل سرویس ذخیره‌سازی اصلی ما کاملاً خالی به نظر می‌رسد:

import { CroppedDetectionResult } from “../../core/worker/ScanbotSDK.Core”;
import worker_script from ‘./worker/sb-storage.worker’;

export default class SBStorage {

private static readonly worker: Worker = new Worker(worker_script);

static async storeDocument(source: CroppedDetectionResult): Promise<boolean> {
this.worker.postMessage({ type: ‘storeDocument’, data: source });

return new Promise<boolean>((resolve, reject) => {
this.worker.onmessage = (event) => {
resolve(true);
}
});
}

static async getDocuments(): Promise<CroppedDetectionResult[]> {
this.worker.postMessage({ type: ‘getDocuments’ });
return new Promise<CroppedDetectionResult[]>((resolve, reject) => {
this.worker.onmessage = (event) => {
resolve(event.data);
}
});
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تنها چیزی که در این فایل باقی می‌ماند، کدی است برای وارد کردن blob، مقداردهی اولیه خود کارگر، ارسال پیام‌ها به worker و حل قول پس از دریافت پیام.

و بس! برخی از مدیریت خطاهای اضافی باید پیاده سازی شود، اما در این مرحله این کار مشغله زیادی دارد. نکته اولیه: تنها در چند مرحله، ما خودمان یک راه حل ذخیره سازی بهینه برای اشیاء پیچیده ایجاد کرده ایم – فقط با استفاده از IndexedDB و کارگران وب، بدون نیاز به کتابخانه های شخص ثالث.

پیش درآمد

همه مرورگرهای مدرن راه هایی برای ذخیره داده ها به صورت محلی ارائه می دهند. اگر آنها این کار را نمی کردند، دنیای ما نسبتاً متفاوت به نظر می رسید. از آنجایی که شما در حال خواندن این مطلب هستید، مطمئن هستم که از قبل اطلاعاتی در مورد فضای ذخیره سازی محلی در مرورگر دارید. با این حال، اجازه دهید به سرعت به اصول اولیه بپردازیم.

من این را در Google Docs می نویسم. فکر می کنم قبل از همگام سازی با سرورهای Google، داده های من را به صورت محلی ذخیره می کند. این چیز خوبی است، زیرا اطمینان حاصل می کند که هر تغییری که من ایجاد می کنم بلافاصله در دستگاه من ذخیره می شود و Docs به طور کامل به در دسترس بودن سرور خارجی وابسته نیست.

کوکی‌ها قدیمی‌ترین راه برای ذخیره داده‌ها به صورت محلی هستند – با این حال، آنها برای سوء استفاده توسط اشخاص ثالث آماده هستند. آنها همچنین برای ارتباط با سرورها به هر حال در نظر گرفته شده اند، بنابراین آنها یک داستان برای زمان دیگری هستند.

سپس ذخیره سازی جلسه و ذخیره سازی محلی مناسب داریم. آنها تقریباً یکسان هستند، به جز دامنه و طول عمرشان.

ذخیره سازی محلی داده ها را در هر مبدأ ذخیره می کند، به این معنی که هر زمان و از هر برگه ای که از همان URL بازدید می کنید، همان داده ها را به صورت محلی ذخیره می کنید.

ذخیره سازی جلسهاز سوی دیگر، در هر نمونه کار می کند، به این معنی که چرخه عمر آن به یک برگه مرورگر محدود می شود. در نظر گرفته شده است که به نمونه های جداگانه ای از یک برنامه وب اجازه دهد تا در ویندوزهای مختلف بدون تداخل با یکدیگر اجرا شوند، که این یک ویژگی بسیار خوب است. با این حال، دقیقاً مطابق با نیازهای ما نیست.

صبر کنید، کدام الزامات؟ دقیقا دنبال چی هستیم؟

ما اینجا هستیم تا گزینه های ذخیره سازی Scanbot Web SDK را بررسی کنیم.

این SDK دارای یک API پیچیده است که با انواع مختلف داده ها سروکار دارد. وقتی یک سند را با آن اسکن می کنید، فقط یک دسته رشته و اعداد دریافت نمی کنید. برای یک، داده های نتیجه همچنین شامل یک آرایه با مختصات مکان سند روی تصویر اسکن شده است. برای دیگری، شما همچنین خود تصویر را به عنوان داده باینری دریافت می کنید (در ادامه در این مورد بیشتر توضیح خواهیم داد). همه اینها باید به درستی ذخیره شوند و تا حد امکان به راحتی برای کاربر مدیریت شوند.

در اینجا نمونه‌ای از شی داده‌ای است که باید ذخیره کنیم (من به‌خاطر اختصار، حاشیه‌نویسی‌ها و انواع نامربوط را حذف کرده‌ام):

class CroppedDetectionResult {
    croppedImage: RawImage | null;
    originalImage: Image;
    status: DetectionStatus = "NOT_ACQUIRED";
    detectionScores: DetectionScores;
    points: Point[];
    horizontalLines: LineSegment[];
    verticalLines: LineSegment[];
    aspectRatio: number;
    averageBrightness: number = 0;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که می بینید، خیلی پیچیده نیست. برخی از اعداد و رشته ها، برخی از کلاس ها به عنوان انواع.

تنها دو بخش وجود دارد که در واقع مشکل ساز هستند: RawImage، که حاوی Uint8Array داده ها، و Image، که نوعی است که می تواند به هر یک از اینترفیس ها اشاره کند ImageData، ArrayBuffer، Uint8Array و RawImage خود

پس چگونه می توانیم همه اینها را ذخیره کنیم؟

ذخیره سازی محلی

بیایید با اصول اولیه، OG ذخیره سازی شروع کنیم: localStorage. از قبل از اینکه بتوانم خودم را توسعه‌دهنده نرم‌افزار بنامم، از سال 2009 در دسترس بوده است. آه، زمان‌های ساده‌تر. localStorage هنوز در انواع برنامه ها استفاده می شود و API در طول اعصار نسبتاً پایدار باقی مانده است، بنابراین من مستقیماً وارد پیاده سازی می شوم.

بیایید یک کلاس ذخیره سازی جداگانه برای شی داده خود ایجاد کنیم. فقط استخوان های خالی برای ذخیره یک مورد و بازیابی آن بدون نگرانی در مورد کلیدهای منحصر به فرد یا چیز دیگری.

تجزیه JSON ضروری است زیرا localStorage فقط رشته‌ها را به‌عنوان مقادیر می‌پذیرد، که نشانه دیگری است که نشان می‌دهد این برای مجموعه‌های داده بزرگ‌تر نیست. و اینجاست:

export default class SBStorage {

    static storeDocument(source: CroppedDetectionResult): void {
        localStorage.setItem("document", JSON.stringify(source));
    }

    static getDocument(): CroppedDetectionResult {
        const item = localStorage.getItem("document");
        return item ? JSON.parse(item) : null;
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

محدودیت ها

در این مرحله، من فقط می‌خواستم به شما نشان دهم که یک سند بازیابی شده چگونه به نظر می‌رسد، اما پروژه شروع React من بلافاصله یک استثنا را ایجاد کرد:

sb-storage.ts:7 Uncaught (in promise) DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of 'document' exceeded the quota.
    at SBStorage.storeDocument (
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این ابزار آنلاین زیبا وجود دارد که به شما نشان می دهد چند کاراکتر را می توان در آن نگه داشت localStorage: https://arty.name/localstorage.html. از زمان نوشتن این، به نظر می رسد محدودیت من 5،200،000 باشد. این عدد بر اساس مرورگر و نسخه ای که استفاده می کنید متفاوت خواهد بود.

نویسنده سایت حتی توضیح می دهد:

Chrome 6.0.472.36 بتا به من اجازه می دهد 2600-2700 هزار کاراکتر را ذخیره کنم، فایرفاکس 3.6.8 – 5200-5300k، Explorer 8 – 4900-5000k، و Opera 10.70 build 9013 یک دیالوگ ظاهر شد که به من اجازه داد تا فضای ذخیره سازی نامحدود را ارائه دهم. Spec خودسرانه 5 مگابایت فضای ذخیره سازی را توصیه می کند، اما چیزی در مورد کاراکترهای واقعی نمی گوید، بنابراین در UTF-16 شما دو برابر کمتر دریافت می کنید.

بنابراین، بیایید بررسی کنیم که چه تعداد کاراکتر از داده‌ها را می‌خواهیم ذخیره کنیم. یک تغییر سریع در قطعه موجود…

const value = JSON.stringify(source);
console.log("Characters:", value.length);
localStorage.setItem("document", value);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

… و نتیجه ای که می گیریم این است:

Characters: 68046772
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که معلوم است، localStorage حتی نمی توان یک دهم آن را ذخیره کرد.

اگرچه ما قبلاً ثابت کرده ایم که در اینجا گزینه مناسبی نیست، به طور خلاصه می خواهم به این واقعیت اشاره کنم که localStorage داده های باینری را پشتیبانی نمی کند. این همان چیزی بود که من در وهله اول قرار بود به آن برسم، اما در عوض بلافاصله با موضوع سهمیه مواجه شدیم.

من فقط می خواهم به سرعت به شما نشان دهم که سریال سازی با فرمت داده باینری چه می کند. برای شروع، قالب ما croppedImage داده یک است Uint8Array. کم و بیش شبیه چیزی است که شما انتظار دارید (این آرایه در واقع 1,504,272 کاراکتر طول دارد، اما شروع آن اینجاست):

[169, 195, 195, 169, 195, 195, 168, 194, 194, 168, ...
Enter fullscreen mode

Exit fullscreen mode

After serialization, the same array looks like the following:

{"0":169,"1":195,"2":195,"3":169,"4":195,"5":195,"6":168,"7":194,"8":194,"9":168, ...
Enter fullscreen mode

Exit fullscreen mode

We’ll get into why this is in a later section, but what we need to know right now is that localStorage only accepts strings and therefore doesn’t support binary formats out of the box.

There are ways to get around this – you can probably already imagine an algorithm to turn this map of keys and values back into an array of integers. It’s always going to be a hack, though. You just shouldn’t have to manipulate binary data manually.

Base64

The correct way to do this would be to convert the image data to a Base64 image first, which we can then store properly.

Base64 is a family of binary-to-text encoding schemes that represent binary data in an ASCII string format by transforming it into a radix-64 representation.

That may sound complicated, but it’s really a straightforward process. You simply create a reader object, pass your array as a blob, and read it in as a data URL using the native JavaScript function. If necessary, also remove the descriptor part of the string. Here’s the convenience function we use:

public static async toBase64(data: Uint8Array): Promise<string> {
    // See https://stackoverflow.com/a/39452119
    const base64url = await new Promise<string>(resolve => {
        const reader = new FileReader()
        reader.onload = () => resolve(reader.result as string)
        reader.readAsDataURL(new Blob([data]))
    })؛
    // قسمت 'data:...;base64' را از ابتدا حذف کنید
    بازگشت base64url.برش(base64url.indexOf('،') + 1)
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و سپس موارد زیر، اگر خیلی طولانی نباشد، می توانند به طور موثر در حافظه محلی نگهداری شوند:

const base64 = await ImageUtils.toBase64(source.croppedImage.data);
// Base64: wsLQw8LQw8LQw8TRxMXSxMXTxcfUxcjVxMjVxcnXx8vaztHh1dbm19np2dvr2Nv...
console.log("Base64: ", base64);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با این حال، هر نوع عملیاتی با این بزرگی از نظر محاسباتی سنگین است، چه دستکاری مستقیم نقشه کلید-مقدار یا نسخه Base64، و ما در اینجا سعی می‌کنیم بهینه باشیم. بنابراین همه اینها آن را قطع نمی کند. ما باید بتوانیم مستقیماً با داده های باینری کار کنیم.

و این ما را به …

داده های باینری

در توسعه وب، ما با داده های باینری بیشتر در هنگام کار با فایل ها (ایجاد، آپلود، دانلود) مواجه می شویم. یکی دیگر از موارد استفاده معمول، پردازش تصویر است – کاری که ما در اینجا انجام می دهیم.

کلاس های زیادی برای پیاده سازی های مختلف داده های باینری در جاوا اسکریپت وجود دارد، مانند ArrayBuffer، Uint8Array، DataView، Blob و File، اما این تفاوت های ظریف مختلف در حال حاضر مهم نیستند.

به هر حال، هدف اصلی است ArrayBuffer – ریشه همه چیز، داده های باینری خام. و این همان چیزی است که ما با آن کار خواهیم کرد. از هر یک از کلاس‌های wrapper فوق‌الذکر که استفاده می‌کنید، دارای تنوعی از یک است ArrayBuffer.

همه چیز یک شی است (تقریبا)

در حال حاضر، توضیح به عنوان دلیل رشته ArrayBuffer یا Uint8Array تبدیل آن به کمی کابوس در واقع بسیار جالب است. ببینید، همه چیز در جاوا اسکریپت در واقع یک شی است، به جز رشته ها. رشته ها هنوز هم رشته هستند. اما با آرایه ها، به این معنی است که می توانید کارهای زیر را انجام دهید:

const a  = [1, 2, 3]; a.foo = "bar"console.log(a); // [ 1, 2, 3, foo: 'bar']
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

دلیلش این است که به طور بومی، یک آرایه هنوز یک نمونه اولیه شی است، اما در آن رشته می شود نگاه کردن مانند یک آرایه ساده فقط به این دلیل toString() تابع برای راحتی توسعه دهندگان بازنویسی شده است. زیرا اغلب مورد نیاز است.

و دقیقاً به همین دلیل است که یک بند ناف Uint8Array آن را به یک شی از جفت های کلید-مقدار تبدیل می کند (به یاد داشته باشید {"0":169,"1":195,"2":195,"3":169 …} مثال قبلی) و اینکه چرا ما به مراحل اضافی برای سریال سازی صحیح آن نیاز داریم.

سریال سازی

در این مرحله، همچنین ارزش آن را دارد که در مورد نکته ای که قبلاً به آن اشاره کردم توضیح دهیم: اینکه داده های باینری قابل سریال سازی نیستند. در حالی که این هنوز اساساً درست است، جاوا اسکریپت این روزها از پشتیبانی بومی برای سریال سازی برخوردار است، با آخرین و بهترین ابزار TextDecoder. این مثال را در نظر بگیرید:

const buffer = new TextEncoder().encode('banana');
const testObj = { encodedText: buffer.toString() };

const serializedTestObj = JSON.stringify(testObj);
console.log(serializedTestObj); // {"encodedText":"98,97,110,97,110,97"}

const deserializedTestObj = JSON.parse(serializedTestObj);
const deserializedBuffer = deserializedTestObj.encodedText.split(',');
const newBuffer = Uint8Array.from(deserializedBuffer);
const str = new TextDecoder().decode(newBuffer);
console.log(str); // banana
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با تشکر از ویل مانسلو از dev.to برای این قطعه!

در حالی که هست نسبتا به سادگی، بلافاصله متوجه خواهید شد که این روش رمزگذاری به موارد زیر نیاز دارد:

  • برای اینکه داده‌ها در ابتدا رشته‌ای باشند که با کلاس کدگذار سریال‌سازی می‌شوند، که یک مورد استفاده نسبتاً غیر معمول است (در بیشتر موارد شما با داده‌های باینری شروع می‌کنید).
  • شکافتن رشته، که یک عمل گران قیمت است.
  • در پایان، هم سریال‌زدایی بافر و هم رمزگشایی متن دوباره.

حالا بیایید به روش بعدی ذخیره سازی وب برویم.

IndexedDB

بنابراین، IndexedDB چیست؟ من با کمال میل می‌توانستم نمای کوتاهی از مفهوم ارائه دهم، اما MDN به بهترین وجه می‌گوید:

IndexedDB یک API سطح پایین برای ذخیره سازی در سمت مشتری مقادیر قابل توجهی از داده های ساختاریافته، از جمله فایل ها/حباب ها است. این API از شاخص ها برای فعال کردن جستجوهای با کارایی بالا در این داده ها استفاده می کند.

آنچه که در واقع برای ما معنی دارد، یک راه مناسب برای ذخیره داده های باینری فوق الذکر است ArrayBuffer قالب بله، دیگر از این منطق رمزگشایی و رمزگشایی پیچیده، مزخرف و تشنه منابع خبری نیست!

برای تکرار سریع، مشکلات اصلی با localStorage عبارتند از:

  • فقط می تواند رشته ها را ذخیره کند.
  • دارای یک درپوش داده بسیار کوچک و ناپایدار است.

IndexedDB هر دوی این مسائل را به راحتی حل می کند. علاوه بر آن، مزایای دیگری نیز دارد، مانند این که می‌توان از آن در وب‌کارگرها استفاده کرد (در ادامه در این مورد بیشتر توضیح خواهیم داد). همچنین ناهمزمان است، به این معنی که چرخه رندر هنگام ذخیره/بارگذاری مسدود نمی شود.

تنها چیز localStorage به دلیل سادگی API آن است. فقط همین است localStorage.setItem و localStorage.getItem. چند کارکرد راحتی اضافی وجود دارد، اما تقریباً همین است. انجام شد!

API و راه حل های آشفته

IndexedDB نیز کامل نیست: API معروف است که یک آشفتگی شگفت انگیز است. در واقع، این چنین آشفتگی است که یک اکوسیستم کامل از کتابخانه ها در اطراف آن ایجاد شده است تا توسعه دهنده معمولی شما مجبور نباشد با مزخرفات آن دست و پنجه نرم کند.

بنابراین، قبل از اینکه در نهایت به اجرای واقعی خود بپردازیم، بیایید نگاهی به آنچه در حال حاضر وجود دارد بیندازیم تا در این مورد به شما کمک کند.

use-indexeddb وجود دارد که قول می‌دهد سبک‌ترین مورد باشد، اما این به این دلیل است که فقط تعدادی از اصول اولیه را در بر می‌گیرد. استفاده از آن چندان راحت نیست، و همچنین دارای برخی گزارش‌های نفوذی است که غیرفعال نمی‌شوند.

بعدی Dexie است، یک چارچوب ذخیره سازی کامل تر که بسیار امیدوارکننده به نظر می رسد. API آن بسیار ساده است. ایجاد یک پایگاه، نوشتن پرس و جو و افزودن مدخل ها فوق العاده بصری است:

const db = new Dexie('MyDatabase');

// Declare tables, IDs and indexes
db.version(1).stores({ friends: '++id, name, age' });

// Find some old friends
const oldFriends = await db.friends.where('age').above(75).toArray();

// or make a new one
await db.friends.add({
    name: 'Camilla',
    age: 25,
    street: 'East 13:th Street',
    picture: await getBlob('camilla.png')
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در نگاه اول، به نظر می رسد که از دستورات LINQ C# الهام گرفته شده است، و همچنین از پرس و جوهای زنده پشتیبانی می کند. این بدان معناست که شما می توانید آن را با مثلاً Redux ترکیب کنید و به طور کامل مدیریت دولت را از طریق آن مدیریت کنید. در حالی که این یک ویژگی خوب است، Dexie در واقع برای مورد استفاده ما کمی پیچیده است.

یکی دیگر idb است که ادعا می کند بیشتر API IndexedDB را منعکس می کند، اما با برخی بهبودهای قابل استفاده قابل توجه است. اما در واقع، با نگاهی به پیاده‌سازی، به نظر می‌رسد که API را در داخل می‌پیچد Promise کلاس و نه چیزهای دیگر. قطعاً برای چنین افزایش جزئی در راحتی، ارزش افزودن وابستگی شخص ثالث را ندارد.

فریم ورک محبوب نهایی JsStore نام دارد که IndexedDB API را در یک API شبیه به SQL قرار می دهد.

var insertCount = await connection.insert({ into: 'Product', values: [value] });

var results = await connection.select({ from: 'Product', where: { id: 5 } });
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

که به نظر حقیر من پیشرفت چندانی ندارد. SQL هنوز بسیار پیچیده است و ایجاد تراکنش های کمتر از حد بهینه آسان است. این SQL خام نیست، بنابراین شاید خیلی بد نباشد، اما من بارها توسط SQL سوزانده شده‌ام که نمی‌توانم دوباره به چنین چیزی اعتماد کنم.

به علاوه، مانند Dexie، به نظر می رسد JsStore برای موارد استفاده ما نیز بیش از حد است.

صادقانه بگویم، از قبل می دانستم که تحت تأثیر هیچ یک از این کتابخانه ها قرار نخواهم گرفت. برنامه همیشه این بود که دستمان را به روش اسپارتی کثیف کنیم و دیگر خود را به هیچ وابستگی بند ندهیم. من فقط می خواستم یک نمای کلی از گزینه های موجود ارائه دهم. اگر راه حل ما پیچیده تر بود، من با دکسی همراه می شدم.

رویکرد اسپارتی

با این حال، ما در حال ساخت یک SDK برای مشتریان هستیم، بنابراین نمی‌خواهیم به هیچ کتابخانه خارجی تکیه کنیم. ممکن است، و ما از آنها استفاده می کنیم، اما آنها را به سادگی اضافه نمی کنیم، زیرا:

  • آنها همیشه با مجوز می آیند که ما باید به آن پایبند باشیم و افشا کنیم.
  • آنها سربار را به توسعه و تعمیرات آتی اضافه می کنند (مطمئنم که درد ارتقاء وابستگی ها را به خوبی می دانید).
  • آنها نقاط شکست اضافی را به پروژه ما اضافه می کنند.

در صورت امکان و در حد معقول، می‌خواهیم بر آنچه در داخل SDK ما اتفاق می‌افتد، کنترل داشته باشیم.

بنابراین، برای منطق ذخیره سازی ما، بیایید خودمان را رول کنیم!

از نظر موارد استفاده، مورد ما نسبتاً ساده است. مدل داده به خودی خود پیچیده است، اما تراکنش های پایگاه داده اینطور نیست، بنابراین من انتظار دردسر زیادی را نداشتم. راه اندازی اولیه به اندازه کافی ساده است:

const request = indexedDB.open("SBSDKBase", 1);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ما درخواست را ایجاد می کنیم، نامی برای پایگاه داده در نظر می گیریم و شماره نسخه پایگاه داده ای را که از آن درخواست می کنیم اضافه می کنیم. تعیین فوری شماره نسخه ممکن است غیر ضروری به نظر برسد، اما مستندات دلیل آن را کاملاً روشن می‌کند: مدل‌های پایگاه داده همیشه تغییر می‌کنند و داشتن یک گزینه آسان برای پیگیری تاریخچه نسخه به اندازه کافی بصری به نظر می‌رسد.

شی درخواست دارای تماس های مختلف است: onerror، onupgradeneeded، و onsuccess. در حال حاضر، می‌توانیم خطاها را نادیده بگیریم، زیرا فقط سعی می‌کنیم برخی از اسناد اولیه را به پایگاه اضافه کنیم و آنها را بازیابی کنیم. هیچ تغییری در مدل وجود ندارد، هیچ نمایه‌سازی وجود ندارد، ما فقط از حالت نوشتن خواندن و افزایش خودکار استفاده می‌کنیم (امیدواریم که این به ما نخورد).

تنها چیزی که در حال حاضر به آن علاقه مندیم، پاسخ به تماس موفقیت آمیز است.

request.onsuccess = (event) => {
    const db = (event.target as IDBOpenDBRequest).result;
    db.createObjectStore("documents", { keyPath: "id", autoIncrement: true });

    const transaction = db.transaction("documents", "readwrite");
    const store = transaction.objectStore("documents");
    store.add(source);

    console.log("Added item to store");

    const getRequest = store.get(1);
    getRequest.onsuccess = (event) => {
        const result = (event.target as IDBRequest).result;
        console.log("Got item from store", result);
    };
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این بی‌سابقه‌ترین کد کدی است که فکر کردم باید سندی را به فضای ذخیره‌سازی اضافه کنم. پایگاه داده را باز کنید، یک جدول سند ایجاد کنید، یک شی را در آن قرار دهید. من مدت زیادی است که این کار را انجام می‌دهم، روی پایگاه‌های اطلاعاتی مختلفی کار کرده‌ام و فکر می‌کنم یک دسته روی آن دارم. فکر می کردم این کار می کند.

نه من همون موقع گاز گرفتم خطا:

Uncaught DOMException: Failed to execute 'createObjectStore' on 'IDBDatabase': The database is not running a version change transaction.
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

چرا هنگام باز کردن پایگاه داده نمی توانم جدول ایجاد کنم؟ من به سختی شروع کرده ام و از قبل این احساس را دارم که نوشتن SQL خام شهودی تر از این است. در آنجا، حداقل می توانید پس از باز کردن پایگاه داده، کوئری ها را اجرا کنید.

بیایید امیدوار باشیم از اینجا به بعد بهتر شود! حداقل خود پیام خطا رمزآلود نیست. با اجرای دوباره تلاش خواهیم کرد onupgradeneeded (فراموش نکنید نسخه db خود را ارتقا دهید):

request.onupgradeneeded = (event) => {
    const db = (event.target as IDBOpenDBRequest).result;
    db.createObjectStore("documents", { keyPath: "id", autoIncrement: true });
    console.log("IndexedDB upgraded", event);
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون می توانیم آن را نیز حذف کنیم create ما یک پایگاه داده ایجاد کردیم، اولین مورد خود را به آن اضافه کردیم و همچنین آن را بازیابی کردیم:

request.onsuccess = (event) => {
    const db = (event.target as IDBOpenDBRequest).result;
    const transaction = db.transaction("documents", "readwrite");
    const store = transaction.objectStore("documents");
    store.add(source);
    console.log("Added item to store");

    const getRequest = store.get(1);
    getRequest.onsuccess = (event) => {
        const result = (event.target as IDBRequest).result;
        console.log("Got item from store", result);
    };
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و شی بازیابی شده همچنان حاوی داده های تصویر ما به درستی است Uint8Array قالب آبدار!

Got item from store {
    status: 'OK', 
    detectionScores: {...}, 
    points: Array(4), 
    horizontalLines: Array(5), 
    verticalLines: Array(5), }
    aspectRatio: 0.698217078300424averageBrightness: 183
    croppedImage: {..., format: 'BGR', data: Uint8Array(1859760)
    ...
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بهبودهای ناهمزمان

زمان آن است که کل فرآیند کمی راحت تر شود. بیایید باز شدن پایگاه داده را در بلوک ناهمزمان خودش بپیچانیم:

private static openDB(): Promise<IDBDatabase> {
    return new Promise((resolve, reject) => {

        const DB_NAME = "SBSDKBase";
        const DB_VERSION = 4;
        const request = indexedDB.open(DB_NAME, DB_VERSION);

        request.onsuccess = (event) => {
            resolve((event.target as IDBOpenDBRequest).result);
        };

        request.onerror = (event) => { reject(event); };
        request.onupgradeneeded = (event) => {
            // Update object stores when necessary
        };
    });
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و یک بلوک دیگر برای دریافت خود فروشگاه، بنابراین دیگر لازم نیست آن را تکرار کنیم:

private static async getDocumentStore(): Promise<IDBObjectStore> {
    const db = await SBStorage.openDB();
    const transaction = db.transaction("documents", "readwrite");
    return transaction.objectStore("documents");
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و اکنون ما دو عملکرد عمومی فوق العاده بصری برای سرویس ذخیره سازی خود برای ذخیره یک سند و بازیابی همه آنها داریم:

static async storeDocument(source: CroppedDetectionResult): Promise<void> {
    const store = await SBStorage.getDocumentStore();
    store.add(source);
}

static async getDocuments(): Promise<CroppedDetectionResult[]> {
    const store = await SBStorage.getDocumentStore();
    return new Promise((resolve, reject) => {
        store.getAll().onsuccess = (event) => {
            const result = (event.target as IDBRequest).result;
            resolve(result);
        };
    });
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در نهایت باید مدیریت صحیح خطا را اضافه کنیم، اما همانطور که می‌بینید، با چند عملکرد wrapper، پیاده‌سازی IndexedDB به تنهایی، بدون نیاز به کتابخانه‌های شخص ثالث، نسبتاً آسان است.

این رویکرد ناهمزمان با Promise در حال حاضر از تماس های برگشتی استفاده می کند و رشته اصلی را مسدود نمی کند. می بینید که جاوا اسکریپت خیلی خوب است. ما فقط برخی از قندهای نحوی اولیه را پیاده سازی کردیم تا کد زیباتر به نظر برسد، و به هر حال این رویکرد بهینه بود.

زمان بندی معاملات

امکان بهینه سازی این مورد حتی بیشتر وجود دارد و ما در عرض یک ثانیه به آن خواهیم رسید. اما ابتدا، بیایید بررسی کنیم که تمام این تراکنش ها چقدر زمان می برد. شما همیشه باید دقیقا بدانید که چه چیزی را بهینه می کنید و چرا. معیارهای عملکرد کلیدی هستند.

قوی ترین راه برای انجام این کار این است که فقط به پیاده سازی اولیه خود برگردیم و اندازه گیری کنیم که چند میلی ثانیه پس از هر عملیات معنی دار گذشته است. به عنوان مثال:

const currentMS = new Date().getTime();
const request = indexedDB.open("SBSDKBase", 4);
console.log("Opening indexedDB", new Date().getTime() - currentMS);

request.onsuccess = (event) => {
    const db = (event.target as IDBOpenDBRequest).result;
    const transaction = db.transaction("documents", "readwrite");
    const store = transaction.objectStore("documents");
    store.add(source);
    console.log("Added item to store", new Date().getTime() - currentMS);

    const getRequest = store.get(1);
    getRequest.onsuccess = (event) => {
        const result = (event.target as IDBRequest).result;
        console.log("Got item from store", new Date().getTime() - currentMS);
    };
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

Opening indexedDB 0
Added item to store 17
Got item from store 30
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این خیلی بد نیست، اما هیچ چیز هم نیست. با توجه به اینکه من در حال حاضر روی یک مک بوک پرو رده بالاتر با تراشه M2 و SSD کار می کنم، این عملیات به راحتی می تواند 20 تا 50 برابر بیشتر در یک دستگاه اندروید یا iOS رده پایین تر طول بکشد.

ما قبلاً استفاده اولیه IndexedDB ناهمزمان را با Promises پیاده‌سازی کرده‌ایم، و در برخی موارد استفاده به اندازه کافی خوب است. اما از آنجایی که ما با تکه های بزرگ داده سر و کار داریم، ارزش آن را دارد که روی موازی سازی واقعی سرمایه گذاری کنیم و همه چیز را از موضوع اصلی خارج کنیم.

و این ما را به …

کارگران وب

کارگران وب ابزار ساده ای برای محتوای وب برای اجرای اسکریپت ها در رشته های پس زمینه هستند. Worker Thread می تواند وظایف را بدون تداخل با رابط کاربری انجام دهد. (MDN)

اینکه چگونه کارگران وب با وعده‌ها/تقاضاها متفاوت هستند بسیار اساسی است.

من خیلی عمیق در threading غوطه ور نمی شوم، اما به طور خلاصه، یک محیط جاوا اسکریپت اساساً تک رشته ای است. در ساده‌ترین شکل، ناهمزمان فقط قند نحوی برای زنجیره‌های وعده است که اجازه می‌دهد رابط کاربری قبل از سایر عملیات طولانی‌مدت ارائه شود.

همچنین مهم است که توجه داشته باشید که دو شغل هرگز نمی توانند به صورت موازی در این محیط اجرا شوند. یک عملیات ناهمگام ممکن است بین دو کار از یکی دیگر اجرا شود، بنابراین از این نظر می‌توانند «همزمان» اجرا شوند. برای موازی سازی واقعی، به عنوان مثال رندر همزمان رابط کاربری و ذخیره داده ها، ما به رشته ها نیاز داریم.

خوشبختانه، همه مرورگرهای مدرن به ما رشته‌های کارگری می‌دهند که امکان انجام این نوع عملیات را فراهم می‌کنند.

پیاده سازی اساسی و مشکلات

بیایید ببینیم چگونه می‌توانیم موضوعات کارگری را برای مورد خاص خود، با پیروی از همان کد مثالی که قبلاً استفاده شده بود، تنظیم کنیم.

تخم ریزی یک کارگر یک کار نسبتاً پیش پا افتاده است. از نمونه‌های MDN، فقط باید یک فایل جاوا اسکریپت کارگر ایجاد کنید:

onmessage = function(e) {
  console.log('Worker: Message received from main script');
  const result = e.data[0] * e.data[1];
  if (isNaN(result)) {
    postMessage('Please write two numbers');
  } else {
    const workerResult = 'Result: ' + result;
    console.log('Worker: Posting message back to main script');
    postMessage(workerResult);
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و سپس می توانید آن را در فایل اصلی جاوا اسکریپت خود وارد کنید و داده ها را به صورت زیر به آن ارسال کنید:

if (window.Worker) {
  const myWorker = new Worker("worker.js");

  [first, second].forEach(input => {
    input.onchange = function() {
      myWorker.postMessage([first.value, second.value]);
      ...
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که می بینید، اجرای یک کد جاوا اسکریپت بر روی یک رشته دیگر به اندازه اجرای ناهمزمان آن بی اهمیت نیست. شما باید یک کارگر اختصاصی را از یک فایل جداگانه تشکیل دهید. در حالی که این کار نسبتاً ساده به نظر می رسد، هنگامی که سیستم ساخت شما پیچیده تر می شود، این کار نیز انجام می شود.

فایل Worker خود را کجا در محیط Vite قرار می دهید؟ وقتی از Webpack استفاده می کنید کجا می رود؟ آیا محیط شما از Webpack بدون استخوان استفاده می کند که قابل تنظیم است یا پشت سیستم ساخت دیگری پیچیده شده است؟ اگر تست‌های خودکاری هم در حال اجرا داشته باشید چه می‌شود – آیا آنها به راحتی می‌توانند فایل کارگر شما را دریافت کنند؟

یا، برای ارائه مثالی دقیق تر: ما از Webpack برای انتقال SDK استفاده می کنیم، و دارای یک افزونه worker-loader جداگانه است که می تواند برای اطمینان از بسته شدن کارگران در محصول نهایی استفاده شود. این خیلی ساده است. با این حال، محیط توسعه ما مستقیماً از کد منبع SDK استفاده می کند، بنابراین لازم است گزینه دیگری برای اطمینان از اینکه این فایل کارگر به درستی در آن محیط کامپایل شده است، داشته باشیم.

بارگذاری یک فایل به عنوان داده

بنابراین، ساده‌ترین راه‌حل این است که به سادگی فایل را همانطور که معمولاً می‌کنید، در کد منبع خود ایجاد کنید، و سپس به سادگی از خود جاوا اسکریپت برای بسته‌بندی آن در یک حباب استفاده کنید. سپس آن لکه در worker بارگذاری می‌شود، دقیقاً مانند یک فایل جاوا اسکریپت خارجی (و از Martins Olumide در dev.to برای این راهنمایی تشکر می‌کنیم).

ممکن است پیچیده به نظر برسد، اما راه حلی کاملاً محکم برای شروع است. شرط می بندم که برای تولید به اندازه کافی خوب است، اما قبل از ارسال، ما قطعاً به معیارهای عملکرد اضافی می پردازیم تا ببینیم آیا استفاده از Webpack برای پیش کامپایل آن در بسته بهتر است یا خیر.

ابتدا فایل worker پایه به شکل زیر خواهد بود:

const workerFunction = function () {
   // Perform every operation we want in this function
   self.onmessage = (event: MessageEvent) => {
      postMessage('Message received!');
   };
};

// This stringifies the whole function
let codeToString = workerFunction.toString();
// This brings out the code in the bracket in string
let mainCode = codeToString.substring(codeToString.indexOf('{') + 1, codeToString.LastIndexOf('}'));
// Convert the code into a raw data
let blob = new Blob([mainCode], { type: 'application/javascript' });
// A url is made out of the blob object and we're good to go
let worker_script = URL.createObjectURL(blob);

export default worker_script;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نظرات در بلوک بالا باید کاملاً واضح باشد که ما قصد انجام آن را داریم: ما اساساً بارگذاری یک فایل worker جداگانه را به سادگی با stringify کردن کل worker و برگرداندن آن به عنوان یک حباب مسخره می‌کنیم.

همه را کنار هم گذاشتن

پس از آن، ما فقط باید کد قبلی خود را کمی تغییر دهیم تا با آن تابع جاوا اسکریپت مطابقت داشته باشد و کارگر خود را فعال کنیم. ما توابع wrapper تعریف شده قبلی را به داخل آن تابع منتقل می کنیم (چون، خوشبختانه، در جاوا اسکریپت می توانید توابع داخلی داشته باشید، در غیر این صورت به هم می خورد) و دقیقاً همان کاری را انجام می دهیم که قبلا انجام می دادیم، فقط اکنون باید به جای برگرداندن پیام، پیام را ارسال کنیم. داده ها:

const workerFunction = function () {

    function openDB(): Promise<IDBDatabase> {
        ...
    }

    async function getDocumentStore(): Promise<IDBObjectStore> {
        ...
    }

    self.onmessage = (event: MessageEvent) => {
        console.log("Received in worker thread:", event.data);
        if (event.data.type === 'storeDocument') {
            console.log("Storing document in worker thread");
            getDocumentStore().then((store) => {
                store.add(event.data.data);
                self.postMessage(true);
            });
        } else if (event.data.type === 'getDocuments') {
            console.log("Getting documents in worker thread");
            getDocumentStore().then((store) => {
                const request = store.getAll();
                request.onsuccess = (event) => {
                    self.postMessage((event.target as IDBRequest).result);
                };
            });
        }
    };
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون که همه پیاده‌سازی IndexedDB API را به worker منتقل کرده‌ایم، فایل سرویس ذخیره‌سازی اصلی ما کاملاً خالی به نظر می‌رسد:

import { CroppedDetectionResult } from "../../core/worker/ScanbotSDK.Core";
import worker_script from './worker/sb-storage.worker';

export default class SBStorage {

    private static readonly worker: Worker = new Worker(worker_script);

    static async storeDocument(source: CroppedDetectionResult): Promise<boolean> {
        this.worker.postMessage({ type: 'storeDocument', data: source });

        return new Promise<boolean>((resolve, reject) => {
            this.worker.onmessage = (event) => {
                resolve(true);
            }
        });
    }

    static async getDocuments(): Promise<CroppedDetectionResult[]> {
        this.worker.postMessage({ type: 'getDocuments' });
        return new Promise<CroppedDetectionResult[]>((resolve, reject) => {
            this.worker.onmessage = (event) => {
                resolve(event.data);
            }
        });
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

تنها چیزی که در این فایل باقی می‌ماند، کدی است برای وارد کردن blob، مقداردهی اولیه خود کارگر، ارسال پیام‌ها به worker و حل قول پس از دریافت پیام.

و بس! برخی از مدیریت خطاهای اضافی باید پیاده سازی شود، اما در این مرحله این کار مشغله زیادی دارد. نکته اولیه: تنها در چند مرحله، ما خودمان یک راه حل ذخیره سازی بهینه برای اشیاء پیچیده ایجاد کرده ایم – فقط با استفاده از IndexedDB و کارگران وب، بدون نیاز به کتابخانه های شخص ثالث.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا