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
相关推荐
测试界柠檬几秒前
面试真题 | web自动化关闭浏览器,quit()和close()的区别
前端·自动化测试·软件测试·功能测试·程序人生·面试·自动化
多多*2 分钟前
OJ在线评测系统 登录页面开发 前端后端联调实现全栈开发
linux·服务器·前端·ubuntu·docker·前端框架
2301_801074152 分钟前
TypeScript异常处理
前端·javascript·typescript
小阿飞_3 分钟前
报错合计-1
前端
caperxi5 分钟前
前端开发中的防抖与节流
前端·javascript·html
霸气小男5 分钟前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js
susu10830189116 分钟前
前端css样式覆盖
前端·css
学习路上的小刘7 分钟前
vue h5 蓝牙连接 webBluetooth API
前端·javascript·vue.js
&白帝&8 分钟前
vue3常用的组件间通信
前端·javascript·vue.js
小白小白从不日白19 分钟前
react 组件通讯
前端·react.js