Kerangka UI dalam 5 menit


Beberapa waktu lalu saya bertanya-tanya mengapa ada begitu banyak kerangka kerja UI untuk web? Saya sudah berada di IT untuk waktu yang lama dan saya tidak ingat bahwa perpustakaan UI pada platform lain lahir dan mati dengan kecepatan yang sama seperti di WEB. Perpustakaan untuk OS desktop, seperti: MFC, Qt, WPF, dll. - adalah monster yang berkembang selama bertahun-tahun dan tidak memiliki banyak alternatif. Semuanya berbeda di Web - kerangka kerja dirilis hampir setiap minggu, para pemimpin berubah - mengapa ini terjadi?


Saya pikir alasan utama adalah bahwa kompleksitas penulisan perpustakaan UI telah menurun tajam. Ya, untuk menulis perpustakaan yang akan digunakan banyak orang - masih membutuhkan waktu dan keahlian yang besar, tetapi untuk menulis prototipe - yang, ketika dibungkus dengan API yang nyaman - akan siap digunakan - dibutuhkan waktu yang sangat sedikit. Jika Anda tertarik bagaimana hal ini dapat dilakukan, baca terus.


Kenapa artikel ini?


Pada suatu waktu di Habré ada serangkaian artikel - untuk menulis X selama 30 baris kode pada js.


Saya pikir - apakah mungkin untuk menulis reaksi dalam 30 baris? Ya, untuk 30 baris saya tidak berhasil, tetapi hasil akhirnya cukup sepadan dengan angka ini.


Secara umum, tujuan artikel ini murni mendidik. Ini dapat membantu pemahaman yang lebih dalam tentang prinsip kerangka UI berdasarkan virtual house. Pada artikel ini saya ingin menunjukkan betapa sederhananya membuat Kerangka UI lain berdasarkan rumah virtual.


Pada awalnya saya ingin mengatakan apa yang saya maksud dengan kerangka UI - karena banyak yang berbeda pendapat tentang ini. Sebagai contoh, beberapa orang percaya bahwa Angular dan Ember adalah kerangka kerja UI dan React hanyalah sebuah perpustakaan yang akan membuatnya lebih mudah untuk bekerja dengan tampilan bagian dari aplikasi.


Kami mendefinisikan kerangka UI sebagai berikut: ini adalah pustaka yang membantu untuk membuat / memperbarui / menghapus elemen halaman atau individu dalam hal ini berbagai pembungkus yang lebih luas atas DOM API dapat berubah menjadi kerangka UI, satu-satunya pertanyaan adalah opsi abstraksi (API) yang disediakan pustaka ini untuk memanipulasi DOM dan dalam efektivitas manipulasi ini


Dalam kata-kata yang diusulkan - Bereaksi adalah kerangka UI.


Nah, mari kita lihat bagaimana menulis Bereaksi Anda dengan blackjack dan banyak lagi. Bereaksi dikenal menggunakan konsep rumah virtual. Dalam bentuk yang disederhanakan, itu terdiri dalam kenyataan bahwa node DOM nyata dibangun sesuai ketat dengan node pohon DOM virtual yang sebelumnya dibangun. Manipulasi langsung dari DOM nyata tidak diterima, jika Anda perlu membuat perubahan ke DOM nyata, perubahan dilakukan ke DOM virtual, maka versi baru dari DOM virtual dibandingkan dengan yang lama, perubahan dikumpulkan yang harus diterapkan ke DOM nyata dan mereka diterapkan sedemikian rupa sehingga interaksi dengan DOM nyata diminimalkan DOM - yang membuat aplikasi lebih optimal.


Karena virtual house tree adalah objek skrip java biasa - cukup mudah untuk memanipulasinya - mengubah / membandingkan node-nya, dengan kata itu mudah di sini saya mengerti bahwa kode assembly adalah virtual tetapi cukup sederhana dan dapat dihasilkan sebagian oleh preprosesor dari bahasa deklaratif JSX tingkat tinggi.


Mari kita mulai dengan JSX


Ini adalah contoh kode JSX


const Component = () => ( <div className="main"> <input /> <button onClick={() => console.log('yo')}> Submit </button> </div> ) export default Component 

kita perlu membuat DOM virtual seperti itu saat memanggil fungsi Component


 const vdom = { type: 'div', props: { className: 'main' }, children: [ { type: 'input' }, { type: 'button', props: { onClick: () => console.log('yo') }, children: ['Submit'] } ] } 

Tentu saja, kami tidak akan menulis transformasi ini secara manual, kami akan menggunakan plugin ini , plugin sudah usang, tetapi cukup sederhana untuk membantu kami memahami cara kerja semuanya. Ini menggunakan jsx-transform , yang mengubah JSX seperti ini:


 jsx.fromString('<h1>Hello World</h1>', { factory: 'h' }); // => 'h("h1", null, ["Hello World"])' 

jadi, semua yang perlu kita lakukan adalah mengimplementasikan konstruktor vdom dari h node, sebuah fungsi yang secara rekursif akan membuat node DOM virtual, dalam kasus reaksi, fungsi React.createElement melakukan ini. Di bawah ini adalah implementasi primitif dari fungsi tersebut


 export function h(type, props, ...stack) { const children = (stack || []).reduce(addChild, []) props = props || {} return typeof type === "string" ? { type, props, children } : type(props, children) } function addChild(acc, node) { if (Array.isArray(node)) { acc = node.reduce(addChild, acc) } else if (null == node || true === node || false === node) { } else { acc.push(typeof node === "number" ? node + "" : node) } return acc } 

