基于 Vue3打造前台+中台通用提效解决方案(上)

基于 Vue3打造前台+中台通用提效解决方案

1、项目架构

本项目使用vite + vue3来实现前中台解决方案

2、为什么使用vite

因为,之前的项目一直都是使用webpack作为构建工具;vite出来这么久了,也没有用过;所以想在当前项目下进行使用;

2.1、为什么vite比webpack块?

webpack

假设我们的项目中有A、B两个页面。
其中A页面是项目首页,里面的代码一切正常。
B页面是一个需要经这跳转才会进入的页面,里面存在一些错误。比如︰我导入一个不存在的文件a.js 然后打印a
当我们去构建这个项目时,明明我们从来都没有进入过B页面,但是此时
webpack依然会给我们抛出一个对应的错误 `Can't resolve './a.js' in xxX`

webpack在开发时构建时,默认会去抓取并构建你的整个应用,然后才能提供服务,这就导致你的项目中,存在的任何一个错误(哪怕这个错误是在用户从来都没有进入过的页面中出现的),它依然会影响到你的整个项目构建。
也正是因为这个原因,当你的项目越大时,构建的时间就会越长,你的项目启动速度也就会越慢。

vite

同样的`Can't resolve './a.js' in xx` 错误,在我们没有进入到B页面的时候,它是不会出现的,只有当我们进入了B页面,才会突然出现这样的一个错误;

而之所以会这样的原因就是因为: vite 不会在一开始就构建你的整个项目,而是会将应用中的模块区分为依赖和源码(项目代码)两部分,对于源码部分,它会根据路由来拆分代码模块,只会去构建一开始就必须要构建的内容。
同时 vite以原生 ESM 的方式为浏览器提供源码,让浏览器接管了打包的部分工作。
因为这样的一个机制,无论你的项目有多大,它只会构建一开始必须要构建的内容,这就让 vite在构建时的速度大大提升了。
这也是vite为什么会快的一个核心原因。

2.2、vite这么快会有什么问题吗?

如果大家对ESM的构建机制有了解的话,那么应该可以发现一个问题。
那就是**vite既然以原生ESM的方式为浏览器提供源码,让浏览器接管了打包的部分工作**,那么假如我们的项目中存在 cormmonJS的内容怎么办?是不是就意味着无法解析呢?
是的!
vite 的早期版本中,确实存在这个问题,这个问题导致的最核心的麻烦就是很多的依赖无法使用。
比如axios 因为 axios 中使用了很多的 commonJS规范,这就让 vite 无法解析对应的内容(对应的 ieeue),从而会抛出一个错误,关于这个问题曾经也在viteissues中进行过激烈的讨论。

2.3、上面这个问题,官方是如何解决的呢?

因为这个问题非常的严重,所以针对于这个问题, vite在后期提供了依赖预构建的功能,其中一个非常重要的目的就是为了解决
CommonJSUMD兼容性问题。目前 vite 会先将CommonJSUMD发布的依赖项转换为ESM之后,再重新进行编译。这也可以理解为速度对业务的一个妥协。

3、初始化项目

  • 1、全局安装vite 版本2.8.5

    $ npm install -g vite@2.8.5
    
  • 2、使用vite创建项目

    $ npm init vite@latest
    # npx: installed 6 in 2.285s
    # √ Project name: ... front
    # √ Select a framework: » vue
    # √ Select a variant: » vue
    
  • 3、运行项目

    $ npm run dev
    
    

image-20220816094012941

可以看到,项目已经启动,但是没有 network地址;我们需要手动配置下

package.json

 "scripts": {
   
    "dev": "vite --host", // dev后面 加上 --host
    "build": "vite build",
    "preview": "vite preview"
  },

4、tailwindcss工具

在正式的项目开发之前,我们还需要了解另外一个工具 tailwindcss .
大家只看它的名字可能会想,这不就是一个处理css的库吗?值得我们专门拿出来一章的内容去学习?
那么我的回答可能是:“是的,这是有价值的。
tailwindcss是一个非常富有争议的库,喜欢它的人和讨厌它的人都非常多。
但是我们去查看taliwindcss下载量可以发现,它的月下载量已经达到了惊人的977万!要知道 vite也只有200多万而已。

4.1、传统的企业级开发css痛点

在前端技术巨变的现在,一直流传着一句话:每隔六个月,你要学习的前端技术就增加了一倍。
或许这句话本身只是个戏言,但是也在一定程度中反映了前端技术是变化非常快的。就像我们在上一章中提到的 vite ,在不到两年的时间里经历了三个大版本的变化。
但是大家仔细的想一下,这样的一个变化好像只适用于js 端, html、css 好像已经有很多年没有发生过大的变化

难道是因为html、css 已经足够成熟,不需要再进行改变了吗?应该也不是的,比如针对于css而言,我们在进行企业开发时,就会遇到很多问题,比如:

  • 1.有时我们需要统一设计方案,比如项目中的红色我们需要使用同样的色值,标题的文字大小我们期望在整个项目中进行统一的划分。这样的一套变量如果通过 css 来实现,那么就不得不维护一个庞大的变量组,这其实是一个非常大的心智负担。

  • 2.html结构是一个非常复杂的结构化内容,为了给这些结构指定对应的样式,那么通常我们都是通过cLssName
    来去指定。这就必
    须要求我们为这套复杂的结构指定各种各样包含语义化的 className。比如: containercontainer-box
    container-box-titlecontainer-box-5ub-title , container-box-sub-title-left-imag 大量的"无意义“命名本身就会增加很多额外的负担。

  • 3.因为 html和 css 是分离的,所以我们通常情况下在开发时,不得不在整个代码文件中,来回的上下翻滚,或者进行分屏操作。无
    论是哪一种其实都不能给我们带来一个很好地开发体验。
    4.针对于一些”复杂”的功能,比如响应式(媒体查询)、主题定制。如果我们想要通过传统的 html + css 的形式来进行实现,无
    疑是非常复杂的。

    除了上面提到的这些之外,还有很多其他的问题,感兴趣的同学可以看一下这篇文章的介绍CSS Utility Classes and “Separation of Concerns”
    总而言之,传统的 html + css 的模式存在着很多的问题,那么有什么好的方案可以解决呢?

    tailwindcss就是一个很好地方向。

