步骤:
引入 createSlice
import { createSlice } from "@reduxjs/toolkit"
创建 slice 对象
const xxxSlice = createSlice({/**/})
配置对象参数
const xxxSlice = createSlice({
name: '',
initialState: {},
reducers: {
setName(state, action) {},
setAddress(state, action) {}
}
})
属性名 | 值类型 | 作用 |
---|---|---|
name | String | 指定唯一标识 |
initialState | any | 设置切片的初始值 |
reducers | Function | 设置切片的方法 |
导出切片和对应的方法
export const { setName, setAddress } = xxxSlice.actions
export const { reducer: xxxReducer } = xxxSlice
例子:
// src/store/schoolSlice.js
import { createSlice } from "@reduxjs/toolkit"
const schoolSlice = createSlice({
name: 'school',
initialState: {
name: 'hdu',
address: '白杨街道'
},
reducers: {
setName(state, action) {
state.name = action.payload
}
}
})
export const { setName, setAddress } = schoolSlice.actions
export const { reducer: schoolReducer } = schoolSlice
步骤:
引入 configureStore
import { configureStore } from "@reduxjs/toolkit"
引入切片
import { xxxReducer } from './xxxSlice'
配置对象参数
const store = configureStore({
reducer: {
xxx: xxxReducer
}
})
属性名 | 类型 | 作用 |
---|---|---|
reducer | 对象 | 指定存储的切片 |
xxx | 切片对象 | 为切片对象起名字 |
导出store
export default store
最终效果
import { configureStore } from "@reduxjs/toolkit"
import { schoolReducer } from "./schoolSlice"
const store = configureStore({
reducer: {
school: schoolReducer
}
})
export default store
main.jsx
使用切片
6.1 引入 store
和 Provider
标签
import { Provider } from "react-redux"
import store from './store/index'
6.2 将 Provider
标签作为 APP
的父标签,并指定 store
root.render(
<Provider store={store}>
<App />
</Provider>
)
6.3 实例
// src/main.jsx
import ReactDOM from "react-dom/client"
import App from "./App"
import { Provider } from "react-redux"
import store from './store/index'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)
组件中使用切片
7.1 导入 useDipatch、useSelector
方法
import { useDispatch, useSelector } from 'react-redux'
7.2 导入切片中的方法
import { setName as setSchoolName, setAddress } from './store/school'
7.3 使用 useSelector
访问切片中的数据
其中 state.school
中的 school 是 步骤3 中指定的 xxx
const student = useSelector(state => state.school)
7.4 使用 useDispatch
访问切片中的方法
const dispatch = useDispatch()
const setNameHandler = () => {
dispatch(setSchoolName('杭腚'))
// 上下两个方法等效,其中 school 是在切片中指定的 name 属性
dispatch({ type: 'school/setName', payload: '杭腚' })
}
console.log(setSchoolName('杭腚'))
返回的数据
7.5 实例
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { setName as setSchoolName, setAddress } from './store/school'
const App = () => {
const school = useSelector(state => state.school)
const dispatch = useDispatch()
return (
<div>
<p>
{school.name} ---
{school.address}
</p>
<button onClick={() => dispatch(setSchoolName('杭腚'))}>修改名字</button>
<button onClick={() => dispatch(setAddress('地球'))}>修改地址</button>
</div>
)
}
export default App
创建 slice
// src/store/schoolSlice.js
import { createSlice } from "@reduxjs/toolkit"
const schoolSlice = createSlice({
name: 'school',
initialState: {
name: 'hdu',
address: '白杨街道'
},
reducers: {
setName(state, action) {
state.name = action.payload
},
setAddress(state, action) {
state.name = action.payload
}
}
})
export const { setName, setAddress } = schoolSlice.actions
export const { reducer: schoolReducer } = schoolSlice
store 入口文件
// src/store/index.js
import { configureStore } from "@reduxjs/toolkit"
import { stuReducer } from "./stuSlice"
import { schoolReducer } from "./school"
const store = configureStore({
reducer: {
school: schoolReducer
}
})
export default store
main.jsx 入口文件
// src/main.jsx
import ReactDOM from "react-dom/client"
import App from "./App"
import { Provider } from "react-redux"
import store from './store/index'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)
组件使用
// src/App.jsx
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { setName as setSchoolName, setAddress } from './store/school'
const App = () => {
const school = useSelector(state => state.school)
const dispatch = useDispatch()
return (
<div>
<p>
{school.name} ---
{school.address}
</p>
<button onClick={() => dispatch(setSchoolName('杭腚'))}>修改名字</button>
<button onClick={() => dispatch(setAddress('地球'))}>修改地址</button>
</div>
)
}
export default App
步骤:
导入 createApi、fetchBaseQuery
方法
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react'
创建 API切片
实例
const studentApi = createApi({
reducerPath: 'studentApi',
baseQuery: fetchBaseQuery({
baseUrl: 'http://127.0.0.1:3033/api/'
}),
tagTypes: ['student', 'teacher'],
endpoints(build) {
return {
getStudents: build.query({
query() {
return 'students'
},
transformResponse(baseQueryReturnValue) {
return baseQueryReturnValue.data
},
providesTags: [{type: 'student', id: 'LIST'}]
}),
addStudent: build.mutation({
query(stu) {
return {
url: 'students',
method: 'post',
body: {
data: stu
}
}
},
invalidatesTags: [{type: 'student', id: 'LIST'}]
})
}
}
})
createApi
参数解析
参数名 | 类型 | 作用 |
---|---|---|
reducerPath | string | 指定唯一标识 |
baseQuery | Function | 一般指定 fetchBaseQuery 方法,指定请求根路径 |
tagTypes | String[] | 标签类型,标签用于方法之间的关联 |
endPoints | Function | 指定请求的一些方法 |
endpoints
解析
发起 get
请求
getXxx: build.query({
query() {
return '子路径'
}
})
发起 post
请求
addXxx: build.mutation({
query() {
return {
url: '子路径',
method: 'post',
body: {
data: 'xxx'
}
}
}
})
发起 put
请求
updateXxx: build.mutation({
query(id) {
return {
method: 'put',
url: `子路径/${id}`
}
}
})
参数
参数名 | 作用 |
---|---|
transformResponse(baseQueryReturnValue) | 指定返回的结果,例如完整的相应结果是 {status: ‘200’, data: [‘xj’, ‘sx’]} ,指定 return baseQueryReturnValue.data 后,返回的结果就是 {data: [‘xj’, ‘sx’]} |
keepUnusedDataFor | 指定缓存的时间,单位 s |
providesTags | 指定某个方法具有的标签,当另一个方法使这个标签无效后,具有这个标签的方法就会触发 |
invalidatesTags | 使某个或多个标签无效 |
如果想要更具体的指定某个数据,可以将 providesTags
、invalidateTags
变为对象或者函数形式
getStudentsById: build.query({
query(id) {
return `students/${id}`
},
providesTags: (result, error, id) => ([{type: 'student', id: id}])
})
updateStudent: build.mutation({
query(stu) {
return {
url: `students\${stu.id}`,
method: 'put',
body: {
data: stu.attributes
}
}
},
invalidatesTags: ((result, error, stu) => [
{ type: 'student', id: stu.id },
{ type: 'student', id: 'LIST' }
])
})
导出方法
注意命名规范
useXxxQuery
useXxxMutation
export const {
useGetStudentsQuery,
useGetStudentByIdQuery,
useDelStudentMutation,
useAddStudentMutation,
useUpdateStudentMutation
} = studentApi
export default studentApi
总结代码
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react'
const studentApi = createApi({
reducerPath: 'studentApi',
baseQuery: fetchBaseQuery({
baseUrl: 'http://127.0.0.1:3033/api/'
}),
tagTypes: ['student', 'teacher'],
endpoints(build) {
return {
getStudents: build.query({
query() {
return 'students'
},
transformResponse(baseQueryReturnValue) {
return baseQueryReturnValue.data
},
providesTags: [{type: 'student', id: 'LIST'}]
}),
getStudentById: build.query({
query(id) {
console.log('id', id)
return 'students/' + id
},
transformResponse(baseQueryReturnValue) {
console.log('baseQueryReturnValue', baseQueryReturnValue)
return baseQueryReturnValue.data
},
keepUnusedDataFor: 5,
providesTags: (result, error, id) => ([{type: 'student', id: id}])
}),
delStudent: build.mutation({
query(id) {
return {
// 如果发送的不是get请求,需要返回一个对象设置请求的信息
url: `students/${id}`,
method: 'delete'
}
}
}),
addStudent: build.mutation({
query(stu) {
return {
url: 'students',
method: 'post',
body: {
data: stu
}
}
},
invalidatesTags: [{type: 'student', id: 'LIST'}]
}),
updateStudent: build.mutation({
query(stu) {
return {
url: `students\${stu.id}`,
method: 'put',
body: {
data: stu.attributes
}
}
},
invalidatesTags: ((result, error, stu) => [
{ type: 'student', id: stu.id },
{ type: 'student', id: 'LIST' }
])
})
}
}
})
export const {
useGetStudentsQuery,
useGetStudentByIdQuery,
useDelStudentMutation,
useAddStudentMutation,
useUpdateStudentMutation
} = studentApi
export default studentApi
const result = useGetStudentsQuery(null, {
// useQuery 可以接受一个对象作为第二个参数,通过该对象可以对请求进行配置
selectFromResult: result => { // 用来指定 useQuery 返回的结果
if(result.data) {
result.data = result.data.filter(item => item.attributes.age < 18)
}
return result
},
pollingInterval: 0, // 设置轮询的间隔,单位毫秒,0为不轮询
skip: false, // 设置是否跳过当前请求,默认false
refetchOnMountOrArgChange: false, // 设置是否每次都重新加载数据,false正常使用缓存,true每次都重新新加载数据,数字设置缓存的时间
refetchOnFocus: true, // 是否在重新获得焦点时加载数据,例如页面切换
refetchOnReconnect: false, // 是否在重新连接后重新加载数据(网又有了)
})
关键词:
可以参考一下目录结构构建
有时候,当网页涉及到权限问题的时候,会将token存储到头部信息的 Authorizaiton
交付给服务器验证,这时候需要使用 prepareHeaders
为 endpoints
的每个方法都指定响应头。
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
headers.set('Authorization', `Bearer ${token}`)
return headers
}
其中 getState
可以获取 name
为 auth
的切片值,使用 headers.set()
方法即可指定头部信息。
完整代码:
其中可以直接写 export const authApi = createApi({/* CODE HERE */})
// auth.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/dist/query/react'
export const authApi = createApi({
reducerPath: 'authApi',
baseQuery: fetchBaseQuery({
baseUrl: 'http://127.0.0.1:3033/api/',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
headers.set('Authorization', `Bearer ${token}`)
return headers
}
}),
endpoints(build) {
return {
register: build.mutation({
query(user) {
return {
url: 'auth/local/register',
method: 'post',
body: user, // username, password, email
}
},
}),
login: build.mutation({
query(user) {
console.log('user', user)
return {
url: 'auth/local',
method: 'post',
body: user // identifier
}
}
})
}
}
})
export const {
useRegisterMutation,
useLoginMutation
} = authApi
initialState
也可以是是一个函数,更灵活地操作和指定返回值。
initialState: () => {
const token = localStorage.getItem('token')
if(!token) {
return /* CODE HERE */
}
return /* CODE HERE */
}
完整代码
import { createSlice } from "@reduxjs/toolkit";
export const authSlice = createSlice({
name: 'auth',
initialState: () => {
const token = localStorage.getItem('token')
if (!token) {
return {
isLogged: false,
token: '',
user: null,
expirationTime: 0
}
}
return {
isLogged: true,
token,
user: JSON.parse(localStorage.getItem('user')),
expirationTime: +localStorage.getItem('expirationTime')
}
},
reducers: {
login(state, action) {
state.isLogged = true
state.token = action.payload.token
state.user = action.payload.user
// 获取当前时间戳
const currentTime = Date.now()
// 设置登录的有效时间
const timeout = 1000 * 60 * 60 *24 * 7 // 一周
setTimeout.expirationTime = currentTime + timeout // 设置失效日期
localStorage.setItem('token', state.token)
localStorage.setItem('user', JSON.stringify(state.user))
localStorage.setItem('expirationTime', state.expirationTime + '')
},
logout(state, action) {
state.isLogged = false
state.token = ''
state.user = null
localStorage.removeItem('token')
localStorage.removeItem('user')
localStorage.removeItem('expirationTime')
}
}
})
export const { login, logout } = authSlice.actions
其实配置的项很少,主要有一些坑,之前一直没注意到:
例如
[authApi.reducerPath]
是一个表达式,在 authApi
文件中我们已经指定了 reducerPath
,所以应该是唯一的。configureStore
中的 reducer
属性,配置的属性后面都有 .reducer
后缀,这样不管是 Api
还是 Slice
都好记一些middleware
中间件配置,concat()
函数可以用 ,
分隔多个中间件const store = configureStore({
reducer: {
[authApi.reducerPath]: authApi.reducer,
[studentApi.reducerPath]: studentApi.reducer,
auth: authSlice.reducer
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(
authApi.middleware,
studentApi.middleware
)
})
完整代码:
import { configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/dist/query";
import { authApi } from "./api/authApi";
import studentApi from "./api/studentApi";
import { authSlice } from "./reducer/authSlice";
const store = configureStore({
reducer: {
[authApi.reducerPath]: authApi.reducer,
[studentApi.reducerPath]: studentApi.reducer,
auth: authSlice.reducer
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(
authApi.middleware,
studentApi.middleware
)
})
setupListeners(store.dispatch)
export default store
主要是忘记了,这里再写一点
如果是 Query Api
Query Api
钩子函数返回的是一个对象,其中 data
是服务器返回的值,isSuccess
是返回是否成功。
const { data, isSuccess } = useGetStudentsQuery()
如果是 Mutation Api
Mutation Api
钩子函数返回的是一个数组,其中数组第一项是请求 Api
函数,即发起请求的函数,第二项是一个数组,有很多属性,依照名字看很好理解,不多介绍。
const [regFn, { error: regError, isSuccess: regIsSuccess }] = useRegisterMutation()
以 regFn
为例,其返回值是一个 Promise
对象,可以对服务器返回的数据进行操作。
loginFn({
identifier: username,
password: password
}).then(res => {
if(!res.error) {
// 登录成功后,需要向系统中添加一个标识,标记用户的登录状态
// 登录状态(布尔值,token(jwt))
// 跳转页面到根目录
dispatch(login({
token: res.data.jwt,
user: res.data.user
}))
navigate('/form')
}
})
react router 可以将 url 地址和组件进行映射。当用户访问某个地址时,与其对应的组件会自动的挂载
安装: npm i react-router-dom@5 -S
HashRouter 会通过url地址中的hash值来对地址进行匹配
HashRouter 对于 seo 不是很友好
BrowserRouter 直接通过url地址进行组件的跳转
使用过程中和普通的url地址没有区别
BrowserRouter 问题在于当我们通过 <Link> 标签跳转时,刷新页面会重新向服务器请求数据,这时候页面会返回404,因为这次请求没有经过 react router。
解决方法:
main.jsx
中使用引入 react-router-dom 包
// 这里重命名是为了以后更换路由模式更加方便
import { BrowerRouter as Router } from 'react-router-dom'
将 <App /> 组件包裹
<Router>
<App />
</Router>
最终代码
// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { BrowserRouter as Router } from 'react-router-dom'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>,
)
App.jsx
和 其他组件
中使用引入 Route 标签和组件
import { Route } from 'react-router-dom'
import About from './components/About'
import Home from './components/home'
import Menu from './components/Menu'
import Student from './components/Student'
配置路由
配置路由有很多方式,依次介绍
2.1 component 方式:
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/student/:id" component={Student} />
属性 | 作用 |
---|---|
path | 路由映射的 url 地址,其中上述例子中的 :id 为 uri 参数 |
component | 路由要挂载的组件 |
exact | 路径是否完全匹配,React Router5 默认不是完全匹配,例如指定 path=“/about” ,访问 .../about/hello 也可以看到被挂载的组件 |
这时在 Student组件
中就可以访问到路由参数了
import React from 'react'
const STU_DATA = [
{ id: 1, name: 'xj' },
{ id: 2, name: 'sx' },
{ id: 3, name: 'yq' },
{ id: 4, name: 'xm' },
]
const Student = (props) => {
console.log(props) // 路由参数
const stu = STU_DATA.find(item => item.id === +props.match.params.id)
return (
<div>
{stu.id} --- {stu.name}
</div>
)
}
export default Student
2.2 render 方式
render 也可以用来指定要挂载的组件,不过需要一个回调函数作为参数,回调函数的返回值最终会被挂载
注意: 如果希望被挂载的组件访问到路由参数,一定要指定组件的路由属性
<Route path="/student/:id" render={(routeProps) => {
console.log(routeProps)
return <Student {...routeProps} /> {/* 一定要这样传递参数,不然 Student 组件无法访问路由参数 */}
}}
/>
2.3 children 方式
children 也可以用来指定被挂载的组件,用法有两种:
所以不建议
<Route path="/student/:id" children={(routeProps) => <Student {...routeProps} />} />
<Route path="/student/:id" children={<Student />} />
2.4 直接写组件
<Route path="student/:id">
<Student />
</Route>
这种方式更加清晰,但需要注意的是,Student组件
是无法直接打印 props 来获取路由参数的,但是我们可以通过钩子函数,这也是更加常用的情况
import React from 'react'
import { useHistory, useLocation, useParams, useRouteMatch } from 'react-router-dom'
const STU_DATA = [
{ id: 1, name: 'xj' },
{ id: 2, name: 'sx' },
{ id: 3, name: 'yq' },
{ id: 4, name: 'xm' },
]
const Student = (props) => {
// 通过钩子函数获取路由参数
const match = useRouteMatch()
const location = useLocation()
const history = useHistory()
const params = useParams()
const stu = STU_DATA.find(item => item.id === +params.id)
return (
<div>
{stu.id} --- {stu.name}
</div>
)
}
export default Student
路由参数
参数名 | 作用 |
---|---|
match | 匹配的信息 |
location | 路由地址信息 |
history | 控制页面的跳转 |
3.1 match 参数
参数名 | 类型 | 含义 |
---|---|---|
isExact | Boolean | 是否与指定路径完全匹配 |
params | 对象 | 路由传递的参数,例如 student/:id 中的 id 便是 params 的一个属性 |
path | String | 指定路由的地址,例如 <Route path=“/student/:id” /> ,那么 path 就是 “/student/:id” |
url | String | 浏览器实际访问的路由地址,例如访问 http://127.0.0.1/student/3 ,那么 url 就是 “/student/3” |
3.2 location 参数
参数名 | 类型 | 含义 |
---|---|---|
hash | 字符串 | hash地址 |
pathname | 字符串 | 与 match 的 path 参数一致 |
search | 字符串 | 查询字符串,即?之后的参数 |
state | 用户指定类型 | 使用 history.push({path: ‘/student/2’, state: {msg: ‘沈昕姐姐,嘿嘿’}}) ,手动跳转地址时,state会显示 |
3.3 history 参数
参数名 | 类型 | 作用 |
---|---|---|
push | Function | 跳转页面,带后退前进功能 |
replace | Function | 也是跳转页面,不过没有后退前进功能 |
可以将 Route 统一放到 Switch 中,一个 Switch 中只会有一个路由显示
注意:根标签要加上 exact,不然只显示 Home 组件了
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about">
<About />
</Route>
<Route path="/login">
<Login />
</Route>
<Route path="/student/:id">
<Student />
</Route>
</Switch>
Redirect 用于重定向,例如未成功登录强制跳转到登录页面,登陆成功跳转到内容页面
<Route path="/form">
{ isLogin ? <MyForm /> : <Redirect to={"/login"} /> }
</Route>
当然也可以指定 哪个路由跳转到哪个路由,例如下面指的是当访问路由地址为 /abc 时跳转到 /login,访问其他路由地址就不会跳转
<Redirect from={"/abc"} to={"/login"} />
在 App.jsx 中使用
// App.jsx
import React from 'react'
import { Route } from 'react-router-dom'
import About from './components/About'
import Hello from './components/Hello'
const App = () => {
return (
<div>
<Route path="/about">
<About />
<Route path="/about/hello">
<Hello />
</Route>
</Route>
</div>
)
}
export default App
当然也可以直接在 About 组件中嵌套
import React from 'react'
import { Redirect, Route, useRouteMatch } from 'react-router-dom'
import Hello from './Hello'
const About = (props) => {
// 获取父路径
const { path } = useRouteMatch()
return (
<div>
<h2>关于我们</h2>
<ul>
<li>xj</li> <li>sx</li> <li>yq</li> <li>xm</li>
</ul>
{/* 子路由嵌套 */}
<Route path={`${path}/hello`}>
<Hello />
</Route>
</div>
)
}
export default About
如果用户随便输入了路径地址,此时要做一定的处理
<Route path="*">
路径匹配错误
</Route>
一般使用组件嵌套 + 钩子函数组合使用
// main.jsx
import React from 'react'
import { Route } from 'react-router-dom'
import About from './components/About'
import Home from './components/home'
import Menu from './components/Menu'
import Student from './components/Student'
const App = () => {
return (
<div className="APP">
<Menu />
<Route path="/">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/student/:id">
<Student />
</Route>
</div>
)
}
export default App
// student.jsx
import React from 'react'
import { useHistory, useLocation, useParams, useRouteMatch } from 'react-router-dom'
const STU_DATA = [
{ id: 1, name: 'xj' },
{ id: 2, name: 'sx' },
{ id: 3, name: 'yq' },
{ id: 4, name: 'xm' },
]
const Student = () => {
const match = useRouteMatch()
const location = useLocation()
const history = useHistory()
const params = useParams()
const stu = STU_DATA.find(item => item.id === +params.id)
return (
<div>
{stu.id} --- {stu.name}
</div>
)
}
export default Student
React Router 6 更改的特性有很多,例如:
<Route />
组件必须由 <Routes />
组件包裹element={<Xxx />}
path
指定路径可以不用加 /// main.jsx
<Routes>
<Route path="student/:id" element={<Student />}></Route>
<Route path="about" element={<About />}>
{/* Hello组件路由地址为: /about/hello */}
<Route path="hello" element={<Hello />}></Route>
{/* Abc组件路由地址为: /about/abc */}
<Route path="abc" element={<Abc />}></Route>
</Route>
</Routes>
组件中的使用,以 About
组件为例,只需要加入 <Outlet />
组件,即可在此区域显示子路由内容
Outlet 用来标识路由中的组件:
import React from 'react'
import { Routes, Route, Outlet } from 'react-router-dom'
const About = () => {
return (
<div>
<h2>这是About组件</h2>
<Outlet />
</div>
)
}
export default About
当然也可以使用另一种方法
// main.jsx
<Routes>
<Route path="about" element={<About />}></Route>
</Routes>
About 组件:
// component/About.jsx
import React from 'react'
import Hello from './Hello'
import { Routes, Route } from 'react-router-dom'
const About = () => {
return (
<div>
{/* 通过子路由来对 Hello 进行映射, /about/hello */}
<Routes>
<Route path="hello" element={<Hello />} />
</Routes>
</div>
)
}
export default About
Navigate
组件与 V5 版本的 Redirect
组件类似,用来指定重定向
Navigate
默认使用 push
跳转,但是可指定 replace
属性更换为 replace
跳转
<Navigate to="student/1" replace />
Link 组件与 V5 版本一致,NavLink 在指定链接样式的时候,需要一个回调函数
回调函数中有一个 isActive 布尔值形参,代表链接是否激活
<NavLink
style={({ isActive }) => {
return isActive ? { backgroundColor: 'red' } : null
}}
to="/home"
>主页</NavLink>
NextJS 是一个轻量级的 React 服务端渲染应用框架
相比于 React 中传统的路由,NextJS 实现路由的方式是通过文件夹目录结构实现
NextJS 会将文件夹名或者文件名当做路由的地址,其中 index.jsx
便是路由地址的入口文件名,如果想要创建子路由,那么可以在对应的路由新建文件,文件名代表子路由地址,也可以新建文件夹,在对应文件夹中新建 index.jsx
,此时文件夹名代表子路由地址。
路由分为静态路由和动态路由,静态路由的实现很简单只需要新建文件就可以,动态路由则需要在文件夹名加入 []
。对于很多参数的路由,还可以使用 [...xxx]
的方式获取参数。
以上图为例解释对应的路由地址:
about/index.jsx --> http://127.0.0.1:3000/about
blog/[...slug].jsx --> http://127.0.0.1:3000/2022/02/02/...
clients/index.jsx --> http://127.0.0.1:3000/clients
clients/[id]/index.jsx --> http://127.0.0.1:3000/clients/2
clients/[id]/[clientprojectid].jsx --> http://127.0.0.1:3000/clients/2/projectA
portfolio/index.jsx --> http://127.0.0.1:3000/portfolio
portfolio/list.jsx --> http://127.0.0.1:3000/portfolio/list
portfolio/[projectid].jsx --> http://127.0.0.1:3000/portfolio/projectA
聪明的你可能已经发现,动态路由和静态路由可能会有冲突的地方,例如 portfolio
中的 list.jsx
和 [projectid].jsx
两个文件,当我访问 http://127.0.0.1:3000/portfolio/list,NextJS 会将 list
当做静态路由还是动态路由的参数?答案是静态路由,这一点 NextJS 处理的还是很好的
获取路由参数主要是动态路由,和 React 设计初衷一样,NextJS 也提供了获取路由的钩子函数:
引入 useRouter
import { useRouter } from 'next/router'
获取路由参数
const router = useRouter()
console.log(router.query)
完整实例
// pages/clients/[id]/[clientprojectid].jsx
import { useRouter } from "next/router"
const SelectedClientProjectPage = () => {
const router = useRouter()
console.log(router.query)
return (
<div>
<h1>The Project Page fro a Specific Project for a Selected Client</h1>
</div>
)
}
export default SelectedClientProjectPage
如果访问 http://127.0.0.1:3000/clients/max/projectA,那么 router.query
便是 { id: 'max', clientprojectid: 'projectA' }
多个路由参数
// pages/blog/[...slug].jsx
import { useRouter } from "next/router"
const BlogPostsPage = () => {
const router = useRouter()
console.log(router.query)
return (
<div>
<h1>The Blog Posts</h1>
</div>
)
}
export default BlogPostsPage
此时访问 http://127.0.0.1:3000/blog/2022/03/03,打印的便是 { slug: ['2022', '03', '03'] }
可以通过 Link 标签
以及 router 对象
中的 push
和 replace
方法进行路由地址的跳转,但是与 React 中的有所不同。
假设我们跳转的路由文件结构为 /clients/[id]/[clientprojectid]
第一种: Link标签形式
与 React 相同方式,只是跳转地址的属性为 href
<link href="/clients/max/projectA">跳转</link>
传入对象方式:
<Link href={{
pathname: 'clients/[id]/[clientprojectid]',
query: { id: 'max', clientprojectid: 'projectA' }
}}>跳转</Link>
第二种: useRouter钩子函数形式
先导入 useRouter
方法以及创建实例对象
import { userRouter } from 'next/router'
与 React 相同方式,直接写路由地址:
router.push('/clients/max/projectA')
转入对象方式:
router.push({
pathname: 'clients/[id]/[clientprojectid]',
query: { id: 'max', clientprojectid: 'projectA' }
})
浏览器访问网站典型的流程是,浏览器首先获取一个HTML 页面,此页面并没有数据,然后 useEffect
函数调用 fetch
从服务器获取数据,然后使用这些数据设置组件 state,state 因为发生改变,react 使组件函数重新执行一遍,页面因此重新渲染,因此页面实际上有两个渲染周期。
而 SEO
(search engine optimization 搜索引擎优化)依据的是第一个渲染周期的页面,此页面数据为空,为了避免这个问题,可以使得预渲染的页面,在第一个 render cycle 就已包含从数据库中获取的数据。
要使得预渲染的页面包含数据,NextJS 提供了两种形式的预渲染:
CSR | SSR | SSG | |
---|---|---|---|
运行端 | 浏览器 | 服务器 | 服务器 |
静态文件 | 单页面 | 由服务器即时生成 | 多个页面 |
SEO | 不适合 | 适合 | 适合 |
静态文件CDN | 适合 | 不适合 | 适合 |
适用场景 | 中后台产品 | 信息展示型网站 | 内容较为固定的资讯类网站 |
NextJS 提供了 getStaticProps
函数来实现在 Build 期间就能显示数据的方法。
使用步骤:
书写逻辑
可以在 getStaticProps
函数中写请求 API 的逻辑
export async function getStaticProps() {
/* CODE HERE */
const data = fetch('xxxx')
if(!data) {
return {
redirect: {
destination: '/no-data'
}
}
}
if(data.length === 0) {
return {
notFound: true
}
}
return {
props: {
products: data
},
revaliate: 10
}
}
getStaticProps
返回值参数
参数名 | 类型 | 作用 |
---|---|---|
redirect | 对象 | 重定向,可以指定其中的 destination 属性,指定路由地址 |
notFound | 布尔值 | 设置为 true 时,会显示 404 页面,默认为 false |
props | 对象 | 组件的 props 参数,用户可以指定从 API 中获取的数据 |
revaliate | 数字 | getStaticProps 内部逻辑和静态页面重新执行、生成的时间,例如设置为 10,在 10s 内无论怎么刷新页面去请求某一个数据,也不会出发请求逻辑,10s 过后,如果有新数据,那么会重新生成静态页面。当用户新添加数据时,如果不指定 revaliate ,虽然页面会显示数据,但这个数据并不会预先生成,检查源代码就能看到,如果指定 revaliate 就可以显示了 |
组件使用 props
const HomePage = (props) => {
console.log(props.data)
return (
<div>{props.data}</div>
)
}
当然这样设置对于某些动态路由组件会报错,甚至会报错,此时需要设置另一个函数:getStaticPaths
,这个以后再讲
效果查看
如果执行的命令是 npm run dev
,那么 revaliate
属性不会起作用,我们需要先执行 npm run build
再执行 npm start
,查看效果
动态路由获取参数
在 getStaticProps
中的 context 上下文获取动态路由参数
// [pid].jsx
export async function getStaticProps(context) {
const { params } = context
const pid = params.pid
return {
props: {
id: pid
}
}
}
当然设置上述是不够的,还要写另一个函数 getStaticPaths
书写 getStaticPaths
函数`
getStaticPaths 有很多种写法,要配合 getStaticProps
和 组件函数
fallback 配置项的作用:当用户传入的路径参数不在规定的范围内之后,NextJS 要给用户展示什么:
- 如果 fallback 为 false,则会展示 404 页面
- 如果 fallback 为 true,那么服务器将会根据用户传递的路由参数(即使与 path 配置项的路由不匹配),获取对应的数据,然后将这些数据传递给组件进行静态页面的生成(可以查看有个新建的页面),最后将这个生成好的静态页面传递给客户端用户
- blocking 会一次性加载所有页面(路由)
2.1 第一种写法
在 getStaticPaths
函数的返回值中,指定所有的路由参数,一般这样选择。
建议加上 fallback
// [pid].jsx
export async function getStaticPaths() {
return {
paths: [
{ params: { pid: 'p1' } },
{ params: { pid: 'p2' } },
{ params: { pid: 'p3' } },
]
}
}
2.2 第二种写法
这样就访问 p1、p2、p3 也可以访问了
export async function getStaticPaths() {
return {
fallback: true
}
}
不过如果这样写,那么当用户访问不存在的地址,会直接报错,如果我们不希望这样,可以在 getStaticProps
中设置 notFound: true
export async function getStaticProps(context) {
const { params } = context
const productId = params.pid
// 假设有个获取数据的方法
const data = await getData()
// 根据路由参数过滤数据
const product = data.products.find(product => product.id === productId)
// 关键代码,如果路由参数不对,那么直接返回 404
if(!product) {
return {
notFound: true
}
}
return {
props: {
loadedProduct: product
}
}
}
同时指定组件函数,因为不太确定各个函数执行时机
const xxx = (props) => {
const { loadedProduct } = props
if(!loadedProduct) {
return <p>loading....</p>
}
return <p>
{loadedProduct}
</p>
}
注意:第二种方法的数据处理方式也适合第一种方法
tips:fallback为false的时候,不需要notFound属性,因为只有从getStaticPaths返回的路径才会被预渲染,当用户访问的路由参数没有在当前函数中返回时,是否显示404,false是显示,true是不显示 。
fallback: false
如果fallback
是false
,那么任何路径都不会生成并且变成一个404页面。你可以在你仅有少量的路径去预渲染的时候这样做,这样的话在构建时都是静态页面。当并不经常添加新的页面的时候这样做很有用。但是当你需要然后新的时候的时候,你就需要重新构建。如果
fallback
是true
,那么getStaticProps
的行为会有如下变化:
由
getStaticPaths
获取的路径会在构建时调用getStaticProps
方法渲染成 HTML 文件;在构建时未生成的路径不会以 404 页面返回。相反,当请求不存在的页面路径时,Next.js 会渲染一个当前页面的
回退
(fallback
) 版本。注意,回退
(fallback
) 版本不会提供给像谷歌这样的爬虫程序,而是以阻塞模式呈现路径。在后台,Next.js 会根据请求路经执行
getStaticProps
方法静态生成页面的HTML和JSON。当这些都完成之后,浏览器接收 JSON 数据根据对应的生成路径。这些数据会自动在页面渲染的时候被使用。从用户的角度来看,页面将从备用页面切换到完整页面。
在同时,Next.js 将路经添加到预渲染页面列表中。对同一路径的后续请求将渲染已经生成的页面,就像构建时预渲染的其他页面一样。
fallback: true
何时最有用? 当你的应用有大量的依赖于数据(depend on data
)的静态页面fallback: true
,你想要预渲染所有的商品页面,但这样你的构建将花费很长时间。取而代之的是,你可以静态生成一个很小的页面集合,其余部分通过使用fallback: true
生成。当有用户请求一个还没有生成的页面时,用户会看到页面中有一个加载指示器。很快的,getStaticProps
执行完成,页面会根据请求到的数据渲染。之后同样请求此页面的任何用户将会得到静态预渲染的页面。 这确保了用户在保持快速构建和静态生成的好处的同时始终拥有最快的体验。fallback: true
并不会更新已经生成的页面,具体可查看增量静态再生(Incremental Static Regeneration)。
fallback: 'blocking'
如果fallback
是blocking
,getStaticPaths
未返回的新路径将等待HTML完全生成,与 SSR 是完全相同的,然后被缓存下来以供将来的请求使用,因此每个路径只发生一次。
代码示例
import fs from 'fs/promises'
import Link from 'next/link'
import path from 'path'
const index = (props) => {
const { products } = props
return (
<ul>
{products.map(product => <li><Link href={`/${product.id}`} key={product.id}>{product.title}</Link></li>)}
</ul>
)
}
export async function getStaticProps() {
console.log('(Re-)Generating...')
const filePath = path.join(process.cwd(), 'data', 'dummy-backend.json')
const jsonData = await fs.readFile(filePath)
const data = JSON.parse(jsonData)
if(!data) {
return {
redirect: {
destination: '/no-data'
}
}
}
if(data.products.length === 0) {
return {
notFound: true
}
}
return {
props: {
products: data.products
},
revalidate: 10,
}
}
export default index
getServerSideProps
只会在服务器并且只在服务器上运行
和 NodeJS 一样,可以从上下文中获取 req
和 res
对象,也可以像 NextJS
一样获取动态路由参数
// [uid.jsx]
const UserIdPage = (props) => {
return (
<h1>{props.id}</h1>
)
}
export const getServerSideProps = async (context) => {
const { params, req, res } = context
const userId = params.uid
return {
props: {
id: 'userid-' + userId
}
}
}
export default UserIdPage
当然,getServerSideProps
也可以和 getStaticProps
一样返回重定向或者404数据
return {
redirect: {
destination: '/xx',
permanent: false // 只有这一次重定向,并不是永久重定向
}
}
return {
notFound: true
}
有时我们的网页有很多路由,如果要更换不同路由的网页标题,该如何做?
NextJS 为我们提供了 Head 标签
,在 Head 标签
内可以书写 meta、title 等标签
。
步骤:
导入 Head 标签
import Head from 'next/head'
使用 Head 标签
在组件函数中添加
const xxx = () => {
return (
<Head>
<title>本页面标题为xxx</title>
<meta name="description" content='NextJS Events' />
</Head>
<div>
<h1>xxx</h1>
</div>
)
}
如果想要加上整个网页通用的 meta 标签
,可以在 _app.jsx
入口文件中添加,以下是完整代码:
注意:_app.jsx
中 Head 标签
中的 title 标签
优先级弱于路由级别的 title 标签
,会被覆盖
import '../styles/globals.css'
import '../components/layout/layout'
import Layout from '../components/layout/layout'
import Head from 'next/head'
const MyApp = ({ Component, pageProps }) => {
return (
<Layout>
<Head>
<title>Next Events</title>
<meta name="description" content='NextJS Events' />
<meta name="viewport" content='initial-scale=1.0, width=device-width' />
</Head>
<Component {...pageProps} />
</Layout>
)
}
export default MyApp
如果希望设计页面结构,可以在 _document.jsx
入口文件修改,以下是完整代码
import Document, { Html, Head, Main, NextScript } from 'next/document'
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initalProps = await Document.getInitialProps(ctx)
return initalProps
}
render() {
return (
<Html lang="en">
<Head>
{/* Head 标签 */}
</Head>
<body>
<div id="overlays" />
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
分析以上代码步骤
导入所需组件
import Document, { Html, Head, Main, NextScript } from 'next/document'
书写类标签架构
class MyDocument extends Document {
static async getInitialProps(ctx) {
const initalProps = await Document.getInitialProps(ctx)
return initalProps
}
render() {
return (
<Html>
<Head>
{/* Head 标签 */}
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
添加属性
return (
<Html lang="en">
<Head></Head>
<body>
<div id="backdrop"></div>
<Main />
<NextScript />
</body>
</Html>
)
默认的 img 标签并不会对图片进行压缩、懒加载等优化,NextJS 提供的 Image 标签可以很好地解决这个问题。
Image 标签的使用:
<Image src="/picURL" alt="鼠标悬浮显示文本" width={500} height={500} />
其中 src、alt 属性
与普通的 img 标签
一致,width
和 height
需要根据实际情况,例如图片的大小上限、父元素的大小等设置。。
使用前后的变化:
前
后
可以看到首先是图片的 Type 属
性由 jpeg
改为 webp
,Size
和 Time
都有所减少,尤其是 Time 响应时间
。
page
页面中的 api
是一个特殊的文件夹,它代表着 api router(api 路由)
简单示例
// pages/api/feedback.js
const feedbackHandler = (req, res) {
res.status(200).json({
message: 'it is working!'
})
}
export default feedbackHandler
如果我们访问 http://127.0.0.1:3000/api/feedbackHandler 就会有 { “message”: “it is working” }
JSON 格式数据返回。
发送请求:
GET 请求
// pages/feedback.jsx
const feedbackPage = () => {
fetch('/api/feedback').then(res => res.json()).then(data => {
console.log(data)
})
}
export default feedbackPage
POST 请求
const feedbackPage = () => {
fetch('/api/feedback', {
method: 'POST',
headers: {
"Content-Type": "application/json"
},
body: {
text: '123456'
}
}).then(res => res.json()).then(data => {
console.log(data)
})
}
export default feedbackPage
req 参数
除了 GET 请求,还会有 POST、PUT 请求,以 POST 为例,看 NextJS 如何处理这些请求
// pages/api/feedback
const feedbackHandler = (req, res) {
if(req.method === 'POST') {
const feedbackText = req.body.text
}
res.status(200).json({
message: 'it is working!'
})
}
export default feedbackHandler
req 参数
参数名 | 参数作用 |
---|---|
body | 客户端 POST、PUT 请求的 body 数据内容 |
method | 请求方法(具体有 POST、GET、PUT、DELETE ...) |
query | 路由参数,具体见下一个页面 |
和普通的 page 页面
一样,api 动态路由也可以使用 []
形式来指定路由参数,同时用 req.query.xxx
来获取动态路由参数
// pages/api/[feedbackId].js
const handler = (req, res) => {
const feedbackId = req.query.feedbackId
res.status(200).json({
feedback: selectedFeedback
})
}
export default handler
命令:
next build
npm run build
使用此命令构建的 NextJs 应用程序是优化的生产版本,它会生成一个服务端的应用程序,因此需要一个 NodeJS 服务器来运行它,所以不能放在某个静态主机上。NextJS 具有内置的服务端功能,可以在服务器上及时渲染页面、重新验证页面、API路由,这些服务端的特性,需要一个 NodeJS 服务器运行这些代码。
命令:
next export
npm run export
会产生 100% 的静态应用程序(只有 HTML、CSS、JavaScript
),所以不需要 NodeJS 服务。
如果你的应用依赖于 API Routes
或者 server-side pages
或者 revalidations
或者将 fallback
设置为 true
或者 blocking
的页面。所以 next export 只适用于不需要任何服务端代码的页面。
这还意味着当你需要重新更改页面时,需要重新书写代码。对于一些非常简单的博客,比如你每周添加一篇新的文章,可能非常好。
一些必要的前置操作
添加页面的 meta 数据、标题、描述;去掉一些不必要的依赖和控制台输出语句;优化代码。不错的是 NextJS
内置了延迟加载。
检查配置
正确配置环境变量、api 密钥,比如说你的测试数据库地址对于其他用户来说肯定没用,需要换成服务器的数据库地址
测试构建
在本地的机器上测试应用程序是否就绪,如果对构建的大小不满意,可以回到第一个步骤优化项目体积
最终部署
在其中书写以下代码:
module.exports = {
env: {
customKey: 'my-value',
mongodb_username: 'xj',
mongodb_password: 'dfa2sf2ad',
mongodb_clustername: 'cluster0',
mongodb_database: 'my-site'
}
}
这样就可以在组件或者其他 js 文件中使用这个 key
值了
// src/page.js
function Page() {
return <h1>The value of customKey is: {process.env.customKey}</h1>
}
export defualt Page
// src/utils/db.js
const connectionString = `mongodb+src://${process.env.mongodb_username}:${process.env.mongodb_password}@${process.env.mongodb_clustername}.ntrwp.mongodb.next:${mongodb_database}`
当然 next.config.js
的内容不止这些,我们还可以默认导出一个函数
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants')
module.exports = (phase) => {
// 如果我们处于开发阶段,则返回这些数据
if(phase === PHASE_DEVELOPMENT_SERVER) {
return {
env: {
customKey: 'my-value',
mongodb_username: 'xj',
mongodb_password: 'dfa2sf2ad',
mongodb_clustername: 'cluster0',
mongodb_database: 'my-site-dev'
}
}
}
return {
env: {
customKey: 'my-value',
mongodb_username: 'xj',
mongodb_password: 'dfa2sf2ad',
mongodb_clustername: 'cluster0',
mongodb_database: 'my-site'
}
}
}
这样就不会影响真正的数据库数据