
我最早碰ExtJS大概是2012年那阵子公司要做一套后台管理系统表格、弹窗、表单、菜单全部要靠jQuery插件拼。插件之间样式冲突弹窗嵌套弹窗时事件经常互相干扰复杂页面上一个联动逻辑能写到怀疑人生。后来项目切到ExtJS整套组件风格统一、交互一致虽然刚上手时被它那一堆配置项搞得很晕但摸清楚组件体系之后写后台的效率确实提升了一大截。这篇内容写给准备入门ExtJS、或者突然被拉去维护老系统ExtJS工程的朋友。我以ExtJS组件为核心从环境搭建、组件体系、布局、表单和表格实操、事件通知、自定义组件到最后常见问题排查把整个使用链路串起来。涉及版本以4.x/5.x/6.x为主老版本项目的差别我会单独指出来。1. 环境准备与组件体系认知先搞清楚ExtJS组件从哪里来1.1 为什么还要学一套“老牌”组件库大家可能第一反应是这年头前端都Vue、React了谁还用ExtJS说实话如果从零搭一个新系统我也大概率不会首选它。但现实是大量企业内部系统、传统制造业的MES、金融行业的报表后台跑了很多年底层就是ExtJS写的。这类系统的特点是页面逻辑重、表格密集、表单校验严格而且当时开发的人已经走了代码文档基本为零。这时候如果你对ExtJS组件体系熟悉就能很快接手。ExtJS本身的优势也很明确它不是一个单纯的UI框架而是一整套组件生态环境。它把按钮、输入框、日期选择、下拉树、标签页、表格、窗口、工具栏甚至图表都做成了配置式的组件统一主题统一事件机制组件之间还有一套成熟的布局容器体系。你不需要处理DIV浮动、CSS覆盖、事件冒泡这类前端杂活只要告诉组件“你放哪、什么尺寸、数据是什么”剩下的渲染、样式、销毁它自己管。对做后台管理系统来说这种“什么都给你做好”的方式非常有吸引力。所以这篇文章的核心目标就是以一个实际项目开发者的视角把ExtJS组件从使用到组合、从事件到自定义一条线讲清楚。适合刚接触ExtJS的开发者也适合扔了ExtJS很久、想要快速捡起来的人。1.2 搭建最小运行环境五个文件跑起第一个面板ExtJS不像普通前端库那样扔一个script进去就结束它分预编译版本和源码版本。初学者不用研究源码直接用官方构建好的SDK包就行。以ExtJS 6.x为例解压后有build、docs、examples这些目录实际页面需要的关键文件是build/ext-all.js整包脚本包含所有核心组件开发阶段用这个最省事。build/ext-all.css默认主题样式文件。主题目录resources里面是图片、字体等资源。我第一次用的时候犯过一个错误只引了JS没引CSS页面加载后组件渲染出来全是裸的HTML块有一半组件还是透明样式。后来习惯了凡是遇到ExtJS组件显示异常第一反应就是检查CSS和resources路径。最小页面可以这样写!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleExtJS 第一个组件/title link relstylesheet hrefpath/to/ext-all.css /head body script srcpath/to/ext-all.js/script script Ext.onReady(function () { Ext.create(Ext.panel.Panel, { title: 你好ExtJS, width: 300, height: 150, html: 这是第一个组件, renderTo: Ext.getBody() }); }); /script /body /html这里面有两个关键点第一Ext.onReady是组件初始化的入口它等DOM加载完成之后再创建组件避免组件渲染时DOM还没就绪第二renderTo指定组件渲染到哪个DOM节点Ext.getBody()表示直接挂在body下面。唯一要提醒的是老版本ExtJS 3.x里没有Ext.create用的是new Ext.Panel。如果维护的是3.x的项目代码风格会明显不同。不过从4.x开始组件规范统一成了Ext.create(Ext.xxx.yyy, config)这种字符串别名形式这也是所有组件实例化的统一入口。1.3 理解xtype组件的“身份证”刚开始用ExtJS时最让我困惑的是xtype。一眼看上去就是个字符串但整个组件配置全靠它来认人。比如Ext.create(Ext.form.field.Text, {});等价于{ xtype: textfield }在容器组件的items数组里你基本不会写Ext.create而是直接写一个配置对象加上xtype让ExtJS在渲染的时候自己创建组件实例。这样做的好处是容器在初始化时可以延迟创建子组件性能更好代码也更简洁。常见的xtype对应关系我列一张表组件xtype说明面板panel最常用的容器按钮button按钮文本输入框textfield单行文本框数字输入框numberfield数字校验输入下拉选择框combo带下拉列表的字段标签页tabpanel选项卡容器表格grid数据表格表单form表单容器窗口window独立弹窗有个细节xtype的缩写是规范好的不能自己随意发明。比如文本输入框是textfield多行文本是textareafield带触发按钮的文本框是triggerfield。缩写虽然多但用几回就能记住规律大部分是组件类名的驼峰去掉点、保留小写。2. 组件的骨架容器、布局与弹窗怎么组合2.1 Container是组件树的根ExtJS里所有能塞子组件的组件共同的祖先是Ext.container.Container。理解这一点很关键整个页面就是一棵组件树根节点是个Container它下面挂PanelPanel里面又挂表单、表格表格里面还有列、工具条。这种树状结构和DOM很像但更抽象——因为你操作的是组件逻辑而不是HTML标签。Container最重要的配置有两个items和layout。items是子组件数组layout决定这些子组件怎么排布。如果不设置layoutExtJS默认使用auto布局子组件会一个接一个按照自己的尺寸往下排基本不做位置管理。很多初学者写完页面发现组件叠在一起就是忘了设置合适的布局。举个例子我想做一个垂直排列的面板组可以这样Ext.create(Ext.panel.Panel, { title: 垂直排列, renderTo: Ext.getBody(), width: 400, height: 300, layout: vbox, items: [{ xtype: panel, title: 上半块, flex: 1 }, { xtype: panel, title: 下半块, flex: 2 }] });这里flex表示按比例分配父容器高度。第一个面板占1份第二个占2份所以第二个会高出一倍。flex在vbox里控制垂直方向的占比在hbox里控制水平方向的占比和CSS里的flex-grow差不多一个意思但是ExtJS帮你写好了不用自己折腾子元素的CSS类。2.2 布局系统选型Fit、HBox、Border一次讲明白布局是ExtJS组件体系里最被低估的一个能力。我见过很多人用ExtJS写复杂页面还是在用绝对定位加百分比宽度看得人头皮发麻。其实ExtJS自带了一套非常成熟的布局系统做后台管理场景基本够用。我常用的有这几种fit只允许一个子组件子组件自动填满父容器。适合单页卡片、路由切换内容区。hbox子组件横向排布可以用flex控制宽度占比。vbox子组件纵向排布可以用flex控制高度占比。border把容器分成五个区域north、south、east、west、center。后台管理系统的经典骨架就是这种顶部导航、左侧菜单、中间内容区一个border布局全搞定。accordion手风琴效果多个面板叠放点开一个收起另一个。侧边菜单常用。拿后台管理系统的经典布局来说用border可以这么写Ext.create(Ext.container.Viewport, { layout: border, items: [{ region: north, height: 60, title: 顶部导航 }, { region: west, width: 180, title: 菜单栏 }, { region: center, title: 主内容区 }] });这里有一个特别容易踩的坑border布局里center区域是必须存在的不能只写north和west。如果没有center区域运行时会报错页面白屏。我第一次用border布局时不知道这个规矩调试了很久才发现只是少了一个region。另外border布局的子组件必须有明确的region配置否则也会报错。报错信息里会提示“No region specified”看到这个基本就是border布局某子项忘了写region了。2.3 Panel、TabPanel、Window三个高频容器容器组件里Ext.panel.Panel是绝对的主力它自带标题栏、工具栏、底部工具条、正文区几乎可以装下任何内容。TabPanel本质上也是Panel的子类它管理多个标签页每个标签页本身又是一个Panel或组件适合把一类功能拆成多个页签。TabPanel使用起来非常简单Ext.create(Ext.tab.Panel, { renderTo: Ext.getBody(), width: 600, height: 400, items: [{ title: 基本信息, html: 这里是基本信息页 }, { title: 扩展信息, html: 这里是扩展信息页 }] });TabPanel有一个值得注意的事件tabchange。如果你需要在切换标签页时刷新对应内容监听这个事件就好。另外一个常见需求是动态增删标签页直接用add和remove方法操作TabPanel不需要手动刷新DOM。Window则是弹窗。它不是渲染在某个容器内部而是默认显示在页面顶层。配置modal: true之后会生成一个遮罩层不允许用户点击外部区域适合做强制交互的弹窗。我把弹窗创建、显示、关闭的代码习惯简写成一个方法function showWindow(title, contentHtml) { var win Ext.create(Ext.window.Window, { title: title, width: 400, height: 300, modal: true, html: contentHtml }); win.show(); return win; }有个我自己常用的技巧Window用完之后如果不打算复用应该在关闭时destroy掉否则它会一直留在内存里。可以在配置里加closeAction: destroy这样点关闭按钮就直接销毁组件。默认值是destroy吗不是默认是hide只隐藏不销毁容易造成页面开很多次窗口后内存越来越大。3. 核心组件实操工具栏、表单、表格一次讲透3.1 按钮与工具栏交互入口是这样搭的按钮是任何系统里最基础的交互组件。ExtJS按钮的xtype是button最核心的配置是text显示文本和handler点击事件处理函数。我经常用viewModel配合按钮的bind配置来动态禁用按钮不过粗糙一点的项目里直接在handler里操作也完全没问题。一个典型的工具栏写法是这样的Ext.create(Ext.panel.Panel, { title: 用户列表, width: 600, height: 300, tbar: [{ text: 新增, iconCls: icon-add, handler: function () { // 打开新增窗口 } }, { text: 删除, disabled: true, handler: function () { // 删除选中记录 } }, -, { text: 刷新, handler: function () { // 刷新表格数据 } }] });这里的-是一个特殊标记表示把后面内容推到工具栏右侧相当于CSS里的margin-left: auto。项目里做权限控制时按钮的disabled和hidden是高频使用项建议封装成独立方法根据登录用户的权限码统一控制而不是在每个handler里到处判断。按钮触发事件不只是点击还有mouseover、mouseout、focus等。不过业务开发里90%都只用handler。关于图标EClassic主题自带iconCls但需要在CSS里定义对应的图标类。3.2 表单组件字段配置与取值校验的核心细节表单是后台系统数据录入的核心。ExtJS表单组件是Ext.form.Panel它内部包含field容器和字段组件。表单最重要的方法有三个getValues()、setValues()、validate()。先看一个字段组合Ext.create(Ext.form.Panel, { width: 400, bodyPadding: 10, items: [{ xtype: textfield, fieldLabel: 姓名, name: name, allowBlank: false }, { xtype: numberfield, fieldLabel: 年龄, name: age, minValue: 0, maxValue: 120 }, { xtype: combo, fieldLabel: 城市, name: city, store: [北京, 上海, 广州], queryMode: local, displayField: text, valueField: text }, { xtype: datefield, fieldLabel: 入职日期, name: joinDate, format: Y-m-d }], buttons: [{ text: 提交, handler: function () { var form this.up(form).getForm(); if (form.isValid()) { var values form.getValues(); console.log(values); } } }] });表单里比较容易出问题的是ComboBox。它内部有displayField和valueField两个概念displayField是显示给用户看的内容valueField是真正提交给后台的值。如果值来自远程接口queryMode需要设为remote并且配置好store的proxy。还有一点getValues()只会拿到所有有name属性的字段值。字段如果没有设置name它的值不会被收集。这个很坑我遇到过同事排查半天最后发现就是一个输入框漏了name。表单校验是个典型的“你以为配好了它还没配好”的场景。allowBlank: false表示必填minValue和maxValue用于数字范围vtype可以指定邮箱、URL等格式。如果内置校验不够还可以自定义validator函数{ xtype: textfield, fieldLabel: 确认密码, name: confirmPassword, inputType: password, validator: function (value) { var pwd this.up(form).down([namepassword]).getValue(); return value pwd ? true : 两次密码不一致; } }validator返回true表示验证通过返回字符串则把该字符串作为错误提示显示在字段下方。这个机制很灵活适合业务里那种“不能等于别人”的校验逻辑。3.3 表格组件GridStore、列模型与分页的配合Grid是ExtJS里最强大的组件也是后台系统最倚重的部分。一个完整的Grid由三部分组成Store数据仓库、columns列模型、grid本身。Store通过proxy从接口取数columns定义每列的标题和展示字段。最基础的写法var store Ext.create(Ext.data.Store, { fields: [name, age, city], data: [ { name: 小明, age: 28, city: 上海 }, { name: 小红, age: 26, city: 北京 } ] }); Ext.create(Ext.grid.Panel, { renderTo: Ext.getBody(), title: 员工名单, width: 600, height: 300, store: store, columns: [{ text: 姓名, dataIndex: name }, { text: 年龄, dataIndex: age }, { text: 城市, dataIndex: city }] });这里dataIndex必须和Store的fields对应否则表格会出现空白列。这是最常见的数据不显示原因之一。实际项目里的Grid不会用本地data而是用ajax proxyvar remoteStore Ext.create(Ext.data.Store, { fields: [id, name, status], proxy: { type: ajax, url: /api/users, reader: { type: json, rootProperty: data } }, autoLoad: true });远程Store要和分页工具条配合。ExtJS的分页工具条是Ext.toolbar.Paging配置好store后它自动负责上一页、下一页、页码跳转{ xtype: pagingtoolbar, store: remoteStore, dock: bottom, displayInfo: true }Grid还有一个高频扩展单元格渲染函数renderer。比如状态字段返回数字1、2、3想显示成文本并且背景高亮就在column里写{ text: 状态, dataIndex: status, renderer: function (value) { var map { 1: 待审核, 2: 已通过, 3: 已驳回 }; return map[value] || 未知; } }renderer里还可以返回值HTML字符串配合样式类做成标签效果。不过一定要防止XSS不要把用户输入直接拼进HTML返回。4. 组件事件与通信机制让组件之间互相协作4.1 事件模型on、un、fireEvent到底做了什么ExtJS有一套自己的事件模型跟DOM原生事件不是一回事。组件继承自Ext.util.Observable可以注册事件监听、触发事件。相比原生addEventListenerExtJS事件模型的最大优势是事件可以自定义而且触发和监听都在组件实例上天然解决this指向问题。基本用法是var panel Ext.create(Ext.panel.Panel, { title: 事件演示, width: 300, height: 200 }); // 绑定 panel.on(customEvent, function (data) { Ext.Msg.alert(触发, 参数是 data); }); // 解绑 var fn function () { console.log(不再执行); }; panel.on(customEvent, fn); panel.un(customEvent, fn); // 触发 panel.fireEvent(customEvent, hello);un操作必须传入与原绑定相同的函数引用否则解绑会失效。这也是新手常犯的错误把匿名函数存到控件某个地方之后想解绑时拿不到原函数。事件还有一个很有用的能力冒泡。子组件触发的事件会一路冒泡到祖先容器前提是enableBubble配置了对应事件名。比如自定义组件里触发一个save事件父容器可以统一监听而不需要给每个子组件单独绑定。4.2 三种组件通信方式从getCmp到事件总线组件之间通信是实际项目绕不开的需求典型场景是点工具栏的“查询”按钮远处的Grid要刷新数据选中左边菜单项右边内容区要切换页面。ExtJS里用了这么多年我归纳起来有三种常见实现方式。方式一全局获取实例用Ext.getCmp(id)拿到组件实例再直接调用它的方法Ext.create(Ext.button.Button, { text: 刷新表格, handler: function () { var grid Ext.getCmp(mainGrid); if (grid) { grid.getStore().load(); } } });这种方式最简单但坏处是全局id在大型应用中容易冲突而且组件销毁后旧引用还在的话调用会报错。适用于小型项目或原型快速验证。方式二父容器统一监听适合组件间有明确父子关系的场景。子组件定义自己的事件父容器统一处理Ext.create(Ext.panel.Panel, { items: [{ xtype: textfield, itemId: keyword }, { text: 搜索, xtype: button, handler: function () { this.up(panel).fireEvent(doSearch, this.up(panel).down(#keyword).getValue()); } }] });父组件通过down(#itemId)查找子组件通过up(panel)向上查找父容器这是ExtJS惯用的组件查询方式比Ext.getCmp灵活很多。方式三全局事件总线多个不相干组件要通信时全局实例方法容易造成混乱可以在模块入口创建一个事件总线var bus Ext.create(Ext.util.Observable, {}); // 表格所在的视图里 bus.on(refreshGrid, function () { mainGridStore.load(); }); // 工具栏按钮里 bus.fireEvent(refreshGrid);事件总线本质上是把“组件A喊一嗓子组件B响应”的模型统一收口。好处是解耦组件A不需要知道谁在听坏处是事件满天飞的时候代码可读性会下降建议按业务模块命名事件。4.3 响应式更新的难点老版本手写刷新新版本用ViewModel组件通信最终的落点是让视图在数据变化时同步刷新。ExtJS 5之前没有ViewModel和双向绑定刷新全靠手动调用方法表格数据变了就store.load()表单数据变了就form.setValues()标题变了就panel.setTitle()。这套方式挺机械但胜在直观。我举一个典型场景点击列表某一行右侧表单显示该行详情。grid.on(select, function (sm, record) { formPanel.getForm().setValues({ name: record.get(name), age: record.get(age) }); });这里record.get()是获取记录字段值的标准方法别直接record.name那会拿到undefined。ExtJS 5之后引入了ViewModel和bind配置组件可以直接绑定数据Ext.create(Ext.panel.Panel, { viewModel: { data: { userName: 未登录 } }, items: [{ xtype: label, bind: 欢迎您{userName} }] });这种写法确实方便但老项目升级成本高不建议为了这种便利迁移。如果维护的是老项目安安心心用手动刷新就好别动不动提升级。5. 自定义组件与复用策略把业务代码收进自己的组件库5.1 用Ext.define封装一个业务组件项目做大了会发现很多页面有相同结构上方搜索栏、中间表格、下方分页工具条。如果把这段逻辑复制粘贴到每个页面后面改一个字段要改几十处。正确的做法是用Ext.define把它封装成一个自定义组件。比如我有一个带搜索框和按钮的工具栏组件Ext.define(MyApp.view.SearchBar, { extend: Ext.container.Container, alias: widget.searchbar, layout: hbox, defaults: { margin: 0 5 0 0 }, initComponent: function () { Ext.apply(this, { items: [{ xtype: textfield, flex: 1, emptyText: 请输入关键词, itemId: searchInput }, { xtype: button, text: 搜索, handler: this.onSearchClick, scope: this }] }); this.callParent(arguments); }, onSearchClick: function () { var keyword this.down(#searchInput).getValue(); this.fireEvent(search, keyword); } });这里有三个关键点用alias给组件取xtype别名之后可以在配置里直接写xtype: searchbar。在initComponent里通过Ext.apply(this, {...})合并自己的配置注意配置放在callParent之前还是之后取决于是否需要父类先做一些准备。scope: this让handler的this指向自定义组件实例方便后续this.fireEvent。业务组件封装之后页面的书写就从零散的组件配置变成了{ xtype: searchbar, listeners: { search: function (keyword) { gridStore.load({ params: { keyword: keyword } }); } } }这就把“做什么”和“怎么做”拆开了。页面关注业务逻辑组件关注UI行为。5.2 Mixin与插件给组件“加技能”ExtJS的复用不只是组件继承还有两个常用机制Mixin和Plugin。Mixins是混入可以把一组通用方法注入到多个组件里。比如很多组件需要用getData、setData来统一读写数据可以写一个纯js对象Ext.define(MyApp.mixin.DataHelper, { getData: function () { return this.__data || {}; }, setData: function (data) { this.__data data; } });组件定义时通过mixins引入Ext.define(MyApp.view.MyPanel, { extend: Ext.panel.Panel, mixins: { dataHelper: MyApp.mixin.DataHelper } });之后实例上就多了getData和setData方法。这个模式适合那种“无继承关系但需要共性行为”的组件。Plugin则是给组件增加能力的扩展工具最典型的是表格编辑插件Ext.create(Ext.grid.Panel, { plugins: [{ ptype: rowediting, clicksToEdit: 2 }] });插件和Mixins的区别在于插件是被动叠加到组件生命周期里的它可以在组件初始化时改动配置、在渲染后绑定状态Mixins更偏向于给类添加方法和逻辑。实际项目里能用插件解决就优先插件它不污染业务代码。5.3 组件库的整理与加载按需引入而不是一把梭开发阶段用ext-all.js图省事但生产环境全量包太大。ExtJS 4之后支持Ext.Loader按需加载核心是通过配置文件里的requires声明依赖Loader自己管理脚本加载顺序。比如Ext.define(MyApp.view.SearchBar, { extend: Ext.container.Container, requires: [ Ext.form.field.Text, Ext.button.Button ], // ... });然后配合Ext.Loader.setConfig({ enabled: true })页面会按需加载对应类文件。前提是文件路径遵循类名规范包结构是build\src\Ext\form\field\Text.js这样。使用Sencha Cmd可以把整个应用打包成一个或多个文件按需加载的依赖图会被静态分析最终输出压缩后的build包。这块对于初学者不是必须掌握的先学会组织代码比纠结构建工具更重要。真正要生产部署时再看官方文档做一次打包。另外多说一句我看到很多人把通用组件和业务组件混在一个大文件里。建议按通用组件、业务组件、页面视图三层分类通用组件放独立的包目录业务组件按模块划分。这样后续排查问题、定位组件都要快得多。6. 常见问题与排查技巧实录这些都是真实踩过的坑6.1 布局渲染乱套先检查父容器和region遇到过最典型的布局问题是页面上几个面板重叠在一起或者宽度怎么设置都不生效。自己踩坑总结的顺序是先看浏览器控制台有没有报错。ExtJS报错通常很直白比如找不到某个xtype或者缺少region。再看父容器的layout配置。如果父容器是auto布局子组件不会自动分配位置宽高是按自身内容决定的。如果你是手动在渲染后往container里加组件加完最好调用container.updateLayout()否则很多情况下新组件不会立即参与布局计算。这个问题在动态添加标签页时特别常见。Border布局还有一个值得注意的规则north和south适合指定固定高度east和west适合指定固定宽度center则自动占满剩余空间。如果把center区域设置了宽度反而会出现多余空白。6.2 数据不刷新、表格空白十有八九是Store和字段没对齐表格空白和表单取值不准最常见的三个原因dataIndex写错或漏写。字段名和后台返回的JSON字段名对不上时表格单元格会一片空白不报错纯靠肉眼发现。建议开发时开一个console.log(store.getRange())看看Store里到底有没有数据。reader的rootProperty配置错误。后台返回结构是{ code: 0, data: [...] }如果rootProperty写成了rowsExtJS就找不到数据。字段类型不匹配。后台返回age是字符串但Store fields里定义成了int某些操作会出问题。定义字段时最好显式声明类型fields: [ { name: age, type: int }, { name: joinDate, type: date, dateFormat: time } ]这个type声明很有用一是让排序按类型走二是让日期能正确格式化显示。6.3 组件销毁与内存泄漏remove和destroy别混用页面性能慢慢变卡先查是不是弹窗和标签页关掉后没销毁。container.remove(child)只是把组件从容器里移走但组件的监听器、DOM节点、数据引用可能都还在child.destroy()才会真正释放资源。正确做法是tabPanel.remove(tab); tab.destroy();更规范的做法是用remove(child, true)第二个参数为true表示移除同时销毁。定时器和全局监听是另一个泄漏重灾区。窗口关闭后组件里的定时器还在调用组件方法就会报Cannot read property ... of undefined。我习惯在组件beforedestroy事件里清理定时器Ext.create(Ext.window.Window, { listeners: { beforedestroy: function () { clearInterval(this.timer); } } });日常排查内存泄漏用Chrome DevTools的Heap Snapshot比较稳妥。操作一遍打开页面拍照做几次开窗、关窗、刷新表格再拍照对比对象数量。如果Window实例数量只增不减那基本就是closeAction没设成destroy。6.4 老版本项目升级最怕的不是API是隐藏依赖最后说一下升级这件事。很多团队一听升级ExtJS就头疼因为API变化只是表面真正麻烦的是老页面对旧版本特性的隐性依赖。比如当年用Ext.apply、Ext.ns这种全局方法新版本可能已经弃用。从3.x升4.x类名变化很大Ext.Panel变成了Ext.panel.Panelxtype也从panel保持兼容到新命名。从4.x升5.x最大的冲击是引入了ViewModel和MVC/MVVM架构底层的数据绑定变了老的全局事件广播式写法虽然能跑但和官方推荐结构格格不入。从5.x升6.x变化相对温和主要是把一些过时的组件迁移到新命名空间比如Ext.ux的很多扩展组件要重新找替代。我的建议是如果老系统稳定运行不要为了“新版本更现代”去冒然升级。更稳妥的策略是保持老系统锁定版本新业务模块采用新版本单独开发通过iframe或者统一路由入口整合进同一个壳子。这样既规避了升级风险还能逐步替换老旧页面。实际维护ExtJS项目的过程中还有一个切身体会遇到问题先搜官方API文档尤其是组件配置项和事件列表。很多看起来像bug的问题其实是某个配置项没有设置导致的。再者坚持多用Ext的组件查询方法down和up别满屏Ext.getCmp代码会好维护很多。踩过一次布局的坑、一次销毁的坑以后你会慢慢习惯这套组件的思维方式——所有东西都是配置所有交互都是事件页面从上到下就是一棵组件树。