[Технология Bei Liao] Рендеринг содержимого HTML в апплете WeChat

внешний интерфейс алгоритм Апплет WeChat HTML

Большая часть расширенного текстового содержимого веб-приложений хранится в виде строк HTML, и, естественно, не возникает проблем с отображением содержимого HTML в документах HTML. Однако в апплете WeChat (далее «мини-программа») как должна отображаться эта часть контента?

решение

wxParse

Когда апплет был впервые запущен, было невозможно напрямую отображать HTML-контент, поэтому родилась библиотека под названием «wxParse». Его принцип заключается в том, чтобы разобрать код HTML на древовидные данные, а затем отобразить данные с помощью шаблона апплета.

rich-text

Позже апплет добавил компонент «форматированный текст» для отображения содержимого форматированного текста. Однако этот компонент имеет огромное ограничение:События всех узлов блокируются в компоненте. Другими словами, в этом компоненте не может быть реализована даже такая простая функция, как «предпросмотр изображения».

web-view

Позже апплет позволяет вкладывать веб-страницы через компонент «веб-просмотр», а отображение содержимого HTML через веб-страницы является наиболее совместимым решением. Однако производительность низкая, потому что загружается еще одна страница.

Когда «WePY» встречается с «wxParse»

Основываясь на рассмотрении пользовательского опыта и функционального взаимодействия, мы отказались от двух нативных компонентов «форматированный текст» и «веб-представление» и выбрали «wxParse». Однако после его использования я обнаружил, что «wxParse» не очень хорошо отвечает потребностям:

  • Наш апплет разработан на основе фреймворка "WePY", а "wxParse" написан на основе нативного апплета. Чтобы сделать их совместимыми, исходный код "wxParse" должен быть изменен.
  • «wxParse» просто отображает и предварительно просматривает изображение исходного элемента img через компонент изображения. При фактическом использовании интерфейс облачного хранилища может использоваться для уменьшения изображения для достижения «Отображение с уменьшенным изображением, предварительный просмотр с исходным изображением"цель.
  • "wxParse" напрямую использует видео компонент апплета для отображения видео, но видео компонентПроблема иерархииЧасто вызывает исключения пользовательского интерфейса (например, блокирует элемент с фиксированным положением).

Кроме того, просмотр репозитория кода «wxParse» показывает, что он не обновлялся два года. Поэтому родилась идея переписать компонент расширенного текста на основе шаблона компонента «WePY», и результатом стал проект «WePY HTML».

Процесс реализации

Разобрать HTML

Во-первых, все еще нужно разобрать строку HTML на древовидные данные, я использую «специальный метод разделения символов». Специальные символы в HTML — это «», первый из которых является начальным, а второй — конечным.

  • Если анализируемый контент начинается с начального символа, перехватить егоОт начала до концаСодержимое анализируется как узел.
  • Если анализируемый контент не начинается с начального символа, перехватить егоначать, чтобы начать персонаж(или конец, если начальный символ отсутствует) анализируется как обычный текст.
  • Оставшийся контент переходит к следующему раунду синтаксического анализа до тех пор, пока не останется остаточного контента.

Как показано на изображении ниже:

HTML代码解析流程

Для формирования древовидной структуры узел контекста (по умолчанию корневой узел) поддерживается в процессе синтаксического анализа:

  • Если перехваченное содержимое является начальным тегом, создайте дочерний узел под текущим узлом контекста в соответствии с совпавшим именем и атрибутом тега. Если тег не является самозакрывающимся тегом (br, img и т. д.), сделайте узел контекста новым узлом.
  • Если перехваченное содержимое является конечным тегом, закройте текущий узел контекста в соответствии с именем тега (установите узел контекста в качестве его родительского узла).
  • Если это обычный текст, текстовый узел создается под текущим узлом контекста, а узел контекста остается неизменным.

Процесс показан в таблице ниже:

контекст (перед синтаксическим анализом) Разобрать содержимое контекст (после разбора)
корневой узел <div class="content"> div
div <p style="text-indent: 2em;"> p
p Hello world p
p </p> div
div </div> корневой узел

После вышеуказанного процесса строка HTML проанализируется в дереве узла.

В сравнении

Сравните приведенный выше алгоритм с другими подобными алгоритмами синтаксического анализа (производительность измеряется путем «анализа HTML-кода длиной 10 000»):

Контраст Алгоритм этого компонента wxParse parse5
представление 3~6ms около 20 мс около 20 мс
Отказоустойчивость Разница в общем мощный
Размер файла (без сжатия) 6kb 22kb около 400кб

