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. Но если вы попробуете, ничего не отобразится. Давайте это исправим.
Метод рендеринга
Чтобы реализовать представление компонента, определите метод с именем 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-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 на отдельные этапы сборки. Общее количество шагов и видимость отдельных блоков доступны через 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. Всякий раз, когда значение одного из этих декорированных свойств изменяется, вызывается ряд методов, которые могут получить доступ к новым и старым значениям свойств. Интересующий нас метод жизненного цикла называется 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>
и увидеть, как выглядит модель кирпича на 5-м этапе построения. Для этого давайте сделаем свойство 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-значок-кнопка
Конечный пользователь нашего <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>
`;
}
}
Использовать Material Design на нашей странице так просто благодаря веб-компонентам!
Привязки событий
Эти кнопки на самом деле должны что-то делать. Кнопка «ответить» должна сбросить шаг построения на 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 станет легче читать и писать. Прощай, БЭМ !
Обновите стили, чтобы они выглядели следующим образом:
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-слайдер
Элементу ползунка требуется несколько важных данных, таких как минимальное и максимальное значение ползунка. Минимальное значение ползунка всегда может быть «1». Максимальное значение ползунка должно быть this._numConstructionSteps
, если модель загрузилась. Мы можем сообщить <mwc-slider>
эти значения через его атрибуты. Мы также можем использовать директиву ifDefined
-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>
`;
}
}
Ву! Мы можем использовать ползунок, чтобы изменить отображаемый шаг.
Данные «вниз»
Есть еще одна вещь. Когда для изменения шага используются кнопки «Назад» и «Далее», необходимо обновить ручку ползунка . Привяжите атрибут value <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. Заключение
Мы узнали много нового о том, как использоватьlit-element для создания собственного HTML-элемента. Мы научились:
- Определить пользовательский элемент
- Объявить API атрибута
- Отображение представления пользовательского элемента
- Инкапсулировать стили
- Используйте события и свойства для передачи данных
Если вы хотите узнать больше оlit-element, вы можете прочитать больше на его официальном сайте .
Вы можете просмотреть готовый элемент Brick-Viewer на stackblitz.com/edit/brick-viewer-complete .
Brick-Viewer также поставляется на NPM, и вы можете просмотреть исходный код здесь: репозиторий Github .