4

I'm trying to figure out how I can load app.js before allowing the user to get the actual application. What I'm attempting to do is load a user's configuration file before all of my class Ext.defines fire... the reason I want to do this is because the Ext.defines actually depend on values in the user's configuration. So for example, in an Ext.define, I could have the title property set to pull from this global user configuration var. And no, I don't want to have to go through and change all of these properties to use initComponent... that could take quite some time.

Instead, what I'd like to do is load the configuration, and then let the Ext.defines run, but I will need Ext JS and one of my defined classes to be loaded before the rest of the classes. Is this possible? I've been looking into Sencha Cmd settings, but I've been extremely unsuccessful with getting this to work. I was playing with the bootstrap.manifest.exclude: "loadOrder" property, which loads classic.json, and doesn't define my classes, but unfortunately, that also doesn't fully load Ext JS, so Ext.onReady can't be used... nor can I use my model to load the configuration.

I have a very high level example below (here's the Fiddle).

Ext.define('MyConfigurationModel', {
    extend: 'Ext.data.Model',
    singleton: true,

    fields: [{
        name: 'testValue',
        type: 'string'
    }],

    proxy: {
        type: 'ajax',
        url: '/configuration',
        reader: {
            type: 'json'
        }
    }
});
// Pretend this would be the class we're requiring in our Main file
Ext.define('MyApp.view.child.ClassThatUsesConfiguration', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.classThatUsesConfiguration',
    /* We get an undefined value here because MyConfigurationModel hasn't
     * actually loaded yet, so what I need is to wait until MyConfigurationModel
     * has loaded, and then I can include this class, so the define runs and
     * adds this to the prototype... and no, I don't want to put this in
     * initComponent, as that would mean I would have to update a ton of classes
     * just to accomplish this */
    title: MyConfigurationModel.get('testValue')
});
Ext.define('MyApp.view.main.MainView', {
    extend: 'Ext.Viewport',
    alias: 'widget.appMain',
    requires: [
        'MyApp.view.child.ClassThatUsesConfiguration'
    ],
    items: [{
        xtype: 'classThatUsesConfiguration'
    }]
});
Ext.define('MyApp.Application', {
    extend: 'Ext.app.Application',
    mainView: 'MyApp.view.main.MainView',
    launch: function() {
        console.log('launched');
    }
});

/* In app.js... right now, this gets called after classic.json is downloaded and
 * after our Ext.defines set up, but I basically want this to run first before
 * all of my classes run their Ext.define */
Ext.onReady(function() {
    MyConfigurationModel.load({
        callback: onLoadConfigurationModel
    })
});
function onLoadConfigurationModel(record, operation, successful) {
    if (successful) {
        Ext.application({
            name: 'MyApp',
            extend: 'MyApp.Application'
        });
    }
    else {
        // redirect to login page
    }
}
incutonez
  • 3,241
  • 9
  • 43
  • 92

3 Answers3

3

I call this "splitting the build", because it removes the Ext.container.Viewport class's dependency tree from the Ext.app.Application class. All Ext JS applications have a viewport that is set as the main view. By moving all requires declarations of the core of the application to the viewport class, an application can load the viewport explicitly from the application class, and the production build can be configured to output two separate files, app.js and viewport.js. Then any number of operations can occur before the core of the application is loaded.

// The app.js file defines the application class and loads the viewport
// file.
Ext.define('MyApp.Application', {
   extend: 'Ext.app.Application',
   requires: [
      // Ext JS
      'Ext.Loader'
   ],
   appProperty: 'application',
   name: 'MyApp',

   launch: function() {
      // Perform additional operations before loading the viewport
      // and its dependencies.
      Ext.Ajax.request({
         url: 'myapp/config',
         method: 'GET',
         success: this.myAppRequestSuccessCallback
      });
   },

   myAppRequestSuccessCallback: function(options, success, response) {
      // Save response of the request and load the viewport without
      // declaring a dependency on it.
      Ext.Loader.loadScript('classic/viewport.js');
   }
});

-

