Yii generated script and renderPartial

Yes, I think the loader idea is probably workable. Here's the scenario that I can think of:

  1. In the first run of the page (full page): if the page requires js files (or even css files?), we will insert the loader script in the head section. We use this loader to include the needed js files.

  2. In the following runs of the page (partial page due to ajax requests): if the page requires js files, we will still insert the loader script at the beginning. We use this loader to include the js files needed by the partial view.

So here are the requires for the loader script:

  1. It should be able to be included and run multiple times without side effects

  2. A js file can be loaded multiple times by the loader, but only the first time is effective, and the rest are ignored.

What do you think? If you can come up with a prototype, that will be really great!

Hmm. So you want to include the loader with every partial page? I guess, you want that, to automate the process of including the loader.

I'm not sure about that: I'd say, it would be easier to include the loader only once (first page load) and let the developer decide, to do so. If we send the loader with each AJAX response, this unneccessarily blows them up.

It's nice to automate things, but this often makes things obscure and you don't really know, whats going on behind the scenes. The principle of a loader is quite simple, and i think, when developing an AJAX page, it's easy to learn how to use the loader.

But i'll also think about your proposal…

Ok, I think it's fine that we only load loader the first time.

More (very free) thinking:

What if the loader is always included? It could take care of all the scripts/css we need on the clientside. To retain compatibility with older browsers, this could be made optional (turning it off, gives the current behaviour), but if anyone wants to use js enhanced stuff on his site, the loader is mandatory. The loader script will not be that big, so not a big deal, i think.

This would prevent duplicate script loading. I see not many alternatives, as the client is the only instance that knows, which scripts are already loaded. We can't solve this on the serverside.

While developing this feature, don’t forget about proposed CClientScript improvements which should allow packing all scripts into a single file so there’s no need for any loaders because all scripts will be always accessible.

You probably would not want to load all scripts into a single file, besides the fact you need to know what all will mean for the whole app, it could mean that you bloat your single file too much.

If the loader is available, it can test for the needed plugin functions as well, and load it dynamically if not present. The loader itself might not be needed on every page, only the paths to the required plugins.

For me it's a showstopper as well currently, eg. not using build-in JS functions since its destroying already present jQuery plugins, and a huge overhead.

After inspecting YUI Loader, i have some more ideas.

YUI Loader adds a <script> tag for each script, that needs to be loaded. This is a good thing, as the browser then will cache the loaded script. I've also thought about dependencies. But i tried to keep it as simple as possible. So how about this:

  1. The loader could become part of jquery.yii.js and provide 2 methods: jquery.yii.load() and jquery.yii.register(). So when using the loader, jquery must already be loaded.

  2. jquery.yii.load(urls, success) accepts two parameters: a literal objects with url/modulename pairs, and a callback function that’s called after all scripts loaded completely.

jquery.yii.load(


  {


    '/assets/1c9535/jquery.yiitab.js' : 'yiitab'   // value could also be an array


  }, 


  function () { 


    /* init stuff, that depends on the loaded script /* 


  }


);
  1. Every loaded js script needs an additional line in the end to register itself with the loader and let him know, that it's loaded completely. E.g. in jquery.yiitab.js this line would get added:
jquery.yii.register('yiitab');

An example page flow could then look like this:

1. Complete Page loads, with e.g. this content

<head>


...


<script type="text/javascript" src="/assets/d652f40f/jquery.js"></script>


<script type="text/javascript" src="/assets/1c978867/jquery.yii.js"></script>


...


</head>


<body>


....


<!-- loader call, generated by some view that uses a CTabView: -->


<script type="text/javascript">


/*<![CDATA[*/


jquery(function(){


    jquery.yii.load(


      {'/assets/abcdefg/jquery.yiitab.js':'yiitab'}, 


      function() { /* init tabview */}


    );


});


/*]]>*/


</script>


When the page is loaded, jquery.yiitab.js will be loaded and the tabview will get initalized afterwards. It's not required to use the loader here. The according script tag could also be added after the jquery.yii.js tag in the header.

2. An AJAX call is made to update parts of the page.

The loaded view contains another CTabView and a CStarRating. At the end of the HTML response, this script is added:



<!-- loader call, generated by some view that uses a CStarRating: -->


<script type="text/javascript">


/*<![CDATA[*/


jquery(function(){


    jquery.yii.load(


      {'/assets/abcdefg/jquery.yiitab.js':'yiitab'}, 


      function() { /* init tabview */}


    );


    jquery.yii.load(


      {


        '/assets/abcdefg/jquery.dimensions.js' : 'dimensions',


        '/assets/abcdefg/jquery.metadata.js' : 'metadata'


      }, 


      function() { /* init CStarRating */}


    );


});


/*]]>*/


</script>


The loader would find yiitab already loaded and immediately call the success callback. The required js for CStarRating would be loaded, and it's init also called afterwards.

To ease generation of these loader block, a util function could be used. It could also allow adding custom js files. So if someone wants to use a minified js file, it would generate something like this:



<script type="text/javascript">


/*<![CDATA[*/


jquery(function(){


    jquery.yii.load(


      {'/assets/abcdefg/my-custom-min.js': ['yiitab','bgiframe', 'dimension'] }, 


      function() { /* init stuff */}


    );


});


/*]]>*/


</script>


The min file must register the according modules:

yii.register( ['yiitab','bgiframe','dimension'] );

What do you guys think? Maybe we also need a type for the urls, to be able to include CSS.

Little addition: A more flexible (but also more verbose) syntax could look like this:

yii.load([


  {


    'name' : ['yiitab','bgiframe', 'dimension'],


    'url' : '/assets/abcdefg/my-custom-min.js'


  },


  {


    'name' : ['yiitabcss'],


    'url' : '/assets/abcdefg/yiitab.css',


    'type' : 'css' // defaults to js


   }


], 


function() { /* init */ }


);





Looks like a neat solution indeed!

Finally I get time to look at your proposal. ;)

Thanks for your investigation. It seems to me that your approach would require that widget init code always follows after the corresponding js file? Also, the js file is loaded at the end of HTML?

Maybe we can go this simpler way?

  1. In the initial page, we use <script> to include js/css files and also call a register() method to remember these inclusions;

  2. In the ajax responses, we use load() method to include js/css files.

  3. In both cases, the <script> tag and load() call should be able to appear anywhere (head, body begin, body end).

I am not sure whether or not we should deal with those callbacks. I think it makes things more complex.

Qiang, thanks for your feedback.

Indeed, my proposal is quite as you described it :).

  1. Yes, that's possible.

  2. Correct. The load call also contains the literal function that would init any widgets contained in the AJAX response.

  3. That would be the case.

