0%

学习资料

redux源码 v 4.x

学习目标

  1. redux源码createStore(Part1)
  2. redux中间件原理(Part2)
  3. redux源码combineReducers(Part3)

过程解析

getUndefinedStateErrorMessage

1
2
3
4
5
6
7
8
9
10
11
function getUndefinedStateErrorMessage(key, action) {
const actionType = action && action.type
const actionDescription =
(actionType && `action "${String(actionType)}"`) || 'an action'

return (
`Given ${actionDescription}, reducer "${key}" returned undefined. ` +
`To ignore an action, you must explicitly return the previous state. ` +
`If you want this reducer to hold no value, you can return null instead of undefined.`
)
}

getUnexpectedStateShapeWarningMessage

getUnexpectedStateShapeWarningMessage函数:一些错误性的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function getUnexpectedStateShapeWarningMessage(
inputState,
reducers,
action,
unexpectedKeyCache
) {
const reducerKeys = Object.keys(reducers)
const argumentName =
action && action.type === ActionTypes.INIT
? 'preloadedState argument passed to createStore'
: 'previous state received by the reducer'

if (reducerKeys.length === 0) {
return (
'Store does not have a valid reducer. Make sure the argument passed ' +
'to combineReducers is an object whose values are reducers.'
)
}

if (!isPlainObject(inputState)) {
return (
`The ${argumentName} has unexpected type of "` +
{}.toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
`". Expected argument to be an object with the following ` +
`keys: "${reducerKeys.join('", "')}"`
)
}

const unexpectedKeys = Object.keys(inputState).filter(
key => !reducers.hasOwnProperty(key) && !unexpectedKeyCache[key]
)

unexpectedKeys.forEach(key => {
unexpectedKeyCache[key] = true
})

if (action && action.type === ActionTypes.REPLACE) return

if (unexpectedKeys.length > 0) {
return (
`Unexpected ${unexpectedKeys.length > 1 ? 'keys' : 'key'} ` +
`"${unexpectedKeys.join('", "')}" found in ${argumentName}. ` +
`Expected to find one of the known reducer keys instead: ` +
`"${reducerKeys.join('", "')}". Unexpected keys will be ignored.`
)
}
}

assertReducerShape(reducers)

assertReducerShape函数:

​ 这部分函数其实就是对reducer的一个校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
assertReducerShape(reducers)函数
输入值分析:
- reducers
输出分析:
- reducers
*/
function assertReducerShape(reducers) {
Object.keys(reducers).forEach(key => {
const reducer = reducers[key]
const initialState = reducer(undefined, { type: ActionTypes.INIT })

if (typeof initialState === 'undefined') {
throw new Error(
`Reducer "${key}" returned undefined during initialization. ` +
`If the state passed to the reducer is undefined, you must ` +
`explicitly return the initial state. The initial state may ` +
`not be undefined. If you don't want to set a value for this reducer, ` +
`you can use null instead of undefined.`
)
}

if (
typeof reducer(undefined, {
type: ActionTypes.PROBE_UNKNOWN_ACTION()
}) === 'undefined'
) {
throw new Error(
`Reducer "${key}" returned undefined when probed with a random type. ` +
`Don't try to handle ${ActionTypes.INIT} or other actions in "redux/*" ` +
`namespace. They are considered private. Instead, you must return the ` +
`current state for any unknown actions, unless it is undefined, ` +
`in which case you must return the initial state, regardless of the ` +
`action type. The initial state may not be undefined, but can be null.`
)
}
})
}

combineReducers(reducers)

combineReducers函数:用于合并reducers

基本思路:

  • reducers浅复制到finalReducers里面
  • reducers基本校验
  • 返回combination函数

combination函数:

基本思路:

  • 基本性的检测
  • 维护两个变量hasChanged = false[用于监听状态有没有改变]和nextState = {}[用于存更新后的状态]
  • 遍历finalReducers,获取reducer和state里面存储的reducer所需要的state
    • 通过reducer(state,action)得到新的状态
    • 得到新的状态存入nextState里面
    • 判断是否状态改变
  • 最终判断状态改变否
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/*
combineReducers函数用于合并reducers
输入参数说明:
- reducers:多个reducer
输出说明:
- combination 函数 (后面具体分析)
*/
export default function combineReducers(reducers) {
/*
mock:
reducers = {
reducer1: reducer1,
reducer2: reducer2,
...
reducern: reducern,
}
维护两个变量:
reducerKeys = [reducer1, reducer2, ..., reducer3]
finalReducers = {}
*/
const reducerKeys = Object.keys(reducers)
const finalReducers = {}

// 以下是浅复制的过程... 得到finalReducers
// 遍历reducers
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]

// process.env.NODE_ENV用于判断生产环境或开发环境
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}

// 如果reducer是一个函数,就添加到finalReducers里面
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}

const finalReducerKeys = Object.keys(finalReducers)

// This is used to make sure we don't warn about the same
// keys multiple times.
let unexpectedKeyCache
if (process.env.NODE_ENV !== 'production') {
unexpectedKeyCache = {}
}

// 校验reducers中的reducer
let shapeAssertionError
try {
assertReducerShape(finalReducers) // --------往上翻这个函数做了什么
} catch (e) {
shapeAssertionError = e
}


/*
combination函数:
输入参数分析:
- state:初始值为空对象
- action:一个对象 {type:xxx, data:xxx}
*/
return function combination(state = {}, action) {
if (shapeAssertionError) {
throw shapeAssertionError
}

if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}

/*
维护两个变量:
hasChanged = false
nextState = {}
*/
let hasChanged = false // 用于监听状态有没有改变的
const nextState = {}
/*
mock:
finalReducers = {
reducer1: reducer1,
reducer2: reducer2,
...
reducern: reducern,
}

finalReducerKeys = [reducer1, reducer2, reducer3, ...]

*/
for (let i = 0; i < finalReducerKeys.length; i++) { // 遍历finalReducers
const key = finalReducerKeys[i] // reducer1, reducer2, reducer3, ...
const reducer = finalReducers[key] // 获取当前reducer: reducer1, reducer2, reducer3
const previousStateForKey = state[key] // 获取当前reducer在state
const nextStateForKey = reducer(previousStateForKey, action) // reducer纯函数加工状态获得新的state
if (typeof nextStateForKey === 'undefined') {
const errorMessage = getUndefinedStateErrorMessage(key, action) // --------往上翻这个函数做了什么
throw new Error(errorMessage)
}
nextState[key] = nextStateForKey // nextState里面存入新的state [key=key, value=新的state]
hasChanged = hasChanged || nextStateForKey !== previousStateForKey // 小状态改变否???
}

hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length // 整体状态改变否?
return hasChanged ? nextState : state // 改变了 返回nextState 否则 返回state
}
}

bindActionCreators(actionCreator, dispatch)

bindActionCreator函数:

将一个或多个action和dispatch组合起来生成mapDispatchToProps需要生成的内容

connect(mapStateToProps, mapDispatchToProps)(UI component) connect用以从UI组件生成容器组件

mapStateToProps:将state映射到UI组件的参数(props)

mapDispatchToProps:将用户对UI组件的操作映射成Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function bindActionCreator(actionCreator, dispatch) {
return function() {
/*
apply修改this指向
*/
return dispatch(actionCreator.apply(this, arguments))
}
}

export default function bindActionCreators(actionCreators, dispatch) {
// 参数校验
/*
如果actionCreators是一个函数,那么就需要bindActionCreator进行一些处理
并且看到bindActionCreator返回的是一个函数

如果actionCreators不是一个object(排除null),报错!
*/
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}

if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error(
`bindActionCreators expected an object or a function, instead received ${
actionCreators === null ? 'null' : typeof actionCreators
}. ` +
`Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
)
}

// 维护一个参数boundActionCreators是一个对象,初始为空对象
const boundActionCreators = {}

/*
遍历actionCreators,如果actionCreator是一个函数,bindActionCreator后返回一个函数
最终返回boundActionCreators对象,key为原来的key,value为函数形式
*/
for (const key in actionCreators) {
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreators
}

学习资料

redux源码 v 4.x

学习目标

  1. redux源码createStore(Part1)
  2. redux中间件原理(Part2)
  3. redux源码combineReducers(Part3)

理解Middleware 中间件

Middleware 中间件

看了官方文档的两个例子:日志和异常监控,对中间件的妙用有了初步的理解

middleware洋葱模型

过程解析

compose

compose函数: 将多个函数按照顺序执行,前一个函数的返回值作为下一个函数的参数,最终返回结果

乍一看,哎呀妈呀,这compose函数也太亲切了吧![开心出东北话~]

1
2
3
4
5
6
7
8
9
10
11
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}

if (funcs.length === 1) {
return funcs[0]
}

return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

compose函数妙用:

1
2
3
4
5
6
7
8
9
10
11
12
13
const funcs = [a, b, c];

function a(x) {
return x+2;
};
function b(x) {
return x+3;
};
function c(x) {
return x+10;
};

console.log(compose(...funcs)(3)) // 18 = 3 + 2 + 3 + 10

middleware

applyMiddleware函数:这个函数主要是返回一个createStore函数,其中createStore函数的返回值之一的dispatch是经过中间件包装的

所以这个函数我们主要关注dispatch是怎么被包装的~~

输入参数说明:

​ - middlewares 多个中间件

输出说明:

​ - createStore函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import compose from './compose'

export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
// 创建一个store
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}

// 下面的代码主要是对dispatch进行包装

/*
middlewareAPI是一个对象,包含getState方法和dispatch方法(起到包装的作用)
*/
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args) // 闭包
}
/*
我们知道,多个middlewares是采用链式调用的解构的
首先把每个middleware都包装成一个函数
然后很好地用到了compose函数,对这些中间件进行嵌套执行,初始参数为store.dispatch
store.dispatch --M1--> res1 --M2--> res2 --M3--> ... --M N--> final res
*/
// map: middleware -> middleware(middlewareAPI),相当于返回的是middleware(middlewareAPI)一次调用后的结果
// 具体看example_redux_thunk里的{dispatch, getState} => next => action => {}这个部分
// 所以chain相当于是
/*
[
(next) => (action) => {}, ---M1
(next) => (action) => {}, ---M2
(next) => (action) => {}, ---M3
...
]

经过compose之后
dispath等于
M2(M1(M3(stroe.dispatch)))的结果

[这一步就是利用洋葱模型一步一步再包装dispatch]

代入洋葱模型想一想是不是特别容易理解?

*/
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch) // 利用中间件一层层包装dispatch,生成一个新的dispatch

return {
...store, // 对象解构
dispatch // 同名属性覆盖
}
}
}

example_redux_thunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createThunkMiddle(extraArgument) {
return ({dispatch, getState} => (next) => (action) => { // 典型的柯里化函数
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument); // 洋葱模型可以提前返回
}

return next(action);
})
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddle;

export default thunk;

学习资料

redux源码 v 4.x

学习目标

  1. redux源码createStore(Part1)
  2. redux中间件原理(Part2)
  3. redux源码combineReducers(Part3)

Redux介绍

Redux是什么

  • redux是专门用于做状态管理的JS库
  • redux的作用是集中式管理react应用中多个组件共享的状态

什么时候需要用redux

  • 某个组件的状态,需要让其他组件可以随时拿到(共享)
  • 一个组件需要改变另一个组件的状态(通信)

Redux组成

State 状态

状态即传递的数据

在React开发的项目的时候,大致可以把State分为三类:

  • DomainDate: 可以理解为成为服务器端的数据,比如:获取用户的信息,商品的列表等等
  • UI State: 决定当前UI决定展现的状态,比如:弹框的显示隐藏,受控组件等等
  • App State: App级别的状态,比如:当前是否请求loading,当前路由信息等可能被多个和组件去使用的到的状态

Action 事件

Action是把数据从应用传到store的载体,是数据的唯一来源,一般来说,可以通过store.dispatch()将action传递给store

  • action包含2个属性
    • type:标识属性, 值为字符串, 唯一, 必要属性
    • data:数据属性, 值类型任意, 可选属性
  • 例子:{ type: ‘ADD_STUDENT’,data:{name: ‘tom’,age:18} }

Reducer

  • Reducer的本质就是一个函数,它用来响应发送过来的actions,然后经过处理,把state发送给Store的,用于初始化状态加工状态
  • 在Reducer函数中,需要return返回值,这样Store才能接收到数据,函数会接收两个参数,一个参数是初始化state【旧】,第二个参数是action,加工时,根据旧的state和action, 产生新的state的纯函数
1
2
3
const initState = {...};

rootReducer = (state = initState, action) => {...return {...}};

Redux三大核心概念

单一数据源

整个应用的state被存储在一颗object tree中,并且这个object tree只存在唯一一个store中,使得状态之间更好管理,更容易调试,以及一些撤销/重做的功能更容易

state是只读的

唯一改变state的方法是通过dispatch触发action

这样能够确保视图和网络请求都不能直接去修改state,相反,它们只能表达想要修改的意图,因为所有的修改都被集中化处理,并且严格按照一个接一个的顺序执行

使用纯函数来执行修改

纯函数

纯函数

  • 数据不可变(Immutable Data):此函数在相同的输入值时,需产生相同的输出
  • 无状态(Statelessness):函数的输出和输入值以外的其他隐藏信息或状态无关,也和由IO设备产生的外部输出无关
  • 没有副作用(No Side Effects):该函数不能有语义上可观察的函数副作用,例如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等

为了描述action如何改变state tree,你需要编写reducers

Reducers只是一些纯函数,它接收先前的state和action,并且返回新的state, 可以复用,可以控制顺序,传入附加参数State状态

Redux工作机制

过程解析

createStore

源码的注释写得很清楚~这里做一下简单的注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
import $$observable from 'symbol-observable'

import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'

/*
此函数用于创建Redux store(store可以认为是一个管理state的仓库)
值得注意的是:
- dispatch是改变state的唯一方法【核心概念2:state只读】
- app里面只能有一个store【核心概念1:单一数据源】:
如果你的app有很多模块的话,你可以通过多个reducer来管理,通过combineReducer来组合reducer

参数说明:
1. reducer {Function} 通过前面的说明已经知道reducer是一个纯函数,用于初始化状态和加工状态
2. preloadedState {any} 初始状态
3. enhancer {Function} 增强器(可认为是拓展功能),比如中间件等等 Redux 附带的唯一存储增强器是`applyMiddleware()`。
返回值说明:
store
有如下功能:
- read state 读取状态 - getSate
- dispatch action 调度动作 - dispatch
- subscribe to changes 监听变化 - subscribe

[replaceReducer和observable不是很重要]

*/
export default function createStore(reducer, preloadedState, enhancer) {
/*
Step1:参数校验
1. 校验1:直接传入多个enhancers的情况,下面注释说明了,你可能是传入了多个enhancers,直接传入是不对的,你需要把它们组合(compose them)
2. 检验2:没传入初始状态的情况,也就是你只穿了两个参数,那么你的第二个参数应该是enhancer,而preloadedState为undefined
3. 校验3:检验enhancer是不是一个函数
4. 校验4:reducer是不是一个函数
*/
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function.'
)
}

if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}

if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}

return enhancer(createStore)(reducer, preloadedState)
}

if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}

/*
Step2:定义内部变量
1. 变量1:currentReducer = reducer | 当前的reducer
2. 变量2:currentState = preloadedState | 当前的state:可被getState()获取
3. 变量3:currentListeners = [] | 监听列表
4. 变量4:nextListeners = currentListeners | 监听列表
5. 变量5:isDispatching = false | 是否正在被dispatch(调度)
*/
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false

/*
ensureCanMutateNextListeners函数:
生成currentListeners的浅拷贝,可以把nextListeners作为临时调度列表
这可以防止消费者调用的任何错误在调度过程中订阅/取消订阅

*/
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}

/*
getState函数
返回当前状态(currentState)
*/
function getState() {
/*
获取state的时候必须保证这个reducer已经结束工作了,store已经接收到新的state了,才能调用getState
*/
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}

return currentState
}


/*
subscribe函数:作用是订阅监听
任何时候dispatch一个action都会调用这个函数【一触即发!】
调用action后state object tree的部分可能已经改变了
所以可以再去调用getState()读取回调内的当前状态树

参数说明:
- listener:监听到变化后执行的函数,比如subscribe(function() {console.log(1)}) 变化后打印1
返回值说明:
- unsubscribe {Fuction}: 返回一个具有取消订阅功能的函数

*/
function subscribe(listener) {
/*
listener必须是一个函数
举个例子,listener是一个render函数,调用subscribe就会重新渲染某部分的页面(取决于你的render函数需要render什么)
*/
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}

/*
同理:获取state的时候必须保证这个reducer已经结束工作了,store已经接收到新的state了,才能调用getState
这也就是说明,顺序是 dispatch action -> state改变 -> 被subscribe(listener) 监听到 -> 获取新的状态 -> listener函数调用
举例: 计时器:dispatch action [目的在于使得value+1] -> value状态改变 -> 被subscribe(render) 监听到 -> 获取新的state -> 重新渲染
*/
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api-reference/store#subscribelistener for more details.'
)
}

// Sub-Step1: 设置isSubscribed 为 true
let isSubscribed = true

// Sub-Step2: (1) 浅复制currentListeners[当前的监听列表],(2) 把监听函数push到监听列表中
ensureCanMutateNextListeners()
nextListeners.push(listener)

/*
unsubscribe函数:具有取消订阅功能
*/
return function unsubscribe() {
// 只有isSubscribed为true的才能被取消订阅
if (!isSubscribed) {
return
}

/*
同理:正在被执行的不能取消订阅
*/
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://redux.js.org/api-reference/store#subscribelistener for more details.'
)
}

// sub-step3: 修改isSubscribed 为 false
isSubscribed = false

// sub-step4: 浅复制监听列表
ensureCanMutateNextListeners()

// sub-step5: 监听完毕,从列表中删除它
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}

/*
dispatch函数:[调度函数] 划重点!回顾:这是改变状态的唯一方法!!!!! 【核心概念2:state只读】

dispatch被调用后,reducer将会被调用,store通过转发dispatch(action) 中包含的previousState和action给redeucer,reducer返回newState给store,React component可以通过getState获取newState
注意:基本实现仅仅支持普通对象的操作,如果的状态是一个Promise Obserable thunk等等,你需要借助与中间件(middleware) 【后面介绍】
参数说明:
- action 是一个object,格式为 {type:xxx, data:xxx}
返回值说明:
- {Object} 为了方便,返回action对象,但是里面的data是经过更改了
*/

function dispatch(action) {
/*
isPlainObject函数不贴代码了~看源码的意思,isPlainObject就是用来判断action是不是一个对象
*/
if (!isPlainObject(action)) {
throw new Error(
'Actions must be plain objects. ' +
'Use custom middleware for async actions.'
)
}

/*
只有定义了action.type才能进行后续操作,所以type不能是undefined
*/
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. ' +
'Have you misspelled a constant?'
)
}

/*
正在被dispatch的action不能传入到reducer中处理
*/
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}

/*
sub-step1:
- 切换isDispatching为true
- currentState为currentReducer(currentState, action)处理后返回的state
reducer函数举例:
function currentReducer(state={count:0}, action) => {
if (action.type === 'INCREMENT') {
return {...state, count: state.count+1}
}
}
执行完毕后,也就是currentState返回后,
切换isDispatching为false
*/
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}

/*
sub-step2:
触发listener函数
*/
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}

return action
}


/*
replaceReducer函数:替换store当前使用的reducer来计算 state
不是特别重要,就是替换当前的reducer

*/
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}

currentReducer = nextReducer

dispatch({ type: ActionTypes.REPLACE })
}

/*
observable函数:
不重要~暂时不说了
*/
function observable() {
const outerSubscribe = subscribe
return {

subscribe(observer) {
if (typeof observer !== 'object' || observer === null) {
throw new TypeError('Expected the observer to be an object.')
}

function observeState() {
if (observer.next) {
observer.next(getState())
}
}

observeState()
const unsubscribe = outerSubscribe(observeState)
return { unsubscribe }
},

[$$observable]() {
return this
}
}
}

dispatch({ type: ActionTypes.INIT })

return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}

整个createStore函数可以抽象为下图:

接下来就稍微解释下getState dispatch subscribe是如何工作的(省略参数校验部分)

首先,在createStore函数内部维护了以下几个变量,并且初始化:

1
2
3
4
5
变量1:currentReducer = reducer             | 当前的reducer 
变量2:currentState = preloadedState | 当前的state:可被getState()获取
变量3:currentListeners = [] | 监听列表
变量4:nextListeners = currentListeners | 监听列表
变量5:isDispatching = false | 是否正在被dispatch(调度)

getState

函数思路非常的简单,就是获取currentState,也就是当前的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getState() {
/*
获取state的时候必须保证这个reducer已经结束工作了,store已经接收到新的state了,才能调用getState
*/
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}

return currentState
}

dispatch

dispatch函数是用于调度action的,思路如下:

  • 首先需要确保action的isDispatching为false
  • 切换isDispatching的状态为true,说明这个action要被dispatch了
  • 把action和currentState传入reducer中,并且产出newState【但仍然以currentState命名】
  • 切换isDispatching的状态为false,说明这个action要dispatch结束了
  • 因为state发生了变化,所以触发listener函数,可能有很多listener函数,所以维护了一个列表,需要去遍历这个列表
  • 最终返回action,state已经进行更新了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function dispatch(action) {	
/*
正在被dispatch的action不能传入到reducer中处理
*/
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}

/*
sub-step1:
- 切换isDispatching为true
- currentState为currentReducer(currentState, action)处理后返回的state
reducer函数举例:
function currentReducer(state={count:0}, action) => {
if (action.type === 'INCREMENT') {
return {...state, count: state.count+1}
}
}
执行完毕后,也就是currentState返回后,
切换isDispatching为false
*/
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}

/*
sub-step2:
触发listener函数
*/
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}

return action
}

subscribe

subscribe函数有订阅监听的意思,就是把监听函数放入到列表里面,然后在dispatch的时候去遍历列表

  • 首先要保证listener是一个函数,结合dispatch函数,state发生变化的时候,需要执行listener,并且要保证没有在调度中
  • 设置isSubscribed 为 true,表示正在被订阅
  • 把listener加入到nextListeners列表
  • 返回一个取消订阅的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
function subscribe(listener) {
/*
listener必须是一个函数
举个例子,listener是一个render函数,调用subscribe就会重新渲染某部分的页面(取决于你的render函数需要render什么)
*/
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}

if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api-reference/store#subscribelistener for more details.'
)
}

// Sub-Step1: 设置isSubscribed 为 true
let isSubscribed = true

// Sub-Step2: (1) 浅复制currentListeners[当前的监听列表],(2) 把监听函数push到监听列表中
ensureCanMutateNextListeners()
nextListeners.push(listener)

/*
unsubscribe函数:具有取消订阅功能
*/
return function unsubscribe() {
// 只有isSubscribed为true的才能被取消订阅
if (!isSubscribed) {
return
}

/*
同理:正在被执行的不能取消订阅
*/
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://redux.js.org/api-reference/store#subscribelistener for more details.'
)
}

// sub-step3: 修改isSubscribed 为 false
isSubscribed = false

// sub-step4: 浅复制监听列表
ensureCanMutateNextListeners()

// sub-step5: 监听完毕,从列表中删除它
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}

实例example_计时器

html部分

1
2
3
4
5
6
7
8
<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
<div class="box">
<p>
Click<span id="value">0</span>
<button id="increment">+</button>
<button id="decrement">-</button>
</p>
</div>

js部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 获取按钮和展示value的标签
const spanValue = document.getElementById('value');
const incrementBtn = document.getElementById('increment');
const decrementBtn = document.getElementById('decrement');
// 封装一个reducer函数:纯函数,用于初始化状态和加工状态
/*
传入两个参数state和action
{
type: xxx,
data: xxx,
}

*/
function count(state, action) {
if (typeof state === 'undefined') return 0;

switch (action.type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
default:
return state;
}
}

// 创建一个store,参数参数为reducer = count,其余两个参数忽略
const store = Redux.createStore(count);
console.log(store);

// 设置listener函数动态展示数据
const listener = function() {
spanValue.innerText = store.getState().toString();
}

// 订阅listener
store.subscribe(listener);

// 设置dispatch
incrementBtn.addEventListener("click", () => {
store.dispatch({type:'INCREMENT'})
})

decrementBtn.addEventListener("click", () => {
store.dispatch({type:'DECREMENT'})
})

需求1

封装一个函数,用于生成颜色(16进制或者rgba)

传入boolean为true时随机生成颜色的十六进制值,传入false时生成rgba值

需求2

封装一个函数,能够随机生成16进制颜色值,要求使用内部rag值的转换

十六进制颜色特点

0-9 A-F组成(字母不区分大小写)

rgb颜色特点

rgb包含三个参数,分别代表红色、绿色、蓝色

三个参数取值为0-255或者是0%-100%

实现思路

利用Math.random()生成随机数进行取值

实现1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getRandomColor(bool) {
// 生成十六进制
if (bool) {
let str = '';
for (let i = 0; i < 6; i++) {
// Math.random() 取值为0-1 toString(16)转为16进制
let c = Math.floor(Math.random()*16.toString(16));
str += c;
};
return `#${str}`
} else { // 生成rgba值
let r = Math.floor(Math.random()*256);
let g = Math.floor(Math.random()*256);
let b = Math.floor(Math.random()*256);
return `rgb(${r}, ${g}, ${b})`;
}
}

