Yii 1.1: nlsclientscript

NLSClientScript extends the CClientScript class smart loading javascript files and optionally merging/minifying js+css files
59 followers

Project moved to github as 7.0beta

Motivated by earlier suggestions, a lot of improvements made on the library.

  • What's new compared to 6.x?
    • huge refactor
    • one-file code splitted to parts
    • completely new css processing part: processing @import-s, url-s
    • composer support
    • demo app
    • moved to github

Visit https://github.com/nlac


NLSClientScript prevents duplicated linking of javascript files when updating a view by ajax, when eg. paging or sorting a gridview, ajax-submitting a form or any custom ajax-updating a part of a view.

The extension does not prevent the multiple loading of CSS files. I simply couldn't find a way to manage it clearly (too long to explain here).

The issue what this extenson fights is for example when you render Jui widgets by CHtml::ajax, the js files used by the widget will be loaded as many times as you render such a widget in a view. The unnecessary bandwidth usage is the smaller problem, the bigger problem is eg. loading jquery.js again may reset some js objects set by previously loaded ui-related js files. That can cause js errors and the view may stop working.

Using NLSClientScript helps to avoid it all.

From 6.0, it optionally merges/caches + minifies the registered js and css files.

History

  • 6.7
    • fixed toAbsUrl() and optimized init() methods (reported + fix by le_top)
  • 6.6
    • fixed regexp in normUrl (reported by le_top)
  • 6.5
    • fixed buggy behavior when more xhr "script"-dataType requests started for the same script
    • eliminated deprecated jQuery.browser reference
  • 6.4
    • followed the change of the registerScriptFile() arguments in yii 1.1.14
    • removed/added some comments
  • 6.3

    • serious bug fixed: filtering duplications (usually) failed when js-merging applied for the response of an xhr request
    • new params: mergeIfXhr, mergeJsExcludePattern, mergeJsIncludePattern, mergeCssExcludePattern, mergeCssIncludePattern, resMap2Request (see the phpdoc comments in the source for more info)
    • appended an extra ; to the js files
    • some other small improvements
  • 6.21

    • fixed another bug: merged files have been re-generated on every request when the appVersion parameter was used
  • 6.2 (see the updated Usage)

    • fixed a serious bug broke the original functionality when merging happened (duplicates couldn't been recognized)
    • added a new parameter appVersion
  • 6.1 (see the updated Usage)

    • fixed several bugs (serverBaseUrl composing, merging css files by media correctly)
    • added parameters mergeAbove, curlTimeOut, curlConnectionTimeOut
  • 6.0 (see the updated Usage)

    • added optional merge and minify functionalities
    • to keep the simpleness of the single-file extension, embedded JSMin.php from https://github.com/eriknyk/jsmin-php
  • 5.0 (see the updated Usage)

    • in 4.0RC found an issue couldn't worked around: it registered also the script tags being in html/css/js comments, input field, textarea value so i had to drop the native source analysis by regexp. Fortunately found the solution in 5.0 looks like the most perfect till now. Tested successfully in IE7+, latest FF,Chrome,Opera. Reports about testing are welcome as always.
  • 4.0 RC

    • refactored, hopefully all bugs reported about 3.x have been eliminated
  • 3.6

    • fixed a typo
  • 3.5 (see the updated Usage)

    • handling special case when updating a table by tr tag
    • further IE fixes
    • added 2 new parameters: ignoredPattern and processedPattern
    • general refactoring
  • 3.4

    • fixed non-script-rendering bug in IE
  • 3.3

    • removes the occasional ...?_=3767454656434 -like timestamps from the url keys used to store/identify the loaded scripts
    • fixed the accidental naming NLSClientScript to EClientScript
  • 3.2

    • fixed accessing HEAD element for IE
    • compressed js code (full source is still there in the php source)
  • 3.1

    • the extension now prevents the duplicated loading of css files also.
  • 3.0

    • brand new approach simplifying dramatically the extension and the usage of the extension, based the great idea of Eirik Hoem
    • see the Usage below!
  • 2.1

    • dirty fix for a rendering bug of jquery.js v1.6.1 affecting binline=true mode in Yii 1.18
  • 2.0

    • brand new approach: resource hash stored at the server side, in the webuser state. All these info deleted when a non-ajax request comes
    • no $.ajax usage - better performance
    • the extension does not require jquery.js and jquery.yii.js to be linked initially any more
    • js/css files can be linked from other domain
    • new parameter: bInlineJs - if true, the scriptFile method will insert the js file content into the html instead of linking the file what can result even better performance
  • 1.3

    • added cache:true to the ajax js load
    • compressed core js code
  • 1.2

    • fixed js error when app not in YII_DEBUG mode
  • 1.1
    • hash key generated on server side
    • two hash key mode: PATH and CONTENT
    • shortened client-side code
  • 1.0
    • base version

If you interest the details, see the comments in the source.

Requirements

Yii 1.x

Limitations

  • The extension identifies the scripts by its paths so it does not prevent to load the same script content from different paths. So eg. if you published the same js file into different asset directories, NLSClientScript considers those to be different and won't prevent to load those several instances.

  • The extension doesn't watch wether a js/css file has been changed. If you set the merge functionality and some file changed, you need to delete the cached merged file manually, otherwise you'll get the old merged one.

Usage (v6.3+)

1 . Set the class for the clientScript component in /protected/config/main.php, like

...
'components'=>array(
  ...
  'clientScript' => array(
    'class' => 'your.path.to.NLSClientScript',
    //'excludePattern' => '/\.tpl/i', //js regexp, files with matching paths won't be filtered is set to other than 'null'
    //'includePattern' => '/\.php/', //js regexp, only files with matching paths will be filtered if set to other than 'null'
 
    'mergeJs' => true, //def:true
    'compressMergedJs' => false, //def:false
 
    'mergeCss' => true, //def:true
    'compressMergedCss' => false, //def:false
 
    'mergeJsExcludePattern' => '/edit_area/', //won't merge js files with matching names
 
    'mergeIfXhr' => true, //def:false, if true->attempts to merge the js files even if the request was xhr (if all other merging conditions are satisfied)
 
    'serverBaseUrl' => 'http://localhost', //can be optionally set here
    'mergeAbove' => 1, //def:1, only "more than this value" files will be merged,
    'curlTimeOut' => 10, //def:10, see curl_setopt() doc
    'curlConnectionTimeOut' => 10, //def:10, see curl_setopt() doc
 
    'appVersion'=>1.0 //if set, it will be appended to the urls of the merged scripts/css
  )
  ...
)
 
For more information about the parameters, see the header comment of NLSClientScript.php.

2 . use Yii::app()->getClientScript() by the standard way to link js and css files/snippets

Example:

$cs = Yii::app()->getClientScript();
$systemJsPath = Yii::app()->getAssetManager()->publish( Yii::getPathOfAlias( 'system.web.js' ), false, -1, false );
$cs->registerScriptFile( $systemJsPath . '/ext/yii_ext.js');
$cs->registerScriptFile( $systemJsPath . '/ext/plugins/jquery.form.js');

3 . DOES NOT WORK FROM v5.0: If you want to do a custom ajax request with "dataType"="json" and there are some fields of the response you want to update your page with, filter that part "by hand" with $.ajaxSettings.dataFilter like eg.:

echo CHtml::ajaxLink('custom update', array('/site/testupdate'), array(
 
  'dataType' => 'json',
 
  'success'=>'js:function(data){ $("#cont").html($.ajaxSettings.dataFilter(data.content)); }',
 
));

Resources

Total 20 comments

#18816 report it
nlac at 2015/01/15 03:54am
@le_top

Well, the first versions of NLSClientScript filtered the received html data (removed the script tags already recent, looking at its "src"). This is similar to your suggestion, just now it should be applied to script tags with no src, considering their content. There were several issues with that technique, therefore i switched to a more clean approach from version 6. Anyway i will think about some general solution but this problem seems to be hard.

#18815 report it
le_top at 2015/01/14 01:25pm
Fix for CJuiButton - which is just a "simple" example

Thanks for having a look. With my post I am mainly sharing a solution and hoping for some "idea" to integrate something in NLSClientScript.

CJuiButton is quite easy as an example and your susggestion does not make the button Jquery-ui'fy itself. It is more complex with jquery-ui dropdowns, date filter fields, ... .

One of the options that could be implemented in NLSClientScript is that it automatically applies the 'scripts' on the page at least only once at the end of each ajax call. In other words, the scripts ('') that were already "added" during the ajax call are not added again, and the other ones are "executed"/added/. Anyway this is what I do with the code I add for 'afterAjaxUpdate' .

So if you do not see another solution, "we" just have to apply what I explained in my comment.

#18814 report it
nlac at 2015/01/14 11:00am
@le_top

Ok, i just updated the 7.0beta, due to fix another issue - i suggest to use always the latest version, won't backport any fix to 6.x (and i'd prefer to have new issues reported to git, consequently;-). I tried your gist in the demo's environment with yii 1.16 and i can see the issue. I'm afraid there's nothing nlsclientscript could do with it, the issue is not related about to prevent re-loading some already loaded scripts, but related how Yii's grid/jQuery handles the grid update. Exactly the same happens there with or without using nlsclientscript. A trivial workaround i would apply for that specific issue is, set 'htmlOptions'=>array('onclick'=>'$("#mygridid").yiiGridView("update")') for the CJuiButton instead of 'onclick' as it is in the gist. It fixes the case.

