-
开发方式
-
并行(优先):
- 先与服务端对接预期API,服务端产出API文档;
- 前端根据文档通过Mock方式开发(或服务端先提供Mock数据的API);
- 当服务端API开发完毕后再用真实API加入前端页面(仅关闭Mock即可)。
-
串行:
服务端比前端提前一个版本,交付的内容包括API+文档。
-
-
分页加载、滚动加载
-
分页加载,前端用
第几页
+每页几项
发起请求,服务端(提前)返回总量
给前端做判断一共有几页。 -
滚动加载,用
游标
作为判断下一批请求内容的依据:-
分页的游标管理
-
普通情况,游标由前端(或客户端)管理
前端用
游标id
发起请求,服务端返回新的游标id
给前端作为下一次请求。 -
若快速变动的数据(如:推荐信息)、或要根据用户操作而快速改变的数据(如已推送给某用户的不再推送给ta、用户标记不喜欢的相关类型不再推送给ta),则游标由服务端管理。
服务端用Redis等内存管理方式记录用户的ID,前端只需要每次请求相同的无参数接口就可从服务端返回分页数据。
-
-
-
若用分页加载的服务端接口实现滚动加载
-
则可能出现请求到重复数据或略过数据的情况。(游标的,若没有管理好数据流,则也会出现重复数据或略过数据情况)。
-
前端(或客户端)也可以模拟游标管理方式:暴露一个加载更多的无参数接口,在接口内部实现类似服务端的游标管理。
e.g.
let arr = [] // 数据 const size = 10 // 每页数量 const total = 111 // 总量 function loadMore () { if (arr.length < total) { console.log('页数:', Math.ceil(arr.length / size) + 1) // 页码:Math.ceil(arr.length / size) + 1;每页数量:size // 用发起异步请求获取数据,数据插入arr arr = arr.concat(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) } else { // 已经加载所有内容 } return arr } loadMore() // 加载更多直接调用,不用管理状态
-
-
-
服务端文档要求
API文档确定的字段,就算为空,也必须按照文档要求返回
[]
或{}
,不允许返回内容丢失字段。 -
扁平化的需要
不同接口、但类别相同的数据,都按照相同的结构约定数据格式(如:normalizr)。
前端可以进行数据扁平化,把不同接口返回的数据都根据类别按照hash的方式存放在各自类别的store,并再保存一份数组记录展示顺序(把数据库的hash保存的方式移植到前端也用hash保存)。
e.g. 一个接口返回的数据包括articles、users数据,进行扁平化
// articles的store(内聚) const articles = {} // articles的store articles.all = {} // 存放articles的元数据(元数据:完整的单项数据,用唯一的id进行hash索引) articles.hot = { // 存放articles的hot的展示顺序 sequence: [], // 元数据的id顺序 hasMore: true // 是否继续请求 } articles.new = { // 存放articles的new的展示顺序 sequence: [], // 元数据的id顺序 hasMore: true // 是否继续请求 } articles.flattenData = (data) => { // 扁平化数据:把单项数据全部保存在同一个地方 articles.all[data.id] = Object.assign({}, articles.all[data.id], data) } articles.changeSequence = (data) => { // 写入某业务的展示顺序 const list = articles[data.category] if (data.refresh) { list.sequence = data.sequence } else { list.sequence = list.sequence.concat(data.sequence) } } // 相同省略:users的store // 请求articles.hot的数据。返回的数据包含多种类别数据(articles、users) function handleData (arr, category) { // 处理数据 articles.changeSequence({ // 写入hot的展示顺序 category: category, refresh: false, sequence: arr.map((data) => { articles.flattenData(data.articles) // 把元数据合并至articles // users.flattenData(data.users) // 把元数据合并至users return data.articles.id // 返回articles的id用于保存顺序 }) }) console.log(category, JSON.parse(JSON.stringify(articles))) // 打印 } // 针对articles.hot的第一次请求 const data1 = [ { articles: { id: '1', data: 'articles第一个数据' }, users: { id: 'a', data: 'users第I个数据' } }, { articles: { id: '20', data: 'articles第二个数据' }, users: { id: 'b', data: 'users第II个数据' } }, { articles: { id: '300', data: 'articles第三个数据' }, users: { id: 'c', data: 'users第III个数据' } }, { articles: { id: '4000', data: 'articles第四个数据' }, users: { id: 'd', data: 'users第IV个数据' } } ] handleData(data1, 'hot') // 针对articles.hot的第二次请求 const data2 = [ { articles: { id: '5000', data: 'articles第五个数据' }, users: { id: 'E', data: 'users第V个数据' } }, { articles: { id: '600', data: 'articles第六个数据' }, users: { id: 'F', data: 'users第VI个数据' } }, { articles: { id: '70', data: 'articles第七个数据' }, users: { id: 'G', data: 'users第VII个数据' } }, { articles: { id: '8', data: 'articles第八个数据' }, users: { id: 'H', data: 'users第VIII个数据' } }, { articles: { id: '1', data: 'articles第一的覆盖内容' }, users: { id: 'a', data: 'users第I个的覆盖内容' } } ] handleData(data2, 'hot') // 针对articles.new的第一次请求 const data3 = [ { articles: { id: '5000', data: 'articles第五的覆盖内容' }, users: { id: 'E', data: 'users第V的覆盖内容' } }, { articles: { id: '8', data: 'articles第八的覆盖内容' }, users: { id: 'H', data: 'users第VIII的覆盖内容' } }, { articles: { id: '300', data: 'articles第三的覆盖内容' }, users: { id: 'c', data: 'users第III的覆盖内容' } }, { articles: { id: '20', data: 'articles第二的覆盖内容' }, users: { id: 'b', data: 'users第II的覆盖内容' } } ] handleData(data3, 'new') console.log('接口获得的数据都进行扁平化处理;在对应类别的store按照id存取数据,再保存一份存放顺序的数组')
-
接口请求失败,不能帮用户静默再次请求
-
提示用户(针对必要展示的信息)
让用户认知网络错误(或其他错误)并且给用户操作重新加载的功能(如:跳转到网络出错或404页面)。
需要请求数据的应用都需要设计404和网络错误等容错页面。
-
静默失败(针对增量加载的信息,如:滚动加载)
不提示用户失败,当用户再次触发时再次请求(减少用户挫败感)。
-
-
错误展示:不能把服务端返回的(错误)信息直接发送给用户
不管是否暴露错误细节,都应该优化错误日志,方便开发查找具体用户、锁定错误原因、自动告警。
-
需要转换成用户能看得懂的语言
也不能把前端错误信息发送给用户。若必须发送给用户错误信息,也需要转换成用户能看得懂的语言。
e.g.
try { asd } catch (e) { alert(e) // 不可以把不经过翻译的错误信息发送给用户 }
在安全的范围内,提示用户失败的原因。
-
为了安全,要隐藏真实的错误信息,只暴露笼统的文案
- toC尽量少暴露错误信息,甚至仅展示
服务器开小差
- toB(尤其是toD)可以暴露错误码、错误码映射文案,甚至直接暴露代码错误信息
- toC尽量少暴露错误信息,甚至仅展示
-
可能在物理机、vps、容器(如:Docker、Kubernetes)中查看日志。
-
HTTP Server日志
-
nginx
nginx -t
获得配置路径;- 根据配置文件查找日志存放地点(
access_log
、error_log
); - 查看日志文件。
-
-
服务端应用程序日志
-
pm2
pm2 list
获得进程列表;pm2 info 「进程id或name」
获取进程信息;pm2 log 「进程id或name」
查看进程日志。
-
Docker
docker ps
获取容器信息;docker logs -f --tail 「数字」 「容器ID」
查看Docker日志。
-
Kubernetes
kubectl get namespace
查看所有命名空间;kubectl get pod -n 「namespace名字」 | grep 「筛选关键字」
查看所有某命名空间的pod;kubectl logs 「pod名字」 -n 「namespace名字」 -c 「container名字」 -f --tail 「数字」
查看pod日志。
-
顺着请求链路排查:域名 -(DNS -> 服务器地址) -> HTTP Server(如:nginx、Apache Tomcat) -> 服务端应用程序(逻辑、IO)。
-
出错维度、链路:
-
判断是否发送到指定服务器地址
如:域名解析、网络问题、本机的hosts/DNS(缓存)。
-
已发送到指定服务器地址,查看出错位置的日志
-
HTTP server错误
如:nginx日志。
-
服务端应用程序错误
如:Node.js、Golang跑的服务端程序日志。
-
运维相关问题
如:磁盘满了、硬盘坏了、数据库出问题、其他各种问题的信息。
-
-
-
前端查错
-
跨域或其他浏览器策略问题会显示在控制台报错。
-
根据HTTP Response查询
-
Headers
-
Body
-
根据错误信息去匹配API文档给出的类型错误。
-
注意那些nginx标记的错误,说明在nginx层被阻止。
e.g.
<html> <head><title>413 Request Entity Too Large</title></head> <body> <center><h1>413 Request Entity Too Large</h1></center> <hr><center>nginx/1.19.0</center> </body> </html>
-
-
-
-
服务端查错
若有错误信息,则方便直接定位出错点;若没有错误信息,则需要顺着链路查询。
-
进入服务器或容器内:
-
宿主机
curl
容器监听端口,确认是否能从宿主机进入容器。 -
进入容器内,
curl
依赖的API,确认是否宿主机内能正常访问API。 -
在宿主机或容器内通用查询:
-
- (一些log监控器提示的错误可能会缺失细节信息,)可以去服务器手动执行相同命令现场复现原始错误信息。
-
方案一:后端生成文件,以流的形式给到浏览器(异步处理、内存充足、流式传递)。
业务体量小、轻量服务才会使用,因为占用服务器资源,也需要处理流传递的一些前后端问题。
-
方案二:
后端利用 对象存储OSS(Object Storage Service) 等云存储服务,后端上传成功后,返回下载链接给前端。
优势:能够提前进行文件生成,有留底文件,相同资源生成一次即可,CDN、缓存,云存储 而不是 自己服务器处理、减少服务端压力。缺点:配置和使用第三方云服务的成本。
-
方案三:
前端请求到JSON数据后再在浏览器产生文件并填入数据。
优势:简单,前端灵活控制导出格式,减少后端工作量。缺点:建议仅针对较少的数据,实时性、每次都需要请求JSON后由浏览器生成(受浏览器性能限制),受后端接口波动而影响速度。