عينة بسيطة من الملاحظات SPA في Mithril.js

Mithril.js هي أداة غير شعبية لبناء تطبيقات الويب العميل. لا توجد منشورات حول حبري عمليا حول هذا الموضوع. في هذا المنشور ، أريد أن أبين كيف يمكنك تقديم طلب صغير على Mithril. سوف يستند التطبيق على هذا المنشور ( الترجمة )

ما هو Mihtril


Mithril هو إطار تفاعلي مصمم لإنشاء SPA (تطبيقات ويب أحادية الصفحة). الكل في الكل ، إنه فقط جافا سكريبت و 13 توقيعات على وظائف API. بالإضافة إلى ذلك ، هناك مكتبة دفق ميثريل غير مدرجة في ميثريل وتستخدم بشكل منفصل. يتضمن جوهر mithril توجيه التطبيق والعمل مع طلبات XHR. المفهوم المركزي هو التجريد - عقدة افتراضية (vnode). العقدة الافتراضية هي مجرد كائن js مع مجموعة من السمات. يتم إنشاء العقد الافتراضية بواسطة الوظيفة الخاصة m (). يتم تخزين الحالة الحالية للواجهة في قائمة العقد الافتراضية (DOM الافتراضي). عند العرض الأولي لصفحة التطبيق ، تتم ترجمة DOM الافتراضي إلى DOM. عندما يبدأ معالج أحداث DOM API ، وعند اكتمال الوعد m.request () ، وعندما يتغير عنوان URL (التنقل على طول مسارات التطبيق) ، يتم إنشاء مصفوفة DOM افتراضية جديدة ،مقارنة مع القديم ، وتغير العقد المتغيرة متصفح DOM. بالإضافة إلى أحداث DOM واستكمال طلب m.request () ، يمكن استدعاء إعادة الرسم يدويًا باستخدام وظيفة m.redraw ().

لا توجد قوالب شبيهة بـ HTML في mithril خارج الصندوق ، لا يوجد دعم JSX ، على الرغم من أنه يمكنك استخدام كل هذا مع مكونات إضافية مختلفة للبناء إذا كنت ترغب في ذلك. لن أستغل هذه الفرص هنا.

إذا كانت الوسيطة الأولى لـ m () عبارة عن سلسلة ، (على سبيل المثال ، "div") ، فستقوم الدالة بإرجاع عقدة افتراضية بسيطة ونتيجة لذلك ، سيتم عرض علامة HTML في DOM

<div></div>

إذا كانت الوسيطة الأولى لـ m () هي الكائن أو الدالة التي ترجع الكائن ، فيجب أن يكون لهذا الكائن طريقة view () ، ويسمى هذا الكائن بمكون. يجب أن تُرجع طريقة view () للمكون بدورها دائمًا وظيفة m () (أو صفيفًا من النوع: [m ()،]). وبالتالي ، يمكننا بناء تسلسل هرمي للكائنات المكونة. ومن الواضح أن جميع المكونات تُرجع في النهاية عُقد vnode البسيطة.

لكل من العقد والمكونات الافتراضية طرق دورة الحياة ، ويطلق عليها نفس oninit () ، oncreate () ، onbeforeupdate () ، إلخ. يتم استدعاء كل من هذه الأساليب في وقت محدد للغاية في عرض الصفحة.

يمكنك تمرير المعلمات إلى العقدة أو المكون الظاهري ككائن ، والذي يجب أن يكون الوسيطة الثانية للدالة m (). يمكنك الحصول على ارتباط لهذا الكائن داخل العقدة باستخدام تدوين vnode.attrs. الحجة الثالثة للدالة m () هي أحفاد هذه العقدة ويمكن الوصول إليها عبر رابط vnode.children. بالإضافة إلى الدالة m () ، يتم إرجاع العقد البسيطة بواسطة الدالة m.trust ().

لا يقدم مؤلف mithril أي أنماط تصميم تطبيقات خاصة ، على الرغم من أنه ينصح بتجنب بعض القرارات غير الناجحة ، على سبيل المثال ، المكونات "السميكة جدًا" أو التلاعب في أحفاد شجرة المكونات. لا يقدم المؤلف أيضًا طرقًا أو طرقًا خاصة للتحكم في حالة التطبيق ككل أو مكونات. على الرغم من أن الوثائق تنص على أنه لا يجب استخدام حالة العقدة نفسها ، تعامل معها.

