Hướng dẫn về Web Components

Bài viết được dịch từ bài gốc: A Guide to Web Components của tác giả Rob Dodson, được đăng trên trang CSS-Tricks.


Bài viết dưới đây là của một vị khách, Rob Dodson (@rob_dodson). Rob và tôi đã thảo luận về cách CodePen hỗ trợ để Polymer (một web components polyfill, đại loại vậy) hoạt động trên demo của anh ấy. Chúng tôi đã làm cho nó hoạt động được, và những thứ đó đã trở thành bài viết này. Hãy bắt đầu nào Rob.

Cập nhật: Rob cập nhật bài viết này vào ngày 5/3/2014, cập nhật mọi thứ, do đây là một công nghệ biến đổi khá nhanh vào thời điểm hiện tại.

Cập nhật: Cập nhật lại vào ngày 9/9/2014!


Gần đây tôi làm việc với một khách hàng qua công việc đào tạo nhóm phát triển của họ cách xây dựng ứng dụng web. Trong quá trình đó tôi thấy rằng cách chúng tôi xây dựng kiến trúc phía front-end khá là lạ và có chút sai sót. Trong nhiều trường hợp bạn phải chép một đống HTML lớn từ tài liệu nào đó và dán chúng vào trong ứng dựng của bạn (Bootstrap, Foundation, v..v..), hay bạn rải lên trang những plugin jQuery phải được cấu hình bằng JavaScript. Điều đó đẩy chúng tôi vào tình huống phải chọn giữa những đoạn HTML cồng kềnh hay những đoạn HTML bí ẩn, và thường thì chúng tôi chọn cả hai.

Trong một trường hợp lý tưởng, ngôn ngữ HTML đủ biểu đạt để có thể tạo ra các widget UI phức tạp và còn có thể mở rộng được để chúng ta, các lập trình viên, có thể lấp đầy mọi khoảng trống với các thẻ của chính mình. Ngày nay, điều này cuối cùng đã có thể thực hiện được thông qua một tập tiêu chuẩn mới được gọi là Web Components.

Web Components?

Web Components là một tập các tiêu chuẩn đang được thiết lập bởi W3C và được triển khai trên trình duyệt trong lúc chúng ta đang nói về nó. Nói đơn giản, chúng cho phép chúng ta nhóm các markup và style thành các phần tử HTML của riêng mình. Điều đáng ngạc nhiên về những phần tử mới này là chúng hoàn toàn đóng gói tất cả HTML và CSS của chúng. Nghĩa là style mà bạn viết sẽ luôn được hiển thị như bạn mong muốn, và HTML của bạn sẽ được an toàn khỏi những con mắt tò mò của code JavaScript bên ngoài.

Nếu bạn muốn thử nghịch với Web Components tự nhiên (native Web Components) thì tôi khuyên bạn nên dùng Chrome do trình duyệt này hỗ trợ tốt nhất. Chrome phiên bản 36 là trình duyệt đầu tiên hiện thực hóa tất cả các chuẩn mới.

Ví dụ

Tưởng tượng rằng bạn đang làm một image slider, nó có thể trông như sau:

<div id="slider">
  <input checked="" type="radio" name="slider" id="slide1" selected="false">
  <input type="radio" name="slider" id="slide2" selected="false">
  <input type="radio" name="slider" id="slide3" selected="false">
  <input type="radio" name="slider" id="slide4" selected="false">
  <div id="slides">
    <div id="overflow">
      <div class="inner">
        <img src="images//rock.jpg">
        <img src="images/grooves.jpg">
        <img src="images/arch.jpg">
        <img src="images/sunset.jpg">
      </div>
    </div>
  </div>
  <label for="slide1"></label>
  <label for="slide2"></label>
  <label for="slide3"></label>
  <label for="slide4"></label>
</div>

Image slider được sửa đổi từ CSScience. Những bức ảnh nhã nhặn của Eliya Selhub

Đó là một đoạn code HTMl trang nhã, và chúng ta thậm chí còn chưa thêm CSS vào nữa! Nhưng thử tưởng tượng rằng chúng ta có thể loại bỏ tất cả nhưng đoạn code phức tạp và tối giản nó để chỉ còn những phần quan trọng. Nó sẽ trông như thế nào?