Видно, что алгоритм этого компонента имеет подавляющее преимущество по сравнению с двумя другими без учета отказоустойчивости (выдача неправильных результатов вместо выдачи исключений), что соответствует небольшой программе "маленький и быстрый"потребности. В нормальных условиях код, сгенерированный редактором форматированного текста, не будет содержать синтаксических ошибок. Так что даже с меньшей отказоустойчивостью это не большая проблема (но это требует улучшения).

Рендеринг шаблона

Рендеринг древовидной структуры неизбежно будет включать подузлырекурсияиметь дело с. Однако шаблон апплета не поддерживает рекурсию, что, похоже, ведет к большой яме.

Глядя на реализацию шаблона "wxParse" решает эту проблему просто и грубо: вложенные вызовы через 13 почти одинаковых шаблонов (1 вызов 2, 2 вызов 3, ..., 12 вызов 13), т.е. скажем, он может поддерживать до 12 вложений. В общем, этой глубины тоже достаточно.

Так как сам фреймворк "WEPY" имеет механизм сборки, вам не нужно писать один и тот же шаблон практически с одной моделью, вы можете сгенерировать его через встроенный плагин.

Ниже приведен шаблон, который необходимо многократно вкладывать (упрощенно), вставлять специальные комментарии до и после начала его кода, чтобы идентифицировать его, и использовать другой логотип специального комментария («»):

<!-- wepyhtml-repeat start -->
<template name="wepyhtml-0">
    <block wx:if="{{ content }}" wx:for="{{ content }}">
        <block wx:if="{{ item.type === 'node' }}">
            <view class="wepyhtml-tag-{{ item.name }}">
                <!-- next template -->
            </view>
        </block>
        <block wx:else>{{ item.text }}</block>
    </block>
</template>
<!-- wepyhtml-repeat end -->

