React-Hooks + Redux 实战——从拿取数据到功能实现带你打造属于你自己的米游社

我正在参加「创意开发 投稿大赛」详情请看:掘金创意开发大赛来了!

前言

前两周写了个米游社的小demo,最近学习了Redux,加之对米游社的首页挺感兴趣(coser她们不美吗) ,所以就诞生了这个米游社的另一个demo,使用React-Hooks + Redux完善了更为完整的米游社。 上成果:

演示.gif 进入正题。

准备阶段

使用工具

vite: 脚手架,初始化react项目
redux: 状态管理
redux-thunk: 处理异步逻辑的redux中间件
styled-components: css in js,吸顶之后的样式改变大部分依靠他,官方文档
classnames: 动态类名,官方文档
axios: 请求后端api数据,中文文档
ahooks:一套高质量可靠的 React Hooks 库,会用到其中的useScroll,监听滚动,制作吸顶,官方文档
react-photo-view:实现图片预览,官方文档

1. 数据获取

1.1 方式

  • 数据量庞大,自己mock显然不理智,还是借用一下官方的接口比较好。

1.2 问题

  • 米游社的内容集中在移动端,而在App端获取数据无非就是打开虚拟机打开小黄鸟(HttpCanary一个移动端抓包工具)) 一波操作,打开米游社,向上一滑,qq_pic_merged_1658118418491.jpg好家伙,寄,面具中刷了相关校验模块,小黄鸟的证书也添加到了信任区,SSL证书校验依旧寄了,思来想去各种求解,在社区得到的答案是反编译一下,我总不可能为了个接口数据去对App进行反编译吧,就算法律允许,我也没那技术啊!菜.png

1.3 解决方法

  • 移动端我获取不了这不还有网页端嘛,数据大差不差,熟练的打开米游社网页F12

调试.png

  • 这熟悉的面板,可比移动端的好解决多了,很常见的反爬措施,断点死循环,停用断点调试,恢复脚本执行就可以正常访问了。

1.4 拿取数据

  • 调试面板点击网络,筛选Fetch/XHR ,剩下的那些就是我们需要的接口,一个个右键打开查看,找到需要的。

2. 数据分析及处理

数据分析

篇幅太长,就不在这里展示了,感兴趣可到我的仓库readme:查看

UNIX 时间戳处理

米有社api 返回的数据其中时间很多都是用的UNIX 时间戳的格式,而显示到页面上的却是几小时前,超过一天的显示日期,超过一年的显示年份,所以这里我在utils下写了个工具函数对其进行处理:

// 时间戳转日期
const getGMT = function (dateTime) {
    if (dateTime === null) {
        return '';
    }
    let date = new Date(parseInt(dateTime) * 1000);
    let now = new Date().getTime();
    let second = Math.floor((now - date) / 1000);
    let minute = Math.floor(second / 60);
    let hour = Math.floor(minute / 60);
    let day = Math.floor(hour / 24);
    let month = Math.floor(day / 31);
    let year = Math.floor(month / 12);
    let Year = date.getFullYear();
    let Moth = date.getMonth() + 1 < 10 ? `0${date.getMonth() + 1}` : date.getMonth() + 1;
    let Day = date.getDate() < 10 ? `0${date.getDate()}` : date.getDate();
    if (year > 0) {
        return `${Year}-${Moth}-${Day}`;
    }else if (day > 0) {
        return `${Moth}-${Day}`;
    } else if (hour > 0) {
        return hour + '小时前';
    } else if (minute > 0) {
        return minute + '分钟前';
    } else if (second > 0) {
        return second + '秒前';
    } else {
        return '刚刚';
    }
};
复制代码

