Chào mừng đến với Cafedev! Trong bài viết này, chúng ta sẽ khám phá cách sử dụng Vue và các thành phần Web (Web Components) cùng nhau. Vuejs không chỉ là một framework mạnh mẽ cho việc phát triển ứng dụng web, mà còn là một công cụ linh hoạt cho việc tích hợp và tạo ra các thành phần web tùy chỉnh. Hãy cùng tìm hiểu cách Vuejs và Web Components làm việc cùng nhau để tạo ra những ứng dụng web đẹp và hiệu quả!

Các thành phần Web là một thuật ngữ tổng quát chỉ một tập hợp các API nguyên thuỷ của web cho phép nhà phát triển tạo ra các phần tử tùy chỉnh có thể tái sử dụng.

Chúng tôi coi Vue và Web Components là các công nghệ bổ sung lẫn nhau. Vue có hỗ trợ tuyệt vời cho cả việc sử dụng và tạo ra các phần tử tùy chỉnh. Cho dù bạn đang tích hợp các phần tử tùy chỉnh vào một ứng dụng Vue hiện có, hoặc sử dụng Vue để xây dựng và phân phối các phần tử tùy chỉnh, bạn đều đang làm việc trong một môi trường tốt.

1. Sử Dụng Phần Tử Tùy Chỉnh trong Vue

Vue đạt điểm số hoàn hảo 100% trong các bài kiểm tra Custom Elements Everywhere. Việc sử dụng các phần tử tùy chỉnh trong một ứng dụng Vue chủ yếu hoạt động giống như sử dụng các phần tử HTML nguyên thuỷ, với một số điều cần lưu ý:

1.1 Bỏ Qua Việc Giải Quyết Thành Phần

Theo mặc định, Vue sẽ cố gắng giải quyết một thẻ HTML không phải là thẻ nguyên thuỷ như một thành phần Vue đã đăng ký trước khi rơi vào việc hiển thị nó như một phần tử tùy chỉnh. Điều này sẽ khiến Vue phát ra cảnh báo “không thể giải quyết thành phần” trong quá trình phát triển. Để thông báo cho Vue biết rằng một số phần tử nên được coi là phần tử tùy chỉnh và bỏ qua việc giải quyết thành phần, chúng ta có thể chỉ định tùy chọn compilerOptions.isCustomElement..

Nếu bạn đang sử dụng Vue với một cài đặt build, tùy chọn này nên được truyền qua cấu hình build vì nó là một tùy chọn thời gian biên dịch.

Ví Dụ Cấu Hình Trình Duyệt

// Only works if using in-browser compilation.
// If using build tools, see config examples below.
app.config.compilerOptions.isCustomElement = (tag) => tag.includes('-')

Ví Dụ Cấu Hình Vite

// vite.config.js
import vue from '@vitejs/plugin-vue'

export default {
  plugins: [
    vue({
      template: {
        compilerOptions: {
          // treat all tags with a dash as custom elements
          isCustomElement: (tag) => tag.includes('-')
        }
      }
    })
  ]
}

Ví Dụ Cấu Hình Vue CLI

// vue.config.js
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => ({
        ...options,
        compilerOptions: {
          // treat any tag that starts with ion- as custom elements
          isCustomElement: tag => tag.startsWith('ion-')
        }
      }))
  }
}

1.2 Truyền Các Thuộc Tính DOM

Vì các thuộc tính DOM chỉ có thể là chuỗi, chúng ta cần truyền dữ liệu phức tạp cho các phần tử tùy chỉnh dưới dạng các thuộc tính DOM. Khi đặt các props cho một phần tử tùy chỉnh, Vue 3 tự động kiểm tra sự hiện diện của thuộc tính DOM bằng toán tử in và sẽ ưu tiên đặt giá trị như một thuộc tính DOM nếu khóa có mặt. Điều này có nghĩa là, trong hầu hết các trường hợp, bạn sẽ không cần phải suy nghĩ về điều này nếu phần tử tùy chỉnh tuân theo các quy ước tốt nhất được khuyến nghị.

