React-Hooks + Redux combat - from data acquisition to function implementation, it will lead you to build your own Miyou Club

I am participating in the "Creative Development Contest" for details, please see: The Nuggets Creative Development Contest is here!

foreword

I wrote a small demo of Miyou Club two weeks ago. I recently learned Redux, and I am very interested in the homepage of Miyou Club (Aren’t coser beautiful), so another demo of this Miyou Club was born, using React-Hooks + Redux completes a more complete Miyou Club. The results above:

demo.gifTo the point.

preparation stage

use tools

vite: Scaffolding, initializing react projects
redux: State management
redux-thunk: Redux middleware that handles asynchronous logic
styled-components: css in js, most of the style changes after ceiling suction depend on him, official document
classnames : dynamic class name, official document
axios : request back-end api data, Chinese Documentation
ahooks : A set of high-quality and reliable React Hooks libraries, which will be used to useScrollmonitor scrolling, make ceilings, official documents
react-photo-view : realize image preview, official documents

1. Data acquisition

1.1 Method

  • The amount of data is huge, and it is obviously irrational to mock it yourself. It is better to borrow the official interface.

1.2 Question

  • The content of Miyoushe is concentrated on the mobile terminal, and getting data on the App side is nothing more than opening the virtual machine and opening the little yellow bird (HttpCanary is a mobile terminal packet capture tool)) One wave of operations, open Miyoushe, swipe up, qq_pic_merged_1658118418491.jpggood guy , sent, the relevant verification module was brushed in the mask, the certificate of the little yellow bird was also added to the trust zone, and the SSL certificate verification was still sent. After thinking about various solutions, the answer I got in the community was to decompile it. It's impossible to decompile the app for the sake of interface data. Even if the law allows it, I don't have that technology!Dish.png

1.3 Solution

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

debug.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,代码有点长,看这儿,效果:

routing.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>
    复制代码

最后

遗憾

  • A bit rushed, the function of the home page channel setting, that is, the function of the previous article , has not been completely migrated
  • The data of article interaction is not available. Miyoushe's articles and article interaction data (likes, comments, and favorites) are sent through two different interfaces, but when I filter out the article id and get the data through axios I've been getting 404 all the time, I've tried many methods, but I still can't get it, it's a pity
  • The delay of changing the background when switching the secondary route is too high. I believe that the friends who have seen the effect have also noticed that when switching from Genshin Impact to Dabieye or other games, other data has arrived, and the background data of the home page has not arrived yet, and This background is written in the home page to get and set, and this display effect cannot be obtained on other pages, but the delay is high, and no solution has been found.

Finally,
this project will continue to improve (after all, there are so many) things that have not been written, there are problems and can be optimized, welcome corrections in the comment area

Guess you like

Origin juejin.im/post/7122728199535984677