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

1.初始化项目

//搭建项目
npm create vite@latest react-jike-mobile -- --template react-ts
//安装依赖
npm i 
//运行
npm run dev

在这里插入图片描述

清理项目目录结构

在这里插入图片描述

安装ant design mobile

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

npm install --save antd-mobile
测试组件
import {
    Button } from 'antd-mobile'

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

export default App

2.初始化路由

react的路由初始化,采用react-router-dom进行配置
在这里插入图片描述

npm i react-router-dom

3. 配置基础路由

//List页面
const List = () => {
   
  return <div>this is List</div>
}

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

export default Detail
//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
//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配置

//修改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类型包

npm i @types/node -D

3️⃣修改tsconfig.json文件

{
   
  "baseUrl": ".",
  "paths": {
   
    "@/*": [
      "src/*"
    ]
  },
}

5. 安装axios

1.安装axios到项目
2.在utils中封装http模块,主要包括接口基地址、超时时间、拦截器
3.在utils中做统一导出

//安装axios
npm i axios
// 封装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 }

//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泛型的参数类型
在这里插入图片描述
在这里插入图片描述

//apis文件下shared.ts
// 1. 定义泛型
export type ResType<T> = {
   
  message: string
  data: T
}

//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',
  })
}

页面使用

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函数,消费其返回的状态和方法

//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 }

//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️⃣实现上来加载功能

// 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

// 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

// 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️⃣滑动到底部触发加载下一页动作

<InfiniteScroll>

2️⃣加载下一页数据
pre_timestamp 接口参数

3️⃣把老数据和新数据做拼接处理
[…oldList,…newList]

4️⃣停止监听边界值
hasMore

// 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️⃣渲染数据到页面中

在这里插入图片描述

// 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

// 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}`,
  })
}

// /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. vue 项目实现

    2024-02-04 18:26:01       55 阅读
  2. uniapp向上刷新

    2024-02-04 18:26:01       71 阅读
  3. element-ui select 框做成更多

    2024-02-04 18:26:01       37 阅读
  4. 微信scroll-view小程序实现上刷新

    2024-02-04 18:26:01       45 阅读
  5. 微信小程序 上列表刷新

    2024-02-04 18:26:01       62 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-02-04 18:26:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-02-04 18:26:01       100 阅读
  3. 在Django里面运行非项目文件

    2024-02-04 18:26:01       82 阅读
  4. Python语言-面向对象

    2024-02-04 18:26:01       91 阅读

热门阅读

  1. Libvirt 迁移标志详解

    2024-02-04 18:26:01       46 阅读
  2. 什么是epoll机制

    2024-02-04 18:26:01       59 阅读
  3. scoped样式隔离原理

    2024-02-04 18:26:01       44 阅读
  4. eCos GPIO读写及其中断处理

    2024-02-04 18:26:01       52 阅读
  5. Relation-graph关系图/流程图,VUE项目基础使用

    2024-02-04 18:26:01       70 阅读
  6. 事件在状态流程图中的工作方式

    2024-02-04 18:26:01       57 阅读
  7. 类银河恶魔城学习记录1-6 Flip基本设置源代码 P33

    2024-02-04 18:26:01       48 阅读
  8. 线阵相机系列-- 1. 什么是线阵相机

    2024-02-04 18:26:01       48 阅读
  9. 【BBF系列协议】TR181-2 TR369的设备数据模型

    2024-02-04 18:26:01       52 阅读
  10. 进程任务通信3种方式

    2024-02-04 18:26:01       58 阅读
  11. 开源计算机视觉库OpenCV详解和实际运用案例

    2024-02-04 18:26:01       51 阅读
  12. windows下docker的使用

    2024-02-04 18:26:01       50 阅读