Tuy nhiên, có thể có các trường hợp hiếm khi dữ liệu phải được truyền dưới dạng thuộc tính DOM, nhưng phần tử tùy chỉnh không định nghĩa/hiển thị đúng thuộc tính (gây ra việc kiểm tra in thất bại). Trong trường hợp này, bạn có thể buộc một v-bind binding được đặt làm một thuộc tính DOM bằng cách sử dụng modifier .prop:

<my-element :user.prop="{ name: 'jack' }"></my-element>

<!-- shorthand equivalent -->
<my-element .user="{ name: 'jack' }"></my-element>

2. Xây Dựng Phần Tử Tùy Chỉnh với Vue

Lợi ích chính của các phần tử tùy chỉnh là chúng có thể được sử dụng với bất kỳ framework nào, hoặc thậm chí là không cần framework. Điều này khiến chúng trở nên lý tưởng cho việc phân phối các thành phần trong trường hợp người tiêu dùng cuối có thể không sử dụng cùng một stack frontend, hoặc khi bạn muốn cách ly ứng dụng cuối khỏi các chi tiết triển khai của các thành phần mà nó sử dụng.

2.1 defineCustomElement

Vue hỗ trợ tạo ra các phần tử tùy chỉnh bằng cách sử dụng chính xác các API thành phần Vue thông qua phương thức defineCustomElement. Phương thức này chấp nhận cùng một đối số như defineComponent, nhưng thay vào đó trả về một constructor phần tử tùy chỉnh mở rộng từ HTMLElement:

<my-vue-element></my-vue-element>
import { defineCustomElement } from 'vue'

const MyVueElement = defineCustomElement({
  // normal Vue component options here
  props: {},
  emits: {},
  template: `...`,

  // defineCustomElement only: CSS to be injected into shadow root
  styles: [`/* inlined css */`]
})

// Register the custom element.
// After registration, all `<my-vue-element>` tags
// on the page will be upgraded.
customElements.define('my-vue-element', MyVueElement)

// You can also programmatically instantiate the element:
// (can only be done after registration)
document.body.appendChild(
  new MyVueElement({
    // initial props (optional)
  })
)

Chu kỳ sống

  • Một phần tử tùy chỉnh Vue sẽ gắn một instance thành phần Vue nội bộ vào trong gốc bóng của nó khi phương thức connectedCallback của phần tử được gọi lần đầu tiên.
  • Khi disconnectedCallback của phần tử được gọi, Vue sẽ kiểm tra xem phần tử có bị tách ra khỏi tài liệu sau mỗi microtask tick không.
  • Nếu phần tử vẫn còn trong tài liệu, đó là một di chuyển và instance của thành phần sẽ được bảo tồn;
  • Nếu phần tử đã được tách ra khỏi tài liệu, đó là một loại loại bỏ và instance của thành phần sẽ được hủy.

Props

  • Tất cả props được khai báo bằng cách sử dụng tùy chọn props sẽ được định nghĩa trên phần tử tùy chỉnh như là các thuộc tính. Vue sẽ tự động xử lý việc phản ánh giữa thuộc tính/properties khi cần thiết.
  • Các thuộc tính luôn được phản ánh vào các thuộc tính tương ứng.
  • Các thuộc tính có giá trị nguyên thủy (string, boolean hoặc number) được phản ánh dưới dạng thuộc tính.
  • Vue cũng tự động ép kiểu các props được khai báo với kiểu Boolean hoặc Number thành kiểu mong muốn khi chúng được đặt làm các thuộc tính (luôn là chuỗi). Ví dụ, với việc khai báo props như sau:
props: {
  selected: Boolean,
  index: Number
}

<my-element selected index="1"></my-element>

Sự kiện

Các sự kiện được phát qua this.$emit hoặc emit được thiết lập sẽ được gửi đi như các CustomEvents native trên phần tử tùy chỉnh. Các đối số sự kiện bổ sung (payload) sẽ được tiết lộ dưới dạng một mảng trên đối tượng CustomEvent qua thuộc tính detail của nó.

