1. מבוא
רכיבי אינטרנט
רכיבי אינטרנט הם אוסף של סטנדרטים לאינטרנט שמאפשרים למפתחים להרחיב HTML באמצעות רכיבים מותאמים אישית. ב-Codelab הזה מגדירים את הרכיב <brick-viewer>
, שיוכל להציג מודלים של לבנים!
אלמנט מואר
כדי לעזור לנו להגדיר את האלמנט המותאם אישית <brick-viewer>
, נשתמש ברכיב lit. light-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, אנחנו יכולים לנצל את ה-API ההצהרתי ולהגדיר מאפיין מקור, בדיוק כמו התג <img>
או <video>
. עם אלמנט מואר, ממש כמו לקשט נכס כיתתי באמצעות @property
. האפשרות type
מאפשרת לציין איך הרכיב lit-Element ינתח את המאפיין כדי להשתמש בו כמאפיין HTML.
מגדירים את המאפיין ואת המאפיין src
:
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
עכשיו יש ב-<brick-viewer>
מאפיין src
שאפשר להגדיר ב-HTML. הערך שלו כבר קריא מתוך הכיתה BrickViewer
הודות לרכיב light.
מוצגים ערכים
נוכל להציג את הערך של המאפיין src
על ידי שימוש בו בליטרל התבנית של שיטת העיבוד. אפשר להפוך ערכים לליטרלים של תבנית באמצעות תחביר ${value}
.
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick model: ${this.src}</div>`;
}
}
עכשיו אנחנו רואים את הערך של המאפיין src ברכיב <brick-viewer>
שבחלון. כדאי לנסות את הפעולה הזו: פותחים את הכלים למפתחים בדפדפן ומשנים באופן ידני את המאפיין src. קדימה, אפשר לנסות...
...האם הבחנת שהטקסט ברכיב מתעדכן באופן אוטומטי? ב-light-Elements מתבוננים בנכסי הכיתה שמקושטים ב-@property
ומעבדים מחדש את התצוגה בשבילכם. המודעה המוארת עושה את העבודה הקשה, אז לא צריך לעשות זאת.
4. הגדרת הסצנה באמצעות Three.js
תאורה, מצלמה, רינדור!
ברכיב המותאם אישית שלנו ייעשה שימוש ב-Three.js כדי לעבד את דגמי הלבנים התלת-ממדיים שלנו. יש כמה דברים שאנחנו רוצים לעשות רק פעם אחת עבור כל מופע של רכיב <brick-viewer>
, כמו להגדיר את הסצנה שלושה.js, את המצלמה ואת התאורה. נוסיף אותם ל-constructor של המחלקה 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 שמציג את הסצנה שעברה רינדור שלוש.js. אפשר לגשת אליו דרך הנכס domElement
. אנחנו יכולים להתאים את הערך הזה לליטרל של תבנית העיבוד באמצעות תחביר ${value}
.
מסירים את הודעת ה-src
שהייתה לנו בתבנית ומוסיפים את רכיב ה-DOM של כלי הרינדור:
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
`;
}
}
כדי לאפשר הצגה של רכיב ברירת המחדל של כלי הרינדור בשלמותו, אנחנו צריכים גם להגדיר את רכיב <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>
סצנה שעברה רינדור שלוש.js:
אבל... הוא ריק. בואו נספק למודל מודל.
הכלי לטעינת לבנים
נעביר את הנכס src
שהגדרנו קודם אל LDrawLoader, שנשלח עם שלושה.js.
קובצי LDraw יכולים להפריד מודל של לבנים לשלבים נפרדים של בניין. ניתן לגשת למספר הכולל של השלבים ולחשיפה של לבנים בודדות דרך ה-API של LDrawLoader.
מעתיקים את המאפיינים הבאים, את השיטה החדשה _loadModel
ואת השורה החדשה ב-constructor:
@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
. ה-method 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;
}
עכשיו נוסיף שיטה מסייעת שהופכת רק את הלבנים עד שלב ה-build הנוכחי לגלויים. נשלח קריאה לעוזר בשיטת העדכון כדי שהוא יפעל בכל פעם שהנכס 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
למשתמש הקצה ב-<brick-viewer>
צריכה להיות גם אפשרות לנווט בשלבי ה-build דרך ממשק המשתמש. בואו נוסיף לחצנים למעבר לשלב הבא, לשלב הקודם ולשלב הראשון. כדי לפשט את התהליך, נשתמש ברכיב האינטרנט של הלחצנים של 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' אמור להגדיל אותו. מאפשר להוסיף בקלות את הפונקציונליות הזו באמצעות קישורי אירועים. בליטרל של תבנית ה-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>
.
מחוון אמבטיה
לרכיב פס ההזזה נדרשים כמה נתונים חשובים, כמו הערך המינימלי והמקסימלי של פס ההזזה. הערך המינימלי של פס ההזזה יכול תמיד להיות 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
, שנקראת אחרי שמגדירים את ה-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. סיכום
למדנו הרבה על השימוש ברכיב light כדי לבנות רכיב HTML משלנו. למדנו איך:
- הגדרת רכיב מותאם אישית
- הצהרה על API של מאפיין
- עיבוד תצוגה של רכיב מותאם אישית
- כיסוי סגנונות
- שימוש באירועים ובנכסים כדי להעביר נתונים
מידע נוסף על ליבה מואר זמין באתר הרשמי.
אפשר לראות רכיב שלם של 'צפייה לבנים' בכתובת stackblitz.com/edit/brick-viewer-complete.
brick-viewer נשלח גם כן ב-NPM, ואפשר לראות את המקור כאן: מאגר GitHub.