#18809 report it
le_top at 2015/01/12 07:23pm
Gist to demonstrate issue

Hi I created a GIST that you can use as a view ('$this->render('viewname');' in a controller will do it).

This example has two grids with a CJuiButton that I inserted in the filter.

The CJuiButton is fully functionnal if the jquery-ui is executed on it.

For the purpose of the test: - Click on 'update me' to reload a gridview. You will get an alert saying 'clicked' and another one saying 'afterAjaxUpdate'. - When reloading the top grid, the button will not be a CJuiButton after the update and it will stop working; - When reloading the bottom grid, the button will be a CJuiButton after the update, but without the Yii fixes, the 'alerts' should show more than once. - Click on 'doe' to get a 'selectionChanged' alert. This alert will appear one time before the update of the second grid, two times after a first update, three times after a second update, etc.

EDIT: I just tested by dropping my gist inside your demo 'site/pages'. Bad behavior is as expected ;-).

#18789 report it
nlac at 2015/01/08 04:00pm
#le_top

Hmm hardcore stuffs:) Interesting, as i remember i definitely tested the 7.0 with CGridView (not sure i did it with CListView) and didn't noticed any trouble...

Could you pls descibe the original issue you experienced, regarding the conflict of NLSClientScript and CGridView/CListView? It is still not clear for me and to be honest i stopped digging into the further details from that point. I got confused from the "afterAjaxUpdate" thing - it was already an attempt to solve an issue or it was the case when the issue happened? Sorry maybe i'm too tired right now and missed something in the description:)

