I have written this piece of JS and CSS loading code and I would like some advice on it. Anything some of the Javascript Gurus could possibly point out would be much appreciated. The code works, but I have not done extensive testing, because I am concerned about replacing functions in this manner.
A single javascript file containing JQuery as well as the below code will be included on all the pages. We write all the components in house and keep them very modular separated into their own folder with the corresponding JS and CSS. You can imagine starting to use for instance a dropown, dialog and a datepicker on one page would require us to add 6 includes and this quite frankly is annoying, because I want the dependencies to resolve automatically and using JSP includes could possibly make multiple calls to the same resources.
Below is the src to load a single datepicker lazily
;(function($){
//All Lazily loaded components go here
$.fn.datepicker = function(settings){
console.log("This should only be displayed once");
loadCSS("/res/component/datepicker/datepicker.css");
var elem = this;
return loadJS("/res/component/datepicker/datepicker.js",
function(){return elem.datepicker(settings)});//After Load Completion the $.fn.datepicker is replaced
//by the proper working implementation, execute it and return it so we maintain the chain
};
}(jQuery));
function loadCSS(absoluteUrl){
if(loadCSS[absoluteUrl])
return;//Css already loaded
$('<link>')
.appendTo('head')
.attr({type : 'text/css', rel : 'stylesheet'})
.attr('href', absoluteUrl);//Appending entire element doesn't load in IE, but setting the href in this manner does
loadCSS[absoluteUrl] = true;//Memoize
}
function loadJS(absoluteUrl, onComplete){
if(loadJS[absoluteUrl])
return;//Script already loaded
loadJS[absoluteUrl] = true;//Memoize
var result;
jQuery.ajax({
async : false,//Synchronized because we need to maintain the JQuery chain
type :'GET',
url : absoluteUrl,
dataType :'script',
success : function(){
result = onComplete();
}
});
return result;
}
Have you looked in to Require JS, it will send async requests for only the modules you need for a given module.
In addition, because dependencies are scoped to the callback function, namespaces clashing is less of an issue
Typically you would have:
require(["jquery", "foo", "bar"], function($, foo, bar){...});
which allows your code to remain modularized both server side, and client side, in separate locations.
Of course, you need to set up require on your server with a config (described in the webpage), and wrap your resources in define blocks:
define("foo", ["jquery"], function($){...});
The downside is performance on pages that require many modules. In this situation you benefit more from having all resources in combined files, but note that query strings will cause the browser not to cache files in any case.. which is also another performance consideration.
Hope that helps
ps. In terms of CSS lazy loading, you could always use javascript to inject link tags into the head adhoc, and provide some javascript interface functions that your other code can call in order to request a CSS dependency dynamically.
Related
I would like to use Plupload in an Angular2 component and access the Plupload JavaScript file from a CDN. I want it specific to a component so that it is not downloaded if it is not required - I want it to be in a lazy loaded module. How can I do this?
Now fully answered on this page!
The result of this quest, which included offering and awarding bounties to two people who worked hard with me on it, is as follows:
Example of using Plupload with Angular 2 and TypeScript
How to Lazy load a script from a CDN in Angular 2
Example of how to use Plupload in a lazy loaded module
How to use a lazy loaded script in Angular 2
(See edit history for the ugly details that used to make up this question.)
Here's the overview of what you need to do to create a lazy-loaded Plupload feature while loading Plupload from a CDN:
When the feature is needed (e.g. user clicks a button or visits a page), dynamically add a <script> tag to the page to load the Plupload library from a CDN.
Wait until the library is loaded to proceed (or you could get a "plupload is undefined" error).
Display the UI to interact with Plupload in one of your Angular templates. In its simplest form, this UI consists of two buttons: "Select files" and "Upload files".
Initialize Plupload and wire it up to the UI.
Complete, working code: https://plnkr.co/edit/4t39Rod4YNAOrHmZdxqc?p=preview
Please take note of the following points in my implementation:
Regarding #2. A better way to check whether Plupload has finished loading would be to poll the global namespace for the existence of the plupload variable. As long as window.plupload does not exist, it means the library hasn't been loaded yet and that we should NOT proceed. For simplicity my code just waits for one second and proceeds.
Number 4 can prove a bit tricky. Plupload makes a heavy use of direct DOM access to wire its API to the HTML (e.g. document.getElementById('filelist')). This is something Angular discourages and that you should try avoiding whenever possible. More specifically direct DOM access is used in the following places:
To tell Plupload which DOM element should trigger the "Select files" dialog (what they call the browse_button config option). For this I could not avoid the direct DOM reference and I used the #ViewChild decorator to get a hold of the "Select Files" button.
To display selected files in the template. For this I converted the Plupload syntax into the regular Angular syntax. I push selected files to a class property called fileList which I display in the template using a standard *ngFor.
The "Upload Files" button triggers some code that does the actual uploading and refreshes the UI to show upload progress. Once more, I converted this to regular Angular syntax using event binding and data binding.
Let me know if you have any questions.
In this approach no need for any extra loader modules.
See example (check console for Woohoo): http://plnkr.co/edit/gfUs4Uhe8kMGzPxwpBay?p=preview
updated plunker: https://plnkr.co/edit/leG062tg7uX8sLrA0i2i?p=preview
You can lazyload some js by adding the script url to you document:
Create a my-lazy-load.function.ts:
export function lazyload(url) {
// based on https://friendlybit.com/js/lazy-loading-asyncronous-javascript/
let scripts = document.getElementsByTagName('script');
for (let i = scripts.length; i--;) {
if (scripts[i].src.match(url)) return true;
}
let s = document.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = url;
let x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
return true;
}
In your component that you want to add plupload:
import {lazyload} from "./my-lazy-load.function.ts";
export class MyComponent implements OnInit {
pluploadInterval:number = null;
hasPlupload: boolean = false;
ngOnInit() {
lazyload("https://cdnjs.cloudflare.com/ajax/libs/plupload/2.3.1/plupload.full.min.js");
this.pluploadInterval = window.setInterval(()=>{
if(window.plupload) { // you can check existence of plupload object any way you want
// Woohoo, I am ready to be used
this.hasPlupload = true; // everything is run outside ngZone, wrap it if angular2 is not reacting to changes, or change window.setInterval to setInterval
window.clearInterval(this.pluploadInterval); // don't forget to clean interval
}
}, 100); // timeinterval can vary
....
The browser will load this automatically.
Notice if(plupload) it assumes that there is global object plupload that the script adds (I do not know if it truely added, check your working example in pure javascript). As it is jquery extension you can check it's prototype like this: jQuery test for whether an object has a method?
OLD HISTORICAL:
#Reid here is plunker: https://plnkr.co/edit/zDWWQbTQUSHBqCsrUMUi?p=preview the plupload is actually loaded, but added to require with define("plupload", ['./moxie'], extract); I am not sure at the moment how to extract from there and which package require is belong to... the code for finding correct module loader belongs to plupload itself, here it is (from plupload.dev.js):
if (typeof define === "function" && define.amd) {
define("plupload", ['./moxie'], extract);
} else if (typeof module === "object" && module.exports) {
module.exports = extract(require('./moxie'));
} else {
global.plupload = extract(global.moxie);
}
I think that your best bet is to use the Require.js Library so that you can dynamically load your scripts from within your components.
The small trade off is that you will have to add this 18KB library to your index.html page (CDN), however this could save you huge amounts of loading if your 3rd party libraries are massive.
I have no experience with using plupload, so instead I put together the following plunkr which uses an external animation library, drawn from a CDN. The plunkr animates a number from 0 - 100.
https://plnkr.co/edit/fJCtezsERYHOYplLh7Jo?p=preview
index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.3/require.min.js"></script>
component.ts
ngOnInit(){
// Dynamically loads the framework from the CDN.
require(["https://cdnjs.cloudflare.com/ajax/libs/gsap/1.19.1/TweenLite.min.js"],
// This function is then called ONCE LOAD IS COMPLETE
function (common) {
// Do greensock things
var demo = {score:0},
scoreDisplay = document.getElementById("scoreDisplay");
//create a tween that changes the value of the score property of the demo object from 0 to 100 over the course of 20 seconds.
var tween = TweenLite.to(demo, 20, {score:100, onUpdate:showScore})
//each time the tween updates this function will be called.
function showScore() {
scoreDisplay.innerHTML = demo.score.toFixed(2);
}
});
}
What I like about this approach, is that in the onLoad callback from require, the syntax is unchanged from a normal implementation of the library, so you can just copy paste your currently working code into the callback.
I have a interesting concept I was working on and looking over, through various stack questions on auto loading JavaScript. I dint want to use a third party tool, aside form jquery, so I thought I would role my own. The concept I have is:
var scripts = {
'name' : 'path/to/script_dir/' // Load all scripts in this file.
}
requireScripts(scripts); // Requires all scripts
// Call your classes, methods, objects and so on ....
The requireScript() function would work something like:
function requireScript(hash){
$.each(hash, function(key, value)){
$.ajax({
url: value,
dataType: "script",
async: false,
error: function () {
throw new Error("Could not load script " + script);
}
});
});
}
Note: The above is just a concept, I don't think it will work.
The above would let you load SPECIFIC scripts. so in essence your hash key value would be 'name' : 'path/to/specific/script'. The issue this posses is that your hash would get rather large ....
The other issue I ran into is what if I simplified this to "php pear naming standard" so, as the trend seems to be - we would create a class, and it would be named after its location:
var some_folder_name_class = function(){}
Would be translated by the autoloader as: some/folder/name/class.js and then loaded that way.
To wrap up and get to my point there are two ways of loading javascript file I am looking at, via rolling my own "require" method. One is loading a directory of javascript files via the hash idea implemented above. (the provided code sample of how this hash would be walked through would have to be changed and fixed....I dont think it works to even load a single file)
OR
to have you just do:
new some_class_name() and have a global function listen for the new word, go find the file your trying to call based on the name of the class and load it, this you never have to worry - as long as you follow "pear naming standards" in both class and folder structure your js file will be loaded.
Can either approach be done? or am I dreaming to big?
I see a lot of frameworks do a bunch of require('/path/to/script') and if I could role my own autoloader to just allow me to either load a directory of js files or even have it where it listens for new before a class instantiation then I could make my life SO MUCH easier.
Have you consider using requirejs and probably Lazy loading.
http://www.joezimjs.com/javascript/lazy-loading-javascript-with-requirejs/
Here is sample version:
You can download here.
The sample is based on this folder structure :
public
index.html
scripts
app.js
lib
** jquery-1.10.2.js
** require.js
3 . From Code:
html
`<!DOCTYPE html><html>
<head><title>Sample Test</title>`
<script src="scripts/lib/require.js"></script> <!-- downloaded from link provide above-->
<script src="scripts/app.js"></script></head>
`<body><h1>My Sample Project</h1><div id="someDiv"></div></body></html>`
application configuration app.js
requirejs.config({
baseUrl: 'scripts',
paths: {
app: 'app',
jquery: 'lib/jquery-1.10.2' //your libraries/modules definitions
}
});
// Start the main app logic. loading jquery module
require(['jquery'], function ($) {
$(document).on('ready',function(){
$('#someDiv').html('Hello World');
});
});
jQuery-only option
If you are looking for a jQuery-only solution, have a look at jQuery.getScript(). It would be a great candidate for handling the script loading portion of your problem. You could then write a very small wrapper around it to load all the scripts—something like you wrote above:
var loadScripts = function(scripts) {
$.each(scripts, function(name, path) {
jQuery.getScript("/root/path/" + path + ".js");
})
}
If you are interested in more information on this approach, read this article by David Walsh.
Other great libraries
I strongly recommend taking a look at the current batch of script-loading libraries. I think that you will pleasantly surprised by what is out there. Plus, they come with the benefit of great community support and documentation. RequireJS seems to be the front runner but David Walsh has great articles on curl.js and LABjs.
I've noticed that RequireJS creates script tags in the tag as it loads modules.
Is there anyway to configure RequireJS to "tag" those elements w/ a class or an attribute of some kind that I could later target w/ jQuery later on?
e.g.:
var $requireJsScripts = $('script.require-script');
--UPDATE--
Ok.. I think I can get by on this little workaround for now. Thanks to this answer for the breadcrumb on require.s.contexts._.defined. I'd still like to hear if anyone knows of a way to configure RequireJS to do something similar to what was laid out in the original question...
var loadedRjsModules = Object.keys(require.s.contexts._.defined);
var $scripts = $('script');
$scripts.each(function () {
if ($(this).data('requiremodule') && $.inArray($(this).data('requiremodule'), loadedRjsModules)) {
console.log(this);
}
});
Looking at the source code, I don't see how RequireJS would allow adding anything custom to the script nodes at creation. The routine that creates them has no provision for it. The code that fleshes them out upon creation does not support it either.
There's an onResourceLoad hook considered part of the internal API. It could be used with the code you've put in your question instead of relying on require.s.contexts._.defined, which as far as I know is fully private and subject to change without notice.
I'm getting into writing some more complex javascript applications, and I'm running into the limitations of my own knowledge-- please forgive any naming errors or obvious noob stuff, I'm not a js pro!
I have about 4 or 5 scripts I've put in their own files, just to keep things a little easier to maintain. So maybe there's one script that deals with building page elements (like complex forms), another that just handles data, creating generic ajax request objects, defining parsers and error functions for the data returned, and another that is purely display-oriented.
I've set global variables in the page that then get populated by various scripts that get loaded at run time. For example, I define var myapp = { }; in the main HTML page, and then in the scripts various function populate this "namespace" like:
myapp.myfunction = function(){
// do stuff
}
The problem is that despite all the scripts including a $(document).ready(function() block that wraps all function definitions, when a function is called from one script that refers to another (that is, if my data.js file calls a function myapp.myDisplayFunction that is in the display.js file, I sometimes get an Object has no method 'myDisplayFunction'
Other than slamming all functions into one massive script, how do you deal with this problem? Is there a best practice that I'm missing? or is this just a question of specifying a different order that the scripts are called in?
Thanks
When you are not sure if method you are about to call exists (is already loaded) you can do a check:
if (myapp) //my app namespace is defined
{
if (myapp.myFunction) //myFunction is defined
{
myapp.myFunction();
}
else
alert('You have to load myFile.js first!');
}
Just check for the function before using:
if(typeof(myapp.myDisplayFunction) !== undefined) {
// do your stuff
} else {
// wait for a while
}
And check whether you have async attribute set while loading the .js files.
I've created a jQuery UI widget which is dependent on some other custom JavaScript modules. I fetch these modules using requirejs during the "_create" method of the widget. This actually works great if, I have my end consumers define my "data-main" property. However, in the situation where my consumers are using requirejs on their own, and defining their own "data-main" property, this doesn't work.
Since I'm using requirejs to inject scripts via my widget from a totally different server, I run into a few problems with requirejs's normal way of dealing with this.
First, I can't use a package.json file unless I assume that all of my consumers have a package.json which contains the exact same resources as I have. On top of that, I've got DEV, TEST and PROD server URLs to deal with.
Second I can't use require.config to set my baseUrl during a load on their server, cause it may break everything that they are using require for.
The current implementation I have working requires the consumer to add a script reference to require with my data-main location (external server). Then add a script ref to my widget (external server). This works because nobody else at my company has ever even heard of requirejs :). The second I start showing them how to bundle all of their code into reusable JavaScript modules my solution is broken.
I want to come up with a solution whereas the end consumer can simply reference my single JavaScript widget, which in turn loads everything it needs to function.
Any suggestions on how to do this? I've thought about hacking my own version of require with a static data-main, then just assume they can have multiple requirejs libs. I WOULD HATE TO DO THAT...but I can't really think of a better way.
Here is what I am going to do...
Couple of notes:
I'm using the jQuery UI widget factory pattern (but this isn't exactly a widget)
The widget code lives on a remote server and consumers only reference it, don't download it
I'm using requirejs to load widget dependencies
I want the greatest ease-of-use for the consuming developer
Since it's required that my jQuery UI widget be loaded ASAP so that the consumer has the context of the widget right away ( $(selector).mywidget ) I've decided to tackle my problem inside of the _create method.
This code basically installs requirejs if it doesn't exist, then uses it to install an array of requirements which the aforementioned widget needs to consume. This allows me to assume that the end user can reference my "widget" script by URL, tack on a "data-requiremodule" attribute of the same name, and get a complete list of remote dependencies.
_create: function () {
var widget = this;
widget._establish(widget, function () {
widget._install(widget);
});
},
_getBaseURL: function (scriptId, callback) {
var str = $('script[data-requiremodule="' + scriptId + '"]').attr('src');
if (callback) callback(str.substring(str.search(/scripts/i), 0));
},
_require: function (requirementAry, baseUrl, callback) {
require.config({ baseUrl: baseUrl });
require(requirementAry, function () {
if (callback) callback();
});
},
_establish: function (widget, callback) {
if (typeof require === 'undefined') {
widget._getBaseURL(widget._configurations.widgetName, function (baseUrl) {
var requireUrl = baseUrl + 'scripts/require.min.js';
baseUrl = baseUrl + 'scripts/';
$.getScript(requireUrl, function (data, textStatus) {
widget._require(widget._configurations.requiredLibs, baseUrl, function () {
callback(textStatus);
});
});
});
}
},
I'm not showing my "_configurations" object here...but you get the idea. I hope this helps someone else besides me :).