تبدو جميع ميزات mithril هذه غير ملائمة للغاية ، ويبدو أن الإطار غير مكتمل ، ولا توجد توصيات خاصة ، ولا توجد حالة / تخزين ، ولا يوجد مُخفض / مرسل الحدث ، ولا قوالب. بشكل عام ، افعل ما تستطيع.

ما تحتاجه كمثال


سوف نستخدم:


خادم الواجهة الأمامية ليس مهمًا هنا ، يجب عليه فقط إعطاء العميل index.html وملفات البرامج النصية والأنماط.

لن نقوم بتثبيت mithril في node_modules ، وربط كود التطبيق والإطار في ملف واحد. سيتم تحميل رمز التطبيق وميثريل إلى الصفحة بشكل منفصل.

لن أصف إجراء التثبيت للأدوات ، على الرغم من أنني أستطيع أن أقول عن postgREST ، ما عليك سوى تنزيل الملف الثنائي ، ووضعه في مجلد منفصل ، وإنشاء ملف تكوين test.conf مثل هذا:

db-uri = "postgres://postgres:user1@localhost:5432/testbase"
server-port= 5000
# The name of which database schema to expose to REST clients
db-schema= "public"
# The database role to use when no client authentication is provided.
# Can (and probably should) differ from user in db-uri
db-anon-role = "postgres" 

في نفس الوقت ، يجب أن تحتوي مجموعة postgesql على قاعدة اختبار و user1. في قاعدة الاختبار هذه ، قم بإنشاء جدول:

-- Postgrest sql notes table 
create table if not exists notes (
id serial primary key,
title varchar(127) NOT NULL,
content text NOT NULL,
created timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed timestamp with time zone,
ddel smallint default 0
)

يتم بدء تشغيل خادم postgREST باستخدام الأمر:

postgrest test.conf

بعد بدء تشغيل الخادم ، يعرض رسائل إخبارية حول الاتصال بقاعدة البيانات ، وعلى أي منفذ سيستمع للعملاء.

التخطيط لمشروع


لذا ، إذا كان من المفهوم تقريبًا كيفية عمل الميتريل ، فأنت بحاجة إلى معرفة كيفية إنشاء التطبيق. ها هي الخطة:

  1. سنقوم بتخزين بيانات التطبيق في كائن محلي ، دعنا نسمي نموذجها
  2. سيتم تخزين تطبيقات API في ملف منفصل
  3. سنقوم بتخزين مسارات التطبيق في ملف منفصل
  4. سيكون ملف المسار هو نقطة الدخول لإنشاء التطبيق.
  5. يتم تخزين كل مكون منفصل (وسأستخدم نظام عرض المكون) والوظائف المرتبطة به في ملف منفصل
  6. سيكون لكل مكون يعرض بيانات النموذج حق الوصول إلى النموذج.
  7. تتم ترجمة معالجات أحداث DOM المكون في المكون

وبالتالي ، لا نحتاج إلى أحداث مخصصة ، أو ما يكفي من أحداث DOM الأصلية ، تتم إعادة الاسترجاعات لهذه الأحداث في المكونات. سأستخدم ملزمة ذات اتجاهين. ربما لا يحب الجميع هذا النهج ، ولكن لا يحب الجميع redux أو vuex. علاوة على ذلك ، يمكن أيضًا تنفيذ تقنية التجليد أحادية الاتجاه بشكل أنيق في الميتريل باستخدام ميثريل-سيرام. لكن في هذه الحالة ، هذا حل زائد.

هيكل مجلد التطبيق




سيتم تقديم المجلد العام من قبل الخادم الأمامي ، وهناك ملف index.html ، وأدلة مع الأنماط والنصوص.

يحتوي المجلد src على جذر الموجه وتعريف واجهة برمجة التطبيقات ، ودليلين للنموذج وطرق العرض.

