vue - 实战2 - 前端

vue 格式化工具 prettier

根目录下创建.prettierrc

{
    // "semi": false 表示代码语句结尾不强制要求添加分号
    "semi": false,
    // "singleQuote": true 表示字符串统一使用单引号(而非双引号)
    "singleQuote": true,
    // "trailingComma": "none" 表示对象、数组等最后一个元素后不添加 trailing 逗号
    "trailingComma": "none"
}

vue 项目 tailwindcss 安装

安装

命令

npm install -D tailwindcss@3 postcss autoprefixer

现在 版本4.1 和vue集成有问题,所有用3

2 初始化配置文件:

npx tailwindcss init -p

3 修改 tailwind.config.js 配置:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

4 建 Tailwind 样式文件

src/styles 目录下创建 tailwind.css 文件:

@tailwind base;
@tailwind components;
@tailwind utilities;

main.js (或 main.ts) 中导入刚才创建的 CSS 文件:

import { createApp } from 'vue'
import App from './App.vue'
import './assets/css/tailwind.css'  // 引入 Tailwind

createApp(App).mount('#app')

设置HTML的基准值REM和xl,sm,base等的大小

https://tailwindcss.com/docs/font-size 版本4

https://tailwind-v3.nodejs.cn/docs/font-size 版本3

手机端,根据浏览器的宽度,设置字体大小

1 根据 (浏览器可视窗口宽度/10),设置 html 的 fontsize,最大不能过40

export const useREM = () => {

  const MAX_FONT_SIZE = 40
  document.addEventListener('DOMContentLoaded', function () {
    // 拿到html的
    const html = document.querySelector('html')
    // 拿到窗口的宽度除以10
    let fontsize = windowWidth.value / 10
    fontsize = fontsize > MAX_FONT_SIZE ? MAX_FONT_SIZE : fontsize
    // 设置html的字体大小
    html.style.fontSize = fontsize + 'px'
  })

}

2 导入main.js

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import '@/styles/index.scss'
import { useREM } from '@/utils/flexble.js'
import router from '@/routers/index.js'

const app = createApp(App)
app.use(createPinia())
app.use(router)
useREM()

app.mount('#app')

3 tailwind.config.js

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {
      fontSize: {
        xs: ['0.375rem', '0.5rem'],
        sm: ['0.5rem', '0.625rem'],
        base: ['0.625rem', '0.75rem'],
        lg: ['0.75rem', '0.875rem'],
        xl: ['0.875rem', '1.125rem'],
      },
    },
  },
  plugins: [],
}

设置自定义的样式

例如:shadow-white-l

/** @type {import('tailwindcss').Config} */
export default {
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {
      fontSize: {
        xs: ['0.375rem', '0.5rem'],
        sm: ['0.5rem', '0.625rem'],
        base: ['0.625rem', '0.75rem'],
        lg: ['0.75rem', '0.875rem'],
        xl: ['0.875rem', '1.125rem'],
      },
    },
    boxShadow: {
      'white-l': '-10px 0 10px white',
    },
  },
  plugins: [],
}

vite 设置

resolve.alias 软连接 - 路径别名

  • 类型:Record<string, string> | Array<{ find: string | RegExp, replacement: string, customResolver?: ResolverFunction | ResolverObject }>

将会被传递到 @rollup/plugin-alias 作为 entries 的选项。也可以是一个对象,或一个 { find, replacement, customResolver } 的数组。

当使用文件系统路径的别名时,请始终使用绝对路径。相对路径的别名值会原封不动地被使用,因此无法被正常解析。

更高级的自定义解析方法可以通过 插件 实现。

https://cn.vitejs.dev/config/shared-options.html#resolve-alias

import { fileURLToPath, URL } from 'node:url'
// import { join } from 'node:path'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue(), vueDevTools()],
  resolve: {
    alias: {
      // '@': join(__dirname, 'src'),
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
})

跨域问题 server.proxy

https://cn.vitejs.dev/config/server-options.html#server-proxy

import { fileURLToPath, URL } from 'node:url'
// import { join } from 'node:path'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue(), vueDevTools()],
  resolve: {
    alias: {
      // '@': join(__dirname, 'src'),
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  //代理配置
  server: {
    proxy: {
      // 代理所有 /api 的请求
      '/api': {
        //代理请求后的地址
        target: 'https://xxxxxxxxxxx/',
        //跨域
        changeOrigin: true,
      },
    },
  },
})

区分开发和生产环境

在 vite 中提供了 .env 文件,该文件为环境变量文件,默认提供了四种文件格式:

.env # 所有情况下都会加载

.env.local # 所有情况下都会加载,但会被 git 忽略

.env.[mode] # 只在指定模式下加载

.env.[mode].local # 只在指定模式下加载,但会被 git 忽略

