How to make Angular Universal and PWA work together? - javascript

I have a SSR Angular app which I am trying to transform into a PWA. I want it to be server-side rendered for SEO and for the "fast first rendering" that it provides.
The PWA mode works fine when combined with SSR, but once the app is loaded, when we refresh it, the client index HTML file is loaded instead of the server-side rendered page.
I have dug into the code of ngsw-worker.js and I saw this:
// Next, check if this is a navigation request for a route. Detect circular
// navigations by checking if the request URL is the same as the index URL.
if (req.url !== this.manifest.index && this.isNavigationRequest(req)) {
// This was a navigation request. Re-enter `handleFetch` with a request for
// the URL.
return this.handleFetch(this.adapter.newRequest(this.manifest.index), context);
}
I have no control over this file since it's from the framework and not exposed to developers.
Did anybody find a solution or workaround for this?

Up-to-date answer (v11.0.0)
Angular now has a navigationRequestStrategy option which allows to prioritize server requests for navigation. Extract of the changelog:
service-worker: add the option to prefer network for navigation
requests (#38565) (a206852), closes #38194
To be used wisely! This warning appears in the documentation:
The freshness strategy usually results in more requests sent to the
server, which can increase response latency. It is recommended that
you use the default performance strategy whenever possible.
Old answer (for archaeological purposes)
I have found a working solution, the navigationUrls property of ngsw-config.json contains a list of navigation URLs included or excluded (with an exclamation mark) like explained in the documentation.
Then I configured it like this:
"navigationUrls": [
"!/**"
]
This way, none of the URLs redirect to index.html and the server-side rendered app comes into play when the app is first requested (or refreshed), whatever the URL is.
To go further, the three kinds of URLs managed by the service worker are:
Non-navigation URLs: static files cached by the service worker and listed in the generated ngsw.json file with their corresponding hashes
Navigation URLs: redirected to index.html by default, forwarded to the server if the "!/**" configuration is used
GET requests to the backend: forwarded to the backend
In order to distinguish a GET XMLHttpRequest from a navigation request, the service worker uses the Request.mode property and the Accept header that contains text/html when navigating and application/json, text/plain, */* when requesting the backend.
Edit: This is actually not a good practice to do that for two reasons:
Depending on the network quality, there is no guarantee that the server-side version will render faster than the cached browser version
It breaks the "update in background" mechanism. Indeed, the server-side rendered app will always refer to the latest versions of the JavaScript files
For more details on this, please take a look at the Angular's team member answer to my feature request: https://github.com/angular/angular/issues/30861

Related

how to use variables in base url and compose config when making the request in Vue.js?

What I want to achieve - for vue to serve a foo1.bar.com where "foo" is a name of the tenant in multitenant project. So base API that vue uses becomes foo2.bar.com/api when foo2.bar.com is accessed and foo3.bar.com/api when foo3.bar.com is accessed.
Context: this is a suggested way to achieve coherence with existing multitenant backend API, URLs of which look like t1.site.com/api and t2.site.com/api .
It was suggested on reddit as a response to this question:
I have almost finished my first decoupled (frond and back ends are
separate) project - the back end is written with django + rest
framework and implements multitenancy (means my api endpoints look
like tenant1.sitename.com/api/endpoint and
tenant2.sitename.com/api/endpoint) . While I was developing, I assumed
that there shouldn't be a problem consuming my api since the front end
is the same for all tenants, so django could just consume same vue
front end no matter which tenant.. ant then it struck me - actually
it's vue consuming django api, not other way around.. and vue doesn't
know which tenant is selected..So now I'm very close to a deadline and
lost.
My main.ts looks like this
axios.defaults.baseURL = 'http://tenant1.mysite.local:8000/api/';axios.defaults.withCredentials
= true;
and works... while I need the first tenant's data....
I'm not entirely sure that variable is supposed to be used in baseUrl, or that typescript is supposed to be used, but as I said, my current setup has baseurl in main.ts .
To reiterate:
I have one back-end serving api for different tenants like t1.foo.com/api and t2.foo.com/api and one front-end that currently only sends requests to only one baseurl defined in settings, for example t1.foo.com/api ; It then serves it on t1.foo.com/home . Problem is, if I would to go to t2.foo.com/home , it would still send requests to t1.foo.com/api .
I know neither how to make different (t1,t2,t3) urls accessible nor how to make it send requests to matching api. I want to acieve my frontent sending the api request to t1.foo.com/api when i go to to t1.foo.com/home and t2.foo.com/api when I go to t2.foo.com/home .
I asked a similar question before and I got this full detailed answer
if I understand you correctly I think the best solution for is to set this in vue.config.js file:
publicPath: './'
which sets the url of all request to the backend to the relative url of the served html file (including static files like css, js...).
For example if you access you application with this url t1.mysite.com/index.html - all request will be sent to t1.mysite.com/..../....
you can read more about publicPath in vue.js docs

How to update service workers?

Situation:
On mywebsite.com/game, I registered a service-worker with
navigator.serviceWorker.register('/service-worker.js', {scope: "/"});
On my server, '/service-worker.js' has a maxAge of 1d.
Problem:
service-worker.js has a major bug. It always displays an empty page and can't fetch anything. service-worker.js must be changed.
The problem is whenever a user goes to mywebsite.com/game, it displays the empty page and does nothing more. I am unable to make the client fetch the new service-worker.js.
How can I make the client fetch the new service-worker.js?
What you're describing—a check for updates to /service-worker.js—happens by default, automatically, under the circumstances laid out in this article:
An update is triggered if any of the following happens:
A navigation to an in-scope page.
A functional events such as push and sync, unless there's been an update check within the previous 24 hours.
Calling .register() only if the service worker URL has changed. However, you should avoid changing the worker URL.
All modern web browsers will ignore any Cache-Control headers you set on /service-worker.js by default and go directly against the web server to obtain the latest copy.
This Stack Overflow answer has some best practices for what the revised service-worker.js file should contain if you want it to behave like a "kill switch."
Just add ?v=1 to your script like this.
navigator.serviceWorker.register('/service-worker.js?v=1', {scope: "/"});
And increment the number of script version when you make changes of service worker's script

Communicating with a web widget-Meteor, React, Node

I'm building a chat dashboard and widget with which a customer should be able to put the widget into their page. Some similar examples would be Intercom or Drift.
Currently, the "main" application is written in Meteor.js (it's front end is in React). I've written a <Widget /> component and thrown it inside a /widget directory. Inside this directory, I also have an index.jsx file, which simply contains the following:
import React from 'react';
import ......
ReactDOM.render(
<Widget/>,
document.getElementById('widget-target')
);
I then setup a webpack configuration with an entry point at index.jsx and when webpack is run spits out a bundle.js in a public directory.
This can then be included on another page by simply including a script and div:
<script src="http://localhost:3000/bundle.js" type="text/javascript"></script>
<div id="widget-target"></div>
A few questions:
What is wrong with this implementation? Are their any security issues to be aware of? Both the examples linked earlier seem make use of an iframe in one form or another.
What is the best way to communicate with my main meteor application? A REST API? Emit events with Socket.io? The widget is a chat widget, so I need to send messages back and forth.
How can I implement some sort of unique identifier/user auth for the user and the widget? Currently, the widget is precompiled.
1 What is wrong with this implementation? Are their any security issues to be aware of? Both the examples linked earlier seem make use of an iframe in one form or another.
As #JeremyK mentioned, you're safer within an iFrame. That being said, there's a middle route that many third parties (Facebook, GA, ...) are using, including Intercom:
ask users to integrate your bundled code within their webpage. It's then up to you to ensure you're not introducing a security vulnerability on their site. This code will do two things:
take care of setting up an iframe, where the main part of your service is going to happen. You can position it, style it etc. This ensure that all the logic happening in the iframe is safe and you're not exposed.
expose some API between your customer webpage and your iframe, using window messaging.
the main code (the iframe code) is then loaded by this first script asynchronously, and not included in it.
For instance Intercom ask customers to include some script on their page: https://developers.intercom.com/docs/single-page-app#section-step-1-include-intercom-js-library that's pretty small (https://js.intercomcdn.com/shim.d97a38b5.js). This loads extra code that sets the iFrame and expose their API that will make it easy to interact with the iFrame, like closing it, setting user properties etc.
2 What is the best way to communicate with my main meteor application? A REST API? Emit events with Socket.io? The widget is a chat widget, so I need to send messages back and forth.
You've three options:
Build your widget as an entire Meteor app. This will increase the size of the code that needs to be loaded. In exchange for the extra code, you can communicate with your backend through the Meteor API, like Meteor.call, get the reactivity of all data (for instance if you send a response to a user through your main Meteor application, the response would pop up on the client with no work to do as long as they are on the same database (no need to be on the same server)), and the optimistic UI. In short you've all what Meteor offers here, and it's probably going to be easier to integrate with your existing backend that I assume is Meteor.
Don't include Meteor. Since you're building a chat app, you'll probably need socket.io over a traditional REST API. For sure you can do a mix of both
Use Meteor DDP. (it's kind of like socket.io, but for Meteor. Meteor app use that for all requests to the server) This will include less things that the full Meteor and probably be easier to integrate to your Meteor backend than a REST API / socket.io, and will be some extra work over the full Meteor.
3 How can I implement some sort of unique identifier/user auth for the user and the widget?
This part should probably do some work on the customer website (vs in your iframe) so that you can set cookies on his page, and send that data to your iframe that's gonna talk to your server and identify the user. Wether you use artwells:accounts-guest (that's based on meteor:accounts-base) is going to depend on wether you decide to include Meteor in your iframe.
If you don't have Meteor in your iframe, you can do something like:
handle user creation yourself, by simply doing on your server
.
const token = createToken();
Users.insert({ tokens: [token] });
// send the token back to your iframe
// and set is as a cookie on your customer website
then for each call to your server, on your iframe:
.
let token;
const makeRequest = async (request) => {
token = token || getCookieFromCustomerWebsite();
// pass the token to your HTTP / socket.io / ... request.
// in the header of whatever
return await callServer(token, request);
};
in the server have a middleware that sets the user. Mine looks like:
.
const loginAs = (userId, cb) => {
DDP._CurrentInvocation.withValue(new DDPCommon.MethodInvocation({
isSimulation: false,
userId,
}), cb);
};
// my middleware that run on all API requests for a non Meteor client
export const identifyUserIfPossible = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return next();
}
const user = Users.findOne({ tokens: token });
if (!user) {
return next();
}
loginAs(user._id, () => {
next();
// Now Meteor.userId() === user._id from all calls made on that request
// So you can do Meteor.call('someMethod') as you'd do on a full Meteor stack
});
};
Asking your customers to embed your code like this doesn't follow the principles of Security by Design.
From their point of view, you are asking them to embed your prebundled code into their website, exposing their site up to any hidden security risks (inadvertent or deliberately malicious) that exist in your code which would have unrestricted access to their website's DOM, localstorage, etc.
This is why using an iframe is the prefered method to embed third party content in a website, as that content is sandboxed from the rest of it's host site.
Further, following the security principle of 'Least Privilege' they (with your guidance/examples) can set the sandbox attribute on the iframe, and explicitly lockdown via a whitelist the privileges the widget will have.
Loading your widget in an iframe will also give you more flexibility in how it communicates with your servers. This could now be a normal meteor client, using meteor's ddp to communicate with your servers. Your other suggestions are also possible.
User auth/identification depends on the details of your system. This could range from using Meteor Accounts which would give you either password or social auth solutions. Or you could try an anonymous accounts solution such as artwells:accounts-guest.
html5rocks article on sandboxed-iframes

What headers do I need for "PUT" request - Kinvey

Hi everyone I make simple SPA application with JS and Kinvey. I have advertisements and every advert. must have views - how many times is seen(when "GET" request for that advert is called, another "PUT" request is called for the advert with increased views). The problem is that I can't figure out which headers to use: Authorization basic with username:pass and "Kinvey + authToken" return 401 Unauthorized. How to modify collection element which is not created by the currently logged in user?
You will want to use the Javascript SDK which means you don't have to do the quite complicated login / token generation process yourself. It's not a Basic Auth system. The SDK's will handle everything for you.
You cannot by default modify elements that are not created by the logged-in user, which is of course a good idea for security reasons. But, in the Collection Settings, you can change the collection permissions from "Shared" to "Public" to allow anybody write access to any element.
If you want finer grained controls, you can use Business Logic to inspect ACLs at runtime: http://devcenter.kinvey.com/tutorials/using-acls

Google OAuth WildCard Domains

I am using the google auth but keep getting an origin mismatch. The project I am working has sub domains that are generated by the user. So for example there can be:
john.example.com
henry.example.com
larry.example.com
In my app settings I have one of my origins being http://*.example.com but I get an origin mismatch. Is there a way to solve this? Btw my code looks like this:
gapi.auth.authorize({
client_id : 'xxxxx.apps.googleusercontent.com',
scope : ['https://www.googleapis.com/auth/plus.me',
state: 'http://henry.example.com',
'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile'],
immediate : false
}, function(result) {
if (result != null) {
gapi.client.load('oath2', 'v2', function() {
console.log(gapi.client);
gapi.client.oauth2.userinfo.get().execute(function(resp) {
console.log(resp);
});
});
}
});
Hooray for useful yet unnecessary workarounds (thanks for complicating yourself into a corner Google)....
I was using Google Drive using the javascript api to open up the file picker, retrieve the file info/url and then download it using curl to my server. Once I finally realized that all my wildcard domains would have to be registered, I about had a stroke.
What I do now is the following (this is my use case, cater it to yours as you need to)
On the page that you are on, create an onclick event to open up a new window in a specific domain (https://googledrive.example.com/oauth/index.php?unique_token={some unique token}).
On the new popup I did all my google drive authentication, had a button to click which opened the file picker, then retrieved at least the metadata that I needed from the file. Then I stored the token (primary key), access_token, downloadurl and filename in my database (MySQL).
Back on step one's page, I created a setTimeout() loop that would run an ajax call every second with that same unique_token to check when it had been entered in the database. Once it finds it, I kill the loop and then retrieve the contents and do with them as I will (in this case I uploaded them through a separate upload script that uses curl to fetch the file).
This is obviously not the best method for handling this, but it's better than entering each and every subdomain into googles cloud console. I bet you can probably do this with googles server side oauth libraries they use, but my use case was a little complicated and I was cranky cause I was frustrated at the past 4 days I've spent on a silly little integration with google.
Wildcard origins are not supported, same for redirect URIs.
The fact that you can register a wildcard origin is a bug.
You can use the state parameter, but be very careful with that, make sure you don't create an open redirector (an endpoint that can redirect to any arbitrary URL).

Categories

Resources