react+ts+antd-mobile 动态tabs➕下拉加载

1.初始化项目

bash 复制代码
//搭建项目
npm create vite@latest react-jike-mobile -- --template react-ts
bash 复制代码
//安装依赖
npm i 
bash 复制代码
//运行
npm run dev
清理项目目录结构
安装ant design mobile

ant design mobile是ant design家族里专门针对于移动端的组件库

bash 复制代码
npm install --save antd-mobile
测试组件
bash 复制代码
import { Button } from 'antd-mobile'

function App() {
  return (
    <>
      <Button>click me </Button>
    </>
  )
}

export default App

2.初始化路由

react的路由初始化,采用react-router-dom进行配置

bash 复制代码
npm i react-router-dom

3. 配置基础路由

bash 复制代码
//List页面
const List = () => {
  return <div>this is List</div>
}

export default List
bash 复制代码
//detail页面
const Detail = () => {
  return <div>this is Detail</div>
}

export default Detail
bash 复制代码
//router文件下index.tsx
import { createBrowserRouter } from 'react-router-dom'
import List from '../pages/List'
import Detail from '../pages/Detail'

const router = createBrowserRouter([
  {
    path: '/',
    element: <List />,
  },
  {
    path: '/detail',
    element: <Detail />,
  },
])

export default router
bash 复制代码
//main.txt
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import router from './router/index.tsx'

ReactDOM.createRoot(document.getElementById('root')!).render(
  <RouterProvider router={router} />
)

4. 配置路径别名

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

步骤:

1.让vite做路径解析(真实的路径转换)

2.让vscode做智能路径提示(开发者体验)

1️⃣修改vite配置

javascript 复制代码
//修改vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

2️⃣安装node类型包

bash 复制代码
npm i @types/node -D

3️⃣修改tsconfig.json文件

json 复制代码
{
  "baseUrl": ".",
  "paths": {
    "@/*": [
      "src/*"
    ]
  },
}

5. 安装axios

1.安装axios到项目

2.在utils中封装http模块,主要包括接口基地址、超时时间、拦截器

3.在utils中做统一导出

bash 复制代码
//安装axios
npm i axios
javascript 复制代码
// 封装axios在utils下http.ts里
import axios from 'axios'

const httpInstance = axios.create({
  baseURL: 'http://geek.itheima.net/v1_0',
  timeout: 5000,
})