其中有一种 .env.[mode] 的格式可以在不同模式下加载不同内容,这不正是我们想要的吗?

默认只有 VITE_ 为前缀的变量才会被 vite 处理

//文件 .env.development

# 开发环境
VITE_BASE_API='/api'

//文件 .env.production

# 生产环境
VITE_BASE_API='/prod-api'

request.js

import axios from 'axios'

//console.log(import.meta.env.VITE_BASE_API)

// 创建axios实例
const service = axios.create({
  timeout: 10000, // 请求超时时间
  baseURL: import.meta.env.VITE_BASE_API
})

功能开发

滑块

下面开始

image

第一个item的宽度为70,和滑块的left距离为0

<template>
  <div class=" bg-white  position-static top-0 left-0 z-10 ">
    <ul class=" flex relative overflow-x-auto text-xs p-1 text-zinc-600">
      <!-- 滑块 -->
      <li ref="sliderTarget" :style="sliderStyle" class=" absolute h-[28px] bg-zinc-900 rounded-lg"></li>
      <!-- 右侧菜单 -->
      <li class=" fixed top-0 right-[-1px] h-5 px-1 flex items-center bg-white z-20 shadow-white-l"><svg-icon class="w-1.5 h-1.5" name="hamburger" size="24" /></li>
      <!-- Menu Items -->
      <li v-for="item in categories" :key="item.id" class=" shrink-0 p-1.5 py-0.5 z-10 duration-200 last:mr-4">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script setup>
import { ref } from 'vue'
defineProps({
  categories: {
    type: Array,
    default: () => [],
  },
});

const sliderStyle = ref({
  transform: 'translateX(0)', //滑块的left距离
  width: '70px', //item的宽度
})

// 1. 选中的 item 下标: currentCategoryIndex
// 2. 所有 item 元素: itemRefs
// 3. ul 的横向滚动偏离位置: ulScrollLeft
// 4. 最后在 currentCategoryIndex 发生改变时, 获取 item 下标元素的 left 和 width, 计算 sliderStyle 即可
</script>

<style lang="scss" scoped>

</style>

要实现的效果

1. 选中的 item 下标: currentCategoryIndex
2. 所有 item 元素: itemRefs
3. ul 的横向滚动偏离位置: ulScrollLeft
4. 最后在 currentCategoryIndex 发生改变时, 获取 item 下标元素的 left 和 width, 计算 sliderStyle 即可

实现代码

<template>
  <div class=" bg-white  position-static top-0 left-0 z-10 ">
    <ul ref="ulTarget" class=" flex relative overflow-x-auto text-xs p-1 text-zinc-600">
      <!-- 滑块 -->
      <li ref="sliderTarget" :style="sliderStyle" class=" absolute h-[28px] bg-zinc-900 rounded-lg  duration-200"></li>
      <!-- 右侧菜单 -->
      <li class=" fixed top-0 right-[-1px] h-5 px-1 flex items-center bg-white z-20 shadow-white-l"><svg-icon class="w-1.5 h-1.5" name="hamburger" size="24" /></li>
      <!-- Menu Items -->
      <li v-for="(item,index) in categories" :key="item.id"
      class=" shrink-0 p-1.5 py-0.5 z-10 duration-200 last:mr-4"
      :ref="el => setItemRef(el)"
      :class="{'text-zinc-100': index == currentCategoryIndex}"
      @click="currentCategoryIndex = index">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script setup>
import { ref, onBeforeUpdate, watch } from 'vue'
import { useScroll } from '@vueuse/core'
defineProps({
  categories: {
    type: Array,
    default: () => [],
  },
});

const sliderStyle = ref({
  transform: 'translateX(0)', //滑块的left距离
  width: '70px', //item的宽度
})

// 1. 选中的 item 下标: currentCategoryIndex
const currentCategoryIndex = ref(0)
// 2. 所有 item 元素: itemRefs
const itemRefs = ref([])
const setItemRef = (el) => {
  if (el) {
    itemRefs.value.push(el)
  }
}
onBeforeUpdate(() => {
  //因为 ref 是响应式数据, 数据变化,就会重新push,所以需要每次更新时,清空 itemRefs 数组
  itemRefs.value = []
})

// 3. ul 的横向滚动偏离位置: ulScrollLeft
const ulTarget = ref(null)
// 获取 ul 的横向滚动偏离位置
const { x: ulScrollLeft } = useScroll(ulTarget)

// 4. 最后在 currentCategoryIndex 发生改变时, 获取 item 下标元素的 left 和 width, 计算 sliderStyle 即可
watch(currentCategoryIndex, (val) => {
  // getBoundingClientRect() 是 JavaScript 中 DOM 元素的一个方法,用于获取元素在视口(viewport)中的位置和尺寸信息。
  const {width , left} = itemRefs.value[val].getBoundingClientRect()
  // 获取 ul 的横向滚动偏离位置,然后加上 item 元素的 left 位置,再减去 10px (UL的padding-left),就是滑块的 left 位置
  sliderStyle.value = {
    transform: `translateX(${ulScrollLeft.value + left - 10}px)`,
    width: `${width}px`,
  }
})