<img-slider>
  <img src="images/sunset.jpg" alt="a dramatic sunset">
  <img src="images/arch.jpg" alt="a rock arch">
  <img src="images/grooves.jpg" alt="some neat grooves">
  <img src="images/rock.jpg" alt="an interesting rock">
</img-slider>

Trông không quá tệ! Chúng ta đã bỏ đi những phần chung và phần code còn lại là những thứ mà chúng ta quan tâm. Đó là thứ mà Web Components cho phép chúng ta làm. Nhưng trước khi tôi đi sâu vào chi tiết, tôi sẽ kể cho bạn một câu chuyện khác.

Ẩn trong bóng tối

Trong nhiều năm, những người làm trình duyệt web có một mẹo bí mật. Hãy nhìn thẻ <video> này và nghĩ về tất cả những thứ được hiển thị ra mà bạn có được chỉ với một dòng HTML.

<video src="./foo.webm" controls></video>

Có một nút play, một thanh tua, thời gian và thanh điều chỉnh âm lượng. Rất nhiều thứ mà bạn không phải viết markup cho chúng, nó hiện ra khi bạn sử dụng <video>.

Nhưng thứ bạn nhìn thấy thực ra chỉ là một ảo ảnh. Những người làm trình duyệt cần một cách để đảm bảo rằng các thẻ mà họ hiện thực hóa sẽ luôn hiển thị như nhau, bất kể có những đoạn HTML, CSS hay JavaScript kỳ quặc nào xuất hiện trên trang của chúng ta. Để làm điều đó, họ tạo ra một lối đi bí mật nơi họ có thể giấu code của mình và giữ nó tránh khỏi những bàn tay nghịch ngợm. Họ gọi nơi bí mật đó là: Shadow DOM.

Nếu bạn đang dùng Google Chrome thì bạn có thể mở Developer Tools và bật tính năng Show user agent shadow DOM. Việc đó sẽ cho phép bạn xem phần tử <video> một cách chi tiết hơn.

Bạn sẽ thấy bên trong nó là một đống HTML được giấu đi. Xem xét xung quanh đủ lâu bạn sẽ thấy nút play, thanh chỉnh âm lượng được nói ở trên và rất nhiều phần tử khác.

Giờ thì, hãy nhớ lại cái image slider của chúng ta. Sẽ ra sao nếu tất cả chúng ta đều có quyền truy cập đến shadow DOM và khả năng tạo ra các thẻ của riêng mình như thẻ <video>? Chúng ta sẽ có thể thực sự tạo và sử dụng thẻ <img-slider> của mình.

Template

Mọi dự án được cấu trúc tốt đều bắt đầu với một bản thiết kế, và với Web Components thì bản thiết kế đó là thẻ <template>. Thẻ <template> cho phép bạn chứa một số markup trên trang và bạn có thể nhân bản và tái sử dụng chúng sau này. Nếu bạn đã từng làm việc với các thư viện như mustache hay handlebars, bạn sẽ cảm thấy quen thuộc với thẻ <template> này.

<template>
  <h1>Hello there!</h1>
  <p>This content is top secret :)</p>
</template>

Mọi thứ bên trong một template sẽ được coi là trơ bởi trình duyệt. Điều đó nghĩa là các thẻ với các tài nguyên bên ngoài - <img>, <audio>, <video>, v..v.. - sẽ không phát sinh các request http và các thẻ <script> sẽ không được thực thi. Điều đó cũng có nghĩa là không có thứ gì bên trong template được hiển thị lên trên trang cho đến khi chúng ta kích hoạt nó bằng JavaScript.

Vậy nên bước đầu tiên trong việc tạo ra thẻ <img-slider> của chúng ta là đặt tất cả HTML và CSS của nó vào trong một <template>.

Khi chúng ta làm xong việc này, chúng ta đã sẵn sàng chuyển qua shadow DOM.

Shadow DOM

Để thực sự chắc chắn rằng HTML và CSS của chúng ta không ảnh hưởng bất lợi tới những nơi dùng chúng, đôi lúc chúng ta dùng đến iframe. Cách này hoạt động, nhưng bạn sẽ không muốn xây dựng toàn bộ ứng dụng với chúng.

Shadow DOM cung cấp cho chúng ta những tính năng tuyệt vời nhất của iframe, style và markup được đóng gói, mà không cồng kềnh cho lắm.

