前端实践问题
拖拽过程中,如果原本的元素消失了,onDrop还能触发么?具体表现是什么?
即使原始元素在拖拽过程中被移除了,只要拖拽操作未被中断,onDrop 事件仍然可以触发
-
拖拽数据独立存储
拖拽操作一旦开始(dragstart),浏览器会将拖拽数据存储在独立的DataTransfer对象中。即使原始元素被移除,已存储的拖拽数据依然有效。 -
视觉反馈与 DOM 解耦
拖拽过程中显示的「拖拽预览图像」是浏览器生成的副本,与原始元素的 DOM 状态无关。即使原始元素消失,预览图像通常仍会正常显示。
具体表现
1. onDrop 正常触发
-
如果拖拽操作成功完成(用户释放鼠标在有效的拖放目标上),
onDrop仍会触发。 -
可以通过
event.dataTransfer.getData()正常获取拖拽数据。 -
示例代码:
// 拖拽源元素 source.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', 'Hello World'); e.target.remove(); // 立即移除原始元素 }); // 放置目标 target.addEventListener('drop', (e) => { e.preventDefault(); console.log(e.dataTransfer.getData('text/plain')); // 输出 "Hello World" });
2. dragend 事件可能异常
-
如果原始元素被移除,其
dragend事件可能无法触发,或触发时因元素不存在而导致关联逻辑出错。 -
示例问题:
source.addEventListener('dragend', () => { console.log('拖拽结束'); // 可能不会执行 });
3. 意外中断拖拽操作
-
如果在
dragstart中直接移除元素,某些浏览器可能因渲染更新而意外终止拖拽操作,导致onDrop无法触发。 - 解决方案:使用异步移除(如
setTimeout)确保拖拽操作初始化完成:
source.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', 'Hello World');
setTimeout(() => e.target.remove(), 0); // 异步移除
});
总结
-
onDrop触发条件:仅取决于用户是否在有效的目标上释放鼠标,与原始元素是否存在无关。 -
风险点:
dragend事件可能不可靠,且直接移除元素可能导致浏览器行为不一致。 -
最佳实践:如需移除原始元素,建议在
dragend或drop事件中处理,而非dragstart。
如何改变拖拽预览图?如何让拖拽预览图有圆角?
一、核心方法:setDragImage()
通过 dragstart 事件的 dataTransfer.setDragImage() 方法,可直接指定拖拽预览图。
代码示例
element.addEventListener('dragstart', (e) => {
// 1. 创建一个自定义预览元素
const preview = document.createElement('div');
preview.textContent = "拖拽我";
preview.style.cssText = `
width: 100px;
height: 40px;
background: #2196F3;
color: white;
border-radius: 8px; // 关键:圆角样式
display: flex;
align-items: center;
justify-content: center;
`;
// 2. 将元素临时添加到 DOM 中(部分浏览器需要渲染才能捕获图像)
document.body.appendChild(preview);
// 3. 设置为拖拽预览图,并指定光标偏移位置(居中)
e.dataTransfer.setDragImage(preview, preview.offsetWidth / 2, preview.offsetHeight / 2);
// 4. 立即移除临时元素(可选)
setTimeout(() => document.body.removeChild(preview), 0);
});
二、实现圆角的注意事项
1. 必须确保元素已渲染
-
部分浏览器(如 Firefox)要求预览元素必须已插入 DOM 并完成渲染,否则无法生成图像。
-
解决方案:临时添加到 DOM 后立即移除。
2. 使用 Canvas 生成复杂预览
若需要更复杂的图形(如带阴影的圆角矩形),可通过 Canvas 生成图像:
const canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 40;
const ctx = canvas.getContext('2d');
// 绘制圆角矩形
ctx.beginPath();
ctx.roundRect(0, 0, 100, 40, 8); // 圆角半径 8px
ctx.fillStyle = '#2196F3';
ctx.fill();
// 设置为拖拽预览
e.dataTransfer.setDragImage(canvas, 50, 20); // 偏移到中心
三、浏览器兼容性
| 方法 | Chrome | Firefox | Safari |
|---|---|---|---|
setDragImage() |
✅ | ✅ | ✅ |
| Canvas 作为预览图 | ✅ | ✅ | ✅ |
未渲染元素的 setDragImage |
✅ | ❌ | ✅ |
四、完整示例(含圆角和动画)
<style>
.drag-item {
width: 120px;
padding: 12px;
background: #4CAF50;
color: white;
cursor: grab;
}
</style>
<div class="drag-item" draggable="true">拖拽我</div>
<script>
document.querySelector('.drag-item').addEventListener('dragstart', (e) => {
// 创建预览元素
const preview = document.createElement('div');
preview.textContent = "正在拖拽...";
preview.style.cssText = `
width: 120px;
padding: 12px;
background: #4CAF50;
color: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
opacity: 0.8;
`;
// 临时插入 DOM
document.body.appendChild(preview);
e.dataTransfer.setDragImage(preview, 60, 20); // 居中偏移
setTimeout(() => preview.remove(), 0);
});
</script>
五、高级技巧
1. 动态预览内容
preview.innerHTML = `
<div style="display: flex; align-items: center;">
<img src="icon.png" width="24">
<span>${e.target.textContent}</span>
</div>
`;
2. 隐藏默认预览
e.dataTransfer.setDragImage(new Image(), 0, 0); // 设置透明图像
通过 setDragImage() 结合 CSS 或 Canvas,可以完全控制拖拽预览图的样式,包括圆角、阴影等复杂效果。
如何让 echart 的内容跟随容器大小而变化?onResize 的时候要怎么做?如果有可伸缩侧边栏之类的,导致容器因为其他原因发生了改变,应该用什么事件监听?
核心方法:resize() + 尺寸监听
ECharts 实例通过 resize() 方法重新计算尺寸,需在容器尺寸变化时手动调用。
一、基础场景:窗口缩放
监听浏览器窗口的 resize 事件:
const chart = echarts.init(document.getElementById('chart-container'));
// 监听窗口变化
window.addEventListener('resize', () => {
chart.resize();
});
二、进阶场景:容器尺寸动态变化(如侧边栏伸缩)
1. 使用 ResizeObserver(推荐)
-
直接监听容器尺寸变化,无需依赖父级元素事件
-
代码示例:
const container = document.getElementById('chart-container'); const chart = echarts.init(container); const resizeObserver = new ResizeObserver(() => { chart.resize(); }); resizeObserver.observe(container); // 开始监听 // 组件卸载时销毁(重要!) // Vue: onBeforeUnmount(() => resizeObserver.disconnect()) // React: useEffect(() => () => resizeObserver.disconnect(), []) -
兼容性处理(旧版浏览器):
npm install resize-observer-polyfillimport ResizeObserver from 'resize-observer-polyfill';
2. 框架特定方案(如侧边栏回调)
-
若使用 Element UI/ Ant Design 等组件库,在侧边栏展开/折叠的回调中触发:
// Element UI 侧边栏折叠事件 <el-menu @collapse="handleCollapse"> → handleCollapse() { this.$nextTick(() => this.chart.resize()); }
三、性能优化
1. 防抖处理(高频变化场景)
let resizeTimer;
const resizeObserver = new ResizeObserver(() => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => chart.resize(), 100);
});
2. 容器动画同步
.chart-container {
transition: width 0.3s; /* 动画持续期间持续触发 ResizeObserver */
}
四、完整示例(Vue3 + TypeScript)
<template>
<div ref="chartContainer" class="chart"></div>
</template>
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue';
import * as echarts from 'echarts';
import ResizeObserver from 'resize-observer-polyfill';
const chartContainer = ref<HTMLElement>();
let chart: echarts.ECharts;
let resizeObserver: ResizeObserver;
onMounted(() => {
chart = echarts.init(chartContainer.value!);
chart.setOption({ /* 配置项 */ });
// 监听容器变化
resizeObserver = new ResizeObserver(() => chart.resize());
resizeObserver.observe(chartContainer.value!);
});
onBeforeUnmount(() => {
resizeObserver?.disconnect();
chart?.dispose();
});
</script>
五、常见问题排查
| 现象 | 解决方案 |
|---|---|
| 图表未响应侧边栏变化 | 确认监听的是图表容器而非父元素 |
| 出现空白间隙 | 检查容器 CSS 是否设置 width: 100% |
| 内存泄漏 | 确保组件卸载时调用 disconnect() |
通过 ResizeObserver + resize() 的组合,可精准响应任意原因导致的容器尺寸变化,无需依赖具体 UI 框架。
如果是 toc 产品,后端为 index.html 设置了 1 h 的 max age,请问从你重新构建代码发布,大概多久之后,所有用户都可以看到新界面?如果用户访问出现了白屏,是什么原因?
问题一:代码发布后用户看到新界面的时间
核心时间范围:最长 1 小时,但存在变量
-
理论最长时间
由于index.html设置了max-age=3600(1小时),未主动刷新页面的用户需等待缓存过期后才会获取新版本。-
用户首次访问时间点不同:若用户恰好在发布前 5 分钟访问过,需等待 55 分钟才能更新。
-
-
实际可能更短
-
强制刷新(Ctrl+F5):用户手动刷新会跳过缓存,立即获取新版本。
-
文件名哈希策略:若 JS/CSS 文件使用哈希指纹(如
app.a1b2c3.js),新版本会触发浏览器重新下载资源,即使index.html仍被缓存,也可能部分更新。 -
CDN 缓存行为:若 CDN 配置的缓存时间短于 1 小时,用户可能提前获取新版本。
-
问题二:用户访问白屏的可能原因
核心原因排查清单
| 原因分类 | 具体场景 | 解决方案 |
|---|---|---|
| 缓存冲突 | 旧版 index.html 引用了已被删除或重命名的资源文件(如未哈希的 JS/CSS) |
使用内容哈希文件名,确保新旧资源共存 |
| 资源加载失败 | 新版本资源未正确部署到服务器,导致 404 错误 | 检查构建产物是否完整上传,验证 CDN/服务器路径 |
| 代码执行错误 | 新版 JS 中存在语法错误或依赖兼容性问题(如浏览器不支持 ES6+ 语法) | 启用 Babel 转译,添加错误监控(如 Sentry)捕获运行时错误 |
| 路由配置问题 | 单页应用(SPA)路由未正确配置,导致空白路由匹配 | 检查路由兜底设置(如 404 重定向到首页) |
| Service Worker | 旧版 Service Worker 强制缓存了过时资源 | 更新 Service Worker 版本并触发立即激活(如 self.skipWaiting()) |
最佳实践建议
-
缓存策略优化
-
对
index.html设置较短缓存(如max-age=300),或使用no-cache配合 ETag 验证。 -
静态资源(JS/CSS/图片):设置长期缓存(如
max-age=31536000)并添加哈希指纹。
-
-
部署流程增强
# 示例:使用 Webpack 生成带哈希的文件名 output: { filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].chunk.js' }-
先上传新资源,再更新
index.html,避免出现中间状态。
-
-
监控与回滚
-
部署后实时监控错误率(如通过 APM 工具)。
-
准备快速回滚方案(如保留前一次构建产物)。
-
-
用户提示机制
// 检测版本更新并提示用户刷新 if (navigator.serviceWorker) { navigator.serviceWorker.addEventListener('controllerchange', () => { window.location.reload(); }); }
示例:强制缓存失效方案
通过修改 index.html 的 URL 路径(如添加版本号)绕过缓存:
# Nginx 配置示例
location / {
if ($request_uri ~* "index\.html$") {
add_header Cache-Control "no-cache, must-revalidate";
}
}
或使用查询参数(不推荐,部分 CDN 会忽略):
<script src="/app.js?v=20231001"></script>
toc 产品大量使用 cdn,请问 cdn 的定价大概多少?针对这样的定价策略,前端应该进行什么样的优化?前端应用中?上传贵还是下载贵?上传快还是下载快?
一、CDN 定价模型(以主流厂商为例)
CDN 的定价通常由 流量、请求次数、存储 和 增值功能 四部分构成,以下为典型价格范围(以中国大陆和全球主流 CDN 服务商为例):
| 计费项 | 价格范围 | 示例场景 |
|---|---|---|
| 流量费用 | - 中国大陆: 0.1~0.3 元/GB - 海外: 0.05~~~~~~~~~~~~~~~~~~~0.2 元/GB |
用户下载 1GB JS/CSS/图片资源,约消耗 0.2 元(按国内均价) |
| HTTP 请求次数 | - 0.01~0.1 元/万次 | 100 万次图片请求 ≈ 5~10 元 |
| 存储费用 | - 0.1~0.3 元/GB/月 | 存储 100GB 静态资源 ≈ 10~30 元/月 |
| 增值服务 | - DDoS 防护: 100~500 元/月 - 全站加速(动态请求): 0.3~~~~~~~~~~~~~~~~~~~~~~1 元/GB |
动态 API 加速 100GB ≈ 30~100 元 |
主流厂商参考:
-
阿里云/腾讯云:流量阶梯计价(用量越大单价越低)
-
Cloudflare:免费套餐含基础防护,Pro 套餐 20 美元/月(不限流量)
-
AWS CloudFront:按区域定价(北美 0.085 美元/GB,亚洲 0.14 美元/GB)
二、前端优化策略(降低成本 + 提升性能)
针对 CDN 定价模型,前端需重点优化 流量消耗 和 请求次数:
1. 减少流量消耗
| 方法 | 效果 | 实施示例 |
|---|---|---|
| 资源压缩 | 减少 50%~70% 体积 | 使用 Brotli(最高压缩率)替代 Gzip,Webpack 配置 compression-webpack-plugin |
| 图片优化 | WebP 比 JPEG 节省 30%+ | <picture> 标签兼容性兜底:<source srcset="img.webp" type="image/webp"> |
| 代码拆分(Code Splitting) | 按需加载减少初始下载量 | Vue/React 使用动态导入:() => import('./Component') |
| Tree Shaking | 移除未使用代码 | Webpack/Rollup 默认支持,确保 sideEffects: false |
2. 降低请求次数
| 方法 | 效果 | 实施示例 |
|---|---|---|
| HTTP/2 多路复用 | 单连接并行传输,减少队头阻塞 | Nginx 配置 listen 443 http2 |
| 资源合并 | 减少小文件请求 | 合并图标为 SVG Sprite(如 icon-sprite.svg#home) |
| 合理缓存策略 | 减少重复请求 | 静态资源设置 Cache-Control: public, max-age=31536000, immutable |
| CDN 边缘计算 | 请求在边缘节点处理(如压缩、重定向) | Cloudflare Workers 实现 A/B 测试分流 |
3. 监控与分析
| 工具 | 用途 |
|---|---|
| Google Lighthouse | 性能评分 + 优化建议 |
| CDN 自带监控 | 分析流量热点、异常请求(如 404 资源) |
| Sentry | 捕获前端错误,定位资源加载失败问题 |
三、上传 vs 下载:成本与速度对比
1. 成本对比
| 方向 | 定价逻辑 | 成本 |
|---|---|---|
| 上传(写入CDN) | 通常免费或极低费用(厂商希望吸引内容注入) | 低(接近0) |
| 下载(用户访问) | 主要收费项(占 CDN 厂商带宽成本的核心) | 高(主要支出) |
2. 速度对比
| 方向 | 速度表现 |
|---|---|
| 上传 | 依赖本地网络上行带宽(家用宽带通常 10~50Mbps),但可通过 分片上传 或 CDN 就近接入点 优化 |
| 下载 | 通过 CDN 全球加速节点,用户从最近节点获取资源,速度更快(通常 100Mbps+) |
四、完整优化案例
场景:一个全球化的电商网站,图片资源占比 70%,JS/CSS 资源频繁更新。
优化步骤:
图片处理:
-
使用
sharp库批量转换为 WebP 格式 -
响应式图片:
srcset按设备分辨率分发
<img src="product.jpg"
srcset="product-400.webp 400w, product-800.webp 800w"
sizes="(max-width: 600px) 400px, 800px">
-
代码优化:
-
启用 Brotli 压缩(Nginx 配置
brotli on;) -
非核心代码动态加载:
// 点击时加载支付模块 button.addEventListener('click', () => { import('./payment.js').then(module => module.process()); });
-
-
缓存策略:
-
静态资源:
Cache-Control: public, max-age=31536000, immutable -
HTML 文件:
Cache-Control: no-cache(通过 ETag 验证更新)
-
五、成本估算(示例)
假设某应用月消耗:
-
流量:100TB(下载)
-
请求次数:10 亿次
-
存储:1TB
费用计算(以阿里云为例):
-
流量费:100,000 GB × 0.18 元/GB ≈ 18,000 元
-
请求费:10 亿次 ÷ 10,000 × 0.06 元 ≈ 6,000 元
-
存储费:1TB × 0.15 元/GB ≈ 150 元
-
总成本 ≈ 24,150 元/月
优化后(预计节省 40%+):
-
WebP 图片减少 30% 流量 → 流量费降至 12,600 元
-
Brotli 压缩减少 20% 请求量 → 请求费降至 4,800 元
-
总成本 ≈ 17,550 元/月
通过前端优化可直接降低 CDN 的核心成本项(流量和请求),同时提升用户体验,形成双赢局面。
图片设置协商缓存后,浏览器会整体缓存,视频能设置协商缓存么?视频的http返回内容与图片有什么区别?如何降低视频展示的成本?
一、视频能否设置协商缓存?
可以,视频与图片的缓存机制原理相同,均可通过 HTTP 缓存头实现协商缓存。
配置示例(Nginx):
location /videos/ {
# 设置强缓存(可选)
add_header Cache-Control "public, max-age=3600";
# 协商缓存:通过 Last-Modified/ETag 验证
if_modified_since before;
etag on;
}
协商缓存流程:
-
首次请求:返回
200 OK+Last-Modified/ETag -
再次请求:浏览器携带
If-Modified-Since或If-None-Match -
资源未修改:服务器返回
304 Not Modified,浏览器使用本地缓存
二、视频与图片的 HTTP 响应差异
核心区别点:
| 特征 | 图片(如 JPEG) | 视频(如 MP4) |
|---|---|---|
| 状态码 | 通常 200 OK |
可能 206 Partial Content(范围请求) |
| Content-Type | image/jpeg |
video/mp4 |
| Accept-Ranges | 通常无(小文件不分片) | bytes(支持分段加载) |
| Content-Range | 无 | bytes 0-999/2000(指示当前传输的字节范围) |
| 缓存行为 | 完整缓存 | 可能分片缓存(取决于是否启用范围请求) |
示例视频请求响应头:
HTTP/1.1 206 Partial Content
Content-Type: video/mp4
Accept-Ranges: bytes
Content-Range: bytes 0-1048575/5242880
Content-Length: 1048576
Cache-Control: public, max-age=3600
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
三、降低视频展示成本的几种策略
1. 启用 HTTP 范围请求(Range Requests)
-
原理:允许用户仅加载观看的部分视频片段,减少无效流量消耗。
-
实现:确保服务器支持
Accept-Ranges: bytes(Nginx 默认开启)。
2. 转码为高效编码格式
-
推荐格式:H.265(HEVC)比 H.264 节省 50% 带宽,AV1 更优但兼容性差。
-
工具示例:
ffmpeg -i input.mp4 -c:v libx265 -crf 28 output-hevc.mp4
3. 自适应码率(ABR)
-
技术方案:使用 HLS 或 DASH 协议,根据网络状况动态切换分辨率。
-
实现步骤:
-
将视频切片(如 10s/段)并生成多码率版本
-
通过
.m3u8(HLS)或.mpd(DASH)描述文件管理播放
-
4. CDN 分层存储
-
策略:
-
热数据:使用 SSD 边缘节点加速
-
冷数据:存储到低价对象存储(如 AWS S3 Glacier)
-
ts 开发下,interface和对象类型声明可不可以用来声明数组和函数?如果声明函数,函数名可不可以重复?函数名如果重复,意味着什么?
一、TypeScript 中 interface 和类型声明的能力
1. 声明数组
-
interface方式:interface NumberArray { [index: number]: number; // 索引签名定义数组 } const arr: NumberArray = [1, 2, 3]; -
类型别名方式:
type StringArray = string[]; const arr: StringArray = ["a", "b"];
2. 声明函数
-
interface定义函数类型:interface SearchFunc { (source: string, keyword: string): boolean; } const mySearch: SearchFunc = (src, kw) => src.includes(kw); -
类型别名定义函数:
type AddFunction = (a: number, b: number) => number; const add: AddFunction = (x, y) => x + y;
二、函数名重复问题
1. 函数重载(允许同名)
TypeScript 支持函数重载,但需满足以下条件:
-
多个签名声明 + 一个实现:
// 重载签名 function greet(name: string): string; function greet(age: number): string; // 实现签名(需兼容所有重载) function greet(input: string | number): string { return typeof input === "string" ? `Hello, ${input}` : `You are ${input} years old`; } -
输出表现:
greet("Alice"); // OK: "Hello, Alice" greet(25); // OK: "You are 25 years old" greet(true); // Error: 没有匹配的重载
2. 重复函数名的本质
-
编译后结果:JavaScript 没有重载机制,最终只会保留一个函数实现。
-
类型安全:重载仅在类型检查阶段生效,确保调用时参数合法。

浙公网安备 33010602011771号