Slots

Bên trong thành phần, các khe cắm có thể được hiển thị bằng cách sử dụng phần tử như thường lệ. Tuy nhiên, khi tiêu thụ phần tử kết quả, nó chỉ chấp nhận cú pháp khe cắm native:
Khe cắm phạm vi không được hỗ trợ.
– Khi chuyển các khe cắm được đặt tên, hãy sử dụng thuộc tính slot thay vì chỉ thị v-slot:

<my-element>
    <div slot="named">hello</div>
  </my-element>

Cung Cấp / Tiêm

API Cung Cấp / Tiêm và phiên bản tương đương API Hợp Thành của nó cũng hoạt động giữa các phần tử tùy chỉnh được định nghĩa bởi Vue. Tuy nhiên, lưu ý rằng điều này chỉ hoạt động chỉ giữa các phần tử tùy chỉnh. Nghĩa là một phần tử tùy chỉnh được định nghĩa bởi Vue sẽ không thể tiêm các thuộc tính được cung cấp bởi một thành phần Vue không phải là phần tử tùy chỉnh.

2.2 SFC như Một Phần Tử Tùy Chỉnh

Hàm defineCustomElement cũng hoạt động với các Single-File Components (SFCs) của Vue. Tuy nhiên, với thiết lập công cụ mặc định, các thẻ <style> bên trong các SFC vẫn sẽ được trích xuất và hợp nhất vào một tệp CSS duy nhất trong quá trình xây dựng sản phẩm cuối.

Khi sử dụng một SFC như một phần tử tùy chỉnh, thường mong muốn làm cho các thẻ <style> được chèn vào shadow root của phần tử tùy chỉnh thay vì được trích xuất ra ngoài.

Các công cụ SFC chính thức hỗ trợ việc nhập SFC trong “chế độ phần tử tùy chỉnh” (yêu cầu @vitejs/plugin-vue@^1.4.0 hoặc vue-loader@^16.5.0). Một SFC được tải trong chế độ phần tử tùy chỉnh sẽ nhúng các thẻ <style> như chuỗi CSS và tiết lộ chúng dưới tùy chọn styles của thành phần. Điều này sẽ được defineCustomElement nhận và chèn vào shadow root của phần tử khi được tạo ra.

Để chọn vào chế độ này, đơn giản là kết thúc tên tệp của bạn bằng .ce.vue:

import { defineCustomElement } from 'vue'
import Example from './Example.ce.vue'

console.log(Example.styles) // ["/* inlined css */"]

// convert into custom element constructor
const ExampleElement = defineCustomElement(Example)

// register
customElements.define('my-example', ExampleElement)

2.3 Một số gợi ý khi xây dựng thư viện Phần tử Tùy chỉnh Vue​:

Khi xây dựng các phần tử tùy chỉnh với Vue, các phần tử sẽ phụ thuộc vào runtime của Vue. Có một chi phí cơ bản khoảng ~16kb, phụ thuộc vào số lượng tính năng đang được sử dụng. Điều này có nghĩa là không lý tưởng khi sử dụng Vue nếu bạn đang gửi một phần tử tùy chỉnh duy nhất – bạn có thể muốn sử dụng JavaScript nguyên thuần, petite-vue, hoặc các framework chuyên về kích thước runtime nhỏ. Tuy nhiên, kích thước cơ bản là hợp lý nếu bạn đang gửi một bộ sưu tập các phần tử tùy chỉnh với logic phức tạp, vì Vue sẽ cho phép mỗi thành phần được tạo ra với ít mã hơn nhiều. Càng nhiều phần tử bạn gửi cùng nhau, thì trao đổi càng tốt.

Nếu các phần tử tùy chỉnh sẽ được sử dụng trong một ứng dụng cũng sử dụng Vue, bạn có thể chọn tách Vue ra khỏi gói xây dựng để các phần tử sẽ sử dụng bản sao Vue từ ứng dụng chủ.

