国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

使用IndexedDB做前端日志持久化

CoderDock / 2607人閱讀

摘要:問題頁面如果表現(xiàn)不符合預(yù)期,前端工程師在沒有日志的情況下,很難。所以就需要針對必要的步驟記錄日志,并上傳。只能在回調(diào)函數(shù)中被調(diào)用。事物關(guān)閉日志對象。處理異常利用的重建因?yàn)橹荒茉谥姓{(diào)用。

問題

頁面如果表現(xiàn)不符合預(yù)期,前端工程師在沒有 javascript 日志的情況下,很難 debug。所以就需要針對必要的步驟記錄日志,并上傳。但是每記錄一條日志就上傳并不是一個(gè)合適的選擇,譬如如果生成日志的操作比較密集,會(huì)頻繁產(chǎn)生上傳日志請求的情況。那么我們可以在頁面做一次日志的緩存,把日志先存在本地,當(dāng)緩存達(dá)到一定數(shù)量的時(shí)候一次批量上傳,即節(jié)約了網(wǎng)絡(luò)資源,對服務(wù)器也不會(huì)帶來過重的負(fù)擔(dān)。

選型

頁面存儲方案悉數(shù)下大概有這些:cookie、localStorage/sessionStorage、IndexedDB、WebSQL、FileSystem。cookie 存儲量有限,顯然不適合。localStorage/sessionStorage 必須自己設(shè)計(jì)及維護(hù)存儲結(jié)構(gòu)。WebSQL 已經(jīng)是一種淘汰的標(biāo)準(zhǔn),因?yàn)楹?IndexedDB 功能重復(fù)了。FileSystem 也是比較邊緣不太推薦的標(biāo)準(zhǔn)。那么 IndexedDB 容量合適,且能按條存儲,不用自己維護(hù)存儲結(jié)構(gòu),相較其他方案是我這次打算的選型。

實(shí)現(xiàn) 主要流程

這里只介紹持久化所需要的基本操作,大而全的 API 操作見MDN文檔

第一、新建數(shù)據(jù)庫及“表”

IndexedDB 幾乎所有的 API 都設(shè)計(jì)成異步的形式:

const DATABASE_NAME = "alita";

let db = null;

let request = window.indexedDB.open( DATABASE_NAME );
request.onerror = function(event) {
  alert( "打開數(shù)據(jù)庫失敗" + event.target.error );
};
request.onsuccess = function( event ) {
  // 如果打開成功,把數(shù)據(jù)庫對象保存下來,以后增刪改查都需要用到。
  db = event.target.result;
}

如果數(shù)據(jù)庫已經(jīng)存在,indexedDB.open 會(huì)打開數(shù)據(jù)庫,如果數(shù)據(jù)庫不存在,indexedDB.open 會(huì)新建并打開。IndexedDB 也有類似于表的概念,在 IndexedDB 中叫 object store。并且新建 object store 還只能在特殊的場景下進(jìn)行,先看下代碼再解釋:

const DATABASE_NAME = "alita";
const OBJECT_STORE_NAME = "battleangel";

let db = null;

let request = window.indexedDB.open( DATABASE_NAME );
// 省略代碼。
// request.onerror = ...
// request.onsuccess = ...
request.onupgradeneeded = function(event) {
  let db = event.target.result;
  // 新建 object store
  let os = db.createObjectStore( OBJECT_STORE_NAME, {autoIncrement: true} );
  // 如果想在新建完 object store 后初始化數(shù)據(jù)可以寫在下面。
  let initDataArray = [...];
  initDataArray.forEach( function(data){
    os.add( data );
  } );
};

db.createObjectStore 只能在 onupgradeneeded 回調(diào)函數(shù)中被調(diào)用。onupgradeneeded 什么時(shí)候觸發(fā)呢?只有在你 indexedDB.open() 的數(shù)據(jù)庫是新的,沒有建立過的時(shí)候才會(huì)被觸發(fā)。所以新建數(shù)據(jù)庫和新建 object store 并不是隨時(shí)隨地都可以的(還有一種場景會(huì)觸發(fā),等會(huì)下面會(huì)說到)。createObjectStore 的第二個(gè)參數(shù) {autoIncrement: true} 表示你以后添加進(jìn)數(shù)據(jù)庫的數(shù)據(jù)存儲策略采用自增 key 的形式。