#18758 report it
le_top at 2014/12/29 04:00pm
CGridView/CListView

NLSClientScript is an excellent extension but it does not work very well for the content of CGridView and CListView. I had to face that issue once more and I decided to have a deeper look and I found a solution.

First, we can rely on the 'afterAjaxUpdate' parameter of the CListView/CGridView like this:

$this->widget('zii.widgets.CListView', array(
        //...
        'afterAjaxUpdate' => "function(id,data){jQuery('body').append(jQuery(data).filter('script:not(\"[src]\")'));}",
));

That works but the second time you will try the Ajax reload will happen twice, the third time three times, etc. The "issue" is with the core library but was not accepted as a bug three years ago so here are the changes I apply after each Yii update for that (I just discovered the change needed for yiilistview.js as I have not been using that until recently).

CButtonColumn:

To remove the 'on' event on a grid before setting it.

if(true) {
 $class=preg_replace('/\s+/','.',$button['options']['class']);
 $js[]="if(typeof(_gridf)==='undefined'){_gridf={};}"
  ."if(typeof(_gridf['on-{$this->grid->id}-{$class}'])!=='undefined') {jQuery(document).off('click','#{$this->grid->id} a.{$class}',_gridf['on-{$this->grid->id}-{$class}']);}"
  ."_gridf['on-{$this->grid->id}-{$class}']=$function;"
  ."jQuery(document).on('click','#{$this->grid->id} a.{$class}',_gridf['on-{$this->grid->id}-{$class}']);";
} else { // Original code.
  $class=preg_replace('/\s+/','.',$button['options']['class']);
  $js[]="jQuery('#{$this->grid->id} a.{$class}').live('click',$function);";
}

jquery.yiigridview.js:

Add lines to switch off event listeners before defining them. -> There is a potential issue that there are other listeners using the same selector that might be deleted. -> However, the risk is low because the selector is specific and likely to be used by the yiiGridView only.