4.2、安装tailwindcss

1、安装依赖

$ npm install -D tailwindcss@3.0.23 postcss@8.4.8 autoprefixer@10.4.2

2、创建配置文件

$ npx tailwindcss init -p
# 执行当前命令生配置文件
/** @type {import('tailwindcss').Config} */
module.exports = {
   
  content: [
  	"./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ], // 表示tailwindcss的作用范围 [src下所有目录下的所有vue/js文件, 当前index.html文件]
  theme: {
   
    extend: {
   },
  },
  plugins: [],
}

3、导入tailwindcss的基础指令组件

创建src/styles/index,scss文件

// 导入`tailwindcss`的基础指令组件
@tailwind base;
@tailwind components;
@tailwind utilities;

4、在项目的入口文件、main.js中引入src/styles/index,scss

import {
    createApp } from 'vue'
import './style.css'
+ import './styles/index.scss'

保存之后,运行后,会报没有sass依赖包的错误,所以我们需要手动安装一下

image-20220816105339932

$ yarn add sass

重启即可

如果postcss报错的话,可以将package.json中 “type”: "module"删除掉

5、安装vscode插件

工欲善其事必先利其器,想要有一个比较爽快的开发体验,那么一些好的开发辅助插件是必不可少的。
我们今天就以VSCode为例,来介绍一些咱们这次项目中需要使用到的一些辅助插件来帮助大家进行项目的开发。

5.1、Prettier 和 Code formatter 格式代码

相信对于很多同学而言代码格式问题,是一个一直让大家头疼的问题,混乱的代码格式非常不利于我们的日常开发工作,如果你的项目被ESLint管理,那么还会得到很多的错误,导致项目无法运行。那么我们有没有什么办法来让我们的代码格式变得更加漂亮呢?

答案是有的,它就是 prettier

prettier是一个让代码变得更加漂亮的工具,我们可以利用它来处理我们代码的格式化问题。
想要使用prettier,那么我们可以按照以下步骤进行:

  • 1、在vscode中插件库中安装 prettier

image-20220816145837118

  • 2、在项目的根目录下创建.prettierrc文件

    {
         
    	"semi": false,
    	"singleQuote": true,
        "trailingComma": "none"
    }
    
  • 3、在.vue.js结尾的文件中,点击右键,选择“使用…格式化文档”,选择“配置默认格式化程序”,选择“Prettier”

image-20220816150512904

image-20220816150527541

image-20220816150548579

  • 4、在vsode的设置页面,搜索“save”,找到“Format On Save” 勾选上;等到保存时会自动格式化代码

    image-20220816150824725

5.2、配置tailwindcss插件

这个插件可以帮助我们在写代码时,进行tailwindcsscss类名提示

image-20220816151044955

5.3、安装Volar插件

这个插件代替了Vuter功能,比Vuter更加贴合Vue3

image-20220816151507317

6、项目结构分析

咱们的项目分为移动端PC端两种显示结果,但是这两种显示结果通过同一套代码进行实现,也就是所谓的响应式构建方案。那么我们在分析的时候就需要分别分析(PS:此处我们只分析大的路由方案,目的是让大家对基本的项目结构有一个初步的认识,以方便我们的项目结构处理,后续具体的细节构建方案不在这次分析行为之内):

  • 1.移动端结构

  • 2.PC端结构

然后把这两种的分析方案,合并到一起,组成一个最终的架构方案。

6.1、移动端结构分析

移动端的结构相对比较简单,当我们去进行路由跳转时,它是以整个页面进行的整体路由切换。
那么由此可知,移动端不存在嵌套路由的概念,只需要在 APP.vue 中保留一个路由出口即可。

image-20220816154619643

6.2、PC端接否分析

pc端相对于移动端、多了一个固定头部的部分,所以处理起来更加复杂一点

image-20220816154910365

我们需要通过两个路由出口进行表示:

  1. App.vue :一级路由出口,用作整页路由切换

  2. Main.vue :二级路由出口,用作局部路由切换

那么由此我们可知,移动端和PC端两者的路由结构是不同的,所以这就要求我们需要根据当前用户所在设备的不同,构建不同的路由表

7、项目结构

项目的整体结构如下图所示

image-20220816160615099

首先,我们项目中使用了vuexvue-router;那么接下来我们先来安装他们吧

$ yarn add vuex@4.0.2 vue-router@4.0.14

8、企业级vite配置方案-让vite得心应手

8.1、前言

在前面的章节中我们通过 vite构建了项目,但是初始的vite配置还比较粗糙,不足以支撑企业级的项目开发。
所以说在本章中,我们就需要来配置vite 。
但是配置vite 不能想当然的进行处理,而是需要依据业务来进行配置。
所以在本章中,我们会:

  • 1.先明确项目的业务处理方赛

  • 2.依据业务需要,来配置对应的vite内容

那么明确好了本章的内容之后,就让我们一起进入业务与vite结合的世界中去吧!

8.2、明确移动瑞和PC端的构建顺序

在上一章中(项目架构基本结构处理分析)中,我们明确了项目包含移动端路由表和PC端路由表两部分,所以我们在开发的时候就需要分别来去处理移动端和pc端对应的内容。

由于tailwindcss是遵循移动端优先的,所以我们在构建项目时,遵循它的规则,移动端优先

8.3、首先我们封装isMoboleTerminal判断是否是移动端方法

我们规定、屏幕宽度大于或等于1280像素的为pc端,小于1280像素的为移动端

import {
    computed } from 'vue'
import {
    PC_DEVICE_WIDTH } from '../constants'

/**
 * 是否是移动端设备; 判断依据: 屏幕宽度小于 PC_DEVICE_WIDTH
 * @returns
 */
export const isMoboleTerminal = computed(() => {
   
  console.log(document.documentElement.clientWidth, PC_DEVICE_WIDTH)
  return document.documentElement.clientWidth < PC_DEVICE_WIDTH
})

上面封装的方法有缺陷,就是:当页面尺寸发生变化时,isMoboleTerminal的值并不会发生响应式改变;这是因为computed重新执行的条件是,内部的响应式数据发生变化computed才会执行;而此时内部没有响应式数据,所以并不会重新执行;所以我们可以监听屏幕的尺寸变化,并设置响应式宽度

这里我们不使用上面的方法,而是使用第三方插件:VueUse 这个插件就像react hook一样,提供响应式数据

  • 1、首先安装vueuse

    $ npm i @vueuse/core
    
  • 2、重构isMoboleTerminal

    import {
          computed } from 'vue'
    import {
          PC_DEVICE_WIDTH } from '../constants'
    import {
          useWindowSize } from '@vueuse/core'
    const {
          width } = useWindowSize()
    /**
     * 是否是移动端设备; 判断依据: 屏幕宽度小于 PC_DEVICE_WIDTH
     * @returns
     */
    export const isMoboleTerminal = computed(() => {
         
      return width.value < PC_DEVICE_WIDTH
    })
    
8.4、配置路由、判断当前是移动端还是pc端加载对应的路由
import {
    createRouter, createWebHistory } from 'vue-router'
import {
    isMoboleTerminal } from '../utils/flexible'
import mobileRoutes from './modules/mobile-routes'
import pcRoutes from './modules/pc-routes'

const router = createRouter({
   
  history: createWebHistory(),
  routes: isMoboleTerminal.value ? mobileRoutes : pcRoutes
})

export default router

9、vite中的一些配置

9.1、使用@符号代理src路径

vite官方给出来了,解决方案:resolve.alias

vite.config.js

export default defineConfig({
   
  resolve: {
   
    alias: {
   
      '@': path.resolve(__dirname, './src'),
      '@@': path.resolve(__dirname, './src/components')
    }
  }
})
9.2、配置开发环境下跨域代理

vite官方给出来了,解决方案:server.proxy

vite.config.js

export default defineConfig({
   
  server: {
   
      proxy: {
   
        '/prod-api': {
   
          target: ' http://localhost:3000',
          changeOrigin: true
        }
      }
    }
})

10、动态设置rem并修修改tailmindcss默认配置

因为我们做的页面需要在不同设备下使用、要想在不同设备下适用;这里移动端我们采用的是flex+rem布局的方式:

首先我们先实现下rem布局

/**
 * 首次加载成功时设置html跟标签的fontSize属性值;最大基准值为40px
 */
export const useREM = () => {
   
  const MAX_FONT_SIZE = 40
  // 当文档被解析成功时调用
  window.addEventListener('DOMContentLoaded', () => {
   
    const html = document.querySelector('html')
    // 设置屏幕基准值的标准为 屏幕的宽度 / 10
    const fontSize = window.innerWidth / 10
    html.style.fontSize = Math.min(fontSize, MAX_FONT_SIZE) + 'px'
  })
}

在mian.js中引入并调用useREM

import {
    useREM } from '@/utils/flexible'

useREM()

测试发现:字体非常大,不符合我们的预期;如下图所示

image-20220820094254567

解决办法: tailwindcss提供了配置文件,我们可以在配置文件中自定义一些样式

我们在tailwind.config.js中进行theme.extend配置

module.exports = {
   
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
   
    extend: {
   
      fontSize: {
   
        xs: ['0.25rem', {
    lineHeight: '0.35rem' }],
        sm: ['0.35rem', {
    lineHeight: '0.45rem' }],
        base: ['0.45rem', {
    lineHeight: '0.55rem' }],
        lg: ['0.55rem', {
    lineHeight: '0.65rem' }],
        xl: ['0.65rem', {
    lineHeight: '0.75rem' }]
      },
      boxShadow: {
   
        'l-white': '-10px 0 10px white' // 自定义类名样式 使用时 shadow-l-white
      }
    }
  },
  plugins: []
}