第二、添加日志數(shù)據(jù)

打開數(shù)據(jù)庫后我們就可以添加數(shù)據(jù)了,我們來看下:

let transaction = db.transaction( OBJECT_STORE_NAME, "readwrite" ); // db 就是上面第一步保存下來的數(shù)據(jù)庫對象。
transaction.oncomplete = function(event) {
  alert( "事物關(guān)閉" );
};
transaction.onerror = function(event) {
  // Don"t forget to handle errors!
};

let os = transaction.objectStore( OBJECT_STORE_NAME );
let request = os.add( {
  // 日志對象。
} );
request.onsuccess = function(event) {
  alert( "添加成功" )
};
request.onerror = function(event) {
  alert( "添加失敗" + event.target.error );
};

第三、讀取所有日志數(shù)據(jù)

在我們的場景中,添加完日志后,并不需要多帶帶查詢,只需要保存到一定數(shù)量后一次獲取全部日志上傳就可以了。獲取表中所有數(shù)據(jù)也有新老 API 之分,先看新的 objectStore.getAll,chrome48及以上支持。

let os = db.transaction( OBJECT_STORE_NAME, "read" ).objectStore( OBJECT_STORE_NAME );
let request = os.getAll();
request.onsuccess = function(event) {
  let logObjectArray = event.target.result;
};

如果你用戶的瀏覽器是不支持 getAll 方法,你還可以通過游標(biāo)輪詢的方式來迭代出所有的數(shù)據(jù):

let os = db.transaction( OBJECT_STORE_NAME, "read" ).objectStore( OBJECT_STORE_NAME );
let logObjectArray = [];
let request = os.openCursor();
request.onsuccess = function(event){
  let cursor = event.target.result;
  if ( cursor ) {
    logObjectArray.push( cursor.value );
    cursor.continue();
  }
};

當(dāng) cursor.continue() 被調(diào)用后,onsuccess 會(huì)被反復(fù)觸發(fā),當(dāng) event.target.result 返回的 cursor 為空時(shí),表示沒有更多的數(shù)據(jù)了。我們的場景有點(diǎn)特殊,當(dāng)日志存儲到一定數(shù)量時(shí),我們除了要讀出所有的數(shù)據(jù)上傳外,還要把已經(jīng)上傳的數(shù)據(jù)刪除掉,這樣就不至于越存越多,把 IndexedDB 存爆掉的情況,所以我們修改代碼如下(請注意 db.transaction 的第二個(gè)參數(shù)這次不同了,因?yàn)槲覀円獎(jiǎng)h數(shù)據(jù),所以不能是只讀):

let os = db.transaction( OBJECT_STORE_NAME, "readwrite" ).objectStore( OBJECT_STORE_NAME );
let logObjectArray = [];
if ( os.getAll ) {
  let request = os.getAll();
  request.onsuccess = function(event) {
    logObjectArray = event.target.result;
    // 刪除所有數(shù)據(jù)
    let clearRequest = os.clear();
    // clearRequest.onsuccess = ...
    // clearRequest.onerror = ...
    // 上傳日志
    upload( logObjectArray );
  };
} else {
  let request = os.openCursor();
  request.onsuccess = function(event){
    let cursor = event.target.result;
    if ( cursor ) {
      logObjectArray.push( cursor.value );
      cursor.continue();
    } else {
      // 刪除所有數(shù)據(jù)
      let clearRequest = os.clear();
      // clearRequest.onsuccess = ...
      // clearRequest.onerror = ...
      // 上傳日志
      upload( logObjectArray );
    }
  };
}

以上的操作能完成我們的日志持久化的主流程了:存日志 - 獲取已存日志 - 上傳。

問題及解決方案

如果只有上述代碼自然是沒有辦法完成一個(gè)健壯的持久化方案,還需要考慮如下幾個(gè)點(diǎn):

當(dāng)存和刪除沖突怎么辦

我們看到代碼了 IndexedDB 的操作都是異步,當(dāng)我們正在獲取所有日志時(shí),又有寫日志的調(diào)用怎么辦?會(huì)不會(huì)在獲取到所有日志和刪除所有日志中間,新日志被添加進(jìn)去了呢?這樣新日志就會(huì)在沒有被上傳前就丟失了。這其實(shí)就是并發(fā)導(dǎo)致的問題,IndexedDB 有沒有鎖機(jī)制?

