Sencha Cmd里一个重要的东西是它的编译器. 本指南描述如何编写代码才能最有效利用编译器,并为以后的 框架感知(framework-aware) 优化做准备.
前提
继续之前推荐先阅读下面的指南:
编译器不是什么?
Sencha Cmd 编译器 不是 对下面工具的替代:
这些工具为 JavaScript 开发者解决不同的问题, 是非常好用的 JavaScript 工具, 但是却不能理解 Sencha 框架的功能,比如Ext.define
这中声明类的方式.
框架感知(Framework Awareness)
Sencha Cmd 编译器的功能是提供 框架感知(framework-aware) 优化 和 诊断. 当代码流经 Sencha Cmd 编译器之后, 它才可以被更通用的工具所处理.
这种优化,在对浏览器“消化”JavaScript 代码的时间效率方面有显著的提升, 特别是对传统浏览器.
然而,为了编译器能提供了这些好处, 现在来看看编译器可以“理解”的编码约定,这是很重要的,然后才能帮你优化. 下面的编码约定,以保证你的代码在现在和以后都可以最大限度的利用 Sencha Cmd.
代码结构
动态加载程序和之前的 JSBuilder 在关于类是如何组织的这方面,都做了某些假设, 但他们并没有受到不遵循这些原则的影响. 这些原则和 Java 很像.
总的来说,这些原则是
- ,每一个 JavaScript 源文件都要在全局作用域下包含一个
Ext.define
声明. - 源文件名必须和类名最后一段一样,比如有这段代码的
Ext.define("MyApp.foo.bar.Thing", ...
文件的名字是“Thing.js”. - 所有源文件都放在基于特定类型命名空间的目录结构下. 比如,
Ext.define("MyApp.foo.bar.Thing", ...
, 这个源文件位于“/foo/bar”这个目录下.
在内部, 编译器将源文件和类视为基本同义. 这使得编译器不需要取分割源文件,然后移除不需要的类. 包含在输出中的都是一整个文件. 意思就是说如果某个源文件中的某个类被 引入(require) 了, 这个源文件里面的所有类都会包含在输出中.
为了让编译器可以再 类级 来自由选择代码, 一个文件中只放一个类还是很有必要的.
类声明
Sencha 类系统 提供了Ext.define
函数来支持高级别,面向对象的编程. 编译器把Ext.define
视为“声明式”编程的一种真正的形式,并相应地处理这个 “类声明” .
显然如果Ext.define
理解为一个声明,类主体的内容不能在代码中动态地构造. 虽然这种做法是罕见的,但它是有效的 JavaScript. 但是, 正如我们看到的下面代码形式, 这与编译器能够理解代码的能力是对立的. 动态类声明通常用来做那些能被编译器其它功能更好地处理的事情. 想了解更多关于此功能, 请看 Sencha 编译器参考.
编译器理解这些 “关键词”:
requires
uses
extend
mixins
statics
alias
singleton
override
alternateClassName
xtype
为了让编译器能够组织你的类声明, 你应该遵守下面几种形式.
标准形式
大部分类使用下面的简单声明:
Ext.define('Foo.bar.Thing', {
// keywords go here ... such as:
extend: '...',
// ...
});
第二个参数是类主体,类主体作为类声明的内容,被编译器所处理.
注意: 所有的形式, 都在全局作用域下调用 Ext.define
.
包裹函数的形式
在有些情况下,类声明主体被包裹在一个函数中,以创建一个闭包作用域. 这个函数需要用 return
声明符结尾,以一个对象的方式返回类的主体,这对编译器至关重要. 其他方式都不能被编译器识别.
函数的形式(Function Form)
为了简化下面要说的那种旧的形式, 如果第二个参数传递的是一个函数,Ext.define
也可以识别出来, 它会调用这个函数来产生类主体. 而且它还把当前类的引用作为唯一的参数传递进了这个函数,这样函数内就可以在这个闭包作用域中利用该类的静态成员. 框架内部来说, 这也是为什么要使用闭包作用域的一个普遍原因.
Ext.define('Foo.bar.Thing', function (Thing) {
return {
// keywords go here ... such as:
extend: '...',
// ...
};
});
注意: 这种形式只在 Ext JS 4.1.2 及以后, 还有Sencha Touch 2.1 及以后的版本支持.
调用函数的形式(Called Function Form)
在旧的版本中, 不支持“函数的形式(Function Form)”, 所以这个函数直接被调用了:
Ext.define('Foo.bar.Thing', function () {
return {
// keywords go here ... such as:
extend: '...',
// ...
};
}());
调用 加了括弧的函数 的形式(Called-Parenthesized Function Form)
这种形式,还有下面的那种,通常为了适配像 JSHint (或 JSLint)这样的工具.
Ext.define('Foo.bar.Thing', (function () {
return {
// keywords go here ... such as:
extend: '...',
// ...
};
})());
加了括弧的函数调用 的形式(Parenthesized-Called Function Form)
另一种直接调用 “函数的形式”,同样是为了适配 JSHint/JSLint.
Ext.define('Foo.bar.Thing', (function () {
return {
// keywords go here ... such as:
extend: '...',
// ...
};
}()));
关键词
各种形式的类声明都包含“关键词”. 每个关键字有它自己的语义, 但它们还是有共性的.
接收字符串的关键词
extend
和 override
关键词只接收字符串字面量.
这2个关键词互斥,一个函数声明只能用其中一个.
接收字符串或字符串数组的关键词
下面的关键词都是这样:
requires
uses
alias
alternateClassName
xtype
它们接收的值类型可以是下面的类型.
仅字符串:
requires: 'Foo.thing.Bar',
//...
字符串数组:
requires: [ 'Foo.thing.Bar', 'Foo.other.Thing' ],
//...
混入(mixins)
的形式
使用一个对象, 对象的key可以加引号,也可以不加:
mixins: {
name: 'Foo.bar.Mixin',
'other': 'Foo.other.Mixin'
},
//...
混入(Mixins) 也可以是一个字符串数组:
mixins: [
'Foo.bar.Mixin',
'Foo.other.Mixin'
],
//...
这种方法需要依赖混入(mixin) 类的mixinId
,但是它可以让 接收混入的类(也就是类声明) 可以控制 混入(mixin) 的顺序. 特别是当 混入(mixins)类 有重复函数或属性的时候,这点尤为重要,以便让 接收混入的类 可以控制到底使用哪一个 混入(mixin)类 提供的函数或属性.
statics
(静态)关键词
放在这个关键词里面的,属于类的成员属性和函数, 与实例的成员属性和函数 相对. 必须是一个对象.
statics: {
// 成员写这里
},
// ...
singleton
(单例)关键词
这个关键词一直都只接收“true” 值:
singleton: true,
下面这种 (没有意义) 用法也支持:
singleton: false,
覆写(Overrides)
在 Ext JS 4.1.0 和 Sencha Touch 2.0 中, Ext.define
新增加了一种可以管理 覆写(overrides) 的能力. 一直以来, 覆写(overrides) 被用来给代码打补丁,以便解决一些bug,或者添加增强功能. 在引入了动态加载之后,这种用法比较复杂,因为执行Ext.override
函数需要特定的时机. 而且, 在含有大量 overrides 的大型应用程序中, 并不是任何页面都需要所有的 overrides (比如, 没有用到某个组件类,就可以不包含这个类的 overrides).
当类系统和加载器理解了 overrides 之后,这一切都发生了改变. This trend only continues with Sencha Cmd. 编译器理解了 overrides 和 它们的依赖的影响,还有加载顺序问题.
未来, 编译器会变的更加智能,以便消除 override 不存在的函数 的那种无用代码. 使用下述可被管理的 overrides 写法,一旦 Sencha Cmd 实现了这个功能,你的代码就可以支持这种优化了.
标准的Override形式
下面是标准的覆写(Override)形式. 命名空间有点随意, 不过可以遵循下面的建议.
Ext.define('MyApp.patches.grid.Panel', {
override: 'Ext.grid.Panel',
...
});
使用案例
随着使用 Ext.define
来管理 overrides, 新的风格已经来袭,并被积极利用. 比如,在Sencha Architect 的代码生成器和框架内, 把大的类比如 Ext.Element
分解成了更易于管理的小的部分.
Overrides用来修补代码
Overrides 用来修补代码是一直以来的用法,也是最常见的做法.
警告: 修补代码时要小心. 虽然支持使用 override, 但是使用 override 覆盖框架方法的结果是未知的. 无论何时升级到新版本框架, 所有的 overrides 代码都应该重新审查.
也就是说, 有时候必须覆盖框架方法. 最常见的原因是用来修复bug. 标准的覆写(Override)形式是最理想的方式. 实际上, Sencha 技术支持有时候会以这种形式提供补丁给客户. 审查程序代码的时候,如果不再需要这个补丁,那么就应该删除它们.
命名建议:
补丁的命名空间 要替换掉 被 override 的类的顶层命名空间. 例如, “MyApp.patches” 替换 “Ext” 命名空间. 假如涉及到的是第三方代码,那么顶层命名空间可能就是其它的层级或命名空间. 这样, 用一个合适的顶层命名空间 加上原本的 子命名空间作为 override 的名称. 之前的例子则是:
Ext -> MyApp.patches).grid.Panel
把 Overrides 看做 部分类(Overrides as Partial Classes)
当和 代码生成(如 Sencha Architect) 打交道的时候, 通常把类看做2部分: 一部分是机器生成的代码,另一部分是用户修改的代码. 在有些语言中, 有一个“部分类”的概念或者叫class-in-two-parts.
使用 override, 你可以很清晰地管理:
在 ./foo/bar/Thing.js
里:
Ext.define('Foo.bar.Thing', {
// NOTE: This class is generated - DO NOT EDIT...
requires: [
'Foo.bar.custom.Thing'
],
method: function () {
// some generated method
},
...
});
在 ./foo/bar/custom/Thing.js
里:
Ext.define('Foo.bar.custom.Thing', {
override: 'Foo.bar.Thing',
method: function () {
this.callParent(); // calls generated method
...
},
...
});
命名建议:
- 用 命名空间 来区分管理 生成的代码 vs. 手写的代码.
- 假如不用命名空间, 可以考虑用后缀来区分, 比如
Foo.bar.ThingOverride
和Foo.bar.ThingGenerated
,这样,类的2个部分可以并排列在一起.
把 Overrides 看做 特征方面(Overrides as Aspects)
面向对象设计中普遍会担心的一个问题是“臃肿的基类”. 这是因为所有类都有某些行为特性. 然而, 假如它们是一些很大的基类,当某些行为 (或者 特征features)不再需要了, 它们并不是很容易就能移除的.
使用 overrides, 这些特征(features)可以在它们自己的层次结构内被收集, 然后按需requires
.
在./foo/feature/Component.js
中:
Ext.define('Foo.feature.Component', {
override: 'Ext.Component',
...
});
在./foo/feature/grid/Panel.js
中:
Ext.define('Foo.feature.grid.Panel', {
override: 'Ext.grid.Panel',
requires: [
'Foo.feature.Component' // since overrides do not "extend" each other
],
...
});
可以 require 它,以使用这个 特征:
...
requires: [
'Foo.feature.grid.Panel'
]
或者使用合适的 “引导”文件 (请看Sencha Cmd 的 Workspace
...
requires: [
'Foo.feature.*'
]
命名建议:
- 用 命名空间 来区分管理 生成的代码 vs. 手写的代码. 这样可以支持使用通配符来引入 特征的所有方面.
在 Override 中使用 requires
和 uses
overrides 支持这几个关键词. requires
可能会限制编译器重排序代码的能力.
使用 callParent
和 callSuper
为了支持所有这些新的用法, 在 Ext JS 4.0 和 Sencha Touch 2.0 中增强了callParent
的“调用下一个函数”的能力. “下一个函数” 可能是被覆写的函数,或者继承来的函数. 只要存在“下一个函数”, callParent
就会调用它.
callParent
对所有形式的Ext.define
都一样使用, 不管是定义类还是定义 overrides.
虽然这对某些方面有帮助, 不幸的是, 绕过原来的函数(补丁或漏洞修补代码)则变得更加困难. Ext JS 4.1 及以后 和 Sencha Touch 2.1 及以后的版本中提供了一个callSuper
函数, 可以绕过被 覆写(overridden) 的函数.
在未来的版本中, 编译器会根据语义的不同来消除被 overridden 函数的无用代码.
Override 兼容性
从 4.2.2 版本开始, overrides 可以基于框架版本或者其他包的版本,来声明compatibility
. 这对选择性地应用补丁很有帮助,可以安全地忽略与当前版本不兼容的补丁代码.
测试框架版本兼容性的示例:
Ext.define('App.overrides.grid.Panel', {
override: 'Ext.grid.Panel',
compatibility: '4.2.2', // only if framework version is 4.2.2
//...
});
如果传递的是一个数组,则数组里各个条件是“或者(OR)”的关系, 只要任意一个满足, 这个 override 就被认为是兼容的.
Ext.define('App.overrides.some.Thing', {
override: 'Foo.some.Thing',
compatibility: [
'4.2.2',
'foo@1.0.1-1.0.2'
],
//...
});
如果想要“并且(AND)”的关系, 可以传入一个对象:
Ext.define('App.overrides.some.Thing', {
override: 'Foo.some.Thing',
compatibility: {
and: [
'4.2.2',
'foo@1.0.1-1.0.2'
]
},
//...
});
因为对象这种形式只是一个递归的检查,所以可以嵌套使用:
Ext.define('App.overrides.some.Thing', {
override: 'Foo.some.Thing',
compatibility: {
and: [
'4.2.2', // exactly version 4.2.2 of the framework *AND*
{
// either (or both) of these package specs:
or: [
'foo@1.0.1-1.0.2',
'bar@3.0+'
]
}
]
},
//...
});
关于版本语法的详细内容, 请看 Ext.Version
的checkVersion
函数.
总结
随着Sencha Cmd继续发展, 它不断推陈出新, 以帮助指出偏离这些指导方针的行为,并给出诊断信息.
上面的信息如何影响你自身内部代码风格的规范和实践,会成为一个良好的开端.