</script>

<style lang="scss" scoped>

</style>

插件开发

统一注册

main.js

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import '@/styles/index.scss'
import { useREM } from '@/utils/flexble.js'
import router from '@/routers/index.js'
import 'virtual:svg-icons-register' // 引入SVG注册脚本
import libs from '@/libs'

const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(libs)
useREM()

app.mount('#app')

处理自动注册 libs 下面的组件

import.meta.glob 和 defineAsyncComponent 自动注册组件

什么是 defineAsyncComponent?

defineAsyncComponent 是 Vue 3 中用来创建异步组件的函数。它允许你将组件定义为一个返回 Promise 的工厂函数,这个 Promise 会在需要渲染组件时才执行,从而实现按需加载组件。

主要用途

  1. 代码分割: 将大型应用拆分成小块,只在需要时加载特定组件
  2. 懒加载: 提高应用初始加载速度,减少首屏加载时间
  3. 处理动态导入: 正确处理通过 import()import.meta.glob() 导入的组件

基本使用

import { defineAsyncComponent } from 'vue'

// 简单形式
const AsyncComponent = defineAsyncComponent(() => import('./MyComponent.vue'))

// 高级形式(带选项)
const AsyncComponent = defineAsyncComponent({
  loader: () => import('./MyComponent.vue'),
  loadingComponent: LoadingComponent,
  errorComponent: ErrorComponent,
  delay: 200,
  timeout: 3000
})

import.meta.glob

const components = import.meta.glob('./**/index.vue', { eager: true })
console.log(components);

image

console.log(Object.entries(components));

image

最后完成的效果

// import SvgIcon from './svg-icons/index.vue'
// import Popup from './popup/index.vue'
import { defineAsyncComponent } from 'vue'

export default{
  //当你在 Vue 应用中使用 app.use(插件) 时,Vue 会自动调用这个 install 方法
  install(app) {
    // app.component('m-svg-icon', SvgIcon) //这行代码将 SvgIcon 组件注册为全局组件,组件的标签名为 svg-icon
    // app.component('m-popup', Popup) // 注册Popup组件为全局组件

    //1 获取当前路径下的 index.vue 文件
    const components = import.meta.glob('./**/index.vue')

    //2 遍历获取的组件模块
    for (const [path, component] of Object.entries(components)) {
      //3 在遍历里,注册组件为全局组件
      const name = path.replace('./', '').split('/')[0]
      app.component("m-" + name, defineAsyncComponent(component))
    }
  }
}

处理SVG图标

image

1 安装vite-plugin-svg-icons插件

yarn add vite-plugin-svg-icons -D
# or
npm i vite-plugin-svg-icons -D
# or
pnpm install vite-plugin-svg-icons -D

2 配置 vite

import { fileURLToPath, URL } from 'node:url'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
// import { join } from 'node:path'

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'

// https://vite.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    vueDevTools(),
    createSvgIconsPlugin({
      // 指定需要缓存的图标文件夹
      // iconDirs: [path.resolve(fileURLToPath(new URL('./src/assets/icons', import.meta.url)))],
      iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
      // 指定symbolId格式
      symbolId: 'icon-[dir]-[name]',
    }),
  ],
  resolve: {
    alias: {
      // '@': join(__dirname, 'src'),
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    },
  },
  //代理配置
  server: {
    proxy: {
      // 代理所有 /api 的请求
      '/api': {
        //代理请求后的地址
        target: 'https://XXXXXXXXXXXXXX/',
        //跨域
        changeOrigin: true,
      },
    },
  },
})

3 创建svg组件

src\libs\svg-icons\index.vue

<template>
  <svg
    class="svg-icon"
    aria-hidden="true"
    :width="size"
    :height="size"
  >
    <use :xlink:href="`#icon-${name}`" :fill="color" :class="fillClass"></use>
  </svg>
</template>

<script setup>
import { defineProps } from 'vue'

defineProps({
  // 图标名称(对应svg的id)
  name: {
    type: String,
    required: true
  },
  // 图标大小
  size: {
    type: [Number, String],
    default: 24
  },
  //svg 图标颜色
  color:{
    type: String,
  },
  //tailwind 填充类
  fillClass:{
    type: String,
  },
})
</script>

<style scoped>
</style>

4 导出插件

src\libs\index.js

import SvgIcon from './svg-icons/index.vue'

export default{
  //当你在 Vue 应用中使用 app.use(插件) 时,Vue 会自动调用这个 install 方法
  install(app) {
    app.component('svg-icon', SvgIcon) //这行代码将 SvgIcon 组件注册为全局组件,组件的标签名为 svg-icon
  }
}

