自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

一文搞懂得物前端監(jiān)控

開發(fā) 前端
前端監(jiān)控是一個成熟業(yè)務線的標配,目前最多的場景是監(jiān)控 JS 錯誤,接口請求和性能優(yōu)化,然后根據(jù)日志信息進行分析分類的可視化展示,在發(fā)生異常的時候通知到相應的業(yè)務開發(fā),監(jiān)控的性能指標給頁面的體驗優(yōu)化提供數(shù)據(jù)對比和優(yōu)化的方向。

一背景

得物的服務端監(jiān)控是比較全面和有效的,除了上報原始日志數(shù)據(jù),還通過數(shù)據(jù)分析制定線上告警機制,調(diào)用鏈路分析,而針對前端項目這一塊,還是不夠全面的。對前端線上問題感應不及時,靠人肉發(fā)現(xiàn),沒有告警機制等問題,所以就有個前端監(jiān)控這個項目。前端監(jiān)控也確實很有必要,我們需要對線上的頁面有個全面的把控,而至于怎么做監(jiān)控,做數(shù)據(jù)上報,以及數(shù)據(jù)分析,如何針對監(jiān)控數(shù)據(jù)分析出有用的核心鏈路的告警等也能有個全面的認識。本文主要是介紹得物針對監(jiān)控做了哪些事情以及對前端底層監(jiān)控手段做個總結。

二監(jiān)控類型

前端監(jiān)控的范圍很廣,如監(jiān)控性能,監(jiān)控異常,監(jiān)控告警等一系列的維度來確保我們的頁面和功能是正常的,在出現(xiàn)問題時研發(fā)可以及時做出響應,及時追蹤,定位問題。

性能監(jiān)控

目前前端平臺在性能監(jiān)控和異常監(jiān)控是分開的,性能監(jiān)控有專門 SDK,去做上報和分析,聯(lián)合得物 App 端上一起做了性能優(yōu)化和數(shù)據(jù)分析,這塊是獨立的。主要包括性能的上報,以及性能的優(yōu)化手段。下圖是前端性能監(jiān)控方面的數(shù)據(jù)展示效果。

圖片圖片

總體來說性能監(jiān)控是做的比較大的,而且監(jiān)控對提高頁面的秒開是有實際優(yōu)化指導意義的,離線包、預請求等都是優(yōu)化手段。

異常告警監(jiān)控

針對異常監(jiān)控也有單獨的平臺和 SDK 去承接和上報,下圖來展示收集一些異常信息,對異常信息進行分類,再通過設置告警機制來通知開發(fā)人員及時發(fā)現(xiàn)問題:

圖片圖片

這些錯誤類的信息不一定是我們都需要關注的,有些疑難雜癥,但是有不影響頁面展示和功能的報錯,也是可以忽略的,要知道不是所有的錯誤都能被解決的,這個時候我們可以只關注那些影響我們頁面核心功能的部分,針對這部分做一個告警配置,例如:

圖片圖片

針對告警信息,再進一步對錯誤進行分析,找到能解決的問題,達到對頁面穩(wěn)定性的把控。

日常巡檢監(jiān)控

還有一種比較特殊的場景,針對運營活動做的營銷會場,在各個配置的坑位去做巡檢,提前發(fā)現(xiàn)會場是否正常,有沒有白屏、API 異常等提前發(fā)現(xiàn),然后聯(lián)系相應的人去人工處理。這種監(jiān)控模式對于要投放出去的頁面做提前檢查是很有效果的,事實證明也確實如此,提前避免了很多線上問題,很厲害。

圖片圖片

三監(jiān)控知識梳理

上面幾個大類型的監(jiān)控都是前端同學在監(jiān)控方面做的努力和成果,涉及到的知識點也很多,同樣對于業(yè)務的把控和理解也很深入,很值得學習并了解,后面的內(nèi)容就是針對監(jiān)控做個梳理總結,了解下大致的實現(xiàn)方式。

前端監(jiān)控的必要性

  • 用戶在訪問頁面的時候大致會經(jīng)歷下面的階段:
  • 向服務端請求獲取靜態(tài)資源;
  • 瀏覽器加載資源;

資源加載成功之后頁面渲染繼續(xù)運行。

