1. 先搞清楚JEECG前端是怎么构建起来的
1.1 新旧版本的构建差异
如果你手上的是老版的jeecgboot-vue,那本质上是Vue2 + Ant Design Vue 1.x + Webpack 4的经典组合,构建命令跑的是vue-cli-service build。"新版的jeecgboot-vue3",底层换成了Vue3 + Vite,构建走的是Rollup。两者虽然表面上都叫npm run build,但底层机制完全不同,踩坑的方向自然也不一样。
我在项目上遇到过两种项目混着维护的情况,一个老版维护了两年,一个新版刚接手。老版构建慢,能跑十分钟,新版构建快很多,但依赖和插件版本特别敏感。所以拿到JEECG前端,第一步不是急着run build,而是先搞清楚你手上是哪个分支、哪个版本、对应Node版本应该在什么范围。否则后面每一步都可能莫名其妙地报错。
1.2 一条构建链路里的关键环节
JEECG前端项目的构建过程,核心做这几件事:读取.env系列环境变量文件,把源码里的Vue单文件组件编译成浏览器认识的JS和CSS,把懒加载的路由拆成一个个chunk,再经过压缩混淆,最后输出到dist目录。这个过程中,任何一个环节出问题,都会让run build直接抛错。
但很多人忽略了一个前置条件:JEECG的源码目录比较大,而且涉及在线表单、在线报表这类动态渲染逻辑,打包时会有大量模块需要处理。这就意味着,构建过程中对内存的消耗远高于普通管理后台。如果你直接用默认Node内存上限去跑,十有八九会撞上JavaScript heap out of memory。
提示:在动手排查所有奇奇怪怪的报错之前,先确认三件事——Node版本、npm/yarn源、依赖是否装全。JEECG前端80%的构建问题其实都出在这三件事上,而不是源码本身。
2. 最恶心的那个坑:构建时内存溢出
2.1 问题现象与根因分析
run build跑到一半,终端直接输出一大段红色报错,开头通常是FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory。还有的会提示CALL_AND_RETRY_LAST Allocation failed这种。
我当时第一次遇到时,第一反应是源码坏了,来回检查了半天。后来才明白,Node.js默认的堆内存上限在64位系统上一般是2GB左右,老一些的版本只有1.4GB左右。Webpack 4在打包时要把所有模块的解析结果放在内存里,JEECG这种动辄几百上千个组件的项目,模块依赖树庞大,再加上路由懒加载、代码压缩、Source Map生成,内存轻轻松松突破2GB。一旦突破上限,进程就被强杀,构建自然失败。
新版Vite的原理不太一样,它用的是ESBuild,构建速度确实快很多,但如果是比较大的项目,并且开了一堆插件,同样可能遇到内存问题。只是概率比Webpack低一些。
2.2 给Node“加内存”的正确姿势
在Node层面,可以通过NODE_OPTIONS环境变量来调整堆内存上限。我实测下来最稳的组合是这样:
# Linux / macOS export NODE_OPTIONS="--max_old_space_size=4096" npm run build# Windows PowerShell $env:NODE_OPTIONS="--max_old_space_size=4096" npm run build但Windows下更省心的方式是在package.json里直接改构建命令,借助cross-env来跨平台设置环境变量。改完之后的package.json中,scripts.build大概是这样的:
"scripts": { "build": "cross-env NODE_OPTIONS=--max_old_space_size=4096 vue-cli-service build" }这里注意,很多人会想直接写node --max_old_space_size=4096 node_modules/.bin/vue-cli-service build,这种方式在Windows下同样会有路径分隔符的问题,不如用cross-env统一处理。而且cross-env这个依赖本身就在JEECG的package.json里,不需要额外安装,省事。
4GB我觉得是起步,有些大型项目直接给6GB甚至8GB。我给一个老版JEECG项目分配过8192MB,构建过程内存占用峰值能到6GB多。量力而行,但别太吝啬。
2.3 内存问题背后容易忽略的两件事
如果你已经加到了8GB还是崩,那问题就不只是内存了。还有两个细节值得关注:
一是Source Map。vue.config.js里如果开了productionSourceMap: true,构建时要把所有源码的映射关系也打进内存,内存占用直接翻倍。老版JEECG默认开了这个,很多人在部署时不看.map文件,其实完全可以在生产构建时关掉。命令可以这样写:
cross-env NODE_OPTIONS=--max_old_space_size=4096 vue-cli-service build --no-source-maps但注意,如果项目里配置了三方监控(比如前端错误上报),Source Map是线上定位问题的重要依据。折中方案是构建后单独保留.map文件不上传到静态服务器,只在排查问题时用。这个后面我还会细说。
二是Webpack的多进程压缩插件。老版JEECG经常用terser-webpack-plugin配合thread-loader来并行压缩JS。并行压缩的好处是构建速度快,坏处是每个worker都要复制内存,几个worker加起来内存翻好几倍。如果你在构建日志里看到terser-worker相关进程,就要留意了。可以在项目里把parallel参数关掉试试,代价是构建慢一些,但内存峰值会明显下降。
注意:改
NODE_OPTIONS之前,先确认Node版本是多少。Node 12、14、16、18的内存管理行为不完全一样,比如Node 16以后默认堆上限就是2GB,再老的版本可能更低。用node -v看一眼,心里有数。
3. 依赖装不上、版本打架:八成卡在环境上
3.1 node-sass和Python环境的历史包袱
老版JEECG有个让人头疼的依赖——node-sass。这货不是纯JS实现,安装时需要从GitHub下载对应Node版本的二进制文件。国内网络环境下,下载经常超时或失败,于是你看到的就是一堆node-sass编译相关的报错。
比这更麻烦的是,当node-sass找不到预编译二进制时,会退回本地源码编译,这时候就需要系统里有Python 2.7和C++编译工具链。很多开发者电脑上根本没装,或者装的是Python 3.x,结果编译失败,报错信息还特别长,看起来像源码问题,其实是环境问题。
我的建议是,能不用node-sass就别用。新版JEECG已经改用了dart-sass(也就是sass包),老版如果想换也是可行的:把package.json里的node-sass删掉,换成sass,然后全局搜一下源码里node-sass相关的引用,改成sass的API。实测下来,构建速度和稳定性都有提升,只是个别项目中Sass的/deep/选择器写法在dart-sass下会报deprecated警告,需要改成::v-deep或:deep()。
3.2 锁版本、换源、清缓存的正确顺序
依赖安装的问题,我总结了一套固定顺序,减少瞎折腾:
- 先删掉
node_modules和package-lock.json,确保没有残留的旧依赖。 - 在项目根目录配一个
.npmrc,写上镜像源和sass的二进制下载地址:
registry=https://registry.npmmirror.com sass_binary_site=https://npmmirror.com/mirrors/node-sass/注意,如果你换成dart-sass,sass_binary_site就用不上了,因为dart-sass是纯JS包。
- 用
npm cache clean --force清一下npm缓存,但别频繁用,清缓存会拖慢后续安装速度。 - 最后再执行
npm install。装完后,我习惯先跑一下npm run serve,确认开发环境能起来,再做build。
提示:千万别在项目里同时混用
npm install和yarn,两个包管理器生成的锁文件格式不一样,混用容易把依赖树搞乱。我踩过这个坑,npm install后发现有个包版本变了,排查半天才发现是之前用yarn装过,锁文件的逻辑全被打乱了。
3.3 不同Node版本切换的实测建议
JEECG老版项目在Node 16下面基本能正常构建,但个别老依赖在Node 18下会报ERR_OSSL_EVP_UNSUPPORTED,这个错是因为OpenSSL 3和新版Node的兼容性问题,Webpack 4在打包时用了旧的md4哈希算法。解决办法有两种:一是把Node版本切回16,二是启动时加--openssl-legacy-provider参数。
我推荐用nvm来管理Node版本,按项目建一个.nvmrc文件,里面写上项目推荐的Node版本号。不同项目切换时用nvm use就行,不用反复卸载重装。实测JEECG老版配Node 16最稳,新版Vite项目则需要Node 18以上,否则Vite起不来。
如果公司CI里的镜像只装了固定Node版本,那就尽量和本地保持一致。别小看这个,本地构建过了,CI构建失败,十有八九是Node版本不一致引起的。
4. 构建成功却打不开页面:部署层的坑更隐蔽
4.1 刷新404:前端路由模式与服务器配置
构建产物拿到手上的时候,dist目录已经生成了。把它扔进Nginx后,首页能打开,但一旦刷新某个子页面,马上404,或者直接白屏。这个问题的根因在于前端路由模式。
JEECG默认开启history路由模式,也就是路由路径是服务器上真实不存在的地址。浏览器刷新时,它去请求/system/user这类路径,服务器上并没有这个文件,Nginx就按找不到资源来处理了。解决办法是配置Nginx的try_files,把所有路由请求回退到index.html:
location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; }配置完记得nginx -s reload。这个坑的特点是没有构建报错、没有任何提示,就是打开白屏。我自己就经历过一次上线后刷新404,排查到凌晨才发现是Nginx配置文件里少了try_files。
4.2 接口全部404:环境变量和代理没配好
构建成功后页面能显示,但所有接口请求全挂了,打开浏览器开发者工具的Network面板,前端请求的API地址要么是localhost:8080,要么是个内网地址,这明显是构建时读的环境变量不对。
JEECG前端项目在src/utils/request.ts或类似封装里,会读取VUE_APP_API_BASE_URL(老版)或VITE_PROXY、VITE_GLOB_API_URL(新版)这类环境变量。构建时,如果.env.production文件里没配好,或者配的是开发环境的地址,打包出来的JS代码里就会带上错误地址。
排查思路:先在构建输出目录里全局搜索一下API域名,确认产物里到底写了什么地址。然后打开.env.production,把API地址改成生产环境后端地址。新版Vite项目还要注意,build模式下代理是不生效的,开发环境用的代理配置不会带到生产环境,必须在环境变量里写实际地址。
如果后端接口支持跨域,直接写完整https://地址就行。如果前后端同域部署,可以写相对路径/jeecg-boot,但这样后端网关的路径前缀必须配置正确,否则还是404。
4.3 白屏和Source Map的排查
还有一种情况:构建成功,部署成功,刷新不404,但页面就是白屏,控制台有JS报错。常见的原因有:浏览器太旧,不支持打包产物里的ES6语法;或者某个CDN上的公共库没加载成功。
老版JEECG默认会把一些公共库设置成CDN引入,比如vue、ant-design-vue。如果页面里的CDN地址被内网策略屏蔽,或者CDN域名解析失败,运行时就会白屏。排查方法:在浏览器Network面板里看有没有红色的请求记录,尤其是.js文件。如果是CDN地址访问不了,要么换可访问的CDN地址,要么把CDN引入改为本地打包引入(在vue.config.js中把对应库从externals里移除,并删掉index.html里的CDN链接)。
Source Map在这里就很有用了。如果构建时保留了.map文件,在浏览器控制台里能看到真实的源码报错位置,而不是一堆压缩后的混淆代码,定位问题会快得多。这也是我为什么建议生产构建可以关掉Source Map,但出了问题的时候,保留一份做排查。
5. 构建体积太大:压缩和拆包的几种实用做法
5.1 先看构建报告,再动手优化
别一上来就疯狂压缩。先搞清楚体积到底大在哪个文件上。Webpack项目用webpack-bundle-analyzer,Vite项目用rollup-plugin-visualizer,构建完自动打开一个树状图,哪个依赖占了几MB、哪个路由打包成了多大的chunk,一目了然。
我在一个老版JEECG项目里优化过,没优化前dist整个目录有45MB,单个JS文件甚至有8MB。后来分析发现,主要大头是ant-design-vue全量引入、echarts全量引入,以及各种在线编辑器组件。这其实是低代码平台常见的问题:组件太多,什么功能都往里塞,但页面上根本用不到那么多。
5.2 拆包、Gzip、去掉预加载的取舍
按按需引入的方式来改,是性价比最高的。JEECG老版默认ant-design-vue是全量引入的,实际上只用到了表格、表单、弹窗、菜单这些基础组件。改成babel-plugin-import按需加载,体积下降非常明显。
路由懒加载这边,如果项目里所有页面都用了import静态引入,构建产物会把几百个页面打成一个巨大的JS。改写成const xxx = () => import('@/views/xxx'),每个页面独立成一个chunk,首屏只加载当前路由的代码,体积和加载速度都会显著改善。
JS和CSS的Gzip压缩也要安排上。webpack项目可以加compression-webpack-plugin,在构建时直接生成.gz文件,Nginx配置gzip_static on;后,服务器优先返回预压缩文件,省CPU又省流量。Vite项目也可以直接用vite-plugin-compression。
但注意一点,拆包不是拆得越碎越好。chunk太多会导致HTTP请求数量爆炸,反而拖慢加载。对于JEECG这种管理后台,合理的做法是把vendor(第三方库)、echarts(图表库)、ant-design-vue(组件库)各拆成一个独立的公共chunk,业务代码按路由懒加载,整体控制在几十个chunk以内最理想。
5.3 构建时长优化的个人经验
除了体积,构建时间也是痛点。老版Webpack 4的JEECG项目,不加优化的情况下,构建一次能跑七八分钟甚至更久。我实测下来,比较有效的手段是:
- 生产构建关闭Source Map,构建时间能降30%以上;
- 多进程压缩打开,配合
parallel: true,压缩阶段时间明显缩短; - 把一些不会变的第三方库(如
ant-design-vue)用DLLPlugin或直接放CDN,跳过重复编译; - 构建时关闭开发环境的
eslint(lintOnSave: false),写代码时用IDE检查就够了。
注意:优化构建时长时,优先做能直接降低内存和体积的操作,比如关Source Map、按需引入,这些是“一举三得”的事。别一上来就配置多进程,多进程虽然加快了速度,但也可能增加内存压力,在CI这种资源受限的环境里反而可能崩。
6. 复盘:一份JEECG前端构建问题速查表
6.1 问题速查表格
下面这个表格是我在维护JEECG前端项目时整理出来的,基本上遇到构建问题,先对照这张表排查,能省很多时间。
| 现象 | 可能原因 | 快速解决办法 |
|---|---|---|
JavaScript heap out of memory | Node默认堆内存不足 | 设置NODE_OPTIONS=--max_old_space_size=4096 |
安装依赖时node-sass报错 | 二进制下载失败或Node版本不匹配 | 换镜像源,或换成sass |
构建时openssl相关错误 | Node版本太高,Webpack 4不兼容 | 切Node 16,或加--openssl-legacy-provider |
| 构建完成但首页白屏 | CDN资源加载失败或浏览器兼容问题 | 查Network,改本地引入或换CDN |
| 刷新子页面404 | 前端history路由未配Nginx | try_files $uri $uri/ /index.html; |
| 接口全部请求失败 | .env.production环境变量错误 | 检查VUE_APP_API_BASE_URL/VITE_GLOB_API_URL |
dist体积过大 | 全量引入组件库、未开路由懒加载 | 按需引入、拆包、Gzip压缩 |
这个表格不一定覆盖你遇到的所有问题,但大部分JEECG前端构建的常见场景,基本都能在这里找到方向。
6.2 我最常用的一条构建命令和操作习惯
说点个人习惯。我一般不在本地直接跑大项目的build,除非就是测一下构建是否通过。真正出正式包,我建议在CI里固定Node版本、固定npm源,用npm ci来安装依赖,构建出来的产物更干净、可复现。本地和CI的环境差异,会引发很多“本地构建好的包打开没问题,CI打的包就是不行”的诡异问题。
如果你没有CI,只能在本地打包,那我建议你至少把这个命令固定好:
npm ci && npm run build:prod这里的build:prod对应的是生产环境构建脚本,如果你用的是JEECG新版,一般脚本里已经内置了NODE_OPTIONS的设置和vite build --mode production。
还有一个小的操作习惯:构建前先把dist目录备份一下,比如mv dist dist_bak。因为构建产物是覆盖式的,如果新构建出问题,旧包还能立刻回滚。别等到线上挂了才想起来没有备份,那会儿手忙脚乱,很容易把问题越搞越大。
最后再说一点,构建脚本这块,多用npm run而不要直接敲vue-cli-service build或vite build,因为项目脚本里可能会包含一些和版本配套的配置逻辑,我见过有人直接绕过package.json里的脚本去手动构建,结果漏掉了脚本里加的参数,产物就是不对。能用项目自带的方式,就不要自己“创造”方式。