image-20220820095829409

配置完成生效

11、在vite中封装通用的svg

我们之前在webpack中封装了通用的svg图标、但是在vite中没有进行分装;所以在本项目中我们对svg图标进行通用封装

image-20220820110904744

我们先看一下文件目录

  • 1、封装svg-icon通用组件libs/svg-icon/index.vue

    <template>
      <svg aria-hidden="true">
        <use :xlink:href="symbolId" :fill="color" :class="fillClass" />
      </svg>
    </template>
    
    <script setup>
    import { computed } from 'vue'
    
    const props = defineProps({
      // 图标名称
      name: {
        type: String,
        required: true
      },
      // 颜色
      color: {
        type: String
      },
      // 类名
      fillClass: {
        type: String
      }
    })
    
    // 生成图标唯一id #icon-xxx
    const symbolId = computed(() => `#icon-${props.name}`)
    </script>
    
  • 2、导出注册组件对象 libs/index.js

    import SvgIcon from './svg-icon/index.vue'
    
    // 导出对象、这个对象有install方法,这样既可以通过app.use(options)来使用
    export default {
         
      install(app) {
         
        app.component('svg-icon', SvgIcon)
      }
    }
    
  • 3、在mian.js中注册组件对象

    import libs from '@/libs'
    createApp(App).use(router).use(libs).mount('#app')
    
  • 4、安装vite-plugin-svg-icons插件,并配置vite

    $ yarn add vite-plugin-svg-icons -D
    

    vite.config.js

    import {
          defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import path from 'path'
    import {
          createSvgIconsPlugin } from 'vite-plugin-svg-icons'
    
    // https://vitejs.dev/config/
    export default defineConfig({
         
      plugins: [
        vue(),
        // svg配置
        createSvgIconsPlugin({
         
          // 指定需要缓存的图标文件夹
          iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
          // 指定symbolId格式
          symbolId: 'icon-[name]'
        })
      ],
    })
    
  • 5、在main.js中注册 import 'virtual:svg-icons-register'

    
    import libs from '@/libs'
    import 'virtual:svg-icons-register' // 为固定格式
    
    createApp(App).use(router).use(libs).mount('#app')
    
  • 6、在组件中使用svg

    <svg-icon
        name="hamburger"
        class="px-1 w-4 h-4 fixed top-0 right-[-2px] z-20 shadow-l-white bg-white"
      />
    

image-20220820111656263

12、实现移动端navigation头部效果

需要实现的效果如下:

20220820_144232

实现思路:

  • 1、滑块绝对定位动态改变滑块的 leftwidth值, 来改变滑块的位置
  • 2、left值计算公式: 滚动x距离 + 点击元素距离屏幕左边的距离
  • 3、width值计算公式: 点击元素的宽度

实现细节:

  • 对于获取v-for生成的子元素的实例,需要使用回调函数获取

    <ul ref="ulEle">
        <li v-for="item in data" :ref="getEleFn"></li>
    </ul>
    
    <script setup>
        import { ref } from 'vue'
        // 获取普通元素的实例,可以使用ref(null)获取
        const ulEle = ref(null)
        //对于获取`v-for`生成的子元素的实例,需要使用回调函数获取
    	const getEleFn = (el) => {
            console.log(el)
        }
    </script>
    
  • 在初始化时,我们需要在li元素渲染完成之后触发一下重新设置一下滑块绝对定位动态改变滑块的 leftwidth值;我们可以监听渲染list的响应式数据是否改变,并且在改变后通过nextTick触发设置选中第一个元素

    // 监听data初次数据渲染之后,将slider条设置到第一项
    watch(
      () => props.data,
      () => {
         
        nextTick(() => {
         
          curretIndex.value = 0
        })
      }
    )
    

完整实例

<template>
  <ul
    class="relative z-10 text-xs bg-white flex overflow-auto p-1 text-zinc-600"
    ref="ulEle"
  >
    <li
      class="absolute top-1 h-[22.5px] bg-zinc-900 rounded-lg duration-200 z-10"
      :style="sliderStyle"
    ></li>
    <li
      v-for="(category, index) in data"
      :key="category.id"
      class="shrink-0 px-1.5 py-0.5 last:mr-6 z-10"
      :class="{ 'text-zinc-50': index === curretIndex }"
      @click="handleSelectCategory(index)"
      :ref="storeLiEle"
    >
      {
  { category.name }}
    </li>
  </ul>
  <svg-icon
    name="hamburger"
    class="px-1 w-4 h-4 fixed top-0 right-[-2px] z-20 shadow-l-white bg-white"
  />
</template>

<script setup>
import { ref, watch, nextTick } from 'vue'
import { useScroll } from '@vueuse/core'
const props = defineProps({
  data: {
    type: Array,
    required: true
  }
})

// 默认选中索引
const curretIndex = ref(-1)
const sliderStyle = ref({
  left: '10px',
  width: '0px'
})
//  ul dom元素
const ulEle = ref(null)
// li dom元素容器
const liEles = ref(new Set())

// ulScrollLeft 向左滚动的距离
const { x: ulScrollLeft } = useScroll(ulEle)

// 选中索引
const handleSelectCategory = (index) => {
  curretIndex.value = index
}
// 获取v-for遍历的子元素dom节点时,需要使用回调函数获取; 注意: 每次页面更新之后storeLiEle,都会重新执行一遍,这样会导致liEles中存储的都是重复的元素
// 所以可以使用Set来存储数据,避免存入重复的数据, 也可以在obBeforeUpdate前设置liEles.value的值为初始化值
const storeLiEle = (el) => {
  liEles.value.add(el)
}

watch(curretIndex, (newIndex, oldIndex) => {
  // 获取点击元素的距离左边屏幕的距离和元素的宽度
  const liEle = Array.from(liEles.value)[newIndex]
  if (!liEle) return false
  const { left, width } = liEle.getBoundingClientRect()
  sliderStyle.value = {
    left: `${left + ulScrollLeft.value}px`,
    width: `${width}px`
  }
})

// 监听data初次数据渲染之后,将slider条设置到第一项
watch(
  () => props.data,
  () => {
    nextTick(() => {
      curretIndex.value = 0
    })
  }
)
</script>

12.1、现在增加一个新功能:点击之后将点击项展示在屏幕的正中央,并且加上过渡**

实现思路

  • 1、在list菜单列表的数据发生改变后,获取每一项如果想要展示在中间需要滚动的距离

    菜单展示中间需要向左滚动的距离l = 每一项距离屏幕左边的距离 - 1/2屏幕的宽度 + 1/2自身的宽度 
    
  • 2、在点击时获取【被点击项向左滚动的距离l】,使得ul平滑滚动到指定位置(本案例使用自定义封装的平滑滚动函数)

    export const scrollTransition = () => {
         
      let timer = null
      return function exec ({
         el = document.body, position = 0, direction = 'v',  time = 150} = options) {
         
        clearInterval(timer)
        // 每步的时间 ms
        const TIME_EVERY_STEP = 5 
        // 最大滚动距离
        const maxScrollSize = el.scrollWidth - el.offsetWidth
        // 限定position的有效滚动范围
        position = Math.max(Math.min(position, maxScrollSize), 0)
        // 可以分为多少步
        let steps = Math.ceil(time / TIME_EVERY_STEP)
        const stepSize = (position - el.scrollLeft) / steps // 每步的长度
        
        timer = setInterval(() => {
         
          // console.log(el.scrollLeft , position)
          if (el.scrollLeft !== Number.parseInt(position) && position >= 0) {
         
            if (stepSize >= 0) {
         
              let scrollX = el.scrollLeft + stepSize >= position ? position :  el.scrollLeft + stepSize
              el.scrollLeft = scrollX
            } else {
         
              let scrollX = el.scrollLeft + stepSize <= position ? position :  el.scrollLeft + stepSize
              el.scrollLeft = scrollX
            }
            
          } else {
         
            clearInterval(timer)
          }
        }, TIME_EVERY_STEP)
      }
    }
    
    
  • 3、我们来处理下滑块的位置,因为滑块的位置是根据被选中项的getBoundingClientRect的属性值决定的;所以我们只要保证,在滑块获取getBoundingClientRect属性是在页面渲染之后即可;所以我们可以使用nextTick保证在页面dom元素发生变化后改变滑块的值

    watch(curretIndex, (newIndex, oldIndex) => {
         
      // 保证渲染之后再进行计算元素的位置, 在这里加上nextTick
      nextTick(() => {
         
        // 获取点击元素的距离左边屏幕的距离和元素的宽度
        const liEle = Array.from(liEles.value)[newIndex]
        if (!liEle) return false
        const {
          left, width } = liEle.getBoundingClientRect()
        sliderStyle.value = {
         
          left: `${
           left + ulScrollLeft.value}px`,
          width: `${
           width}px`
        }
      })
    })
    

实现代码

<template>
  <ul
    class="relative z-10 text-sm bg-white flex overflow-auto p-1 text-zinc-600"
    ref="ulEle"
  >
    <li
      class="absolute top-1 h-[22.5px] bg-zinc-900 rounded-lg duration-200 z-10"
      :style="sliderStyle"
    ></li>
    <li
      v-for="(category, index) in data"
      :key="category.id"
      class="shrink-0 px-1.5 py-0.5 last:mr-6 z-10"
      :class="{ 'text-zinc-50': index === curretIndex }"
      @click="handleSelectCategory(index)"
      :ref="storeLiEle"
    >
      {
   {
    category.name }}
    </li>
  </ul>
  <svg-icon
    name="hamburger"
    class="px-1 w-4 h-4 fixed top-0 right-[-2px] z-20 shadow-l-white bg-white"
    @click="visible = true"
  />
  <popup v-model="visible" class="aaa" style="color: red">
    <Menu :categorys="data" @handleSelectCategory="handleSelectCategory" />
  </popup>
</template>

<script setup>
import {
    ref, watch, nextTick } from 'vue'
import {
    useScroll } from '@vueuse/core'
import Menu from '@/views/main/components/menu/index.vue'
import {
    scrollTransition } from '@/utils'
const run = scrollTransition()
const props = defineProps({
   
  data: {
   
    type: Array,
    required: true
  }
})

// 默认选中索引
const curretIndex = ref(-1)
const sliderStyle = ref({
   
  left: '10px',
  width: '0px',
  bottom: 0,
})
//  ul dom元素
const ulEle = ref(null)
// li dom元素容器
const liEles = ref(new Set())
// 每一项在屏幕中央时,需要向左滚动的距离
const scrollRaces = ref([])

// ulScrollLeft 向左滚动的距离
const {
    x: ulScrollLeft } = useScroll(ulEle)

const visible = ref(false)

// 选中索引
const handleSelectCategory = (index) => {
   
  curretIndex.value = index
  visible.value = false
  // ulEle.value.scrollTo(scrollRaces.value[index], 0)
  run({
    el: ulEle.value, position: scrollRaces.value[index], direction: 'l', time: 200 })
}
// 获取v-for遍历的子元素dom节点时,需要使用回调函数获取; 注意: 每次页面更新之后storeLiEle,都会重新执行一遍,这样会导致liEles中存储的都是重复的元素
// 所以可以使用Set来存储数据,避免存入重复的数据, 也可以在obBeforeUpdate前设置liEles.value的值为初始化值
const storeLiEle = (el) => {
   
  liEles.value.add(el)
}

watch(curretIndex, (newIndex, oldIndex) => {
   
  // 保证渲染之后再进行计算元素的位置
  nextTick(() => {
   
    // 获取点击元素的距离左边屏幕的距离和元素的宽度
    const liEle = Array.from(liEles.value)[newIndex]
    if (!liEle) return false
    const {
    left, width, height } = liEle.getBoundingClientRect()
    sliderStyle.value = {
   
      left: `${
     left + ulScrollLeft.value}px`,
      width: `${
     width}px`,
      height: `${
     height}px`
    }
  })
}, {
   
  immediate: true
})

// 监听data初次数据渲染之后,将slider条设置到第一项
watch(
  () => props.data,
  () => {
   
    nextTick(() => {
   
      if (props.data.length <= 0) return
      curretIndex.value = 0
      // 获取1/2屏幕的宽度
      const halfScreenWidth = window.innerWidth / 2
      // 每一项向左滚动的距离 = 每一项距离屏幕左边的距离 - 1/2屏幕的宽度 + 1/2自身的宽度 
      scrollRaces.value = Array.from(liEles.value).map(el => el.getBoundingClientRect().left - halfScreenWidth + el.offsetWidth / 2)
    })
  }, {
   
    immediate: true
  }
)
</script>

<style scoped>
/* ul {
  scroll-behavior: smooth;
} */
</style>

20220822_104005

13、封装通用组件 - popup

当我们点击面包屑按钮时,会有一个弹出窗口 popup自低而上弹出,那么这样的一个功能,我们一样可以把它处理为项目的通用组件
那么想要处理popup的话,首先就需要先搞清楚 popup的能力。

  • 1.当 popup展开时,内容视图应该不属于任何一个组件内部,而应该直接被插入到 body下面

  • 2、popup应该包含两部分内容,一部分为背景蒙版,一部分为内容的包裹容器

  • 3、popup应该通过一个双向绑定进行控制展示和隐藏

  • 4、popup展示时,滚动应该被锁定

  • 5、内容区域应该接收所有的attrs,并且应该通过插槽让调用方指定其内容

那么明确好了这些能力之后,接下来大家可以先根据这些能力进行下通用组件 popup 的构建尝试,尝试之后再继续来看咱们的后续内容。

libs/popup/index.vue

<template>
  <Teleport to="body">
    <Transition name="popup-mask" mode="out-in">
      <!-- 遮罩层 -->
      <div
        class="fixed left-0 top-0 right-0 bottom-0 bg-black/80 z-30"
        @click="onMask"
        v-if="modelValue"
      ></div>
    </Transition>

    <Transition name="popup-slide" mode="out-in">
      <!-- 内容区域 -->
      <div
        class="bg-white overflow-y-auto z-30 fixed left-0 bottom-0 right-0"
        :style="style"
        v-bind="$attrs"
        v-if="modelValue"
      >
        <slot />
      </div>
    </Transition>
  </Teleport>
</template>

<script setup>
import { watch } from 'vue'
const props = defineProps({
  modelValue: Boolean,
  style: String | Object
})
const emits = defineEmits(['update:modelValue'])

const onMask = () => {
  emits('update:modelValue', false)
}

watch(
  () => props.modelValue,
  (v) => {
    const body = document.querySelector('body')
    let initStyle = ''
    if (v) {
      initStyle = body.style.overflow
      body.style.overflow = 'hidden'
    } else {
      body.style.overflow = initStyle
    }
  }
)
</script>

<style scoped lang="scss">
.popup-mask-enter-from,
.popup-mask-leave-to {
  opacity: 0;
}
.popup-mask-enter-active,
.popup-mask-leave-active {
  transition: all 0.3s;
}

.popup-slide-enter-from,
.popup-slide-leave-to {
  transform: translateY(100%);
}
.popup-slide-enter-active,
.popup-slide-leave-active {
  transition: all 0.3s;
}
</style>

通用组件注册

import SvgIcon from './svg-icon/index.vue'
import Popup from './popup/index.vue'

// 导出对象、这个对象有install方法,这样既可以通过app.use(options)来使用
export default {
   
  install(app) {
   
    app.component('svg-icon', SvgIcon)
    app.component('Popup', Popup)
  }
}

在使用通用组件

  <Popup v-model="visible" class="aaa" style="color: red" />
 const visible = ref(false)

20220820_172315

14、Vite通用组件自动化注册

目前我们在项目中已经完成了两个通用组件,将来我们还会完成更多的通用组件开发。那么如果每次开发完成一个通用组件之后,都去手动进行注册,未免有些过于麻烦了,所以我们期望通过 vite 提供的功能,进行通用组件的自动化注册
那么,如果想要完成这个功能的话,就需要使用到两个关键的知识点:

  • 1、vite的Glob 导入功能:该功能可以帮助我们在文件系统中导入多个模块

    const modules = import.meta.glob('./dir/*.js')
    // 以上将会被转译为下面的样子:
    const modules = {
         
      './dir/foo.js': () => import('./dir/foo.js'),
      './dir/bar.js': () => import('./dir/bar.js')
    }
    
  • 2、vue的 defineAsyncComponent方法:该方法可以创建一个按需加载的异步组件
    基于以上两个方法,实现组件自动注册

我们先来看下现在的代码

import SvgIcon from './svg-icon/index.vue'
import Popup from './popup/index.vue'

// 导出对象、这个对象有install方法,这样既可以通过app.use(options)来使用
export default {
   
  install(app) {
   
    app.component('svg-icon', SvgIcon)
    app.component('Popup', Popup)
  }
}

改成动态导入的形式

import {
    defineAsyncComponent } from 'vue'

// 导出对象、这个对象有install方法,这样既可以通过app.use(options)来使用
export default {
   
  install(app) {
   
    // 1、获取当前文件下所有以index.vue结尾的文件
    const components = import.meta.glob('./*/index.vue')
    for (const [path, fn] of Object.entries(components)) {
   
      // 2、根据path生成组件名称, defineAsyncComponent生成动态组件
      const componentName = path.replace(/(\.\/)|(\/index\.vue)/g, '')
      const Com = defineAsyncComponent(fn)
      // 3、将组件注册到app上
      app.component(componentName, Com)
    }
  }
}

15、封装通用的组件 - button

需要实现的组件如下

image-20220823102101628

实现代码

<template>
  <button
    class="duration-300 inline-flex items-center justify-center active:scale-105"
    :class="[
      sizeClass,
      typeClass,
      plainClass,
      block ? 'block' : '',
      { 'opacity-50 active:scale-100': isDisbaled }
    ]"
    :disabled="isDisbaled"
    @mouseover="mouseIsOver = true"
    @mouseleave="mouseIsOver = false"
  >
    <svg-icon
      v-if="loading"
      name="loading"
      class="w-[1em] h-[1em] duration-300 animate-spin"
      :class="{ 'mr-0.5': !!$slots.default || icon }"
      :color="svgColorClass"
    />
    <svg-icon
      v-if="icon"
      :name="icon"
      class="w-[1em] h-[1em] duration-300"
      :class="{ 'mr-0.5': !!$slots.default && icon }"
      :color="svgColorClass"
    />
    <slot />
  </button>