实现2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getColor16() {
let arr = [];
for (let i = 0; i < 3; i++) {
// 生成r/g/b
arr[i] = Math.floor(Math.random()*256);

// 16进制转换
arr[i] = arr[i] < 16 ? `0${arr[i].toString(16)}` : arr[i].toString(16);
}
return '#' + arr.join('');
}

// test
console.log(getColor16()); // #6861b8

设计需求

题目来源:字节校招题_todoList设计

设计一个TODO List,页面结构如下图所示,要求:

  1. 使用HTML与CSS完成界面开发
  2. 实现添加功能:输入框中可输入任意字符,按回车后将输入字符串添加到下方列表的最后,并清空输入框
  3. 实现删除功能:点击列表项后面的“X”号,可以删除该项
  4. 实现模糊匹配:在输入框中输入字符后,将当前输入字符串与已添加的列表项进行模糊匹配,将匹配到的结果显示在输入框下方。如匹配不到任何列表项,列表显示空

注:以上代码实现需要能在浏览器中正常显示与执行。

img

实现思路

(1)UI部分

CSS自己魔改就行~

可以增加一些小细节,比如hover到input框的时候显示input的边框等~

(2)操作部分

  • keydown监听——在input框输入,按下回车键后,动态显示
    • input输入——输入内容可以由input.value获取
    • 按下回车键后——回车键的keycode为13
    • 动态显示——动态创建li并为li添加显示value的span和显示叉叉的span作为子节点,最后li作为子节点插入到ul中
  • keyup监听——模糊搜索
    • 通过监听keyup事件,获取input.value
    • 获取所有的显示value的span,遍历其内容(spanValue),判断spanValue中是否包含value(通过indexOf判断)
    • 如果包含,则利用正则表达式,把符合的部分替换成红色字体
  • onclick监听——如果点击叉叉则删除内容
    • 实际上是删除li
    • 需要找到onclick绑定事件的元素,找到其父元素,并通过父元素的父元素(parentNode)删除子节点即可
    • 在这个案例里面,则是找到显示叉叉的span的li节点,并且通过ul删除li即可(removeChildNode)

