极客

一个跟着尚硅谷做的小项目

主要涉及的知识点如下:

智慧商城

emm,看黑马vue视频跟着做的vue2项目,应该只能算是练手项目。

一个基于vue2,axiso和vant2开发的移动商城项目

链接:https://www.sanye.space/projects/wisdomShop

大事件

链接:https://www.sanye.space/projects/bigEvent

这个项目涉及到的主要知识点,都记录在笔记《vue》中了。

这个项目主要是训练使用element-plus组件库编写项目的能力,熟悉富文本编辑器,husky,eslint和prettier的使用,本身不是一个复杂的项目,所以也不会放到简历中。

如何将网络图片转化成file对象?

因为我们展示文章图片的时候,使用的是网络图片,但是我们修改后,提交的图片格式是file对象,这就意味着如果我们不修改文章图片,直接提交的就是网络图片,这是不符合接口规范的,随意如何如何将网络图片转化成file对象?

import axios from 'axios'
export const imageUrlToFile = async (url, fileName) => {
  try {
    // 第一步:使用axios获取网络图片数据
    const response = await axios.get(url, { responseType: 'arraybuffer' })
    console.log('response', response)
    const imageData = response.data

    // 第二步:将图片数据转换为Blob对象
    const blob = new Blob([imageData], {
      type: response.headers['content-type']
    })

    // 第三步:根据创建好的blob对象,创建一个新的File对象
    const file = new File([blob], fileName, { type: blob.type })
    // 返回的是一个promise对象
    return file
  } catch (error) {
    console.error('将图片转换为File对象时发生错误:', error)
    throw error
  }
}

如何做分页?

在图书管理页面做了一个分页功能,它是如何实现的?

小兔鲜

介绍

itheima的vue3项目

链接:https://www.sanye.space/projects/rabbit

项目结构

布局页面

布局页面除了有一个router-view来展示其他二级页面组件,还要一些固定不变的组件。

fixedNav

其中比较有意思的就是fixedNav,监听页面滚动事件,当html顶部滚动出可视区域的高度(html.scrollTop)超过一定的值的时候,添加show样式,让fixedNav显示出来。

<div :class="{ fixedHeader: true, show: y >= 80 }">
.fixedHeader[data-v-e9857545] {
    width: 100%;
    position: fixed;
    left: 0;
    top: 0;
    transform: translateY(-100%);
    background-color: #fff;
    transition: all .3s linear;
    z-index: 999;
}
.show[data-v-e9857545] {
    transform: none;
    opacity: 1;
}
let y = ref(0)
const html = document.documentElement
window.addEventListener('scroll', () => {
  y.value = html.scrollTop
})

购物车图标

还有就是Nav组件的购物车图标,鼠标放到上面会显示购物车列表,这个效果实现核心如下:

.layer {
   transform-origin: top;//变换的中心是top
   transform: scale(1, 0);//x轴不变,y轴缩放为0,由于高度为0了,元素自然就隐藏了
   transition: all 0.4s 0.2s;//添加过渡,过渡事件是0.4s,延迟0.2秒开始动画
}

删除效果

同时还对删除购物车商品做了动画效果,道理也是一样的。

.del {
  transform: translateX(100%);
  opacity: 0;
}

组件划分

因为这些组件只会在layout页面中用到,所以被放到layout/components目录下。

虽然说一个页面对应一个组件,但是为了降低项目的耦合度,把一个页面分解为多个组件才比较合理。

home页面

业务逻辑

home页面无非就是通过api获取数据然后渲染页面,逻辑较为简单。

商品组件的动态效果

比较有意思的就是商品组件hover后的样式:

.goods-item {
  display: block;
  width: 220px;
  padding: 20px 30px;
  text-align: center;
  border-radius: 4px;
  transition: all 0.5s;

  &:hover {
    transform: translate(0, -3px);//向上平移3px
    box-shadow: 0 3px 8px rgb(0 0 0 / 20%);//展示阴影
  }
}

透明度:

box-shadow

商品详情页面

beforeRouteUpdate

当在商品详情组件中点击其他商品链接,但是跳转的组件还是商品详情组件,这就会导致组件被复用,内部js代码不会执行,组件就不会更新。使用组件内的beforeRouteUpdate路由守卫,来解决这个问题。

