20230725----重返学习-vue3项目实战-知乎日报第2天

day-120-one-hundred-and-twenty-20230725-vue3项目实战-知乎日报第2天

vue3项目实战-知乎日报第2天

新闻列表

  1. 每一天的新闻都是一个可遍历的区块,一天中的新闻有多个新闻,每一个新闻都是一个组件。
  2. 用到的日期要转格式。
  3. 组件要注册为全局组件。可通过插件把components目录中的组件变成全局组件。也可以自己注册全局组件-更灵活,也可添加函数式调用的组件。
  4. 骨架屏用v-ifv-else来区分,非一个独立的根节点的地方,用template标签包起来。
  • src/views/Home.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>
组件封装
  1. 决定组件怎样用,是函数式组件还是模板式组件。
  2. 组件的复杂与灵活度,决定是jsx语法还是模板语法
全局属性和全局方法
  • src/main.js

    import { 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

    import '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

    <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图片懒加载
触底加载
方法自动导入
  • 有两种方案。
  1. 用一个组件,把vuevue-router,优势在于可以直接使用。但没提示信息,写代码起来会麻烦。同时给不了解的人一些疑惑,不清楚那些方法是从那里导入的。

  2. 用自定义hook。优势在于有提示信息,同时更灵活,没有额外学习成本。

    • src/useAutoImport.js

      import * 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

      <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
  1. 一般有业务的组件之类的方法和状态中,可以使用自定义hooks。
  2. 一般通用逻辑中,也用的自定义hooks。
  • src/useAutoImport.js 定义自定义hooks

    import * 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

    <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>
    
组件缓存
  1. vue3中组件缓存用keep-alive,不过语法和vue2有区别。
  • 新写法可以。

    • src/App.vue

      <template>
        <router-view v-slot="{ Component }">
          <keep-alive include="Home">
            <component :is="Component" />
          </keep-alive>
        </router-view>
      </template>
      
    • src/views/Home.vue

      <script setup>
      defineOptions({
        name: 'Home'
      })
      </script>
      
  • 旧写法不行。

    • src/App.vue

      <template>
        <keep-alive include="Home">
          <router-view></router-view>
        </keep-alive>
      </template>
      
    • src/views/Home.vue

      <script setup>
      defineOptions({
        name: 'Home'
      })
      </script>
      

详情页

并行请求
<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>
动态加载样式
  1. 创建一个link标签。
<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结构。
<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>
点击事件延时问题
yarn add fastclick
  • src/main.js

    import { FastClick } from 'fastclick'
    
    import { createApp } from 'vue'
    // 创建应用
    const app = createApp(App)
    
    // 解决click 300ms延迟问题。
    FastClick.attach(document.body)
    
    app.mount('#app')
    

    核心

    import { FastClick } from 'fastclick'
    
    // 解决click 300ms延迟问题。
    FastClick.attach(document.body)
    

登录页

特定请求添加token
  • 全部代码

    • src/api/index.js

      import 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

      import 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

      import 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

      import 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
      
规则校验
  1. 表单校验总结:
    • 有一个form表单,它对应一些实例。
    • form内部有form-item。
    • 收集各个表单的name。
    • 用v-model绑定到表单数据中。
    • 有一个name。
    • 规则校验
      • 一般写到各个表单上。或者全部放在form上。
      • 内置规则
      • 自定义规则:正则或函数。
    • 触发检验:
      • 自动触发-用表单中的submit类型按钮点击后,执行。
      • 手动触发-通过表单实例。
  • 通过内置校验进行校验

  • 通过表单实例进行校验

  • src/views/Login.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>
发送验证码
  1. 对手机号单独做校验-表单的手动校验。
  2. 向服务器发送请求。
  3. 禁止用户点击,同时开启定时器。
  • src/views/Login.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>

提交表单信息
  1. 对表单进行校验。
  2. 发送请求。
  3. 登录成功:存储token、进行提示。
  4. 获取登录者信息、进行页面的跳转。
jsx组件
  1. 把后缀名改为jsx

    • src/components/ButtonAgain.jsx

      const ButtonAgain = <div>哈哈</div>
      export default ButtonAgain
      
    • src/components/ButtonAgain.jsx

      export default function ButtonAgain() {
        return <div>哈哈</div>
      }
      
    • src/components/ButtonAgain.jsx

      import { Button } from "vant"
      
      export default function ButtonAgain() {
        return <Button>哈哈</Button>
      }
      
  2. 使用时导入并使用就好了。

    • src/views/Login.vue

      <script setup>
      
      import ButtonAgain from '@/components/ButtonAgain.jsx'
      </script>
      
      <template>
        <ButtonAgain></ButtonAgain>
      </template>
      
      
注册jsx全局组件
  • src/components/ButtonAgain.jsx
import { Button } from 'vant'

export default function ButtonAgain(props, context) {
  return <Button>哈哈</Button>
}
  • src/global.js
import ButtonAgain from './components/ButtonAgain'
export default function global(app) {
    app.component('ButtonAgain', ButtonAgain)
}
  • src/views/Login.vue
<script setup>
</script>

<template>
  <button-again></button-again>
  <ButtonAgain></ButtonAgain>
</template>

button按钮loading封装

  1. vue的jsx语法react的不太一样。
  • src/components/ButtonAgain.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 使用计算属性之后。
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

进阶参考

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值