Привет всем, я начал свое обучение кодированию с Android Development несколько лет назад, и когда я пришел к веб-разработке, я подумал, что в Интернете также есть элемент HTML, такой как RecyclerView в Android, для отображения больших наборов данных. нет такого элемента. Итак, я просто попытался создать WebRecycler, например RecyclerView.

Это не очень хороший RecyclerView, но да, он перерабатывает узлы и показывает данные. А также я не проверял производительность, и у нее много проблем. Для проверки на производительность и оптимизацию нужен специалист по веб-разработке, а я в этом только новичок. Если вы опытный веб-разработчик и хотите внести свой вклад или просто проверить это, загляните в этот репозиторий GitHub.

Итак, эта история о первом испытании по созданию HTML-элемента, который перерабатывает свои дочерние узлы, и имя этого элемента — WebRecycler. Это первая часть этого эксперимента, и, возможно, в следующих историях появится улучшенная версия WebRecycler. А еще я плохо говорю по-английски, так что извините за неправильную грамматику и орфографические ошибки.

В этом WebRecycler я использую следующие концепции:

  • Пользовательские веб-компоненты: для создания пользовательского HTML-элемента, чтобы мы могли управлять всеми его элементами.
  • Intersection Observer: чтобы определить, когда дочерние узлы уходят с экрана (из прямоугольника web-recycler) и когда появляются на экране.
  • Импорт/экспорт JavaScript: создать отдельный файл для веб-ресайклера и адаптера для создания узлов и привязки данных.

Прежде чем начать, я предлагаю вам, если вы не знаете о Intersection Observer, сначала перейти к документам MDN и прочитать о нем.

Итак, давайте начнем. Добавьте пользовательский элемент в тег body, указав его webRecycler в качестве идентификатора.

<body>
    <web-recycler id="webRecycler"></web-recycler>
</body>

Создайте класс JavaScript с именем Adapter, наследуемый от WRAdapter, полученный из файла WebRecycler, и переопределите onCreateNode() ,onBindData(postion, держатель) и функции getItemSize() .

import './style.css';
import { WRAdapter } from "./WebRecycler.js";

class Adapter extends WRAdapter{
  constructor(){
    super();
  }
  onCreateNode(){
    let root = document.createElement('div'); root.classList.add('post');
    root.innerHTML = '<img src="https://images.unsplash.com/photo-1517694712202-14dd9538aa97?fit=crop&w=320" alt=""><div style="margin:10px;"><span class="post-title"></span><span class="post-desc"></span><a class="post-btn" href="">Read More</a></div>';
    let p = root.querySelector('.post-title'); p.textContent = "Hi";
    root.postTitle = root.querySelector('.post-title');
    root.desc = root.querySelector('.post-desc');
    return root;
  }
  onBindData(postion,holder){
    holder.postTitle.textContent = "This is title "+(postion+1);
    holder.desc.innerHTML = "This is description <b>"+(postion+1)+'</b>.<br>Lorem ipsum dolor sit, amet consectetur adipisicing elit. Labore vitae soluta ullam perspiciatis facilis illum aut similique, eius est alias aperiam sint dolore eum ipsum aliquam dignissimos ipsa iste, doloremque at? Quod aperiam recusandae, sunt error, porro officiis, sapiente fugiat cumque incidunt excepturi expedita ad voluptates natus mollitia facere voluptatem?';
  }
  getItemSize(){
    return 1000;
  }
} 

const webRecycler = document.getElementById("webRecycler");
const adapter = new Adapter();
webRecycler.setAdapter(adapter);

В onCreateNode только что созданном itemNode сохраните его дочерние элементы, чтобы мы могли получить их в onBindData и вернуть.

В onBindData у нас есть itemNode как держатель и позиция данных. В приведенном выше коде я просто связываю некоторый фиктивный текст в заголовке и описании и изображение Unsplash, но мы можем связать фактические данные с помощью позиции. А в getItemSize просто возвращается размер данных.

И создайте переменную webRecycler, сославшись на пользовательский элемент HTML, создайте объект Adapter и установите адаптер в webRecycler.

И, наконец, наш основной логический код в JS-файле WebRecycler.

