vue3项目实战---知乎日报----首页功能

87 阅读4分钟

vue3项目实战---知乎日报----首页功能

目录

[TOC]

网络请求封装

GET传参格式 www.baidu.com/info?t=0&ag…

传递当天日期返回以往日期信息

params` 是即将与请求一起发送的 URL 参数 ,必须是一个无格式对象(plain object)或URLSearchParams对象

import axios from './http'
 
//获取最新新闻 && 轮播图信息
export const NewsLatest = () => {
    return axios.get('/api/news_latest')
}
//获取以往信息
export const NewsBefore = time => {
    return axios.get('/api/news_before', {
        params: {
            time
        }
    })
}
header

用类库处理时间格式(不传为当前时间,传递字符串返回数组格式)

通过计算属性 处理数组对应月份(计算属性有缓存)

// 日期格式化
export const formatTime = function formatTime(time, template) {
    if (typeof time !== "string") {
        time = new Date().toLocaleString('zh-CN', { hour12: false });
    }
    if (typeof template !== "string") {
        template = "{0}年{1}月{2}日 {3}:{4}:{5}";
    }
    let arr = [];
    if (/^\d{8}$/.test(time)) {
        let [, $1, $2, $3] = /^(\d{4})(\d{2})(\d{2})$/.exec(time);
        arr.push($1, $2, $3);
    } else {
        arr = time.match(/\d+/g);
    }
    return template.replace(/\{(\d+)\}/g, (_, $1) => {
        let item = arr[$1] || "00";
        if (item.length < 2) item = "0" + item;
        return item;
    });
};
<script>
 
 
import { reactive, ref, toRefs, computed } from "vue";
import { formatTime } from "../assets/utils";
export default {
    name: "Header",
 
    setup() {
        //状态
        let state = reactive({
            today: formatTime(null, '{0}{1}{2}'),
        })
 
        //基于计算属性,处理时间
        let time = computed(() => {
            let { today } = state
            let [month, day] = formatTime(today, '{1}-{2}').split('-')
            let newMonth = month.slice(1, 2)
            let arr = ['', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
            return {
                day,
                month: arr[newMonth] + '月'
            }
        })
 
        return {
            ...toRefs(state),
            time,
        }
    },
}   
</script>
swiper

通过useRouter(路由实例对象).push跳转 动态路由匹配

<template>
    <section class="banner-box">
        <van-swipe :autoplay="3000" lazy-render v-if="bannerList.length > 0">
            <van-swipe-item v-for="item in bannerList" :key="item.id" @click="goToDetail(item.id)">
                <img class="abbre" :src=item.image alt="">
                <div class="desc">
                    <h2 class="title">
                        {{ item.title }}
                    </h2>
                    <p class="author">
                        {{ item.hint }}
                    </p>
 
                </div>
            </van-swipe-item>
        </van-swipe>
    </section>
</template>
 
<script>
import { useRouter } from "vue-router";
export default {
    name: "Swiper",
    props: ['bannerList'],
    setup() {
        const router = useRouter()
        //跳转详情
        const goToDetail = (id) => router.push(`/detail/${id}`)
        return {
            goToDetail
        }
    },
 
}
</script>
 
<style lang="less" scoped>
.banner-box {
    box-sizing: border-box;
    height: 375px;
    background: #eee;
    overflow: hidden;
 
    .van-swipe {
        height: 100%;
        overflow: hidden;
 
        .abbre {
            display: block;
            width: 100%;
            min-height: 100%;
        }
    }
 
    .desc {
        position: absolute;
        bottom: 0;
        left: 0;
        z-index: 10;
        box-sizing: border-box;
        padding: 15px 20px;
        width: 100%;
        background: rgba(0, 0, 0, .4);
        background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, .4));
        color: #fff;
 
        .title {
            line-height: 25px;
            font-size: 18px;
            max-height: 50px;
            overflow: hidden;
            display: -webkit-box;
            -webkit-line-clamp: 2;
            -webkit-box-orient: vertical;
        }
 
        .author {
            font-size: 14px;
            line-height: 25px;
            color: rgba(255, 255, 255, .8);
        }
    }
 
    /deep/.van-swipe__indicators {
        left: auto;
        transform: none;
        right: 15px;
 
        .van-swipe__indicator {
            width: 5px;
            height: 5px;
            background: rgba(255, 255, 255, .8);
        }
 
        .van-swipe__indicator--active {
            width: 15px;
            background: #fff;
            border-radius: 5px;
        }
    }
}
</style>
items新闻列表、

通过router-link路由导航跳转

使用vant实现图片懒加载

main.js导入使用

import Vant, { Lazyload } from 'vant';

app.use(Lazyload, {

lazyComponent: true,

});

把src换为lazy

<template>
    <div class="news-box">
        <router-link :to="`/detail/${cur.id}`">
            <h3 class="title">
                {{ cur.title }}
            </h3>
            <div class="desc"> {{ cur.hint }}</div>
            <div class="pic">
                <img class="abbre" v-if="cur.images && cur.images.length > 0" v-lazy='cur.images[0]' alt="">
            </div>
        </router-link>
    </div>
</template>
        
<script>
export default {
    name: "NewsItem",
    props: {
        cur: {
            type: Object,
            require: true
        }
    },
    setup() { },
}
 
</script>
 
<style lang="less" scoped>
.news-box {
    position: relative;
 
    a {
        display: block;
        padding-right: 90px;
        min-height: 70px;
        overflow: hidden;
        margin-top: 30px;
 
 
 
        .pic {
            position: absolute;
            right: 0;
            top: 0;
            width: 70px;
            height: 70px;
            background: #eee;
 
            img {
                display: block;
                width: 100%;
                height: 100%;
            }
        }
 
        .title {
            font-size: 16px;
            color: #000;
            line-height: 25px;
            max-height: 50px;
            overflow: hidden;
            display: -webkit-box;
            -webkit-line-clamp: 2;
            -webkit-box-orient: vertical;
        }
 
        .desc {
            line-height: 20px;
            font-size: 12px;
            color: #999;
        }
    }
 
 
}
</style>
home

v-for与v-if同时使用时 v-for权重高

v-if控制元素销毁创建,v-show控制元素显示隐藏(在组件渲染完毕后想获取dom应该使用v-show)

<template>
    <Header></Header>
    <Swiper :bannerList="bannerList"></Swiper>
    <van-skeleton title :row="5" v-if="newList === 0" />
    <section class="news-box " v-else>
        <div class="day-box" v-for="(item, index ) in newList" :key="item.date">
            <van-divider dashed content-position="left" v-if="index > 0">{{ formatTime(item.date, '{1}月{2}日') }}
            </van-divider>
            <div class="list">
                <NewsItem v-for="cur in item.stories" :key="cur.id" :cur="cur" />
            </div>
        </div>
    </section>
    <section class="loadmore-box" ref="loadmore" v-show="newList">
        <van-loading size="18px">小主,奴家正在努力加载中...</van-loading>
    </section>
</template>
 
<script>
import Header from '../components/header.vue'
import Swiper from '../components/swiper.vue'
import NewsItem from '../components/items.vue'
import { reactive, ref, toRefs, onBeforeMount, onMounted, onBeforeUnmount } from "vue";
import { NewsLatest, NewsBefore } from "../api/index";
import { formatTime } from "../assets/utils";
 
export default {
    name: "Home",
    components: {
        Header,
        Swiper,
        NewsItem
    },
    setup() {
        let state = reactive({
            today: '',
            bannerList: [],
            newList: []
        })
 
        //在第一次渲染之前获取数据:今天日期 ,轮播图数据, 今日新闻
        onBeforeMount(async () => {
            let { date, stories, top_stories } = await NewsLatest();
            state.today = date
            state.bannerList = Object.freeze(top_stories)
            state.newList.push(Object.freeze({
                date,
                stories
            }))
        })
 
        //加载更多
        let loadmore = ref(null)
        const ob = new IntersectionObserver(async changes => {
            let item = changes[0]
            if (item.isIntersecting) {
                let len = state.newList.length
                if (len === 0) return
                let data = await NewsBefore(state.newList[len - 1].date)
                state.newList.push(Object.freeze(data))
            }
        });
        onMounted(() => {
            if (!loadmore.value) return
            ob.observe(loadmore.value)
        })
 
        onBeforeUnmount(() => {
            if (!loadmore.value) return
            ob.unobserve(loadmore.value)
        })
        return {
            ...toRefs(state),
            formatTime,
            loadmore
        }
    },
};
</script>
<style lang="less" scoped>
.van-skeleton {
    padding: 15px;
}
 
.news-box {
    padding: 0 15px;
 
    .day-box {
        margin-top: 30px;
    }
}
 
.loadmore-box {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 50px;
    margin-top: 20px;
    background: #f4f4f4;
}
</style>
 
IntersectionObserver API 使用教程

语法

可以自动"观察"元素是否可见,Chrome51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做"交叉观察器"。

IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。

规格写明, IntersectionObserver 的实现,应该采用 requestIdleCallback() ,即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行

var io = new IntersectionObserver(callback, option);

IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数: callback 是可见性变化时的回调函数, option 是配置对象(该参数可选)。

方法

构造函数的返回值是一个观察器实例。实例的 observe 方法可以指定观察哪个 DOM 节点。

如果要观察多个节点,就要多次调用这个方法。

// 开始观察
io.observe(document.getElementById('example'));
 
// 停止观察
io.unobserve(element);
 
// 关闭观察器
io.disconnect();

callback

callback 一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见) callback 函数的参数是一个数组

Option

IntersectionObserver 构造函数的第二个参数是一个配置对象。它可以设置以下属性。

threshold 属性决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为 [0] ,即交叉比例( intersectionRatio )达到 0 时触发回调函数。

new IntersectionObserver(
  entries => {/* ... */}, 
  {
    threshold: [0, 0.25, 0.5, 0.75, 1]
  }
);

用户可以自定义这个数组。比如, [0, 0.25, 0.5, 0.75, 1] 就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。

root 属性,rootMargin 属性

很多时候,目标元素不仅会随着窗口滚动,还会在容器里面滚动(比如在 iframe 窗口里滚动)。容器内滚动也会影响目标元素的可见性,

IntersectionObserver API 支持容器内滚动。 root 属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点

var opts = { 
  root: document.querySelector('.container'),
  rootMargin: "500px 0px" 
};
 
var observer = new IntersectionObserver(
  callback,
  opts
);

上面代码中,除了 root 属性,还有 rootMargin 属性。后者定义根元素的 margin ,用来扩展或缩小 rootBounds 这个矩形的大小,从而影响 intersectionRect 交叉区域的大小。它使用CSS的定义方法,比如 10px 20px 30px 40px ,表示 top、right、bottom 和 left 四个方向的值。

这样设置以后,不管是窗口滚动或者容器内滚动,只要目标元素可见性变化,都会触发观察器。

性能优化

vue默认做了对象的深层级劫持 ,内层数据不需要修改的情况下使用 Object.freeze冻结就不会劫持

在组件销毁时清除定时器,监听器

使用vant组件库实现图片懒加载