這些階段都有報錯的可能,而前端要做的就是監(jiān)控后面這階段:資源加載和頁面交互。

做前端監(jiān)控也有很多其他好處, 例如:

  • 第一時間上報異常,解決問題;
  • 能夠比較完整的重現(xiàn)問題用戶的操作全流程路徑,方便開發(fā)者復現(xiàn)問題,定位問題;
  • 針對頁面的 PV、UV 等信息可以為產(chǎn)品和運營做推廣決策提供數(shù)據(jù)依據(jù)。

前端監(jiān)控是很有必要的,通過監(jiān)控,我們能在線上應用異常時,第一時間收到反饋,并及時止損。對業(yè)務的發(fā)展是有正向作用的。

前端監(jiān)控目標

保證穩(wěn)定性(錯誤監(jiān)控)

錯誤監(jiān)控包括 JavaScript 代碼錯誤、Promsie 錯誤、接口(XHR,F(xiàn)etch)錯誤、資源加載錯誤(Script,Link等)等,這些錯誤大多會導致頁面功能異常甚至白屏。

提升用戶體驗(性能監(jiān)控)

性能監(jiān)控包括頁面的加載時間、接口響應時間等,側面反應了用戶體驗的好壞。

  • 加載時間:頁面運行時各個階段的加載時間;
  • TTFB(Time To First Byte)(首字節(jié)時間):瀏覽器發(fā)起第一個請求到數(shù)據(jù)返回第一個字節(jié)所消耗的時間,這個時間包含了網(wǎng)絡請求時間、后端處理時間;
  • FP(First Paint)(首次繪制):首次繪制包括了任何用戶自定義的背景繪制,它是將第一個像素點繪制到屏幕的時刻;
  • FCP(First Content Paint)(首次內(nèi)容繪制):首次內(nèi)容繪制是瀏覽器將第一個 DOM 渲染到屏幕的時間,可以是任何文本、圖像、SVG 等的時間;
  • FMP(First Meaningful paint)(首次有意義繪制):首次有意義繪制是頁面可用性的量度標準;
  • LCP(Largest Contentful Paint):視窗內(nèi)最大的圖片或者文本渲染的時間,當最大的內(nèi)容塊渲染完的時候,基本上主內(nèi)容都加載完了,與現(xiàn)有的頁面加載指標相比,與用戶體驗的相關性更好;
  • FID(First Input Delay)(首次輸入延遲):用戶首次和頁面交互到頁面響應交互的時間;
  • 卡頓:指超過 50ms 的長任務,具體的指標可以根據(jù)頁面的內(nèi)容進行調(diào)節(jié),一般 50ms 人眼就能感覺到卡頓。

針對業(yè)務進行統(tǒng)計

  • PV:Page View 即頁面瀏覽量或點擊量;
  • UV:指訪問某個站點的不同 IP 地址的人數(shù);
  • 頁面的停留時間:用戶在每一個頁面的停留時間。

前端監(jiān)控的流程

前端埋點(通過 SDK 給頁面的 DOM 都加上標記)

  • 數(shù)據(jù)上報(收集,存儲)
  • 分析和計算(將采集到的數(shù)據(jù)進行加工匯總)
  • 可視化展示(按照緯度將數(shù)據(jù)展示)
  • 監(jiān)控報警(發(fā)現(xiàn)異常后按一定的條件觸發(fā)報警)

前端埋點方案

代碼埋點

代碼埋點,就是項目中引入埋點 SDK,手動在業(yè)務代碼中標記,觸發(fā)埋點事件進行上報。比如頁面中的某一個模塊的點擊事件,會在點擊事件的監(jiān)聽中加入觸發(fā)埋點的代碼 this.$track('事件名', { 需要上傳的業(yè)務數(shù)據(jù) }),將數(shù)據(jù)上報到服務器端。

  • 優(yōu)點:能夠在任何時刻,更精確的發(fā)送需要的數(shù)據(jù)信息,上報數(shù)據(jù)更靈活;
  • 缺點:工作量大,代碼侵入太強,過于耦合業(yè)務代碼,一次埋點的更改就要引起發(fā)版之類的操作。