class WebRecycler extends HTMLElement{
    adapter = null; dataSize = 0;;
    container; holders = [];observer;
    constructor(){
        super();
    }
    setAdapter(adap){
    }
    connectedCallback(){
    }
    createAtFirstTime(){
    }
    getHolder(){
        let holder = this.holders.pop();
        if(!holder){
            holder = this.adapter.onCreateNode();
        }   
        return holder;
    }
}
customElements.define('web-recycler',WebRecycler);
export class WRAdapter {
    constructor(){}
    onCreateNode(){}
    onBindData(position,holder){}
    getItemSize(){}
}

В JS-файле WebRecycler создайте класс с именем WebRecycler, расширив его из HTMLElement для пользовательского элемента. Этот класс имеет пять переменных: адаптердля создания узла и привязки данных, dataSizeдля хранения размера набора данных, контейнер –элемент div для удержания все itemNodes этого WebRecycler,холдеры — массив для хранения отсоединенных itemNodes и наблюдатель объект Itersection Observer. И конструктор с супервызовом и четырьмя функциями — setAdapterдлянастройки адаптера, connectedCallback метод обратного вызова HTMLElement для обнаружения присоединения Webrecycler к DOM, createAtFirstTimeдля создания узлов в первый раз, когда WebRecycler прикреплен к DOM и getHolder для получения itemNode из массива holders или создания нового.

Определите пользовательский элемент, который мы создали, создайте и экспортируйте класс WRAdapter с его необходимыми функциями.

В конструкторе создайте и инициализируйте контейнер и наблюдатель с контейнером в качестве корня. Наша самая важная логика заключается в функции обратного вызова наблюдателя. И нашу сложную часть логики делает за нас наблюдатель пересечения. Это когда узел элемента исчезает или появляется на экране и удаляет (перерабатывает) или добавляет (повторно прикрепляет) узел элемента соответственно.

constructor(){
    super();
    this.container = document.createElement('div');
    this.observer = new IntersectionObserver((entries,observer)=>{
        for (let i = 0; i < entries.length; i++) {
            const entrie = entries[i];
            if (entrie.target.firstTime) {
                entrie.target.firstTime = false;
            }else{
                if (entrie.isIntersecting) {
                    console.log("INTER");
                    if (entrie.rootBounds.bottom<=entrie.boundingClientRect.bottom) {
                        // come in from bottom side
                        let last = this.container.lastElementChild;
                        let p = last.position + 1;
                        if(p < this.dataSize){
                            let holder = this.getHolder();
                            this.adapter.onBindData(p,holder);
                            this.container.appendChild(holder);
                            holder.position = p;
                            if (holder.firstTime === undefined) {
                                this.observer.observe(holder);
                                holder.firstTime = true;
                            }
                        }
                    }else{
                        // come in from top side
                        let last = this.container.firstElementChild;
                        let p = last.position - 1;
                        if(p>=0){
                            let holder = this.getHolder();
                            this.adapter.onBindData(p,holder);
                            this.container.insertBefore(holder,last);
                            holder.position = p;
                            if (holder.firstTime === undefined) {
                                this.observer.observe(holder);
                                holder.firstTime = true;
                            }
                        }
                    }
                }else{
                    console.log("OUT");
                    if (entrie.rootBounds.bottom<=entrie.boundingClientRect.bottom) {
                        // go out from bottom side
                        let last = this.container.lastElementChild;
                        if (entrie.rootBounds.bottom + 200 <= last.getBoundingClientRect().top) {
                            last.remove();
                            this.holders.push(last);
                        }
                    }else{
                        // come out from top side
                        let first = this.container.firstElementChild;
                        if (entrie.rootBounds.top - 200 >= first.getBoundingClientRect().bottom) {
                            first.remove();
                            this.holders.push(first);
                        }
                    }
                }
            }
        }
    },{root:this});
}

Для каждого узла элемента, который исчезает или появляется на экране, у нас есть объект входа IntersectionObserverEntry, в котором есть вся информация, необходимая для проверки того, выходит ли узел элемента за пределы экрана с верхней или нижней стороны и приходит в экран сверху или снизу. Мы перебираем записи, чтобы мы могли выполнять действия для каждого узла элемента.

Ничего не делаем, если он вызывается во время регистрации в обозревателе проверкой булевой переменной firstTime, просто меняем значение firstTime на false.

if (entrie.target.firstTime) {
    entrie.target.firstTime = false;
}

