cocosCreator’s localStorage local storage and packaging expansion

Version: 3.8.0

Language: TypeScript

Environment: Mac

Introduction

In cocosCreator, the localStorage interface is mainly used for local storage, and data is stored and read in the key-value format.

The main interfaces are:

Interface Description
setItem(key, value ) Save the data of the specified index
getItem(key) Get Data at the specified index
removeItem(key) Remove data at the specified index
clear() Clear all data

The definition file is as follows:

//cc.d.ts
export const sys: {<!-- -->
  // The local storage function of localStorage in the HTML5 standard is equivalent to window.localStorage on the Web side
  localStorage: Storage;
}

//lib.dom.d.ts
interface Storage {<!-- -->
    // Return the number of data items
    readonly length: number;
    // Remove all stored data
    clear(): void;
    // Get data based on key name, null if not
    getItem(key: string): string | null;
    // Get the key name at the specified index, null if not found
    key(index: number): string | null;
    //Remove specified data based on key name
    removeItem(key: string): void;
    // Store the key name and data. Note that the storage may be full, which will throw an exception.
    setItem(key: string, value: string): void;
    [name: string]: any;
}

In cocosCreator, local data is stored in sqlite database format.

Let’s use setItem to briefly look at the engine’s packaging:

  • C++ related, directory in: …/engine-native/cocos/storage/local-storage