這個方案也是我們實際項目中現(xiàn)有的方案。

可視化埋點

通過可視化交互的手段,代替代碼埋點,可以新建、編輯、修改埋點。在組件和頁面的維度進行埋點的設計。

將業(yè)務代碼和埋點代碼分離,提供一個可視化交互的頁面,輸入為業(yè)務代碼,通過這個可視化系統(tǒng),可以在業(yè)務代碼中自定義的增加埋點事件,最后輸出的代碼耦合了業(yè)務代碼和埋點代碼。

這個方案是可以解決第一種代碼埋點的痛點,也是我們目前正準備做的方案。

無痕埋點

前端的任意一個事件都被綁定一個標識,所有的事件都被記錄下來,通過定期上傳記錄文件,配合文件解析,解析出來我們想要的數(shù)據(jù),并生成可視化報告。

無痕埋點的優(yōu)點是采集全量數(shù)據(jù),不會出現(xiàn)漏埋和誤埋等現(xiàn)象。缺點是給數(shù)據(jù)傳輸和服務器增加壓力,也無法靈活定制數(shù)據(jù)結構。針對業(yè)務數(shù)據(jù)的準確性不高。

監(jiān)控腳本

日志存儲

前端的埋點上報需要存儲起來,這個可以使用阿里云的日志服務,不需要投入開發(fā)就可以采集。

新建一個項目比如:xxx-monitor

新建一個存儲日志,根據(jù)阿里云的要求發(fā)起請求,攜帶需要上報的數(shù)據(jù):

http://${project}.${host}/logstores/${logStore}/track

圖片圖片

代碼中調(diào)用 Track 上報日志:

日志的上報可以封裝成公共的調(diào)用方式, monitor/utils/里面放所有的工具方法;

tracker.js 的實現(xiàn)就是按照阿里云的上報格式發(fā)送請求,并帶上處理好的需要上報的業(yè)務數(shù)據(jù)即可,下面的都是固定的,在日志服務建好:

圖片圖片

實現(xiàn)一個 Tracker 類導出類的實例即可,這樣在監(jiān)控的核心代碼中直接調(diào)用 tracker.send(data),核心實現(xiàn)代碼如下:

// monitor/utils/get/track.js
...
class SendTrackLoger {
  constructor() {
    this.url = `http://${project}.${host}/logstores/${logStore}/track`
    this.xhr = new XMLHttpRequest()
  }


  send(data = {}, callback) {
    const logData = {...logData}
    for(let key in logs) {
      if (typeof logs[key] === 'number') {
        logs[key] = `${logs[key]}` // 這是阿里云的要求,字段不能是數(shù)字類型
      }
    }


    let body = JSON.stringify({
      __logs__: [logs]
    })
    this.xhr.open('POST', this.url, true)
    this.xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
    this.xhr.setRequestHeader('x-log-apiversion', '0.6.0')
    this.xhr.setRequestHeader('x-log-bodyrawsize', body.length)
    this.xhr.onload = function() {
      if (this.status >= 200 && this.status <= 300 || this.status === 304) {
        callback && callback()
      }
    }
    this.xhr.onerror = function(error) {
      console.log(error)
    }
    this.xhr.send(body)
  }
}


export default new SendTrackLoger()

這里展示的是自定義要上報的數(shù)據(jù)字段:

圖片圖片

監(jiān)控錯誤

前端需要監(jiān)控的錯誤有兩類:

  • Javascript 錯誤(JS 錯誤,Promise 異常)
  • 監(jiān)聽 Error 錯誤(資源加載錯誤)

腳本實現(xiàn)

新建一個 fronend-monitor  項目,這個項目就相當于我們的工程項目,監(jiān)控的核心實現(xiàn)可以寫到項目里面,也可以抽成 SDK 的形式 Import 引入進來,這里先寫到項目中。

webpack.config.js 用來打包項目,做接口數(shù)據(jù) Mock,測試 XHR 請求監(jiān)控接口錯誤等。

const path = require('path')
const HtmlWebpackPlugin = xxx


