几年前接了一个别人留下的后台项目第一眼看到的就是满屏的layui标签。那会儿我对它并不熟悉甚至一度觉得这些 class 带layui-前缀的按钮像某种私有约定。可当我花了一个晚上把那套权限管理页改完第二天同事追问“你昨天是不是偷偷套模板了”时我突然意识到这门老技术确实有东西。layui 是一个经典 Web UI 组件库主打极简引入、模块化加载和开箱即用的后台组件无数企业内部系统至今仍在用它。这篇文章不聊那些照着文档敲的入门代码而是想把围绕 layui 的那些容易被忽略的机制、踩过的坑、版本沿革和真实选型建议都摊开来给正在接手这类项目或还在犹豫选型的你作一份参考。1. 不装构建工具也能上手layui 当年凭什么被后端盯上1.1 一条 link 加一行 script 就是全部准备大多数人第一次接触 layui应该都是在某个后台模板里看到一种几乎统一的引入方式link relstylesheet href/static/layui/css/layui.css script src/static/layui/layui.js/script如果项目在内网环境直接把整个 layui 目录丢进静态资源目录页面里留着这两行组件就可以开始工作了。不需要 node_modules不需要 webpack 配置甚至不需要专门的前端同学来搭一套工程。对很多后端团队来说这套接入成本低到几乎可以忽略。我见过不少企业系统到今天仍是这么部署的Java 或者 PHP 后端模板负责渲染页面骨架layui 负责提供表格、弹窗、表单控件两边互不干扰。端到端的开发流程就是“写接口 - 拼页面 - 调参数”中间省掉了一整条前端工程化链路。放在今天看也许觉得原始但在五六年以前这种模式几乎是中小团队做管理后台的标准答案。当然layui.js并不是一个把所有 JS 都塞进去的大文件。它更像一个入口真正干活的是它下面的模块目录。通过layui.use去按需加载form、table、layer等功能模块这套机制本身也很轻后端同事读一遍文档基本就能理解。1.2 默认审美不追求惊艳但求协调耐看layui 的默认样式属于“不会让你一眼惊艳但放进后台里怎么看都不突兀”的那种。按钮有自己的主色和基础圆角表格自带斑马纹和边框控制表单控件默认高度统一弹窗的遮罩层级也很收敛。对一个以“把事办了”为目标的系统来说这个审美刚刚好。你可能会问Bootstrap 当年也流行为什么不干脆用 Bootstrap我的经验是Bootstrap 更偏“打通理念”它提供的是栅格和公共类样式细节需要自己拼而 layui 更偏“业务封装”把表格、分页、弹层、日期这些具体场景直接做成了组件。尤其在权限管理、订单列表、配置编辑这类页面上layui 几乎就是照着需求画的左侧菜单配一个树形组件中间放一张带分页的表格右上角弹出编辑框。这套节奏一旦跑顺后续项目基本就是复制粘贴改改字段名和接口地址。1.3 为什么不是 Vue、React 这些现代化框架Vue 和 React 的优势没人否定但要在传统后台项目里真正落地首先得把团队的思维方式从“操作 DOM 元素”切换到“维护数据状态”。后端同事通常没有这个时间窗口来转型。更多的情况是页面由模板引擎渲染部分区域做局部刷新交互诉求其实并不复杂。这种情况下直接用 layui 的 DOM 渲染和事件绑定反而更直给点一个按钮发一个请求回来之后再刷新表格。用现代化框架解决这些问题成本未必降下来因为你还要考虑路由、状态管理、编译链路。尤其是团队里没有专职前端时把简单问题复杂化才是最大的坑。所以不装构建工具也能上手这句话背后其实是 layui 最核心的竞争力它把专业前端和普通后端之间的差距用一套“拿来就能用”的组件库抹平了。2. 模块加载机制use、all.js 和那些“全局”的真相2.1 layui.use 回调里到底发生了什么新手在第一周最容易犯的错是在layui.use回调外面直接使用模块方法。比如layui.use([table], function () { // ... }); table.render({/* ... */}); // 报错table is not defined我第一次接触时也栽过这个跟头后来才明白原因layui.use内部做的是异步加载它要先找到对应的模块文件等文件加载完成后才执行回调。回调外面的代码是同步执行的执行顺序和作用域都对不上。规范写法是这样的layui.use([form, table, layer], function () { var form layui.form; var table layui.table; var layer layui.layer; // 所有初始化代码统一写在这里 table.render({/* ... */}); form.render(); });其实layui.use并不神秘你可以把它理解成一种轻量的依赖加载器模块文件存在就直接用不存在就按路径请求请求完成后统一执行回调。如果回调里真的需要操作 DOM可以再通过layui.use包一层它本身也会等待当前页面 DOM 就绪。2.2 all.js 和按需加载怎么选layui 目录里有一个特殊文件叫layui.all.js它一次性包含所有内置模块加载完成之后可以直接用layui.table、layui.form这类全局对象。如果你彻底不想用模块化异步加载的写法可以引入这个文件script src/static/layui/layui.all.js/script这两种方式的取舍其实不难我用一个表格给你总结引入方式优点缺点适用场景layui.jslayui.use按需加载首屏快有异步回调节点逻辑稍绕页面较多、依赖管理较复杂的系统layui.all.js同步、简单、写起来爽文件体积更大后台小项目、快速原型、离线部署我自己的习惯是原型和演示项目直接用all.js正式老系统的重构尽量用layui.use保持性能。因为后台页面一多每个页面都塞进去整包模块加载压力是不可忽略的。2.3 全局变量与命名空间还有一个常见误解是“layer这个变量是不是全局的可以直接用”。严格来说layui 把所有能力都挂在了layui这个命名空间下你通过layui.use拿到的layer、table其实是layui.layer、layui.table的引用。所以我在项目里通常只信任一种写法var layer layui.layer; var table layui.table;把需要的模块变量统一在回调头部声明出来既清晰又不容易跟其它库冲突。有些老模板里会直接写全局layer.msg那多半是单独引过layer.js文件属于另一种使用方式。新人接手时建议先看页面引的是layui.js还是layer.js避免在后面调试时莫名其妙找不到函数。3. 后台上最常用的四个模块layer、form、table、laydate 逐个盘3.1 layer后台弹窗这门手艺的精髓layer 在 layui 全家桶里地位很特殊它几乎是系统里出现频率最高的模块。基础的用法就三类// 普通提示 layer.msg(保存成功, {icon: 1}); // 确认框 layer.confirm(确定要删除这条记录吗, {icon: 3}, function (index) { layer.close(index); // 之后执行删除逻辑 }); // 带 iframe 的编辑弹窗 layer.open({ type: 2, title: 编辑用户, area: [680px, 460px], content: /views/user/edit.html?id id });type: 2是我用得最多的模式它表示弹窗内容是一个独立 iframe 页面。这个设计的价值在于业务复杂的编辑页可以完全独立维护不受父页面 JS 污染。弹窗里的子页面如果想要关闭自己最常见的做法是var index parent.layer.getFrameIndex(window.name); parent.layer.close(index);这里的关键是window.name。它在 iframe 弹窗里会被自动填成当前窗口的索引值父页面再用getFrameIndex解析出来就能精确关闭对应弹窗。这个技巧很多刚用 layer 的同学不知道经常绕一大圈去改父页面的全局变量。3.2 form不是表单组件库是“表单渲染器”很多人以为 form 模块只是做表单验证的其实它更重要的职责是“渲染”。先看一个最典型的场景你在页面里写了一个原生select但发现它显示出来并不是 layui 那种带搜索框和样式化的下拉效果。原因很简单页面至少要写成这样div classlayui-form select namestatus option value0禁用/option option value1启用/option /select /div然后在 JavaScript 里调用一次form.render(select);这个方法会扫描classlayui-form范围内的表单元素把原生控件升级成 layui 的皮肤和交互。它内部还支持直接监听 select 的变更事件form.on(select(statusFilter), function (data) { console.log(data.value); });statusFilter是select上的lay-filter属性值。用这个写联动逻辑非常顺手比如选择不同分类时重新加载表格数据。但这里有个前提如果是动态往 DOM 里塞进去的 select必须在插入 HTML 之后再次调用form.render(select)否则新节点只是原生控件事件也不会触发。这个坑我在后面第 4 部分专门展开。3.3 table数据表格的真正核心是配置而不是写 HTMLtable 模块大概是 layui 里最能体现“后端友好”的地方。你不需要自己拼接表格头、遍历数据、处理分页只需要给一个配置对象table.render({ elem: #userTable, url: /api/users, method: post, page: true, cols: [[ { field: id, title: ID, width: 80 }, { field: name, title: 姓名 }, { title: 操作, toolbar: #userTableBar } ]], parseData: function (res) { return { code: res.code, msg: res.msg, count: res.data.total, data: res.data.list }; } });parseData是我特别想让新手注意的配置项。layui 表格的默认数据格式是{ code: 0, msg: , count: 100, data: [] }但很多后端接口返回的字段可能叫total、list这时候不要硬凑默认格式直接用parseData做一次映射就好。这个回调在每个页面几乎都会用到算是最实用的配置之一。操作列的事件通过table.on来绑定table.on(tool(userTable), function (obj) { var data obj.data; if (obj.event edit) { layer.open({/* 编辑弹窗 */}); } else if (obj.event delete) { layer.confirm(确认删除, function (index) { obj.del(); // 删除当前行 layer.close(index); }); } });这里userTable对应table.render里的id。注意id和elem不是一回事elem是页面里表格的容器选择器id是给这个表格命名的逻辑标识后续table.reload全靠它。3.4 laydate日期选择的范围限制日期选择器在后台系统里出现频率非常高laydate 的用法也不复杂laydate.render({ elem: #dateRange, range: ~, // 开启区间选择 format: yyyy-MM-dd, done: function (value, date, endDate) { // value 形如 2024-01-01 ~ 2024-01-31 table.reload(userTable, { where: { dateRange: value } }); } });done回调在用户选完日期后触发。我习惯在这里直接触发表格刷新比如按日期范围筛选订单。想限制可选范围时用min和max两个字段传0表示今天传具体日期字符串表示区间边界。还有一个细节laydate 渲染完成的日期框可能还是会允许手输。后台上线后如果有用户乱填格式接口就会挂。所以我一般会在后端做兜底校验前端尽量只负责体验不把格式安全寄托在控件上。4. 动态内容与重载问题三个坑我花了两天才填平4.1 下拉框“渲染了但事件没反应”的排查链路这个坑几乎每个用 layui 的人都会踩一遍。场景是这样的页面上有一个“新增用户”弹窗里面有一个type下拉框需要根据接口返回动态塞入选项。代码大概长这样// 假设已经拿到了接口返回的 list var html option value请选择/option; list.forEach(function (item) { html option value item.id item.name /option; }); $(#userType).html(html);然后你满怀信心地打开弹窗发现下拉框确实有数据但样式完全变了form.on(select)监听也像死了一样。用浏览器开发者工具看 DOMoption 都在只是外面少了 layui 生成的一层定位元素。根因很明确动态插入选项之后没有重新渲染。正确做法是在塞完 HTML 后随手补一句form.render(select);如果新增的是整个表单片段直接form.render()也行。这个操作的本质是让 form 模块重新扫描 DOM把新出现的控件纳入管理。记住一个规律几乎可以避开 80% 的动态内容问题任何在页面初始化之后新增的表单元素都需要补一次 render。4.2 table.reload 后搜索状态乱掉的真正原因表格搜索是另一个高频场景。界面上一个搜索框、一个查询按钮点击查询时期望是带上关键词重新加载表格第一页。很多人第一版代码是这样写的table.reload(userTable, { where: { keyword: keywordVal } });看起来没毛病但实测时会发现如果当前停留在第 5 页点击搜索后请求发出的参数虽然带了keyword但page还是 5而第 5 页很可能根本没有匹配数据。于是用户看到的结果是“搜索后是空白页”第一反应是系统坏了。这个问题的本质是table.reload不会主动重置页码。分页状态是保留的如果你希望搜索后回到第一页必须显式声明table.reload(userTable, { where: { keyword: keywordVal }, page: { curr: 1 } });就这么一行很多资深同事也不一定记得。另一个小习惯是搜索条件变化时最好把之前的选中行状态一并清掉否则表格数据已经刷新选中状态还留在旧行上会给人一种“残留”的错觉。后端同学接这个接口时也要记住表格分页提交的参数名是page和limit搜索条件通过where里的自定义字段透传。调试时直接在浏览器开发者工具里看请求参数往往一眼就能定位是前端传错还是后端取错。4.3 双层弹窗的索引混乱一层弹窗套一层弹窗第三类坑在权限管理、多级配置这类页面里很常见。流程是先打开一个编辑弹窗编辑弹窗内部又弹出一个确认框或选择框。表面现象是关闭外层弹窗后屏幕上可能残留了一层遮罩或者里面那层窗口怎么也关不掉。我最初排查时以为是指定层级的问题后来理清楚才发现是索引管理的问题。每一个弹窗都有自己独立的index如果你在回调里总是写layer.close(index)但index已经被外层覆盖了就会关错窗口。正确的做法是分别记录每一层的索引并且在关闭外层时主动清理内层var innerIndex; var outerIndex layer.open({ type: 2, title: 编辑配置, area: [800px, 600px], content: /config/edit?id id, end: function () { // 外层关闭时确保内层也跟着关掉 if (innerIndex) { layer.close(innerIndex); } } });如果内层是一个纯页面层的弹窗type: 1还要注意给它设置唯一 ID 或维护在局部变量里避免重复点击时生成多个实例。这里我给一句实用建议不要图省事全程用layer.closeAll()。它会一次性把所有弹窗全关掉表面上解决了问题实际上会破坏父级弹窗的交互状态用户回来一看表单内容丢了。这三个坑串起来其实就是一句话layui 的动态能力靠的是“重新渲染”和“精确索引”记不住这两个关键词后面就会反复在细节上折腾。5. 从“停止维护”到恢复迭代冷静聊聊 layui 的现状5.1 一场让老用户集体慌了的“暂停更新”风波2021 年的时候layui 的作者因为个人原因宣布项目暂停维护。消息一出来后台开发圈里直接炸了。很多公司连夜开会评估这套系统里还有几十个页面用着 layui到底要不要全部重写那时候我也在维护两个上了年头的后台项目收到消息第一反应是把当前使用的 layui 版本和文档一并存档到内网。毕竟对于企业系统来说框架是否继续更新虽重要但更重要的是已跑起来的代码是否可维护。好在冷静下来后发现一个成熟稳定的 UI 组件库就算停在某个版本只要浏览器还在、浏览器兼容性没有被打破它就能继续服役很多年。5.2 恢复迭代之后的变化与兼容路线后来作者又恢复更新并发布了新的大版本系列对应 2.8 以后。这一轮更新有几个值得注意的变化。首先是引入了 CSS 变量体系改主题不再靠写一堆覆盖样式换肤这件事变得舒服多了。其次是整体视觉细节做了现代化调整边框、阴影、间距看起来更平顺但核心 API 没有大刀阔斧地翻新。这对存量项目是非常友好的节奏。很多老框架一旦大版本升级就是兼容地狱layui 这里明显是想在“保持老用户不迁移成本过高”和“逐步跟上时代审美”之间找平衡。我的体验是新版本可以直接拿来替换旧文件只需要重点回归测试弹层、表格、表单这三类高频组件其他组件基本没动。5.3 老项目还在跑这是不得不承认的事实我现在仍能接到不少以 layui 为底座的管理系统维护需求。有的是内部运营平台有的是某行业软件的控制面板。它们的共同点是业务稳定界面朴素用户习惯了不想动。这种情况下与其纠结“要不要换框架”不如先把当前系统的依赖版本、自定义扩展模块、常见踩坑点全部记录成内部文档。等到真有重构需求时再考虑局部页面渐进替换而不是一把梭。关于 layui 的那点事如果不了解上面这些模块设计和坑位接手一个数年前的项目时第一眼头晕都很正常。6. 要不要在新项目里继续选 layui我的个人判断6.1 仍然适合选 layui 的几种场景场景不是“越先进越好”而是“越匹配越好”。我在下面这些场景里还会继续选 layui团队只有后端开发没有专职前端。系统形态是内部管理后台交互密度不高。需要快速交付原型或给客户演示工期紧。部署环境封闭希望直接丢静态文件就能跑。这类项目的共同点是“图省事、要稳定”。你不需要搞一套完整的前端工程来确定团队规范也没有频繁复杂的交互要设计。layui 的引入方式和组件形态天然贴合这种状态。6.2 需要绕开 layui 的场景反过来如果遇到下面这些情况我就不推荐继续选 layui页面需要大量实时交互和复杂状态联动。面向 C 端用户的品牌官网或高自定义视觉项目。移动端体验优先需要完整的响应式和手势支持。团队已经有成熟的 Vue 或 React 工程实践不要为了省事而叠加另一套技术栈。在现代前端工程里组件通信、状态管理、路由、懒加载是一套体系。layui 在这里能提供的支持比较有限硬要缝合反而会拉低整体开发效率。6.3 如果新旧混用怎么过渡才不踩雷很多老系统面临一个现实问题不能一刀切重写但新功能想用现代框架做。我的建议是优先用 iframe 做隔离。把老页面包在旧技术栈里新页面用新框架生成通过 iframe 拼接在一个整体布局下。这样两边互不干扰也不用处理复杂的共享状态。反过来如果把 layui 页面强行塞进 Vue 项目里同页面混合渲染很容易出现样式污染和 DOM 覆盖问题。我见过不少团队因为混用导致主框架状态被破坏最后不得不回滚。所以除非你有非常足够的把握否则尽量在物理层面做拆分。这几年我接手的后台项目几乎没有哪个没用过 layui。坦率地说新项目我会更慎重地选择框架但在那些“先把活儿干完”的场合我依然会打开 layui 的文档把那两行引入代码原样贴下来。就像一位老同事回来聊起当年那些事它也许不是最潮的选择却是最顺手的答案。
阅读完成 · 觉得有帮助?