React + Node.JS 巧妙實現(xiàn)后臺管理系統(tǒng)の各種小技巧(前后端)

> 目前因?qū)W業(yè)任務(wù)比較重,沒有好好的完善,現(xiàn)在比較完善的只有題庫管理,新增題庫,修改題庫以及登錄的功能,但搭配小程序使用,主體功能已經(jīng)實現(xiàn)了

### 此后臺系統(tǒng)是為了搭配我的另一個項目 `School-Partners學習伴侶`微信小程序而開發(fā)的。是一個采用`Taro`多端框架開發(fā)的跨平臺的小程序。感興趣的可以看一下之前的文章?

這篇文章主要是分享一下在開發(fā)這個東東的時候,遇到的一些問題,以及一些技術(shù)的巧妙的方法分享給大家,如果對大家有幫助的話,請給我點贊一下給個star鼓勵一下~無比感謝嘿嘿

![](https://user-gold-cdn.xitu.io/2020/2/1/16ffe713d1a3b94a?w=70&h=57&f=gif&s=16110)

希望大佬們走過路過可以給個star鼓勵一下~感激不盡~這個是小程序還有后臺都整合在一起的倉庫,`client`是小程序端的前端代碼,`server`是小程序端和管理端的后臺,`admin`是管理端的前端代碼

> https://github.com/zhcxk1998/School-Partners

這個是小程序的介紹文章?

[這是配套的小程序介紹文章,使勁戳!](https://juejin.im/post/5dd161675188254efb3bceea)

無圖無真相!先上幾個圖~

## 運行截圖

### 1. 登錄界面

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff951b2e55cfd1?w=1920&h=1080&f=png&s=1311104)

### 2. 題庫管理

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff9521995fee62?w=1920&h=1080&f=png&s=165232)

### 3. 修改題庫

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff9525742f881c?w=1920&h=1080&f=png&s=108784)

## 技術(shù)分析

就來說一下項目中自己推敲做出來的幾個算是亮點的東西吧

### 1. 使用Hook封裝API訪問工具

本項目采用的UI框架是Ant-Design框架?

因為這個項目的后臺對于表格有著比較大的需求,而表格加載就需要使用到`Loading`的狀態(tài),所以就特地封裝一下便于之后使用

首先我們先新建一個文件`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`的攔截器,基本配置這些寫好先

接著我們實現(xiàn)一個獲取接口信息的方法`useServiceCallback`

```typescript

const useServiceCallback = (fetchConfig: FetchConfig) => {

? // 定義狀態(tài),包括返回信息,錯誤信息,加載狀態(tài)等

? 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)

? ? ? // 調(diào)用axios來進行接口訪問,并且將傳來的參數(shù)傳進去

? ? ? instance(url, {

? ? ? ? method,

? ? ? ? data: params,

? ? ? ? ...config

? ? ? })

? ? ? ? .then((response: any) => {

? ? ? ? ? // 獲取成功后,則將loading狀態(tài)恢復(fù),并且設(shè)置返回信息

? ? ? ? ? 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`來判斷前后傳過來的參數(shù)是否一致,如果不一樣且接口訪問配置信息的`url`不為空就可以開始調(diào)用`useServiceCallback`方法來進行接口訪問了

具體使用如下:?

我們先在組件內(nèi)render外使用這個鉤子,并且定義好返回的信息?

接口返回體如下

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff962e25349c58?w=443&h=106&f=png&s=7711)

```

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

}

```

所以我們只需要再頁面加載時候調(diào)用`useEffect`來進行更新這個`fetchConfig`就可以觸發(fā)這個獲取數(shù)據(jù)的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])

```

這樣就大功告成啦!然后我們再到表格組件內(nèi)傳入相關(guān)數(shù)據(jù)就可以啦

```

<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="暫無數(shù)據(jù)" />

? ? ? ? ? }}

? ? ? ? />

