Guns脚手架页面制作与ECharts图表接入全链路避坑

发布时间:2026/11/3 22:53:00
Guns脚手架页面制作与ECharts图表接入全链路避坑 1. Guns 脚手架里“页面”到底该放在哪一层接手这套 Guns 后台脚手架做页面制作最容易犯的错误是文件放错位置。我见过不止一个刚上手的人把页面写得漂漂亮亮样式和脚本也都调通了结果一刷新就 404或者模板变量原样输出在页面上然后就卡在那里怀疑框架有问题。实际上问题几乎都出在“不知道该放哪儿”这件事上Guns 是 Spring Boot 加 Beetl 模板引擎的组合它把页面模板、静态资源、公共片段这三类东西分得很清楚位置放对了后面写 HTML、引 jQuery、挂 Bootstrap、接图表都是顺的位置放错了你会花一整天在路径上打转。这一节我想先把这个“空间坐标”讲明白因为它决定了后面所有页面制作和图表制作能不能跑起来。你可以把 Guns 理解成一个已经搭好承重墙的房子它给了你登录、权限、菜单、代码生成这些基础设施但房间内部怎么装修得你自己动手。页面制作就是装修jQuery 和 Bootstrap 是你的工具图表是挂在墙上的画。工具和画都放对位置房子才好看。1.1 一次目录穿梭从控制器返回值到浏览器拿到的 HTML我习惯在动手写页面前先在脑子里把一次请求的完整链路走一遍。假设我要做一个设备台账列表页链路大概是这样浏览器请求/device→ 控制器方法返回/device/device.html这个视图名 → Beetl 去templates目录下找对应文件 → 模板渲染时把${}里的变量替换掉、把公共片段 include 进来 → 生成最终 HTML 返回给浏览器 → 浏览器再根据 HTML 里的script、link标签去请求静态资源 → 静态资源由 Spring Boot 从static目录直接提供。对应的物理目录大致是src/main/resources ├── templates ← Beetl 模板返回给浏览器的骨架 │ ├── common │ │ ├── _container.html ← 页面公共外壳头部、左侧菜单、底部 │ │ └── _taglib.html ← 标签与公共变量 │ └── device │ └── device.html ← 设备台账页面本体 └── static ← 静态资源浏览器直接下载 ├── css ├── js │ └── plugins └── modular ← 按业务模块分目录的自定义脚本 └── device └── device.js这个结构里最关键的一条认知是templates下的文件不会被浏览器直接访问它只由服务端渲染static下的文件才会被浏览器直接请求。所以你在模板里写脚本逻辑和在static/modular下写脚本是两件完全不同的事。我的经验是模板里只放极少量初始化参数比如当前用户 ID、上下文路径真正的业务逻辑一律写到static/modular/模块名/xxx.js里。原因是模板文件不好做版本管理也容易被后端改动波及而独立 JS 文件可以加版本号、可以缓存、可以单独排查。1.2 模板、静态资源、公共片段的三处约定把三处约定记住能省掉大量试错类型位置访问方式常见错误页面模板templates/模块/页面.html只能由控制器视图名返回直接敲 URL 访问模板路径公共外壳templates/common/_container.html用 Beetl 的 layout 指令引入每个页面复制一遍头部导致改一处漏十处业务脚本static/modular/模块/页面.jsscript src${ctxPath}/modular/...忘写上下文路径第三方库static/js/plugins/同上版本混用Bootstrap 和 jQuery 版本对不上这里有个小细节我在多个项目里都吃过同样的亏静态资源路径一定要带上下文路径变量。Guns 里通常有类似${ctxPath}的变量可用如果团队的应用不是部署在根路径下而你写死了/static/js/xxx.js本地跑得好好的一上测试环境全部 404。我现在的习惯是项目一开始就在公共片段里定义一个全局 JS 变量把上下文路径吐出去之后所有脚本里的接口请求、图片路径都用它拼script var ctx ${ctxPath}; /script然后在业务脚本里统一用ctx /device/list这种写法。这样即使将来部署路径调整也只改一个地方。1.3 版本差异带来的第一个坑Bootstrap 是 3 还是 4写页面前先花两分钟确认 Bootstrap 版本这一步能避免后面一半的样式问题。Bootstrap 3 和 4/5 之间的类名、栅格行为、模态框属性都有差异混着写就会出现“样式像又不像”的诡异状态。我遇到过最典型的一次一个兄弟照着旧文档写div classcol-xs-6但项目里实际是 Bootstrap 4col-xs-*早就没有了结果是两个 div 上下叠着排他调了半天以为是浮动清除的问题。判断版本其实很快看引入的 CSS 文件名或者打开控制台输入$.fn.modal.Constructor.VERSION看一眼就知道了。我一般会在项目笔记里固定记一行本项目 Bootstrap 4.6 / jQuery 3.5之后所有代码片段都按这个版本来不再去网上随手复制。版本确认之后还有一个连带问题Bootstrap 的 JS 组件依赖 jQuery顺序必须先是 jQuery 再是 Bootstrap 的 JS否则模态框、下拉菜单全都不工作控制台报bootstrap is not defined或者$(...).modal is not a function。这个顺序我建议直接写死在公共片段里不要每个页面自己引一遍引乱了就是给自己找麻烦。2. 页面制作先搭骨架再填肉Bootstrap 栅格与组件的落地顺序我见过很多页面制作的流程是“边写边调”先写一个输入框觉得位置不对加个 margin再写一个按钮觉得不齐再加个 float。这样写出来的页面第一版看着还行第二版加两个查询条件就全乱了因为整个结构没有骨架全靠零散的样式撑着。我现在固定用一个顺序先用栅格划区域再往区域里塞组件最后才做细节美化。这个顺序看起来慢实际上省时间因为你改需求的时候改的是“区域”不是一堆散落的 div。2.1 为什么我坚持先用栅格画“灰盒子”画灰盒子的意思是先不写任何业务内容只用带背景色的空 div 把页面的分区确定下来顶部是查询条件区中间是数据表格区底部是分页右上角是操作按钮组。这一步做完你会立刻发现几个问题查询条件放几列合适表格区要不要横向滚动按钮和搜索框在窄屏下会不会挤在一起用 Bootstrap 的栅格系统来做这件事特别顺手因为它本身就是 12 列等分。我常用的布局是查询条件一行放 4 个字段每个占 3 列操作按钮单独占一行靠右表格区独占 12 列。这样在 1366 宽度的常见办公屏幕上刚好铺满不会出现一行只有两个字段、右边大片空白的浪费。!-- 查询条件区 -- div classrow div classcol-md-3 div classform-group label设备编号/label input typetext classform-control iddeviceNo /div /div div classcol-md-3 div classform-group label设备状态/label select classform-control idstatus option value全部/option option value1在用/option option value0停用/option /select /div /div div classcol-md-6 text-right button classbtn btn-primary idbtnSearch查询/button button classbtn btn-default idbtnReset重置/button /div /div这里有个实践中的取舍查询条件超过 6 个的时候一行 4 个就太挤了我一般改成一行 3 个col-md-4或者干脆把次要条件折叠起来点“更多条件”再展开。折叠这个交互看着小实际上对使用者很友好因为大部分查询只用一两个条件。2.2 查询条件区、表格区、按钮区的三段式模板三段式是我在后台页面里用得最多的一种布局几乎所有列表页都能套。它的好处是使用者形成了肌肉记忆上面查中间看下面翻页右边点操作。我把它整理成了一个可以复制粘贴的骨架div classwrapper wrapper-content !-- 第一段查询条件 -- div classibox float-e-margins div classibox-content !-- 上面那段 row 放在这里 -- /div /div !-- 第二段数据表格 工具条 -- div classibox float-e-margins div classibox-title div classbtn-group button classbtn btn-primary btn-sm idbtnAdd新增/button button classbtn btn-danger btn-sm idbtnDelete批量删除/button /div /div div classibox-content table iddeviceTable/table /div /div /div表格我一般用 bootstrap-table它跟 Bootstrap 的样式天然一致分页、排序、列显示隐藏这些功能开箱就有配置项写起来也直白。选它而不是自己手写一个table加循环理由很实际后台系统里表格是最核心的信息载体一旦自己造分页联动、列宽拖拽、固定表头、导出这些需求会一个一个找上门最后你会发现自己写了个半成品插件。2.3 模态框里的表单一份结构两处复用新增和编辑这两个操作表单字段通常是完全一样的区别只在于编辑时要回填数据。很多人会写两个模态框结果字段一改要改两处早晚漏一处。我的做法是只写一个模态框用一个隐藏字段区分当前是新增还是编辑div classmodal fade iddeviceModal tabindex-1 div classmodal-dialog div classmodal-content div classmodal-header h4 classmodal-title idmodalTitle新增设备/h4 /div div classmodal-body input typehidden iddeviceId div classform-group label设备编号 span classtext-danger*/span/label input typetext classform-control ideditDeviceNo /div !-- 其余字段... -- /div div classmodal-footer button classbtn btn-default>// 绑在不会变的父容器上动态行也能触发 $(#deviceTable).on(click, .btn-edit, function () { var row $(this).closest(tr); var id $(this).data(id); openEditModal(id); });$(this).data(id)取的是渲染时写在按钮上的>function getFormData($form) { var obj {}; $.each($form.serializeArray(), function (i, item) { obj[item.name] item.value; }); return obj; }这个函数看着简单但它让我在提交前能方便地补字段、做校验、做类型转换比字符串拼接灵活得多。3.3 一层薄封装把 ajax 的重复代码收掉一个后台系统几十个页面每个页面都有增删改查四个请求如果不封装你会写几十遍$.ajax({ type:POST, dataType:json, success:..., error:... })。重复本身不是最大的问题最大的问题是错误处理的写法不统一这个页面弹 alert那个页面静默失败还有一个直接把异常打到控制台使用者根本不知道发生了什么。我在项目里固定放一个公共的请求函数function request(url, data, onSuccess, method) { $.ajax({ url: ctx url, type: method || POST, contentType: application/json; charsetutf-8, data: data ? JSON.stringify(data) : null, dataType: json, success: function (res) { if (res.code 200) { onSuccess onSuccess(res.data); } else { layer.msg(res.message || 操作失败); } }, error: function (xhr) { if (xhr.status 401) { layer.msg(登录已失效请重新登录); return; } layer.msg(网络异常状态码 xhr.status); } }); }这层封装带来的收益在后期特别明显要加请求头、要加 loading、要统一处理未登录都只改这一处。我个人的习惯是封装的返回值结构跟后端约定死code / message / data三段团队里所有人都按这个来不要今天返回{success:true}明天返回{ret:0}那就是给自己埋雷。3.4 Beetl 和 jQuery、原生 JS 的语法撞车这是 Guns 这套组合里最容易被忽略的一个坑也是我踩过之后印象最深的。Beetl 的取值占位符是${...}而现代 JS 的模板字符串也是${...}。你在模板文件里写一段 ES6 模板字符串Beetl 会在服务端先把它当变量解析掉结果要么报错要么输出一堆乱码要么静默变成空字符串。我第一次遇到时盯着浏览器控制台看了很久因为报错信息指向的位置跟实际原因完全对不上。规避方式有这么几种我按推荐程度排方式写法适用场景业务逻辑外移把 JS 写到static/modular下首选最干净字符串拼接共 n 条少量拼接转义占位符具体写法依版本而定需实测不得已在模板里写我现在基本只走第一种。模板里除了必要的初始化变量一行业务 JS 都不写。这样还有个附加好处JS 文件可以被浏览器缓存页面刷新不用重新下载。原生 JS 这边还有一个小细节在 Beetl 模板里写和一般没问题但如果写在script里又恰好遇到 Beetl 的定界符就需要留意。稳妥做法还是外移。4. 图表制作ECharts 接进 Guns 页面的完整链路图表制作是这次任务里我觉得最有意思的部分。后台系统里图表的价值不在于好看而在于让人一眼看出趋势和异常——一堆数字排成表格没人看画成折线立刻能发现某天掉了个坑。我一般用 ECharts原因是它对中文场景支持好、配置项直观、文档里的例子基本可以直接改着用。但把图表塞进 Guns 的页面里并不是引一个 JS 然后 init 一下那么简单。我总结下来有三个必须处理的环节容器、时机、数据。下面一个一个说。4.1 容器高度、初始化时机与“图表不显示”的前三个原因图表不显示九成以上是这三个原因之一按出现频率排第一个容器没有高度。ECharts 需要知道画多大如果父容器高度是 0比如放在一个还没有内容的 div 里或者用了height: auto而内部又没有内容撑开它算出来的画布尺寸就是 0看起来就是一片空白。解决办法很朴素给容器一个明确的高度。div idtrendChart stylewidth: 100%; height: 360px;/div我一般不用内联样式而是抽一个类统一管理方便不同页面配置不同高度。第二个初始化时容器还没进 DOM 或者还被隐藏。典型场景是图表放在标签页里用户切到那个标签页时才可见。如果在页面加载时就 init容器宽度是 0图表会挤成一团。处理方式是切到该标签页时再 init或者 init 之后立刻调一次resize()。第三个脚本执行时 ECharts 还没加载完。脚本标签的顺序很重要业务脚本必须在 echarts 的script之后。我见过把业务脚本放在head里、ECharts 放在/body前的写法那必然报echarts is not defined。4.2 后端返回结构到 setOption 的映射图表的数据一般来自一个接口。我不建议后端直接返回 ECharts 的 option 结构那样前后端耦合太死后端要改一个颜色都得动 Java 代码。我的做法是后端只返回干净的数据前端自己组装 option。约定一个简单的返回结构{ code: 200, data: { categories: [周一, 周二, 周三, 周四, 周五], series: [ { name: 入库, values: [12, 18, 9, 22, 15] }, { name: 出库, values: [8, 11, 14, 9, 20] } ] } }前端组装function renderTrend(data) { var option { tooltip: { trigger: axis }, legend: { data: data.series.map(function (s) { return s.name; }) }, grid: { left: 40, right: 20, top: 40, bottom: 30 }, xAxis: { type: category, data: data.categories }, yAxis: { type: value }, series: data.series.map(function (s) { return { name: s.name, type: line, smooth: true, data: s.values }; }) }; trendChart.setOption(option, true); }这里setOption的第二个参数传true表示不合并、完全替换。为什么不合并因为当你的图表类型从折线切成柱状或者系列数量变少的时候合并模式会把旧系列的残留配置留着出现“数据已经变了但图上还有一条线”的诡异现象。全量替换虽然稍微多一点点开销但行为可预测我认为值得。4.3 resize、dispose 与重复渲染浏览器窗口一拖动图表不会自动跟着变宽这是新手最容易忽略的一点。加上这一行就好window.addEventListener(resize, function () { trendChart trendChart.resize(); });但要注意别写反方向的问题——如果页面里图表很多每个都挂一个 resize 监听拖窗口的时候会卡。我的做法是在页面级只挂一个监听统一遍历页面上的图表实例调 resize。图表数量超过三四个的时候这个优化能明显感觉到差别。另一个容易忽略的是dispose()。当你的页面是标签页结构、每次切回来都重新 init 一次如果不先 dispose旧的实例还挂在 DOM 上内存会一点一点涨同时可能出现两个画布叠在一起的情况。正确的流程是if (trendChart) { trendChart.dispose(); } trendChart echarts.init(document.getElementById(trendChart));我自己的习惯是写一个小的图表管理器把实例存在一个对象里页面切换或者弹窗关闭时统一清理。这个习惯是从一次内存占用越用越高的问题之后养成的。4.4 空数据、单位、按需引入这几个细节空数据要兜底。接口偶尔会返回空数组这时图表画出来是一片只剩坐标轴的空白使用者会以为系统坏了。我一般判断一下数据为空就显示一个“暂无数据”的提示比空白友好得多。单位要标清楚。纵轴只写数字不写单位是图表里最影响可读性的问题。yAxis上加个name: 件或者用axisLabel的格式化函数加后缀成本很低效果很直接。按需引入。如果项目对首屏加载有要求ECharts 支持只打包用到的图表类型和组件。后台系统一般整包引入也不至于太慢但如果首页就有三四个图表按需引入能把首次加载的脚本体积压下来不少。这一步属于优化项先把功能跑通再做。颜色别用默认五色。默认配色放到正式系统里往往太艳。我通常从公司已有的视觉规范里取三到五个色值配一套color数组所有图表统一用。统一配色这件事看起来是审美问题实际上是认知负担问题——同一个指标在所有图里颜色一致看的人才能快速对上号。5. 排查链路实录404、样式错乱与图表空白这一节我想把几个真实排查过程完整写出来因为排错这件事光看结论没用得看过程。下面三段都是我自己遇到过的问题我把当时的判断顺序原样还原。5.1 一次静态资源 404 的完整定位过程现象页面能出来但样式全丢控制台一堆 404。我的排查顺序是这样的先看 404 的是哪个 URL。控制台里那行红色请求完整路径是什么。这一步就能区分是路径写错了还是文件真的不存在。对照本地目录。请求的是/static/css/xxx.css而文件实际在src/main/resources/static/css/xxx.css路径是对的。那问题在别处。看上下文路径。应用部署在/admin下而页面里引的是/static/...少了前缀。这就是 1.2 里提到的那个坑。改完再刷还是 404。这时候要考虑是不是浏览器缓存了旧的 404强刷一次。强刷之后还是不行。最后发现是打包时静态资源没被包含进去pom.xml里的资源过滤配置漏了某些后缀。整个链条走下来其实每一环都很快慢在“没想到”。所以我现在会先做完前面两步再动手改不瞎试。提示Spring Boot 里静态资源默认映射到/static、/public、/resources这些目录如果你自己加了拦截器或者安全框架要确认这些路径在放行名单里否则会返回 302 或 401 而不是 404看起来更像“文件丢了”。5.2 样式错乱的三个高频来源样式问题看着玄学其实来源就那几个一是 CSS 加载顺序。自定的业务 CSS 必须放在 Bootstrap 之后否则同优先级的规则会被框架覆盖。这个错误特别容易在复制别人的页面时发生。二是类名拼错或者版本不匹配。col-xs-6在 Bootstrap 4 里不存在pull-right在 Bootstrap 4 里改成了float-right。这类问题没有报错只是不生效只能靠对照版本文档。三是选择器权重打架。你自己写的.form-group label被框架的.form-group label盖住了看样式面板能看到被划掉的那条规则顺着往上找就能定位。我的习惯是样式不对劲的时候先按 F12 看元素实际生效的规则谁盖了谁一目了然比对着代码猜快十倍。5.3 图表空白和“数据不对”要分开查这两个问题经常被混为一谈实际上排查方向完全不同。图空白—— 看 DOM 里有没有 canvas 元素。有 canvas 但没内容多半是 option 结构不对或者数据为空完全没 canvas就是 init 没执行或者容器尺寸为 0。图有内容但数据不对—— 去 Network 面板看接口实际返回了什么再在控制台把chart.getOption()打出来对比。九成的情况是前端映射时字段名写错了或者map的时候索引错位。我给自己定了个规矩排查图表问题先看数据再看渲染。因为数据错会导致图看着“怪”而渲染错会导致图“没有”两者的处理动作完全相反。6. 把页面做“耐改”公共片段、按页加载与命名规范页面做到能跑只是及格线真正决定这套东西好不好用的是三个月后加需求的时候。我参与过几次接手别人页面的活儿最大的感受是写得乱的项目不是不能跑而是每改一次都心惊胆战怕改一处崩三处。所以这一节聊聊怎么把页面做得“耐改”。6.1 公共 JS 和 CSS 的抽取边界抽取公共代码这件事抽多了和抽少了都难受。抽少了到处重复抽多了牵一发动全身。我自己的边界是这样划的该抽的请求封装、日期格式化、表单校验规则、弹窗提示、通用表格初始化配置。这些东西几乎每个页面都用且改动会波及全局集中管理最划算。不该抽的某个业务模块内部的字段转换、某个页面特有的交互流程。这类逻辑放公共文件里用的人少看的人多最后变成了无人敢动的“祖传代码”。我一般的做法是分两层common.js放跨模块通用的工具modular/模块/common.js放模块内通用的部分。这样边界清楚删模块的时候也方便。6.2 缓存和版本号改了没生效的元凶“我明明改了 CSS怎么刷新还是旧样式”——这个问题我遇到过太多次尤其是在测试环境。原因就是浏览器缓存了旧文件。解决办法是给静态资源的引用加版本参数link relstylesheet href${ctxPath}/css/device.css?v20240717 script src${ctxPath}/modular/device/device.js?v20240717/script版本号手动维护确实有点笨但它简单可靠。有些团队会做构建时自动打 hash那是更好的方案但需要构建流程支持。在 Guns 这种后端渲染的项目里手动加参数是最实际的过渡办法。注意如果用的是 Beetl 的公共片段来统一引入静态资源版本号改一处就全站生效这个收益比每个页面单独写要大得多。6.3 交接时的目录与命名约定最后说一个容易被忽视但影响很大的事命名。我给自己定的规矩是目录名、文件名、页面标题、脚本函数前缀保持一致。设备模块就是templates/device/device.html、static/modular/device/device.js、函数前缀device。这样别人接手时看到一个函数名就知道去哪个文件找不用全局搜索。变量命名上我会把 DOM 元素和普通变量区分开const $table $(#deviceTable)前面加$一眼能看出这是个 jQuery 对象不会误当成普通变量去调原生方法。这个习惯是从一次“对 jQuery 对象调数组方法”的低级错误之后改过来的。还有一个实操建议交接前花十分钟写一份README把页面清单、接口清单、用到的第三方插件版本列出来。这份东西的价值在你离职半年后依然存在而在你刚写完的那天它是零成本的。最后分享一个我做页面制作时的小习惯每做完一个页面我会把浏览器窗口从 1920 一直拖到 1280中间停两三次看看有没有挤压、错位、按钮掉行。后台系统的实际使用环境宽度千差万别这个话题在需求评审时基本没人提但真正影响每天用的人的体验。花两分钟拖一拖窗口比事后被反馈“小屏幕上排版乱了”再回来改要省事得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询