Để tạo shadow DOM, chọn một phần tử và gọi phương thức createShadowRoot của nó. Lời gọi này sẽ trả về một phần document mà bạn có thể điền nội dung vào đó.

<div class="container"></div>

<script>
  var host = document.querySelector('.container');
  var root = host.createShadowRoot();
  root.innerHTML = '<p>How <em>you</em> doin?</p>'
</script>

Shadow Host

Trong thuật ngữ của shadow DOM, phần tử mà bạn gọi createShadowRoot trên nó được gọi là Shadow Host. Nó là phần duy nhất được nhìn thấy bởi người dùng, và đó là nơi mà bạn sẽ yêu cầu người dùng thêm nội dung vào.

Nếu bạn nhớ lại thẻ <video> của chúng ta, bản thân phần tử <video> là shadow host, và nội dung là các thẻ con bạn thêm vào bên trong nó.

<video>
  <source src="trailer.mp4" type="video/mp4">
  <source src="trailer.webm" type="video/webm">
  <source src="trailer.ogv" type="video/ogg">
</video>

Shadow Root

Phần document được trả về bởi createShadowRoot được gọi là Shadow Root. Shadow root và các thành phần con của nó được ẩn giấu khỏi người dùng, nhưng nó là thứ mà trình duyệt thực sự hiển thị khi nó thấy thẻ của chúng ta.

Trong ví dụ về thẻ <video>, nút play, thanh tua, thời gian hiển thị, v..v.. đều là thành phần con của shadow root. Chúng được hiển thị trên màn hình nhưng markup của chúng không được nhìn thấy bởi người dùng.

Shadow Boundary

Bất cứ code HTML và CSS nào bên trong shadow root đều được bảo vệ khỏi document cha bởi một rào chắn vô hình được gọi là Shadow Boundary. Shadow boundary ngăn CSS của document cha tác động vào bên trong shadow DOM, và nó cũng ngăn JavaScript bên ngoài truy xuất đến nội dung bên trong shadow root.

Dịch nghĩa: giả sử bạn có một thẻ style bên trong shadow DOM quy định tất cả các thẻ h3 đều có color là đỏ. Cùng lúc, trong document cha, bạn có style quy đinh các thẻ h3 có color màu xanh dương. Trong trường hợp này, các thẻ h3 xuất hiện bên trong shadow DOM sẽ có chữ màu đỏ và các thẻ h3 bên ngoài shadow DOM sẽ có chữ màu xanh dương. Hai style sẽ vui vẻ bỏ qua nhau nhờ người bạn của chúng ta, shadow boundary.

Và giả sử, một lúc nào đó, document cha tìm kiếm các thẻ h3 với $('h3'), shadow boundary sẽ ngăn bất cứ sự thâm nhập nào vào trong shadow root và việc tìm kiếm trên sẽ chỉ trả về các thẻ h3 nằm bên ngoài shadow DOM.

Cấp độ riêng tư này là thứ mà chúng ta từng mơ đến và tìm cách đạt được trong nhiều năm. Nói rằng thứ này sẽ thay đổi cách chúng ta xây dựng ứng dụng web hoàn toàn không ngoa.

Shadowy Sliders

Để đưa img-slider của chúng ta vào trong shadow DOM, chúng ta sẽ phải tạo một shadow host và điền nội dung template của chúng ta vào.

<template>
  <!-- Full of slider awesomeness -->
</template>

<div class="img-slider"></div>

<script>
  // Add the template to the Shadow DOM
  var tmpl = document.querySelector('template');
  var host = document.querySelector('.img-slider');
  var root = host.createShadowRoot();
  root.appendChild(document.importNode(tmpl.content, true));
</script>

Trong ví dụ này chúng ta tạo một div và gán class img-slider cho nó để nó làm shadow host.

Chúng ta chọn template và sao chép sâu nội dung của nó với lệnh document.importNode. Nội dung đó sẽ được thêm vào shadow root mới được tạo. của chúng ta.

Nếu bạn đang dùng Chrome, bạn có thể thấy code trên hoạt động trong pen sau đây:

Insertion points