// The clasic/viewport.js file requires the viewport class which in turn
// requires the rest of the application.    
Ext.require('MyApp.container.Viewport', function() {
   // The viewport requires all additional classes of the application.
   MyApp.application.setMainView('MyApp.container.Viewport');
});

When building in production, the viewport and its dependencies will not be included in app.js, because it is not declared in the requires statement. Add the following to the application's build.xml file to compile the viewport and all of its dependencies into viewport.js. Conveniently, the development and production file structures remain the same.

<target name="-after-js">
   <!-- The following is derived from the compile-js target in
        .sencha/app/js-impl.xml. Compile the viewport and all of its
        dependencies into viewport.js. Include in the framework
        dependencies in the framework file. -->
    <x-compile refid="${compiler.ref.id}">
        <![CDATA[
            union
              -r
              -class=${app.name}.container.Viewport
            and
            save
              viewport
            and
            intersect
              -set=viewport,allframework
            and
            include
              -set=frameworkdeps
            and
            save
              frameworkdeps
            and
            include
              -tag=Ext.cmd.derive
            and
            concat
              -remove-text-references=${build.remove.references}
              -optimize-string-references=${build.optimize.string.references}
              -remove-requirement-nodes=${build.remove.requirement.nodes}
              ${build.compression}
              -out=${build.framework.file}
              ${build.concat.options}
            and
            restore
              viewport
            and
            exclude
              -set=frameworkdeps
            and
            exclude
              -set=page
            and
            exclude
              -tag=Ext.cmd.derive,derive
            and
            concat
              -remove-text-references=${build.remove.references}
              -optimize-string-references=${build.optimize.string.references}
              -remove-requirement-nodes=${build.remove.requirement.nodes}
              ${build.compression}
              -out=${build.out.base.path}/${build.id}/viewport.js
              ${build.concat.options}
            ]]>
    </x-compile>

    <!-- Concatenate the file that sets the main view. -->
    <concat destfile="${build.out.base.path}/${build.id}/viewport.js" append="true">
       <fileset file="classic/viewport.js" />
    </concat>
</target>

<target name="-before-sass">
    <!-- The viewport is not explicitly required by the application,
         however, its SCSS dependencies need to be included. Unfortunately,
         the property required to filter the output, sass.name.filter, is
         declared as local and cannot be overridden. Use the development
         configuration instead. -->
    <property name="build.include.all.scss" value="true"/>
</target>

This particular implementation saves the framework dependencies in their own file, framework.js. This is configured as part of the output declaration in the app.json file.

"output": {
   ...
   "framework": {
      // Split the framework from the application.
      "enable": true
   }
}

https://docs.sencha.com/extjs/6.2.0/classic/Ext.app.Application.html#cfg-mainView https://docs.sencha.com/extjs/6.2.0/classic/Ext.container.Viewport.html https://docs.sencha.com/cmd/guides/advanced_cmd/cmd_build.html#advanced_cmd-_-cmd_build_-_introduction