module.exports = {
  mode: 'development',
  context: process.cwd(),
  entry:'./src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'monitor.js'
  },
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    before(router) {
      router.get('/success', function(req, res) {
        res.json({ id: 1 })
      })
      router.post('/error', function(req, res) {
        res.sendStatus(500)
      })
    },
  },
  module: {},
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      inject: "head"
    })
  ]
}

新建一個 src/index.html 在這個里面寫一些問題代碼,然后測試監(jiān)控的錯誤捕獲。

// src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" cnotallow="width=device-width, initial-scale=1.0">
  <title>monitor</title>
</head>
<body>
  <input id="jsErrorBtn" type="button" value="js 代碼錯誤" notallow="btnClick()" />
  <input id="promiseErrorBtn" type="button" value="promise 錯誤" notallow="promiseClick()" />
  <input id="successBtn" type="button" value="成功 ajax 請求" notallow="successAjax()" />
  <input id="errorBtn" type="button" value="失敗 ajax 請求" notallow="errorAjax()" />
  <script>
    function btnClick() {
      window.goods.type = 2
    }


    function promiseClick() {
      new Promise((resolve, reject) => {
        resolve(1)
      }, () => {
        console.log(123)
      })
    }


    function successAjax() {
      var xhr = new XMLHttpRequest()
      xhr.open('GET', '/success',  true)
      xhr.responseType = 'json'
      xhr.onload = function () {
        console.log(xhr.response)
      }
      xhr.send()
    }


    function errorAjax() {
      var xhr = new XMLHttpRequest()
      xhr.open('POST', '/error',  true)
      xhr.responseType = 'json'
      xhr.onload = function() {
        console.log(xhr.response)
      }
      xhr.onerror = function(err) {
        console.log(err)
      }
      xhr.send('name=123')
    }
</script>
</body>
</html>

上報未捕獲的 Javascript 錯誤

Javascript 錯誤分為 2 種:語法錯誤,資源家加載錯誤,這些錯誤都會被 window.addEventListener('error', function(event) {})捕獲,來判斷是否是資源加載錯誤。

window.addEventListener('error', function(event) {


    // 如果 target 是script link 等資源
    if (event.target && (event.target.src || event.target.href)) {
      const element = getElement(event.target || event.path)
      tracker.send({
        ...
        title: document.title,
        url: location.href,
        timestamp: event.timeStamp,
        userAgent: navigator.userAgent,
        type: 'resourceError',
        ...
      })
    } else {
      tracker.send({
        ...
        title: document.title,
        url: location.href,
        timestamp: event.timeStamp,
        userAgent: navigator.userAgent,
        type: 'jsError',
        ...
      })
    }
  }, true)

代碼中未被捕獲的 Promise 錯誤,要監(jiān)聽 unhandledrejection 事件 window.addEventListener('unhandledrejection', function(event) {})。

// 監(jiān)聽未捕獲的 promise 錯誤
  window.addEventListener('unhandledrejection', function(event) {
    // PromiseRejectionEvent
    let message = ''
    let stack = ''
    const reason =  event.reason
    let filename = ''
    let lineno = ''
    let colno = ''
    if (reason) {
      message = reason.message
      stack = reason.stack
      const match = stack.match(/\s+at\s+(.+):(\d+):(\d+).+/)
      filename = match[1]
      lineno = match[2]
      colno = match[3]
    }


    tracker.send({
      ...
      title: document.title,
      url: location.href,
      timestamp: event.timeStamp,
      userAgent: navigator.userAgent,
      type: 'promiseError',
      ...
    })
  }, true)

接口異常上報

接口異常上報主要是攔截請求,攔截 XMLHttpRequest 對象,改寫 XHR 的 Open 和 Send 方法,將需要上報的數(shù)據(jù)發(fā)到阿里云存儲,監(jiān)聽 Load,Error,Abort 事件,上報數(shù)據(jù):

// src/monitor/lib/xhr.js
import tracker from '../utils/tracker'