onBeforeRouteUpdate(async (to) => {
  //初始化商品数量,规格对象
  num.value = 1
  skuObj.value = {}
  //根据新的id获取新的商品信息
  getDetails(to.params.id)
  //修改热榜商品查询信息
  params.id = to.params.id
  params2.id = to.params.id
  //发送请求修改数据
  getHotGoodsDay(params)
  getHotGoodsWeek(params2)
})

小图片

还要一个难点就是放大镜,感觉就是在炫技;其中的小图片部分也很有意思:

 <ul class="small-image">
    <li
         v-for="(item, index) in picList"
         :key="index"
         :class="{ active: index === activeIndex }"
         @mouseover="activeIndex = index"
    >
        <img :src="item" alt="" />
    </li>
</ul>

可以看出,监听limouseover事件,这是一个dom原生事件,监听对象也确实是个dom对象,就好比:

<input @input='(e)=>{console.log(e.target.value)}'>

放大镜

然后就是具体的放大镜部分,项目中使用的是vueuse的api,但是要是面试问这个问的肯定是自己如何实现,所以我直接自己实现了

//捕获要检测对象
const target = ref(null)
//数据是响应式的
function mouseInElement(target) {
  //使用onMounted,确保能捕获到dom
  onMounted(() => {
    target.value.addEventListener('mousemove', (e) => {
      const rect = target.value.getBoundingClientRect()
      console.log(e.clientX - rect.left, e.clientY - rect.top)
    })
    target.value.addEventListener('mouseleave', () => {
      console.log('出去了')
    })
  })
}
mouseInElement(target)

调用一个dom元素的getBoundingClientRect()方法,会返回一个domrect对象。

总之就是通过实时获取鼠标在dom元素中的位置,来不断修改放大镜组件中的背景图片的位置。

还有一个组件就是商品规格组件,选择用户选择了特定的商品规格就会导出一个规格对象,但是这个组件实现的原理如何,没有去了解。

其实还有一种更为简洁的实现方式

//捕获要检测对象
const target = ref(null)
//数据是响应式的
function mouseInElement(target) {
  //使用onMounted,确保能捕获到dom
  onMounted(() => {
    target.value.addEventListener('mousemove', (e) => {
      //e.offsetX, e.offsetY是鼠标相对于目标元素左上角的的位置
      console.log(e.offsetX, e.offsetY)
    })
    target.value.addEventListener('mouseleave', () => {
      console.log('出去了')
    })
  })
}
mouseInElement(target)

e.offsetX, e.offsetY是鼠标相对于目标元素左上角的的位置,但是由于目标元素是蒙层小图片,但是我们期望目标元素是商品图片,所以还要给蒙层小图片添加css样式:point-events:none,让鼠标时间穿透到商品图片,这样商品图片就能成为目标元素。

sku组件

实现步骤如下:

二级分类页面

一级分类页面就是通过ajax请求获取数据,然后渲染页面,没什么好说的;二级分类页面的特点就是实现了按条件筛选商品,并且借助elementplus提供的全局指令v-infinite-scroll实现了商品列表的无限滚动:

<ul
  v-infinite-scroll="load"
  :infinite-scroll-disabled="disabled"
  class="body"
>
  <li v-for="item in list" :key="item.id">
    <router-link :to="`/detail/${item.id}`">
      <img v-lazy-load="item.picture" alt="" />
      <p class="name ellipsis">{{ item.name }}</p>
      <p class="desc ellipsis">{{ item.desc }}</p>
      <p class="price">{{ item.price }}</p>
    </router-link> 
  </li>
</ul>

如果面试要问肯定就是问这个指令的实现原理:

就是监听ul的触底事件,然后修改查询参数(page++),发送请求获取新的商品数据,与原来旧的数据拼接。

登录页面

表单校验

登录页面使用elementplus提供的el-form组件简化了表单校验的过程,如果需要手动实现的话,考察的只是就是正则表达式了。

每个表单项可以对应多个校验规则。

const rules = {
  account: [
    { required: true, message: '用户名不能为空', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,//正则表达式
      message: '密码必须是6-15位的非空字符', //提示的消息
      trigger: 'blur' //触发表单校验的方式是光标消失
    }
  ],
  private: [
    {
      //自定义表单校验规则
      validator: (rule, value, callback) => {
        if (formModel.value.checked == false) {
          callback(new Error('请勾选协议!'))
        } else {
          callback()
        }
      },
      trigger: 'change' //触发validator方式是表单的值改变
    }
  ]
}

订单页面

比较有意思的就是点击切换配送时间,支付方式等,原理都是一样的:

<span
  v-for="(item, index) in payment"
  :key="index"
  :class="{ active: activeIndex2 === index }"
  @click="activeIndex2 = index"
>{{ item }}</span>

支付页面

倒计时

export const countDown = () => {
  const router = useRouter()
  const Time = ref(0)
  // 格式化后的时间,计算属性是惰性取值的,
  // 当计算属性依赖的响应式数据改变的时候,不会调用计算属性 Watcher 的 getter 更新值,而是更新watcher.dirty属性为true
  // 然后下次使用计算属性的时候,再重新计算值
  const formatTime = computed(() => dayjs.unix(Time.value).format('mm分ss秒'))
  const start = (time) => {
    Time.value = time  //设置倒计时的总时长
    let n = setInterval(() => {
      Time.value--
      if (Time.value == 0) {
        clearInterval(n) //关闭定时器
        ElMessage.error('订单超时')
        router.push('/cartList') //提示订单超时然后跳转到购物车
      }
    }, 1000)
  }
  return { formatTime, start }
}

购物车页面

在这个项目中,还持久化存储了商品信息,这样刷新,关闭页面,购物车中的商品数据也不会丢失,用户在未登录的时候也能操作购物车。只需要在登录的时候,将本地购物车商品数据合并到登录账号下的远程购物车就行。

当我在这么介绍的时候面试管问我,将商品数据存储在购物车不会有安全问题吗?确实购物车商品数据存储在本地,可以通过js修改商品价格,但是最终订单的生成,是后端通过选中的商品id对应的商品数量计算得到的,并没有太大的安全问题。

咸虾米壁纸

链接:https://www.sanye.space/projects/wallpaper.apk

介绍

基于vue3+uni-app开发的移动端壁纸应用,点击缩略图能进入到预览页面查看大图。

但是由于部署到github pages上,获取壁纸链接的请求就跨域了,就没有部署。

项目结构

home页面

category分类页面

没啥好说的,就是发请求获取数据然后复用theme-item组件,然后使用grid来布局这个组件,顺便使用uni-app提供的onPullDownRefreshonReachBottom钩子实现了下拉刷新触底加载,面试官要问就是问你自己如何实现这个。

下拉刷新

触底加载

user页面

功能包括显示用户ip,查看我的下载,我的收藏,联系客服等。

比较有意思的就是如何获取用户ip吧,虽然在这个项目中是后端服务器实现的。

wallpaperList页面

我的收藏,我的下载,图片搜索等所有需要展示缩略图的页面(除了home页面),复用的都是这个页面(其实就是wallpaperList组件)。

根据跳转到这个页面传递的参数不同(通过onLoad接收),来展示不同的navigationBar,来发送不同的请求。其实就类似动态路由参数

然而所有需要展示缩略图的页面,都需要通过修改缩略图链接得到大图链接,然后为了在preview页面展示(页面间数据共享),所以需要把图片对象数组存储在pinia仓库中,然后点击缩略图查看大图的时候(跳转到preview页面),就直接使用pinia中的数据。

因为所有需要展示缩略图的页面不会同时存在,所以我们只设置一个picList,只需要确保当前处在某个展示缩略图的页面的时候,仓库中的picList中存储的是该页面的图片对象数组

//放在home页面,如果home页面没销毁则直接使用页面组件内的recommendList.value
//如果页面组件被销毁,重新切换到home,由于这个操作会先于请求的数据到达被执行
//所以最后存储进仓库的还是通过请求返回的数据
onShow(() => {
    usePic.setPicList(recommendList.value)
})

而且这样有个好处就是,preview页面不需要纠结到底应该从那个页面的picList中取数据展示,因为这些页面的图片对象数组都存储在picList中。

notice页面

主要用来展示公告详情的页面,大概也就是通过请求获取数据然后渲染页面,使用了uni-dateformat来格式化事件,使用rich-text组件来解析富文本。

preview页面

是这个项目功能最复杂的页面,实现了滑动,预览图片,查看图片详细信息,收藏图片,下载图片的功能。

主要是基于大小占满整个页面的swiper轮播图组件实现的(不需要自动轮播,移除autoplay,interval属性)。

有多个展示缩略图的页面,点击缩略图会都跳转到这里,从仓库中的picList中取数据(大图链接)来展示。

并且只渲染当前预览的图片和它左右的两张图片,减少了http请求的次数,图片的按需渲染的方式是通过v-if实现的。

