Skip to content

React进阶

一、useReducer

1. 基础使用

作用: 让 React 管理多个相对复杂的状态数据(作用与 useState相同,用于管理状态数据)

  1. 定义一个 reducer函数(用于更新 state 的纯函数,参数为stateaction,返回值是更新后的 state
  2. 在组件中调用 useReducer,并传入 reducer函数和状态的初始值,返回一个数组内部包含 state变量和修改状态的 dispatch函数
  3. 事件发生时,通过 dispatch 函数(用于更新 state 并触发组件的重新渲染)分配一个 action对象(通知 reducer要返回哪个新状态并渲染 UI
jsx
// useReducer
import { useReducer } from "react";
// 1.定义reducer函数 根据不同的action返回不同的状态
function numReducer(number, action) {
  // 返回一个新的state
  switch (action.type) {
    case "INC":
      return number + 1;
    case "DEC":
      return number - 1;
    default:
      return number;
  }
}

function App() {
  // 2.在组件中调用useReducer(reducer函数,初始值) => [state,dispatch] state:状态 dispatch:修改状态的方法
  const [number, numDispatch] = useReducer(numReducer, 0);
  return (
    <>
      <div>{number}</div>
      {/* 3.调用dispatch传入action对象 dispatch({type:"INC"}) => 通知reducer产生一个新的状态,使用新状态更新视图 */}
      <button onClick={() => numDispatch({ type: "INC" })}>+</button>
      <button onClick={() => numDispatch({ type: "DEC" })}>-</button>
    </>
  );
}

export default App;

注意:reducer函数中的state 是只读的,不能对其进行修改,应返回最新的 state来替换之前的 state

2. 更新流程

image.png

3. 调用action传参

需求:更新状态到指定的值

做法:分派action时如果想要传递参数,需要在action对象中添加一个payload参数,放置状态参数

jsx
// useReducer
import { useReducer } from "react";
// 1.定义reducer函数 根据不同的action返回不同的状态
function numReducer(number, action) {
  // 返回一个新的state
  switch (action.type) {
    case "INC":
      return number + 1;
    case "DEC":
      return number - 1;
    case "UPDATE":
      return action.payload; // 根据传递的payload参数返回新的状态
    default:
      return number;
  }
}

function App() {
  // 2.在组件中调用useReducer(reducer函数,初始值) => [state,dispatch] state:状态 dispatch:修改状态的方法
  const [number, numDispatch] = useReducer(numReducer, 0);
  return (
    <>
      <div>{number}</div>
      {/* 3.调用dispatch传入action对象 dispatch({type:"INC"}) => 通知reducer产生一个新的状态,使用新状态更新视图 */}
      <button onClick={() => numDispatch({ type: "INC" })}>+</button>
      <button onClick={() => numDispatch({ type: "DEC" })}>-</button>
      {/* 4.调用action传参 dispatch({type:"UPDATE":payload:参数}) reducer函数可以根据传递的payload参数返回新的状态 */}
      <button onClick={() => numDispatch({ type: "UPDATE", payload: 100 })}>
        update to 100
      </button>
    </>
  );
}

export default App;

二、useMemo(性能优化)

作用:它在每次重新渲染组件当依赖计算的状态数据未发生变化时,能够缓存上次计算的结果,不会在重新执行计算函数(作为性能优化的方案,作用于 Vue中的计算属性相似)

1. 使用场景

当计算消耗特别大且,不必要组件每次渲染时重新计算

举例:当 count1发生变化时重新计算斐波那契数列之和,但当修改 count2的状态时,斐波那契数列求和函数也会被执行(无用的计算,造成性能浪费)

计算函数只会在依赖的状态发生变化时才会重新执行(当非依赖状态变化时计算函数不会执行,而是使用上一次计算的结果进行渲染)

2. 基础使用

基础用法:

jsx
const cachedValue = useMemo(()=>{
	// 返回计算的结果
},[count1]) // 依赖项,当count1发生变化时,函数才会执行重新计算

使用 useMemo做缓存之后可以保证只有 count1 依赖项发生变化时才会重新计算

jsx
import { useMemo, useState } from "react";

function fib(n) {
  console.log("计算函数执行了");
  if (n < 3) return 1;
  return fib(n - 2) + fib(n - 1);
}

function App() {
  const [count1, setCount1] = useState(0);
  // 返回计算结果,只有当count1变化时才计算 fib(count1)
  const result = useMemo(() => fib(1), [count1]);
  /* 状态数据发生变化组件重新渲染时也会执行 fib(count1)
  const result = fib(count1); */
  const [count2, setCount2] = useState(0);
  console.log("组件重新渲染了");
  return (
    <>
      <div>result:{result}</div>
      <button onClick={() => setCount1(count1 + 1)}>+count1:{count1}</button>
      <button onClick={() => setCount2(count2 + 1)}>+count2:{count2}</button>
    </>
  );
}

export default App;

三、React.memo(性能优化)

作用:允许组件在props没有改变的情况下跳过重新渲染

1. 组件默认的渲染机制

React组件默认渲染机制:顶层组件发生重新渲染,这个组件树的子级组件都会被重新渲染(即父组件重新渲染,子组件也会随之重新渲染)

2. React.memo基础使用

基础语法:

​ 使用 memo函数将需要缓存的子组件包裹,返回一个缓存子组件

jsx
const MemoComponent = memo(function SomeComponent(props){...})

说明:经过 memo函数包裹生成的缓存组件只有在 props 发生变化的时候才会重新渲染

jsx
// 1.默认渲染机制: 父组件重新渲染 -> 子组件重新渲染
import { useState, memo } from "react";
// 2.使用memo函数包裹需要缓存子组件,返回一个缓存组件(当缓存组件props发生变化时重新渲染)
const MemoSon = memo(function Son() {
  console.log("我是子组件,我重新渲染了");
  return <>this is son</>;
});

function App() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount(count + 1)}>+</button> // 父组件中的状态数据发生变化引起组件重新渲染,子组件使用memo缓存后就不会被重新渲染了
      <MemoSon />
    </>
  );
}

export default App;

3. props的比较机制

机制:在使用 memo缓存组件之后,React会对每一个prop使用 Object.is比较新值和老值,全部返回 true表示没有变化

  • prop是简单类型

    Object.is(3,3) => true 没有变化

  • prop 是引用类型(对象 / 数组)

    Object.is([],[]) => false 有变化,React只关心**引用(组件重新渲染引用发生变化)**是否变化不关心对象中的具体属性

注意:组件重新渲染相同的引用类型数据会生成不同的引用(地址),即使不改变其值缓存子组件也会重新渲染

