ES6's modules are based on a flexible loader architecture (although the standard is not final, so ...).
Does this mean ES6's loader, based on system.js, can load all assets? I.e. CSS, HTML, Images, Text, .. files of any sort?
I ask because I'm starting to use WebComponents & Polymer which have their own HTML import, and implementing them with ES6, which has its own import/loader (system.js).
If you use SystemJS then you can load assets by using plugins:
// Will generate a <link> element for my/file.css
System.import('my/file.css!')
.then(() => console.log('CSS file loaded'));
Alternatively, you can use an import statement. This will make sure that the CSS file is loaded before the your script executes:
import 'my/file.css!';
Finally, you can retrieve the contents of the file using the text plugin:
import cssContent from 'my/file.css!text';
console.log('CSS file contents: ', cssContent);
Another option is to add the css as a dependency in JSPM config files. Basically adding the dependency in the specific package .json file and then running 'jspm install' which will add the override to package.js & jspm.config.js
I know you mentioned ES6 modules, but as that does not appear to support CSS natively, if you're looking for something standards-based to load resources dynamically and wish for something possibly somewhat less unpleasant than XMLHttpRequest, the new Fetch API might be used like this:
var myStylesheets = ['myStyles1.css', 'myStyles2.css'];
Promise.all(myStylesheets.map(url => fetch(url))).
then(arr => Promise.all(arr.map(url => url.text()))).
then(arr => {
var style = document.createElement('style');
style.textContent = arr.reduce(
(prev, fileContents) => prev + fileContents, ''
);
document.head.appendChild(style);
}).then(() => {
// Do whatever now
});
This is even cleaner with async functions:
var myStylesheets = ['myStyles1.css', 'myStyles2.css'];
async function loadStyles(stylesheets) {
let arr = await Promise.all(stylesheets.map(url => fetch(url)))
arr = await Promise.all(arr.map(url => url.text()))
const style = document.createElement('style')
style.textContent = arr.reduce(
(prev, fileContents) => prev + fileContents, ''
)
document.head.appendChild(style);
// Do whatever now
}
loadStyles(myStylesheets)
For other resource types, you can use the blob() method for images, and pending ES6 modules support, eval() for JavaScript, etc.
I faced that question 8 years later :D
In my option, I think is interesting we can use the import.meta.url to be able to load assets relatively in a web scenario. That makes it more similar that esmodules import way.
In addition to the Zamir answer, if the assets/styles are public and we do not have to handle their content, there is no need to use fetch or any ajax at all. Just use HTML tags: link, img, scripts ...
eg: Loading a sibling css file relatively
mywebsite.com
assets
a-component.css
a-component.js
function loadStyle(styleUrl) {
const linkEl = document.createElement("link");
linkEl.setAttribute("rel", "stylesheet");
linkEl.setAttribute("href", styleUrl);
document.head.append(linkEl);
}
function load(currentPath, relativePath) {
// [https:,,,mywebsite.com,assets,a-component.js]
const parts = currentPath.split('/');
// [https:,,,mywebsite.com,assets]
parts.pop();
// [https:,,,mywebsite.com,assets,./a-component.css]
parts.push(relativePath);
// https://mywebsite.com/assets/a-component.css
const absoluteUrl = parts.join('/').replace('/./', '/');
// css
const extension = relativePath.split('.').pop();
switch(extension) {
case 'css':
loadStyle(absoluteUrl);
break;
// others types
}
}
// usage in a-component.js
// https://mywebsite.com/assets/a-component.js
load(import.meta.url, "./a-component.css");
That did help me out in a pure JS Legacy personal project without SystemJS.
Related
Let's say I have an MVC/WebAPI/AngularJS site that I'm running locally, e.g. ;
localhost/Test/
which I then want to move to
www.test.com
While local, I have a lot of references to various directories (jsfiles, etc) of the following format (in either JS or HTML files)
app.directive('rpdbSpinner', function() {
return {
restrict: 'E',
**templateUrl: '/Test/templates/directives/spinner.html',**
scope: {
isLoading:'='
}
}
})
when updating/web publishing, I'd have to change everything to:
app.directive('rpdbSpinner', function() {
return {
restrict: 'E',
**templateUrl: '/templates/directives/spinner.html',**
scope: {
isLoading:'='
}
}
})
I can do this manually (which is what I've been doing),but the larger the project grows, the harder it becomes. I could, of course, only change it once and then excluded the files during publishing phase (web.config/rest), but it still feels like I am going about it the wrong way. Using "~/" wouldn't work on plain HTML/JS files as far as I'm aware, and this I can't really use it...
Any suggestions to map to paths globally regardless of whether in a Virtual Directory or the root of a project?
Thanks :)
If you simply care about getting the root/base url of the site so you can append that to get the other url you are after, you may simply use / as the first character of your url.
var getUsersUrl = "/api/users";
Here is an alternate approach if you want more than just the app root (Ex : Specific urls( built using mvc helper methods such as Url.RouteUrl etc)
You should not hard code your app base path like that. You may use the Url.Content or Url.RouteUrl helper methods in your razor view to generate the url to the app base. It will take care of correctly building the url regardless of your current page/path.Once you get this value, assign it to a javascript variable and use that in your other js code to build your other urls. Always make sure to use javascript namespacing when doing so to avoid possible issues with global javascript variables.
So in your razor view (Layout file or specific view), you may do this.
<script>
var myApp = myApp || {};
myApp.Urls = myApp.Urls || {};
myApp.Urls.baseUrl = '#Url.Content("~")';
myApp.Urls.userListUrl = '#Url.Action("Index","User")';
</script>
<script src="~/Scripts/NonAngularJavaScript.js"></script>
<script src="~/Scripts/AngularControllerForPage.js"></script>
<script>
var a = angular.module("app").value("appSettings", myApp);
</script>
In your angular controller, you can access it like,
var app = angular.module("app", []);
var ctrl = function (appSettings) {
var vm = this;
console.log(appSettings.Urls.userListUrl);
vm.baseUrl = appSettings.Urls.baseUrl;
//build other urls using the base url now
var getUsersUrl = vm.baseUrl + "api/users";
console.log(getUsersUrl);
};
app.controller("ctrl", ctrl)
You can also access this in your data services, directives etc.
In your non angular java script files.
// With the base url, you may safely add the remaining url route.
var urlToJobIndex2= myApp.Urls.baseUrl+"jobs/GetIndex";
Using "~/" wouldn't work on plain HTML/JS files as far as I'm aware,
and this I can't really use it...
Yes, but you could inject it in your main server-side served webpage as a variable:
<script>
var baseUrl = ... get the base url from the server using ~/
</script>
and then in your external scripts simply concatenate the relative urls with it. As far as static html files are concerned, then it could be a little more problematic. You could serve them through some special server side handler that will take care of injecting this logic.
You can use module.constant to create an injectable which you can use.
app.constant("URL_BASE", "/Test");
app.directive('rpdbSpinner', function(URL_BASE) {
return {
restrict: 'E',
**templateUrl: URL_BASE + '/templates/directives/spinner.html',**
scope: {
isLoading:'='
}
}
})
You can also use module.value if you register it before you register your directive.
For more information see AngularJS Module Guide -- configuration.
I would like to deploy my web application to several environments. Using Continuous Integration I can run a task to generate a config.json for a particular environment. This file will contain, among others, the particular URLs to use for it.
{
"baseUrl": "http://www.myapp.es/",
"baseApiUrl": "http://api.myapp.es/",
"baseAuthUrl": "http://api.myapp.es/auth/"
}
The issue comes up when I try to set my different services through providers in the config phase. Of course, services are not available yet in the phase so I cannot use $http to load that json file and set my providers correctly.
Basically I would like to do something like:
function config($authProvider) {
$authProvider.baseUrl = config.baseAuthUrl;
}
Is there a way to load those values on runtime from a file? The only thing I can think about is having that mentioned task altering this file straight away. However I have several modules and therefore, that would have to do in all of them which doesn´t seem right.
You can create constants in the config of your main module:
Add $provide as a dependency in your config method
use the provider method to add all constants like this
$provide.provider('BASE_API_URL', {
$get: function () {
return 'https://myexample.net/api/';
}
});
You can use BASE_API_URL as a dependency in your services.
I hope this helps
Optionally you can set the url depending of your environment:
$provide.provider('BASE_API_URL', {
$get: function () {
if(window.location.hostname.toLowerCase() == 'myapp.myexample.net')
{
return 'https://myexample.net/api/' //pre-production
}else
{
return 'http://localhost:61132/'; //local
}
}
});
Regards!
Finally, the solution was generating an angular constants file using templating (gulp-template) through a gulp task. At the end, I am using a yaml file instead a json one (which is the one generated my CI engine with the proper values for the environment I want to deploy to).
Basically:
config.yml
baseUrl: 'http://www.myapp.es/'
baseApiUrl: 'http://api.myapp.es/'
auth:
url: 'auth/'
config.module.constants.template
(function () {
'use strict';
angular
.module('app.config')
.constant('env_variables', {
baseUrl: '<%=baseUrl%>',
baseApiUrl: '<%=baseApiUrl%>',
authUrl: '<%=auth.url%>'
});
}());
gulpfile.js
gulp.task('splicing', function(done) {
var yml = path.join(conf.paths.src, '../config/config.yml');
var json = yaml.safeLoad(fs.readFileSync(yml, 'utf8'));
var template = path.join(conf.paths.src, '../config/config.module.constants.template');
var targetFile = path.join(conf.paths.src, '/app/config');
return gulp.src(template)
.pipe($.template(json))
.pipe($.rename("config.module.constants.js"))
.pipe(gulp.dest(targetFile), done);
});
Then you just inject it in the config phase you need:
function config($authProvider, env_variables) {
$authProvider.baseUrl = env_variables.baseApiUrl + env_variables.authUrl;
}
One more benefit about using gulp for this need is that you can integrate the generation of these constants with your build, serve or watch tasks and literally, forget about doing any change from now on. Hope it helps!
I am trying to use ES6 modules in my current GULP setup. I've read that this is yet to be supported by browsers or Babel, so there is a need some elaborate setup to make this work, using things such Browserify, babelify, vinyl-source-stream. (Seems extremely complex setup).
What I want is different from examples I had found online. All the examples are with external files being imported, and I really don't want that. I want all the files to be bundled into a single file, with all the modules there already. Here's what I have:
My current GULP setup is like this:
gulp.task('buildJS', function() {
var src = [
'./js/dist/app.js',
'./js/dist/templates.js',
'./js/dist/connect.js',
'./js/dist/config.js',
'./js/dist/utilities.js',
'./js/dist/components/*.js',
'./js/dist/pages/**/*.js',
'./js/dist/modals/*.js',
'./js/dist/init.js' // must be last
];
gulp.src(src)
.pipe(concat('app.js'))
.pipe(babel({modules:"common"})) // I have no idea what "modules" actually does
.pipe(gulp.dest('../js/'))
});
And this is an example of a component file in /js/dist/components/.
There are many files like this, and they are all combined to a single file.
module "components/foo" {
export function render(settings) {
return ...
}
}
So later in some page controller I would use it:
import { render } from "components/foo";
Question:
Now that I have a single file, (been transformed using Babel), how can I use the modules via Import?
No, don't naively concatenate the files. Use browserify to bundle them, with babelify to compile them (via babel). A basic example would look something like this:
browserify('./entry')
.transform(babelify)
.bundle()
// ...
It's hard to give more specific advice because your use case is so unclear. Do you have a dependency graph that begins at one file, or are you trying to bundle together a bunch of indepdendent modules? Are you trying to run a script to kick off an application, or do you just want to be able to access modules individually?
Based on the example you linked to in your comment you should have something like this:
components/doughnut.js
export default function Doughnut (settings = {}) {
// ...
};
Doughnut.prototype = {}
routes/home.js
import Doughnut from './components/doughnut';
export default function () {
var component = new Doughnut();
$('body').html(component.render());
};
Have each module export what you want to be available from any other module. Have each module import whatever it needs from any other module(s). Whatever uses the controller from this example should then do import home from './routes/home'; These modules aren't tied to a global variable App and can be reused in other applications (as long as you otherwise make them reusable).
.pipe(babel({modules:"common"})) // I have no idea what "modules"
modules is a babel option that determines what module format it compiles ES6 module syntax to. In this case, CommonJS.
module "components/foo" {
Thanks to your comments I now understand why you have this. You need to eliminate that. Your component file should look something like:
export function render (settings) {
return ...
}
Paired with:
import { render } from "components/foo";
Or if you want a default export / import:
export default function render (settings) {
return ...
}
import render from "components/foo";
import { render } from "components/foo";
If you're browserifying your modules, you're probably going to need to use relative paths like ./components/foo or use something else to deal with the paths, like babel's resolveModuleSource option.
Since the end of 2015 I have been using rollupjs in order to create a bundle of ES2015 (ES6) modules, so I could use import/export freely in my code.
I've found Rollupjs to be very good and easy to use. The people behind
it are great people which devote themselves to the project. I've had
many questions which I had posted on the project's Github issues page
and I always got answered pretty quickly.
Setup includes these rollupjs plugins:
rollup (basic rollupjs bundler)
rollup-plugin-babel (converts ES2015 code to ES5 or earlier, for legacy browsers support)
rollup-plugin-eslint (verify the javascript code is valid)
rollup-plugin-uglify (minify the code, to make it smaller)
rollup-plugin-progress (shows bundle progress in terminal. shows which file being "worked on")
beepbeep (Make a console beep sound. I use this to inform me of compilaction errors)
Simplified GULP setup I'm using:
var gulp = require('gulp'),
gutil = require('gulp-util'),
rollup = require('rollup').rollup,
babelRollup = require('rollup-plugin-babel'),
eslintRollup = require('rollup-plugin-eslint'),
uglifyRollup = require('rollup-plugin-uglify'),
rollupProgress = require('rollup-plugin-progress'),
beep = require('beepbeep');
// ESlint
var eslint_settings = {
rulePaths: [],
rules: {
"no-mixed-spaces-and-tabs" : [2, "smart-tabs"],
"block-spacing" : [2, "always"],
"comma-style" : [2, "last"],
"no-debugger" : [1],
"no-alert" : [2],
"indent-legacy" : [1, 4, {"SwitchCase":1}],
'strict' : 0,
'no-undef' : 1
},
ecmaFeatures : {
modules: true,
sourceType: "module"
},
"parserOptions": {
"ecmaVersion" : 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": false,
"experimentalObjectRestSpread": true
}
},
globals : ['$', '_', 'afterEach', 'assert', 'beforeEach', 'Cookies', 'd3', 'dataLayer', 'describe', 'done', 'expect', 'ga', 'it', 'jQuery', 'sinon'], baseConfig: {
//parser: 'babel-eslint',
},
envs: [
'browser', 'es6'
]
};
// Rollup plugins configuration
function getRollupPlugins( settings = {} ){
var rollupPlugins = [];
rollupPlugins.push({
presets : [['es2015', {"modules": false}]], //['es2015-rollup'],
runtimeHelpers : true,
exclude : 'node_modules/**',
plugins : ["external-helpers"]
});
rollupPlugins.push(eslintRollup( Object.assign({throwOnError:true}, eslint_settings) ))
rollupPlugins.push(rollupProgress({
clearLine:true // default: true
}))
// I would advise Babel to only be used for production output since it greatly slower bundle creation
if( settings.ENV == 'production' ){
rollupPlugins.push(uglifyRollup())
rollupPlugins.push(babelRollup(rollupPlugins__babel));
}
return rollupPlugins;
}
var rollupPlugins = getRollupPlugins();
/**
* a generic Rollup bundle creator
* #param {String} outputPath [where to save the bundle to (must end with /)]
* #param {String} outputFileName [bundle file name]
* #param {String} entryFile [rollup entry file to start scanning from]
* #return {Object} [Promise]
*/
function rollupBundle(outputPath, outputFileName, entryFile, bundleOptions){
bundleOptions = bundleOptions || {};
bundleOptions.plugins = bundleOptions.plugins || rollupPlugins;
return new Promise(function(resolve, reject) {
outputFileName += '.js';
var cache;
// fs.truncate(outputPath + outputFileName, 0, function() {
// gutil.log( gutil.colors.dim.gray('Emptied: '+ outputPath + outputFileName) );
// });
rollup({
entry : entryFile,
plugins : bundleOptions.plugins,
cache : cache
})
.then(function (bundle) {
var bundleSettings = {
format : bundleOptions.format || 'umd',
sourceMap : false,
banner : config.banner
},
result = bundle.generate(bundleSettings),
mapFileName = outputFileName + '.map',
sourceMappingURL = '\n//# sourceMappingURL='+ mapFileName;
cache = bundle;
// if folder does not exists, create it
if( !fs.existsSync(outputPath) ){
gutil.log( gutil.colors.black.bgWhite('Creating directory ' + outputPath) );
fs.mkdirSync(outputPath);
}
// save bundle file to disk
fs.writeFile( outputPath + outputFileName, result.code + (bundleSettings.sourceMap ? sourceMappingURL : ''), function(){
resolve();
});
// save map file to disk
if( bundleSettings.sourceMap )
fs.writeFile( outputPath + mapFileName, result.map.toString());
})
.catch(function(err){
beep(1);
gutil.log( gutil.colors.white.bgRed('Rollup [catch]: ', err.stack) );
resolve();
})
});
}
// This task bundles the main application, using an entry file which itself has many imports,
// and those imports also has imports.. like a tree branching
gulp.task('bundle-app', ()=>{
return rollupBundle('../dist/js/', 'app', 'js/dist/app.js', {format:'cjs'});
});
In my website build-process, I use gulp-changed to prevent doing all task if unnecessary. However, sometimes it has different behaviours even if declarations are similar.
Does anyone know what I am doing wrong?
case 1 : building htlm / php using partials.
works fine! the task process only the first time, if I run it twice in a row
gulp.task('pages', function () {
var toBuild = src + config.pages.src_pages; // many php/html files
var partials = src + config.pages.src_partials; // "to be included"
var DEST = target;
return gulp.src([toBuild, "!" + partials])
.pipe(changed(DEST))
.pipe(fileinclude({ prefix: '##', basepath: '#file' }))
.pipe(size())
.pipe(gulp.dest(DEST));
});
case 2 : building a SVG sprite from multiple svg files.
doesn't work! the task runs again if I run it twice in a row
gulp.task('vector', function () {
var DEST = target + config.assets.images.vector_dest;
var configsvg = { // ... config stuff // };
return gulp.src(src + config.assets.images.vector_src)
.pipe(changed(DEST))
.pipe(svgo())
.pipe(svgSprite(configsvg)).on('error', function (error) {
console.log(error);
})
.pipe(size())
.pipe(gulp.dest(DEST));
});
The solution is to use gulp-newer module in case of "many to one" file compilation (concatenation etc...)
Thank you to Lim H.
gulp-newer vs gulp-changed
May I also suggest gulp-newy in which you can manipulate the path and filename in your own function. Then, just use the function as the callback to the newy(). This gives you complete control of the files you would like to compare.
This will allow 1:1 or many to 1 compares.
newy(function(projectDir, srcFile, absSrcFile) {
// do whatever you want to here.
// construct your absolute path, change filename suffix, etc.
// then return /foo/bar/filename.suffix as the file to compare against
}
I am building a Chrome Extension and I have a requirement to overlay a blob of html on top of a few websites. At the moment I am using a JQuery .Get to pull the html from my server. In order to improve performance I am wondering if it is possible to include the html as a file in the extension directory and access the source directly from there? Does anyone know if this is possible?
UPDATE
Rob's suggestion does the job (see accepted answer). The only additional step is to register the file in the manifest under web_accessible_resources.
{
...
"web_accessible_resources": [
"myimportfile1.html",
"myimportfile2.html"
],
...
}
Yes, that's possible. Use chrome.runtime.getURL to get an absolute URL for the resource. For example:
Step 1 (standard JavaScript):
fetch(chrome.runtime.getURL('/template.html')).then(r => r.text()).then(html => {
document.body.insertAdjacentHTML('beforeend', html);
// not using innerHTML as it would break js event listeners of the page
});
Step 1 (jQuery):
$.get(chrome.runtime.getURL('/template.html'), function(data) {
$(data).appendTo('body');
// Or if you're using jQuery 1.8+:
// $($.parseHTML(data)).appendTo('body');
});
Step 2:
Register the resource in the manifest.json under web_accessible_resources:
"web_accessible_resources": [
"template.html",
"foo.jpg"
]
Another way of doing it is to use new Fetch API:
If the file's name is modal.html - update manifest.json accordingly
"web_accessible_resources": [
"modal.html",
],
and inject it like this:
fetch(chrome.runtime.getURL('/modal.html'))
.then(response => response.text())
.then(data => {
document.getElementById('inject-container').innerHTML = data;
// other code
// eg update injected elements,
// add event listeners or logic to connect to other parts of the app
}).catch(err => {
// handle error
});
This is my approach using a synchronous XHR:
var xmlHttp = null;
xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", chrome.runtime.getURL ("src/inject/inject.html"), false );
xmlHttp.send( null );
var inject = document.createElement("div");
inject.innerHTML = xmlHttp.responseText
document.body.insertBefore (inject, document.body.firstChild);
Without jQuery etc.
I use this code. It's only 3 lines of code and you don't need any jquery's garbage.
var iframe = document.createElement ('iframe');
iframe.src = chrome.runtime.getURL ('iframe.html');
document.body.appendChild (iframe);
If you're using Angular in your Chrome extension, you can make use of ng-include
var injectedContent = document.createElement("div");
injectedContent.setAttribute("ng-include", "");
//ng-include src value must be wrapped in single quotes
injectedContent.setAttribute("src", "'" + chrome.runtime.getURL("template.html") + "'");
existingElement.appendChild(injectedContent);