```

大功告成!!

### 2. 實現(xiàn)懶加載通用組件

我們這里使用的是`react-loadable`這個組件,挺好用的嘿嘿,搭配`nprogress`來進行過渡處理,具體效果參照`github`網(wǎng)站上的加載效果?

我們先封裝好一個組件,在`components/LoadableComponent`內(nèi)定義如下內(nèi)容

```

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`的加載條進行顯示,組件卸載時候則結(jié)束,而下面的`div`則可以由用戶自己定義需要展示的樣式效果

下面的`LoadableCompoennt`就是我們這個的主體,我們需要獲取到一個組件,賦值給`loader`,具體的賦值方法如下,我們可以在項目內(nèi)的`pages`部分將所有需要展示的頁面引入進來,再導(dǎo)出,這樣就可以方便的實現(xiàn)所有頁面的懶加載了

```

// 引入剛剛定義的懶加載組件

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'))

// 導(dǎo)出,到時候再從這個pages/index.ts中引入,即可擁有懶加載效果了

export {

? Login,

? Register,

? Index,

? ExerciseList,

? ExercisePublish,

? ExerciseModify

}

```

大功告成?。。?/p>

### 3. 使用嵌套路由

項目因為涉及到后臺信息的管理,所以個人認為導(dǎo)航欄與主題信息欄應(yīng)該一同顯示,如同下圖?

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff96cd89fd6959?w=1920&h=943&f=png&s=154028)

這樣可以清晰的展示出信息以及給用戶提供導(dǎo)航效果?

我們現(xiàn)在項目的`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這個組件就是負責包裹導(dǎo)航條以及內(nèi)容部分的組件啦

接下來看看`components/Main`中的內(nèi)容吧

```

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">

? ? // 頭部導(dǎo)航欄

? ? <HeaderNav />

? ? <Layout>

? ? ? // 側(cè)邊欄

? ? ? <SiderNav />

? ? ? <Layout>

? ? ? ? // 主體內(nèi)容

? ? ? ? <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來包裹一下,然后在這里再次定義路由信息,這樣就可以只切換主體部分的內(nèi)容而不改變導(dǎo)航欄啦

大功告成?。。?/p>

### 4. 側(cè)邊欄的選中條目動態(tài)變化

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff972021c83c47?w=334&h=626&f=png&s=20027)

通過圖片我們可以看出,側(cè)邊導(dǎo)航欄有一個選中的內(nèi)容,那么我們該如何判斷不同的url頁面對應(yīng)哪一個選中部分呢?

```

? 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])

```

如果是用React的沒有使用到hook,則這里可以使用`componentWillReceiveProps()` 還有 `componentDidMount()`搭配使用,意思就是頁面加載好之后設(shè)置一下這個選中,然后有更新也設(shè)置一下

這就是最重要的部分啦,我們通過定義幾個狀態(tài)`selectedKeys`選中的條目,`openedKeys`打開的多級導(dǎo)航欄

我們通過在頁面加載時候,判斷頁面url路徑,如果是一級目錄,例如首頁,就直接設(shè)置選中的條目即可,如果是二級目錄,例如導(dǎo)航欄中`內(nèi)容管理/題庫管理`這個功能,他的url鏈接是`/admin/content/exercise-list`,所以我們的`case 4`就可以捕獲到啦,然后設(shè)置當前選中的條目以及打開的多級導(dǎo)航,具體的導(dǎo)航信息請看下面

```

<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" />

? ? ? ? ? ? ? 內(nèi)容管理

? ? ? ? ? ? </span>

? ? ? ? ? }

? ? ? ? >

? ? ? ? ? <Menu.Item key="/admin/content/exercise-list">

? ? ? ? ? ? <Link to="/admin/content/exercise-list">題庫管理</Link>

? ? ? ? ? </Menu.Item>

? ? ? ? </SubMenu>

? ? </Menu>