// 拦截器
httpInstance.interceptors.request.use(
  (config) => {
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

httpInstance.interceptors.response.use(
  (response) => {
    return response
  },
  (error) => {
    return Promise.reject(error)
  }
)

export { httpInstance }
javascript 复制代码
//utils下index.ts文件
// 模块中转导出文件
import { httpInstance } from './http'

export { httpInstance as http }

6.封装API模块---axios和ts的配合使用

场景:axios提供了request泛型方法,方便我们传入类型参数推导出接口返回值的类型

说明:泛型参数type的类型决定了res.data的类型

步骤:

1️⃣根据接口文档创建一个通用的泛型接口类型(多个接口返回值的结构是相似的)

2️⃣根据接口文档创建特有的接口类型(每个接口有自己特殊的数据格式)

3️⃣组合1和2的类型,得到最终传给request泛型的参数类型

javascript 复制代码
//apis文件下shared.ts
// 1. 定义泛型
export type ResType<T> = {
  message: string
  data: T
}
javascript 复制代码
//apis文件下list.ts
import { http } from '@/utils'
//引入泛型
import type { ResType } from './shared'

//  2. 定义具体的接口类型
export type ChannelItem = {
  id: number
  name: string
}

type ChannelRes = {
  channels: ChannelItem[]
}

// 请求频道列表

export function fetchChannelAPI() {
  return http.request<ResType<ChannelRes>>({
    url: '/channels',
  })
}

页面使用

javascript 复制代码
import { fetchListAPI } from '@/apis/list'
fetchChannelAPI().then((res) => {
  console.log(res.data.data.channels)
})

7.home模块


Home模块---Tabs区域实现

实现步骤:

1️⃣使用ant-mobile组件库中的tabs组件进行页面结构的创建

2️⃣使用真实接口数据进行渲染

3️⃣有优化的点进行优化处理

Home模块---Tabs自定义hook函数优化

针对上面代码封装hook函数进行代码优化

场景:当前状态数据的各种操作逻辑和组件渲染是写在一起的,可以采用自定义hook封装的方式让逻辑和渲染相分离

实现步骤:

1️⃣把和tabs相关的响应式数据状态以及操作数据的方法放到hook函数中

2️⃣组件中调用hook函数,消费其返回的状态和方法

javascript 复制代码
//home文件下useTabs.ts
import { useEffect, useState } from 'react'
import { ChannelItem, fetchChannelAPI } from '@/apis/list'

function useTabs() {
  const [channels, setChannels] = useState<ChannelItem[]>([])

  useEffect(() => {
    const getChannels = async () => {
      try {
        const res = await fetchChannelAPI()
        setChannels(res.data.data.channels)
      } catch (error) {
        throw new Error('fetch channel error')
      }
    }
    getChannels()
  }, [])

  return {
    channels,
  }
}

export { useTabs }
javascript 复制代码
//home文件下 index.tsx
import './style.css'
import { Tabs } from 'antd-mobile'
import { useTabs } from './useTabs'
const Home = () => {
  const { channels } = useTabs()
  return (
    <div>
      <div className="tabContainer">
        {/* tab区域 */}
        <Tabs defaultActiveKey={'0'}>
          {channels.map((item) => (
            <Tabs.Tab title={item.name} key={item.id}>
            </Tabs.Tab>
          ))}
        </Tabs>
      </div>
    </div>
  )
}

export default Home
Home模块---List组件实现

实现步骤:

1️⃣搭建基础结构,并获取基础数据

2️⃣为组件设计channelld参数,点击tab时传入不同的参数

3️⃣实现上来加载功能

javascript 复制代码
// home/homeList/index.tsx
import { Image, List } from 'antd-mobile'
// mock数据
// import { users } from './users'
import { useEffect, useState } from 'react'
import { ListRes, fetchListAPI } from '@/apis/list'
type Props = {
  channelId: string
}

const HomeList = (props: Props) => {
  const { channelId } = props
  // 获取列表数据
  const [listRes, setListRes] = useState<ListRes>({
    results: [],
    pre_timestamp: '' + new Date().getTime(),
  })

  useEffect(() => {
    const getList = async () => {
      try {
        const res = await fetchListAPI({
          channel_id: channelId,
          timestamp: '' + new Date().getTime(),
        })
        setListRes({
          results: res.data.data.results,
          pre_timestamp: res.data.data.pre_timestamp,
        })
      } catch (error) {
        throw new Error('fetch list error')
      }
    }
    getList()
  }, [channelId])

 

  return (
    <>
      <List>
        {listRes.results.map((item) => (
          <List.Item
            onClick={() => goToDetail(item.art_id)}
            key={item.art_id}
            prefix={
              <Image
                src={item.cover.images?.[0]}
                style={{ borderRadius: 20 }}
                fit="cover"
                width={40}
                height={40}
              />
            }
            description={item.pubdate}>
            {item.title}
          </List.Item>
        ))}
      </List>
   
    </>
  )
}

export default HomeList
javascript 复制代码
// home/index.tsx
import './style.css'
import { Tabs } from 'antd-mobile'
import { useTabs } from './useTabs'
import HomeList from './HomeList'
const Home = () => {
  const { channels } = useTabs()
  return (
    <div>
      <div className="tabContainer">
        {/* tab区域 */}
        <Tabs defaultActiveKey={'0'}>
          {channels.map((item) => (
            <Tabs.Tab title={item.name} key={item.id}>
              {/* list组件 */}
              {/* 别忘嘞加上类名 严格控制滚动盒子 */}
              <div className="listContainer">
                <HomeList channelId={'' + item.id} />
              </div>
            </Tabs.Tab>
          ))}
        </Tabs>
      </div>
    </div>
  )
}

export default Home
javascript 复制代码
// apis/list.ts
import { http } from '@/utils'

import type { ResType } from './shared'

//  2. 定义具体的接口类型
// 请求文章列表

type ListItem = {
  art_id: string
  title: string
  aut_id: string
  comm_count: number
  pubdate: string
  aut_name: string
  is_top: number
  cover: {
    type: number
    images: string[]
  }
}

export type ListRes = {
  results: ListItem[]
  pre_timestamp: string
}

type ReqParams = {
  channel_id: string
  timestamp: string
}

export function fetchListAPI(params: ReqParams) {
  return http.request<ResType<ListRes>>({
    url: '/articles',
    params,
  })
}
Home模块---List列表无限滚动实现

交互要求:List列表在滑动到底部时,自动加载下一页列表数据

实现思路:

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

javascript 复制代码
<InfiniteScroll>

2️⃣加载下一页数据

pre_timestamp 接口参数

3️⃣把老数据和新数据做拼接处理

...oldList,...newList

4️⃣停止监听边界值

hasMore

javascript 复制代码
// home/homeList/index.tsx
import { Image, List, InfiniteScroll } from 'antd-mobile'
// mock数据
// import { users } from './users'
import { useEffect, useState } from 'react'
import { ListRes, fetchListAPI } from '@/apis/list'
import { useNavigate } from 'react-router-dom'

type Props = {
  channelId: string
}

const HomeList = (props: Props) => {
  const { channelId } = props
  // 获取列表数据
  const [listRes, setListRes] = useState<ListRes>({
    results: [],
    pre_timestamp: '' + new Date().getTime(),
  })

  useEffect(() => {
    const getList = async () => {
      try {
        const res = await fetchListAPI({
          channel_id: channelId,
          timestamp: '' + new Date().getTime(),
        })
        setListRes({
          results: res.data.data.results,
          pre_timestamp: res.data.data.pre_timestamp,
        })
      } catch (error) {
        throw new Error('fetch list error')
      }
    }
    getList()
  }, [channelId])

  // 开关 标记当前是否还有新数据
  // 上拉加载触发的必要条件:1. hasMore = true  2. 小于threshold
  const [hasMore, setHasMore] = useState(true)
  // 加载下一页的函数
  const loadMore = async () => {
    // 编写加载下一页的核心逻辑
    console.log('上拉加载触发了')
    try {
      const res = await fetchListAPI({
        channel_id: channelId,
        timestamp: listRes.pre_timestamp,
      })
      // 拼接新数据 + 存取下一次请求的时间戳
      setListRes({
        results: [...listRes.results, ...res.data.data.results],
        pre_timestamp: res.data.data.pre_timestamp,
      })
      // 停止监听
      if (res.data.data.results.length === 0) {
        setHasMore(false)
      }
    } catch (error) {
      throw new Error('fetch list error')
    }
    // setHasMore(false)
  }



  return (
    <>
      <List>
        {listRes.results.map((item) => (
          <List.Item
            key={item.art_id}
            prefix={
              <Image
                src={item.cover.images?.[0]}
                style={{ borderRadius: 20 }}
                fit="cover"
                width={40}
                height={40}
              />
            }
            description={item.pubdate}>
            {item.title}
          </List.Item>
        ))}
      </List>
      <InfiniteScroll loadMore={loadMore} hasMore={hasMore} threshold={10} />
    </>
  )
}

export default HomeList

8.详情模块-路由跳转&数据渲染

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

1️⃣通过路由跳转方法进行挑战,并传递参数

2️⃣在详情路由下获取参数,并请求数据

3️⃣渲染数据到页面中

javascript 复制代码
// home/homeList/index.tsx
import { Image, List, InfiniteScroll } from 'antd-mobile'
// mock数据
// import { users } from './users'
import { useEffect, useState } from 'react'
import { ListRes, fetchListAPI } from '@/apis/list'
import { useNavigate } from 'react-router-dom'

type Props = {
  channelId: string
}

const HomeList = (props: Props) => {
  const { channelId } = props
  // 获取列表数据
  const [listRes, setListRes] = useState<ListRes>({
    results: [],
    pre_timestamp: '' + new Date().getTime(),
  })

  useEffect(() => {
    const getList = async () => {
      try {
        const res = await fetchListAPI({
          channel_id: channelId,
          timestamp: '' + new Date().getTime(),
        })
        setListRes({
          results: res.data.data.results,
          pre_timestamp: res.data.data.pre_timestamp,
        })
      } catch (error) {
        throw new Error('fetch list error')
      }
    }
    getList()
  }, [channelId])

  // 开关 标记当前是否还有新数据
  // 上拉加载触发的必要条件:1. hasMore = true  2. 小于threshold
  const [hasMore, setHasMore] = useState(true)
  // 加载下一页的函数
  const loadMore = async () => {
    // 编写加载下一页的核心逻辑
    console.log('上拉加载触发了')
    try {
      const res = await fetchListAPI({
        channel_id: channelId,
        timestamp: listRes.pre_timestamp,
      })
      // 拼接新数据 + 存取下一次请求的时间戳
      setListRes({
        results: [...listRes.results, ...res.data.data.results],
        pre_timestamp: res.data.data.pre_timestamp,
      })
      // 停止监听
      if (res.data.data.results.length === 0) {
        setHasMore(false)
      }
    } catch (error) {
      throw new Error('fetch list error')
    }
    // setHasMore(false)
  }

  const navigate = useNavigate()
  const goToDetail = (id: string) => {
    // 路由跳转
    navigate(`/detail?id=${id}`)
  }

  return (
    <>
      <List>
        {listRes.results.map((item) => (
          <List.Item
            onClick={() => goToDetail(item.art_id)}
            key={item.art_id}
            prefix={
              <Image
                src={item.cover.images?.[0]}
                style={{ borderRadius: 20 }}
                fit="cover"
                width={40}
                height={40}
              />
            }
            description={item.pubdate}>
            {item.title}
          </List.Item>
        ))}
      </List>
      <InfiniteScroll loadMore={loadMore} hasMore={hasMore} threshold={10} />
    </>
  )
}

export default HomeList
javascript 复制代码
// apis/detail.ts
import { type ResType } from './shared'
import { http } from '@/utils'
/**
 * 响应数据
 */
export type DetailDataType = {
  /**
   * 文章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
}

export function fetchDetailAPI(id: string) {
  return http.request<ResType<DetailDataType>>({
    url: `/articles/${id}`,
  })
}
javascript 复制代码
// /detail/index.tsx
import { DetailDataType, fetchDetailAPI } from '@/apis/detail'
import { NavBar } from 'antd-mobile'
import { useEffect, useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'

const Detail = () => {
  const [detail, setDetail] = useState<DetailDataType | null>(null)

  // 获取路由参数
  const [params] = useSearchParams()
  const id = params.get('id')
  useEffect(() => {
    const getDetail = async () => {
      try {
        const res = await fetchDetailAPI(id!)
        setDetail(res.data.data)
      } catch (error) {
        throw new Error('fetch detail error')
      }
    }
    getDetail()
  }, [id])

  const navigate = useNavigate()
  const back = () => {
    navigate(-1)
  }

  // 数据返回之前 loading渲染占位

  if (!detail) {
    return <div>this is loading...</div>
  }

  // 数据返回之后 正式渲染的内容
  return (
    <div>
      <NavBar onBack={back}>{detail?.title}</NavBar>
      <div
        dangerouslySetInnerHTML={{
          __html: detail?.content,
        }}></div>
    </div>
  )
}

export default Detail
相关推荐
辻戋1 小时前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保1 小时前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun2 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp2 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.3 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl5 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫7 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友7 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理9 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻9 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js