</template>

<script>
const defineType = {
  primary:
    'bg-blue-400 hover:bg-blue-500 duration-300 text-white rounded-sm border border-blue-400',
  warning:
    'bg-amber-400 hover:bg-amber-500 duration-300 text-white rounded-sm border border-amber-400',
  danger:
    'bg-red-400 hover:bg-red-500 duration-300 text-white rounded-sm border border-red-400',
  success:
    'bg-emerald-400 hover:bg-emerald-500 duration-300 text-white rounded-sm border border-emerald-400',
  default:
    'bg-white hover:bg-zinc-200 duration-300 text-zinc-600 rounded-sm border border-white-400'
}

const defineSize = {
  small: 'py-0.5 px-0.5 text-xs',
  middle: 'py-[6px] px-1 text-sm',
  default: 'py-[8px] px-1.5 text-sm',
  large: 'py-1 px-2 text-sm'
}
</script>

<script setup>
import { computed, ref, useSlots } from 'vue'
// const slot = useSlots()
// console.log(slot.default)
const mouseIsOver = ref(false)
const props = defineProps({
  type: {
    type: String,
    default: 'primary', // 'primary', 'warning', 'danger', 'success', 'default'
    validator(key) {
      const isContant = Object.keys(defineType).includes(key)
      if (!isContant) {
        throw new Error(
          `type must be 【${Object.keys(defineType).join('、')}】`
        )
      }
      return true
    }
  },
  size: {
    type: String,
    default: 'middle', // large , default, middle, small
    validator(key) {
      const isContant = Object.keys(defineSize).includes(key)
      if (!isContant) {
        throw new Error(
          `size must be 【${Object.keys(defineSize).join('、')}】`
        )
      }
      return true
    }
  },
  icon: {
    type: String
  },
  loading: {
    type: Boolean,
    default: false
  },
  block: {
    type: Boolean,
    default: false
  },
  plain: {
    type: Boolean,
    default: false
  },
  icon: {
    type: String
  },
  disabled: {
    type: Boolean,
    default: false
  }
})