```

這樣我們無論是通過點擊側(cè)邊導(dǎo)航欄,或者是直接輸入url訪問頁面,這個導(dǎo)航欄選中的條目就會與我們訪問的頁面對應(yīng)的啦~

大功告成!??!

### 5. 巧妙利用Antd表單來構(gòu)造特殊的數(shù)據(jù)結(jié)構(gòu)

使用過Antd表單的胖友們一定知道`this.props.form.validateFields()`這個方法吧嘿嘿,他是如果驗證成功就返回表單的值給你,不用自己去綁定輸入組件的值,很方便,來看看官方的例子

![](https://user-gold-cdn.xitu.io/2020/2/1/16ffe5c3647acf7e?w=1419&h=724&f=png&s=85764)

可以看到,最簡單的一個登錄框,然后我們就可以得到一組數(shù)據(jù)啦,不過我們可以發(fā)現(xiàn),這些數(shù)據(jù)就是一個對象中的幾個值。?

假如我們有很多數(shù)據(jù),想用多個對象來構(gòu)造數(shù)據(jù)結(jié)構(gòu),這應(yīng)該怎么辦呢,就例如這樣子的數(shù)據(jù)結(jié)構(gòu),我們還是舉上面這個例子

![](https://user-gold-cdn.xitu.io/2020/2/1/16ffe5f52d53246c?w=502&h=358&f=png&s=59764)

假如吼,我們提交后臺的數(shù)據(jù)需要是這樣子的數(shù)據(jù)結(jié)構(gòu),用戶名和密碼在`userInfo`這個對象內(nèi),然后是否記住密碼是在`other`對象里面,自己得到數(shù)據(jù)之后再構(gòu)造又十分麻煩,這可怎么辦呢。?

在此之前,我們不如看看官方給的另一個例子,一個動態(tài)添加表單項的例子,于此我們就可以發(fā)揮想象力,然后就可以解決我們上面的問題啦

![](https://user-gold-cdn.xitu.io/2020/2/1/16ffe620163efad1?w=1341&h=745&f=png&s=103764)

可以看到這個動態(tài)添加表單項的,是以數(shù)組形式來存儲數(shù)據(jù)的,他的代碼是這樣的

```

{getFieldDecorator(`names[${k}]`, {

? validateTrigger: ['onChange', 'onBlur'],

? rules: [

? ? {

? ? ? required: true,

? ? ? whitespace: true,

? ? ? message: "Please input passenger's name or delete this field.",

? ? },

? ],

})(<Input placeholder="passenger name" style={{ width: '60%', marginRight: 8 }} />)}

```

Antd表單的構(gòu)造數(shù)據(jù)關(guān)鍵就在于里面的`getFieldDecorator`內(nèi)的第一個參數(shù),也就是我們的`propName`用來指定數(shù)據(jù)叫啥,跟之后驗證表單傳回的值是對應(yīng)的了。這就給了我們一個很大的提示啦??!?

> 這個`propName`叫什么,之后生成的數(shù)據(jù)結(jié)構(gòu)里面就是什么,是`a`,之后數(shù)據(jù)就對應(yīng)`a`,是`b`,就對應(yīng)`b`

這里通過一個`names[$k]`,就可以讓之后得到的數(shù)據(jù)變成一個數(shù)組`names:Array(2): ['1', '2']`這樣子的形式,那么我們稍加改造一下,就可以變成對象的形式啦!下面看看代碼,其實也很簡單!

```

<Form.Item label="題目內(nèi)容" >

{getFieldDecorator(`topicList[${index}].topicContent`, {

? rules: TopicContentRules,

? initialValue: topicList[index].topicContent

})(<Input.TextArea />)}

</Form.Item>

```

這里我就直接舉項目中題庫提交的例子啦,`topicList`是一個列表,里面存的是每一個題目對應(yīng)的數(shù)據(jù)對象

![](https://user-gold-cdn.xitu.io/2020/2/1/16ffe6790b090234?w=1023&h=241&f=png&s=132789)

這里的`propName`,我指定成了`topicList[$(index)]`就代表,這個屬于這個列表里面的第幾個對象,然后后面的`.topicContent`就代表這個對象里面的值是什么,最后我們的出的結(jié)構(gòu)就是這樣子的啦!

![](https://user-gold-cdn.xitu.io/2020/2/1/16ffe6a59130cebe?w=850&h=477&f=png&s=68397)

我們?nèi)缭傅玫搅讼胍臄?shù)據(jù)結(jié)構(gòu)了,這里面有對象,有數(shù)組,十分方便,可以靈活根據(jù)實際情況進行使用,關(guān)鍵就在于`getFieldDecorator()`里面的`propName`,直接以對象的形式命名,就可以啦!就按照下面這種形式就好啦!

```

