编辑 | blame | 历史 | 原始文档

Compiler-Friendly Code Guidelines

One of the major components new to V3 of Sencha Cmd is the compiler. This guide tells you
all about how to write code that gets the most out of the compiler today, but also to help
your code base immediately benefit from the framework-aware optimizations that we have
planned for future releases.

{@img ../command/sencha-command-128.png}

What The Compiler Is Not

For starters, it is important to explain that th Sencha Cmd compiler is not a replacement
for tools like these:

These tools solve different problems for JavaScript developers in general. These tools are
very good at the world of JavaScript but have no understanding of Sencha framework features
such as Ext.define for declaring classes.

Framework Awareness

The role of the Sencha Cmd compiler is then to provide framework-aware optimizations and
diagnostics. Once code has passed through the Sencha Cmd compiler, it is ready to go on
to more general tools like the above.

In early testing, these kinds of optimizations have shown to significantly improve "ingest"
time of JavaScript code by the browser... especially on legacy browsers.

In order for the compiler to provide these benefits, however, it is now important to look
at coding conventions that it can "understand" and therefore optimize for you. Following
the conventions described in this guide will ensure that your code is positioned to get
the most from Sencha Cmd today and in the future.

Code Organization

The dynamic loader and the previous JSBuilder have always made certain assumptions about
how classes are organized, but they were not seriously impacted by failure to follow those
guidelines. These guidelines are very similar to Java.

