umijshttps://umijs.org/docs/introduce/introduce
react:环境搭建、jsx、组件、useState、useEffect、useRef、自定义Hook、智能组件和UI组件
redux:RTK、同步状态、异步状态、状态和视图分离
Router:基础使用、嵌套路由、路由模式、声明/编程式导航
基础扩展:useMemo/useCallback、useReducer、useImperativeHandle、Class API vc Hooks、新一代状态管理zustand
react+ts:react+ts基础、axios+ts、zustand+ts
渲染环境 【重点】
有两种环境可以呈现 Web 应用程序:客户端和服务器。
客户端渲染 CSR:在客户端渲染中,服务器会发送一个最小化的HTML页面和一些JavaScript代码给客户端。然后,客户端的浏览器会执行这些JavaScript代码,动态地生成并插入页面的内容。
优势:快速页面切换,减轻服务器压力
劣势:SEO不友好,首屏加载慢,对客户端性能要求高
服务端渲染 SSR:服务器负责将页面的HTML内容渲染成最终的形式,再发送给客户端。用户在访问页面时,收到的是已经渲染好的HTML内容,而不是一段空白的HTML和一堆JavaScript代码。
优势
- SEO: 搜索引擎对于能够提供已渲染HTML内容的页面更加友好,可以更容易地索引和理解页面的内容
- 首屏加载性能:用户在访问页面时能够更快地看到内容,因为不需要等待大量的JavaScript代码下载和执行
- 无需客户端渲染能力
劣势:页面切换慢,服务器压力大
Next.js
next.jshttps://nextjs.org/docs/getting-started/installation
建议Node.js环境在18.17以上
手动创建新的 Next.js 【不推荐】
npm install next@latest react@latest react-dom@latest
# 具体配置流程请查看官网
注意:用tsx文件会报错,运行npm run dev 启动开发服务器,会自动引入ts开发依赖,创建配置文件
1、创建一个 app/ 文件夹,然后添加一个 layout.tsx 和 page.tsx 文件。当用户访问应用程序的根目录时,将呈现这些内容 ( / )。推荐
// 在内部 app/layout.tsx 创建一个根布局,其中包含所需的 <html> 和 <body> 标签
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
// 创建一个 app/page.tsx 包含一些初始内容的主页
export default function Page() {
return <h1>Hello, Next.js!</h1>
}
2、如果您更喜欢使用页面路由器而不是应用程序路由器,则可以在项目的根目录下创建一个 pages/
目录。【记得删除 app/page.tsx,有冲突】 可选,不推荐
- 在 pages 文件夹中添加一个 index.tsx 文件。这将是您的主页 ( / )
-
在pages/ 添加一个 _app.tsx 文件来定义全局布局。详细了解自定义应用文件。
-
在pages/ 添加一个 _document.tsx 文件来控制来自服务器的初始响应。详细了解自定义文档文件
// _app.tsx
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
// _document.tsx
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
// index.tsx
export default function Page() {
return <h1>Hello, Next.js!</h1>
}
3、创建一个 public
文件夹来存储静态资产,例如图像、字体等。然后,代码可以从基本 URL ( ) 开始引用 public
目录中的文件 /
。 【可选】
4、运行npm run dev启动项目。【初始化已完成】
根布局必须定义 <html>
和 <body>
标记,因为 Next.js 不会自动创建它们。
默认情况下,根布局是服务器组件,不能设置为客户端组件。
create-next-app【推荐】
建议使用 create-next-app
启动新的 Next.js 应用程序,它会自动为您设置所有内容。
npx create-next-app@latest
#npx Node.js工具命令,查找并执行后续的包命令
#create-next-app 设置 Next.js 应用程序
React
zh-hans.reacthttps://zh-hans.react.dev/
React 和 Next.js
renderinghttps://nextjs.org/docs/app/building-your-application/rendering
- React 和 Next.js 允许您创建混合 Web 应用程序,其中部分代码可以在服务器或客户端上呈现。
- 在 React 中,您可以选择将客户端-服务器网络边界放置在最有意义的位置。
- 你可以使用 React
"use client"
约定来定义边界。还有一个"use server"
约定,它告诉 React 在服务器上做一些计算工作。
基础语法
创建react项目【建议直接使用nextjs创建】
npx create-react-app 你的项目名
# create-react-app (核心包),用于创建react项目
如果你的标签和 return
关键字不在同一行,则必须把它包裹在一对括号中
jsx:js和xml(html)的缩写,表示在js代码中编写html模块结构。【html声明式模板写法,js的可编程能力】
1、大括号语法{}
jsx可以通过大括号语法{}识别js中的表达式,比如常见的变量、函数调用、方法调用等等
- 使用引号传递字符串
- 使用js变量
- 函数调用和方法调用
- 使用js对象
- if、switch语句、变量声明属于语句,不是表达式。不能出现{}
2、基础样式
行内样式、class类名控制
3、map方法
jsx可以使用原生js中的map方法遍历渲染列表
4、条件渲染
可以通过逻辑与运算符&&、三元表达式(?:)实现基础的条件渲染
5、事件绑定
事件绑定(属于客户端渲染,添加use client标识),语法:on+事件名称={方法名},遵循驼峰命名法
注意:传递自定义参数,不能直接写函数调用,改成箭头函数写法。
6、自定义组件
一个组件就是用户界面的一部分,拥有自己的逻辑和外观,组件之间可以互相嵌套,也可以多次复用【自定义组件首字母要大写,否则不起作用】
/* 页面是路由独有的 UI。您可以通过从 page.js 文件导出组件来定义页面。
使用嵌套文件夹定义路由,并使用 page.js 文件使路由可公开访问。*/
"use client"; // 绑定事件报错,添加使用客户端use client【允许在客户端(浏览器中)渲染 React 组件】
// 引入css
import './base.css'
// 引入useState
// 自定义组件首字母要大写,否则不起作用
function FirstComponent() {
return (
<article>
<h1>我的第一个组件</h1>
<ol>
<li>组件:UI 构成要素</li>
<li>定义组件</li>
<li>使用组件</li>
</ol>
</article>
);
}
// 变量
const count = 1;
const flag = true;
const list = [
{ id: 1, name: "小吴" },
{ id: 2, name: "小兰" },
{ id: 3, name: "小红" },
{ id: 4, name: "小明" },
];
// 函数调用测试
function getName() {
return "函数调用";
}
// 函数调用测试
function ifCont() {
if (count === 1) {
return <div>数字1</div>;
} else if (count === 2) {
return <div>数字2</div>;
}
}
export default function Page() {
// 基础事件绑定
function handleTestClick() {
alert("点击事件");
}
// 自定义参数事件绑定
function handleParamsClick(evt, params) {
console.log(evt, params);
}
return (
// 只能有一个根元素
<section>
<h1>注意:自定义组件首字母要大写,否则不起作用</h1>
<FirstComponent />
{/* 调用js对象 */}
<div style={{ color: "blue" }} className="h-fontSize">
{"使用引号传递字符串"} <br />
{/* 识别js变量 */}
{count}
{/* 函数调用 */}
{ifCont()}
{getName()} <br />
{/* 方法调用 */}
{new Date().getDate()} <br />
</div>
{/* 列表渲染 */}
{/* map循环那个结构 return结构 */}
{/* 注意必须添加key,作用:react框架内部使用,提升更新性能 */}
<ul>
{list.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
{/* 条件渲染 */}
{/* 逻辑与 && */}
{flag && <span>条件渲染</span>}
<br />
{/* 三元运算 */}
{flag ? <span>三元运算true</span> : <span>三元运算false</span>}
{/* 基础事件绑定 */}
<button onClick={handleTestClick}>基础事件绑定</button>
{/* 自定义参数事件绑定 */}
<button onClick={(e) => handleParamsClick(e, "hui")}>自定义参数</button>
</section>
);
}
7、useState(react hook)
useState是一个react hook(函数),它允许我们向组件添加一个状态变量,从而控制影响组件的渲染结果【本质:和普通的js变量不同的是,状态变量一旦发生变化,组件视图UI也会跟着变化(数据驱动视图)】
- useState是一个函数,返回值是一个数组
- 数组中的第一个参数是状态变量,第二个参数是set函数用来修改状态变量的
- useState的参数将作为状态变量的初始值
- 状态变量不能直接修改,直接修改无法引发视图更新。
- 修改对象类型的状态变量,给set函数传一个全新的对象来进行修改【直接修改源对象,不引发视图变化】
- 惯例是将这对返回值命名为
const [thing, setThing]
。你也可以将其命名为任何你喜欢的名称,但遵照约定俗成能使跨项目合作更易理解。 - 对象的展开语法是浅层的:它的复制深度只有一层。
- 可以通过使用像
filter()
和map()
这样不会直接修改原始值的方法,从原始数组生成一个新的数组
8、表单绑定
【useState/useRef】
"use client"; // 使用客户端渲染
// 引入useState
import { useRef, useState } from "react";
export default function Page() {
// num: 状态变量 setNum修改状态变量
// useState的参数将作为num的初始值
const [num, setNum] = useState(0);
function handleNumClick() {
console.log(num);
// 作用:1、用传入的新值修改num 2、重新使用新的num渲染ui
setNum(num + 1);
}
// 修改对象状态
const [form, setForm] = useState({ name: "jocker" });
function handleObjClick() {
// 错误写法
// form.name = 'clown'
// 传入一个全新的对象
setForm({
...form,
name: "clown",
});
}
// 表单绑定
// 使用React组件的状态useState控制表单的状态
const [inputValue,setInputValue] = useState('')
// 通过useRef钩子函数获取dom
// 1、创建ref对象,并与jsx绑定
const inputRef = useRef(null)
// 2、通过inputRef.current拿到Dom对象
function getInputDom(){
// console.log(inputRef.current);
console.dir(inputRef.current);
}
return (
<div>
<button onClick={handleNumClick}>增加状态变量:{num}</button>
<button onClick={handleObjClick}>修改对象状态:{form.name}</button>
{/* 通过value属性绑定状态,通过change事件绑定状态同步的函数 */}
<input ref={inputRef} value={inputValue} onChange={(e) => setInputValue(e.target.value)}></input>
<button onClick={getInputDom}>获取dom元素:{inputValue}</button>
</div>
);
}
9、父子组件
父传子-props
props可传递任意的数据:数字、字符串、布尔值、数组、对象、函数、JSX
props是只读对象:子组件中只能读取props中的数据,不能直接进行修改,父组件的数据只能由父组件修改
特殊的prop children:在子组件标签中嵌套内容,父组件会自动在名为children的prop属性中接收改内容
子传父
在子组件中调用父组件中的函数并传递参数
使用状态提升实现兄弟组件通信
借助“状态提升”机制,通过父组件进行兄弟组件之间的数据传递
- A组件先通过子传父的方式把数据传给父组件
- 父组件拿到数据后通过父传子的方式传给B组件
使用Context机制跨层级组件通信
- 使用createContext方法创建一个上下文对象ctx
- 在顶层组件中通过ctx.Provider组件提供数据
- 在底层组件中通过useContext钩子函数获取数据
"use client";
import { useState,createContext, useContext } from "react";
// 父传子
// 1、父组件传递数据,子组件标签身上绑定属性
// 2、子组件接收数据,props
// 子组件
function Son(props) {
// props: 对象里面包含了父组件传递过来的所有的数据
// console.log(props,'参数');
return <div>Son Component, {props.name}</div>;
}
// 特殊的prop children
function Child(props) {
// console.log(props,'参数');
return <div>Child Component, {props.children}</div>;
}
// 子传父
function SonParams({ onGetSonMsg }) {
const sonMsg = "son msg";
return (
<div>
Son Component
<button onClick={() => onGetSonMsg(sonMsg)}>子传父</button>
</div>
);
}
// 使用状态提升实现兄弟组件通信
function AState({ onGetAName }) {
const name = "A component"
return (
<div>
A组件---------
<button onClick={() => onGetAName(name)}>A组件:子传父</button>
</div>
);
}
function BState({ name }) {
return <div>B Component---------父传子{name}</div>;
}
// 使用Context机制跨层级组件通信
const getContext = createContext("Context默认值")
function A(){
return (
<div>
Context A
<B></B>
</div>
)
}
function B(){
const msg = useContext(getContext)
return (
<div>
Context B,{msg}
</div>
)
}
export default function Page() {
const name = "Father Component";
function getMsg(msg: string) {
console.log(msg);
}
// 兄弟组件
const [ABName, setABName] = useState("");
function getAName(name) {
setABName(name);
}
// Context机制
const msgContext = "App Context msg"
return (
<div>
<Son
name={name}
age={18}
bol={true}
list={["react", "test"]}
obj={{ name: "jocker" }}
cb={() => console.log(123)}
child={<span>this is span</span>}
></Son>
<Child>
<span>在子组件标签中嵌套内容</span>
</Child>
<SonParams onGetSonMsg={getMsg}></SonParams>
{/* 状态提升 */}
<AState onGetAName={getAName}></AState>
<BState name={ABName}></BState>
{/* 使用Context机制跨层级组件通信 */}
{/* 默认值 */}
<A />
{/* 自定义 */}
<getContext.Provider value = {msgContext}>
{/* 顶层组件 */}
<A />
</getContext.Provider>
</div>
);
}
10、useEffect(hook函数)
用于在react组件中创建不是由事件引起而是由渲染本身引起的操作。【axios,更改DOM】
由渲染本身引起的操作:不会发生任何的用户事件,组件渲染完毕之后就需要和服务器要数据
基本使用
useEffect(() => {},[])
参数1 () => {}: 副作用函数,在函数内部可以放置要执行的操作
参数2 []:在数组里放置依赖项,不同依赖项会影响第一个参数函数的执行,当是一个空数组的时候,副作用函数只会在组件渲染完毕之后执行一次
useEffect副作用函数的执行时机存在多种情况,根据传入依赖项的不同,会有不同的执行表现
依赖项 | 副作用函数执行时机 |
---|---|
没有依赖项 | 组件初始渲染+组件更新时执行 |
空数组依赖 | 只在初始渲染时执行一次 |
添加特定依赖项 | 组件初始渲染+特定依赖项变化时执行 |
清除副作用
在useEffect中编写的由渲染本身引起的对接组件外部的操作【副作用操作】
比如在useEffect中开启一个定时器,想在组件卸载时把这个定时器清理掉,这个过程就是清除副作用
"use client"
import { useEffect, useState } from "react"
const url = "http://localhost:8080/users" // 查询接口
export default function Effect(){
/*const [list, setList] = useState([])
useEffect(() => {
// 额外的操作 获取列表数据
async function getList() {
const res = await fetch(url)
const jsonRes = await res.json()
console.log(res);
console.log(jsonRes);
setList(jsonRes.data.channels)
}
getList()
},[])*/
const [count,setCount] = useState(0)
// 没有依赖项 初始+组件更新
// useEffect(() => {
// console.log("副作用函数执行了");
// })
// 空数组依赖 初始执行一次
// useEffect(() => {
// console.log("副作用函数执行了");
// },[])
// 添加特定依赖项 初始+依赖项更新
// useEffect(() => {
// console.log("副作用函数执行了");
// },[count])
// 清除副作用
const [show,setShow] = useState(true)
// 自定义组件
function Test(){
useEffect(() => {
console.log("副作用函数执行了");
const timer = setInterval(() => {
console.log("开启定时器");
})
// 清除副作用(组件卸载执行)
return () => {
clearInterval(timer)
}
},[])
return <div>测试清除副作用操作</div>
}
return (
<div>
测试数据
{show && <button onClick={() => setCount(count + 1)}>修改count依赖项:{count}</button>}
{show && <Test/>}
<button onClick={() => setShow(!show)}>组件挂起与卸载(组件更新)</button>
</div>
)
}
11、自定义hook函数
在 React 中,useState
以及任何其他以“use
”开头的函数都被称为 Hook。
以use开头的函数,通过自定义hook函数可以用来实现逻辑的封装与复用
使用规则
- 只能在组件中或者其他自定义hook函数中调用
- 只能在组件的顶层调用,不能嵌套在if、for、其他函数中
步骤
- 编写一个use开头的函数
- 在函数内部编写封装的逻辑
- return出去组件中用到的状态和方法
- 组件中调用函数解构赋值使用
"use client"
import { useState } from "react"
// 自定义hook函数
function useToggle(){
const [value,setValue] = useState(true)
const toggle = () => {
setValue(!value)
}
return {
value,
toggle
}
}
export default function Hook(){
const {value,toggle} = useToggle()
return (
<div>
{value && <div>控制显示隐藏</div>}
{/* <button onClick={() => setValue(!value)}>切换按钮</button> */}
<button onClick={toggle}>切换按钮</button>
</div>
)
}
12、图片使用 Image
13、缓存计算结果 useMemo
定义tsx文件
"use client"
import { Dispatch, ReactNode, SetStateAction,createContext, useContext, useMemo, useState } from "react"
type State = {
displayNavigation: boolean
}
type AppContextProps = {
state: State,
setState: Dispatch<SetStateAction<State>>
}
const AppContext = createContext<AppContextProps>(null!)
export function useAppContext(){
return useContext(AppContext)
}
export default function AppContextProvider({
children
}:{
children: ReactNode
}){
const [test,setTest] = useState(false)
console.log("AppContextProvider 重新渲染");
const [state,setState] = useState({displayNavigation:true})
// 使用缓存计算
const contextValue = useMemo(() => {
return {state,setState}
},[state,setState])
return (
<AppContext.Provider value={contextValue}>
<button className="bg-blue-600 p-3 text-white" onClick={() => setTest((v) => !v)}>useMemo使用按钮测试</button>
{children}
</AppContext.Provider>
)
// return <AppContext.Provider value={{state,setState}}>{children}</AppContext.Provider>
}
在layout.tsx注入
import AppContextProvider from "../components/AppContext";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="dark">
layout:
<AppContextProvider>AppContextProvider: {children}</AppContextProvider>
</body>
</html>
);
}
定义测试文件
"use client"
import { useAppContext } from "../AppContext";
export default function Navigation() {
const {
state: { displayNavigation }
} = useAppContext();
console.log("Navigation 重新渲染");
return (
<nav className={`${displayNavigation ? "" : "hidden"} h-full w-[260px] bg-gray-900 text-gray-300 p-2`}>
测试useMemo
</nav>
);
}
14、Icons
npm i react-icons
- 创建自定义组件文件 components/about/myButton.tsx 【注意:自定义组件首字母大写】
- 导入自定义组件MyButton,测试文件testButton.tsx
import { ComponentPropsWithoutRef } from "react";
// 引入图标类型
import { IconType } from "react-icons"
// 定义ts类型
type MyButtonType = {
icon?: IconType,
variant?: "default" | "outline" | "text"
} & ComponentPropsWithoutRef<"button">
// 自定义button 悬浮 激活样式
// export default function MyButton({
// children,
// className,
// ...props
// }:ComponentPropsWithoutRef<"button">){
// return (
// <button className={`border border-gray-600 rounded px-3 py-1.5 hover:bg-gray-800 active:bg-gray-700 ${className}`} {...props}>
// {children}
// </button>
// )
// }
export default function MyButton({
icon: Icon,
children,
className = "",
variant = "default",
...props
}:MyButtonType){
return (
<button
className={`inline-flex items-center min-w-[38px] min-h-[38px] rounded p-2 bg-red-900
${variant === "default" ? "border border-red-900 bg-red-900" : "border-blue-900 bg-blue-900"}
${className}`}
{...props}
>
{Icon && <Icon className={`text-lg ${children ? "mr-1" : ""}`}></Icon>}
{children}
</button>
)
}
import MyButton from "../about/myButton";
// 导入图标
import { HiPlus } from "react-icons/hi"
import { LuPanelLeft } from "react-icons/lu"
export default function TestButton(){
return <nav className="h-full w-[260px] bg-gray-900 text-gray-300 p-2">
<MyButton icon={HiPlus} variant="outline" className="flex-1">新建对话</MyButton>
<MyButton icon={LuPanelLeft}></MyButton>
</nav>
}
JavaScript库推荐
className
字符拼接,不够直观,推荐使用 classnames库
Lodash
使JavaScript更容易处理数组、数字、对象、字符串等。lodash
dayjs
解析、验证、操作和显示现代浏览器的日期和时间 dayjs
uuid
创建随机的uuid uuid
// 导入classnames
import classNames from 'classnames'
// lodash
import _ from 'lodash'
// h-font 默认类名样式 active 动态样式
// className={typeTab === item.type ? "active" : ""}
className={`h-font ${typeTab === item.type && 'active'}`}
// 使用js库
className={classNames('h-font',{active:typeTab === item.type})}
// 排序
const [stateList, setStateList] = useState(_.orderBy(list,'hot','desc'));
Immerjs
state 有多层的嵌套,建议使用immerjs库,它可以让你使用简便但可以直接修改的语法编写代码,并会帮你处理好复制的过程。
尝试使用 Immer
- 运行
npm install use-immer
添加 Immer 依赖 - 用
import { useImmer } from 'use-immer'
替换掉import { useState } from 'react'
"use client"
/**
* 由 Immer 提供的 draft 是一种特殊类型的对象,被称为 Proxy,它会记录你用它所进行的操作。这就是你能够随心所欲地直接修改对象的原因所在!
* 从原理上说,Immer 会弄清楚 draft 对象的哪些部分被改变了,并会依照你的修改创建出一个全新的对象。
*/
import { useImmer } from 'use-immer';
export default function Form() {
const [person, updatePerson] = useImmer({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
}
});
function handleNameChange(e) {
updatePerson(draft => {
draft.name = e.target.value;
});
}
function handleTitleChange(e) {
updatePerson(draft => {
draft.artwork.title = e.target.value;
});
}
function handleCityChange(e) {
updatePerson(draft => {
draft.artwork.city = e.target.value;
});
}
return (
<>
<label>
Name:
<input
value={person.name}
onChange={handleNameChange}
/>
</label>
<label>
Title:
<input
value={person.artwork.title}
onChange={handleTitleChange}
/>
</label>
<label>
City:
<input
value={person.artwork.city}
onChange={handleCityChange}
/>
</label>
<p>
<i>{person.artwork.title}</i>
{' by '}
{person.name}
<br />
(located in {person.artwork.city})
</p>
</>
);
}
Redux
React最常用的集中状态管理工具 redux官网,类似与Vue中的Pinia(Vuex),可以独立于框架运行
作用:通过集中管理的方式管理应用的状态
独立于框架运行(html) 步骤
- 定义一个reducer函数
- 使用createStore方法传入reducer函数,生成一个Store实例对象
- 使用Store实例的subscribe方法订阅数据的变化,数据一旦变化,可以得到通知
- 使用Store实例的dispatch方法提交action对象触发数据变化
- 使用Store实例的getState方法获取最新的状态数据更新到视图中
<button id="decrement">-</button>
<span id="count">0</span>
<button id="increment">+</button>
<script src="https://cdn.bootcdn.net/ajax/libs/redux/4.2.1/redux.min.js"></script>
<script>
// 1、定义reducer函数
// 作用:根据不同的action对象,返回新的state
// state:数据初始状态
// action:修改数据
function reducer(state = {count: 0},action){
// 数据不可变,基于原始状态生成一个新的状态
if(action.type === "DEC"){
return {count: state.count - 1}
}else if(action.type === "ADD"){
console.log(state.count,'加1',state.count + 1);
return {count: state.count + 1}
}else{
return state
}
}
// 2、使用reducer函数生成store实例
const store = Redux.createStore(reducer)
// 3、通过store实例的subscribe订阅数据的变化
store.subscribe(() => {
console.log("state数据变化",store.getState());
// 更新视图
document.getElementById('count').innerText = store.getState().count
})
// 4、通过store实例的dispatch函数提交action修改状态
// 添加按钮
const addbtn = document.getElementById('increment')
addbtn.addEventListener('click',() => {
store.dispatch({
type: "ADD"
})
})
// 减号按钮
const delbtn = document.getElementById('decrement')
delbtn.addEventListener('click',() => {
store.dispatch({
type: "DEC"
})
})
// 5、通过store实例的getState方法获取最新状态更新视图
</script>
Redux管理数据流程分成3个核心
- state:对象,存放管理的数据状态
- action:对象,修改数据
- reducer :根据action的操作生成一个新的state
react引入redux
安装依赖:Redux Toolkit and React-Redux
谷歌插件:
npm install @reduxjs/toolkit react-redux
- Redux Toolkit:官方推荐的编写Redux逻辑的方法,简化了书写方式,包括强大的数据获取和缓存功能【RTK查询】
- React-Redux:连接React和Redux的中间件
1、CRA(create-react-app):react项目 react-redux
2、CNA(create-next-app
):next项目
创建lib/StoreProvider.tsx文件。
/*
要使用这个新的 makeStore 功能,我们需要创建一个新的“客户端”组件,
该组件将创建存储并使用React Redux Provider 组件共享存储。
任何与Redux存储交互的组件(创建、提供、读取或写入)都需要是客户端组件。
这是因为访问存储需要React上下文,而上下文仅在客户端组件中可用。
每个切片文件都应该为其初始状态值定义一个类型,这样 createSlice 就可以在每个情况下正确地推断 state 的类型。
*/
"use client";
import { useRef } from "react";
import { Provider } from "react-redux";
import { makeStore, AppStore } from "./store";
// 如果需要使用父组件的数据初始化存储,则将该数据定义为客户端 StoreProvider 组件上的道具,并在切片上使用Redux操作来设置存储中的数据
// import { initializeCount } from "./features/countStore";
export default function StoreProvider({
// 初始化存储
// count,
children,
}: {
// count: number;
children: React.ReactNode;
}) {
const storeRef = useRef<AppStore>();
if (!storeRef.current) {
// 首次渲染时创建存储实例
storeRef.current = makeStore();
// 初始化存储 count类型报错:【初始状态定义类型】 https://redux.js.org/tutorials/typescript-quick-start
// storeRef.current.dispatch(initializeCount(count));
}
return <Provider store={storeRef.current}>{children}</Provider>;
}
创建 lib/features/countStore.ts文件
import { createSlice,PayloadAction } from '@reduxjs/toolkit'
// 导入的“RootState”类型
import type { RootState } from '../store'
// 定义初始化状态的类型
export interface CounterState {
value: number,
username: string
}
// 使用该类型定义初始状态
const initialState: CounterState = {
value: 0,
username: 'admin'
}
const counterSlice = createSlice({
name: 'counter',
// `createSlice`将从`initialState`参数推断状态类型
initialState,
// 初始化state
// initialState: {
// value: 0
// },
// 修改状态方法 同步方法
reducers: {
// 传参,参数会传递到action对象payload属性上
// 使用PayloadAction类型声明`action.payload的内容`
initializeCount: (state,action: PayloadAction<number>) => {
console.log("chushizhi",action);
state.value += action.payload
},
incremented: state => {
state.value += 1
},
decremented: state => {
state.value -= 1
},
// 重置仓库【初始化】
resetStore: () => {
return initialState
},
// 修改仓库值【需替换新值】
editUsername: (state,action: PayloadAction<string>) => {
return {
value: state.value,
username: action.payload
}
}
}
})
// 按需导出 解构出来的action创建的函数
export const { incremented, decremented,initializeCount,resetStore,editUsername } = counterSlice.actions
// 默认导出 获取reducer
export default counterSlice.reducer
// 其他代码(如选择器)可以使用导入的“RootState”类型
export const selectCount = (state: RootState) => state.countStore.value
创建lib/store.ts文件
import { configureStore } from '@reduxjs/toolkit'
import countStore from './features/countStore'
// makeStore:为每个请求创建一个存储实例
export const makeStore = () => {
return configureStore({
reducer: {
countStore
}
})
}
// 推断makeStore的类型
export type AppStore = ReturnType<typeof makeStore>
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<AppStore['getState']>
export type AppDispatch = AppStore['dispatch']
创建lib/whooks.ts文件
import { useDispatch, useSelector, useStore } from 'react-redux'
import type { TypedUseSelectorHook } from 'react-redux'
import type { RootState, AppDispatch, AppStore } from './store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppStore: () => AppStore = useStore
修改layout布局文件
/*
创建一个 app/ 文件夹,然后添加一个 layout.tsx 和 page.tsx 文件。
当用户访问应用程序的根目录时,将呈现这些内容 ( / )
在内部 app/layout.tsx 创建一个根布局,其中包含所需的 <html> 和 <body> 标签
*/
import StoreProvider from "../lib/StoreProvider";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
layout:根布局
<StoreProvider>{children}</StoreProvider>
</body>
</html>
);
}
创建测试文件
"use client"; // 使用客户端渲染
import { useAppSelector, useAppDispatch } from "../../../lib/whooks";
import {
incremented,
decremented,
resetStore,
editUsername,
} from "../../../lib/features/countStore";
export default function Page() {
// 读取仓库属性
const username = useAppSelector((state) => state.countStore.username);
const count = useAppSelector((state) => state.countStore.value);
// 修改action
const dispatch = useAppDispatch();
function add() {
console.log("加1");
dispatch(incremented());
}
function del() {
console.log("减1");
dispatch(decremented());
}
function reset() {
dispatch(resetStore());
}
return (
<div>
<div>username: {username}</div>
<button id="decrement" onClick={del}>
-
</button>
<span id="count">{count}</span>
<button id="increment" onClick={add}>
+
</button>
<div>
<button onClick={reset}>重置</button>
</div>
<div>
<button onClick={() => dispatch(editUsername("用户名被修改拉!"))}>修改用户</button>
</div>
</div>
);
}
Tailwind
tailwindcsshttps://www.tailwindcss.cn/
Router
Next.js有两个不同的路由器:应用程序路由器和页面路由器。
- App Router是一款更新的路由器,允许您使用React的最新功能,如服务器组件和流媒体。
- Pages路由器是最初的Next.js路由器,它允许您构建服务器渲染的React应用程序,并继续支持旧的Next.jsp应用程序
顶级文件夹:app目录:App Router 应用程序路由器;pages:Pages Router 页面路由器
app
目录中的页面默认为服务器组件。这与页面是客户端组件的 pages
目录不同。
App Router
一个特殊的 page.js
文件用于使管段可以公开访问。
通过在 app
目录中添加 page.js
文件来创建您的第一个页面。
app路由约定
路由文件名 | 文件后缀 | 说明 |
---|---|---|
layout | .js / .jsx / .tsx | Layout |
page | .js / .jsx / .tsx | Page |
loading | .js / .jsx / .tsx | Loading UI |
not-found | .js / .jsx / .tsx | Not found UI |
error | .js / .jsx / .tsx | Error UI |
global-error | .js / .jsx / .tsx | Global error UI |
router | .js / .ts | API endpoint |
template | .js / .jsx / .tsx | Re-rendered layout |
default | .js / .jsx / .tsx | Parallel route fallback page |
<Link>
是一个内置组件,它扩展了HTML <a>
标记,以提供路由之间的预取和客户端导航。这是Next.js中在路线之间导航的主要方法,也是推荐的方法。
"use client";
import Link from 'next/link'
import { usePathname } from "next/navigation";
import { useRouter } from "next/navigation";
export default function Page() {
// 获取当前页面路由地址
const pathname = usePathname();
// 使用路由跳转 【对于服务器组件,请改用 redirect 函数。】
const router = useRouter();
return (
<div>
<Link href="/dashboard">dashboard</Link>
当前路由地址:{pathname}
{/* 客户端组件路由跳转 */}
<button type="button" onClick={() => router.push("/dashboard")}>
Dashboard
</button>
</div>
)
}
Pages Router
pages路由约定
文件名 | 文件后缀 | 说明 |
---|---|---|
_app() | .js / .jsx / .tsx | Custom App |
_document | .js / .jsx / .tsx | Custom Document |
_error | .js / .jsx / .tsx | Custom Error Page |
404 | .js / .jsx / .tsx | 404 Error Page |
500 | .js / .jsx / .tsx | 500 Error Page |
index | .js / .jsx / .tsx | Home page |
file | .js / .jsx / .tsx | Nested page |