It's important to note, that each loaded js core file needs the call to register() in the end. The init code for widgets would stay as it is now, but get "encapsuled" inside a yii.load() call. There are 2 drawbacks i see with this:

  1. All js core scripts that are loaded by the loader need this register() call at the end.

  2. This means: We always need the loader present, even, if we don't use the loader for a page. Otherwhise we would get an error about missing jquery.yii.register().

Not sure, if this is acceptable. A workaround would be:

We add a "dummy" script with some bytes to every page that doesn't use the loader which contains an empty jquery.yii.register() method.

Then we need a way to differ between a loader and a non-loader page. This would require an additional parameter for render()/renderPartial() i think.

More feedback welcome :)

Quote

You probably would not want to load all scripts into a single file, besides the fact you need to know what all will mean for the whole app, it could mean that you bloat your single file too much.

Roundtrip between client and server to load additional even tiny js file could be more than the overhead of loading whole gzipped minified js while at once which contains all functions even not needed for this particular page. At the same time you are introducing code complexity and some additional assumptions. You won't gain much speed (maybe even opposite) because of network roundtrip latency but you will lose code clarity.

That's not necessarily the case. You could also load a gzipped minified file with the loader, if you want to. You just tell the loader what "core" scripts your minified js file provides and everything's fine.

Think more of it like an AJAX response tells the loader "hey, i need jquery.yiitab". The loader would check, if any of the already loaded scripts provides yiitab and loads it, if necessary.

Quote

That's not necessarily the case. You could also load a gzipped minified file with the loader, if you want to. You just tell the loader what "core" scripts your minified js file provides and everything's fine.

You didn’t get my point. The issue is that you can’t remove network roundtrip, independent of the size of the script to load. This roundtrip, not the script size, will provide the most negative speed impact. You need to minimize roundtrips, and one way to do that is to load all scripts in advance in a single file. Even if you load more scripts than you need, it will be faster than loading less scripts as a separate files.

Quote

Quote

That's not necessarily the case. You could also load a gzipped minified file with the loader, if you want to. You just tell the loader what "core" scripts your minified js file provides and everything's fine.

You didn’t get my point. The issue is that you can’t remove network roundtrip, independent of the size of the script to load. This roundtrip, not the script size, will provide the most negative speed impact. You need to minimize roundtrips, and one way to do that is to load all scripts in advance in a single file. Even if you load more scripts than you need, it will be faster than loading less scripts as a separate files.

I agree with this viewpoint, additional http requests does have significant impact on the overall perceived user experience. A goal that I often pursue is to only have 1 external js file and 1 external css file. Additional js and css on a per functionality basis are rendered inline within the page.

I see. So the alternative for the problem would be, that we include all js files (or a packed version of them) for all widgets that might get used in any AJAX HTML response for a page.

I guess this will not be a problem for "simple" widget js. When thinking about the loader i had more complex AJAX applications in mind with many >100K js modules. But in cases like that, YUI Loader could still be used separately, so also no problem.

Any chance that one of the solutions will make it in 1.0.2?

I'm afraid not. We won't rush to put in new methods in the framework until we are fairly confident and the code is relatively stable.

I changed some protected methods in CClientScript to public. That could be a workaround to solve your problem.

Another idea. How about adding adding a method registerAjaxView() to CController? In it’s render method it renderPartials all registered views without storing the output. The Ajax components in these views will register their necessary core scripts in the ClientscriptManager. That way we can do something like this in the action for the initial page:

<?php


public function actionIndex()


{


    $this->registerAjaxView(array('show','update'));


    ...


}

That would ease the process of deciding which clientscripts to include, as views are more handy to identify. This could also be used to find out, which js files to include in a packaged version for production mode.

I forgot the missing view data when calling renderPartial. So this approach is problematic.