// LocalStorage.cpp
void localStorageSetItem(const std::string & amp;key, const std::string & amp;value) {
  assert(_initialized);
  int ok = sqlite3_bind_text(_stmt_update, 1, key.c_str(), -1, SQLITE_TRANSIENT);
  ok |= sqlite3_bind_text(_stmt_update, 2, value.c_str(), -1, SQLITE_TRANSIENT);

  ok |= sqlite3_step(_stmt_update);

  ok |= sqlite3_reset(_stmt_update);

  if (ok != SQLITE_OK & amp; & amp; ok != SQLITE_DONE)
    printf("Error in localStorage.setItem()\\
");
}
  • Related to the Android platform, the directory is in …/libcocos/intermediates/javac/com/cocos/lib
// LocalStorage-android.cpp
void localStorageSetItem(const std::string & amp;key, const std::string & amp;value) {<!-- -->
  assert(gInitialized);
  JniHelper::callStaticVoidMethod(JCLS_LOCALSTORAGE, "setItem", key, value);
}

// CocosLocalStorage.class
public static void setItem(String key, String value) {<!-- -->
  try {<!-- -->
    String sql = "replace into " + TABLE_NAME + "(key,value)values(?,?)";
    mDatabase.execSQL(sql, new Object[]{<!-- -->key, value});
  } catch (Exception var3) {<!-- -->
    var3.printStackTrace();
  }
}

Simply look at the internal implementation and understand that local data is stored in the sqlite database.

Use

Local storage is used in scripts. Commonly used interfaces are:

  • setItem(key: string, value: string): void; Store data

  • getItem(key: string): string | null Get data

  • removeItem(key: string): void; Remove data

Simple example:

const key = "Debug_Storage";
// save data
sys.localStorage.setItem(key, "cocosCreator");
// retrieve data
let value = sys.localStorage.getItem(key);
console.log("----- The stored data is:", value);

Notice:

  • The data stored in setItem is of type string, so when storing data, pay attention to the data type conversion
  • The storage of setItem is full. Please pay attention to the occurrence of exceptions.
  • getItem obtains data as string or null, pay attention to the safety judgment of the returned data

Therefore, encapsulation management of localStorage can be added to the project to support:

  1. Supports storage of different basic data types, including but not limited to string type, just use data conversion

  2. Supports storage of complex data types such as arrays and Maps, using Json conversion

  3. Data reading, supports default data settings

The main interface for Json conversion:

  • JSON.stringify Convert data to Json string
  • JSON.parse is used to parse Json strings into data

The main implementation logic is as follows:

import {<!-- --> _decorator, sys} from 'cc';
const {<!-- --> ccclass, property } = _decorator;

export class StorageManager {<!-- -->
  private static _instance: StorageManager = null;
  static get instance() {<!-- -->
    if (this._instance) {<!-- -->
      return this._instance;
    }
    this._instance = new StorageManager();
    return this._instance;
  }

  // save data
  public setItem(key: string, value:any) {<!-- -->
    if (value === undefined || value === null) {<!-- -->
      console.log(`Local storage data is illegal, key:${<!-- -->key}`);
      return;
    }
    let valueType = typeof(value);
    if (valueType === "number" & amp; & amp; isNaN(value)) {<!-- -->
      console.log(`Local storage data is NaN, key:${<!-- -->key}`);
      return;
    }

    // Convert data
    if (valueType === "number") {<!-- -->
      value = value.toString();
    } else if (valueType === "boolean") {<!-- -->
      // Convert boolean type to 0 or 1
      value = value ? "1" : "0";
    } else if (valueType === "object") {<!-- -->
      // Convert array or Map type to JSON string
      value = JSON.stringify(value);
    }
    sys.localStorage.setItem(key, value);
  }

  // read data
  public getItem(key: string, defaultValue: any = ""): any {<!-- -->
    let value = sys.localStorage.getItem(key);
    // If data acquisition fails, just use the default settings.
    if (value === null) {<!-- -->
      return defaultValue;
    }

    // Check whether it is a JSON string
    const regex = /^\s*{[\s\S]*}\s*$/;
    if (regex.test(value)) {<!-- -->
      return JSON.parse(value);
    }
    return value;
  }
}

Test case:

private debugStorage() {<!-- -->
  let storageManager = StorageManager.instance;

  // Check data validity
  storageManager.setItem("Storage_Debug_1", null);
  storageManager.setItem("Storage_Debug_2", undefined);
  storageManager.setItem("Storage_Debug_3", NaN);

  //storage
  storageManager.setItem("Storage_Int", 10);
  storageManager.setItem("Storage_Boolean", true);
  storageManager.setItem("Storage_Array1", [1,2,3]);
  storageManager.setItem("Storage_Array2", new Array(4,5,6));
  storageManager.setItem("Storage_Map", {<!-- -->name: "TypeScript", index:10});

  // retrieve data
  console.log("Storage_Int", storageManager.getItem("Storage_Int"));
  console.log("Storage_Boolean", storageManager.getItem("Storage_Boolean"));
  console.log("Storage_Array1", storageManager.getItem("Storage_Array1"));
  console.log("Storage_Array2", storageManager.getItem("Storage_Array2"));
  console.log("Storage_Map", storageManager.getItem("Storage_Map"));
}

Please add a picture description

As for the implementation of removeItem, key, clear, etc., just call the relevant methods of localStorage directly.

Expansion 1: Support saving multiple copies of data

In actual project development, frequent functional testing may require us to save multiple copies of local storage data.

You can store the data of different users by using the key key + the player’s unique identifier ID to save multiple copies.

A rough modification to the StorageManager class can be as follows:

//Initialize the role ID, which can be set after the project successfully obtains user data.
private _roleId: string = "";
public setRoleId(id: string) {<!-- -->
this._roleId = id;
}

//Add new method
private getNewKey(key: string) {<!-- -->
  let newKey = key;
  if (this._roleId.length <= 0) {<!-- -->
    newKey = `${<!-- -->key}_${<!-- -->this._roleId}`;
  }
  return newKey;
}

// Just call getNewKey in the interface of setItem or getItem

Use ${key}_${this._roleId} to construct the key to avoid data overwriting caused by repeated key.

Expansion 2: Data Security

Although cocosCreator uses sqlite database for storage, the data exists in clear text, which is not conducive to the security of the project.

Therefore, it is necessary for the project to use encryption algorithms to encrypt plain text content. You need to refer to the blog: cocosCreator’s crypto-es data encryption

Using encryption algorithms to process locally stored data can be done as follows:

  • Use MD5 encryption for key
  • AES encryption is performed on the stored data of value, and AES decryption is performed when obtaining the data.

That is, before saving or retrieving data, key and value are encrypted, and the final code is implemented:

import {<!-- --> _decorator, sys} from 'cc';
const {<!-- --> ccclass, property } = _decorator;
import CryptoES from "crypto-es";
import {<!-- --> EncryptUtil } from './EncryptUtil';

export class StorageManager {<!-- -->
  private static _instance: StorageManager = null;
  private _secretKey: string = "";
  private _roleId: string = "";

  static get instance() {<!-- -->
    if (this._instance) {<!-- -->
      return this._instance;
    }
    this._instance = new StorageManager();
    this._instance.init();
    return this._instance;
  }

  private init() {<!-- -->
    EncryptUtil.initCrypto("key", "vi");
  }

  //Set role ID
  public setRoleId(id: string) {<!-- -->
    this._roleId = id;
  }

  private getNewKey(key: string) {<!-- -->
    let newKey = key;
    if (this._roleId.length <= 0) {<!-- -->
      newKey = `${<!-- -->key}_${<!-- -->this._roleId}`;
    }
    return EncryptUtil.md5(newKey);
  }

  // save data
  public setItem(key: string, value:any) {<!-- -->
    if (value === undefined || value === null) {<!-- -->
      console.log(`Local storage data is illegal, key:${<!-- -->key}`);
      return;
    }
    let valueType = typeof(value);
    if (valueType === "number" & amp; & amp; isNaN(value)) {<!-- -->
      console.log(`Local storage data is NaN, key:${<!-- -->key}`);
      return;
    }

    if (valueType === "number") {<!-- -->
      value = value.toString();
    } else if (valueType === "boolean") {<!-- -->
      value = value ? "1" : "0";
    } else if (valueType === "object") {<!-- -->
      value = JSON.stringify(value);
    }
    // Encrypt data
    let newKey = this.getNewKey(key);
    let newValue = EncryptUtil.aesEncrypt(value);
    sys.localStorage.setItem(newKey, newValue);
  }

  // read data
  public getItem(key: string, defaultValue: any = ""): any {<!-- -->
    let newKey = this.getNewKey(key);
    let value = sys.localStorage.getItem(newKey);
    // If data acquisition fails, use the default settings.
    if (value === null) {<!-- -->
      return defaultValue;
    }
    // Decrypt data
    let newValue = EncryptUtil.aesDecrypt(value);
    // Check whether it is a JSON string
    const regex = /^\s*{[\s\S]*}\s*$/;
    if (regex.test(value)) {<!-- -->
      return JSON.parse(newValue);
    }

    return value;
  }
}

At this point, all the content has been described. I wish everyone a happy study and life!