واجهة Odnoklassniki جديدة: إطلاق رد فعل في Java. الجزء الثاني



نواصل قصة كيف ، داخل Odnoklassniki ، بمساعدة GraalVM ، تمكنا من تكوين صداقات مع Java و JavaScript وبدء الهجرة إلى نظام ضخم مع الكثير من التعليمات البرمجية القديمة.

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

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

تكوين واجهة المستخدم


لكتابة رمز واجهة المستخدم ، اخترنا الأدوات الأكثر تقدمًا: تفاعل مع MobX و CSS Modules و ESLint و TypeScript و Lerna. يتم جمع كل هذا باستخدام Webpack.



بنية التطبيق


كما هو مكتوب في الجزء السابق من هذه المقالة ، من أجل تنفيذ الترحيل التدريجي ، سنقوم بإدراج مكونات جديدة على الموقع في عناصر DOM بأسماء مخصصة ستعمل داخل حزمة واجهة المستخدم الجديدة ، بينما ستبدو لبقية الموقع كعنصر DOM مع API الخاص به. يمكن تقديم محتويات هذه العناصر على الخادم.

ما هذا؟ يوجد في الداخل تطبيق MVC رائع وعصري يعمل على React ويوفر واجهة DOM API الخارجية: السمات والأساليب في عنصر DOM هذا والأحداث.



لتشغيل هذه المكونات ، قمنا بتطوير آلية خاصة. ماذا يفعل؟ أولاً ، يقوم بتهيئة التطبيق وفقًا لوصفه. ثانيًا ، يربط المكون بالعقدة DOM المحددة التي يبدأ فيها. هناك أيضًا محركان (للعميل والخادم) يمكنهما العثور على هذه المكونات وتقديمها.



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

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

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

{
    "events-calendar": {
        "bundleName": "events-calendar",
        "js": "events-calendar-h4h5m.js",
        "css": "events-calendar-h4h5m.css"
    }
}


تتم إضافة علامات خاصة إلى سمات علامة المكون ، والتي تنص على أن هذا التطبيق من نوع جديد ، ويمكن أخذ رمزه من ملف JS محدد. في الوقت نفسه ، له سماته الخاصة اللازمة لتهيئة هذا المكون: فهي تشكل الحالة الأولية للمكون في المتجر.

<events-calendar	data-module="react-loader"
			data-bundle="events-calendar.js"
			date=".."
			marks="[{..}]"
			…
/>


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

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

كيفية تشغيل الكود؟


تم اختبار المفهوم على الوظائف البسيطة التي تعطي خطًا للخادم أو تكتب innerHTML للعميل. ولكن في الكود الحقيقي هناك وحدات و TypeScript.

هناك حلول قياسية للعميل ، على سبيل المثال ، جمع التعليمات البرمجية باستخدام Webpack ، والتي تطحن نفسها كل شيء وتعطيها للعميل في شكل حزمة من الحزم. وماذا تفعل للخادم عند استخدام GraalVM؟



دعونا نفكر في خيارين. الأول هو كتابة TypeScript في JavaScript ، كما يفعلون مع Node.js. للأسف ، لا يعمل هذا الخيار في تكويننا عندما تكون JavaScript هي لغة الضيف في GraalVM. في هذه الحالة ، ليس لدى JavaScript نظام معياري ، أو حتى غير متزامن. لأن النمطية والعمل مع عدم التزامن يوفر وقت تشغيل محدد: NodeJS أو متصفح. وفي حالتنا ، يحتوي الخادم على جافا سكريبت يمكنه تنفيذ التعليمات البرمجية بشكل متزامن فقط.

الخيار الثاني - يمكنك ببساطة تشغيل رمز الخادم من نفس الملفات التي تم جمعها للعميل. ويعمل هذا الخيار. ولكن هناك مشكلة في أن الخادم يحتاج إلى تطبيقات أخرى لعدد من الأساليب. على سبيل المثال ، سيتم استدعاء الوظيفة التي يتم تقديمها على وحدة الخدمة لتقديم وظيفة ، و ReactDOM.render () على العميل. أو مثال آخر من المقالة السابقة: للحصول على النصوص والإعدادات على الخادم ، سيتم استدعاء الوظيفة التي توفرها Java ، وعلى العميل سيتم تنفيذها في JS.

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



لكن ملفي التكوين هما تجميعان. في كل مرة ، يكون جمع كل شيء بشكل منفصل للخادم وللعميل طويلًا وصعبًا في الدعم.

