[TOC]

readme

本笔记用于记录在开发黑马小兔鲜项目中值得学习的习惯和技术

1.创建项目

第一步:打开命令行窗口
image.png
第二步:执行创建项目命令行

执行:vue create 【项目名】

image.png
第三步:选择自定义创建

可以选择基础的模板,也可以自定义选择其它配置

image.png
第四步:选择基本配置

在这里可以使用空格 选择/取消 配置,选择完毕后按enter进入下一步 这里我们 选中vue-router,vuex,css Pre-processors选项

image.png
第五步:选择vue版本
image.png
第六步:选择hash模式的路由
image.png
第七步:选择less作为预处理器
image.png
第八步:选择 standard 标准代码风格
image.png
第九步:保存代码校验代码风格,代码提交时候校验代码风格
image.png
第十步:依赖插件或者工具的配置文件分文件保存
image.png
第十一步:是否记录以上操作,选择否
image.png
第十二步:等待安装…
image.png
最后:安装完毕

有些步骤根据当前项目的配置不一定会出现,所以要根据当前的项目情况而定

image.png

2.常用目录结构

当目录建成之后,我们需要删除一些无用的代码和文件,然后完善项目整体的目录结构,我们选用一套开发常用的通用目录结构,良好的目录结构组成能方便我们后续的开发协作。

image.png

文件夹说明

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的时候需要单独引入到文件中。
image.png
解决方法:使用vuecli的style-resources-loader插件来完成自动注入到每个less文件或者vue组件中style标签中。
在当前项目下执行一下命令 vue add style-resources-loader ,添加一个vuecli的插件
vue add pluginName 是vue-cli3提供的。vue add 是用yarn安装插件的,
image.png

注意:如果使用 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() (文件名称数组)
image.png
至此,这就是批量注册组件的的主要代码

3 Message工具函数的封装

备注:

  1. 首先我们要封装给出一个可用的弹窗组件,先确保这个组件可以正常使用
  2. 接着我们尝试穿件一个工具函数,利用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>

效果如下:
image.png

接着我们将其封装成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中全局属性配置的一种替代。

image.png
image.png

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>

效果如图:
image.png
接着我们实现函数式调用组件方式和完成交互。
原理图:
image.png
定义函数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>

image.png
成功渲染,并且根据点击取消或是确认进行不同的回调

5 其它组件的实现思路

5.1 分类-复选框组件

样式:
image.png
分析:组件样式较为简单,可以作为绑定组件的v-model的练习
大致步骤

  1. 实现组件本身的选中与不选中效果
  2. 实现组件的v-model命令
  3. 改造成 @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 分类-面包屑组件

样式:
image.png
作用:可以通过点击上面的路由数据进行便捷式的跳转
实现思路:当进入商品详情的时候,根据商品详情的id查询对应的数据,数据结构如下
image.png
其重点数据为返回数据中的 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节点的滑动时间,并监听是否滑动至底部。 如果滑动到底部就加载新的商品数据,实现商品信息的懒加载和无限信息加载。
具体步骤:

  1. 完成结果区域商品布局
  2. 完成 xtx-infinte-loading 组件封装
  3. 实现 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)无限列表加载组件
image.png
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 订单管理-步骤条组件

image.png
订单的详细数据内包含的订单的state,根据state进行渲染即可,难度不大、
大致步骤:

  • xtx-steps 封装一个静态步骤条
  • xtx-steps-item 封装步骤条-条目
  • xtx-steps 组织组件结构
  • xtx-steps 设置激活步骤
  • 使用steps组件显示订单进度

落的代码:

  1. 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 },

· 小兔鲜路由划分知识

image.png

路径 组件(功能) 嵌套级别
/ 首页布局容器(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级