https://cn.vuejs.org/guide/reusability/plugins

一个插件可以是一个拥有 install() 方法的对象,也可以直接是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给 app.use() 的额外选项作为参数:

const myPlugin = {
install(app, options) {
// 配置此应用
}
}

插件没有严格定义的使用范围,但是插件发挥作用的常见场景主要包括以下几种:

  1. 通过 app.component()app.directive() 注册一到多个全局组件或自定义指令。
  2. 通过 app.provide() 使一个资源可被注入进整个应用。
  3. app.config.globalProperties 中添加一些全局实例属性或方法
  4. 一个可能上述三种都包含了的功能库 (例如 vue-router)。

5 导入 mian.js

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import '@/styles/index.scss'
import { useREM } from '@/utils/flexble.js'
import router from '@/routers/index.js'
import 'virtual:svg-icons-register' // 引入SVG注册脚本
import libs from '@/libs'

const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(libs)
useREM()

app.mount('#app')

6 可以使用了

<svg-icon class="w-1.5 h-1.5" name="hamburger" size="24" />

image

  1. 当 popup 展开时,内容视图应该不属于任何一个组件内部,而应该直接被插入到 body 下面
  2. popup 应该包含两部分内容,一部分为背景蒙版,一部分为内容的包裹容器
  3. popup 应该通过一个双向绑定进行控制展示和隐藏
  4. popup 展示时,滚动应该被锁定
  5. 内容区域应该接收所有的 attrs,并且应该通过插槽让调用方指定其内容

需要的VUE3内置组件

1 Teleport 组件

https://cn.vuejs.org/guide/built-ins/teleport.html

<Teleport> 是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。

<button @click="open = true">Open Modal</button>

<Teleport to="body">
  <div v-if="open" class="modal">
    <p>Hello from the modal!</p>
    <button @click="open = false">Close</button>
  </div>
</Teleport>

2 <Transition> 组件

https://cn.vuejs.org/guide/built-ins/transition.html#transition

image

实现组件

src\libs\popup\index.vue

<template>
  <Teleport to="body">
    <Transition name="fade">
      <div v-if="modelValue" class="popup-overlay" @click.self="handleClose">
        <div :class="['popup-content']" v-bind="$attrs">
          <slot></slot>
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

<script>
import { onMounted, onUnmounted, watch } from 'vue'

export default {
  name: 'Popup',
  props: {
    modelValue: {
      type: Boolean,
      default: false
    }
  },
  emits: ['update:modelValue'],
  setup(props, { emit }) {
    // 处理关闭事件
    const handleClose = () => {
      emit('update:modelValue', false)
    }

    // 监听显示状态变化,控制滚动锁定
    watch(() => props.modelValue, (newValue) => {
      if (newValue) {
        document.body.style.overflow = 'hidden'
      } else {
        document.body.style.overflow = ''
      }
    })

    // 组件卸载时恢复滚动
    onUnmounted(() => {
      document.body.style.overflow = ''
    })

    return {
      handleClose
    }
  }
}
</script>

<style scoped>
.popup-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  z-index: 2000;
}

.popup-content {
  background-color: white;
  border-radius: 4px;
  width: 100%;
  max-height: 70vh;
  overflow-y: auto;
}

/* 过渡动画 */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
</style>

注册为

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

export default{
  //当你在 Vue 应用中使用 app.use(插件) 时,Vue 会自动调用这个 install 方法
  install(app) {
    app.component('svg-icon', SvgIcon) //这行代码将 SvgIcon 组件注册为全局组件,组件的标签名为 svg-icon
    app.component('m-popup', Popup) // 注册Popup组件为全局组件
  }
}

// 导出单个组件供按需引入
export { SvgIcon, Popup }

使用方式

<!-- 正确的Popup使用方式 -->
  <m-popup v-model="showCategoriesPopup">
    <div class="popup-body">
      <ul class="categories-list">
        <li v-for="(item, index) in categories" :key="item.id"
            class="category-item text-base" @click="selectCategory(index)">
          {{ item.name }}
        </li>
      </ul>
    </div>
  </m-popup>

<script>
// 控制Popup显示/隐藏的状态 - 必须是布尔值
const showCategoriesPopup = ref(false)
// 选择分类并关闭弹窗
const selectCategory = (index) => {
  currentCategoryIndex.value = index
  showCategoriesPopup.value = false
}
</script>

<style  lang="scss" scoped>
.popup-body {
  padding: 16px;
}

.categories-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.category-item {
  padding: 12px 0;
  border-bottom: 1px solid #eee;
  cursor: pointer;
  transition: color 0.2s;
}

.category-item:hover {
  color: #3b82f6;
}
</style>
posted @ 2025-10-02 20:52  【唐】三三  阅读(5)  评论(0)    收藏  举报