• cocosCreator 之localStorage本地存储和封装拓展


    版本: 3.8.0

    语言: TypeScript

    环境: Mac


    简介


    在cocosCreator中,针对于本地存储主要使用localStorage接口,通过key-value的格式进行存储和读取数据

    主要接口有:

    接口描述
    setItem(key, value)保存指定索引的数据
    getItem(key)获取指定索引的数据
    removeItem(key)移除指定索引的数据
    clear()清空所有数据

    定义文件如下:

    // cc.d.ts
    export const sys: {
      // HTML5 标准中的 localStorage 的本地存储功能,在 Web 端等价于 window.localStorage
      localStorage: Storage;
    }
    
    // lib.dom.d.ts
    interface Storage {
        // 返回数据项的数量
        readonly length: number;
        // 移除所有存储的数据
        clear(): void;
        // 根据键名获取数据,如果没有则null
        getItem(key: string): string | null;
        // 获取指定索引处的键名,如果没有则null
        key(index: number): string | null;
        // 根据键名移除指定数据
        removeItem(key: string): void;
        // 存储键名和数据, 注意可能会存在存储已满的情况,这样会抛出异常
        setItem(key: string, value: string): void;
        [name: string]: any;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在cocosCreator中,本地数据的存储是以sqlite数据库格式存储的。

    我们以setItem简单看下引擎的封装相关:

    • C++相关,目录在: …/engine-native/cocos/storage/local-storage
    // LocalStorage.cpp
    void localStorageSetItem(const std::string &key, const std::string &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 && ok != SQLITE_DONE)
        printf("Error in localStorage.setItem()\n");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • Android平台相关, 目录在 …/libcocos/intermediates/javac/com/cocos/lib
    // LocalStorage-android.cpp
    void localStorageSetItem(const std::string &key, const std::string &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();
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    简单的看下内部的实现,了解本地数据的存储在sqlite数据库中即可。


    使用


    脚本中使用本地存储,常用的接口是:

    • setItem(key: string, value: string): void; 存储数据

    • getItem(key: string): string | null 获取数据

    • removeItem(key: string): void; 移除数据

    简单的示例:

    const key = "Debug_Storage";
    // 保存数据
    sys.localStorage.setItem(key, "cocosCreator");
    // 获取数据
    let value = sys.localStorage.getItem(key);
    console.log("----- 存储的数据是:", value);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注意:

    • setItem存储的数据是string类型,因此存储数据时,注意对数据类型转换
    • setItem的存储存在已满的情况,注意异常的发生
    • getItem获取数据为stringnull, 注意对返回数据的安全判定

    因此,在项目中可增加对localStorage的封装管理,以支持:

    1. 支持不同基础数据类型的存储,包括但不限于string类型, 使用数据转换即可

    2. 支持数组、Map等复杂数据类型的存储, 使用 Json 转换

    3. 数据读取,支持默认数据的设置

    Json转换的主要接口:

    • JSON.stringify 将数据转换为Json字符串
    • JSON.parse 用于将Json字符串解析为数据

    主要实现逻辑如下:

    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;
      }
    
      // 保存数据
      public setItem(key: string, value:any) {
        if (value === undefined || value === null) {
          console.log(`本地存储数据非法, key:${key}`);
          return;
        }
        let valueType = typeof(value);
        if (valueType === "number" && isNaN(value)) {
          console.log(`本地存储数据为NaN, key:${key}`);
          return;
        } 
    
        // 转换数据
        if (valueType === "number") {
          value = value.toString();
        } else if (valueType === "boolean") {
          // boolean类型转换为0或1
          value = value ? "1" : "0";
        } else if (valueType === "object") {
          // 数组或Map类型转换为JSON字符串
          value = JSON.stringify(value);
        }
        sys.localStorage.setItem(key, value);
      }
    
      // 读取数据
      public getItem(key: string, defaultValue: any = ""): any {
        let value = sys.localStorage.getItem(key);
        // 数据获取失败,就走默认设置
        if (value === null) {
          return defaultValue;
        }
    
        // 检测是否为JSON字符串
        const regex = /^\s*{[\s\S]*}\s*$/;
        if (regex.test(value)) {
          return JSON.parse(value);
        }
        return value;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    测试用例:

    private debugStorage() {
      let storageManager = StorageManager.instance;
    
      // 检测数据合法性
      storageManager.setItem("Storage_Debug_1", null);
      storageManager.setItem("Storage_Debug_2", undefined);
      storageManager.setItem("Storage_Debug_3", NaN);
    
      // 存储
      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});
    
      // 获取数据
      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"));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    请添加图片描述

    至于 removeItem, key, clear等实现,直接调用localStorage的相关方法即可。


    拓展1: 支持保存多份数据


    在实际的项目开发中,频繁的功能测试可能需要我们保存多份本地存储数据。

    可以通过key键 + 玩家的唯一标识符ID的方式,存储不同用户的数据,以实现保存多份。

    StorageManager类的大概修改,可以这样:

    // 初始化角色ID, 可用于项目获取用户数据成功后进行设置
    private _roleId: string = "";
    public setRoleId(id: string) {
    	this._roleId = id;
    }
    
    // 增加新方法
    private getNewKey(key: string) {
      let newKey = key;
      if (this._roleId.length <= 0) {
        newKey = `${key}_${this._roleId}`;
      }
      return newKey;
    }
    
    // 在setItem或getItem的接口中调用getNewKey即可
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    使用${key}_${this._roleId} 这种方式构建key,可避免重复性的key导致数据被覆盖。


    拓展2: 数据安全

    虽然cocosCreator采用的是sqlite数据库存储,但数据的存在是明文性的,这样不利于项目的安全。

    因此项目有必要采用加密算法对明文内容进行加密,需要参考博客:cocosCreator 之 crypto-es数据加密

    使用加密算法对本地存储数据可做如下处理:

    • key采取MD5加密
    • value 的存储数据进行AES加密,获取数据时进行AES解密

    也就是在数据保存或获取前,对 keyvalue 进行下加密处理,最终实现代码:

    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");
      }
    
      // 设置角色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);
      }
    
      // 保存数据
      public setItem(key: string, value:any) {
        if (value === undefined || value === null) {
          console.log(`本地存储数据非法, key:${key}`);
          return;
        }
        let valueType = typeof(value);
        if (valueType === "number" && isNaN(value)) {
          console.log(`本地存储数据为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);
        }
        // 加密数据
        let newKey = this.getNewKey(key);
        let newValue = EncryptUtil.aesEncrypt(value);
        sys.localStorage.setItem(newKey, newValue);
      }
    
      // 读取数据
      public getItem(key: string, defaultValue: any = ""): any {
        let newKey = this.getNewKey(key);
        let value = sys.localStorage.getItem(newKey);
        // 数据获取失败,就走默认设置
        if (value === null) {
          return defaultValue;
        }
        // 解密数据
        let newValue = EncryptUtil.aesDecrypt(value);
        // 检测是否为JSON字符串
        const regex = /^\s*{[\s\S]*}\s*$/;
        if (regex.test(value)) {
          return JSON.parse(newValue);
        }
    
        return value;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    至此,所有内容讲述完毕,祝大家学习生活愉快!

  • 相关阅读:
    redis中使用lua脚本
    第五章 Java编程-异常处理
    IDEA Springboot整合log4j2
    Camera2 OpenCamera流程
    Leetcode—515.在每个树行中找最大值【中等】
    Redis-应用问题(缓存穿透/缓存击穿/缓存雪崩/分布式锁)
    《Linux内核设计与实现》
    寒假作业2月13号
    C++知识精讲14 | 算法篇之二分查找算法
    基于 TLS 1.3的百度安全通信协议 bdtls 介绍
  • 原文地址:https://blog.csdn.net/qq_24726043/article/details/134066932