readme
本笔记用于记录在开发黑马小兔鲜项目中值得学习的习惯和技术
1.创建项目
第一步:打开命令行窗口
第二步:执行创建项目命令行
执行:vue create 【项目名】
第三步:选择自定义创建
可以选择基础的模板,也可以自定义选择其它配置
第四步:选择基本配置
在这里可以使用空格 选择/取消 配置,选择完毕后按enter进入下一步 这里我们 选中vue-router,vuex,css Pre-processors选项
第五步:选择vue版本
第六步:选择hash模式的路由
第七步:选择less作为预处理器
第八步:选择 standard 标准代码风格
第九步:保存代码校验代码风格,代码提交时候校验代码风格
第十步:依赖插件或者工具的配置文件分文件保存
第十一步:是否记录以上操作,选择否
第十二步:等待安装…
最后:安装完毕
有些步骤根据当前项目的配置不一定会出现,所以要根据当前的项目情况而定
2.常用目录结构
当目录建成之后,我们需要删除一些无用的代码和文件,然后完善项目整体的目录结构,我们选用一套开发常用的通用目录结构,良好的目录结构组成能方便我们后续的开发协作。
文件夹说明
api - 接口函数
这里有对应着views内不同的路由所需的请求接口封装,如:我们路由中有home页面,home页面的请求就可以在api这个文件夹中单独创建一个home.js,home.js中会配合utils>request.js 封装暴露的axios请求进行接口的封装
accets - 资源管理
accets中包含的是项目开发所需的公共资源和静态资源(logo等),本项目中accets文件夹包含images和styles两个子目录,images包含项目中所需的静态资源,styles中包含初始化样式和一些全局样式(这两个样式我们会通过一些方法导入所有vue实例中,后面会介绍)
components - 全局组件,公共组件
components中包含一些全局复用的公共组件和全局组件
router - 路由vue-router
router文件夹中包含index.js,它的作用控制全局的路由
store - 状态管理 vuex
store文件中包含index.js和modules,index.js是vuex的实例,modules文件夹中对vuex中的数据进行了分类管理。
utils - 工具模块
utils中存放一些公共复用的工具模块,例如:request.js,它对axios进行二次封装,使其更加便捷好用
vender - 第三方js库
存放一些npm没有的第三方库函数
views - 路由级别视图
views中对路由中的页面进行了分类,例如:将home和user等页面分别存放在不同的文件夹。各自的文件夹中还会有components子目录,存放当前视图中复用的组件。
App.vue - 根组件
main.js - 入口文件
3.vuex的持久化
开发过程中,用户的一些信息通常我们会存储在vuex进行状态管理,但是vuex在页面刷新之后会丢失数据,所以我们要使用浏览器的本地存储进行vuex的持久化存储。
1)首先:我们需要安装一个vuex的插件vuex-persistedstate来支持vuex的状态持久化。
npm i vuex-persistedstate
2)然后:在src/store 文件夹下新建 modules 文件,在 modules 下新建 user.js 和 cart.js
src/store/modules/user.js
// 用户模块
export default {
namespaced: true,
state () {
return {
// 用户信息
profile: {
id: '',
avatar: '',
nickname: '',
account: '',
mobile: '',
token: ''
}
}
},
mutations: {
// 修改用户信息,payload就是用户信息对象
setUser (state, payload) {
state.profile = payload
}
}
}
src/store/modules/cart.js
// 购物车状态
export default {
namespaced: true,
state: () => {
return {
list: []
}
}
}
3)继续:在 src/store/index.js 中导入 user cart 模块。
import { createStore } from 'vuex'
import user from './modules/user'
import cart from './modules/cart'
export default createStore({
modules: {
user,
cart
}
})
4)最后:使用vuex-persistedstate插件来进行持久化
import { createStore } from 'vuex'
+ import createPersistedstate from 'vuex-persistedstate'
import user from './modules/user'
import cart from './modules/cart'
import cart from './modules/category'
export default createStore({
modules: {
user,
cart,
category
},
+ plugins: [
+ createPersistedstate({
+ key: 'erabbit-client-pc-store',
+ paths: ['user', 'cart']
+ })
+ ]
})
注意:
===> 默认是存储在localStorage中
===> key是存储数据的键名
===> paths是存储state中的那些数据,如果是模块下具体的数据需要加上模块名称,如user.token
===> 修改state后触发才可以看到本地存储数据的的变化。
测试: user模块定义一个mutation在main.js去调用下,观察浏览器application的localStorage下数据。
4.axios工具封装
因为axios基本配置满足不了我们后续的需求,所以需要对axios进行二次封装,实现请求拦截器、响应拦截器、token自动携带等功能,最后我们要导出一个函数,调用当前的axios实例发送请求,返回值promise。
1.安装 axios
npm i axios
2.新建 src/utils/request.js 模块,代码如下
// 1. 创建一个新的axios实例
// 2. 请求拦截器,如果有token进行头部携带
// 3. 响应拦截器:1. 剥离无效数据 2. 处理token失效
// 4. 导出一个函数,调用当前的axsio实例发请求,返回值promise
import axios from 'axios'
import store from '@/store'
import router from '@/router'
// 导出基准地址,原因:其他地方不是通过axios发请求的地方用上基准地址
export const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net/'
const instance = axios.create({
// axios 的一些配置,baseURL timeout
baseURL,
timeout: 5000
})
instance.interceptors.request.use(config => {
// 拦截业务逻辑
// 进行请求配置的修改
// 如果本地又token就在头部携带
// 1. 获取用户信息对象
const { profile } = store.state.user
// 2. 判断是否有token
if (profile.token) {
// 3. 设置token
config.headers.Authorization = `Bearer ${profile.token}`
}
return config
}, err => {
return Promise.reject(err)
})
// res => res.data 取出data数据,将来调用接口的时候直接拿到的就是后台的数据
instance.interceptors.response.use(res => res.data, err => {
// 401 状态码,进入该函数
if (err.response && err.response.status === 401) {
// 1. 清空无效用户信息
// 2. 跳转到登录页
// 3. 跳转需要传参(当前路由地址)给登录页码
store.commit('user/setUser', {})
// 当前路由地址
// 组件里头:`/user?a=10` $route.path === /user $route.fullPath === /user?a=10
// js模块中:router.currentRoute.value.fullPath 就是当前路由地址,router.currentRoute 是ref响应式数据
const fullPath = encodeURIComponent(router.currentRoute.value.fullPath)
// encodeURIComponent 转换uri编码,防止解析地址出问题
router.push('/login?redirectUrl=' + fullPath)
}
return Promise.reject(err)
})
// 请求工具函数
export default (url, method, submitData) => {
// 负责发请求:请求地址,请求方式,提交的数据
return instance({
url,
method,
// 1. 如果是get请求 需要使用params来传递submitData ?a=10&c=10
// 2. 如果不是get请求 需要使用data来传递submitData 请求体传参
// [] 设置一个动态的key, 写js表达式,js表达式的执行结果当作KEY
// method参数:get,Get,GET 转换成小写再来判断
// 在对象,['params']:submitData ===== params:submitData 这样理解
[method.toLowerCase() === 'get' ? 'params' : 'data']: submitData
})
}
5.公共样式/变量的全局引入
目的:将accets/styles中一些需要全局应用的css/less文件全自动导入,这样子我们在任何vue文件中随时使用定义好的变量和使用一些特定的样式。
1)准备要用的变量和混入代码
变量 src/assets/styles/variables.less
// 主题 @xtxColor:#27BA9B; // 辅助 @helpColor:#E26237; // 成功 @sucColor:#1DC779; // 警告 @warnColor:#FFB302; // 价格 @priceColor:#CF4444;
混入 src/assets/styles/mixins.less
// 鼠标经过上移阴影动画 .hoverShadow () { transition: all .5s; &:hover { transform: translate3d(0,-3px,0); box-shadow: 0 3px 8px rgba(0,0,0,0.2); }
2)完成自动注入公共变量和混入
传统导入遇到的问题:每次使用公共变量和mixin的时候需要单独引入到文件中。
解决方法:使用vuecli的style-resources-loader插件来完成自动注入到每个less文件或者vue组件中style标签中。
在当前项目下执行一下命令 vue add style-resources-loader ,添加一个vuecli的插件
vue add pluginName 是vue-cli3提供的。vue add 是用yarn安装插件的,
注意:如果使用 vue add style-resources-loader 这个命令去下载依赖,vue会下载两个loader(开发依赖),分别是 “vue-cli-plugin-style-resources-loader” 和 “style-resources-loader” ,如果单单使用npm i 下载一个style-resources-loader 应用在vuecli的项目里会无法引入变量,一使用就错误。所以最好的解决方法就是使用 vue add 命令来下
安装完毕后会在vue.config.js中自动添加配置,如下
module.exports = { pluginOptions: { 'style-resources-loader': { preProcessor: 'less', patterns: [] } } }
把你需要注入的文件配置一下后,重启服务即可。 ```javascript
- const path = require(‘path’)
module.exports = {
pluginOptions: {
‘style-resources-loader’: {
preProcessor: 'less', patterns: [
- path.join(__dirname, ‘./src/assets/styles/variables.less’),
- path.join(__dirname, ‘./src/assets/styles/mixins.less’) ] } } } ``` 总结: 知道如何定义less变量和混入代码并使用他们,通过vue-resources-loader完成代码注入再每个less文件和vue组件中
6.项目样式的重置和公用
目的:准备网站所需的重置样式代码,以及一些公用样式代码。
- 重置样式
执行 npm i normalize.css 安装重置样式的包,然后在 main.js 导入 normalize.css 即可。
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
+ import 'normalize.css'
createApp(App).use(store).use(router).mount('#app')
- 公用样式
新建文件 src/assets/styles/common.less 在该文件写入常用的样式,然后在 main.js 导入即可。
src/assets/styles/common.less
// 重置样式
* {
box-sizing: border-box;
}
html {
height: 100%;
font-size: 14px;
}
body {
height: 100%;
color: #333;
min-width: 1240px;
font: 1em/1.4 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI', 'Hiragino Sans GB', 'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei', sans-serif
}
ul,
h1,
h3,
h4,
p,
dl,
dd {
padding: 0;
margin: 0;
}
a {
text-decoration: none;
color: #333;
outline: none;
}
i {
font-style: normal;
}
input[type="text"],
input[type="search"],
input[type="password"],
input[type="checkbox"]{
padding: 0;
outline: none;
border: none;
-webkit-appearance: none;
&::placeholder{
color: #ccc;
}
}
img {
max-width: 100%;
max-height: 100%;
vertical-align: middle;
}
ul {
list-style: none;
}
#app {
background: #f5f5f5;
user-select: none;
}
.container {
width: 1240px;
margin: 0 auto;
position: relative;
}
.ellipsis {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.ellipsis-2 {
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.fl {
float: left;
}
.fr {
float: right;
}
.clearfix:after {
content: ".";
display: block;
visibility: hidden;
height: 0;
line-height: 0;
clear: both;
}
src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import 'normalize.css'
+import '@/assets/styles/common.less'
createApp(App).use(store).use(router).mount('#app')
总结: 重置样式使用normalize.css,项目公用样式common.less
7. vueuse/core包的使用
vueuse/core包封装了一些常见的交互逻辑,这是它的官方文档https://vueuse.org/
下面展示一下基础应用
安装:@vueuse/core 包
npm i @vueuse/core@4.9.0
使用:使用vueuser/core包的useWindowScroll() 返回当前页面滚动时候蜷曲的距离。x横向,y纵向
<script>
import { useWindowScroll } from '@vueuse/core'
export default {
setup () {
const { y,x } = useWindowScroll()
return { y,x }
}
}
</script>
总结:
- useWindowScroll() 是@vueuse/core提供的api可返回当前页面滚动时候蜷曲的距离。x横向,y纵向
- vue3.0组合API提供了更多逻辑代码封装的能力。@vueuse/core 基于组合API封装好用的工具函数。
8.骨架屏组件的封装和应用
目的:为了在加载的过程中等待效果更好,封装一个骨架屏组件
大致步骤:
- 需要一个组件,做占位使用。这个占位组件有个专业术语:骨架屏组件。
- 暴露一些属性:高,宽,背景,是否有闪动画。
- 这是一个公用组件,需要全局注册,将来这样的组件建议再vue插件中定义。
- 使用组件完成左侧分类骨架效果。
1) 封装骨架屏组件:src/components/library/xtx-skeleton.vue
<template>
<div class="xtx-skeleton" :style="{width,height}" :class="{shan:animated}">
<!-- 1 盒子-->
<div class="block" :style="{backgroundColor:bg}"></div>
<!-- 2 闪效果 xtx-skeleton 伪元素 --->
</div>
</template>
<script>
export default {
name: 'XtxSkeleton',
// 使用的时候需要动态设置 高度,宽度,背景颜色,是否闪下
props: {
bg: {
type: String,
default: '#efefef'
},
width: {
type: String,
default: '100px'
},
height: {
type: String,
default: '100px'
},
animated: {
type: Boolean,
default: false
}
}
}
</script>
<style scoped lang="less">
.xtx-skeleton {
display: inline-block;
position: relative;
overflow: hidden;
vertical-align: middle;
.block {
width: 100%;
height: 100%;
border-radius: 2px;
}
}
.shan {
&::after {
content: "";
position: absolute;
animation: shan 1.5s ease 0s infinite;
top: 0;
width: 50%;
height: 100%;
background: linear-gradient(
to left,
rgba(255, 255, 255, 0) 0,
rgba(255, 255, 255, 0.3) 50%,
rgba(255, 255, 255, 0) 100%
);
transform: skewX(-45deg);
}
}
@keyframes shan {
0% {
left: -100%;
}
100% {
left: 120%;
}
}
</style>
2) 封装组件:插件定义 src/componets/library/index.js 使用插件 src/main.js
目的:写完组件后我们可以在其它vue文件中直接引入,但是因为骨架屏组件要在多个vue文件中多次使用,所以我们使用插件定义将组件在app上进行扩展
src/componets/library/index.js
// 扩展vue原有的功能:全局组件,自定义指令,挂载原型方法,注意:没有全局过滤器。
// 这就是插件
// vue2.0插件写法要素:导出一个对象,有install函数,默认传入了Vue构造函数,Vue基础之上扩展
// vue3.0插件写法要素:导出一个对象,有install函数,默认传入了app应用实例,app基础之上扩展
import XtxSkeleton from './xtx-skeleton.vue'
export default {
install (app) {
// 在app上进行扩展,app提供 component directive 函数
// 如果要挂载原型 app.config.globalProperties 方式
app.component(XtxSkeleton.name, XtxSkeleton)
}
}
src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './mock'
+ import ui from './components/library'
import 'normalize.css'
import '@/assets/styles/common.less'
+ // 插件的使用,在main.js使用app.use(插件)
+ createApp(App).use(store).use(router).use(ui).mount('#app')
3)最后使用组件完成左侧分类骨架屏效果: src/views/home/components/home-category.vue
<ul class="menu">
<li :class="{active:categoryId===item.id}" v-for="item in menuList" :key="item.id" @mouseenter="categoryId=item.id">
<RouterLink to="/">{{item.name}}</RouterLink>
<template v-if="item.children">
<RouterLink to="/" v-for="sub in item.children" :key="sub.id">{{sub.name}}</RouterLink>
</template>
+ <span v-else>
+ <XtxSkeleton width="60px" height="18px" style="margin-right:5px" bg="rgba(255,255,255,0.2)" />
+ <XtxSkeleton width="50px" height="18px" bg="rgba(255,255,255,0.2)" />
+ </span>
</li>
</ul>
加上骨架屏样式
.xtx-skeleton {
animation: fade 1s linear infinite alternate;
}
@keyframes fade {
from {
opacity: 0.2;
}
to {
opacity: 1;
}
}
9.组件数据懒加载
目的: 实现当组件进入可视化区域再加载数据
我们可以使用 @vueuse/core 中的 useIntersectionObserver 来实现监听进入可视区域行为,但是必须配合vue3.0的组合API的方式才能实现。
大致步骤:
- 理解 useIntersectionObserver 的使用,各个参数的含义
- 改造 home-new 组件成为数据懒加载,掌握 useIntersectionObserver 函数的用法
- 封装 useLazyData 函数,作为数据懒加载公用函数
- 把 home-new 和 home-hot 改造成懒加载方式
落地代码:
1.先分析下这个useIntersectionObserver 函数:
// stop 是停止观察是否进入或移出可视区域的行为
const { stop } = useIntersectionObserver(
// target 是观察的目标dom容器,必须是dom容器,而且是vue3.0方式绑定的dom对象
target,
// isIntersecting 是否进入可视区域,true是进入 false是移出
// observerElement 被观察的dom
([{ isIntersecting }], observerElement) => {
// 在此处可根据isIntersecting来判断,然后做业务
},
)
2.开始改造 home-new 组件:rc/views/home/components/home-new.vue
<div ref="box" style="position: relative;height: 406px;">
// 省略。。。
<script>
import HomePanel from './home-panel'
import HomeSkeleton from './home-skeleton'
import { findNew } from '@/api/home'
import { ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
export default {
name: 'HomeNew',
components: { HomePanel, HomeSkeleton },
setup () {
const goods = ref([])
const box = ref(null) //取得主体容器
const { stop } = useIntersectionObserver(
box, ([{ isIntersecting }]) => {
if (isIntersecting) {
stop()
findNew().then(data => {
goods.value = data.result
})
}
}
)
return { goods, box }
}
}
</script>
3)由于首页面板数据加载都需要实现懒数据加载,所以封装一个钩子函数,得到数据。
src/hooks/index.js
// hooks 封装逻辑,提供响应式数据。
import { useIntersectionObserver } from '@vueuse/core'
import { ref } from 'vue'
// 数据懒加载函数
export const useLazyData = (apiFn) => {
// 需要
// 1. 被观察的对象
// 2. 不同的API函数
const target = ref(null)
const result = ref([])
const { stop } = useIntersectionObserver(
target,
([{ isIntersecting }], observerElement) => {
if (isIntersecting) {
stop()
// 调用API获取数据
apiFn().then(data => {
result.value = data.result
})
}
}
)
// 返回--->数据(dom,后台数据)
return { target, result }
}
4)再次改造 home-new 组件:rc/views/home/components/home-new.vue
import { findNew } from '@/api/home'
+import { useLazyData } from '@/hooks'
export default {
name: 'HomeNew',
components: { HomePanel, HomeSkeleton },
setup () {
+ const { target, result } = useLazyData(findNew)
+ return { goods: result, target }
}
}
<div ref="target" style="position: relative;height: 426px;">
5)然后改造 home-hot 组件:src/views/home/components/home-hot.vue
<div ref="target" style="position: relative;height: 426px;">
import { findHot } from '@/api/home'
import HomePanel from './home-panel'
import HomeSkeleton from './home-skeleton'
+ import { useLazyData } from '@/hooks'
export default {
name: 'HomeHot',
components: { HomePanel, HomeSkeleton },
setup () {
+ const { target, result } = useLazyData(findHot)
+ return { target, list: result }
}
}
10.图片懒加载
目的:当图片进入可视区域内去加载图片,且处理加载失败,封装成指令
介绍一个webAPI:IntersectionObserver
// 创建观察对象实例
const observer = new IntersectionObserver(callback[, options])
// callback 被观察dom进入可视区离开可视区都会触发
// - 两个回调参数 entries , observer
// - entries 被观察的元素信息对象的数组 [{元素信息},{}],信息中isIntersecting判断进入或离开
// - observer 就是观察实例
// options 配置参数
// - 三个配置属性 root rootMargin threshold
// - root 基于的滚动容器,默认是document
// - rootMargin 容器有没有外边距
// - threshold 交叉的比例
// 实例提供两个方法
// observe(dom) 观察哪个dom
// unobserve(dom) 停止观察那个dom
基于vue3.0和IntersectionObserver封装懒加载指令
src/components/library/index.js
export default {
install (app) {
app.component(XtxSkeleton.name, XtxSkeleton)
app.component(XtxCarousel.name, XtxCarousel)
app.component(XtxMore.name, XtxMore)
+ defineDirective(app)
}
}
import defaultImg from '@/assets/images/200.png'
// 指令
const defineDirective = (app) => {
// 图片懒加载指令
app.directive('lazyload', {
mounted (el, binding) {
const observer = new IntersectionObserver(([{ isIntersecting }]) => {
if (isIntersecting) {
observer.unobserve(el)
el.onerror = () => {
el.src = defaultImg
}
el.src = binding.value
}
}, {
threshold: 0.01
})
observer.observe(el)
}
})
}
使用指令:
src/views/home/component/home-product.vue
<RouterLink class="cover" to="/">
+ <img alt="" v-lazyload="cate.picture">
<strong class="label">
<span>{{cate.name}}馆</span>
<span>{{cate.saleInfo}}</span>
</strong>
</RouterLink>
src/views/home/component/home-goods.vue
<RouterLink to="/" class="image">
+ <img alt="" v-lazyload="goods.picture" />
</RouterLink>
总结:
- 在img上使用使用v-lazyload值为图片地址,不设置src属性。指令会观察图片是否进入可视区域,如果进入会动态的给图片src属性赋值实现懒加载。
11 本地购物车和服务端购物车的实现思路
· 小兔鲜组件封装知识
redeme:小兔鲜项目针对了PC端购物商场类的公共组件库较少的特点,项目封装了多个公用组件,并且集中全局注册,并且使用虚拟dom技术封装了弹窗和提示组件,这些知识点需要重点学习
1组件封装:批量注册组件
备注:重点在几种导出的 index.js 文件,需要在该文件中导出一个对象,对象能包含一个 install函数 ,install会传入一个app实例,我们利用这个实例上面的 component 集中的进行注册。
接着在 主文件_main.js 中导入index.js中导出的对象,进行vue.use()调用即可全局注册组件
文件目录:src/components/library
使用方法:在文件中创建组件(组件正常书写),在library文件中创建index.js用来统一注册组件,然后再main.js中引入 src/components/library/index.js 并挂载·,就可以批量注册组件了。
Vue2中的使用方法
src/components/library/myTest.vue
<template>
<div>
<span>测试注册全局组件,我是组件Test</span>
</div>
</template>
<script>
export default {
name:'MyTest'
}
</script>
<style scoped></style>
src/components/library/index.js
// 扩展vue原有的功能:全局组件,自定义指令,挂载原型方法等。
// 这就是插件
// vue2.0插件写法要素:导出一个对象,有install函数,默认传入了Vue构造函数,Vue基础之上扩展
import Vue from 'vue'
import MyTest from './myTest.vue' //我们书写的组件
export default {
// Vue2中的install函数中默认传入的Vue的构造函数,我们要在全局上组件组件就要通过在Vue这个构造函数上进行扩展
install (Vue) {
// 在Vue上进行扩展,使用Vue提供的 component 方法
// 也可以直接在Vue.prototype.[keyName] 中进行挂载
Vue.prototype.$aa = '你好' // 测试一个全局属性
Vue.component(MyTest.name, MyTest) //挂载一个组件
}
}
src/main.js
import Vue from 'vue'
import App from './App.vue'
import ui from './components/library/index' // 导入全局组件
Vue.config.productionTip = false
Vue.use(ui) // 使用Vue.use() 方法进行挂载,use会执行对象中的install方法
const app = new Vue({
render: h => h(App),
}).$mount('#app')
console.log(app.$aa) // 输出:你好
Vue3中的使用方法
src/components/library/myTest.vue
<template>
<div>
我是Vue3的myTest组件
</div>
</template>
<script>export default {name:'MyTest'}</script>
src/components/library/index.js
// vue3.0插件写法要素:导出一个对象,有install函数,默认传入了app应用实例,app基础之上扩展
import MyTest from './MyTest.vue'
export default {
install (app) {
// 在app上进行扩展,app提供 component directive 函数
// 如果要挂载原型 app.config.globalProperties 方式
app.component(MyTest.name, MyTest)
}
}
src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import ui from './components/library/index' // 引入
createApp(App).use(ui).mount('#app') //use 进行挂载
理解
这样子的封装方式避免了我们直接在main.js中大量的引入组件/注册组件,使用一个单独的 js 文件来管理我们全局注册的组件。
2 全局自动批量注册组件
理解:上面的组件注册方法,在我们写好一个组件之后,想要引入就要在index.js中引入组件并在install方法中进行挂载。为了方便我们开发,所以需要写好一个自动注册组件的方法。
src/components/library/index.js
// 导入library文件夹下的所有组件
// 批量导入需要使用一个函数 require.context(dir,deep,matching)
// 参数:1. 目录 2. 是否加载子目录 3. 加载的正则匹配
const importFn = require.context('./', false, /\.vue$/)
// console.dir(importFn.keys()) 文件名称数组
export default {
install (app) {
// 批量注册全局组件
importFn.keys().forEach(key => {
// 导入组件
const component = importFn(key).default
// 注册组件
app.component(component.name, component)
})
}
}
输出的 importFn.keys() (文件名称数组)
至此,这就是批量注册组件的的主要代码
3 Message工具函数的封装
备注:
- 首先我们要封装给出一个可用的弹窗组件,先确保这个组件可以正常使用
- 接着我们尝试穿件一个工具函数,利用js动态控制弹窗的显示,
作用:Message组件是一个弹出的提醒框,固定顶部显示,有成功、警告、失败三种状态,可以提示用户的操作是否成功。组件要在多个地方重复使用,所以要进行全局注册,且为了配合状态返回要封装成 工具函数 方式。
学习工具函数封装之前,我们先使用传统的组件形式进行书写,以便于我们更好的理解后续的工具函数的实现
myMessage.vue 组件
<template>
<Transition name="down">
<!-- 绑定样式 -->
<div class="xtx-message" :style="style[type]" v-show="visible">
<!-- 绑定图标 -->
<i class="iconfont" :class="[style[type].icon]"></i>
<!-- 绑定文字 -->
<span class="text">{{text}}</span>
</div>
</Transition>
</template>
<script>
import { onMounted, ref } from 'vue'
export default {
name: 'XtxMessage',
props: {
type: {
type: String,
default: 'warn',
desc:'描述弹窗的状态,有三个值 warn(警告|默认) success(成功) error(错误)'
},
text: {
type: String,
default: '',
desc:'文本框的文字'
}
},
setup () {
// 定义一个对象,包含三种情况的样式,对象key就是类型字符串
// 分别对应三种不同的样式(图表、字体颜色、背景颜色、边框颜色)
const style = {
warn: {
icon: 'icon-warning',
color: '#E6A23C',
backgroundColor: 'rgb(253, 246, 236)',
borderColor: 'rgb(250, 236, 216)'
},
error: {
icon: 'icon-shanchu',
color: '#F56C6C',
backgroundColor: 'rgb(254, 240, 240)',
borderColor: 'rgb(253, 226, 226)'
},
success: {
icon: 'icon-queren2',
color: '#67C23A',
backgroundColor: 'rgb(240, 249, 235)',
borderColor: 'rgb(225, 243, 216)'
}
}
// 控制元素显示隐藏
const visible = ref(false)
onMounted(() => {
visible.value = true // 当组件显示加载完成,让元素进行显示
})
return { style, visible }
}
}
</script>
<style scoped lang="less">
.down {
&-enter {
&-from { // 进入动画的初态
transform: translate3d(0,-75px,0);
opacity: 0;
}
&-active { // 进入动画的起始状态
transition: all 0.5s;
}
&-to { // 进入动画的终态
transform: none;
opacity: 1;
}
}
}
.xtx-message { //基础样式
width: 300px;
height: 50px;
position: fixed;
z-index: 9999;
left: 50%;
margin-left: -150px;
top: 25px;
line-height: 50px;
padding: 0 25px;
border: 1px solid #e4e4e4;
background: #f5f5f5;
color: #999;
border-radius: 4px;
i {
margin-right: 4px;
vertical-align: middle;
}
.text {
vertical-align: middle;
}
}
</style>
在app.vue中引入并使用
<template>
<div>
<MyMessage type="success" text="成功显示啦"></MyMessage>
</div>
</template>
<script>
import MyMessage from './components/library/my-message.vue'
export default {
components:{MyMessage}
}
</script>
效果如下:
接着我们将其封装成vue实例函数式调用
- vue3.0使用app.config.globalProperties挂载原型方法
- 也支持直接导入函数使用
src/components/library/Message.js
// 实现使用函数调用xtx-message组件逻辑
import {createVNode,render} from 'vue' // 导入需要的函数
import MyMessage from './my-message.vue' // 导入组件
// 准备容器 : 创建一个容器(div元素)
const div = document.createElement('div')
// 设置其class为xtx-message-container
div.setAttribute('class','xtx-message-container')
// 将其放入body中
document.body.appendChild(div)
// 定时器标识
let timer = null
// 这里导出一个方法
export default ({type,text}) => {
// 实现:根据my-messgae.vue 渲染消息提示
// 1.导入组件
// 2.根据组件创建虚拟节点
const vnode = createVNode(MyMessage,{type,text})
// 3.准备一个DOM容器
// 4.把虚拟节点渲染DOM容器中
render(vnode, div)
// 5.开启定时,移出DOM容器内容
clearTimeout(timer)
timer = setTimeout(() => {
render(null,div)
},3000)
}
在app.vue中使用
<script>
import Message from '@/components/library/Message.js'
setTimeout(()=>{
Message({type:"success",text:'你好'})
},1000)
export default{}
</script>
尝试引入到全局中(vue3)
在 index.js中
+ import Message from './Message'
+ app.config.globalProperties.$Message = Message //绑定全局的方法 (vue3的方法)
// 在vue2中需要对install传入的的Vue实例的prototype上绑定方法,通过this来访问
在app.vue中进行使用(第一张是vue3中setup写法,第二张是vue的写法)
在vue3中官方不推荐设置全局属性了,在官网中也无法找到 getCurrentInstance 这个api的相关说明(2022.8.19),app.config.globalProperties 的这种配置方式也会说明为是vue2中全局属性配置的一种替代。
4 Confirm 工具函数的封装
my-confirm.vue 组件
<template>
<div class="xtx-confirm" :class="{fade}">
<div class="wrapper" :class="{fade}">
<div class="header">
<h3>{{title}}</h3>
<a @click="cancelCallback()" href="JavaScript:;" class="iconfont icon-close-new"></a>
</div>
<div class="body">
<i class="iconfont icon-warning"></i>
<span>{{text}}</span>
</div>
<div class="footer">
<button @click="cancelCallback()">取消</button>
<button @click="submitCallback()">确认</button>
</div>
</div>
</div>
</template>
<script>
import { onMounted, ref } from 'vue'
export default {
name: 'MyConfirm',
props:{
title:{
type:String,
default:'温馨提示',
desc:'弹出框的标题'
},
text:{
type:String,
default:"",
desc:'弹出框的内容'
},
submitCallback:{
type:Function,
desc:'成功回调'
},
cancelCallback:{
type:Function,
desc:'取消回调'
}
},
setup(){
const fade = ref(false) //控制隐藏和显示
onMounted(() => {
// 当元素渲染完毕立即过度的动画不会触发
setTimeout(() => {
fade.value = true
},0)
})
return { fade }
}
}
</script>
<style scoped lang="less">
.xtx-confirm {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 8888;
background: rgba(0,0,0,0);
&.fade {
transition: all 0.4s;
background: rgba(0,0,0,.5);
}
.wrapper {
width: 400px;
background: #fff;
border-radius: 4px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-60%);
opacity: 0;
&.fade {
transition: all 0.4s;
transform: translate(-50%,-50%);
opacity: 1;
}
.header,.footer {
height: 50px;
line-height: 50px;
padding: 0 20px;
}
.body {
padding: 20px 40px;
font-size: 16px;
.icon-warning {
color: #CF4444;
margin-right: 3px;
font-size: 16px;
}
}
.footer {
text-align: right;
.xtx-button {
margin-left: 20px;
}
}
.header {
position: relative;
h3 {
font-weight: normal;
font-size: 18px;
}
a {
position: absolute;
right: 15px;
top: 15px;
font-size: 20px;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
color: #999;
&:hover {
color: #666;
}
}
}
}
}
</style>
在app.vue中使用
<template>
<div>
<MyConfirm text="你好"></MyConfirm>
</div>
</template>
<script setup>
import MyConfirm from '@/components/library/my-confirm.vue'
</script>
效果如图:
接着我们实现函数式调用组件方式和完成交互。
原理图:
定义函数src/components/library/my-confirm.js
import {createVNode,render} from 'vue' // 导入需要的函数
import MyConfirm from './my-confirm.vue' // 导入组件
// 准备div
const div = document.createElement('div')
div.setAttribute('class', 'my-confirm-container')
document.body.appendChild(div)
// 该函数渲染MyConfirm组件,标题和文本
// 函数的返回值是promise对象
export default({title,text}) => {
return new Promise((resolve,reject) => {
const submitCallback = () => {
render(null,div)
resolve()
}
const cancelCallback = () => {
render(null,div)
reject(new Error('点击取消'))
}
// 1.渲染组件
// 2.点击确认按钮,触发resolve同时销毁组件
// 3.点击取消按钮,触发reject同时销毁组件
const vnode = createVNode(MyConfirm,{title,text,submitCallback,cancelCallback})
render(vnode, div)
})
}
在app.vue中使用
<template>
<div>
</div>
</template>
<script setup>
import Confirm from '@/components/library/my-confirm'
setTimeout(() => {
Confirm({ title: '这是一个标题', text: '这是一段文字' }).then(() => {
console.log('点击了确认')
}, (e) => {
console.log(e)
})
}, 300)
</script>
成功渲染,并且根据点击取消或是确认进行不同的回调
5 其它组件的实现思路
5.1 分类-复选框组件
样式:
分析:组件样式较为简单,可以作为绑定组件的v-model的练习
大致步骤
- 实现组件本身的选中与不选中效果
- 实现组件的v-model命令
- 改造成 @vueuse/core 的函数写法
1)实现基本功能
<template>
<div class="xtx-checkbox" @click="changeChecked()">
<i v-if="checked" class="iconfont icon-checked"></i> //渲染选中的样式
<i v-else class="iconfont icon-unchecked"></i> //渲染不选中的样式
<span v-if="$slots.default"><slot /></span> //文字信息,利用插槽进行插入
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'XtxCheckbox',
setup () {
const checked = ref(false) // 控制组件选择或不选中的方法
const changeChecked = () => { //改变组件选择和不选中的方法
checked.value = !checked.value
}
return { checked, changeChecked }
}
}
</script>
2)实现双向绑定
vue3中的v-model会被拆解为 属性 modelValue 和 事件 update:modelValue
import { ref, watch } from 'vue'
// v-model ====> :modelValue + @update:modelValue
export default {
name: 'XtxCheckbox',
props: { // 通过props得到父组件传递的 modelValue 数据
modelValue: {
type: Boolean,
default: false
}
},
setup (props, { emit }) {
const checked = ref(false)
const changeChecked = () => {
checked.value = !checked.value
// 使用emit通知父组件数据的改变
emit('update:modelValue', checked.value) //通知父组件进行修改数据
}
// 使用侦听器,得到父组件传递数据,给checked数据
watch(() => props.modelValue, () => {
checked.value = props.modelValue
}, { immediate: true })
return { checked, changeChecked }
}
}
3)补充 @vueuse/core 的实现
import { useVModel } from '@vueuse/core'
// v-model ====> :modelValue + @update:modelValue
export default {
name: 'XtxCheckbox',
props: {
modelValue: {
type: Boolean,
default: false
}
},
setup (props, { emit }) {
// 使用useVModel实现双向数据绑定v-model指令
// 1. 使用props接收modelValue
// 2. 使用useVModel来包装props中的modelValue属性数据
// 3. 在使用checked.value就是使用父组件数据
// 4. 在使用checked.value = '数据' 赋值,触发emit('update:modelvalue', '数据')
const checked = useVModel(props, 'modelValue', emit)
const changeChecked = () => {
const newVal = !checked.value
// 通知父组件
checked.value = newVal
// 让组件支持change事件
// 兼容性支持,但是不写也行, 通过了useVModel进行包装之后会自动触发 update:modelvalue 事件
emit('change', newVal)
}
return { checked, changeChecked }
}
}
5.2 分类-面包屑组件
样式:
作用:可以通过点击上面的路由数据进行便捷式的跳转
实现思路:当进入商品详情的时候,根据商品详情的id查询对应的数据,数据结构如下
其重点数据为返回数据中的 categories ,内包含路由层级数据,如第一张图所示的数据,首页我们可以直接通过 “/“ 进行固定渲染,服饰和商品列表的链接通过categories 中的信息渲染,其次商品的链接我们通过商品id进行渲染即可。 具体实现主要通过后端,和传统管理系统中的面包屑有所不同。
组件书写思路: 封装为xtx-bread.vue 和 xtx-bread-item.vue 两个组件(终极版本)
xtx-bread.vue
<script>
import { h } from 'vue'
export default {
name: 'XtxBread',
render () {
// 用法
// 1. template 标签去除,单文件组件
// 2. 返回值就是组件内容
// 3. vue2.0 的h函数传参进来的,vue3.0 的h函数导入进来
// 4. h 第一个参数 标签名字 第二个参数 标签属性对象 第三个参数 子节点
// 需求
// 1. 创建xtx-bread父容器
// 2. 获取默认插槽内容
// 3. 去除xtx-bread-item组件的i标签,因该由render函数来组织
// 4. 遍历插槽中的item,得到一个动态创建的节点,最后一个item不加i标签
// 5. 把动态创建的节点渲染再xtx-bread标签中
const items = this.$slots.default()
const dymanicItems = []
items.forEach((item, i) => {
// 对插槽节点进行判断(是XtxBreadItem和Transition才进行组装)
if (item.type.name === 'XtxBreadItem' || item.type.displayName === 'Transition') {
dymanicItems.push(item)
if (i < (items.length - 1)) {
dymanicItems.push(h('i', { class: 'iconfont icon-angle-right' }))
}
}
})
return h('div', { class: 'xtx-bread' }, dymanicItems)
}
}
</script>
<style lang='less'>
// 去除 scoped 属性,目的:然样式作用到xtx-bread-item组件
.xtx-bread{
display: flex;
padding: 25px 10px;
// ul li:last-child {}
// 先找到父元素,找到所有的子元素,找到最后一个,判断是不是LI,是就是选中,不是就是无效选择器
// ul li:last-of-type {}
// 先找到父元素,找到所有的类型为li的元素,选中最后一个
&-item {
a {
color: #666;
transition: all .4s;
&:hover {
color: @xtxColor;
}
}
}
i {
font-size: 12px;
margin-left: 5px;
margin-right: 5px;
line-height: 22px;
// 样式的方式,不合理
// &:last-child {
// display: none;
// }
}
}
</style>
xtx-bread-item.vue
<template>
<div class="xtx-bread-item">
<RouterLink v-if="to" :to="to"><slot /></RouterLink>
<span v-else><slot /></span>
</div>
</template>
<script>
export default {
name: 'XtxBreadItem',
props: {
to: {
type: [String, Object],
default: '/'
}
}
}
</script>
具体思路点击链接: http://zhoushugang.gitee.io/erabbit-client-pc-document/guide/04-category.html#_01-%E9%A1%B6%E7%BA%A7%E7%B1%BB%E7%9B%AE-%E9%9D%A2%E5%8C%85%E5%B1%91%E7%BB%84%E4%BB%B6-%E5%88%9D%E7%BA%A7
5.3 分类-商品列表无限加载组件
作用 和 实现思路:
利用 @vueuse/core 的 useIntersectionObserver 方法进行处理,useIntersectionObserver 第一个参数为监视的dom元素,第二参数为一个回调函数,回调函数接受两个参数,第一个参数是观察的结果,第二个参数是dom元素,可以监听传入的DOM节点的滑动时间,并监听是否滑动至底部。 如果滑动到底部就加载新的商品数据,实现商品信息的懒加载和无限信息加载。
具体步骤:
- 完成结果区域商品布局
- 完成 xtx-infinte-loading 组件封装
- 实现 xtx-infinte-loading 完成数据加载和渲染
1)实现基础布局
<template>
<div class='sub-category'>
<div class="container">
<!-- 面包屑 -->
<SubBread />
<!-- 筛选区 -->
<SubFilter />
<!-- 结果区域 -->
<div class="goods-list">
<!-- 排序 -->
<SubSort />
<!-- 列表 -->
<ul>
<li v-for="i in 20" :key="i" >
<GoodsItem :goods="{}" />
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import SubBread from './components/sub-bread'
import SubFilter from './components/sub-filter'
import SubSort from './components/sub-sort'
import GoodsItem from './components/goods-item'
export default {
name: 'SubCategory',
components: { SubBread, SubFilter, SubSort, GoodsItem }
}
</script>
<style scoped lang='less'>
.goods-list {
background: #fff;
padding: 0 25px;
margin-top: 25px;
ul {
display: flex;
flex-wrap: wrap;
padding: 0 5px;
li {
margin-right: 20px;
margin-bottom: 20px;
&:nth-child(5n) {
margin-right: 0;
}
}
}
}
</style>
2)无限列表加载组件
xtx-infinte-loading.vue
<template>
<div class="xtx-infinite-loading" ref="container">
<div class="loading" v-if="loading">
<span class="img"></span>
<span class="text">正在加载...</span>
</div>
<div class="none" v-if="finished">
<span class="img"></span>
<span class="text">亲,没有更多了</span>
</div>
</div>
</template>
<script>
import { ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
export default {
name: 'XtxInfiniteLoading',
props: {
loading: {
type: Boolean,
default: false
},
finished: {
type: Boolean,
default: false
}
},
setup(props, { emit }) {
const container = ref(null)
useIntersectionObserver(
container,
([{ isIntersecting }], dom) => {
if (isIntersecting) {
if (props.loading === false && props.finished === false) {
emit('infinite')
}
}
},
{
threshold: 0
}
)
return { container }
}
}
</script>
<style scoped lang='less'>
.xtx-infinite-loading {
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
.img {
width: 50px;
height: 50px;
background: url(../../assets/images/load.gif) no-repeat center / contain;
}
.text {
color: #999;
font-size: 16px;
}
}
.none {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
.img {
width: 200px;
height: 134px;
background: url(../../assets/images/none.png) no-repeat center / contain;
}
.text {
color: #999;
font-size: 16px;
}
}
}
</style>
5.4 商品详情-sku组件
http://zhoushugang.gitee.io/erabbit-client-pc-document/guide/05-detail.html#_07-%E2%98%85%E8%A7%84%E6%A0%BC%E7%BB%84%E4%BB%B6-sku-spu%E6%A6%82%E5%BF%B5
5.5 订单管理-步骤条组件
订单的详细数据内包含的订单的state,根据state进行渲染即可,难度不大、
大致步骤:
- xtx-steps 封装一个静态步骤条
- xtx-steps-item 封装步骤条-条目
- xtx-steps 组织组件结构
- xtx-steps 设置激活步骤
- 使用steps组件显示订单进度
落的代码:
- xtx-steps 封装一个静态步骤条 ```vue
2. xtx-steps-item 封装步骤条-条目
```vue
<script>
export default {
name: 'XtxStepsItem',
props: {
title: {
type: String,
default: ''
},
desc: {
type: String,
default: ''
}
}
}
</script>
使用steps组件显示订单进度
src/views/member/order/components/detail-steps.vue
<template>
<div class="detail-steps" style="padding:20px">
<XtxSteps :active="order.orderState===6?1:order.orderState">
<XtxStepsItem title="提交订单" :desc="order.createTime" />
<XtxStepsItem title="付款成功" :desc="order.payTime" />
<XtxStepsItem title="商品发货" :desc="order.consignTime" />
<XtxStepsItem title="确认收货" :desc="order.evaluationTime" />
<XtxStepsItem title="订单完成" :desc="order.endTime" />
</XtxSteps>
</div>
</template>
<script>
export default {
props: {
order: {
type: Object,
default: () => ({})
}
},
name: 'DetailSteps'
}
</script>
<style scoped lang="less"></style>
src/views/member/order/index.vue
<!-- 步骤条-->
<DetailSteps :order="order" />
import DetailSteps from './components/detail-steps'
export default {
name: 'OrderDetailPage',
components: { DetailAction, DetailSteps },
· 小兔鲜路由划分知识
路径 | 组件(功能) | 嵌套级别 |
---|---|---|
/ | 首页布局容器(layout.vue) | 1级 |
/ | 首页 | 2级 |
/category/:id | 一级分类 | 2级 |
/category/sub/:id | 二级分类 | 2级 |
/product/:id | 商品详情 | 2级 |
/login | 登录 | 1级 |
/login/callback | 第三方登录回调 | 1级 |
/cart | 购物车 | 2级 |
/member/checkout | 填写订单 | 2级 |
/member/pay | 进行支付 | 2级 |
/member/pay/result | 支付结果 | 2级 |
/member | 个人中心布局容器 | 2级 |
/member | 个人中心 | 3级 |
/member/order | 订单管理 | 3级 |
/member/order/:id | 订单详情 | 3级 |