規(guī)范中規(guī)定 "readwrite" 模式的 transaction 同時(shí)只能有一個(gè)在處理 request,其他 "readwrite" 模式的 transaction 即使生成了 request 也會(huì)被鎖住不會(huì)觸發(fā) onsuccess。

let request1 = db.transaction( OBJECT_STORE_NAME, "readwrite" ).objectStore( OBJECT_STORE_NAME ).add({})
let request2 = db.transaction( OBJECT_STORE_NAME, "readwrite" ).objectStore( OBJECT_STORE_NAME ).add({})
let request3 = db.transaction( OBJECT_STORE_NAME, "readwrite" ).objectStore( OBJECT_STORE_NAME ).add({})
// request1 沒有處理完,request2 和 request3 就處于 pending 狀態(tài)

當(dāng)前一個(gè) transaction 完成后,后一個(gè) transaction 才能響應(yīng),所以我們無需寫額外的代碼,IndexedDB 內(nèi)部幫我們實(shí)現(xiàn)了鎖機(jī)制。那么你要問了,什么時(shí)候 transaction 完成呢?沒有看到你上面顯式調(diào)用代碼結(jié)束 transaction 呀?transaction 自動(dòng)完成的條件有兩個(gè):

必須有至少有一個(gè)和 transaction 關(guān)聯(lián)的 request。也就是說如果你生成了一個(gè) transaction 而沒有生成對應(yīng)的 request,那么這個(gè) transaction 就成了孤兒事物,其他 transaction 沒有辦法繼續(xù)操作數(shù)據(jù)庫了,形成死鎖。

當(dāng) transaction 一個(gè)關(guān)聯(lián)的 request 的 onsuccess/onerror 被調(diào)用,并且同時(shí)沒有其他關(guān)聯(lián)的 request 時(shí),transaction 自動(dòng) commit。用代碼舉個(gè)例子:

let os = db.transaction( OBJECT_STORE_NAME, "readwrite" ).objectStore( OBJECT_STORE_NAME );
let request = os.getAll();
  request.onsuccess = function(event) {
    logObjectArray = event.target.result;
    // 刪除所有數(shù)據(jù)
    let clearRequest = os.clear();
  };

上述代碼中 os.clear() 之所以能被成功調(diào)用,是因?yàn)?os.getAll() 生成的 request 的 onsuccess 還沒有執(zhí)行完,os.clear() 就又生成了一個(gè) request。所以當(dāng)前 transaction 在 os.getAll().onsuccess 時(shí)并沒有結(jié)束。但是如下代碼中的 os.clear() 調(diào)用就會(huì)拋異常:

let os = db.transaction( OBJECT_STORE_NAME, "readwrite" ).objectStore( OBJECT_STORE_NAME );
let request = os.getAll();
  request.onsuccess = function(event) {
    logObjectArray = event.target.result;
    // 刪除所有數(shù)據(jù)
    setTimeout( function(){
      let clearRequest = os.clear(); // 這里會(huì)拋異常說 os 對應(yīng)的 transaction 已經(jīng)被關(guān)閉了。
    }, 10 );
    
  };
怎么來判斷數(shù)據(jù)庫中存了多少數(shù)據(jù)

我們解決了并發(fā)問題,那么我們?nèi)绾蝸砼袛嗍裁磿r(shí)候該上傳日志了呢?有兩個(gè)方案:1 基于數(shù)據(jù)庫所存數(shù)據(jù)條數(shù);2 基于數(shù)據(jù)庫所存數(shù)據(jù)的大小。因?yàn)槊織l日志的數(shù)據(jù)或多或少都不一樣,用條數(shù)來判斷會(huì)出現(xiàn)同樣30條數(shù)據(jù),這次數(shù)據(jù)只占10k,下次可能有30k。所以相對理想的,我們應(yīng)該以所存數(shù)據(jù)大小并設(shè)定一個(gè)閾值。這樣每次上傳量比較穩(wěn)定。不過告訴大家一個(gè)悲傷的消息,IndexedDB 提供了查詢條數(shù)的 API:objectStore.count,但是并沒有提供查詢?nèi)萘康?API。所以我們采取了預(yù)估的方式先把查出來的所有數(shù)據(jù)轉(zhuǎn)成 string,然后按 utf-8 的編碼規(guī)則,逐個(gè) char 累加,大致的代碼如下:

/**
 * UTF-8 是一種可變長度的 Unicode 編碼格式,使用一至四個(gè)字節(jié)為每個(gè)字符編碼
 *
 * 000000 - 00007F(128個(gè)代碼)      0zzzzzzz(00-7F)                             一個(gè)字節(jié)
 * 000080 - 0007FF(1920個(gè)代碼)     110yyyyy(C0-DF) 10zzzzzz(80-BF)             兩個(gè)字節(jié)
 * 000800 - 00D7FF
   00E000 - 00FFFF(61440個(gè)代碼)    1110xxxx(E0-EF) 10yyyyyy 10zzzzzz           三個(gè)字節(jié)
 * 010000 - 10FFFF(1048576個(gè)代碼)  11110www(F0-F7) 10xxxxxx 10yyyyyy 10zzzzzz  四個(gè)字節(jié)
 */
function sizeOf( str ) {
  let size = 0;
  if ( typeof str==="string" ) {
    let len = str.length;
    for( let i = 0; i < len; i++ ) {
      let charCode = str.charCodeAt( i );
      if ( charCode<=0x007f ) {
        size += 1;
      } else if ( charCode<= 0x07ff ) {
        size += 2;
      } else if ( charCode<=0xffff ) {
        size += 3;
      } else {
        size += 4;
      }
    }
  }
  return size;
}

所以我們添加日志的代碼可以進(jìn)一步完善成如下:

function writeLog( logObj ) {
  let os = db.transaction( OBJECT_STORE_NAME, "readwrite" ).objectStore( OBJECT_STORE_NAME );
  let request = os.getAll();
  request.onsuccess = function(event) {
    let logObjectArray = event.target.result;
    logObjectArray.push( logObj );
    let allDataStr = logObjectArray.map( l=>JSON.string(l) ).join( `分隔符` );
    let allDataSize = sizeOf( allDataStr );
    // 如果已存日志加上此次要添加的日志數(shù)據(jù)總和超過閾值,則上傳并清空數(shù)據(jù)庫
    if ( allDataSize > `預(yù)設(shè)閾值` ) {
      os.clear();
      upload( allDataStr );
    } else {
      // 如果還沒有達(dá)到閾值,則把日志添加進(jìn)數(shù)據(jù)庫
      os.add( logObj );
    }
  }
}
隱式問題:自增 key

到上面為止正常的日志持久化方案已經(jīng)較為完整了,上線也能夠跑了(當(dāng)然我示例代碼里面省略了異常處理的代碼)。但是這其中有一個(gè)隱形的問題存在,我們新建 object store 的時(shí)候存儲結(jié)構(gòu)使用的是自增 key。每個(gè) object store 的自增 key 會(huì)隨著新加入的數(shù)據(jù)不斷的增加,刪除和 clear 數(shù)據(jù)也不會(huì)重置這個(gè) key。key 的最大值是2的53次方(9007199254740992)。當(dāng)達(dá)到這個(gè)數(shù)值時(shí),再 add 就會(huì) add 不進(jìn)數(shù)據(jù)了。此時(shí) request.onerror 會(huì)得到一個(gè) ConstraintError。我們可以通過顯式得把 key 設(shè)置成最大的來模擬下:

let os = db.transaction( OBJECT_STORE_NAME, "readwrite" ).objectStore( OBJECT_STORE_NAME );
let request = os.add( {}, 9007199254740992 );

setTimeout( function(){
  let os = db.transaction( OBJECT_STORE_NAME, "readwrite" ).objectStore( OBJECT_STORE_NAME );
  let request = os.add( {} );
  request.onerror = function(event) {
    console.log( event.target.error.name ); // ConstraintError
  }
}, 2000 );