Tips:保持引用稳定(组件重新渲染,数据引用不发生变化))使用 useMemo定义引用类型的数据

jsx
import { useState, memo, useMemo } from "react";
// 1. 传递简单类型的prop   prop变化时缓存组件重新渲染
// 2. 传递的数据为引用类型  比较的是新值与就旧值的引用是否相同(当父组件重新渲染时,数据会形成新的引用)
// 3. 保证引用稳定(组件重新渲染,数据引用不发生变化) -> useMemo(组件渲染过程中缓存一个值)
const MemoSon = memo(function Son({ count, list }) {
  console.log("我是子组件,我重新渲染了");
  return <>this is son {list}</>;
});

function App() {
  const [count, setCount] = useState(0);
  const [number, setNumber] = useState(100);
  const list = [1, 2, 3];
  const MemoList = useMemo(() => list, []); // 只在组件挂载完毕后执行一次,状态更新不会执行
  return (
    <>
      <button onClick={() => setCount(count + 1)}>+</button>
      {/* 缓存组件重新渲染 */}
      {/* <MemoSon count={count} /> */}
      {/* 缓存组件不重新渲染 */}
      <MemoSon count={number} />

      {/* 父组件重新渲染 组件内的代码会重新执行list会得到一个新的引用,缓存组件会重新渲染 */}
      <MemoSon list={MemoList} />
    </>
  );
}

export default App;

4. 自定义比较函数

如果我们不想通过引用来比较,而是完全比较数组的成员是否完全一致,则可以通过自定义比较函数来实现

jsx
import React, { useState } from 'react'

// 自定义比较函数
function arePropsEqual(oldProps, newProps) {
  console.log(oldProps, newProps)
  return (
    oldProps.list.length === newProps.list.length &&
    oldProps.list.every((oldItem, index) => {
      const newItem = newProps.list[index]
      console.log(newItem, oldItem)
      return oldItem === newItem
    })
  )
}

const MemoSon = React.memo(function Son() {
  console.log('子组件被重新渲染了')
  return <div>this is span</div>
}, arePropsEqual)

function App() {
  console.log('父组件重新渲染了')
  const [list, setList] = useState([1, 2, 3])
  return (
    <>
      <MemoSon list={list} />
      <button onClick={() => setList([1, 2, 3])}>
        内容一样{JSON.stringify(list)}
      </button>
      <button onClick={() => setList([4, 5, 6])}>
        内容不一样{JSON.stringify(list)}
      </button>
    </>
  )
}

export default App

四、React.useCallback(性能优化)

1. 使用场景

