Marko.js - الواجهة الأمامية من موقع ebay.com

Marko.js ليس شائعًا مثل Angular أو React.js أو Vue.js أو Svelte. Marko.js هو مشروع ebay.com أصبح ملكية مفتوحة المصدر منذ عام 2015. في الواقع ، تم إنشاء الواجهة الأمامية لموقع ebay.com في هذه المكتبة ، مما يسمح لنا باستخلاص نتيجة حول قيمتها العملية للمطورين.



يشبه الوضع مع Marko.js إلى حدٍ ما الموقف مع إطار عمل Ember.js ، والذي ، على الرغم من حقيقة أنه يعمل كواجهة أمامية للعديد من المواقع المحملة بشكل كبير (على سبيل المثال ، Linkedin) ، لا يعرف الكثير عن المطور العادي. في حالة Marko.js ، يمكن القول إنه لا يعرف على الإطلاق.



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



إطار عمل SSR الأول



يمكن أن يكون Marko.js أساسًا لواجهة أمامية كلاسيكية (مع عرض من جانب الخادم) ، لتطبيق أحادي الصفحة (مع عرض من جانب العميل) ، ولتطبيق متماثل / عالمي (سيتم مناقشة مثال على ذلك لاحقًا). ولكن لا يزال ، يمكن اعتبار Marko.js مكتبة SSR الأولى ، أي أنها تركز بشكل أساسي على عرض الخادم. ما يميز Marko.js عن أطر عمل المكونات الأخرى هو أن مكون جانب الخادم لا يقوم ببناء DOM ، والذي يتم بعد ذلك تحويله إلى سلسلة ، ولكن يتم تنفيذه كتدفق إخراج. لتوضيح ما يدور حوله هذا الأمر ، سأقدم قائمة بمكون خادم بسيط:



// Compiled using marko@4.23.9 - DO NOT EDIT
"use strict";

var marko_template = module.exports = require("marko/src/html").t(__filename),
    marko_componentType = "/marko-lasso-ex$1.0.0/src/components/notFound/index.marko",
    marko_renderer = require("marko/src/runtime/components/renderer");

function render(input, out, __component, component, state) {
  var data = input;

  out.w("<p>Not found</p>");
}

marko_template._ = marko_renderer(render, {
    ___implicit: true,
    ___type: marko_componentType
  });

marko_template.meta = {
    id: "/marko-lasso-ex$1.0.0/src/components/notFound/index.marko"
  };


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



لماذا هو مهم



كشف التقدم في إنشاء تطبيقات الصفحة الواحدة ، والذي لوحظ مع الاستخدام الواسع النطاق لـ Angular و React.js و Vue.js ، إلى جانب اللحظات الإيجابية ، عن العديد من الأخطاء الفادحة في البنية الموجهة للعميل. في عام 2013 ، نشر سبايك بريهم من Airbnb مقالة برمجية بعنوان "ذبابة في المرهم". في الوقت نفسه ، أصابت جميع النقاط السلبية النشاط التجاري:



  • يزيد وقت تحميل الصفحة الأولى ؛
  • لا تتم فهرسة المحتوى بواسطة محركات البحث ؛
  • مشاكل الوصول للأشخاص ذوي الإعاقة.


كبديل ، تم أخيرًا إنشاء أطر تطوير تطبيقات متشابهة / عالمية: Next.js و Nust.js. ثم يأتي دور عامل آخر - الأداء. يعلم الجميع أن node.js ليس جيدًا عند تحميله بحسابات معقدة. وفي حالة إنشاء DOM على الخادم ، ثم بدء تسلسله ، يتلاشى node.js بسرعة كبيرة. نعم ، يمكننا رفع عدد لا نهائي من النسخ المتماثلة لـ node.js. ولكن ربما تحاول أن تفعل الشيء نفسه ولكن في Marko.js؟



كيف تبدأ مع Marko.js



بالنسبة للمعارف الأولى ، أوصي بالبدء كما هو موضح في الوثائق بالأمر npx @marko/create --template lasso-express.



نتيجة لذلك ، سنحصل على أساس لمزيد من التطوير للمشاريع باستخدام خادم Express.js مهيأ ورابط Lasso (تم تطوير هذا الرابط بواسطة ebay.com وهو الأسهل للتكامل معه).



عادةً ما توجد المكونات في Marko.js في أدلة المكونات / في الملفات ذات الامتداد .marko. رمز المكون بديهي. كما تقول الوثائق ، إذا كنت تعرف لغة html ، فأنت تعرف Marko.js.



يتم تقديم المكون على الخادم ثم ترطيبه على العميل. أي أننا لا نتلقى على العميل لغة html ثابتة ، بل مكون عميل كامل مع الحالة والأحداث.



عند بدء مشروع في وضع التطوير ، تعمل إعادة التحميل على الساخن.



لبناء تطبيق معقد ، نحتاج على الأرجح إلى شيء آخر إلى جانب مكتبة المكونات ، على سبيل المثال ، التوجيه ، والتخزين ، وإطار عمل لإنشاء تطبيقات متشابهة / عالمية. وهنا ، للأسف ، المشكلات هي نفسها التي واجهها مطورو React.js في السنوات الأولى - لا توجد حلول جاهزة ومقاربات معروفة. لذلك ، يمكن تسمية كل شيء وصل إلى هذه النقطة بمقدمة للمحادثة حول إنشاء تطبيق بناءً على Marko.js.