這里有個(gè)一個(gè)問題,ConstraintError 并不是一個(gè)特定的 error 表示數(shù)據(jù)庫“寫滿”了,其他場景也會(huì)觸發(fā)拋出 ConstraintError,譬如添加 index 時(shí)候重復(fù)了。規(guī)范中也沒有特定的 error 給到這種場景,所以這里要特別注意下。當(dāng)然這個(gè)最大值是很大的,我們5秒鐘寫一次日志也需要14億年寫滿。不過我比較任性,為了代碼完備性,我給理論上兜個(gè)底。那么怎么才能重置 key 呢?很直接,就是刪了當(dāng)前的 object store,再建一個(gè)。這個(gè)時(shí)候坑爹的事又出現(xiàn)了。就像上面提到的 db.createObjectStore 只能在 onupgradeneeded 回調(diào)函數(shù)中被調(diào)用一樣。db.deleteObjectStore 也只能在 onupgradeneeded 回調(diào)函數(shù)中被調(diào)用。那么我們上面提到了只有在新建的 db 的時(shí)候才能觸發(fā)這個(gè)回調(diào),怎么辦?這個(gè)時(shí)候輪到 window.indexedDB.open 的第二個(gè)參數(shù)出場了。我們?nèi)绻枰庐?dāng)前 db,那么就可以在第二個(gè)參數(shù)上傳入一個(gè)比當(dāng)前版本高的版本,就會(huì)觸發(fā) upgradeneeded 事件(第一次不傳默認(rèn)新建數(shù)據(jù)庫的 version 就是1),代碼如下:

let nextVersion = 1;
if ( db ) {
  nextVersion = db.version + 1;
  db.close(); // 這里一定要注意,一定要關(guān)閉當(dāng)前 db 再做 open,要不然代碼往下執(zhí)行在 chrome 上根本不 work(其他瀏覽器沒有測)。
  db = null;
}
let request = window.indexedDB.open( DATABASE_NAME, nextVersion );
request.onerror = function() {
  // 處理異常
};
request.onsuccess = ( event )=>{
  db = event.target.result;
};
// 利用open version+1 的 db 重建 object store,因?yàn)?deleteObjectStore 只能在 onupgradeneeded 中調(diào)用。
request.onupgradeneeded = function(event) {
  let currentDB = event.target.result;
  currentDB.deleteObjectStore( OBJECT_STORE_NAME );
  currentDB.createObjectStore( OBJECT_STORE_NAME, {
    autoIncrement: true
  } );
}

所以添加日志的代碼最終形態(tài)是:

function recreateObjectStore( success ) {
  let nextVersion = 1;
  if ( db ) {
    nextVersion = db.version + 1;
    db.close(); // 這里一定要注意,一定要關(guān)閉當(dāng)前 db 再做 open,要不然代碼往下執(zhí)行在 chrome 上根本不 work(其他瀏覽器沒有測)。
    db = null;
  }
  let request = self.indexedDB.open( DATABASE_NAME, nextVersion );
  request.onerror = function() {
    // 處理異常
  };
  request.onsuccess = ( event )=>{
    db = event.target.result;
    success && success();
  };
  // 利用open version+1 的 db 重建 object store,因?yàn)?deleteObjectStore 只能在 onupgradeneeded 中調(diào)用。
  request.onupgradeneeded = function(event) {
    let currentDB = event.target.result;
    currentDB.deleteObjectStore( OBJECT_STORE_NAME );
    currentDB.createObjectStore( OBJECT_STORE_NAME, {
      autoIncrement: true
    } );
  }
}

let recreating = false; // 標(biāo)志位,為了在沒有重新建立 object store 前不要重復(fù)觸發(fā) recreate 

function writeLog( logObj ) {
  let os = db.transaction( OBJECT_STORE_NAME, "readwrite" ).objectStore( OBJECT_STORE_NAME );
  let request = os.getAll();
  request.onsuccess = function(event) {
    let logObjectArray = event.target.result;
    logObjectArray.push( logObj );
    let allDataStr = logObjectArray.map( l=>JSON.string(l) ).join( `分隔符` );
    let allDataSize = sizeOf( allDataStr );
    // 如果已存日志加上此次要添加的日志數(shù)據(jù)總和超過閾值,則上傳并清空數(shù)據(jù)庫
    if ( allDataSize > `預(yù)設(shè)閾值` ) {
      os.clear();
      upload( allDataStr );
    } else {
      // 如果還沒有達(dá)到閾值,則把日志添加進(jìn)數(shù)據(jù)庫
      let addRequest = os.add( logObj );
      addRequest.onerror = function(e) {
        // 如果添加新數(shù)據(jù)失敗了
        if ( error.name==="ConstraintError" ) {
          // 1.先把已有數(shù)據(jù)上傳
          uploadAllDbDate();
          // 2. 看看是否已經(jīng)在重置了
          if ( !recreating ) {
            recreating = true;
            // 3. 如果沒有重置,就重置 object store
            recreateObjectStore( function(){
              // 4. 重置完成,再添加一遍數(shù)據(jù)
              recreating = false;
              writeLog( logObj );
            } )
          }
        }
      }
    }
  }
}

