HTML 5 когда-то считался будущим мобильных приложений, но нативные приложения легко уступили ему с точки зрения производительности и функциональности, и Интернет постепенно стал дочерним предприятием приложений. Однако «отец» Джека Ма сказал нам: «Есть еще мечта, а вдруг она осуществится?» Теперь мы на шаг ближе к мечте.
PWA, полное название «Progressive Web App», представляет собой серию технических решений, предложенных Google для обеспечения пользовательского опыта в Интернете, подобного приложению. Его преимущества в основном отражаются в:
- Доступны нане в сетиилиплохая сетьОткройте страницу в обычном режиме в среде.
- Безопасный (HTTPS).
- Будьте в курсе (в курсе).
- Установка поддержки (добавить на домашний экран)а такжесообщение.
- обратная совместимость, к нему по-прежнему можно получить доступ в браузерах, не поддерживающих соответствующие технологии.
В этой статье один за другим описываются основные аспекты PWA.
CacheStorage
CacheStorage — это новое локальное хранилище, структура которого выглядит следующим образом:
Каждый домен имеет несколько модулей хранения, и каждый модуль может хранить несколько пар ключ-значение. его ключ сетевойпросить(Запрос), значение соответствует запросуотклик(Ответ). Интерфейс CacheStorage сосредоточен в глобальной переменной caches и доступен только по протоколу HTTPS (или домену localhost:*).Перед вызовом проверьте совместимость. Ниже приведен пример кода, реализующего загрузку ресурса и запись в кеш:
if (typeof 'caches' !== 'undefined') {
// 要缓存资源的URL
const URL = 'https://s3.imgbeiliao.com/assets/imgs/icons/app/1.0.0/parent-192x192.png';
// 存储模块名
const CACHE_KEY = 'v1';
fetch(URL, {
mode: 'no-cors'
}).then((response) => {
// 打开存储模块后往里面添加缓存
caches.open(CACHE_KEY).then((cache) => {
cache.put(url, response);
});
});
}
Который использует Fetch API для запроса ресурсов, цель этого API — заменить XMLHttpRequest.
Помимо записи в кеш, естественно есть интерфейсы для сопоставления кешей и удаления кешей:
// 在所有存储模块中匹配资源
caches.match(URL).then((response) => {
console.log(response);
});
// 在单个存储模块中匹配资源
caches.open(CACHE_KEY).then((cache) => {
cache.match(URL).then((response) => {
console.log(response);
});
});
// 删除整个存储模块
caches.delete(CACHE_KEY).then((flag) => {
console.log(flag);
});
// 删除存储模块中的某个存储项
caches.open(CACHE_KEY).then((cache) => {
if (cache) {
cache.delete(url).then((flag) => {
console.log(flag)
});
}
});
Хотя его можно вызывать независимо, CacheStorage обычно используется вместе с сервис-воркером, описанным ниже.
Service worker
Поскольку задачи, выполняемые Интернетом, становились все более и более сложными, браузеры также предоставили JavaScript возможности многопоточности —Web worker. Веб-воркеры позволяют программе JavaScript работать в потоке, отличном от основного потока. Но исходя из соображений безопасности потоков:
- Рабочие потоки не могут манипулировать некоторыми объектами (например, DOM) основного потока.
- Рабочий поток и основной поток не обмениваются данными и могут передавать данные только через механизм сообщений (postMessage).
Сервисный работник также является разновидностью веб-воркера, но его возможности намного сильнее, чем у обычных веб-воркеров, что в основном отражается в:
- После установки он существует навсегда, если не вытеснено;
- Просыпаться при использовании, спать при бездействии;
- Может действовать как прокси для перехвата запросов и ответов;
- Также доступно в автономном режиме.
- С большой силой приходит большая ответственность, поэтому сервис-воркеры доступны только по протоколу HTTPS (или доменам localhost:*).
регистр
новый сервисный работник пройтирегистр,Установить,активацияЭти три шага могут вступить в силу на странице. Первый шаг — зарегистрировать файл скрипта как Service worker:
function setupSW() {
var serviceWorker = window.navigator.serviceWorker;
if (!serviceWorker || typeof fetch !== 'function') {
return;
}
serviceWorker.register('/sw.js').then(function(reg) {
console.info('[SW]: Registered at scope "' + reg.scope + '"');
});
}
window.addEventListener('load', setupSW, false);
Суть операции регистрации заключается в открытии нового потока, что имеет определенные накладные расходы (от регистрации до активации фактическое время измерения iOS Safari и Chrome составляет 70~100 мс, а потребление времени браузером UC и браузером QQ больше чем 200 мс, все из которых являются тестами интрасети. В результате сетевые накладные расходы sw.js также включены в реальную среду), поэтому лучше всего выполнять его после загрузки страницы.
После завершения регистрации, установки и активации сервисный работник можетобъемСтраница внутри вступает в силу. Упомянутая здесь область действия — это не область действия переменной, а каталог, в котором находится сценарий сервисного работника. По умолчанию сервис-воркер может действоватьКаталог, в котором находится скрипт, и его подкаталогиВсе страницы ниже. Например, сервис-воркер, зарегистрированный в «/a/sw.js», может работать с «/a/page1.html» и «/a/b/page2.html», но не может работать с «/index.html». Однако также можно указать область действия с помощью параметров, например:
serviceWorker.register('/a/sw.js', {
scope: '/'
});
Однако этот код работает с исключением:
Failed to register a ServiceWorker: The path of the provided scope ('/') is not under the max scope allowed ('/a/'). Adjust the scope, move the Service Worker script, or use the Service-Worker-Allowed HTTP header to allow the scope.
Причина в том, что по умолчанию области могут быть только понижены, но не повышены. Если это необходимо улучшить, добавьте заголовок ответа HTTP в файл сценария"Service-Worker-Allowed". Например:
server {
location /a/sw.js {
add_header 'Service-Worker-Allowed' '/';
}
}
Кроме того, сценарий сервисного работника должен находиться в том же домене, что и страница. Во избежание проблем с областью действия рекомендуется поместить файл сценария вКорневой каталог домена, где находится страницаВниз.
Кстати, в практических приложениях рекомендуется добавлять переключатели в сервис-воркер. Поскольку это все-таки новая функция, я не знаю, будут ли неизвестные ямы.Как только произойдет крупномасштабный сбой, должен быть быстрый способ сделать его недействительным. Пример кода выглядит следующим образом:
fetch('/sw-enable?' + Date.now()).then(
// 200状态为开,其他状态为关
function(res) { return res.status === 200 ? 1 : -1; },
// 请求失败时不做任何操作
function() { return 0; }
).then(function(flag) {
if (flag === 1) {
serviceWorker.register('/sw.js');
} else if (flag === -1) {
serviceWorker.getRegistration('/sw.js').then(function(reg) {
if (reg) { reg.unregister(); }
});
}
});
Важно отметить, что если он закрыт, обязательно выйдите из Service worker. В противном случае для клиента, который зарегистрировал сервис-воркер, он все еще существует.
играет роль
Как только сервис-воркер активирован, он становитсяМежду страницей и браузеромагент. Все HTTP-запросы для всех страниц в его области (кроме себя) вызовут егополучить событие. Ниже в качестве примера используется совместимая обработка WebP, иллюстрирующая прокси-роль сервис-воркера.
WebP — это формат файла изображения, опубликованный Google. По сравнению с JPG, PNG и другими форматами файлы в формате WebP, как правило, меньше при одинаковом качестве. Однако Microsoft и Apple пока не поддерживают этот формат в своих браузерах, поэтому вопросы совместимости необходимо решать в практических приложениях.
В прошлом метод обработки совместимости в основном заключался в динамическом выводе пути к изображению после проверки совместимости. Но этот метод требует дополнительной обработки всех выходных изображений и не подходит для SEO. Service Worker может перехватить исходный запрос изображения (PNG, JPG) и «изменить» его на соответствующий запрос WebP.
// sw.js
self.addEventListener('fetch', (e) => {
// accept: image/webp,image/apng,image/*,*/*;q=0.8
const headers = e.request.headers;
const supportsWebP = headers.has('accept') && headers.get('accept').includes('webp');
const url = new URL(e.request.url);
if (supportsWebP && url.host.includes('qiniu')) {
url.search = '?imageMogr2/format/webp';
e.respondWith(
fetch(url.toString(), { mode: 'no-cors' })
);
}
});
Приведенный выше код прослушивает событие выборки, слушая:
- Определить поддержку браузером WebP (браузер, который поддерживает WebP, будет иметь «image/webp» в заголовке запроса на принятие);
- Если браузер поддерживает WebP, а пространство для хранения изображений также поддерживает преобразование WebP, генерируется соответствующий URL-адрес запроса WebP, и запрос выполняется через Fetch API;
- Используйте ответ Fetch API в качестве ответа на этот запрос через метод «respondWith» объекта события.
На этом функция перехвата исходного запроса и перенаправления его на другой запрос завершена.
Взаимодействие с CacheStorage
Мы также можем взаимодействовать с CacheStorage в сценариях сервис-воркеров для кэширования и извлечения ресурсов.
Первая стратегия кэшированияпредварительное кэширование. Его принцип заключается в кэшировании некоторых ресурсов в событии установки сервис-воркера и завершении установки после успешного кэширования этих ресурсов.
// sw.js
const CACHE_KEY = 'v1';
const cacheList = [
'/js/jquery.js',
'/style/reset.css'
];
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open(CACHE_KEY).then((cache) => {
return cache.addAll(cacheList);
});
);
});
Преимущество этой стратегии заключается в том, что если сервис-воркер успешно установлен, кэш гарантированно будет доступен (исключая такие факторы, как нехватка места для хранения). Однако нельзя игнорировать его недостатки: до тех пор, пока не будет выполнен запрос на предварительно кэшированный ресурс, установка сервис-воркера не будет выполнена. следовательно,Чем меньше ресурсов для предварительного кэширования, тем лучше.
После успешного предварительного кэширования вы можете ответить, сопоставив ресурсы в кэше в событии выборки:
// sw.js
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then((response) => {
if (response != null) {
return response;
} else {
return fetch(e.request.url);
}
})
);
});
Вторая стратегия кэшированияИнкрементный кеш, процесс очень прост: если запрошенный ресурс сопоставляется в кеше, он ответит напрямую, в противном случае запрос будет отправлен, а ресурс будет закэширован перед ответом. Следует отметить, что ресурсы с ненормальным статусом (таким как код состояния HTTP 404 или 500) не должны кэшироваться. Код реализован следующим образом:
// sw.js
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then((res) => {
if (res != null) {
return res;
} else {
return fetch(url).then((res) => {
if (res && (res.status === 200 || res.status === 304)) {
const resCache = res.clone();
caches.open(CACHE_KEY).then((cache) => {
cache.put(url, resCache);
});
}
return res;
});
}
});
);
});
При практическом применении необходимо исключить некоторые специальные запросы:
- Браузеры позволяют загружать такие ресурсы, как изображения и видео протокола HTTP, через теги HTML на страницах протокола HTTPS. Однако Fetch API этого не позволяет. Поэтому не используйте Fetch API для отправки запросов протокола HTTP.
- Запросы на сторонние ресурсы не должны кешироваться, например ресурсы с различных статистических платформ.
- Запросы, отличные от GET, не следует кэшировать, так как большинство из них связано с отправкой данных на серверную часть и выполнением ею каких-либо действий.
- Интерфейсы коммутатора Service Worker не должны кэшироваться.
Код реализован следующим образом:
// sw.js
self.addEventListener('fetch', (e) => {
let url = new URL(e.request.url);
if (url.protocol === 'http:' ||
(url.host !== location.host && url.host.includes('.abc-cdn.com')) ||
e.request.method !== 'GET' ||
url.pathname.indexOf('sw-enable') !== -1
) {
return;
}
url = url.toString();
e.respondWith(
// ...
);
});
возобновить
Новый сервис-воркер устанавливается всякий раз, когда браузер обнаруживает, что содержимое файла скрипта сервис-воркера изменилось. Однако по умолчанию новый сервис-воркер находится в состоянии ожидания, и все страницы, связанные со старым сервис-воркером, должны быть закрыты и повторно открыты, прежде чем новый сервис-воркер будет активирован. Если вы хотите, чтобы новый сервис-воркер вступил в силу немедленно, вы можете вызвать «self.skipWaiting» в событии установки:
// sw.js
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open(CACHE_KEY).then((cache) => {
return cache.addAll(cacheList);
}).then(() => {
return self.skipWaiting();
})
);
});
Важно отметить, что для файла сценария Service worker должно быть задано значениеникогда не кэшировать(максимальный возраст: 0). В противном случае, даже если его содержимое изменится, браузер об этом не узнает и не сможет его обновить. На самом деле браузеры тоже учитывают кеширование: чтобы плохие скрипты не действовали долгое время, скрипты сервис-воркеров нужно скачивать каждые 24 часа.
Если говорить об этом, то по сути реализовано только обновление самого сервис-воркера, а как дальше обновлять кеш ресурсов в CacheStorage? Как упоминалось выше, CacheStorage хранится в модулях, используя эту структуру хранения, можно заменять модуль хранения каждый раз при выпуске кода. Поскольку новый модуль хранения пуст, согласно механизму инкрементного кэширования браузер получит этот ресурс через сеть или HTTP-кеш. код показывает, как показано ниже:
// sw.js
const CACHE_KEY = 'v2'; // 下次发布时改成v3
caches.keys().then(function(keys) {
keys.forEach(function(key) {
if (key !== CACHE_KEY) {
caches.delete(key);
}
});
});
Жизненный цикл
Говоря об этом, на самом деле было затронуто большинство звеньев в жизненном цикле сервисного работника.Следующее резюмируется с помощью диаграммы жизненного цикла:
Сравнение производительности
После реализации добавочного кэша он эквивалентен просмотру в автономном режиме, если страница открывается один раз. Ниже приведено сравнение производительности двух схем кэширования (Service worker + CacheStorage, кэширование HTTP). Первый — сравнение при нормальной скорости сети:
Можно обнаружить, что особой разницы нет. На самом деле это тоже легко понять.Кэшированные ресурсы, будь то CacheStorage или HTTP-кеш, либо существуют на диске, либо были перенесены в память браузером.Поскольку источник один и тот же, скорость чтения, естественно, примерно равна такой же.
Давайте посмотрим на ситуацию в медленной сети 3G:
Можно обнаружить, что скорость запроса HTML-документов сильно отличается. В схеме Service worker + CacheStorage HTML-документ кэшируется, в схеме HTTP-кэширования код состояния HTML-документа равен 304, что указывает на то, что браузер отправил запрос на сервер. На этот раз HTTP-запрос занимает много времени, когда сеть работает медленно.
Если вы установите время истечения срока действия (max-age) для HTML-документа и позволите браузеру кэшировать его, не будет ли этой разницы? Реальность не так проста:
- Даже с установленным сроком действия некоторые браузеры по-прежнему запрашивают сервер, например Chrome для ПК и платформ Android.
- Нет хорошего способа сказать браузеру очистить кеш при изменении кода.
- В традиционных внутренних приложениях рендеринга слишком много HTML-документов (например, каждая новостная статья NetEase является HTML-документом), и все они будут занимать много места при кэшировании.
Поэтому обычно не устанавливайте время кэширования для HTML-документов или устанавливайте очень короткое время кэширования. Однако HTML-документ как точка входа на страницу имеет большое значение для кэширования. Поскольку есть Service worker, вы можете сделать:
Перехватывайте запросы HTML-документов, проверяйте CacheStorage и затем решайте, запрашивать ли сервер; Вовремя очищайте кеш, изменяя скрипт сервис-воркера. Кроме того, режим внешнего рендеринга может реализовать, что одному HTML-документу соответствует несколько копий одного и того же контента, одностраничные приложения, разработанные на основе Vue.js, React, Angular и других фреймворков, даже имеют только один HTML-документ.
Подводя итог, можно сказать, что кэширование HTML-документов с помощью Service Worker и CacheStorage в режиме внешнего рендеринга может эффективно повысить скорость загрузки страниц при нестабильной сети. А поскольку сами статические ресурсы имеют HTTP-кеширование, нет необходимости кэшировать все статические ресурсы в CacheStorage (кешируются только критические части).
резюме
Наконец, мы должны разобраться с вопросом: механизм кэширования Service worker + CacheStorage на самом деле похож на HTTP-кеширование, зачем нам два одинаковых кэша?
- Во-первых, кеш HTTP контролируется сервером (заголовок ответа), и сервер не может уведомить браузер о необходимости очистки кеша до истечения срока действия кеша;
- Во-вторых, сервис-воркер может реализовать эффективный контроль над кешем на стороне браузера, включая политику кеша и очистку кеша;
- В-третьих, сервис-воркер поддерживает работу в автономном режиме и может быстро реагировать в случае отсутствия связи или плохой сети, что особенно важно для мобильных сетей с нестабильным сигналом.
Кстати, Application Cache (автономный кеш) в HTML 5 больше не рекомендуется из-за отсутствия гибкости в практических приложениях, да и от стандарта отказались.
Работник службы доступа в проекте Vue.js
Преимущества, которые дает Service Worker, побуждают меня интегрировать его в проект Давайте возьмем в качестве примера типичный проект Vue.js, чтобы поговорить о процессе интеграции.
Первым шагом является регистрация скрипта сервис-воркера.Чтобы выполнить этот шаг после того, как компоненты страницы будут загружены максимально, вы можете поместить этот фрагмент кода в смонтированный хук корневого экземпляра Vue.js (main.js ) выполнить:
// main.js
new Vue({
mounted() {
// 本地开发时不启用Service worker
if (['test', 'pre', 'prod'].indexOf(env) === -1) { return; }
const serviceWorker = window.navigator.serviceWorker;
if (!serviceWorker || typeof fetch !== 'function') { return; }
fetch('/sw-enable?' + Date.now()).then(
(res) => { return res.status === 200 ? 1 : -1; },
() => { return 0; }
).then((flag) => {
if (flag === 1) {
serviceWorker.register('/sw.js');
} else if (flag === -1) {
serviceWorker.getRegistration('/sw.js').then((reg) => {
if (reg) { reg.unregister(); }
});
}
});
});
});
Содержимое скрипта сервис-воркера примерно такое же, как указано выше (здесь только предварительно кэшировано):
// 缓存模块(版本号)
const CACHE_KEY = 'v$REV';
// 要预缓存的资源列表
const cacheList = [
'/index.html',
'https://abc-cdn.com/polyfill.min.js'
];
self.addEventListener('install', (e) => {
e.waitUntil(
caches.keys().then((keys) => {
// 清理旧缓存
keys.forEach((key) => {
if (key !== CACHE_KEY) { caches.delete(key); }
});
}).then(() => {
// 预缓存
return caches.open(CACHE_KEY)
.then((cache) => { return cache.addAll(cacheList); })
}).then(() => {
// 跳过等待
return self.skipWaiting();
});
);
});
self.addEventListener('fetch', (e) => {
const url = new URL(e.request.url);
if (url.protocol === 'http:' ||
url.pathname.includes('sw-enable') ||
e.request.method !== 'GET' ||
(url.host !== location.host && cacheList.indexOf(e.request.url) === -1)
) {
return;
}
// 判断是否HTML文档的请求
const isHTMLDoc = e.request.headers.has('accept') &&
e.request.headers.get('accept').includes('text/html') &&
(url.pathname.endsWith('.html') || !/\.\w+$/.test(url.pathname));
// 基于Vue.js的单页应用只有一个HTML文档,所有HTML文档的请求可以全部指向一个文件
const request = isHTMLDoc ? new Request('/index.html') : e.request;
e.respondWith(
caches.match(request).then((res) => {
if (res != null) {
return res;
} else {
return fetch(url.toString());
}
})
);
});
Особо следует отметить:
- «$REV» — это заполнитель, который нужно заменить на конкретный номер версии в процессе сборки Webpack; Первый элемент в предварительно кэшированном ресурсе — это HTML-документ (одностраничное приложение имеет только один HTML-документ, и только он кэшируется), а второй элемент — это ключевой статический ресурс (полифилл ES6);
- Запросы для всех HTML-документов в текущем домене фактически указывают на один и тот же запрос (index.html).
Наконец, добавьте шаг в процесс сборки Webpack, замените «$REV» сценария сервис-воркера новым номером версии (временной меткой) и скопируйте его по пути, где находится index.html (убедитесь, что они находятся в тот же домен):
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../src/sw.js'),
to: path.dirname(config.build.index), // index.html所在路径
transform(content, path) {
return content.toString().replace('$REV', Date.now());
}
}
])
Web App Manifest
В этом разделе представлен простой файл конфигурации JSON, пример кода выглядит следующим образом (manifest.json):
{
"name": "贝聊官网",
"short_name": "贝聊官网",
"start_url": "/",
"display": "standalone",
"background_color": "#fff",
"theme_color": "#fff",
"orientation": "portrait",
"description": "中国幼儿园家长工作平台",
"icons": [{
"src": "https://s2.imgbeiliao.com/assets/imgs/icons/app/1.0.0/parent-192x192.png",
"type": "image/png",
"sizes": "192x192"
}, {
"src": "https://s2.imgbeiliao.com/assets/imgs/icons/app/1.0.0/parent-512x512.png",
"type": "image/png",
"sizes": "512x512"
}]
}
Несколько ключевых элементов конфигурации включают в себя:
- имя: Имя приложения.
- short_name: короткое имя приложения, которое используется для отображения в местах с меньшим пространством, например, в виде значков на рабочем столе.
- start_url: Путь к начальной странице.
- отображение: существует четыре режима отображения: полноэкранный (полный экран), автономный (пробел, кроме строки состояния), минимальный интерфейс (меню навигации в браузере), браузер (открыть в браузере).
- значки: укажите значки различных размеров.
После написания такого конфигурационного файла необходимо сослаться на него в HTML-документе через тег ссылки:
<link rel="manifest" href="/manifest.json" />
На этом основании, если также выполняются следующие условия:
- Файл манифеста настраивает следующие элементы:
- короткое имя;
- Так;
- начальный_адрес;
- Иконка 192×192 png.
- На странице используется протокол HTTPS и зарегистрирован сервис-воркер.
- Пройдите интервью не менее двух раз с интервалом не менее пяти минут между интервью.
После открытия страницы в браузере Chrome появится всплывающий баннер «Добавить на главный экран» (далее «Баннер A2HS»). После нажатия значка главного экрана для входа в приложение сначала появится экран запуска (Примечание: здесь будут отображаться значки размером 512x512 или больше), а затем вы перейдете на стартовую страницу приложения.
Браузеры, поддерживающие баннеры A2HS, включают Chrome, браузер UC и браузер Xiaomi, все они работают на платформе Android. В других браузерах функциональные меню или кнопки можно найти только вручную и добавить на главный экран.
Наконец, давайте поговорим о некоторых проблемах с файлом манифеста:
- После изменения файла манифеста его необходимо повторно добавить на главный экран, чтобы он вступил в силу.
- Проблемы под iOS:
- Начальный экран представляет собой белый экран;
- Потеряв контекст, каждый раз, когда вы входите в приложение (включая перезапуск, возврат на главный экран и последующий вход), вы будете возвращаться на стартовую страницу, что является самой серьезной проблемой.
- Некоторые элементы конфигурации недействительны, включая background_color, theme_color, ориентацию и значки. Где значки можно настроить через метки:
<link rel="apple-touch-icon" sizes="192x192" href="..." />
статус-кво
Статус-кво PWA можно резюмировать в этом классическом предложении:
Будущее светлое, дорога извилистая
Сначала посмотрите на картинку совместимости:
видимый:
- Самая совершенная поддержка PWA — это Chrome, но его доля на рынке в Китае невелика, а некоторые сервисы недоступны.
- Доступность Service Worker и CacheStorage высока;
- Доступность push-уведомлений низкая (поэтому в этой статье это не рассматривается);
- В браузерах отечественных производителей отсутствует функциональное меню «Добавить на рабочий стол», если баннер A2HS закрыт, то нет другого способа добавить приложения на рабочий стол.
Кроме того, iOS Safari поддерживает большинство функций PWA, начиная с iOS 11.3, но есть более серьезные проблемы с опытом — каждый раз, когда вы выходите из PWA, контекст будет потерян.
Подводя итог, можно сказать, что для большинства предприятий создание полноценного приложения PWA не является разумным выбором. Однако имеет смысл улучшить взаимодействие с пользователем с помощью хорошо поддерживаемых Service Worker и CacheStorage. С другой стороны, хотя Интернет и конкурирует с нативными приложениями, в большинстве случаев они сотрудничают друг с другом — большинство приложений имеют встроенные веб-страницы для реализации некоторых функций. Поэтому вы можете рассмотреть возможность поддержки вышеуказанных технологий в WebView приложения, чтобы обеспечить поддержку Интернета.
Эта статья также опубликована в личном блоге автора:Муронг Луо.life/статья/Порядочность….