一个跟着尚硅谷做的小项目
主要涉及的知识点如下:
echarts:一个基于 JavaScript 的开源可视化图表库,跟着文档敲代码,使用起来还是很简单的,唯一要注意的点就是,在dom挂载后再操作dom,挂载图表
当dispatch一个异步action的时候,可以对dispatch使用await,因为此时dispatch会返回一个promise对象。这个promise对象会根据异步creator返回的async函数的返回值,来确定自己的状态:
async function func(params) {
const p = new Promise((resolve) => { setTimeout(() => { resolve(1) }, 3000) })
await p
return new Promise((resolve) => { setTimeout(() => { resolve('你好') }, 3000) })
}
const res = func()
console.log(res)//返回一个状态没确定的promise实例
setTimeout(() => { console.log(res) }, 6000)//6s后,promise的状态变为fulfilled,值是"你好"
使用正则表达式校验电话号码:
/^1[3-9]\d{9}$/
富文本组件ReactQuill
自定义hooks:
import { useEffect, useState } from 'react'
import { getChannelListApi } from '../apis/article';
// 事实证明,react hooks在普通函数中也可以使用
// 这样其实就实现了类似redux的效果
// 但是每次调用这个函数,都会创建新的state,都会发送请求
export default function useChannels() {
let [channels, setChannels] = useState([])
const getChannelList = async () => {
try{
const res = await getChannelListApi()
// console.log(res.data.channels)
setChannels(res.data.channels)
}catch(e){
console.log(e)
}
}
useEffect(() => {
getChannelList()
}, [])
//把状态return出去
return {channels}
}
自定义hooks和react hooks一样,只能在函数顶层作用域中使用
大部分知识点是关于antd组件的使用,多看文档就熟悉了,不得不说antd组件是真好用啊。
路由懒加载:通过 Suspense
组件和lazy
函数配合实现
路由守卫:通过自定义组件实现
//AutoRoute.js
import React from 'react'
import { Navigate } from 'react-router-dom'
export default function AutoRoute(props) {
const token = localStorage.getItem('token')
//如果能拿到token说明用户已经登录
if(token){
//通过props.children拿到AutoRoute标签体内的结构(虚拟dom)
//空标签不会被实际渲染,等同于vue中的template
return <> {props.children} </>
}else{
// 否则渲染Navigate,跳转到/login
return (
<Navigate to={'/login'}></Navigate>
)
}
}
import { Suspense,lazy } from 'react'
import { createBrowserRouter} from 'react-router-dom'
import Layout from '../pages/layout/index'
import Login from '../pages/login/index'
import AutoRoute from '../components/AutoRoute'
//使用动态导入的方式
const Home = lazy(() => import('../pages/Home'))
const Article = lazy(() => import('../pages/Article'))
const Publish = lazy(() => import('../pages/Publish'))
const router = createBrowserRouter([
{
path:'/',
element:<AutoRoute><Layout></Layout></AutoRoute>,
children:[
//fallback指定组件未加载出来的时候显示的默认结构
{path:'home',element: <Suspense fallback={<h3>加载中...</h3>}><Home></Home></Suspense>},
{path:'article',element:<Suspense fallback={<h3>加载中...</h3>}><Article></Article></Suspense>},
{path:'publish',element:<Suspense fallback={<h3>加载中...</h3>}><Publish></Publish></Suspense>}
]
},
{
path:'/login',
element: <Login></Login>
}
])
redux:使用@reduxjs/toolkit
和react-redux
这2个包实现,非常方便。
localStorage语法:
如果localStorage中并没有某个字段,则getItem
直接返回null
JSON.parse(null)
返回null
initialState:{
// 如果localStorage中并没有token字段,则直接返回null
token: localStorage.getItem('token')|| '',
// JSON.parse(null)返回null,所以能正确取到默认值{}
userInfo: JSON.parse(localStorage.getItem('useInfo'))|| {}
}
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对象?
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
,监听页面滚动事件,当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
})
fixedHeader
由于translateY(-100%)
,即向上移动的自身高度的100%,所以被隐藏了。.show
样式,即transform: none
,然后fixedHeader
就会滑动出来还有就是Nav组件的购物车图标,鼠标放到上面会显示购物车列表,这个效果实现核心如下:
.layer {
transform-origin: top;//变换的中心是top
transform: scale(1, 0);//x轴不变,y轴缩放为0,由于高度为0了,元素自然就隐藏了
transition: all 0.4s 0.2s;//添加过渡,过渡事件是0.4s,延迟0.2秒开始动画
}
transform: none
,购物车就出现了同时还对删除购物车商品做了动画效果,道理也是一样的。
.del {
transform: translateX(100%);
opacity: 0;
}
因为这些组件只会在layout页面中用到,所以被放到layout/components
目录下。
虽然说一个页面对应一个组件,但是为了降低项目的耦合度,把一个页面分解为多个组件才比较合理。
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%);//展示阴影
}
}
透明度:
rgb(0 0 0 / 20%)
:这是CSS Color Module Level 4中引入的新语法。它使用空格分隔RGB值,并通过一个斜杠 /
后跟透明度值来指定颜色的透明度。这里的透明度是以百分比形式给出的(例如 20%
),意味着该颜色有20%的不透明度(或80%的透明度)。rgba(0, 0, 0, 0.2)
:这是较旧的标准语法,其中RGBA代表红、绿、蓝和Alpha透明度通道。Alpha值是一个介于0(完全透明)到1(完全不透明)之间的数字,在这个例子中为 0.2
,表示20%的不透明度(或80%的透明度)。box-shadow:
当在商品详情组件中点击其他商品链接,但是跳转的组件还是商品详情组件,这就会导致组件被复用,内部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>
可以看出,监听li
的mouseover
事件,这是一个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)
e.clientX
:表示鼠标指针相对于浏览器视口左边界的水平距离e.clientY
:表示鼠标指针相对于浏览器视口顶部的垂直距离调用一个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
,让鼠标时间穿透到商品图片,这样商品图片就能成为目标元素。
实现步骤如下:
发送请求获取数据{specs,skus}
,然后把这个数据变成响应式的,传递给sku组件
sku
组件通过props
接收,父组件传递过来的数据
分析specs和skus
specs
specs是一个对象数组,每个对象代表一种规格,每个规格都有id,比如:颜色;每种规格都有具体的类别,比如:蓝色
skus是一个规格组合数组,包含了用户可能选择的所有组合。12=3*2*2
先将所有specs渲染到页面
然后实现点击spec添加边框的功能,本质是一种排他思想。
const clickSpecs = function (spec, val) {
if (!val.selected) {
spec.values.forEach((value) => {
value.selected = false
})
val.selected = true
} else {
val.selected = false
}
}
然后开始考虑实现类别禁用功能
从skus数组中筛选出有效的sku,即inventory(库存)大于0的sku
然后提取每个有效的sku的规格路径
const validSkuPath = validSkus.map((sku) => {
return sku.specs.map((spec) => spec.valueName)
})
对每个有效的规格路径(就是一个数组),我们都使用PowerSet算法,计算出它的全部子集
然后再构造PathMap,将每个规格路径的子数组拼接成字符串作为key
构造好PathMap
,然后我们就能初始化disabled
了。遍历specs中的所有spec,再遍历每个spec的所有values,判断每种类别是否可以再PathMap
中找到,如果能找到,disabled
就会false,否则就为true
然后再实现,点击动态修改disabled
。
const updateDisabled = function (specs, selectedArr) {
specs.forEach((spec, index) => {
spec.values.forEach((val) => {
//判断每个未被选中的类别是否是disabled
//存储选中的类别
const oldValue = selectedArr[index]
if (!val.selected) {
//修改selectedArr对应位置上的值
selectedArr[index] = val.name
const key = selectedArr.filter((value) => value).join('-')
//判断在pathMap中是否可以找到
if (pathMap[key]) {
val.disabled = false
} else {
val.disabled = true
}
}
//复原
selectedArr[index] = oldValue
})
})
}
一级分类页面就是通过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>
-1
,分:秒
的形式。要实现这一点,我们还借助了dayjs库
来格式化时间,还有计算属性
,每当总秒数改变,就返回最新的格式化时间。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上,获取壁纸链接的请求就跨域了,就没有部署。
就是发请求获取数据,然后渲染页面;
页面结构部分就是使用了一些uni-app内置的组件或者扩展的组件,还使用了自己编写的组件,然后使用的语法还是vue的语法,没什么好说的。
比较新奇的就是使用backdrop-filter:blur()
属性做了磨砂效果。然后还使用了渐变色堆叠
。但是属于css范畴,其实也没什么好说的
没啥好说的,就是发请求获取数据然后复用theme-item
组件,然后使用grid
来布局这个组件,顺便使用uni-app提供的onPullDownRefresh
,onReachBottom
钩子实现了下拉刷新
和触底加载
,面试官要问就是问你自己如何实现这个。
touchstart
事件中,记录第一个触点距离视口顶部的距离e.touches[0].clientY
touchend
事件中,计算用户滑动的垂直距离,判断是否进行下拉刷新功能包括显示用户ip
,查看我的下载,我的收藏,联系客服等。
比较有意思的就是如何获取用户ip吧,虽然在这个项目中是后端服务器实现的。
我的收藏,我的下载,图片搜索等所有需要展示缩略图的页面(除了home页面),复用的都是这个页面(其实就是wallpaperList组件)。
根据跳转到这个页面传递的参数不同(通过onLoad接收),来展示不同的navigationBar
,来发送不同的请求。其实就类似动态路由参数
。
然而所有需要展示缩略图的页面
,都需要通过修改缩略图链接得到大图链接
,然后为了在preview页面展示(页面间数据共享),所以需要把图片对象数组
存储在pinia仓库中,然后点击缩略图查看大图的时候(跳转到preview页面),就直接使用pinia中的数据。
因为所有需要展示缩略图的页面不会同时存在,所以我们只设置一个picList
,只需要确保当前处在某个展示缩略图的页面的时候,仓库中的picList
中存储的是该页面的图片对象数组
。
//放在home页面,如果home页面没销毁则直接使用页面组件内的recommendList.value
//如果页面组件被销毁,重新切换到home,由于这个操作会先于请求的数据到达被执行
//所以最后存储进仓库的还是通过请求返回的数据
onShow(() => {
usePic.setPicList(recommendList.value)
})
而且这样有个好处就是,preview页面
不需要纠结到底应该从那个页面的picList中取数据展示,因为这些页面的图片对象数组
都存储在picList中。
主要用来展示公告详情的页面,大概也就是通过请求获取数据然后渲染页面,使用了uni-dateformat
来格式化事件,使用rich-text
组件来解析富文本。
是这个项目功能最复杂的页面,实现了滑动,预览图片,查看图片详细信息,收藏图片,下载图片的功能。
主要是基于大小占满整个页面的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({})//让用户设置权限,调起客户端小程序设置界面,返回用户设置的操作结果,配置属性只有那三个回调函数
可以根据用户输入的关键词
搜索图片,并保存搜索记录。
基于 vue3+Element-Plus+阿里图标,开发的网上商城项目;(一级页面包括布局页面和登录页面,二级页面包括 home 页面,分类页面,商品详情页面,购物车页面,订单页面,支付页面,用户页面;)
在实现购物车页面
的时候,持久化存储了原本存储在pinia
中,购物车中的商品数据cartList
,这样即使刷新,关闭页面,购物车中的商品数据也不会丢失,支持用户在未登录状态操作购物车。
持久化是通过pinia-plugin-persistedstate
插件实现的,想靠自己实现,就得依赖localStorage Api
。
然后在登录的时候,会自动将本地购物车商品数据,合并到远程购物车。
退出登录的时候,会清空购物车中数据。
cartList.value = []
(当我在这么介绍的时候面试管问我,将商品数据存储在购物车不会有安全问题吗?确实购物车商品数据存储在本地,可以通过js修改商品价格,但是最终订单,是后端通过选中的商品id
和对应的商品数量
生成的,并没有太大的安全问题。)
在商品详情
页面,实现商品图片的放大镜效果的时候
e.offsetX,e.offsetY
获取鼠标在商品图片中的相对位置当在商品详情组件中点击其他商品链接,但是跳转的组件还是商品详情组件,这就会导致组件被复用,内部js代码不会执行,组件就不会更新。使用组件内的beforeRouteUpdate
路由守卫,来解决这个问题。
selected
属性设置为true
,其他类别数据的selected
属性设置为false
,然后通过动态样式绑定修改样式。skus
数组中筛选出有效的sku
,然后构建pathMap
selecteArr
selectedArr
中对应位置的数据,然后根据pathMap
判断某个类别是否应该被禁用,如果应当被禁用,则修改这个类别数据的disabled
属性为true。在展示商品列表的时候,使用自定义的 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 })
}
}
其中:
obj.el.getBoundingClientRect().bottom
,获取的是一个dom元素底部
距离视口顶部
的距离document.documentElement.clientHeight+80
,其中document.documentElement
就代表html元素,而html.clientHeight
代表的就是视口的高度binding.value
就是元素触底后执行的回调函数。infinite指令的实现思路就是:
v-infinite
指令的dom元素,都会连同对应的回调函数(binding.value
),被push到handler
数组中。v-infinite
指令的dom元素,调用它们的getBoundingClientRect
方法,判断它们的底部是否将要出现在视口,如果满足条件,则调用对应的回调函数,发请求获取新的数据,与原有的数据合并。然后在项目优化方面,对于非首屏组件,使用路由懒加载,提高了首屏幕加载速度;
自定义 v-lazy 指令,实现了图片懒加载,减少了首屏请求量;
//因为自定义指令只会被导入一次,所以observer对象只会被创建一次
//写在外面的好处就是不用每次使用指令都创建一个observer,减少了内存占用
const observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
//不能通过entry.target.val的方式访问,val 是一个 Symbol 类型的键
entry.target.src = entry.target[val]
//图片出现在视口,就移除对图片的监听
//传入的还是dom对象,对应添加监听的时候传入的是dom对象
observer.unobserve(entry.target)
}
})
},
{ threshold: 0 }//第二个参数传入一个配置对象
)
let val = Symbol('value')
//导出一个对象
export const lazyLoad = {
mounted(el, binding) {
//记住img元素的src属性
el[val] = binding.value
observer.observe(el)//传入绑定的dom对象
}
}
使用keep-alive缓存组件实例,防止重复渲染dom。
把同一组件,不同功能的代码分别提取出来,封装成单独的文件,导出一个函数,需要的时候再引入,提高了代码的复用率和可维护性;
基于 vue3+uni-app 的壁纸项目。App 分为首页,分类,我的三个 tab 页面,分类列表,图片详情等其他页面。能实现根据关键字搜索图片,滑动预览图片,下载图片,给图片评分,分享页面,查看我的下载和评分,联系客服等功能;
为每个需要的页面都做了下拉刷新,触底加载。
touchstart
事件中,记录第一个触点距离视口顶部的距离e.touches[0].clientY
touchend
事件中,计算用户滑动的垂直距离,判断是否进行下拉刷新使用 Promise 包装 uni.request
这个异步api,封装成 request 函数,简化了请求代码,实现了类似 axios 的某些效果;
在实现下载功能的时候,为了考虑到用户的各种选择,使用了各种通过回调实现的异步api,导致代码可读性极其的差,难以维护(回调函数地狱问题),我尽量使用了promise来优化代码的可读性。
所有需要查看缩略图的页面,使用的都是同一个组件,根据路由跳转传入的参数不同,发起不同的请求,实现了类似动态路由
传参的效果。
图片预览页面,基于 uni-app 的 swiper 组件展示图片,并且只渲染用户正在预览的图片和其左右2张图片,在保证用户体验的同时减少了网络请求。