如果只是想单纯地测试本机的网络速度,那么可以在线测试www.speedtest.cn/,或者很多桌面工具也都有提供测速的工具。
本文所谓的测网速,是测试你的网站服务器的网速,而不是你本机的网速~
原理解析
测网速的原理其实很简单:
- 在服务器上准备一些文件供下载用;
- 多次测试下载这些文件用了多长时间;
- 然后用时间和下载的文件大小计算一下,即可得出平均网速;
示例如下:
代码实操
了解了原理,那么我们就可以写一个测网速的组件了。
我们要准备两点:
- 在服务器上准备一个文件,并记录文件大小size(一般几M大小即可)
- 实现代码
我准备的文件配置如下
const TEST_FILE = {
url:'https://www.xxx.com/20240412/10/34/4/0dedaa5dbc6c.mp4?v=1',
size: 2680833 // 约等于2.57M大小
}
效果图如下:
整个组件的代码如下,因为是8k大屏弹框组件,所以文字大小、长宽等都很大,这里不做整理了,仅提供逻辑思路。
注意点:测试下载的文件,一定要加唯一标识的后缀,否则后面可能会从浏览器缓存取,导致测速不准确;
牛马上班抽空写文章、没时间整理了,请见谅。还有冗余的代码,都是从其他同事的组件里复制的
<template>
<div class="layer" v-if="isShowLockBox == 1">
<div class="dialog" v-if="isShowLockBox == 1">
<div class="title">测试网速</div>
<div class="fl-right" @click="isShowLockBox = 0;averageSpeed = 0">
<img class="fl-img" src="@/assets/images/home/crossOff.png"/>
<img class="fl-img-active" src="@/assets/images/home/crossOffActive.png"/>
</div>
<div class="speed-test">
<div class="controls">
<button @click="startTest" :disabled="isTesting" style="font-size: 70px;">
{{ isTesting ? '测试中...' : '开始测速' }}
</button>
<button @click="cancelTest" v-if="isTesting" style="font-size: 70px;">取消</button>
</div>
<div class="progress" v-if="isTesting">
<div class="progress-bar" :style="{ width: progress + '%' }"></div>
</div>
<div class="results">
<div class="speed-display">
<span class="value">{{ averageSpeed }}</span>
<span class="unit">M/s</span>
</div>
</div>
<div class="chart" v-if="speedHistory.length > 0">
<div
v-for="(speed, index) in speedHistory"
:key="index"
class="chart-bar"
:style="{ height: Math.min(speed * 5, 100) + '%' }"
></div>
</div>
</div>
</div>
</div>
<div class="lock-btns" title="测网速">
<img :src="$loadLocalImg('lock/netSpeed.png')" class="lock-icon" alt="测试网速" @click="handleLockOpen">
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 是否显示遮罩层的输入框,默认不显示
const isShowLockBox = ref(0)
/** 解锁,仅显示输入框 */
const handleLockOpen = () => {
isShowLockBox.value = 1
}
// 测试文件配置
const TEST_FILE = {
url: 'https://www.xxx.com/group1/default/20240412/10/34/4/0da5dbc6c.mp4?v=1',
size: 2680833 // 500KB
}
const isTesting = ref(false)
const progress = ref(0)
const speedHistory = ref([])
const controller = ref(null)
const testCount = ref(0)
const totalTests = 10
// 计算平均速度
const averageSpeed = computed(() => {
if (speedHistory.value.length === 0) return '0.00'
const sum = speedHistory.value.reduce((a, b) => a + b, 0)
return (sum / speedHistory.value.length).toFixed(2)
})
// 开始测试
const startTest = async () => {
resetState()
isTesting.value = true
controller.value = new AbortController()
try {
for (let i = 0; i < totalTests; i++) {
await measureSpeed()
testCount.value = i + 1
progress.value = ((i + 1) / totalTests) * 100
}
} finally {
isTesting.value = false
}
}
// 核心测速方法
const measureSpeed = async () => {
let loadedBytes = 0
const startTime = performance.now()
const randomParam = Math.random()
const urlWithRandom = `${TEST_FILE.url}&r=${randomParam}`
try {
const response = await fetch(urlWithRandom, {
signal: controller.value.signal
})
const reader = response.body.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
loadedBytes += value?.length || 0
}
const duration = (performance.now() - startTime) / 1000 // 秒
const speedMbps = (loadedBytes * 8) / duration / 1e6 // 转换为Mbps
speedHistory.value.push(speedMbps)
} catch (error) {
if (error.name !== 'AbortError') {
console.error('测速失败:', error)
}
}
}
// 取消测试
const cancelTest = () => {
controller.value?.abort()
isTesting.value = false
}
// 重置状态
const resetState = () => {
progress.value = 0
speedHistory.value = []
testCount.value = 0
}
</script>
<style lang="scss" scoped>
.speed-test {
width: 1200px;
height: 640px;
margin: 2rem auto;
padding: 20px;
background: rgba(11, 20, 39, 0.8);
border-radius: 12px;
}
.controls {
margin-bottom: 1rem;
}
button {
padding: 8px 16px;
margin-right: 10px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background: #6c757d;
}
.progress {
height: 20px;
background: #ddd;
border-radius: 5px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: #28a745;
transition: width 0.3s ease;
}
.speed-display {
text-align: center;
margin: 2rem 0;
}
.value {
font-size: 8rem;
font-weight: bold;
color: #fff;
}
.unit {
font-size: 4.2rem;
color: #fff;
}
.chart {
display: flex;
height: 150px;
align-items: flex-end;
gap: 2px;
margin-top: 1rem;
}
.chart-bar {
flex: 1;
background: #007bff;
transition: height 0.5s ease;
}
.layer {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 9999;
background-color: rgba(0, 0, 0, 0.05);
display: flex;
justify-content: center;
align-items: center;
pointer-events: auto;
}
.dialog {
position: relative;
width: 1372px;
border-radius: 20px;
text-align: center;
color: #ffffff;
background-color: #020A1A;
.title {
font-size: 80px;
background: #134392;
height: 150px;
line-height: 150px;
}
.input-box {
width: 1202px;
height: 117px;
margin: 123px auto 0;
:deep(.el-input__wrapper) {
padding: 0;
border-radius: 20px;
overflow: hidden;
}
:deep(.el-input__inner) {
height: 118px;
line-height: 118px;
font-size: 64px;
border: 4px solid #ffffff;
border-radius: 20px;
padding-left: 40px;
background-color: #000000;
color: #ffffff;
& > ::placeholder {
color: rgba(255, 255, 255, 0.5)
}
}
}
.btns {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 1202px;
margin: 200px auto 85px;
.lock-btn {
width: 518px;
height: 113px;
line-height: 113px;
border-radius: 20px;
text-align: center;
background: rgba(255,255,255,0.2);
font-size: 44px;
color: #ffffff;
cursor: pointer;
}
& > div:last-child {
background-color: #0067E6;
}
}
}
.lock-btns {
position: fixed;
bottom: 25px;
right: 1400px;
z-index: 10000;
}
.lock-icon {
width: 130px;
cursor:pointer;
pointer-events: auto;
}
.fl-right {
pointer-events: auto;
cursor: pointer;
position: absolute;
top: 40px;
right: 40px;
&:hover {
img {
&.fl-img {
display: none;
}
&.fl-img-active {
display: initial;
}
}
}
img {
width: 57px;
height: 57px;
&.fl-img-active {
display: none;
}
}
}
</style>