بناء تطبيق متماثل / عالمي



كما قلت ، لا يوجد العديد من المقالات حول Marko.js ، لذا فكل شيء أدناه هو ثمرة تجاربي ، والتي تعتمد جزئيًا على العمل مع أطر أخرى.



يسمح لك Marko.js بتعيين وتغيير اسم العلامة أو المكون ديناميكيًا (أي برمجيًا) - وهذا ما سنستخدمه. دعونا نطابق المسارات - أسماء المكونات. نظرًا لعدم وجود توجيه خارج منطقة الجزاء في Marko.js (من المثير للاهتمام معرفة كيفية إنشاء ذلك على موقع ebay.com) ، سنستخدم الحزمة ، والتي هي مخصصة فقط لمثل هذه الحالات - جهاز التوجيه العالمي:



const axios = require('axios');
const UniversalRouter = require('universal-router');

module.exports = new UniversalRouter([
  { path: '/home', action: (req) => ({ page: 'home' }) },
  {
    path: '/user-list',
    action: async (req) => {
      const {data: users} = await axios.get('http://localhost:8080/api/users');
      return { page: 'user-list', data: { users } };
    }
  },
  {
    path: '/users/:id',
    action: async (req) => {
      const {data: user} = await axios.get(`http://localhost:8080/api/users/${req.params.id}`);
      return { page: 'user', data: { req, user } };
    }
  },
  { path: '(.*)', action: () => ({ page: 'notFound' }) }
])


وظيفة حزمة جهاز التوجيه العالمي بسيطة للغاية. يقوم بتحليل سلسلة عنوان url ، واستدعاء إجراء الوظيفة غير المتزامن (req) مع السلسلة التي تم تحليلها ، حيث يمكننا ، على سبيل المثال ، الوصول إلى معلمات السلسلة المحللة (req.params.id). ونظرًا لأن الوظيفة الإجراء (طلب) تسمى بشكل غير متزامن ، يمكننا تهيئة البيانات بطلبات واجهة برمجة التطبيقات هنا.



كما تتذكر ، في القسم الأخير ، تم إنشاء مشروع بواسطة فريق npx @marko/create --template lasso-express. لنأخذها كأساس لتطبيقنا المتشابه / الشامل. للقيام بذلك ، دعنا نغير ملف server.js قليلاً



app.get('/*', async function(req, res) {
    const { page, data } = await router.resolve(req.originalUrl);
    res.marko(indexTemplate, {
            page,
            data,
        });
});


سنقوم أيضًا بتغيير قالب الصفحة المحملة:



<lasso-page/>
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <title>Marko | Lasso + Express</title>
    <lasso-head/>
    <style>
      .container{
        margin-left: auto;
        margin-right: auto;
        width: 800px;
    }
    </style>
  </head>
  <body>
    <sample-header title="Lasso + Express"/>
    <div class="container">
      <router page=input.page data=input.data/>
    </div>
    <lasso-body/>
    <!--
    Page will automatically refresh any time a template is modified
    if launched using the browser-refresh Node.js process launcher:
    https://github.com/patrick-steele-idem/browser-refresh
    -->
    <browser-refresh/>
  </body>
</html>


المكون <router /> هو بالضبط الجزء الذي سيكون مسؤولاً عن تحميل المكونات الديناميكية ، التي نحصل عليها من الموجه في سمة الصفحة.



<layout page=input.page>
  <${state.component} data=state.data/>
</layout>

import history from '../../history'
import router from '../../router'

class {
  onCreate({ page, data }) {
    this.state = {
      component: require(`../${page}/index.marko`),
      data
    }
    history.listen(this.handle.bind(this))
  }

  async handle({location}) {
    const route = await router.resolve(location);
    this.state.data = route.data;
    this.state.component = require(`../${route.page}/index.marko`);
  }
}


تقليديًا ، يمتلك Marko.js this.state ، والذي يؤدي إلى تغيير وجهة نظر المكون ، وهو ما نستخدمه.



عليك أيضًا تنفيذ العمل مع التاريخ بنفسك:



const { createBrowserHistory } = require('history')
const parse = require('url-parse')
const deepEqual = require('deep-equal')
const isNode = new Function('try {return !!process.env;}catch(e){return false;}') //eslint-disable-line
let history

if (!isNode()) {
  history = createBrowserHistory()
  history.navigate = function (path, state) {
    const parsedPath = parse(path)
    const location = history.location
    if (parsedPath.pathname === location.pathname &&
      parsedPath.query === location.search &&
      parsedPath.hash === location.hash &&
      deepEqual(state, location.state)) {
      return
    }
    const args = Array.from(arguments)
    args.splice(0, 2)
    return history.push(...[path, state, ...args])
  }
} else {
  history = {}
  history.navigate = function () {}
  history.listen = function () {}
}

module.exports = history


وأخيرًا ، يجب أن يكون هناك مصدر تنقل يعترض أحداث النقر على الرابط ويستدعي التنقل على الصفحة:



import history from '../../history'

<a on-click("handleClick") href=input.href><${input.renderBody}/></a>

class {
  handleClick(e) {
    e.preventDefault()
    history.navigate(this.input.href)
  }
}


لتسهيل دراسة المادة ، قدمت النتائج في المستودع .



apapacy@gmail.com

22 نوفمبر 2020



All Articles