const typeClass = computed(() =>
  defineType[props.type] ? defineType[props.type] : defineType.primary
)

const sizeClass = computed(() =>
  defineSize[props.size] ? defineSize[props.size] : defineType.middle
)

const plainClass = computed(() =>
  props.plain
    ? `bg-transparent ${
        props.type === 'primary'
          ? 'text-blue-400 hover:text-white'
          : props.type === 'warning'
          ? 'text-amber-400 hover:text-white'
          : props.type === 'danger'
          ? 'text-red-400 hover:text-white'
          : props.type === 'success'
          ? 'text-emerald-400 hover:text-white'
          : props.type === 'default'
          ? 'text-zinc-700 hover:text-white'
          : ''
      }`
    : ''
)
const svgColorClass = computed(() =>
  props.plain && !mouseIsOver.value
    ? `${
        props.type === 'primary'
          ? 'rgb(96, 165, 250)'
          : props.type === 'default'
          ? 'rgb(63, 63, 70)'
          : props.type === 'danger'
          ? 'rgb(248, 113, 113)'
          : props.type === 'success'
          ? 'rgb(52, 211, 153)'
          : props.type === 'warning'
          ? 'rgb(251, 191, 36)'
          : '#ffffff'
      }`
    : '#ffffff'
)
const isDisbaled = computed(() => props.disabled || props.loading)
</script>