Nó được khuyến khích xuất các hàm xây dựng phần tử riêng lẻ để cung cấp linh hoạt cho người dùng của bạn để nhập chúng theo yêu cầu và đăng ký chúng với tên thẻ mong muốn. Bạn cũng có thể xuất một hàm tiện ích để tự động đăng ký tất cả các phần tử. Dưới đây là một ví dụ về điểm nhập của một thư viện phần tử tùy chỉnh Vue:

import { defineCustomElement } from 'vue'
import Foo from './MyFoo.ce.vue'
import Bar from './MyBar.ce.vue'

const MyFoo = defineCustomElement(Foo)
const MyBar = defineCustomElement(Bar)

// export individual elements
export { MyFoo, MyBar }

export function register() {
  customElements.define('my-foo', MyFoo)
  customElements.define('my-bar', MyBar)
}

Nếu bạn có nhiều thành phần, bạn cũng có thể tận dụng các tính năng của các công cụ xây dựng như Vite’s glob import hoặc webpack’s require.context để tải tất cả các thành phần từ một thư mục.

Web Components và TypeScript

Nếu bạn đang phát triển một ứng dụng hoặc một thư viện, bạn có thể muốn kiểm tra kiểu cho các thành phần Vue của mình, bao gồm cả những thành phần được định nghĩa là các phần tử tùy chỉnh.

Các phần tử tùy chỉnh được đăng ký toàn cục bằng cách sử dụng các API nguyên thủy, vì vậy theo mặc định chúng sẽ không có thông tin về kiểu khi sử dụng trong các mẫu Vue. Để cung cấp hỗ trợ kiểu cho các thành phần Vue được đăng ký như các phần tử tùy chỉnh, chúng ta có thể đăng ký các kiểu toàn cục cho thành phần bằng cách sử dụng giao diện GlobalComponents trong các mẫu Vue và/hoặc trong JSX:

import { defineCustomElement } from 'vue'

// vue SFC
import CounterSFC from './src/components/counter.ce.vue'

// turn component into web components
export const Counter = defineCustomElement(CounterSFC)

// register global typings
declare module 'vue' {
  export interface GlobalComponents {
    'Counter': typeof Counter,
  }
}

3. Web Components vs. Vue Components

Cuộc tranh luận giữa việc sử dụng các thành phần dựa trên framework và việc sử dụng các phần tử tùy chỉnh của Web Components luôn được nhắc đến. Một số nhà phát triển tin rằng các mô hình thành phần thuộc về framework nên tránh xa, và việc chỉ sử dụng Các phần tử Tùy chỉnh sẽ làm cho ứng dụng “đảm bảo tương lai”. Dưới đây là lý do tại sao chúng tôi cho rằng đây là một quan điểm quá đơn giản về vấn đề.

Thực sự có một mức độ trùng lặp tính năng giữa Các phần tử Tùy chỉnh và Các thành phần Vue: cả hai đều cho phép chúng ta định nghĩa các thành phần có thể tái sử dụng với việc truyền dữ liệu, phát ra sự kiện và quản lý vòng đời. Tuy nhiên, các API của Các phần tử Tùy chỉnh có độ cấp thấp và cơ bản. Để xây dựng một ứng dụng thực sự, chúng ta cần một số tính năng bổ sung mà nền tảng không hỗ trợ:

Hệ thống templating phổ cập và hiệu quả;

Hệ thống quản lý trạng thái phản ứng mà giúp cho việc trích xuất và tái sử dụng logic giữa các thành phần một cách dễ dàng;

Một cách hiệu quả để render các thành phần trên máy chủ và hydrate chúng trên máy khách (SSR), điều quan trọng cho SEO và các chỉ số về Web Vitals như LCP. SSR cho các phần tử tùy chỉnh tại cơ bản liên quan đến mô phỏng DOM trong Node.js và sau đó là việc tuần tự hóa DOM đã thay đổi, trong khi SSR của Vue biên dịch thành chuỗi khi có thể, điều này hiệu quả hơn nhiều.