<Form.Item label="itemName" >

? ? {getFieldDecorator(`object.itemName`, {

? ? ? initialValue: 'BB小天使'

? ? })(<Input />)}

</Form.Item>

```

之后就可以得到對象類型的表單值啦!

大功告成?。?!

### 6. 后臺接口獲取信息后填充Antd表單

因為有一個題庫修改的功能,所以打算獲取完接口信息之后,直接將內(nèi)容通過Antd表單的`setFields`的方法來直接填充表格中的信息,結(jié)果控制臺報錯了

![](https://user-gold-cdn.xitu.io/2020/1/31/16ffbaf24642b709?w=1039&h=69&f=png&s=10756)

看了看大致意思就是說emmmm不可以在渲染之前就設(shè)置表單的值,嘶~這可難受了,這時候想到他的表單內(nèi)有一個`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)

? })


? // 獲取信息后,設(shè)置狀態(tài)

? setTopicList([...topicList])

? setExerciseInfo({

? ? exerciseName,

? ? exerciseContent,

? ? exerciseDifficulty,

? ? exerciseType,

? ? isHot,

? })

```

這樣我們就得到了題庫信息的對象啦,待會我們就可以用來傳默認值給表單啦!

```

// 這里就通過題庫名稱來做例子,就從剛才設(shè)置的信息對象中取值然后設(shè)置默認值就可以啦

<Form.Item label="題庫名稱">

? {getFieldDecorator('exerciseName', {

? ? rules: ExerciseNameRules,

? ? initialValue: exerciseInfo.exerciseName

? })(<Input />)}

</Form.Item>

```

因為題庫的題目是有挺多,所以是一個列表,類似下圖

![](https://user-gold-cdn.xitu.io/2020/1/31/16ffbb790d7df312?w=1920&h=1080&f=png&s=98397)

所以我們實現(xiàn)設(shè)置好`topicList`這個數(shù)組來存儲題目的信息,然后我們通過遍歷這個列表來實現(xiàn)多題目編輯

```

<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="題目內(nèi)容" >

? ? ? ? ? ? {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>

```

例如**題目內(nèi)容**的話,我們就設(shè)置他的`initialValue`為`topicList[index].topicContent`即可,別的屬性同理,然后點擊新增題目按鈕,就直接往topicList內(nèi)添加對象信息即可完成題目列表的增加,點擊刪除圖標,就刪除列表中某一項,是不是十分方便??!哈哈哈

大功告成?。?!

### 7. 使用JWTToken來驗證用戶登錄狀態(tài)以及返回信息

要想使用登錄注冊功能,還有用戶權(quán)限的問題,我們就需要使用到這個token啦!為什么我們要使用token呢?而不是用傳統(tǒng)的cookies呢,因為使用token可以避免跨域啊還有更多的復(fù)雜問題,大大簡化我們的開發(fā)效率

> 本項目后臺采用nodeJs來進行開發(fā)?

我們先在后臺定義一個工具`utils/token.js`

```

// token的秘鑰,可以存在數(shù)據(jù)庫中,我偷懶就卸載這里面啦hhh

const secret = "zhcxk1998"

const jwt = require('jsonwebtoken')

// 生成token的方法,注意前面一定要有Bearer ,注意后面有一個空格,我們設(shè)置的時間是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')

? }