<swiper-item v-for="(i,idx) in usePic.picList" :key="i._id">
     <image :src="i.bigPic" mode="aspectFill"
          v-if="index==idx||(index+1)%usePic.picList.length==idx||(idx+1)%usePic.picList.length==index">          </image>
</swiper-item>

同时添加了遮罩层mask(显示时间和功能组件),但是又为了让mask不影响轮播图滑动,让mask的高度为0。

图片详情信息被放在uni-popup组件中,给壁纸评分则是使用了uni-popup组件和uni-icons星标组件,评分好分后则发送请求到后端。

然后下载功能是整个项目实现起来最难的功能,因为为了考虑到用户的各种选择并提供良好的提示,调用了多次toast还有loading,除此之外还有各种通过回调实现的异步api,导致代码可读性极其的差(回调函数地狱问题),我尽量使用了promise来优化代码的可读性。

如果是H5端提示长按图片保存。

如果是非H5端,先获取图片的临时下载地址,再开始下载图片。

下载图片的过程,如果是非H5端,会先询问是否授予下载权限,如果授予成功并确认下载则图片下载完毕,同步到服务器。

如果授权失败或者取消下载,则下载失败。分析失败的原因,如果是取消下载,则提示“下载取消”,如果是授权失败,则提示用户给与下载权限,如果用户同意了,则打开权限设置面板。

关键的api

uni.getImageInfo({
      src: paperObj.value.bigPic,//可以是相对路径,临时文件路径,存储文件路径,网络图片路径
      success({
        path //图片的本地临时下载地址
      }) {//}
})
//H5不支持,微信小程序,app均支持,初次下载需要询问用户权限
uni.saveImageToPhotosAlbum({
	filePath:path,//图片的本地临时下载地,通过uni.getImageInfo得到
	success(){},
	fail(err){}//拒绝授权触发的回调函数,或者授权失败后无权限再下载的时候触发,或者取消下载也会触发
})
uni.openSetting({})//让用户设置权限,调起客户端小程序设置界面,返回用户设置的操作结果,配置属性只有那三个回调函数

search页面

可以根据用户输入的关键词搜索图片,并保存搜索记录。

面试:项目介绍

小兔鲜

页面情况

基于 vue3+Element-Plus+阿里图标,开发的网上商城项目;(一级页面包括布局页面和登录页面,二级页面包括 home 页面,分类页面,商品详情页面,购物车页面,订单页面,支付页面,用户页面;)

购物车页面

(当我在这么介绍的时候面试管问我,将商品数据存储在购物车不会有安全问题吗?确实购物车商品数据存储在本地,可以通过js修改商品价格,但是最终订单,是后端通过选中的商品id对应的商品数量生成的,并没有太大的安全问题。)

商品详情

放大镜

商品详情页面,实现商品图片的放大镜效果的时候

beforeRouteUpdate

当在商品详情组件中点击其他商品链接,但是跳转的组件还是商品详情组件,这就会导致组件被复用,内部js代码不会执行,组件就不会更新。使用组件内的beforeRouteUpdate路由守卫,来解决这个问题。

sku组件

商品列表

在展示商品列表的时候,使用自定义的 v-infinite指令,实现了商品列表瀑布流无限滚动的效果。

使用getBoundingClientRect来实现

const handler = []
window.addEventListener('scroll', () => {
  handler.forEach((obj) => {
    if (
      //这里给一个80是希望元素底部距离出现在视口还有 80px 的时候就获取新的数据,用户体验更好
      obj.el.getBoundingClientRect().bottom <=
      document.documentElement.clientHeight+80
    ) {
      obj.func()
    }
  })
})
export const infinite = {
  mounted(el, binding) {
    handler.push({ el, func: binding.value })
  }
}

其中:

infinite指令的实现思路就是:

项目优化

咸虾米壁纸

页面情况

基于 vue3+uni-app 的壁纸项目。App 分为首页,分类,我的三个 tab 页面,分类列表,图片详情等其他页面。能实现根据关键字搜索图片,滑动预览图片,下载图片,给图片评分,分享页面,查看我的下载和评分,联系客服等功能;

为每个需要的页面都做了下拉刷新,触底加载。

下拉刷新

触底加载

user页面

Promise

动态路由传参

所有需要查看缩略图的页面,使用的都是同一个组件,根据路由跳转传入的参数不同,发起不同的请求,实现了类似动态路由传参的效果。

网络请求

图片预览页面,基于 uni-app 的 swiper 组件展示图片,并且只渲染用户正在预览的图片和其左右2张图片,在保证用户体验的同时减少了网络请求。