Vue3项目从0开发生产级后台系统,核心坑和实用落地策略有哪些?
最近两年接手或重构Vue3后台系统的开发者越来越多,毕竟官方明确说过Vue3是未来的主推版本,性能优化、组合式API、TypeScript原生支持这些特性,确实能解决Vue2遗留的不少问题,但真正从0搭到上线,踩过的坑、踩坑后总结的落地方法,才是新手最缺的干货,今天就结合3个不同量级项目的实战经验,跟大家聊透这个话题。
为什么建议生产级后台必须用组合式API?
很多刚从Vue2转过来的开发者,总觉得组合式API“代码散、没结构、不如Options API直观”,一开始写甚至会把所有逻辑堆在setup()里,反而搞得比Options还乱,但实际跑过生产级后台——尤其是有大量复杂表单、实时数据交互、权限控制关联的模块,组合式API的优势就会凸显出来。
逻辑复用的自由度,Options API做逻辑复用,主要靠mixin,但mixin有个致命问题:命名冲突和数据来源不透明,比如你引入3个mixin,分别叫useTable、useForm、usePermission,这三个里都有loading、confirmModal这两个属性或方法,最后生效的是谁的?出了bug要找代码,得翻遍所有引入的mixin才能定位,组合式API就不一样了,你可以把useTable的状态和方法单独命名,比如const { loading: tableLoading, getTableData } = useTable(),命名冲突一目了然,数据来源也通过解构清晰展现。
然后是代码的可维护性,Options API里,一个功能的代码会被拆到data、methods、computed、watch这些地方,比如一个“用户列表筛选+分页+导出”的功能,筛选条件写在data,筛选的触发方法写在methods,总页数的计算写在computed,监听筛选条件变化重置分页写在watch,想改这个功能得上下跳着找代码,组合式API可以把这一整套逻辑封装成一个独立的composable文件,比如useUserList,所有相关的状态、方法、计算属性都在这一个文件里,哪里有问题点哪里,维护起来特别爽。
还有TypeScript的类型支持,Options API虽然也能加TS,但需要用Vue.extend或者@Component装饰器,写起来繁琐不说,对某些复杂类型的推导也不够友好,组合式API的setup()天然支持TS,你可以直接定义类型,编译器会自动推导属性和方法的类型,开发时就能发现很多潜在的类型错误,上线后的bug率能降不少。
所以别再纠结用不用组合式API了,生产级后台必须用,关键是要学会怎么合理组织代码结构。
Vue3项目从0搭建时,哪些配置是必须提前做的?
很多新手搭项目,直接用Vite或者Vue CLI的默认配置就开始写代码,结果写着写着遇到各种问题:比如路径别名用不了、ESLint校验太严太松都不行、打包后代码体积太大、环境变量不生效等等,这些问题如果提前做好配置,后面写代码会省很多事。
路径别名和TypeScript配置
路径别名是开发后台系统时必加的,毕竟后台的文件层级可能很深,用@/components/Table比../../../../../components/Table舒服多了,Vite和Vue CLI的配置方法不一样,但核心都是修改配置文件和tsconfig.json(如果用TS的话)。
比如用Vite + TS的项目,vite.config.ts里要加resolve.alias:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})
然后tsconfig.json里要加baseUrl和paths,让TS编译器也能识别路径别名:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
ESLint和Prettier的统一配置
团队开发的话,ESLint和Prettier的统一配置是必不可少的,不然每个人写的代码风格不一样,合并代码时会有一堆冲突,看着也难受,很多新手容易把ESLint和Prettier的配置搞混,其实ESLint是用来做代码质量校验的,比如有没有未使用的变量、有没有使用eval这种危险函数;Prettier是用来做代码格式化的,比如换行、缩进、分号、引号。
这里推荐一个比较通用的配置组合:eslint-plugin-vue + @typescript-eslint/eslint-plugin(如果用TS)+ eslint-config-prettier + eslint-plugin-prettier,eslint-config-prettier用来关闭ESLint和Prettier冲突的规则,eslint-plugin-prettier用来把Prettier的规则当成ESLint的规则来校验。
配置好之后,可以在package.json里加两条命令:一条是检查代码格式的lint,一条是自动修复的lint:fix,开发完代码先跑一下lint:fix,大部分格式问题和简单的质量问题都会自动解决。
环境变量的配置
后台系统一般会有开发环境、测试环境、预发布环境、生产环境这四个环境,不同环境的接口地址、全局配置不一样,所以环境变量的配置也很重要。
Vite的环境变量配置很简单,在项目根目录下创建.env.development(开发环境)、.env.test(测试环境)、.env.pre(预发布环境)、.env.production(生产环境)这四个文件,文件名要和Vite的模式对应,环境变量必须以VITE_开头,不然不会被Vite暴露到客户端代码里。
env.development文件:
VITE_API_BASE_URL=http://localhost:3000/api VITE_APP_TITLE=后台管理系统(开发)
然后在代码里可以通过import.meta.env.VITE_API_BASE_URL来访问这些环境变量,注意import.meta.env是Vite提供的全局对象,只能在客户端代码和Vite配置文件里使用,不能在Node.js的普通模块里使用。
打包体积的预优化
生产级后台系统对打包体积有要求,不然首屏加载会很慢,用户体验不好,Vite默认的打包配置已经做了一些优化,但还有一些地方可以提前配置:
- 按需引入UI组件库,现在主流的Vue3 UI组件库,比如Element Plus、Ant Design Vue、Naive UI,都支持按需引入,不用把整个组件库都打包进去,比如按需引入Element Plus,可以用vite-plugin-element-plus插件,配置起来很简单。
- 开启gzip压缩,可以用vite-plugin-compression插件,开启gzip或者brotli压缩,打包后的文件体积能减少60%左右。
- 代码分割,Vite默认会把第三方库和业务代码分开打包,但可以进一步优化,比如把一些常用的第三方库单独打包成一个chunk,比如把lodash-es单独打包。
这些预优化配置做好之后,后面再根据实际情况调整,首屏加载速度应该不会有太大问题。
Vue3后台系统开发中,有哪些高频踩坑点?
响应式数据的陷阱
Vue3的响应式系统和Vue2不一样,Vue2用的是Object.defineProperty,Vue3用的是Proxy,Proxy确实比Object.defineProperty强大,能监听数组的索引变化和对象属性的新增删除,但也有一些陷阱需要注意:
- 不要直接替换reactive的对象,比如你有一个reactive的对象:const state = reactive({ list: [] }),如果你直接把state替换成state = reactive({ list: [1,2,3] }),state就不再是响应式的了,因为你替换的是整个对象的引用,Proxy监听的是原来的引用,正确的做法是修改state的属性:state.list = [1,2,3],如果你非要替换整个对象,可以用ref来包裹:const state = ref({ list: [] }),然后直接state.value = { list: [1,2,3] }。
- reactive不能监听基本数据类型,比如const count = reactive(1),count的变化不会触发视图更新,因为reactive只能监听对象或数组,基本数据类型要用ref来包裹。
- ref在模板里不需要加.value,但在setup()、composable文件、普通JS/TS文件里需要加.value,很多新手一开始会忘加.value,导致代码报错。
- 不要解构reactive的对象,比如const state = reactive({ name: '张三', age: 18 }),然后const { name, age } = state,这时候name和age就不再是响应式的了,正确的做法是用toRefs来解构:const { name, age } = toRefs(state),这样解构出来的name和age都是ref类型的,还是响应式的。
watch和watchEffect的区别
Vue3提供了watch和watchEffect两个监听API,很多新手一开始不知道用哪个,其实它们的区别很明显:
- watch需要指定监听的源,只有源变化时才会执行回调函数,watchEffect不需要指定监听的源,会自动收集回调函数里用到的响应式数据,只要其中任何一个变化就会执行回调函数。
- watch可以获取旧值和新值,watchEffect只能获取新值。
- watch默认是惰性执行的,第一次渲染时不会执行回调函数,只有源变化时才会执行,watchEffect是立即执行的,第一次渲染时就会执行一次回调函数。
- watch可以监听多个源,用数组包裹,比如watch([count1, count2], ([newCount1, newCount2], [oldCount1, oldCount2]) => { ... })。
举个例子,比如你有一个表单,需要监听表单的变化,然后自动保存草稿,这时候用watchEffect比较合适,因为表单里有很多字段,不用一个个指定监听的源,只要任何一个字段变化就会执行保存草稿的回调函数,但如果你只需要监听某个特定字段的变化,然后执行某个操作,比如监听用户名的变化,检查用户名是否已被注册,这时候用watch比较合适,因为可以获取旧值,避免重复请求。
路由跳转和组件销毁的时机问题
Vue3的路由跳转和组件销毁的时机,有时候会和新手想的不一样,导致一些问题:
- 路由跳转时,如果是从同一个组件跳转到同一个组件(比如从用户列表第1页跳转到第2页,路由参数变了但组件没变),组件不会重新创建,只会复用,这时候created、mounted这些生命周期钩子不会再执行,这时候需要监听路由的变化,比如用useRoute().params或者watch(useRoute(), () => { ... })来重新获取数据。
- 组件销毁时,定时器、事件监听器、WebSocket连接这些资源如果没有及时清理,会导致内存泄漏,Vue3的组合式API里,不需要像Options API那样在beforeUnmount或者unmounted里清理,只要在setup()里返回一个清理函数,或者用onBeforeUnmount、onUnmounted钩子即可。
import { ref, onBeforeUnmount } from 'vue'
const timer = ref(null)
timer.value = setInterval(() => { console.log('定时器执行') }, 1000)
onBeforeUnmount(() => { clearInterval(timer.value) })
或者可以用一个更优雅的方式,比如封装一个composable文件useInterval:
```typescript
import { ref, onBeforeUnmount } from 'vue'
export function useInterval(callback: () => void, delay: number) {
const timer = ref<number | null>(null)
const start = () => {
if (timer.value) return
timer.value = setInterval(callback, delay)
}
const stop = () => {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
}
start()
onBeforeUnmount(() => {
stop()
})
return { start, stop }
}
这样在组件里用useInterval,组件销毁时会自动清理定时器。
权限控制的实现方式
生产级后台系统肯定离不开权限控制,比如菜单权限、按钮权限、接口权限,Vue3后台系统的权限控制,一般有两种实现方式:前端控制和后端控制,推荐前端控制+后端验证的方式,这样既能提升用户体验,又能保证安全性。
前端控制的流程大概是这样的:
- 用户登录成功后,后端返回用户的token和权限信息(比如角色、菜单列表、按钮权限列表)。
- 前端把token存储在localStorage或者sessionStorage里,把权限信息存储在Vuex/Pinia里。
- 路由守卫(比如router.beforeEach)里检查用户是否登录,如果没登录就跳转到登录页;如果登录了,就根据用户的权限信息动态生成可访问的路由表,然后用router.addRoutes或者router.addRoute添加到路由里。
- 菜单组件根据Vuex/Pinia里存储的菜单列表渲染。
- 按钮权限可以用自定义指令实现,比如v-permission,当用户没有这个按钮的权限时,就把按钮隐藏或者禁用。
后端验证的流程大概是这样的:
- 前端每次请求接口时,都在请求头里带上token。
- 后端验证token是否有效,如果无效就返回401,前端跳转到登录页。
- 后端验证用户是否有这个接口的权限,如果没有就返回403。
这里要注意,前端控制只是为了提升用户体验,不能代替后端验证,因为前端的代码是可以被篡改的,比如用户可以通过浏览器的开发者工具修改路由表或者权限信息,所以所有的关键操作都必须在后端做权限验证。
Vue3后台系统开发中,有哪些实用的composable封装思路?
组合式API的核心就是composable,合理封装composable可以大大提高代码的复用率和可维护性,下面给大家介绍几个后台系统开发中常用的composable封装思路:
useRequest:封装接口请求
后台系统最常用的就是接口请求,封装一个useRequest composable,可以统一处理接口请求的loading状态、错误状态、成功回调、失败回调、取消请求等逻辑。
比如useRequest可以接受一个请求函数和一些配置项,返回loading、error、data、run、cancel这些状态和方法:
import { ref, onUnmounted } from 'vue'
import axios from 'axios'
export function useRequest<T>(
requestFn: () => Promise<T>,
options?: {
immediate?: boolean
onSuccess?: (data: T) => void
onError?: (error: any) => void
}
) {
const { immediate = true, onSuccess, onError } = options || {}
const loading = ref(false)
const error = ref<any>(null)
const data = ref<T | null>(null)
const controller = ref<AbortController | null>(null)
const run = async () => {
loading.value = true
error.value = null
controller.value = new AbortController()
try {
const res = await requestFn()
data.value = res
onSuccess?.(res)
} catch (err) {
if (!axios.isCancel(err)) {
error.value = err
onError?.(err)
}
} finally {
loading.value = false
}
}
const cancel = () => {
controller.value?.abort()
}
if (immediate) {
run()
}
onUnmounted(() => {
cancel()
})
return { loading, error, data, run, cancel }
}
这样在组件里用useRequest,就不用每次都写loading状态、错误处理这些重复代码了:
import { useRequest } from '@/composables/useRequest'
import { getUserList } from '@/api/user'
const { loading, error, data: userList, run } = useRequest(getUserList)
// 如果需要手动触发请求,比如点击搜索按钮
const handleSearch = () => {
run()
}
useTable:封装表格逻辑
后台系统的表格一般都有筛选、分页、排序、导出这些功能,封装一个useTable composable,可以把这些通用逻辑封装起来:
import { ref, reactive } from 'vue'
import { useRequest } from './useRequest'
export function useTable<T>(
requestFn: (params: any) => Promise<{ list: T[]; total: number }>,
defaultParams?: any
) {
const searchParams = reactive({ ...defaultParams })
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0
})
const { loading, data, run } = useRequest(
() => requestFn({ ...searchParams, ...pagination }),
{
immediate: false,
onSuccess: (res) => {
pagination.total = res.total
}
}
)
const handleSearch = () => {
pagination.current = 1
run()
}
const handleReset = () => {
Object.assign(searchParams, defaultParams)
pagination.current = 1
run()
}
const handlePageChange = (page: number) => {
pagination.current = page
run()
}
const handlePageSizeChange = (pageSize: number) => {
pagination.pageSize = pageSize
pagination.current = 1
run()
}
// 初始化数据
run()
return {
searchParams,
pagination,
loading,
data,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange
}
}
这样在组件里用useTable,只需要传入接口请求函数和默认筛选参数,就能得到所有表格相关的状态和方法:
import { useTable } from '@/composables/useTable'
import { getUserList } from '@/api/user'
const {
searchParams,
pagination,
loading,
data: userList,
handleSearch,
handleReset,
handlePageChange,
handlePageSizeChange
} = useTable(getUserList, { name: '', status: '' })
除了useRequest和useTable,还可以封装useForm(表单逻辑)、usePermission(权限检查)、useLocalStorage/sessionStorage(本地存储)、useDebounce/throttle(防抖节流)这些常用的composable。
Vue3开发生产级后台系统,核心就是合理利用组合式API、提前做好项目配置、避开高频踩坑点、封装通用的composable,只要掌握了这些方法,开发效率和代码质量都会大大提升,实战中还会遇到其他问题,比如WebSocket的封装、大数据量表格的性能优化、国际化的实现等等,这些以后有机会再跟大家聊。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



