1. مقدمة
مكونات الويب
مكوّنات الويب هي مجموعة من معايير الويب التي تسمح للمطوّرين بتوسيع HTML باستخدام عناصر مخصّصة. في هذا الدليل التعليمي حول الرموز البرمجية، ستحدِّد عنصر <brick-viewer>
الذي سيتمكّن من عرض نماذج من الطوب.
عنصر مضاء
لمساعدتنا في تحديد العنصر المخصّص <brick-viewer>
، سنستخدم lit-element. lit-element هي فئة أساسية خفيفة الوزن تضيف بعض التحسينات النحوية إلى معيار مكوّنات الويب. وهذا سيجعل من السهل علينا بدء استخدام العنصر المخصص.
البدء
سنقوم بالترميز في بيئة 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. ولكن إذا جربتها، فلن يتم عرض أي شيء. دعنا نصلح ذلك.
طريقة العرض
لتنفيذ طريقة عرض المكوِّن، حدد طريقة تُسمى العرض. من المفترض أن تعرِض هذه الطريقة نموذجًا حرفيًا تم وضع علامة عليه باستخدام الدالة 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>
. باستخدام عنصر الإضاءة، يمكنك تزيين سمة فئة باستخدام @property
بسهولة. يتيح لك الخيار type
تحديد كيفية تحليل lit-element للسمة لاستخدامها كسمة HTML.
حدِّد السمة src
والموقع 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 يدويًا. ابدأ التجربة...
...هل لاحظت أن النص الموجود في العنصر يتم تحديثه تلقائيًا؟ العنصر المضيء يراقب خصائص الفئة المزينة بـ @property
ويعيد عرض العرض لك! العنصر المضيء يقوم بالأعباء الثقيلة بحيث لا تضطر إلى ذلك.
4. تعيين المشهد باستخدام Three.js
استمتِع بتجربة العرض.
سيستخدم العنصر المخصّص مكتبة three.js لعرض نماذج الطوب الثلاثية الأبعاد. هناك بعض الإجراءات التي نريد تنفيذها مرة واحدة فقط لكل نسخة من عنصر <brick-viewer>
، مثل إعداد المشهد الثالث.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>
الآن مشهدًا 3.js معروضًا:
لكنها فارغة. لنقدّم له نموذجًا.
عامل تحميل الطوب
سننقل السمة src
التي حدّدناها سابقًا إلى LDrawLoader، والذي يتمّ شحنه باستخدام three.js.
يمكن أن تفصل ملفات LDraw نموذجًا من الطوب إلى خطوات بناء منفصلة. يمكن الوصول إلى إجمالي عدد الخطوات ومستوى رؤية كلّ قطعة من الطوب من خلال LDrawLoader API.
انسخ هذه السمات وطريقة _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
، فعّلنا الموقع الإلكتروني في دورة حياة تعديل العنصر المضاء. كلما تغيّرت قيمة إحدى هذه الخصائص المزيّنة، يتمّ استدعاء سلسلة من الطرق التي يمكنها الوصول إلى القيم الجديدة والقديمة للخصائص. وتُسمى طريقة دورة الحياة التي نحن مهتم بها 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);
}
}
حسنًا، افتح الآن devtools في المتصفّح، وفحص عنصر <brick-viewer>
. أضِف سمة step
إليها، على النحو التالي:
راقِب ما يحدث للنموذج المعروض. يمكننا استخدام سمة step
للتحكّم في مقدار عرض الطراز. في ما يلي الشكل الذي يجب أن تظهر به السمة step
عند ضبطها على "10"
:
6- التنقّل في مجموعة الطوب
زر-رمز-mwc
يجب أن يتمكّن المستخدم النهائي من <brick-viewer>
أيضًا من التنقّل في خطوات الإنشاء من خلال واجهة المستخدم. لنضيف أزرارًا للانتقال إلى الخطوة التالية والخطوة السابقة والخطوة الأولى. سنستخدم مكون الويب لزر Material Design لتسهيل الأمر. بما أنّه تم استيراد @material/mwc-icon-button
من قبل، نحن مستعدون للإسقاط في <mwc-icon-button></mwc-icon-button>
. يمكننا تحديد الرمز الذي نريد استخدامه مع سمة الرمز، كما يلي: <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>
`;
}
}
من السهل استخدام أسلوب Material Design على صفحتنا، وذلك بفضل مكونات الويب.
عمليات ربط الأحداث
يجب أن تفعل هذه الأزرار شيئًا في الواقع. من المفترض أن يؤدي الزر "ردّ" إلى إعادة ضبط خطوة الإنشاء إلى 1. من المفترض أن يقلل الزر "navigate_before" من خطوة الإنشاء، ومن المفترض أن يؤدي الزر "navigate_next" إلى زيادة السرعة. ويسهل عنصر lit إضافة هذه الوظيفة باستخدام عمليات ربط الأحداث. في الصيغة الحرفية لنموذج 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
lifecycle، والتي يتمّ استدعاؤها بعد ترتيب DOM لأول مرّة. يمكن أن يساعدنا العنصر الزخرفي 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- الخاتمة
لقد تعلمنا الكثير عن كيفية استخدام عنصر مضيء لإنشاء عنصر HTML الخاص بنا. لقد تعلمنا كيفية:
- تحديد عنصر مخصّص
- تعريف واجهة برمجة التطبيقات للسمة
- عرض عنصر مخصّص
- تضمين الأنماط
- استخدام الأحداث والخصائص لتمرير البيانات
إذا كنت تريد معرفة المزيد عن lit-element، يمكنك الاطّلاع على مزيد من المعلومات على الموقع الرسمي.
يمكنك الاطّلاع على عنصر brick-viewer مكتمل على الرابط stackblitz.com/edit/brick-viewer-complete.
يتم أيضًا شحن أداة brick-viewer على NPM، ويمكنك الاطّلاع على المصدر هنا: مستودع GitHub.