产品笔记

2026-7-17

原始笔记链接:https://cloud.seatable.cn/dtable/external-links/59b453a8639945478de2/

0187 表单长度设计原则

如果是一个 toC 的产品,设计一个调查问卷或者表单,表单项最好不要太长。如果一个表单需要用户填写消耗5分钟以上,大部分用户都会没有耐心。如果是必填的项,那么用户可能随便填写,这样调查就失去了正确性。

所以需要根据用户的水平,确定表单的长度和深度,尽量让用户做选择题,而不是让用户做填空题,而且这样更便于用户填写,便于最后数据分析整理等。

如果表单确实很长,那么可以尝试分页显示,然后顶部显示进度条,表示用户填写了多少表单项。

界面加载过程类似:尽量减少加载时间,时间小于3S,界面显示 loading 图标。加载时间超过 5秒,最好显示进度条。如果已经确定加载的框架,只是需要后台返回数据,那么可以先显示骨架屏。

具体参考:

https://developers.weixin.qq.com/miniprogram/dev/devtools/skeleton.html

undefined

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

编辑内容如下

1
test <img src=\"https://2024-02/bird.jpeg\" height=\"60\"> @foo

渲染评论

1
2
3
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 动画,这样可以从缓存中获取动画。

1
2
3
4
5
6
<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、计算当前图片是否在网页视口中,也就是计算图片距离网页顶部的位置,和当前滚动位置和屏幕高度。

1
2
3
4
5
6
7
componentDidMount() {
    this.checkLoad();
}

onScroll = () => {
    this.checkLoad();
}

3、监听界面垂直滚动事件(节流监听),当某个图片开始进入网页内部(或者即将进入网页内部),那么设置成真实的 src,向服务器发出请求,界面显示图片,就实现了图片的懒加载。伪代码如下:

1
2
3
4
5
6
7
8
function checkLoad() {
    // 检测当前图片是否在视口内部
    // 图片距离顶部的距离 < 网页滚动距离 + 屏幕高度
    const isLoad = imageOffsetTop < containerSrollTop + window.innerHeight
    if (isLoad) {
        img.src = img.getArrtibute('data-src');
    }
}

0432 鼠标经过切换图片

需求:鼠标经过,切换到另一张图片(犀牛书 21 章第一节的案例,JavaScript权威指南)

原理:把一个图片地址和另一个图片的地址设置到节点中,然后鼠标经过,获取 data 属性然后改变 src 属性,这样实现了图片切换。为了避免缓存,可以先 JS 创建图片,然后直接从缓存中读取图片。

HTML

1
<img src="https://michael18811380328.github.io/background/pub_31.jpg" data-rollover="https://michael18811380328.github.io/background/pub_32.jpg"/>

JS

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  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

0838 SaaS 是什么

SaaS software-as-a-service 具体是什么

软件即服务,通过互联网提供软件服务的创新模式

Michael: 不需要用户安装软件到本地,直接云服务使用软件,类似云游戏

基本原理

基于云计算技术,软件供应商将应用程序部署在自己的服务器上,多个用户可以通过互联网浏览器等方式,远程访问和使用这些软件,就如同使用本地安装的软件一样,不过用户无需在本地硬盘或服务器上进行复杂的软件安装、配置与维护等操作。

特点

  • 低前期成本:对于企业用户来说,不用像传统模式那样购买软件许可证,花费大量资金进行前期投入,而是采用按需付费或者订阅的方式,按照使用的时长、功能模块、用户数量等付费,降低了使用软件的门槛和成本。

  • 易于部署:只要有网络连接,用户能快速开始使用软件,大大缩短了从决定使用到实际应用的时间周期,尤其适合中小企业快速开展业务,提高运营效率。

  • 可灵活扩展或缩减:企业可以根据自身业务发展情况,方便地增加或减少使用的功能、用户账号数量等,随时匹配业务规模变化,避免资源浪费或不够用的情况。

  • 由供应商维护更新:软件的升级、维护、安全防护等工作都由软件供应商负责,供应商会不断优化软件性能、修复漏洞、增添新功能,用户无需投入额外的人力、物力来进行这些操作,能始终使用到较新且稳定的软件版本。

