第4章:构建自己的物料解决方案
数据拦截简化数据获取流程
我们可以发现原来我们获取到的数据是这样的情况,里面的东西太多太复杂了,故此我们需要将进行简化。而我们在开发的过程中主要是需要data,code,message。
在src\utils\request.js里面添加这个代码
/*** 响应拦截器:* 服务器返回数据之后,前端 .then之前被调用*/
service.interceptors.response.use(response =>{const{success , message ,data } =response.dataif (success){return data}//TODO: 业务请求错误return Promise.reject(new Error(message))
})
更改完代码后我们可以发现,变成了如下的json数据。
业务组件:移动端navigationBar
首先我们要将mobile里面的调用数据的方法放到去父文件中的index.vue里面。
import { ref } from 'vue';
import {getCategory} from '@/api/category'const categorys = ref([ ])
const getCategoryData = async () => {const { categorys } = await getCategory()categorys.value = categorysconsole.log(categorys.value)
}
getCategoryData()
渲染数据
- 首先要在navigation里面修改为以下数据
<template><mobile-navigation-vue v-if="isMobile" :data="categoryData"/>
</template><script setup>
import { isMobile } from '@/utils/flexible'
import mobileNavigationVue from './mobile/index.vue'
import { ref } from 'vue';
import {getCategory} from '@/api/category'const categoryData = ref([ ])
const getCategoryData = async () => {const { categorys } = await getCategory()categoryData.value = categorys
}
getCategoryData()
</script><style scoped lang="scss"></style>
- 然后要在mobile里面渲染数据
<template><div ><ul><li v-for="item in data" :key="item.id">{{ item.name }}</li></ul></div>
</template>
- 最后通过tailwind修改其样式
<template><div class="bg-white sticky top-0 left-0 z-10" ><ul class="relative flex overflow-x-auto p-1 text-xs text-zinc-600 overflow-hidden"><li v-for="item in data" :key="item.id" class="shrink-0 px-1.5 py-0.5 z-10 duration-200">{{ item.name }}</li></ul></div>
</template>
动态rem基准值+修正tailwindcss样式
- 在src\utils\flexible.js里面添加动态rem修改方法
/*** 动态rem基准值,最大不超过40px* 根据用户的屏幕宽度,进行一些计算,把计算出来的值赋值给 html根标签作为fontsize大小*/
export const useREM =()=>{//定义最大的 fontsizeconst MAX_SIZE=40//监听 html 文档被解析完成的事件document.addEventListener('DOMContentLoaded',() =>{//拿到 html 标签const html = document.querySelector('html')//计算 fontsize,根据屏幕宽度/10let fontsize = window.innerWidth/10fontsize = fontsize>MAX_SIZE?MAX_SIZE:fontsize//赋值给 htmlhtml.style.fontSize = fontsize+'px'})
}
- 在mian.js里面修改
import { createApp } from 'vue'
import App from './App.vue'
import './styles/index.scss'
import router from './router'
import { useREM } from './utils/flexible'useREM()
createApp(App).use(router).mount('#app')
- 最后修改tailwind.config.js里面的方法
module.exports = {//tailwind可以应用的地方content: ['./index.html','./src/**/*.{vue,js}'],theme: {extend: {fontSize:{xs: ['0.25rem','0.35rem'],sm: ['0.35rem','0.45rem'],base: ['0.45rem','0.55rem'],lg: ['0.55rem','0.65rem'],xl: ['0.65rem','0.75rem']}},},plugins: [],
}
处理通用组件svg-icon
- 首先我们要构建svg-icon
<template><svg aria-hidden="true"><use :xlink:href="symbolId" :class="fillClass" :fill="color" /></svg>
</template><script setup>import { computed } from 'vue'const props = defineProps({// 显示的 svgname: {type: String,required: true},// svg 图标的颜色color: {type: String},// tailwind 指定 svg 颜色的类名fillClass: {type: String}
})
// 真实显示的 svg 图标 (拼接 #icon-)
const symbolId = computed(() => `#icon-${props.name}`)
</script><style lang="scss" scoped></style>
- 其次注册svg-icon
import svgIcon from './svg-icon/index.vue'export default{install(app){app.component('m-svg-icon',svgIcon)}
}
import { createApp } from 'vue'
import App from './App.vue'
import './styles/index.scss'
import router from './router'
import { useREM } from './utils/flexible'
import mLibs from './lib'useREM()
createApp(App).use(router).use({mLibs}).mount('#app')
- 修改svg-icon占位符
<template><div class="bg-white sticky top-0 left-0 z-10" ><ul class="relative flex overflow-x-auto p-1 text-xs text-zinc-600 overflow-hidden"><li class="fixed top-0 right-[-1px] h-4 px-1 flex items-center bg-white z-20 shadow-l-white"><m-svg-icon class="w-1.5 h-1.5" name="hamburger"></m-svg-icon></li><li v-for="item in data" :key="item.id" class="shrink-0 px-1.5 py-0.5 z-10 duration-200 last:mr-4">{{ item.name }}</li></ul></div>
</template>
module.exports = {//tailwind可以应用的地方content: ['./index.html','./src/**/*.{vue,js}'],theme: {extend: {fontSize:{xs: ['0.25rem','0.35rem'],sm: ['0.35rem','0.45rem'],base: ['0.45rem','0.55rem'],lg: ['0.55rem','0.65rem'],xl: ['0.65rem','0.75rem']}},boxShadow:{'l-white':'-10px 0 10px white'}},plugins: [],
}
vite处理svg-icon
无论是vue-cli还是vite默认它们都不会主动导入svg矢量图标,因此我们需要使用一个vite的plugin
- 首先安装vite-plugin-svg-icons到项目中
npm i --save-dev vite-plugin-svg-icons@2.0.1
- 在vite.config.js中注册
......
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),createSvgIconsPlugin({// 指定需要缓存的图标文件夹iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],// 指定 symbolId 格式symbolId: 'icon-[name]'})],......
})
- 在main.js里面注册icon图标
// 注册 svg-icons
import 'virtual:svg-icons-register'
slider滑块处理
在src\views\main\components\navigation\mobile\index.vue里面添加代码
<!-- 滑块 --><li ref="sliderTarget" :style="sliderStyle" class=" absolute h-[22px] bg-zinc-900 rounded-lg duration-200"><script setup>
import { ref } from 'vue';......const sliderStyle = ref({transform: 'translateX(0px)',width:'60px'
})
</script>
处理滑块要想到达目的主要是从以下几个方面考虑
- 选中的item下标:currentCategoryIndex
- 所有item元素:itemRefs
- ul的横向滚动偏离位置:ulScrollLeft
- 最后在currentCategoryIndex发生改变时,获取item下标元素的left和width,计算sliderStyle即可
<template><div class="bg-white sticky top-0 left-0 z-10"><ulref="ulTarget"class="relative flex overflow-x-auto p-1 text-xs text-zinc-600 overflow-hidden"><!-- 滑块 --><liref="sliderTarget":style="sliderStyle"class="absolute h-[22px] bg-zinc-900 rounded-lg duration-200"></li><liclass="fixed top-0 right-[-1px] h-4 px-1 flex items-center bg-white z-20 shadow-l-white"><m-svg-icon class="w-1.5 h-1.5" name="hamburger"></m-svg-icon></li><!-- items --><liv-for="(item, index) in data":key="item.id"class="shrink-0 px-1.5 py-0.5 z-10 duration-200 last:mr-4":class="{'text-zinc-100': currentCategoryIndex === index}":ref="setItemRef"@click="onItemClick(index)">{{ item.name }}</li></ul></div>
</template><script setup>
import { useScroll } from '@vueuse/core'
import { onBeforeUpdate, ref, watch } from 'vue'// 在vite 构建项目中,我们可以直接使用 defineProps方法
defineProps({data: {type: Array,required: true}
})const sliderStyle = ref({transform: 'translateX(0px)',width: '60px'
})//选中 item下标
const currentCategoryIndex = ref(0)//获取所有的item函数
let itemRefs = []
const setItemRef = (el) => {if (el) {itemRefs.push(el)}
}//数据改变之后,DON改变之前
onBeforeUpdate(() => {itemRefs = []
})//获取url元素
const ulTarget = ref(null)
//通过vueuse里面的useScroll获取响应式的scroll滚动距离
const { x: ulScrollLeft } = useScroll(ulTarget)// watch 监听
watch(currentCategoryIndex, (val) => {const { left, width } = itemRefs[val].getBoundingClientRect()sliderStyle.value = {// 滑块的位置 = ul 横向滚动的位置 + 当前元素的 left - ul 的 paddingtransform: `translateX(${ulScrollLeft.value + left - 10}px)`,width: width + 'px'}
})// item 点击事件
const onItemClick = (index) => {currentCategoryIndex.value = index
}
</script><style scoped lang=""></style>
补全category
首先要在src\constants\index.js定义一个常量
//category的本地构建数据
export const ALL_ITEM= {id: 'all',name: '全部'
}
然后在src\views\main\components\navigation\index.vue里面,添加首个元素
import { ALL_ITEM } from '@/constants';const categoryData = ref([ ])
const getCategoryData = async () => {const { categorys } = await getCategory()categoryData.value = categoryscategoryData.value.unshift(ALL_ITEM)
}
弹出窗口popup
我们点击按钮时候,会有一个弹出窗口popup自低而上弹出,那么这样的一个功能,我们一样可以把它处理为项目的通用组件。以下是popup的能力:
- 当popup展开时,内容视图应该不属于任何一个组件内部,而应该直接被插入到body下面
- popip应该包含两部分内容,一部分是背景蒙板,一部分为内容的包裹容器
- popip应该通过一个双向绑定进行控制展示和隐藏
- popup展示时,滚动应该被锁定
- 内容区域应该接受所有的attrs,并且应该通过插槽让调用方指定其内容
简单导入:
- 在src\libs\popup\index.vue添加一下内容
<template><div><teleport to='body' ><!-- 蒙版 --><div>蒙版</div><!-- 内容 --><div>组件</div></teleport></div>
</template><script setup>
</script><style lang='scss' scoped></style>
- 将m-popup注册
import svgIcon from './svg-icon/index.vue'
import popup from './popup/index.vue'export default {install(app) {app.component('m-svg-icon', svgIcon)app.component('m-popup',popup)}
}
- 将其在src\views\main\components\navigation\mobile\index.vue实现
<template><div class="bg-white sticky top-0 left-0 z-10"><ul>......</ul><m-popup/></div>
</template>
最终实现
src\views\main\components\navigation\mobile\index.vue
<template><div class="bg-white sticky top-0 left-0 z-10"><ulref="ulTarget"class="relative flex overflow-x-auto p-1 text-xs text-zinc-600 overflow-hidden"><!-- 滑块 --><liref="sliderTarget":style="sliderStyle"class="absolute h-[22px] bg-zinc-900 rounded-lg duration-200"></li><!-- 按钮 --><liclass="fixed top-0 right-[-1px] h-4 px-1 flex items-center bg-white z-20 shadow-l-white"@click="onShowPopup"><m-svg-icon class="w-1.5 h-1.5" name="hamburger"></m-svg-icon></li><!-- items --><liv-for="(item, index) in data":key="item.id"class="shrink-0 px-1.5 py-0.5 z-10 duration-200 last:mr-4":class="{'text-zinc-100': currentCategoryIndex === index}":ref="setItemRef"@click="onItemClick(index)">{{ item.name }}</li></ul><m-popup v-model="isVisable"><div>我是内容</div></m-popup></div>
</template><script setup>
import { useScroll } from '@vueuse/core'
import { onBeforeUpdate, ref, watch } from 'vue'// 在vite 构建项目中,我们可以直接使用 defineProps方法
defineProps({data: {type: Array,required: true}
})const sliderStyle = ref({transform: 'translateX(0px)',width: '52px'
})//选中 item下标
const currentCategoryIndex = ref(0)//获取所有的item函数
let itemRefs = []
const setItemRef = (el) => {if (el) {itemRefs.push(el)}
}//数据改变之后,DON改变之前
onBeforeUpdate(() => {itemRefs = []
})//获取url元素
const ulTarget = ref(null)
//通过vueuse里面的useScroll获取响应式的scroll滚动距离
const { x: ulScrollLeft } = useScroll(ulTarget)// watch 监听
watch(currentCategoryIndex, (val) => {const { left, width } = itemRefs[val].getBoundingClientRect()sliderStyle.value = {// 滑块的位置 = ul 横向滚动的位置 + 当前元素的 left - ul 的 paddingtransform: `translateX(${ulScrollLeft.value + left - 10}px)`,width: width + 'px'}
})// item 点击事件
const onItemClick = (index) => {currentCategoryIndex.value = index
}//控制popup展示
const isVisable = ref(false)
const onShowPopup =() =>{isVisable.value=true
}
</script><style scoped lang=""></style>
src\libs\popup\index.vue
<template><div ><teleport to='body' ><!-- 蒙版 --><transition name="fade" v-if="modelValue" @click="emits('update:modelValue',false)"><div class="w-screen h-screen bg-zinc-900/80 z-40 fixed top-0 left-0"></div></transition><!-- 内容 --><transition name="popup-down-up"><div v-bind="$attrs" class="w-screen bg-white z-60 fixed bottom-0" v-if="modelValue"><slot/></div></transition> </teleport></div>
</template><script setup>
import { ref, watch } from 'vue';
import { useScrollLock } from '@vueuse/core';const props = defineProps({modelValue:{required: true,type: Boolean}
})const emits = defineEmits(['update:modelValue'])//锁定滚动
const isLocked = useScrollLock(document.body)
watch(() => props.modelValue,(val) =>{isLocked.value = val},{immediate:true}
)
</script><style lang='scss' scoped>//fade动画
.fade-enter-active,
.fade-leave-active{transition: all 0.3s;
}//准备进入,离开完成
.fade-enter-from,
.fade-leave-from{opacity: 0;
}.popup-down-up-enter-active,
.popup-down-up-leave-active{transition: all 0.3s;}.popup-down-up-enter-from,
.popup-down-up-leave-from{transform: translateY(100%);
}
</style>
双向数据绑定优化
useVModel可以直接帮我们完成数据间的双向绑定,主要是将项目里面的v-model改为isVisable
<template><div ><teleport to='body' ><!-- 蒙版 --><transition name="fade" v-if="isVisable" @click="isVisable =false"><div class="w-screen h-screen bg-zinc-900/80 z-40 fixed top-0 left-0"></div></transition><!-- 内容 --><transition name="popup-down-up"><div v-bind="$attrs" class="w-screen bg-white z-60 fixed bottom-0" v-if="modelValue"><slot/></div></transition> </teleport></div>
</template><script setup>
import { ref, watch } from 'vue';
import { useScrollLock,useVModel } from '@vueuse/core';const props = defineProps({modelValue:{required: true,type: Boolean}
})defineEmits(['update:modelValue'])//是一个响应式数据,当isVisable 值发送改变时,会自动触发emit修改modelValue
const isVisable = useVModel(props)//锁定滚动
const isLocked = useScrollLock(document.body)
watch(isVisable,(val) =>{isLocked.value = val},{immediate:true}
)
</script><style lang='scss' scoped>//fade动画
.fade-enter-active,
.fade-leave-active{transition: all 0.3s;
}//准备进入,离开完成
.fade-enter-from,
.fade-leave-from{opacity: 0;
}.popup-down-up-enter-active,
.popup-down-up-leave-active{transition: all 0.3s;}.popup-down-up-enter-from,
.popup-down-up-leave-from{transform: translateY(100%);
}
</style>
vite通用组件自动化注册
目前我们在项目中已经完成了两个通用组件,将来我们还需要更多的通用组件开发,如果每次开发完成一个通用组件之后,都要手动去注册,未免太麻烦了,所以我们通过vite提供的功能,进行组件自动化注册。
- vite的Glob功能:改功能帮助我们可以在文件系统中导入多个模块
- vue的defineAsyncComponent方法:该方法可以创建一个按需加载的异步组件
基于上面的两个方法,实现组件自动注册
最终代码
import { defineAsyncComponent } from 'vue'export default {install(app) {//1.获取当前路径下所有文件夹中的index.vueconst components = import.meta.glob('./*/index.vue')//2.遍历获取到的组件模块for(const [fullPath,fn] of Object.entries(components)){//3.利用app.component进行注册 ./popup/index.vu分割为popupconst componentName ='m-'+fullPath.replace('./','').split('/')[0]app.component(componentName,defineAsyncComponent(fn))}}
}
最终代码
<template><div class="bg-white sticky top-0 left-0 z-10"><ulref="ulTarget"class="relative flex overflow-x-auto p-1 text-xs text-zinc-600 overflow-hidden"><!-- 滑块 --><liref="sliderTarget":style="sliderStyle"class="absolute h-[22px] bg-zinc-900 rounded-lg duration-200"></li><!-- 按钮 --><liclass="fixed top-0 right-[-1px] h-4 px-1 flex items-center bg-white z-20 shadow-l-white"@click="onShowPopup"><m-svg-icon class="w-1.5 h-1.5" name="hamburger"></m-svg-icon></li><!-- items --><liv-for="(item, index) in data":key="item.id"class="shrink-0 px-1.5 py-0.5 z-10 duration-200 last:mr-4":class="{'text-zinc-100': currentCategoryIndex === index}":ref="setItemRef"@click="onItemClick(index)">{{ item.name }}</li></ul><m-popup v-model="isVisable"><menu-vue :categorys="data" @onItemClick="onItemClick"></menu-vue></m-popup></div>
</template><script setup>
import { useScroll } from '@vueuse/core'
import { onBeforeUpdate, ref, watch } from 'vue'
import MenuVue from '@/views/main/components/menu/index.vue'// 在vite 构建项目中,我们可以直接使用 defineProps方法
defineProps({data: {type: Array,required: true}
})const sliderStyle = ref({transform: 'translateX(0px)',width: '52px'
})//选中 item下标
const currentCategoryIndex = ref(0)//获取所有的item函数
let itemRefs = []
const setItemRef = (el) => {if (el) {itemRefs.push(el)}
}//数据改变之后,DON改变之前
onBeforeUpdate(() => {itemRefs = []
})//获取url元素
const ulTarget = ref(null)
//通过vueuse里面的useScroll获取响应式的scroll滚动距离
const { x: ulScrollLeft } = useScroll(ulTarget)// watch 监听
watch(currentCategoryIndex, (val) => {const { left, width } = itemRefs[val].getBoundingClientRect()sliderStyle.value = {// 滑块的位置 = ul 横向滚动的位置 + 当前元素的 left - ul 的 paddingtransform: `translateX(${ulScrollLeft.value + left - 10}px)`,width: width + 'px'}
})// item 点击事件
const onItemClick = (index) => {currentCategoryIndex.value = indexisVisable.value=false
}//控制popup展示
const isVisable = ref(false)
const onShowPopup =() =>{isVisable.value=true
}
</script><style scoped lang=""></style>
<template><div class="py-2 h-[80vh] flex flex-col"><h2 class="text-xl text-zinc-900 font-bold mb-2 px-1">所有分类</h2><ul class="overflow-y-scroll"><liv-for="(item, index) in categorys":key="item.id"class="text-lg text-zinc-900 px-1 py-1.5 duration-100 active: bg-zinc-100"@click="$emit('onItemClick',index)">{{ item.name }}</li></ul></div></template>
<script setup>
defineProps({categorys:{type: Array,required:true}})//推荐使用的item进行注册
defineEmits(['onItemClick'])
</script><style lang='scss' scoped></style>