3. 跨域处理

  • 因为用了官方的接口,和本地开发环境不在同一个域名和端口下,触发了浏览器的同源策略限制,所以需要对项目做跨域处理
  • 本次项目使用了vite 作为脚手架,所以只需要在vite 的配置文件下添加如下代码:
  server: {
    "proxy": {
      "/api": {
        "target": "https://bbs-api.mihoyo.com",
        "changeOrigin": true,
        "pathRewrite": {
          "^/api": ""
        }
      }
    },
复制代码

将官方的接口地址代理到本地即可。

4. axios 封装数据请求

  • axios 官方文档写的相当详细,这里就分享下我的大概配置:
// 配置请求对象
import axios from 'axios';
export const baseUrl = 'http://localhost:3000';
const axiosInstance = axios.create({
    baseURL: baseUrl,
    timeout: 5000,
});

// 拦截器
 axiosInstance.interceptors.request.use(
     req => {
        ...
     },
     err => {
        ...
     }
 )
axiosInstance.interceptors.response.use(
    res => {
        ...
    },
    err => {
        ...
    }
)

export { axiosInstance };
复制代码

请求拦截和相应拦截中写入自己的成功和失败的操作即可。

5. 给项目添加 redux

从数据分析可以看见,这次官方接口返回的数据量是相当多的,数据量大了,项目趋于复杂,很多数据是需要在组件之间共享的,所以还是请上我们的redux

5.1 创建store

  • src目录下创建一个store文件夹,然后在文件夹下创建一个index.js文件和reducer.js文件。 (这里是store相当于总仓 汇总多个reducer 合并子仓的store
  1. index.js文件 (创建整个项目的store)
// 组件 - 中间件redux-thunk - 数据
import { createStore, compose, applyMiddleware } from 'redux';
import thunk from 'redux-thunk'; // 支持异步数据管理(接口请求)
import reducer from './reducer';

// 这里是启用Redux DevTools,我用的官方文档的第一种方式
// 也可下载插件,那样代码更优雅,我懒,就先这样用着
// compose 合并中间件
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

// 创建仓库管理数据流 createStore
const store = createStore(reducer,
    composeEnhancers(
        // 异步
        applyMiddleware(thunk)
    )
    )

export default store;
复制代码
  1. reducer.js文件 (集合各个子数据仓库)
import { combineReducers } from "redux";
import { reducer as homeReducer } from "@/pages/Home/store/index";
import { reducer as yuanshenReducer } from "@/pages/Home/Yuanshen/store/index";
import { reducer as dabieyeReducer } from "@/pages/Home/Dabieye/store/index";
import { reducer as searchReducer } from "@/pages/Search/store/index";
import { reducer as selectReducer } from "@/pages/SelectChannel/store/index";
...
// 引入并合并分仓
export default combineReducers({
    home: homeReducer,
    yuanshen: yuanshenReducer,
    dabieye: dabieyeReducer,
    search: searchReducer,
    select: selectReducer
    ...
})
复制代码

5.2 首页main.jsx配置

main.jsx作为容器组件,引入Provider 组件 声明式引入数据管理功能

import { Provider } from 'react-redux'
import store from './store'

ReactDOM.createRoot(document.getElementById('root')).render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>
)
复制代码

页面级别组件下创建分仓store

5.2.1 创建分仓

页面级别组件创建自己的自仓,方便管理每个页面的状态和数据,store下分别创建四个文件:

store.png

  • index.js文件为子仓库的核心,暴露子仓库的reduceractionCreatorsconstants
import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as constants from './constants';

export { 
    reducer, 
    actionCreators, 
    constants 
}
复制代码
  • reducer.js 文件是redux最关键的地方,操作数据在这里进行,给状态设置一个默认值,没有发生改变时,redux会把这个默认值传给组件,一旦有状态发生改变,接收action,去匹配相应的type并执行相应的操作,同步给组件,完成MVVM操作
    这里需要注意Reducer 里只能接受state,不能改变state,所以一般在更改数据时都是运用以下方式:
// 克隆
let newState = JSON.parse(JSON.stringify(state));
// 或者使用对象展开运算符
case actionTypes.GET_LIST:
     return {
         ...state,
         List: action.data
     }
// 又或者使用Object.assign() 创建一个副本
case actionTypes.CHECK_LIST:
    // state 旧状态  保全
    let checkList = state.list
    // 新状态
    // 必须把第一个参数设置为空对象,因为它会改变第一个参数的值
    return Object.assign({}, state, {list:[...checkList]})
复制代码
  • actionCreators.js这里暴露相应的函数,数据通过dispatch action来更新,拉取数据
import * as actionTypes from './constants'
import {
    getGameListRequest,
} from '@/api/request'

// 获取游戏分区列表
const changeGameList = (data) => ({
    type: actionTypes.SET_GAME_LIST,
    data
})
export const getGameList = () => {
    return (dispatch) =>{
        getGameListRequest().then(data => {
            let list = data.data.list
            dispatch(changeGameList(list))
        })
    }
}
复制代码
  • contains.js 这里是配置文件 给type取一个别名(常量),方便引用
export const SET_GAME_LIST = 'SET_GAME_LIST'
复制代码

5.2.2 页面连接仓库

import React, { useEffect } from 'react'
import { connect } from 'react-redux'
import {
    getGameList,
} from './store/actionCreators'

const Home = (props) => {
    // 解构出全局的state和dispatch
    const {
        gameList,
    } = props

    const {
        getGameListDispatch,
    } = props
    // dispatch
    useEffect(() => {
        getGameListDispatch();
    },[])
    
    return (
        ...
    )
}
// 映射Redux全局的state到组件的props上
const mapStateToProps = (state) => {
    return {
        gameList: state.home.gameList,
    }
}
// 映射dispatch到props上
const mapDispatchToProps = (dispatch) => {
    return {
        getGameListDispatch(){
            dispatch(getGameList())
        },
    }
}
// 将ui组件包装成容器组件
export default connect(mapStateToProps, mapDispatchToProps)(Home)
复制代码

5.2.3 Redux使用顺序

按照: 声明常量 -> 编写action -> 编写reducer -> 组件使用,的顺序来进行

6. 首页

6.1 页面分析

打开米游社,可以看到米游社首页的页面组件和布局几乎一模一样,所以首页的组件可以全部拆分成公共组件,高度复用,看看官方页面: Screenshot_2022-07-20-23-52-01-426_com.mihoyo.hyp.jpg

可以看到官方的页面大体可以分为以下几个部分:

  1. 底部导航栏,一级路由
  2. Home顶部Tab导航栏,二级路由
  3. 活动导航List组件
  4. 套路区组件
  5. 官方资讯组件
  6. 推荐文章组件(含轮播图)

6.2 底部导航栏及路由设计

6.2.1 路由

直接上代码,没什么好多说的:

import { lazy, Suspense } from 'react'
import { Routes, Route, Navigate } from 'react-router-dom';
import Home from '@/pages/Home';
const Dynamic = lazy(() => import('@/pages/Dynamic'))
const Information = lazy(() => import('@/pages/Information'))
const Mypage = lazy(() => import('@/pages/Mypage'))
const Search = lazy(() => import('@/pages/Search'))
const SelectChannel = lazy(() => import('@/pages/SelectChannel'))
const Yuanshen = lazy(() => import('@/pages/Home/Yuanshen'))
const Dabieye = lazy(() => import('@/pages/Home/Dabieye'))

const RouterConfig = ({gamename}) => {
    return (
        <Suspense fallback={null}>
        <Routes>
            <Route path="/" element={<Navigate to={`/home/${gamename}`}/>} replace={true} />
            <Route path="/home" element={<Navigate to={`/home/${gamename}`}/>} replace={true} />
            <Route path="/dynamic" element={<Dynamic />} />
            <Route path="/information" element={<Information />} />
            <Route path="/mypage" element={<Mypage />} />
            <Route path="/search" element={<Search />} />
            <Route path="/select" element={<SelectChannel/>} />
            <Route path="/home" element={<Home />} >
                <Route path="/home/yuanshen" element={<Yuanshen/>} />
                <Route path="/home/dabieye" element={<Dabieye/>} />
            </Route>
        </Routes>
        </Suspense>
    );
};

export default RouterConfig;
复制代码

6.2.2 底部导航栏

导航栏每个图标为一级路由,中间的添加按钮,弹出层使用了antd-mobile 中的Popup,代码有点长,看这儿,效果:

路由.gif

6.2.3 顶部导航栏

顶部导航栏中的数据是变动的,因为每一个页面都是一个游戏的资讯页,所以我将其每一个都作为了二级路由,并给了每一个二级路由页面一个分仓store 方便管理数据(这里也可以不用设置分仓,可以直接使用主页的store,但是每一个二级路由也是页面级别,而且内部组件也有细微区别,所以我还是给了每个二级路由一个store),部分代码:

    const SelectTop = () => {
        return (
            <SelectItem searchHidden={searchHidden}>
                <div className="swiper-container">
                        <div className="swiper-wrapper">
                {
                    data.map((item) => {
                        if(item.has_wiki || item.en_name == 'dby'){
                            return(
                                <div key={item.id} className="swiper-slide">
                                    <NavLink to={`/home/${selectGame(item)}`}>
                                        <span>
                                            {item.name}
                                        </span>
                                    </NavLink>
                                </div>
                            )
                        }
                    })
                }
                    </div>
                </div>
            </SelectItem>
        )
    }
        return (
        <Wrapper>
            ...
            <Outlet />
        </Wrapper>
    )
复制代码

完整的看这儿

6.3 吸顶

先看看效果

chrome-capture-2022-6-17.gif

  • 一共两个吸顶,分别是最上方的二级路由以及下方页面中的讨论区,都要做到吸顶后改变样式。

ahooks + useRef + styled-components

  • 吸顶的实现我最开始是想的找一个现成的库或者使用css的sticky,可是前者找来找去发现github 里面知名的那几个都已经几年没维护了,后者又有兼容性问题,无奈还是得自己来。
  • 我这里使用的是ahooks + useRef 来完成的,利用ahooks中的useScroll 监听屏幕滚动,Ref 来获取DOM 元素,当组件超出范围时吸顶

导航栏代码如下:

    // searchHidden 传给样式组件用,实现吸顶时改变字体颜色
    const [searchHidden, setSearchHidden] = useState(false);
    const searchRef = useRef(null)
    const scroll = useScroll()
    // 监听屏幕滚动,超出顶部,组件吸顶
    useEffect(() => {
        if(scroll && scroll.top > 0){
            if(!searchHidden && searchRef.current){
                setSearchHidden(true);
                searchRef.current.style.position = 'fixed'
                searchRef.current.style.backgroundColor ='white'
                searchRef.current.style.zIndex = '9999'
                searchRef.current.style.opacity = '0.5'
            }
        }else {
            if(searchHidden && searchRef.current){
                searchRef.current.style = ''
                setSearchHidden(false);
            }
        }
        if(scroll && scroll.top > 100){
            searchRef.current.style.backgroundColor ='rgb(242, 243, 244)'
            searchRef.current.style.opacity = '1'
        }
    }, [JSON.stringify(scroll)])
    
    // 传递参数给样式组件
    <SelectItem searchHidden={searchHidden}>
复制代码
  • 使用组件中传入的searchHidden 在样式组件中实现吸顶之后的样式改变:
            &.active {
                font-weight: 500;
                font-size: 1rem;
                color: ${props => (props.searchHidden ? 'black' : 'white')};
            }
复制代码

讨论区代码如下:

    const [searchHidden, setSearchHidden] = useState(false);
    const searchRef = useRef(null)
    const scroll = useScroll()

        // 监听屏幕滚动,超出顶部,组件吸顶
        useEffect(() => {
            if(scroll && scroll.top > 144){
                if(!searchHidden && searchRef.current){
                    setSearchHidden(true);
                    searchRef.current.style.position = 'fixed'
                    searchRef.current.style.backgroundColor ='white'
                    searchRef.current.style.marginTop = '-2.98rem'
                    searchRef.current.style.zIndex = '9999'
                }
            }else {
                if(searchHidden && searchRef.current){
                    searchRef.current.style = ''
                    setSearchHidden(false);
                }
            }
        }, [JSON.stringify(scroll)])
复制代码
  • 样式组件
    height: ${props => (props.searchHidden ? '2rem' : '4rem')};
    display: flex;
    align-items: center;
    justify-content: center;
    a{
        position: relative;
        display: inline-block;
        display: flex;
        align-items: center;
        justify-content: center;
        width: ${props => (props.searchHidden ? '100%' : '90%')};
        height: ${props => (props.searchHidden ? '2rem' : '3rem')};
        color: black;
        background: white;
        border: 1px solid rgba(0,0,0,0.05);
        border-radius: ${props => (props.searchHidden ? '0' : '8px')};
        img {
            position: absolute;
            height: ${props => (props.searchHidden ? '1.3rem' : '1.5rem')};
            width: ${props => (props.searchHidden ? '1.3rem' : '1.5rem')};
            margin-left: ${props => (props.searchHidden ? '-16rem' : '-14rem')};
        }
        >p {
            position: absolute;
            font-size: 0.6rem;
            margin-right: ${props => (props.searchHidden ? '-14rem' : '-12.5rem')};
        }
    }
复制代码
  • 可以看到,这里我大量的使用了styled-components 的css in js 的特性,像写js 一样写css

6.4 资讯栏组件

老规矩先上效果

下拉刷新.gif

下拉刷新

下拉刷新使用了antd-mobile 中的 PullToRefresh 组件,现成的轮子,可以自定义下拉时的显示内容,每次下拉触发函数重新拉取数据:

    async function doRefresh() {
        await sleep(1000);
        // dispatch 拉取数据
        getOfficialListDispatch(2);
        getCarouselsListDispatch(2);
        Toast.show({
            content: '推荐已更新'
        })
    }

    return (
         <PullToRefresh
             onRefresh={doRefresh}
             refreshingText={<DotLoading color='#2df4fe'/>}
             completeText={ <h3>&nbsp;&nbsp;</h3>}
         >
           ...
         </PullToRefresh>
    )
复制代码

资讯数据处理

米游社的api 返回的资讯数据是固定的,不管刷新多少次固定的时间段都是那些,所以要实现每次下拉更新需要对返回的数组进行处理,这里我写了个小工具函数:

//截取打乱后的数组的前num 位
const getRandomArr = (arr, num) => {
    //打乱数组顺序
    const getArrRandomly = arr => {
        let len = arr.length;
        for (let i = len - 1; i >= 0; i--) {
            let randomIndex = Math.floor(Math.random() * (i + 1));
            let itemIndex = arr[randomIndex];
            arr[randomIndex] = arr[i];
            arr[i] = itemIndex;
        }
        return arr;
    };

    const tmpArr = getArrRandomly(arr);
    let arrList = [];
    for (let i = 0; i < num; i++) {
        arrList.push(tmpArr[i]);
    }
    return arrList;
};
复制代码

6.5 推荐文章组件

推荐文章.gif

6.5.1 整体布局

米游社推荐文章页面api 一页返回二十个数据,通过api 返回数据中的top 字段判断是否置顶,没有指定就切割前两个显示在最上方,下面插入轮播图滑动展示组件,切割剩余的文章继续在轮播图下面展示

    const frontPost = suggestPostList.slice(0,2)
    const lastPost = suggestPostList.slice(2)
复制代码

6.5.2 作者文章信息栏及弹出层

在数据方面头像和头像框,发布时间和作者信息都有现成数据,这里就变成简单的切图了,下方的标题和内容预览也是切图,这里主要分享下内容显示两行多余省略的写法:

        .post_content {
            display: block;
            font-size: 0.7rem;
            color: #482929;
            letter-spacing: 0;
            line-height: 1.1rem;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: normal;
            display: -webkit-box;
            -webkit-box-orient: vertical;
            -webkit-line-clamp: 2;
            opacity: 0.5;
        }
复制代码

这对非谷歌内核的浏览器可能有兼容问题,我的知识量也没想到更好的办法,有更好方法的小伙伴可以分享下。
弹出层用了和底部导航栏的处理方法一样用了antd-mobile的Popup

6.5.3 图片布局

米游社api 返回的图片是多样的,封面图、内容图、长图,的显示都不一样,仔细翻看米游社发现他也没做到真正的适配每一张图的内容,所以这里就对其进行了一个简单的判断,图片以背景的形式呈现方便布局,大致如下:

            <div className="cover_container">
                // 是否有封面图,有则展示,并将封面图数据传给样式组件
                {
                    Post.post.cover && 
                    <CoverImg 
                        coverUrl={Post.post.cover}
                        viewType={Post.post.view_type}
                        />
                }
                // 没有封面时,是否有内容图,有就展示
                {
                Post.post.images && !Post.post.cover &&
                Post.post.images.map((item, index) =>(
                        <ImageItem imgUrl={item} key={index}/>
                        ))
                }
                {
                    Post.post.images && !Post.post.cover &&
                    <div className="label">
                        <i className='iconfont icon-gengduotupian'></i>
                        +{Post.post.images.length}
                    </div>
                }
            </div>
复制代码

然后再在css 中进行一些处理,比如判断是否是长图片,根据判断显示不同样式,再次吹一波styled-components,部分代码如下:

export const CoverImg = styled.div`
    /* 图片以背景的形式呈现 */
    background-image: url(${props => props.coverUrl});
    /* 判断是否是长图片,根据判断显示不同样式 */
    width: ${props => (props.viewType == 1 ? '100%' : '10rem')};
    height: ${props => (props.viewType == 1 ? '10rem' : '12rem')};
    background-size: cover;
    background-repeat: no-repeat;
    background-position: 50% 38.2%;
    border-radius: 0.2rem;
`;

export const ImageItem = styled.div`
    background-image: url(${props => props.imgUrl});
    height: 8.7rem;
    width: 8.7rem;
    background-size: cover;
    background-position: 50% 38.2%;
    display: inline-block;
    margin-right: 0.5rem;
    border-radius: 0.2rem;
    vertical-align: top;
    position: relative;
`;
复制代码

6.5.4 图片预览

使用后效果如下

图片预览2.gif

  • 使用相当便捷,只需要下载后在入口引入,在需要的地方用PhotoProvider包裹图片组件,以 PhotoProvider为界限,里面所有的 PhotoView 图片会按照运行顺序提取为一组图片预览画廊。当某个 <img /> 被点击,则会定位到指定的图片并打开预览,所加入图片预览后上面的代码变成了下面这样:
            <PhotoProvider>
            <div className="cover_container">
                {
                    Post.post.cover && 
                    <PhotoView src={Post.post.cover}>
                    <CoverImg 
                        coverUrl={Post.post.cover}
                        viewType={Post.post.view_type}
                        />
                    </PhotoView>
                }
                {
                Post.post.images && !Post.post.cover &&
                Post.post.images.map((item, index) =>(
                        <PhotoView key={index} src={item}>
                        <ImageItem imgUrl={item} />
                        </PhotoView>
                        ))
                }
                {
                    Post.post.images && !Post.post.cover &&
                    <div className="label">
                        <i className='iconfont icon-gengduotupian'></i>
                        +{Post.post.images.length}
                    </div>
                }
            </div>
            </PhotoProvider>
复制代码

开箱即用,真的便捷好用。

6.5.5 轮播图

轮播图使用了swiper,不自动轮播,不循环,这里就直接写在了推荐文章的组件里,封装成一个函数

    let swiper = null;
    useEffect(() => {
        if(swiper) return;
        swiper = new Swiper('.swiper-container',{
            observer: true,
            observerParants: true,
            slidesPerView : 'auto',
            freeMode: {
                enabled: true,
            },
        })
    },[])
    const carousels = () => {
        return (
            <SwiperItem>
                <div className="swiper-container mySwiper">
                    <div className="swiper-wrapper">
                        {
                        carouselsList.map((item,index) =>
                            <div key={index} className="swiper-slide">
                                <img src={item.cover} />
                            </div>
                        )
                        }
                    </div>
                </div>
            </SwiperItem>
        )
    }
复制代码

7. 搜索

搜索页面参考了神三元大佬的云音悦项目,Search 组件嵌套Search-box 组件,实现效果如下:

搜索1.gif Search-box 组件实现输入数据并对数据进行防抖处理,将输入的值传入父组件,父组件根据传过来的值dispatch并对返回的搜索结果进行渲染。 搜索2.gif 使用useRef 在操作清除按钮和进入页面后,输入框中没有数据自动聚焦。 这里就不放代码了,具体的实现代码可以到三元大佬的云音悦项目中查看,或者到我的仓库,查看简化版。

8. 性能优化

memo

-   import { memo } from 'react'
-   export default memo(xxxx)
-   实现减少未变数据重复渲染
复制代码

useMemo

也是从三元大佬那里学来的 useMemo 可以缓存 上一次函数计算的结果,搜索的防抖就是放那里面,当handleQuery 发生改变了才重新计算

    let handleQueryDebounce = useMemo(() => {
        return debounce(handleQuery, 500)
    },[handleQuery])
复制代码

lazyLoad 懒加载

还是三元大佬那里学来的,大概用法:

<LazyLoad
     // 占位图片
     placeholder={<img
                  src={placeholderImg} 
                  className='m-bfs-pic pic'
                  />}
     >
     <img src={pic} 
          className={classnames("m-bfs-pic pic", { notfond: !pic })} />
</LazyLoad>
复制代码
  • 路由懒加载

    • import { lazy, Suspense } from "react"
    • const XXX = lazy(() => import('@/pages/XXX'))
    const Dynamic = lazy(() => import('@/pages/Dynamic')) 
    <Suspense fallback={null}> 
    <Routes>
        <Route path="/dynamic" element={<Dynamic />} />
    </Routes> 
    </Suspense>
    复制代码

最后

遗憾

  • 有点赶,首页频道设置的功能,也就是上篇文章的功能还没完整迁移过来
  • 文章互动的数据拿不到,米游社的文章和文章互动数据(点赞,评论,收藏数)是分两个不同的接口传过来的,但是当我筛选出文章id 通过axios 去拿数据却一直404,试了很多方法,依旧拿不到,很是遗憾
  • 二级路由切换时改变背景延迟太高,相信看了效果的小伙伴也注意到了,当从原神切换到大别野或者其他游戏的时候其他数据都到了,首页的背景数据还没到,而这个背景是写在home 的页面中去拿取并设置的,在其他页面也得不到这种显示效果,但是延迟就是高,没找到方法解决

最后的最后
这个项目还将继续完善(毕竟还有那么多)东西没写,有问题和可优化点,欢迎大佬评论区指正

猜你喜欢

转载自juejin.im/post/7122728199535984677
今日推荐