Loading multiple webpack bundles correctly - javascript

tl;dr; how should you load multiple webpack bundle without littering your code with script tags
I've been looking into using webpack more however there seems to be a piece i'm missing when it comes to loading webpack bundles, up to now i've been using requirejs which allows you to split you code into modules (which i consider the equivalent of bundles in webpack) but requirejs allows you to require scripts by name in your code which is then mapped to a url in the config but with webpack you would just include a script tag onto the page which feels like it could get out of control quite easily as you would end up with scripts littered throughout the views of your application, and makes it more difficult to switch out bundles as instead of updating a single url you need to find and replace every occurance of that url, not the end of the world but it seems like i've either missed some functionality to make this easier or maybe it's an accepted difference between requirejs and webpack.
I should mention the none of the code bases i'm considering adding webpack too are single page applications so perhaps webpack is just not suited to this kind of environment?
Just to add a little bit of context to what i would intend this to be included in, our server side code uses a mvc pattern so it would look something like this
page skelton/layout
<html>
<head><!-- stuff here --></head>
<body>
<!-- some included view here -->
</body>
</html>
view 1
<div>
<!-- ... -->
<!-- Currently it has this -->
<script>
require(['something'], function(s){ /* new s(); */ /* s(); */ /* etc */ });
</script>
<!-- and i'd imagine it would be like this with webpack -->
<script src="my_bundle.js"></script>
</div>
view 2
<div>
<!-- ... -->
<!-- Currently it has this -->
<script>
require(['something', 'another_thing'], function(s){ /* new s(); */ /* s(); */ /* etc */ });
</script>
<!-- and i'd imagine it would be like this with webpack -->
<script src="my_bundle.js"></script>
<script src="my_bundle2.js"></script>
</div>

