ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ExtJS组件实战指南:从组件树、布局到生命周期管理

ExtJS组件实战指南:从组件树、布局到生命周期管理 提到ExtJS很多前端老手的第一反应是“老牌企业级UI框架”。它从Ext 2时代一路火到4.x再到现在的6/7虽然不像React、Vue那样天天上热搜但在后台管理系统、数据中台、内部ERP这类需要大量表格、表单和树形结构的场景里依然有一批忠实用户。这篇文章不打算照着官网文档CtrlC而是从一个实际做项目的角度把ExtJS组件怎么用、怎么搭、怎么避开日常坑讲清楚。适合刚上手ExtJS的初学者也适合被临时派去维护老ExtJS项目的同学参考。1. 先理解ExtJS的组件世界观1.1 组件不是“控件”而是一棵组件树我在刚接触ExtJS时踩过最深的坑就是把它当成像input、button那样的独立控件来用。其实ExtJS里的“组件”概念比HTML控件大得多一个按钮是组件一个表格是组件一个窗口是组件一个表单也是组件甚至整个页面布局都可以是一个组件。这些组件不是散落各处的零散元素而是按照层级关系组织成一棵“组件树”。比如一个Ext.panel.Panel面板里可以放一个Ext.grid.Panel表格表格里又可以放一列Ext.button.Button操作按钮。父组件负责管理子组件的创建、渲染、布局和销毁子组件通过xtype定义自己的类型。这种树形结构和浏览器里的DOM树非常像区别在于ExtJS在DOM之上又封装了一层组件管理逻辑。理解这棵树很重要因为后面所有布局、查找、通信、销毁都是基于这棵树的。拿到一个ExtJS页面先别急着看代码逻辑先画一下组件树最外层是什么容器里面分了几块每块里又嵌套了什么。这样排查问题会快很多尤其是处理“某个组件不显示”的时候十有八九是组件树某个节点放错位置。1.2 组件的生命周期从创建到销毁组件从创建到页面消失经历的过程远比想象中复杂。简化来看大概是这样一条线创建实例通过Ext.create或者容器items配置创建这时组件对象已经存在但还没有HTML。初始化调用initComponent等方法完成属性合并、子组件创建。渲染把组件挂到页面上生成DOM节点触发render、afterrender事件。运行用户操作数据进行各种交互。销毁调用destroy方法移除DOM、解绑监听、销毁子组件。这里面最关键的一点是组件对象存在不等于渲染完成。很多新手在launch里直接Ext.create一个组件然后立刻去操作它的DOM结果拿到的是null。正确做法是等afterrender事件触发后再操作或者通过renderTo明确指定渲染位置。我踩过的一个典型的坑是在beforerender里访问组件的高度和宽度拿到的全是0。因为此时还没有生成DOM布局引擎还没有跑过。如果有类似需求应该监听boxready事件这时候布局已经完成能拿到真实的宽高。1.3 为什么要用xtype而不是疯狂newxtype可以理解为组件类型的字符串别名。Ext.panel.Panel对应panelExt.grid.Panel对应gridExt.form.field.Text对应textfield。在容器配置里你可以写{ xtype: panel, title: 面板 }也可以写Ext.create(Ext.panel.Panel, {...})。很多人不理解为什么要多一个xtype。直接new不好吗不好。第一xtype写法简洁尤其在items数组写一堆组件时一眼能看清组件类型。第二容器在创建子组件时会延迟实例化也就是说组件的new操作真正发生在容器初始化时而不是写配置时这样可以避免一些不必要的内存占用。第三后续配合Ext.ComponentQuery查询时panel这类xtype可以直接当选择器用比如down(grid)查表格组件写起来非常自然。我在实际项目里推荐遵循一个原则所有静态配置的组件一律用xtype写在items里只有需要动态创建、或者需要在创建后立即调用方法时才用Ext.create。这样代码结构会清晰很多可维护性也高。2. 环境准备与第一个能跑的组件页面2.1 引入ExtJS的两种方式先说环境。ExtJS不是npm包那么简单它自带一套样式系统和几十个JS文件。传统开发方式很简单下载ExtJS正式包后在页面里引入CSS和核心JS文件即可。以ExtJS 6.x为例目录结构大概是link relstylesheet hrefext/build/classic/theme-classic/resources/theme-classic-all.css script srcext/build/ext-all.js/scriptext-all.js是打包好的完整框架核心内容很多加载会慢一点。如果项目很重也可以用ext-all-debug.js方便调试但正式环境一定记得切回压缩版。另一种方式是使用Sencha Cmd来搭建工程自动管理依赖编译还能生成优化的版本。但对初学者来说用Sencha Cmd会瞬间淹没在构建工具的各种概念里反而不容易理解组件本身。我的建议是第一周先直接用静态页面跑通组件等理解组件树和布局了再考虑工程化工具。2.2 最小的Ext.application配置我用得最多的入口是Ext.application它负责初始化整个应用。一个最简单的配置长这样Ext.application({ name: DemoApp, launch: function() { Ext.create(Ext.panel.Panel, { title: 第一个ExtJS页面, width: 400, height: 200, html: pHello ExtJS/p, renderTo: Ext.getBody() }); } });name是应用命名空间随便取。launch会在ExtJS框架加载完成、DOM准备好之后执行。在这里创建组件最为安全。注意那个renderTo: Ext.getBody()它的意思是把组件直接渲染到body上。如果你不写这一点组件只是存在于内存中页面上什么都看不到。很多新手第一次试跑页面白屏多半就是漏了这一行。2.3 页面挂载与加载顺序的细节在真实项目中很少会把组件直接render到body更多是放到容器或Viewport里。但了解renderTo依然很重要因为它帮助你理解组件的“挂载”行为。挂载涉及一个核心概念一个组件只能有一个父容器。你不能把一个组件同时renderTo到两个地方也不能把它放到一个已经渲染完成的父容器里然后又手动调用render。所以动态添加组件时推荐用container.add(comp)再container.updateLayout()而不是尝试手动改DOM。如果页面加载后发现样式不对优先检查CSS有没有引入。ExtJS组件极度依赖主题CSS少了主题文件组件会以最原始的HTML裸奔看起来就像样式全部丢了。这个问题我在引入新版本主题时遇到过升级ExtJS版本时旧的主题CSS不能直接沿用必须换成配套版本。3. 日常工作绕不开的核心组件3.1 Button事件绑定的最小范例按钮是最简单的组件却是学习事件机制最好的入口。看这段代码Ext.create(Ext.Button, { text: 点我保存, iconCls: x-fa fa-save, handler: function(btn, event) { Ext.Msg.alert(提示, 你点击了保存按钮); } });handler是点击事件的简写适用于“只关心点击后做什么”的场景。如果除了点击还想监听mouseover、mouseout、focus等状态可以用listeners配置listeners: { click: function(btn) { console.log(clicked); }, mouseover: function() { console.log(mouse over); } }这里要注意一个细节handler本质上是listeners里click的快捷方式如果同时配置handler和listeners.clickhandler会先执行再执行listeners里的click。虽然不冲突但混乱我一般只选一种。3.2 Panel几乎所有组件的容器如果ExtJS只能记住一个组件我选Panel。它自带标题栏、底部工具栏、侧边工具栏还可以嵌套各种组件。一个典型的Panel配置是这样的Ext.create(Ext.panel.Panel, { title: 用户管理, width: 600, height: 400, collapsible: true, tbar: [ { text: 新增, handler: function() {} }, { text: 删除, handler: function() {} } ], items: [ { xtype: grid, ... } ], renderTo: Ext.getBody() });tbar是顶部工具栏bbar是底部工具栏这两个属性在后台管理页面中太常用了。collapsible允许用户折叠面板节省空间。使用Panel时要注意如果Panel的items里放了多个组件必须设置layout否则这些子组件会全部叠在一起效果非常难看。关于布局在第4章专门讲。3.3 Grid最常打交道的表格组件后台项目里表格是绝对的主角。ExtJS的Grid组件功能强大但结构也比按钮复杂。一个最简单的Grid需要两部分数据仓库Store和列配置。Ext.create(Ext.grid.Panel, { title: 订单列表, store: { fields: [id, status, amount], data: [ { id: 1, status: 已支付, amount: 199.00 }, { id: 2, status: 待发货, amount: 299.00 } ] }, columns: [ { text: 订单ID, dataIndex: id, width: 80 }, { text: 状态, dataIndex: status, flex: 1 }, { text: 金额, dataIndex: amount, flex: 1 } ], renderTo: Ext.getBody() });store是数据容器fields定义字段结构data可以直接给本地数据也可以配置proxy从后台API读取。columns里的dataIndex必须和fields里的字段名一一对应否则表格就是空白的。flex属性很常用它表示该列会“吃掉”剩余宽度。比如flex: 1让状态列和金额列自动拉伸填满表格而ID列固定80像素。想做好后台布局必须理解flex的伸缩逻辑它类似于CSS的flex-grow。Grid的难点在动态列、单元格渲染、行选中和分页。建议初学者先把本地数据跑通再去接远程数据。远程数据往往涉及代理配置、异步加载、loading状态变量一下子很多容易把人绕晕。3.4 Form表单数据采集与回填表单在业务系统里和Grid是双胞胎一个负责展示一个负责录入。ExtJS的Form支持各种输入字段常见的组合是Ext.create(Ext.form.Panel, { title: 新增用户, items: [ { xtype: textfield, name: username, fieldLabel: 用户名 }, { xtype: combobox, name: role, fieldLabel: 角色, store: [管理员, 操作员], queryMode: local }, { xtype: datefield, name: birthday, fieldLabel: 生日, format: Y-m-d }, { xtype: numberfield, name: age, fieldLabel: 年龄, minValue: 1, maxValue: 120 } ], buttons: [ { text: 提交, formBind: true, handler: function(btn) { var form btn.up(form); var values form.getValues(); console.log(values); }} ] });fieldLabel是表单字段左侧的文字标签name是提交数据的键名。getValues()一次性拿到所有字段值非常方便。Form使用中最大的坑是“回填”。很多新手直接用form.setValues(obj)发现下拉框、日期框不显示或者数据显示了但格式不对。正确做法是确保setValues的键名和字段name完全一致并且日期字段要传Date对象或符合format的字符串不能传任意格式的字符串。3.5 Tree和Tab侧边导航与页签组织Tree和Tab在后台管理界面里出镜率也很高。Tree的典型用途是组织架构、权限菜单Tab用于在一个区域内切换多个业务页面。Ext.create(Ext.tree.Panel, { title: 菜单树, store: { type: tree, root: { text: 根节点, expanded: true, children: [ { text: 用户管理, children: [{ text: 新增用户 }, { text: 用户列表 }] }, { text: 订单管理 } ] } }, renderTo: Ext.getBody() });TabPanel则是一个容器内部每个子组件对应一个Tab页Ext.create(Ext.tab.Panel, { items: [ { title: 订单列表, items: [/* grid */] }, { title: 统计报表, items: [/* chart */] } ] });树组件结合Store的异步加载可以做树形菜单TabPanel结合动态添加删除可以做多页签业务。这两个组件本身不是最难但它们和布局系统结合紧密建议学完布局后再回来看。4. 布局组件摆放的核心机制4.1 布局和容器是绑定的ExtJS组件不会自动排列。如果你往一个Panel里塞两个子组件而不设置layout最后看到的就是两个组件重叠或者挤在左上角非常难看。这是因为每个容器都有一个layout配置layout决定了子组件的排列方式。我把ExtJS布局理解为“排版引擎”。它不关心组件具体是什么只关心组件放在哪个位置、占多大空间。默认布局通常是auto它不会做复杂排版。实际项目中我强烈建议每个容器都显式指定layout哪怕只有一个子组件也写明layout: fit这样代码意图明确排查问题也方便。4.2 五个最常用布局fit子组件自动填满父容器。适合“什么布局都不想要只想让一个组件占满空间”的场景。border把区域划分为上北、下南、左西、右东、中五个区域最经典的框架布局。hbox子组件水平排列可用flex控制宽度比例。vbox子组件垂直排列可用flex控制高度比例。column按列排用columnWidth百分比控制宽度比hbox更古老一些。我平时用得最多的是border、fit和hbox/vbox。border搭页面骨架fit填内容hbox/vbox做局部排版。4.3 用border布局搭建后台主界面后台管理系统的经典布局是顶部导航栏、左侧菜单、中间内容区。用border布局实现非常直观Ext.create(Ext.container.Viewport, { layout: border, items: [ { region: north, height: 60, title: 系统标题, collapseMode: none }, { region: west, width: 220, title: 菜单, split: true }, { region: center, xtype: tabpanel, activeTab: 0 } ] });Viewport是ExtJS的根容器它会自动占满整个窗口。region指定在border布局中的方位north和west必须给固定宽度或高度center不用指定它会自动吃掉剩余空间。这里有个容易犯的错忘了设置layout: border只在items里写了region结果区域全部不生效。或者north和west都写了flex导致高度分配失控。记住border布局下north/south固定高度west/east固定宽度center自适应。4.4 flex参数让组件学会伸缩hbox和vbox里的flex是控制主轴方向空间分配的关键。类似CSS flex布局给一个组件flex: 1另一个组件flex: 2后者占的空间是前者的两倍。Ext.create(Ext.panel.Panel, { layout: hbox, items: [ { xtype: panel, title: A, flex: 1 }, { xtype: panel, title: B, flex: 2 }, { xtype: panel, title: C, width: 200 } ] });在这个例子里A和B动态分配剩余宽度C固定200像素。如果窗口宽度变化A和B的宽度会跟着变化C始终保持200。经验之谈尽可能用flex而不写死所有宽度。我做后台页面时很多表格列宽和分区宽度都是用flex控制的。写死宽度在固定分辨率下因为现代显示器尺寸多样很容易出现横向滚动条。5. 组件通信从父找子到兄弟互喊5.1 用up、down、query在组件树里查找组件之间经常需要互相调用。最直接的方式就是沿组件树查找。component.up(selector)向上找最近的匹配祖先。component.down(selector)向下找第一个匹配后代。component.query(selector)向下找所有匹配后代。component.child(selector)只查找直接子组件。举个例子一个Window里有Form和Grid点击保存按钮时要在handler里拿到表单数据和表格选中行handler: function(btn) { var win btn.up(window); var form win.down(form); var grid win.down(grid); var values form.getValues(); var selection grid.getSelectionModel().getSelection(); console.log(values, selection); }这段代码的核心是按钮通过up(window)找到自己所在的窗口再由窗口向下查找表单和表格。只要你在items里按正常结构组织选择器就一定能命中。query返回数组即使只匹配到一个也是数组。down返回单个组件。初学时我经常把这两者记混导致调用方法时报cannot call ... of undefined。记一句话要找单个用down要找多个用query。5.2 自定义事件让组件解耦当组件树很深时一直用up/down会写出“组件寻亲记”耦合度非常高。更好的方式是让组件自己抛出事件由感兴趣的组件来监听。ExtJS的组件天然具备事件能力你可以用fireEvent抛出自定义事件Ext.define(MyApp.view.OrderGrid, { extend: Ext.grid.Panel, xtype: ordergrid, listeners: { itemdblclick: function(grid, record) { this.fireEvent(orderopen, record); } } });然后在父容器里items: [{ xtype: ordergrid, listeners: { orderopen: function(record) { // 打开订单详情窗口 } } }]这样OrderGrid不需要知道谁会处理它的事件只负责抛出调用方自行监听。组件之间代码耦合度明显降低也更容易做单元测试。5.3 兄弟组件之间传值兄弟组件指同一个父容器下的两个组件。表面看起来它们没有直接关系但只要有共同的父容器就能搭桥。我在项目里常用的做法是父容器控制整个逻辑监听子组件事件更新另一个子组件。比如左侧是商品列表右侧是商品详情点击列表项时更新详情listeners: { select: function(grid, record) { var detail grid.up(panel).down(detailpanel); detail.updateData(record); } }从grid往上找到公共父容器再向下找到detailpanel调用它的更新方法。这个方法比用全局变量好在于作用域清晰不受页面其他模块影响。如果两个组件距离较远跨越了很多层那么用全局事件总线。ExtJS没有官方内置总线但可以用一个Ext.util.Observable子类当总线或者直接挂在Ext.app.Application上。不过过度使用全局总线会让数据流变得很难跟踪我一般只在跨模块、非层级关系时才用。5.4 数据层Store的联动还有一种特殊的“通信”是Store之间的联动。比如选择了省份城市下拉框的Store要根据省份ID重新加载。这种适合监听第一个字段的change事件再调用第二个Store的loadlisteners: { change: function(field, newValue) { var cityCombo field.up(form).down([namecity]); cityCombo.getStore().getProxy().extraParams { provinceId: newValue }; cityCombo.getStore().reload(); } }注意getProxy().extraParams用于给请求额外参数重新加载时会带上省份ID。这里也体现了数据层和组件的配合组件负责展示和交互Store负责管理和加载数据。6. 组件的生命周期与销毁艺术6.1 渲染前后的关键钩子在真实项目里经常需要在组件渲染完成后做一些外部插件初始化、DOM样式调整、数据预加载。ExtJS为组件提供了几个生命周期钩子最常用的是afterrender和boxready。Ext.create(Ext.panel.Panel, { title: 图表容器, html: div idchartDiv/div, listeners: { afterrender: function(panel) { // 在这里初始化图表因为DOM已经存在了 var div document.getElementById(chartDiv); // init chart }, boxready: function(panel, width, height) { console.log(组件实际宽高, width, height); } } });afterrender保证组件DOM存在但此时不一定完成了布局计算boxready则保证布局完成可以拿到真实宽高。如果两者都需要我习惯在boxready里做依赖尺寸的初始化比如图表组件、第三方富文本编辑器。6.2 定时器与监听器的清理这是内存泄漏的重灾区。很多人在组件里写了setInterval定时刷新数据或者给window绑定resize事件组件销毁时忘记清理。正确做法是在destroy事件或beforedestroy里释放资源listeners: { destroy: function() { if (this.timer) { clearInterval(this.timer); this.timer null; } window.removeEventListener(resize, this.onResizeHandler); } }销毁组件本身会清理内部事件监听和子组件但你自己附加出去的东西比如全局定时器、window事件、第三方实例ExtJS管不到必须手动清理。另一个常见的坑是Window的closeAction。默认closeAction: destroy关闭窗口会销毁组件但如果你把它改成hide窗口只是隐藏对象还在内存中。如果反复open和close又不做数据清理就会出现隐藏组件越来越多页面越来越卡。6.3 容器销毁时孙组件的去向当父容器销毁时它的直接子组件会被自动销毁。但如果某个子组件的Store是外部共享的比如多个Grid共用一个StoreStore并不会随着Grid销毁。如果Store里注册了很多监听器或者有定时器它就会一直留在内存里。我的经验是局部Store一定挂在组件内部定义这样随组件销毁如果确实要共享Store必须单独管理生命周期在页面关闭或退出登录时手动销毁Store。判断一个组件是否泄漏可以打开浏览器DevTools的内存快照反复开关页面看组件类名和Store是否不断增长。7. 常见问题与排查技巧实录7.1 页面白屏组件完全没显示白屏原因有很多我按出现概率排一下没有引入主题CSS或JS路径错误。忘了写renderTo或者组件没有加到任何容器里。Ext.application的launch没执行比如JS报错。组件配置了layout但子组件配置错误导致渲染中断。遇到白屏第一步打开浏览器控制台看有没有红色报错。第二步确认页面里body下面有没有组件的DOM节点。第三步检查Style如果DOM节点存在但样式不对大概率是主题CSS问题。7.2 组件出现但全部叠在左上角百分之九十是因为容器没有设置layout。ExtJS不会像浏览器文档流那样自动摆正位置必须靠布局系统。找到最外层容器根据想要的效果设置fit、border或hbox。这个问题容易在复制代码时发生一个不留神删了layout整个页面就乱了。还有个小技巧如果某个区域子组件很多且布局复杂尽量不要一层层嵌套太多容器。布局嵌套越深重算成本越高出现调整问题的概率也越大。能用border一屏拆几个区域就不要拆成十层小Panel。7.3 表格数据没渲染出来表格没有数据先分三层排查第一层Store有没有数据。控制台打印store.getCount()如果是0说明数据加载就有问题。本地数据看data数组远程数据看Network请求是否返回正确。第二层字段是否匹配。store的fields里定义了字段名columns里的dataIndex必须和字段名完全一致。大小写、空格都可能让列变成空白。第三层列宽度是否被压缩。如果columns里所有列都没设宽度表格可能把所有列都渲染成0。至少要有一列用flex或明确给width。7.4 事件不触发事件不触发的原因五花八门最常见的是这几种选择器写错。down(grid)匹配的是xtype如果你给组件起的xtype是mygrid那就要写down(mygrid)。监听器加错了对象。比如想监听Grid的行点击却把listeners写在了Grid的父容器上。组件还没有渲染完成就调用了bind。比如在launch里对还没渲染的组件绑定第一次监听而组件在后续渲染过程中可能重置了配置。handler函数作用域错了。用了普通function内部的this不再是组件导致this.up(...)报错。在listeners里可以直接用scope: this或者用箭头函数。我排查这类问题有个土办法在监听器第一行加一句console.log(this, arguments)看执行到没有this指向谁。八成问题能在这一步找到。7.5 ExtJS常见报错速查表报错信息原因与处理Cannot read property getStore of undefined组件还没创建成功或者选择器没有匹配到组件。检查up/down的路径。Ext.container.Container: cannot insert child at index试图向容器添加一个已经有父组件的组件。先remove再add。Unable to find theme stylesheet主题CSS引入失败或版本不匹配。Uncaught TypeError: records[0].get is not a function数据不是Ext.data.Model实例检查Store的fields定义或者数据源格式。Layout run failed或boxready无限循环通常是组件在boxready里修改了自身宽高触发再次布局形成死循环。避免在布局回调里改动自身尺寸。form.getValues() is not a function拿到的对象不是Form组件是其他Panel。检查up(form)是否匹配。这几类报错基本覆盖了我多年维护ExtJS项目的大部分异常。遇到不认识的报错先试着用英文关键词搜索很多老开发者都踩过同样的坑Stack Overflow上有大量现成答案。关键是定位问题时要有顺序先看网络请求再看DOM再看Store数据最后看事件绑定。我自己在实际操作中最大的体会是ExtJS的组件化思想其实非常成熟只要把组件树、布局、生命周期这三件事琢磨透后面写代码就是套模板。别被它庞大的API吓到先掌握Button、Panel、Grid、Form和布局已经能应付绝大多数后台页面了。如果遇到组件不显示或者布局乱了不用急着改代码先画组件树再层层排查通常都能快速定位问题。最后分享一个小技巧在做Table列配置时尽量用flex而非固定width在窗口大小变化时体验会好很多。如果你正在维护老版本ExtJS项目记得升级时先替换主题CSS这能帮你省下至少一个下午的调试时间。
返回列表