предисловие
В этой статье описывается, как строится автономная система ведения журналов переднего плана, как с теоретической, так и с практической точек зрения.Из-за большого объема контента я разделю статью на три части, чтобы описать структуру всей системы ведения журналов.
- Внешний дизайн хранилища данных — IndexedDB
- Дизайн сервера — node + express/koa и сжатие данных — deflate/gzip
- (Изучить) WebRTC реализует сбор журналов
Зачем нужны автономные журналы
По мере того, как интерфейсные проекты становятся более сложными, важность интерфейсных журналов становится все более заметной.Обычно мы используем метод отчетов о сетевых запросах для записи журналов, таких какbadjs, Союзникcnzzи т.п. Существует сетевая просьба отчетности о боли от:
- Плохая поддержка слабых сетевых сред или отключенных сетевых сред.
- Требования к серверу высокие.
- Постоянные отчеты журнала могут тратить ресурсы сети пользователя.
При разработке баджей, чтобы решить вышеуказанные проблемы, мы сообщаем о некоторых логах без ошибок путем внесения в белый список (для облегчения проблем с запросами), то есть сообщать данные могут только пользователи, отвечающие определенным условиям. Метод белых списков также приносит некоторые проблемы, например, страница отзывов пользователей имеет белый экран, но мы не нашли лог текущего пользователя в фоне badjs, это может быть просто из-за того, что сеть пользователя не загружает определенный js, но Вы не можете привести убедительных доказательств.
Так появились автономные журналы, которые практически решили все вышеперечисленные болевые точки и широко используются в клиентах. Почему не существует подходящей платформы офлайн-приложений для внешнего интерфейса? Во-первых, из-за недостатков предыдущей технологии до IndexedDB почти не было простого способа хранить автономные журналы на внешнем интерфейсе. Хотя localstorage может в определенной степени удовлетворить потребности, его проблемы также очевидны.
- Синхронное чтение и запись данных приведет к определенной степени блокировки.
- Ограничение размера данных.
- По сути строки, что приводит к большому количеству операций со строками.
- Метод хранения «ключ-значение» обеспечивает более сложные операции CURD.
Только с рождением IndexedDB и поддержкой большинства браузеров офлайн-журнал переднего плана получил зрелую возможность.
Введение в IndexedDB
Проще говоря, IndexedDB — это база данных «ключ-значение» на основе браузера, которая поддерживает транзакционные транзакции и поддерживает индексы. Хотя IndexedDB не может использовать операторы SQL, для хранилища требуется структура данных (можно хранить как текст, файлы, так и большие двоичные объекты), а операция запроса выполняется с помощью указателя, сгенерированного индексом.
IndexedDB имеет следующие преимущества:
- Простое в использовании хранилище ключей и значений на основе объектов JavaScript.
- Асинхронный API. Это очень важно для внешнего интерфейса, поскольку доступ к базе данных не блокирует вызывающий поток.
- Очень большое место для хранения. Теоретически максимального ограничения нет, если он превышает 50 МБ, пользователю потребуется подтвердить разрешение на запрос.
- Поддерживаются транзакции, и любая операция в IndexedDB происходит в транзакции.
- Поддержка веб-воркеров. Синхронный API должен использоваться в одних и тех же веб-воркерах.
- Та же политика происхождения, обеспечить безопасность.
- Неплохосовместимость
основная концепция
Поскольку IndexedDB — это низкоуровневый API, перед использованием IndexedDB необходимо понять некоторые основные понятия.
- IDBFactory: window.indexedDB, который обеспечивает доступ к базе данных.
- IDBOpenDBRequest: возвращаемый результат indexedDB.open() представляет запрос на открытие базы данных.
- IDBDatabase: указывает на подключение к базе данных IndexedDB, через которое можно получить только одну транзакцию базы данных.
- IDBObjectStore: Store Object, могут быть несколько IDBObjectStores в IDBDatabase, аналогично таблице или документу в MongoDB.
- IDBTransaction: представляет собой транзакцию, транзакция создается, когда необходимо указать область доступа и тип доступа (чтение или запись).
- IDBCursor: индекс базы данных, используемый для обхода пространства хранения объектов.
Основная операция
Первый шаг — открыть базу данных
const request = window.indexedDB.open('test', 1)
test представляет имя базы данных, если база данных не существует, она будет создана автоматически. Второй параметр указывает версию базы данных, выраженную целым числом, по умолчанию 1.
indexedDB.open()
Возвращает объект IDBOpenDBRequest через три события.onerror
, onsuccess
, onupgradeneeded
для обработки операции открытия базы данных.
let db
const request = indexedDB.open('test')
request.onerror = function(event) {
console.error('open indexedDB error')
}
request.onsuccess = function(event) {
db = event.target.result
console.log('open indexedDB success')
}
request.onupgradeneeded = function(event) {
db = event.target.result
console.log('upgrade indexedDB success')
}
Событие onupgradeneeded запускается при создании новой базы данных или увеличении номера версии существующей базы данных (при открытии базы данных укажите более высокий номер версии, чем раньше).
существуетonsuccess
а такжеonupgradeneeded
прошедшийevent.target.result
чтобы получить экземпляр базы данных.
Второй шаг — создать новую базу данных и таблицу
В использованииindexedDB.open()
После метода база построена, но ничего не произошло. мы проходимdb.createObjectStore()
для создания таблицы.
request.onupgradeneeded = function(event) {
db = event.target.result
console.log('upgrade indexedDB success')
if (!db.objectStoreNames.contains('logs')) {
const objectStore = db.createObjectStore('logs', { keyPath: 'id' })
}
}
Приведенный выше код создаст таблицу с именем logs, первичный ключ — id, если вы хотите автоматически сгенерировать первичный ключ, вы также можете написать:
const objectStore = db.createObjectStore('logs', { autoIncrement: true })
keyPath & autoIncrement
keyPath | autoIncrement | описывать |
---|---|---|
No | No | ObjectStore может хранить значения любого типа, но если вы хотите добавить значение, вы должны указать отдельный ключевой параметр. |
Yes | No | Можно сохранять только объекты JavaScript, и объект должен иметь свойство с тем же именем, что и ключевой путь. |
No | Yes | Любой тип значения может быть сохранен. Ключи генерируются автоматически. |
Yes | Yes | Можно хранить только объекты JavaScript, обычно когда генерируется ключ, значение сгенерированной клавиши хранится в свойстве объекта с тем же именем, что и ключевой путь. Однако, если такой атрибут уже существует, значение этого атрибута используется в качестве ключа без генерации нового ключа. |
Третий шаг — создание индекса
Создайте индекс через objectStore:
// 创建一个索引来通过时间搜索,时间可能是重复的,所以不能使用 unique 索引。
objectStore.createIndex('time_idx', 'time', { unique: false })
// 使用邮箱建立索引,为了确保邮箱不会重复,使用 unique 索引
objectStore.createIndex("email", "email", { unique: true })
Три параметра IDBObject.createIndex(): «имя индекса», «атрибут, соответствующий индексу», атрибут индекса (уникальный индекс или нет).
Четвертый шаг, вставка данных
Вставка данных в IndexedDB должна выполняться через транзакции.
// 使用事务的 oncomplete 事件确保在插入数据前对象仓库已经创建完毕
objectStore.transaction.oncomplete = function(event) {
// 将数据保存到新创建的对象仓库
const transaction = db.transaction('logs', 'readwrite')
const store = transaction.objectStore('logs')
store.add({
id: 18,
level: 20,
time: new Date().getTime(),
uin: 380034641,
msg: 'xxxx',
version: 1
})
}
При инициализации IndexedDB будет срабатывать событие onupgradeneeded, а при последующих обращениях к БД будет срабатывать только событие onsuccess. Поэтому мы будем инкапсулировать операцию CURD базы данных следующим образом.
CURD
Добавить данные
Предположим, ранее была создана база данных с именем logs и keyPath 'id'.
function addLog (db, data) {
const transaction = db.transaction('logs', 'readwrite')
const store = transaction.objectStore('logs')
const request = store.add(data)
request.onsuccess = function (e) {
console.log('write log success')
}
request.onerror = function (e) {
console.error('write log fail')
}
}
addLog(db, {
id: 1,
level: 20,
time: new Date().getTime(),
uin: 380034641,
msg: 'add new log',
version: 1
})
При записи данных нужно указать имя таблицы, затем создать транзакцию, получить объект IDBObjectStore через objectStore, а затем вставить его через метод add.
обновить данные
Операцию обновления данных можно выполнить с помощью метода put объекта IDBObjectStore.
function updateLog (db, data) {
const transaction = db.transaction('logs', 'readwrite')
const store = transaction.objectStore('logs')
const request = store.put(data)
request.onsuccess = function (e) {
console.log('update log success')
}
request.onerror = function (e) {
console.error('update log fail')
}
}
updateLog(db, {
id: 1,
level: 20,
time: new Date().getTime(),
uin: 380034641,
msg: 'this is new log',
version: 1
})
IndexeDB использует метод put для обновления данных, но предпосылка put заключается в том, что должен быть уникальный индекс, и IndexeDB обновляет данные в соответствии с уникальным индексом в качестве ключа. Метод put аналогичен upsert: если значение, соответствующее уникальному индексу, не существует, новые данные вставляются напрямую.
читать данные
С помощью метода get объекты IDBObjectStore могут быть выполнены для операции чтения данных. Обновите те же данные, данные считываются методом get, также требуется уникальный индекс. Просмотрите данные, прочитанные при событии успеха.
function getLog (db, key) {
const transaction = db.transaction('logs', 'readwrite')
const store = transaction.objectStore('logs')
const request = store.get(key)
request.onsuccess = function (e) {
console.log('get log success')
console.log(e.target.result)
}
request.onerror = function (e) {
console.error('get log fail')
}
}
getLog(db, 1)
удалить данные
function deleteLog (db, key) {
const transaction = db.transaction('logs', 'readwrite')
const store = transaction.objectStore('logs')
const request = store.delete(key)
request.onsuccess = function (e) {
console.log('delete log success')
}
request.onerror = function (e) {
console.error('delete log fail')
}
}
При удалении данных, даже если данные не существуют, они обычно входят в событие onsuccess.
использовать курсор
Поскольку IndexedDB не предоставляет возможности SQL, часто, когда мы хотим найти какие-то данные, мы можем только просмотреть их.
function getAllLogs (db) {
const transaction = db.transaction('logs', 'readwrite')
const store = transaction.objectStore('logs')
const request = store.openCursor()
request.onsuccess = function (e) {
console.log('open cursor success')
const cursor = event.target.result
if (cursor && cursor.value) {
console.log(cursor.value)
cursor.continue()
}
}
request.onerror = function (e) {
console.error('oepn cursor fail')
}
}
Курсор перемещается по таблице рекурсивным образом и входит в следующий цикл через метод cursor.continue().
использовать индекс
В предыдущих примерах мы получали данные через первичный ключ, а через метод индекса мы можем использовать другие атрибуты для поиска данных.
Предположим, что индекс uin создается при создании таблицы.
objectStore.createIndex('uin_index', 'uin', { unique: false })
При запросе данных вы можете использовать метод индекса uin:
function getLogByIndex (db) {
const transaction = db.transaction('logs', 'readonly')
const store = transaction.objectStore('logs')
const index = store.index('uin_index')
const request = index.get(380034641) // 注意这里数据类型要一致
request.onsuccess = function (e) {
const result = e.target.result
console.log(result)
}
}
Используя приведенный выше метод запроса индекса, можно найти только первые данные, соответствующие условиям.Если вы хотите найти больше данных, вам нужно объединить курсор для работы.
function getAllLogsByIndex (db) {
const transaction = db.transaction('logs', 'readonly')
const store = transaction.objectStore('logs')
const index = store.index('uin_index')
const request = index.openCursor(IDBKeyRange.only(380034641)) // 这里可以直接写值
request.onsuccess = function (e) {
const cursor = event.target.result
if (cursor && cursor.value) {
console.log(cursor.value)
cursor.continue()
}
}
}
- IDBKeyRange.only(val) Получить только указанные данные
- IDBKeyRange.lowerBuund(val, isOpened) Получить данные до или меньше, чем val, isOpened — это открытый и закрытый интервал, false включает val (закрытый интервал), true — исключает val (открытый интервал)
- IDBKeyRange.upperBuund(val, isOpened) Получить данные после val или big, isOpened — это значение открытого и закрытого интервала, false — включать val (закрытый интервал), true — не включать val (открытый интервал)
- IDBKeyRange.buund(val1, val2, isOpened1, isOpened2) Получить данные между value1 и value2, isOpened1 и isOpened2 — левый и правый интервалы открытия и закрытия соответственно
Как правило, многорежимные операции запроса выполняются с помощью указанных выше методов объекта IDBKeyRange.
Как спроектировать интерфейсную автономную базу данных
В предыдущем примере вы уже можете видеть прототип базы данных, структура таблицы выглядит следующим образом:
- from - источник журнала
- id - идентификатор эскалации
- level - уровень лога
- msg - информация журнала
- time - время генерации журнала
- uin - уникальный идентификатор пользователя
- version - версия лога
Это сообщаемый контент, как разработать интерфейс?
Во внешней автономной системе журналов должны быть предусмотрены как минимум пять следующих интерфейсов:
- Очистите интерфейс журнала. Поскольку пользовательские журналы генерируются постоянно, данные не могут накапливаться бесконечно, поэтому обычно устанавливается фиксированное количество дней, а журналы с истекшим сроком действия очищаются путем проверки журналов при каждом запуске системы.
- Написать интерфейс журнала. Благодаря асинхронной записи журналов система позволяет системе постоянно записывать новые журналы.
- Поиск связанных интерфейсов. В том числе поиск в логах текущего пользователя, логах фиксированного периода времени, логах фиксированного уровня и т.д. Это удобно для составления отчетов и сбора данных для получения соответствующей информации журнала.
- Интерфейс сортировки и сжатия данных. Поскольку объем журнала пользователя может быть очень большим, размер сообщаемых данных может быть эффективно уменьшен путем сортировки и сжатия данных.
- Интерфейс отчетности данных.
В частности, вы можете просмотретьwardjs-reportавтономный модуль в проекте.
Потому что естьwx.getStorage(Object object)
интерфейс, поэтому он также может имитировать функцию хранения автономных журналов. эта веткаfeat_miniprogramЭто решение для автономной отчетности нашего апплета.
Тест производительности IndexedDB
Производительность IndexedDB очень высока, и он в основном асинхронный, поэтому, хотя он и воздействует на браузер, обычные операции чтения и записи в принципе не будут иметь дополнительного влияния на продукт.
тестовая среда
iMac 4GHz i7/16GB DDR3 macOS Majave 10.14.2 Chrome 72.0
подготовка данных
Вставьте 1w фрагментов данных журнала, каждый длиной 500.
Время тестирования
Время подключения к БД (в среднем 10 раз): 3,5 мс Вставка 1 данных (в среднем 10 раз): менее 1 мс Подключить БД -> вставить данные -> освободить соединение (в среднем 10 раз): 4,3 мс Вставка 10 данных одновременно (в среднем 10 раз): менее 1 мс
Тест мобильного телефона
iPhone 6sp iOS 12.1.4 safari
Время тестирования
Время подключения к БД (в среднем 10 раз): 2,3 мс Вставка 1 данных (в среднем 10 раз): менее 1 мс Подключение к базе данных для вставки данных (в среднем 10 раз): 2,3 мс. Вставка 10 данных одновременно (в среднем 10 раз): менее 1 мс
Результаты теста довольно странные, результаты на мобильном телефоне лучше, чем на ПК. Возможно, это как-то связано с браузером, и когда я тестировал его в то время, на компьютере было много приложений, которые не были связаны с вкладкой Chrome.Это также может иметь влияние.
Но я не проводил тест снова, потому что приведенные выше данные достаточно удивительны, вы можете понять, что нам в принципе не нужно много времени, чтобы просто вставить данные. Да, IndexedDB настолько мощен.
Эпилог
Из-за слишком большого содержания он разбит на несколько статей. В этом документе кратко представлены тесты использования и производительности IndexedDB. При построении внешней автономной системы журналов это наиболее критическое кольцо, хранилище данных, как для обеспечения надежности данных, так и для поддержания определенной производительности, и не может создавать побочных эффектов от нормальной работы пользователя. , с помощью нашего простого теста я думаю, что indexeddb вполне может конкурировать за эту работу.
Обратите внимание на общедоступный номер:【Сообщество IVWEB】, и продвигайте еженедельные журналы о бутиках технологий каждую неделю.