Ниже приведен соответствующий код сборки (необходимо установить "wepy-plugin-replace»):

// wepy.config.js
{
    plugins: {
        replace: {
            filter: /\.wxml$/,
            config: {
                find: /<\!-- wepyhtml-repeat start -->([\W\w]+?)<\!-- wepyhtml-repeat end -->/,
                replace(match, tpl) {
                    let result = '';
                    // 反正不要钱,直接写个20层嵌套
                    for (let i = 0; i <= 20; i++) {
                        result += '\n' + tpl
                            .replace('wepyhtml-0', 'wepyhtml-' + i)
                            .replace(/<\!-- next template -->/g, () => {
                                return i === 20 ?
                                    '' :
                                    `<template is="wepyhtml-${ i + 1 }" wx:if="{{ item.children }}" data="{{ content: item.children"></template>`;
                            });
                    }
                    return result;
                }
            }
        }
    }
}

Однако после его запуска было обнаружено, что узлы второго и более глубоких уровней не отрисовывались, что свидетельствует о сбое вложенности. Глядя на файл wxml, созданный в каталоге dist, вы можете увидеть, что имя переменной не совпадает с исходным кодом компонента:

<block wx:if="{{ $htmlContent$wepyHtml$content }}" wx:for="{{ $htmlContent$wepyHtml$content }}">

Когда "WePY" генерирует код компонента, во избежание конфликта имени переменной между данными компонента и данными страницы, он будетДобавлять префикс к имени переменной компонента по определенным правилам(как в приведенном выше коде "htmlContentwepyHtml$"). Поэтому при создании вложенных шаблонов вы также должны использовать имена переменных с префиксом.

Сначала добавьте переменную thisIsMe в код компонента, чтобы определить префикс:

<!-- wepyhtml-repeat start -->
<template name="wepyhtml-0">
    {{ thisIsMe }}
    <block wx:if="{{ content }}" wx:for="{{ content }}">
        <block wx:if="{{ item.type === 'node' }}">
            <view class="wepyhtml-tag-{{ item.name }}">
                <!-- next template -->
            </view>
        </block>
        <block wx:else>{{ item.text }}</block>
    </block>
</template>
<!-- wepyhtml-repeat end -->

Затем измените код сборки:

replace(match, tpl) {
    let result = '';
    let prefix = '';

    // 匹配 thisIsMe 的前缀
    tpl = tpl.replace(/\{\{\s*(\$.*?\$)thisIsMe\s*\}\}/, (match, p) => {
        prefix = p;
        return '';
    });

    for (let i = 0; i <= 20; i++) {
        result += '\n' + tpl
            .replace('wepyhtml-0', 'wepyhtml-' + i)
            .replace(/<\!-- next template -->/g, () => {
                return i === 20 ?
                    '' :
                    `<template is="wepyhtml-${ i + 1 }" wx:if="{{ item.children }}" data="{{ ${ prefix }content: item.children }}"></template>`;
            });
    }

    return result;
}

На данный момент проблема рендеринга решена.

картина

Чтобы сэкономить трафик и повысить скорость загрузки, при отображении форматированного текстового содержимого внутреннее изображение обычно уменьшается в соответствии с требуемым размером, а исходное изображение отображается только при нажатии на маленькое изображение для предварительного просмотра. В основном это включает изменение свойств узла:

  • Сохраните исходный путь к изображению (значение атрибута src) в пользовательском атрибуте (например, «data-src») и добавьте его в массив изображений предварительного просмотра.
  • Измените значение атрибута src изображения на сокращенный URL-адрес изображения (обычно поставщики облачных услуг предоставляют такие правила для URL-адресов).
  • При нажатии на изображение предварительно отображается значение пользовательского свойства.

Чтобы выполнить это требование, этот компонент предоставляет ловушку при разборе узлов (onNodeCreate):

onNodeCreate(name, attrs) {
    if (name === 'img') {
        attrs['data-src'] = attrs.src;
        // 预览图数组
        this.previewImgs.push(attrs.src);
        // 缩图
        attrs.src = resizeImg(attrs.src, 640);
    }
}

Соответствующий шаблон и логика обработки событий выглядят следующим образом:

<template name="wepyhtml-img">
    <image class="wepyhtml-tag-img" mode="widthFix" src="{{ elem.attrs.src }}" data-src="{{ elem.attrs['data-src'] || elem.attrs.src }}" @tap="imgTap"></image>
</template>
// 点击小图看大图
imgTap(e) {
    wepy.previewImage({
        current: e.currentTarget.dataset.src,
        urls: this.previewImgs
    });
}

видео

В апплете уровень видеосоставляющей выше (и понизить нельзя). Если в дизайне страницы есть элементы, которые могут блокировать видео, для борьбы с ними требуются определенные навыки:

  • Скрыть видеокомпонент и занять место имиджевого компонента (обложка видео);
  • Когда изображение нажато, пусть видео воспроизводится в полноэкранном режиме;
  • Приостанавливает воспроизведение при выходе из полноэкранного режима.

Соответствующий код выглядит следующим образом:

<template name="wepyhtml-video">
    <view class="wepyhtml-tag-video" @tap="videoTap" data-nodeid="{{ elem.nodeId }}">
        <!-- 视频封面 -->
        <image class="wepyhtml-tag-img wepyhtml-tag-video__poster" mode="widthFix" src="{{ elem.attrs.poster }}"></image>
        <!-- 播放图标 -->
        <image class="wepyhtml-tag-img wepyhtml-tag-video__play" src="./imgs/icon-play.png"></image>
        <!-- 视频组件 -->
        <video style="display: none;" src="{{ elem.attrs.src }}" id="wepyhtml-video-{{ elem.nodeId }}" @fullscreenchange="videoFullscreenChange" @play="videoPlay"></video>
    </view>
</template>
{
    // 点击封面图,播放视频
    videoTap(e) {
        const nodeId = e.currentTarget.dataset.nodeid;
        const context = wepy.createVideoContext('wepyhtml-video-' + nodeId);
        context.play();
        // 在安卓微信下,如果视频不可见,则调用play()也无法播放
        // 需要再调用全屏方法
        if (wepy.getSystemInfoSync().platform === 'android') {
            context.requestFullScreen();
        }
    },
    // 视频层级较高,为防止遮挡其他特殊定位元素,造成界面异常,
    // 强制全屏播放
    videoPlay(e) {
        wepy.createVideoContext(e.currentTarget.id).requestFullScreen();
    },
    // 退出全屏则暂停
    videoFullscreenChange(e) {
        if (!e.detail.fullScreen) {
            wepy.createVideoContext(e.currentTarget.id).pause();
        }
    }
}

Открытый исходный код Наконец, вставьте репозиторий проекта «WePY HTML»:GitHub.com/Подготовка-Веб…, см. README в проекте для конкретного использования. Если у вас возникнут проблемы во время использования или у вас есть хорошие предложения и комментарии, вы можете отправить их в разделе «Вопросы».

(Эта статья также была опубликована в личном блоге автораМуронг Луо.life/статья/Порядочность…)

Категории