يوجد في جذر المشروع ملف تكوين rollup.config ، ويتم إنشاء المشروع باستخدام الأمر:

rollup –c

من أجل عدم حمل القارئ بالقطع الطويلة من التعليمات البرمجية المتاحة على github.com ، سأعلق فقط على عناصر التنفيذ الرئيسية لإظهار نهج اصطلاحي للتاجي.

API والموجه


كود API:

// used by backend server
export const restApi= {
  notes: { url: 'notes', editable: ['add', 'edit', 'del'] }
}

// used by routers
export const appApi = {
  root: "/",
}
// used by navBar
// here array of arrays though it may be hash eg
export const appMenu = [
  [`${appApi.root}`, 'Home'],
  [`${appApi.root}`, 'About'],
  [`${appApi.root}`, 'Contacts'],
]

قمت بتعريف API لخادم REST وجهاز التوجيه.

جهاز التوجيه:

import { restApi, appApi } from './appApi';
import { moModel } from './model/moModel';
import { vuView, vuApp } from './view/vuApp';
import { vuNavBar } from './view/vuNavBar';

// application router
const appRouter = { [appApi.root]: {
  render() {
    const view = m(vuApp, {
      model: moModel.getModel( restApi.notes ),
    });
    return vuView( {menu: vuNavBar}, view);
  }
}};

// once per app
m.route(document.body, "/", appRouter);

هنا يتم تركيب جهاز التوجيه على نص المستند. جهاز التوجيه نفسه عبارة عن كائن يصف المسارات والمكونات التي سيتم استخدامها في هذا المسار ، يجب أن تُرجع الدالة () تقديم vnode.

يعرّف كائن appApi جميع مسارات التطبيق الصالحة ، ويحدد كائن appMenu جميع عناصر التنقل الممكنة للتطبيق.

تقوم وظيفة التجسيد ، عند استدعاؤها ، بإنشاء نموذج تطبيق وتمريره إلى العقدة الجذرية.

نموذج التطبيق


الهيكل الذي يخزن البيانات ذات الصلة ، دعوت النموذج. كود المصدر للدالة التي ترجع النموذج:

getModel(
    {url=null, method="GET", order_by='id', editable=null} = {}
  ) {
/**
  * url - string of model's REST API url
  * method - string of model's REST method
  * order_by - string "order by" with initially SELECT 
  * editable - array defines is model could changed
*/
    const model = {
      url: url,
      method: method,
      order_by: order_by,
      editable: editable,
      key: order_by, // here single primary key only
      list: null, // main data list 
      item: {}, // note item
      error: null, // Promise error
      save: null, // save status
      editMode: false, // view switch flag
      word: '' // dialog header word
    };  
    model.getItem= id => {
      model.item= {};
      if ( !id ) {
        model.editMode= true;
        return false;
      }
      const key= model.key;
      for ( let it of model.list ) {
        if (it[key] == id) {
          model.item= Object.assign({}, it);
          break;
        }
      }
      return false;
    };
    return model;
  },

هنا تتم تهيئته ، وإرجاع كائن النموذج. قد تتغير الارتباطات الموجودة داخل الكائن ، ولكن يظل الارتباط بالكائن نفسه ثابتًا.

بالإضافة إلى وظيفة getModel ، فإن كائن moModel العام يحتوي على وظائف مجمّعة لوظيفة m.request () ، وهذه هي getList (نموذج) ، و formSubmit (حدث ، نموذج ، طريقة). إن معلمة النموذج هي في الواقع مرجع إلى كائن النموذج ، والحدث هو كائن الحدث الذي يتم إنشاؤه عند إرسال النموذج ، والطريقة هي طريقة HTTP التي نريد من خلالها حفظ الملاحظة (POST هي ملاحظة جديدة ، PATCH ، DELETE قديمة).

التمثيل


يحتوي مجلد العرض على وظائف مسؤولة عن عرض عناصر الصفحة الفردية. قسمتها إلى 4 أجزاء:

  • vuApp - المكون الأساسي للتطبيق ،
  • vuNavBar - شريط التنقل ،
  • vuNotes - قائمة الملاحظات ،
  • vuNoteForm - نموذج تحرير الملاحظات ،
  • vuDialog - عنصر حوار HTML