好了到現(xiàn)在為止,整個(gè)日志持久化方案的流程就閉環(huán)了,當(dāng)然實(shí)際代碼肯定要更精細(xì),結(jié)構(gòu)更好。因?yàn)椴l(fā)鎖問題,數(shù)據(jù)大小問題,重置 object store 問題都不是很容易查到解決方案,網(wǎng)上大多數(shù)只有一些基本操作,所以這里記錄下,方便有需要的人。

參考文檔:

Using IndexedDB.

Locking model for IndexedDB?.

How do you keep an indexeddb transaction alive?.

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/109011.html

相關(guān)文章

  • 【js】前端久化存儲

    摘要:與服務(wù)器端通信每次都會(huì)攜帶在頭中,如果使用保存過多數(shù)據(jù)會(huì)帶來性能問題。但如果要存儲大量的復(fù)雜的數(shù)據(jù),這并不是一種很好的方案。使用索引存儲數(shù)據(jù),各種數(shù)據(jù)庫操作放在事務(wù)中執(zhí)行。通過監(jiān)聽正確類型的事件以等待操作完成。 cookie 生命期為只在設(shè)置的cookie過期時(shí)間之前一直有效,即使窗口或?yàn)g覽器關(guān)閉。 存放數(shù)據(jù)大小為4K左右 。有個(gè)數(shù)限制(各瀏覽器不同),一般不能超過20個(gè)。與服務(wù)器端通...

    Steve_Wang_ 評論0 收藏0
  • 初探IndexedDB

    背景 隨著前端技術(shù)日新月異地快速發(fā)展,web應(yīng)用功能和體驗(yàn)也逐漸發(fā)展到可以和原生應(yīng)用媲美的程度,前端緩存技術(shù)的應(yīng)用對這起到了不可磨滅的貢獻(xiàn),因此想一探前端的緩存技術(shù),這篇文章主要會(huì)介紹在日常開發(fā)中比較少接觸的IndexedDB IndexedDB 什么是IndexedDB IndexedDB簡單理解就是前端數(shù)據(jù)庫,提供了一種在用戶瀏覽器中持久存儲數(shù)據(jù)的方法,但是和前端關(guān)系型數(shù)據(jù)不同的是,Index...

    jsyzchen 評論0 收藏0
  • 很全很全的前端本地存儲講解

    摘要:正文開始三種本地存儲方式前言網(wǎng)絡(luò)早期最大的問題之一是如何管理狀態(tài)。這個(gè)特點(diǎn)很重要,因?yàn)檫@關(guān)系到什么樣的數(shù)據(jù)適合存儲在中。特點(diǎn)生命周期持久化的本地存儲,除非主動(dòng)刪除數(shù)據(jù),否則數(shù)據(jù)是永遠(yuǎn)不會(huì)過期的。 最近一直在搞基礎(chǔ)的東西,弄了一個(gè)持續(xù)更新的github筆記,可以去看看,誠意之作(本來就是寫給自己看的……)鏈接地址:Front-End-Basics 此篇文章的地址:三種本地存儲方式 ...

    Cciradih 評論0 收藏0
  • 前端開發(fā)的客戶端本地存儲

    摘要:在前端開發(fā)過程中,為了與服務(wù)器更方便的交互或者提升用戶體驗(yàn),我們都會(huì)在客戶端用戶本地保存一部分?jǐn)?shù)據(jù),比如。這篇文章的客戶端本地存儲,我們主要講到四種技術(shù)。回調(diào)函數(shù)傳入的事件屬性就指向該請求,即。刪除索引不會(huì)影響數(shù)據(jù),所以沒有回調(diào)函數(shù)。 在前端開發(fā)過程中,為了與服務(wù)器更方便的交互或者提升用戶體驗(yàn),我們都會(huì)在客戶端(用戶)本地保存一部分?jǐn)?shù)據(jù),比如cookie/localStorage/se...

    ACb0y 評論0 收藏0

發(fā)表評論

0條評論

最新活動(dòng)
閱讀需要支付1元查看
<