Мы используем следующие свойства объекта entry (entri in code): -

  • isIntersecting: это логическая переменная, и значение true означает появление на экране, а значение false означает исчезновение с экрана.
  • rootBounds: это объект DOMRectReadOnly WebRecycler.
  • boundingClientRect: это объект DOMRectReadOnly узла item.
if (entrie.isIntersecting) {
// in condition
}else{
// out condition
}

Итак, на изображении ниже показаны четыре условия в теле приведенного выше кода.

В условии 1:  получите позицию из lastChildElement и увеличьте ее на 1, проверьте, не находится ли вне индекса, получите itemNode, вызвав функцию getHolder, вызовите onBindData для привязки данных добавьте его в container в качестве последнего элемента, сохраните позицию в itemNode для следующего повторного использования, присоедините наблюдатель, если держатель новый, и установите для firstTime значение true, чтобы ничего не делать во время регистрации.

В условии 2: - сделайте то же самое, что и в условии 1, просто уменьшите позицию вместо увеличения и добавьте ее в качестве первого элемента.

Условия 3 и 4: - В условиях 3 и 4 работа заключается в удалении узла элемента, но не текущего узла, который просто выходит за пределы экрана, вместо этого проверьте, выходит ли какой-либо элемент за пределы экрана на расстоянии 200 пикселей от границы WebRecycler, поэтому контейнер имеет достаточную высоту, чтобы вызвать функцию обратного вызова наблюдателя для следующего присоединения или отсоединения узла элемента.

В setAdapter установите adapter, dataSize и проверьте, подключен ли он к DOM или нет, и вызовите функцию createAtFirstTime, если размер данных больше нуля.

setAdapter(adap){
    this.adapter = adap;
    this.dataSize = adap.getItemSize();
    if (this.isConnected) {
        if(0<this.dataSize) this.createAtFirstTime();
    }
}

В connectedCallback добавьте контейнер, а здесь также проверьте наличие адаптера и установите размер данных и вызовите createAtFirstTime, если размер данных не равен нулю.

connectedCallback(){
    this.appendChild(this.container);
    if(this.adapter !== null) {
        this.dataSize = this.adapter.getItemSize();
        if(0<this.dataSize) this.createAtFirstTime();
    }
}

setAdapter и connectedCallback выполняют одну и ту же работу, но разница заключается в том, что адаптер устанавливается перед добавлением, а затем connectedCallbackвыполняет эту работу, а когда адаптер устанавливается после добавления, то setAdapter выполнил эту работу.

createAtFirstTimeфункция создает и связывает данные и добавляет их в контейнер, а также обратите внимание, что здесь сохраняется позиция в каждом элементеNode(держатель) и прикрепляется к Intersection Observer и сохранить логическое значение, чтобы мы могли избежать выполнения какой-либо работы, когда обратный вызов наблюдателя вызывается во время отставки.

createAtFirstTime(){
    let position = 0;
    while (this.container.clientHeight < this.clientHeight + 200 && position < this.dataSize) {
        let holder = this.getHolder();
        this.container.appendChild(holder);
        this.adapter.onBindData(position,holder);
        holder.position = position;
        if (holder.firstTime === undefined) {
            this.observer.observe(holder);
            holder.firstTime = true;
        }
        position++;
    }
}

А также здесь позаботьтесь о создании достаточного количества узлов элементов, чтобы несколько узлов элементов выходили за пределы прямоугольника WebRecycler, чтобы Intersection Observer выполнял свою работу по уведомлению, когда узел элемента исчезает или появляется на экране.

Ниже приведены стили, используемые в этом примере.

body{
    margin: 0;
}
web-recycler{
    display: block;
    overflow: auto;
    box-sizing: border-box;
}
#webRecycler{
    padding: 10px;
    /* max-width: 1080px; */
    height: 100vh;
    /* border: 1px solid gainsboro; */
    /* margin: auto; */
}
.post{
    max-width: 1080px;
    margin: 10px auto;
    display: flex;
    align-items: start;
    padding: 10px;
    border: 1px solid gainsboro;
    border-radius: 5px;
}
.post-title{
    font-size: 25px;
    font-weight: bold;
    display: block;
}
.post-desc{
    font-size: 18px;
    display: block;
}
.post-btn{
    display: inline-block;
    margin-top: 10px;
    border: 2px solid purple;
    border-radius: 50px;
    text-decoration: none;
    font-weight: bold;
    padding: 5px 10px;
}
.post-btn:hover{
    background: purple;
    color: white;
}

