友好编译器编码指南
Sencha Cmd的主要组件之一是它的编译器。
本指南描述了如何编写能够从编译器中获益最多的代码,并为未来的框架级别的优化做好准备。
先决条件
在继续进一步阅读之前,推荐先阅读以下指南:
- Sencha Cmd概论
- 在Ext JS 6下使用Sencha Cmd
编译器不是什么
Sencha Cmd编译器并不是像如下这样的工具的替代品:
- YUI Compressor
- Google Closure Compiler
- UglifyJS
这些工具为JavaScript开发人员解决了不同的问题,并且在JavaScript的世界里表现良好,
但是却不了解Sencha框架的特性,比如用Ext.define
来声明类。
框架感知
Sencha Cmd编译器的作用是提供框架感知的优化和诊断。
一旦代码通过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”结尾的路径中。在内部,编译器将源文件和类基本上视为同义词。
它不尝试分割文件以删除不需要的类。
只有完整的文件被选中并包含在输出中。
这意味着,如果源文件中的引用了任何类,那么文件中的所有类都将包含在输出中。为了让编译器能够在类级别自由选择代码,在每个文件中只放置一个类是非常必要的。
类的声明
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的作用域是在全局范围。
包装函数形式
在某些用例中,类声明被包装在一个函数中,为类方法创建一个闭包范围。
在各种各样的形式中,对于编译器来说,函数以返回语句结尾,以文本对象的方式返回类的主体是非常重要的。
其他的技术没有被编译器识别。
函数形式
为了简化下面描述的这种技术的较老形式,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和它的后续版本。
调用函数的方式
在以前的版本中,不支持“函数方式”,因此函数仅仅被立即调用:
Ext.define('Foo.bar.Thing', function () {
return {
// keywords go here ... such as:
extend: '...',
// ...
};
}());
加括号调用函数形式
这种方式和下一种通常用于满足类似JSHint(或JSLint)等的工具。
Ext.define('Foo.bar.Thing', (function () {
return {
// keywords go here ... such as:
extend: '...',
// ...
};
})());
调用加括号函数形式
另一种为了适配JSHint/JSLint的“函数方式”的变体。
Ext.define(‘Foo.bar.Thing’, (function () {
return {
// keywords go here ... such as:
extend: '...',
// ...
};
}()));
关键字
在许多类声明方式中,基本都包含“关键字”。
每个关键字都有自己的语义,但是有很多都有一个共同的“形式”。
使用字符串的关键字
extend和override关键字只接受字符串文本。
在只允许一个关键字可以在声明中使用的情况下,这些关键字是相互排斥的。
使用字符串或字符串数组的关键字
下面的关键字都有相同的使用方式:
- requires
- uses
- alias
- alternateClassName
- xtype
这些关键字的支持方式如下。
只支持一个字符串:
requires: 'Foo.thing.Bar',
//...
支持字符串数组:
requires: [ 'Foo.thing.Bar', 'Foo.other.Thing' ],
//...
混合形式
在对象文本中,使用mixin,成员名称可以使用引号或不使用:
mixins: {
name: 'Foo.bar.Mixin',
'other': 'Foo.other.Mixin'
},
//...
mixin也可以被指定为字符串:
mixins: [
'Foo.bar.Mixin',
'Foo.other.Mixin'
],
//...
这种方法依赖于mixin类的mixinId,但也允许接收类控制mixin顺序。
如果mixin有重叠的方法或属性,并且接收类想要控制那些mixin提供重叠的方法或属性,
那么这一点很重要。
statics关键字
在类的声明里放置这个关键字,并在其中声明属性或方法,这样就不需要在每个实例上声明方法了。
此项必须是一个对象文本。
statics: {
// members go here
},
// ...
singleton关键字
这个关键字在以往只与布尔类型的“true”值使用:
singleton: true,
以下(冗余)使用方式也得到了支持:
singleton: false,
覆盖
在Ext JS 4.1.0和Sencha Touch 2.0中,Ext.define获得了管理重写的能力。
从历史上来看,重写是用来修补代码以处理bug或添加增强功能的。
由于执行Ext.override
方法需要一定时间,所以在引入动态加载器时,这种用法非常复杂。
而且,在具有许多覆盖的大型应用程序中,并非所有的页面或构建都需要代码库中的所有重写(例如,如果目标类没有被引入)。
一旦类系统和加载器理解重写,所有这些都改变了。
这一趋势只会在Sencha Cmd中延续。
编译器理解覆盖和它们的依赖关系和加载顺序问题。
在将来,编译器会在死代码被重写取代的方法消除上变得更加明显。
使用下面所述的管理覆盖的方法,可以在Sencha Cmd中实现对代码的优化。
标准覆盖形式
下面是覆盖的标准形式。
名称空间的选择有点武断,但请参阅下面的建议。
Ext.define('MyApp.patches.grid.Panel', {
override: 'Ext.grid.Panel',
...
});
用例
有了使用Ext.define
来管理覆盖的能力,新的语法实现已经被打开,并且正在被积极地利用。
例如,在Sencha Architect 的代码生成器中,
在框架内部,
将诸如Ext.Element这样的大型类分解成更易于管理和内聚的部分。
覆盖的补丁
作为补丁的覆盖是历来使用的用例,因此在实践中是最常见的。
注意:在修补代码时要小心。
当使用覆盖本身时,不支持覆盖覆盖框架方法的最终结果。
在升级到一个新的框架版本时,应该仔细检查所有重写。
也就是说,有时需要覆盖框架方法。
最常见的情况是修复bug。
在本例中,标准覆盖形式是理想的。
事实上,Sencha Support
有时会为客户提供这种形式的补丁。
然而,一旦提供了这些补丁并在不再需要的时候删除它们,这对于前面提到的审查过程是一个问题。
命名的建议:
在与目标的顶级命名空间相关联的命名空间中组织补丁。
例如,“MyApp.patches“与”Ext“命名空间”关联。
如果涉及到第三方代码,那么可能会选择另一个级别或命名空间来对应其相应的顶级命名空间。
然后,使用匹配的名称和子命名空间来命名覆盖。
在前面的例子:Ext - > .grid.Panel MyApp.patches)
覆盖部分类
在处理代码生成(如同在Sencha Architect中),通常一个类包含两个部分:一个机器生成的,一个人为手工编辑的。
在一些语言中,正式支持的概念是partial class
或 a class-in-two-parts
。
在使用重写(覆盖)过程中,您可以像正面这样清晰地管理:
在./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.
或
ThingOverrideFoo.bar.ThingGenerated
。
这样一来,类的各个部分就会在列表中进行排序。
覆盖各方面
基类在面向对象设计的一个常见的问题是“肥基类”(fat base class)。
这是因为一些行为(方法)适用于所有类。
当这些行为(或特性)并不被子类需要,然而,他们不能被轻易删除,因为他们是一些大型的基类的实现部分。
使用覆盖,这些特性可以被收集在他们自己的层次结构内,然后在需要的时候使用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
],
...
});
这一功能现在可以通过引用它来使用:
...
requires: [
'Foo.feature.grid.Panel'
]
或者使用一个适当的“引导(bootstrap)”文件(参见Sencha Cmd中的工作区)
...
requires: [
'Foo.feature.*'
]
命名的建议:
- 根据命名空间组织生成的代码vs手动编辑的代码。
这允许使用通配符来实现该特性的所有方面。
在重写时使用 requires
和uses
在重写得支持这两个关键字的使用。
使用requires
可能会限制编译器重新排序重写代码的能力。
使用callParent
和callSuper
为了支持所有这些新用例,callParent
在Ext JS 4.0和Sencha Touch 2.0中得到了增强,
以“调用下一个方法”。
“下一个方法”可能是覆盖的方法或继承的方法。
只要有下一个方法,callParent
就会调用它。
另一种看待这个问题的方式是,callParent
对所有类型的Ext.define
都是相同的,它们是类或覆盖(重写)。
虽然这在某些领域有所帮助,但不幸的是,它让绕过最初的方法(作为补丁或bug修复)更困难。
Ext JS 4.1(含)以后的版本和Sencha Touch 2.1(含)以后的版本,提供了一个名为callSuper
的方法,可以绕过覆盖的方法。
在将来的版本中,编译器将使用这种语义差异来淘汰重写方法的死代码。
覆盖的兼容性
从版本4.2.2开始,覆盖可以根据框架版本或其他包的版本声明它们的兼容性(compatibility
)。
这对于选择性地应用那些与目标类版本不兼容的已经被安全忽略掉的补丁是很有用的。
最简单的用例是测试框架版本的兼容性:
Ext.define('App.overrides.grid.Panel', {
override: 'Ext.grid.Panel',
compatibility: '4.2.2', // only if framework version is 4.2.2
//...
});
数组被当作一个OR
(或的关系),因此,如果任何规格匹配,那么覆盖是兼容的。
Ext.define('App.overrides.some.Thing', {
override: 'Foo.some.Thing',
compatibility: [
'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',
'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继续发展,将会继续引入新的诊断消息帮助指出这些指南间的差异。
一个很好的起点是思考如何使用这些信息来帮助形成自己的内部代码风格规则和实践方法。