تحتاج إلى التوصل إلى مثل هذا التكوين بحيث يتم جمع كل شيء دفعة واحدة.

تكوين Webpack لتشغيل JS على الخادم والعميل


لإيجاد حل لهذه المشكلة ، دعنا نرى الأجزاء التي يتكون منها المشروع:



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

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

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

في المجموع ، يتم الحصول على ثلاثة أجزاء تحتاج إلى تجميع. نحن نريد:
  • تكوين تجميع كل جزء بشكل منفصل.
  • ضع التبعيات بينهما بحيث لا يقع كل جزء في الجزء الآخر.
  • اجمع كل شيء بتمريرة واحدة.


كيف تصف بشكل منفصل الأجزاء التي ستتألف منها الجمعية؟ هناك تكوين متعدد في حزمة الويب: أنت ببساطة تتخلى عن مجموعة من عمليات تصدير الوحدات المضمنة في كل جزء.

module.exports = [{
  entry: './vendors.js',
}, {
  entry: './core.js'
}, {
 entry: './app.js'
}];


سيكون كل شيء على ما يرام ، ولكن في كل جزء من هذه الأجزاء ، سيتم تكرار رمز تلك الوحدات التي يعتمد عليها هذا الجزء:



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

عند بناء جزء آخر ، على سبيل المثال ، المكتبات الأساسية ، يمكننا القول أنها تعتمد على جزء البائع.



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

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

تسليم الموارد


ضع في اعتبارك المشكلة بمثال العمل مع النصوص المترجمة المخزنة في قاعدة بيانات منفصلة.

إذا كنت بحاجة إلى نص في المكون في وقت سابق على الخادم ، فيمكنك استدعاء الوظيفة للحصول على النص.

const pkg = l10n('smiles');

<div>
    : { pkg.getText('title') }
</div>


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

كيفية الحصول على نصوص في مكونات على رد فعل يتم تقديمها على خادم في GraalVM؟

كما تمت مناقشته في الجزء الأول من المقالة ، يمكنك إضافة طرق إلى الكائن العام في كائن JS الذي تريد الوصول إليه من JavaScript. تقرر إنشاء فصل دراسي بكل الطرق المتاحة لـ JavaScript.

public class ServerMethods {
    
    /**
     *     
     */
    public String getText(String pkg, String key) {
    }
    
}


ثم ضع نسخة من هذه الفئة في سياق JavaScript العالمي:

//     Java   
js.putMember("serverMethods", serverMethods);


نتيجة لذلك ، من JavaScript في تنفيذ الخادم ، نسمي ببساطة الوظيفة:

function getText(pkg: string, key: string): string {
    return global.serverMethods.getText(pkg, key);
}


في الواقع ، سيكون هذا استدعاء دالة في Java سيعيد النص المطلوب. تفاعل متزامن مباشر ولا مكالمات HTTP.

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



سوف يتعب المستخدم من الانتظار حتى يتم تنزيل كل شيء قبل بدء التطبيق. لذلك ، هذه الطريقة ليست مناسبة.

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

كيف تعرف النصوص التي يحتاجها التطبيق؟

لقد أبرمنا اتفاقية بأن حزم النصوص في الكود يتم الحصول عليها عن طريق استدعاء الوظيفة l10n () ، التي يتم نقل اسم الحزمة فيها فقط في شكل سلسلة حرفية:

const pkg = l10n('smiles');

<div>
    { pkg.getLMsg('title') }
</div>


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

عند الإخراج بعد التجميع لكل تطبيق ، نحصل على تكوين مع موارده:

{
    "events-calendar": {
       "pkg":  [
           "calendar",
           "dates"
       ],
       "cfg":  [
           "config1",
           "config2"
       ],
       "bundleName":  "events-calendar",
       "js":  "events-calendar.js",
       "css":  "events-calendar.css",
    }
}


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

الرمز القديم في الجديد


مع الانتقال السلس ، تنشأ المهمة عن إعادة استخدام الكود القديم في المكونات الجديدة ، لأن هناك مكونات كبيرة ومعقدة (على سبيل المثال ، مشغل فيديو) ، ستستغرق إعادة كتابتها الكثير من الوقت ، وتحتاج إلى استخدامها في المكدس الجديد الآن.



ما هي المشاكل؟

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


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

export class OldCodeBase<T> extends React.Component<T> {

    ref: React.RefObject<HTMLElement> = React.createRef();

    componentDidMount() {
        //       DOM
        this.props.activate(this.ref.current!); 
    }