Lúc này img-slider của chúng ta đã nằm trong shadow DOM nhưng những đường dấn của ảnh đang được chỉ định một cách cố định. Giống như thẻ <source> nằm trong thẻ <video>, chúng ta cũng muốn các ảnh được chỉ định bởi người dùng, vậy nên chúng ta sẽ lấy chúng từ shadow host.

Để đưa item vào shadow DOM, chúng ta sẽ sử dụng một thẻ mới là <content>. Thẻ <content> sử dụng CSS selector để chọn lấy các phần tử từ shadow host và ánh xạ chúng vào bên trong shadow DOM. Các anh xạ này được biết đến như là các insertion point .

Chúng ta sẽ đơn giản giả sử slider chỉ chứa ảnh, do vậy chúng ta có thể tạo một insertion point với img selector.

<template>
  ...
  <div class="inner">
    <content select="img"></content>
  </div>
</template>

Vì chúng ta ánh xạ nội dung vào shadow DOM bằng một insertion point nên chúng ta cũng phải sử dụng một pseudo-element mới là ::content để cập nhật CSS.

#slides ::content img {
  width: 25%;
  float: left;
}

Nếu bạn muốn biết thêm về các selector và combinator mới của CSS được thêm bởi Shadow DOM, hay xem ở bản tham khảo tôi đã tổng hợp này.

Giờ chúng ta đã sẵn sàng điền nội dung vào img-slider.

<div class="img-slider">
  <img src="images/rock.jpg" alt="an interesting rock">
  <img src="images/grooves.jpg" alt="some neat grooves">
  <img src="images/arch.jpg" alt="a rock arch">
  <img src="images/sunset.jpg" alt="a dramatic sunset">
</div>

Điều này thật tuyệt! Chúng ta đã giảm bớt lượng markup mà người dùng nhìn thấy. Nhưng sao lại dừng ở đây nhỉ? Chúng ta có thể tiếp tục và biến img-slider thành một thẻ riêng.

Custom Elements

Tạo một phần tử HTML của riêng bạn nghe có vẻ hơi khó khăn, nhưng thực ra khá dễ. Trong thuật ngữ của Web Components, phần tử mới đó được gọi là một Custom Element, và chỉ có hai yêu cầu duy nhất là tên của nó phải chứa một dấu gạch ngang và prototype của nó phải được kế thừa từ HTMLElement.

Hãy xem việc đó được thực hiện như thế nào.

<template>
  <!-- Full of image slider awesomeness -->
</template>

<script>
  // Grab our template full of slider markup and styles
  var tmpl = document.querySelector('template');

  // Create a prototype for a new element that extends HTMLElement
  var ImgSliderProto = Object.create(HTMLElement.prototype);

  // Setup our Shadow DOM and clone the template
  ImgSliderProto.createdCallback = function() {
    var root = this.createShadowRoot();
    root.appendChild(document.importNode(tmpl.content, true));
  };

  // Register our new element
  var ImgSlider = document.registerElement('img-slider', {
    prototype: ImgSliderProto
  });
</script>

Phương thức Object.create trả về một prototype mới kế thừa từ HTMLElement. Khi chương trình phân tích tìm thấy thẻ của chúng ta trong document, nó sẽ kiểm tra xem liệu phần tử đó có phương thức tên là createdCallback hay không. Nếu trình phân tích thấy phương thức này thì nó sẽ chạy phương thức đó ngay lập tức. Phương thức đó là một chỗ tốt để thực hiện các công việc thiết lập, nên chúng ta sẽ tạo Shadow DOM và sao chép template của chúng ta vào bên trong shadow DOM đó.

Chúng ta truyền tên và prototype của thẻ vào một phương thức mới của document, tên là registerElement, và sau đó chúng ta đã xong.

Giờ phần tử của chúng ta đã được đăng ký và chúng ta có thể sử dụng chúng theo một số cách khác nhau. Cách thứ nhất, và đơn giản nhất, là sử dụng thẻ <img-slider> ở đâu đó trong HTML của chúng ta. Chúng ta cũng có thể gọi document.createElement("img-slider") hay sử dụng hàm khởi tạo được trả về bởi document.registerElement và lưu nó vào biến ImgSlider. Tùy thuộc vào cách mà bạn muốn dùng.

Hỗ trợ

Việc hỗ trợ các tiêu chuẩn xây dựng nên Web Components đang được khuyến khích và luôn được cải thiện. Bảng sau minh họa trạng thái hiện tại mà chúng ta có.

