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:
- npm i -D tailwindcss@3.0.23 postcss@8.4.8 autoprefixer@10.4.
- npx tailwindcss init -p 创建 tailwind.config.js
- 添加 tailwindcss 应用范围:
module.exports = {
// Tailwind 应用范围
content: ['./index.html', './src/**/*.{vue,js}'],
theme: {
extend: {}
},
plugins: []
}
- 在 src/styles/index.scss 中导入 tailwind 基础指令组件,并在 src/main.js 引入:
@tailwind base;
@tailwind components;
@tailwind utilities; - 安装 scss:
npm i -D sass@1.45.0
开发插件:
Prettier
- 根目录创建 .prettierrc
{
"semi": false, // 分号
"singleQuote": true, // 引号
"trailingComma": "none" // 尾随逗号
} - .vue .js 文件中,右键 使用...格式化文档 -> 配置默认格式化程序 -> prettier
- 使用:右键 格式化文档;保存自动格式化(设置 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; });
- transition
- 搜索提示
- 防抖:...如果这段时间内,输入事件被再次触发,则上次等待执行的事件取消...
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 闪烁
- 构建 categorys 的常量初始化数据
- 每次请求后 更新初始化数据
vuex-persistedstate 自动缓存读取 vuex 数据
npm i --save vuex-persistedstate@4.1.0
src/store/index.jsimport { 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>
- 生成随机色值 (src/utils/colors)
- F12 右键 清空缓存并重新加载
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库