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 中。但如果你嘗試看看,系統不會顯示任何內容。讓我們一起解決這個問題!
轉譯方法
如要實作元件的檢視畫面,請定義已命名的轉譯方法。這個方法應會傳回標記 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>
標記一樣。使用 LED 元素,就像使用 @property
裝飾類別屬性一樣簡單。type
選項可讓您指定 lit-element 剖析屬性的方式,以便做為 HTML 屬性使用。
定義 src
屬性和屬性:
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
<brick-viewer>
現在具備可在 HTML 中設定的 src
屬性!由於光學元素的關係,其值已經可以從我們的 BrickViewer
類別中讀取。
顯示值
我們可以在算繪方法的範本常值中使用 src
屬性的值,以便顯示該屬性的值。使用 ${value}
語法將值插入範本常值。
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick model: ${this.src}</div>`;
}
}
現在,我們看到視窗 <brick-viewer>
元素中的 src 屬性值。請嘗試以下方法:開啟瀏覽器的開發人員工具,並手動變更 src 屬性。請繼續,試試看...
...你有註意到元素中的文字會自動更新嗎?lit-element 會觀察以 @property
裝飾的類別屬性,然後重新轉譯檢視畫面!LED 燈可代你處理繁重工作,因此你不必手動操作。
4. 使用 Three.js 設定場景
燈光攝影效果
我們的自訂元素將使用 3.js 呈現 3D 磚塊模型。我們可以為每個 <brick-viewer>
元素例項執行一次操作,例如設定 3.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 元素,用於顯示算繪的 3.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,後者透過 3.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 屬性變更時,都必須叫用這個函式。透過使用 @property
裝飾 src
屬性,我們已為屬性選擇套用光照元素更新的生命週期。每當有裝飾物的情況下值發生變化,就會呼叫一系列的方法,可存取屬性的新值和舊值。我們感興趣的生命週期方法稱為 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 個建構步驟中,應該也能看到磚塊模型的樣貌。為此,我們可以使用 @property
裝飾 step
屬性,使其成為觀察到的屬性。
裝飾 step
屬性:
export class BrickViewer extends LitElement {
@property({type: Number})
step?: number;
}
現在,我們要新增 Helper 方法,僅顯示目前建構步驟之前的積木。我們會在更新方法中呼叫輔助程式,讓它在每次變更 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-icon-button
<brick-viewer>
的使用者也應該能夠透過 UI 瀏覽建構步驟。我們來新增前往下一個步驟、上一步和第一步的按鈕。我們會使用 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-element 可透過事件繫結輕鬆新增這項功能。在 HTML 範本常值中,使用語法 @eventname=${eventHandler}
做為元素屬性。現在系統會在該元素上偵測到 eventname
事件時執行 eventHandler
!假設我們先在三個圖示按鈕中加入點擊事件處理常式:
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>
這些值。如果 _numConstructionSteps
屬性尚未定義,我們也可以使用 ifDefined
lit-html 指令式避免設定 max
屬性。
在「返回」之間加上 <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>
`;
}
}
資料「向上」
使用者移動滑桿時,目前的建構步驟應隨之變更,模型的顯示設定也會隨之更新。拖曳滑桿時,Slider 元素會發出輸入事件。在滑桿上新增事件繫結,藉此擷取這個事件並變更建構步驟。
新增事件繫結:
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. 結語
我們學到瞭如何運用 lit-element 建立自己專屬的 HTML 元素,我們學到以下內容:
- 定義自訂元素
- 宣告屬性 API
- 算繪自訂元素的檢視畫面
- 封裝樣式
- 使用事件和屬性來傳送資料
如要進一步瞭解光照元素,歡迎前往官方網站瞭解詳情。
您可以在 stackblitz.com/edit/brick-viewer-complete 中,查看已完成的磚頭檢視器元素。
此外,實機觀眾也會在 NPM 上出貨,您可以前往 GitHub 存放區查看原始碼。