React知乎日报项目(二)

首页和详情页

结构->样式->功能

首页头部时间、首页轮播图、首页新闻列表数据都需要向服务器发送请求,从服务器获取,因而首页要设置三个状态。

首页头部

views/Home.jsx:在首页引入头部组件

import React, { useState } from "react";
import HomeHead from "../components/HomeHead";
import _ from '../assets/utils'

const Home = function Home() {
    //创建所需状态
    let [today, setToday] = useState(_.formatTime(null, '{0}{1}{2}'))
    return <div className="home-box">
        {/* 头部 */}
        <HomeHead today={today} />
    </div>
}
export default Home

在JSX视图中,想直接导入静态资源图片,不能使用相对地址:
<img. src="../assets/images/timg.jpg" alt="" />
因为经过webpack打包处理,项目的结构目录是改变的:

在这里插入图片描述

但是JSX中的导入图片的地址,还是我们写的相对地址:
在这里插入图片描述
这样找不到图片。

但是如果是在CSS样式中,我们使用图片,是可以使用相对地址的:因为webpack打包的时候,会处理CSS中的图片导入:

  1. 把需要的图片进行打包
  2. 把打包后的地址重新覆盖CSS中写的地址(jsx缺)

在视图中使用图片,我们该如何处理呢?
方法一:使用绝对地址
<img. src="http://……" alt="" />图片部署到服务器了,即便webpack不处理,打包后,也是从服务器获取图片渲染

方法二:基于ES6 Module模块方式,导入图片(如下views/HomeHead.jsx使用)webpack将对图片进行打包处理,将打包后的地址赋值给src

在这里插入图片描述


结构
import React from "react";
import timg from '../assets/images/timg.jpg'
import './HomeHead.less'

const HomeHead = function HomeHead() {
    return <header className="home-head-box">
        <div className="info">
            <div className="time">
                <span>25</span>
                <span>十月</span>
            </div>
            <h2 className="title">知乎日报</h2>
        </div>
        <div className="picture">
            <img src={timg} alt="" />
        </div>
    </header>
}
export default HomeHead
样式
.home-head-box {
    box-sizing: border-box;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 15px 30px;

    .info {
        display: flex;
        align-items: center;

        .time {
            padding-right: 30px;
            height: 70px;

            span {
                display: block;
                text-align: center;
                line-height: 35px;
                font-size: 24px;

                &:nth-child(1) {
                    font-size: 32px;
                }
            }
        }

        .title {
            padding-left: 24px;
            height: 64px;
            line-height: 64px;
            font-size: 40px;
            border-left: 2px solid #EEE;
        }
    }

    .picture {
        width: 64px;
        height: 64px;
        border-radius: 50%;
        overflow: hidden;

        img {
            display: block;
            width: 100%;
            height: 100%;
        }
    }
}

在这里插入图片描述

功能:计算时间中的月和日:

在这里插入图片描述

import React, { useMemo } from "react";
import timg from '../assets/images/timg.jpg'
import './HomeHead.less'

const HomeHead = function HomeHead(props) {
    let { today } = props // 复用性强的组件做一下属性规则校验,否则没必要
    let time = useMemo(() => {
        let [, month, day] = today.match(/^\d{4}(\d{2})(\d{2})$/),
            area = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
        return {
            month: area[+month],
            day
        }
    }, [today])
    return <header className="home-head-box">
        <div className="info">
            <div className="time">
                <span>{time.day}</span>
                <span>{time.month}</span>
            </div>
            <h2 className="title">知乎日报</h2>
        </div>
        <div className="picture">
            <img src={timg} alt="" />
        </div>
    </header>
}
export default HomeHead

在这里插入图片描述

首页轮播图

结构
...
import './Home.less'
import { Swiper } from "antd-mobile";
import { Link } from "react-router-dom";

