# 111 **Repository Path**: Sentokaxi/111 ## Basic Information - **Project Name**: 111 - **Description**: 111求求求求求求求求求 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-10-31 - **Last Updated**: 2022-11-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ### 功能实现 ##### 微信登录 使用微信公众平台测试号,将网页授权之后回调地址设置为`127.0.0.1:3000`。设置授权回调地址为`http://127.0.0.1:3000/api/user/wxlogin`,该地址为前端地址,但是调用会代理到后端。当用户微信授权完成后,携带`code`参数调用服务器接口,向微信服务器请求换取`access_token`,之后通过`access_token`向微信服务器请求换取用户信息,调用自己服务端登录注册流程。 ##### 阿里云 oss 使用阿里云oss保存应用上传的图片,使用oss图片处理功能在列表展示时使用压缩过的缩略图,查看大图时返回清晰度更高的压缩图,以节省流量、提高加载速度。创建`bucket`上传文件,开放公网读权限,写入文件时需要使用密钥。选择后端生成签名直传的方式上传文件,前端携带当前文件名向应用服务器请求上传文件,应用服务器将上传`Policy`和签名返回给前端,该`Policy`信息中存在服务器生成的唯一文件名,且该签名存在过期时间,之后前端可以将文件上传到oss中。后端签名直传可以防止oss密钥的泄漏。 ##### 项目结构 - router层:路由的分发处理,对路由进行统一的管理 - schema:使用`joi`对`query`、`params`、`body`中的参数进行统一校验统一参数校验 - controller:将路由使用对应的`service`进行处理,并对返回值进行处理 - service:操作数据库和`service`对业务逻辑进行处理 ##### 中间件 - 登录验证中间件:根据`cookie`以及用户状态进行拦截,非登录状态下只能调用登录接口,注册状态下只能调用获取用户信息和注册接口 - 错误处理中间件:拦截自定义的错误类型和其他异常,结合统一消息返回格式中间件对消息进行处理 - 统一返回消息格式中间件:对每次进入的请求提供对应`ctx`环境下的构造错误信息和成功信息的函数,供错误处理中间件使用 - 日志中间件:使用`log4js`,当每次请求对应的日志写入到文件中进行管理 #### feed-front ##### 登录鉴权 - 使用axios拦截器通过判断后端返回的状态码来实现登录鉴权 - 当拦截到401,且当前不为登录页时,跳转到登录页。当拦截到状态码为`1004`未完成注册时,跳转到注册页面。当获取用户信息接口返回用户信息成功且当前不为登录页时,根据用户状态,未完成注册状态跳转到注册页,完成注册直接跳转到首页 ##### 登录注册 - 登录,使用微信登录的方式,前端点击授权登陆后跳转到微信的重定向地址 - 注册 输入用户名进行注册 在发起请求前先进行表单验证,并提示用户,表单验证通过时才能发起请求 ##### 返回顶部 - 组件的封装 封装一个公用组件 UpPage ,当 主页、搜索页和用户主页滚动条高度大于一定值时就显示该组件 - 两种返回顶部的方式:一种是通过双击主页图标,进行数据的重新加载和返回顶部;另一种是通过返回顶部按钮返回。 返回顶部的原理是获取当前页面的scrollTop ,然后通过ref的方式将scrollTop设置为0实现置顶,通过设置`scroll-behavior: smooth`实现平稳的滚动。 ##### 滚动条的缓存 ​ 考虑到帖子或者回复列表中数据量可能较大,为了提高用户体验,用户切换到其他页面之后再次回到当主页信息流列表或者用户在主题帖中点击进入评论后返回,需要回到上次浏览的位置。需要对主页信息流列表和主题帖列表的数据和滚动条位置进行缓存。对于主页信息流列表,当页面跳转,即页面销毁时,在`mobx`中缓存浏览的滚动条的位置。对于主题帖,由于跳转到评论未发生组件的销毁,所以可以通过监听当前帖子的`id`变换,从而判断是否跳转,来进行滚动条位置的缓存和主题帖页面数据的缓存。 ##### 帖子相关操作 - 帖子的封装 封装一个公共组件 postItem,首页、用户主页、搜索页都使用这个组件 帖子的图片使用 antd Image 组件,自定义其样式 使用缩略图,未点击图片预览时显示缩略图,点进图片预览时显示原图 - 骨架屏及错误处理 在网络或接口错误时显示错误页面,点击刷新按钮重新获取数据 在第一次渲染数据时,并且在 loading 状态时显示骨架屏 不在 loading 状态帖子列表为空时显示帖子列表为空 - 首页帖子的渲染 首次进入首页时请求数据,将拿到的数据存入 mobx 做一份缓存,通过缓存实现从其他页面再次进入主页不再重复请求数据。 使用游标的方式进行分页,滚动到最底部时如果还有数据就通过最后一条帖子的 id 作为参数请求后面的帖子 支持下拉刷新,下拉刷新时刷新列表 通过轮询判断有无新帖子,如果有新帖子则在首页的 tab 图标显示小红点,双击 tab 图标请求最新的帖子,把拿到的帖子添加到列表项最前面,同时返回顶部。 - 发帖 封装一个组件 PopCreate 用于发帖,点击发帖按钮弹出一个弹出层,输入内容和图片进行发帖。 上传图片使用 antd ImageUploade 组件,自定义其样式符合要求。 在发帖时做判断,如果内容和图片都为空时则提示内容为空,不允许发帖,如果图片正在上传提示图片未上传。 在上传图片前需要判断图片类型和大小,只有图片类型符合要求、图片大小小于 4M 才允许上传,最多只允许上传 4 张图片。 在点击提交按钮时进行防抖处理,只会在最后一次提交结束后等待 300ms 执行,防止频繁点击导致重复发帖。 发帖成功后调用一个回调函数改变本地状态。 - 转发、评论 转发评论逻辑与发帖相似,都做防抖处理,点击提交按钮发起请求,请求成功后返回一个对应的状态,前端拿到这个状态来改变本地的状态。 - 点赞 点赞为了优化用户体验,在点赞时先改变本地的状态,再发起请求,如果请求失败则回滚本地的状态。这样在网络很慢时频繁点击也不会有延迟。 ##### 主题帖 ​ 主题帖主要由两部分组成,帖子本身内容,包括关联的评论内容,以及当前帖子的所有的评论。主题帖有进入查看评论的需求,为了用户体验更好,在帖子发生切换时,对滚动条的位置进行缓存,保证退出到上一个页面时在上次浏览的位置。而且为了防止返回时再次请求帖子信息,在帖子发生切换时同时将当前页面数据进行缓存,可以有效节省流量、降低白屏时间、提高速度和用户体验。并且主题帖缓存的数据将在当前组件发生卸载时清除。 ​ 主题帖对于帖子信息和评论信息在加载时使用两块不同的骨架屏,当帖子信息发生错误时,错误提示将会在全局。当帖子信息加载完成,但是评论加载错误时,将保留已经加载出的帖子内容,错误提示只在评论处。 ​ 评论的删除逻辑:在别人的帖子下只能删除自己的评论,在自己的帖子下,可以删除这条帖子下的所有评论。 ##### 搜索 - 将搜索内容存储在 localStorage 持久化 并将最新的搜索放置第一位,获取 localStorage 中的数据将其显示在搜索记录中 - 将数据存入localStorage时进行去重操作,保证搜索记录的简洁和用户使用体验。 - 使用正则表达式判断,搜索内容不能为空、不能为全空格,否则不能搜索。 - 清空搜索记录就是将 localStorage 清空。 - 对输入的搜索内容进行加密 `encodeURIComponent()` ,通过路由传递给搜索结果页,在获取路由传递的参数时再进行解密 `decodeURIComponent()` ,防止特殊字符搜索不到的情况。 - 搜索结果页 使用游标的方式进行分页,滚动到最底部时如果还有数据就通过最后一条的 id 作为参数请求后面的帖子。 帖子和图片列表使用组件 PostList ,用户列表使用组件 FocusList ##### 通知 - 使用游标的方式进行分页,滚动到最底部时如果还有数据就通过最后一条通知的 id 作为参数请求后面的帖子 - 通过轮询判断有无最新通知,如果有新通知则在通知栏 tab 图标显示未读消息数,同时把请求到的通知添加到列表项最前面 - 未读消息数为0时不显示,大于99时显示99+ - 通知可以标记为已读和删除,标为已读和删除时先请求接口,如果服务端返回成功消息则修改本地状态。 - 点击头像可以跳到用户主页,判断通知类型,如果为转发和评论,点击内容可以跳到对应的主题帖,如果是关注则跳到对应用户主页。同时将通知设为已读。 ##### 个人主页 - 封装组件 UserHeader,用于展示所访问用户的背景图、头像、昵称、账号、注册时间、个人介绍及关注数。 - 个人介绍采用 antd Ellipsis 组件,自定义其样式符合要求。 - 访问当前登录账号主页显示编辑用户信息按钮,访问其他用户主页显示关注状态按钮,用于进行关注或取关操作。 ![](https://cdn.lixingyong.com/2022/08/20/Snipaste_2022-08-20_20-09-41.png) - 封装一个公用的组件进行帖子列表的渲染,根据传入的参数不同渲染不同的数据。帖子页展示用户发布、转发、回复的帖子,照片页展示带有照片的帖子,喜欢页展示用户喜欢的帖子。 - 采用 antd Tabs 组件及 antd Swiper 组件实现帖子的滑动切换页面,分别展示访问用户的帖子、照片及喜欢三个页面,页面下滑时导航栏吸顶。 - 使用游标的方式进行分页,滚动到最底部时如果还有数据就通过最后一条帖子的 id 作为参数请求后面的帖子 - 支持下拉刷新,下拉刷新时刷新列表 ##### 关注 - 根据访问路由参数获取当前用户信息及关注用户相关信息,根据关注状态将用户分为关注者和正在关注两类 - 添加关注及取关功能按钮,按钮样式根据关注状态不同分为正在关注、回关、已互关及关注 - 使用游标的方式进行分页,滚动到最底部时如果还有数据就通过最后一条关注用户的 id 作为参数请求后面的关注用户 ##### 私信 - 私信会话列表 ​ 私信会话列表分页展示当前的消息,并且轮询拉取最新的消息,拉取到的最新消息将与当前列表进行对比,当有相同的会话用户时,进行删除,最终合并最新的会话。开始使用会话进行对比是否有相同记录时,遇到多端异地登录时,一端删除会话,其他会话会产生多个相同用户的会话。所以合并条件应该为会话用户`id`。 - 私信列表 ​ 私信会话列表展示当前最新的消息,下拉时展示历史消息,使用轮询拉取最新的消息。开始采用的是发送消息,在服务器返回成功后显示到页面上,但是当网络较差时、发送图片太大时无法及时展示消息和清空输入框。而且当发送消息失败时也交难处理。图片发送失败时无法判断其是在上传过程中失败还是在发送过程中失败。所以采用异步处理,分上传中、失败、发送中状态来进行处理。当发送消息是,立刻将消息展示到页面上,当图片上传成功,将其插入到上传成功队列等待处理,发送成功将其放入到发送成功队列进行处理,失败后将其放入到失败队列中等待处理。并设置定时器对三个队列定时处理,修改对应数据的状态。 ### 遇到的问题及解决 #### 分页问题 最开始使用skip的方式进行分页,这样当触底加载时,如果前面的数据发生了新增或删除,那么翻到下一页时,就会出现数据不匹配的情况。 `解决方法:`采用基于游标的方式进行分页,因为mongoDB的_id是按顺序排列的,在加载下一页数据时,把当前的列表的最后一个id值作为参数,加载这个id值之后的limit条数据。这样翻页及时前面的数据发生了新增或删除,翻到下一页时也不会产生影响,因为不影响游标。 #### 首页加载最新消息的问题 判断首页有无最新消息时,是通过轮询的方式,以首次加载的第一条数据的id值作为参数,判断有无最新消息。这样如果列表为空时,就拿不到id值,无法获取最新消息。 `解决方法:`消息列表为空时,说明自己和关注的人都没有发过帖子,所以当参数为空时,就去判断关注的用户有没有发帖,如果发了帖,就返回true,前端再加载最新消息,保存加载到的第一条数据的id值,再通过这个id值做轮询。 #### 首页双击事件的问题 如果有新消息时,首页tab栏会显示一个徽标,通过双击tab栏可以加载最新消息并滚动到页面顶部,但是onDoubkeCilck事件不生效。 `解决方法:`自己写一个函数,使用定时器判断用户在300ms内点击次数,如果点击了两次再触发这个事件 #### 个人主页滚动条的问题 `swiper`组件共用一个滚动条,滑动后并且修改之后页面出现两个滚动条,帖子列表的滚动条单独滑动无法实现当`tab`未吸顶时滚动外层滚动条,`tab`吸顶后滑动才滚动帖子列表 `解决方法:` `swiper`默认共用一个滚动条,可以将每个滑动项设置为`overflow: auto`并且高度刚好和`tab`占满整个视口,这样就可以实现三个滑动项使用单独的滚动条,滑动不影响其他的滚动位置。但是这样就出现页面出现内外两层滚动条,而使得滑动列表时,不先吸顶。可以监听`tab`是否吸顶,当未吸顶时,将列表设置为`overflow: hidden`禁用列表滚动条,发生吸顶时,将列表设置为`overflow: auto`,允许滚动。使得此时滚动时,先滚动外层,触发吸顶后再滚动列表数据。 #### 发帖时的防抖问题 在发帖时,如果频繁快速的点击提交按钮,会多次调用接口,发多个一样的帖子。 `解决方法:`对发帖进行防抖处理,如果短时间内触发多次提交,只会在最后一次触发结束后等待 300ms 执行 #### 中文输入`hook`问题 输入框限制最大长度只在输入过程中有效,当最长字符数为18时,首次输入框限制长度为18,如果此时复制进去超出长度为18的中文,输入框无法自动截断 `解决方法`:在当前`hook`中设置最大字符数进行截断,可以解决复制文本的问题。但是会出现在输入中文时将拼音自动截断而无法输入的问题。需要监听`onCompositionStart`中文输入开始事件,`onCompositionEnd`中文输入结束事件,在输入中文时禁止禁止截断,输入中文完成后对文本进行截断。 #### 私信会话列表获取问题 之前根据私信记录查找最新的私信并聚合用户来获取私信会话列表,拉取最新消息根据首条记录`id`,获取对应记录之后的消息会话。使用会话表中的时间判断未读消息数。但是获取之后的消息会话较为复杂,要排除之前已经获取到的用户。且如果消息记录太多查询会很慢 `解决方案`:通过会话表获取对应的消息会话,根据时间查找最新的消息和分页之后的消息,获取到最新的私信会话后,相同用户的会话进行合并 ### 缺陷 #### 私信使用轮询产生的问题 私信由于使用轮询,需要使用时间戳或者最后一条数据的`id`来获取接下来的数据,由于消息状态改变为异步的,直接根据最后一条消息拉取下来的数据可能与状态改变之后的消息存在重复。且由于异步问题,可能当前发送消息被拉下来,消息状态仍然在成功队列中。这样之后的将当前消息改变状态到视图中,会导致`key`重复。而这两种方式的解决方法为在拉取到最新消息时去重,在处理成功队列时去重。这样就可能导致消息的丢失。 如果不使用两个方法中双向去重,为了保证不重复,可以只拉取用户接受到的消息。但是这样轮询拉取最新消息的条件较为复杂,且无法拉取到用户自己发送的最新消息,多端登录下存在问题。 所以,私信发送选择`websocket`更好,可以有效防止重复,且在消息转变状态丢失时,在一种状态无法改变时,可以获取当前消息的状态来修正。