知识点

  • 事件监听
    • 可监听的事件
    • event.target指向被监听的元素
  • 获取value
    • input.value
    • 标签.innerText和innerHTML的区别
  • DOM
    • 获取元素的方式:getElementById getElementsByClassName …
    • 动态创建节点:createElement(标签)
    • 添加元素属性:setAttribute(‘calss’, ’box‘)
    • dom节点属性:parentNode childNodes
    • 元素classList属性【HTML新增】:看这篇:DOM | classList属性

实现效果

Code

todoList_demo

需求

实现一个登录功能页面

要求如下:

  1. 能够正常登录登出
  2. 规定用户名和密码的格式

准备工作

  1. react项目构建——不说了
  2. 封装后端接口,实现登录调用和身份验证
  3. antd实现登录页面和注册页面部署

封装后端接口

看这篇:登录功能_node+express+mysql实现用户注册以及token校验

登录页面实现与优化思路

看这篇:react+antd实现登录页面

Bug与问题汇总

看这篇:Bug与问题汇总

问题1:跨域

跨域问题真的太太太太太太重要了!

一开始我采取CROS的方法解决跨域问题,但是不成功,主要原因在于客户端拒绝添加不安全的请求头也就是Origin,后面我采取配置代理的方式解决跨域的问题

代理原理请看下图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/ setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function (app) {
app.use(
"/api",
createProxyMiddleware({
//api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
target: "http://localhost:5000", //配置转发目标地址(能返回数据的服务器地址)
changeOrigin: true, //控制服务器接收到的请求头中host字段的值
/* changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000 changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000 changeOrigin默认值为false,但我们一般将changeOrigin值设为true */
pathRewrite: {
"^/api": "" }, //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
})
)
}