const Home = function Home() {
    /* 创建所需状态 */
    let [today, setToday] = useState(_.formatTime(null, '{0}{1}{2}'))
    return <div className="home-box">
        {/* 头部 */}
        <HomeHead today={today} />
        {/* 轮播图 */}
        <div className="swiper-box">
            <Swiper autoplay={false} loop={true}>
                <Swiper.Item>
                    <Link to='/detail/xxx'>
                        <img src="https://img0.baidu.com/it/u=4162443464,2854908495&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500" alt="" />
                        <div className="desc">
                            <h3 className="title">在未来,沙漠有可能被改造为适合人类居住的环境吗?</h3>
                            <p className="hint">作者 / Peter</p>
                        </div>
                    </Link>
                </Swiper.Item>
            </Swiper>
        </div>
    </div>
}
export default Home
样式

样式:嵌套的层级样式类名太多css渲染会影响性能,能实现就好了,尽量不要嵌套太深。

.home-box {
    .swiper-box {
        box-sizing: border-box;
        width: 750px;
        height: 750px;
        background: #DDD;

        .adm-swiper {
            height: 100%;

            a {
                display: block;
                height: 100%;

                img {
                    display: block;
                    width: 100%;
                    height: 100%;
                }

                .desc {
                    position: absolute;
                    left: 0;
                    bottom: 40px;
                    box-sizing: border-box;
                    padding: 0 36px;
                    width: 100%;

                    .title {
                        font-size: 36px;
                        color: #FFF;
                    }

                    .hint {
                        line-height: 60px;
                        font-size: 24px;
                        color: rgba(255, 255, 255, .7);
                    }
                }
            }

            .adm-swiper-indicator {
                bottom: 20px;
                left: auto;
                right: 0;
                transform: translateX(-30px);

                .adm-page-indicator-dot {
                    margin: 0 6px;
                    width: 12px;
                    height: 12px;
                    border-radius: 6px;
                    background: rgba(255, 255, 255, .7);

                    &.adm-page-indicator-dot-active {
                        width: 36px;
                        background: #FFF;
                    }
                }
            }
        }
    }
}

在这里插入图片描述

功能-数据请求与数据绑定

创建所需状态 & 组件第一次渲染完毕,向服务器发送数据请求:

useEffect(callback,[depends]),useEffect()如果有返回值则必须是一个函数,若使用async/await直接修饰callback则useEffect()返回一个promise实例,因而不能直接将callback使用async修饰。
应该这么写:
在这里插入图片描述

import React, { useState, useEffect } from "react";
import { Swiper, Image } from "antd-mobile";
import api from '../api/index'
...