export default function injectXHR() {
  // 獲取 window 上的 XMLHttpRequest 對象
  const XMLHttpRequest = window.XMLHttpRequest
  // 保存舊的open, send函數(shù)
  const prevOpen = XMLHttpRequest.prototype.open
  const prevSend = XMLHttpRequest.prototype.send


  // 不可使用箭頭函數(shù),不然會找不到 this 實例
  XMLHttpRequest.prototype.open = function (method, url, async, username, password) {
    // 重寫open,攔截請求
    // 不攔截 track 本身以及 socket, 直接放行
    if (!url.match(/logstores/) && !url.match(/sockjs/)) {
      this.logData = { method, url, async, username, password }
    }
    return prevOpen.apply(this, arguments)
  }


  XMLHttpRequest.prototype.send = function (body) {
    // 重寫 send,攔截有 logData 的請求,獲取 body 參數(shù)
    if (this.logData) {
      this.logData.body = body
      let startTime = Date.now()
      function handler(type) {
        return function (event) {
          // event: ProgressEvent
          let duration = Date.now() - startTime
          let status = this.status
          let statusText = this.statusText
          console.log(event)


          tracker.send({
            type: 'xhr',
            eventType: type,
            pathname: this.logData.url,
            status: `${status} ${statusText}`,
            duration: `${duration}`, // 接口響應時長
            response: this.response ? JSON.stringify(this.response) : '',
            params: body || '',
          })
        }
      }
      this.addEventListener('load', handler('load'), false)
      this.addEventListener('error', handler('error'), false)
      this.addEventListener('abort', handler('abort'), false)
    }


    return prevSend.apply(this, arguments)
  }
}

監(jiān)控白屏

白屏就是頁面上什么東西也沒有,在頁面加載完成之后,如果頁面上的空白點很多,就說明頁面是白屏的,需要上報,這個上報的時機是:document.readyState === 'complete' 表示文檔和所有的子資源已完成加載,表示load(window.addEventListener('load')狀態(tài)事件即將被觸發(fā)。

document.readyState 有三個值:loading(document正在加載),interactive(可交互,表示正在加載的狀態(tài)結束,但是圖像,樣式和框架之類的子資源仍在加載),complete 就是完成,所以監(jiān)控白屏需要在文檔都加載完成的情況下觸發(fā)。

// src/monitor/utils/onload.js
export function onload(callback) {
  if (document.readyState === 'complete') {
    callback()
  } else {
    window.addEventListener('onload', callback)
  }
}

監(jiān)控白屏的思路主要是:可以將可視區(qū)域中心點作為坐標軸的中心,在X、Y軸上各分 10 個點,找出這個 20 個坐標點上最上層的 DOM 元素,如過這些元素是包裹元素,空白點數(shù)就加一,包裹元素可以自定義比如 Html Body App Root Container Content 等,空白點數(shù)大于 0 就上報白屏日志。

export default function computedBlankScreen() {
  // 包裹玉元素列表
  const wrapperSelectors = ['body', 'html', '#root', '#App']
  // 空白節(jié)點的個數(shù)
  let emptyPoints = 0
  // 判斷20個點處的元素是否是包裹元素
  function isWrapper(element) {
    const selector = getSelector(element)
    console.log(selector)
    if (wrapperSelectors.indexOf(selector) >= 0) { // 表示是在包裹元素里面,空白點就要加一
      emptyPoints++
    }
  }
  onload(function() {
    let xElements, yElements
    for (let i = 0; i <=9; i++) {
      xElements = document.elementFromPoint(window.innerWidth * i / 10, window.innerHeight / 2)
      yElements = document.elementFromPoint(window.innerHeight * i / 10, window.innerWidth / 2)
      // document.elementFromPoint 返回的是某一個坐標點的由到外的html元素的集合
      isWrapper(xElements[0])
      isWrapper(yElements[0])
    }
    if (emptyPoints >= 0) {
      let centerPoint = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2)
      tracker.send()
    }
  })
}

監(jiān)控卡頓

用戶交互的響應時間如果大于某一個時間,用戶就會感覺卡頓??梢远ㄒ粋€時間比如 100 毫秒,就代表響應時間長,會卡頓。

PerformanceObserver 構造函數(shù)使用給定的觀察者 Callback 生成新的PerformanceObserver 對象,當通過 Observe() 方法注冊條目類型(需要監(jiān)控的類型)的性能條目被記錄下來時,會調(diào)用該觀察者回調(diào)。

