Vue3+TypeScript+Vite实践:JeecgBoot低代码平台的工程化重构与权限体系

发布时间:2026/9/14 3:52:33
Vue3+TypeScript+Vite实践:JeecgBoot低代码平台的工程化重构与权限体系 简介JeecgBoot-Vue3版前端源码面向企业级低代码平台二次开发与中后台系统构建场景适合具备一定前端基础、想深入Vue3工程化实践或需要快速搭建权限敏感型应用的开发者。项目以Vue3.0、TypeScript、Vite、Ant-Design-Vue为核心栈将通用能力封装成二次封装组件、utils工具函数、hooks组合式逻辑同时提供动态菜单、页面权限与按钮级别权限校验整体能力明显强于Vue2版。压缩包共1441个文件其中666个Vue文件负责页面和组件实现554个TypeScript文件承担类型定义与业务逻辑其余配合less样式、png/svg图标以及eslint、prettier、Vite等工程化配置文件包体仅8.28MB目录结构清晰便于按模块取用。目前已有82人学习下载。通过源码可理解低代码前端架构中菜单权限如何动态生成、按钮操作如何做细粒度鉴权也可借鉴组件封装、类型约束、构建优化的实战经验适合作为企业项目二次开发起点或进阶学习资料。1. 从 Vue2 到 Vue3JeecgBoot 前端重构的选型与收益接手 JeecgBoot 老项目时我最头疼的不是业务逻辑而是 Vue2 时代的 mixin 满天飞、this 上下文到处窜以及每次改完权限都要重新刷新路由的挫败感。Vue3 版前端源码的思路很直接用 Composition API 把可复用逻辑收拢成 hooks用 TypeScript 把接口和状态约束住再用 Vite 解决 Dev 服务器启动慢的痼疾。这套方案对低代码平台尤其合适——因为低代码平台真正复杂的地方不在页面渲染而在动态菜单、按钮级权限和数据模型的动态映射。如果你正在评估企业级后台管理系统或者想把老项目往 Vue3 迁移这套源码值得拆开看一遍它能告诉你权限链路和组件封装在真实项目中应该长什么样。2. 工程基座Vite TypeScript 的项目结构与启动链路如果只是把 Vue2 项目升级到 Vue3你不一定需要 Vite。但 JeecgBoot 的模块数量多组件依赖复杂Webpack 冷启动要等十几秒改一行代码热更新又要等两三秒这种体验在低代码开发中是致命的。Vite 利用浏览器原生 ESM按需加载模块冷启动只需要秒级。源码里vite.config.ts是核心它同时管着开发服务器、构建优化和路径别名。2.1 入口与目录结构先看这几层src下主要分components、hooks、utils、directives、store、router、views。和 Vue2 版最大的区别是没有mixins目录替代者是hooks。以典型的登录后获取用户信息为例Vue2 里会写在mixin里再用this.$store访问Vue3 版把这段逻辑抽成useUserStore风格的 hook在setup中直接调用。main.ts里的创建方式也要注意import { createApp } from vue import App from ./App.vue import router from ./router import { setupStore } from ./store const app createApp(App) app.use(router) setupStore(app) app.mount(#app)这段代码有两点值得注意第一createApp取代了 Vue2 的全局构造函数每个应用实例独立避免多个应用共享配置的污染第二setupStore(app)是 JeecgBoot 封装的 Pinia 初始化方法参数传入app是为了让 store 实例可以访问应用上下文比如在 store 里调用路由跳转。如果你用过 Vuex会发现 Pinia 的setup风格与 Composition API 天然统一。2.2 Vite 配置中的路径别名与按需加载vite.config.ts里最常见的配置是路径别名和build.rollupOptionsimport { defineConfig } from vite import vue from vitejs/plugin-vue import viteImagemin from vite-plugin-imagemin export default defineConfig({ resolve: { alias: { : /src, components: /src/components } }, build: { rollupOptions: { output: { manualChunks: { antd: [ant-design-vue], axios: [axios] } } }, chunkSizeWarningLimit: 1500 } })manualChunks这里手动拆包把 Ant-Design-Vue 单独打成antd包axios 单独打一个包这样公共库的缓存不会被业务代码的更新失效。chunkSizeWarningLimit: 1500是因为 AntD Vue 本身就很大不调高会一直刷警告。真实项目中我一般还会加上vite-plugin-compression做 gzip 预压缩否则首屏加载在低带宽环境下会很慢。2.3 环境变量与多环境构建JeecgBoot 的前后端分离场景需要区分开发、测试、生产环境。.env.development和.env.production中通常定义VITE_APP_BASE_URL/jeecg-boot VITE_APP_API_URLhttp://localhost:8080/jeecg-boot VITE_APP_TITLEJeecgBoot低代码平台在代码里通过import.meta.env.VITE_APP_API_URL读取。注意变量名必须以VITE_开头否则 Vite 不会暴露给客户端。这里有个坑如果你用 Nginx 代理/jeecg-boot到后端开发环境可以写全路径但生产环境最好只写路径前缀让 Nginx 处理跨域否则浏览器会直接请求后端地址容易暴露内部端口。配置项作用常见坑resolve.alias缩短 import 路径必须用绝对路径/src不要用相对路径拼接server.host局域网访问默认localhost手机上调试要设为0.0.0.0build.target浏览器兼容默认modules老浏览器要降为es2015optimizeDeps.include预构建依赖某些 ESM 包不兼容时手动加入vite build之后产物默认在dist目录需要确保后端网关把静态资源目录指到这儿。JeecgBoot 官方推荐用 Nginx 托管dist并配置try_files $uri $uri/ /index.html;解决前端路由在刷新时 404 的问题。这一条在部署时最容易漏后面第 6 章会专门讲验证方法。3. 组件与逻辑复用二次封装组件、utils 与 hooks 的实现模式企业级后台管理系统里UI 库提供的组件往往不能满足业务需求。比如表格需要列配置、导入导出按钮、行级权限表单需要根据后端返回的 JSON Schema 动态渲染。JeecgBoot-Vue3 版把这类通用逻辑拆成了三层二次封装组件、utils 函数、hooks 组合式函数。这三层各司其职配合不好就会出现代码冗余。3.1 二次封装组件以JTable为例原始 Ant-Design-Vue 的 Table 已经很强但低代码场景下后端返回的是列配置、数据源 URL、分页参数这些动态数据。所以 JeecgBoot 封装了JTable它接收一个url属性内部自动管理数据请求、分页、loading 和刷新。template j-table url/sys/user/list :columnscolumns :row-selection{ selectedRowKeys, onChange: onSelectChange } / /template script setup langts import { JTable } from /src/components/jeecg/JTable const columns [ { title: 用户名, dataIndex: username, width: 150 }, { title: 角色, dataIndex: roleName, ellipsis: true } ] /scripturl参数会在内部被拼上?pageNo1pageSize10请求返回体必须符合约定的格式{ records: [], total: 0 }。如果你对接的是老接口返回字段是rows和totalCount就要在封装的JTable里通过dataKey属性重新映射。实际开发中我通常还会在 JTable 基础上再包一层JtTable把「导出当前页」「批量删除」这类按钮逻辑也收进去避免每个页面重复写。3.2 utils工具函数应该聚焦纯逻辑utils目录里最常用的是auth和token相关函数。以权限判断为例后端返回的权限标识是一个数组比如[user:add, user:edit]前端需要判断当前用户是否包含某个权限点。// /src/utils/auth.ts export function hasPermission(permission: string): boolean { const permissions JSON.parse(localStorage.getItem(perms) || []) return permissions.includes(permission) || permissions.includes(*) } export function getToken(): string { return localStorage.getItem(pro__Access-Token) || } export function setToken(token: string): void { localStorage.setItem(pro__Access-Token, token) }localStorage的 key 命名要和后端过滤器对上pro__Access-Token是 JeecgBoot 前后端约定的请求头名称。如果你改了这里的 key后端TokenInterceptor也要同步修改否则登录鉴权会失效。utils 里不建议放访问localStorage之外的重逻辑比如网络请求应该交给 Vuex/Pinia 的 action 层。3.3 hooks把业务状态和副作用收拢起来hooks 是 Vue3 版最值得借鉴的部分。比如一个典型的用户信息加载 hook// /src/hooks/useUserInfo.ts import { ref, computed } from vue import { getUserInfo } from /src/api/system export function useUserInfo() { const userInfo ref(null) const roles computed(() userInfo.value?.roles || []) const loadUserInfo async () { const res await getUserInfo() userInfo.value res.result } return { userInfo, roles, loadUserInfo } }这个 hook 的优点在于不使用全局状态也能在多个组件间共享逻辑比如页头显示用户名、侧边栏根据角色渲染菜单、按钮权限判断各自调用useUserInfo()但只取需要的部分。和 Vue2 的 mixin 相比hook 不会隐式提升变量作用域变量来源清晰编辑器也能做更准确的类型推导。多组件间共享同一份用户信息时更好的做法是把 userInfo 放进 Piniahook 里只做数据加载。JeecgBoot 的 store 里就有useUserStore它内部调getUserInfo并缓存后续页面直接storeToRefs取值避免重复请求。这里给大家一个判断封装边界的原则如果逻辑需要在多个页面重复使用且依赖响应式状态loading/error/data可以放 hook 里如果状态需要跨越多个组件共享就放 store 里。如果只是一个页面内的局部逻辑直接写在setup里就够了过度封装反而增加学习成本。3.4 坑直接 import AntD 组件导致样式丢失Vue3 版中如果按需引入组件但没有引入对应样式运行时会出现组件无样式的情况。JeecgBoot 的setup脚本里已经通过 unplugin-vue-components 自动导入组件和样式。如果你自己新增了独立的弹窗组件记得检查是否显式引入ant-design-vue/es/modal/style/index.css或者在vite.config.ts里配置Components({ resolvers: [AntDesignVueResolver()] })。这类问题在控制台不会报错只在浏览器里表现为布局错乱很难排查。我建议在本地开发时打开 Vue Devtools 的 Performance 面板看运行时是否发出样式加载的警告。4. 动态菜单与权限校验从路由表到按钮级控制的完整链路动态菜单和按钮级权限是 JeecgBoot 这类低代码平台区别于普通后台模板的核心。Vue2 版里常见的方案是在路由守卫里拉一次菜单树然后用addRoutes动态注册。Vue3 中 Vue Router 4 去掉了addRoutes支持addRoute单个添加并且路由状态由router实例统一管理。JeecgBoot-Vue3 版的实现可以概括为后端返回菜单树 → 前端转换成路由记录 → 渲染侧边菜单 → 在按钮层绑定权限标识。4.1 后端返回菜单树的数据模型后端返回的菜单项通常是树形结构核心字段包括{ id: 1, parentId: 0, path: /system, component: system/user/UserList, name: 用户管理, redirect: , meta: { title: 用户管理, icon: user, permission: user:list }, children: [] }component字段在这里只给一个视图组件的路径字符串前端需要把它转换成() import(/src/views/ component .vue)。这一步不能直接在路由守卫里同步执行因为组件加载是异步的。实际项目里常见做法是在生成路由表时就预构建所有可能的组件映射或者利用 Vite 的import.meta.glob批量导入// /src/router/dynamic.ts const viewModules import.meta.glob(/src/views/**/*.vue) export function buildRoutes(menus: any[]) { return menus.map(menu { const component menu.component ? viewModules[/src/views/${component}.vue] : viewModules[/src/layouts/BlankLayout.vue] return { path: menu.path, name: menu.name, component, meta: menu.meta, children: menu.children ? buildRoutes(menu.children) : [] } }) }这么写的优点是不用手动维护组件映射表新增页面时只要在views目录下建文件后端配置好component路径就行。但要注意import.meta.glob会一次性把所有views下的文件都打包到异步块中如果你不需要首屏加载所有页面最好配合 Vite 的dynamicImport机制实际是生成多个懒加载 chunk浏览器只会按需加载。import.meta.glob返回的是函数调用后才真正发请求。4.2 路由守卫与权限校验顺序前端路由守卫一般放在src/router/index.ts中router.beforeEach(async (to, from, next) { const token getToken() if (!token to.path ! /login) { next(/login) return } if (token !userStore.isUserLoaded) { try { const menus await userStore.fetchUserInfo() const routes buildRoutes(menus) routes.forEach(route router.addRoute(route)) next({ ...to, replace: true }) } catch (e) { userStore.reset() next(/login) } } else { next() } })这里的核心逻辑是首次进入系统时拉取用户菜单数据构建路由后addRoute然后next({ ...to, replace: true })重新进入当前路由。如果不加replace: true路由地址不会刷新但组件已经被注册了实际也可以工作不过next()可能让某些依赖to.meta的进入守卫重跑一次。加上replace更稳妥。4.3 按钮级权限控制指令与函数的配合按钮级权限在页面里最常用JeecgBoot 提供了两个方案一个是v-auth指令一个是auth函数。// /src/directives/auth.ts import { hasPermission } from /src/utils/auth export const auth { mounted(el: HTMLElement, binding: any) { if (!hasPermission(binding.value)) { el.parentNode?.removeChild(el) } } }在模板中使用template v-ifhasPermission(user:add) a-button typeprimary clickhandleAdd新增/a-button /template我用得更多的是函数方式的hasPermission因为指令方式在v-if和异步渲染时可能失效——如果按钮的父节点在指令mounted之后才渲染出来直接删除元素会留下空节点。函数方式配合v-if更直观也方便在业务代码里做更复杂的判断比如「同时拥有A和B权限才显示」。权限标识的定义规则需要前后端一致通常采用「模块:操作」的格式如sys:user:add注意与后端 Shiro/Spring Security 注解的权限字符串对应。如果你把权限点写错按钮不会显示排查时可以先看登录用户返回的perms数组里有没有对应值。这里有个经验在开发环境临时开启hasPermission返回true可以方便调试布局但上线前务必改回来防止越权操作。4.4 动态菜单的刷新与保持动态菜单的一个常见问题是修改权限后用户重新登录菜单不会自动更新。JeecgBoot 的做法是在登录成功后的用户信息接口里返回菜单数据前端在userStore.fetchUserInfo中覆盖menus由于菜单渲染是响应式的重新赋值后侧边栏会自动刷新。但路由不能直接覆盖需要先把旧路由清掉。最简单的方式是用router.removeRoute遍历旧的动态路由名称来删除再重新添加。我在一个真实项目里踩过坑旧路由没删干净新老路由同时存在导致页面重复渲染。所以建议在fetchUserInfo成功之后先执行一次router.getRoutes().forEach(r router.removeRoute(r.name))再添加新路由。这个操作不会影响静态路由因为静态路由你可以维护一个白名单列表跳过。5. 低代码平台上的业务接入Ant-Design-Vue 主题定制与表单引擎对接把 JeecgBoot-Vue3 源码跑起来容易真正接入业务时你会发现大多数时间不是写页面而是在调 UI 主题、改表单渲染逻辑、对接后端接口格式。这一章我挑三个低频但必备的实战点主题变量覆盖、动态表单组件解析、以及文件上传/导入的回调封装。5.1 Ant-Design-Vue 主题定制不只是改颜色AntD 的样式基于 LESS 变量Ant-Design-Vue 3.x 在 Vite 下推荐用ConfigProvider配合cssVar来覆盖主题。JeecgBoot 源码里的design目录有theme相关的样式入口。如果你想修改主色调最简单的做法是在src/styles/antd.less中覆盖变量primary-color: #0366d6; // 注意新版 AntD 升级为 colorPrimary border-radius-base: 4px; font-size-base: 14px;然后在vite.config.ts里启用css.preprocessorOptions.less的额外变量注入避免在每个组件样式里重复引变量。有一点需要留意JeecgBoot 3.x 已经逐步迁移到 AntD 4.x 的 token 体系Design Token如果直接用primary-color可能不生效需要同时修改ConfigProvider的theme.token.colorPrimary。建议以ConfigProvider为准样式表变量只作为兜底。实际体验中通过 token 修改主题可以实时更新不需要重新编译对更换企业品牌色的需求很友好。5.2 动态表单根据 JSON Schema 渲染控件低代码平台最常见的业务是后端返回表单字段配置前端动态渲染。JeecgBoot 中JForm组件相当于是 Ant Design Form 的增强版支持在schema中指定控件类型[ { field: username, label: 用户名, component: Input, rules: [{ required: true, message: 请输入用户名 }], props: { placeholder: 请输入用户名, maxlength: 20 } }, { field: status, label: 状态, component: RadioGroup, options: [ { label: 启用, value: 1 }, { label: 禁用, value: 0 } ] } ]封装服务器会做两件事一是根据component字段动态导入相应的 AntD 组件二是把rules转成 AntD 校验对象。这样做最大的收益是后端可以统一管理表单元数据前端不再为每个页面手写一遍表单。如果是开发者自己使用也可以通过JForm组件上的formSchema属性传入注意字段必须和表单数据模型model对应。我在对接时发现component名称必须与全局注册的组件名一致如果用了Input但组件没注册会渲染成纯文本输入框且没有样式。可以写一个调试代码在渲染前打印当前组件是否注册import { resolveComponent } from vue const resolved resolveComponent(schema.component) console.log(Component ${schema.component}:, resolved || Not Found)5.3 文件上传与导入的回调封装后台管理经常有导入 Excel 的功能JeecgBoot 的JUpload组件支持action字段指定上传地址并返回fileId和fileName。如果我们想在导入完成后刷新列表需要监听上传成功的回调j-upload :actionuploadUrl successhandleUploadSuccess :headers{ token: getToken() } a-button上传文件/a-button /j-upload注意headers里传入 token不能写成Authorization之外的怪名字要和后端的TOKEN_PREFIX一致。另外如果上传接口返回的数据是远程 URL且配置了 OSS那么action应该指向后端的upload接口由后端完成文件流中转避免前端直接访问对象存储导致签名过期。还有一种容易忽略的情况组件会自动把文件name字段当成文件名如果你的文件名在返回的result字段里叫originalName就要在onChange里手动取值否则表单提交时拿不到正确的文件对象。这些细节在官方示例里不会展示但在真实对接 OSS 时必踩。5.4 错误处理与全局提示低代码平台接口调用失败非常频繁JeecgBoot 在 axios 拦截器里做了统一处理。我们一般会在src/utils/request.ts中配置service.interceptors.response.use( (response) { const res response.data if (res.code ! 200) { message.error(res.message || 请求失败) if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.message)) } return res }, (error) { const status error.response?.status if (status 403) { message.warning(没有权限访问该功能) } else if (status 500) { message.error(服务器内部错误请查看控制台日志) } return Promise.reject(error) } )这里要注意JeecgBoot 后端返回的code字段可能是200或0不同版本不一致。如果你看到的正常响应码不是 200需要修改这里的判断条件。我在迁移到新后端时发现老项目用success布尔值新项目用code一定要统一。全局拦截器不要吞掉错误要reject出去否则页面的catch块接受不到会导致 loading 状态卡死。另外message.error在移动端可能不友好可以预留一个基于notification的替换方案。总之请求层是低代码平台的命脉配置上宁可保守也别激进。6. 构建部署验证与首屏加载优化从构建产物到线上排查所有功能验证完后部署阶段往往比写代码更折磨人。这里分享一条我常用的验证链路覆盖构建、Nginx 配置、首屏和权限校验四个环节读者可以照着一项一项过。6.1 构建产物清单确认执行pnpm build后先看dist目录结构。一个合理的产物应该包含index.html不要有内联脚本动态加载外部地址assets/下列出若干个 chunk JS 和 CSS按模块拆分不应出现体积超过 2MB 的恐怖 chunk除非你手动确认过du -sh dist ls -lh dist/assets/如果发现index-xxxx.js超过 2MB先不要急着优化看控制台是否有vite:build的警告然后对照rollupOptions.output.manualChunks是否成功拆分。通常 AntD 组件库和业务代码混在一起才会炸。6.2 Nginx 配置验证用一份基础的 Nginx 配置server { listen 80; server_name your-domain.com; root /opt/jeecg-vue3/dist; index index.html; location /jeecg-boot/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }验证时重点看两处/jeecg-boot/代理是否覆盖了后端所有接口且proxy_pass末尾的/不能少否则路径前缀会被重复拼接前端history路由刷新是否被try_files兜住。直接在浏览器里打开https://your-domain.com/system/user刷新后若出现 404就是try_files配置不对。6.3 首屏加载性能验证打开 Chrome DevTools 的 Network 面板记录FCP和LCP。关键优化两处一是 gzip 压缩二是按需加载路由组件。在 Nginx 开启gzip on; gzip_types application/javascript text/css application/json; gzip_min_length 1k;改完配置后响应头里应该出现Content-Encoding: gzip。如果网络面板里 JS 文件仍然没有压缩检查上游是否有反代或者 CDN 把 gzip 去掉了。6.4 权限动态菜单的冒烟测试权限验证要在生产环境跑一遍完整流程登录一个新账号看到菜单树是否正确访问一个没有权限的 URL前端路由守卫是否拦截打开一个含按钮的列表页确认v-auth不显示的按钮在 DOM 中不存在。用如下方式快速验证动态路由是否被正确注册curl https://your-domain.com/system/user | grep app如果index.html能正常返回但curl页面里的 JS 路径是 404多半是base配置不对。Vite 默认base: /如果你的站点部署在子路径比如/admin/需要在vite.config.ts里设置base: /admin/否则资源全路径对不上。最后一个具体技巧如果你怀疑动态菜单在某些浏览器上不显示不要只刷新前端在控制台执行router.getRoutes()检查动态路由是否注册成功。如果路由表里有但菜单不渲染问题出在菜单树组件如果路由表都没有问题出在后端返回的菜单数据或buildRoutes的路径匹配。按照这条链路排查五分钟内就能定位问题比反复刷新页面有效得多。本文还有配套的精品资源点击获取