作用:在组件多次重新渲染的时候缓存函数使其引用不发生变化(主要用于缓存用于父子通信传递的函数,使子组件不频繁重新渲染

使用场景:使用React.memo()缓存的组件只有当props发生变化时才重新渲染,父组件重新渲染时传递给子组件的函数(用于组件通信)虽未发生改变但其引用了发生改变,最终导致缓存组件重新渲染,此时可以使用 useCallback缓存需要传递的函数保持引用稳定(不引起子组件的重新渲染)

2. useCallback基础用法

jsx
const changeHandler = useCallback(() => console.log(value),[]) // useCallback函数只在组件挂载完毕执行一次

使用useCallback包裹函数之后,返回的缓存函数可以在组件渲染时保持引用稳定,也就是返回同一个引用

jsx
import { memo, useCallback, useState } from "react";

/* 问题:使用React.memo()缓存的组件只有当props发生变化时才重新渲染
父组件重新渲染时传递给子组件的函数虽未改变但引用发生了改变导致缓存组件重新渲染*/
// 解决办法:使用useCallback缓存函数
const Input = memo(function Input({ onChange }) {
  console.log("子组件重新渲染了");
  return <input type="text" onChange={(e) => onChange(e.target.value)} />;
});

function App() {
  // 使用useCallback缓存传递给子组件的函数(使其在组件重新渲染时引用不发生变化)
  const changeHandler = useCallback((value) => console.log(value), []);
  // 触发父组件重新渲染的函数
  const [count, setCount] = useState(0);
  return (
    <>
      {/* 将函数作为props传递给子组件,实现父组件和子组件的通信 */}
      <Input onChange={changeHandler}></Input>
      <button onClick={() => setCount(count + 1)}>{count}</button>
    </>
  );
}

export default App;

五、React.forwardRef(暴露子组件Dom)

作用:允许组件使用ref将组件内的一个DOM节点暴露给父组件(实现父组件使用子组件中的DOM元素)

image-20240916161822780

基础使用:

jsx
import { forwardRef, useRef } from "react";
// 子组件
const Input = forwardRef((_props, ref) => {
  return (
    <>
      <input type="text" ref={ref} />
    </>
  );
});
// 父组件
function App() {
  const inputRef = useRef(null);
  // 需求:点击按钮,让子组件的input获取焦点
  const changeFocus = () => {
    console.log(inputRef.current); // <input type="text" />
    inputRef.current.focus();
  };
  return (
    <>
      <Input ref={inputRef} />
      <button onClick={changeFocus}>focus</button>
    </>
  );
}

export default App;

六、useImperativeHandle(暴露子组件方法)

作用:通过 ref暴露子组件内部的方法(实现父组件调用子组件中的方法)

image-20240917111659066
jsx
import { forwardRef, useRef, useImperativeHandle } from "react";
// 需求:点击父子组件按钮,使子组件中的input输入框聚集

// 子组件(forwardRef用于接收父组件传递的ref)
const Input = forwardRef((_props, ref) => {
  // 获取input组件的实例对象
  const inputRef = useRef(null);
  // 聚焦函数
  const focusHandler = () => {
    inputRef.current.focus();
  };
  // 将聚焦函数暴露给父组件(父组件在合适的时候调用)
  useImperativeHandle(ref, () => {
    return {
      focusHandler, // 暴露给父组件的方法
    };
  });
  return <input type="text" ref={inputRef} />;
});
// 父组件
function App() {
  // 获取子组件实例对象上通过useImperativeHandle暴露的focusHandler函数
  const sonRef = useRef(null);
  const focusHandler = () => {
    // 调用子组件暴露的函数
    sonRef.current.focusHandler();
  };
  return (
    <>
      <Input ref={sonRef} />
      <button onClick={focusHandler}>点我聚焦</button>
    </>
  );
}

export default App;

八、Class类组件

1. 基础使用

Class API就是使用ES6支持的原生Class API来编写React组件

使用 Class API 编写一个简单的 Counter自增组件

  1. 通过类属性 state 定义状态数据
  2. 通过 this.setState方法来修改状态数据
  3. 通过 render来写 UI模板
jsx
import { Component } from "react";
// 类组件
class Counter extends Component { // 继承Component基类
  // 编写组件的逻辑代码
  // 1.定义状态变量
  state = {
    count: 0,
  };
  // 2.定义事件回调修改状态数据
  setCount = () => {
    // 修改状态数据
    this.setState({
      // this.setState: 修改状态数据
      count: this.state.count + 1,
    });
  };
  // 3.渲染UI结构(jsx)
  render() {
    return (
      <>
        {/* 注意:在类组件中访问组件中的状态的方法需要通过this访问 */}
        <div>count:{this.state.count}</div>
        <button onClick={this.setCount}>点我count加一</button>
      </>
    );
  }
}
// 函数式组件
function App() {
  return (
    <>
      <Counter />
    </>
  );
}

export default App;

Tips:class组件已经不再被推荐使用,推荐使用 hook风格的函数式组件

注意:在类组件中访问组件中的状态的方法需要通过this进行访问

2.生命周期函数

概念:组件从创建到销毁各个阶段自动执行的函数就是生命周期函数

image-20240917135029333

常用的两个生命周期函数(钩子)

  1. componentDidMount:组件挂载完毕自动执行 -- 异步数据获取(发请求获取数据)
  2. componentwillUnmount:组件卸载时自动执行 -- 清理副作用(清除定时器)
jsx
import { Component, useState } from "react";
class Son extends Component {
  // 生命周期函数
  // 1.组件挂载完毕自动执行(只执行一次)用于发送网络请求
  componentDidMount() {
    console.log("组件挂载完毕了");
    // 组件挂载完毕开启定时器,将定时器对象挂到this上便于全局访问
    this.timer = setInterval(() => {
      console.log("我是子组件中的定时器");
    }, 1000);
  }
  // 2.组件卸载时自动执行(只执行一次)副作用清除
  componentWillUnmount() {
    console.log("组件即将卸载关闭定时器");
    clearInterval(this.timer);
  }
  state = {};
  render() {
    return (
      <>
        <p>我是子组件</p>
      </>
    );
  }
}

function App() {
  const [show, setShow] = useState(true);
  return (
    <>
      {/* 动态渲染子组件(挂载/卸载) */}
      {show && <Son />}
      <button onClick={() => setShow(!show)}>
        点我{show ? "卸载" : "挂载"}子组件
      </button>
    </>
  );
}

export default App;

3.组件通信

概念:类组件和 Hooks编写的组件通信的思想完全一致

  1. 父传子:通过 props 绑定数据
  2. 子传父:通过 props 绑定父组件中的函数,在子组件中调用
  3. 兄弟通信:状态提升,通过父组件做桥接
jsx
import { Component } from "react";
/* 类组件通信
1.父传子 直接通过props在子组件身上绑定父组件的数据即可
2.子传父 在子组件标签身上绑定父组件中的函数,子组件调用时传递参数给父组件 */

// 子组件
class Son extends Component {
  state = {};
  render() {
    // 使用 this.props 获取父组件传递的props数据
    return (
      <>
        <div>我是子组件</div>
        <div>{this.props.msg}</div>
        <button onClick={() => this.props.onSendMsg("哈哈哈哈")}>
          点我给父组件传递数据
        </button>
      </>
    );
  }
}
// 父组件
class Parent extends Component {
  state = {
    msg: "this is parent msg",
  };
  getSonMsg = (msg) => {
    console.log("子组件传递的数据:" + msg);
  };
  render() {
    return (
      <>
        <div>我是父组件</div>
        {/* 通过props向子组件传递数据 */}
        <Son msg={this.state.msg} onSendMsg={this.getSonMsg} />
      </>
    );
  }
}

function App() {
  return (
    <>
      <Parent />
    </>
  );
}

export default App;

九、zustand

极简的状态管理工具(代替繁杂的 redux),Zustand Documentation

1. 快速上手

安装:pnpm i zustan

jsx
// zustand
import { create } from "zustand";
// 1.创建store
const useStore = create((set) => {
  // create函数要求返回一个对象
  // set用于修改数据的方法,传入一个函数/对象
  return {
    // 定义状态数据
    count: 0,
    // 修改状态数据方法(依赖state)
    inc: () => {
      set((state) => ({
        count: state.count + 1,
      }));
    },
    // 修改状态数据的方法(不依赖state)
    addTo: (num) => {
      set({
        count: num,
      });
    },
  };
});

function App() {
  // 2.绑定store到组件获取store中的状态数据和修改状态的方法
  const { count, inc, addTo } = useStore(); // 返回一个对象
  return (
    <>
      <div>count:{count}</div>
      <button onClick={inc}>点我count++</button>
      <button onClick={() => addTo(100)}>点我count=100</button>
    </>
  );
}

export default App;

2. 异步支持

对于异步操作的支持不需要特殊的操作,直接在函数中编写异步逻辑,最后只需要调用set方法传入新状态即可

jsx
import { create } from "zustand";
import { useEffect } from "react";
// 1.创建store
const useChannelStore = create((set) => {
  return {
    // 频道列表数据
    channelList: [],
    // 异步获取并修改状态数据的方法
    getChannelList: async () => {
      // 获取频道列表数据
      const res = await fetch("http://geek.itheima.net/v1_0/channels");
      const jsonRes = await res.json();
      // 调用set方法修改状态数据
      set({
        channelList: jsonRes.data.channels,
      });
    },
  };
});

function App() {
  // 2.获取store中的数据和方法
  const { channelList, getChannelList } = useChannelStore();
  useEffect(() => {
    // 调用store中的异步获取数据的方法
    getChannelList();
  }, []);
  return (
    <>
      <ul>
        {/* 渲染store中的数据 */}
        {channelList.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </>
  );
}

export default App;

3. 切片模式

场景:当我们单个store比较大的时候,可以采用一种切片模式进行模块拆分再组合

image-20240917171907495
  1. 拆分并组合切片
  2. 组件使用
jsx
import { create } from "zustand";
import { useEffect } from "react";

// 1.拆分子模块
// 计数相关的子模块
const createCounterStore = (set) => {
  return {
    count: 0,
    inc: () => {
      set((state) => {
        return {
          count: state.count + 1,
        };
      });
    },
  };
};

// 频道列表相关的子模块
const createChannelStore = (set) => {
  return {
    channelList: [],
    getChannelList: async () => {
      // 获取频道列表数据
      const res = await fetch("http://geek.itheima.net/v1_0/channels");
      const jsonRes = await res.json();
      // 调用set方法修改状态数据
      set({
        channelList: jsonRes.data.channels,
      });
    },
  };
};

// 2.组合子模块
const useStore = create((...a) => {
  return {
    ...createCounterStore(...a),
    ...createChannelStore(...a),
  };
});

function App() {
  const { channelList, count, inc, getChannelList } = useStore();
  useEffect(() => {
    // 调用store中的异步获取数据的方法
    getChannelList();
  }, []);
  return (
    <>
      <div>
        {count} <button onClick={inc}>点我加一</button>
      </div>
      <ul>
        {/* 渲染store中的数据 */}
        {channelList.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </>
  );
}

export default App;

拆分为多个文件(推荐写法)

src\store\modules\channelStore.js

js
// 频道相关的子模块
const createChannelStore = (set) => {
  // 初始化数据
  return {
    channelList: [],
    getChannelList: async () => {
      // 获取频道列表数据
      const res = await fetch("http://geek.itheima.net/v1_0/channels");
      const jsonRes = await res.json();
      // 调用set方法修改状态数据
      set({
        channelList: jsonRes.data.channels,
      });
    },
  };
};

export default createChannelStore;

src\store\modules\counterStore.js

js
// 计数相关的子模块
const createCounterStore = (set) => {
  // 初始化数据
  return {
    // 状态数据
    count: 0,
    // 修改状态数据的方法
    inc: () => {
      set((state) => {
        return {
          count: state.count + 1,
        };
      });
    },
  };
};
// 暴露子模块
export default createCounterStore;

src\store\index.js

js
import { create } from "zustand";
import createCounterStore from "./modules/counterStore";
import createChannelStore from "./modules/channelStore";
// 组合子模块暴露大仓库
const useStore = create((...a) => {
  return {
    ...createCounterStore(...a),
    ...createChannelStore(...a),
  };
});

export default useStore;

src\App.jsx

jsx
import { useEffect } from "react";
import useStore from "./store";

function App() {
  const { channelList, count, inc, getChannelList } = useStore();
  useEffect(() => {
    // 调用store中的异步获取数据的方法
    getChannelList();
  }, []);
  return (
    <>
      <div>
        {count} <button onClick={inc}>点我加一</button>
      </div>
      <ul>
        {/* 渲染store中的数据 */}
        {channelList.map((item) => (
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </>
  );
}

export default App;

4. 对接DevTools

简单的调试我们可以安装一个 名称为 simple-zustand-devtools 的调试工具

  1. 安装调试包:pnpm i simple-zustand-devtools -D

  2. 配置调试工具 src\store\index.js

js
import { create } from "zustand";
import createCounterStore from "./modules/counterStore";
import createChannelStore from "./modules/channelStore";
+// 导入调试核心方法
+import { mountStoreDevtool } from "simple-zustand-devtools";

// 组合子模块暴露大仓库
const useStore = create((...a) => {
  return {
    ...createCounterStore(...a),
    ...createChannelStore(...a),
  };
});

+// 开发环境开启调试
+if (process.env.NODE_ENV === "development") {
+  mountStoreDevtool("useStore", useStore);
+}
export default useStore;
  1. 打开 React调试工具

image-20240917190352231

React与TypeScript

一、React+TS环境搭建

  1. 使用 vite 创建 react-ts 的项目: pnpm create vite react-ts-pro --template react-ts

  2. 安装依赖:pnpm i

  3. 配置运行时打开浏览器:package.json

    json
    "scripts": {
    +   "dev": "vite --open",
  4. 运行项目:pnpm run dev

vite:下一代的前端工具链,为开发提供极速响应

pnpm:快速的,节省磁盘空间的包管理工具

二、Hooks与TypeScript

1. useState

1.1 自动推导

简单场景下,可以使用TS的自动推断机制,不用特殊编写类型注解,运行良好

typescript
const [val, toggle] = useState(false)
// val 会被自动推断为布尔类型
// toggle 方法调用时只能传入布尔类型

1.2 泛型参数

复杂数据类型,useState支持通过泛型参数指定初始参数类型以及setter函数的参数类型

typescript
type User = {
  name: string
  age: number
}
const [user, setUser] = useState<User>(); // user: User | undefined
const [user, setUser] = useState<User>({ // user:User
  name: 'jack',
  age: 18
})
const [student, setStudent] = useState<User>(() => ({
  name: "John",
  age: 20,
}));
const changeUser = () => {
  setUser({
    name: "John",
    age: 20,
  });
};
const changeStudent = () => {
  setStudent(() => ({
    name: "John",
    age: 20,
  }));
};
  1. 限制 useState 函数参数的初始值必须满足类型为:User | ()=>User
  2. 限制 setUser 函数的参数必须满足类型为:User | ()=>User | undefined
  3. user 状态数据具备 User类型相关的类型提示

1.3 初始值为null

实际开发时,有些时候useState的初始值可能为null或者undefined,按照泛型的写法是不能通过类型校验的,此时可以通过完整的联合类型null或者undefined类型即可

tsx
type User = {
  name: String
  age: Number
}
const [user, setUser] = React.useState<User>(null) // 报错
// 上面会类型错误,因为null并不能分配给User类型

const [user, setUser] = React.useState<User | null>(null)
// 上面既可以在初始值设置为null,同时满足setter函数setUser的参数可以是具体的User类型

// 访问变量时使用可选链的方式 ?. user可能为null/undefined
return <>{user?.name}</>;

2. useRef

TypeScript的环境下,useRef 函数返回一个只读 或者 可变 的引用,只读的场景常见于获取真实dom,可变的场景,常见于缓存一些数据,不跟随组件渲染,下面分俩种情况说明

2.1 获取dom

获取 dom 的场景,可以直接把要获取的 dom元素的类型当成泛型参数传递给useRef,可以推导出**.current属性的类型**

tsx
function App() {
  // 获取input组件
  const inputRef = useRef<HTMLInputElement>(null);
  useEffect(() => {
    inputRef.current?.focus(); // ?.可选链(不知道是否为null) 避免出现null/undefined报错
    inputRef.current!.focus(); // !.非空断言(确定其不为null) 避免出现null/undefined报错
  }, []);
  return (
    <>
      <input type="text" ref={inputRef} />
    </>
  );
}

2.2 稳定引用存储器

useRef 当成引用稳定的存储器使用的场景可以通过泛型传入联合类型来做,比如定时器的场景

tsx
function App() {
  // 存储定时器id
  const timerId = useRef<number>(); // timerId:number | undefined
  useEffect(() => {
    // setInterval: 返回一个定时器id
    timerId.current = setInterval(() => {
      console.log("1111");
    }, 1000);
    return () => {
      // 销毁定时器
      clearInterval(timerId.current);
    };
  }, []);
  return <></>;
}

三、Component与TypeScript

1. 为Props添加类型

为组件 props 添加类型,本质就是给函数的参数做类型注解,可以使用 type对象类型或者 interface接口来做注解

1.1 使用interface接口

tsx
// 定义props参数的类型
interface Props {
  className: string; // 必传参数
}

function Button(props: Props) {
  const { className } = props;
  return <button className={className}>Click me</button>;
}

function App() {
  return <Button className="button" />;
}

1.2 使用自定义类型Type

tsx
// 定义props参数的类型
type Props = {
  className: string; // 必填参数
  title?:stiring   // 可选参数
};

function Button(props: Props) {
  const { className } = props;
  return <button className={className}>Click me</button>;
}

function App() {
  return <Button className="button" />;
}

2. 为Props的chidren属性添加类型

children是一个比较特殊的 props,支持多种不同类型数据的传入,需要通过一个内置的 ReactNode类型来做注解

tsx
import { ReactNode } from "react";

interface Props {
  className: string;
  children?: ReactNode; // 或react.ReactNode
}

function Button(props: Props) {
  const { className, children } = props;
  return <button className={className}>{children}</button>;
}

function App() {
  return <Button className="button">Click me</Button>;
}

Tips:React.ReactNode是一个React内置的联合类型,包括 React.ReactElementstringnumber React.ReactFragmentReact.ReactPortalbooleannullundefined

3. 为事件prop添加类型

组件经常会执行类型为函数的 props 实现子传父,这类 props 重点在于函数参数类型的注解

  1. 在组件内部调用时需要遵守类型的约束,参数传递需要满足要求
  2. 绑定 props 时如果绑定内联函数直接可以推断出参数类型,否则需要单独注解匹配的参数类型
tsx
interface Props {
  onSendMsg?: (msg: string) => void; // 父组件传递的事件prop的类型
}

function Son(props: Props) {
  const { onSendMsg } = props;
  return <button onClick={() => onSendMsg?.("hello")}>sedMsg</button>;
}

function App() {
  const getMsg = (msg: string) => {
    console.log(msg);
  };
  return (
    <>
      <Son onSendMsg={getMsg} />
    </>
  );
}

4. 为事件回调 e 添加类型

为事件回调添加类型约束需要使用React内置的泛型函数来做,比如最常见的鼠标点击事件和表单输入事件:

tsx
function App(){
  const changeHandler: React.ChangeEventHandler<HTMLInputElement> = (e)=>{
    console.log(e.target.value)
  }
  
  const clickHandler: React.MouseEventHandler<HTMLButtonElement> = (e)=>{
    console.log(e.target)
  }

  return (
    <>
      <input type="text" onChange={ changeHandler }/>
      <button onClick={ clickHandler }> click me!</button>
    </>
  )
}

极客园-移动端

一、项目初始化

1. 创建项目

1.1 使用 vite+pnpm 创建项目

创建项目:pnpm create vite project-jike-mobile --template react-ts

安装依赖包:pnpm i

1.2 配置浏览器自动打开

package.json

json
"scripts": {
  "dev": "vite --open",

1.3 初始化项目结构

image-20240918165209095

1.4 集成 scss

安装依赖:pnpm i sass -D

2. 集成 antd-mobile

Ant Design Mobile 是 Ant Design 家族里针对于移动端的组件库

2.1 安装

bash
pnpm install --save antd-mobile

2.2 使用

直接引入组件即可,antd-mobile 会自动为你加载 css 样式文件image-20240918164447043

tsx
import { Button } from "antd-mobile"; // 按需导入

function App() {
  return (
    <>
      <Button color="success" fill="outline" shape="rounded">
        成功
      </Button>
    </>
  );
}

export default App;

3. 初始化路由

3.1 安装路由

bash
pnpm i react-router-dom

3.2 配置基础路由

  1. 创建路由组件
tsx
// src\pages\Home\index.tsx
function Home() {
  return <>this is home</>;
}
export default Home;
tsx
// src\pages\Detail\index.tsx
function Detail() {
  return <>this is detail</>;
}
export default Detail;
  1. 配置路由规则
tsx
import { createBrowserRouter } from "react-router-dom";
import Home from "../pages/Home";
import Detail from "../pages/Detail";

const router = createBrowserRouter([
  {
    path: "/",
    element: <Home />,
  },
  {
    path: "/detail",
    element: <Detail />,
  },
]);

export default router;
  1. 渲染路由组件
tsx
// src\App.tsx
import { RouterProvider } from "react-router-dom";
import router from "./router";

function App() {
  return (
    <>
      <RouterProvider router={router} />
    </>
  );
}
export default App;

4. 配置路径别名

场景:在项目中各个模块之间的互相导入导出,可以通过 @ 别名路径做路径简化,经过配置 @ 相当于 src目录

../pages/Detail === @/pages/Detail === src/pages/Detail

4.1 修改vite配置

目的:将 @解析为 ./src

ts
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

export default defineConfig({
  plugins: [react()],
  // 路径解析
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

4.2 安装node类型包

pnpm i @types/node -D

目的:解决 node模块 ts 类型报错

image-20240918171438101

4.3 修改tsconfig.json文件

目的:输入 @ 时有提示

json
// tsconfig.app.json
{
  "compilerOptions": {
    "target": "ES2020",
  	......
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["src/*"]
+    }
  },
  "include": ["src"]
}

5. axios二次封装

5.1 安装axios

bash
pnpm i axios

5.2 简易封装

  1. 创建 axios 实例
  2. 封装请求拦截器
  3. 封装响应拦截器
typescript
import axios from "axios";

// 创建axios实例
const http = axios.create({
  baseURL: "http://geek.itheima.net/v1_0",  // 基础路径
  timeout: 5000, // 超时时间
});

// 请求拦截器
http.interceptors.request.use(
  (config) => {
    return config;
  },
  (err) => {
    return Promise.reject(err);
  }
);

// 响应拦截器
http.interceptors.response.use(
  // responsr.status == 2xxx
  (response) => {
    // 简化返回数据
    return response.data;
  },
  // response.status == 3xxx | 4xx | 5xxx ....
  (error) => {
    // 错误提示
    // 返回失败的Promise终结状态
    return Promise.reject(error);
  }
);
// 暴露请求实例
export default http;

6. 封装请求函数(重要)

image-20240918203802385

image-20240918203953028

axios的实例方法接收两个泛型参数:

  1. Tres.data 的数据类型 (即后端返回的数据类型)
  2. Rres的数据类型(即请求返回的数据类型)

如果只传递第一个泛型参数,接口返回的数据类型为 AxiosResponse<Type>

使用场景:在响应拦截器的成功回调中直接返回 response

ts
const res = await axios.get<Type>("url"); // Type为res.data的数据类型
ts
res:{
	data:Type,
	status:number,
	statusText:string,
	.....
}
Type:{
	code:string,
	data:XXX,
	message:string,
}

如果传递的第一个参数为 any 第二个参数为 Type,接口返回的数据类型为 Type(覆盖axios提供的返回值类型) :

使用场景:在响应拦截器成功回调中返回简化后的数据 response.data(简化返回数据)

ts
const res = await axios.get<any, Type>("url"); // Type为res的数据类型
res:Type
Type:{
	code:string,
	data:XXX,
	message:string,
}
无标题-2023-11-04-1803.png

步骤:

  1. 根据接口文档创建一个通用的泛型接口类型(多个接口返回值的结构是相似的)
  2. 根据接口文档创建特有的接口数据类型(每个接口有自己特殊的返回数据格式)
  3. 组合1和2的类型,得到最终传给请求函数泛型的参数类型

接口文档

6.1 定义接口类型

封装所有接口通用返回泛型:src\types\global.d.ts

typescript
// 定义所有接口通用返回类型(data的类型由泛型传入 / 也可采用接口继承的方式)
export interface ResType<T> {
  message: string;
  data: T;
}

定义获取频道列表返回的数据类型:src\types\list.d.ts

ts
import { ChannelList } from "./list";
// 每个频道项的类型
export interface ChannelItem {
  id: number;
  name: string;
}
// 频道列表的类型
export interface ChannelListRes {
  channels: ChannelItem[];
}

6.2 封装请求函数

src\apis\list.ts

tsx
import type { ResType } from "@/types/global";
import type { ChannelListRes } from "@/types/list";
import http from "@/utils/http";

/**
 * 获取频道列表的接口方法
 */
export const getChannelListAPI = () =>
  http.get<any,ResType<ChannelListRes>>("/channels");

6.3 测试接口函数

src\pages\Home\index.tsx

jsx
import { getChannelListAPI } from "@/apis/list";
import { useEffect } from "react";
function Home() {
  // 获取频道列表的接口方法
  const getChannelList = async () => {
    const res = await getChannelListAPI();
    console.log(res);
  };
  useEffect(() => {
    getChannelList();
  }, []);
  return <>this is home</>;
}

export default Home;

二、列表模块

1. 整体结构设计

image.pngimage-20240918195134165

1.1 准备Home入口组件

tsx
import "./index.scss";

const Home = () => {
  return (
    <>
      {/* tba区域 */}
      <div className="tabContainer"></div>
      {/* 列表区域 */}
      <div className="listContainer"></div>
    </>
  );	
}

export default Home

1.2 准备配套样式

scss
// src\pages\Home\index.scss
.tabContainer {
  position: fixed;
  height: 50px;
  top: 0;
  width: 100%;
}

.listContainer {
  position: fixed;
  top: 50px;
  bottom: 0px;
  width: 100%;
}

2. Tabs模块实现

image.png

2.1 封装请求接口和定义类型

ts
// src\types\list.d.ts
// 每个频道项的类型
export interface ChannelItem {
  id: number;
  name: string;
}
// 返回的频道列表的类型
export interface ChannelListRes {
  channels: ChannelItem[];
}
ts
// src\apis\list.ts
import type { ResType } from "@/types/global";
import type { ChannelListRes } from "@/types/list";
import http from "@/utils/http";

/**
 * 获取频道列表的接口方法
 */
export const getChannelListAPI = () =>
  http.get<any,ResType<ChannelListRes>>("/channels");

2.2 获取数据并渲染

tsx
// src\pages\Home\index.tsx
import { getChannelListAPI } from "@/apis/list";
import type { ChannelItem } from "@/types/list";
import { useEffect, useState } from "react";
import { Tabs } from "antd-mobile";
import "./index.scss";
function Home() {
  // 频道列表
  const [channelList, setChannelList] = useState<ChannelItem[]>([]);
  // 获取频道列表的接口方法
  const getChannelList = async () => {
    const res = await getChannelListAPI();
    // 存储频道列表数据
    setChannelList(res.data.channels);
  };
  useEffect(() => {
    getChannelList();
  }, []);
  return (
    <>
      {/* tba区域 */}
      <div className="tabContainer">
        <Tabs>
          {channelList.map((item) => (
            <Tabs.Tab title={item.name} key={item.id}></Tabs.Tab>
          ))}
        </Tabs>
      </div>
      {/* 列表区域 */}
      <div className="listContainer"></div>
    </>
  );
}

export default Home;

2.3 封装数据请求hook

typescript
// src\hooks\useTabs.ts
import { getChannelListAPI } from "@/apis/list";
import { ChannelItem } from "@/types/list";
import { useEffect, useState } from "react";

// 获取tabs(频道列表)数据的自定义hook函数
export const useTabs = () => {
  const [channelList, setChannelList] = useState<ChannelItem[]>([]);
  // 获取频道列表的方法
  const getChannelList = async () => {
    const res = await getChannelListAPI();
    setChannelList(res.data.channels);
  };
  // useEffect(生命周期函数也会执行且先于组件的生命周期函数执行)
  useEffect(() => {
    getChannelList();
  }, []);
  // 暴露数据/方法
  return {
    channelList,
  };
};

2.4 调用hook渲染数据

tsx
import { Tabs } from "antd-mobile";
import "./index.scss";
+import { useTabs } from "@/hooks/useTabs";
function Home() {
+  // 获取频道列表数据
+  const { channelList } = useTabs();
  return (
    <>
      {/* tba区域 */}
      <div className="tabContainer">
        <Tabs>
          {channelList.map((item) => (
            <Tabs.Tab title={item.name} key={item.id}></Tabs.Tab>
          ))}
        </Tabs>
      </div>
      {/* 列表区域 */}
      <div className="listContainer"></div>
    </>
  );
}

export default Home;

3. List模块实现

3.1 准备基础结构

tsx
// src\pages\Home\HomeList\index.tsx
import { Image, List } from 'antd-mobile'
// mock数据
const users = [
  {
    id: '1',
    avatar:
      'https://images.unsplash.com/photo-1548532928-b34e3be62fc6?ixlib=rb-1.2.1&q=80&fm=jpg&crop=faces&fit=crop&h=200&w=200&ixid=eyJhcHBfaWQiOjE3Nzg0fQ',
    name: 'Novalee Spicer',
    description: 'Deserunt dolor ea eaque eos',
  },
  {
    id: '2',
    avatar:
      'https://images.unsplash.com/photo-1493666438817-866a91353ca9?ixlib=rb-0.3.5&q=80&fm=jpg&crop=faces&fit=crop&h=200&w=200&s=b616b2c5b373a80ffc9636ba24f7a4a9',
    name: 'Sara Koivisto',
    description: 'Animi eius expedita, explicabo',
  },
]

const HomeList = () => {
  return (
    <>
      <List>
        {users.map((item) => (
          <List.Item
            key={item.id}
            prefix={
              <Image
                src={item.avatar}
                style={{ borderRadius: 20 }}
                fit="cover"
                width={40}
                height={40}
              />
            }
            description={item.description}
            >
            {item.name}
          </List.Item>
        ))}
      </List>
    </>
  )
}

export default HomeList
scss
// src\pages\Home\HomeList\index.scss
.home-list {
  position: fixed;
  top: 50px;
  overflow: auto;
  height: 100%;
  width: 100%;
}

3.2 封装请求API

定义接口类型:

ts
// src\types\list.d.ts
// 文章列表组件的props类型
export interface HomeListProps {
  activeKey: string;
}
// 单个文章类型
export interface ArticleItem {
  /** 文章id*/
  art_id: string;
  /** 文章标题*/
  title: string;
  /** 作者id*/
  aut_id: string;
  /**评论数量 */
  comm_count: number;
  /**发表时间 */
  pubdate: string;
  /**作者名称 */
  aut_name: string;
  /** 是否置顶*/
  is_top: number;
  /**文章封面 */
  cover: {
    /**封面类型 */
    type: number;
    /**封面图片数组 */
    images: string[];
  };
}
// 获取文章列表的返回值类型
export interface ArticleListRes {
  results: ArticleItem[];
  /**请求下一页数据的时间戳 */
  pre_timestamp: string;
}
// 获取文章列表的请求参数类型
export interface ReqArticleListParams {
  /**频道id */
  channel_id: string;
  /**请求时间戳 */
  timestamp: string;
}

封装请求函数:

ts
// src\apis\list.ts
/**
 * 获取文章列表的接口方法
 * @param params query参数
 */
export const getArticleListAPI = (params: ReqArticleListParams) =>
  http.get<any, ResType<ArticleListRes>>("/articles", { params: params });

3.3 获取当前激活频道的id

Tabs 组件的 onChange事件回调中获取当前激活频道id并传递到文章列表 HomeList组件中

tsx
import { Tabs } from "antd-mobile";
import { useTabs } from "@/hooks/useTabs";
import HomeList from "./HomeList";
import "./index.scss";
import { useEffect, useState } from "react";
function Home() {
  // 获取频道列表数据
  const { channelList } = useTabs();
+  // 当前激活的频道key(id)
+  const [activeKey, setActiveKey] = useState("");
+  // 切换tab的回调
+  const changeTabs = (key: string) => {
+    // 更新当前激活频道key
+    setActiveKey(key);
+  };
  // 组件挂载完毕
+  useEffect(() => {
+    setActiveKey(channelList[0]?.id.toString()); // 初始化默认激活为第一项的频道id
+  }, [channelList]); // 当获取到channelList时初始化默认激活的key
  return (
    <>
      {/* tba区域 */}
      <Tabs
        activeKey={activeKey}
        style={{ backgroundColor: "#fff" }}
        onChange={changeTabs}
      >
        {channelList.map((item) => (
          <Tabs.Tab title={item.name} key={item.id}>
+            <HomeList activeKey={activeKey} />  // 将激活的频道id传递给子组件
          </Tabs.Tab>
        ))}
      </Tabs>
    </>
  );
}

export default Home;

3.4 获取文章列表数据并渲染

tsx
import { Image, List } from "antd-mobile";
import type { ArticleItem, HomeListProps } from "@/types/list";
import { useEffect, useState } from "react";
import { getArticleListAPI } from "@/apis/list";
import "./index.scss";
function HomeList(props: HomeListProps) {
  // 获取父组件传递的当前激活频道的id
  const { activeKey } = props;
  // 文章列表数据
  const [articleList, setArticleList] = useState<ArticleItem[]>([]);
  // 用于获取下一页文章列表的时间戳(默认为当前时间戳)
  const [timestamp, setTimestamp] = useState(new Date().getTime().toString());
  // 是否还有更多数据
  const [hasMore, setHasMore] = useState(true);
  // 获取文章列表的方法
  const getArticleList = async () => {
    // 准备请求参数
    const params = {
      channel_id: activeKey,
      timestamp: timestamp,
    };
    // 调用接口获取数据
    const res = await getArticleListAPI(params);
    // 存储文章列表数据
    setArticleList(res.data.results);
    // 存储获取下一页数据的时间戳
    setTimestamp(res.data.pre_timestamp);
  };
  return (
    <>
      <div className="home-list">
        <List header="评论列表" mode="card">
          {articleList.map((article) => (
            <List.Item
              key={article.art_id}
              prefix={
                <Image
                  src={article.cover.images?.[0]}
                  style={{ borderRadius: 20 }}
                  fit="cover"
                  width={40}
                  height={40}
                />
              }
              description={article.pubdate}
            >
              {article.title}
            </List.Item>
          ))}
        </List>
      </div>
    </>
  );
}

export default HomeList;

4. List模块无限加载实现

4.1 实现上拉加载更多

需求:List列表在滑动到底部时,自动加载下一页数据(即上拉加载更多)

实现思路:

  1. 滑动到底部触发加载下一页动作

    使用antd-mobile提供的 <InfiniteScroll loadMore={loadMore} hasMore={hasMore} />组件

  2. 加载下一页数据

    hasMore 属性为 true 时,用户页面滚动到底部 threshold (默认为 250px)时无限滚动组件会调用定义的 loadMore 函数

  3. loadMore函数中拼接新老数据

    [...oldList,...newList]

  4. 当数据加载完毕关闭加载

    设置 hasMore = false

jsx
import { Image, InfiniteScroll, List } from "antd-mobile";
import type { ArticleItem, HomeListProps } from "@/types/list";
import { useEffect, useState } from "react";
import { getArticleListAPI } from "@/apis/list";
import "./index.scss";
function HomeList(props: HomeListProps) {
  // 获取父组件传递的当前激活频道的id
  const { activeKey } = props;
  // 文章列表数据
  const [articleList, setArticleList] = useState<ArticleItem[]>([]);
+  // 用于获取下一页文章列表的时间戳(默认为当前时间戳)
+  const [timestamp, setTimestamp] = useState(new Date().getTime().toString());
+  // 是否还有更多数据
+  const [hasMore, setHasMore] = useState(true);
  // 获取文章列表的方法
  const getArticleList = async () => {
    // 准备请求参数
    const params = {
      channel_id: activeKey,
      timestamp: timestamp,
    };
    // 调用接口获取数据
    const res = await getArticleListAPI(params);
    // 存储文章列表数据
+    setArticleList([...articleList, ...res.data.results]);
+    /* 判断是否还有更多数据
+        没有更多数据:pre_timestamp===null 禁用上拉加载更多
+        有更多数据:则存储获取下一页数据的时间戳 */
+    res.data.pre_timestamp
+      ? setTimestamp(res.data.pre_timestamp)
+      : setHasMore(false);
+  };
+  // 上拉加载更多的方法
+  const loadMore = async () => {
+    await getArticleList();
+  };
  // 组件挂载完毕(activeKey发生变化时重新获取数据)
  useEffect(() => {
    getArticleList();
  }, [activeKey]);
  return (
    <>
      <div className="home-list">
        <List header="评论列表" mode="card">
          {articleList.map((article) => (
            <List.Item
              key={article.art_id}
              prefix={
                <Image
                  src={article.cover.images?.[0]}
                  style={{ borderRadius: 20 }}
                  fit="cover"
                  width={40}
                  height={40}
                />
              }
              description={article.pubdate}
            >
              {article.title}
            </List.Item>
          ))}
        </List>
+        {/* 无限滚动 上拉加载更多
+	        hasMore:是否还有更多数据
+	        loadMore:加载更多方法*/}
+        <InfiniteScroll loadMore={loadMore} hasMore={hasMore} threshold={10} />
      </div>
    </>
  );
}

export default HomeList;

4.2 优化:上拉加载函数节流处理

使用 lodash 提供的 throttle函数实现

jsx
// @ts-ignore
import _ from "lodash";

// 上拉加载更多的方法(节流处理)
const loadMore = _.throttle(async () => {
  await getArticleList();
}, 500);

作用:避免用户重复上拉发送相同的请求,造成数据重叠,资源浪费

三、详情模块

需求:点击列表中的某一项跳转到详情路由并显示当前文章

实现步骤:

  1. 通过编程式路由导航进行路由跳转,并传递参数
  2. 在详情路由下获取参数,并请求数据
  3. 渲染数据到页面中

1. 路由跳转传参

tsx
function HomeList(props: HomeListProps) {
  .......
+  // 路由跳转方法
+  const navigator = useNavigate();
+  // 跳转到路由详情页的函数
+  const goToDetail = (article_id: string) => {
+    // 路由跳转并传参
+    navigator(`/detail?article_id=${article_id}`);
+  };
  return (
    <>
      <div className="home-list">
        <List header="评论列表" mode="card">
          {articleList.map((article) => (
            <List.Item
              // 函数传参必须使用箭头函数包裹回调函数
+             onClick={() => goToDetail(article.art_id)}   // 传递文章id
              key={article.art_id}
              prefix={
                <Image
                  src={article.cover.images?.[0]}
                  style={{ borderRadius: 20 }}
                  fit="cover"
                  width={40}
                  height={40}
                />
              }
              description={article.pubdate}
            >
              {article.title}
            </List.Item>
          ))}
        </List>
        {/* 无限滚动 上拉加载更多
        hasMore:是否还有更多数据
        loadMore:加载更多方法*/}
        <InfiniteScroll loadMore={loadMore} hasMore={hasMore} threshold={10} />
      </div>
    </>
  );
}

2. 获取详情数据并渲染

2.1 封装接口并定义类型

ts
// src\types\detail.ts
/**文章详情类型 */
export interface ArticleDetail {
  /**文章id*/
  art_id: string;
  /**文章-是否被点赞,-1无态度, 0未点赞, 1点赞, 是当前登录用户对此文章的态度*/
  attitude: number;
  /** 文章作者id*/
  aut_id: string;
  /**文章作者名*/
  aut_name: string;
  /**文章作者头像,无头像, 默认为null*/
  aut_photo: string;
  /** 文章_评论总数*/
  comm_count: number;
  /**文章内容*/
  content: string;
  /** 文章-是否被收藏,true(已收藏)false(未收藏)是登录的用户对此文章的收藏状态*/
  is_collected: boolean;
  /** 文章作者-是否被关注,true(关注)false(未关注), 说的是当前登录用户对这个文章作者的关注状态*/
  is_followed: boolean;
  /** 文章_点赞总数*/
  like_count: number;
  /**文章发布时间 */
  pubdate: string;
  /**文章_阅读总数*/
  read_count: number;
  /** 文章标题 */
  title: string;
}
ts
// src\apis\detail.ts
import type { ArticleDetail } from "@/types/detail";
import type { ResType } from "@/types/global";
import http from "@/utils/http";

/**
 * 获取文章详情的接口方法
 * @param id path参数 文章id
 */
export const getArticleDetailAPI = (id: string) =>
  http.get<any, ResType<ArticleDetail>>(`/articles/${id}`);

Tips:使用apifox快速生成返回数据类型声明

image-20240919202901025image-20240919202914859

2.2 获取数据并渲染

tsx
import { getArticleDetailAPI } from "@/apis/detail";
import type { ArticleDetail } from "@/types/detail";
import { NavBar } from "antd-mobile";
import { useEffect, useState } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";

function Detail() {
  // 获取路由参数
  const [searchParams] = useSearchParams();
  // 获取文章列表页传递的文章id
  const id = searchParams.get("article_id");
  // 文章详情
  const [articleDetail, setArticleDetail] = useState<ArticleDetail | null>(
    null
  );
  const navigate = useNavigate();
  // 获取文章详情的方法
  const getArticleDetail = async () => {
    const res = await getArticleDetailAPI(id!);
    setArticleDetail(res.data);
  };

  useEffect(() => {
    getArticleDetail();
  }, [id]);
  return (
    <>
      <NavBar back="返回" onBack={() => navigate(-1)}>
        {articleDetail?.title || "loading..."}
      </NavBar>
      {/* 渲染带有html标签的字符串 */}
      <div
        dangerouslySetInnerHTML={{
          //数据可能还未返回避免undefined报错(先渲染模板 -> 执行useEffect)
          __html: articleDetail?.content || "loading...",
        }}
      ></div>
    </>
  );
}

export default Detail;

Tisp:

  1. 渲染带有html标签的字符串:
tsx
{/* 渲染带有html标签的字符串 */}
<div
  dangerouslySetInnerHTML={{
    //数据可能还未返回避免undefined报错(先渲染模板 -> 执行useEffect)
    __html: articleDetail?.content || "loading...",
  }}
></div>
  1. 避免undefined报错:

    数据可能还未返回避免undefined报错(先渲染模板 -> 执行useEffect)

jsx
articleDetail?.content || "loading..."
  1. 返回上一级:
ts
navigate(-1)