Membangun Pustaka Pola Dengan Shadow DOM Dalam Penurunan Harga
Diterbitkan: 2022-03-10Alur kerja khas saya menggunakan pengolah kata desktop berjalan seperti ini:
- Pilih beberapa teks yang ingin saya salin ke bagian lain dari dokumen.
- Perhatikan bahwa aplikasi telah memilih sedikit lebih banyak atau lebih sedikit daripada yang saya suruh.
- Coba lagi.
- Menyerah dan memutuskan untuk menambahkan bagian yang hilang (atau menghapus bagian tambahan) dari pilihan yang saya maksudkan nanti.
- Salin dan tempel seleksi.
- Perhatikan bahwa format teks yang ditempelkan entah bagaimana berbeda dari aslinya.
- Cobalah untuk menemukan preset styling yang cocok dengan teks aslinya.
- Cobalah untuk menerapkan preset.
- Menyerah dan menerapkan keluarga font dan ukuran secara manual.
- Perhatikan bahwa ada terlalu banyak ruang putih di atas teks yang ditempel, dan tekan "Backspace" untuk menutup celah.
- Perhatikan bahwa teks yang dimaksud telah meninggikan dirinya beberapa baris sekaligus, bergabung dengan teks judul di atasnya dan mengadopsi gayanya.
- Renungkan kematianku.
Saat menulis dokumentasi web teknis (baca: pustaka pola), pengolah kata tidak hanya tidak patuh, tetapi juga tidak pantas. Idealnya, saya menginginkan mode penulisan yang memungkinkan saya memasukkan komponen yang saya dokumentasikan sebaris, dan ini tidak mungkin kecuali dokumentasi itu sendiri terbuat dari HTML, CSS, dan JavaScript. Pada artikel ini, saya akan membagikan metode untuk dengan mudah memasukkan demo kode di Markdown, dengan bantuan kode pendek dan enkapsulasi DOM bayangan.
CSS Dan Penurunan Harga
Katakan apa yang Anda mau tentang CSS, tetapi tentu saja ini adalah alat penyusunan huruf yang lebih konsisten dan andal daripada editor atau pengolah kata WYSIWYG mana pun yang ada di pasaran. Mengapa? Karena tidak ada algoritme kotak hitam tingkat tinggi yang mencoba menebak gaya apa yang sebenarnya ingin Anda tuju. Alih-alih, ini sangat eksplisit: Anda menentukan elemen mana yang mengambil gaya mana dalam situasi apa, dan itu menghormati aturan itu.
Satu-satunya masalah dengan CSS adalah Anda harus menulis padanannya, HTML. Bahkan pecinta HTML yang hebat kemungkinan akan mengakui bahwa menulisnya secara manual adalah hal yang sulit ketika Anda hanya ingin menghasilkan konten prosa. Di sinilah Markdown masuk. Dengan sintaks singkat dan set fitur yang dikurangi, ia menawarkan mode penulisan yang mudah dipelajari tetapi masih dapat — setelah diubah menjadi HTML secara terprogram — memanfaatkan fitur pengaturan huruf CSS yang kuat dan dapat diprediksi. Ada alasan mengapa ini menjadi format de facto untuk generator situs web statis dan platform blogging modern seperti Ghost.
Di mana markup yang lebih kompleks dan dipesan lebih dahulu diperlukan, sebagian besar parser penurunan harga akan menerima HTML mentah dalam input. Namun, semakin seseorang bergantung pada markup yang kompleks, sistem authoring seseorang yang kurang dapat diakses adalah mereka yang kurang teknis, atau mereka yang kekurangan waktu dan kesabaran. Di sinilah shortcode masuk.
Kode Pendek Di Hugo
Hugo adalah generator situs statis yang ditulis dalam Go — bahasa kompilasi multiguna yang dikembangkan di Google. Karena konkurensi (dan, tidak diragukan lagi, fitur bahasa tingkat rendah lainnya yang saya tidak sepenuhnya mengerti), Go menjadikan Hugo generator konten web statis yang sangat cepat. Ini adalah salah satu dari banyak alasan mengapa Hugo dipilih untuk versi baru Majalah Smashing.
Selain kinerja, ia bekerja dengan cara yang mirip dengan generator berbasis Ruby dan Node.js yang mungkin sudah Anda kenal: Penurunan harga plus meta data (YAML atau TOML) yang diproses melalui template. Sara Soueidan telah menulis primer yang sangat baik tentang fungsionalitas inti Hugo.
Bagi saya, fitur pembunuh Hugo adalah implementasi kode pendeknya. Mereka yang berasal dari WordPress mungkin sudah akrab dengan konsep ini: sintaks yang dipersingkat terutama digunakan untuk memasukkan kode embed kompleks dari layanan pihak ketiga. Misalnya, WordPress menyertakan kode pendek Vimeo yang hanya mengambil ID video Vimeo yang dimaksud.
[vimeo 44633289]
Tanda kurung menandakan bahwa konten mereka harus diproses sebagai kode pendek dan diperluas ke markup sematan HTML penuh saat konten diuraikan.
Memanfaatkan fungsi template Go, Hugo menyediakan API yang sangat sederhana untuk membuat kode pendek khusus. Misalnya, saya telah membuat kode pendek Codepen sederhana untuk disertakan di antara konten penurunan harga saya:
Some Markdown content before the shortcode. Aliquam sodales rhoncus dui, sed congue velit semper ut. Class aptent taciti sociosqu ad litora torquent. {{<codePen VpVNKW>}} Some Markdown content after the shortcode. Nulla vel magna sit amet dui lobortis commodo vitae vel nulla sit amet ante hendrerit tempus.
Hugo secara otomatis mencari template bernama codePen.html
di subfolder shortcodes
untuk mengurai shortcode selama kompilasi. Implementasi saya terlihat seperti ini:
{{ if .Site.Params.codePenUser }} <iframe height='300' scrolling='no' title="code demonstration with codePen" src='//codepen.io/{{ .Site.Params.codepenUser | lower }}/embed/{{ .Get 0 }}/?height=265&theme-id=dark&default-tab=result,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true'> <div> <a href="//codepen.io/{{ .Site.Params.codePenUser | lower }}/pen/{{ .Get 0 }}">See the demo on codePen</a> </div> </iframe> {{ else }} <p class="site-error"><strong>Site error:</strong> The <code>codePenUser</code> param has not been set in <code>config.toml</code></p> {{ end }}
Untuk mendapatkan gambaran yang lebih baik tentang cara kerja paket template Go, Anda dapat membaca "Go Template Primer" Hugo. Sementara itu, perhatikan saja hal berikut:
- Ini cukup fugly tapi tetap kuat.
- Bagian
{{ .Get 0 }}
adalah untuk mengambil argumen pertama (dan, dalam hal ini, hanya) yang diberikan — ID Codepen. Hugo juga mendukung argumen bernama, yang disediakan seperti atribut HTML. -
.
sintaks mengacu pada konteks saat ini. Jadi,.Get 0
berarti “Dapatkan argumen pertama yang diberikan untuk kode pendek saat ini.”
Bagaimanapun, saya pikir shortcode adalah hal terbaik sejak shortbread, dan implementasi Hugo untuk menulis shortcode khusus sangat mengesankan. Saya harus mencatat dari penelitian saya bahwa dimungkinkan untuk menggunakan Jekyll termasuk untuk efek yang sama, tetapi saya merasa mereka kurang fleksibel dan kuat.
Demo Kode Tanpa Pihak Ketiga
Saya punya banyak waktu untuk Codepen (dan taman bermain kode lain yang tersedia), tetapi ada masalah yang melekat dengan memasukkan konten seperti itu di perpustakaan pola:
- Ini menggunakan API sehingga tidak dapat dengan mudah atau efisien dibuat untuk bekerja secara offline.
- Itu tidak hanya mewakili pola atau komponen; itu adalah antarmuka kompleksnya sendiri yang dibungkus dengan mereknya sendiri. Ini menciptakan kebisingan dan gangguan yang tidak perlu ketika fokus harus pada komponen.
Untuk beberapa waktu, saya mencoba menyematkan demo komponen menggunakan iframe saya sendiri. Saya akan mengarahkan iframe ke file lokal yang berisi demo sebagai halaman webnya sendiri. Dengan menggunakan iframe, saya dapat merangkum gaya dan perilaku tanpa bergantung pada pihak ketiga.
Sayangnya, iframe agak berat dan sulit diubah ukurannya secara dinamis. Dalam hal kerumitan penulisan, ini juga memerlukan pemeliharaan file yang terpisah dan harus menautkannya. Saya lebih suka menulis komponen saya di tempat, termasuk hanya kode yang diperlukan untuk membuatnya berfungsi. Saya ingin dapat menulis demo saat saya menulis dokumentasinya.
Kode pendek demo
Untungnya, Hugo memungkinkan Anda membuat kode pendek yang menyertakan konten antara membuka dan menutup tag kode pendek. Konten tersedia dalam file kode pendek menggunakan {{ .Inner }}
. Jadi, misalkan saya menggunakan kode pendek demo
seperti ini:
{{<demo>}} This is the content! {{</demo>}}
"Ini isinya!" akan tersedia sebagai {{ .Inner }}
dalam template demo.html
yang menguraikannya. Ini adalah titik awal yang baik untuk mendukung demo kode sebaris, tetapi saya perlu membahas enkapsulasi.
Enkapsulasi Gaya
Ketika membahas gaya enkapsulasi, ada tiga hal yang perlu dikhawatirkan:
- gaya yang diwarisi oleh komponen dari halaman induk,
- halaman induk mewarisi gaya dari komponen,
- gaya yang dibagikan secara tidak sengaja antar komponen.
Salah satu solusinya adalah dengan hati-hati mengelola pemilih CSS sehingga tidak ada tumpang tindih antara komponen dan antara komponen dan halaman. Ini berarti menggunakan pemilih esoterik per komponen, dan itu bukan sesuatu yang saya tertarik untuk pertimbangkan ketika saya bisa menulis kode yang singkat dan mudah dibaca. Salah satu keuntungan iframe adalah bahwa gaya dienkapsulasi secara default, jadi saya bisa menulis button { background: blue }
dan yakin itu hanya akan berlaku di dalam iframe.
Cara yang kurang intensif untuk mencegah komponen mewarisi gaya dari halaman adalah dengan menggunakan all
properti dengan nilai initial
pada elemen induk terpilih. Saya dapat mengatur elemen ini di file demo.html
:
<div class="demo"> {{ .Inner }} </div>
Kemudian, saya perlu menerapkan all: initial
ke instance elemen ini, yang menyebar ke anak-anak dari setiap instance.
.demo { all: initial }
Perilaku initial
cukup… idiosinkratik. Dalam praktiknya, semua elemen yang terpengaruh akan kembali mengadopsi gaya agen pengguna saja (seperti display: block
untuk elemen <h2>
). Namun, elemen yang diterapkan — class=“demo”
— perlu memiliki gaya agen pengguna tertentu yang secara eksplisit dipulihkan. Dalam kasus kami, ini hanya display: block
, karena class=“demo”
adalah <div>
.
.demo { all: initial; display: block; }
Catatan: all
sejauh ini tidak didukung di Microsoft Edge tetapi sedang dipertimbangkan. Dukungan, sebaliknya, sangat luas. Untuk tujuan kami, nilai revert
akan lebih kuat dan andal tetapi belum didukung di mana pun.
Shadow DOM'ing Kode Pendek
Menggunakan all: initial
tidak membuat komponen sebaris kita sepenuhnya kebal terhadap pengaruh luar (kekhususan masih berlaku), tetapi kita dapat yakin bahwa gaya tidak disetel karena kita berurusan dengan nama kelas demo
yang dicadangkan. Sebagian besar hanya gaya yang diwarisi dari pemilih dengan spesifisitas rendah seperti html
dan body
akan dihilangkan.
Meskipun demikian, ini hanya berkaitan dengan gaya yang berasal dari induk ke dalam komponen. Untuk mencegah gaya yang ditulis untuk komponen memengaruhi bagian lain halaman, kita perlu menggunakan shadow DOM untuk membuat subpohon yang dienkapsulasi.
Bayangkan saya ingin mendokumentasikan elemen <button>
yang ditata. Saya ingin dapat menulis sesuatu seperti berikut ini, tanpa takut bahwa pemilih elemen button
akan berlaku untuk elemen <button>
di perpustakaan pola itu sendiri atau di komponen lain di halaman perpustakaan yang sama.
{{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } </style> {{</demo>}}
Triknya adalah dengan mengambil bagian {{ .Inner }}
dari template kode pendek dan memasukkannya sebagai innerHTML
dari ShadowRoot
baru. Saya mungkin menerapkan ini seperti ini:
{{ $uniq := .Inner | htmlEscape | base64Encode | truncate 15 "" }} <div class="demo"></div> <script> (function() { var root = document.getElementById('demo-{{ $uniq }}'); root.attachShadow({mode: 'open'}); root.innerHTML = '{{ .Inner }}'; })(); </script>
-
$uniq
ditetapkan sebagai variabel untuk mengidentifikasi wadah komponen. Ini menyalurkan di beberapa fungsi template Go untuk membuat string unik… semoga(!) — ini bukan metode antipeluru; itu hanya untuk ilustrasi. -
root.attachShadow
menjadikan wadah komponen sebagai host DOM bayangan. - Saya mengisi
innerHTML
dariShadowRoot
menggunakan{{ .Inner }}
, yang menyertakan CSS yang sekarang dienkapsulasi.
Mengizinkan Perilaku JavaScript
Saya juga ingin memasukkan perilaku JavaScript di komponen saya. Pada awalnya, saya pikir ini akan mudah; sayangnya, JavaScript yang dimasukkan melalui innerHTML
tidak diurai atau dieksekusi. Ini dapat diselesaikan dengan mengimpor dari konten elemen <template>
. Saya mengubah implementasi saya sesuai dengan itu.
{{ $uniq := .Inner | htmlEscape | base64Encode | truncate 15 "" }} <div class="demo"></div> <template> {{ .Inner }} </template> <script> (function() { var root = document.getElementById('demo-{{ $uniq }}'); root.attachShadow({mode: 'open'}); var template = document.getElementById('template-{{ $uniq }}'); root.shadowRoot.appendChild(document.importNode(template.content, true)); })(); </script>
Sekarang, saya dapat menyertakan demo sebaris, katakanlah, tombol sakelar yang berfungsi:
{{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } [aria-pressed="true"] { box-shadow: inset 0 0 5px #000; } </style> <script> var toggle = document.querySelector('[aria-pressed]'); toggle.addEventListener('click', (e) => { let pressed = e.target.getAttribute('aria-pressed') === 'true'; e.target.setAttribute('aria-pressed', !pressed); }); </script> {{</demo>}}
Catatan: Saya telah menulis secara mendalam tentang tombol sakelar dan aksesibilitas untuk Komponen Inklusif.
Enkapsulasi JavaScript
JavaScript, yang mengejutkan saya, tidak dienkapsulasi secara otomatis seperti CSS dalam shadow DOM. Artinya, jika ada tombol [aria-pressed]
lain di halaman induk sebelum contoh komponen ini, maka document.querySelector
akan menargetkannya sebagai gantinya.
Yang saya butuhkan adalah document
yang setara dengan subpohon demo saja. Ini dapat didefinisikan, meskipun cukup verbosely:
document.getElementById('demo-{{ $uniq }}').shadowRoot;
Saya tidak ingin harus menulis ekspresi ini setiap kali saya harus menargetkan elemen di dalam wadah demo. Jadi, saya membuat peretasan di mana saya menetapkan ekspresi ke variabel demo
lokal dan skrip awalan yang disediakan melalui kode pendek dengan tugas ini:
if (script) { script.textContent = `(function() { var demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; ${script.textContent} })()` } root.shadowRoot.appendChild(document.importNode(template.content, true));
Dengan ini, demo
menjadi setara dengan document
untuk subpohon komponen apa pun, dan saya dapat menggunakan demo.querySelector
untuk dengan mudah menargetkan tombol sakelar saya.
var toggle = demo.querySelector('[aria-pressed]');
Perhatikan bahwa saya telah menyertakan konten skrip demo dalam ekspresi fungsi yang segera dipanggil (IIFE), sehingga variabel demo
— dan semua variabel lanjutan yang digunakan untuk komponen — tidak berada dalam cakupan global. Dengan cara ini, demo
dapat digunakan dalam skrip kode pendek apa pun tetapi hanya akan merujuk ke kode pendek yang ada.
Di mana ECMAScript6 tersedia, pelokalan dimungkinkan menggunakan "blok pelingkupan", hanya dengan kurung kurawal yang melampirkan pernyataan let
atau const
. Namun, semua definisi lain di dalam blok harus menggunakan let
atau const
(menghindari var
) juga.
{ let demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; // Author script injected here }
Dukungan Shadow DOM
Tentu saja, semua hal di atas hanya mungkin jika shadow DOM versi 1 didukung. Chrome, Safari, Opera, dan Android semuanya terlihat cukup bagus, tetapi browser Firefox dan Microsoft bermasalah. Dimungkinkan untuk mendeteksi fitur dukungan dan memberikan pesan kesalahan saat attachShadow
tidak tersedia:
if (document.head.attachShadow) { // Do shadow DOM stuff here } else { root.innerHTML = 'Shadow DOM is needed to display encapsulated demos. The browser does not have an issue with the demo code itself'; }
Atau Anda dapat menyertakan Shady DOM dan ekstensi Shady CSS, yang berarti ketergantungan yang agak besar (60 KB+) dan API yang berbeda. Rob Dodson cukup baik untuk memberi saya demo dasar, yang dengan senang hati saya bagikan untuk membantu Anda memulai.
Keterangan Untuk Komponen
Dengan fungsionalitas demo inline dasar yang ada, dengan cepat menulis demo yang berfungsi sesuai dengan dokumentasinya sangat mudah. Ini memberi kami kemewahan untuk dapat mengajukan pertanyaan seperti, "Bagaimana jika saya ingin memberikan keterangan untuk melabeli demo?" Ini sangat mungkin karena — seperti yang disebutkan sebelumnya — Penurunan harga mendukung HTML mentah.
<figure role="group" aria-labelledby="caption-button"> {{<demo>}} <button>My button</button> <style> button { background: blue; padding: 0.5rem 1rem; text-transform: uppercase; } </style> {{</demo>}} <figcaption>A standard button</figcaption> </figure>
Namun, satu-satunya bagian baru dari struktur yang diubah ini adalah kata-kata dari keterangan itu sendiri. Lebih baik menyediakan antarmuka sederhana untuk memasoknya ke output, menghemat waktu dan tenaga saya di masa depan — dan siapa pun yang menggunakan kode pendek — dan mengurangi risiko kesalahan ketik pengkodean. Ini dimungkinkan dengan memberikan parameter bernama ke kode pendek — dalam hal ini, cukup beri nama caption
:
{{<demo caption="A standard button">}} ... demo contents here... {{</demo>}}
Parameter bernama dapat diakses di template seperti {{ .Get "caption" }}
, yang cukup sederhana. Saya ingin keterangan dan, oleh karena itu, <figure>
dan <figcaption>
di sekitarnya menjadi opsional. Menggunakan klausa if
, saya dapat menyediakan konten yang relevan hanya jika kode pendek memberikan argumen teks:
{{ if .Get "caption" }} <figcaption>{{ .Get "caption" }}</figcaption> {{ end }}
Beginilah tampilan template demo.html
lengkap sekarang (diakui, ini sedikit berantakan, tetapi berhasil):
{{ $uniq := .Inner | htmlEscape | base64Encode | truncate 15 "" }} {{ if .Get "caption" }} <figure role="group" aria-labelledby="caption-{{ $uniq }}"> {{ end }} <div class="demo"></div> {{ if .Get "caption" }} <figcaption>{{ .Get "caption" }}</figcaption> {{ end }} {{ if .Get "caption" }} </figure> {{ end }} <template> {{ .Inner }} </template> <script> (function() { var root = document.getElementById('demo-{{ $uniq }}'); root.attachShadow({mode: 'open'}); var template = document.getElementById('template-{{ $uniq }}'); var script = template.content.querySelector('script'); if (script) { script.textContent = `(function() { var demo = document.getElementById(\'demo-{{ $uniq }}\').shadowRoot; ${script.textContent} })()` } root.shadowRoot.appendChild(document.importNode(template.content, true)); })(); </script>
Satu catatan terakhir: Jika saya ingin mendukung sintaks penurunan harga dalam nilai teks, saya dapat menyalurkannya melalui fungsi markdownify
Hugo. Dengan cara ini, penulis dapat memberikan penurunan harga (dan HTML) tetapi tidak dipaksa untuk melakukan keduanya.
{{ .Get "caption" | markdownify }}
Kesimpulan
Untuk kinerjanya dan banyak fitur unggulannya, Hugo saat ini cocok untuk saya dalam hal pembuatan situs statis. Tetapi penyertaan kode pendek adalah yang menurut saya paling menarik. Dalam hal ini, saya dapat membuat antarmuka sederhana untuk masalah dokumentasi yang telah saya coba selesaikan selama beberapa waktu.
Seperti dalam komponen web, banyak kerumitan markup (terkadang diperburuk dengan penyesuaian aksesibilitas) dapat disembunyikan di balik kode pendek. Dalam hal ini, saya mengacu pada penyertaan role="group"
dan hubungan aria-labelledby
, yang memberikan "label grup" yang didukung lebih baik ke <figure>
— bukan hal yang disukai siapa pun lebih dari sekali, terutama di mana nilai atribut unik perlu dipertimbangkan dalam setiap contoh.
Saya percaya kode pendek adalah untuk Penurunan harga dan konten apa komponen web untuk HTML dan fungsionalitas: cara untuk membuat kepengarangan lebih mudah, lebih andal, dan lebih konsisten. Saya menantikan evolusi lebih lanjut di bidang web kecil yang aneh ini.
Sumber daya
- dokumentasi Hugo
- “Template Paket,” Bahasa Pemrograman Go
- “Kode pendek,” Hugo
- "semua" (properti singkatan CSS), Jaringan Pengembang Mozilla
- “awal (kata kunci CSS), Jaringan Pengembang Mozilla
- “Shadow DOM v1: Komponen Web Mandiri,” Eric Bidelman, Dasar-Dasar Web, Pengembang Google
- “Pengantar Elemen Template,” Eiji Kitamura, WebComponents.org
- “Termasuk,” Jekyll