يحدد جهاز التوجيه أن vuView (القائمة ، العرض) يتم إرجاعه في مسار واحد.

تعريف هذه الوظيفة:

export const vuView= (appMenu, view)=> m(vuMain, appMenu, view);

هذا مجرد غلاف يقوم بإرجاع مكون vuMain ، إذا كان كائن appMenu معقدًا بما يكفي ليكون له كائنات متداخلة في هيكل مشابه لكائن خارجي ، فإن هذا الغلاف هو طريقة مناسبة لإرجاع مكون بعناصر تنقل ومكونات فرعية مختلفة (ما عليك سوى كتابة رمز أقل) .

مكون VuMain:

const vuMain= function(ivnode) {
  // We use ivnode as argument as it is initial vnode
  const { menu }= ivnode.attrs;
  return { view(vnode) {
    // IMPORTANT !
    // If we use vnode inside the view function we MUST provide it for view
    return [
      m(menu),
      m('#layout', vnode.children)
    ];
  }};
};

هذا ببساطة إرجاع vnodes التنقل والمحتوى الفعلي للصفحة.

فيما يلي ، عندما يكون من الممكن تحديد مكون ، سأستخدم الإغلاق. يتم استدعاء عمليات الإغلاق مرة واحدة أثناء العرض الأولي للصفحة ، وتخزين جميع الروابط التي تم تمريرها إلى كائنات وتعريفات لوظائفها محليًا.

يجب أن يؤدي الإغلاق كتعريف دائمًا إلى إرجاع أحد المكونات.

وفي الواقع مكون محتوى التطبيق:

export const vuApp= function(ivnode) {
  const { model }= ivnode.attrs;
  //initially get notes
  moModel.getList( model );
  
  return { view() { 
    return [
      m(vuNotes, { model }),
      m(vuNoteForm, { model }),
      vuModalDialog(model)
    ];
  }};
}

نظرًا لأن لدينا بالفعل نموذجًا ، عندما أسمي الإغلاق ، أريد الحصول على قائمة كاملة بالملاحظات من قاعدة البيانات. ستكون هناك ثلاثة مكونات في الصفحة:

  • vuNotes - قائمة الملاحظات مع زر الإضافة ،
  • vuNoteForm - نموذج تحرير الملاحظات ،
  • vuModalDialog - عنصر حوار سنعرضه بشكل مشروط ، وننتقد عند الضرورة.

نظرًا لأن كل مكون من هذه المكونات يحتاج إلى معرفة كيفية رسم نفسه ، فإننا نقوم بتمرير ارتباط إلى كائن النموذج في كل عنصر.

قائمة ملاحظات المكونات:

//Notes List
export const vuNotes= function(ivnode) {
  const { model }= ivnode.attrs;
  const _display= ()=> model.editMode ? 'display:none': 'display:block';
  const vuNote= noteItem(model); // returns note function
  
  return { view() {
    return model.error ? m('h2', {style: 'color:red'}, model.error) :
    !model.list ? m('h1', '...LOADING' ) :
    m('div', { style: _display() }, [
      m(addButton , { model } ),
      m('.pure-g', m('.pure-u-1-2.pure-u-md-1-1',
        m('.notes', model.list.map( vuNote ) )
      ))
    ]);
  }};
}

يتم تخزين إشارة bool editMode في كائن النموذج ؛ إذا كانت قيمة العلامة صحيحة ، فإننا نعرض نموذج التحرير ، وإلا - قائمة بالملاحظات. يمكنك رفع الاختبار إلى مستوى أعلى ، ولكن بعد ذلك يتغير عدد العقد الافتراضية وعقد DOM الفعلية في كل مرة يتم فيها تبديل العلم ، وهذا عمل غير ضروري.

هنا نحن اصطلاحيون بالميتريل ، نقوم بإنشاء صفحة ، والتحقق من وجود أو عدم وجود سمات في النموذج باستخدام عوامل التشغيل الثلاثية.

فيما يلي الإغلاق الذي يُرجع وظيفة عرض الملاحظات:

const noteItem= model=> {
  // click event handler
  const event= ( msg, word='', time=null)=> e=> {
    model.getItem(e.target.getAttribute('data'));
    if ( !!msg ) {
      model.save= { err: false, msg: msg };
      model.word= word;
      if ( !!time )
        model.item.completed= time;
      vuDialog.open();
    } else {
      model.editMode= true;
    }
  };
  // trash icon's click handler
  const _trash= event('trash', 'Dlelete');
  
  // check icon's click handler
  const _check= event('check', 'Complete',
    // postgre timestamp string
    new Date().toISOString().split('.')[0].replace('T', ' '));
  
  // edit this note
  const _edit= event('');
  
  const _time= ts=> ts.split('.')[0];
  
  // Single Note 
  const _note= note=> m('section.note', {key: note.id}, [
    m('header.note-header', [ m('p.note-meta', [
      // note metadata
      m('span', { style: 'padding: right: 3em' }, `Created: ${_time( note.created )}`),
      note.completed ? m('span', `  Completed: ${_time( note.completed )}`) : '', 
      // note right panel 
      m('a.note-pan', m('i.fas.fa-trash', { data: note.id, onclick: _trash } )),
      note.completed ? '' : [
        m('a.note-pan', m('i.fas.fa-pen', {data: note.id, onclick: _edit } )),
        m('a.note-pan', m('i.fas.fa-check', { data: note.id, onclick: _check} ))
      ]
    ]),
      m('h2.note-title', { style: note.completed ? 'text-decoration: line-through': ''}, note.title)
    ]),
    m('.note-content', m('p', note.content))
  ]);
  return _note;
}

يتم تحديد كافة معالجات النقرات محليًا. لا يعرف النموذج كيف يتم تنظيم كائن الملاحظة ، لقد قمت فقط بتعريف الخاصية الرئيسية ، والتي يمكننا من خلالها تحديد العنصر المطلوب من صفيف model.list. ومع ذلك ، يجب أن يعرف المكون بالضبط كيفية هيكلة الكائن الذي يرسمه.

لن أعطي النص الكامل للرمز الخاص بالنموذج لتحرير الملاحظة ؛ نحن فقط ننظر إلى معالج إرسال النموذج بشكل منفصل:

// form submit handler
  const _submit= e=> {
    e.preventDefault();
    model.item.title= clean(model.item.title);
    model.item.content= clean(model.item.content);
    const check= check_note(model.item);
    if ( !!check ) {
      model.save= { err: true, msg: check }; 
      model.word= 'Edit';
      vuDialog.open();
      return false;
    } 
    return moModel.formSubmit(e, model, _method() ).then(
      ()=> { model.editMode=false; return true;}).catch(
      ()=> { vuDialog.open(); return false; } );
  };

نظرًا لأن التعريف يحدث في الإغلاق ، فلدينا رابط للنموذج ، ونعود بوعد مع المعالجة اللاحقة للنتيجة: حظر عرض النموذج عند اكتمال الطلب بشكل طبيعي ، أو في حالة حدوث خطأ - فتح مربع حوار بنص الخطأ.

في كل مرة تقوم فيها بالوصول إلى خادم الواجهة الخلفية ، يتم إعادة قراءة قائمة الملاحظات. في هذا المثال ، ليست هناك حاجة لضبط القائمة في الذاكرة ، على الرغم من أنه يمكن القيام بذلك.

يمكن عرض مكون الحوار في المستودع ، الشيء الوحيد الذي يجب التأكيد عليه هو أنني في هذه الحالة استخدمت كائنًا حرفيًا لتعريف المكون ، لأنني أريد إتاحة وظائف فتح وإغلاق النافذة للمكونات الأخرى.

استنتاج


كتبنا تطبيق SPA صغير في جافا سكريبت و mithril.js ، محاولين التمسك بعبارات هذا الإطار. أريد الانتباه مرة أخرى إلى أن هذا مجرد رمز جافا سكريبت. ربما ليست نظيفة تماما. يسمح لك النهج بتغليف أجزاء صغيرة من التعليمات البرمجية ، وعزل آليات المكون ، واستخدام حالة مشتركة لجميع المكونات.

All Articles