简介:面向具备 JavaScript 基础、希望构建企业级应用的开发者,《Sencha Ext JS 7.8+入门指南》以安装、项目创建、基本配置和视图/组件使用为主线,配合数据网格、柱状图、表单捕获等示例,系统讲解 Ext JS 核心概念与 MVC 架构。从获取许可证、安装 Sencha Command、npm 环境管理到 IDE 选择与项目结构配置均有覆盖,适合前端团队作为快速上手企业级框架的参考读物。
资源包内为 1 份 PDF 电子文档,容量约 2.65MB,内容按目录模块组织,便于按章节查阅和反复对照练习。目前已有 191 人学习下载;文档除基础概念外,还提供详细的代码示例、操作步骤和项目构建说明,其中 Trial 与 Commercial 许可项目的创建流程分别给出,模板选择与构建步骤清晰,便于按自身授权情况直接落地。同时推荐官方文档、视频教程和社区支持等扩展资源,可帮助读者从零掌握数据网格、图表、表单等常用组件的用法,并在实际项目中灵活应用。
1. Sencha Ext JS 7.8 不是给个人站长的框架:先想清楚这三个问题
我第一次见到 Ext JS 7.8 的工程,第一反应是“这都什么年代了还有人用这套”。后来在某公司接手一个数据管理平台——左侧菜单、主从表联动、几十个字段的动态表单、还有一堆汇总图表——用通用前端框架从零拼组件少说一个月,而 Ext JS 的 Grid、Form、Chart 是开箱即用的。这个框架的典型战场是后台管理系统和数据密集型应用,不是个人博客,也不是营销页。决定要不要入坑,先看三件事:团队能不能接受商业授权成本;业务是不是以表格、表单、复杂交互为主;有没有人愿意啃 Sencha Cmd 这条工具链。这篇笔记就把从安装 SDK、用模板生成工程,到手动创建和封装组件的全过程过一遍,你跟着跑通一个最小应用,再判断值不值得投入。
2. 安装与首次启动:把 Sencha Cmd 和 SDK 配到能生成项目
2.1 先分清 Sencha Cmd、SDK 和 Workspace 的分工
很多人拿到 Ext JS 的第一个动作是下载 zip 解压扔进项目里,这个思路在其他框架下没问题,在 Ext JS 这里会立刻卡住。Ext JS 的工程不是“引入一个 js 文件就能跑”的形态,至少在 7.8 这个时代不是。它的类系统按命名空间动态加载,主题是 Sass 编译产物,连 index.html 都是靠 Microloader 生成的清单来驱动,这三样东西都要靠 Sencha Cmd 这根指挥棒。
Cmd 是 Java 写的命令行工具链,负责脚手架生成、依赖解析、Sass 主题编译和产物打包。SDK 是框架本体,包含类库、主题源文件和自带示例。Workspace 是包在应用外面的容器,一个 workspace 可以装多个应用,共享同一份 SDK 和公共组件包。关系可以简化成:Cmd 是工具,SDK 是原料,workspace 是车间。
为什么不能绕过 Cmd?技术上可以,但你会同时丢掉浏览器端的动态类加载清单、主题编译管线、生产构建的压缩与指纹。更现实的是,社区里几乎所有关于 Ext JS 的排错方案都是建立在 Cmd 工程之上的,脱离 Cmd 等于把自己丢进黑匣子。
提示:安装前先确认授权。Ext JS 是商业授权框架,试用版会在界面上带水印,团队评估时把授权费用算进成本,别等项目跑了一半才补。
2.2 下载与 PATH 配置:三行命令验证环境
下载环节没什么可炫技的,去 Sencha 官方下载页拿 Sencha Cmd 安装包和 Ext JS 7.8 SDK 压缩包。Cmd 装完是一个独立目录,内部按版本号分层;Windows 安装包通常会自动写 PATH,macOS 和 Linux 下需要手动加。版本号以实际下载到的为准,不要照抄别人的路径,不同年份的 Cmd 目录命名规则不完全一样。
# macOS / Linux 下把 Sencha Cmd 加入 PATH,版本号按实际安装目录写 export PATH="$HOME/bin/Sencha/Cmd/8.0.0.314:$PATH" # 验证 Cmd 是否可用 sencha which # Cmd 依赖 Java 运行时,顺手确认一下 java -version逻辑说明:export只对当前终端会话生效,建议写入~/.bashrc或~/.zshrc,避免每次开新终端都补一遍。sencha which会输出当前生效的 Cmd 版本号,如果提示 command not found,八成是 PATH 写错了,先检查路径里有没有多写或少写一层目录。java -version是确认 JVM 存在,新版 Cmd 安装包会自带运行时,但如果你装的是早期的 7.x Cmd,系统里没有 Java 会在后续任何子命令上报错。
参数说明:Cmd 目录下的版本号子目录每次升级都会变,PATH 里写死版本号是常见做法,但升级后要记得同步修改,否则你会对着旧版本排查一个已经修掉的问题。
| 命令 | 作用 | 失败时看什么 |
|---|---|---|
sencha which | 输出 Cmd 版本号 | PATH 配置错误 |
sencha -sdk <路径> generate app | 生成应用骨架 | SDK 路径指向错误 |
sencha app watch | 启动开发服务器并增量编译 | 端口被占用、主题编译报错 |
2.3 生成 Workspace 与应用:一条命令链跑出第一个页面
环境就绪后按下面顺序执行。我习惯先建 workspace,再在 workspace 里生成应用,这样以后加第二个应用时,主题和公共包都不用重新折腾。
# 1. 初始化工作区,路径不存在会自动创建 sencha workspace init /data/workspace/myent-demo # 2. 进入工作区,用 SDK 生成一个叫 MyEnt 的应用 cd /data/workspace/myent-demo sencha -sdk /opt/Sencha/ext-7.8.0 generate app MyEnt ./apps/myent-admin # 3. 进入应用目录,启动开发模式 cd apps/myent-admin sencha app watch逻辑说明:workspace init会生成 workspace.json 和 packages 目录;generate app从 SDK 的模板把一套标准工程拷到./apps/myent-admin,包括 app.json、app 目录、sass 目录和 index.html。sencha app watch是开发模式的核心命令:它起一个本地静态服务器,默认端口 1841,同时监听源码变化做增量编译,改一行保存,浏览器刷新就能看到效果。
参数说明:-sdk后面跟的是 SDK 解压后的根目录,里面应该有 ext-all.js 和 packages 目录,不要指到 ext 子目录。MyEnt是应用名,同时也是命名空间前缀,它决定后面所有类的包名,比如MyEnt.view.main.Main,起名时别用带横杠的名字。./apps/myent-admin是应用目录,多个应用就并列放在 apps 下。
浏览器打开 http://localhost:1841,看到带主题的空白主界面就说明安装链路通了。如果 watch 起不来,十有八九是 SDK 路径写错或 Java 环境有问题,把终端里的错误日志往上翻,它比浏览器控制台更早暴露问题。
3. 模板与工程骨架:app.json、启动链和 MVVM 三件套
3.1 app.json:先读懂这几个配置项再动模板
generate app生成的应用根目录下有个 app.json,它就是整个工程的配置中心。模板长什么样、主题是哪套、依赖哪些本地包,全在这里声明。先看一份最小化的:
{ "name": "MyEnt", "namespace": "MyEnt", "type": "modern", "framework": "ext", "theme": "theme-triton", "requires": [], "sdk": "/opt/Sencha/ext-7.8.0", "output": { "base": "${workspace.dir}/build/${build.id}", "page": "index.html" } }逻辑说明:name是应用名也是产物文件名前缀;namespace决定类的根命名空间;type声明界面风格取向,7.x 统一后的现代应用用 modern,老项目迁移才用 classic;theme指定主题;requires里放需要引用的本地 package 名;sdk指向 SDK 根目录,生成工程时会被自动填入绝对路径。
参数说明:theme常见值有 theme-triton、theme-material、theme-classic 等。换主题只要改这里再重启 watch,Cmd 会重新编译 Sass,不需要手改任何 CSS。output.base一般保持默认,生产构建会用到。
| 配置项 | 作用 | 容易踩的坑 |
|---|---|---|
name | 应用名与产物前缀 | 不要用横杠,影响目录命名 |
type | modern / classic | 7.x 新工程一律 modern |
theme | 主题包名 | 拼写错误不会立刻报错,而是样式丢失 |
requires | 本地 package 依赖 | 漏写后 xtype 一直 Unknown |
sdk | SDK 绝对路径 | 换机器后要改,建议写成环境变量 |
3.2 启动链:index.html 为什么是空的
打开生成工程里的 index.html,会发现 body 是空的。这不是模板没生成好,而是 Ext JS 7.x 的标准做法:界面全部运行时渲染,入口只留一个引导脚本。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>MyEnt</title> <script src="bootstrap.js">Ext.application({ name: 'MyEnt', extend: 'MyEnt.Application', requires: ['MyEnt.view.main.Main'], mainView: 'MyEnt.view.main.Main' });逻辑说明:这是整个应用的入口配置对象。extend指向应用主类;requires声明首屏依赖;mainView指定主视图的类名,应用启动后会自动创建它并挂到视口上。Application 类里还有一个 launch 方法:
Ext.define('MyEnt.Application', { extend: 'Ext.app.Application', launch: function () { // 应用完全就绪后执行,适合读取本地缓存、注册全局事件 Ext.GlobalEvents.fireEvent('app-ready'); } });launch在首屏数据渲染完成后触发,适合做不阻塞的初始化。耗时逻辑(比如批量请求)不要放在 launch 里,会明显拖慢首屏。
3.3 ViewModel 与 ViewController:模板里最容易被忽略的绑定关系
模板生成的主界面自带 MVVM 三件套:视图类负责结构,ViewModel 负责数据状态,ViewController 负责交互逻辑。它们靠 alias 关联:视图里写controller: 'main',框架就去查 alias 为controller.main的类。下面这个例子是手动新建一个工作台视图,完整演示三者关系。
Ext.define('MyEnt.view.dashboard.Main', { extend: 'Ext.panel.Panel', xtype: 'dashboard-main', controller: 'dashboard', viewModel: { type: 'dashboard' }, title: '工作台', tbar: [{ xtype: 'button', text: '刷新数据', handler: 'onRefreshClick' }], items: [{ xtype: 'label', bind: '{statusText}' }] });对应的 ViewModel 类:
Ext.define('MyEnt.view.dashboard.MainViewModel', { extend: 'Ext.app.ViewModel', alias: 'viewmodel.dashboard', data: { statusText: '就绪' }, formulas: { welcome: function (get) { return get('user.name') + ',欢迎回来'; } } });对应的 ViewController 类:
Ext.define('MyEnt.view.dashboard.MainViewController', { extend: 'Ext.app.ViewController', alias: 'controller.dashboard', onRefreshClick: function () { this.getViewModel().set('statusText', '刷新中……'); } });逻辑说明:bind: '{statusText}'是单向绑定语法,把ViewModel.data.statusText同步到 label 的 text。按钮 handler 写成字符串'onRefreshClick',框架会自动在对应 ViewController 里找同名方法,这是模板工程默认的行为约定,比手写匿名函数更好排查。
参数说明:ViewModel 类的命名必须是“视图类名 + ViewModel”,且和视图类放在同一个包路径下;ViewController 同理。alias 的前缀viewmodel.和controller.是硬约定,写错不会报编译错误,只会让绑定或事件静默失效,这是新手最容易踩的翻车点。
提示:formulas 里的函数每次依赖项变化时重新计算,适合做基于原始数据的派生展示,不要在 data 里直接放需要计算的字段。
4. 组件创建:从 xtype 到自定义组件的完整路径
4.1 为什么推荐声明式 xtype,而不是到处 new
页面里写new Ext.Button({ renderTo: ... })能用,但这是老写法,有三个问题:组件被立即实例化,无法享受框架的延迟渲染;需要手动管理销毁,否则页面跳转后组件还挂在 DOM 上;renderTo 把渲染位置写死,组件一旦被放进容器里就会渲染到错误位置。正确姿势是让容器通过 xtype 在渲染阶段按需创建组件。
// 不推荐:立即实例化 + 手动挂载 var btn = new Ext.Button({ text: '旧写法', renderTo: Ext.getBody() }); // 推荐:声明式,交给容器在渲染时创建 Ext.create('Ext.panel.Panel', { title: '示例面板', items: [ { xtype: 'button', text: '新写法', handler: function () { /* ... */ } } ] });逻辑说明:Ext.create适合创建“根组件”,比如一个需要挂到 body 上的 Window;而 items 数组里的子组件一律写 xtype 字符串。框架在父容器渲染时才实例化子组件,父容器销毁时统一销毁,这套生命周期是 Ext JS 组件体系的地基,也是它与直接操作 DOM 的框架最本质的区别。
参数说明:xtype 是组件类的短名称,Ext.Button的 xtype 是button,Ext.panel.Panel的 xtype 是panel。框架内部维护一张 xtype 到类的映射表,自定义组件注册后同样能用这套机制。
4.2 组合一个真实查询面板:表单、Store 与 Grid 联动
查询面板是后台系统里最常见的组件组合。拆成三块:表单收集条件、Store 负责请求数据、Grid 负责展示。先建 Store:
Ext.define('MyEnt.store.OrderStore', { extend: 'Ext.data.Store', alias: 'store.orders', fields: ['orderNo', 'amount', 'status', 'createDate'], proxy: { type: 'ajax', url: '/api/orders', reader: { type: 'json', rootProperty: 'data' } }, autoLoad: false });逻辑说明:alias: 'store.orders'让视图里可以用store: { type: 'orders' }引用它。fields 定义记录字段,必须和接口返回的 JSON 字段对应。proxy 配置请求方式,rootProperty: 'data'告诉 Reader 从返回 JSON 的 data 字段里取数组,这是后端常见的分页包装格式。
参数说明:autoLoad设为 false 是故意不自动加载。查询面板应该在用户点击查询后才发起请求,避免页面一打开就无差别请求全量数据。
接着建视图,把表单和表格放进一个容器:
Ext.define('MyEnt.view.order.OrderView', { extend: 'Ext.container.Container', xtype: 'order-view', controller: 'order', layout: 'vbox', items: [{ xtype: 'form', reference: 'searchForm', layout: 'hbox', bodyPadding: 10, items: [{ xtype: 'textfield', name: 'orderNo', fieldLabel: '订单号', margin: '0 10 0 0' }, { xtype: 'datefield', name: 'beginDate', fieldLabel: '开始日期', margin: '0 10 0 0' }, { xtype: 'button', text: '查询', handler: 'onSearchClick' }] }, { xtype: 'ordergrid', reference: 'resultGrid', flex: 1 }] });订单表格:
Ext.define('MyEnt.view.order.OrderGrid', { extend: 'Ext.grid.Panel', xtype: 'ordergrid', store: { type: 'orders' }, columns: [ { text: '订单号', dataIndex: 'orderNo', width: 140 }, { text: '金额', dataIndex: 'amount', width: 100, align: 'right', renderer: 'usMoney' }, { text: '状态', dataIndex: 'status', width: 80 } ] });逻辑说明:columns 里 text 是表头,dataIndex 对应 Store 字段名。renderer 可以是函数也可以是内置格式化字符串,'usMoney'会把数字渲染成带货币符号的格式。reference: 'resultGrid'是给组件起一个可供控制器查询的引用名,配合getReferences()使用。
控制器里的查询逻辑:
Ext.define('MyEnt.view.order.OrderViewController', { extend: 'Ext.app.ViewController', alias: 'controller.order', onSearchClick: function () { var grid = this.getReferences('resultGrid'); var form = this.getReferences('searchForm'); var store = grid.getStore(); var values = form.getValues(); store.getProxy().setExtraParam('orderNo', values.orderNo); store.getProxy().setExtraParam('beginDate', values.beginDate); store.load(); } });逻辑说明:setExtraParam会把参数附加到 AJAX 请求的 query string 上,这是查询类场景的标准做法,比手动拼接 URL 可靠。form.getValues()返回所有带 name 字段的值对象,name 属性在这一步是刚需。
4.3 封装自定义组件:config 的 getter/setter 与混入
业务里经常要复用“带徽标的卡片”这类小组件。封装它的标准姿势是用 config 声明对外接口:
Ext.define('MyEnt.view.common.BadgeCard', { extend: 'Ext.panel.Panel', xtype: 'badgecard', layout: 'fit', config: { badgeText: '新' }, initComponent: function () { var me = this; me.items = [{ xtype: 'container', cls: 'badge-card-body', items: [{ xtype: 'label', cls: 'badge-tag', text: me.getBadgeText() }] }]; me.callParent(); }, updateBadgeText: function (value) { var tag = this.el && this.el.down('.badge-tag'); if (tag) { tag.setHtml(value); } } });逻辑说明:config声明了组件对外暴露的配置项,框架会自动生成 getBadgeText / setBadgeText 两个方法,并在 setter 触发时调用updateBadgeText钩子。外部代码执行setBadgeText('已读')就能更新界面,这是 Ext JS 组件封装的标配模式。
参数说明:initComponent里先组装 items 再callParent,是 Ext JS 5+ 之后的推荐顺序,顺序反了可能导致 items 被父类覆盖。updateBadgeText里用this.el.down('.badge-tag')定位内部元素,组件没渲染完成时 el 不存在,所以要加判空。自定义 xtype 必须全小写,类名用“命名空间.模块.功能”三段式,避免和 SDK 内置类冲突。
多个组件共用行为时用混入:
Ext.define('MyEnt.mixin.Resetable', { mixinId: 'resetable', resetForm: function () { this.getForm().reset(); } }); // 在表单组件里混入 Ext.define('MyEnt.view.order.SearchForm', { extend: 'Ext.form.Panel', mixins: ['MyEnt.mixin.Resetable'], xtype: 'searchform' });mixinId是混入的短标识,把混入类加进 mixins 数组后,resetForm()就出现在组件实例上。适合封装一批组件共用的行为,比如重置、导出、校验提示。
5. 安装与组件阶段最常翻车的 5 个现场:避坑排查
5.1 页面白屏但控制台没报错
现象:打开 dev server 地址,一片白,F12 控制台干净得像没运行过。
原因:这类白屏多半不是代码问题,而是 Microloader 的引导文件缺失。最常见的是手工改过 index.html,把 bootstrap.js 删了或改了>find node_modules -type f -name "theme-*.css" | head -20
拿到实际路径后,把 import 语句写到入口文件所有代码之前,避免样式加载晚于组件渲染导致闪烁。
6. 进阶:把业务组件抽成 Sencha Package,在多个应用里复用
当你手上有两个以上 Ext JS 应用,并且都在用同一批业务组件时,把组件抽成 package 是迟早的事。我见过一个同事把同一个查询面板在三个项目里复制了三份,后来一个字段改动要同步三处,改漏一处线上才暴露,这就是没做 package 的代价。
生成 package 在 workspace 根目录下执行:
sencha package generate my-widgets ./packages/my-widgets生成后的结构:package.json 记录包名和版本号,src 目录放组件类,sass 目录放主题定制。把 BadgeCard 这类通用组件移进包的 src 里,类名改成myWidgets.view.common.BadgeCard之类的命名空间,然后在应用的 app.json 的 requires 数组里加上包名:
{ "requires": ["my-widgets"] }重启sencha app watch,应用里就能直接使用 my-widgets 里声明的 xtype。验证是否生效有个笨但有效的办法:在组件类里暂时写一行 console.log 看会不会打印;或者在生产构建后搜索自定义 xtype 是否进了产物:
sencha app build --production grep -r "badgecard" build/production/MyEnt/ | head我自己的习惯是:一个新组件先在应用里写、跑通、踩完坑,再抽进 package。抽包前先把组件的 config 接口想清楚,接口丑的组件抽出去只会把坑带进更多工程。包内组件尽量不要直接依赖某个应用的 ViewModel 数据,数据从配置项传进去,这样复用面最大。
这套路径跑顺之后,新项目起步就是从 packages 里拖组件,而不是重新写一遍。如果你正卡在“每个后台项目都要重写一遍表单和表格”这个循环里,值得花一个迭代把第一版 package 抽出来,后续的收益是持续吃到的。希望帮到你。
本文还有配套的精品资源,点击获取