Tentu saja, rekursi menyulitkan kode sedikit di sini, tapi saya harap jelas, sekarang dengan fungsi ini kita dapat membangun vdom


 'h("h1", null, ["Hello World"])' => { type: 'h1', props:null, children:['Hello World']} 

dan untuk node-node dari sarang apa pun


Hebat, sekarang fungsi Komponen kami mengembalikan simpul vdom.


Sekarang bagian yang adalah, kita perlu menulis fungsi patch yang mengambil elemen DOM root dari aplikasi, vdom lama, vdom baru, dan memperbarui node DOM nyata sesuai dengan vdom baru.


Mungkin Anda dapat menulis kode ini dengan lebih mudah, tetapi ternyata saya mengambil kode dari paket picodom


 export function patch(parent, oldNode, newNode) { return patchElement(parent, parent.children[0], oldNode, newNode) } function patchElement(parent, element, oldNode, node, isSVG, nextSibling) { if (oldNode == null) { element = parent.insertBefore(createElement(node, isSVG), element) } else if (node.type != oldNode.type) { const oldElement = element element = parent.insertBefore(createElement(node, isSVG), oldElement) removeElement(parent, oldElement, oldNode) } else { updateElement(element, oldNode.props, node.props) isSVG = isSVG || node.type === "svg" let childNodes = [] ; (element.childNodes || []).forEach(element => childNodes.push(element)) let oldNodeIdex = 0 if (node.children && node.children.length > 0) { for (var i = 0; i < node.children.length; i++) { if (oldNode.children && oldNodeIdex <= oldNode.children.length && (node.children[i].type && node.children[i].type === oldNode.children[oldNodeIdex].type || (!node.children[i].type && node.children[i] === oldNode.children[oldNodeIdex])) ) { patchElement(element, childNodes[oldNodeIdex], oldNode.children[oldNodeIdex], node.children[i], isSVG) oldNodeIdex++ } else { let newChild = element.insertBefore( createElement(node.children[i], isSVG), childNodes[oldNodeIdex] ) patchElement(element, newChild, {}, node.children[i], isSVG) } } } for (var i = oldNodeIdex; i < childNodes.length; i++) { removeElement(element, childNodes[i], oldNode.children ? oldNode.children[i] || {} : {}) } } return element } 

Implementasi naif ini, ini sangat tidak optimal, tidak memperhitungkan pengidentifikasi elemen (kunci, id) - untuk benar memperbarui elemen yang diperlukan dalam daftar, tetapi dalam kasus primitif itu berfungsi dengan baik.


Implementasi createElement updateElement removeElement saya tidak membawanya ke sini, yang penting, siapa pun yang tertarik dapat melihat sumbernya di sini .


Ada satu-satunya peringatan - ketika properti value untuk elemen input diperbarui, perbandingan tidak boleh dilakukan dengan vnode lama tetapi dengan atribut value di rumah nyata - ini akan mencegah elemen aktif memperbarui properti ini (karena sudah diperbarui di sana) dan mencegah masalah dengan kursor dan seleksi.


Nah, itu saja sekarang kita hanya perlu mengumpulkan potongan-potongan ini dan menulis Kerangka UI
Kami tetap dalam 5 baris .


  1. Seperti dalam Bereaksi, untuk membangun aplikasi kita membutuhkan 3 parameter
    export function app(selector, view, initProps) {
    selector - selector dom root di mana aplikasi akan di-mount (secara default 'body')
    view - fungsi yang membangun vnode root
    initProps - properti aplikasi awal
  2. Ambil elemen root di DOM
    const rootElement = document.querySelector(selector || 'body')
  3. Kami mengumpulkan vdom dengan properti awal
    let node = view(initProps)
  4. Kami memasang vdom yang diterima di DOM sebagai vdom lama yang kami ambil batal
    patch(rootElement, null, node)
  5. Kami mengembalikan fungsi pembaruan aplikasi dengan properti baru
    return props => patch(rootElement, node, (node = view(props)))

Kerangka sudah siap!


'Hello world' pada Kerangka ini akan terlihat seperti ini:


 import { h, app } from "../src/index" function view(state) { return ( <div> <h2>{`Hello ${state}`}</h2> <input value={state} oninput={e => render(e.target.value)} /> </div> ) } const render = app('body', view, 'world') 

Pustaka ini, seperti Bereaksi, mendukung komposisi komponen, menambahkan, menghapus komponen saat runtime, sehingga dapat dianggap sebagai Kerangka UI . Sebuah use case yang sedikit lebih kompleks dapat ditemukan di sini ToDo example .


Tentu saja, ada banyak hal di perpustakaan ini: peristiwa siklus hidup (walaupun tidak sulit untuk mempercepatnya, kami sendiri mengelola pembuatan / pemutakhiran / penghapusan node), pembaruan terpisah dari node anak seperti ini.setState (untuk ini Anda perlu menyimpan tautan ke elemen DOM untuk masing-masing simpul vdom - ini akan sedikit mempersulit logika), kode patchElement sangat tidak optimal, tidak akan berfungsi dengan baik pada sejumlah besar elemen, tidak melacak elemen dengan pengenal, dll.


Bagaimanapun, perpustakaan dikembangkan untuk tujuan pendidikan - jangan menggunakannya dalam produksi :)


PS: Saya terinspirasi oleh perpustakaan Hyperapp yang luar biasa untuk artikel ini, bagian dari kode diambil dari sana.


Pengodean yang bagus!

Source: https://habr.com/ru/post/id415857/


All Articles