1. مقدمة
مكوّنات الويب
مكوّنات الويب هي مجموعة من معايير الويب التي تتيح للمطوّرين توسيع نطاق HTML باستخدام عناصر مخصّصة. في هذا الدرس التطبيقي، ستحدّد عنصر <brick-viewer> الذي سيتمكّن من عرض نماذج المكعبات.
lit-element
لمساعدتنا في تحديد العنصر المخصّص <brick-viewer>، سنستخدم lit-element، وهي صنف أساسي خفيف الوزن يضيف بعض التجميل اللغوي على بنية معيار Web Components. سيُسهّل ذلك علينا بدء استخدام العنصر المخصّص.
البدء
سنكتب الرمز في بيئة Stackblitz على الإنترنت، لذا افتح هذا الرابط في نافذة جديدة:
stackblitz.com/edit/brick-viewer
لِنبدأ.
2. تحديد عنصر مخصّص
تعريف الفئة
لتحديد عنصر مخصّص، أنشئ فئة توسّع LitElement وزيِّنها باستخدام @customElement. ستكون وسيطة @customElement هي اسم العنصر المخصّص.
في ملف brick-viewer.ts، ضَع ما يلي:
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
}
أصبح العنصر <brick-viewer></brick-viewer> الآن جاهزًا للاستخدام في HTML. ولكن إذا حاولت استخدامه، لن يتم عرض أي شيء. لنحاول إصلاح هذه المشكلة.
طريقة العرض
لتنفيذ طريقة عرض المكوّن، حدِّد طريقة باسم render. يجب أن تعرض هذه الطريقة سلسلة نموذجية حرفية موسومة بالدالة html. ضَع أي رمز HTML تريده في السلسلة الحرفية لنموذج موسوم. سيتم عرض هذا العنصر عند استخدام <brick-viewer>.
أضِف طريقة render:
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick viewer</div>`;
}
}
3- تحديد ملف LDraw
تحديد سمة
سيكون من الرائع لو تمكّن مستخدم <brick-viewer> من تحديد نموذج الطوب الذي يريد عرضه باستخدام سمة، مثل:
<brick-viewer src="path/to/model.ldraw"></brick-viewer>
بما أنّنا بصدد إنشاء عنصر HTML، يمكننا الاستفادة من واجهة برمجة التطبيقات التعريفية وتحديد سمة مصدر، تمامًا مثل العلامة <img> أو <video>. باستخدام lit-element، يصبح الأمر بسيطًا مثل تزيين خاصية فئة بالرمز @property. يتيح لك الخيار type تحديد طريقة تحليل lit-element للسمة لاستخدامها كسمة HTML.
حدِّد السمة src والخاصية:
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
تتضمّن <brick-viewer> الآن السمة src التي يمكننا ضبطها في HTML. يمكن قراءة قيمته من داخل الفئة BrickViewer بفضل lit-element.
عرض القيم
يمكننا عرض قيمة السمة src باستخدامها في السلسلة الحرفية للنموذج في طريقة العرض. يمكنك إدخال القيم في السلاسل الحرفية للنماذج باستخدام بنية ${value}.
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick model: ${this.src}</div>`;
}
}
الآن، نرى قيمة السمة src في العنصر <brick-viewer> في النافذة. جرِّب ما يلي: افتح أدوات المطوّرين في المتصفّح وغيِّر يدويًا السمة src. جرِّب ذلك الآن.
...هل لاحظت أنّ النص في العنصر يتم تعديله تلقائيًا؟ تراقب مكتبة lit-element خصائص الفئة التي تم تزيينها باستخدام @property وتعيد عرض طريقة العرض لك. تتولّى مكتبة lit-element المهام الصعبة حتى لا تضطر إلى تنفيذها.
4. ضبط المشهد باستخدام Three.js
أضواء، كاميرا، عرض!
سيستخدم العنصر المخصّص three.js لعرض تصاميم الطوب الثلاثية الأبعاد. هناك بعض الإجراءات التي نريد تنفيذها مرة واحدة فقط لكل مثيل من العنصر <brick-viewer>، مثل إعداد المشهد والكاميرا والإضاءة في three.js. سنضيف هذه القيم إلى الدالة الإنشائية لفئة BrickViewer. سنحتفظ ببعض العناصر كسمات للفئة حتى نتمكّن من استخدامها لاحقًا: الكاميرا والمشهد وعناصر التحكّم والعارض.
أضِف إعداد مشهد three.js:
export class BrickViewer extends LitElement {
private _camera: THREE.PerspectiveCamera;
private _scene: THREE.Scene;
private _controls: OrbitControls;
private _renderer: THREE.WebGLRenderer;
constructor() {
super();
this._camera = new THREE.PerspectiveCamera(45, this.clientWidth/this.clientHeight, 1, 10000);
this._camera.position.set(150, 200, 250);
this._scene = new THREE.Scene();
this._scene.background = new THREE.Color(0xdeebed);
const ambientLight = new THREE.AmbientLight(0xdeebed, 0.4);
this._scene.add( ambientLight );
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(-1000, 1200, 1500);
this._scene.add(directionalLight);
this._renderer = new THREE.WebGLRenderer({antialias: true});
this._renderer.setPixelRatio(window.devicePixelRatio);
this._renderer.setSize(this.offsetWidth, this.offsetHeight);
this._controls = new OrbitControls(this._camera, this._renderer.domElement);
this._controls.addEventListener("change", () =>
requestAnimationFrame(this._animate)
);
this._animate();
const resizeObserver = new ResizeObserver(this._onResize);
resizeObserver.observe(this);
}
private _onResize = (entries: ResizeObserverEntry[]) => {
const { width, height } = entries[0].contentRect;
this._renderer.setSize(width, height);
this._camera.aspect = width / height;
this._camera.updateProjectionMatrix();
requestAnimationFrame(this._animate);
};
private _animate = () => {
this._renderer.render(this._scene, this._camera);
};
}
يوفّر العنصر WebGLRenderer عنصر DOM يعرض مشهد three.js المعروض. ويمكن الوصول إليه من خلال السمة domElement. يمكننا إدخال هذه القيمة في سلسلة النموذج المعروضة، باستخدام بنية ${value}.
أزِل الرسالة src التي كانت في النموذج، وأدرِج عنصر DOM الخاص بأداة العرض:
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
`;
}
}
للسماح بعرض عنصر DOM الخاص بأداة العرض بالكامل، علينا أيضًا ضبط العنصر <brick-viewer> نفسه على display: block. يمكننا توفير الأنماط في سمة ثابتة تُسمى styles، ويتم ضبطها على سلسلة نموذج css.
أضِف هذا النمط إلى الفئة:
export class BrickViewer extends LitElement {
static styles = css`
/* The :host selector styles the brick-viewer itself! */
:host {
display: block;
}
`;
}
تعرض <brick-viewer> الآن مشهدًا تم عرضه باستخدام three.js:

لكنّها فارغة. لنقدّم له نموذجًا.
عامل تحميل الطوب
سننقل السمة src التي حدّدناها سابقًا إلى LDrawLoader، والتي يتم شحنها مع three.js.
يمكن لملفات LDraw تقسيم نموذج Brick إلى خطوات بناء منفصلة. يمكن الوصول إلى إجمالي عدد الخطوات وإمكانية عرض كل قطعة على حدة من خلال واجهة برمجة التطبيقات LDrawLoader.
انسخ هذه السمات وطريقة _loadModel الجديدة والسطر الجديد في الدالة الإنشائية:
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
private _loader = new LDrawLoader();
private _model: any;
private _numConstructionSteps?: number;
step?: number;
constructor() {
// ...
// Add this line right before this._animate();
(this._loader as any).separateObjects = true;
this._animate();
}
private _loadModel() {
if (this.src === null) {
return;
}
this._loader
.setPath('')
// Using our src property!
.load(this.src, (newModel) => {
if (this._model !== undefined) {
this._scene.remove(this._model);
this._model = undefined;
}
this._model = newModel;
// Convert from LDraw coordinates: rotate 180 degrees around OX
this._model.rotation.x = Math.PI;
this._scene.add(this._model);
this._numConstructionSteps = this._model.userData.numConstructionSteps;
this.step = this._numConstructionSteps!;
const bbox = new THREE.Box3().setFromObject(this._model);
this._controls.target.copy(bbox.getCenter(new THREE.Vector3()));
this._controls.update();
this._controls.saveState();
});
}
}
متى يجب استدعاء _loadModel؟ يجب استدعاؤها في كل مرة تتغير فيها السمة src. من خلال تزيين السمة src باستخدام @property، فعّلنا الموقع الإلكتروني في دورة حياة التعديل في Lit-element. عندما تتغيّر قيمة إحدى هذه الخصائص المزخرفة، يتم استدعاء سلسلة من الطرق التي يمكنها الوصول إلى القيم الجديدة والقديمة للخصائص. طريقة دورة الحياة التي تهمّنا تُسمى update. تتلقّى الطريقة update وسيطة PropertyValues، والتي ستتضمّن معلومات حول أي خصائص تم تغييرها للتو. هذا هو المكان المثالي للاتصال بالرقم _loadModel.
أضِف طريقة update:
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
super.update(changedProperties);
}
}
يمكن لعنصر <brick-viewer> الآن عرض ملف طوب، يتم تحديده باستخدام السمة src.

5- عرض نماذج جزئية
الآن، لنضبط إعدادات خطوة الإنشاء الحالية. نريد أن نتمكّن من تحديد <brick-viewer step="5"></brick-viewer>، ويجب أن نرى شكل نموذج الطوب في خطوة البناء الخامسة. لإجراء ذلك، لنحوّل السمة step إلى سمة قابلة للمراقبة من خلال إضافة @property إليها.
تزيين السمة step:
export class BrickViewer extends LitElement {
@property({type: Number})
step?: number;
}
الآن، سنضيف طريقة مساعدة تجعل الطوب مرئيًا فقط حتى خطوة الإنشاء الحالية. سنستدعي العنصر المساعد في طريقة التعديل لكي يتم تشغيله في كل مرة يتم فيها تغيير السمة step.
عدِّل الطريقة update وأضِف الطريقة الجديدة _updateBricksVisibility:
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
if (changedProperties.has('step')) {
this._updateBricksVisibility();
}
super.update(changedProperties);
}
private _updateBricksVisibility() {
this._model && this._model.traverse((c: any) => {
if (c.isGroup && this.step) {
c.visible = c.userData.constructionStep <= this.step;
}
});
requestAnimationFrame(this._animate);
}
}
حسنًا، افتح الآن أدوات المطوّرين في المتصفّح، وافحص العنصر <brick-viewer>. أضِف سمة step إليها، على النحو التالي:

شاهِد ما يحدث للنموذج المعروض. يمكننا استخدام السمة step للتحكّم في مقدار النموذج المعروض. في ما يلي الشكل الذي يجب أن يظهر به عندما يتم ضبط السمة step على "10":

6. التنقّل في مجموعة الطوب
mwc-icon-button
يجب أن يتمكّن المستخدم النهائي <brick-viewer> أيضًا من التنقّل بين خطوات الإنشاء من خلال واجهة المستخدم. لنضِف أزرارًا للانتقال إلى الخطوة التالية والخطوة السابقة والخطوة الأولى. سنستخدم مكوّن زر الويب في Material Design لتسهيل ذلك. بما أنّ @material/mwc-icon-button تم استيراده من قبل، يمكننا إضافة <mwc-icon-button></mwc-icon-button>. يمكننا تحديد الرمز الذي نريد استخدامه مع السمة icon، على النحو التالي: <mwc-icon-button icon="thumb_up"></mwc-icon-button>. يمكنك العثور على جميع الرموز المتاحة هنا: material.io/resources/icons.
لنضِف بعض أزرار الرموز إلى طريقة العرض:
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
<div id="controls">
<mwc-icon-button icon="replay"></mwc-icon-button>
<mwc-icon-button icon="navigate_before"></mwc-icon-button>
<mwc-icon-button icon="navigate_next"></mwc-icon-button>
</div>
`;
}
}
يمكنك استخدام التصميم المتعدد الأبعاد على صفحتك بهذه السهولة بفضل مكوّنات الويب.
ربط الأحداث
يجب أن تؤدي هذه الأزرار وظيفة ما. يجب أن يؤدي النقر على الزر "ردّ" إلى إعادة ضبط خطوة الإنشاء على 1. يجب أن يؤدي الزرّ "navigate_before" إلى إنقاص خطوة الإنشاء، ويجب أن يؤدي الزرّ "navigate_next" إلى زيادتها. تسهّل مكتبة lit-element إضافة هذه الوظيفة من خلال ربط الأحداث. في سلسلة HTML الحرفية، استخدِم البنية @eventname=${eventHandler} كسمة عنصر. سيتم الآن تنفيذ eventHandler عند رصد حدث eventname على هذا العنصر. كمثال، لنضِف معالِجات أحداث النقر إلى أزرار الرموز الثلاثة:
export class BrickViewer extends LitElement {
private _restart() {
this.step! = 1;
}
private _stepBack() {
this.step! -= 1;
}
private _stepForward() {
this.step! += 1;
}
render() {
return html`
${this._renderer.domElement}
<div id="controls">
<mwc-icon-button @click=${this._restart} icon="replay"></mwc-icon-button>
<mwc-icon-button @click=${this._stepBack} icon="navigate_before"></mwc-icon-button>
<mwc-icon-button @click=${this._stepForward} icon="navigate_next"></mwc-icon-button>
</div>
`;
}
}
جرِّب النقر على الأزرار الآن. رائع.
الأنماط
تعمل الأزرار، ولكن مظهرها غير جيد. تتجمّع كلّها في الأسفل. لنضبط نمطها لتظهر فوق المشهد.
لتطبيق أنماط على هذه الأزرار، نعود إلى السمة static styles. يتم تحديد نطاق هذه الأنماط، ما يعني أنّها لن تنطبق إلا على العناصر داخل مكوّن الويب هذا. هذه إحدى مزايا كتابة المكوّنات على الويب: يمكن أن تكون أدوات الاختيار أبسط، وستكون لغة CSS أسهل في القراءة والكتابة. إلى اللقاء يا BEM
عدِّل الأنماط لتصبح على النحو التالي:
export class BrickViewer extends LitElement {
static styles = css`
:host {
display: block;
position: relative;
}
#controls {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
}
`;
}