这样一来,所有带api前缀的请求都会被转发到目标(target)上,最后请注意把/api替换为空字符串

问题2: http-proxy-middleware版本问题导致localhost拒绝

一开始,我配置了createProxyMiddleware之后,再次启动react app的时候,竟然!!!localhost拒绝访问!!!【惊了个大呆!】我不信邪,于是我采取最原始的方式,重新创建一个react项目,确定能跑起来之后,把文件一个个复制进去,排查问题在setupProxy.js上,后面发现,不同版本的http-proxy-middleware配置的方式是不同的,主要差别在于createProxyMiddleware引入方式上【高版本按照上面的代码写就好了,顺便提一下,我采用的是"http-proxy-middleware": "^2.0.6"

具体看这里:react中配置setupProxy.js后localhost拒绝访问的问题

问题3: post请求数据格式问题

这个问题困扰了我整整一个下午!!!!

我解决跨域问题之后,不管我怎么输入数据访问一直报错!

我目前的请求配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 登录请求(我单独写出来了~)
import axios from 'axios';

const { username, password } = values; // 解构
const data = {
username,
password,
}

const response = aixos(
method: 'POST',
url,
data,
)

我百思不得其解,postman能够请求成功,但是我通过登录页面却不能成功,并且状态码为500,说明服务器在执行请求的时候错误,于是开始了排bug之旅

  • 首先我想知道服务器有没有接收到我的请求,所以我在服务器写了一个test接口
