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.
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.
Is it possible to add a local html file in the nativescript webview ?
If yes How can I do it using javascript ?
When I add online page it works , I can add www.google.com in the webview it works .But I want to add a local page but I don't find a way to do this .
Yes, it's possible. You need to consider that all NativeScript apps are build by default with Webpack and the default webpack.config.js will take care of certain files (like the ones in a fonts folder or like all images with *.png and *..jpg extensions). The webpack build will bundle all JavaScript files and in the case of the Angular flavor will also cognitively include the Angular related HTML files. However, the default webpack.config.js won't "know" about your custom HTML file.
The solution is to let Webpack know that it should copy the specific HTML file. This should be done via the CopyWebpackPlugin section in webpack.config.js file.
Example (assuming we have a file called test.html in the app directory)
new CopyWebpackPlugin([
{ from: { glob: "test.html" } }, // HERE
{ from: { glob: "fonts/**" } },
{ from: { glob: "**/*.jpg" } },
{ from: { glob: "**/*.png" } },
], { ignore: [`${relative(appPath, appResourcesFullPath)}/**`] }),
For real-life example see this config where this HTML file is the one we are targeting.
I'm recently working on some website optimization works, and I start using code splitting in webpack by using import statement like this:
import(/* webpackChunkName: 'pageB-chunk' */ './pageB')
Which correctly create the pageB-chunk.js, now let's say I want to prefetch this chunk in pageA, I can do it by add this statement in pageA:
import(/* webpackChunkName: 'pageB-chunk' */ /* webpackPrefetch: true */ './pageB')
Which will result in a
<link rel="prefetch" href="pageB-chunk.js">
being append to HTML's head, then the browser will prefetch it, so far so good.
The problem is the import statement I use here not just prefetch the js file, but also evaluate the js file, means the code of that js file is parsed & compile to bytecodes, the top-level code of that JS is executed.
This is a very time-consuming operation on a mobile device and I want to optimize it, I only want the prefetch part, I don't want the evaluate & execute part, because later when some user interactions happen, I will trigger the parsing & evaluate myself
↑↑↑↑↑↑↑↑ I only want to trigger the first two steps, pictures come from https://calendar.perfplanet.com/2011/lazy-evaluation-of-commonjs-modules/ ↑↑↑↑↑↑↑↑↑
Sure I can do this by adding the prefetch link myself, but this means I need to know which URL I should put in the prefetch link, webpack definitely knows this URL, how can I get it from webpack?
Does webpack have any easy way to achieve this?
UPDATE
You can use preload-webpack-plugin with html-webpack-plugin it will let you define what to preload in configuration and it will automatically insert tags to preload your chunk
note if you are using webpack v4 as of now you will have to install this plugin using preload-webpack-plugin#next
example
plugins: [
new HtmlWebpackPlugin(),
new PreloadWebpackPlugin({
rel: 'preload',
include: 'asyncChunks'
})
]
For a project generating two async scripts with dynamically generated
names, such as chunk.31132ae6680e598f8879.js and
chunk.d15e7fdfc91b34bb78c4.js, the following preloads will be injected
into the document head
<link rel="preload" as="script" href="chunk.31132ae6680e598f8879.js">
<link rel="preload" as="script" href="chunk.d15e7fdfc91b34bb78c4.js">
UPDATE 2
if you don't want to preload all async chunk but only specific once you can do that too
either you can use migcoder's babel plugin or with preload-webpack-plugin like following
first you will have to name that async chunk with help of webpack
magic comment example
import(/* webpackChunkName: 'myAsyncPreloadChunk' */ './path/to/file')
and then in plugin configuration use that name like
plugins: [
new HtmlWebpackPlugin(),
new PreloadWebpackPlugin({
rel: 'preload',
include: ['myAsyncPreloadChunk']
})
]
First of all let's see the behavior of browser when we specify script tag or link tag to load the script
whenever a browser encounter a script tag it will load it parse it
and execute it immediately
you can only delay the parsing and evaluating with help of async and
defer tag only until DOMContentLoaded event
you can delay the execution (evaluation) if you don't insert the script tag ( only preload it with link)
now there are some other not recommended hackey way is you ship your entire script and string or comment ( because evaluation time of comment or string is almost negligible) and when you need to execute that you can use Function() constructor or eval both are not recommended
Another Approach Service Workers: ( this will preserve you cache event after page reload or user goes offline after cache is loaded )
In modern browser you can use service worker to fetch and cache a recourse ( JavaScript, image, css anything ) and when main thread request for that recourse you can intercept that request and return the recourse from cache this way you are not parsing and evaluating the script when you are loading it into the cache
read more about service workers here
example
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'/sw-test/',
'/sw-test/index.html',
'/sw-test/style.css',
'/sw-test/app.js',
'/sw-test/image-list.js',
'/sw-test/star-wars-logo.jpg',
'/sw-test/gallery/bountyHunters.jpg',
'/sw-test/gallery/myLittleVader.jpg',
'/sw-test/gallery/snowTroopers.jpg'
]);
})
);
});
self.addEventListener('fetch', function(event) {
event.respondWith(caches.match(event.request).then(function(response) {
// caches.match() always resolves
// but in case of success response will have value
if (response !== undefined) {
return response;
} else {
return fetch(event.request).then(function (response) {
// response may be used only once
// we need to save clone to put one copy in cache
// and serve second one
let responseClone = response.clone();
caches.open('v1').then(function (cache) {
cache.put(event.request, responseClone);
});
return response;
}).catch(function () {
// any fallback code here
});
}
}));
});
as you can see this is not a webpack dependent thing this is out of scope of webpack however with help of webpack you can split your bundle which will help utilizing service worker better
Updates:
I include all the things into a npm package, check it out!
https://www.npmjs.com/package/webpack-prefetcher
After few days of research, I end up with writing a customize babel plugin...
In short, the plugin work like this:
Gather all the import(args) statements in the code
If the import(args) contains /* prefetch: true */ comment
Find the chunkId from the import() statement
Replace it with Prefetcher.fetch(chunkId)
Prefetcher is a helper class that contain the manifest of webpack output, and can help us on inserting the prefetch link:
export class Prefetcher {
static manifest = {
"pageA.js": "/pageA.hash.js",
"app.js": "/app.hash.js",
"index.html": "/index.html"
}
static function fetch(chunkId) {
const link = document.createElement('link')
link.rel = "prefetch"
link.as = "script"
link.href = Prefetcher.manifest[chunkId + '.js']
document.head.appendChild(link)
}
}
An usage example:
const pageAImporter = {
prefetch: () => import(/* prefetch: true */ './pageA.js')
load: () => import(/* webpackChunkName: 'pageA' */ './pageA.js')
}
a.onmousehover = () => pageAImporter.prefetch()
a.onclick = () => pageAImporter.load().then(...)
The detail of this plugin can found in here:
Prefetch - Take control from webpack
Again, this is a really hacky way and I don't like it, if u want webpack team to implement this, pls vote here:
Feature: prefetch dynamic import on demand
Assuming I understood what you're trying to achieve, you want to parse and execute a module after a given event (e.g click on a button). You could simply put the import statement inside that event:
element.addEventListener('click', async () => {
const module = await import("...");
});
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>
Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
This question does not appear to be about programming within the scope defined in the help center.
Closed 4 years ago.
Improve this question
I would like to share how to bundle an application that acts as a plugin host and how it can load installed plugins dynamically.
Both the application and the plugins are bundled with Webpack
The application and plugins are compiled and distributed independently.
There are several people on the net who are looking for a solution to this problem:
Multi-project build and dynamically loading modules with webpack
Loading prebuilt webpack bundles at runtime
How to expose objects from Webpack bundle and inject external libs into compiled bundle?
Dynamic Requires
https://github.com/webpack/webpack/issues/118
The solution described here is based on #sokra's Apr 17, 2014 comment on Webpack issue #118 and is slightly adapted in order to work with Webpack 2.
https://github.com/webpack/webpack/issues/118
Main points:
A plugin needs an ID (or "URI") by which it registers at the backend server, and which is unique to the application.
In order to avoid chunk/module ID collisions for every plugin, individual JSONP loader functions will be used for loading the plugin's chunks.
Loading a plugin is initiated by dynamically created <script> elements (instead of require()) and let the main application eventually consume the plugin's exports through a JSONP callback.
Note: You may find Webpack's "JSONP" wording misleading as actually no JSON is transferred but the plugin's Javascript wrapped in a "loader function". No padding takes place at server-side.
Building a plugin
A plugin's build configuration uses Webpack's output.library and output.libraryTarget options.
Example plugin configuration:
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
publicPath: '/' + pluginUri + '/',
filename: 'js/[name].js',
library: pluginIdent,
libraryTarget: 'jsonp'
},
...
}
It's up to the plugin developer to choose an unique ID (or "URI") for the plugin and make it available in the plugin configuration. Here I use the variable pluginURI:
// unique plugin ID (using dots for namespacing)
var pluginUri = 'com.companyX.pluginY'
For the library option you also have to specify an unique name for the plugin. Webpack will use this name when generating the JSONP loader functions. I derive the function name from the plugin URI:
// transform plugin URI into a valid function name
var pluginIdent = "_" + pluginUri.replace(/\./g, '_')
Note that when the library option is set Webpack derives a value for the output.jsonpFunction option automatically.
When building the plugin Webpack generates 3 distribution files:
dist/js/manifest.js
dist/js/vendor.js
dist/js/main.js
Note that vendor.js and main.js are wrapped in JSONP loader functions whose names are taken from output.jsonpFunction and output.library respectively.
Your backend server must serve the distribution files of each installed plugin. For example, my backend server serves the content of a plugin's dist/ directory under the plugin's URI as the 1st path component:
/com.companyX.pluginY/js/manifest.js
/com.companyX.pluginY/js/vendor.js
/com.companyX.pluginY/js/main.js
That's why publicPath is set to '/' + pluginUri + '/' in the example plugin config.
Note: The distribution files can be served as static resources. The backend server is not required to do any padding (the "P" in JSONP). The distribution files are "padded" by Webpack already at build time.
Loading plugins
The main application is supposed to retrieve the list of the installed plugin (URI)s from the backend server.
// retrieved from server
var pluginUris = [
'com.companyX.pluginX',
'com.companyX.pluginY',
'org.organizationX.pluginX',
]
Then load the plugins:
loadPlugins () {
pluginUris.forEach(pluginUri => loadPlugin(pluginUri, function (exports) {
// the exports of the plugin's main file are available in `exports`
}))
}
Now the application has access to the plugin's exports. At this point, the original problem of loading an independently compiled plugin is basically solved :-)
A plugin is loaded by loading its 3 chunks (manifest.js, vendor.js, main.js) in sequence. Once main.js is loaded the callback will be invoked.
function loadPlugin (pluginUri, mainCallback) {
installMainCallback(pluginUri, mainCallback)
loadPluginChunk(pluginUri, 'manifest', () =>
loadPluginChunk(pluginUri, 'vendor', () =>
loadPluginChunk(pluginUri, 'main')
)
)
}
Callback invocation works by defining a global function whose name equals output.library as in the plugin config. The application derives that name from the pluginUri (just like we did in the plugin config already).
function installMainCallback (pluginUri, mainCallback) {
var _pluginIdent = pluginIdent(pluginUri)
window[_pluginIdent] = function (exports) {
delete window[_pluginIdent]
mainCallback(exports)
}
}
A chunk is loaded by dynamically creating a <script> element:
function loadPluginChunk (pluginUri, name, callback) {
return loadScript(pluginChunk(pluginUri, name), callback)
}
function loadScript (url, callback) {
var script = document.createElement('script')
script.src = url
script.onload = function () {
document.head.removeChild(script)
callback && callback()
}
document.head.appendChild(script)
}
Helper:
function pluginIdent (pluginUri) {
return '_' + pluginUri.replace(/\./g, '_')
}
function pluginChunk (pluginUri, name) {
return '/' + pluginUri + '/js/' + name + '.js'
}