应用场景举例

  • 企业资源规划(ERP)​:制造企业、商贸企业等通过 SaaS 模式的 ERP 系统管理生产、采购、销售、库存、财务等各个环节,众多员工可以依据权限远程登录操作,提升企业整体管理水平。

  • 客户关系管理(CRM)​:销售团队利用 SaaS CRM 系统记录客户信息、跟踪销售线索、管理销售流程、分析客户数据等,方便团队成员协同工作,提高销售业绩。

  • 办公自动化:很多企业采用 SaaS 模式的在线办公软件(Seatable),实现文档协作编辑、项目管理、审批流程电子化等功能,员工可以随时随地办公,打破空间限制。

优势与局限

  • 优势:从用户角度来看,能节省成本、快速应用、聚焦核心业务而不用操心软件技术层面的问题;从软件供应商角度,能扩大用户群体,通过持续服务获得长期稳定收益,还能更高效地收集用户反馈来改进产品。

  • 局限:对网络的依赖程度较高,如果网络不稳定或者中断,可能会影响软件的正常使用;另外,在数据安全和隐私方面,部分用户可能会担心数据存放在供应商服务器上存在一定风险,尽管正规供应商都会有严格的数据安全保障措施。

0885 复杂项目的分析过程

架构图+文档+代码

(图文并茂,类似论文答辩)

例如阅读器前后端整体架构

0884 如何处理错误?

代码鲁棒性最佳实践:

出现异常-提示异常-恢复原始的状态,下次重新启动(服务器设计)

算法内部就不需要特别复杂(处理各种异常和特殊情况)

如果各种错误码 400, 401, 404, 500 都处理不同的情况,然后继续在内部循环中操作,性能不好,还可能影响服务器

不如直接停止这次操作,恢复默认状态,从头开始进行执行(数据库的一个事务)

0871 滑动验证码前端实现

https://github.com/yeild/jigsaw

一个第三方前端滑动验证码,目前不维护,实现思路(jquery + canvas 实现验证),商业使用还需要其他的验证工具

基本思路:给定一个随机图片和随机数,很容易使用 canvas 进行放置。

下方增加一个拖动条,用户拖动时,记录拖动的位置,并让上面的滑块移动到对应的位置,当用户停止拖动时,计算拖动的距离和实际距离,进行滑动验证。

0872 文件夹多层级

需求:需要支持一层文件夹(例如文件管理和其他项目管理中)

技术分析:前端文件夹的数据结构,可以存放在前端本地浏览器中,可以存放在后端数据库中

前端本地浏览器实现

如果是前端的本地浏览器中,那么可以把文件夹数据结构(一维的数组,多层文件夹就是树结构,在数据结构和UI层面分别实现树结构文件夹)存储使用序列化和反序列化实现即可。

1
2
3
4
5
6
7
8
folder = {
  name: 'china',
  id: '111',
  children: [], // 先试用空数组实现,未来支持多级目录,然后再实现目录树,
  isOpen: false, // 默认打开还是关闭状态
} 

folders: [] 数组

1、实现单层文件夹,存储在前端实现

2、界面支持文件夹和拖拽功能(react-dnd)库的使用

3、文件夹的数据结构存储在后端,这样不同浏览器显示的文件夹是一致的;用户对应的文件夹结构即可,在用户表新加入一个字段即可。

4、考虑支持文件夹树结构,在数据结构和UI展示有深度。因为单页面应用,所以不考虑后端性能

后端数据库存储

将保存的文件夹树状结构,转换成 json 存储到后端(早期是 restfulAPI 这种直接保存JSON结构)

0873 显示新手引导教程

如何判断用户是否首次显示?通过浏览器 localStorage 或者用户登录时,后端返回的信息显示

方案1:比较复杂,这是 canvas 蒙层实现,首次使用加载这个教程(箭头指向选中的 TAB,然后按照步骤进行操作)

首次加载界面时,能否展现给用户操作步骤,可以把需要展示的几个 TAB ID 拿到,然后 JS 可以找到位置,生成对应的箭头样式。

1
2
3
4
if (need_tutorial) {
    let tab1 = getElementByID('new');
    let text = 'xxx'
}

方案2:比较简单,直接使用 Popover 组件即可实现效果。给定对一个的按钮序列(ID)然后循环打开 popover 即可,显示帮助内容。

1
2
3
4
5
6
let btns = [];

btns.forEach(item => {
    let dom = item;
    dom.tooltip = true; // 用户点击下一个(打开下一个),还是用户点击关闭(跳出循环)
})
undefined

0880 产品区分主要次要功能