1
2
3
router.post('/register', async (req, res) => {
res.send({msg: 'test接口调用成功!'})
})

我在前端调用后,返回结果证明,服务器正常返回,接口调用成功

  • 既然接口能够调用成功,说明基本功能是没有问题的,所以我开始思考是不是数据格式的问题,我查看了postman数据格式,我发现postman发送请求的时候,数据格式为x-www-form-urlencoded

后面了解到,发送post请求有几种常见的数据格式:HTTP POST body常见的四种数据格式,之前有看到过说POST请求的时候必须指定数据格式,当时不以为意,现在终于了解了,发送POST请求的时候需要:

  • 设置请求头:'Context-type': 'application/xxx'
  • 转换xxx数据格式

因为平时最熟悉的莫过于JSON数据,于是乎,我一顿操作之后得到了下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 登录请求(我单独写出来了~)
import axios from 'axios';

const { username, password } = values; // 解构
const data = {
username,
password,
}

const response = aixos(
method: 'POST',
url,
data: JSON.stringify(data),
headers: {
'Context-type': 'application/json',
},
)

我想我的春天要来了,终于能够请求成功了!但是依旧报错500!我不甘心,后端打印了’req.body’,发现了问题

什么?数据怎么变这样了!

最终只有一位大哥点题了!

最终代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 登录请求(我单独写出来了~)
import axios from 'axios';
import qs from 'qs';

const { username, password } = values; // 解构
const data = {
username,
password,
}

const response = aixos(
method: 'POST',
url,
data: qs.stringify(data),
headers: {
'Context-type': 'application/json',
},
)

成功~撒花!

血泪教训~

问题3: React-router-dom v6的一些改变

不得不说,React-router-dom v6的变革是真的大!不过咱也要与时俱进,不可以逃避呀~

多写学习吧!

问题4: 组件间通讯

这个我打算后面单独出一篇博客,到时候贴地址,因为我现在还没完全解决组件通信的问题~

HTML5新增了一个classList属性:用于操作元素的className属性,满足基本的增删查

classList是一个新的集合类型DOMTokenList的实例,具有以下方法:

  • length:表示有几项
  • item或者中括号取得元素
  • add(value):向类名列表中添加指定的字符串值value,如果这个值已经存在,则什么也不做
  • contains(value):返回布尔值,表示给定的value 是否存在
  • remove(value):从类名列表中删除指定的字符串值value
  • toggle(value):如果类名列表中已经存在指定的value,则删除;如果不存在,则添加

基本语法

1
element.classList.[method](value)

实例分析

假设现在有一个div,它的class名有:container box selected

1
2
3
4
5
6
<div class="container box selected"></div>

<script>
const div = document.getElementsByTagName('div')[0];
console.log(div.classList);
</script>
  1. 获取classList

从上图可以看到,div.classList是一个DOMTokenList的实例,里面分别枚举了div的class属性值,并且包含length和value属性

  1. 增加disabled类
1
2
div.classList.add('disabled');
console.log(div.classList);
  1. 判断是否包含disabled类
1
console.log(div.classList.contains('disabled'));
  1. 删除disabled类
1
2
div.classList.remove('disabled');
console.log(div.classList);
  1. 切换selected类和user类
1
2
3
div.classList.toggle('selected');
div.classList.toggle('user');
console.log(div.classList);

迭代

classList可以通过for…of来迭代

1
2
3
4
5
const div = document.getElementsByTagName('div')[0];

for (let name of div.classList) {
console.log(name);
}

实现需求

  • 登录页面展示
  • 实现注册和登录功能
  • 实现登出功能
  • 登录成功实现页面跳转
  • 短时间不需要再登录

实现过程

创建React项目

注意:新版React和之前有较大不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';


// @ts-ignore
const root = createRoot(document.getElementById('root'));
root.render(
<App></App>
);


// 之前的写法是
// ReactDOM.render(<App></App>, document.getElementById('root'))

前端UI登录界面基本布局

这部分我是在pages/login/index.js完成的

当然不要忘记把组件放到App中去~

需求:

  1. 完成登录界面基本布局

  2. 完成登录表单

实现登录界面

采用antd里面的基本布局(header content footer)和表单实现

【不要怕!按照你想的魔改就好了!】

实现效果:

完善注册页面

还是采用antd里面的表单进行实现,这里要注意的是,是点击register now!之后,才会切换到注册页面,这里我觉得注册页面单独没什么必要,所以我采取的是把注册页面嵌套在对话框里面,根据自己的喜好啦~

实现效果:

接口配置

Promise对axios进行二次封装

二次封装的好处在于:

  1. 实现全局axios模块,因为平时发送网络请求多样,有get请求,post请求等等
  2. 可以全局部署请求拦截器和响应拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// src/api/ajax.js
import axios from 'axios';
import qs from 'qs';
import {message} from 'antd';


const instance = axios.create({
timeout: 10000,
})

// 添加请求拦截器
instance.interceptors.request.use(config => {
// console.log('config', config);
config.headers.Authorization = sessionStorage.getItem('token') || '';
return config;
}, err => {
return Promise.reject(err);
});

// 添加响应拦截器
instance.interceptors.response.use(response => {
console.log(response);
// 我后端是权限校验成功
if (response.data.msg === '权限校验成功!') {
window.history.pushState(null, 'home','/');
} else {
return Promise.reject(response.data.msg);
}
}, err => {
if (err.response) {
// token失效,回到登录页面
window.history.pushState(null, 'login','/login');
}
})

