nokosocial(自用)

142 阅读12分钟

社交网站前后端开发记录

一:项目结构搭建

下载nodemon

下载MySQL2,用以链接数据库,

.env文件里存放一些全局变量,项目的配置文件。借助库 dotenv 他可以读取根目录下 .env文件里的内容并且加载到环境变量里(process.env),导入dotenv调用config方法就可以了。

在配置文件(package.json)中,添加脚本命令 start": "nodemon ./src/main.js" 只需运行 npm start就会执行。

使用 koa-bodyparser 对客户端传过来的JSON数据进行解析。app.use(bodyparser())

结构划分:main文件作为整个项目的入口,app文件夹下的index文件用以实例化koa,使用路由,监听错误等。在router文件夹下存放各个功能模块的路由,而路由中的处理函数(中间件)又提取到controller文件夹下面,对于数据库的操作提取到service中(在datebase中连接数据库)。一些校验的中间件则提取到middlware文件夹下。

错误处理,在某处发出错误ctx.app.emit('error',error,ctx),在index中监听错误app.on('error',(error,ctx)=>{})。值得注意的是监听错误中的错误处理函数提取到了一个单独的文件,errhandle中。

post请求参数在请求体(body)里,而get请求参数一般携带在url里(params)。

登录,使用 jsonwebtoken 进行非对称加密,私钥颁发token,公钥解析token。在gitbush中生成,私钥(genrsa -out private.key 1024) 公钥(rsa -in private.key -pubout -out public.key)。登录成功后保存token,有token的话就可以从token里获取到用户相关信息。

get请求在url后面拼接的参数 shareRouter.get(' /:momentid', detail) moment/5 获取方式为ctx.params.momentid. 冒号表示的是可变的。有点类似于动态路由。

还有一种则是 moment?offset=0&size=10 获取方式为 ctx.query。

在获取用户动态(单条)的时候,用到了多表连接查询(moment表和user表),并且把查询到的user信息放到一个对象里。获取动态列表则是把所有的动态全部获取到。用到了分页查询,参数直接跟在url后面。

创建评论表的时候要注意,评论是对于动态的评论,因此需要记录动态的id,还有就是对于评论的评论(回复评论),因此需要一个评论id字段。当动态删除了,对应的评论也应该一并删除。外键上设置(ON DELETE CASCADE ON UPDATE CASCADE)

设置外键的时候要注意他们的关联情况,一起删除,还是不影响。

CREATE TABLE IF NOT EXISTS comment(
    id INT PRIMARY KEY AUTO_INCREMENT,
    content VARCHAR(1000) NOT NULL,
    moment_id INT NOT NULL,
    user_id INT NOT NULL,
    comment_id INT DEFAULT NULL,
    creatAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updateAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY(moment_id) REFERENCES moment(id) ON DELETE CASCADE ON UPDATE CASCADE,
    FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
    FOREIGN KEY(comment_id) REFERENCES comment(id) ON DELETE CASCADE ON UPDATE CASCADE
);

封装校验用户是否具备操作动态,评论···的中间件,获取表名,用户名id,动态,评论id,进行查询。字符串替换,replace方法

const commentid replace('id','') 把commentid 中的id替换为空字符串。数组也可以解构赋值。

查询动态列表的时候,不仅需要额外查询到用户相关的信息,还需要查询到评论条数,使用到了子查询,聚合函数

SELECT 
    m.id id,m.content content,m.createAt createTime,m.updateAt updateTime,
    JSON_OBJECT('id',u.id,'name',u.name) author,
        (SELECT COUNT(*) FROM comment WHERE comment.moment_id = m.id) commentCount
    FROM moment m
    LEFT JOIN users u ON m.user_id = u.id
    LIMIT 0,5;

理解node里面的相对路径,到底是相对于谁的,是相对于项目启动的目录。

点赞功能的数据库是多对多关系。标签相关视频里有提到过。

上传图片(文件的逻辑),首先应该有一个上传图片的接口,服务器端保存图片,保存图片信息到数据库。然后提供一个接口,可以让用户获取图片,然后在请求用户信息的时候同时获取图片的信息。

使用koa-multer处理文件的上传。文件信息在ctx.req.file中。

根据用户id获取用户头像信息,文件保存在服务器是一个buffer,所以给客户端响应使用fs读取文件后的结果,不过还需要设置响应的类型为图片,浏览器可以解析,否则浏览器会直接下载文件。具体做法是使用用户id获取到保存在数据库中的头像信息,然后根据头像信息找到保存文件夹中的头像,用fs读取,响应给客户端。

ctx.response.set('content-type',avaterMessage[0].mimetype)
ctx.body = fs.createReadStream(`./uploads/avater/${avaterMessage[0].filename}`)

