目前因學業任務比較重,沒有好好的完善,如今比較完善的只有題庫管理,新增題庫,修改題庫以及登陸的功能,但搭配小程序使用,主體功能已經實現了css
School-Partners學習伴侶
微信小程序而開發的。是一個採用Taro
多端框架開發的跨平臺的小程序。感興趣的能夠看一下以前的文章但願大佬們走過路過能夠給個star鼓勵一下~感激涕零~前端
這個是小程序的介紹文章
小程序介紹文章,使勁戳!react
無圖無真相!先上幾個圖~ios
就來講一下項目中本身推敲作出來的幾個算是亮點的東西吧git
本項目採用的UI框架是Ant-Design框架
由於這個項目的後臺對於表格有着比較大的需求,而表格加載就須要使用到Loading
的狀態,因此就特意封裝一下便於以後使用github
首先咱們先新建一個文件useService.ts
而後咱們先引入axios
來做爲咱們的api訪問工具web
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
typescript
const useServiceCallback = (fetchConfig: FetchConfig) => {
// 定義狀態,包括返回信息,錯誤信息,加載狀態等
const [isLoading, setIsLoading] = useState<boolean>(false)
const [response, setResponse] = useState<any>(null)
const [error, setError] = useState<any>(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<FetchConfig>({
url: '', method: 'GET', params: {}, config: {}
})
...
useEffect(() => {
const fetchConfig: FetchConfig = {
url: '/exercises',
method: 'GET',
params: {},
config: {}
}
setFetchConfig(Object.assign({}, fetchConfig))
}, [fetchFlag])
複製代碼
這樣就大功告成啦!而後咱們再到表格組件內傳入相關數據就能夠啦
<Table
rowSelection={rowSelection}
dataSource={exerciseList}
columns={columns}
rowKey="exerciseId"
scroll={{
y: "calc(100vh - 300px)"
}}
loading={{
spinning: isLoading,
tip: "加載中...",
size: "large"
}}
pagination={{
pageSize: 10,
total: totalPage,
current: currentPage,
onChange: (pageNo) => setCurrentPage(pageNo)
}}
locale={{
emptyText: <Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description="暫無數據" />
}}
/>
複製代碼
大功告成!!
咱們這裏使用的是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 (
<div className="load-component" />
)
}
const LoadableComponent = (component: () => Promise<any>) => Loadable({
loader: component,
loading: () => <LoadingPage />,
})
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
}
複製代碼
大功告成!!!
項目由於涉及到後臺信息的管理,因此我的認爲導航欄與主題信息欄應該一同顯示,如同下圖
這樣能夠清晰的展現出信息以及給用戶提供導航效果
咱們如今項目的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 = () => (
<Switch>
<Route exact path="/login" component={Login} />
<Route exact path="/register" component={Register} />
<PrivateRoute component={Main} path="/admin" />
<Redirect exact from="/" to="/admin" />
</Switch>
)
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 = () => (
<Layout className="index__container">
// 頭部導航欄
<HeaderNav />
<Layout>
// 側邊欄
<SiderNav />
<Layout>
// 主體內容
<ContentMain />
</Layout>
</Layout>
</Layout>
)
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<RouteComponentProps> = () => {
return (
<div className="main__container">
<Switch>
<Route exact path="/admin" component={Index} />
<Route exact path="/admin/content/exercise-list" component={ExerciseList} />
<Route exact path="/admin/content/exercise-publish" component={ExercisePublish} />
<Route exact path="/admin/content/exercise-modify/:id" component={ExerciseModify} />
<Redirect exact from="/" to="/admin" />
</Switch>
</div>
)
}
export default withRouter(ContentMain)
複製代碼
這個就是一個嵌套路由啦,在這裏面使用withRouter來包裹一下,而後在這裏再次定義路由信息,這樣就能夠只切換主體部分的內容而不改變導航欄啦
大功告成!!!
經過圖片咱們能夠看出,側邊導航欄有一個選中的內容,那麼咱們該如何判斷不一樣的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
就能夠捕獲到啦,而後設置當前選中的條目以及打開的多級導航,具體的導航信息請看下面
<Menu
mode="inline"
defaultSelectedKeys={['/admin']}
selectedKeys={selectedKeys}
openKeys={openedKeys}
onOpenChange={handleMenuChange}
>
<Menu.Item key="/admin">
<Link to="/admin">
<Icon type="home" />
首頁
</Link>
</Menu.Item>
<SubMenu
key="/admin/content"
title={
<span>
<Icon type="profile" />
內容管理
</span>
}
>
<Menu.Item key="/admin/content/exercise-list">
<Link to="/admin/content/exercise-list">題庫管理</Link>
</Menu.Item>
</SubMenu>
</Menu>
複製代碼
大功告成!!!
由於有一個題庫修改的功能,因此打算獲取完接口信息以後,直接將內容經過Ant表單的setFields
的方法來直接填充表格中的信息,結果控制檯報錯了
看了看大體意思就是說emmmm不能夠在渲染以前就設置表單的值,嘶~這可難受了,這時候想到他的表單內有一個initialValue
的屬性,是表單項的默認值,這可好辦啦,這樣咱們先拉取信息,存入對象中,而後再經過這個屬性給表單傳值,果真不出所料,真的ok了沒有報錯了哈哈哈,具體看下面
// 定義選項列表來存儲題庫的題目列表信息
const [topicList, setTopicList] = useState<TopicList[]>([{
topicType: 1,
topicAnswer: [],
topicContent: '',
topicOptions: []
}])
// 定義題庫基本信息對象
const [exerciseInfo, setExerciseInfo] = useState<ExerciseInfo>({
exerciseName: '',
exerciseContent: '',
exerciseDifficulty: 1,
exerciseType: 1,
isHot: false
})
// 首先先拉取信息,這就是題庫的信息啦
const { data } = await http.get(`/exercises/${id}`)
const {
exerciseName,
exerciseContent,
exerciseDifficulty,
exerciseType,
isHot,
topicList } = data
topicList.forEach((_: any, index: number) => {
topicList[index].topicOptions = topicList[index].topicOptions.map((item: any) => item.option)
})
// 獲取信息後,設置狀態
setTopicList([...topicList])
setExerciseInfo({
exerciseName,
exerciseContent,
exerciseDifficulty,
exerciseType,
isHot,
})
複製代碼
這樣咱們就獲得了題庫信息的對象啦,待會咱們就能夠用來傳默認值給表單啦!
// 這裏就經過題庫名稱來作例子,就從剛纔設置的信息對象中取值而後設置默認值就能夠啦
<Form.Item label="題庫名稱">
{getFieldDecorator('exerciseName', {
rules: ExerciseNameRules,
initialValue: exerciseInfo.exerciseName
})(<Input />)}
</Form.Item>
複製代碼
由於題庫的題目是有挺多,因此是一個列表,相似下圖
因此咱們實現設置好topicList
這個數組來存儲題目的信息,而後咱們經過遍歷這個列表來實現多題目編輯
<Form.Item label="新增題目">
{topicList && topicList.map((_: any, index: number) => {
return (
<Fragment key={index}>
<div className="form__subtitle">
第{index + 1}題
<Tooltip title="刪除該題目">
<Icon
type="delete"
theme="twoTone"
twoToneColor="#fa4b2a"
style={{ marginLeft: 16, display: topicList.length > 1 ? 'inline' : 'none' }}
onClick={() => handleTopicDeleteClick(index)} />
</Tooltip>
</div>
<Form.Item label="題目內容" >
{getFieldDecorator(`topicList[${index}].topicContent`, {
rules: TopicContentRules,
initialValue: topicList[index].topicContent
})(<Input.TextArea />)}
</Form.Item>
...... 省略一堆~
</Fragment>
)
})}
<Form.Item>
<Button onClick={handleTopicAddClick}>新增題目</Button>
</Form.Item>
</Form.Item>
複製代碼
例如題目內容的話,咱們就設置他的initialValue
爲topicList[index].topicContent
便可,別的屬性同理,而後點擊新增題目按鈕,就直接往topicList內添加對象信息便可完成題目列表的增長,點擊刪除圖標,就刪除列表中某一項,是否是十分方便!!哈哈哈
大功告成!!!
要想使用登陸註冊功能,還有用戶權限的問題,咱們就須要使用到這個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必定要在路由信息以前,不然是攔截不到的喲(若是在後面,路由都先執行了,還攔截啥嘛!)
大功告成!!!
咱們在處理用戶的信息的時候,須要存儲密碼,可是直接存儲確定不安全啦!因此咱們須要加密以及加鹽的處理,在這裏我用到的是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
}
複製代碼
大功告成!!!
大部分的內容就大概這樣子,這是本身開發中遇到的小問題還有解決方法,但願對你們有所幫助,你們一塊兒成長!如今得看看面試題準備一波春招了,否則大學畢業了都找不到工做啦!有時間再繼續更新這個文章!