Trevor Karjanis
  • 1,485
  • 14
  • 25
  • This looks very interesting and promising... I'll be sure to try it out Monday morning and follow-up here. Thanks! – incutonez Jul 16 '17 at 02:00
  • So this is pretty awesome... I was not aware you could split the framework and any required code you needed before proceeding with the true application load. It also seems like the Sencha Support engineer and Mitchell Simoens weren't aware of this, andor advise against it... either way, thank you very much for this solution! – incutonez Jul 17 '17 at 16:22
  • where did you find that `output.framework.enable` property? I couldn't find it in any of the links you referenced, nor could I find it in any of the Sencha documentation. – incutonez Jul 17 '17 at 16:27
  • 1
    I thought I had seen this in the documentation, but I could not find it there either. It is discussed in the forums, linked below. The app.output.framework.enable property enables split mode in .sencha/app/init-impl.xml on line 341. Its default value of false is defined in .sencha/app/defaults.properties on line 96. I enabled it, because the non-split, concatenated version of app.js was so large it would crash the Chrome developer tools. https://www.sencha.com/forum/showthread.php?275828-enable-split-mode-in-compile-js-target – Trevor Karjanis Jul 17 '17 at 21:24
  • Yeah, I was reading that thread earlier today... Interesting that it's still not documented, but thanks for all of your help; I really appreciate it. – incutonez Jul 18 '17 at 02:27
  • I'm actually having a problem with building this in production... using `sencha app watch`, the CSS files are generated properly, but once I switch to production mode, it's almost like it leaves an entire CSS file out, and that's because I'm not using `Ext.require('MainView')` in the Application.js file... if I do that, then Sencha Cmd picks up that this file is required and includes its appropriate CSS classes, but because it lives in `viewport.js`, and that's not parsed... it gets dropped. Any thoughts? – incutonez Jul 18 '17 at 21:28
  • 1
    Whoops! I left the sass step out, but I added it to the bottom of the build.xml example. – Trevor Karjanis Jul 18 '17 at 22:46
  • Man, this is now working beautifully. I can't thank you enough! – incutonez Jul 18 '17 at 22:58
  • Is there any way to include overrides in with the framework.js file? I need to override Ext.data.Connection, so all AJAX requests, including Ext.Ajax--which is a singleton of Ext.data.Connection--get overridden. But because of how this split occurs, the Ext.data.Connection override is coming in with app.js, which is too late. – incutonez Jan 16 '18 at 22:21
  • I was not able to do this; ${build.tag.name}-overrides does not work. I get around this issue by extending Ext.data.Connection and defining an Ajax class, MyApp.Ajax, in the application. I use this instead of Ext.Ajax. Alternatively, you could disable splitting the framework. However, that would require modifying the build defined above. – Trevor Karjanis Jan 21 '18 at 17:38
  • I was able to do this, but I think it might be working hackily... see [this thread](https://stackoverflow.com/a/48327692/1253609) – incutonez Jan 22 '18 at 16:37
2

As far as I know, this is not possible with Sencha Cmd, because while Sencha Cmd can load framework and application separately, it is not possible to tell the production microloader to wait with the second file until the code from the first file has done something (presumably loaded something from the server?).

So the only approach would be to get the options outside ExtJS, before loading ExtJS.

You would have to write your own javascript that loads the configuration into a global variable using a bare, synchronous XmlHttpRequest, and include that into the index.html before the ExtJS script. That way, the script is executed before ExtJS is loaded at all, and you have completely consistent behaviour across development, testing and production builds without modifying any framework file that may be overwritten during framework upgrades.

I guess this is what you are searching for.

So how I did it: In index.html, I added a custom script that fills some global variables:

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<script type="text/javascript">
    APIURI = '../api/', // <- also used in ExtJS.
    var xhr = new XMLHttpRequest();
    xhr.open('GET', APIURI+'GetOptions', false);
    xhr.setRequestHeader('Accept','application/json');
    xhr.send(null);
    try {
        var configdata = eval("(" + xhr.responseText + ")");
    } catch(e) {
         // snip: custom code for the cases where responseText was invalid JSON because of a shitty backend
    }
    if(configdata.options!=undefined) Settings = configdata.options;
    else Settings = {};
    if(configdata.translations!=undefined) Translations = configdata.translations;
    else Translations = {};
    Translations.get=function(str) {
        if(typeof Translations[str]=="string") return Translations[str];
        return "Translation string "+str+" missing.";
    };
 </script>
<link rel="icon" type="image/vnd.microsoft.icon" href="../favicon.ico">
<title>Application</title>
<script id="microloader" data-app="1a7a9de2-a3b2-2a57-b5af-df428680b72b" type="text/javascript" src="bootstrap.js"></script>

Then I could use in Ext.define() e.g. title: Translations.get('TEST') or hidden: Settings.HideSomeButton or url: APIURI + 'GetUserData'.

However, this has major drawbacks you should consider before proceeding.

After a short period of time, new feature requests emerged and settings I had considered fixed should change at runtime, and I realized that always reloading the application when a setting changes is not good user experience. A while later, I also found that Chrome has deprecated synchronous XmlHttpRequests, and that this approach delays application startup time.

So, the decision was made that in the long run, the only sane approach is to be able to react to changes of any configuration value at runtime, without a full reload of the application. That way, settings could be applied after loading the application, and the requirement could be dropped to wait for settings load before proceeding with the application.

For this, I had to completely work out everything needed for full localization support, so the user can switch between languages without reload of the application, and also any other setting can change at runtime and is automatically applied to the application.

Short-term, this is quite some work, which didn't really matter to me because I was scheduled to rework the whole application layout, but long-term, this will save quite some time and headache, especially when someone decides we should start polling for changes to the settings from the server, or that we should use an ExtJS form for login instead of good old Basic authentication (which was by then already asked for multiple times, but we couldn't deliver because of said shitty ExtJS app architecture).

