Vue3中前台前端解决方案 拿来就用

Webpack VS Vite:

Webpack 默认构建整个应用;稳妥
Vite 只构建必须构建的内容;以原生 ESM 方式提供源码,让浏览器构建;快

用 Vite 创建一个项目:

npm i -g vite@2.8.5
npm init vite@latest

在局域网内运行项目:

package.json

"scripts": {
  "dev": "vite --host",

在 Vite 项目中安装 tailwindcss:

  1. npm i -D tailwindcss@3.0.23 postcss@8.4.8 autoprefixer@10.4.
  2. npx tailwindcss init -p 创建 tailwind.config.js
  3. 添加 tailwindcss 应用范围:
   module.exports = {
    // Tailwind 应用范围
    content: ['./index.html', './src/**/*.{vue,js}'],
    theme: {
      extend: {}
    },
    plugins: []
   }
  1. 在 src/styles/index.scss 中导入 tailwind 基础指令组件,并在 src/main.js 引入:
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
  2. 安装 scss:
    npm i -D sass@1.45.0

开发插件:

Prettier

  1. 根目录创建 .prettierrc
    {
    "semi": false, // 分号
    "singleQuote": true, // 引号
    "trailingComma": "none" // 尾随逗号
    }
  2. .vue .js 文件中,右键 使用...格式化文档 -> 配置默认格式化程序 -> prettier
  3. 使用:右键 格式化文档;保存自动格式化(设置 Editor: Format On Save)

Tailwind CSS IntelliSense

项目结构分析

  • 路由
    移动端 无嵌套路由,App.vue 一个路由出口
    PC 端
    App.vue 一级路由出口 整页路由切接
    Main.vue 二级踏由出口 局部路由切换
  • 项目结构
|- src
|   |- App.Vue // 项目根组件,一级路由出口
|   |- api // 接口请求
|   |- assets // 静态资源
|   |   |- icons // svg icon 图标
|   |   |- images // image 图标,比如:xxx.png
|   |   |- logo.png // logo
|   |- components // 通用的业务组件: 一个组件在多个页面中用到
|   |- constants
|   |- directives // 自定义指令
|   |- libs // 通用组件(不包含业务),用于构建任意中台物料库或通用组件库
|   |- router
|   |   |- index.is // 路由处理中心
|   |   |- modules // 路由模块
|   |   |   |- mobile-routes.js //移动端路由
|   |   |   |- pc-routes.is // PC端路由
|   |- store // 全局状态
|   |   |- getters.js // 全局状态访问处理
|   |   |- index.js // 全局状态中心
|   |   |- modules // 状态子模块
|   |- styles // 全局样式
|   |   |- index.scss // 全局通用的样式处理
|   |- utils // 工具模块
|   |- vendor // 外部供应资源,比如:人类行为认证
|   |- views // 页面组件,对应路由表
|   |   |- layout // PC端布局(header main二级路由 float) 一级路由
|   |   |   |- components // 该页面组件下的业务组件
|   |   |   |- index.vue // layout 组件
|   |   |- main(home) // 首页
|   |   |   |- components
|   |   |   |- index.vue
|   |- main.js // 入口文件
|   |- permission.js // 页面权限控制中心
|- tailwind.config.js
|- vite.config.js

Vueuse (响应式)

npm i @vueuse/core@8.1.2
import { useWindowSlze } from '@vueuse/core

  • useWindowSize
  • useScroll
  • useScrollLock
  • useVModel
  • onClickOutside
  • useIntersectionObserver
  • watchDebounced

配置 Vite

  • 构建顺序
    尊重 tailwindcss 先构建移动端,后构建 PC 端
  • 屏幕宽度判断,宽度 < 1280
    src/utils/flexible.js
    const { width,height } = useWindowSize()
    export const isMobileTerminal = computed(() =>
      // return document.documentElement.clientwidth < PC_DEVICE_WIDTH
      return width.value < PC_DEVICE_WIDTH
    })
    
    // clientWidth = width + padding
    // offsetWidth = width + padding + scrollWidth + border
    
    移动设备判断
    export const isMobileTerminal = computed(() => {
      return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
        navigator.userAgent
      );
    });
    
  • 支持 @ 软链接,@ 对应 src
    vite.config.js
