整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          Vue3 頂部進度條

          Vue3 頂部進度條

          Progress是一個輕量級的進度條組件,在Github上已經2.4萬star數了,雖然這個組件已經好久沒有更新了,最近一次更新是20年4月份,改了jQuery的版本,但是該組件的使用頻率還是很高的。

          插件安裝

           $ npm install --save nprogress 
           $ yarn add nprogress

          插件使用

          App.vue

          <script lang="ts">
          import NProgress from "nprogress";
          import "nprogress/nprogress.css";
          import { useRouter } from "vue-router";
          export default {
            setup() {
              const router=useRouter();
              router.beforeEach((to, from, next)=> {
                NProgress.start();
                next();
              });
              router.afterEach(()=> {
                NProgress.done();
              });
            },
          };
          </script>

          axios中使用

          main.ts

          import 'nprogress/nprogress.css'

          axios.ts

          import NProgress from 'nprogress'
          
          axios.interceptors.request.use(
            function (config: any) {
             // 在發送請求之前做某件事
              NProgress.start() //開始
              return config
            },
            (error: { data: { error: { message: any } } })=> {
              return Promise.reject(error.data.error.message)
            }
          )
          
          
          axios.interceptors.response.use(
            function (config: any) {
              aspShow.value=false
              NProgress.done() //結束
              if (config.status===200 || config.status===204) {
                return Promise.resolve(config)
              }
              return Promise.reject(config)
            },
            function (error: any) {
              aspShow.value=false
              NProgress.done() //結束
              if (error.response.status) {
                switch (error.response.status) {
                  // 401: 未登錄
                  // 未登錄則跳轉登錄頁面,并攜帶當前頁面的路徑
                  // 在登錄成功后返回當前頁面,這一步需要在登錄頁操作。
                  case 401:
                    router.replace({
                      path: '/login',
                      query: {}
                    })
                    break
                 
                  // 其他錯誤,直接拋出錯誤提示
                  default:
                    message.error(`${error.response.status}:${error.response.statusText}`)
                }
               
                NProgress.done()////結束
                return Promise.reject(error)
              }
              
            }
          )

          路由中使用

          import NProgress from 'nprogress'
          
          // 頁面切換之前取消上一個路由中未完成的請求
          router.beforeEach((_to: any, _from: any, next: ()=> void)=> {
            NProgress.start()
            next()
          })
          router.afterEach(()=> {
            // 進度條
            NProgress.done()
          })

          全局配置

          //全局進度條的配置
          NProgress.configure({
            easing: 'ease', // 動畫方式
            speed: 1000, // 遞增進度條的速度
            showSpinner: false, // 是否顯示加載ico
            trickleSpeed: 200, // 自動遞增間隔
            minimum: 0.3, // 更改啟動時使用的最小百分比
            parent: 'body', //指定進度條的父容器
          })

          TS的項目,還需要安裝其類型聲明文件,命令如下:

          npm i @types/nprogress -D

          或者如下聲明

          declare module 'nprogress';

          代前端應用邏輯日趨復雜,在平衡性能和數據實效性方面,前端也逐漸開始承擔一些責任。

          最近我們在項目中就碰到了這樣一個場景。我們的項目只是一個非常傳統的數據看板類項目,用戶打開頁面,通過調用API讀取數據,渲染頁面,完成任務。

          但是這個項目有幾個特點,我需要特別說明一下:

          • 公司高管們每天都會使用,并且非常關注
          • 高管們在手機上使用,網絡條件不定
          • 高管們查看數據時通常都比較焦躁

          于是乎,一個本來看似簡單的項目,就逐漸變成性能優化的急先鋒。

          version alpha

          最開始我們的策略非常簡單,就是給把數據存儲到indexedDB中,并設置一個過期時間。整體流程如下:

          關于indexedDB的初始版本代碼大致包含如下幾個部分

          獲取indexedDB鏈接

          const TABLE_NAME='xhr_cache'
          export const getDBConnection=()=> {
            const request=window.indexedDB.open(DB_NAME)
            request.onupgradeneeded=function (event) {
              const db=event.target.result
              if (!db.objectStoreNames.contains(TABLE_NAME)) {
                const table=db.createObjectStore(TABLE_NAME, {
                  keyPath: 'uid'
                })
                table.createIndex('uid', 'uid', { unique: true })
              }
            }
            return new Promise((resolve, reject)=> {
              request.onsuccess=function () {
                resolve(request.result)
              }
            })
          }
          const dbConn=await getDBConnection()

          根據request生成唯一key

          import MD5 from 'crypto-js/md5'
          getKey(config) {
            const hashedKey=MD5( `${config.url}_${JSON.stringify(config.payload)}` ).toString()
            return hashedKey
          }

          根據一個請求的URL + payload,我們可以識別一個唯一的請求。

          對其值進行md5哈希之后得到一個唯一的鍵,代表一個請求,并將其作為存儲在indexedDB中的主鍵。

          讀取數據和寫入數據

          /* 寫入API response數據 */

          const response={
            uid: key,
            content: axiosRequest.response.data,
            created_at: new Date().getTime(),
            expired: expired_at
          }
          const addResponseToIndexedDB=function (response) {
            dbConn
              .transaction([TABLE_NAME], 'readwrite')
              .objectStore(TABLE_NAME)
              .put(response)
          }
          /* 讀取緩存 */
          const request=dbConn
            .transaction([TABLE_NAME], 'readonly')
            .objectStore(TABLE_NAME)
            .index('uid')
            .get(key)
          
          
          const result=await new Promise((resolve=> {
              request.onsuccess=function () {
                  resolve(request.result)
              }
          })

          清除過期緩存

          雖然indexedDB可以存儲遠大于localStorage的數據,但我們也不希望indexedDB隨著用戶不斷訪問存儲大量冗余數據。因此,會在每次應用加載的開始對于過期數據統一進行一次清理:

          const isExpireded=(result, expired=60000)=> {
            const now=new Date().getTime()
            const created_at=result.created_at
            return !created_at || (now - created_at > expired) ? true : false
          }
          const delCacheByExpireded=()=> {
            var request=dbConn
              .transaction([TABLE_NAME], 'readwrite')
              .objectStore(TABLE_NAME)
              .openCursor();
            request.onsuccess=function (e) {
              var cursor=e.target.result;
              if (cursor && cursor !==null) {
                const key=cursor.key
                const expireded=isExpireded(cursor.value)
                if (expireded) {
                  that.delCacheByKey(key)
                }
                cursor.continue();
              }
            }
          }

          Axios Request / Response Interceptor

          有了上述這些能力,我們就可以在自己的Axios攔截器中使用indexedDB的緩存數據。

          axios request 攔截器

          ...
          const CACHED_URL_REGEX=[
            'somepath/data/version/123',
            'user/info/name',
            ...
          ]
          Axios.interceptors.request.use(async function (config) {
            const r=new Regex(`${CACHED_URL_REGEX.join('|')}$`)
            if (r.test(config.url)) {
              const key=getKey()
              const request=dbConn
                .transaction([TABLE_NAME], 'readonly')
                .objectStore(TABLE_NAME)
                .index('uid')
                .get(key)
              const result=await new Promise((resolve)=> {
                request.onsuccess=function (event) {
                  resolve(request.result)
                }
                request.onerror=function (event) {
                  resolve()
                }
              })
              if (result && isExpired(result)) {
                config.adapter=function (config) {
                  return new Promise((resolve)=> {
                    const res={
                      data: result.content,
                      status: 200,
                      statusText: 'OK',
                      headers: { 'content-type': 'text/plain; charset=utf-8' },
                      config,
                      request: {}
                    }
                    return resolve(res)
                  })
                }
              }
              return config
            }
          })
          ...

          可以看到,我們在request 攔截器中進行了以下操作:

          • axios request interceptor的參數中包含URL和payload屬性
          • 根據URL判斷當前資源是否需要緩存
          • 如需要緩存,則根據URL和payload信息生成唯一的key
          • 根據此key去indexedDB中查找是否已有緩存
          • 如有則直接構建一個response并返回
          • 如沒有則返回原始config,繼續進行axios默認行為

          注意下面這段代碼

          const result=await new Promise((resolve)=> {
            request.onsuccess=function (event) {
              resolve(request.result)
            }
            request.onerror=function (event) {
              resolve()
            }
          })

          這里的代碼使用了await,以此等待indexedDB的異步查詢結束。異步查詢結束之后才能根據其結果判斷是否要直接返回還是繼續axios默認行為。

          axios response 攔截器

          Axios.interceptors.response.use(function (response) {
            ...
            let success=response.status < 400
            const key=getKey(response.config)
            dbConn
              .transaction([TABLE_NAME], 'readwrite')
              .objectStore(TABLE_NAME)
              .put({
                uid: key,
                content: response.data,
                created_at: new Date().getTime()
              })
            ...
            return response
          }

          在response攔截器中,無需等待indexedDB的異步寫入過程,因此不需要使用await。

          截至目前,基于Axios + indexedDB的緩存方案已經大體可用,當然以上代碼并不完全,如需使用還得根據自己的項目做一些修改。

          IndexedDB不夠快?

          上述設計方案實現之后,我們發現在讀取indexedDB的時候有時會很快,但有些時候卻非常慢。根據觀測,在某些手機上,讀取一小段不超過100K的數據,有時候需要400ms以上。根據經驗這是無法理解的。

          進一步調查發現,在主線程繁忙時,初始化indexedDB事務到indexedDB返回數據就會比較慢;反之,在主線程空閑時,經過測量,同一過程耗時大約在5ms以下,這才在數據庫讀取速度的正常認知范圍之內。

          但眾所周知,基于react + antd的前端應用,DOM結構復雜,主線程在渲染時會非常繁忙,這就造成了我們觀察到的讀取indexedDB耗時較長。

          說到這里,還記得上邊在Axios Request Interceptor中需要先等待讀取到indexedDB數據,根據結果判斷是否要請求API的代碼嗎?

          于是尷尬的一幕出現了。假設一次請求疊加了如下因素:

          • 主線程正在進行大范圍的DOM渲染,造成CPU繁忙
          • indexedDB讀取耗時從若干毫秒跳級到幾百毫秒
          • 讀取到的數據過期,經過判斷需要請求API
          • 請求API耗時200ms以上

          本來應該提高性能的手段,在這種條件下不僅沒有節省耗時,反而會增加耗時。更進一步,在我們自己的調試過程中,發現對于某些低級手機機型,渲染初始頁面時CPU本就繁忙,此時即便從本地緩存獲取到的數據沒有過期,耗時也可能高達無法理解的一秒左右。這種結果表示,此場景下的緩存方式顯然是得不償失的。

          下表為我們針對alpha版本緩存方案在Chrome瀏覽器上的性能做出的統計。其中每一列分別表示在React進行初始化渲染階段的indexedDB請求耗時。

          API 1

          API 2

          API 3

          180ms

          82ms

          51ms

          如果將Chrome的CPU throttle調低到1/4的效率,數據則更加無法理解

          API 1

          API 2

          API 3

          956ms

          183ms

          253ms

          與之對應的,在CPU空閑的時候,也就是初始化渲染完畢之后的indexedDB請求耗時分別為:

          API 1

          API 2

          API 3

          13ms

          12ms

          13ms

          Version BETA

          由于上一節的結論,這樣的緩存策略顯然無法達到本來的目的。因此我們又設計了幾個方案進行對比:

          • 利用serviceWorker進行數據緩存
          • 在應用開始之初將indexedDB數據dump到內存,之后的取用直接通過內存緩存。根據dump的時間點,又細分為
            • 在react app初始化時進行dump
            • 在html script標簽中使用主線程執行dump代碼
            • 在html script標簽中使用web worker執行dump代碼

          其中dump數據到內存中進行緩存取用的三種細分,我們分別命名為:

          • ReactAPP初始化MemCache的方案
          • HTML加載時初始化MemCache的方案
          • webWorker初始化MemCache的方案

          策略的對比如下:

          方案

          對比

          ReactAPP初始化MemCache

          為了避免API調用在dump數據到內存完成之前,需要等待初始化MemCache之后再調用react app的render方法 由于這種順序執行,會犧牲一部分APP渲染的耗時

          HTML加載時初始化MemCache

          HTML加載時初始化,CPU相對比較空閑,進行dump操作效率較高,但也取決于當時是否正在對加載的JS資源進行script evaluate 如瀏覽器正在進行腳本文件的執行和編譯,dump時長仍然比較長

          webWorker初始化MemCache

          利用webworker在主線程之外進行indexedDB的dump操作,可以避免主渲染線程繁忙與否對于indexedDB讀取耗時的影響 但初始化webworker本身仍然需要額外耗時

          由于以上方案相對于上一節中單次indexedDB調用增加了前置dump數據到內存的操作耗時,所以我們這次對測量方案增加了TOTAL一欄,表示從html頁面載入到react app完全渲染完畢的耗時。

          下表中包含共5種方案的性能對比:alpha版本,serviceWorker方案,以及MemCache的三種方案。每種方案測試十次,取四個階段以及TOTAL耗時的平均值:

          • HTML開始加載到靜態資源加載完成的耗時
          • 初始化渲染過程中的三次indexedDB調用耗時
          • TOTAL耗時

          評測數據見下表(細字體的部分為正常CPU負載情況下,粗體字的部分表示CPU效率降級為1/4時的情況):

          方案

          靜態資源

          API 1

          API 2

          API 3

          TOTAL

          靜態資源

          API 1

          API 2

          API 3

          TOTAL

          alpha版本

          525.4

          180.4

          82.2

          51.3

          1544.7

          2500.5

          956.5

          183.6

          253

          6562.5

          service worker方案

          827.5

          60.9

          208.4

          351.6

          1777.2

          4053.7

          138.4

          991.4

          546.3

          7357.3

          ReactAPP初始化MemCache

          1042.9

          1.8

          26

          9.8

          1659.5

          4512.9

          7.5

          31.3

          35.5

          6410.1

          HTML加載時初始化MemCache

          1021

          2.4

          10.3

          9.7

          1564.7

          5273.1

          7.2

          31.6

          34.5

          7178.3

          webWorker初始化MemCache

          797.9

          0.9

          8.9

          7.1

          1299.6

          3853.7

          5.6

          31.2

          45

          5975

          Finally!! We have a winner

          根據數據顯示,對于我們的場景來說,使用webworker啟動MemCache的方案是最經濟的。方案設計如下圖所示:

          1. HTML加載時就啟動WebWorker,WebWorker內部執行dump操作
          2. dump完成之后通過postMessage向主線程發送dump之后的數據
          3. 主線程收到數據后會將其暫存在特定全局變量上
          4. APP啟動之后初始化MemCache,會先判斷該全局變量是否已經被賦值,若還未賦值,則會在初始化MemCache之前自行執行一次dump數據,以保證indexedDB數據已全量dump出來
          5. 在這之后,與alpha版本不同,Axios的請求/響應攔截器會通過MemCache類進行緩存的查詢和添加
          6. MemCache類負責返回和更新緩存,并將其同步回indexedDB

          WebWorker 腳本 / APP內部的dump數據腳本

          由于dump數據的操作基本一致,因此WebWorker腳本和APP內部用于dump數據的lib文件內容基本一致。大體代碼可見:

          const DB_NAME='db_name'
          const TABLE_NAME='xhr_cache'
          export const getDBConnection=()=> {
            const request=window.indexedDB.open(DB_NAME)
            request.onupgradeneeded=function (event) {
              const db=event.target.result
              if (!db.objectStoreNames.contains(TABLE_NAME)) {
                const table=db.createObjectStore(TABLE_NAME, {
                  keyPath: 'uid'
                })
                table.createIndex('uid', 'uid', { unique: true })
              }
            }
            return new Promise((resolve, reject)=> {
              let completed=false
              request.onsuccess=function () {
                if (completed===false) {
                  completed=true
                  resolve(request.result)
                } else {
                  request.result.close()
                }
              }
              request.onerror=function (err) {
                if (completed===false) {
                  completed=true
                  reject(err)
                }
              }
              setTimeout(()=> {
                if (completed===false) {
                  completed=true
                  reject(new Error('getDBConnection timeout after app rendered'))
                }
              }, 1000)
            })
          }
          export const dump2Memory=async (db)=> {
            const transaction=db.transaction([TABLE_NAME], 'readonly')
            const table=transaction.objectStore(TABLE_NAME)
            const request=table.index('uid').getAll()
            const records=await new Promise((resolve, reject)=> {
              request.onsuccess=function () {
                resolve(request.result)
              }
              request.onerror=function () {
                console.log('dump2Memory error')
                resolve()
              }
            })
            return records
          }
          export const delCacheByExpireded=async (records)=> {
            const validRecords=records.filter((record)=> !getExpireded(record))
            const objectStore=DBCache.conn
              .transaction(['xhr_cache'], 'readwrite')
              .objectStore('xhr_cache')
            const clearRequest=objectStore.clear()
            clearRequest.onsuccess=function () {
              validRecords.forEach((record)=> {
                objectStore.add(record)
              })
            }
            return validRecords
          }

          在這里定義了三個函數

          • 獲取indexedDB鏈接的函數
          • 從indexedDB中dump所有數據到內存的函數
          • 對內存中的全量數據進行過期篩查的函數,其中篩查出已過期的數據進行刪除操作,留下來的有效緩存再次存回到indexedDB

          注意在獲取indexedDB鏈接的函數中,相對alpha版本增加了容錯處理。如果一個瀏覽器多個tab同時打開同一個indexedDB的鏈接,可能會導致后面打開的indexedDB鏈接被block住。因此在這里做了超時處理。

          如果新的鏈接打開超時則不初始化內存緩存,作為降級處理方案。

          于此同時,MemCache類也需要對這種降級做出兼容。

          MemCache類

          DBCache.conn=null
          
          DBCache.memCache={
            __memCache: null,
            initialize: function (records) {
              this.__memCache=new Map(records.map((record)=> [record.uid, record]))
            },
            get: function (key) {
              const result=this.__memCache.get(key)
              if (result) {
                return cloneDeep(result)
              } else {
                return null
              }
            },
            add: function (record) {
              this.__memCache.set(record.uid, record)
            }
          }
          
          DBCache.prepare=async function () {
            try {
              DBCache.conn=await getDBConnection()
              let dbRecordList=[]
              if (window.__db_cache_prepared_records__.length) {
                dbRecordList=cloneDeep(window.__db_cache_prepared_records__)
              } else {
                console.time('dump')
                dbRecordList=await dump2Memory(DBCache.conn)
                console.timeEnd('dump')
              }
              const validRecords=await delCacheByExpireded(dbRecordList)
              DBCache.memCache.initialize(validRecords || [])
            } catch (err) {
              DBCache.memCache.initialize([])
              console.error(err)
            }
          }
          
          DBCache.updateRecord=(record)=> {
            if (DBCache.conn) {
              DBCache.memCache.add(record)
              DBCache.conn
                .transaction(['xhr_cache'], 'readwrite')
                .objectStore('xhr_cache')
                .put(record)
            }
          }

          請注意,DBCache對象的prepare靜態方法中:

          由于獲取鏈接超時會拋出異常,因此在getDBConnection方法外圍添加了try{}catch{}塊。

          如果獲取DB連接發生異常,則會給MemCache初始化為空數組,這樣Axios攔截器在調用DBCache.memCache.get方法時則會永遠返回緩存未命中,于是所有Axios請求全部降級為API調用。

          另外一個需要注意的點是,DBCache.memCache.get的方法實現中對于內存中的數據進行深拷貝的操作。原因在于,如果直接向react業務代碼傳遞該內存塊的引用,很顯然業務代碼會對該內存引用的對象進行修改。那么下次再使用命中的緩存時,就會因為緩存數據與API返回的數據結構不一致導致報錯。

          初始化WebWorker

          到現在為止,幾乎所有必須模塊的代碼都已經實現了。整個流程只剩下最后一塊磚:HTML里script標簽內用于啟動WebWorker以及WebWorker中通知主線程的代碼。

          <script>
            window.__db_cache_prepared_records__=[]
            if (window.Worker) {
              console.time('dump in html')
              const dbWorker=new Worker('./webworker.dump.prepare.js');
              dbWorker.onmessage=function(e) {
                if (e.data.eventName='onDBDump') {
                  if (window.__db_cache_prepared_records__.length===0)
                    window.__db_cache_prepared_records__=e.data.data
                  console.timeEnd('dump in html')
                }
              }      
            }
          </script>

          PostMessage

          // other codes in dump script section. 
          // I'm not gonna repeat those. see it yourself please  
          ...
          if (indexedDB) {
            console.time('dump2Memory')
            getDBConnection().then(conn=> {
              dump2Memory(conn).then(records=> {
                console.timeEnd('dump2Memory')
                postMessage({
                  eventName: 'onDBDump',
                  data: records
                })
              })
            }).catch(err=> {
              console.error(err)
            })
          }

          結論

          截至目前,我們使用Axios + indexedDB + WebWorker實現的最高效的前端API緩存方案就到此為止了。

          實話實說,現在還只是搭建了一個高效緩存的框架,至于各種適合不同應用場景的緩存策略還沒有實現。

          如果你有有意思的緩存場景或需要何種緩存策略,歡迎留言。

          務端渲染(SSR)是什么

          • 渲染:HTML + CSS + JS + DATA=> 渲染后的 HTML

          服務端

          • 所有模板等資源都存儲在服務端
          • 內網機器拉取數據更快(優勢)
          • 一個 HTML 返回所有數據 | | 客戶端渲染 | 服務端渲染 | | :--- | :---: | :---: | | 請求 | 多個請求(HTML,數據等) | 1個請求 | | 加載過程 | HTML & 數據串行加載 | 1個請求返回 HTML & 數據 | | 渲染 | 前端渲染 | 服務端渲染 | | 可交互 | 圖片等靜態資源加載完成,JS 邏輯執行完成可交互 | 兩端一樣 |

          優勢

          • 減少白屏時間
          • 對于 SEO 友好

          打包存在的問題

          • 瀏覽器的全局變量(Node.js 中沒有 window,document) 組件適配:將不兼容的組件根據打包環境進行適配請求適配:推薦使用 axios
          • 樣式問題(Node.js 無法解析 css) 服務端打包通過 ignore-loader 忽略掉 css 的解析將 style-loader 替換成 isomorphic-style-loader使用打包出來的瀏覽器端 html 為模板設置占位符,動態插入組件

          主站蜘蛛池模板: 精品国产一区二区三区久| 久久久久人妻一区二区三区vr| 中文字幕人妻丝袜乱一区三区| 日本无卡码免费一区二区三区| 亚洲国产综合无码一区二区二三区| 亚洲日本中文字幕一区二区三区 | 国产精品污WWW一区二区三区| 一区二区手机视频| 亚洲日本乱码一区二区在线二产线| 中文字幕在线不卡一区二区| 51视频国产精品一区二区| 国产婷婷色一区二区三区深爱网| 一区二区免费国产在线观看| 久久无码人妻精品一区二区三区| 最美女人体内射精一区二区| 精品福利一区二区三区免费视频| 国产精品无码一区二区三区免费| 中文字幕一精品亚洲无线一区| 精品一区二区三区水蜜桃| 东京热人妻无码一区二区av| 五月婷婷一区二区| 一区二区三区四区精品视频| 国产福利日本一区二区三区| 不卡一区二区在线| 亚洲国产激情在线一区| 色系一区二区三区四区五区| 人妻体体内射精一区二区| 美女福利视频一区二区| 亚洲av无码不卡一区二区三区| 亚洲一区二区三区影院| 亚洲AV午夜福利精品一区二区| 日韩AV无码一区二区三区不卡 | 国产怡春院无码一区二区| 无码人妻精品一区二区三区在线 | 人妻免费一区二区三区最新| 精品国产一区二区三区不卡| 国产波霸爆乳一区二区| 精品国产日韩亚洲一区91| 一区二区三区日韩| 国产亚洲福利一区二区免费看| 日韩精品一区二区三区毛片|