某一个产品的重心应该确定,例如小说阅读器,文件阅读器

  • 核心功能就是阅读

  • 次要功能就是用户书籍评论(数据库可以定期删除以前的评论)

  • 其他功能就是评论的回复

所以产品设计的重心主要是核心功能,次要功能可以不做或者后期再做,或者分成两个独立的项目单独做(只要是用户登录的系统,都有评论功能,包括非实时评论,实时评论,那么可以单独展示一个评论区域,这个评论功能就可以重复使用)。

0837 错误处理的思路

算法设计时候的鲁棒性:

如果程序出现错误后,再次打开不需要显示错误,不需要考虑错误后细节处理的步骤。

需要考虑如何恢复原始情况,让用户刷新页面时正常使用,这也符合常见用户的操作习惯。用户遇到错误后,通常考虑刷新一下网页,正确显示就可以了。

0827 项目开发时间安排

和朋友交流,项目开发的时间

  • 如果是小项目或者互联网项目,1个月全职工作内需要把第一版完成(包括需求调研,产品设计,前后端开发,打包上线等),小项目指的是功能简单,开发资金1万元。

  • 如果是大型项目(例如当前的编辑器),这种开发周期就是半年起步。因为前期调研,功能设计需要花大量的时间,后期技术细节很多。

具体案例:小说阅读器项目,2个月的业余时间,完成了基本的前端后端开发,基本达到可以上线的标准。

那么大概的时间安排是:

  • 前端:阅读器中心部分(渲染文本),阅读器左侧(文件树,搜索文件),阅读器右侧(设置项),登录页面,注册页面,后台管理页面(使用echarts显示数据情况)

  • 后端:用户增删改查,小说增删改查,评论增删改查,评论小说用户之间主键外键关联,分词和增加标签

  • 各种借口联合调试,上线测试

如果全天做,这个代码量,两周可以实现。

0695 图片9图切换效果实现

需求:产品需要这个效果,具体怎么实现?

从数据层面和交互层面分析

数据分析

1、数组获取:文件树结构,转换成数组

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 原理
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);
		}
	}
}

原始版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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 操作的函数, 有大佬可以解释下吗?

本意是为了方便开发者调试,结果变成了拦截开发者

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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 中选择文件,网页中如何拖动选择多个文件?

undefined

解决:

处理拖动和灰色蒙层

1、监听鼠标点击事件,获取位置 x1, y1

1
2
startPoint: { x1: 0, y1: 0 };
endPoint: { x2: 0, y2: 0 };

2、当鼠标移动时,每次移动到的位置是 x2 y2,此时 x1 x2 y1 y2 四个点渲染成一个方形的图层,然后设置边框和背景色,和透明度。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
  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 算出每一个文件所在的位置。然后和蒙层求交集即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    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 的效果,这个怎么实现?

undefined

产品需求描述:

1、鼠标经过这个树形结构,显示左侧竖线;鼠标移除后不显示竖线

2、竖线和上一级目录的三角形对齐

3、竖线和背景灰色不冲突,竖线浮在背景灰色上面

实现:文件树已经实现,现在只处理竖线。

文件结构是树形结构,那么可以先遍历树节点,然后传递一个变量 offsets = [],每次递归子节点,都增加一个缩进值。例如 [20, 40, 60, 80, 100]。这里的值是缩进值。

在叶子节点中,渲染左侧的N个边框

1
2
3
4
5
6
7
8
{offsets.map(offset => {
  return (
    <div style={{
      // height 是每一项的高度
      position: 'absolute', border: '1px solid #ccc', left: offset, height: 28, top: 0,
    }}>
  );
})}

然后进行微调,即可实现左侧的竖线效果。

0442 文件上传的几种情况

普通文件上传

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<!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>

0301 如何不让用户选择文本

1、设置 user-select:none 可以不让用户用鼠标选择文本。

2、高级百度文库:实现不能复制的技术,先实现 dom 节点,然后 html-to-canvas 转换成 canvas,避免了浏览器打开复制文本内容。

3、界面增加水印,避免用户用其他软件文字识别等。

4、页面增加混淆字符串,即使用户复制到剪切板也不能正常使用,也可以阻止爬虫获取网页信息(例如实际数据是 100元,爬虫获取到1000元)。

理论上,只要是显示在屏幕上的内容,并可以通过摄像截图转换成图片,那么必然可以提取文本,这个是 JS 层面无法实现的。

0821 微信支付的前后端逻辑