    componentWillUnmount() {
        //       DOM
        this.props.deactivate(this.ref.current!); 
    }

    shouldComponentUpdate() {
        // React     , 
        //   React-. 
        //     .
        return false;
    }

    render() {
        return (
            <div ref={this.ref}></div>
        );
    }
}


يتيح لك الفصل إما إنشاء أجزاء من التعليمات البرمجية التي تعمل بالطريقة القديمة ، أو تدميرها ، في حين لن يكون هناك تفاعل متزامن معها.

قم بلصق كود قديم على الخادم


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

في مقال سابق ، تحدثنا عن استخدام السمات لتمرير المعلمات إلى مكونات جديدة على العميل والخادم.

<cool-app users="[1,2,3]" />


والآن ما زلنا نريد إدراج جزء من الترميز هناك ، وهو ما لا يمثل سمة. لهذا ، تقرر استخدام نظام فتحات.

<cool-app>
    <ui:part id="old-code">
        <div>old component</div>
    </ui:part>
</cool-app>


كما ترى في المثال أعلاه ، داخل كود مكون التطبيق الرائع ، يتم وصف فتحة شفرة قديمة تحتوي على مكونات قديمة. ثم ، داخل مكون التفاعل ، يشار إلى المكان الذي تريد لصق محتويات هذه الفتحة فيه:

render() {
    return (
        <div>
            <UiPart id="old-code" />
        </div>
    );
}


يعرض محرك الخادم مكون التفاعل هذا ويضع محتويات الفتحة في علامة <ui-part> ، مع تعيين سمة data-part-id = "old-code" إليه.

<cool-app>
    <div>
        <ui-part data-part-id="old-code">
            old code
        </ui-part>
    </div>
</cool-app>


إذا لم يكن عرض خادم JS في GraalVM مناسبًا للمهلة الزمنية ، فإننا نتراجع عن عرض العميل. للقيام بذلك ، يعطي المحرك على الخادم فتحات فقط ، ويضعها في إطار علامة القالب بحيث لا يتفاعل المستعرض مع التعليمات البرمجية الخاصة بهم.

<cool-app>
    <template>
        <ui-part data-part-id="old-code">
            old code
        </ui-part>
    </template>
</cool-app>


ماذا يحدث للعميل؟ يقوم محرك العميل ببساطة بمسح كود المكون ، وجمع علامات <ui-part> ، واستلام محتوياتها في شكل سلاسل ، وتمريرها إلى وظيفة التقديم مع بقية المعلمات.

var tagName = 'cool-app';
var reactComponent = components[tagName];
reactComponent.render({
       tagName: tagName,
       attrs: attrs,
       parts: parts,
       node: element
});


رمز المكون الذي يدرج الفتحات في الموقع المطلوب هو كما يلي:

export class UiPart extends OldCodeBase<IProps> {

	render() {
		const id = this.props.id;
		const parts = this.props.parts;

		if (!parts.hasOwnProperty(id)) {
			return null;
		}

		return React.createElement('ui-part', {
			'data-part-id': id,
			ref: this.ref,
			dangerouslySetInnerHTML: { __html: parts[id] }
		});
	}
}


علاوة على ذلك ، فهي موروثة من فئة OldCodeBase ، والتي تحل مشاكل التفاعل بين المكدس القديم والجديد.



الآن يمكنك كتابة نافذة منبثقة وملؤها باستخدام المكدس الجديد أو طلب من الخادم باستخدام النهج القديم. في هذه الحالة ، ستعمل المكونات بشكل صحيح.

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

ملخص


يتساءل الجميع عن مدى سرعة عمل GraalVM. أجرى مطورو Odnoklassniki اختبارات مختلفة مع تطبيقات React.

وظيفة بسيطة تقوم بإرجاع سلسلة بعد الإحماء تستغرق حوالي 1 ميكروثانية.

المكونات (مرة أخرى بعد التسخين) - من 0.5 إلى 6 مللي ثانية ، حسب حجمها.

تسارع GraalVM بشكل أبطأ من V8. ولكن بالنسبة لوقت الإحماء ، يتم تخفيف الوضع بفضل التراجع عن عرض العميل. نظرًا لوجود عدد كبير من المستخدمين ، يتم تسخين الجهاز الظاهري بسرعة.

ماذا تمكنت من القيام به



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


نأمل أن تكون تجارب Odnoklassniki والأمثلة مفيدة لك وستجدها لاستخدامها في عملك.

Source: https://habr.com/ru/post/undefined/


All Articles