export default function ajax(method='GET', url, data={}, config) {

return new Promise((resolve, reject) => {
let promise;
// 执行异步ajax请求
if (method === 'GET') {
promise = instance({
method:'GET',
url,
params:data,
...config,
}) // params配置指定的是query参数
} else {
promise = axios({
method: 'POST',
url,
data: qs.stringify(data),
headers: {
'Context-type': 'application/json',
'Authorization': 'Bearer ' + window.sessionStorage.getItem('token'),
},
...config,
})
};

promise.then(response => {
// 如果成功了,调用resolve(response.data)
resolve(response);
}).catch(error => { // 对所有ajax请求出错做统一处理,这样外层就不用再处理错误了
// 如果失败了,提示请求后台出错
reject(message.error(error));
})
})
}

写接口

接口主要根据写的后端接口配

解决跨域问题

一开始,我采用CORS解决跨域

报错问题: Refused to set unsafe header “Origin”

这说明设置的Origin字段是无效的,不管我设置什么或者不设置这个字段时查看Origin字段时它的值都是null,有人说那个Origin字段本来就应该是浏览器来设置的,自己设置不安全

所以导致我在服务端写了Access-Control-Allow-Origin一直还是没用,所以后面采用了代理的方式进行跨域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function (app) {
app.use(
"/api",
createProxyMiddleware({
//api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
target: "http://localhost:5000", //配置转发目标地址(能返回数据的服务器地址)
changeOrigin: true, //控制服务器接收到的请求头中host字段的值
/* changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000 changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000 changeOrigin默认值为false,但我们一般将changeOrigin值设为true */
pathRewrite: {
"^/api": "" }, //去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
})
)
}

最终接口代码:

1
2
3
4
5
6
7
8
9
10
11
// src/api/index.js
import ajax from './ajax';

// 注册
export const reqRegister = (username, password) => ajax('POST', '/api/register', {username, password});

// 登录
export const reqLogin = (username, password) => ajax('POST', '/api/login', {username, password});

// 权限校验
export const reqAuth = () => ajax('POST', '/api/auth', {});

路由部署

实现效果:

  1. 访问home页面,如果没有登录,重定向到login页面
  2. 登录成功后跳转到home页面
  3. home页面设置登出
  4. 设置注册页面,可以提交表单信息

好久没写router了,此时最新的是"react-router-dom": "^6.3.0",发现更新了不少~

不要因为不熟悉就卸载下载旧版本呀【以前的我经常这么干!哈哈哈哈哈】

还是要不断学习!拥抱变化!

(干货) 全网最全 react-router-dom v6.0学习指南

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/router/index.js
import Home from '../pages/home';
import Login from '../pages/login';