<style></style>

16、封装通用组件 - popover

通用组件popover应具备以下功能:

  • 1、指定两个插槽、分别插入触发内容和弹出内容
  • 2、触发弹出内容的方式分为多种,clickhoverfocusmanual
  • 3、可以设定弹出层相对于触发元素的位置 bottom,bottom-start, bottom-end, top, top-start, top-end
  • 4、将弹出层指定挂载到body元素上、并且当页面滚动和页面尺寸发生变化时、弹出层也应虽则触发元素的位置改变而改变
  • 5、弹出层展示和隐藏时要有过渡效果

实现思路

  • 1、对用户指定的属性值进行校验
  • 2、当页面挂载之后获取父元素的 宽度高度距离屏幕左边left距离屏幕顶边top
  • 3、当触发弹出元素显示后,立即获取显示元素的宽度高度, 结合触发元素的属性与显示的位置,计算出弹出元素应该显示到的位置 left, top
  • 4、当页面滚动/尺寸发生改变、重新计算生成新的显示到的位置 left, top
  • 5、根据触发方式对应的显示和隐藏弹出元素;(注意: 在hover触发下、鼠标触发元素触发弹出元素显示后、然后再移动到显示元素上时,我们需要处理一下,避免弹出层先隐藏再展示的bug; 处理方法可以使用setTimeout延时修改元素的隐藏、在定时器触发之前、如果触发元素的显示、则先清理定时器)

