day-120-one-hundred-and-twenty-20230725-vue3项目实战-知乎日报第2天
vue3项目实战
-知乎日报第2天
新闻列表
- 每一天的新闻都是一个可遍历的区块,一天中的新闻有多个新闻,每一个新闻都是一个组件。
- 用到的日期要转格式。
- 组件要注册为全局组件。可通过插件把
components目录中的组件
变成全局组件。也可以自己注册全局组件-更灵活,也可添加函数式调用的组件。 - 骨架屏用
v-if
与v-else
来区分,非一个独立的根节点的地方,用template标签
包起来。
- src/views/Home.vue
vue
<script setup>
import HomeHead from '@/components/HomeHead.vue'
// import { reactive, onBeforeMount, onMounted, ref } from 'vue'
// import dayjs from 'dayjs'
// import API from '@/api'
import useAutoImport from '@/useAutoImport'
const { reactive, onBeforeMount, onMounted, onUnmounted, ref, dayjs, API } = useAutoImport()
/* 定义状态和数据 */
const moreBox = ref(null)
const state = reactive({
today: dayjs().format('YYYYMMDD'),
bannerData: [],
newsList: []
})
defineOptions({
name: 'Home'
})
/* 第一次渲染之前:向服务器发送数据请求 */
onBeforeMount(async () => {
try {
let { date, stories, top_stories } = await API.queryNewsLatest()
state.today = date
state.bannerData = Object.freeze(top_stories)
state.newsList.push(
Object.freeze({
date,
stories
})
)
} catch (_) {}
})
// 第一次渲染完毕:创建监听器,实现触底加载。
let ob = null
let isRun = false
onMounted(() => {
ob = new IntersectionObserver(async (changes) => {
let item = changes[0]
if (!item.isIntersecting) {
return
}
// 到达页面底部(触底):获取以往的新闻数据。
if (isRun) {
return
}
isRun = true
try {
let time = state.newsList[state.newsList.length - 1].date
let data = await API.queryNewsBefore(time)
state.newsList.push(Object.freeze(data))
} catch (error) {
console.log(`error:-->`, error)
}
isRun = false
})
console.log(`moreBox.value-->`, moreBox.value) //需要v-show,而不是v-if。
ob.observe(moreBox.value)
})
onUnmounted(() => {
//组件销毁后:移除创建的监听器。此时moreBox?.value真实DOM已经销毁。
console.log(`moreBox.value-->`, moreBox.value)
})
</script>
<template>
<!-- 头部区域 -->
<home-head :today="state.today" />
<!-- 轮播图 -->
<section class="banner-box">
<van-swipe v-if="state.bannerData.length > 0" :autoplay="3000" lazy-render>
<van-swipe-item v-for="item in state.bannerData" :key="item.id">
<router-link :to="`/detail/${item.id}`">
<img :src="item.image" alt="" class="pic" />
<div class="desc">
<h3 class="title">{{ item.title }}</h3>
<p class="author">{{ item.hint }}</p>
</div>
</router-link>
</van-swipe-item>
</van-swipe>
</section>
<!-- 新闻列表 -->
<van-skeleton title :row="5" v-if="!state.newsList.length"></van-skeleton>
<template v-else>
<section class="news-box" v-for="(item, index) in state.newsList" :key="item.date">
<van-divider content-position="left" v-if="index > 0">
{{ dayjs(item.date).format('MM月DD日') }}
</van-divider>
<div class="content">
<news-item v-for="cur in item.stories" :key="cur.id" :info="cur" from="home" />
</div>
</section>
</template>
<!-- 加载更多 -->
<div class="lazy-more" ref="moreBox" v-show="state.newsList.length > 0">
<van-loading size="12px">小主,精彩数据准备中...</van-loading>
</div>
</template>
<style lang="less" scoped>
.banner-box {
box-sizing: border-box;
height: 375px;
background: #eee;
overflow: hidden;
.van-swipe {
height: 100%;
a {
display: block;
height: 100%;
}
.pic {
display: block;
width: 100%;
height: 100%;
}
.desc {
position: absolute;
bottom: 0;
left: 0;
box-sizing: border-box;
padding: 20px;
width: 100%;
background: rgba(0, 0, 0, 0.5);
background: -webkit-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.5));
.title {
line-height: 25px;
font-size: 20px;
color: @CR_W;
}
.author {
line-height: 30px;
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
}
}
}
:deep(.van-swipe__indicators) {
left: auto;
right: 20px;
transform: none;
.van-swipe__indicator--active {
background-color: @CR_W;
width: 18px;
border-radius: 3px;
}
}
}
.news-box {
padding: 0 15px;
.van-divider {
margin: 5px 0;
font-size: 12px;
&:before {
display: none;
}
}
}
.van-skeleton {
padding: 30px 15px;
}
.lazy-more {
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
padding: 0 10px;
height: 50px;
background: #f4f4f4;
}
</style>
组件封装
- 决定组件怎样用,是函数式组件还是模板式组件。
- 组件的复杂与灵活度,决定是
jsx语法
还是模板语法
。
全局属性和全局方法
-
src/main.js
jsimport { createApp } from 'vue' import App from './App.vue' import router from './router' import { createPinia } from 'pinia' import createPersistedState from 'pinia-plugin-persistedstate' import global from './global' // pinia持久化存储 const pinia = createPinia() pinia.use(createPersistedState) // 创建应用 const app = createApp(App) app.use(router) app.use(pinia) app.use(global) app.mount('#app')
-
src/global.js
jsimport 'lib-flexible' import { showToast, Lazyload } from 'vant' import NewsItem from '@/components/NewsItem.vue' import NavBack from '@/components/NavBack.vue' // vant@4中函数组件的样式 import 'vant/es/toast/style' import 'vant/es/dialog/style' import 'vant/es/notify/style' export default function global(app) { // 注册全局组件 app.component('NewsItem', NewsItem) app.component('NavBack', NavBack) // app.component('Lazyload', { Lazyload: true }) app.use(Lazyload) // 注册全局属性/方法-可以用子组件中用this来访问到。类似于Vue2中,把信息放在Vue.prototype。目的:在视图中可以直接使用这些信息、会挂载到组件的this上(vue2)、可以基于getCurrentInstance获取实例后调用! app.config.globalProperties.msg = "全局属性" app.config.globalProperties.$toast = showToast }
-
src/components/NewsItem.vue
vue<script setup> // 注册接收属性。 defineProps({ info: Object, from: String }) </script> <script> export default { mounted() { // console.log(`this-->`, this) // console.log(`this.$toast-->`, this.$toast) // console.log(`this.msg-->`, this.msg) this.$toast('哈哈哈') } } </script>
vant图片懒加载
触底加载
方法自动导入
- 有两种方案。
-
用一个组件,把
vue
与vue-router
,优势在于可以直接使用。但没提示信息,写代码起来会麻烦。同时给不了解的人一些疑惑,不清楚那些方法是从那里导入的。 -
用自定义hook。优势在于有提示信息,同时更灵活,没有额外学习成本。
-
src/useAutoImport.js
jsimport * as vue from 'vue'//直接导入vue中全部的api,方便后面使用vue中的东西可以不用再导入。 import { useRouter, useRoute } from 'vue-router' import { showSuccessToast, showFailToast } from 'vant' import API from './api' import dayjs from 'dayjs' import utils from './assets/utils' export default function useAutoImport() { // 二次处理一些事情,直接拿到想要的结果。防止重复进行操作。 const router = useRouter() const route = useRoute() // 把想要导出的东西统一return出去。 return { ...vue,//把全部导入的东西原样返回。 router, route, dayjs, API, showSuccessToast, showFailToast, utils } }
-
src/views/Detail.vue
vue<script setup> import useAutoImport from '@/useAutoImport' const { reactive, onBeforeMount, onUnmounted, nextTick, route, router, API } = useAutoImport() // 定义状态。 const newsId = route.params.id const state = reactive({ info: null, extra: null }) </script>
-
自定义hook
- 一般有业务的组件之类的方法和状态中,可以使用自定义hooks。
- 一般通用逻辑中,也用的自定义hooks。
-
src/useAutoImport.js 定义自定义hooks
jsimport * as vue from 'vue'//直接导入vue中全部的api,方便后面使用vue中的东西可以不用再导入。 import { useRouter, useRoute } from 'vue-router' import { showSuccessToast, showFailToast } from 'vant' import API from './api' import dayjs from 'dayjs' import utils from './assets/utils' export default function useAutoImport() { // 二次处理一些事情,直接拿到想要的结果。防止重复进行操作。 const router = useRouter() const route = useRoute() // 把想要导出的东西统一return出去。 return { ...vue,//把全部导入的东西原样返回。 router, route, dayjs, API, showSuccessToast, showFailToast, utils } }
-
src/views/Detail.vue 使用自定义hooks
vue<script setup> import useAutoImport from '@/useAutoImport' const { reactive, onBeforeMount, onUnmounted, nextTick, route, router, API } = useAutoImport() // 定义状态。 const newsId = route.params.id const state = reactive({ info: null, extra: null }) </script>
组件缓存
vue3
中组件缓存用keep-alive
,不过语法和vue2
有区别。
-
新写法可以。
-
src/App.vue
vue<template> <router-view v-slot="{ Component }"> <keep-alive include="Home"> <component :is="Component" /> </keep-alive> </router-view> </template>
-
src/views/Home.vue
vue<script setup> defineOptions({ name: 'Home' }) </script>
-
-
旧写法不行。
-
src/App.vue
vue<template> <keep-alive include="Home"> <router-view></router-view> </keep-alive> </template>
-
src/views/Home.vue
vue<script setup> defineOptions({ name: 'Home' }) </script>
-
详情页
并行请求
vue
<script setup>
import useAutoImport from '@/useAutoImport'
const { reactive, onBeforeMount, onUnmounted, nextTick, route, router, API } = useAutoImport()
// 定义状态。
const newsId = route.params.id
const state = reactive({
info: null,
extra: null
})
// 并行请求-1。
onBeforeMount(async () => {
try {
let data = await API.queryNewsInfo(newsId)
state.info = Object.freeze(data) //得到首页数据。
} catch (error) {
console.log(`error:-->`, error)
}
})
// 并行请求-2。
onBeforeMount(async () => {
try {
let data = await API.queryStoryExtra(newsId)
state.extra = Object.freeze(data)
} catch (error) {
console.log(`error:-->`, error)
}
})
</script>
动态加载样式
- 创建一个link标签。
vue
<script setup>
const state = reactive({
info: null,
extra: null
})
let link = null
const handleInfoStyle = (cssLink = '') => {
console.log(`cssLink-->`, cssLink)
let css = cssLink //css样式表路径。
if (!css) {
return
}
link = document.createElement('link')
link.rel = 'stylesheet'
link.href = css
document.head.appendChild(link)
}
onUnmounted(() => {
if (link) {
document.head.removeChild(link)
}
})
/* data = {
"body": "<div class=\"main-wrap content-wrap\">\n<div class=\"headline\">\n\n<div class=\"img-place-holder\"><\/div>\n\n\n\n<\/div>\n\n<div class=\"content-inner\">\n\n\n\n\n<div class=\"question\">\n<h2 class=\"question-title\"><\/h2>\n\n<div class=\"answer\">\n\n<div class=\"meta\">\n<img class=\"avatar\" src=\"https:\/\/pica.zhimg.com\/v2-d849d9cf1e4a011b3a3c467bdaf121d3_l.jpg?source=8673f162\">\n<span class=\"author\">霍与赫<\/span>\n<a href=\"https:\/\/www.zhihu.com\/question\/569319591\/answer\/2990847867\" class=\"originUrl\" hidden>查看知乎原文<\/a>\n<\/div>\n\n<div class=\"content\">\n<p>黑洞不是吸尘器。<\/p>\r\n<p>曾经见到一个外国教授用下面的这个双关语来描述黑洞,我觉得很赞。<\/p>\r\n<blockquote>black holes suck at sucking(黑洞"吸"得太"烂")。<\/blockquote>\r\n<p>取一个质量为太阳 3 倍的黑洞,再取一个同样质量的普通恒星。<\/p>\r\n<p>现在我问:哪个能更有效地捕捉物质?<\/p>\r\n<p>如果你说,"当然是黑洞",那对不起,回答错误。<\/p>\r\n<p>我来解释一下原因。<\/p>\r\n<p>那颗普通恒星的大小是我们太阳的三倍,直径大约为 200 万到 300 万公里。那是相当的大。靠近这颗恒星的任何东西:尘埃颗粒,流浪的小行星,甚至是因摄动而偏离轨道的行星或卫星,都可能最终与恒星表面碰撞,即落入恒星中。换句话说,如果你把这颗恒星想象成一个靶子,它是一个非常非常非常大的靶子,即使你没有太精确地瞄准,也非常容易击中。<\/p>\r\n<p>但是我们来看看黑洞。那个三个太阳质量的黑洞的视界半径不到 10 公里。任何以更大一些的距离经过黑洞的东西都不会落入黑洞。当然,如果一颗行星靠得足够近,它会被潮汐力撕裂,至少行星上的一些物质会落入黑洞,但即使这样,行星也需要靠得相当近。与上面那个几百万公里的靶子相比简直就是小巫见大巫。<\/p>\r\n<p>当然,随着黑洞的成长,它的视界半径也会增加。所以就变得更容易瞄准。然而与此同时,潮汐力在其活动视界附近减弱,这意味着经过视界的物体有更大的机会不被撕裂。<\/p>\r\n<p>看看潜伏在银河系中心区域的黑洞。它大约有 400 万个太阳那么重。这意味着它的视界约为 1200 万公里。这与我们的太阳相比是很大的(几乎大 20 倍)。但是从大尺度来看,这仍然是一个极其微小的目标。<\/p>\r\n<p>银河系中的大多数恒星都不会有任何靠近这个黑洞几千光年(或者说,几亿亿公里)的危险。<\/p>\r\n<p>所以,在很多星系中心区域发现的超大质量黑洞绝对不会把各自的星系都吃掉。星系动力学不是这样的。这也是超大质量黑洞的成因至今为止仍然是个未解之谜的原因之一,因为理论上黑洞周围并没有太多美食可以让它们在短短 100 多亿年的时光里变得那么胖。<\/p>\r\n<p>对了,虽然这些黑洞在中心区域,但它们不是"中心"。当然,对于像星系这样的不规则形状,"中心"本身是很难定义的。我是指,"在中心"不意味着星系中的所有恒星都在围绕着这个黑洞旋转,即使是最大的超大质量黑洞也不是这样。尽管超大质量黑洞很大,但它们的质量与星系本身的质量相比就相形见绌了。因此,除了那些"生活"在黑洞附近的极个别恒星之外,星系中绝大多数恒星的轨道主要不是由黑洞决定的,而是由星系中的物质总量决定的,而黑洞只占其中的一小部分。<\/p>\r\n<p>只有在非常长的时间里(我不是指几亿年这样的短时间,而是像亿亿年,亿亿亿年或更多),星系中的许多恒星会被超大质量黑洞所捕获。但仍然会有许多其他的恒星可能"叛离"母星系最终逃避掉被吞噬的命运。<\/p>\n<\/div>\n<\/div>\n\n\n<div class=\"view-more\"><a href=\"https:\/\/www.zhihu.com\/question\/569319591\">查看知乎讨论<span class=\"js-question-holder\"><\/span><\/a><\/div>\n\n<\/div>\n\n\n<\/div>\n<\/div><script type="text\/javascript">window.daily=true<\/script>",
"image_hue": "0xb37d7d",
"title": "为什么黑洞吞噬其他天体是罕见的行为,不是有什么东西靠近他都会被吞噬掉吗?",
"url": "https:\/\/daily.zhihu.com\/story\/9763920",
"image": "https:\/\/pic1.zhimg.com\/v2-733f354a20e354ad92dc370a1e395a86.jpg?source=8673f162",
"share_url": "http:\/\/daily.zhihu.com\/story\/9763920",
"js": [],
"ga_prefix": "072407",
"images": [
"https:\/\/pica.zhimg.com\/v2-75e7c4926929c2161181996d5937ebfe.jpg?source=8673f162"
],
"type": 0,
"id": 9763920,
"css": [
"http:\/\/news-at.zhihu.com\/css\/news_qa.auto.css?v=4b3e3"
]
} */
onBeforeMount(async () => {
try {
let data = await API.queryNewsInfo(newsId)//data由接口得来,data在上方。
state.info = Object.freeze(data) //得到首页数据。
// 把首页数据中的css样式填充进去。
handleInfoStyle(state.info?.css?.[0])
} catch (error) {
console.log(`error:-->`, error)
}
})
</script>
渲染头图
- 等待组件更新完毕之后,再进行更新外部传入的html结构。
vue
<script setup>
const state = reactive({
info: null,
extra: null
})
const handleHeaderImage = () => {
const holderBox = document.querySelector('.img-place-holder')
if (!holderBox) {
return
}
let imgTemp = new Image()
imgTemp.src = state.info.image
imgTemp.onload = () => {
holderBox.appendChild(imgTemp)
}
console.log(`[holderBox]-->`, [holderBox])
imgTemp.onerror = () => {
const p = holderBox.parentNode
console.log(`[p]-->`, [p])
p.parentNode.removeChild(p)
imgTemp = null
}
}
/* data = {
"body": "<div class=\"main-wrap content-wrap\">\n<div class=\"headline\">\n\n<div class=\"img-place-holder\"><\/div>\n\n\n\n<\/div>\n\n<div class=\"content-inner\">\n\n\n\n\n<div class=\"question\">\n<h2 class=\"question-title\"><\/h2>\n\n<div class=\"answer\">\n\n<div class=\"meta\">\n<img class=\"avatar\" src=\"https:\/\/pica.zhimg.com\/v2-d849d9cf1e4a011b3a3c467bdaf121d3_l.jpg?source=8673f162\">\n<span class=\"author\">霍与赫<\/span>\n<a href=\"https:\/\/www.zhihu.com\/question\/569319591\/answer\/2990847867\" class=\"originUrl\" hidden>查看知乎原文<\/a>\n<\/div>\n\n<div class=\"content\">\n<p>黑洞不是吸尘器。<\/p>\r\n<p>曾经见到一个外国教授用下面的这个双关语来描述黑洞,我觉得很赞。<\/p>\r\n<blockquote>black holes suck at sucking(黑洞"吸"得太"烂")。<\/blockquote>\r\n<p>取一个质量为太阳 3 倍的黑洞,再取一个同样质量的普通恒星。<\/p>\r\n<p>现在我问:哪个能更有效地捕捉物质?<\/p>\r\n<p>如果你说,"当然是黑洞",那对不起,回答错误。<\/p>\r\n<p>我来解释一下原因。<\/p>\r\n<p>那颗普通恒星的大小是我们太阳的三倍,直径大约为 200 万到 300 万公里。那是相当的大。靠近这颗恒星的任何东西:尘埃颗粒,流浪的小行星,甚至是因摄动而偏离轨道的行星或卫星,都可能最终与恒星表面碰撞,即落入恒星中。换句话说,如果你把这颗恒星想象成一个靶子,它是一个非常非常非常大的靶子,即使你没有太精确地瞄准,也非常容易击中。<\/p>\r\n<p>但是我们来看看黑洞。那个三个太阳质量的黑洞的视界半径不到 10 公里。任何以更大一些的距离经过黑洞的东西都不会落入黑洞。当然,如果一颗行星靠得足够近,它会被潮汐力撕裂,至少行星上的一些物质会落入黑洞,但即使这样,行星也需要靠得相当近。与上面那个几百万公里的靶子相比简直就是小巫见大巫。<\/p>\r\n<p>当然,随着黑洞的成长,它的视界半径也会增加。所以就变得更容易瞄准。然而与此同时,潮汐力在其活动视界附近减弱,这意味着经过视界的物体有更大的机会不被撕裂。<\/p>\r\n<p>看看潜伏在银河系中心区域的黑洞。它大约有 400 万个太阳那么重。这意味着它的视界约为 1200 万公里。这与我们的太阳相比是很大的(几乎大 20 倍)。但是从大尺度来看,这仍然是一个极其微小的目标。<\/p>\r\n<p>银河系中的大多数恒星都不会有任何靠近这个黑洞几千光年(或者说,几亿亿公里)的危险。<\/p>\r\n<p>所以,在很多星系中心区域发现的超大质量黑洞绝对不会把各自的星系都吃掉。星系动力学不是这样的。这也是超大质量黑洞的成因至今为止仍然是个未解之谜的原因之一,因为理论上黑洞周围并没有太多美食可以让它们在短短 100 多亿年的时光里变得那么胖。<\/p>\r\n<p>对了,虽然这些黑洞在中心区域,但它们不是"中心"。当然,对于像星系这样的不规则形状,"中心"本身是很难定义的。我是指,"在中心"不意味着星系中的所有恒星都在围绕着这个黑洞旋转,即使是最大的超大质量黑洞也不是这样。尽管超大质量黑洞很大,但它们的质量与星系本身的质量相比就相形见绌了。因此,除了那些"生活"在黑洞附近的极个别恒星之外,星系中绝大多数恒星的轨道主要不是由黑洞决定的,而是由星系中的物质总量决定的,而黑洞只占其中的一小部分。<\/p>\r\n<p>只有在非常长的时间里(我不是指几亿年这样的短时间,而是像亿亿年,亿亿亿年或更多),星系中的许多恒星会被超大质量黑洞所捕获。但仍然会有许多其他的恒星可能"叛离"母星系最终逃避掉被吞噬的命运。<\/p>\n<\/div>\n<\/div>\n\n\n<div class=\"view-more\"><a href=\"https:\/\/www.zhihu.com\/question\/569319591\">查看知乎讨论<span class=\"js-question-holder\"><\/span><\/a><\/div>\n\n<\/div>\n\n\n<\/div>\n<\/div><script type="text\/javascript">window.daily=true<\/script>",
"image_hue": "0xb37d7d",
"title": "为什么黑洞吞噬其他天体是罕见的行为,不是有什么东西靠近他都会被吞噬掉吗?",
"url": "https:\/\/daily.zhihu.com\/story\/9763920",
"image": "https:\/\/pic1.zhimg.com\/v2-733f354a20e354ad92dc370a1e395a86.jpg?source=8673f162",
"share_url": "http:\/\/daily.zhihu.com\/story\/9763920",
"js": [],
"ga_prefix": "072407",
"images": [
"https:\/\/pica.zhimg.com\/v2-75e7c4926929c2161181996d5937ebfe.jpg?source=8673f162"
],
"type": 0,
"id": 9763920,
"css": [
"http:\/\/news-at.zhihu.com\/css\/news_qa.auto.css?v=4b3e3"
]
} */
onBeforeMount(async () => {
try {
let data = await API.queryNewsInfo(newsId)//data由接口得来,data在上方。
state.info = Object.freeze(data) //得到首页数据。
nextTick(handleHeaderImage)
} catch (error) {
console.log(`error:-->`, error)
}
})
</script>
点击事件延时问题
js
yarn add fastclick
-
src/main.js
jsimport { FastClick } from 'fastclick' import { createApp } from 'vue' // 创建应用 const app = createApp(App) // 解决click 300ms延迟问题。 FastClick.attach(document.body) app.mount('#app')
核心
jsimport { FastClick } from 'fastclick' // 解决click 300ms延迟问题。 FastClick.attach(document.body)
登录页
特定请求添加token
-
全部代码
-
src/api/index.js
jsimport http from './http' // 获取最新的新闻信息 const queryNewsLatest = () => http.get('/news_latest') // 获取以往的新闻信息 const queryNewsBefore = (time) => { return http.get('/news_before', { params: { time } }) } // 获取新闻的详细信息 const queryNewsInfo = (id) => { return http.get('/news_info', { params: { id } }) } // 获取新闻的点赞信息 const queryStoryExtra = (id) => { return http.get('/story_extra', { params: { id } }) } // 用户登录 const userLogin = (phone, code) => { return http.post('/login', { phone, code }) } // 发送验证码 const userSendCode = (phone) => { return http.post('/phone_code', { phone }) } // 获取登录者信息 const userInfo = () => http.get('/user_info') // 收藏新闻 const storeAdd = (nwesId) => { return http.post('/store', { nwesId }) } // 移除收藏 const storeRemove = (id) => { return http.get('/store_remove', { params: { id } }) } // 获取收藏列表 const storeList = (id) => { return http.get('/store_list') } /* 暴露API */ const API = { queryNewsLatest, queryNewsBefore, queryNewsInfo, queryStoryExtra, userLogin, userSendCode, userInfo, storeAdd, storeRemove, storeList, } export default API
-
src/api/http.js
jsimport axios from 'axios' import qs from 'qs' import { showNotify } from 'vant' import { isPlainObject } from 'lodash' import utils from '@/assets/utils' const http = axios.create({ baseURL: '/api', timeout: 60000, transformRequest: data => { if (isPlainObject(data)) return qs.stringify(data) return data } }) const safeList = ['/user_info', '/store', '/store_remove', '/store_list'] http.interceptors.request.use(config => { if (safeList.includes(config.url)) { // 请求头携带token。 const token = utils.storage.get('TK') if (token) { config.headers['authorzation'] = token } } return config }) http.interceptors.response.use(response => { return response.data }, reason => { showNotify({ type: 'danger', message: '网络繁忙,稍后再试!', duration: 2000 }) return Promise.reject(reason) }) export default http
-
-
核心代码
-
src/api/index.js
jsimport http from './http' // 获取最新的新闻信息 const queryNewsLatest = () => http.get('/news_latest') // 获取以往的新闻信息 const queryNewsBefore = (time) => { return http.get('/news_before', { params: { time } }) } // 获取新闻的详细信息 const queryNewsInfo = (id) => { return http.get('/news_info', { params: { id } }) } // 获取新闻的点赞信息 const queryStoryExtra = (id) => { return http.get('/story_extra', { params: { id } }) } // 用户登录 const userLogin = (phone, code) => { return http.post('/login', { phone, code }) } // 发送验证码 const userSendCode = (phone) => { return http.post('/phone_code', { phone }) } // 获取登录者信息 const userInfo = () => http.get('/user_info') // 收藏新闻 const storeAdd = (nwesId) => { return http.post('/store', { nwesId }) } // 移除收藏 const storeRemove = (id) => { return http.get('/store_remove', { params: { id } }) } // 获取收藏列表 const storeList = (id) => { return http.get('/store_list') } /* 暴露API */ const API = { userLogin, userSendCode, userInfo, storeAdd, storeRemove, storeList, } export default API
-
src/api/http.js
jsimport axios from 'axios' import utils from '@/assets/utils' const http = axios.create({baseURL: '/api',}) const safeList = ['/user_info', '/store', '/store_remove', '/store_list'] http.interceptors.request.use(config => { if (safeList.includes(config.url)) { // 请求头携带token。 const token = utils.storage.get('TK') if (token) { config.headers['authorzation'] = token } } return config }) export default http
-
规则校验
- 表单校验总结:
- 有一个form表单,它对应一些实例。
- form内部有form-item。
- 收集各个表单的name。
- 用v-model绑定到表单数据中。
- 有一个name。
- 规则校验
- 一般写到各个表单上。或者全部放在form上。
- 内置规则
- 自定义规则:正则或函数。
- 触发检验:
- 自动触发-用表单中的submit类型按钮点击后,执行。
- 手动触发-通过表单实例。
-
通过内置校验进行校验
-
通过表单实例进行校验
-
src/views/Login.vue
vue
<script setup>
// import NavBack from "@/components/NavBack.vue"
import useAutoImport from '@/useAutoImport'
const { reactive, ref, onUnmounted } = useAutoImport()
const { API, router, route, utils } = useAutoImport()
const { showSuccessToast, showFailToast } = useAutoImport()
// 定义状态
const formIns = ref(null)
const state = reactive({
phone: '',
code: '',
btn: {
disabled: false,
text: '发送验证码'
}
})
// 发送验证码。
let timer = null
let count = 30
const handleSendCode = async () => {
try {
// 先对手机号进行检验。
await formIns.value.validate('phone')
// 向服务器发送请求。
let { code } = await API.userSendCode(state.phone)
if (+code === 0) {
// 开启倒计时。
state.btn.disabled = true
state.btn.text = '30s后重发'
timer = setInterval(() => {
if (count === 1) {
clearInterval(timer)
count = 30
state.btn.disabled = false
state.btn.text = '发送验证码'
return
}
count--
state.btn.text = `${count}s后重发`
}, 1000)
return
}
showFailToast('发送失败,稍后再试')
} catch (error) {
console.log(`error:-->`, error)
}
}
onUnmounted(() => {
clearInterval(timer)
})
// 登录提交
const submit = async () => {
try {
await formIns.value.validate()
let { code, token } = await API.userLogin(state.phone, state.code)
if (+code !== 0) {
showFailToast('登录失败,请稍后再试')
return
}
// 登录成功:存储token,获取登录者信息、提示、跳转。
utils.storage.set('TK', token)
showSuccessToast('登录成功')
} catch (error) {
console.log(`error:-->`, error)
}
}
</script>
<template>
<nav-back title="登录/注册" />
<van-form ref="formIns" validate-first>
<van-cell-group inset>
<van-field
center
label="手机号"
label-width="50px"
name="phone"
v-model.trim="state.phone"
:rules="[
{ required: true, message: '手机号是必填项' },
{ pattern: /^(?:(?:\+|00)86)?1\d{10}$/, message: '手机号格式不正确' }
]"
>
<template #button>
<van-button
class="form-btn"
size="small"
type="primary"
loading-text="处理中"
:rules="[
{ required: true, message: '手机号是必填项' },
{ pattern: /^\d{6}$/, message: '手机号格式不正确' }
]"
:disabled="state.btn.disabled"
@click="handleSendCode"
>
{{ state.btn.text }}
</van-button>
</template>
</van-field>
<van-field label="验证码" label-width="50px" name="code" v-model.trim="state.code" />
</van-cell-group>
<div style="margin: 20px 40px">
<van-button round block type="primary" loading-text="正在处理中..." @click="submit">
立即登录/注册
</van-button>
</div>
</van-form>
</template>
发送验证码
- 对手机号单独做校验-表单的手动校验。
- 向服务器发送请求。
- 禁止用户点击,同时开启定时器。
- src/views/Login.vue
vue
<script setup>
// import NavBack from "@/components/NavBack.vue"
import useAutoImport from '@/useAutoImport'
const { reactive, ref, onUnmounted } = useAutoImport()
const { API, router, route, utils } = useAutoImport()
const { showSuccessToast, showFailToast } = useAutoImport()
// 定义状态
const formIns = ref(null)
const state = reactive({
phone: '',
code: '',
btn: {
disabled: false,
text: '发送验证码'
}
})
// 发送验证码。
let timer = null
let count = 30
const handleSendCode = async () => {
try {
// 先对手机号进行检验。
await formIns.value.validate('phone')
// 向服务器发送请求。
let { code } = await API.userSendCode(state.phone)
if (+code === 0) {
// 开启倒计时。
state.btn.disabled = true
state.btn.text = '30s后重发'
timer = setInterval(() => {
if (count === 1) {
clearInterval(timer)
count = 30
state.btn.disabled = false
state.btn.text = '发送验证码'
return
}
count--
state.btn.text = `${count}s后重发`
}, 1000)
return
}
showFailToast('发送失败,稍后再试')
} catch (error) {
console.log(`error:-->`, error)
}
}
onUnmounted(() => {
clearInterval(timer)
})
// 登录提交
const submit = async () => {
try {
await formIns.value.validate()
let { code, token } = await API.userLogin(state.phone, state.code)
if (+code !== 0) {
showFailToast('登录失败,请稍后再试')
return
}
// 登录成功:存储token,获取登录者信息、提示、跳转。
utils.storage.set('TK', token)
showSuccessToast('登录成功')
} catch (error) {
console.log(`error:-->`, error)
}
}
</script>
<template>
<nav-back title="登录/注册" />
<van-form ref="formIns" validate-first>
<van-cell-group inset>
<van-field
center
label="手机号"
label-width="50px"
name="phone"
v-model.trim="state.phone"
:rules="[
{ required: true, message: '手机号是必填项' },
{ pattern: /^(?:(?:\+|00)86)?1\d{10}$/, message: '手机号格式不正确' }
]"
>
<template #button>
<van-button
class="form-btn"
size="small"
type="primary"
loading-text="处理中"
:rules="[
{ required: true, message: '手机号是必填项' },
{ pattern: /^\d{6}$/, message: '手机号格式不正确' }
]"
:disabled="state.btn.disabled"
@click="handleSendCode"
>
{{ state.btn.text }}
</van-button>
</template>
</van-field>
<van-field label="验证码" label-width="50px" name="code" v-model.trim="state.code" />
</van-cell-group>
<div style="margin: 20px 40px">
<van-button round block type="primary" loading-text="正在处理中..." @click="submit">
立即登录/注册
</van-button>
</div>
</van-form>
</template>
<style lang="less" scoped>
.van-form {
margin-top: 30px;
.form-btn {
width: 78px;
}
}
</style>
提交表单信息
- 对表单进行校验。
- 发送请求。
- 登录成功:存储token、进行提示。
- 获取登录者信息、进行页面的跳转。
jsx组件
-
把后缀名改为
jsx
。-
src/components/ButtonAgain.jsx
jsxconst ButtonAgain = <div>哈哈</div> export default ButtonAgain
-
src/components/ButtonAgain.jsx
jsxexport default function ButtonAgain() { return <div>哈哈</div> }
-
src/components/ButtonAgain.jsx
jsximport { Button } from "vant" export default function ButtonAgain() { return <Button>哈哈</Button> }
-
-
使用时导入并使用就好了。
-
src/views/Login.vue
vue<script setup> import ButtonAgain from '@/components/ButtonAgain.jsx' </script> <template> <ButtonAgain></ButtonAgain> </template>
-
注册jsx全局组件
- src/components/ButtonAgain.jsx
jsx
import { Button } from 'vant'
export default function ButtonAgain(props, context) {
return <Button>哈哈</Button>
}
- src/global.js
js
import ButtonAgain from './components/ButtonAgain'
export default function global(app) {
app.component('ButtonAgain', ButtonAgain)
}
- src/views/Login.vue
vue
<script setup>
</script>
<template>
<button-again></button-again>
<ButtonAgain></ButtonAgain>
</template>
button按钮loading封装
vue的jsx语法
和react的
不太一样。
- src/components/ButtonAgain.jsx 不用计算属性。
jsx
import { Button } from 'vant'
import { ref, useAttrs, useSlots } from 'vue'
// 把传递的属性,去除特殊的,其余的都赋值给Vant内部的组件
const filter = (attrs) => {
let props = {}
Reflect.ownKeys(attrs).forEach((key) => {
if (key === 'loading' || key === 'onClick') return
props[key] = attrs[key]
})
return props
}
const ButtonAgain = {
inheritAttrs: false,
setup() {
const attrs = useAttrs(),
slots = useSlots()
// 自己控制loading效果
const loading = ref(false)
const handle = async (ev) => {
loading.value = true
try {
await attrs.onClick(ev)
} catch (_) {}
loading.value = false
}
return () => {
let props = filter(useAttrs())//更新时调用。
return (
<Button {...props} loading={loading.value} onClick={handle}>
{slots.default()}
</Button>
)
}
}
}
export default ButtonAgain
- src/components/ButtonAgain.jsx 使用计算属性之后。
jsx
import { Button } from 'vant'
import { ref, useAttrs, useSlots, computed } from 'vue'
const ButtonAgain = {
inheritAttrs: false,
setup(theProps, context) {
// console.log(`theProps-->`, theProps)
// console.log(`context-->`, context)
const attrs = useAttrs()
const slots = useSlots()
const props = computed(() => {
let theProp = {}
Reflect.ownKeys(attrs).forEach((key) => {
if (key === 'loading' || key === 'onClick') {
return
}
theProp[key] = attrs[key]
})
return theProp
})
const loading = ref(false)
const handle = async (ev) => {
loading.value = true
console.log(`1. loading-->`, loading)
try {
await attrs.onClick(ev)
} catch (error) {
console.log(`error:-->`, error)
}
loading.value = false
console.log(`2. loading.value-->`, loading.value)
}
return () => {
return (
<Button {...props.value} loading={loading.value} onClick={handle}>
{slots.default()}
</Button>
)
}
}
}
export default ButtonAgain