目前因学业任务比较重,没有好好的完善,现在比较完善的只有题库管理,新增题库,修改题库以及登录的功能,但搭配小程序使用,主体功能已经实现了
此后台系统是为了搭配我的另一个项目 School-Partners学习伴侣
微信小程序而开发的。是一个采用Taro
多端框架开发的跨平台的小程序。感兴趣的可以看一下之前的文章
希望大佬们走过路过可以给个star鼓励一下~感激不尽~
https://github.com/zhcxk1998/...
这个是小程序的介绍文章
小程序介绍文章,使劲戳!
无图无真相!先上几个图~
运行截图
1. 登录界面
2. 题库管理
3. 修改题库
技术分析
就来说一下项目中自己推敲做出来的几个算是亮点的东西吧
1. 使用Hook封装API访问工具
本项目采用的UI框架是Ant-Design框架
因为这个项目的后台对于表格有着比较大的需求,而表格加载就需要使用到Loading
的状态,所以就特地封装一下便于之后使用
首先我们先新建一个文件useService.ts
然后我们先引入axios
来作为我们的api访问工具
import axios from 'axios'
const instance = axios.create({
baseURL: '/api',
timeout: 10000,
headers: {
'Content-Type': "application/json;charset=utf-8",
},
})
instance.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.common['Authorization'] = token;
}
return config
},
error => {
return Promise.reject(error)
}
)
instance.interceptors.response.use(
res => {
let { data, status } = res
if (status === 200) {
return data
}
return Promise.reject(data)
},
error => {
const { response: { status } } = error
switch (status) {
case 401:
localStorage.removeItem('token')
window.location.href = './#/login'
break;
case 504:
message.error('代理请求失败')
}
return Promise.reject(error)
}
)
先将axios
的拦截器,基本配置这些写好先
接着我们实现一个获取接口信息的方法useServiceCallback
const useServiceCallback = (fetchConfig: FetchConfig) => {
// 定义状态,包括返回信息,错误信息,加载状态等
const [isLoading, setIsLoading] = useState(false)
const [response, setResponse] = useState(null)
const [error, setError] = useState(null)
const { url, method, params = {}, config = {} } = fetchConfig
const callback = useCallback(
() => {
setIsLoading(true)
setError(null)
// 调用axios来进行接口访问,并且将传来的参数传进去
instance(url, {
method,
data: params,
...config
})
.then((response: any) => {
// 获取成功后,则将loading状态恢复,并且设置返回信息
setIsLoading(false)
setResponse(Object.assign({}, response))
})
.catch((error: any) => {
const { response: { data } } = error
const { data: { msg } } = data
message.error(msg)
setIsLoading(false)
setError(Object.assign({}, error))
})
}, [fetchConfig]
)
return [callback, { isLoading, error, response }] as const
}
这样就完成了主体部分了,可以利用这个hook来进行接口访问,接下来我们再做一点小工作
const useService = (fetchConfig: FetchConfig) => {
const preParams = useRef({})
const [callback, { isLoading, error, response }] = useServiceCallback(fetchConfig)
useEffect(() => {
if (preParams.current !== fetchConfig && fetchConfig.url !== '') {
preParams.current = fetchConfig
callback()
}
})
return { isLoading, error, response }
}
export default useService
我们定义一个useService的方法,我们通过定义一个useRef
来判断前后传过来的参数是否一致,如果不一样且接口访问配置信息的url
不为空就可以开始调用useServiceCallback
方法来进行接口访问了
具体使用如下:
我们先在组件内render外使用这个钩子,并且定义好返回的信息
接口返回体如下
const { isLoading = false, response } = useService(fetchConfig)
const { data = {} } = response || {}
const { exerciseList = [], total: totalPage = 0 } = data
因为我们这个hook是依赖fetchConfig
这个对象的,这里是他的类型
export interface FetchConfig {
url: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
params?: object,
config?: object
}
所以我们只需要再页面加载时候调用useEffect
来进行更新这个fetchConfig
就可以触发这个获取数据的hook啦
const [fetchConfig, setFetchConfig] = useState({
url: '', method: 'GET', params: {}, config: {}
})
...
useEffect(() => {
const fetchConfig: FetchConfig = {
url: '/exercises',
method: 'GET',
params: {},
config: {}
}
setFetchConfig(Object.assign({}, fetchConfig))
}, [fetchFlag])
这样就大功告成啦!然后我们再到表格组件内传入相关数据就可以啦
setCurrentPage(pageNo)
}}
locale={{
emptyText:
}}
/>
大功告成!!
2. 实现懒加载通用组件
我们这里使用的是react-loadable
这个组件,挺好用的嘿嘿,搭配nprogress
来进行过渡处理,具体效果参照github
网站上的加载效果
我们先封装好一个组件,在components/LoadableComponent
内定义如下内容
import React, { useEffect, FC } from 'react'
import Loadable from 'react-loadable'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
const LoadingPage: FC = () => {
useEffect(() => {
NProgress.start()
return () => {
NProgress.done()
}
}, [])
return (
)
}
const LoadableComponent = (component: () => Promise) => Loadable({
loader: component,
loading: () => ,
})
export default LoadableComponent
我们先定义好一个组件LoadingPage
这个是我们再加载中的时候需要展示的页面,在useEffect
中使用nprogress
的加载条进行显示,组件卸载时候则结束,而下面的div
则可以由用户自己定义需要展示的样式效果
下面的LoadableCompoennt
就是我们这个的主体,我们需要获取到一个组件,赋值给loader
,具体的赋值方法如下,我们可以在项目内的pages
部分将所有需要展示的页面引入进来,再导出,这样就可以方便的实现所有页面的懒加载了
// 引入刚刚定义的懒加载组件
import { LoadableComponent } from '@/admin/components'
// 定义组件,传给LoadableCompoennt组件需要的组件信息
const Login = LoadableComponent(() => import('./Login'))
const Register = LoadableComponent(() => import('./Register'))
const Index = LoadableComponent(() => import('./Index/index'))
const ExerciseList = LoadableComponent(() => import('./ExerciseList'))
const ExercisePublish = LoadableComponent(() => import('./ExercisePublish'))
const ExerciseModify = LoadableComponent(() => import('./ExerciseModify'))
// 导出,到时候再从这个pages/index.ts中引入,即可拥有懒加载效果了
export {
Login,
Register,
Index,
ExerciseList,
ExercisePublish,
ExerciseModify
}
大功告成!!!
3. 使用嵌套路由
项目因为涉及到后台信息的管理,所以个人认为导航栏与主题信息栏应该一同显示,如同下图
这样可以清晰的展示出信息以及给用户提供导航效果
我们现在项目的routes/index.tsx
定义一个全局通用的路由组件
import React from 'react'
import {
Switch, Redirect, Route,
} from 'react-router-dom'
// 这个是私有路由,下面会提到
import PrivateRoute from '../components/PrivateRoute'
import { Login, Register } from '../pages'
import Main from '../components/Main/index'
const Routes = () => (
)
export default Routes
这里的意思就是,登录以及注册页面是独立开来的,而Main这个组件就是负责包裹导航条以及内容部分的组件啦
接下来看看components/Main
中的内容吧
import React, { ComponentType } from 'react'
import { Layout } from 'antd';
import HeaderNav from '../HeaderNav'
import ContentMain from '../ContentMain'
import SiderNav from '../SiderNav'
import './index.scss'
const Main = () => (
// 头部导航栏
// 侧边栏
// 主体内容
)
export default Main as ComponentType
接下来重点就是这个ContentMain
组件啦
import React, { FC } from 'react'
import { withRouter, Switch, Redirect, RouteComponentProps, Route } from 'react-router-dom'
import { Index, ExerciseList, ExercisePublish, ExerciseModify } from '@/admin/pages'
import './index.scss'
const ContentMain: FC = () => {
return (
)
}
export default withRouter(ContentMain)
这个就是一个嵌套路由啦,在这里面使用withRouter来包裹一下,然后在这里再次定义路由信息,这样就可以只切换主体部分的内容而不改变导航栏啦
大功告成!!!
4. 侧边栏的选中部分动态变化
通过图片我们可以看出,侧边导航栏有一个选中的内容,那么我们该如何判断不同的url页面对应哪一个选中部分呢?
const [selectedKeys, setSelectedKeys] = useState(['index'])
const [openedKeys, setOpenedKeys] = useState([''])
const { location: { pathname } } = props
const rank = pathname.split('/')
useEffect(() => {
switch (rank.length) {
case 2: // 一级目录
setSelectedKeys([pathname])
setOpenedKeys([''])
break
case 4: // 二级目录
setSelectedKeys([pathname])
setOpenedKeys([rank.slice(0, 3).join('/')])
break
}
}, [pathname])
这就是最重要的部分啦,我们通过定义几个状态selectedKeys
选中的条目,openedKeys
打开的多级导航栏
我们通过在页面加载时候,判断页面url路径,如果是一级目录,例如首页,就直接设置选中的条目即可,如果是二级目录,例如导航栏中内容管理/题库管理
这个功能,他的url链接是/admin/content/exercise-list
,所以我们的case 4
就可以捕获到啦,然后设置当前选中的条目以及打开的多级导航,具体的导航信息请看下面
大功告成!!!
5. 使用JWTToken来验证用户登录状态以及返回信息
要想使用登录注册功能,还有用户权限的问题,我们就需要使用到这个token啦!为什么我们要使用token呢?而不是用传统的cookies呢,因为使用token可以避免跨域啊还有更多的复杂问题,大大简化我们的开发效率
本项目后台采用nodeJs来进行开发
我们先在后台定义一个工具utils/token.js
// token的秘钥,可以存在数据库中,我偷懒就卸载这里面啦hhh
const secret = "zhcxk1998"
const jwt = require('jsonwebtoken')
// 生成token的方法,注意前面一定要有Bearer ,注意后面有一个空格,我们设置的时间是1天过期
const generateToken = (payload = {}) => (
'Bearer ' + jwt.sign(payload, secret, { expiresIn: '1d' })
)
// 这里是获取token信息的方法
const getJWTPayload = (token) => (
jwt.verify(token.split(' ')[1], secret)
)
module.exports = {
generateToken,
getJWTPayload
}
这里采用的是jsonwebtoken
这个库,来进行token的生成以及验证。
有了这个token啦,我们就可以再登录或者注册的时候给用户返回一个token信息啦
router.post('/login', async (ctx) => {
const responseBody = {
code: 0,
data: {}
}
try {
if (登录成功) {
responseBody.data.msg = '登陆成功'
// 在这里就可以返回token信息给前端啦
responseBody.data.token = generateToken({ username })
responseBody.code = 200
} else {
responseBody.data.msg = '用户名或密码错误'
responseBody.code = 401
}
} catch (e) {
responseBody.data.msg = '用户名不存在'
responseBody.code = 404
} finally {
ctx.response.status = responseBody.code
ctx.response.body = responseBody
}
})
这样前端就可以获取这个token啦,前端部分只需要将token存入localStorage
中即可,不用担心localStorage
是永久保存,因为我们的token有个过期时间,所以不用担心
/* 登录成功 */
if (code === 200) {
const { msg, token } = data
// 登录成功后,将token存入localStorage中
localStorage.setItem('token', token)
message.success(msg)
props.history.push('/admin')
}
好嘞,现在前端获取token也搞定啦,接下来我们就需要在访问接口的时候带上这个token啦,这样才可以让后端知道这个用户的权限如何,是否过期等
需要传tokne给后端,我们可以通过每次接口都传一个字段token
,但是这样十分浪费成本,所以我们再封装好的axios
中,我们设置请求头信息即可
import axios from 'axios'
const instance = axios.create({
baseURL: '/api',
timeout: 10000,
headers: {
'Content-Type': "application/json;charset=utf-8",
},
})
instance.interceptors.request.use(
config => {
// 请求头带上token信息
const token = localStorage.getItem('token');
if (token) {
config.headers.common['Authorization'] = token;
}
return config
},
error => {
return Promise.reject(error)
}
)
...
export default instance
如上图所示,我们每次请求接口的时候就会带上这个请求头啦!那么接下来我们就谈谈后端如何获取这个token并且验证吧
有获取token,以及验证部分,那么就需要出动我们的中间件啦!
我们验证token的话,要是用户是访问的登录或者注册接口,那么这个时候token其实是没有作用哒,所以我们需要将它隔离一下,所以我们定义一个中间件,用来跳过某些路由,我们再middleware/verifyToken.js
中定义(这里我们采用koa-jwt
来验证token)
const koaJwt = require('koa-jwt')
const verifyToken = () => {
return koaJwt({ secret: 'zhcxk1998' }).unless({
path: [
/login/,
/register/
]
})
}
module.exports = verifyToken
这样就可以忽略这登录注册路由啦,别的路由就验证token
拦截已经成功啦,那么我们该如何捕获,然后进行处理呢?我们再middleware/interceptToken
定义一个中间件,来处理捕获的token信息
const interceptToken = async (ctx, next) => {
return await next().catch((err) => {
const { status } = err
if (status === 401) {
ctx.response.status = 401
ctx.response.body = {
code: 401,
data: {
msg: '请登录后重试'
}
}
} else {
throw err
}
})
}
module.exports = () => (
interceptToken
)
由于koa-jwt
拦截的token,如果过期,他会自动抛出一个401的异常以表示该token已经过期,所以我们只需要判断这个状态status
然后进行处理即可
好嘞,中间件也定义好了,我们就在后端服务中使用起来吧!
const Koa = require('koa')
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser')
const cors = require('koa2-cors');
const routes = require('../routes/routes')
const router = new Router()
const admin = new Koa();
const {
verifyToken,
interceptToken
} = require('../middleware')
const {
login,
info,
register,
exercises
} = require('../routes/admin')
admin.use(cors())
admin.use(bodyParser())
/* 拦截token */
admin.use(interceptToken())
admin.use(verifyToken())
/* 管理端 */
admin.use(routes(router, { login, info, register, exercises }))
module.exports = admin
我们直接使用router.use()
的方法就可以使用中间件啦,这里要记住!验证拦截token一定要在路由信息之前,否则是拦截不到的哟(如果在后面,路由都先执行了,还拦截啥嘛!)
大功告成!!!
6. 密码使用加密加盐的方式存储
我们在处理用户的信息的时候,需要存储密码,但是直接存储肯定不安全啦!所以我们需要加密以及加盐的处理,在这里我用到的是crypto
这个库
首先我们再utils/encrypt.js
中定义一个工具函数用来生成盐值以及获取加密信息
const crypto = require('crypto')
// 获取随机盐值,例如 c6ab1 这样子的字符串
const getRandomSalt = () => {
const start = Math.floor(Math.random() * 5)
const count = start + Math.ceil(Math.random() * 5)
return crypto.randomBytes(10).toString('hex').slice(start, count)
}
// 获取密码转换成md5之后的加密信息
const getEncrypt = (password) => {
return crypto.createHash('md5').update(password).digest('hex')
}
module.exports = {
getRandomSalt,
getEncrypt
}
这样我们就可以通过验证密码与数据库中加密的信息对不对得上,来判断是否登录成功等等
我们现在注册中使用上,当然我们需要两个表进行数据存储,一个是用户信息,一个是用户密码表,这样分开更加安全,例如这样
这样就可以将用户信息还有密码分开存放,更加安全,这里就不重点叙述啦
const { getRandomSalt, getEncrypt } = require('../../utils/encrypt')
// 注册部分
router.post('/register', async (ctx) => {
const { username, password, phone, email } = ctx.request.body
// 获取盐值以及加密后的信息
const salt = getRandomSalt()
// 数据库存放的密码是由用户输入的密码加上随机盐值,然后再进行加密所得到的的炒鸡加密密码
const encryptPassword = getEncrypt(password + salt)
// 插入用户信息,以及获取这个的id
const { insertId: user_id } = await query(INSERT_TABLE('user_info'), { username, phone, email });
// 插入用户密码信息,user_id与上面对应
await query(INSERT_TABLE('user_password'), {
user_id,
password: encryptPassword,
salt
})
...
})
接下来再来看登录部分,登录的话,就需要从用户密码表中取出加密密码,以及盐值,然后进行对比
// 通过用户名,先获取加密密码以及盐值
const { password: verifySign, salt } = await query(`select password, salt from user_password where user_id = '${userId}'`)[0]
// 这个就是用户输入的密码加上盐值一起加密后的密码
const sign = getEncrypt(password + salt)
// 这个加密的密码与数据库中加密的密码对比,如果一样则登陆成功
if (sign === verifySign) {
responseBody.data.msg = '登陆成功'
responseBody.data.token = generateToken({ username })
responseBody.code = 200
} else {
responseBody.data.msg = '用户名或密码错误'
responseBody.code = 401
}
大功告成!!!
结语
大部分的内容就大概这样子,这是自己开发中遇到的小问题还有解决方法,希望对大家有所帮助,大家一起成长!现在得看看面试题准备一波春招了,不然大学毕业了都找不到工作啦!有时间再继续更新这个文章!
最后还是顺便求一波star还有点赞!!!
github项目猛戳进来star一下嘿嘿
小程序介绍文章,使劲戳!
你可能感兴趣的:(前端,node.js,react.js,typescript,jwt)