предисловие
localStorage — это схема сохранения клиентских данных в спецификации HTML 5. LocalStorage можно использовать для кэширования данных, хранения журналов и других сценариев приложений. Из-за некоторых особенностей самого localStorage:
- В соответствии с политикой того же происхождения
- Место для хранения обычно составляет около 5 МБ.
- Окончательная форма хранения пары ключ-значение — это строка.
Правильно использовать localStorage не так просто, в этой статье рассматриваются некоторые рекомендации по его использованию.
совместимость
Так как скорость поддержки браузером новых функций отличается от версии браузера пользователя, перед использованием localStorage необходимо использовать сниффинг, чтобы определить, поддерживает ли текущая среда:
function isLocalStorageUsable() {
const localStorageTestKey = '__localStorage_support_test';
const localStorageTestValue = 'test';
let isSupport = false;
try {
localStorage.setItem(localStorageTestKey, localStorageTestValue);
if (localStorage.getItem(localStorageTestKey) === localStorageTestValue) {
isSupport = true;
}
localStorage.removeItem(localStorageTestKey);
return isSupport;
} catch(e) {
return isSupport;
}
}
Хотя операции чтения и записи можно использовать для проверки того, поддерживает ли текущий браузер функцию localStorage, браузеры, не поддерживающие localStorage, должны иметь возможность выполнять операции записи, как упоминалось ранее."Место для хранения, выделенное браузером для localStorage, ограничено", когда сохраненный контент достигает верхнего предела, дальнейшие операции записи выполняться не могут.
try {
localStorage.setItem(localStorageTestKey, localStorageTestValue);
if (localStorage.getItem(localStorageTestKey) === localStorageTestValue) {
isSupport = true;
}
localStorage.removeItem(localStorageTestKey);
return isSupport;
} catch(e) {
if (e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
console.warn('localStorage 存储已达上限!')
} else {
console.warn('当前浏览器不支持 localStorage!');
}
return isSupport;
}
При вызове методов, связанных с localStorage, убедитесь, что текущий браузер поддерживает функцию localStorage.Здесь можно использовать кэширование значений, чтобы избежать потери производительности, вызванной многократными вызовами этого метода:
// 类的实例方法
ready() {
if (this.isSupport === null) {
this.isSupport = isLocalStorageUsable();
}
if (this.isSupport) {
return Promise.resolve();
}
return Promise.reject();
}
Определив вышеупомянутый готовый метод, метод сниффинга имеет"ленивое выполнение"характеристики.
пара ключ-значение
При передаче объекта напрямую в localStorage в виде пары ключ-значение неявно вызывается метод toString:
// 最终存储的键值为 key:[object Object] value: [object Object]
localStorage.setItem({}, {});
Если не обращать внимания на тип имен ключей, может возникнуть проблема потери данных из-за повторяющихся имен ключей.
Когда ключ localStorage является объектом, должны быть даны соответствующие предупреждения, чтобы в определенной степени избежать ошибок, вызванных низкоуровневыми ошибками:
function normalizeKey(key) {
if (typeof key !== 'string') {
console.warn(`${key} used as a key, but it is not a string.`);
key = String(key);
}
return key;
}
Для значения, если это сделать, сохраненное значение будет бессмысленным, поэтому сериализацию и десериализацию необходимо выполнять в соответствии с типом данных.
Сериализация
При вызове метода setItem для сохранения значения в localStorage сохраненное значение должно быть сериализовано единообразно:
// 类的实例方法
setItem(key, value) {
key = normalizeKey(key);
return this.ready().then(() => {
if (value === undefined) {
value = null;
}
serialize(value, (error, valueString) => {
if (error) {
return Promise.reject(error);
}
try {
// 可能会因超出最大存储空间,存储失败。
localStorage.setItem(key, valueString);
return Promise.resolve();
} catch(e) {
return Promise.reject(e);
}
})
})
}
Как правило, данные хранятся в формате JSON, который сериализуется с помощью JSON.stringify:
function serialize(value, callback) {
try {
const valueString = JSON.stringify(value);
callback(null, valueString);
} catch(e) {
callback(e);
}
}
Здесь вам нужно отлавливать исключения для метода JSON.stringify,"Этот метод выдает исключение при наличии циклической ссылки на сериализованный объект.". При десериализации также необходимо отлавливать исключения в JSON.parse.Частые ошибки:
JSON.parse('undefined');
// VM20179:1 Uncaught SyntaxError: Unexpected token u in JSON at position 0
В ответ на эту ситуацию вы можете сделать слой фильтрации в методе setItem:
if (value === undefined) {
value = null;
}
Однако нельзя полностью избежать недопустимых строк JSON, поэтому вам все равно нужно использовать try/catch для перехвата исключений.
Если бизнес-требования более сложны, необходимо выполнить конкретную обработку сериализации, оценив конкретный тип хранимого значения:
const toString = Object.prototype.toString;
function serialize(value, callback) {
const valueType = toString.call(value).replace(/^\[object\s(\w+?)\]$/g, '$1');
switch(valueType) {
case 'Blob':
const fileReader = new FileReader();
fileReader.onload = function() {
// 需要标记该值的类型
var str =
BLOB_TYPE_PREFIX +
value.type +
'~' +
bufferToString(this.result);
callback(null, SERIALIZED_MARKER + TYPE_BLOB + str);
}
fileReader.readAsArrayBuffer(value);
break;
default:
try {
const valueString = JSON.stringify(value);
callback(null, valueString);
} catch(e) {
callback(e);
}
}
}
Это увеличивает требования к хранилищу типа Blob, который необходимо сериализовать с помощью FileReader + ArrayBuffer, и требуется идентификатор, чтобы указать тип значения во время десериализации.
Оптимизация JSON.stringify
Метод JSON.stringify должен анализировать структуру объекта и тип пар ключ-значение во время выполнения (рантайма), что занимает очень много времени при работе со сложными вложенными объектами.
Метод оптимизации фактически состоит в том, чтобы передать эту часть трудоемкой работы на стадию компиляции, например:
const testObj = {
firstName: 'Matteo',
lastName: 'Collina',
age: 32
}
function stringify({ firstName, lastName, age }) {
return `"{"firstName":"${firstName}","lastName":"${lastName}","age":${age}}"`
}
В приведенном выше примере пара ключ-значение и тип объекта могут быть определены до выполнения, а соответствующие данные эталонного теста могут быть получены с помощью Benchmark.js:
const benchmark = require('benchmark');
const fastjson = require('fast-json-stringify');
const suite = new benchmark.Suite();
const testObj = {
firstName: 'Matteo',
lastName: 'Collina',
age: 32
}
function stringify({ firstName, lastName, age }) {
return `"{"firstName":"${firstName}","lastName":"${lastName}","age":${age}}"`
}
suite.add('JSON.stringify obj', function () {
JSON.stringify(testObj)
})
suite.add('fast-json-stringify obj', function () {
stringify(testObj)
})
suite.on('cycle', (e) => console.log(e.target.toString()))
.on('complete', function() {
console.log(`Fastest is ${this.filter('fastest').map('name')}`);
})
suite.run()
Полученные результаты тестирования показывают, что оптимизированная схема работает лучше с точки зрения количества выполнений тестового кода в секунду и статистической ошибки относительно максимальной скорости.
# 测试结果
JSON.stringify obj x 1,695,618 ops/sec ±0.62% (90 runs sampled)
fast-json-stringify obj x 787,253,287 ops/sec ±0.36% (92 runs sampled)
Fastest is fast-json-stringify obj
Приведенные выше примеры не являются универсальными. В реальном бизнес-разработке пользовательская схема JSON может использоваться для создания определенного метода stringify. Существуют зрелые платформы с открытым исходным кодом на выбор:
- fast-json-stringify
- slow-json-stringify (шутка от программиста)
Пространства имен
localStorage ограничен политикой того же источника.Этот уровень изоляции эквивалентен уровню приложения, но в реальном бизнес-процессе некоторые сценарии этого уровня изоляции не могут быть охвачены.
Основные возможности хранения пар ключ-значение в localStorage следующие:
- Количество пар ключ-значение можно получить через свойство length
- Пары ключ-значение индексируются по имени ключа
- Пары ключ-значение отсортированы в обратном хронологическом порядке.
Если каждому модулю в текущем приложении необходимо использовать localStorage для хранения данных, как изолировать его от уровня модуля?
Поскольку пары ключ-значение индексируются по имени ключа, вы можете добавить пространство имен к имени ключа, чтобы различать:
const keyPrefix = name + '/';
localStorage.setItem(keyPrefix + key, value);
В случае с несколькими модулями хранения вызов метода clear напрямую вызовет проблему ошибочного удаления данных другого модуля хранения, после введения пространства имен этой ситуации можно избежать:
function clear(keyPrefix) {
const keys = Object.keys(localStorage);
keys.forEach(key => {
if (key.indexOf(keyPrefix) === 0) {
localStorage.removeItem(key);
}
})
}
Суммировать
Наконец, резюмируя лучшие практики, упомянутые в этой статье:
- Используйте try/catch, чтобы проверить совместимость браузера, но следите за превышением лимита хранилища.
- Для функции, заключающейся в том, что все ключи и значения localStorage являются строками, принят унифицированный метод сериализации и десериализации.
- Для метода JSON.stringify можно принять соглашение о схеме JSON, чтобы перевести анализ структуры объекта на стадию компиляции для оптимизации эффективности выполнения.
- Командное пространство было введено для улучшения управления несколькими модулями.
использованная литература
- Исходный код localForage.js
В этой статье используетсяmdniceнабор текста