Đừng để sự thiếu hụt hỗ trợ của một số trình duyệt khiến bạn nản lòng khi sử dụng Web Components. Những người thông minh ở Mozilla và Google đã cố gắng xây dựng các thư viện polyfill giúp hỗ trợ cho Web Components trên **tất cả các trình duyệt hiện đại**! Điều đó có nghĩa là bạn có thể bắt đầu nghịch với công nghệ này ngay hôm nay và gửi phản hồi cho những người đang viết đặc tả. Những phản hồi đó là điều quan trọng giúp chúng ta không phải sử dụng những cú pháp xấu và khó sử dụng.

Hãy cùng xem cách chúng ta viết lại img-slider bằng cách sử dụng thư viện Web Component của Google, Polymer.

Polymer tới giải cứu!

Polymer thêm một thẻ mới vào trình duyệt, <polymer-elements>, thứ tự động biến đổi template thành shadow DOM và đăng ký các custom element cho chúng ta. Tất cả những gì chúng ta cần làm là báo cho Polymer tên của tag và đảm bảo rằng chúng ta đã khai báo template markup.

Tôi thấy việc tạo phần tử bằng Polymer dễ dàng hơn vì tất cả những tiện ích được tích hợp sẵn trong thư viện. Nó chứa liên kết hai chiều giữa phần tử và model, tự động tìm node và hỗ trợ cho các chuẩn mới như Web Animations. Ngoài ra, các lập trình viên trên polymer-dev mailing list cực kỳ tích cực và hữu ích, việc này rất tốt khi bạn tìm hiểu lần đầu, và cộng đồng StackOverflow thì đang phát triển.

Đây chỉ là một ví dụ nhỏ về thứ mà Polymer có thể làm, vậy nên hãy đến thăm trang của dự án và xem bản thay thế của Mozilla, X-Tag.

Vấn đề

Bất cứ chuẩn mới nào cũng có thể gây tranh cãi và trong trường hợp của Web Components thì có vẻ rất mâu thuẫn. Trước khi tôi tóm gọn lại, tôi muốn thảo luận một số phản hồi mà tôi nghe được trong vài tháng qua và đưa ra ý kiến của mình.

Ôi trời đó là XML!!!

Tôi nghĩ thứ có thể khiến hầu hết các lập trình viên sợ hãi khi lần đầu thấy Custom Element là ký pháp vì nó biến document thành một chồng lớn XML, nơi mọi thứ trên trang có tên thẻ riêng và, theo cách này, chúng ta làm cho web trở nên cực kỳ khó đọc. Đó là ý kiến tranh luận đúng nên tôi quyết định chọc tổ ong và đưa nó lên Polymer mailing list.

Việc tranh luận qua lại khá thú vị nhưng tôi nghĩ điều nhất trí là chúng ta sẽ thử nghiệm xem thứ gì hoạt động và thứ gì không. Liệu việc nhìn thấy một tên thẻ như <img-slider> có tốt hơn và mang ngữ nghĩa hơn hay chúng ta sẽ có một "nồi lẩu các thẻ div"? Alex Rusell đã viết ra một bài viết chứa nhiều suy nghĩ về chủ đề này và tôi khuyên mọi người nên dành thời gian đọc nó trước khi định hình suy nghĩ của riêng mình.

SEO

Ở thời điểm hiện tại, vẫn chưa rõ là các crawlers hỗ trợ Custom Elements và Shadow DOM tới mức nào. Polymer FAQ phát biểu:

Các engine tìm kiếm đã đối phó với các ứng dụng dựa nhiều vào AJAX trong một khoảng thời gian. Rời bỏ JS và chuyển sang một thứ mang tính mô tả hơn là một điều tốt và thông thường sẽ làm mọi thứ tốt hơn.

Blog của Google Webmaster gần đây thông báo rằng Google crawler sẽ chạy JavaScript trên trang của bạn trước khi đánh chỉ mục nó. Và sử dụng các công cụ như Fetch as Google sẽ cho phép bạn thấy những gì mà crawler thấy khi nó phân tích trang của bạn. Một ví dụ tốt là trang của Polymer, được xây dựng với các custom element và dễ dàng được tìm thấy trên Google.

