深入了解前端监控原理

开发 前端
前端监控分为性能监控和错误监控。其中监控又分为两个环节:数据采集和数据上报。本文主要讲的就是如何进行数据采集和数据上报。

 前端监控分为性能监控和错误监控。其中监控又分为两个环节:数据采集和数据上报。本文主要讲的就是如何进行数据采集和数据上报。

数据采集

性能数据采集

性能数据采集需要使用 window.performance  API 

https://developer.mozilla.org/zh-CN/docs/Web/API/Performance  

Performance 接口可以获取到当前页面中与性能相关的信息,它是 High Resolution Time API 的一部分,同时也融合了 Performance Timeline API、Navigation Timing API、 User Timing API 和 Resource Timing API。

从 MDN 的文档可以看出,window.performance.timing 包含了页面加载各个阶段的起始及结束时间。

这些属性需要结合下图一起看,更好理解:

在这里插入图片描述

为了方便大家理解 timing 各个属性的意义,我在知乎找到一位网友对于 timing 写的简介,在此转载一下。 

  1. timing: {  
  2.         // 同一个浏览器上一个页面卸载(unload)结束时的时间戳。如果没有上一个页面,这个值会和fetchStart相同。  
  3.  navigationStart: 1543806782096,  
  4.  // 上一个页面unload事件抛出时的时间戳。如果没有上一个页面,这个值会返回0。 
  5.  unloadEventStart: 1543806782523,  
  6.  // 和 unloadEventStart 相对应,unload事件处理完成时的时间戳。如果没有上一个页面,这个值会返回0。  
  7.  unloadEventEnd: 1543806782523,  
  8.  // 第一个HTTP重定向开始时的时间戳。如果没有重定向,或者重定向中的一个不同源,这个值会返回0。  
  9.  redirectStart: 0,  
  10.  // 最后一个HTTP重定向完成时(也就是说是HTTP响应的最后一个比特直接被收到的时间)的时间戳。  
  11.  // 如果没有重定向,或者重定向中的一个不同源,这个值会返回0.   
  12.  redirectEnd: 0,  
  13.  // 浏览器准备好使用HTTP请求来获取(fetch)文档的时间戳。这个时间点会在检查任何应用缓存之前。 
  14.  fetchStart: 1543806782096,  
  15.  // DNS 域名查询开始的UNIX时间戳。  
  16.         //如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和fetchStart一致。  
  17.  domainLookupStart: 1543806782096,  
  18.  // DNS 域名查询完成的时间.  
  19.  //如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等  
  20.  domainLookupEnd: 1543806782096,  
  21.  // HTTP(TCP) 域名查询结束的时间戳。  
  22.         //如果使用了持续连接(persistent connection),或者这个信息存储到了缓存或者本地资源上,这个值将和 fetchStart一致。  
  23.  connectStart: 1543806782099,  
  24.  // HTTP(TCP) 返回浏览器与服务器之间的连接建立时的时间戳。  
  25.         // 如果建立的是持久连接,则返回值等同于fetchStart属性的值。连接建立指的是所有握手和认证过程全部结束。  
  26.  connectEnd: 1543806782227,  
  27.  // HTTPS 返回浏览器与服务器开始安全链接的握手时的时间戳。如果当前网页不要求安全连接,则返回0。  
  28.  secureConnectionStart: 1543806782162,  
  29.  // 返回浏览器向服务器发出HTTP请求时(或开始读取本地缓存时)的时间戳。  
  30.  requestStart: 1543806782241,  
  31.  // 返回浏览器从服务器收到(或从本地缓存读取)第一个字节时的时间戳。  
  32.         //如果传输层在开始请求之后失败并且连接被重开,该属性将会被数制成新的请求的相对应的发起时间。  
  33.  responseStart: 1543806782516,  
  34.  // 返回浏览器从服务器收到(或从本地缓存读取,或从本地资源读取)最后一个字节时  
  35.         //(如果在此之前HTTP连接已经关闭,则返回关闭时)的时间戳。  
  36.  responseEnd: 1543806782537,  
  37.  // 当前网页DOM结构开始解析时(即Document.readyState属性变为“loading”、相应的 readystatechange事件触发时)的时间戳。  
  38.  domLoading: 1543806782573,  
  39.  // 当前网页DOM结构结束解析、开始加载内嵌资源时(即Document.readyState属性变为“interactive”、相应的readystatechange事件触发时)的时间戳。  
  40.  domInteractive: 1543806783203,  
  41.  // 当解析器发送DOMContentLoaded 事件,即所有需要被执行的脚本已经被解析时的时间戳。  
  42.  domContentLoadedEventStart: 1543806783203,  
  43.  // 当所有需要立即执行的脚本已经被执行(不论执行顺序)时的时间戳。  
  44.  domContentLoadedEventEnd: 1543806783216,  
  45.  // 当前文档解析完成,即Document.readyState 变为 'complete'且相对应的readystatechange 被触发时的时间戳  
  46.  domComplete: 1543806783796,  
  47.  // load事件被发送时的时间戳。如果这个事件还未被发送,它的值将会是0。  
  48.  loadEventStart: 1543806783796,  
  49.  // 当load事件结束,即加载事件完成时的时间戳。如果这个事件还未被发送,或者尚未完成,它的值将会是0.  
  50.  loadEventEnd: 1543806783802  

通过以上数据,我们可以得到几个有用的时间 

  1. // 重定向耗时  
  2. redirect: timing.redirectEnd - timing.redirectStart,  
  3. // DOM 渲染耗时 
  4. dom: timing.domComplete - timing.domLoading,  
  5. // 页面加载耗时  
  6. load: timing.loadEventEnd - timing.navigationStart,  
  7. // 页面卸载耗时  
  8. unload: timing.unloadEventEnd - timing.unloadEventStart,  
  9. // 请求耗时  
  10. request: timing.responseEnd - timing.requestStart,  
  11. // 获取性能信息时当前时间  
  12. time: new Date().getTime(), 

还有一个比较重要的时间就是白屏时间,它指从输入网址,到页面开始显示内容的时间。

将以下脚本放在 </head> 前面就能获取白屏时间。 

  1. <script>  
  2.     whiteScreen = new Date() - performance.timing.navigationStart  
  3.     // 通过 domLoading 和 navigationStart 也可以  
  4.     whiteScreen = performance.timing.domLoading - performance.timing.navigationStart  
  5. </script> 

通过这几个时间,就可以得知页面首屏加载性能如何了。

另外,通过 window.performance.getEntriesByType('resource') 这个方法,我们还可以获取相关资源(js、css、img...)的加载时间,它会返回页面当前所加载的所有资源。

在这里插入图片描述

它一般包括以下几个类型:

  •  sciprt
  •  link
  •  img
  •  css
  •  fetch
  •  other
  •  xmlhttprequest

我们只需用到以下几个信息: 

  1. // 资源的名称  
  2. name: item.name,  
  3. // 资源加载耗时  
  4. duration: item.duration.toFixed(2),  
  5. // 资源大小  
  6. size: item.transferSize,  
  7. // 资源所用协议  
  8. protocol: item.nextHopProtocol, 

现在,写几行代码来收集这些数据。 

  1. // 收集性能信息  
  2. const getPerformance = () => {  
  3.     if (!window.performance) return  
  4.     const timing = window.performance.timing  
  5.     const performance = {  
  6.         // 重定向耗时  
  7.         redirect: timing.redirectEnd - timing.redirectStart,  
  8.         // 白屏时间  
  9.         whiteScreen: whiteScreen,  
  10.         // DOM 渲染耗时  
  11.         dom: timing.domComplete - timing.domLoading,  
  12.         // 页面加载耗时  
  13.         load: timing.loadEventEnd - timing.navigationStart,  
  14.         // 页面卸载耗时  
  15.         unload: timing.unloadEventEnd - timing.unloadEventStart,  
  16.         // 请求耗时  
  17.         request: timing.responseEnd - timing.requestStart,  
  18.         // 获取性能信息时当前时间  
  19.         time: new Date().getTime(),  
  20.     }  
  21.     return performance  
  22.  
  23. // 获取资源信息  
  24. const getResources = () => {  
  25.     if (!window.performance) return  
  26.     const data = window.performance.getEntriesByType('resource')  
  27.     const resource = {  
  28.         xmlhttprequest: [],  
  29.         css: [],  
  30.         other: [],  
  31.         script: [],  
  32.         img: [],  
  33.         link: [],  
  34.         fetch: [],  
  35.         // 获取资源信息时当前时间  
  36.         time: new Date().getTime(),  
  37.     } 
  38.     data.forEach(item => {  
  39.         const arry = resource[item.initiatorType]  
  40.         arry && arry.push({  
  41.             // 资源的名称  
  42.             name: item.name,  
  43.             // 资源加载耗时  
  44.             duration: item.duration.toFixed(2),  
  45.             // 资源大小  
  46.             size: item.transferSize,  
  47.             // 资源所用协议  
  48.             protocol: item.nextHopProtocol,  
  49.         })  
  50.     })  
  51.     return resource  

小结

通过对性能及资源信息的解读,我们可以判断出页面加载慢有以下几个原因:

  1.  资源过多、过大
  2.  网速过慢
  3.  DOM 元素过多

除了用户网速过慢,我们没办法之外,其他两个原因都是有办法解决的,性能优化的文章和书籍网上已经有很多了,有兴趣可自行查找资料了解。

PS:其实页面加载慢还有其他原因,例如没有使用按需加载、没有使用 CDN 等等。不过这里我们强调的仅通过对性能和资源信息的解读来获取原因。

错误数据采集

目前所能捕捉的错误有三种:

  1.  资源加载错误,通过 addEventListener('error', callback, true) 在捕获阶段捕捉资源加载失败错误。
  2.  js 执行错误,通过 window.onerror 捕捉 js 错误。
  3.  promise 错误,通过 addEventListener('unhandledrejection', callback)捕捉 promise 错误,但是没有发生错误的行数,列数等信息,只能手动抛出相关错误信息。

我们可以建一个错误数组变量 errors 在错误发生时,将错误的相关信息添加到数组,然后在某个阶段统一上报,具体如何操作请看下面的代码: 

  1. // 捕获资源加载失败错误 js css img...  
  2. addEventListener('error', e => {  
  3.     const target = e.target 
  4.     if (target != window) { 
  5.          monitor.errors.push({  
  6.             type: target.localName,  
  7.             url: target.src || target.href,  
  8.             msg: (target.src || target.href) + ' is load error',  
  9.             // 错误发生的时间  
  10.             time: new Date().getTime(),  
  11.         })  
  12.     }  
  13. }, true)  
  14. // 监听 js 错误  
  15. window.onerror = function(msg, url, row, col, error) {  
  16.     monitor.errors.push({  
  17.         type: 'javascript',  
  18.         row: row,  
  19.         col: col,  
  20.         msg: error && error.stack? error.stack : msg,  
  21.         url: url,  
  22.         // 错误发生的时间  
  23.         time: new Date().getTime(), 
  24.     })  
  25.  
  26. // 监听 promise 错误 缺点是获取不到行数数据  
  27. addEventListener('unhandledrejection', e => {  
  28.     monitor.errors.push({  
  29.         type: 'promise',  
  30.         msg: (e.reason && e.reason.msg) || e.reason || '',  
  31.         // 错误发生的时间  
  32.         time: new Date().getTime(),  
  33.     })  
  34. }) 

小结

通过错误收集,可以了解到网站发生错误的类型及数量,从而做出相应的调整,以减少错误发生。完整代码和 DEMO 会在文章末尾放出,大家可以复制代码(HTML文件)在本地测试一下。 

数据上报

性能数据上报

性能数据可以在页面加载完之后上报,尽量不要对页面性能造成影响。 

  1. window.onload = () => {  
  2.     // 在浏览器空闲时间获取性能及资源信息  
  3.     // https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback  
  4.     if (window.requestIdleCallback) {  
  5.         window.requestIdleCallback(() => {  
  6.             monitor.performance = getPerformance()  
  7.             monitor.resources = getResources()  
  8.         })  
  9.     } else {  
  10.         setTimeout(() => {  
  11.             monitor.performance = getPerformance()  
  12.             monitor.resources = getResources()  
  13.         }, 0)  
  14.     }  

当然,你也可以设一个定时器,循环上报。不过每次上报最好做一下对比去重再上报,避免同样的数据重复上报。

错误数据上报

我在 DEMO 里提供的代码,是用一个 errors 数组收集所有的错误,再在某一阶段统一上报(延时上报)。

其实,也可以改成在错误发生时上报(即时上报)。这样可以避免“收集完错误,但延时上报还没触发,用户却已经关掉网页导致错误数据丢失”的问题。 

  1. // 监听 js 错误  
  2. window.onerror = function(msg, url, row, col, error) {  
  3.     const data = {  
  4.         type: 'javascript',  
  5.         row: row,  
  6.         col: col,  
  7.         msg: error && error.stack? error.stack : msg,  
  8.         url: url,  
  9.         // 错误发生的时间  
  10.         time: new Date().getTime(),  
  11.     }     
  12.     // 即时上报  
  13.     axios.post({ url: 'xxx', data, })  

经网友提醒,可以使用 navigator.sendBeacon() 来进行上报。 

  1. window.addEventListener('unload', logData, false);  
  2. function logData() {  
  3.     navigator.sendBeacon("/log", analyticsData);  

它的技术特点是:

使用 sendBeacon() 方法会使用户代理(浏览器)在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题:数据可靠,传输异步并且不会影响下一页面的加载。

扩展

SPA

window.performance API 是有缺点的,在 SPA 切换路由时,window.performance.timing 的数据不会更新。所以我们需要另想办法来统计切换路由到加载完成的时间。拿 Vue 举例,一个可行的办法就是切换路由时,在路由的全局前置守卫 beforeEach 里获取开始时间,在组件的 mounted 钩子里执行 vm.$nextTick 函数来获取组件的渲染完毕时间。 

  1. router.beforeEach((to, from, next) => {  
  2.  store.commit('setPageLoadedStartTime', new Date())  
  3. })  
  1. mounted() {  
  2.  this.$nextTick(() => {  
  3.   this.$store.commit('setPageLoadedTime', new Date() - this.$store.state.pageLoadedStartTime)  
  4.  })  

除了性能和错误监控,其实我们还可以收集更多的信息。

用户信息收集

navigator

使用 window.navigator 可以收集到用户的设备信息,操作系统,浏览器信息...

UV(Unique visitor)

是指通过互联网浏览这个网页的访客,00:00-24:00 内相同的设备访问只被计算一次。一天内同个访客多次访问仅计算一个 UV。

在用户访问网站时,可以生成一个随机字符串+时间日期,保存在本地。在网页发生请求时(如果超过当天24小时,则重新生成),把这些参数传到后端,后端利用这些信息生成 UV 统计报告。

PV(Page View)

即页面浏览量或点击量,用户每 1 次对网站中的每个网页访问均被记录 1 个PV。用户对同一页面的多次访问,访问量累计,用以衡量网站用户访问的网页数量。

页面停留时间

传统网站

用户在进入 A 页面时,通过后台请求把用户进入页面的时间捎上。过了 10 分钟,用户进入 B 页面,这时后台可以通过接口捎带的参数可以判断出用户在 A 页面停留了 10 分钟。

SPA

可以利用 router 来获取用户停留时间,拿 Vue 举例,通过 router.beforeEach、destroyed 这两个钩子函数来获取用户停留该路由组件的时间。

浏览深度

通过 document.documentElement.scrollTop 属性以及屏幕高度,可以判断用户是否浏览完网站内容。

页面跳转来源

通过 document.referrer 属性,可以知道用户是从哪个网站跳转而来。

小结

通过分析用户数据,我们可以了解到用户的浏览习惯、爱好等等信息,想想真是恐怖,毫无隐私可言。

DEMO 

  1. <!DOCTYPE html>  
  2. <html>  
  3. <head>  
  4.     <meta charset="UTF-8">  
  5.     <meta name="viewport" content="width=device-width, initial-scale=1.0">  
  6.     <meta http-equiv="X-UA-Compatible" content="ie=edge">  
  7.     <script>  
  8.         function monitorInit() {  
  9.             const monitor = {  
  10.                 // 数据上传地址  
  11.                 url: '',  
  12.                 // 性能信息  
  13.                 performance: {},  
  14.                 // 资源信息  
  15.                 resources: {},  
  16.                 // 错误信息  
  17.                 errors: [],  
  18.                 // 用户信息  
  19.                 user: {  
  20.                     // 屏幕宽度  
  21.                     screen: screen.width,  
  22.                     // 屏幕高度  
  23.                     height: screen.height,  
  24.                     // 浏览器平台  
  25.                     platform: navigator.platform,  
  26.                     // 浏览器的用户代理信息  
  27.                     userAgent: navigator.userAgent,  
  28.                     // 浏览器用户界面的语言  
  29.                     language: navigator.language,  
  30.                 },  
  31.                 // 手动添加错误  
  32.                 addError(error) {  
  33.                     const obj = {}  
  34.                     const { type, msg, url, row, col } = error  
  35.                     if (type) obj.type = type  
  36.                     if (msg) obj.msg = msg  
  37.                     if (url) obj.url = url  
  38.                     if (row) obj.row = row 
  39.                     if (col) obj.col = col  
  40.                     obj.time = new Date().getTime()  
  41.                     monitor.errors.push(obj)  
  42.                 },  
  43.                 // 重置 monitor 对象  
  44.                 reset() { 
  45.                     window.performance && window.performance.clearResourceTimings()  
  46.                     monitor.performance = getPerformance()  
  47.                     monitor.resources = getResources()  
  48.                     monitor.errors = []  
  49.                 },  
  50.                 // 清空 error 信息  
  51.                 clearError() {  
  52.                     monitor.errors = []  
  53.                 },  
  54.                 // 上传监控数据  
  55.                 upload() {  
  56.                     // 自定义上传  
  57.                     // axios.post({  
  58.                     //     url: monitor.url, 
  59.                     //     data: {  
  60.                     //         performance,  
  61.                     //         resources,  
  62.                     //         errors,  
  63.                     //         user, 
  64.                     //     }  
  65.                     // })  
  66.                 },  
  67.                 // 设置数据上传地址  
  68.                 setURL(url) {  
  69.                     monitor.url = url  
  70.                 },  
  71.             }  
  72.             // 获取性能信息  
  73.             const getPerformance = () => {  
  74.                 if (!window.performance) return  
  75.                 const timing = window.performance.timing  
  76.                 const performance = {  
  77.                     // 重定向耗时  
  78.                     redirect: timing.redirectEnd - timing.redirectStart,  
  79.                     // 白屏时间  
  80.                     whiteScreen: whiteScreen,  
  81.                     // DOM 渲染耗时  
  82.                     dom: timing.domComplete - timing.domLoading,  
  83.                     // 页面加载耗时  
  84.                     load: timing.loadEventEnd - timing.navigationStart,  
  85.                     // 页面卸载耗时  
  86.                     unload: timing.unloadEventEnd - timing.unloadEventStart,  
  87.                     // 请求耗时  
  88.                     request: timing.responseEnd - timing.requestStart,  
  89.                     // 获取性能信息时当前时间  
  90.                     time: new Date().getTime(),  
  91.                 }  
  92.                 return performance  
  93.             }  
  94.             // 获取资源信息  
  95.             const getResources = () => {  
  96.                 if (!window.performance) return  
  97.                 const data = window.performance.getEntriesByType('resource')  
  98.                 const resource = {  
  99.                     xmlhttprequest: [], 
  100.                     css: [],  
  101.                     other: [],  
  102.                     script: [],  
  103.                     img: [],  
  104.                     link: [],  
  105.                     fetch: [],  
  106.                     // 获取资源信息时当前时间  
  107.                     time: new Date().getTime(),  
  108.                 }  
  109.                 data.forEach(item => {  
  110.                     const arry = resource[item.initiatorType]  
  111.                     arry && arry.push({  
  112.                         // 资源的名称  
  113.                         name: item.name,  
  114.                         // 资源加载耗时 
  115.                         duration: item.duration.toFixed(2),  
  116.                         // 资源大小  
  117.                         size: item.transferSize,  
  118.                         // 资源所用协议  
  119.                         protocol: item.nextHopProtocol,  
  120.                     })  
  121.                 })  
  122.                 return resource  
  123.             }  
  124.             window.onload = () => {  
  125.                 // 在浏览器空闲时间获取性能及资源信息 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback  
  126.                 if (window.requestIdleCallback) {  
  127.                     window.requestIdleCallback(() => {  
  128.                         monitor.performance = getPerformance()  
  129.                         monitor.resources = getResources()  
  130.                         console.log('页面性能信息')  
  131.                         console.log(monitor.performance)  
  132.                         console.log('页面资源信息')  
  133.                         console.log(monitor.resources)  
  134.                     })  
  135.                 } else {  
  136.                     setTimeout(() => {  
  137.                         monitor.performance = getPerformance()  
  138.                         monitor.resources = getResources()  
  139.                         console.log('页面性能信息') 
  140.                         console.log(monitor.performance)  
  141.                         console.log('页面资源信息')  
  142.                         console.log(monitor.resources)  
  143.                     }, 0)  
  144.                 }  
  145.             }  
  146.             // 捕获资源加载失败错误 js css img...  
  147.             addEventListener('error', e => { 
  148.                 const target = e.target  
  149.                 if (target != window) {  
  150.                     monitor.errors.push({  
  151.                         type: target.localName,  
  152.                         url: target.src || target.href,  
  153.                         msg: (target.src || target.href) + ' is load error',  
  154.                         // 错误发生的时间  
  155.                         time: new Date().getTime(),  
  156.                     })  
  157.                     console.log('所有的错误信息')  
  158.                     console.log(monitor.errors)  
  159.                 }  
  160.             }, true)  
  161.             // 监听 js 错误  
  162.             window.onerror = function(msg, url, row, col, error) {  
  163.                 monitor.errors.push({  
  164.                     type: 'javascript', // 错误类型  
  165.                     row: row, // 发生错误时的代码行数 
  166.                      col: col, // 发生错误时的代码列数 
  167.                      msg: error && error.stack? error.stack : msg, // 错误信息  
  168.                     url: url, // 错误文件  
  169.                     time: new Date().getTime(), // 错误发生的时间  
  170.                 })  
  171.                 console.log('所有的错误信息')  
  172.                 console.log(monitor.errors)  
  173.             }  
  174.             // 监听 promise 错误 缺点是获取不到行数数据  
  175.             addEventListener('unhandledrejection', e => {  
  176.                 monitor.errors.push({  
  177.                     type: 'promise',  
  178.                     msg: (e.reason && e.reason.msg) || e.reason || '',  
  179.                     // 错误发生的时间  
  180.                     time: new Date().getTime(),  
  181.                 })  
  182.                 console.log('所有的错误信息')  
  183.                 console.log(monitor.errors)  
  184.             })  
  185.             return monitor  
  186.         } 
  187.         const monitor = monitorInit()  
  188.     </script>  
  189.     <link rel="stylesheet" href="test.css">  
  190.     <title>Document</title>  
  191. </head>  
  192. <body>  
  193.     <button class="btn1">错误测试按钮1</button>  
  194.     <button class="btn2">错误测试按钮2</button>  
  195.     <button class="btn3">错误测试按钮3</button>  
  196.     <img src="https://avatars3.githubusercontent.com/u/22117876?s=460&v=4" alt="">  
  197.     <img src="test.png" alt="">  
  198. <script src="192.168.10.15/test.js"></script>  
  199. <script>  
  200. document.querySelector('.btn1').onclick = () => {  
  201.     setTimeout(() => {  
  202.         console.log(button)  
  203.     }, 0)  
  204.  
  205. document.querySelector('.btn2').onclick = () => {  
  206.     new Promise((resolve, reject) => {  
  207.         reject({  
  208.             msg: 'test.js promise is error'  
  209.         })  
  210.     }) 
  211.   
  212. document.querySelector('.btn3').onclick = () => {  
  213.     throw ('这是一个手动扔出的错误')  
  214.  
  215. </script>  
  216. </body>  
  217. </html>  

 

责任编辑:庞桂玉 来源: 前端大全
相关推荐

2021-04-28 10:13:58

zookeeperZNode核心原理

2021-01-12 09:03:17

MySQL复制半同步

2010-07-13 09:36:25

2010-11-19 16:22:14

Oracle事务

2010-06-23 20:31:54

2009-08-25 16:27:10

Mscomm控件

2022-08-26 13:48:40

EPUBLinux

2020-09-21 09:53:04

FlexCSS开发

2020-07-20 06:35:55

BashLinux

2020-11-06 16:50:43

工具GitLab CICD

2011-07-18 15:08:34

2022-06-03 10:09:32

威胁检测软件

2010-11-15 11:40:44

Oracle表空间

2023-10-08 00:02:07

Java排序算法

2011-02-21 13:14:29

Qmail

2010-09-27 09:31:42

JVM内存结构

2013-04-16 10:20:21

云存储服务云存储SLA服务水平协议

2018-06-22 13:05:02

前端JavaScript引擎

2010-11-08 13:54:49

Sqlserver运行

2023-10-13 00:09:20

桶排序排序算法
点赞
收藏

51CTO技术栈公众号