const Home = function Home() {
    /* 创建所需状态 */
    let [today, setToday] = useState(_.formatTime(null, '{0}{1}{2}')),
        [banner, setBanner] = useState([])
    /* 组件第一次渲染完毕:向服务器发送数据请求 */
    useEffect(() => {
        (async () => {
            try {
                let { date, stories, top_stories } = await api.queryNewsLatest()
                setToday(date)
                setBanner(top_stories)
            } catch (_) { }
        })()
    }, [])
    console.log(banner);

数据绑定 & 图片懒加载等:

    return <div className="home-box">
        {/* 头部 */}
        <HomeHead today={today} />
        {/* 轮播图 */}
        <div className="swiper-box">
            {banner.length > 0 ? <Swiper autoplay={true} loop={true}>
                {
                    banner.map(item => {
                        let { id, image, title, hint } = item
                        return <Swiper.Item key={id}>
                            <Link to={{ pathname: `/detail/${id}` }}>
                                {/* <img src={image} alt="" /> */}
                                <Image src={image} lazy></Image>
                                <div className="desc">
                                    <h3 className="title">{title}</h3>
                                    <p className="hint">{hint}</p>
                                </div>
                            </Link>
                        </Swiper.Item>
                    })
                }
            </Swiper> : null}
        </div>
    </div >

在这里插入图片描述

设置若图片加载失败后的默认占位图片样式:

                .adm-image,
                img {
                    display: block;
                    width: 100%;
                    height: 100%;

                    .adm-image-tip {
                        svg {
                            width: 20%;
                            height: 20%;
                        }
                    }
                }

在这里插入图片描述

首页新闻列表

新闻列表数据结构:
在这里插入图片描述

结构

新闻列表结构:Home.jsx

import { Swiper, Image, Divider, DotLoading } from "antd-mobile";
import SkeletonAgain from '../components/SkeletonAgain'
import NewsItem from '../components/NewsItem'
...
        {/* 新闻列表 */}
        <SkeletonAgain />
        <div className="news-box">
            <Divider contentPosition="left">1011</Divider>
            <div className="news-list">
                <NewsItem></NewsItem>
                <NewsItem></NewsItem>
                <NewsItem></NewsItem>
                <NewsItem></NewsItem>
                <NewsItem></NewsItem>
            </div>
        </div>
        {/* 加载更多 */}
        <div className="loadmore-box">
            <DotLoading />
            数据加载中...
        </div>

新闻单项结构:NewsItem.jsx

import React from "react";
import { Image } from "antd-mobile";
import { Link } from "react-router-dom";
import './NewsItem.less'

const NewsItem = function NewsItem() {
    return <div className="news-item-box">
        <Link to={{ pathname: '/detail/xxx' }}>
            <div className="content">
                <h4 className="title">小事 · 捕捉生活里的光</h4>
                <p className="hint">菲凡 · 两分钟阅读</p>
            </div>
            <Image src="http://www.bimant.com/blog/content/images/2022/08/image-78.png" lazy />
        </Link>
    </div>
}
export default NewsItem
样式

新闻列表样式:

  1. 设置骨架屏样式:components/SkeletonAgain.jsx下引入SkeletonAgain.less
.skeleton-again-box {
    padding: 20px 30px;

    .adm-skeleton-title {
        margin: 30px 0;
        height: 50px;
    }

    .adm-skeleton-paragraph-line {
        margin: 20px 0;
        height: 32px;
    }
}
  1. 然后发现root内容部分高度不够了,显示出html的颜色(灰色):index.less原 #root height : 100%,高度只有一屏高度,内容超过一屏高度后则无法应用#root样式

在这里插入图片描述

将内容高度height:100%改为min-height:100%

#root {
    // height: 100%;
    min-height: 100%;
    background: #FFF;
    margin: 0 auto;
    // max-width: 750px;--->写在这里也会转为rem,因此需要在js中设置
}

在这里插入图片描述

  1. 设置加载更多盒子样式:
    .loadmore-box {
        height: 80px;
        line-height: 80px;
        text-align: center;
        background: #EEE;
        font-size: 24px;
        color: #999;

        .adm-dot-loading {
            position: static;
            transform: none;
            font-size: 32px;
        }
    }

在这里插入图片描述

  1. 设置新闻列表样式及Divider分隔线左侧段长度:
    .news-box {
        padding: 10px 30px;

        .adm-divider-left {
            &.adm-divider-horizontal {
                &:before {
                    max-width: 2%;
                }
            }
        }
    }

新闻单项样式:

.news-item-box {
    position: relative;
    box-sizing: border-box;
    padding: 15px 0;
    height: 160px;

    a {
        display: block;
        height: 100%;

        .content {
            margin-right: 160px;

            .title {
                font-size: 32px;
                color: #000;
                max-height: 90px;
                line-height: 45px;
                overflow: hidden;
            }

            .hint {
                font-size: 24px;
                color: #999;
                line-height: 40px;
            }
        }

        .adm-image {
            position: absolute;
            right: 0;
            top: 15px;
            width: 130px;
            height: 130px;

            img {
                display: block;
                width: 100%;
                height: 100%;
            }
            
            .adm-image-tip {
                svg {
                    width: 50px;
                    height: 50px;
                }
            }
        }
    }
}

在这里插入图片描述
在这里插入图片描述

功能-数据请求与数据绑定

Home.jsx中进行新闻列表数据的请求和数据传递:

问题: 函数组件的每次渲染都是将函数组件重新执行创建一个新的闭包,在新的闭包当中引用新修改的状态值。组件首次渲染,在let [news, setNews] = useState([])中创建新闻列表状态及修改状态的方法,将空数组(实则是一存储空数组的堆内存地址)赋值给news新闻列表状态;news.push({ date, stories })修改原数组(实则是修改原堆内存地址所指向内容)后,若使用setNews(news)来修改news状态,由于状态news一直是指向数组的堆内存地址,鉴于setState()内部自带的优化机制,前后修改的状态内容一致(均为原本的堆内存地址,修改的只是堆内存地址指向的内容),setNews()将不会执行,因而无法完成预期中的视图更新(组件重新渲染、状态更新)。
解决方法:setNews([...news])创建一个新数组(新的堆内存)并将原数组内容浅拷贝

