layui表格星级评分排序实战:从字段映射到前后端方案

发布时间:2026/10/14 4:26:08
layui表格星级评分排序实战:从字段映射到前后端方案
你是不是也遇到过这种情况layui 表格里明明有评分列星星渲染出来了但一点表头排序要么纹丝不动要么排出来的顺序完全是乱的跟按分数排八竿子打不着。我在实际项目里处理过好几回这种需求——用户列表要按评分排序、课程列表按星级排序、点评列表按好评度排序。最开始我也天真地以为只要在th上挂个lay-sort就完事了结果被狠狠教育了一顿。问题在于你页面上看到的是星星但星星背后的真实值是分数、是字符串、还是对象排序依据完全不是一回事。如果不把这三层关系理顺排序永远做不对。这篇文章我就把 layui 表格中按星级评分排序这件事整个捋一遍从默认排序机制、字段存储、渲染方式、前端排序到后端排序最后再附上我实际踩过的坑和排查思路。不管你是刚接触 layui 的新手还是已经被这问题折磨过的老手看完应该都能直接把方案搬到项目里。1. 先搞清楚 layui 表格排序的基本机制1.1 表头排序的触发与参数传递layui 的 table 模块在做排序时其实靠的是表头th上的lay-sort属性。如果你写过类似下面这种代码说明已经摸到门槛了table iddemoTable lay-filterdemoTable thead tr th lay-data{field:score, sort:true}评分/th th lay-data{field:name}名称/th /tr /thead /table当用户点击带sort: true的表头时layui 会触发table.on(sort(filter)事件并且传递一个包含三个关键字段的对象table.on(sort(demoTable), function(obj){ // obj.field —— 当前排序的字段名就是 lay-data 里的 field // obj.type —— asc 升序 / desc 降序 / null 取消排序 // obj.init —— 是否初始化排序一般用不到 console.log(obj); });这是 layui 排序的基础玩法。问题来了obj.field传给你的只有那个字段名至于这个字段存的是数字、字符串还是别的类型layui 在默认情况下并不关心。它只关心你 lau-data 里写的 field 到底是什么。所以很多人会想既然排序事件能拿到 field那我直接把当前表格的数据源重新排一下不就行了理论上行但实践里藏着几个坑。1.2 为什么星级评分的排序不能直接使用默认 sort常规的文本列、数字列比如价格、时间戳你把sort: true一开layui 自带的前端排序就能工作。但星级评分不一样它有特殊性第一评分列渲染到页面的是星星图标不是数字。如果你把field直接指定为展示星星的字段这个字段存的是一个渲染后的 HTML 字符串比如★★★★☆或者一个组装好的模板字符串。拿字符串去比大小结果自然是按 Unicode 码点排的数字顺序完全被打乱。比如 9、10、8 会排成 10、8、9因为字符串比较时先比第一位字符。第二即使你存的是数字如果把templet里的原始值和展示值搞混排序时拿到的可能只是展示层的文本而不是底层的数值。星星组件渲染后原始数值早就被覆盖或者被隐藏了。第三layui 的默认前端排序是一个简单的数组排序它不会去区分你的字段是4.5还是 4.5如果从后端返回的数据里 score 是字符串4.5排序就会按字符串比较来4.5 会被排在 4.12 前面。所以核心结论是星级排序列必须把“排序依据的真实值”和“展示星星的渲染值”分离。你是用真实值排序渲染值只是给人看的。1.3 需求拆解排序依据是分数不是星星我们在做需求评审时通常会把“按星级排序”拆成两层含义层按多少颗星排序本质是按用户打的分值排序比如 5 分、4.5 分、4 分展示层在表格单元格里显示成星星图案让人一眼看出评分高低。如果需求说“点击表头评分列按高到低排序”那么排序一定发生在原始数据层面而不是在已渲染的 DOM 上做文章。因此整体方案基本是每行数据里维护一个纯数字的评分字段比如score用templet把它渲染成星星表头的sort绑定这个score字段排序时不管是前端排序还是后端排序操作的始终是score这个数字字段。明白这个基础逻辑后后面所有方案都顺了。2. 数据准备评分字段的存储与渲染2.1 两种常见的评分数据存储方式我见过很多项目里评分字段的存储方式五花八门但归纳起来就两种第一种纯数字字段。比如score: 4.5取值范围 0 到 5支持一位或两位小数。这是最理想的排序字段因为数字类型天然支持大小比较。第二种字符串或对象。比如score: 4.5或者score: {value: 4.5, count: 18}甚至有的后端把评分和评价数量拼成一个字段比如4.5分(18人)。这种数据如果直接拿来排序必出问题。对于第二种你需要和对接的后端商量要么后端额外返回一个纯数字字段比如scoreValue要么你在前端拿到数据后自己预处理一次把纯数字提取出来存到新的列字段里。我强烈建议新增一个隐藏字段也不要去改原字段的展示结构。我在模拟项目 X 里是这么处理的后端返回的数据结构是下面这样{ code: 0, data: [ {id: 1, courseName: 前端入门, rating: 4.5分(128人)}, {id: 2, courseName: 后端实战, rating: 5分(89人)} ] }如果在表格里直接对rating排序肯定不行因为里面有汉字、冒号、括号。我在前端parseData回调里做了预处理table.render({ elem: #demoTable, url: /api/course/list, parseData: function(res){ var data res.data.map(function(item){ var match item.rating.match(/^(\d(\.\d)?)/); item.score match ? parseFloat(match[1]) : 0; return item; }); return { code: res.code, msg: res.msg, count: res.count, data: data }; } });这样每行就多了个纯数字的score字段排序时直接用score展示时仍用原来的rating或者重新渲染。2.2 使用 templet 把数字渲染成星级有了数字score后下一步是把它渲染成星星。layui 自带rate组件但你没法在templet里直接调用它的 JS 方法去实例化因为templet返回的是 HTML 字符串组件实例化需要额外的 DOM 操作。所以大多数人的做法是写一个简易的模板函数生成星星 HTML。我常用的渲染函数是这样的function renderStar(score){ score Number(score) || 0; var full Math.floor(score); var half (score - full) 0.5 ? 1 : 0; var empty 5 - full - half; var html ; for(var i 0; i full; i){ html i classlayui-icon layui-icon-rate-solid stylecolor:#ffb800;/i; } if(half){ html i classlayui-icon layui-icon-rate-half stylecolor:#ffb800;/i; } for(var j 0; j empty; j){ html i classlayui-icon layui-icon-rate stylecolor:#ffb800;/i; } return html; }然后在cols配置里cols: [[ { field: courseName, title: 课程名称 }, { field: score, title: 评分, width: 160, align: center, sort: true, templet: function(d){ return renderStar(d.score) span stylemargin-left:6px;color:#999; d.score /span; }} ]]这里关键点在于field仍然是score排序时 layui 拿到的是数字templet只是影响了这个单元格展示的内容并不影响排序字段的真实值。我甚至会把分数放在星星后面一起展示这样用户既能看到星星又能看到精确分数排序结果也更直观。2.3 隐藏原始分数列但保留排序字段有一种场景是表格里不想直接展示数字分数只想展示星星但排序仍然要按分数。这时有些人会干脆建一个隐藏列或者不显示分数文本。实现方式有两种方式一仍然用score作为 fieldtemplet 里只返回星星不返回分数。表头sort: true照旧。方式二如果你需要把原始分数留给别处用但不想在表格里展示任何数字你可以在cols里单独加一个隐藏列{ field: score, title: 评分, width: 0, hide: true, sort: true }不过实际操作中我不太推荐用hide: true来隐藏排序字段因为 layui 在后续版本里隐藏列对排序事件的支持偶尔会有小问题。更稳妥的做法是直接让展示列绑定score字段templet 里只画星星。这样既没有数字露出来排序字段也一直存在。如果你用的是老版本 layui比如 2.5.xhide功能和排序组合可能会出现点击表头无反应的 bug。基于常见实践优先选择前者。3. 实现星级排序的完整方案3.1 方案一layui 自带 sort 配合纯数字字段如果你的表格数据量不大几百行以内并且数据是一次性加载到前端的内存数组里那么用 layui 自带排序是最省事的。先看完整示例代码layui.use([table, util], function(){ var table layui.table; var util layui.util; // 模拟数据 var data [ { id: 1, name: 课程A, score: 4.5 }, { id: 2, name: 课程B, score: 3.0 }, { id: 3, name: 课程C, score: 5.0 }, { id: 4, name: 课程D, score: 4.5 }, { id: 5, name: 课程E, score: 2.5 } ]; table.render({ elem: #tableDemo, id: tableDemoId, data: data, cols: [[ { field: name, title: 课程名称 }, { field: score, title: 评分, width: 180, align: center, sort: true, templet: function(d){ return renderStar(d.score); }} ]], page: false }); // 监听排序事件 table.on(sort(tableDemo), function(obj){ // 直接使用 layui 内置排序注意需要重新载入数据 table.reloadData(tableDemoId, { initSort: obj, // 让表头排序图标状态同步 data: data.slice().sort(function(a, b){ var v1 a[obj.field]; var v2 b[obj.field]; if(v1 null) v1 -1; if(v2 null) v2 -1; if(obj.type asc){ return v1 - v2; } else if(obj.type desc){ return v2 - v1; } return 0; }) }); }); });这里有两个容易忽略的细节第一个是initSort: obj。如果你不传这个参数点击表头排序后表头上的箭头状态不会更新用户会以为没排上。很多新手在这步卡住总觉得功能没生效其实数据已经变了只是图标没变。第二个是data.slice()。因为sort()是原地排序会修改原数组如果你重复点击不同字段排序原数组顺序已经被改过了再在已排序基础上排可能会出现奇怪结果。所以每次排序前先slice()复制一份保证基于原始数据排序。这种方案适合数据量小、纯前端表格的场景。我当年在做一个后台管理系统的课程列表时数据只有几十条就用这个方案几行代码就解决了。3.2 方案二完全自定义排序事件手动重载表格有些场景是你不想用 layui 内置的 sort 逻辑比如数据中 score 可能为空、可能需要根据评分人数作为次级排序、或者需要在前端做更复杂的权重计算。这时可以彻底摆脱data.sort()在sort事件里自己控制要显示的数据列表。思路是维护一个全局的原始数据源每次排序时复制原始数据按自定义规则排序后再通过table.reloadData灌回去。var allData []; // 原始数据 // 模拟从接口获取 allData [ { id: 1, name: 课程A, score: 4.5, count: 200 }, { id: 2, name: 课程B, score: 4.5, count: 100 }, { id: 3, name: 课程C, score: 3, count: 500 } ]; table.render({ elem: #tableDemo, id: tableDemoId, data: allData, // cols 配置略 }); table.on(sort(tableDemo), function(obj){ var sortedData allData.slice(); if(obj.type){ sortedData.sort(function(a, b){ // 第一排序分数 if(a.score ! b.score){ return obj.type asc ? a.score - b.score : b.score - a.score; } // 第二排序评分人数 return obj.type asc ? a.count - b.count : b.count - a.count; }); } table.reloadData(tableDemoId, { initSort: obj, data: sortedData }); });这个方案的好处是排序规则完全透明你可以任意组合字段比如先按分数再按人数甚至再按创建时间。缺点是要自己保证原始数据不被污染一旦经过其他操作比如删除行、编辑行allData也需要同步更新。我遇到过一个需求同一个评分数值下希望评价人数多的排在前面。用内置 sort 只能排一个字段但用了自定义排序后两行代码就实现了。所以如果你的业务里也有类似的“并列怎么排”的需求这个方案很适合。3.3 方案三后端排序前端只传 sort 字段数据量超过几千行时前端一次性加载就不现实了。这时候必须走后端排序。流程是表头加sort: true监听table.on(sort(...))在事件回调里重新请求数据把排序字段和排序方式带给后端后端根据参数拼接 ORDER BY返回新数据调用table.reload重新加载表格并传入initSort保持表头状态。代码大致是table.on(sort(tableDemo), function(obj){ var param { page: 1, limit: 10, sortField: obj.field, sortOrder: obj.type || }; // 请求后端接口 $.ajax({ url: /api/course/list, data: param, success: function(res){ table.reload(tableDemoId, { initSort: obj, data: res.data, page: { curr: 1 } }); } }); });后端伪代码以常见框架为例-- 前端传来 sortFieldscore, sortOrderdesc SELECT * FROM course ORDER BY score DESC LIMIT 0, 10注意两点一是obj.type可能是 null也就是用户点击的是“取消排序”这时后端应该返回默认顺序。你可以在前端判断一下var sortOrder obj.type ? (obj.type asc ? asc : desc) : ;二是分页参数。排序后第一页数据很可能和之前完全不同所以页码最好重置为 1否则用户可能在第二页点排序结果还停留在第二页页面数据是排序后的第二页体验很别扭。我实际项目中更多的还是这种后端排序方案。因为业务表格几乎都会配分页前端排序只能排当前页数据一多就暴露问题。星级评分的排序字段在后端就是普通数值字段处理起来非常简单重点反而在前端如何正确传递obj.field和obj.type。3.4 三种方案怎么选决策建议我根据自己的项目经验整理一个简单的选型参考场景推荐方案原因数据量小几百行内一次性加载方案一内置 sort reloadData代码少维护简单无额外请求数据量中等需要并列次排序方案二自定义排序事件可自由控制排序规则效果直观数据量大、有分页、服务端渲染方案三后端排序排序的是全量数据性能最好最严谨表格里的评分同时用在其他联动场景方案二或三避免前端排序破坏原数据引用如果你实在拿不准就直接选方案三。后端排序是最不容易出错的也不会因为浏览器端数据不一致导致排序错乱。唯一增加的工作量就是后端接口加一个ORDER BY参数性价比很高。4. 实战踩坑与问题排查4.1 点击表头排序没反应先检查这几处排序功能看似简单但实际项目中我遇到最多的问题就是“点了没反应”。排查看下来90% 集中在下面几个原因第一lay-filter和监听名称对不上。表格渲染时elem对应 HTML 里的id但table.on(sort(filterName))里的filterName是lay-filter的值不是elem的 id。写错一个字母事件永远不触发。table iddemoTable lay-filterdemoTable/tabletable.render({ elem: #demoTable, ... }); // 这里的 demoTable 是 lay-filter 的值 table.on(sort(demoTable), function(obj){ ... });第二表头lay-data里没写sort: true。你只在 JS 配置里写sort: true但 HTML 表头里用的是lay-data两者都得有。我一开始用纯 JS 配置 cols 时会忘记写所以经常需要在表头 HTML 和 JS 配置之间反复检查。第三使用了自定义templet的列但field名称写错。排序监听的obj.field是根据 cols 里的field回来的如果你写的是score但数据里字段是scoreValue排序时比较的值就是 undefined自然没反应或者排成乱序。建议排查顺序先在table.on(sort)回调里打一个console.log(obj)看点击表头后有没有触发。如果没触发检查 filter 名和 lay-data如果触发了再打印数据内容看 field 是否对应。4.2 排序后星星不显示或者错位有一种情况用方案一排序后星星消失了或者出现错位。这通常不是排序逻辑的问题而是templet函数执行时你依赖的外部函数在重载时作用域丢了。比如我在templet里调用了renderStar但renderStar是在某个模块回调里定义的局部函数重载表格后由于渲染上下文变化函数还在但如果函数内部引用了错误的作用域变量就可能报错导致整列渲染失败。解决办法是把renderStar这类渲染函数定义在layui.use外层或者挂到全局对象上避免块级作用域带来的意外。另外还有一种错位场景你使用了hide: true隐藏了某个列但隐藏列的字段和展示列字段在排序后因为数据顺序变化出现了行数据与行 DOM 错位。这是 layui 表格自身的渲染机制决定的通常刷新页面或者重新 render 能解决。但从根上避免还是不要在同一列里既隐藏又排序最好只保留一个不可见但真实存在的排序字段。4.3 数字字符串导致的排序错乱这是最容易踩的隐形坑。后端返回的 score 是字符串4.5、5、3前端直接用数字减法比较a.score - b.score如果 a.score 是abc减法结果是 NaN排序直接乱。哪怕都是数字字符串也会出现精度问题。我遇到过一次线上 bug评分 4.5 的排在了 4.15 前面。因为字符串比较是按位比较4.1 和 4.5 比较时先比4再比.同再比1和5这一下就把 4.15 排到 4.5 前面了。所以排序前一定要做类型归一化function normalizeScore(v){ var n Number(v); if(isNaN(n)) return 0; return n; }然后在排序比较里用normalizeScore(a.score) - normalizeScore(b.score)。一个好的习惯是在数据进入表格之前就统一转成 number。上面 2.1 节里的parseData预处理器就是干这个的。前端一旦保证score始终是数字类型后续所有排序方案都稳了。4.4 常见问题速查表我把平时维护项目时遇到的高频问题整理成一个速查表方便大家直接对号入座现象可能原因解决方案点表头没反应filter 名字写错 / 未加 sort:true核对 lay-filter 和 table.on 名称表头加 lay-sort 或 cols 配 sort:true排序结果不正确score 是字符串或带单位统一用 parseFloat 预处理生成纯数字字段排序后表头箭头不更新重载时没传 initSort在 reload/reloadData 里增加 initSort: obj星星排序后消失templet 函数作用域问题把渲染函数提取到全局或 layui.use 外层并列排序不定只排了 score 一个字段自定义 sort 事件增加次级排序字段分页状态下排序错乱前端排序只排了当前页改用后端排序或一次性加载全量数据再前端排序分数相同但顺序随机缺少稳定排序规则增加 id 或 count 作为兜底排序条件4.5 一个完整的可运行示例最后我贴一个综合方案三后端排序思路但用本地数据模拟的完整示例适合新手直接跑!DOCTYPE html html head meta charsetutf-8 titlelayui 星级排序示例/title link relstylesheet hreflayui/css/layui.css /head body table idscoreTable lay-filterscoreTable/table script srclayui/layui.js/script script function renderStar(score){ score Number(score) || 0; var full Math.floor(score); var half score - full 0.5 ? 1 : 0; var empty 5 - full - half; var html ; for(var i 0; i full; i){ html i classlayui-icon layui-icon-rate-solid stylecolor:#ffb800;/i; } if(half){ html i classlayui-icon layui-icon-rate-half stylecolor:#ffb800;/i; } for(var j 0; j empty; j){ html i classlayui-icon layui-icon-rate stylecolor:#ffb800;/i; } return html; } layui.use(table, function(){ var table layui.table; var originData [ { id: 1, name: 课程A, score: 4.5 }, { id: 2, name: 课程B, score: 4 }, { id: 3, name: 课程C, score: 5 }, { id: 4, name: 课程D, score: 3.5 }, { id: 5, name: 课程E, score: 4.5 }, { id: 6, name: 课程F, score: 2 } ]; table.render({ elem: #scoreTable, id: scoreTableId, data: originData, cols: [[ { field: name, title: 课程名称 }, { field: score, title: 评分, width: 180, align: center, sort: true, templet: function(d){ return renderStar(d.score) span stylemargin-left:8px; d.score /span; }} ]] }); table.on(sort(scoreTable), function(obj){ var sorted originData.slice().sort(function(a, b){ var v1 Number(a[obj.field] || 0); var v2 Number(b[obj.field] || 0); if(obj.type asc){ return v1 - v2; } if(obj.type desc){ return v2 - v1; } return a.id - b.id; }); table.reloadData(scoreTableId, { initSort: obj, data: sorted }); }); }); /script /body /html把这段代码里的layui路径换成你自己的实际路径就能跑起来。里面我把取消排序时按 id 顺序复原也做了这样体验更完整。5. 进阶优化与经验建议5.1 评分相同时的次级排序规则真实业务里评分相同的情况很多。比如 100 个商品里有三四十个都是 4.5 分如果只按分数排这三四十个的先后顺序在每次刷新时都可能变化用户看起来就会觉得“排序不稳定”。我的习惯是设置一个稳定的次级排序字段优先级是这样主排序指定字段比如 score次级排序score 相同的人数比如 count 评价人数多的靠前兜底排序主键 id 或创建时间保证顺序绝对稳定。代码里就是sortedData.sort(function(a, b){ if(a.score ! b.score){ return obj.type asc ? a.score - b.score : b.score - a.score; } if(a.count ! b.count){ return obj.type asc ? a.count - b.count : b.count - a.count; } return a.id - b.id; });这里有个小技巧无论升序还是降序兜底排序都建议用固定的 id 升序因为在主次排序条件都相同的情况下id 升序是最符合直觉的也最容易保证稳定。你要是跟着主排序方向来反而会让顺序每次刷新都不一样。5.2 防止快速点击表头导致请求混乱用后端排序方案时用户如果手快连续点了好几次表头前端就可能发出多个并发的 Ajax 请求。后返回的响应可能不是最后一次请求的结果于是表格数据被旧请求覆盖出现排序状态和显示数据不一致的 bug。我的处理方式有两种第一种是简单粗暴的防抖。在 sort 事件回调里加一个锁var isSorting false; table.on(sort(scoreTable), function(obj){ if(isSorting) return; isSorting true; // 发请求回调里面再 isSorting false });第二种是记录最后一次请求的标记。每次发请求时生成一个自增 id只有最新 id 的请求返回时才能更新表格var requestSeq 0; table.on(sort(scoreTable), function(obj){ var seq requestSeq; $.ajax({ url: /api/list, data: { sortField: obj.field, sortOrder: obj.type }, success: function(res){ if(seq ! requestSeq) return; // 过期请求直接丢弃 table.reload(...); } }); });第二种更好它不会阻塞用户操作只是丢弃旧响应。实际项目中我更喜欢用这种。5.3 排序状态下其他操作的联动处理表格如果支持行内编辑、删除、批量操作排序后这些操作可能会影响到排序结果。比如当前按评分降序排列用户删掉了一条高分行那么下一行分数可能变成当前最高但在显示层面它还在原来的位置整体看起来像是“该排序了却没排”。这种情况没法完全靠前端自动处理我的经验是在删除或编辑的操作回调里再次触发当前表头的排序事件让表格重新按当前排序字段排一次。实现方式不复杂你可以在表格 reload 之后手动调用// 当前排序状态 var currentSort { field: score, type: desc }; table.on(tool(scoreTable), function(obj){ if(obj.event delete){ // 删除后重新按当前字段排序 sortData(currentSort.field, currentSort.type); } }); function sortData(field, type){ // 模拟点击表头或者直接执行排序逻辑 // 具体实现和方案二/三一样 }这种方法对用户来说体验是“删除后列表自动排好了”不会出现空白或者顺序异常。5.4 有关评分的细节体验优化最后分享几个我在项目中积累的细节优化不算必不可少但加上了会更专业一是评分数字精度。排序用原始值但展示时可以保留一位或两位小数。如果后端存的是 4.55前端直接展示出来可能很长你可以在templet里用d.score.toFixed(1)做显示。注意排序仍然用原始 d.score而不是 toFixed 后的字符串。二是半星的处理。layui 自带图标里有半星图标但很多业务其实不需要展示半星。遇到 4.5 分时有人显示 4 颗实星加一颗空星有人显示 4 颗实星加半星这两种都可以但排序时都应该按 4.5 参与计算而不是按 4。所以渲染函数里半星的实现只是为了视觉和排序无关。三是表尾合计行。如果表格启用了showFooter: true某个字段配了totalRow: true对评分列做合计是没意义的。我通常会在评分列不配置 totalRow或者只展示平均值。如果非要展示平均值可以用totalRow的回调自己算。四是排序的默认状态。需求有时候要求首次进入页面就按评分降序排列让高分靠前。这可以在table.render配置里通过initSort属性实现table.render({ elem: #scoreTable, initSort: { field: score, type: desc }, // 其他配置 });这样页面加载时表头就会自动带上降序箭头数据也会按这个规则排序用户一进来看到的就是“评分从高到低”的结果。最后再分享一个小技巧如果你在项目里遇到排序后星星明明正常但分数文本却消失的情况优先检查templet返回的字符串里是否包含 HTML 实体或者复杂的引号嵌套。我早期写渲染函数时经常在拼接字符串时忘掉单双引号的转义结果表格单元格渲染出错。后来我习惯把所有图标和文本全放进一个模板函数里通过拼接变量生成避免在 template 串里直接写死带引号的复杂 HTML。另外再啰嗦一句虽然 layui 的 Rate 组件很漂亮但在表格排序这个场景里尽量不要在templet里使用需要 JS 实例化的组件。表格每一行渲染时都是纯 HTML 输出如果强行实例化 Rate你会陷入“渲染后找不到节点”的泥潭。用纯 CSS 字体图标拼星星绝对是最稳妥的。我自己从第一次接触 layui 表格排序到现在经历了从“完全不会”到“踩坑无数”再到“闭眼写”的过程最大的体会就是任何排序问题先分清排序依据和展示内容再考虑数据在哪一层解决。星级评分只是其中一个表象背后其实是一个“字段映射”的通用问题。把这个思路想透了以后遇到任何类似的自定义排序需求都能很快定位到解决方案。希望这篇文章能帮你少走几次弯路。