preface
LocalStorage is the HTML5 specification as a persistent client data preservation scheme, localStorage can be used for data caching, log storage and other application scenarios. Due to some characteristics of localStorage itself:
- Restricted by the same origin policy
- The storage space is about 5MB
- The key-value pairs are ultimately stored as strings
Using localStorage well is not that simple, and this article focuses on some best practices for its use.
compatibility
Browsers support new features at different speeds than browsers of different versions. Before using localStorage, you need to check whether the current environment supports the new features by sniffing:
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; } } Copy the code
Although read and write operations can be used to verify whether the current browser supports the localStorage feature, but does not support the localStorage browser must be able to write operations, as mentioned earlier “the browser to localStorage allocated space is limited”, When the number of stored content reaches the upper limit, no more write operations can be performed.
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 has reached the upper limit! ') } else { console.warn('Current browser does not support localStorage! '); } return isSupport; } Copy the code
When calling localstorage-related methods, ensure that the current browser supports the localStorage feature. Value caching can be used here to avoid the performance cost of calling the method multiple times:
// Class instance methods
ready() {
if (this.isSupport === null) {
this.isSupport = isLocalStorageUsable();
}
if (this.isSupport) { return Promise.resolve(); } return Promise.reject(); } Copy the code
By defining the ready method described above, the sniffer method is made “lazy execution”.
Key/value pair
The toString method is implicitly called when an object is passed directly to localStorage as a key-value pair:
// The final stored key is key: [object object] value: [object object]
localStorage.setItem({}, {});
Copy the code
If you do not pay attention to the type of key names, data loss due to duplicate key names may occur.
When the key of localStorage is an object, appropriate warning should be given to avoid bugs caused by low-level errors to a certain extent:
function normalizeKey(key) {
if (typeofkey ! = ='string') {
console.warn(`${key} used as a key, but it is not a string.`);
key = String(key);
}
return key; } Copy the code
For value, this makes the stored value meaningless, so you need to serialize and deserialize according to the type of data.
serialization
When you call setItem to store a value in localStorage, you need to perform a unified serialization of the stored value:
// Class instance methods
setItem(key, value) {
key = normalizeKey(key);
return this.ready().then((a)= > {
if (value === undefined) {
value = null; } serialize(value, (error, valueString) => { if (error) { return Promise.reject(error); } try { // The storage may fail because the maximum storage space is exceeded. localStorage.setItem(key, valueString); return Promise.resolve(); } catch(e) { return Promise.reject(e); } }) }) } Copy the code
In general, the data is stored in JSON format and is serialized using json. stringify:
function serialize(value, callback) {
try {
const valueString = JSON.stringify(value);
callback(null, valueString);
} catch(e) {
callback(e); } } Copy the code
This requires an exception catch for the json.stringify method, which “throws an exception when the serialized object has a circular reference”. The deserialization process also needs to catch exceptions to json. parse.
JSON.parse('undefined');
// VM20179:1 Uncaught SyntaxError: Unexpected token u in JSON at position 0
Copy the code
In this case, a layer of filtering can be done in the setItem method:
if (value === undefined) {
value = null;
}
Copy the code
However, you can’t completely avoid illegal JSON strings, so you still need to use try/catch to catch exceptions.
If the business requirements are more complex, then you need to determine the exact type of the stored value for a specific serialization:
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() { // The type of the value needs to be marked 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); } } } Copy the code
There is an added storage requirement for the Blob type, which needs to be serialized with FileReader + ArrayBuffer, and an identifier to tell the type of the value during deserialization.
JSON. Stringify optimization
The json.stringify method needs to analyze the structure of the object and the type of the key-value pairs during execution (at run time), which can be time-consuming when dealing with complex nested objects.
The best way to optimize is to actually bring this time-consuming work to the compile stage. Here’s an example:
const testObj = {
firstName: 'Matteo'. lastName: 'Collina'. age: 32
}
function stringify({ firstName, lastName, age }) { return `"{"firstName":"${firstName}","lastName":"${lastName}","age":${age}} "` } Copy the code
For the example above, you can determine the key-value pair and the type of the object before running, and you can get benchmark data with 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 The ${this.filter('fastest').map('name')}`); }) suite.run() Copy the code
The test results show that the optimized scheme performs better, both in terms of the number of times the test code is executed per second and the statistical error of the relative fastest speed.
Stringify obj x 1,695,618ops/SEC ±0.62% (90 runs laboratories) fast-json-stringify obj x 787,253,287 ops/ SEC ±0.36% (92 runs speciality) Fastest is fast-json-stringify objCopy the code
The above examples are not universal. In actual business development, custom JSON Schema can be used to generate a specific Stringify method. There are mature open source frameworks for choice:
- fast-json-stringify
- Slow-json-stringify
The namespace
LocalStorage is restricted by the same origin policy. This isolation level is equivalent to the application level. However, in the actual business development process, some scenarios of this isolation level cannot be covered.
When storing key-value pairs in localStorage, the following features are available:
- The number of key-value pairs can be obtained through the length attribute
- Key-value pairs are indexed by key name
- Key and value pairs are arranged in reverse order of the time they were added
If each module in the current application has the need to use localStorage data, then how to isolate from the module level?
Because key-value pairs are indexed by their key names, you can distinguish them by adding a namespace to the key names:
const keyPrefix = name + '/';
localStorage.setItem(keyPrefix + key, value);
Copy the code
In the case of multiple storage modules, directly calling the clear method will cause the problem of deleting data from other storage modules by mistake. After the introduction of namespace, this situation can be avoided:
function clear(keyPrefix) {
const keys = Object.keys(localStorage);
keys.forEach(key= > {
if (key.indexOf(keyPrefix) === 0) {
localStorage.removeItem(key);
} }) } Copy the code
conclusion
Finally, to summarize the best practices mentioned in this article:
- Use try/catch sniffs for browser compatibility, but be aware of cases where storage limits are exceeded.
- In view of the characteristics of the localStorage key value is a string, the unified serialization and deserialization methods are adopted.
- For the json. stringify method, you can use the convention JSON Schema to advance the analysis of the object structure to the compilation stage to optimize the execution efficiency.
- Command space is introduced to enhance multi-module management.
The resources
- LocalForage. Js source code
This article was typeset using MDNICE