产品笔记¶
统计信息:字数 24724 阅读50分钟
原始笔记链接:https://cloud.seatable.cn/dtable/external-links/59b453a8639945478de2/
0187 表单长度设计原则¶
如果是一个 toC 的产品,设计一个调查问卷或者表单,表单项最好不要太长。如果一个表单需要用户填写消耗5分钟以上,大部分用户都会没有耐心。如果是必填的项,那么用户可能随便填写,这样调查就失去了正确性。
所以需要根据用户的水平,确定表单的长度和深度,尽量让用户做选择题,而不是让用户做填空题,而且这样更便于用户填写,便于最后数据分析整理等。
如果表单确实很长,那么可以尝试分页显示,然后顶部显示进度条,表示用户填写了多少表单项。
界面加载过程类似:尽量减少加载时间,时间小于3S,界面显示 loading 图标。加载时间超过 5秒,最好显示进度条。如果已经确定加载的框架,只是需要后台返回数据,那么可以先显示骨架屏。
具体参考:
https://developers.weixin.qq.com/miniprogram/dev/devtools/skeleton.html
0342 简单设计一个后台管理系统¶
如何做后台管理系统?
可以参考实际项目和小说项目进行说明
功能层面¶
- 用户管理:用户的增删改查
- 群组管理:某个群组对应的用户和权限(ToB 和 C的区分)
- 权限管理:权限增删改查
- 通知管理:新增全局通知,删除全局通知
- 产品管理(表格,小说):具体说某个产品的内容管理,数据变化趋势(图表的库等),表格和折线图等都可使用
- 投诉管理:处理用户投诉的表格和信息
后台管理系统的交互细节可以不需要很细,主要是展示数据的整理变化
API 层面¶
后台管理相关 API 通常单独写,加一个 system-admin 前缀,用于和普通 API 的区分
每次进入后台界面,需要单独验证管理员信息,这里设置的 token 过期时间比较短,可能几个小时。相对于应用网页 token 过期时间可能比较久(7天或者30天等)
0348 文件断点续传¶
主要逻辑:文件切片——上传切片——合并切片——验证切片。
前端浏览器读取文件后,把文件切片后,每一个片段分片上传(滑动窗口算法),然后后端接收到切片后,返回当前已经上传的文件大小,然后把切片放在硬盘的临时目录下。后端检测全部上传后,拼接成完整的大文件,然后把文件路径和成功信息返回给前端。
特殊处理:如果某个切片没有上传到(丢包),那么前端需要循环上传没有成功上传的切片。如果中途网络中断,那么就暂停上传进程。等网络恢复后,再次上传,就实现了断点续传。
适应于大于10M小于1G的文件。超过1G的文件,可能浏览器无法读取到内存中。
后端 Java 实现:https://juejin.cn/post/7266265543412351030
后端 express 实现:https://juejin.cn/post/7233613362888966205
0357 输入框插入图片¶
Table 中的做法是:
创建一个 div 富文本编辑器,然后插入对应的图片节点,协作人节点。
然后删除时,可以删除对应的节点。这样也支持复制粘贴图片上传,后续可以写个技术文档。content=editable
主要步骤:编辑HTML、提交(转义)、发送到服务器、请求返回本地、渲染只读的HTML
编辑内容如下
test <img src=\"https://2024-02/bird.jpeg\" height=\"60\"> @foo
渲染评论
dangerouslySetInnerHTML={{ __html: commentContent }}
"rest @foo <img src=\"https://test.png\" height=\"60\"> ",
0421 文档树¶
需求:一个文档树,前端如何维护和更新?
阶段1:静态文档树渲染
-
从后端获取到文档结构(字符串),使用 JSON.stringify 方法转换成对象,进一步转换成有效的树结构(如果后端存储的是树结构,可以直接使用;如果候选存储的是数组,需要把数组每一项遍历,前端建立树结构)
-
把 JS 树渲染成 JSX 树结构(对应有 Folder 对象,File 对象,界面上不同层级渲染不同的树节点)
-
文件夹支持展开和折叠
阶段2:支持文档树编辑
-
增加:在某个文件夹节点,下面新增一个文件夹或者文件节点
-
删除:删除某个文件夹或文件夹点(同时删除内部的子树)
-
重命名:更改某个文件或者文件夹属性
-
复制:把一个子树节点,复制到另一个节点(注意需要深拷贝树节点)
-
保存:把树结构转换成字符串,发送到服务器
阶段3:支持复杂操作和边界情况
-
通过菜单移动:把一个子树节点,移动到另一个子树节点
-
通过拖拽移动
-
采用 react-dnd 库,设置全部的节点可以拖动,拖动时包括内部节点;
-
设置全部的文件夹可以被释放,根据释放的位置,三种情况:
-
文件夹上面10px 就是移动到文件夹的前一个节点(渲染释放线)
-
文件夹下面10px 就是移动的文件夹的后一个节点(渲染释放线)
-
文件夹中间,移动到文件夹内部(背景变色)
-
-
设置全部的文件可以被释放,两种情况,分别是前一个位置和后一个位置(渲染释放线)
-
支持撤销(每一个操作记录到数组中,当撤销时,使用 revert 函数获取对应的反向操作)
-
支持本地文件拖动到浏览器树结构(把本地文件上传到服务器,服务器返回路径,然后插入到树中)
-
判断最大树嵌套深度(避免树深度过深,或者某个文件夹内部太多文件,造成性能问题)
0422 图片懒加载¶
为什么要做图片懒加载?如果一个网页有很多图片,初始网页就加载全部的图片,下载图片比较慢,给服务器带来很多压力。解决的办法就是界面滚动到某个位置后,然后再加载这个位置的图片。适应于内容很多的长网页的实现,例如画廊等。
技术实现:
1、默认把 img 标签的 src 设置一个空,或者是 loading 动画,这样可以从缓存中获取动画。
<div className="container">
<img src="./loading.gif" alt="" data-src="http://www.baidu.com/logo.png" />
<img src="./loading.gif" alt="" data-src="http://www.baidu.com/logo.png" />
<img src="./loading.gif" alt="" data-src="http://www.sina.com/logo.png" />
<img src="./loading.gif" alt="" data-src="http://www.taobao.com/logo.png" />
</div>
2、计算当前图片是否在网页视口中,也就是计算图片距离网页顶部的位置,和当前滚动位置和屏幕高度。
componentDidMount() {
this.checkLoad();
}
onScroll = () => {
this.checkLoad();
}
3、监听界面垂直滚动事件(节流监听),当某个图片开始进入网页内部(或者即将进入网页内部),那么设置成真实的 src,向服务器发出请求,界面显示图片,就实现了图片的懒加载。伪代码如下:
function checkLoad() {
// 检测当前图片是否在视口内部
// 图片距离顶部的距离 < 网页滚动距离 + 屏幕高度
const isLoad = imageOffsetTop < containerSrollTop + window.innerHeight
if (isLoad) {
img.src = img.getArrtibute('data-src');
}
}
0432 鼠标经过切换图片¶
需求:鼠标经过,切换到另一张图片(犀牛书 21 章第一节的案例,JavaScript权威指南)
原理:把一个图片地址和另一个图片的地址设置到节点中,然后鼠标经过,获取 data 属性然后改变 src 属性,这样实现了图片切换。为了避免缓存,可以先 JS 创建图片,然后直接从缓存中读取图片。
HTML
<img src="https://michael18811380328.github.io/background/pub_31.jpg" data-rollover="https://michael18811380328.github.io/background/pub_32.jpg"/>
JS
window.onload = (function() {
for (let i = 0; i < document.images.length; i++) {
let img = document.images[i];
let rollover = img.getAttribute('data-rollover');
// 忽略没有 rollover 属性的图片
if (!rollover) continue;
// 缓存图片
(new Image()).src = rollover;
img.setAttribute('data-rollout', img.src);
img.onmouseenter = function() {
this.src = this.getAttribute('data-rollover');
}
img.onmouseleave = function() {
this.src = this.getAttribute('data-rollout');
}
}
});
本地浏览器调试时,应该设置浏览器允许缓存,这样首次加载全部图片后,然后鼠标进入滑出就不会再次从服务器获取图片了。
上面是原生 JS 的实现,如果是 react 在不同的 image 上绑定 mousenter - mouseleave 事件即可
0434 mp4 视频转换成 m3u8 格式展示¶
参考:https://blog.csdn.net/weixin_41697143/article/details/139750963
0695 图片9图切换效果实现¶
需求:产品需要这个效果,具体怎么实现?
从数据层面和交互层面分析
数据分析¶
1、数组获取:文件树结构,转换成数组
[{ name: xxx, url: xxx, time: xxx }]
2、数据过滤:右上角有过滤和排序,就是自定义 filter 和 sort 函数即可
3、时间实现:循环数组,如果后一个和前一个时间隔天,那么后一个就显示天数
4、动态拖动算法实现:当拖动浏览器或者其他元素,造成当前容器宽度变化时,重新获取容器宽度,按照每行显示N个,计算当前每一个元素的宽度即可:获取屏幕的总宽度(可能动态值)、设置每一行显示的个数范围,例如 4-10个、然后滑块调整时,给出一个百分比的值;实际显示的个数,例如滑动到一半,就是 Math.floor(4 + (10 - 4) * 0.5) = 7
交互展示¶
可以使用百分比处理,每一个宽度是 100% / 7
或者使用 grid 进行布局,每一行宽度是7个(因为大小都相等)
性能问题¶
如果拖动到底部,然后调整过滤筛选等,那么让页面滚动位置是0
如果图片特别多(10000),那么就使用虚拟列表+图片缩略图+图片懒加载实现,避免一次性渲染太多图片造成卡顿
虚拟列表:前面9000个显示成一个 div,后面1000个显示成 img 数组,避免 doms 节点过多
图片缩略图:避免使用原图,减少网络负担
图片懒加载:滚动到某个图片时,再替换 img 的 src 属性进行加载
0475 一个左侧序号列冻结算法¶
算法:一个左侧序号列冻结算法
需求描述:一个二维表格,上下滚动时,顶部的表头是固定的(已经实现);左右滚动时,需要首列固定。
表格结构:外部一个 div container,然后渲染每一行 div,每一行渲染多个单元格 div 水平排列。
基本思路:
主要处理左侧的冻结的列。
默认状态下:设置第一列是 position: absolute,marginleft 或者 left 是0; 然后这个位置会塌陷,后面使用一个相同的 DIV 占个位置。
上下滚动时,不变化。
左右滚动时:设置第一列是 position: fixed 固定定位,然后设置 marginLeft 是固定的0即可。需要处理表头的 z-index 确保左右滚动时,第一列不会显示到表格外部。
注意顶部表头行的样式(表头滚动,那么全部需要滚动,表内容滚动,也是全部需要滚动,通过 ref 直接更改 style)
0481 如何禁止用户打开浏览器¶
很多网站为了禁止用户打开控制台,通常使用定时器,循环定时检测用户是否打开控制台(例如文心一言)
那么在网页加载完成后,清空全部定时器即可
参考:https://www.zhihu.com/question/597286223
// 原理
window.onload = function () {
// 清理所有的定时器
if (location.host == "yiyan.baidu.com") {
// 定时器新建过程中是自增长的,那么这里新建一个定时器,index 默认最大,然后循环清空已有定时器即可
let endTid = setTimeout(function () {});
for (let i = 0; i <= endTid; i++) {
clearTimeout(i);
clearInterval(i);
}
}
}
原始版本
setInterval(function() {
check()
}, 4000);
var check = function() {
function doCheck(a) {
if (("" + a/a)["length"] !== 1 || a % 20 === 0) {
(function() {}
["constructor"]("debugger")())
} else {
(function() {}
["constructor"]("debugger")())
}
doCheck(++a)
}
try {
doCheck(0)
} catch (err) {}
};
check();
简化版
猜测是在一瞬间瞬间让你的函数调用超出最大限制, 然后导致控制报错,
此时这个函数下的就存在了某个可以让你进行 debugger 操作的函数, 有大佬可以解释下吗?
本意是为了方便开发者调试,结果变成了拦截开发者
const check = () => {
const doCheck = (a) => {
(function() {}["constructor"]("debugger")());
// 立即执行匿名函数 调用 constructor函数传入 字符串 "debugger"
doCheck(++a); // 递归调用
};
try {
doCheck(0);
} catch (err) {
console.log("err", err); // 超出最大调用限制 Maximum call stack size exceeded
}
};
check();
// 每隔4秒检测一次
setInterval(check, 4000);
0660 如何实现网页中拖动选择多个内容¶
问题:类似 mac 中选择文件,网页中如何拖动选择多个文件?
解决:¶
处理拖动和灰色蒙层¶
1、监听鼠标点击事件,获取位置 x1, y1
startPoint: { x1: 0, y1: 0 };
endPoint: { x2: 0, y2: 0 };
2、当鼠标移动时,每次移动到的位置是 x2 y2,此时 x1 x2 y1 y2 四个点渲染成一个方形的图层,然后设置边框和背景色,和透明度。
renderSelectionBox = () => {
const { startPoint, endPoint } = this.state;
if (!this.state.isSelecting) return null;
const left = Math.min(startPoint.x, endPoint.x);
const top = Math.min(startPoint.y, endPoint.y);
const width = Math.abs(startPoint.x - endPoint.x);
const height = Math.abs(startPoint.y - endPoint.y);
return (
<div
className="selection-box"
style={{ left, top, width, height }}
/>
);
};
3、当鼠标抬起,x1 x2 y1 y2 清空,灰色蒙层去掉。
处理选择文件¶
如果文件绝对定位,那么比较好处理,直接判断文件的坐标 x3x4y3y4 和蒙层是否有交集。如果有交集,那么就是选中。如果没有交集,那么就是没有选中。选中的文件,增加灰色背景,文字背景变成蓝色。
如果文件不是绝对定位,那就比较麻烦。这里假设文件之间没有空位,是按照行列充满进行排列的,那么可以计算出每一行每一列的文件位置。
鼠标上一个的位置 x0, y1 鼠标当前的位置是 x2 y2,x2-x1, y2-y1 那么可以计算出当前鼠标拖动的方向,向左上方,右上方,左下方,右下方四个方向拖动。然后当鼠标进入一个文件,那么就把对应初始点和当前点的位置的文件选中。当鼠标离开一个文件,如果文件不在选区内部,那么这一行和这一列文件就不选中。具体状态可以维护在上层组件,或者组件之间互相通信。
当文件不是规范图形,那么就是两个图,是否存在交集的情况,就是求两个函数是否有交点的问题了,就转化成数学问题。
如果文件不是绝对定位,然后可以通过 DOM 算出每一个文件所在的位置。然后和蒙层求交集即可。
const selectionRect = {
left: Math.min(startPoint.x, endPoint.x),
top: Math.min(startPoint.y, endPoint.y),
right: Math.max(startPoint.x, endPoint.x),
bottom: Math.max(startPoint.y, endPoint.y),
};
const items = container.querySelectorAll('.file-item');
items.forEach(item => {
const bounds = item.getBoundingClientRect();
const relativeBounds = {
left: bounds.left - container.getBoundingClientRect().left,
top: bounds.top - container.getBoundingClientRect().top,
right: bounds.right - container.getBoundingClientRect().left,
bottom: bounds.bottom - container.getBoundingClientRect().top,
};
// Check if the element is within the selection box's bounds
if (relativeBounds.left < selectionRect.right && relativeBounds.right > selectionRect.left &&
relativeBounds.top < selectionRect.bottom && relativeBounds.bottom > selectionRect.top) {
newSelectedItemsList.push(item);
}
});
进阶:如果网页存在滚动条,如何处理拖动到边界的情况?¶
如果文件很多,向下拖动到边界,网页发生滚动,那么此时主要计算 Y 轴方向的变化。整体蒙层的尺寸变化,选择文件也需要进行变化。(一般不会同时出现四边都滚动的情况)
进阶:如果拖动超出了网页,交互如何实现?¶
当前拖动范围,存在一个容器。鼠标拖动时,监听 mouseLeave 事件,触发后,关闭蒙层,已选中的文件继续保留,这就可以实现拖出网页的特殊情况。
0682 树状结构左侧加竖线实现¶
在一个文件树左侧,需要加一个竖线,类似 github 的效果,这个怎么实现?
产品需求描述:
1、鼠标经过这个树形结构,显示左侧竖线;鼠标移除后不显示竖线
2、竖线和上一级目录的三角形对齐
3、竖线和背景灰色不冲突,竖线浮在背景灰色上面
实现:文件树已经实现,现在只处理竖线。
文件结构是树形结构,那么可以先遍历树节点,然后传递一个变量 offsets = [],每次递归子节点,都增加一个缩进值。例如 [20, 40, 60, 80, 100]。这里的值是缩进值。
在叶子节点中,渲染左侧的N个边框
{offsets.map(offset => {
return (
<div style={{
// height 是每一项的高度
position: 'absolute', border: '1px solid #ccc', left: offset, height: 28, top: 0,
}}>
);
})}
然后进行微调,即可实现左侧的竖线效果。
0442 文件上传的几种情况¶
普通文件上传¶
function fn(files) {
if (files.length) {
// 优化:如果上传多文件,可以使用循环上传(上传多文件 input multiple 有一部分浏览器不支持,移动端和打开的APP有关)
for (let file of files) {
let reader = new FileReader();
// 不同类型的文件,使用不同的编码上传(readAsText, readAsDataURL)
// 通常根据文件名后缀判断文件类型,更严格的方法是根据文件开头的编码判断(文件后缀和真实文件类型可能不一样)
if (/.txt/.test(file.type)) {
// txt file
reader.onload = function() {
console.log(this.result);
}
reader.readAsText(file);
}
else if (/.png/.test(file.type)) {
// image file
reader.onload = function() {
console.log('success');
}
reader.readAsDataURL(file);
}
}
}
}
大文件分片上传¶
大文件分片上传思路(详见开课吧笔记)
-
先把 file 异步读取到 JS 内存中 (fs.readFile)
-
类数组切片成 chunks (files.slice(current, current + chunkLength))
-
前端生成一个 hash (三种方法,idle,布隆过滤器)
-
然后 chunks.map() 给每一个分片name加上hash,调用 API 并上传,根据上传的chunks数量,设置进度条。
-
上传后,需要后端协同处理(根据文件的 hash 确定文件唯一性,然后根据 chunk 的 index 进行排序,把多个文件片段合并后,存储到数据库)
-
特殊:如果丢失分片,类似网络请求丢包处理思路(断点续传或者重传)前端再次传递分片
其他特殊情况:¶
-
很多小文件上传(本地JS压缩成一个文件,本地用 JSZip 或者 gzip 等格式,然后后端收到再解包);
-
网络很差(经常中断)前端后端需要查询是否某个片段已经上传,来确定是否重新上传等(断点续传)
-
拖拽文件上传,复制粘贴上传(需要调用前端的事件获取文件,监听 DIV 的 drag drop 事件,然后从 event 中获取文件)
0267 canvas ——实现刮刮乐¶
前端刮刮乐的实现,canvas API,先创建一个矩形灰色区域,然后监听鼠标点击事件,绘制新的区域,和第一个区域求差值即可,鼠标按下后,监听鼠标移动事件。鼠标抬起后,清空鼠标移动事件。关键是 canvas 的属性和 API。
https://juejin.cn/post/7142839691203575838
https://blog.csdn.net/qq_44907926/article/details/119881880
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>中奖啦</title>
<style>
h1 {
text-align: center;
}
#container {
width: 400px;
height: 100px;
position: relative;
left: 50%;
transform: translate(-50%, 0);
}
.back, canvas {
position: absolute;
width: 400px;
height: 100px;
left: 0;
top: 0;
text-align: center;
font-size: 25px;
line-height: 100px;
color: deeppink;
}
</style>
</head>
<body>
<h1>刮刮乐</h1>
<div id="container">
<div class="back">二等奖</div>
<!-- 如果背景是图片,那么这里再加一个图片 -->
<canvas id="canvas" width="400" height="100"></canvas>
</div>
<script>
// 避免选中背景的文字(二等奖)
document.addEventListener("selectstart", function (e) {
e.preventDefault();
});
// 前景灰色蒙版 canvas
let canvas = document.querySelector("#canvas");
let ctx = canvas.getContext('2d');
ctx.fillStyle = 'darkgray';
ctx.fillRect(0, 0, 400, 100);
// 判断当前状态是否点击
let isDraw = false;
canvas.onmousedown = function () {
isDraw = true;
}
let containerDom = document.querySelector('#container');
canvas.onmousemove = function (e) {
// 鼠标按下时,刮奖
if (isDraw) {
let x = e.pageX - containerDom.offsetLeft + containerDom.offsetWidth / 2;
let y = e.pageY - containerDom.offsetTop;
ctx.beginPath();
// 绘制圆形:xy 是圆心坐标,r 是半径,0, 2 * Math.PI 是开始的角度和结束的角度
ctx.arc(x, y, 30, 0, 2 * Math.PI);
// 关键: globalCompositeOperation = type 这个属性设定了在画新图形时采用的遮盖策略
// 具体属性参考:https://www.jianshu.com/p/ff425bfa6f41
ctx.globalCompositeOperation = 'destination-out';
ctx.fill();
ctx.closePath();
}
}
document.onmouseup = function () {
isDraw = false;
}
// 中奖情况
let arr = [
{ content: '一等奖:一个大嘴巴子', p: 0.1 },
{ content: '二等奖:两个大嘴巴子', p: 0.2 },
{ content: '三等奖:三个大嘴巴子', p: 0.3 }
];
let tmp = Math.random();
let backImageDom = document.querySelector('.back');
if (tmp < arr[0].p) {
backImageDom.innerHTML = arr[0].content;
} else if (tmp < arr[1].p) {
backImageDom.innerHTML = arr[1].content;
} else if (tmp < arr[2].p) {
backImageDom.innerHTML = arr[2].content;
}
</script>
</body>
</html>