Good day.
Problem definition:
I would like to deploy my Vue app, that utilizes vue-router, on dynamic path, which should be controlled by the WebServer, utilize vue-router in history mode, and avoid re-packaging the application for each Deployment.
F.e.
Run same app at
http://localhost/subpath/index.html
and
http://localhost/another-subpath/index.html
As vue-router configuration is done at the packaging stage (f.e. by webpack), and thus is not designed to be controlled after the packaging stage, making this simple but so-common setup not viable.
Also, vue-router have quite a complicated lifecycle, which does not allow to easily override that base setting at the application-level.
In addition - webpack ends up hardcoding actual resources into the html body, which ensures their proper consumption by the Client at the load-time, but as chunks are inter-dependent - they are almost impossible to be injected/edited dynamically. Application integrity fails in case of any modification to those on-post-DomReady event.
Research:
My search so far have not yielded any viable options to set such configuration up.
I came up with a couple of viable solutions.
One of the problems is that changing the Router Base is not the only thing one needs, in order to dynamically change the App's root, and make it function properly on dynamic URL.
In case of vue+webpack - actual scripts are added to the index.html on-build-stage, and thus - again - end up being hardcoded.
There are few options of fixing that, which I came up with.
The Ugly solution:
Regex-replace resources URLs at the index.html, on the web-server level, and hardcoded setting for your vue-router-base.
This is very questionable approach, but viable.
Unfortunately I do not have examples of that approach left, so - I can't provide any, but this should be pretty straightforward:
On your servlet-side, Pseudocode:
let fileContent = getFileContent(requested_file_name);
if (servingFile == "index.html") {
fileContent = fileContent.regexReplace("http://url_hardcoded_at_packaging_stage", "http://url_application_is_deployed_at");
fileContent = fileContent.regexReplace("setting_token_in_your_index_html", "http://url_application_is_deployed_at");
}
return fileContent;
In addition to that you will need to add this setting_token_in_your_index_html in your index.html:
<head>
<script type="text/javascript">
window.my_app_settings = {routerBase:"setting_token_in_your_index_html"}
</script>
</head>
And consume it at the vue-router level:
export default new Router({
mode: "history",
base: window.my_app_settings.routerBase,
routes: [...]
...
});
This approach is good in a sense that there is no need to modify anything on your vue-app level, and all the changes can be kept only on your servlet-side, whatever it is.
Also it has 0 performance impact on the vue-app itself.
Still ugly, but a lots better, from code-perspective at least:
Make vue-app base-aware.
This solution is not super-simple, but works reliably on all major web-servers and can be summarized as
Dynamically add vue-resources at the app init, based on cookie provided by the Web-Server.
This approach allows to "elegantly" modify all the resources URLs, with minimum impact on the performance-side, and keep things as dynamic as they can be.
This approach consists of few small changes on the Packaging, and the app-levels, and also relies on cookie to inform the app about custom base.
Provide the custom URL cookie from Server:
At your web-server add the cookie header (all major web-servers support such functionality):
(f.e. in Scalatra)
val contextShiftCookie = "subpath_where_ui_deployed";
val cookie = new Cookie("ui_deployment_root", contextShiftCookie );
cookie.setPath("/");
response.addCookie( cookie );
App modifications (index.html):
At the <head> section:
<script type="text/javascript">
// Build script names, for later injection.
let stringsJs = [
<% for (let js in htmlWebpackPlugin.files.js) { %>
"<%= htmlWebpackPlugin.files.js[js] %>",
<% } %>
];
let stringsCss = [
<% for (let css in htmlWebpackPlugin.files.css) { %>
"<%= htmlWebpackPlugin.files.css[css] %>",
<% } %>
];
// Simple vanilla Cookie getter (replace with something else if needed).
function getCookie(cname) {
var name = cname + "=";
var decodedCookie = decodeURIComponent(document.cookie);
var ca = decodedCookie.split(';');
for(var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
// Simple vanilla onDomReady handler (replace with something else if needed).
(function(exports, d) {
function domReady(fn, context) {
function onReady(event) {
d.removeEventListener("DOMContentLoaded", onReady);
fn.call(context || exports, event);
}
function onReadyIe(event) {
if (d.readyState === "complete") {
d.detachEvent("onreadystatechange", onReadyIe);
fn.call(context || exports, event);
}
}
d.addEventListener && d.addEventListener("DOMContentLoaded", onReady) ||
d.attachEvent && d.attachEvent("onreadystatechange", onReadyIe);
}
exports.domReady = domReady;
})(window, document);
// Calculating vue router base.
let routerBase = "/";
if (getCookie("ui_deployment_root")) {
routerBase = getCookie("ui_deployment_root");
// console.log("Found base cookie", routerBase);
} else {
console.log("No base cookie found");
}
let scriptsBase = routerBase == "/"? "" : routerBase;
// Prefilling basic settings.
window.my_vue_app_config = {
routerBase: routerBase
};
function loadOneScript(){
if (window.my_vue_app_scripts.length > 0) {
let currentScript = window.my_vue_app_scripts.shift();
currentScript.onload = loadOneScript;
document.getElementsByTagName("body").item(0).append(currentScript);
}
}
// Injecting scripts using deployment context.
domReady(function(event) {
let head = document.getElementsByTagName("head").item(0);
stringsCss.forEach(css => {
let script = document.createElement("link");
script.setAttribute("rel","stylesheet");
script.setAttribute("href", scriptsBase + css);
head.append(script);
});
window.my_vue_app_scripts = [];
stringsJs.forEach(js => {
let script = document.createElement("script");
script.setAttribute("type","text/javascript");
script.setAttribute("src", scriptsBase + js);
window.my_vue_app_scripts.push(script);
});
loadOneScript();
});
</script>
Step-by-step explanation:
Consume base setting as-soon-as-it-is-available, BEFORE actual chunks are loaded, to ensure that vue-router can consume it nicely.
Gather all the script names from WebPack (see let stringsJs and let stringsCss)
Load all the css as soon as the DomReady (because css is consumed on-the-fly, and there is nothing specific about loading them. They just should be available). See stringsCss.forEach for that.
Load all the js chunks one-by-one, as this is the only way to ensure that webpack-ed app will be initialized properly (see stringsJs.forEach, and loadOneScript routine).
Scripts loading is done in such manner that each script (chunk) requests another chunk as soon as it is loaded and consumed by the Client (browser). This ensures app integrity, and proper initialization, regardless of how it was packed.
Chunks are processed in proper order, provided by the webpack, again - to ensure integrity.
vue-router changes:
Consume new setting at the router-level:
export default new Router({
mode: "history",
base: window.my_vue_app_config.routerBase,
routes: [...]
...
});
Webpack mechanism changes:
To support custom bundling at the webpack:
<% for (let js in htmlWebpackPlugin.files.js) { %>
"<%= htmlWebpackPlugin.files.js[js] %>",
<% } %>
, you will need to modify the /app/build/webpack.ENV.conf.js file:
...
new HtmlWebpackPlugin({
...
inject: false,
...
}),
Where .ENV. should be your desired bundle target, but I suggest changing this for all your environments, as then you will be sure that your index.html changes work properly on all Environments.
Performance considerations:
This was my biggest concern with this "sequential scripts injection", because it sounds very bad, from performance perspective.
But, to my biggest surprise, actual tests have shown only ~0.100-0.150ms raize in the JavaScript processing time, compared to the fully-static serve of the Application, according to tests on all major Web Clients (browsers).
This basically renders performance concern irrelevant in my case, but we wary that this might differ in your case.
Other impact on the application lifecycle is totally neglectable, at least in my case (less than 0.05ms in overall impact on the App Load).
P.S.
As this approach is something I've developed purely myself - I would appreciate constructive criticism, and improvement proposals :)
I'm looking for a solution/idea to dynamically change the translation value of each key in Sparatcus translations files outside the code. I don't want only to create a file and override the I18nModule config, I'm looking for some kind of service/API like Lokalize API to be able to change the translation values outside the code.
Thanks in advance!
The internationalisation features are prepared for this. Although we do not have a service at hand for the localised labels, Spartacus is prepared for this. You can read more about this at https://sap.github.io/spartacus-docs/i18n/#lazy-loading. You can configure loadPath to an API endpoint, including the variable language (lng) and namespace (ns).
imports: [
B2cStorefrontModule.withConfig({
i18n: {
backend: {
loadPath: 'assets/i18n-assets/{{lng}}/{{ns}}.json'
// crossOrigin: true (use this option when i18n assets come from a different domain)
},
chunks: translationChunksConfig
}
})
];
We have hosted jsreport node application on EBS. We created template and using css and javascripts from a static website(hosted internally). In the external javascript file we are using variables similar to what jsreport requires i.e. {{variablename}} which does not work. When we add the javascript inline in the template it works.
We know there should be some other way around to specify this but could not find it.
This won't work. jsreport templating engines only compile and process the html output, not the referenced scripts.
However you can try this approach:
Put a placeholder in a template content where you want to put external script. Lets say we want to put inline jquery
<script>
$$$myScript
</script>
<script>
$(() => {
alert('yes I have jquery inlined')
})
</script>
Create jsreport custom server script which downloads your external script, in this case jquery, and replace the placeholder with its content
var getReq = require('request').get
function beforeRender(req, res, done) {
getReq('https://code.jquery.com/jquery-3.1.0.min.js', (err, res, body) => {
req.template.content = req.template.content.replace('$$$myScript', body.toString())
done()
})
}
The script will run before the templating engines are executed therefore you can use templating engines tags inside it now.
playground live demo here
I am trying to compile and render a Dust template in Express using content from two different sources:
Dust files located under the /views directory
A response as a string from an external CDN
My goal is to receive a string response from the CDN, which will have content referencing the Dust files stored locally in /views. It will look something like this:
"{>layout/}
{<content}
<h1>Here is the dynamic content that will change based on the CDN request</h1>
{<content}"
The layout.dust file is stored locally under /views, which is referenced from the CDN's string response.
I am trying to compile the string response in my route by doing:
var compiled = dust.compile(templateStr, 'catalog_template');
dust.loadSource(compiled);
dust.render('catalog_template', dustParams, function(err, out) {
if(err) {
console.log(err);
}
console.log(out);
res.render(out);
});
But rendering the file causes an error:
[Error: Template Not Found: layout]
So somehow I need to compile the CDN's string with layout.dust (which is located in my /views directory). What is the best way to do this?
You just need to compile layout.dust by itself and when you render the CDN template, dust will pull in the layout partial. You can have dust dynamically compile templates from your filesystem like so:
dust.onLoad = function (name, callback) {
// Get src from filesystem
fs.readFile('path to template', function (err, data) {
if (err) throw err;
callback(err, data.toString());
});
}
Make sure you require the node fs module in your code.
Using rails 3 asset pipeline, I've structured the javascript (by using coffeescript) to files regarding the model. For example, all comment writing related javascript is stored to /app/assets/javascripts/comments.js.coffee, and user overlay related (fetching a:href's and triggering ajax on them) are stored in /app/assets/javascripts/users.js.coffee.
However, now I'm using more and more AJAX calls, where HTML content is pulled dynamically to the site. The problem is that I need to execute the javascript in various files, but as coffeescript is scoped inside a function, I can not access them.
Let's say that I've got a general.js.coffee file with following code
$(document).ready ->
# Parse all images with action
$("img.clickableaction").click ->
# Fetch some content
$.ajax
url: "something.php"
dataType: "html"
complete: (xhr, status) ->
# We got the content, set it up to a container
$("#somecontainer").html(receiveddata)
# The tricky part:
# run code in comments.js.coffee for #somecontainer (or for the whole dom)
# run code in users.js.coffee for #somecontainer (or for the whole dom)
And comments.js.coffee contains for example the following:
$(document).ready ->
commentDiv = $('div#commentsContainer')
commentsFetch = $('a.commentsFetch')
# Set commentid for comments fetch
commentsFetch.bind 'ajax:beforeSend', (event, xhr, settings) ->
# do stuff
The comments.js.coffee code works for the initial page view, e.g. the HTML code that was received when user loaded the page. But now, I need to parse the comments.js.coffee code for the content returned from the ajax call.
I can not make a function inside comments.js because it is scoped away, and can not be accessed from the general.js. This is what coffeescript produces:
(function() {
$(document).ready(function() {
var commentDiv, commentsFetch;
commentDiv = $('div#commentsContainer');
commentsFetch = $('a.commentsFetch');
}
})
I could make a global function for each separate file, e.g. window.comments && window.users, but then I'd need to call window.comments from the many places where I need to have ajax oncomplete call. On the long term, that will produce duplicate and hard to maintain -code.
How could something like this be made:
// comments.js
window.ajaxExecuteBlocks.push(function() { // comments related stuff });
// user.js
window.ajaxExecuteBlocks.push(function() { // user related stuff });
// general.js ajax on complete:
window.runExecuteBlocks()
Then, runExecuteBlocks would somehow run through all the functions that have been initialized in various controller-specific javascript files.
Anyone implemented similar system?