زر إعادة ضبط الكاميرا
يمكن للمستخدمين النهائيين لـ <brick-viewer> تدوير المشهد باستخدام عناصر التحكّم بالماوس. بما أنّنا بصدد إضافة أزرار، لنضِف زرًا لإعادة ضبط الكاميرا على موضعها التلقائي. يمكنك استخدام <mwc-icon-button> آخر مع ربط حدث ناتج عن النقر لإنجاز المهمة.
export class BrickViewer extends LitElement {
private _resetCamera() {
this._controls.reset();
}
render() {
return html`
<div id="controls">
<!-- ... -->
<!-- Add this button: -->
<mwc-icon-button @click=${this._resetCamera} icon="center_focus_strong"></mwc-icon-button>
</div>
`;
}
}
سرعة التنقل
تتضمّن بعض مجموعات المكعبات الكثير من الخطوات. قد يريد المستخدم الانتقال إلى خطوة معيّنة. يمكن أن تساعد إضافة شريط تمرير يتضمّن أرقام الخطوات في التنقّل بسرعة. سنستخدم العنصر <mwc-slider> لهذا الغرض.
mwc-slider
يحتاج عنصر شريط التمرير إلى بعض البيانات المهمة، مثل الحد الأدنى والحد الأقصى لقيمة شريط التمرير. يمكن أن تكون القيمة الدنيا لشريط التمرير دائمًا "1". يجب أن تكون القيمة القصوى لشريط التمرير this._numConstructionSteps، إذا تم تحميل النموذج. يمكننا معرفة <mwc-slider> هذه القيم من خلال سماتها. يمكننا أيضًا استخدام التوجيه ifDefined lit-html لتجنُّب ضبط السمة max إذا لم يتم تحديد السمة _numConstructionSteps.
أضِف <mwc-slider> بين الزرّين "رجوع" و "تقديم":
export class BrickViewer extends LitElement {
render() {
return html`
<div id="controls">
<!-- ... backwards button -->
<!-- Add this slider: -->
<mwc-slider
step="1"
pin
markers
min="1"
max=${ifDefined(this._numConstructionSteps)}
></mwc-slider>
<!-- ... forwards button -->
</div>
`;
}
}
البيانات "أعلى"
عندما يحرك المستخدم شريط التمرير، يجب أن تتغير خطوة الإنشاء الحالية، ويجب تعديل مستوى رؤية النموذج وفقًا لذلك. سيُصدر عنصر شريط التمرير حدث إدخال كلما تم سحب شريط التمرير. أضِف ربط حدث على شريط التمرير نفسه لرصد هذا الحدث وتغيير خطوة الإنشاء.
أضِف ربط الحدث:
export class BrickViewer extends LitElement {
render() {
return html`
<div id="controls">
<!-- ... -->
<!-- Add the @input event binding: -->
<mwc-slider
...
@input=${(e: CustomEvent) => this.step = e.detail.value}
></mwc-slider>
<!-- ... -->
</div>
`;
}
}
رائع! يمكننا استخدام شريط التمرير لتغيير الخطوة المعروضة.
البيانات "غير متاحة"
هناك شيء آخر. عند استخدام الزرّين "السابق" و "التالي" لتغيير الخطوة، يجب تعديل مقبض شريط التمرير. اربط سمة القيمة <mwc-slider> بالسمة this.step.
أضِف عملية الربط value:
export class BrickViewer extends LitElement {
render() {
return html`
<div id="controls">
<!-- ... -->
<!-- Add the value property binding: -->
<mwc-slider
...
value=${ifDefined(this.step)}
></mwc-slider>
<!-- ... -->
</div>
`;
}
}
أوشكنا على الانتهاء من شريط التمرير. أضِف نمطًا مرنًا ليتوافق مع عناصر التحكّم الأخرى:
export class BrickViewer extends LitElement {
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
}
بالإضافة إلى ذلك، علينا استدعاء layout على عنصر شريط التمرير نفسه. سننفّذ ذلك في طريقة دورة الحياة firstUpdated، التي يتم استدعاؤها بعد وضع نموذج المستند لأول مرة. يمكن أن يساعدنا أداة التزيين query في الحصول على مرجع لعنصر شريط التمرير في النموذج.
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
}
في ما يلي جميع الإضافات إلى شريط التمرير مجمّعة (مع سمات pin وmarkers إضافية على شريط التمرير لجعل مظهره رائعًا):
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
render() {
return html`
${this._renderer.domElement}
<div id="controls">
<mwc-icon-button @click=${this._restart} icon="replay"></mwc-icon-button>
<mwc-icon-button @click=${this._stepBack} icon="navigate_before"></mwc-icon-button>
<mwc-slider
step="1"
pin
markers
min="1"
max=${ifDefined(this._numConstructionSteps)}
?disabled=${this._numConstructionSteps === undefined}
value=${ifDefined(this.step)}
@input=${(e: CustomEvent) => this.constructionStep = e.detail.value}
></mwc-slider>
<mwc-icon-button @click=${this._stepForward} icon="navigate_next"></mwc-icon-button>
<mwc-icon-button @click=${this._resetCamera} icon="center_focus_strong"></mwc-icon-button>
</div>
`;
}
}
إليك المنتج النهائي.

7. الخاتمة
لقد تعلّمنا الكثير حول كيفية استخدام lit-element لإنشاء عنصر HTML خاص بنا. لقد تعلّمنا كيفية:
- تحديد عنصر مخصّص
- تعريف واجهة برمجة تطبيقات للسمات
- عرض طريقة عرض لعنصر مخصّص
- تغليف الأنماط
- استخدام الأحداث والسمات لتمرير البيانات
إذا أردت معرفة المزيد عن lit-element، يمكنك الاطّلاع على الموقع الإلكتروني الرسمي.
يمكنك الاطّلاع على عنصر مكتمل من أداة عرض الطوب على stackblitz.com/edit/brick-viewer-complete.
يتم أيضًا شحن brick-viewer على NPM، ويمكنك الاطّلاع على المصدر هنا: مستودع Github.