Mô hình thành phần của Vue được thiết kế với những nhu cầu này trong tâm trí như một hệ thống nhất quán.

Với một đội ngũ kỹ sư có năng lực, có thể bạn có thể xây dựng được một hệ thống tương đương dựa trên Các phần tử Tùy chỉnh nguyên thuỷ – nhưng điều này cũng đồng nghĩa với việc bạn phải đảm nhận gánh nặng duy trì dài hạn của một framework nội bộ, trong khi mất đi những lợi ích về hệ sinh thái và cộng đồng của một framework chín muối như Vue.

Cũng có các framework được xây dựng bằng cách sử dụng Các phần tử Tùy chỉnh làm cơ sở cho mô hình thành phần của họ, nhưng tất cả đều phải giới thiệu các giải pháp độc quyền của riêng họ cho các vấn đề được liệt kê ở trên. Việc sử dụng các framework này đồng nghĩa với việc phải chấp nhận các quyết định kỹ thuật của họ về cách giải quyết các vấn đề này – điều này, mặc dù có thể được quảng cáo, nhưng không đảm bảo làm cho bạn tránh khỏi những thay đổi tiềm tàng trong tương lai.

Cũng có một số lĩnh vực mà chúng tôi thấy các phần tử tùy chỉnh có hạn chế:

Việc đánh giá khe slot một cách tức thì làm hạn chế việc sự kết hợp thành phần. Các khe slot có phạm vi của Vue là một cơ chế mạnh mẽ cho việc kết hợp thành phần, mà không thể được hỗ trợ bởi các phần tử tùy chỉnh do tính tức thì của khe slot nguyên thuỷ. Khe slot tức thì cũng có nghĩa là thành phần nhận không thể kiểm soát khi nào hoặc liệu có hiển thị một phần nội dung của khe slot.

Việc vận chuyển các phần tử tùy chỉnh với CSS phạm vi trong shadow DOM ngày nay đòi hỏi nhúng CSS vào trong JavaScript để chúng có thể được chèn vào các gốc shadow khi chạy. Chúng cũng dẫn đến các phong cách trùng lặp trong markup trong các kịch bản SSR. Hiện có các tính năng nền tảng đang được làm việc trong lĩnh vực này – nhưng đến thời điểm này chúng vẫn chưa được hỗ trợ phổ biến, và vẫn còn những lo ngại về hiệu suất sản xuất / SSR cần được giải quyết. Trong khi đó, SFCs của Vue cung cấp các cơ chế phạm vi CSS hỗ trợ việc trích xuất các phong cách vào các tệp CSS thuần túy.

Vue luôn sẽ cập nhật với các tiêu chuẩn mới nhất trong nền tảng web, và chúng tôi sẽ sẵn lòng tận dụng bất kỳ điều gì nền tảng cung cấp nếu nó làm cho công việc của chúng tôi dễ dàng hơn. Tuy nhiên, mục tiêu của chúng tôi là cung cấp các giải pháp hoạt động tốt và hoạt động ngay bây giờ. Điều đó có nghĩa là chúng tôi phải tích hợp các tính năng nền tảng mới với một tư duy phê phán – và điều đó liên quan đến việc lấp đầy những khoảng trống mà các tiêu chuẩn vẫn chưa đáp ứng được.

Trên đây là một cái nhìn tổng quan về cách Vuejs và các thành phần Web (Web Components) có thể hoạt động cùng nhau để tạo ra các ứng dụng web mạnh mẽ và linh hoạt. Tại Cafedev, chúng tôi tin rằng sự kết hợp giữa Vue và Web Components không chỉ mở ra nhiều cơ hội sáng tạo mà còn tăng cường khả năng tái sử dụng và tính linh hoạt của ứng dụng. Hy vọng bạn đã có cái nhìn rõ ràng hơn về cách kết hợp hai công nghệ này để xây dựng các ứng dụng web đáng kinh ngạc!

Tham khảo thêm: MIỄN PHÍ 100% | Series tự học Vuejs từ cơ bản tới nâng cao

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!