实现代码

<template>
  <div ref="popoverRoot" class="select-none inline-flex" @click.stop>
    <slot name="reference" />
  </div>
  <Teleport to="body">
    <transition name="popover-tip">
      <div
        v-if="tipVisible"
        ref="tipRoot"
        class="fixed shadow-lg p-1 rounded-sm border border-zinc-100 z-20 bg-white"
        :style="tipStyle"
        @click.stop
      >
        <slot />
      </div>
    </transition>
  </Teleport>
</template>

<script>
const PLACEMENTS = [
  'bottom',
  'bottom-start',
  'bottom-end',
  'top',
  'top-start',
  'top-end'
]
const TRIGGERS = ['click', 'focus', 'hover', 'manual']
</script>

<script setup>
import { ref, watch, computed, nextTick } from 'vue'
import useRootPosition from './useRootPosition'
import useTrigger from './useTrigger'
const props = defineProps({
  placement: {
    // 弹框显示位置
    type: String,

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/431403.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

android开发文档下载,你的技术真的到天花板了吗

Android 基础 1.Activity 1、 什么是 Activity&#xff1f; 2、 请描述一下 Activity 生命周期 …… 2.Service 3.Broadcast Receiver32 4.ContentProvider 5.ListView 6.Intent 7.Fragment 1.Fragment 跟 Activity 之间是如何传值的 2.描述一下 Fragment 的生命周期 3.Fragme…

Qt多弹窗实现包括QDialog、QWidget、QMainWindow

1.相关说明 独立Widget窗口、嵌入式Widget、嵌入式MainWindow窗口、独立MainWindow窗口等弹窗的实现 相关界面包含关系 2.相关界面 3.相关代码 mainwindow.cpp #include "mainwindow.h" #include "ui_mainwindow.h" #include "tformdoc.h" #incl…

容器云平台巡检实战:运维进阶技巧与策略

1 docker容器日常巡检 通过以下方式进行检查&#xff1a; 1.1 docker/podman ps查看容器状态 Docker/podman ps -a 查看容器状态STATUS&#xff1a; Exited(0)&#xff1a;表示容器正常退出 Exited(其他数字)&#xff1a;容器异常退出&#xff0c;需要通过log 查看原因 Up…

080|为什么阿里的价值观值得你关注?

在阿里巴巴20周年年会现场&#xff0c;万众瞩目之下&#xff0c;马云和张勇完成了阿里巴巴董事长职务的交接。 不过你也知道&#xff0c;这次接棒在一年前就已经公布了&#xff0c;在年会上只是一个仪式。在20周年年会过后&#xff0c;我找到了互联网圈的资深媒体人阳淼&#…

julia语言使用PyCall包调用Python代码及Python包

Julia语言虽然好&#xff0c;但是包管理方面和生态环境感觉还有一点小小的缺陷&#xff0c;但是Julia可以调用Python丰富的包&#xff0c;用起来很方便。 安装PyCall 在安装之前先确认下Julia和Python的版本&#xff0c;我使用的稳定版本的 Julia1.6.7&#xff0c;Python版本是…

电磁兼容(EMC):单、双面PCB板设计要点

目录 1 产品设计原则&#xff1a;性价比为第一要素 2 布局设计要点 3 布线设计要点 4 完整地平面不是最优方案 1 产品设计原则&#xff1a;性价比为第一要素 PCB在电磁兼容设计中通常是要求有完整的地和电源平面。但多层价格让对价格敏感的产品望而却步&#xff0c;只能采…

GPT本地化研究(JAVA版本)

1.我觉得gpt3 600多G个人是不可能部署得成功的,回想我自己个人不可能每一方面知识都知道,我只是知道最多的是我自己擅长的,百事通需要靠大公司才能解决,我们只是要关注这个gpt是哪个领域的, 我想做的是工业—>自动化gpt(貌似这个方向日本很专业了*_*) 它山之石可以攻玉 2.gp…

【设计模式 03】抽象工厂模式

一个具体的工厂&#xff0c;可以专门生产单一某一种东西&#xff0c;比如说只生产手机。但是一个品牌的手机有高端机、中端机之分&#xff0c;这些具体的属于某一档次的产品都需要单独建立一个工厂类&#xff0c;但是它们之间又彼此关联&#xff0c;因为都共同属于一个品牌。我…

视觉Transformers中的位置嵌入 - 研究与应用指南

视觉 Transformer 中位置嵌入背后的数学和代码简介。 自从 2017 年推出《Attention is All You Need》以来&#xff0c;Transformer 已成为自然语言处理 (NLP) 领域最先进的技术。 2021 年&#xff0c;An Image is Worth 16x16 Words 成功地将 Transformer 应用于计算机视觉任务…

【go语言开发】yaml文件配置和解析

本文主要介绍使用第三方库来对yaml文件配置和解析。首先安装yaml依赖库&#xff1b;然后yaml文件中配置各项值&#xff0c;并给出demo参考&#xff1b;最后解析yaml文件&#xff0c;由于yaml文件的配置在全局中可能需要&#xff0c;可定义全局变量Config&#xff0c;便于调用 文…

面试题HTML+CSS+网络+浏览器篇

文章目录 Css预处理sass less是什么&#xff1f;为什么使用他们怎么转换 less 为 css&#xff1f;重绘和回流是什么http 是什么&#xff1f;有什么特点HTTP 协议和 HTTPS 区别什么是 CSRF 攻击HTML5 新增的内容有哪些Css3 新增的特性flex VS grid清除浮动的方式有哪些&#xff…

SAR ADC学习笔记(3)

一、SAR ADC采样电路 1.采样网络的时域响应&#xff1a;采保信号 2.采样网络的KT/C噪声 3.采样抖动 采样开关的种类 1.单MOS管开关 2.传输门开关 3.栅极自举&#xff08;Bootstrap&#xff09;开关 结论&#xff1a;M4的衬底需要和B点短接&#xff0c;保证B点能够到达高压&…

完美解决Iframe嵌入帆软报表出现跨域cookie写不进去的问题

随着google chrome对第三方cookie的限制越来越狠,现在发现之前使用iframe嵌入的帆软报表已经不好使了。官方现在解决iframe嵌入帆软报表出现跨域导致cookie写不进去的方案是主推 统一主域名的方案(谷歌浏览器单点登录失败- FineReport帮助文档 - 全面的报表使用教程和学习资料…

大唐杯学习笔记:Day5

1.1 小区搜索 搜索流程 PLMN选择 自动模式&#xff1a;UE根据NAS的请求或自主地向NAS报告可用的PLMN 手动模式&#xff1a;通过手动选择一个可用的VPLMN获取正常服务 频点选择 5G NR中,3GPP主要指定了两个频率范围,一个是6GHZ以下,另一个是毫米波,分别称之为FR1和FR2。 N…

稀碎从零算法笔记Day5-LeetCode:轮转数组

题型&#xff1a;数组、数学、双指针 前言&#xff1a;LC说你得用三种方法做出来(悲) 链接&#xff1a;189. 轮转数组 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。 …

专业145+总分410+西工大西北工业大学827信号与系统考研经验电子信息与通信工程,海航,真题,大纲,参考书。

经过一年的努力&#xff0c;分数终于出来。今年专业课827信号与系统145&#xff08;很遗憾差了一点点满分&#xff0c;没有达到Jenny老师的最高要求&#xff09;&#xff0c;数一130&#xff0c;英语和政治也都比较平衡&#xff0c;总分410分&#xff0c;当然和信息通信考研Jen…

学习java第一天(下载并配置环境+写第一个java程序)

一.安装 1.下载 直接去官网上选择与你电脑符合的版本下载 官网链接Java Archive Downloads - Java SE 8u211 and later &#xff08;拿我的为例 Windows x64版本&#xff09; ​ 2.然后安装好exe&#xff08;要让自己知道在哪&#xff09; 3.配置环境 大佬链接&#xff1…

蓝桥杯前端Web赛道-新鲜的蔬菜

蓝桥杯前端Web赛道-新鲜的蔬菜 题目链接&#xff1a;1.新鲜的蔬菜 - 蓝桥云课 (lanqiao.cn) 题目要求如下&#xff1a; 其实很容易联想到使用flex布局&#xff0c;这是flex布局一种非常经典的骰子布局&#xff0c;推荐Flex 布局教程&#xff1a;实例篇 - 阮一峰的网络日志 (r…

Java基于SpringBoot网上超市的设计与实现论文

摘 要 网络技术和计算机技术发展至今&#xff0c;已经拥有了深厚的理论基础&#xff0c;并在现实中进行了充分运用&#xff0c;尤其是基于计算机运行的软件更是受到各界的关注。加上现在人们已经步入信息时代&#xff0c;所以对于信息的宣传和管理就很关键。因此超市商品销售信…

鸿蒙Harmony应用开发—ArkTS声明式开发(通用属性:点击回弹效果)

设置组件点击时回弹效果。 说明&#xff1a; 从API Version 10开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 clickEffect clickEffect(value: ClickEffect | null) 设置当前组件点击回弹效果。 系统能力&#xff1a; SystemCapabilit…