Alexander
  • 19,906
  • 19
  • 75
  • 162
  • Thanks for the answer, and there really doesn't seem to be a better alternative... I was hoping I could use `Ext.beforeLoad` and tap into `Ext.Boot` or `Ext.Microloader`, but none of those seemed to work. – incutonez Jul 14 '17 at 13:19
  • I'm curious, with your newer approach do you define all of your proxy configurations in the constructor for models/stores? What about ViewModels? The only reason I ask is because you have that `APIURL`, and that's one of my biggest offenders in this case. – incutonez Jul 14 '17 at 13:35
  • Also, I'm playing with your idea of how `async: false` will go away... so I fire off my XHR request asynchronously, but I don't append the microloader's script until after it's loaded, and that's done by doing a simple `head.appendChild(script);` – incutonez Jul 14 '17 at 15:24
  • My proxy configurations are defined in the store's constructors, yes. Plus the forms, which are views that have urls, and even some `Ext.Ajax.requests` mostly fired in controllers. Maybe I don't understand the question; because I don't see why you shouldn't be able to define the URL in ViewModels as well. – Alexander Jul 15 '17 at 01:24
  • Well, the API URL comes back in the configuration, and ideally, this configuration would be a model... unfortunately, the problem I'm running into now is populating that model with the data, so the rest of the classes can make use of this model when the JavaScript engine loads the files. If it's a flat object, then it's no sweat, but because it's a model, and it doesn't have the data yet, the URL would be an empty string. – incutonez Jul 15 '17 at 02:44
  • Well, if you only read values from your configuration at definition time, a model is useless overhead (IMO), and an object suffices. But how does your app know where to read the Config from if the APIURI is not known before the first call? – Alexander Jul 15 '17 at 03:17
  • You could make that argument about models in general... but we want to have custom methods, and possibly reload the configuration as needed, so containing it all in a model makes sense in my mind. The first call is simple, as it's on the same server as our UI, but we have other APIs that we hit, which all comes back in the config. – incutonez Jul 15 '17 at 03:25
  • And we're not only using the configuration at definition time... we re-use it a lot throughout our app, especially for security checking. – incutonez Jul 15 '17 at 03:27
  • Well, if you possibly want to reload the configuration, you will have to use my current approach, which allows the program to respond to any configuration change at runtime. Then you don't have to get the config before the program starts; just set a sane default that happens to work for the first second until the correct config has been loaded from the server. – Alexander Jul 15 '17 at 08:03
2

We actually do use a Sencha CMD approach. As @Alexander mentioned, we also use a global variable for keeping the application's configuration. This approach also implies that the server returns the actual declaration of the global config variable.

If you dig into the app.json file, and find the js config key, you will see that in the description it says

List of all JavaScript assets in the right execution order.

So, we add the configuration's endpoint before the app.js asset

"js": [
    {
        "path": "data/config",
        "remote": true
    },
    {
        "path": "${framework.dir}/build/ext-all-debug.js"
    },
    {
        "path": "app.js",
        "bundle": true
    }
]

also specifying remote: true.

// Specify as true if this file is remote and should not be copied into the build folder

The "data/config" endpoint returns something like:

var CONFIG = {
    user: {
        id: 1,
        name: 'User'
    },
    app: {
        language: 'en'
    }
}

And now we can have a reference to the CONFIG variable anywhere in our classes.

scebotari66
  • 3,395
  • 2
  • 27
  • 34
  • This is an interesting approach, but the only thing is, my path to this config will sort of be dynamic and potentially CORS, so that won't really work for me. It definitely makes sense though and thanks for sharing! – incutonez Jul 14 '17 at 13:22