А ниже полный код файла WebRecycler: -

class WebRecycler extends HTMLElement{
    adapter = null; dataSize = 0;;
    container; holders = [];
    constructor(){
        super();
        this.container = document.createElement('div');
        this.observer = new IntersectionObserver((entries,observer)=>{
            for (let i = 0; i < entries.length; i++) {
                const entrie = entries[i];
                if (entrie.target.firstTime) {
                    entrie.target.firstTime = false;
                }else{
                    if (entrie.isIntersecting) {
                        console.log("INTER");
                        if (entrie.rootBounds.bottom<=entrie.boundingClientRect.bottom) {
                            // come in from bottom side
                            let last = this.container.lastElementChild;
                            let p = last.position + 1;
                            if(p < this.dataSize){
                                let holder = this.getHolder();
                                this.adapter.onBindData(p,holder);
                                this.container.appendChild(holder);
                                holder.position = p;
                                if (holder.firstTime === undefined) {
                                    this.observer.observe(holder);
                                    holder.firstTime = true;
                                }
                            }
                        }else{
                            // come in from top side
                            let last = this.container.firstElementChild;
                            let p = last.position - 1;
                            if(p>=0){
                                let holder = this.getHolder();
                                this.adapter.onBindData(p,holder);
                                this.container.insertBefore(holder,last);
                                holder.position = p;
                                if (holder.firstTime === undefined) {
                                    this.observer.observe(holder);
                                    holder.firstTime = true;
                                }
                            }
                        }
                    }else{
                        console.log("OUT");
                        if (entrie.rootBounds.bottom<=entrie.boundingClientRect.bottom) {
                            // go out from bottom side
                            let last = this.container.lastElementChild;
                            if (entrie.rootBounds.bottom + 200 <= last.getBoundingClientRect().top) {
                                last.remove();
                                this.holders.push(last);
                            }
                        }else{
                            // come out from top side
                            let first = this.container.firstElementChild;
                            if (entrie.rootBounds.top - 200 >= first.getBoundingClientRect().bottom) {
                                first.remove();
                                this.holders.push(first);
                            }
                        }
                    }
                }
            }
        },{root:this});
    }
    setAdapter(adap){
        console.log("setAdapter");
        this.adapter = adap;
        this.dataSize = adap.getItemSize();
        if (this.isConnected) {
            if(0<this.dataSize) this.createAtFirstTime();
        }
    }
    connectedCallback(){
        this.appendChild(this.container);
        if(this.adapter !== null) {
            this.dataSize = this.adapter.getItemSize();
            if(0<this.dataSize) this.createAtFirstTime();
        }
    }
    createAtFirstTime(){
        let position = 0;
        while (this.container.clientHeight < this.clientHeight + 200 && position < this.dataSize) {
            let holder = this.getHolder();
            this.container.appendChild(holder);
            this.adapter.onBindData(position,holder);
            holder.position = position;
            if (holder.firstTime === undefined) {
                this.observer.observe(holder);
                holder.firstTime = true;
            }
            position++;
        }
    }
    getHolder(){
        let holder = this.holders.pop();
        if(!holder){
            holder = this.adapter.onCreateNode();
        }   
        return holder;
    }
}
customElements.define('web-recycler',WebRecycler);
export class WRAdapter {
    constructor(){}
    onCreateNode(){}
    onBindData(position,holder){}
    getItemSize(){}
}

В настоящее время у этого WebRecycler много проблем и незавершенных работ.

Проблемы: –

  • Не работает должным образом, когда размер браузера быстро увеличивается или уменьшается.
  • Это может быть не так, как мы ожидаем, когда высота узлов элементов настолько велика.
  • И многие другие проблемы могут быть у него.

Отложенная работа:-

  • Добавьте методы для изменения набора данных, удаления элемента, вставки и обновления.
  • Напишите код, чтобы его можно было использовать в качестве Grid WebRecycler.

Так что я надеюсь, вам понравится и вы узнаете что-то из этой истории. А это репозиторий GitHub для получения кода.

И если вы эксперт и опытный веб-разработчик, то у меня есть вопрос, действительно ли можно создать WebRecyler или нет.