export const routes = [
{
path: '/',
component: Home,
exact: true,
}, {
path: '/login',
component: Login,
}
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/App.js
import React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { routes } from './router';

export default function App() {
return (
<BrowserRouter>
<Routes>
{
routes.map((route) => (
<Route
key={route.path}
path={route.path}
//exact={route.exact}
element={<route.component/>}
></Route>
))
}
<Route path='*' element={<Navigate to='/login'/>}></Route>
</Routes>
</BrowserRouter>
)
}

这样能够实现路由正常跳转了~

接口调用

接口主要用于实现登录功能,所以基本是下面的流程:

  1. 输入用户名和密码,登录(发送登录请求)
  2. 根据后端返回的结果判断是哪种状态
    1. 登录成功:跳转到主页(’/‘),把返回的token存在sessionStorage里面
    2. 用户不存在:停留在原页面,提示用户不存在
      1. 用户点击register now!之后,弹出注册页面,用户输入信息后,注册(发送注册请求)
    3. 密码错误:停留在原页面,提示密码错误,用户需要再次输入

具体看/src/login部分源码吧~

值得注意的是:跳转页面采用useNavigate

自动校验token

我这里采取的是,在login页面渲染的时候发送自动校验的请求,目前只想到这个~

登出页面

我主要设置在home组件里面,登出就很简单,要做的就是:

  1. 删除sessionStorage里面的token
  2. 跳转到登录页面(非必要!)

优化思考

优化1: 考虑设置token的有效时长

优化2: 考虑组件间传值

比如用户登陆后可以在home组件访问到是谁登录的,欢迎XXX诸如此类

知识点总结

  • 登录流程
  • antd组件应用
  • axios请求相关:封装、请求头设置、发请求、拦截器等
  • 跨域
  • react-router:基本配置,跳转路由
  • token相关
  • cookie sessionStorage localStorage

参考

antd

styled-components

(干货) 全网最全 react-router-dom v6.0学习指南

实现需求

  • 登录
  • 注册
  • 权限校验

实现过程

  • 客户端使用用户名和密码请求登录
  • 服务端收到请求后验证是否登录成功
    • 成功:返回一个token给客户端
    • 失败:提示失败信息
  • 客户端收到token后存储token(token采用jwt进行加密,而不是普通的base64)
  • 每次发起请求时将token发给服务端
  • 服务端接收到请求后,token的合法性
    • 成功:返回客户端所需数据
    • 失败:返回验证失败的信息

流程图解

实现流程

准备工作

安装

  • node
  • express
  • mysql

初始化项目

1
npm init -y

安装express

1
npm i express

启动node服务_新建app.js文件

1
2
3
4
5
6
7
8
9
10
11
// app.js
const express = require('express');

const app = express();

/*
port: 4000
*/
app.listen(4000, () => {
console.log('serve is running on port: 4000');
})

app.listen参数

启动端口

1
node app.js 

连接MySQL

下面的过程都可以看文档:sequelize文档

安装Sequelize和MySql

1
2
npm i sequelize # 这将安装最新版本的 Sequelize
npm i mysql2 # MySQL

连接到数据库_database/init.js

要连接到数据库,必须创建一个 Sequelize 实例. 这可以通过将连接参数分别传递到 Sequelize 构造函数或通过传递一个连接 URI 来完成

1
2
3
4
5
6
7
8
// database/init.js
const { Sequelize } = require('sequelize');

const sequelize= new Sequelize('数据库名称', 'root', '你的密码', {
host: 'localhost',
port: '3306',
dialect: 'mysql'
})

测试连接

引入到app.js

1
require('./database/init')

你可以使用 .authenticate() 函数测试连接是否正常:

1
2
3
4
5
6
try {
await sequelize.authenticate();
console.log('Connection has been established successfully.');
} catch (error) {
console.error('Unable to connect to the database:', error);
}

关闭连接

默认情况下,Sequelize 将保持连接打开状态,并对所有查询使用相同的连接. 如果你需要关闭连接,请调用 sequelize.close()(这是异步的并返回一个 Promise).

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// database/init.js
const { Sequelize } = require('sequelize');

const sequelize= new Sequelize('test_login', 'root', 'yyy674531', {
host: 'localhost',
// @ts-ignore
port: '3306',
dialect: 'mysql'
})


// 测试连接
try {
sequelize.authenticate();
console.log('Connection has been established successfully.');
} catch (error) {
console.error('Unable to connect to the database:', error);
}


module.exports = {Sequelize, sequelize}; // 这里导出,下面有用!

同步表模型

创建模型实例_database/models/User.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// database/models/User.js
const { Op, Model, DataTypes } = require('sequelize');
const { Sequelize, sequelize } = require('../init.js');

const User = sequelize.define('users_info', {
// 应用属性: 禁止null 唯一约束 验证器
username: {
type: DataTypes.STRING,
allowNull: false, // 1. 禁止 null 值
unique: true, // 2. 唯一约束
},
password: {
type: DataTypes.STRING,
allowNull: false,
// validate: { // 3. 验证器
// is: /^[0-9a-f]{64}$/i
// }
}
});

// 将模型与数据库同步
/*
注意:如果表已经存在,使用{force: true}将该表删除
*/
(async () => {
await User.sync().then(() => {
console.log('user表模型已经同步');
})
})();

导出到app.js并启动

实现路由部署(准备)

Express 教程 4:路由和控制器

路由部署_express.Router

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// router/user.js
const express = require('express');
const router = express.Router();

// middleware that is specific to this router
// router.use(function timeLog(req, res, next) {
// console.log('Time:', Date.now());
// next();
// });

// 注册
router.get('/home', (req, res) => {
res.send({msg: 'Welcome!'});
})

module.exports = router;

引入使用

1
2
3
4
5
// app.js
const router = require('./router/user');

app.use(express.urlencoded({extended: false}));
app.use('/user', router);

postman测试

实现注册功能

路由部署

1
2
3
4
5
// router/user.js
router.post('/register', async (req, res) => {
const {username, password} = req.body;
console.log(username); // postman测试能够拿到username信息
})

可以看到控制台已经打印了~说明能够获取到username的信息

注册功能说明:

服务器获取到浏览器传过来的username

首先检验是否存在这个username,如果存在就返回“该用户已经存在,请直接登录”

如果不存在,再进行创建

  1. 去哪里检验——MySQL数据库里面
  2. 怎么创建——利用MySQL的create

接下来会详细说明

用户查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// router/user.js
// 注册
const bcryptjs = require('bcryptjs');

router.post('/register', async (req, res) => {
const {username, password} = req.body;
// console.log(username); // postman测试能够拿到username信息

// 用户查询
const model = await User.findOne({where: {username: username}});

if (model) {
return res.send({msg: '该用户已经存在,请直接登录!'})
}else {
const user = await User.create({username, password:bcryptjs.hashSync(password, 5)}); // 加密
// console.log(user.dataValues);
res.send({msg: '注册成功!'});
}
})

postman测试:创建成功~

查询数据库:创建成功~

当我再次用同样的username进行注册请求的时候,返回结果为:该用户已经存在,请直接登录!

注册功能验证成功~

实现登录功能

登录功能说明:

通过发送username和password进行登录

  • 如果uername不存在,就返回“该用户不存在,请先注册”

  • 如果username存在,要先进行密码校验

    • 密码校验不成功——返回“密码错误”
    • 密码校验成功——返回token

登录功能部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// router/user.js
// 登录功能
const bcryptjs = require('bcryptjs');

router.post('/login', async (req, res) => {
const {username, password} = req.body;
console.log(req.body);
const model = await User.findOne({where: {username: username}});
console.log(model);

if (!model) {
return res.send({msg: '该用户不存在,请先注册!'})
} else {
// 如果存在username就要进行密码校验
const passwordValid = bcryptjs.compareSync(password, model.dataValues.password);

if (!passwordValid) {
return res.send({msg: '密码错误!'})
} else {
res.send({msg: '登录成功!'})
}
}

})

正确输入用户名和密码:

输入错误密码:

输入未注册用户名:

token生成及返回_jsonwebtoken

jwt中文文档

1
jwt.sign(payload, secretOrPrivateKey, [options, callback]);
1
2
3
// router/user.js
const token = jwt.sign({username}, 'ccken')
res.send({token})

实现权限校验

权限校验说明:

  1. token包含在请求头里面,所以第一步我们要获取token
    • 如果没有token说明前面没有登录过或者是登录已经失效,返回“请登录”
  2. 利用token 密钥 和 payload对token进行解析
  3. 对解析结果进行校验查询
    • 如果查询结果为空,返回’请注册‘
    • 如果不为空,则说明“权限校验成功!”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// router/user.js
// 权限校验
router.post('/auth', async (req, res) => {
// 获取token,token是在请求头里面的
const token = String(req.headers.authorization).split(' ').pop();
// 如果没有token说明前面没有登录过或者是登录已经失效
if (!token) return res.send({msg: '请登录!'})
// 根据密钥和字段解析token
const {username} = jwt.verify(token, 'ccken'); // 之前使用username进行注册的
// 对解析结果进行校验查询
const model = User.findOne({where: {username: username}});

if (!model) {
return res.send({msg: '请注册!'});
} else {
res.send({msg: '权限校验成功!'})
}
})

代码

以上过程的代码放在github了~

参考

app.listen参数

sequelize文档

Express 教程 4:路由和控制器

jwt中文文档

cookie、session、token、jwt、单点登录