项目面经
1.nokosocial社交网站
用户注册
后端创建用户表,创建校验的中间件,用户名密码通过校验后(是否重名),对于用户密码,使用node自带的库crypto进行md5加密然后next,将用户数据插入数据库。
用户登录
-
后端拿到用户信息,通过校验的中间件,在数据库查找是否有该用户。密码是否正确,通过校验,使用jwt颁发验证token(http是无状态的,需要登录凭证(为什么不用coookie和session,因为coolie会附着在每个请求上,无形中增加了流量,cookie是明文传输的,存在安全性问题,cookie限制4k)。使用的是非对称加密,私钥颁发令牌,公钥验证令牌。(可以设置过期时间)。在返回的数据中携带上token。
-
前端登录成功后得到token,以及用户信息保存早pina中,并且做持久化存储,可以在axios请求拦截器中在请求头添加token。
-
对于验证用户是否有登录,后端添加校验的中间件,token在请求头的authorization中,如果用户根本没有传入token,就根本没有authorization这个字段,注意,要去除掉Bearer,jwt.verify(公钥)进行校验,通过将用户信息保存到 ctx.user await next()。
-
对于用户是否有权限跳转到某一页面,在路由前置守卫中进行判断。
发表动态
-
后端首先验证用户是否登录(有无token),调用校验token的中间件。通过,拿到用户id和content ,将他们插入数据库。
-
动态配图上传,后端使用koa-multer处理文件上传,将文件保存到服务器文件夹中,动态配图可以是多张,并且在获取图片的时候可以展示大小不同的图片。要对上传的图片做处理(变化为3张尺寸不同的图片),需要使用到jimp,对fiel进行遍历,resize图片大小给图片尾部拼接small,middle,large。注意要拼接图片的url保存起来。
const pictureResize = async (ctx, next) => {
console.log(ctx.req.files)
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`)
})
}
await next()
}
-
获取图片则是通过flename加图片尺寸获取,返回数据前设置响应类型为图片,然后读取文件,返回。图片创建了一个表来保存基本信息。
-
动态配图是动态发表之后添加的,所以图片表里还应记录动态的id。前端这边使用到element的upload组件,获取到图片后,new一个fomdata,append图片进去,通过data传入整个fomdata,发起请求。
发表评论
主要是在后端建表的时候注意,分为几种情况,一是动态下的评论,二是对于评论的评论,当动态删除,评论也应该删除,评论删除,评论的评论也应该删除。
//分为几种情况,一是动态下的评论,二是对于评论的评论,当动态删除,评论也应该删除,评论删除,评论的评论也应该删除
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,//评论的评论相关信息,有,评论id,无,null
FOREIGN KEY(moment_id) REFERENCES `moment`(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY(user_id) REFERENCES `user`(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY(comment_id) REFERENCES `comment`(id) ON DELETE CASCADE ON UPDATE CASCADE
);
点赞
主要是后端表设计,是一个多对多的关系,在动态与用户之间创建一个关系表,记录那个用户点赞了那条动态。
2.React新闻发布管理系统
menu
根据后端返回的数据递归动态渲染menu,后端返回有key,点击根据key进行页面的跳转。
function renderMenu(list) {
let arr = []
list.map((item) => {
if (item.children && item.children.length !== 0) {
return arr.push(getItem(item.title, item.key, renderMenu(item.children)))
} else {
return item.pagepermission && arr.push(getItem(item.title, item.key))
}
})
return arr
}
刷新后原来点击项仍然高亮
通过 const location = useLocation() // 接收跳转传过来的值(path,state)获取当前url地址,作为参数传入。
角色列表中分配权限
主要就是树形控件的制作,根据后端返回的权限列表数据,渲染初始状态的树形控件。
点击显示对话框的时候,获得当前角色信息(里面包含自己拥有的权限)的权限,保存为state(currentRights),用以渲染勾选状态。
// 当前点击的某一角色,获取他具有的权限(来自用户数据)
const [currentRights, changeCurrentRights] = useState([])
// 处理对话框
const [isModalOpen, setIsModalOpen] = useState(false)
// 打开对话框 当前点击的某一角色,获取他具有的权限(来自用户数据)
const showModal = (item) => {
setIsModalOpen(true)
changeCurrentRights(item.rights)
setRoleId(item.id) // 角色id
}
// 点击勾选或取消勾选 返回的是剩余的权限
const onCheck = (checkKeys) => {
changeCurrentRights(checkKeys.checked) //赋值给当前应该勾选的权限
}
点击切换勾选状态,调用组件自身的方法,获得剩余权限,给currentRights。
点击ok,根据先获取到的角色id,更改角色数据里的权限数据。然后更改后端数据。
// 点击确定修改
const handleOk = () => {
console.log(rolesData)
setIsModalOpen(false)
// 同步前端数据(修改角色数据中的权限)
setTableData(tableData.map((item) => {
if (item.id == roleId) {
return {
...item,
rights: currentRights // 替换
}
} else {
return item
}
}))
// 发起后端修改
zrequest.request({
method: 'patch',
url: `/roles/${roleId}`,
data: {
rights: currentRights
}
})
用户列表
动态路由
有一个问题,比如区域管理员登录后,自己只能看到自己有权限的东西,侧边栏没有的东西不会显示出来,但是,可以通过地址栏访问到。因为已经把所有的路由匹配关系创建好了。
解决:
- 通过路由导航守卫解决。
- 通过动态创建路由匹配关系解决。
因为用户的权限是可以随意修改的,所以用动态创建路由匹配比较好。
使用自定义hooks函数,拿到用户具有的权限,与根据所有权限创建出来的路由映射表,做一个匹配,返回匹配后的路由表。放在routes数组中。
import React, { useEffect, useState } from 'react'
import zrequest from '../service'
// 自定义钩子函数
export const useUtiles = () => {
let { role } = JSON.parse(localStorage.getItem('token')) // 个人具备的权限
const [allRights, setAllRights] = useState([]) // 所有权限 这里其实没啥用
useEffect(() => {
Promise.all([
zrequest.request({
method: 'get',
url: '/rights'
}),
zrequest.request({
method: 'get',
url: '/children'
})
]).then((res) => {
setAllRights([...res[0].data, ...res[1].data])
})
}, [])
// 路由懒加载
const lazyLoad = (path) => {
const Comp = React.lazy(() => import(`../views/sendBox/${path}`))
return (
<React.Suspense fallback={<>加载中...</>}>
<Comp></Comp>
</React.Suspense>
)
}
let routRelation = [
{
path: 'home',
element: lazyLoad('home/Home')
},
{
path: 'user-manage/list',
element: lazyLoad('user-manager/UserList')
},
{
path: 'right-manage/role/list',
element: lazyLoad('right-manager/RoleList')
},
{
path: 'right-manage/right/list',
element: lazyLoad('right-manager/RightList')
},
{
path: 'news-manage/add',
element: lazyLoad('news-manager/WriteNews')
},
{
path: 'news-manage/draft',
element: lazyLoad('news-manager/DraftBox')
},
{
path: 'news-manage/category',
element: lazyLoad('news-manager/NewCategory')
},
{
path: 'audit-manage/audit',
element: lazyLoad('audit-manager/AuditNews')
},
{
path: 'audit-manage/list',
element: lazyLoad('audit-manager/AuditList')
},
{
path: 'publish-manage/unpublished',
element: lazyLoad('publish-manager/Released')
},
{
path: 'publish-manage/published',
element: lazyLoad('publish-manager/ReadyPublish')
},
{
path: 'publish-manage/sunset',
element: lazyLoad('publish-manager/Offline')
}
]
let arr = []
routRelation.map((item) => {
if (role.rights.includes('/' + item.path)) {
return arr.push(item)
}
})
return arr
}
export default function YRouter() {
const element = useRoutes([
{
path: '/',
element: <Navigate to={'/home'}></Navigate>
},
{
path: '/login',
element: <Login></Login>
},
{
path: '/',
element: <SendBox></SendBox>,
children: [
{
path: '',
element: lazyLoad('home/Home')
},
// 动态路由
...useUtiles(),
{
path: '*',
element: lazyLoad('notFound/NotFound')
}
]
},
])
return (
element
)
}
撰写新闻/审核管理
逻辑:撰写新闻,可保存到草稿箱,或直接提交审核。其实就是修改auditState字段。在草稿箱中显示的都是auditState为1的。草稿箱中可选择提交审核,就是修改auditState为2(审核中),在审核列表可查看。审核列表中可以看到自己有权限(过滤数据)看到的新闻,包括各个状态,已通过,未通过,审核中,可修改。
发布管理
使用了封装的公共组件和自定义hooks
使用插槽传递不同的按钮,使用hooks传入不同type请求不同table数据。
在点击按钮的时候,如何获取每一列的id?
使用插槽的传值。
{props.children && props.children(item2.id)}
<NewsPublish tabData={tabData}>
{
(id) => {
return <Button type='primary' onClick={() => publish(id)}>发布</Button>
}
}
</NewsPublish>
发起网络请求的方法放在hooks中一起导出,需要使用到的地方导入对应的方法。(学习这种思想)
自定义hook
import { useState, useEffect } from 'react'
import { message } from 'antd'
import zrequest from "../../service"
// 自定义hook
function usePublish(type) {
const { username } = JSON.parse(localStorage.getItem('token'))
const [tabData, setTabData] = useState([])
useEffect(() => {
zrequest.request({
method: 'get',
url: `/news?author=${username}&publishState=${type}&_expand=category`
}).then((res) => {
setTabData(res.data)
})
}, [username])
// 发布
const thePublish = (id) => {
setTabData(tabData.filter(item => item.id !== id))
zrequest.request({
method: 'patch',
url: `/news/${id}`,
data: {
'publishState': 2,
'publishTime': Date.now()
}
}).then(() => {
message.success('发布成功,您可以到【发布管理/已发布】中查看')
})
}
// 下线
const sunSet = (id) => {
setTabData(tabData.filter(item => item.id !== id))
zrequest.request({
method: 'patch',
url: `/news/${id}`,
data: {
'publishState': 3,
}
}).then(() => {
message.success('下线成功,您可以到【发布管理/已下线】中查看')
})
}
// 删除
const theDelete = (id) => {
setTabData(tabData.filter(item => item.id !== id))
zrequest.request({
method: 'delete',
url: `/news/${id}`,
}).then(() => {
message.success('删除成功')
})
}
return {
tabData,
thePublish,
sunSet,
theDelete
}
}
export default usePublish