所以可以 new PerformanceObserver 來監(jiān)控 longTask,監(jiān)控的資源加載如果超過 100 毫秒就表示卡頓,可以瀏覽器空閑(requestIdleCallback)的時候上報數(shù)據(jù)。

....
export default function longTask() {
  new PerformanceObserver(function(list) {
    list.getEntries().forEach(function(entry) {
      if (entry.duration > 100) {
        // 瀏覽器空閑的時候上報
        requestIdleCallback(() => {
          tracker.send({
            type: 'longTask',
            eventType: lastEvent.type,
            startTime: formatTime(entry.startTime),
            duration: formatTime(entry.duration),
          });
        });
      }
    })
  }).observe({ entryTypes: ['longtask']})
}

性能指標

PerformanceObserver.observe 方法用于觀察傳入的參數(shù)中指定的性能條目類型的集合。當記錄一個指定類型的性能條目時,性能監(jiān)測對象的回調(diào)函數(shù)將會被調(diào)用。performance.timing 記錄了從輸入 URL 到頁面加載完成的所有的時間,從這些字段中可以提取對對頁面性能的監(jiān)控,通過分析這些指標來優(yōu)化頁面的體驗,比如統(tǒng)計 FMP、LCP 等,具體可以查看 MDN。

統(tǒng)計pv (頁面的停留時間)

navigator.connection 對象獲取網(wǎng)絡連接的信息:effectiveType(網(wǎng)絡類型),RTT(估算餓往返時間)等,還能通過監(jiān)聽 window.addEventListener('unload')事件計算用戶在頁面的停留時間。

import tracker from '../util/tracker';
export function pv() {
    var connection = navigator.connection;
    tracker.send({
        type: 'pv',
        networkType: connection.effectiveType,  // 網(wǎng)絡類型
        rtt: connection.rtt, // 往返時間
        screen: `${window.screen.width}x${window.screen.height}` // 設備分辨率
    });
    let startTime = Date.now();
    window.addEventListener('unload', () => {
        let stayTime = Date.now() - startTime; // 頁面停留時間
        tracker.send({
            type: 'stayTime',
            stayTime
        });
    }, false);
}

四、總結

前端監(jiān)控是一個成熟業(yè)務線的標配,目前最多的場景是監(jiān)控 JS 錯誤,接口請求和性能優(yōu)化,然后根據(jù)日志信息進行分析分類的可視化展示,在發(fā)生異常的時候通知到相應的業(yè)務開發(fā),監(jiān)控的性能指標給頁面的體驗優(yōu)化提供數(shù)據(jù)對比和優(yōu)化的方向。

參考文章:

  • https://juejin.cn/post/6939703198739333127
  • https://wicg.github.io/largest-contentful-paint/
  • https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver/PerformanceObserver
  • https://developer.mozilla.org/zh-CN/docs/Web/API/Long_Tasks_API
  • https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceTiming
  • https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator
責任編輯:武曉燕 來源: 得物技術
相關推薦

2020-12-07 06:19:50

監(jiān)控前端用戶

2022-06-07 10:13:22

前端沙箱對象

2024-04-12 12:19:08

語言模型AI

2022-03-24 08:51:48

Redis互聯(lián)網(wǎng)NoSQL

2021-03-22 10:05:59

netstat命令Linux

2023-09-08 08:20:46

ThreadLoca多線程工具

2023-09-15 12:00:01

API應用程序接口

2021-01-13 05:21:59

參數(shù)

2021-06-30 08:45:02

內(nèi)存管理面試

2022-08-15 15:39:23

JavaScript面向?qū)ο?/a>數(shù)據(jù)

2023-04-03 15:04:00

RPCPHP語言

2023-08-24 16:50:45

2023-10-16 08:16:31

Bean接口類型

2024-06-05 11:43:10

2020-03-18 14:00:47

MySQL分區(qū)數(shù)據(jù)庫

2019-11-19 08:00:00

神經(jīng)網(wǎng)絡AI人工智能

2021-02-28 20:53:37

Cookie存儲瀏覽器

2021-03-04 00:09:31

MySQL體系架構

2021-07-08 10:08:03

DvaJS前端Dva

2023-03-06 21:29:41

mmap技術操作系統(tǒng)
點贊
收藏

51CTO技術棧公眾號