To recap, these guidelines are:

  • Each JavaScript source file should contain one Ext.define statement at global scope.
  • The name of a source file matches the last segment of the name of the defined type (e.g.,
    the name of the source file containing Ext.define("MyApp.foo.bar.Thing", ... would be
    "Thing.js".
  • All source files are stored in a folder structure that is based on the namespace of the
    defined type. For example, given Ext.define("MyApp.foo.bar.Thing", ..., the source file
    would be in a path ending with "/foo/bar".

Internally, the compiler views source files and classes as basically synonymous. It makes
no attempt to split up files to remove classes that are not required. Only complete files
are selected and included in the output. This means that if any class in a source file is
required, all classes in the file will be included in the output.

To give the compiler the freedom to select code at the class-level, it is essential to put
only one class in each file.

Class Declaration

The Sencha Class System provides the Ext.define function to enable higher-level, object
oriented programming. The compiler takes the view that Ext.define is really a form of
"declarative" programming and processes the "class declaration" accordingly.

Clearly if Ext.define is understood as a declaration, the content of the class body cannot
be constructed dynamically in code. While this practice is rare, it is valid JavaScript.
But as we shall see below in the code forms, this is antithetical to the compiler's
ability to understand the code is parses. Dynamic class declarations are often used to do
things that can better be handled by other features of the compiler. For more on these
features, see the Sencha Compiler Reference.

The compiler understands the "keywords" of this declarative language:

  • requires
  • uses
  • extend
  • mixins
  • statics
  • alias
  • `singleton
  • override
  • alternateClassName
  • xtype

In order for the compiler to recognize your class declarations, they need to follow one of
the following forms.

Standard Form

Most classes use simple declarations like this:

Ext.define('Foo.bar.Thing', {
    // keywords go here ... such as:

    extend: '...',

    // ...
});

The second argument is the class body which is processed by the compiler as the class
"declaration".

** In all forms, Ext.define should be called at global scope. **

Wrapped Function Forms

In some use cases the class declaration is wrapped in a function to create a closure scope
for the class methods. In all of the various forms, it is critical for the compiler that
the function end with a return statement that returns the class body as an object
literal. Other techniques are not recognized by the compiler.

Function Form

To streamline the older forms of this technique described below, Ext.define understands
that if given a function as its second argument, that it should invoke that function to
produce the class body. It also passes the reference to the class as the single argument
to facilitate access to static members via the closure scope. Internally to the framework,
this was the most common reason for the closure scope.

Ext.define('Foo.bar.Thing', function (Thing) {

    return {
        // keywords go here ... such as:

        extend: '...',

        // ...
    };
});

NOTE: This form is only supported in Ext JS 4.1.2 and Sencha Touch 2.1 (or newer).

Called Function Form

In previous releases, the "Function Form" was not supported, so the function was simply
invoked immediately:

Ext.define('Foo.bar.Thing', function () {

    return {
        // keywords go here ... such as:

        extend: '...',

        // ...
    };
}());

Called-Parenthesized Function Form

This form and the next are commonly used to appease tools like JSHint (or JSLint).

Ext.define('Foo.bar.Thing', (function () {

    return {
        // keywords go here ... such as:

        extend: '...',

        // ...
    };
})());

Parenthesized-Called Function Form

Another variation on immediately called "Function Form" to appease JSHint/JSLint.

Ext.define('Foo.bar.Thing', (function () {

    return {
        // keywords go here ... such as:

        extend: '...',

        // ...
    };
}()));

Keywords

The class declaration in its many forms ultimately contains "keywords". Each keyword has
its own semantics, but there are many that have a common "shape".

Keywords using String

The extend and override keywords only accept a string literal.

These keywords are also mutually exclusive in that only one can be used in any declaration.

Keywords using String or String[]

The following keywords all have the same form:

  • requires
  • uses
  • alias
  • alternateClassName
  • xtype

The supported forms for these keywords are as follows.

Just a string:

requires: 'Foo.thing.Bar',
//...

An array of strings:

requires: [ 'Foo.thing.Bar', 'Foo.other.Thing' ],
//...

Forms of mixins

Using an object literal, the name given the mixin can be quoted or not:

mixins: {
    name: 'Foo.bar.Mixin',
    'other': 'Foo.other.Mixin'
},
//...

Mixins can also be specified as a String[]:

mixins: [
    'Foo.bar.Mixin',
    'Foo.other.Mixin'
],
//...

The statics Keyword

This keyword is used to place properties or methods on the class as opposed to on each of
the instances. This must be an object literal.

statics: {
    // members go here
},
// ...

The singleton Keyword

This keyword was historically only used with a boolean "true" value:

singleton: true,

The following (redundant) use is also supported:

singleton: false,

Overrides

In Ext JS 4.1.0 and Sencha Touch 2.0, Ext.define gained the ability to manage overrides.
Historically, overrides have been used to patch code to work around bugs or add
enhancements. This use was complicated with the introduction of the dynamic loader because
of the timing required to execute the Ext.override method. Also, in large applications
with many overrides, not all overrides in the code base were need by all pages or builds
(e.g., if the target class was not required).

All this changed once the class system and loader understood overrides. This trend only
continues with Sencha Cmd. The compiler understands overrides and their dependency effects
and load-sequence issues.

In the future, the compiler will become even more aggressive at dead-code elimination of
methods replaced by an override. Using managed overrides as described below will enable
this optimization of your code once available in Sencha Cmd.

Standard Override Form

Below is the standard form of an override. The choice of namespace is somewhat arbitrary,
but see below for some suggestions.

Ext.define('MyApp.patches.grid.Panel', {
    override: 'Ext.grid.Panel',

    ...
});

Use Cases

With the ability to use Ext.define to manage overrides, new idioms have opened up and
are actively being leveraged. For exampled in the code generators of
Sencha Architect and internal to the framework,
to break apart large classes like Ext.Element into more manageable and cohesive pieces.

Overrides as Patches

This is the historical use case and hence the most common in practice today.

CAUTION: Care should be taken when patching code. While the use of override itself is
supported, the end result of overriding framework methods is not supported. All overrides
should be carefully reviewed whenever upgrading to a new framework version.

That said, it is, at times, necessary to override framework methods. The most common case
for this to fix a bug. The Standard Override Form is ideal in this case. In fact, Sencha
Support will at times provide customer with patches in this form. Once provided, however,
managing such patches and removing them when no longer needed is a matter for the review
process previously mentioned.

Naming Recommendation:

  • Organize patches in a namespace associated with the top-level namespace of the target.
    For example, "MyApp.patches" targets the "Ext" namespace. If third party code is involved
    then perhaps another level or namespace should be chosen to correspond to its top-level
    namespace. From there, name the override using a matching name and sub-namespace. In the
    above example:

    (Ext -> MyApp.patches).grid.Panel

Overrides as Partial Classes

When dealing with code generation (as in Sencha Architect), it is common for a class to
consist of two parts: one machine generated and one human edited. In some languages, there
is formal support for the notion of a "partial class" or a class-in-two-parts.

Using an override, you can manage this cleanly:

In ./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
    },

    ...
});

In ./foo/bar/custom/Thing.js:

Ext.define('Foo.bar.custom.Thing', {
    override: 'Foo.bar.Thing',

    method: function () {
        this.callParent(); // calls generated method
        ...
    },

    ...
});

Naming Recommendations:

  • Organize generated vs. hand-edited code by namespace.
  • If not by namespace, consider a common base name with a suffix on one or the other
    (e.g., "Foo.bar.ThingOverride" or "Foo.bar.ThingGenerated") so that the parts of a class
    collate together in listings.

Overrides as Aspects

A common problem for base classes in object-oriented designs is the "fat base class". This
happens because some behaviors apply across all classes. When these behaviors (or features)
are not needed, however, they cannot be readily removed if they are implemented as part of
some large base class.

Using overrides, these features can be collected in their own hierarchy and then requires
can be used to select these features when needed.

In ./foo/feature/Component.js:

Ext.define('Foo.feature.Component', {
    override: 'Ext.Component',

    ...
});

In ./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
    ],

    ...
});

This feature can be used now by requiring it:

...
requires: [
    'Foo.feature.grid.Panel'
]

Or with a proper "bootstrap" file (see Workspaces in Sencha Cmd
or Single-Page Ext JS Apps):

...
requires: [
    'Foo.feature.*'
]

Naming Recommendation:

  • Organize generated vs. hand-edited code by namespace. This enables use of wildcards to
    bring in all aspects of the feature.

Using requires and uses in an Override

These keywords are supported in overrides. Use of requires may limit the compiler's
ability to reorder the code of an override.

Using callParent and callSuper

To support all of these new uses cases, callParent was enhanced in Ext JS 4.0 and Sencha
Touch 2.0 to "call the next method". The "next method" may be an overridden method or an
inherited method. As long as there is a next method, callParent will call it.

Another way to view this is that callParent works the same for all forms of Ext.define,
be they classes or overrides.

While this helped in some areas, it unfortunately made bypassing the original method (as a
patch or bug fix) more difficult. So in Ext JS 4.1.1a, Ext JS 4.1.2a and Sencha Touch 2.1,
there is now a method named callSuper that can be used to bypass an overridden method.

In future releases, the compiler will use this semantic difference to perform dead-code
elimination of overridden methods.

Conclusion

As Sencha Cmd continues to evolve, it will continue to introduce new diagnostic messages
to help point out deviations from these guidelines.

A good place to start is to see how this information can help inform your own internal
code style guidelines and practices.