CDN caching for React.JS SSR - javascript

I have the below code to do my server-side rending:
// Load in our HTML file from our build
fs.readFile(
path.resolve(__dirname, '../build/index.html'),
'utf8',
(err, htmlData) => {
// If there's an error... serve up something nasty
...
// Pass all this nonsense into our HTML formatting function above
const html = injectHTML(htmlData, {
html: helmet.htmlAttributes.toString(),
title: helmet.title.toString(),
meta: helmet.meta.toString(),
headScript: helmet.script.toString(),
link: helmet.link.toString(),
body: routeMarkup,
scripts: extraChunks,
state: JSON.stringify(store.getState()).replace(/</g, '\\u003c')
});
// We have all the final HTML, let's send it to the user already!
res.send(html);
It is working fine. However, all my static assets are loaded from ../build. I want to connect a CDN, such as S3 to cache assets.
To do this, I need to prepend the CDN url to links to static assets so <script src="/static/js/main.7e3b844f.chunk.js"></script> becomes <script src="https://cdn.mydomain.com/static/js/main.7e3b844f.chunk.js"></script>
The urls of interest are inside htmlData. I could use regular expressions to replace /static/css with ${prefix}/static/css and the same for /static/js.
Are there better alternatives than running a regex? Suggestoins?

I ended-up doing the below before injecting HTML with body, meta etc:
const prefix =
process.env.REACT_APP_STAGE === 'production'
? 'https://prod-cdn.mydomain.com'
: '';
const processedHtmlData = htmlData.replace(
/(\/static)/g,
`${prefix}$1`
);
const html = injectHTML(processedHtmlData, {
html: helmet.htmlAttributes.toString(),
title: helmet.title.toString(),
meta: helmet.meta.toString(),
headScript: helmet.script.toString(),
link: helmet.link.toString(),
body: routeMarkup,
scripts: extraChunks,
state: JSON.stringify(store.getState()).replace(/</g, '\\u003c')
});
It works.

Related

vueHtml2Pdf renders blank page (Nuxt)

I am using vueHtml2Pdf to generate my page to pdf, but when I wrap my content inside VueHtml2pdf tag nothing renders on my page, but it downloads when I click the download button. (Nuxt)
methods: {
downloadPDF() {
this.$refs.html2Pdf.generatePdf()
},
},
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<ArticleActions #download="downloadPDF()" />
<client-only>
<vue-html2pdf
ref="html2Pdf"
:show-layout="false"
:enable-download="true"
:pdf-quality="2"
:manual-pagination="true"
pdf-content-width="100%"
:html-to-pdf-options="htmlToPdfOptions"
>
<section slot="pdf-content">
<!-- content -->
<div
v-interpolation="{ newWindow: true }"
class="articleContent__content"
v-html="article.content"
></div>
<!-- /content -->
</section>
</vue-html2pdf>
</client-only>
I have a working solution for Nuxtv3 (with server-side rendering). After trying a bunch of different vue-specific packages, including vue-html2pdf, I realized that most of them have been written for Vue2.
Instead, I chose to use html2pdf directly.
Upon directly importing html2pdf in the component where I need to add the functionality for converting html to pdf, Nuxt throws the following error: ReferenceError: self is not defined. This essentially happens because the line where the library is being imported runs on the server side as well and when it is imported, it tries to access a variable that isn't defined on the server side.
My solution was to create a custom plugin that runs only on the client side. It is very simple to do this in Nuxtv3 by just ending the filename with .client.ts as opposed to just .ts. Here is what plugins/html2pdf.client.ts looks like:
import html2pdf from 'html2pdf.js'
export default defineNuxtPlugin(() => {
// had to make a plugin because directly importing html2pdf.js in the component
// where I need to use it was causing an error as the import would run on the server
// side and html2pdf.js is a client-side-only library. This plugin runs on the
// client side only (due to the .client extension) so it works fine.
return {
provide: {
html2pdf: (element, options) => {
return html2pdf(element, options)
}
}
}
})
Now, I can safely use it in my component as:
const { $html2pdf } = useNuxtApp()
function downloadPDF() {
if (document) {
const element = document.getElementById('html2pdf')
// clone the element: https://stackoverflow.com/questions/60557116/html2pdf-wont-print-hidden-div-after-unhiding-it/60558415#60558415
const clonedElement = element.cloneNode(true) as HTMLElement
clonedElement.classList.remove('hidden')
clonedElement.classList.add('block')
// need to append to the document, otherwise the downloading doesn't start
document.body.appendChild(clonedElement)
// https://www.npmjs.com/package/html2pdf.js/v/0.9.0#options
$html2pdf(clonedElement, {
filename: 'filename.pdf',
image: { type: 'png' },
enableLinks: true
})
clonedElement.remove()
}
}
Basic usage of html2pdf: https://www.npmjs.com/package/html2pdf.js/v/0.9.0#usage
Configuration for html2pdf: https://www.npmjs.com/package/html2pdf.js/v/0.9.0#options
If someone looking for how to use html2pdf in nuxt 2.
install html2pdf.js using npm or yarn
create html-to-pdf.js file in nuxt plugin directory with below code
import html2pdf from 'html2pdf.js'
export default (context, inject) => {
inject('html2pdf', html2pdf)
}
Then add plugin to nuxt.config.js
plugins: [
'#/plugins/axios',
......
{ src: '#/plugins/html-to-pdf', mode: 'client' },
],
How to use , example in your component menthod
methods: {
export() {
this.$html2pdf(this.$refs.document, {
margin: 1,
filename: 'file-name.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 1,
dpi: 192,
letterRendering: true,
ignoreElements: true,
},
jsPDF: { unit: 'pt', format: 'a2', orientation: 'portrait' },
})
},

NextJs :pid routes

Does anybody know what is the exportPathMap: (next.config.js) for NextJS of a path that has a :pid?
My expotPathMap
exportPathMap: async (defaultPathMap) => {
return {
'/': { page: '/', query: {} },
'/login': { page: '/login', query: { verifySuccess: null } },
'/signup': { page: '/signup', query: {} },
'/search': { page: '/search', query: { s: '', category: '' } },
'/messages': { page: '/messages', query: { t: '' } },
'/messages/:pid': { page: '/messages/:pid', query: { t: '' } },
Issue is that I was tasked to create a page that would look like
/messages/925255252
instead of a page that uses a query param like /message?id=9252552252&t=foo
Now when building & exporting I am getting this error
Cannot find module for page: /messages/:pid
The files.
pages > messages > index.js (/messages), [pid].js (messages/:id)
PS.
Not using SSR, rendering is client sided!
PPS.
All is fine on localhost, needs to work in production.
What you are trying to achieve is not possible, from the spectrum chat of next.js :
You have to return a mapping of every possible route, dynamic matching
wouldn't have any effect even if we did support it, how would you know
what /show/:id is going to be when exporting? We have to know exactly
what is going to be exported at export time
So you have to generate all possibile pages (in your case you need all possible messages ids), example fetching your database.
Or switch to SSR and handle your requests server side.

Use i18next with XHR backend in client-side javascript

The documentation at i18next-xhr-backend tells me to use import to load their module. But when I use the import-statement, nothing happens and Firefox gives me a SyntaxError in the developer console:
SyntaxError: import declarations may only appear at top level of a module
So how can I use i18next library with the XHR-backend? The following code example works if the .use(XHR)-line and the corresponding import is commented out (Warning: i18next::backendConnector: No backend was added via i18next.use. Will not load resources.). But it fails, if it is not: ReferenceError: XHR is not defined
//import Fetch from 'i18next-fetch-backend';
let t = null;
i18next
.use(XHR)
.init({
debug: true,
fallbackLng: ['en'],
preload: ['en'],
ns: 'translation',
defaultNS: 'translation',
keySeparator: false, // Allow usage of dots in keys
nsSeparator: false,
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
}, (err, _t) => {
if (err) {
reject(err);
return;
}
t = _t;
//resolve();
});
jqueryI18next.init(i18next, $, {
tName: 't', // --> appends $.t = i18next.t
i18nName: 'i18n', // --> appends $.i18n = i18next
handleName: 'localize', // --> appends $(selector).localize(opts);
selectorAttr: 'data-i18n', // selector for translating elements
targetAttr: 'i18n-target', // data-() attribute to grab target element to translate (if different than itself)
optionsAttr: 'i18n-options', // data-() attribute that contains options, will load/set if useOptionsAttr = true
useOptionsAttr: false, // see optionsAttr
parseDefaultValueFromContent: true // parses default values from content ele.val or ele.text
});
$(".nav").localize();
I needed to use i18nextXHRBackend instead of just XHR, since that is the name the class gets loaded as if no loader is used. As the README.md says:
If you don't use a module loader it will be added to window.i18nextXHRBackend
I didn't see that before, and I didn't know that this will happen automatically, but it seems that you have to find that out on your own if not using a module loader. Lesson learned, hopefully this will help some other newbies being stuck on how to use modules in javascript. Therefore, my complete localisation.js looks like this:
$(document).ready(function() {
i18next
.use(i18nextXHRBackend)
.use(i18nextBrowserLanguageDetector)
.init({
debug: true,
backend: {
loadPath: 'locales/{{lng}}/{{ns}}.json',
addPath: 'locales/add/{{lng}}/{{ns}}'
}
}, function(err, t) {
jqueryI18next.init(i18next, $);
$('.translatable').localize();
$('.language-button').click(function() {
i18next.changeLanguage(this.firstElementChild.alt).then(function(t) {
$('.translatable').localize();
$('#signupPassword').pwstrength("forceUpdate");
$('#signupPasswordConfirm').pwstrength("forceUpdate");
});
});
});
});

How to add authorization header when runtime import webpack chunks of Vue components

The purpose of this task is to make it impossible to download the Vue-component package (*.js file) knowing the address of the component, but not having an access token.
I'm developing an access control system and a user interface in which the set of available components depends on the user's access level.
The system uses the JSON API and JWT authorization. For this, Axios is used on the client side. To build the application, we use Webpack 4, to load the components, we use the vue-loader.
After the user is authorized, the application requests an array of available routes and metadata from the server, then a dynamically constructed menu and routes are added to the VueRouter object.
Below I gave a simplified code.
import axios from 'axios'
import router from 'router'
let API = axios.create({
baseURL: '/api/v1/',
headers: {
Authorization: 'Bearer mySecretToken12345'
}
})
let buildRoutesRecursive = jsonRoutes => {
let routes = []
jsonRoutes.forEach(r => {
let path = r.path.slice(1)
let route = {
path: r.path,
component: () => import(/* webpackChunkName: "restricted/[request]" */ 'views/restricted/' + path)
//example path: 'dashboard/users.vue', 'dashboard/reports.vue', etc...
}
if (r.children)
route.children = buildRoutesRecursive(r.children)
routes.push(route)
})
return routes
}
API.get('user/routes').then(
response => {
/*
response.data =
[{
"path": "/dashboard",
"icon": "fas fa-sliders-h",
"children": [{
"path": "/dashboard/users",
"icon": "fa fa-users",
}, {
"path": "/dashboard/reports",
"icon": "fa fa-indent"
}
]
}
]
*/
let vueRoutes = buildRoutesRecursive(response.data)
router.addRoutes(vueRoutes)
},
error => console.log(error)
)
The problem I'm having is because Webpack loads the components, by adding the 'script' element, and not through the AJAX request. Therefore, I do not know how to add an authorization header to this download. As a result, any user who does not have a token can download the code of the private component by simply inserting his address into the navigation bar of the browser.
Ideally, I would like to know how to import a vue component using Axios.
Or, how to add an authorization header to an HTTP request.
I needed something similar and came up with the following solution. First, we introduce a webpack plugin that gives us access to the script element before it's added to the DOM. Then we can munge the element to use fetch() to get the script source, and you can craft the fetch as needed (e.g. add request headers).
In webpack.config.js:
/*
* This plugin will call dynamicImportScriptHook() just before
* the script element is added to the DOM. The script object is
* passed to dynamicImportScriptHook(), and it should return
* the script object or a replacement.
*/
class DynamicImportScriptHookPlugin {
apply(compiler) {
compiler.hooks.compilation.tap(
"DynamicImportScriptHookPlugin", (compilation) =>
compilation.mainTemplate.hooks.jsonpScript.tap(
"DynamicImportScriptHookPlugin", (source) => [
source,
"if (typeof dynamicImportScriptHook === 'function') {",
" script = dynamicImportScriptHook(script);",
"}"
].join("\n")
)
);
}
}
/* now add the plugin to the existing config: */
module.exports = {
...
plugins: [
new DynamicImportScriptHookPlugin()
]
}
Now, somewhere convenient in your application js:
/*
* With the above plugin, this function will get called just
* before the script element is added to the DOM. It is passed
* the script element object and should return either the same
* script element object or a replacement (which is what we do
* here).
*/
window.dynamicImportScriptHook = (script) => {
const {onerror, onload} = script;
var emptyScript = document.createElement('script');
/*
* Here is the fetch(). You can control the fetch as needed,
* add request headers, etc. We wrap webpack's original
* onerror and onload handlers so that we can clean up the
* object URL.
*
* Note that you'll probably want to handle errors from fetch()
* in some way (invoke webpack's onerror or some such).
*/
fetch(script.src)
.then(response => response.blob())
.then(blob => {
script.src = URL.createObjectURL(blob);
script.onerror = (event) => {
URL.revokeObjectURL(script.src);
onerror(event);
};
script.onload = (event) => {
URL.revokeObjectURL(script.src);
onload(event);
};
emptyScript.remove();
document.head.appendChild(script);
});
/* Here we return an empty script element back to webpack.
* webpack will add this to document.head immediately. We
* can't let webpack add the real script object because the
* fetch isn't done yet. We add it ourselves above after
* the fetch is done.
*/
return emptyScript;
};
Although sspiff's answer looks quite promising, it did not work directly for me.
After some investigation this was mainly due to me using Vue CLI 3 and thus a newer version of webpack. (which is kinda weird as sspiff mentioned using webpack 4.16.1).
Anyway to solve it I used the following source: medium.com,
Which gave me the knowledge to edit the given code.
This new code is situated in vue.config.js file:
/*
* This plugin will call dynamicImportScriptHook() just before
* the script element is added to the DOM. The script object is
* passed to dynamicImportScriptHook(), and it should return
* the script object or a replacement.
*/
class DynamicImportScriptHookPlugin {
apply(compiler) {
compiler.hooks.compilation.tap(
"DynamicImportScriptHookPlugin", (compilation) =>
compilation.mainTemplate.hooks.render.tap(
{
name: "DynamicImportScriptHookPlugin",
stage: Infinity
},
rawSource => {
const sourceString = rawSource.source()
if (!sourceString.includes('jsonpScriptSrc')) {
return sourceString;
} else {
const sourceArray = sourceString.split('script.src = jsonpScriptSrc(chunkId);')
const newArray = [
sourceArray[0],
'script.src = jsonpScriptSrc(chunkId);',
"\n\nif (typeof dynamicImportScriptHook === 'function') {\n",
" script = dynamicImportScriptHook(script);\n",
"}\n",
sourceArray[1]
]
return newArray.join("")
}
}
)
);
}
}
module.exports = {
chainWebpack: (config) => {
config.plugins.delete('prefetch')
},
configureWebpack: {
plugins: [
new DynamicImportScriptHookPlugin()
]
}
}
The second piece of code provided by sspiff has stayed the same and can be placed in the App.vue file or the index.html between script tags.
Also to further improve this answer I will now explain how to split the chunks in Vue CLI 3 for this specific purpose.
as you can see I also added the chainWebpack field to the config. This makes sure that webpack does not add prefetch tags in the index.html. (e.g. it will now only load lazy chunks when they are needed)
To further improve your splitting I suggest changing all your imports to something like:
component: () => import(/* webpackChunkName: "public/componentName" */ /* webpackPrefetch: true */'#/components/yourpubliccomponent')
component: () => import(/* webpackChunkName: "private/componentName" */ /* webpackPrefetch: false */'#/components/yourprivatecomponent')
This will make sure that all your private chunks end up in a private folder and that they will not get prefetched.
The public chunks will end up in a public folder and will get prefetched.
For more information use the following source how-to-make-lazy-loading-actually-work-in-vue-cli-3
Hope this helps anyone with this problem!
To perform a simple component download using an access token, you can do the following...
1) Use asynchronous component loading with file extraction. Use webpackChunkName option to separate file or directory/file, like:
components: {
ProtectedComp: () => import(/* webpackChunkName: "someFolder/someName" */ './components/protected/componentA.vue')
}
2) Configure server redirection for protected files or direcory. Apache htaccess config for example:
RewriteRule ^js/protected/(.+)$ /js-provider.php?r=$1 [L]
3) write a server-side script that checks the token in the header or cookies and gives either the contents of .js or 403 error.

How to fix template paths in templateCache? (gulp-angular-templatecache)

I'm using gulp-angular-templatecache to generate a templateCache.js file which combines all my HTML template files into 1. (my full gulpfile)
After injecting that new module into my app, my Directives will automatically pick up the templates and I won't need to add the partial .html files into my build folder.
The problem is that the leading folder path is getting cut off, see my example below:
The paths in my Directives:
templateUrl : "panels/tags/tagsPanel.html"...
templateUrl : "header/platform_header/platformHeader.html"...
The paths in my produced templateCache file:
$templateCache.put("tags/tagsPanel.html"...
$templateCache.put("platform_header/platformHeader.html"...
^ panels and header are getting lost.
I'm trying to write a function that will fix that in my Gulpfile.
The config section of my Gulpfile:
var config = {
srcPartials:[
'app/beta/*.html',
'app/header/**/*.html',
'app/help/*.html',
'app/login/*.html',
'app/notificaitons/*.html',
'app/panels/**/*.html',
'app/popovers/**/*.html',
'app/popovers/*.html',
'app/user/*.html',
'app/dashboard.html'
],
srcPaths:[
'beta/',
'header/',
'help/',
'login/',
'notificaitons/',
'panels/',
'popovers/',
'popovers/',
'user/',
'dashboard.html'
],
destPartials: 'app/templates/'
};
My html-templates gulp.task
gulp.task('html-templates', function() {
return gulp.src(config.srcPartials)
.pipe(templateCache('templateCache.js', {
root: updateRoot(config.srcPaths)
},
{ module:'templateCache', standalone:true })
).pipe(gulp.dest(config.destPartials));
});
function updateRoot(paths) {
for (var i = 0; i < paths.length; i++) {
// console.log(paths);
console.log(paths[i]);
return paths[i];
}
}
^ The above is working, in that it uses the root option in gulp-angular-templatecache to append a new string in front of the template paths.
Problem is my code above returns once and updates all the paths to the first item in the paths Array which is beta/.
How would you write this so that it correctly replaces the path for each file?
Figured it out! I should not have been using exact folder names, but globs ** in my config
var config = {
srcTemplates:[
'app/**/*.html',
'app/dashboard.html',
'!app/index.html'
],
destPartials: 'app/templates/'
};
The updated gulp.task:
gulp.task('html-templates', function() {
return gulp.src(config.srcTemplates)
.pipe(templateCache('templateCache.js', { module:'templateCache', standalone:true })
).pipe(gulp.dest(config.destPartials));
});
Now the output is correct:
$templateCache.put("beta/beta.html"...
$templateCache.put("header/control_header/controlHeader.html"...
$templateCache.put("panels/tags/tagsPanel.html"...
I ran into a similar issue, but in my case, it wasn't possible to use the same directory for all the gulp.src entries.
There is a solution that will work all those folders
return gulp.src(['public/assets/app1/**/*.tpl.html', 'public/assets/common_app/**/*.tpl.html'])
.pipe(templateCache({
root: "/",
base: __dirname + "/public",
module: "App",
filename: "templates.js"
}))
.pipe(gulp.dest('public/assets/templates'))
That presumes the gulpfile is one directory up from the public directory. It will remove the base string from the full path of the files from src.
Base can also be a function that will be passed the file object which could be helpful in more complicated situations. It's all around line 60 of the index.js in gulp-angular-templatecache
You can also use this gulp plugin which can read your routes, directives and replace the templateUrl with the template referenced in the templateUrl.
This will remove all headache regarding handling templateUrl in your application. This uses relative url to directive js files instead of absolute url.
src
+-hello-world
|-hello-world-directive.js
+-hello-world-template.html
hello-world-directive.js:
angular.module('test').directive('helloWorld', function () {
return {
restrict: 'E',
// relative path to template
templateUrl: 'hello-world-template.html'
};
});
hello-world-template.html:
<strong>
Hello world!
</strong>
gulpfile.js:
var gulp = require('gulp');
var embedTemplates = require('gulp-angular-embed-templates');
gulp.task('js:build', function () {
gulp.src('src/scripts/**/*.js')
.pipe(embedTemplates())
.pipe(gulp.dest('./dist'));
});
gulp-angular-embed-templates will generate the following file:
angular.module('test').directive('helloWorld', function () {
return {
restrict: 'E',
template:'<strong>Hello world!</strong>'
};
});

Categories

Resources