1. Giới thiệu
Thành phần web
Các thành phần web là một tập hợp các tiêu chuẩn web cho phép nhà phát triển mở rộng HTML bằng các phần tử tuỳ chỉnh. Trong lớp học lập trình này, bạn sẽ xác định phần tử <brick-viewer>
có thể hiển thị các mô hình khối hình!
phần tử chiếu sáng
Để xác định phần tử tuỳ chỉnh <brick-viewer>
, chúng ta sẽ dùng phần tử lit-element. phần tử lit là một lớp cơ sở nhẹ thêm một số cú pháp dễ hiểu vào tiêu chuẩn thành phần web. Việc này sẽ giúp chúng ta thiết lập và chạy phần tử tuỳ chỉnh một cách dễ dàng.
Bắt đầu
Chúng ta sẽ lập trình trong một môi trường Stackblitz trực tuyến, do đó hãy mở đường liên kết này trong một cửa sổ mới:
stackblitz.com/edit/brick-viewer
Hãy bắt đầu!
2. Xác định Phần tử tuỳ chỉnh
Định nghĩa lớp
Để xác định một phần tử tuỳ chỉnh, hãy tạo lớp mở rộng LitElement
và trang trí lớp đó bằng @customElement
. Đối số cho @customElement
sẽ là tên của phần tử tuỳ chỉnh.
Trong Gạch-viewer.ts, hãy đặt:
@customElement('brick-viewer')
export class BrickViewer extends LitElement {
}
Bây giờ, phần tử <brick-viewer></brick-viewer>
đã sẵn sàng để sử dụng trong HTML. Tuy nhiên, nếu dùng thử, bạn sẽ không thấy kết quả nào xuất hiện. Hãy khắc phục vấn đề đó.
Phương thức hiển thị
Để triển khai khung hiển thị của thành phần, hãy xác định một phương thức có tên là kết xuất. Phương thức này sẽ trả về một giá trị cố định của mẫu được gắn thẻ bằng hàm html
. Đặt bất kỳ HTML nào bạn muốn vào giá trị cố định của mẫu được gắn thẻ. Kết quả này sẽ hiển thị khi bạn sử dụng <brick-viewer>
.
Thêm phương thức render
:
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick viewer</div>`;
}
}
3. Chỉ định tệp LDraw
Xác định thuộc tính
Thật tuyệt nếu người dùng <brick-viewer>
có thể chỉ định mô hình khối hình cần hiển thị bằng một thuộc tính, như sau:
<brick-viewer src="path/to/model.ldraw"></brick-viewer>
Do đang tạo phần tử HTML nên chúng ta có thể tận dụng API khai báo và xác định thuộc tính nguồn, chẳng hạn như thẻ <img>
hoặc <video>
. Với phần tử chiếu sáng, bạn có thể dễ dàng trang trí một thuộc tính lớp bằng @property
. Tuỳ chọn type
cho phép bạn chỉ định cách phần tử lit phân tích cú pháp thuộc tính để sử dụng làm thuộc tính HTML.
Xác định thuộc tính src
và thuộc tính:
export class BrickViewer extends LitElement {
@property({type: String})
src: string|null = null;
}
<brick-viewer>
hiện có một thuộc tính src
mà chúng ta có thể đặt trong HTML! Giá trị của thuộc tính này đã có thể đọc được từ trong lớp BrickViewer
nhờ phần tử lit-element.
Đang hiển thị giá trị
Chúng ta có thể cho thấy giá trị của thuộc tính src
bằng cách sử dụng thuộc tính này trong giá trị cố định của mẫu của phương thức hiển thị. Nội dung các giá trị vào giá trị cố định của mẫu bằng cú pháp ${value}
.
export class BrickViewer extends LitElement {
render() {
return html`<div>Brick model: ${this.src}</div>`;
}
}
Bây giờ, chúng ta sẽ thấy giá trị của thuộc tính src trong phần tử <brick-viewer>
của cửa sổ. Hãy thử cách này: mở công cụ cho nhà phát triển của trình duyệt và thay đổi thuộc tính src theo cách thủ công. Hãy tiếp tục, hãy dùng thử...
...Bạn có thấy văn bản trong thành phần này tự động cập nhật không? phần tử lit quan sát các thuộc tính lớp được trang trí bằng @property
rồi kết xuất lại khung hiển thị cho bạn! nhẹ nhàng sẽ đảm nhận phần việc nặng nhọc, nên bạn không cần phải làm gì cả.
4. Đặt Scene bằng Three.js
Ánh sáng, máy ảnh, kết xuất!
Phần tử tùy chỉnh của chúng ta sẽ sử dụng đảm bảo 3.js để hiển thị các mô hình khối hình 3D. Có một số việc mà chúng ta chỉ muốn thực hiện một lần cho mỗi thực thể của phần tử <brick-viewer>
, chẳng hạn như thiết lập cảnh Three.js, camera và ánh sáng. Chúng ta sẽ thêm các đối tượng này vào hàm khởi tạo lớp BrickViewer. Chúng ta sẽ giữ một số đối tượng làm thuộc tính lớp để có thể sử dụng sau này: camera, cảnh, thành phần điều khiển và trình kết xuất đồ hoạ.
Thêm trong quá trình thiết lập cảnh 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);
};
}
Đối tượng WebGLRenderer
cung cấp phần tử DOM hiển thị cảnh Three.js được kết xuất. Bạn có thể truy cập vào đường liên kết này thông qua thuộc tính domElement
. Chúng ta có thể nội suy giá trị này vào giá trị cố định của mẫu kết xuất bằng cú pháp ${value}
.
Xoá thông báo src
mà chúng ta có trong mẫu rồi chèn phần tử DOM của trình kết xuất:
export class BrickViewer extends LitElement {
render() {
return html`
${this._renderer.domElement}
`;
}
}
Để phần tử dom của trình kết xuất hiển thị toàn bộ, chúng ta cũng cần đặt chính phần tử <brick-viewer>
thành display: block
. Chúng ta có thể cung cấp kiểu trong một thuộc tính tĩnh có tên là styles
, được đặt thành giá trị cố định mẫu css
.
Thêm kiểu này vào lớp:
export class BrickViewer extends LitElement {
static styles = css`
/* The :host selector styles the brick-viewer itself! */
:host {
display: block;
}
`;
}
Bây giờ, <brick-viewer>
đang hiển thị cảnh ba.js được kết xuất:
Nhưng... bảng này trống. Hãy cung cấp cho nó một mô hình.
Trình tải gạch
Chúng ta sẽ chuyển thuộc tính src
mà chúng ta đã xác định trước đó vào LDrawLoader. Thuộc tính này sẽ được vận chuyển cùng với Three.js.
Các tệp LDraw có thể phân tách mô hình Gạch thành các bước xây dựng riêng biệt. Bạn có thể truy cập tổng số bước và mức độ hiển thị khối hình riêng lẻ thông qua API LDrawLoader.
Sao chép các thuộc tính này, phương thức _loadModel
mới và dòng mới trong hàm khởi tạo:
@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();
});
}
}
Khi nào nên gọi _loadModel
? Thuộc tính này cần được gọi mỗi khi thuộc tính src thay đổi. Bằng cách trang trí thuộc tính src
bằng @property
, chúng ta đã đưa thuộc tính này vào vòng đời cập nhật phần tử chiếu sáng. Bất cứ khi nào một trong những cơ sở lưu trú được trang trí này thay đổi giá trị, một loạt phương thức được gọi có thể truy cập vào các giá trị mới và cũ của thuộc tính. Phương thức vòng đời mà chúng ta quan tâm có tên là update
. Phương thức update
lấy một đối số PropertyValues
. Đối số này sẽ chứa thông tin về bất kỳ thuộc tính nào vừa thay đổi. Đây là địa điểm hoàn hảo để gọi _loadModel
.
Thêm phương thức update
:
export class BrickViewer extends LitElement {
update(changedProperties: PropertyValues) {
if (changedProperties.has('src')) {
this._loadModel();
}
super.update(changedProperties);
}
}
Giờ đây, phần tử <brick-viewer>
của chúng ta có thể hiển thị một tệp gạch, được chỉ định bằng thuộc tính src
.
5. Hiển thị mô hình từng phần
Bây giờ, hãy làm cho bước xây dựng hiện tại có thể định cấu hình. Chúng ta muốn có thể chỉ định <brick-viewer step="5"></brick-viewer>
và chúng ta sẽ xem mô hình khối hình trông như thế nào ở bước xây dựng thứ 5. Để làm được việc đó, hãy làm cho thuộc tính step
trở thành thuộc tính quan sát được bằng cách trang trí thuộc tính đó với @property
.
Trang trí thuộc tính step
:
export class BrickViewer extends LitElement {
@property({type: Number})
step?: number;
}
Bây giờ, chúng ta sẽ thêm một phương thức trợ giúp để chỉ hiển thị những khối hình lên đến bước xây dựng hiện tại. Chúng ta sẽ gọi trình trợ giúp trong phương thức cập nhật để trình trợ giúp này chạy mỗi khi thuộc tính step
thay đổi.
Cập nhật phương thức update
và thêm phương thức _updateBricksVisibility
mới:
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);
}
}
Được rồi, bây giờ, hãy mở devtools của trình duyệt rồi kiểm tra phần tử <brick-viewer>
. Thêm thuộc tính step
vào thuộc tính đó, như sau:
Hãy xem điều gì sẽ xảy ra với mô hình được kết xuất! Chúng ta có thể sử dụng thuộc tính step
để kiểm soát phần mô hình sẽ xuất hiện. Khi đặt thuộc tính step
thành "10"
, bạn sẽ thấy như sau:
6. Điều hướng đặt gạch
nút biểu tượng mwc
Người dùng cuối của <brick-viewer>
cũng có thể di chuyển các bước xây dựng thông qua giao diện người dùng. Hãy thêm các nút để chuyển sang bước tiếp theo, bước trước và bước đầu tiên. Chúng ta sẽ sử dụng thành phần web cho nút của Material Design để dễ dàng thực hiện việc này. Vì @material/mwc-icon-button
đã được nhập nên chúng ta đã sẵn sàng thêm <mwc-icon-button></mwc-icon-button>
. Chúng ta có thể chỉ định biểu tượng mà chúng ta muốn sử dụng với thuộc tính biểu tượng, như sau: <mwc-icon-button icon="thumb_up"></mwc-icon-button>
. Bạn có thể tìm thấy tất cả các biểu tượng có thể sử dụng tại đây: material.io/resources/icons.
Hãy thêm một số nút biểu tượng vào phương thức kết xuất:
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>
`;
}
}
Việc sử dụng Material Design trên trang của chúng tôi trở nên dễ dàng nhờ các thành phần web!
Liên kết sự kiện
Những nút này thực sự có chức năng gì đó. "Trả lời" nên đặt lại bước xây dựng về 1. Cụm từ "Navigate_before" nút sẽ giảm giá trị bước xây dựng và " nút điều hướng_next" sẽ tăng nó. lit-element giúp bạn dễ dàng thêm chức năng này thông qua các liên kết sự kiện. Trong giá trị cố định của mẫu html, hãy sử dụng cú pháp @eventname=${eventHandler}
làm thuộc tính phần tử. eventHandler
hiện sẽ chạy khi phát hiện sự kiện eventname
trên phần tử đó! Ví dụ: hãy thêm trình xử lý sự kiện nhấp vào 3 nút biểu tượng:
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>
`;
}
}
Bây giờ, hãy thử nhấp vào các nút. Tốt!
Kiểu
Các nút hoạt động được nhưng trông không đẹp mắt. Tất cả chúng được tập trung ở phía dưới cùng. Hãy tạo kiểu cho chúng để phủ trên cảnh.
Để áp dụng kiểu cho các nút này, chúng ta quay lại thuộc tính static styles
. Các kiểu này có giới hạn, nghĩa là sẽ chỉ áp dụng cho các phần tử trong thành phần web này. Đó là một trong những niềm vui khi viết thành phần web: bộ chọn có thể đơn giản hơn và CSS sẽ dễ đọc và ghi hơn. Tạm biệt, BEM!
Cập nhật kiểu để chúng trông giống như sau:
export class BrickViewer extends LitElement {
static styles = css`
:host {
display: block;
position: relative;
}
#controls {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
}
`;
}
Nút đặt lại camera
Người dùng cuối trên <brick-viewer>
của chúng tôi có thể xoay cảnh bằng các nút điều khiển bằng chuột. Khi thêm các nút, hãy thêm một nút để đặt lại camera về vị trí mặc định. Một <mwc-icon-button>
khác có liên kết sự kiện nhấp chuột sẽ hoàn thành công việc.
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>
`;
}
}
Duyệt nhanh hơn
Một số khối gạch có rất nhiều bậc. Người dùng có thể muốn chuyển tới một bước cụ thể. Thêm thanh trượt có số bước có thể giúp điều hướng nhanh. Chúng ta sẽ sử dụng phần tử <mwc-slider>
cho việc này.
thanh trượt mwc
Thành phần thanh trượt cần một số dữ liệu quan trọng, chẳng hạn như giá trị thanh trượt tối thiểu và tối đa. Giá trị nhỏ nhất của thanh trượt luôn có thể là "1". Giá trị thanh trượt tối đa phải là this._numConstructionSteps
nếu mô hình đã tải. Chúng ta có thể cho <mwc-slider>
biết các giá trị này thông qua các thuộc tính của nó. Chúng ta cũng có thể sử dụng lệnh ifDefined
lit-html để tránh đặt thuộc tính max
nếu chưa xác định thuộc tính _numConstructionSteps
.
Thêm một <mwc-slider>
vào giữa nút "quay lại" và "tiến lên" các nút:
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>
`;
}
}
Dữ liệu "tăng"
Khi người dùng di chuyển thanh trượt, bước xây dựng hiện tại sẽ thay đổi và chế độ hiển thị của mô hình phải được cập nhật tương ứng. Phần tử thanh trượt sẽ phát một sự kiện đầu vào bất cứ khi nào thanh trượt được kéo. Thêm một liên kết sự kiện vào chính thanh trượt để nắm bắt sự kiện này và thay đổi bước thiết lập.
Thêm liên kết sự kiện:
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>
`;
}
}
Tuyệt vời! Chúng ta có thể dùng thanh trượt để thay đổi bước sẽ hiển thị.
Dữ liệu "bị giảm"
Tôi còn một bước nữa. Khi người dùng "quay lại" và "tiếp theo" các nút dùng để thay đổi bước, tay cầm thanh trượt cần được cập nhật. Liên kết thuộc tính giá trị của <mwc-slider>
với this.step
.
Thêm liên kết 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>
`;
}
}
Chúng ta gần như đã hoàn tất với thanh trượt. Thêm kiểu linh hoạt để chơi mượt mà với các chế độ điều khiển khác:
export class BrickViewer extends LitElement {
static styles = css`
/* ... */
mwc-slider {
flex-grow: 1;
}
`;
}
Ngoài ra, chúng ta cần gọi layout
trên chính phần tử thanh trượt. Chúng ta sẽ thực hiện việc đó trong phương thức vòng đời firstUpdated
, được gọi sau khi DOM được bố trí lần đầu. Trình trang trí query
có thể giúp chúng ta tham chiếu đến phần tử thanh trượt trong mẫu.
export class BrickViewer extends LitElement {
@query('mwc-slider')
slider!: Slider|null;
async firstUpdated() {
if (this.slider) {
await this.slider.updateComplete
this.slider.layout();
}
}
}
Dưới đây là tất cả thành phần bổ sung cho thanh trượt (với các thuộc tính pin
và markers
bổ sung trên thanh trượt để thanh trượt trông bắt mắt):
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>
`;
}
}
Đây là thành phẩm cuối cùng!
7. Kết luận
Chúng ta đã tìm hiểu nhiều về cách sử dụng phần tử lit để xây dựng phần tử HTML của riêng mình. Chúng ta đã tìm hiểu cách:
- Xác định phần tử tuỳ chỉnh
- Khai báo API thuộc tính
- Kết xuất khung hiển thị cho một phần tử tuỳ chỉnh
- Đóng gói kiểu
- Sử dụng sự kiện và thuộc tính để truyền dữ liệu
Nếu muốn tìm hiểu thêm về nguyên tố chiếu sáng, bạn có thể đọc thêm tại trang web chính thức của nguyên tố này.
Bạn có thể xem phần tử trình xem viên gạch đã hoàn tất tại stackblitz.com/edit/brick-viewer-complete.
viên gạch-viewer cũng được chuyển trên Trình quản lý nội dung đa phương tiện (N) và bạn có thể xem nguồn tại đây: Kho lưu trữ GitHub.