
مرحباً بالجميع ، أريد مشاركة نتيجة أفكاري حول ما يمكن أن يكون عليه تطبيق ويب حديث. كمثال ، فكر في تصميم لوحة إعلانات للكاريكاتير. بمعنى من المعاني ، تم تصميم المنتج المعني لجمهور من المهوسون والمتعاطفين معه ، والذي يسمح لك بإظهار الحرية في الواجهة. في المكون التقني ، على العكس من ذلك ، الاهتمام بالتفاصيل ضروري.
في الحقيقة ، أنا لا أفهم أي شيء في الكوميديا ، لكني أحب أسواق السلع المستعملة ، وخاصة في تنسيق المنتدى ، والتي كانت شائعة في الصفر. وبالتالي ، فإن الافتراض (الذي ربما يكون خاطئًا) ، والذي تنبثق منه الاستنتاجات التالية ، هو واحد فقط - النوع الرئيسي للتفاعل مع التطبيق هو عرض الإعلانات والمناقشات الثانوية.
سيكون هدفنا هو إنشاء تطبيق بسيط ، بدون الدراية الفنية صفارات إضافية ، ومع ذلك ، بما يتفق مع الحقائق الحديثة. المتطلبات الرئيسية التي أود تحقيقها هي:
جانب الخادم:
أ) يؤدي وظائف تخزين ، والتحقق من صحة ، وإرسال بيانات المستخدم إلى العميل
ب) تستهلك العمليات المذكورة أعلاه كمية مقبولة من الموارد (الوقت ، بما في ذلك)
ج) التطبيق والبيانات محمية من ناقلات الهجوم الشعبية
د) لديه واجهة برمجة تطبيقات بسيطة لعملاء الطرف الثالث والتفاعل بين الخوادم
ه) عبر منصة ، نشر بسيط
جانب العميل:
أ) يوفر الوظائف اللازمة لإنشاء واستهلاك المحتوى
ب) واجهة مريحة للاستخدام المنتظم ، والحد الأدنى للمسار إلى أي إجراء ، والحد الأقصى لمقدار البيانات لكل شاشة
ج) خارج التواصل مع الخادم ، تتوفر جميع الوظائف المتاحة في هذا الموقف
د) تعرض الواجهة الإصدار الحالي من الحالة والمحتوى ، دون إعادة التشغيل والانتظار
د) إعادة تشغيل التطبيق لا يؤثر على حالته
و) إن أمكن ، أعد استخدام عناصر DOM ورمز JS
ز) لن نستخدم مكتبات وإطارات عمل طرف ثالث في وقت التشغيل
ح) التخطيط هو الدلالي للوصول ، والمحللين ، الخ
i) يمكن التنقل في المحتوى الرئيسي باستخدام عنوان URL ولوحة المفاتيح
في رأيي ، المتطلبات المنطقية ، ومعظم التطبيقات الحديثة بدرجة أو بأخرى تلبي هذه الشروط. دعونا نرى ما يحدث معنا (رابط للمصدر والعرض التجريبي في نهاية المنشور).
تحذيرات:- أريد أن أعتذر للمؤلفين المجهولين عن الصور المستخدمة في العرض التوضيحي دون إذن ، وكذلك Gösse G. و Prozorovskaya B. و دار النشر "مكتبة فلورنس بافلنكوف" لاستخدام مقتطفات من أعمال "Siddhartha".
- المؤلف ليس مبرمجًا حقيقيًا ، ولا أوصي باستخدام الكود أو التقنيات المستخدمة في هذا المشروع إذا كنت لا تعرف ما الذي تفعله.
- أعتذر عن أسلوب الكود ، يمكن كتابته بشكل أكثر قابلية للقراءة وواضح ، لكن هذا ليس ممتعًا. مشروع للروح ولصديق ، كما يقولون.
- أعتذر أيضًا عن معدل الإلمام بالقراءة والكتابة ، لا سيما في النص الإنجليزي. سنوات الكلام من ماي هارت.
- تم اختبار أداء النموذج الأولي المقدم في [chromium 70؛ linux x86_64؛ 1366 × 768] ، سأكون ممتنًا للغاية لمستخدمي الأنظمة الأساسية والأجهزة الأخرى لرسائل الخطأ.
- هذا نموذج أولي وموضوع مقترح للمناقشة - النهج والمبادئ ، أطلب أن تصاحب جميع الانتقادات للتنفيذ والجانب الجمالي الحجج.
الخادم
لغة الخادم هي golang. لغة بسيطة وسريعة مع مكتبة ووثائق قياسية ممتازة ... مزعج قليلاً. وقع الاختيار الأولي على الإكسير / إيرلانغ ، لكن بما أني أعرف بالفعل go (نسبيًا) ، فقد تقرر عدم تعقيده (وكانت الحزم الضرورية متوفرة فقط).
لا يتم تشجيع استخدام أطر الويب في مجتمع الإنترنت (من المبرر ، الأمر يستحق الاعتراف) ، فنحن نختار حلاً وسطًا ونستخدم العمل المصغر لأدوات labstack / echo ، مما يقلل من حجم الروتين ، ويبدو لي أنه لا يفقد الكثير في الأداء.
نحن نستخدم tidwall / buntdb كقاعدة البيانات. أولاً ، الحل المدمج هو أكثر ملاءمة ويقلل من التكاليف العامة ، وثانياً ، في الذاكرة + مفتاح / قيمة - المألوف ، أنيق سريع وليس ذاكرة التخزين المؤقت اللازمة. نقوم بتخزين وإعطاء البيانات في JSON ، التحقق فقط عند التغيير.
على الجيل الثاني من i3 ، يعرض المسجل المدمج وقت التنفيذ للطلبات المختلفة من 0.5 إلى 10 مللي ثانية. إن تشغيل wrk على نفس الجهاز يظهر أيضًا نتائج كافية لأغراضنا:
➜ comico git:(master) wrk -t2 -c500 -d60s http://localhost:9001/pub/mtimes Running 1m test @ http://localhost:9001/pub/mtimes 2 threads and 500 connections Thread Stats Avg Stdev Max +/- Stdev Latency 20.74ms 16.68ms 236.16ms 72.69% Req/Sec 13.19k 627.43 15.62k 73.58% 1575522 requests in 1.00m, 449.26MB read Requests/sec: 26231.85 Transfer/sec: 7.48MB
➜ comico git:(master) wrk -t2 -c500 -d60s http://localhost:9001/pub/goods Running 1m test @ http://localhost:9001/pub/goods 2 threads and 500 connections Thread Stats Avg Stdev Max +/- Stdev Latency 61.79ms 65.96ms 643.73ms 86.48% Req/Sec 5.26k 705.24 7.88k 70.31% 628215 requests in 1.00m, 8.44GB read Requests/sec: 10454.44 Transfer/sec: 143.89MB
هيكل المشروع
تنقسم الحزمة comico / model إلى ثلاثة ملفات:
model.go - يحتوي على وصف لأنواع البيانات والوظائف العامة: الإنشاء / التحديث (buntdb لا يميز بين هذه العمليات ونحن نتحقق من وجود سجل يدويًا) ، والتحقق من الصحة ، وحذفها ، واستعادة سجل واحد واسترداد قائمة ؛
rules.go - يحتوي على قواعد التحقق من الصحة لنوع معين ووظيفة التسجيل ؛
files.go - العمل مع الصور.
يقوم نوع Mtimes بتخزين البيانات على آخر تغيير للأنواع المتبقية في قاعدة البيانات ، وبالتالي إبلاغ العميل بالبيانات التي تغيرت.
تحتوي الحزمة comico / bd على وظائف معممة للتفاعل مع قاعدة البيانات: إنشاء وحذف وتحديد ، إلخ. يحفظ Buntdb جميع التغييرات على ملف (في حالتنا ، مرة واحدة في الثانية) ، في تنسيق نصي ، وهو مناسب في بعض الحالات. لم يتم تحرير ملف ديسيبل ، تتم إضافة التغييرات في حالة نجاح المعاملة إلى النهاية. كل محاولاتي لانتهاك سلامة البيانات لم تنجح ، وفي أسوأ الحالات ، ضاعت التغييرات في الثانية الأخيرة.
في تطبيقنا ، يتوافق كل نوع مع قاعدة بيانات منفصلة في ملف منفصل (باستثناء السجلات المخزنة حصريًا في الذاكرة ويتم إعادة تعيينها إلى الصفر عند إعادة التشغيل). ويرجع ذلك إلى حد كبير إلى راحة النسخ الاحتياطي والإدارة ، وهي علامة صغيرة - معاملة مفتوحة لتحرير كتل الوصول إلى نوع واحد فقط من البيانات.
يمكن استبدال هذه الحزمة بسهولة بحزمة مشابهة باستخدام قاعدة بيانات أخرى ، SQL ، على سبيل المثال. للقيام بذلك ، يكفي تنفيذ الوظائف التالية:
func Delete(db byte, key string) error func Exist(db byte, key string) bool func Insert(db byte, key, val string) error func ReadAll(db byte, pattern string) (str string, err error) func ReadOne(db byte, key string) (str string, err error) func Renew(db byte, key string) (err error, newId string)
تحتوي الحزمة comico / cnst على بعض الثوابت اللازمة في جميع الحزم (أنواع البيانات ، أنواع الإجراءات ، أنواع المستخدمين). بالإضافة إلى ذلك ، تحتوي هذه الحزمة على جميع الرسائل القابلة للقراءة البشرية والتي سيرد بها خادمنا على العالم الخارجي.
تحتوي الحزمة comico / server على معلومات التوجيه. بالإضافة إلى ذلك ، يتم تكوين التفويض فقط بسطر (بفضل مطوري Echo) ، باستخدام JWT و CORS ورؤوس CSP والمسجل والتوزيع الثابت و gzip وشهادة ACME التلقائية وما إلى ذلك.
نقاط دخول API
عنوان URL | البيانات | الوصف |
---|
get / pub / (goods | مشاركات | مستخدمون | cmnts | ملفات) | - | الحصول على مجموعة من الإعلانات والمشاركات والمستخدمين والتعليقات والملفات ذات الصلة |
الحصول على / حانة / أوقات | - | الحصول على وقت التغيير الأخير لكل نوع البيانات |
آخر / حانة / تسجيل الدخول | {id *: تسجيل الدخول ، pass *: password} | إرجاع الرمز المميز JWT ومدته |
آخر / حانة / تمرير | {id * ، pass *} | ينشئ مستخدمًا جديدًا إذا كانت البيانات صحيحة |
وضع / api / pass | {id * ، pass *} | تحديث كلمة المرور |
آخر | وضع / api / البضائع | {id *، auth *، title *، type *، price *، text *، images: []، Table: {key: value}} | إنشاء / تحديث الإعلان |
آخر | وضع / api / المشاركات | {id *، auth *، title *، type *، text *} | إنشاء / تحديث منتدى آخر |
آخر | وضع / api / المستخدمين | {id * ، العنوان ، النوع ، الحالة ، الكتبة: [] ، يتجاهل: [] ، جدول: {مفتاح: قيمة}} | إنشاء / تحديث المستخدم |
آخر / api / cmnts | {id *، auth *، owner *، type *، to، text *} | إنشاء التعليق |
حذف / api / (goods | posts | المستخدمين | cmnts) / [id] | - | حذف إدخال بمعرف |
الحصول على / api / النشاط | - | تحديث آخر وقت قراءة للتعليقات الواردة للمستخدم الحالي |
get / api / (اشترك | تجاهل) / [علامة] | - | يضيف أو يزيل (إن وجد) مستخدم العلامة في قائمة الاشتراكات / |
البريد / api / upload / (البضائع | المستخدمون) | متعدد الأجزاء (الاسم ، الملف) | تحميل الإعلانات المصورة / صورة العضو |
* - الحقول المطلوبة
API - يتطلب إذن ، حانة - لا
مع طلب الحصول الذي لا يتطابق مع ما سبق ، يبحث الخادم عن ملف في الدليل للاستاتيك (على سبيل المثال ، / img / * - images ، /index.html - العميل).
أي نقطة api ستُرجع كود استجابة 200 إذا نجحت ، 400 أو 404 لخطأ ، ورسالة قصيرة إذا لزم الأمر.
حقوق الوصول بسيطة: إنشاء إدخال متاح للمستخدم المصرح به ، والتحرير للمؤلف والمشرف ، يمكن للمسؤول تحرير وتعيين المشرفين.
واجهة برمجة التطبيقات (API) مجهزة بأبسط عمليات مكافحة التخريب: يتم تسجيل الإجراءات مع معرف المستخدم وعنوان IP ، وفي حالة الوصول المتكرر ، يتم إرجاع خطأ يطلب منك الانتظار قليلاً (مفيد في مقابل تخمين كلمة المرور).
العملاء
يعجبني مفهوم الويب التفاعلي ، أعتقد أنه يجب إجراء معظم المواقع / التطبيقات الحديثة إما في إطار هذا المفهوم ، أو ثابت تمامًا. من ناحية أخرى ، فإن الموقع البسيط الذي يحتوي على ميغابايت من كود JS لا يمكن إلا أن يضغط. في رأيي ، هذه المشكلة (وليس فقط) يمكن حلها بواسطة Svelte. هذا الإطار (أو بالأحرى اللغة المستخدمة في إنشاء واجهات تفاعلية) ليس أدنى من Vue في الوظيفة الضرورية ، ولكن له ميزة لا يمكن إنكارها - يتم تجميع المكونات في vanilla JS ، مما يقلل من حجم الحزمة والحمل على الجهاز الظاهري (bundle.min.js.gz سوق البرغوث لدينا متواضع ، وفقا لمعايير اليوم ، 24KB). يمكن الاطلاع على التفاصيل في الوثائق الرسمية.
نختار سوق البرغوث SvelteJS لجانب العميل من سوق البرغوث ، ونتمنى كل الخير لريتش هاريس ومواصلة تطوير المشروع!
سكرتير خاص أنا لا أريد الإساءة إلى أي شخص. أنا متأكد من أن كل متخصص وكل مشروع له أدواته الخاصة.
العميل / البيانات
عنوان URL
نحن نستخدم للملاحة. لن نقوم بمحاكاة مستند متعدد الصفحات ؛ وبدلاً من ذلك ، نستخدم صفحات التجزئة ذات معلمات الاستعلام. بالنسبة للتحولات ، يمكنك استخدام <a> بدون js المعتاد.
تتوافق الأقسام مع أنواع البيانات: / # goods ، / # posts ، / # من المستخدمين .
المعلمات :؟ Id = record_id،؟ Page = page_number،؟ Search = search_query .
بعض الأمثلة:
- / # posts؟ id = 1542309643 & page = 999 & search = {auth: anon} - منشورات القسم ، معرف المشاركة - 1542309643 ، صفحة التعليق - 999 ، استعلام البحث - {auth: anon}
- / # goods؟ page = 2 & search = siddhartha - قسم البضائع ، قسم الصفحة - 2 ، استعلام البحث - siddhartha
- / # goods؟ search = wer {key: value} - قسم البضائع ، استعلام البحث - يتكون من البحث عن السلسلة الفرعية wert في رأس أو نص الإعلان وقيمة السلسلة الفرعية في الخاصية الرئيسية للجزء الجدولي من الإعلان
- / # goods؟ search = {model: 100، display: 256} - أعتقد أن كل شيء واضح هنا عن طريق القياس
تبدو وظائف التحليل وإنشاء عنوان URL في تطبيقنا كما يلي:
window.addEventListener('hashchange', function() { const hash = location.hash.slice(1).split('?'), result = {} if (!!hash[1]) hash[1].split('&').forEach(str => { str = str.split('=') if (!!str[0] && !!str[1]) result[decodeURI(str[0]).toLowerCase()] = decodeURI(str[1]).toLowerCase() }) result.type = hash[0] || 'goods' store.set({ hash: result }) }) function goto({ type, id, page, search }) { const { hash } = store.get(), args = arguments[0], query = [] new Array('id', 'page', 'search').forEach(key => { const value = args[key] !== undefined ? args[key] : hash[key] || null if (value !== null) query.push(key + '=' + value) }) location.hash = (type || hash.type || 'goods') + (!!query.length ? '?' + query.join('&') : '') }
API
لتبادل البيانات مع الخادم ، سنستخدم إحضار api. لتنزيل السجلات المحدّثة على فترات زمنية قصيرة ، نقوم بتقديم طلب إلى / pub / mtimes ، إذا كان وقت التغيير الأخير لأي نوع يختلف عن السجل المحلي ، فإننا نقوم بتحميل قائمة من هذا النوع. نعم ، كان من الممكن تنفيذ الإخطار بالتحديثات عبر SSE أو WebSockets والتحميل التدريجي ، لكن في هذه الحالة يمكننا الاستغناء عنه. ماذا حصلنا؟
async function GET(type) { const response = await fetch(location.origin + '/pub/' + type) .catch(() => ({ ok: false })) if (type === 'mtimes') store.set({ online: response.ok }) return response.ok ? await response.json() : [] } async function checkUpdate(type, mtimes, updates = {}) { const local = store.get()._mtimes, net = mtimes || await GET('mtimes') if (!net[type] || local[type] === net[type]) return const value = updates['_' + type] = await GET(type) local[type] = net[type]; updates._mtimes = local if (!!value && !!value.sort) store.set(updates) } async function checkUpdates() { setTimeout(() => checkUpdates(), 30000) const mtimes = await store.GET('mtimes') new Array('users', 'goods', 'posts', 'cmnts', 'files') .forEach(type => checkUpdate(type, mtimes)) }
من أجل التصفية والصفحات ، نستخدم الخصائص المحسوبة لـ Svelte ، استنادًا إلى بيانات التنقل. اتجاه القيم المحسوبة هو: العناصر (صفيفات السجلات الواردة من الخادم) => ignoredItems (السجلات التي تمت تصفيتها بناءً على قائمة التجاهل للمستخدم الحالي) => scribedItems (تصفية السجلات وفقًا لقائمة الاشتراكات ، إذا تم تنشيطها) => curItem و curItems (حساب السجلات الحالية بالاعتماد على القسم) => filteredItems (تصفية السجلات اعتمادًا على استعلام البحث ، إذا كان هناك سجل واحد فقط - تصفية التعليقات عليه) => maxPage (يحسب عدد الصفحات بناءً على 12 سجل / تعليقات لكل صفحة) => pagedItem (إرجاع الصفيف النهائي بـ المشاركات / التعليقات بناءً على رقم الصفحة الحالية).
يتم حساب التعليقات والصور ( التعليقات و _images ) بشكل منفصل ، مجمعة حسب النوع وسجل المالك.
تحدث العمليات الحسابية تلقائيًا وفقط عندما تتغير البيانات المرتبطة ، تكون البيانات الوسيطة دائمًا في الذاكرة. في هذا الصدد ، نتوصل إلى نتيجة غير سارة - بالنسبة إلى كمية كبيرة من المعلومات و / أو تحديثها المتكرر ، يمكن إنفاق قدر كبير من الموارد.
مخبأ
وفقًا لقرار إنشاء تطبيق دون اتصال بالإنترنت ، ننفذ تخزين السجلات وبعض جوانب الحالة في localStorage وملفات الصور في CacheStorage. العمل مع localStorage بسيط للغاية ، نوافق على أن الخصائص التي تحتوي على البادئة "_" يتم حفظها تلقائيًا واستعادتها عند إعادة التشغيل عند تغييرها. ثم قد يبدو حلنا كما يلي:
store.on('state', ({ changed, current }) => { Object.keys(changed).forEach(prop => { if (!prop.indexOf('_')) localStorage.setItem(prop, JSON.stringify(current[prop])) }) }) function loadState(state = {}) { for (let i = 0; i < localStorage.length; i++) { const prop = localStorage.key(i) const value = JSON.parse(localStorage.getItem(prop) || 'null') if (!!value && !prop.indexOf('_')) state[prop] = value } store.set(state) }
الملفات أكثر تعقيدًا قليلاً. بادئ ذي بدء ، سوف نستخدم قائمة بجميع الملفات ذات الصلة (مع وقت الإنشاء) القادمة من الخادم. عند تحديث هذه القائمة ، نقوم بمقارنتها بالقيم القديمة ، ونضع الملفات الجديدة في CacheStorage ، ونحذف الملفات القديمة منها:
async function cacheImages(newFiles) { const oldFiles = JSON.parse(localStorage.getItem('_files') || '[]') const cache = await caches.open('comico') oldFiles.forEach(file => { if (!~newFiles.indexOf(file)) { const [ id, type ] = file.split(':') cache.delete(`/img/${type}_${id}_sm.jpg`) }}) newFiles.forEach(file => { if (!~oldFiles.indexOf(file)) { const [ id, type ] = file.split(':'), src = `/img/${type}_${id}_sm.jpg` cache.add(new Request(src, { cache: 'no-cache' })) }}) }
بعد ذلك ، تحتاج إلى إعادة تعريف سلوك الجلب بحيث يتم أخذ الملف من CacheStorage دون الاتصال بالخادم. للقيام بذلك ، عليك استخدام ServiceWorker. في الوقت نفسه ، سنقوم بتكوين ملفات أخرى لتخزينها مؤقتًا للتشغيل خارج الخادم:
const CACHE = 'comico', FILES = [ '/', '/bundle.css', '/bundle.js' ] self.addEventListener('install', (e) => { e.waitUntil(caches.open(CACHE).then(cache => cache.addAll(FILES)) .then(() => self.skipWaiting())) }) self.addEventListener('fetch', (e) => { const r = e.request if (r.method !== 'GET' || !!~r.url.indexOf('/pub/') || !!~r.url.indexOf('/api/')) return if (!!~r.url.lastIndexOf('_sm.jpg') && e.request.cache !== 'no-cache') return e.respondWith(fromCache(r)) e.respondWith(toCache(r)) }) async function fromCache(request) { return await (await caches.open(CACHE)).match(request) || new Response(null, { status: 404 }) } async function toCache(request) { const response = await fetch(request).catch(() => fromCache(request)) if (!!response && response.ok) (await caches.open(CACHE)).put(request, response.clone()) return response }
يبدو خرقاء بعض الشيء ، لكنه يؤدي وظائفه.
العميل / واجهة
هيكل المكون:
index.html | main.js
== header.html - يحتوي على شعار ، شريط الحالة ، القائمة الرئيسية ، قائمة التنقل المنخفضة ، نموذج إرسال التعليقات
== aside.html - هو حاوية لجميع المكونات مشروط
==== goodForm.html - نموذج لإضافة إعلان وتحريره
==== userForm.html - تحرير نموذج المستخدم الحالي
====== tableForm.html - جزء من النموذج لإدخال بيانات جدولية
==== postForm.html - نموذج لنشر المنتدى
==== login.html - نموذج تسجيل الدخول / التسجيل
==== activity.html - يعرض التعليقات الموجهة إلى المستخدم الحالي
==== goodImage.html - عرض إعلانات الصور الرئيسية والإضافية
== main.html - حاوية للمحتوى الرئيسي
==== goods.html - قائمة أو بطاقات إعلان واحدة
==== users.html - نفس الشيء بالنسبة للمستخدمين
==== posts.html - أعتقد أنه واضح
==== cmnts.html - قائمة التعليقات على المشاركة الحالية
====== cmntsPager.html - ترقيم الصفحات للتعليقات
- في كل مكون ، نحاول تقليل عدد علامات html.
- نحن نستخدم الطبقات فقط كمؤشر على الحالة.
- نحن نأخذ وظائف مماثلة للمخزن (يمكن استخدام خصائص وطرق تخزين svelte مباشرة من المكونات عن طريق إضافة البادئة '$' إليها).
- تتوقع معظم الوظائف حدوث حدث للمستخدم أو تغيير بعض الخصائص ، ومعالجة بيانات الحالة ، وحفظ نتيجة عملهم مرة أخرى إلى الحالة والنهاية. وبالتالي ، يتم تحقيق تماسك صغير وقابلية للمد من الشفرة.
- من أجل السرعة الظاهرة للتحولات وأحداث واجهة المستخدم الأخرى ، نفصل ، قدر الإمكان ، عن التلاعب بالبيانات التي تحدث في الخلفية والإجراءات المرتبطة بالواجهة ، والتي بدورها تستخدم نتيجة الحساب الحالية ، وإعادة البناء إذا لزم الأمر ، سيتم تنفيذ الباقي بواسطة الإطار.
- يتم تخزين بيانات النموذج المراد ملؤه في localStorage لكل إدخال من أجل منع ضياعها.
- في جميع المكونات ، نستخدم الوضع الثابت الذي يتم فيه تغيير كائن الخاصية فقط عند تلقي رابط جديد ، بغض النظر عن التغيير في الحقول ، وبالتالي تسريع تطبيقاتنا قليلاً ، وإن كان ذلك بسبب زيادة طفيفة في حجم الكود.
العملاء / الإدارة
للتحكم باستخدام لوحة المفاتيح ، نستخدم المجموعات التالية:
Alt + s / Alt + a - تبديل صفحة السجلات للأمام / للخلف ، لسجل واحد بتبديل صفحة التعليقات.
Alt + w / Alt + q - ينتقل إلى السجل التالي / السابق (إن وجد) ، ويعمل في وضع القائمة وسجل واحد وعرض الصور
Alt + x / Alt + z - تمرير الصفحة لأسفل / لأعلى. في عرض الصورة ، يتم تبديل الصور للأمام / للخلف
Escape - يغلق نافذة الوسائط ، إذا كان مفتوحًا ، ويعود إلى القائمة ، إذا كان هناك إدخال واحد مفتوحًا ، يلغي استعلام البحث في وضع القائمة
Alt + c - يركز على حقل البحث أو التعليق ، اعتمادًا على الوضع الحالي
Alt + v - تمكين / تعطيل وضع عرض الصور لإعلان واحد
Alt + r - يفتح / يغلق قائمة التعليقات الواردة للمستخدم المصرح به
Alt + t - تبديل الضوء / الظلام المظاهر
Alt + g - قائمة الإعلانات
البديل + ش - المستخدمين
البديل + ع - المنتدى
أعلم أنه في العديد من المتصفحات ، يتم استخدام هذه المجموعات بواسطة المتصفح نفسه ، لكن بالنسبة إلى chrome الخاص بي ، لم أتمكن من التوصل إلى شيء أكثر ملاءمة. سأكون سعيدا لاقتراحاتكم.
بالإضافة إلى لوحة المفاتيح ، يمكنك بالطبع استخدام وحدة التحكم في المتصفح. على سبيل المثال ، store.goBack () ، store.nextPage () ، store.prevPage () ، store.nextItem () ، store.prevItem () ، store.search (stringValue) ، store.checkUpdate ('goods' || ' المستخدمين '||' منشورات '||' ملفات '||' cmnts ') - افعل ما يوحي الاسم ؛ store.get (). comments and store.get () ._ images - إرجاع الملفات المجمعة والتعليقات ؛ store.get (). ignoredItems و store.get (). scribedItems هي قوائم بالسجلات التي تتجاهلها وتتتبعها . تتوفر قائمة كاملة بجميع البيانات الوسيطة والمحسوبة من store.get () . لا أعتقد أن أي شخص قد يحتاج إلى هذا على محمل الجد ، ولكن ، على سبيل المثال ، يبدو أن تصفية السجلات بحسب المستخدم وحذفها تبدو مريحة جدًا من وحدة التحكم.
الخاتمة
هذا هو المكان الذي يمكنك فيه إنهاء معرفتك بالمشروع ؛ يمكنك العثور على مزيد من التفاصيل في التعليمات البرمجية المصدر. ونتيجة لذلك ، حصلنا على تطبيق سريع وصغير إلى حد ما ، في معظم أجهزة التحقق ، الأمان ، والسرعة ، والتوافر ، وما إلى ذلك ، لعبة الداما ، فإنه يظهر نتائج جيدة دون تحسين مستهدف.
أود أن أعرف رأي المجتمع إلى أي مدى يتم تبرير الأساليب المتبعة في تنظيم التطبيقات المستخدمة في النموذج الأولي ، وما هي المخاطر التي يمكن أن تكون ، ما الذي يمكنك تنفيذه بطريقة مختلفة اختلافًا جذريًا؟
شفرة المصدر ، تعليمات التثبيت عينة والعرض هنا (يرجى تخريب لاختبار في إطار القانون الجنائي).
بوستسكريبت. قليلا التجارية في الختام. أخبرني ، في هذا المستوى ، هل من الممكن حقًا البدء في البرمجة مقابل المال؟ إذا لم يكن الأمر كذلك ، فما الذي تبحث عنه أولاً ، إذا كان الأمر كذلك ، أخبرني أين يبحثون عن عمل مثير للاهتمام على كومة مماثلة الآن. شكرا لك
بوستسكريبت. أكثر قليلاً عن المال والعمل. كيف تحب هذه الفكرة: لنفترض أن الشخص مستعد للعمل في مشروع مثير للاهتمام بالنسبة له مقابل أي راتب ، ومع ذلك ، فإن البيانات المتعلقة بالمهام ودفعها ستكون متاحة للجمهور (توافر ورمز لتقييم جودة الأداء مرغوب فيه) ، إذا كان الدفع أقل بكثير من السوق ، فإن منافسي صاحب العمل يمكن أن تقدم الكثير من المال لأداء مهامهم ، إذا كان أعلى - سيتمكن العديد من الفنانين من تقديم خدماتهم بسعر أقل. هل سيؤدي مثل هذا المخطط في بعض المواقف إلى تحقيق توازن أفضل وعادل في السوق (تقنية المعلومات)؟