Một điều tôi đã học được khi nói chuyện với các thành viên của Polymer team là cố gằng đảm bảo rằng nội dung bên trong custom element của bạn là nội dung tĩnh, không được sinh ra do liên kết dữ liệu (data binding).

<!-- probably good -->
<x-foo>
  Here is some interesting, and searchable content...
</x-foo>

<!-- probably bad -->
<x-foo>
  {{crazyDynamicContent}}
</x-foo>

<!-- also probably bad -->
<a href="{{aDynamicLink}}">Click here</a>

Công bằng mà nói, đây không phải là một vấn đề mới. Các trang dựa nhiều vào AJAX đã đối phó với vấn đề này được một vài năm và may mắn là đã có giải pháp.

Khả năng truy cập

Chắc chắn là khi bạn giấu markup trong shadow DOM thì vấn đề về khả năng truy cập là rất quan trọng. Steve Faulkner xem xét khả năng truy cập trong shadow DOM và có vẻ hài lòng với những gì anh ấy thấy.

Kết quả kiểm tra ban đầu cho thấy việc thêm các role, state, property ARIA vào nội dung trong Shadow DOM hoạt động tốt. Khả năng truy cập thông tin được để lộ ra thông qua API truy cập. Các chương trình đọc màn hình có thể truy cập nội dung bên trong Shadow DOM mà không gặp vấn đề gì.

Bài viết đầy đủ ở đây.

Marcy Sutton* cũng viết một bài viết khám phá chủ đề này và giải thích:

Web Components, bao gồm Shadow DOM, có thể truy cập được bởi các công nghệ phụ trợ giao tiếp với trang đã được hiển thị, nghĩa là toàn bộ document được đọc như thể "một cái cây vui vẻ".

*Marcy cũng chỉ ra rằng img-slider tôi xây dựng trong bài viết này không truy cập được vì mẹo sử dụng nhãn css khiến nó không thể truy cập được từ bàn phím. Hãy nhớ điều đó nếu bạn muốn tái sử dụng nó trong một dự án.

Chắc chắn là sẽ có nhiều cải tiến nữa nhưng đó có vẻ là một khởi đầu tuyệt vời!

Style tags? Ờm, không, cảm ơn.

Không may là thẻ <link> không hoạt động bên trong Shadow DOM, nghĩa là cách duy nhất để đẩy CSS từ bên ngoài vào là thông qua @import. Nói cách khác, việc sử dụng thẻ <style> - hiện tại - là không thể tránh được. *

Hãy nhớ rằng style chúng ta đang nói đến chỉ liên quan tới một component, trong khi chúng ta đã được dạy rằng ưu tiên dùng file ngoài do chúng thường được áp dụng lên toàn bộ ứng dụng. Vậy liệu có xấu không khi đặt một thẻ <style> bên trong một phần tử, nếu tất cả các style này chỉ áp dụng cho thực thể đó? Cá nhân tôi thấy điều này ổn, nhưng khả năng sử dụng file ngoài sẽ rất đáng có được.

* Trừ khi bạn dùng Polymer do nó đã vượt qua giới hạn này bằng cách sử dụng XHR.

Giờ tới lượt của bạn

Việc tìm ra lối đi cho những tiêu chuẩn này và những cách thực hiện tốt nhất để định hướng chúng là phụ thuộc vào chúng ta. Hãy thử Polymer và bản thay thế của Mozilla, X-Tag (thứ hỗ trợ mọi thứ tới tận IE9).

Và hãy chắc chắn là bạn tiếp cận với các lập trình viên ở GoogleMozilla, những người dẫn đường cho các chuẩn này. Họ sẽ tiếp nhận các phản hồi của chúng ta để định hình các công cụ này thành những thứ mà chúng ta đều muốn sử dụng.

Cho dù vẫn còn một số thứ thô sơ, tôi nghĩ rằng Web Components cuối cùng sẽ mở ra một cách thức phát triển ứng dụng mới, thứ gì đó lắp ráp như Lego và ít giống với phương pháp hiện tại của chúng ta, phương pháp mà thường bị cản trở bởi những phần dư thừa, Tôi rất phấn khởi bởi tất cả những điều này đang hướng về phía trước, và tôi mong chờ thứ mà tương lai đang nắm giữ.