1. مقدمه
اجزای وب
اجزای وب مجموعه ای از استانداردهای وب هستند که به توسعه دهندگان اجازه می دهد HTML را با عناصر سفارشی گسترش دهند. در این نرم افزار کد، عنصر <brick-viewer>
را تعریف می کنید که می تواند مدل های آجری را نمایش دهد!
عنصر روشن
برای کمک به ما در تعریف عنصر سفارشی <brick-viewer>
، از عنصر روشن استفاده می کنیم. 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 آماده است. اما، اگر آن را امتحان کنید، هیچ چیز ارائه نمی شود. بیایید آن را درست کنیم.
روش رندر
برای پیاده سازی نمای کامپوننت، متدی به نام 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 هستیم، میتوانیم از API اعلانی استفاده کنیم و یک ویژگی منبع را تعریف کنیم، درست مانند تگ <img>
یا <video>
. با lit-element، تزئین یک ویژگی کلاس با @property
آسان است. گزینه type
به شما امکان می دهد تعیین کنید که عنصر lit چگونه ویژگی را برای استفاده به عنوان یک ویژگی HTML تجزیه می کند.
ویژگی و ویژگی src
را تعریف کنید:
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
<brick-viewer>
اکنون یک ویژگی src
دارد که می توانیم آن را در HTML تنظیم کنیم! مقدار آن در حال حاضر از داخل کلاس BrickViewer
ما به لطف عنصر روشن قابل خواندن است.
نمایش مقادیر
ما میتوانیم مقدار ویژگی src
را با استفاده از آن در قالب literal متد render نمایش دهیم. با استفاده از نحو ${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 را به مراحل ساختمان جداگانه تقسیم کنند. تعداد کل مراحل و نمایان شدن تک تک آجرها از طریق API 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>
ما نیز باید بتواند مراحل ساخت را از طریق UI پیمایش کند. بیایید دکمه هایی برای رفتن به مرحله بعد، مرحله قبل و مرحله اول اضافه کنیم. ما از کامپوننت وب دکمه 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
باشد._numConstructionSteps، اگر مدل بارگذاری شده باشد. ما می توانیم به <mwc-slider>
این مقادیر را از طریق ویژگی های آن بگوییم. اگر ویژگی _numConstructionSteps
تعریف نشده باشد، می توانیم از دستورالعمل ifDefined
lit-html برای جلوگیری از تنظیم ویژگی max
استفاده کنیم.
یک <mwc-slider>
بین دکمه های "back" و "forward" اضافه کنید:
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>
`;
}
}
داده ها "بالا"
هنگامی که کاربر نوار لغزنده را جابجا می کند، مرحله ساخت فعلی باید تغییر کند و دید مدل باید بر این اساس به روز شود. هر زمان که نوار لغزنده کشیده شود، عنصر لغزنده یک رویداد ورودی منتشر می کند. برای گرفتن این رویداد و تغییر مرحله ساخت، یک رویداد binding روی خود اسلایدر اضافه کنید.
پیوند رویداد را اضافه کنید:
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
binding را اضافه کنید:
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. نتیجه گیری
ما چیزهای زیادی در مورد نحوه استفاده از عنصر lit برای ساختن عنصر HTML خودمان یاد گرفتیم. ما یاد گرفتیم که چگونه:
- یک عنصر سفارشی را تعریف کنید
- یک ویژگی API را اعلام کنید
- نمایش یک عنصر سفارشی را ارائه دهید
- کپسوله کردن سبک ها
- از رویدادها و ویژگی ها برای ارسال داده ها استفاده کنید
اگر میخواهید درباره عنصر روشن بیشتر بدانید، میتوانید در سایت رسمی آن بیشتر بخوانید.
میتوانید یک عنصر کاملشده آجری را در stackblitz.com/edit/brick-viewer-complete مشاهده کنید.
brick-viewer نیز در NPM ارسال میشود، و میتوانید منبع را در اینجا مشاهده کنید: Github repo .