...
const Home = function Home() {
    /* 创建所需状态 */
    let ...
        [news, setNews] = useState([])
    /* 组件第一次渲染完毕:向服务器发送数据请求 */
    useEffect(() => {
        (async () => {
            try {
                let { date, stories, top_stories } = await api.queryNewsLatest()
                ...
                // 更新新闻列表状态
                news.push({
                    date,
                    stories
                })
                setNews([...news])
            } catch (_) { }
        })()
    }, [])
    console.log(news);
    return <div className="home-box">
        ...
        {/* 新闻列表 */}
        {news.length === 0 ? <SkeletonAgain /> :
            <>
                {
                    news.map((item, index) => {
                        let { date, stories } = item
                        return <div className="news-box" key={date}>
                            {index !== 0 ? <Divider contentPosition="left">{_.formatTime(date, '{1}月{2}日 ')}</Divider> : null}
                            <div className="news-list">
                                {
                                    stories.map(cur => {
                                        return <NewsItem key={cur.id} info={cur}></NewsItem>
                                    })
                                }
                            </div>
                        </div>
                    })
                }
            </>}

NewsItem.jsx中进行属性规则处理及数据绑定:

如果你封装的组件只有你调用,并且只调用几次,那么不用对属性进行属性规则校验;如果你封装的组件复用性很高,除了你调用外还有其他同事调用,并且调用频次很高,则需要做属性规则校验。

...
import PropTypes from 'prop-types'

const NewsItem = function NewsItem(props) {
    let { info } = props
    return <div className="news-item-box">
        ...
    </div>
}
/* 属性规则处理 */
NewsItem.defaultProps = {
    info: null
}
NewsItem.propTypes = {
    info: PropTypes.object
}
const NewsItem = function NewsItem(props) {
    let { info } = props
    if (!info) return null
    let { id, title, hint, images } = info
    if (!Array.isArray(images)) images = ['']
    return <div className="news-item-box">
        <Link to={{ pathname: `/detail/${id}` }}>
            <div className="content">
                <h4 className="title">{title}</h4>
                <p className="hint">{hint}</p>
            </div>
            <Image src={images[0]} lazy />
        </Link>
    </div>
}

在这里插入图片描述


访问过的新闻项样式调整(变灰色):

.news-item-box {
    ...

    a {
        display: block;
        height: 100%;

        &:visited {
            .content {
                .title {
                    color: #999;
                }

                .hint {
                    color: #AAA;
                }
            }
        }

在这里插入图片描述

首页滑动触底后加载更多新闻

获取加载更多的DOM元素实例:

import React, { useState, useEffect, useRef } from "react";
...

const Home = function Home() {
    /* 创建所需状态 */
    ...
    let loadMore = useRef()
    ...
    /* 组件第一次渲染完毕:设置监听器,实现触底加载 */
    useEffect(() => {
        console.log(loadMore.current);
    }, [])
    ...
    	{/* 加载更多 */}
        <div className="loadmore-box" ref={loadMore}>
            <DotLoading />
            数据加载中
        </div>
        ...

在这里插入图片描述

基于数据控制元素显示隐藏:

  1. 控制其是否渲染 <=> v-if
    没有数据可以不渲染「结构中找不到它,也获取不了DOM元素」,有数据再渲染。
    在这里插入图片描述

  2. 控制元素的样式display <=> v-show
    不管是否有数据,都会进行渲染,只不过没有数据则样式为none「可以获取DOM元素」

正常情况下,对于显示隐藏不经常变化的操作来讲,我们应该采用方案1:这样可以优化性能,尤其是在组件第一次渲染的时候,没有数据的情况下,我们可以少渲染很多东西,加快页面第一次呈现的速度!
对于选项卡这样频繁切换的操作,我们更应该用第二种方案,只是改变样式,不要去频繁创建和销毁元素!

如果我们想获取DOM元素,但是还不想让其展示,那么只能用方案2。

        {/* 加载更多 */}
        <div className="loadmore-box" ref={loadMore}
            style={{ display: news.length === '0' ? 'none' : 'block' }}>
            <DotLoading />
            数据加载中
        </div>

在这里插入图片描述

在组件释放的时候,React内部会移除:

  • 虚拟DOM
  • 真实DOM
  • 合成事件绑定
  • ….

所以我们后面自己新加的DOM元素,比如<img.>/<h2.>标题等就不需要再手动移除了

不会移除的东西:

  • 设置的定时器
  • 设置的监听器
  • 基于addEventListener手动做的事件绑定
  • ….

为了性能优化,我们需要自己手动去移除

相关文档:Intersection Observer API

    /* 组件第一次渲染完毕:设置监听器,实现触底加载 */
    useEffect(() => {
        let ob = new IntersectionObserver(async changes => {
            let { isIntersecting } = changes[0]
            // 加载更多的按钮出现在视口中(也就是触底了)
            if (isIntersecting) {
                try {
                    let time = news[news.length - 1]['date']
                    let res = await api.queryNewsBefore(time)
                    news.push(res)
                    setNews([...news])
                } catch (_) { }
            }
        })
        let loadMoreBox = loadMore.current
        ob.observe(loadMore.current)
        console.log(loadMore.current);
        // 在组件销毁释放的时候:手动销毁监听器
        return () => {
            ob.unobserve(loadMoreBox)
            ob = null
        }
    }, [])

console.log(changes);
在这里插入图片描述

问题: 由于return () => { console.log(loadMore.current); ob.unobserve(loadMore.current); ob = null }是在组件完全释放完毕后触发,此时组件当中的虚拟DOM和真实DOM都不存在了,因而每次从首页跳转到详情页后会报错:ob.unobserve()不了一个已经不存在的DOM元素—>console.log(loadMore.current);//=>loadMore.current已经是null了
在这里插入图片描述
解决方法: 将DOM元素事先存放在一变量中,组件释放后该变量中储存的DOM元素还存在
let loadMoreBox = loadMore.current; ob.observe(loadMore.current); // 在组件销毁释放的时候:手动销毁监听器 return () => { console.log(loadMoreBox); ob.unobserve(loadMoreBox); ob = null; }
—>console.log(loadMoreBox);//=>变量中存储的DOM元素依然还存在

在这里插入图片描述

新闻详情页

由于新闻详情页因每条新闻的不同,结构样式内容也都不同,不确定性太大,因而实际开发过程中新闻详情的结构内容通常都是由服务器直接返回的,而不需要前端一个个单独去写。

在这里插入图片描述

结构
import React from "react";
import './Detail.less'
import { LeftOutline, ChatCheckOutline, LikeOutline, StarOutline, UploadOutline } from 'antd-mobile-icons'
import { Space, Badge } from "antd-mobile";

const Detail = function Detail(props) {
    let { navigate } = props
    return <div className="detail-box">
        {/* 新闻内容 */}
        <div className="content"></div>
        {/* 底部图标 */}
        <div className="tab-bar">
            <div className="back" onClick={() => navigate(-1)}><LeftOutline /></div>
            <div className="icons">
                <Space style={{ fontSize: 22 }}>
                    <Badge content='9'>
                        <ChatCheckOutline />
                    </Badge>
                    <Badge content='107'>
                        <LikeOutline />
                    </Badge>
                    <span className="stored"><StarOutline /></span>
                    <span className="zhuanfa"><UploadOutline /></span>
                </Space>
            </div>
        </div>
    </div>
}
export default Detail
样式
.detail-box {
    .tab-bar {
        box-sizing: border-box;
        position: fixed;
        bottom: 0;
        left: 0;
        z-index: 999;
        width: 100%;
        height: 90px;
        background: rgb(244, 244, 244);
        display: flex;
        justify-content: space-between;
        align-items: center;

        .back {
            box-sizing: border-box;
            width: 100px;
            height: 50px;
            line-height: 50px;
            text-align: center;
            font-size: 40px;
            font-weight: 900;
            border-right: 2px solid #CCC;
        }

        .icons {
            width: 650px;
            height: 50px;

            .adm-space {
                box-sizing: border-box;
                display: flex;
                justify-content: space-between;
                width: 100%;
                height: 100%;
                padding: 0 35px;

                .adm-space-item {
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    margin-right: 0;

                    .adm-badge-wrapper {
                        .adm-badge-fixed {
                            right: -20%;
                            top: 20%;
                        }

                        .adm-badge {
                            background: none;

                            .adm-badge-content {
                                color: #555;
                            }
                        }
                    }
                }

                span {
                    &.stored {
                        color: #108ee9;
                    }

                    &.zhuanfa {
                        color: #AAA;
                    }
                }
            }
        }
    }
}

在这里插入图片描述

注意样式中&:nth-last-of-type(1) 和&:nth-last-child(1)区别(本次我们没用):

span {
    &:nth-last-of-type(1) { //先找到所有span标签,再设定最后一个span标签样式 | &:nth-last-child(1){ //找到所有子元素中的最后一个,而且还得是span标签
        color: #AAA;
    }
}
功能-新闻详情数据的请求与绑定 & 组件缓存

定义相关状态(新闻内容和新闻点赞信息),组件第一次渲染完毕后向服务器发送数据请求获取数据:

两个数据请求并行处理即可,因为两个请求并无前后依赖关系,谁先获取到先处理谁即可。
注意:如果这么写的话,实际上两次请求串行处理,因为await只有在上一个请求成功以后才可以发下一个请求。若要两次请求并行处理,useEffect()写两次分别进行两次数据请求即可。
在这里插入图片描述

import React, { useState, useEffect } from "react";
import './Detail.less'
...
import api from "../api";
import SkeletonAgain from "../components/SkeletonAgain";

const Detail = function Detail(props) {
    let { navigate, params } = props
    /* 定义状态 */
    let [info, setInfo] = useState(null),
        [extra, setExtra] = useState(null)
    /* 组件第一次渲染完毕:向服务器获取数据 */
    useEffect(() => {
        (async () => {
            try {
                let result = await api.queryNewsInfo(params.id)
                setInfo(result)
            } catch (_) { }
        })()
    }, [])
    useState(() => {
        (async () => {
            try {
                let result = await api.queryStoreExtra(params.id)
                setExtra(result)
            } catch (_) { }
        })()
    }, [])
    ...

分别console.log(result)
在这里插入图片描述

将获取到的新闻点赞信息绑定到底部图标(评论数和点赞数):

                    <Badge content={extra ? extra.comments : 0}>
                        <ChatCheckOutline />
                    </Badge>
                    <Badge content={extra ? extra.popularity : 0}>
                        <LikeOutline />
                    </Badge>

在这里插入图片描述

基于胡子语法绑定的内容全部会作为普通文本进行渲染(所以<div className="content">{ info.body }</div>无法达成预期效果,会将所有html标签作为文本渲染),如果想把内容中的html字符串识别为标签需要基于dangerouslySetInnerHTML = {{ __html: htmlString }}

    ...
    return <div className="detail-box">
        {/* 新闻内容 */}
        {
            !info ? <SkeletonAgain /> :
                <div className="content" dangerouslySetInnerHTML={{ __html: info.body }}></div>
        }
        ...

Detail.less调整样式:

.detail-box {
    .content {
        overflow-x: hidden;
        margin: 0;
        padding-bottom: 90px;
    }

	...

在这里插入图片描述

引入了内容但还没有引入样式,相关内容布局比较杂乱,此时处理相关样式和图片:

flushSync将异步操作改成同步,将异步操作立即执行:
在这里插入图片描述
解决方法一:
在这里插入图片描述
解决方法二就是我们下面使用的方式。

    let link
    /* 定义样式和图片处理方法 */
    const handleStyle = (result) => {
        let { css } = result
        if (!Array.isArray(css)) return
        css = css[0]
        if (!css) return
        // 创建<Link>导入样式
        link = document.createElement('link')
        link.rel = 'stylesheet'
        link.href = css
        document.head.appendChild(link)
    }
    const handleImage = (result) => { }
    /* 组件第一次渲染完毕:向服务器获取数据 */
    useEffect(() => {
        (async () => {
            try {
                let result = await api.queryNewsInfo(params.id)
                setInfo(result)
                // 处理样式和图片
                handleStyle(result)
                handleImage(result)
            } catch (_) { }
        })()
    }, [])

扩展优化项:可选链 | 深层次对象访问的时候,即便相关对象不存在也不会报错,只会返回undefined或者null。
在这里插入图片描述

在这里插入图片描述

此时从详情页返回首页后,添加到<head.>中的link样式依然还存在,影响到了首页样式,且再次进入详情页后又插入一个link样式:

在这里插入图片描述

所以我们需要在从详情页返回首页、详情页组件销毁后,移除创建的样式,防止插入的样式对其他组件产生影响:

    /* 组件第一次渲染完毕:向服务器获取数据 */
    useEffect(() => {
        ...
        // 销毁组件:移除创建的样式
        return () => {
            if (link) document.head.removeChild(link)
        }
    }, [])

在这里插入图片描述

小总结:常用的使用useEffect()模拟的周期函数有哪些?
componentDidMount()组件第一次渲染完毕:获取数据/设置监听器/获取DOM元素/设置定时器/手动给DOM元素做事件绑定…
componentUnMounted()组件销毁:手动清除我们设置的定时器、监听器以及做的事件绑定和逻辑事件(比如上面的把link移除)
类似于componentDidUpdate()只不过设置了依赖

通过与设计稿对比及观察新闻详情页DOM结构:

在这里插入图片描述

确定放图片的地方获取DOM元素,进行图片相关处理:

    /* 定义样式和图片处理方法 */
    ...
    const handleImage = (result) => {
        // 自己写在JSX中的可以通过ref来获取DOM元素,但是要获取通过动态导入加载进来的DOM元素只能通过DOM元素相关操作获取了
        let imgPlaceHolder = document.querySelector('.img-place-holder')
        if (!imgPlaceHolder) return
        // 创建大图
        let tmpImg = new Image // new Image就相当于document.createElement('img')创建一个<img>标签
        tmpImg.src = result.image
        tmpImg.onload = () => {
            imgPlaceHolder.appendChild(tmpImg)
        };
        tmpImg.onerror = () => {
            let parent = imgPlaceHolder.parentElement
            parent.parentElement.removeChild(parent)
        }
    }

我们发现图片没有出来:

在这里插入图片描述

原因:let imgPlaceHolder = document.querySelector('.img-place-holder')要获取DOM元素,必须等到修改状态并且视图更新之后【相关元素已经绑定到视图中】才能获取到。但此时try { let result = await api.queryNewsInfo(params.id); setInfo(result); handleStyle(result); handleImage(result); } catch (_) { }请求数据并修改状态、更新视图是异步操作,相关状态还没更改、视图还没更新便继续执行样式/图片处理操作。而图片处理操作handleImage需要用到视图更新完毕后页面绑定好的DOM元素,因而无法获取到相关的DOM元素,handleImage后续代码也就不会再执行了。
解决方法:使用flushSync()使得setInfo(result);handleImage(result);同步执行【flushSync()内异步外同步】| 或者使用useEffect()为setImage()操作添加info依赖

import { flushSync } from "react-dom";
...
    /* 组件第一次渲染完毕:向服务器获取数据 */
    useEffect(() => {
        (async () => {
            try {
                let result = await api.queryNewsInfo(params.id)
                flushSync(() => {
                    setInfo(result)
                    // 处理样式和图片
                    handleStyle(result)
                })
                handleImage(result)
            } catch (_) { }
        })()
        ...
    }, [])

图片能够正常加载:

在这里插入图片描述

在Detail.less中修改加载的图片样式:

.detail-box {
    .content {
        ...

        .img-place-holder {
            // overflow: hidden;
            height: 750px;

            img {
                margin: 0;
                width: 100%;
            }
        }
		// 修改知乎返回的错误样式-作者头像部分
        .meta {
            .avatar {
                display: inline-block;
                margin-top: 0;
                margin-bottom: 0;
            }
        }
    }

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值