微信支付分成两种情况:

第一种、网页在微信内部浏览器打开,直接调用内置浏览器的全局变量进行支付(微信会弹出一个支付框)

第二种、网页在电脑普通浏览器打开,需要生成支付二维码,用户手机微信扫码进行支付

两个支付的基本流程如下

第一种情况具体支付过程:

1、用户点击页面的按钮,开始结算(例如购买一件衣服,一个手机)。JS 调用购物网站服务器的 API。

1
2
3
4
5
document.getElementById('payButton').addEventListener('click', function () {
    // 这里应该调用你的后端接口获取支付参数
    fetch('/get_payment_params')
      .then(response => response.json())
});

2、购物网站服务器,把用户具体的金额算出来(例如10000元和具体信息),发送到微信支付的服务器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 微信支付配置(需要商户先在微信注册,获取这些信息)
APPID = 'your_appid'
MCHID = 'your_mch_id'
API_KEY = 'your_api_key'
NOTIFY_URL = 'your_notify_url'
TRADE_TYPE = 'JSAPI'
OPENID = 'your_openid'

@app.route('/get_payment_params', methods=['GET'])
def get_payment_params():
    nonce_str = generate_nonce_str()
    out_trade_no = f'{int(time.time())}'
    total_fee = 1  # 支付金额,单位为分
    body = '测试商品'

    params = {
        'appid': APPID,
        'mch_id': MCHID,
        'nonce_str': nonce_str,
        'body': body,
        'out_trade_no': out_trade_no,
        'total_fee': total_fee,
        'spbill_create_ip': request.remote_addr,
        'notify_url': NOTIFY_URL,
        'trade_type': TRADE_TYPE,
        'openid': OPENID
    }
    params['sign'] = generate_sign(params)
    
    # 发送给微信支付服务器
    xml_data = dict_to_xml(params)
    url = 'https://api.mch.weixin.qq.com/pay/unifiedorder'
    response = requests.post(url, data=xml_data.encode('utf-8'), headers={'Content-Type': 'application/xml'})
    result = xml_to_dict(response.text)

3、微信支付服务器,验证商户信息后,返回 pre_pay_id 预支付ID 到购物网站服务器

1
2
if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
    prepay_id = result.get('prepay_id')

4、购物网站服务器,把相关数据发送给浏览器(APPID, signType 等信息)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
prepay_id = result.get('prepay_id')
timeStamp = str(int(time.time()))
nonceStr = generate_nonce_str()
package = f'prepay_id={prepay_id}'
signType = 'MD5'

pay_params = {
    'appId': APPID,
    'timeStamp': timeStamp,
    'nonceStr': nonceStr,
    'package': package,
    'signType': signType
}
pay_params['paySign'] = generate_sign(pay_params)

return jsonify(pay_params)

5、微信内置浏览器接收到 package,通过内置浏览器的 全局变量,调用函数(类似 JSBridge),微信弹出支付窗口,用户进行支付

1
2
3
4
5
6
7
8
9
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
    "appId": data.appId,
    "timeStamp": data.timeStamp,
    "nonceStr": data.nonceStr,
    "package": data.package,
    "signType": data.signType,
    "paySign": data.paySign
},callback)

6、支付完成后,执行回调函数,网页可以看到是否购买成功,微信支付也会通知服务器是否完成支付

1
2
3
4
5
6
7
function (res) {
    if (res.err_msg === "get_brand_wcpay_request:ok") {
        alert('支付成功');
    } else {
        alert('支付失败');
    }
}

至此,支付完成

如果是第二种方案,在普通浏览器进行微信支付,那么微信服务器返回的就是二维码链接

1
2
3
if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
    code_url = result.get('code_url')
    return jsonify({'code_url': code_url})

前端需要用 qrcode 渲染二维码,然后手机扫码支付

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
document.getElementById('payButton').addEventListener('click', function () {
    fetch('/get_qrcode_url')
      .then(response => response.json())
      .then(data => {
            if (data.code_url) {
                // 使用 QRCode.js 生成二维码
                var qrcode = new QRCode(document.getElementById("qrcodeContainer"), {
                    text: data.code_url,
                    width: 256,
                    height: 256
                });
            } else {
                alert('获取二维码链接失败: ' + data.error);
            }
        })
      .catch(error => {
            console.error('获取二维码链接失败:', error);
        });
});

具体思路是 AI 写的,还没有实际使用过。