用户表里还应该包含用户的头像信息,所以要增加一个字段。这里有个数据库设计问题,头像相关信息也可以直接保存在用户表里,但是提取到了专门的头像表里,只保存头像的url到用户表,更加简洁。

上传头像后还需把头像url保存到用户表。注意,不是保存相对路径,而是 http://localhost:8888/users/5/avater 这样的格式。

参数不属于url?

注意,执行数据库语句,传入的参数只能是字符串

在这里插入图片描述

发表评论需要携带的参数为 内容和动态id content,momentId 。

回复评论 commentRouter.post('/:commentid/replay' 需要携带的参数为 content momentId commentid。

修改评论需要携带的参数为 content commentid。

修改用户信息,在url传入用户id作为参数,表示要修改的用户。在在body中携带要传递的参数。

用户权限管理,设置的相对简单,给用户表添加 isAdministrators 字段,生成token的时候带上它,用户登录后从token里拿出来,判断是否为管理员。是管理员才渲染管理员界面,当然,在进行管理员相关操作的时候应该加一个middlware校验是否为管理员。

管理员发布消息,必须要登,并且要是管理员身份(从token获取)。

管理员获取所有用户,根据前端这边传过去的偏移量和数量({{baseURL}}/administrater/allUsers?offset=0&size=5),在数据库分页查询 、

在获取动态信息的时候使用子查询,获取到动态配图的url,并且放进一个数组中使用concat做一个字符串拼接。

SELECT 
    m.id id,m.content content,m.createAt createTime,m.updateAt updateTime,
    JSON_OBJECT('id',u.id,'name',u.name,'avaterUrl',u.avater_url) author,
    (SELECT JSON_ARRAYAGG(CONCAT('http://localhost:8888/images/',file.filename)) FROM file WHERE m.id=file.moment_id) images
    FROM moment m
    LEFT JOIN users u ON m.user_id = u.id
    WHERE m.id = ?;

获取动态配图,因为动态配图可以使一张,也可以是很多张,所以思路和获取头像不一样。用momentid查询的话会出来很多张,所以应该用filename查询,再给前端返回图片。

服务器端对上传图片的处理,把一张图片变为三张尺寸不同的图片,以适应前端的需求。借助jimp库。在上传动态配图的接口添加中间件。但是存储在数据库中的只有原图片。

const pictureResize = async(ctx,next)=>{
  const files = ctx.req.files
  for(let file of files){
    const destination = path.join(file.destination,file.filename)
    // 读取到图片,得到一个image对象,写入对应文件夹
    jimp.read(file.path).then(image=>{
      // 高度自适应
      image.resize(1280,jimp.AUTO).write(`${destination}-larg`)
      image.resize(720,jimp.AUTO).write(`${destination}-middle`)
      image.resize(320,jimp.AUTO).write(`${destination}-small`)
    })

获取动态配图的时候可以在filename后面拼接参数。获得对应图片的大小。

let { filename } = ctx.params
    // 查询图片相关信息
    const pictureMessge = await getPicture(filename)
    const { type } = ctx.query
    const types = ['small','middle','larg']
    if(types.some(item=>item === type)){
      filename = filename +'-'+ type
    }
    ctx.response.set('content-type',pictureMessge[0].mimetype)
    ctx.body = fs.createReadStream(`./uploads/picture/${filename}`)

用户点赞功能,这是一个多对多关系,一个用户可以点赞多条动态,一条动态可以被多个用户点赞。所以需要在他们之间穿件一个关系表。点赞只需要把用户id和动态id插入关系表即可。在获动态列表的时候还要获取到点赞数量(子查询)

根据动态id获取单条动态点赞情况,动态表左连接关系表再左连接用户表

async likeStuations(mid){
    const statement = `SELECT m.id,m.content,m.user_id,m.createAt,u.name,u.avater_url
    FROM moment m
    LEFT JOIN user_like_moment ulm ON m.id=ulm.moment_id
    LEFT JOIN users u ON u.id=ulm.user_id
    WHERE m.id=?;`
    const res = await connection.execute(statement,[mid])
    return res[0]
  }

获取用户所有动态的点赞情况,动态表左连接关系表再左连接用户表(条件是根据用户的id),也就是先查询到用户的所有动态,然后左连接关系表(根据momentid),然后再左连接用户表,查询到点赞的用户。

注意,上线后也要替换地址

SELECT 
    m.id id,m.content content,u.name,u.avater_url,ulm.createAt,u.name uname,
    (SELECT JSON_ARRAYAGG(CONCAT('http://localhost:8888/moment/images/',file.filename)) FROM file WHERE m.id=file.moment_id) images
    FROM moment m
        LEFT JOIN user_like_moment ulm ON m.id=ulm.moment_id
        LEFT JOIN users u ON u.id=ulm.user_id
    WHERE m.user_id = ?
    ORDER BY id DESC

注意,获取动态配图的时候用的本地地址(请求动态列表),上线后要改掉。

修改用户头像逻辑(也需要登录),应该先根据userid查询用户是否已经有头像了,如果能查询到头像信息,则需要把保存在文件中的头像删除,然后在保存用户头像,修改头像表里的数据。

  // 删除文件夹中的头像
  async removeFile(ctx,next){
    const { id } = ctx.user
    const fileInfo = await getAvaterInfo(id)
    if(fileInfo){
      // 用户已经有头像了 删除文件夹中的头像
      const filename = fileInfo[0].filename
      fs.unlink(`./uploads/avater/${filename}`,function(error){
        if(error){
            console.log(error)
            return false
        }
        console.log('删除文件成功')
    })
    }
    await next()
  }

koa-multer上传图片这样写才有效果。

var avaterStorage = Multer.diskStorage({
  //头像文件保存路径
  destination: function(req, file, cb) {
  //存储目录是手动建的 好像没办法自动建立二级目录
      cb(null, './uploads/avater')
  },
  //修改文件名称
  // filename: function(req, file, cb) {
  //     var fileFormat = (file.originalname).split(".")
  //     cb(null, Date.now() + "." + fileFormat[fileFormat.length - 1])
  // }
})
​
//加载配置
var avaterUpload = Multer({ storage: avaterStorage })
​
// 单个头像文件
const avaterHandler = avaterUpload.single('file')

聊天室逻辑:

socket基于原来的服务器?

单页面路由跳转貌似不会断开来连接?页面刷新会断开连接

(是否可以考虑创建的是全校的聊天室,也就是全校的人进入一个聊天室)

用户点击进入 聊天室,向socket服务器发送一个事件,把用户信息传给socket服务器。用一个数组记录用户是否已经进入聊天室,记录用户信息?

有人进入聊天室,给所有用户广播 io.emit() ,提示谁进入了群聊 而socket.emit()只能给当前用户发消息。并且把用户信息保存起来,socket.user = xxx

用户进入聊天室,还应该给用户返回当前聊天室总人数,以及每个用户的信息,以供前端渲染在页面上。

用户离开聊天室,socket.on('disconnect',()=>{}),1把用户数据从user数组中删除,2告诉所有人,有人离开(发送用户信息到前端)。3更新前端用户列表。

用户发消息,1给服务器发送 用户信息,消息。,存储到数据库? 2 广播给所有用户 。3,前端渲染消息,要判断是自己的消息还是别人的消息,别人的,渲染在左边,显示用户名。自己的,渲染在右边,不显示用户名。

让当前的页面元素滚动到浏览器窗口的可视区域 element.scrollItoView(),给盒子里的最后一个dom元素加上。

发送图片功能,使用a标签包裹lable和input标签。前端这边需要把文件发送到服务器,借助于H5新增的fileReader

//获取图片
var file = this.files[0]
//读取成二进制文件
let fr = new FileReader()
fr.readAsDataURL(file)
fr.onload = function(){
    socket.emit('sendImage',{
        username:xxx
        avater:xxx
        img:fr.result
    })
}

图片滚动到底部不好使,因为图片还没有加载完成,需要等待图片加载完成

xxx.on('load',function(){
    scrollIntoView()
})

css有个calc函数,可用于计算 移动元素的位置可以用 transform里的translate 透明度可用opcity,这个是dom元素透明度 rgba这个是颜色透明度 伪元素选择器有个 ::placeholder 可以选择到里面的 flex:1表示子元素划分父元素剩余空间的份数

router注意有两个坑,一是子路由路径前面不加 / 二是跳转的时候路径要写完整 ‘/message/system’ 前面那个斜杠一定要加

动态里面的图像大小处理暂时不知道。?

background-size属性的值可以是预定义关键字 cover | contain,也可以是使用长度值、百分比或 auto 定义背景图像的尺寸,长度和百分比不允许负值。

使用预定义值时,cover 表示背景图像完全覆盖容器的背景区域,如果图像过大或过小,则会将背景图像等比缩放到完全覆盖容器,背景图像有可能超出容器;contain 表示背景图像始终只填充容器的背景区域,如果图像过大或过小,也会对背景图像等比缩放。但是,当宽度与容器的宽度相等,或高度与容器的高度相等时,则停止缩放。所以,停止放大后,某个方向可能没有完全覆盖,则会显示背景颜色。

使用长度、百分比或 auto 定义尺寸时,需要提供两个参数。如果提供两个,第一个为背景图像的宽度,第二个为背景图像的高度;如果只提供一个,该值为背景图像的宽度,第 2 个值默认为 auto,即高度为 auto,背景图像按提供的宽度等比缩放。

在使用vuex的action发送网络请求时会出现很多问题,应为在页面中dispatch,这是异步的,而下面的赋值语句是同步的,得到的就会是state里面的初始数据,解决办法是使用awaite dispatch,或者使用计算属性赋值。

系统消息页面也是必须要登录才能获取数据。对token和userid做了持久化。

有很多接口必须登录后才能调用,所以在用户登录后,在请求拦截器添加token。

消息页面和聊天页面必须要登录才能进入。也就是说本地必须要有token。使用路由导航守卫。

登录后显示用户头像,用户名,未登录显示登录/注册。

pinia坑 当你通过action里面的方法,对state里面的数据进行操作完成后,不要立即切换路由,否则数据可能存不到本地,稍等一会再切。

用户个人信息级联选择器的数据使用的是,element-china-area-data 插件。

由于设计的失误,在登录成功的时候后再调用获取用户信息的接口,保存到store做持久化。在header里面有获取用户信息。

是管理员才会显示 管理员权限。

前端图片上传,使用element 的upload组件实现。

router.go(0)实现页面的强制刷新。

在上传图片的时候,传递图片数据用的是FormData。直接将整个formdata作为参数。formData是ajax2.0(XMLHttpRequest Level2)新提出的接口,利用FormData和可以提高工作效率。它的主要用途有两方面:

  • 将form表单元素的name与value进行组合,实现表单数据的序列化,从而减少表单元素的拼接,提高工作效率。
  • 异步上传文件
//通过FormData构造函数创建一个空对象
var formdata=new FormData();
//可以通过append()方法来追加数据
formdata.append("name","laotie");
//通过get方法对值进行读取
console.log(formdata.get("name"));//laotie
//通过set方法对值进行设置
formdata.set("name","laoliu");
console.log(formdata.get("name"));//laoliu

在form表单中如果要上传文件,一定要将headers参数 Content-Type=”multipart/form-data”

使用防抖函数对页面部分功能做防抖处理。防抖函数封装在utils中。

动态是自己封装的组件,图片的展示规则根据图片的数量判断。动态组件中有使用到封装的comment组件。

图片裁剪几种方方式:

CSS 是具备裁剪网页中图片的能力的,如果是 背景图片,那么使用的裁剪方法是 background-size 属性。而如果是普通图片,那么使用的裁剪方法则是 object-fit。他们两者的裁剪模式大致相同,都是fill、contain、cover、scale-down ... 这几种。

object-fit: cover

该模式是最常见的裁剪模式,作用是把图片等比例裁剪,并且居中。其中有一边是一定会刚好填充完整的,剩余的将会裁剪掉(这里的填充是一定是会填满整个内容的)

object-fit: fill

这个属性的作用是将图片拉伸填满整个新设置的大小里面,实际中用的不多的一个原因是它拉伸之后会使 图片扭曲变形

object-fit: contain

这个属性也是一种比较有意思的裁剪模式,它是按原图的比例把图片缩放在新设置的大小里面,但不作任何裁剪也没作填充。

object-fit: contain 裁剪后显示如下。

发表动态bug,内容长度问题,删除图片后的问题。发表动态后,动态配图不显示问题(用计算属性获取可解决(原始值))。

无限滚动问题

你需要做的是从你安装的element-plus中引入InfiniteScroll,并进行插件的注册,做法如下:

import InfiniteScroll from "element-plus";
const app = createApp(App);
app.use(InfiniteScroll); 

infinite-scroll-distance="1"(触发加载的距离阈值) 、

滚动到底部请求新的5条数据,合并到data中传给动态组件。动态组件渲染。

表情用的插件 V3Emoji。

整个上传动态配图的逻辑是,先上传动态,成功后拿到动态id,在根据id上传配图。而配图是根据 elupload onchange事件得到file对象后append到一个fomdata中。

封装的moment组件需要传入动态数据,是否展示举报动态report。

删除动态后,向父组件发出事件,父组件重新请求删除后剩余的动态。

点赞逻辑,需要登录,先查询自己有无对当前动态点赞,用以改变样式,根据点赞请求返回的结果判断是点赞还是取消点赞。由于技术问题(父传子,moment数据已经拿到了,无法更新),点赞成功点赞数量++,失败,数量--。

在用vidio播放音乐的时候,在setup中调用play方法失败,原因是,此时组件还未渲染。经过测试可知,他们的执行顺序应该是,setup中的同步任务最快,然后是渲染组件,最后才是异步任务。

行内块元素(图片)默认沿基线对齐,解决办法,转化为块元素。