$(document).off('click.yiiGridView', '#' + id + ' .' + settings.tableClass + ' > tbody > tr');
$(document).on('click.yiiGridView', '#' + id + ' .' + settings.tableClass + ' > tbody > tr', function (e) {
$(document).off('click.yiiGridView', '#' + id + ' .select-on-check-all');
$(document).on('click.yiiGridView', '#' + id + ' .select-on-check-all', function () {
$(document).off('change.yiiGridView keydown.yiiGridView', settings.filterSelector);
$(document).on('change.yiiGridView keydown.yiiGridView', settings.filterSelector, function (event) {

jquery.yiilistview.js:

$(document).off('click.yiiListView', settings.updateSelector);
$(document).on('click.yiiListView', settings.updateSelector,function(){

Have fun with it!

#18614 report it
samarhaider at 2014/12/02 07:01am
Very Cool

Very Good extension, Its solve my issue as well as it boost my application speed

#17951 report it
le_top at 2014/08/15 03:56am
CSS urls

Hi I suggest that you mention the limitation for the CSS at least in the notes above, and also in the comments of the source code (e.g., as a comment to the variable enabling the merge).

Also, while the comments say that Js and CSS merging are disabled by default, they are actually active by default in the extension. I recommend to keep buggy/limited options inactive - so CSS merging should really be false because relative Urls are essential (to the framework CSS for starters). If things do not work "immediately", many developers try and abandon the extension.

I understand the limitation of time completely - you'll likely add proper Css merging when you are in need of it yourself, which is fully understandable. That's why I generally fix bugs myself, the difficulty is often to get them into the official release.

#17949 report it
nlac at 2014/08/15 02:14am
@le_top

Yep, i'm aware that url issue in the merged css, that was a topic in some earlier commments (btw not sure why only the top 20 comments are shown, near 90 comments are here). It's a plan to allow configuring a 3rd pary service to do the proper merging (or handle it properly without 3rd party, still not sure how much work it is). Anyway it requires effort, i can't say will be done in some weeks, but it is in my list (as other things...:( ).

#17926 report it
le_top at 2014/08/12 01:32am
CSS merging and relative resources

Hi Now that merging is working, I'v done a few tests. Css merging has an important issue: relative resources are not rebased.

Example:

.grid-view table.items th {
    color: white;
    background: url("bg.gif") repeat-x scroll left top white;
    text-align: center
}

After merging, the navigators looks in the assets folder for 'bg.gif', but it is not there.

Other issues with combining CSS files can be expected, and it might be a good idea to rely on an existing open source to do the job.

#17925 report it
nlac at 2014/08/11 07:47pm
@moa, @le_top

@moa: when the error comes, when the page is loaded or at the first partial update? I guess the second case, i suspect the fancybox plugin has broken due to loading again a js file (maybe fancybox.pack or other). Please check with Firebug what js file is loaded twice - remember if you refer a js once xxx.js?v=2.14, later xxx.js or xxx.js?v=2.15, NLSClientScript will consider those files to be different and won't prevent to load again. Please normalize the url's, to be the same for the same js content.

@le_top: nice, i'll test it and add to the next version, thx.

#17924 report it
le_top at 2014/08/11 04:54pm
Fix for compression bug

Hi I found the solution for the compression bug. Just add the following line to the initCurlHandler method:

curl_setopt($this->ch, CURLOPT_ENCODING, "");
#17922 report it
msoa at 2014/08/11 01:31pm
problem in mergeJs to false

Thanks for your work I have this code in widget:

public function init()
    {
        $assetsPath = Yii::getPathOfAlias('application.widgets.gallery.assets');
        $assetsUrl = Yii::app()->assetManager->publish($assetsPath);
        Yii::app()->clientScript->registerCssFile($assetsUrl.'/gallery.css');
        Yii::app()->clientScript->registerScriptFile($assetsUrl.'/gallery.js');
        // fancybox
        Yii::app()->clientScript->registerCssFile($assetsUrl.'/fancybox/source/jquery.fancybox.css?v=2.1.5');
        Yii::app()->clientScript->registerScriptFile($assetsUrl.'/fancybox/lib/jquery.mousewheel-3.0.6.pack.js');
        Yii::app()->clientScript->registerScriptFile($assetsUrl.'/fancybox/source/jquery.fancybox.pack.js?v=2.1.5');
    }

But when i set mergeJs to false:

'mergeJs' => false, //def:true

I get this error:

TypeError: $(...).fancybox is not a function
$(".fancybox").fancybox();

How i can resolve it?

#17912 report it
le_top at 2014/08/10 04:32pm
Compression issue

Well, the absUrl fix is integrated ;-).

The compression issue is that the resulting file contains gzip compressed content, and is not related to content minimized (or compacted) by JsMin.

#17911 report it
nlac at 2014/08/10 04:20pm
@le_top

Ok, somehow i skipped the toAbsUrl issue last time, focused only the second comment.. the fixes seem to be fine, have been added now. About the compression issue, the compression is done by the embedded JSMin.php, that project is unfortunately abandoned, no further buxfixes provided. Possible minification issues should be work-arounded, eg. deal with originally minified files and just concat them by NLSClientScript.

#17908 report it
le_top at 2014/08/09 06:57pm
Fix(es)

Ok for dataFilter - I suppose that is why my JSON calls do not seem to give any trouble. Some of the logic is done behind the scenes so I thought it did not apply.

I also reported about the 'toAbsUrl' method which is equally important. Otherwise combining(merging) scripts/css is not functional for 'localhost/workspace/index.php' or '//blabla.google.com/blabla'. A resource like 'themes/mytheme/js/script.js' would not work without one of the fixes for toAbsUrl.

Anyway, I also had some kind of compression issue where the combined file had compressed content - but only on the production server, so I suppose it has to do with compression from the content served by the production server and not with content served from third parties.

#17907 report it
nlac at 2014/08/09 04:36pm
@le_top

Thanks for the bug hunt, i added the fix.

About the old trick you pointed ($.ajaxSettings.dataFilter(data)), there's no need for that at all. From 5.0, the extension is rewritten in a totally different way, so you can work with any json ajax response without any neccessary post-processing.

#17904 report it
le_top at 2014/08/08 06:23pm
Duplicate library load fixed...

The core mystery has been solved: my url had a trailing '&' after removing the '='. That is because I added a timestamp to the assets which postfixes the urls with '?' which becomes '?&='.

So the normUrl function had to be updated with:

return url.replace(/\?*\&?(_=\d+)?$/g,"");

Finally, any plan to provide an alternative to

$.ajaxSettings.dataFilter(data)

?

#17902 report it
le_top at 2014/08/08 04:56pm
Issues - some solutions

I was still using 5.0, but for some reason my jQuery gets loaded two times when fetching a CJuiDialog through Ajax. So I was digging into that issue and decided to upgrade to the latest version to see if things were fixed.

Actually things are worse. My page layout and javascript were completely "scrambled". First, the function to combine files is on by default and some "urls" were not found. Second, my production server serves compressed files and they remain compressed after combining - they should be combined uncompressed!

The missing urls issue could be fixed by fixing the way the absUrl is computed. The following code works for me:

protected function toAbsUrl($relUrl) {
        if(substr($relUrl,0,2)==='//')
            return (Yii::app()->getRequest()->getIsSecureConnection()?'https:':'http:').$relUrl;
        else
            return preg_match('&^(http(s?):)?//&',$relUrl) ? $relUrl : rtrim($this->serverBaseUrl,'/') . '/' . (substr($relUrl,0,1)==='/'?'':Yii::app()->getRequest()->getBaseUrl().'/').ltrim($relUrl,'/');
    }

Changes are that if the url starts with '//' that only the scheme is prefixed. Also, when the url starts with '/', the path is absolute from the host, but when the '/' is missing, the url is relative, so 'baseUrl' has to be added, which is also done above.

I prefer using Yii methods if they exist, so in 'init()', I changed a few lines:

if (!$this->serverBaseUrl) {
            $this->serverBaseUrl=Yii::app()->getRequest()->getHostInfo();

So that works on my dev machine, but not on the server where compressed content is combined. I haven't checked that issue yet. I have now disabled combining again.

I still have the dual jQuery loading, so I continue to look into that core issue for me.

#17862 report it
nlac at 2014/07/31 05:55pm
moving to github

good point, it would have several advantages. i registered it on my todo list:)

Leave a comment

Please to leave your comment.

Create extension