Recently I used webpack's code splitting functionality in my singe page application to dynamically load bundles based on the route. This doesn't require you to litter script tags throughout your application necessarily. If you use a routing mechanism of any sort you can dynamically import the dependency when that route is accessed like so:
// Index
router.on(() => {
import(/* webpackChunkName: "routeA" */ '../routes/routeA').then((module) => {
// do something with your loaded module
}).resolve();
});
Dynamically loading this 'root' style module, i.e. the root of a dependency tree bundled by webpack means you can only fetch it when you need to. This means the client will only fetch this bundle.js when you execute this route.
Using the dynamic imports requires the addition of the chunkFilename property in your webpack config:
output: {
filename: '[name].bundle.js',
chunkFilename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
Edit: If you're using express.js you could do the same following its routing mechanism. Your best bet is to refer to the code splitting documentation provided by webpack (linked above).

I wasn't entirely happy with the router approach as it would have effectivly meant having to duplicate our url rewriting config in javascript and duplicating import statements where pages share code and i'm not sure but the import docs seem to imply that any imported scripts would be included in the final bundle instead of dynamically loaded during page load which would mean a lot of code in the bundle that is unused most of the time.
it seems like the best approach for me would be to effectively create a simpler version of requirejs to load bundle files (example below) which would then allow me to keep a similar structure to the existing code, admittedly i might still look for a library instead of rolling my own loader but i haven't decided yet.
Loader
class Loader{
constructor(config){
this.config = config;
}
load(modName){
if(this.config.debug)console.log('loader - ', 'load called', arguments);
if(!document.getElementById(modName)){
if(this.config.debug)console.log('loader - ', 'loading new script', modName, this.config.map[modName]);
var script = document.createElement('script');
script.id = modName;
script.type = 'text/javascript';
script.src = this.config.map[modName];
script.async = true;
document.head.appendChild(script);
}
}
}
export default Loader;
Loader Config
window.loader = new Loader({
'debug': true,
'map': {
'something': '/scripts/bundle.js',
'another_thing': '/scripts/bundle2.js'
}
});
View 1
<div>
<!-- ... -->
<!-- Currently it has this -->
<script>
require(['something'], function(s){ /* new s(); */ /* s(); */ /* etc */ });
</script>
<!-- which would change to (any of the callback code would be inside the bundle) -->
<script>
window.loader.load('something');
</script>
</div>

Related

Loading a third-party javascript library

I developed a web radio player using Vue Cli. Now I have to add a new functionality using an external library (it's aimed to handle audio advertising) and this library must be served from the remote host. I can't just download the library and import it locally.
Setup
In my Vue App I have several components and in one of them I have an audio tag handling the radio playback. I need to detect the click on the play button, load the ad, play it and then go to the radio regular playback.
Approachs I tried
Loading the external library in the index.html file. It works but I can't interact with the player being loaded in Vue. For example, if I try to listen to the play event in the index.html file (audio.addEventListener("play", onPlay);, I just receive "audio not defined" in the web console.
Loading the external library in the mounted () section of my component:
const triton = document.createElement('script')
triton.setAttribute('src', '//sdk.listenlive.co/web/2.9/td-sdk.min.js')
document.head.appendChild(triton)
this.initPlayerSDK()
triton.onload = () => {
let player = new TDSdk(this.tdPlayerConfig)
console.log(player)
}
The problem with this approach is that after npm run serveI receive the message 'TDSdk' is not defined which makes complete sense. I'm loading the external JS file but webpack isn't interpreting its content because that it's done in runtime. I have to add the external in my vue.config.js, but this doesn't work neither:
vue.config.js
const path = require('path')
module.exports = {
publicPath: './',
/*configureWebpack: {
externals: {
tdSdk: 'tdSdk'
}
},*/
chainWebpack: config => {
config.module
.rule('images')
.test(/\.(png|jpe?g|gif|webp)(\?.*)?$/)
.use('url-loader')
.loader('file-loader') // not url-loader but file-loader !
.tap((options) => { // not .option() but .tap(options...)
// modify the options...
options.name = 'img/[name].[ext]'
return options
}),
config.externals([
{
'tdSdk': 'TDSdk'
},
])
},
css: {
loaderOptions: {
sass: {
sassOptions: {
includePaths: [path.resolve(__dirname, './node_modules/compass-mixins/lib')]
}
}
}
},
externals: {
tdSdk: 'TDSdk'
}
}
myComponent.vue
import tdSdk from 'tdSdk'
My solution was to load the library in the public/index.html file and then wait for the DOM being loaded so I could add the event listener to the audio element already loaded:
document.addEventListener('DOMContentLoaded', function() {
var playControl = document.getElementById('playIcon');
playControl.addEventListener("click", onPlay);
}
Then, in the Vuex store I needed to access the variables defined in the javascript located in the index.html. To do that, I set the window.adState (the var I'm using) as a global var in my store file:
Vuex.Store.prototype.$adState = window.adState
Finally, in my actions/mutations I used this.$adState to check its content:
playPause ({ commit, dispatch }) {
console.log('AdState', this.$adState)
(...)
}
Answer added on behalf of OP.
The import cannot be resolved at the time when the script is evaluated because TDSdk global is not available. A script that is dynamically added to head is loaded asynchronously so there will be race condition any way.
<script> needs to be added dynamically if there's dynamic behaviour involved or like a condition or a developer doesn't have control over page layout. For static script, Vue CLI project's public index.html can be modified:
<body>
<div id="app"></div>
<script src="//sdk.listenlive.co/web/2.9/td-sdk.min.js"></script>
<!-- built files will be auto injected -->
</body>
Application bundle is evaluated after the script and the global is expected to be available there.
Externals are commonly used for packages that were swapped to ones that were externally loaded, usually from CDN. Since tdSdk is not a real package and has no prospects to be swapped for one, it doesn't serve a good purpose to map it to a global and import it. It can be just used as TDSdk global in the application.

Using non-npm(legacy) javascript library with Jspm

I'm trying to integrate this library which is non-npm. I failed many times already as I always thrive for using some modern framework which makes it impossible for me to integrate.
I tried backbone.js with require.js, even Dart and now I'm stubbornly trying to achieve the same using gulp, jspm, aurelia. The problem is that this library probably doesn't follow the module concept. I had lot of problems with initialization of this lib, made a lot of shimming.
So the question is how can I use such kind of libraries. Using in the same time modern ways to build javascript applications.
For older libraries that don't follow modern module patterns the approach is usually to shim them.
If you're using webpack, you can shim modules by declaring imports and exports.
RequireJS has a similar shim config, but needs more wiring to declare dependencies. I'd strongly recommend webpack over Grunt/gulp/RequireJS.
However, looking at the mapy.cz library you linked, it dynamically loads many other assets by writing script tags to the page. I can see how that's hard to work with.
I think your options are really:
If the licence is a friendly open-source one, fork it and expose it in a more modern module format that can be easily imported via npm. Check out the UMD style - you can write a declaration that'll export the module in a format usable by most module systems (AMD, CommonJS, etc). The webpack library and externals page has some guidelines for writing modules in a format that others can use.
If it's not an open-source licence, you could get in touch with the author(s) to ask them to change how the library is bundled and loaded. It should be an easy sell: an npm module would allow more people to use their code, and would be easier to work with - especially if they started versioning it. You could offer to do it for them, or just do it as an example they can copy from.
They have a page detailing terms and conditions, as well as a 'contact us' button - I'd start there: http://napoveda.seznam.cz/cz/mapy/mapy-licencni-podminky/licencni-podminky-mapovych-podkladu/
After looking at the code, I got it working (I used require.js, but you can use whatever you like):
// main.js
////////////////
require(['mapy-loader'], function (Loader) {
// load mapy async and wait for it to finish
Loader.async = true;
Loader.load(null, null, function () {
var stred = SMap.Coords.fromWGS84(14.41, 50.08);
var mapa = new SMap(JAK.gel("mapa"), stred, 10);
mapa.addDefaultLayer(SMap.DEF_BASE).enable();
mapa.addDefaultControls();
});
});
<!doctype html>
<html>
<head>
<script data-main="main.js" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.2.0/require.min.js"></script>
<script>
requirejs.config({
paths: {
"mapy-loader": "//api.mapy.cz/loader"
},
shim: {
'mapy-loader': {exports: 'Loader'}
}
});
</script>
</head>
<body>
<div id="mapa" style="width:600px; height:400px;"></div>
</body>
</html>
(It won't run in the snippet here, since the JavaScript should be placed in a file named main.js)
EDIT:
Adding jspm / System.js snippet:
(main.js is unchanged)
// main.js
////////////////
define(['mapy-loader'], function (Loader) {
// load it async and wait for it to finish!
Loader.async = true;
Loader.load(null, null, function () {
var stred = SMap.Coords.fromWGS84(14.41, 50.08);
var mapa = new SMap(JAK.gel("mapa"), stred, 10);
mapa.addDefaultLayer(SMap.DEF_BASE).enable();
mapa.addDefaultControls();
});
});
<!doctype html>
<html>
<head>
<script src="jspm_packages/system.js"></script>
<script>
System.config({
baseURL: "/",
defaultJSExtensions: true,
transpiler: "babel",
paths: {
"mapy-loader": "//api.mapy.cz/loader"
},
meta: {
'mapy-loader': {
format: 'global',
exports: 'Loader'
}
}
});
</script>
<script>
System.import('main.js');
</script>
Run
</head>
<body>
<div id="mapa" style="width:600px; height:400px;"></div>
</body>
</html>

Calling require(['app']) Only Once

How can I call requireJS require(['app'], function() {}); only once at the beginning for the whole application so that any subsequent require(["..."], function(...){}); don't need to be wrapped within require(['app']?
This is my set up:
1) Load require.js
<script data-main="js/app.js" src="requirejs/require.min.js"></script>
2) Have app.js shims and basUrl configured properly.
requirejs.config({
baseUrl: "scripts/js",
paths: {
"jquery": "../bower_components/jquery/dist/jquery.min",
"modernizr": "../bower_components/modernizr/modernizr",
.
.
.
},
shim: {
"jquery.migrate": ['jquery'],
.
.
.
}
});
3) Dynamically load JS on different pages:
// Home Page
require(['app'], function() {
require(["jquery", "foundation", "foundation.reveal"], function ($, foundation, reveal){
$(document).foundation();
});
});
// Catalog Page
require(['app'], function() {
require(["jquery", "lnav/LeftNavCtrl","controllers/ProductCtrl", "controllers/TabsCtrl"], function ($, NavCtrl, ProductCtrl, TabsCtrl){
$(function() {
NavCtrl.initLeftNav();
});
});
});
Unless I wrap with require(['app'], function()) each time I call require("...") to load external JS or AMD modules, the app is not initialized and I get JavaScript errors. The above code works but it's not very efficient.
Is there a way to start my requireJS app before I try loading scripts?
I tried calling at the very beginning right after I load require.min.js:
require(["app"], function (app) {
app.run();
});
but it didn't work.
There are no provisions in RequireJS to ensure that a specific module is always loaded before any other module is loaded, other than having your first module load the rest. What you are trying to do is share your first module among multiple pages so it cannot perform the work of loading what is specific to each page.
One way you can work around this is simply to load app.js with a regular script element:
<script src="requirejs/require.min.js"></script>
<script src="js/app.js"></script>
Then the next script element can start your application without requiring app.js:
<script>
require(["jquery", "foundation", "foundation.reveal"], function ($, foundation, reveal){
$(document).foundation();
});
</script>
This is actually how I've decided to launch my modules in the applications I'm working on right now. True, it is not as optimized as it could be because of the extra network round-trip, but in the case of the applications I'm working on, they are still in very heavy development, and I prefer to leave this optimization for later.
Note that generally you don't want to use script to load RequireJS modules but your app.js is not a real module as it does not call define, so this is okay.
Another option would be to use a building tool like Grunt, Gulp, Make or something else and create one app.js per page and have each page load its own app.js file. This file would contain the configuration and the first require call to load the modules specific to your page.

Requirejs, basic information

I'm learning Requirejs and I started with two simple .html pages: index.html and second.html.
On the index.html I worte:
<script data-main="assets/js/app.min" src="js/vendor/require.js"></script>
The app.min.js file look like this:
requirejs.config({
baseUrl: 'js/vendor',
paths: {
app: '../app',
jquery: 'jquery-1.10.1.min'
}
});
requirejs(["app/main"]);
My app/main.js file has just a jQuery alert:
define(['jquery'], function($) {
$(function() {
alert('Hello World');
});
});
It works fine!
Now I'm worried just about one thing... What about if I need to load the app/main globally for all my pages and then another file like app/second that run only on second.html page?
Probably I'm missing something about Requirejs... I don't thinks that I need to load everything on the app.min.js file like did for the app/main.
I understand that I can define modules on separate js files but then how can I manage different files for different pages without loading everything in just one file? Probably I'm wrong, I hope you can open the light in my brain for that.
Thanks
I understand that a page might need its own code in addition to what is in app.min. You could do something like this:
<script data-main="assets/js/app.min" src="js/vendor/require.js"></script>
<script>
// You can call the config function as many times as you need to add new configuration.
requirejs.config({
// Presumably, baseUrl does not need to be changed.
// baseUrl: 'js/vendor',
paths: {
// additional paths you may need
}
});
// This loads the code proper to this page.
requirejs(["app/second"]);
</script>
If app/second depends on app/main make sure to have that dependency listed in app/second's define call.
Take a look at this example: https://github.com/requirejs/example-multipage. The example demonstrates how you can create page1.js and page2.js and in those files load the common stuff + page specific things. That's one of several ways to do it.
Another way to do which is what I often use is putting something like this in all your pages:
<script src="require.js"></script> <!-- just require -->
<script src="app.min.js"></script> <!-- your config and also loading the main module -->
and then on second.html, you would also add this
<script>require(["app/second"])</script>
You can use this setup for development, and for production you can replace the first 2 lines with just <script src="optimized-bundle.js"></script>. The optimized-bundle.js could include require.js + config + app/main + app/second. Or if you want to load app/second only on the second.html in production to make your main script smaller, you can have require.js + config + app/main in the primary bundle and optimize app/second into a separate bundle - the html would stay the same in both cases.
Hope this helps.

Is it possible to stop requireJS from adding the .js file extension automatically?

I'm using requireJS to load scripts. It has this detail in the docs:
The path that is used for a module name should not include the .js
extension, since the path mapping could be for a directory.
In my app, I map all of my script files in a config path, because they're dynamically generated at runtime (my scripts start life as things like order.js but become things like order.min.b25a571965d02d9c54871b7636ca1c5e.js (this is a hash of the file contents, for cachebusting purposes).
In some cases, require will add a second .js extension to the end of these paths. Although I generate the dynamic paths on the server side and then populate the config path, I have to then write some extra javascript code to remove the .js extension from the problematic files.
Reading the requireJS docs, I really don't understand why you'd ever want the path mapping to be used for a directory. Does this mean it's possible to somehow load an entire directory's worth of files in one call? I don't get it.
Does anybody know if it's possible to just force require to stop adding .js to file paths so I don't have to hack around it?
thanks.
UPDATE: added some code samples as requested.
This is inside my HTML file (it's a Scala project so we can't write these variables directly into a .js file):
foo.js.modules = {
order : '#Static("javascripts/order.min.js")',
reqwest : 'http://5.foo.appspot.com/js/libs/reqwest',
bean : 'http://4.foo.appspot.com/js/libs/bean.min',
detect : 'order!http://4.foo.appspot.com/js/detect/detect.js',
images : 'order!http://4.foo.appspot.com/js/detect/images.js',
basicTemplate : '#Static("javascripts/libs/basicTemplate.min.js")',
trailExpander : '#Static("javascripts/libs/trailExpander.min.js")',
fetchDiscussion : '#Static("javascripts/libs/fetchDiscussion.min.js")'
mostPopular : '#Static("javascripts/libs/mostPopular.min.js")'
};
Then inside my main.js:
requirejs.config({
paths: foo.js.modules
});
require([foo.js.modules.detect, foo.js.modules.images, "bean"],
function(detect, images, bean) {
// do stuff
});
In the example above, I have to use the string "bean" (which refers to the require path) rather than my direct object (like the others use foo.js.modules.bar) otherwise I get the extra .js appended.
Hope this makes sense.
If you don't feel like adding a dependency on noext, you can also just append a dummy query string to the path to prevent the .js extension from being appended, as in:
require.config({
paths: {
'signalr-hubs': '/signalr/hubs?noext'
}
});
This is what the noext plugin does.
requirejs' noext plugin:
Load scripts without appending ".js" extension, useful for dynamic scripts...
Documentation
check the examples folder. All the info you probably need will be inside comments or on the example code itself.
Basic usage
Put the plugins inside the baseUrl folder (usually same folder as the main.js file) or create an alias to the plugin location:
require.config({
paths : {
//create alias to plugins (not needed if plugins are on the baseUrl)
async: 'lib/require/async',
font: 'lib/require/font',
goog: 'lib/require/goog',
image: 'lib/require/image',
json: 'lib/require/json',
noext: 'lib/require/noext',
mdown: 'lib/require/mdown',
propertyParser : 'lib/require/propertyParser',
markdownConverter : 'lib/Markdown.Converter'
}
});
//use plugins as if they were at baseUrl
define([
'image!awsum.jpg',
'json!data/foo.json',
'noext!js/bar.php',
'mdown!data/lorem_ipsum.md',
'async!http://maps.google.com/maps/api/js?sensor=false',
'goog!visualization,1,packages:[corechart,geochart]',
'goog!search,1',
'font!google,families:[Tangerine,Cantarell]'
], function(awsum, foo, bar, loremIpsum){
//all dependencies are loaded (including gmaps and other google apis)
}
);
I am using requirejs server side with node.js. The noext plugin does not work for me. I suspect this is because it tries to add ?noext to a url and we have filenames instead of urls serverside.
I need to name my files .njs or .model to separate them from static .js files. Hopefully the author will update requirejs to not force automatic .js file extension conventions on the users.
Meanwhile here is a quick patch to disable this behavior.
To apply this patch (against version 2.1.15 of node_modules/requirejs/bin/r.js) :
Save in a file called disableAutoExt.diff or whatever and open a terminal
cd path/to/node_modules/
patch -p1 < path/to/disableAutoExt.diff
add disableAutoExt: true, to your requirejs.config: requirejs.config({disableAutoExt: true,});
Now we can do require(["test/index.njs", ...] ... and get back to work.
Save this patch in disableAutoExt.diff :
--- mod/node_modules/requirejs/bin/r.js 2014-09-07 20:54:07.000000000 -0400
+++ node_modules/requirejs/bin/r.js 2014-12-11 09:33:21.000000000 -0500
## -1884,6 +1884,10 ##
//Delegates to req.load. Broken out as a separate function to
//allow overriding in the optimizer.
load: function (id, url) {
+ if (config.disableAutoExt && url.match(/\..*\.js$/)) {
+ url = url.replace(/\.js$/, '');
+ }
+
req.load(context, id, url);
},
The patch simply adds the following around line 1887 to node_modules/requirejs/bin/r.js:
if (config.disableAutoExt && url.match(/\..*\.js$/)) {
url = url.replace(/\.js$/, '');
}
UPDATE: Improved patch by moving url change deeper in the code so it no longer causes a hang after calling undef on a module. Needed undef because:
To disable caching of modules when developing with node.js add this to your main app file:
requirejs.onResourceLoad = function(context, map)
{
requirejs.undef(map.name);
};

Categories