I've got a webpack script
const webpack = require('webpack');
const path = require('path');
module.exports = (env, options) => ({
entry: "./clickscape.js",
output: {
path: path.resolve(__dirname, '../priv/static/js'),
filename: 'clickscape-bundle.js'
},
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
})
]
})
that generates a bundle. Inside my clickscape.js, there are functions that I want to be able to access in the HTML page in which the bundle is loaded, i.e.:
<script src="http://localhost:4000/js/clickscape-bundle.js"></script>
<script>do_something_clickscape_dot_js()</script>
It seems that because webpack obfuscates all the code it compiles, it's not obvious what to call outside it.
What do I need to do to make my calls work?
Unless such a variable is global, you can't. Which generally is a good thing as it prevents name collisions between your own variables and those of other (external) scripts.
Inside my clickscape.js, there are functions that I want to be able to
access in the HTML page in which the bundle is loaded (...)
If you necessarily want to access them in the HTML page, you could make them global in ./clickscape.js:
window.do_something_clickscape_dot_js = function() {
// your code
}
Now do_something_clickscape_dot_js() works in your HTML page. But why not call it directly in ./clickscape.js?
const do_something_clickscape_dot_js = () => {
// do something here
}
/*
* calling the function in this file has same effect as calling it
* in script tag in the HTML page
*/
do_something_clickscape_dot_js();
I can't think of a use case where you necessarily want to call it in the HTML page, but if you want to: go for a global.
Related
I am brand new to using webpack. I do not understand how to use the libraries I have created with webpack in browser based JavaScript. I have built a library with the following modules from AWS SDK JavaScript version 3.
#aws-sdk/client-cognito-identity
#aws-sdk/credential-provider-cognito-identity
#aws-sdk/client-dynamodb
#aws-sdk/client-dynamodb-streams
#aws-sdk/client-s3
Here are the contents of webpack.config.js:
const path = require('path');
module.exports = {
entry: './aws-sdk-build.js',
output: {
path: path.resolve(__dirname, './src/js'),
filename: 'aws-sdk-js-v3.js',
library: "awssdk"
},
mode: 'production', //development
target: 'web'
};
I added <script type="module" src="js/aws-sdk-js-v3.js"></script> to the head of my index.html.
What I can't figure out is how to access the modules inside aws-sdk-js-v3.js from within the browser's javascript.
In the entry point of your module aws-sdk-build.js, you can create some on load behavior to interact with the DOM.
e.g. with JQuery:
$(() => {
const someElement = document.getElementById("targetElement");
if (someElement) {
// initialize your cool thing with someElement as mount point
}
});
You can also expose certain pieces of your code by assigning them to the window object.
For example, you can use something like this in your module entry point:
Object.assign(window, {
MyGreatClass,
reallyUsefulFunction
});
...and then you will be able to play with MyGreatClass / reallyUsefulFunction within that page containing the script tag (and in the dev console).
If memory serves, another alternative is to do something within that script tag:
<script type="module" src="js/aws-sdk-js-v3.js">
// module exports can be accessed within here
</script>
Although I'm not sure on the usage for the last option or if I'm mistaken so someone please correct me.
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.
First time for me on Stack Overflow, please be kind ;) I'll try to do my best!
The context:
I am working on a Rails 6 app with webpacker. This is a program that will be shared by several companies and in order to apply the 'one code, multiple setups' paradigm, we decided to move all the company related configuration files to separate folders, and to put the company name as a variable in our .env file. We need to change some config variables as well as some geofencing data (so our customers can create a new delivery to some address). Basically that's what it looks like:
Project folder
| config
| companies
| a_first_company
| rails_config.rb
| geofencing.js
| a_second_company
| rails_config.rb
| geofencing.js
| ....
In the .env file:
COMPANY=a_first_company
And in the rails configuration (application.rb), we are using a simple:
require_relative "companies/#{ENV['COMPANY']}/rails_config"
But now, here comes the JS part! And I am running into trouble.
The problem:
I would like to include dynamically a JSON object in an existing script. A sample geofencing.js looks like that:
module.exports = {
"countries": ["be"],
"polygon": [
50.8917729, 4.3004608,
...
50.9162381, 4.3450928,
50.8917729, 4.3004608
]
}
And I am trying to import it as a geofencing variable in my existing address autocompletion script:
/app/javascript/plugins/places.js
// I know it doesn't work that way, but basically that what I would like to do:
const geofencing = require(`/config/companies/${process.env.COMPANY}/geofencing`);
...
const initPlaceAutocomplete = () => {
...
var placesAutocomplete = places(
{
// And use the variable here...
insidePolygon: [geofencing.polygon],
type: 'address',
// And there...
countries: geofencing.countries,
templates: {
value: (suggestion) => {
return suggestion.name;
}
},
container: addressInput
}
);
...
}
export { initPlaceAutocomplete };
This file is imported in the view with a <%= javascript_pack_tag 'delivery_new' %>:
/app/javascript/packs/delivery_new.js
import { initPlaceAutocomplete } from '../plugins/places';
initPlaceAutocomplete();
...
The solution (that I haven't found yet):
I have tried several things, like importing the file in the webpack config (/config/webpack/environment.js), just like in the ProvidePlugin documentation:
const {environment} = require('#rails/webpacker')
const path = require('path');
const webpack = require('webpack')
environment.plugins.prepend('Provide',
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
Popper: ['popper.js', 'default'],
geofencing: path.resolve(path.join(__dirname, '..', '..', 'config', 'companies', process.env['COMPANY'], 'geofencing'))
})
)
module.exports = environment
... But it didn't work.
I also tried various places to import 'geofencing' in several places, with always the same result in the Chrome console: Uncaught ReferenceError: geofencing is not defined.
I noticed, though, that I had access to the process.env variables in the places.js script: writing console.log(process.env['COMPANY']); in the file prompts me the company name in the dev console when I reload the page in Chrome.
Apart from this, I have to say that I am lost. I am basically a newbie to the Webpack 'magic' ;)
Please tell me if you need more info about my setup.
Thanks in advance for your help!
You should be able to require modules using interpolation in the require() expression similar to your example. Try using a relative path, e.g. (assuming parent directories are siblings):
require(`../config/${process.env.COMPANY}/geofencing`)
The above expression will add only the COMPANY geofencing module to the bundle.
If you wish this to be dynamic, webpack can resolve the interpolation at runtime assuming the require paths are scoped to a directory; you're already doing this here with "../config/". As a result, webpack will include all COMPANY geofencing modules in the bundle. As for usage, I might move the require statement inside a function:
function initPlaceAutocomplete(company) {
const geofencing = require(`../config/${company}/geofencing`)
// ...
}
// usage
import { initPlaceAutocomplete } from "../plugins/places"
initPlaceAutocomplete(process.env.COMPANY)
At this point, this could is a good use case for dynamic imports, i.e., webpack supports the TC39 proposal for dynamically loading modules at runtime.
Instead of require("../config..."), you can use the import() function syntax. Unlike require(), the import() function syntax resolves asynchronously.
Now webpack will bundle all the COMPANY geofencing modules, but as separate "chunks" to keep the size of your initial script down. webpack will insert code to resolve these "chunks" asynchronously at runtime. To support this, the import() expression returns a Promise so, I'm using the async/await syntax here as a result.
async function initPlaceAutocomplete(company) {
const geofencing = await import(`../companies/${company}/config`)
// ...
}
I am quite new to Webpack, so bear with me if thats a stupid question.
My goal is to transform my old, AMD based codebase to a ES6 Module based solution. What I am struggling with is handling dynamic import()s. So my app router works on a module basis, i.e. each route is mapped to a module path and then required. Since I know what modules will be included, I just add those dynamically imported modules to my r.js configuration and am able to build everything in a single file, with all require calls still working.
Now, I am trying to do the same with ES6 modules and Webpack. With my devmode this is no problem as I can just replace require() with import(). However I cannot get this to work with bundling. Either Webpack splits my code (and still fails to load the dynamic module anyways), or - if I use the Array format for the entry config, the dynamic module is included in the bundle but loading still fails: Error: Cannot find module '/src/app/DynClass.js'
This is how my Webpack config looks like:
const webpack = require('webpack');
const path = require('path');
module.exports = {
mode: "development",
entry: ['./main.js', './app/DynClass.js'],
output: {
filename: 'main.js',
path: path.resolve(__dirname, "../client/")
},
resolve: {
alias: {
"/src": path.resolve(__dirname, '')
}
},
module: {
rules: [
{
test: /\.tpl$/i,
use: 'raw-loader',
},
]
}
};
So basically I want to tell Webpack: "hey, there is another module (or more) that is to be loaded dynamically and I want it to be included in the bundle"
How can I do this?
So yeah, after much fiddling there seems to be a light at the end of the tunnel. Still, this is not a 100% solution and it is surely not for the faint of heart, as it is quite ugly and fragile. But still I want to share my approach with you:
1) manual parsing of my routes config
My router uses a config file looking like this:
import StaticClass from "/src/app/StaticClass.js";
export default {
StaticClass: {
match: /^\//,
module: StaticClass
},
DynClass: {
match: /^\//,
module: "/src/app/DynClass.js"
}
};
So as you can see the export is an object, with keys acting as the route id, and an object that contains the matches (regex based) and the module which should be executed by the router if the route matches. I can feed my router with both a Constructor function (or an object) for modules which are available immediatly (i.e. contained in the main chunk) or if the module value is a string, this means that the router has to load this module dynamically by using the path specified in the string.
So as I know what modules could be potentially loaded (but not if and when) I can now parse this file within my build process and transform the route config to something webpack can understand:
const path = require("path");
const fs = require("fs");
let routesSource = fs.readFileSync(path.resolve(__dirname, "app/routes.js"), "utf8");
routesSource = routesSource.substr(routesSource.indexOf("export default"));
routesSource = routesSource.replace(/module:\s*((?!".*").)*$/gm, "module: undefined,");
routesSource = routesSource.replace(/\r?\n|\r/g, "").replace("export default", "var routes = ");
eval(routesSource);
let dummySource = Object.entries(routes).reduce((acc, [routeName, routeConfig]) => {
if (typeof routeConfig.module === "string") {
return acc + `import(/* webpackChunkName: "${routeName}" */"${routeConfig.module}");`;
}
return acc;
}, "") + "export default ''";
(Yeah I know this is quite ugly and also a bit brittle so this surely could be done better)
Essentially I create a new, virtual module where every route entry which demands a dynamic import is translated, so:
DynClass: {
match: /^\//,
module: "/src/app/DynClass.js"
}
becomes:
import(/* webpackChunkName: "DynClass" */"/src/app/DynClass.js");
So the route id simply becomes the name of the chunk!
2) including the virtual module in the build
For this I use the virtual-module-webpack-plugin:
plugins: [
new VirtualModulePlugin({
moduleName: "./app/dummy.js",
contents: dummySource
})
],
Where dummySource is just a string containing the sourcecode of my virtual module I just have generated. Now, this module is pulled in and the "virtual imports" can be processed by webpack. But wait, I still need to import the dummy module, but I do not have any in my development mode (where I use everything natively, so no loaders).
So in my main code I do the following:
let isDev = false;
/** #remove */
isDev = true;
/** #endremove */
if (isDev) { import('./app/dummy.js'); }
Where "dummy.js" is just an empty stub module while I am in development mode. The parts between that special comments are removed while building (using the webpack-loader-clean-pragma loader), so while webpack "sees" the import for dummy.js, this code will not be executed in the build itself since then isDev evaluates to false. And since we already defined a virtual module with the same path, the virtual module is included while building just like I want, and of course all dependencies are resolved as well.
3) Handling the actual loading
For development, this is quite easy:
import routes from './app/routes.js';
Object.entries(routes).forEach(async ([routeId, route]) => {
if (typeof route.module === "function") {
new route.module;
} else {
const result = await import(route.module);
new result.default;
}
});
(Note that this is not the actual router code, just enough to help me with my PoC)
Well, but for the build I need something else, so I added some code specific to the build environment:
/** #remove */
const result = await import(route.module);
new result.default;
/** #endremove */
if (!isDev) {
if (typeof route.module === "string") { await __webpack_require__.e(routeId); }
const result = __webpack_require__(route.module.replace("/src", "."));
new result.default;
}
Now, the loading code for the dev environment is just stripped out, and there is another loading code that uses webpack internally. I also check if the module value is a function or string, and if it is the latter I invoke the internal require.ensure function to load the correct chunk: await __webpack_require__.e(routeId);. Remember that I named my chunks when generating the virtual module? Now thats why I still can find them now!
4) more needs to be done
Another thing I encountered is when several dynamically loaded modules have the same dependencies, webpack tries to generate more chunks with names like module1~module2.bundle.js, breaking my build. To counter this, I needed to make sure that all those shared modules go into a specific named bundle I called "shared":
optimization: {
splitChunks: {
chunks: "all",
name: "shared"
}
}
And when in production mode, I simply load this chunk manually before any dynamic modules depending on it are requested:
if (!isDev) {
await __webpack_require__.e("shared");
}
Again, this code only runs in production mode!
Finally, I have to prevent webpack renaming my modules (and chunks) to something like "1", "2" etc, but rather keep the names I just have defined:
optimization: {
namedChunks: true,
namedModules: true
}
Se yeah, there you have it! As I said this wasn't pretty but seems to work, at least with my simplified test setup. I really hope there aren't any blockers ahead of me when I do all the rest (like ESLint, SCSS etc)!
I'm fairly new to RequireJS and I've run into a bit of a problem. I've written a little framework built on Backbone using RequireJS and I want it to be re-usable in different projects. So, with some searching I learned that require allows packages. This seemed like what I was looking for. I have a main.js file to launch my app that essentially looks like this:
require.config({
packages: ['framework']
});
require(['framework'], function(framework) {
framework.createDash();
});
Then in the same directory as my main.js I have another directory called "framework" which contains another main.js which looks like this:
define(function(require, exports, module) {
exports.createDash = function(dash, element) {
require(['dash/dash.model', 'dash/dash.view'], function(DashModel, DashView) {
return new DashView({
model: new DashModel(dash),
el: element ? element : window
});
});
};
});
In searching I found this page which indicates that the 'require' argument should be scoped to the submodule. However, when I try to require things they are still relative to my original main.js. I've tried a number of things and searched for hours to no avail. Is there any way I can have my require/define calls within my package included relative to the main.js in it's root?
You need to define your submodule as package in the require configuration:
require.config({
packages: [
{ name: 'packagename',
location: 'path/to/your/package/root', // default 'packagename'
main: 'scriptfileToLoad' // default 'main'
}]
... some other stuff ...
});
To load your module you just need to use your 'packagename' at the requirements:
define(['jquery', 'packagename'], function($, MyPackage) {
MyPackage.useIt()
});
In your package you must use the ./ prefix to load your files relative to your submodule:
define(['globalDependency', './myLocalFile'], function(Asdf, LocalFile) {
LocalFile.finallyLoaded();
});
There is a useful shortcut: If your package name equals to your location and your main file is called 'main.js', then you can replace this
packages: [
{ name: 'packagename',
location: 'packagename',
main: 'main'
}]
to this:
packages: ['packagename']
As far as I can see, you already tried to define a package but did you also use the ./ prefix? Without this prefix require will try to find the files in it's global root-path. And without a package, ./ will be useless because the relative path is the same as the global root-path.
Cheers
I figured out the answer to my question, and the solution (they were not the same apparently). I guess I'll post it here in case it can help someone else in the future.
Essentially what I was wanting was to load my framework within its own context. I found the context option under the configuration section on require's website and an example of how to use it. Originally I tried this by doing something like:
var req = require.config({
baseUrl: 'framework',
context: 'framework',
paths: {
jQuery: 'lib/jquery/jquery-1.7.min.js',
Underscore: 'lib/underscore/underscore.min.js',
Backbone: 'lib/backbone/backbone.min.js',
etc...
}
});
req(['main'], function() {});
There were two problems with this. First, my 'req' variable was being defined outside of the framework, but I wanted the framework to define it's own paths. And second, whenever a file outside of the framework would require a file within the framework, which would in turn require 'jQuery', for example, then jQuery (or whatever else) wouldn't be required from within the context of the framework instance of require and so it couldn't find the file.
What I ended up doing was defining my framework's main.js to look something like this:
var paths = {
jQuery: 'lib/jquery/jquery-1.7.min.js',
Underscore: 'lib/underscore/underscore.min.js',
Backbone: 'lib/backbone/backbone.min.js',
etc...
};
define(function() {
var exports = {};
exports.initialize = function(baseUrl, overridePaths, callback) {
if(!overridePaths) {
overridePaths = {};
}
if(baseUrl && baseUrl[baseUrl.length - 1] != '/') {
baseUrl = baseUrl + '/';
}
var fullpaths = {};
for(var path in paths) {
// Don't add baseUrl to anything that looks like a full URL like 'http://...' or anything that begins with a forward slash
if(paths[path].match(/^(?:.*:\/\/|\/)/)) {
fullpaths[path] = paths[path];
}
else {
fullpaths[path] = baseUrl + paths[path];
}
}
var config = {paths: fullpaths};
for(var pathName in overridePaths) {
config.paths[pathName] = overridePaths[pathName];
}
require.config(config);
// Do anything else you need to do such as defining more functions for exports
if(callback) {
callback();
}
}
return exports;
});
And then in my project's main.js file I just do this:
require(['framework/main'], function(framework) {
// NOTE: This setTimeout() call is used because, for whatever reason, if you make
// a 'require' call in here or in the framework without it, it will just hang
// and never actually go fetch the files in the browser. There's probably a
// better way to handle this, but I don't know what it is.
setTimeout(function() {
framework.initialize('framework', null, function() {
// Do stuff here
}
}, 0);
});
This takes whatever is passed in to the framework's initialize() method for 'baseURL' and prepends that to any paths that the framework defines that do not start with a forward slash or 'anything://', unless they are override paths. This allows the package using the framework to override things like 'jQuery'.
This worked for me, adding a "./" prefix to the module names:
define(function (require, exports, module) {
exports.createDash = function (dash, element) {
require([ './dash/dash.model', './dash/dash.view' ], function (DashModel, DashView) {
return new DashView({
model : new DashModel(dash),
el : element ? element : window
});
});
};
});
A process that worked well for me for allowing a package with submodules to be used directly from data-main or from an outside framework, assuming that a main.js (or other package main) is called by a particular name, was to use var baseUrl = require.toUrl('packageName') + '/../' as a prefix to a require.config({ paths: { ... } }) configuration file. For instance:
var music21Base = require.toUrl('music21') + '/../';
require.config({ paths: {
'jquery': music21Base + 'ext/jquery/jquery.2.1.10.min';
'subModuleLoader': music21Base + 'src/subModuleLoader';
} });
The setting of context: "xxx" worked fine for calling normal modules with ./modName but did not work for the paths argument for me.