```

好嘞,現(xiàn)在前端獲取token也搞定啦,接下來我們就需要在訪問接口的時候帶上這個token啦,這樣才可以讓后端知道這個用戶的權(quán)限如何,是否過期等

需要傳tokne給后端,我們可以通過每次接口都傳一個字段`token`,但是這樣十分浪費成本,所以我們再封裝好的`axios`中,我們設(shè)置請求頭信息即可

```

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

```

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff9814a9d0e5d8?w=1094&h=306&f=png&s=40494)

如上圖所示,我們每次請求接口的時候就會帶上這個請求頭啦!那么接下來我們就談?wù)労蠖巳绾潍@取這個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

攔截已經(jīng)成功啦,那么我們該如何捕獲,然后進行處理呢?我們再`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已經(jīng)過期,所以我們只需要判斷這個狀態(tài)`status`然后進行處理即可

好嘞,中間件也定義好了,我們就在后端服務(wù)中使用起來吧!

```

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一定要在路由信息之前,否則是攔截不到的喲(如果在后面,路由都先執(zhí)行了,還攔截啥嘛?。?/p>

大功告成?。?!

### 8. 密碼使用加密加鹽的方式存儲

我們在處理用戶的信息的時候,需要存儲密碼,但是直接存儲肯定不安全啦!所以我們需要加密以及加鹽的處理,在這里我用到的是`crypto`這個庫

首先我們再`utils/encrypt.js`中定義一個工具函數(shù)用來生成鹽值以及獲取加密信息

```

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)

}

// 獲取密碼轉(zhuǎn)換成md5之后的加密信息

const getEncrypt = (password) => {

? return crypto.createHash('md5').update(password).digest('hex')

}

module.exports = {

? getRandomSalt,

? getEncrypt

}

```

這樣我們就可以通過驗證密碼與數(shù)據(jù)庫中加密的信息對不對得上,來判斷是否登錄成功等等

我們現(xiàn)在注冊中使用上,當然我們需要兩個表進行數(shù)據(jù)存儲,一個是用戶信息,一個是用戶密碼表,這樣分開更加安全,例如這樣

![](https://user-gold-cdn.xitu.io/2020/1/31/16ff98e72c1d20f5?w=1525&h=613&f=png&s=183724)

這樣就可以將用戶信息還有密碼分開存放,更加安全,這里就不重點敘述啦

```

const { getRandomSalt, getEncrypt } = require('../../utils/encrypt')

// 注冊部分

router.post('/register', async (ctx) => {

? const { username, password, phone, email } = ctx.request.body

? // 獲取鹽值以及加密后的信息

? const salt = getRandomSalt()

? // 數(shù)據(jù)庫存放的密碼是由用戶輸入的密碼加上隨機鹽值,然后再進行加密所得到的的炒雞加密密碼

? const encryptPassword = getEncrypt(password + salt)


? // 插入用戶信息,以及獲取這個的id

? const { insertId: user_id } = await query(INSERT_TABLE('user_info'), { username, phone, email });

? // 插入用戶密碼信息,user_id與上面對應(yīng)

? 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)

// 這個加密的密碼與數(shù)據(jù)庫中加密的密碼對比,如果一樣則登陸成功

if (sign === verifySign) {

? responseBody.data.msg = '登陸成功'

? responseBody.data.token = generateToken({ username })

? responseBody.code = 200

} else {

? responseBody.data.msg = '用戶名或密碼錯誤'

? responseBody.code = 401

}

```

大功告成?。?!

## 結(jié)語

大部分的內(nèi)容就大概這樣子,這是自己開發(fā)中遇到的小問題還有解決方法,希望對大家有所幫助,大家一起成長!現(xiàn)在得看看面試題準備一波春招了,不然大學畢業(yè)了都找不到工作啦!有時間再繼續(xù)更新這個文章!

## 最后還是順便求一波star還有點贊?。。?/p>

何時才能上100點贊,100star啊嗚嗚嗚

[github項目猛戳進來star一下嘿嘿](https://github.com/zhcxk1998/School-Partners)?

[小程序介紹文章,使勁戳!](https://juejin.im/post/5dd161675188254efb3bceea)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容