import { join } from 'path'
export default defineConfig({
  plugins: [vue()],
  resolve: {
    // 软链接
    alias: {'@': join(__dirname, '/src')}
  },

初始化移动端路由(详见源码)

/router 构建路由表
App.vue 指定一级路由出口
main.js 注册路由

配置 axios github DOC

npm i --save axios@0.26.1
src/utils/request.js

import axios from "axios";
const service = axios.create({
  baseURL: "https://api.imooc-front.lgdsunday.club/api/",
  timeout: 5000,
});
// 响应拦截器
// 服务端返回数据之后,前端 .then 之前被调用
service.interceptors.response.use((response) => {
  const { success, message, data } = response.data;
  if (success) {
    return data;
  } else {
    $message("warn", message);
    return Promise.reject(new Error(message));
  }
});

src/api/category.js

import request from "@/utils/request";
export const getCategory = () => {
  return request({
    url: "/category",
  });
};

Vite 解决跨域 server.proxy

vite.config.js

export default defineConfig({
  server: {
    proxy: {
      // 代理所有 /api 的请求,该求情将被代理到 target 中
      "/api": {
        target: "https://api.imooc-front.lgdsunday.club/",
        changeOrigin: true,
      },
    },
  },
});

src/utils/request.js
baseURL: '/api',

Vite 配置环境变量(详见源码)

.env.development
.env.production
baseURL: import.meta.env.VITE_BASE_API

tailwindcss 默认使用 rem 作为基准值(html 根标签 fontSize)(适配移动端)

动态设置 rem 基准值

src/utils/flexible.js

// 初始化 rem 基准值,最大为 40px
export const useREM = () => {
  const MAX_FONT_SIZE = 40;
  document.addEventListener("DOMContentLoaded", () => {
    const html = document.querySelector("html");
    // 基准值为 幕宽度 / 10
    let fontSize = window.innerWidth / 10;
    fontSize = fontSize > MAX_FONT_SIZE ? MAX_FONT_SIZE : fontSize;
    html.style.fontSize = fontSize + "px";
  });
};

main.js: useREM()

修改 tailwindcss 预设值

extend: {
  fontSize: {
    xs: ['0.25rem', '0.35rem'], // text-xs
    sm: ['0.35rem', '0.45rem'],
    base: ['0.42rem', '0.52rem'],
    lg: ['0.55rem', '0.65rem'],
    xl: ['0.65rem', '0.75rem']
  },
  height: {
    header: '72px', // h-header
    main: 'calc(100vh - 72px)'
  },
  boxShadow: {
    'l-white': '-10px 0 10px white', // shadow-l-white
    'l-zinc': '-10px 0 10px #18181b'
  },
  colors: {
    main: '#f44c58', // bg-main text-main
    'hover-main': '#f32836', // bg-hover-main
    'success-100': '#F2F9EC',
    'success-200': '#E4F2DB',
    'success-300': '#7EC050',
    'warn-100': '#FCF6ED',
    'warn-200': '#F8ECDA',
    'warn-300': '#DCA550',
    'error-100': '#ED7456',
    'error-200': '#f3471c',
    'error-300': '#ffffff'
  },
  backdropBlur: {
    '4xl': '240px'
  },
  variants: {
    scrollbar: ['dark']
  }
}

svg-icon 组件

Vite 处理 svg 矢量图

  • npm i --save-dev vite-plugin-svg-icons@2.0.1
  • vite.config.js
import path from 'path'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export default defineConfig({
  plugins: [
    vue(),
    createSvgIconsPlugin({
      // 指定需要缓存的图标文件夹
      iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
      // 指定symbolId格式
      // const symbolId = computed(() => `#icon-${props.name}`)
      symbolId: 'icon-[name]'
    })
  ],
  • main.js 注册 svg-icons
    import 'virtual:svg-icons-register'

Vite 自动注册组件 (src/libs/index.js)

开发移动端首页 Navigation 组件

  • 滑块(slider)
    • ref 指定多个 item
    • getBoundingClientRect()
    • :class="{'text-sm': Boolean}"
      :class="['text-sm', 'bg-white']"
    • useScroll
      const { x: ulScrollLeft } = useScroll(ulTarget)
  • 弹出层(popup)
    • teleport
    • transition
    • <slot />
    • modelValue
    • useScrollLock 锁定滚动
    const isLocked = useScrollLock(document.body);
    watch(
      () => props.modelValue,
      (val) => {
        isLocked.value = val;
      },
      { immediate: true }
    );
    
    • useVModel
    const props = defineProps({
      modelValue: {
        reguired: true,
        type: Boolean,
      },
    });
    defineEmits(["update:modelvalue"]);
    // 是一个响应式数据,当 isVisable 值发生变化时,会自动触发 emit 修改 modelValue
    const isVisable = useVModel(props);
    

PC 端基础架构搭建 (views/layout/, router/)

PC 端 Header 组件

  • Search
    • 下拉区
      • transition
        .slide-enter-active, .slide-leave-active {
          transition: all 0.5s;
          <!-- transition: opacity 0.5s, transform 0.5s; -->
        }
        .slide-enter-from,
        .slide-leave-to {
          opacity: 0;
          transform: translateY(20px);
        }
        
      • <slot />
      • modelValue
      • onClickOutside
      // 点击containerTarget(ref)区域外,触发事件
      onClickOutside(containerTarget, () => {
        isFocus.value = false;
      });
      
    • 搜索提示
      • 防抖:...如果这段时间内,输入事件被再次触发,则上次等待执行的事件取消...
        watchDebounced(() => props.searchText, getHintData, {
          immediate: true,
          // 每次事件触发时,延迟的时间
          debounce: 500
        })
        
    • 搜索历史

button 组件

  • 中定义常量 emun emit
    调试 export default
  • defindProps: validator
  • <slot />

popover 组件

  • (具名 + 匿名)
  • useElementSize(DOM)
  • nextTick(() => {}) // 数据改变之后,视图改变之后的回调。vue 在数据改变之后,需要等待一段时间 DOM 才会变化
  • 防抖 解决鼠标移出气泡消失
    let timeout = null;
    const onMouseenter = () => {
      isVisable.value = true;
      // 再次触发时,清理延时装置
      if (timeout) clearTimeout(timeout);
    };
    const onMouseleave = () => {
      // 延时装置
      timeout = setTimeout(() => {
        isVisable.value = false;
        timeout = null;
      }, DELAY_TIME);
    };
    

PC 端 navigation 组件

  • 响应模式下实现方案:
    抽离公用逻辑 数据 vuex
    封装私有逻辑 视图 组件中

Vuex 初始化

src/store/modules/category.js

import { getCategory } from "@/api/category";
export default {
  namespaced: true, // 独立作用域
  state: () => ({
    categorys: [],
  }),
  mutations: {
    // 为 categorys 赋值
    setCategorys(state, categorys) {
      state.categorys = [CATEGORY_ITEM_ALL, ...categorys];
    },
  },
  actions: {
    // 获取 category 数据,并自动保存到 vuex 中
    async useCategoryData(context) {
      const { categorys } = await getCategory();
      context.commit("setCategorys", categorys);
    },
  },
};

src/store/index.js

import { createStore } from "vuex";
import category from "./modules/category";
import getters from "./getters";
const store = createStore({
  getters,
  modules: {
    category,
  },
});
export default store;

src/store/getters.js

export default {
  categorys: (state) => state.category.categorys, // ($)store.getters.categorys
  currentCategory: (state) => state.app.currentCategory,
  currentCategoryIndex: (state, getters) => {
    return getters.categorys.findIndex(
      (item) => item.id === getters.currentCategory.id
    )
  },
};

监听 getters: watch(() => store.getters.key, (val) => {})
页面

import { useStore } from "vuex";
const store = useStore();
store.dispatch("category/useCategoryData");
store.commit("theme/changeThemeType", theme.type); // 没有actions

解决 category 闪烁

  1. 构建 categorys 的常量初始化数据
  2. 每次请求后 更新初始化数据
    vuex-persistedstate 自动缓存读取 vuex 数据
    npm i --save vuex-persistedstate@4.1.0
    src/store/index.js
    import { createStore } from "vuex";
    import createPersistedState from "vuex-persistedstate";
    const store = createStore({
      // ...
      plugins: [
        createPersistedState({
          // 保存到 localStorage 中的 key
          key: "imooc-front",
          // 需要保存的 module
          paths: ["category", "theme", "search", "user"],
        }),
      ],
    });
    export default store;
    

主题切换

  • 原理: 类名 -> CSS 改变
  • 配置 tailwindcss 暗黑模式
module.exports = {
  // 手动切换暗模式
  darkMode: 'class',
  • src/store/
  • src/utils/theme.js 初始化主题/监听主题改变,配置 class
  • src/utils/theme.js 监听主题跟随系统变化

waterfall 组件 (复杂)

  • item 遮罩层只有 PC 端显示 class="opacity-0 group-hover:opacity-100 hidden xl:block"
  • 作用域插槽
    <m-waterfall ...>
      <template v-slot="{ _item, _width }"> // _item, _width 是父组件传出来的
        <itemVue :data="_item" :width="_width" @click="onToPins" />
      </template>
    </m-waterfall>
    
    // waterfall 组件内部
    <div v-for="(item, index) in data">
      <slot :_item="item" :_width="columnWidth" :index="index" />
    </div>
    
  • const { paddingLeft, paddingRight } = getComputedStyle(containerTarget.value, null)
  • 图片预加载:图片预加载就是在将图片加载到页面之前,先把图片资源以某种方式加载进浏览器的缓存之中,从而加快图片的展示速度 Vue-lazyload。实现 CSS JS
    src\libs\waterfall\utils.js
  • 当服务端返回 img 高度,不需预加载,业务逻辑更加简单;位移动画 懒加载占位
  • setTimeout 解决屏宽变化后 containerWidth 计算不准确的问题
    setTimeout(() => { useColumnWidth() }, 200)

infinite-list 组件

  • intersectionObserver 监听列表滚动到底部(某个元素的可见性)
  • useIntersectionObserver
useIntersectionObserver(
  laodingTarget,
  ([{ isIntersecting }], observerElement) => {
    // 当 ‘加载更多’可见时 && loading为flase && 数据未全部加载完
    if (isIntersecting && !loading.value && !props.isFinished) {
      // 修改加载数据标记
      loading.value = true;
      // 触发加载更多行为
      emits("onLoad");
    }
  }
) -
  modelValue(loading) -
  `<slot>`;
  • setTimeout 列表渲染之后执行操作

图片懒加载

  • <img /> 添加指令
import { useIntersectionObserver } from "@vueuse/core";
export default {
  // 图片懒加载:在用户无法看到图片时,不加载图片,在用户可以看到图片后加载图片
  // 如何判断用户是否看到了图片:useIntersectionObserver
  // 如何做到不加载图片(网络):img 标签渲染图片,指的是 img 的 src 属性,src 属性是网络地址时,则会从网络中获取该图片资源。那么如果 img 标签不是网络地址呢?把该网络地址默认替换为非网络地址,然后当用户可见时,在替换成网络地址。
  mounted(el) {
    // 1. 拿到当前 img 标签的 src
    const imgSrc = el.src;
    // 2. 把 img 标签的 src 替换为本地地址
    el.src = "";
    const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => {
      if (isIntersecting) {
        el.src = imgSrc;
        // 停止监听
        stop();
      }
    });
  },
};

import lazy from './modules/lazy'
export default {
  install(app) {
    app.directive('lazy', lazy)
  }
}
  • Vite 自动注册指令
  • 指定彩色占位图
    • 生成随机色值 (src/utils/colors)
      const r = Math.floor(Math.random() * 255)
    • <div :style="{backgroundColor: randomRGB()}"><img /></div>
  • F12 右键 清空缓存并重新加载

posted @   bulvbuting1  阅读(53)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
点击右上角即可分享
微信分享提示