Serving a web application with built-in router in 3 ways :
Later it could be interesting to handle the rendering on CDNs or to use also CDNs for loopback requests. The goal is to have a high performance delivery service for the user.
Does not propose a default way to use a routed App such as Angular, React, Vuejs leading to 404 errors.
I will speak for Angular first even if I think VueJS and React should be really close in the use.
In loopback 3 there was a need to change the middleware and let the angular app handle routes, so if your web application was in a public folder you would need to serve index.html and redirect every routes to the index.html to let Angular deal with these routes.
Here is a post on how to do it : stackoverflow - configure loopback 3 to serve Angular 4+
Now get back to loopback 4.
I think the file to change to let the web app deal with routes is the sequence file : src/sequence.ts
If I serve the dist folder of a fresh Angular in the public folder I will have some 404 errors from Angular in browser console :
GET http://localhost:3000/runtime.js net::ERR_ABORTED 404 (Not Found)
5Refused to execute script from '<URL>' because its MIME type ('application/json') is not executable, and strict MIME type checking is enabled.
localhost/:1 Refused to execute script from 'http://localhost:3000/runtime.js' because its MIME type ('application/json') is not executable, and strict MIME type checking is enabled.
localhost/:1 Refused to execute script from 'http://localhost:3000/polyfills.js' because its MIME type ('application/json') is not executable, and strict MIME type checking is enabled.
localhost/:1 Refused to execute script from 'http://localhost:3000/styles.js' because its MIME type ('application/json') is not executable, and strict MIME type checking is enabled.
localhost/:1 Refused to execute script from 'http://localhost:3000/vendor.js' because its MIME type ('application/json') is not executable, and strict MIME type checking is enabled.
localhost/:1 Refused to execute script from 'http://localhost:3000/main.js' because its MIME type ('application/json') is not executable, and strict MIME type checking is enabled.
4localhost/:13 GET http://localhost:3000/polyfills.js net::ERR_ABORTED 404 (Not Found)
For now I am trying to play with the sequence.ts file to re-send http routes in browser to the angular app like in loopback 3, any help would be appreciated.
Regards.
@mightytyphoon thank you for starting this discussion. Could you please create a small application showing what have you tried so far, to give us something to start playing with?
@bajtos Hi, yes sure, here is the repo to clone : https://github.com/mightytyphoon/lb4-ng-quickstart
(/!\ you will need nodemon : ng i -g nodemon /!)
I've made a readme to explain the configuration. It just deals with :
ng build --deploy-url /public/ if angular js files are in public folder (/public/ is not the folder but the urlpath) in public folder of loopback.ng build --output-path ../server/public --deploy-url /public/To make it easy, you just need to type npm install and it should install client and server for live reloading loopback. Then it will launch the app
git clone https://github.com/mightytyphoon/lb4-ng-quickstart
cd lb4-ng-quickstart
npm i
With the installation of loopback + angular + + the launch of ng serve + ng build watch + lb-tsc --watch + nodemon, there is some wait, but at the end it should work.
EDIT : for now it's just angular without router, I'll dig into it today.
I've added the routing, if the angular app is served on another server than loopback (in my repo localhost:4200) the routing works, which means serving the app only on a CDN and getting datas from the server should works.
Now the goal is to serve the angular app with loopback public folder and let it handle the routes. If you try to go directly to localhost:3000/contact it will not work as loopback will throw a 404 error and does not redirect the route to the angular app. But if you use the routerlink in the angular home page it will work.
Once this is done I will try to make a server side rendering of the angular app and it should be all good as it will cover all the classic use cases of angular (app served on other server as a browser client or a mobile app client, app served on same server and app served with server side rendering + bonus server side rendering + use of CDN)
It means angular should ignore the route going to the api (something like localhost:3000/api/* which should be where the loopback api will handle requests)
@bajtos Hi again, Actually to make it work, I just need to know how to configure loopback 4 to rewrite unmatched URLs to the index.html.
Something like a example.com/api url to handle all REST requests under the /api path
And then whatever is not example.com/api/* is rewritten to the public/index.html or the get('/') route.
It should also be possible to make it work with some rewrite rules and .htaccess but I think doing it with loopback directly is more 'clean'.
The loopback 3 equivalent was to catch all routes. in express it's something like this :
//configure the order of operations for request handlers:
app.configure(function(){
app.use(express.static(__dirname+'/assets')); // try to serve static files
app.use(app.router); // try to match req with a route
app.use(redirectUnmatched); // redirect if nothing else sent a response
});
function redirectUnmatched(req, res) {
res.redirect("http://www.mysite.com/");
}
...
// your routes
app.get('/', function(req, res) { ... });
...
// start listening
app.listen(3000);
Regards.
So, this code is working, I hope I'm not doing anything wrong with loopback 4 :
//in server/src/sequence.ts
async handle(context: RequestContext) {
try {
//first we try to find a matching route in the api
const {request, response} = context;
const route = this.findRoute(request);
const args = await this.parseParams(request, route);
const result = await this.invoke(route, args);
this.send(response, result);
} catch (err) {
//if there is an error, it means no route were matched
//we send the angular app to deal with the unmatched routes,
//if these routes are not matched either, angular app will handle their redirect
//context.response.sendfile('./public/index.html')
////updated from deprecated sendfile to sendFile
context.response.sendFile('public/index.html', {root: './'})
}
}
I'm just concerned with security problems, normally angular sanitize anything coming from the url and if no path is matched on the loopback side it's directly given to angular. So it should not be a security problem. What do you think ? A sent token could be ignored on server side maybe ?
I will do some tests to see if all is good with (REST requests) params, body, headers....
EDIT : res.sendfile is deprecated, I'm looking into using res.sendFile instead.
Thank you @mightytyphoon for the example app and further information. I am afraid I won't have time to look into this topic this week.
Here is an idea for you to consider: use LB4 as an API router only, create a top-level express app for dealing with your single-page routes & assets, and mount LB4 as a middleware in your express app.
See https://github.com/strongloop/loopback-next/issues/691#issuecomment-415736710
const mainApp = express();
const apiApp = new MyLb4Application();
mainApp.configure(function(){
mainApp.use('/api', apiApp.requestHandler);
mainApp.use(express.static(__dirname+'/assets'));
mainApp.use(redirectUnmatched);
});
function redirectUnmatched(req, res) {
res.redirect("http://www.mysite.com/");
}
mainApp.listen(3000);
The idea is to keep all your API endpoints under /api path, this path is fully owned by LB4 (including 404 responses). The rest of URL paths are handled by the top-level express app, where you can configure static assets, Angular virtual routes, etc.
I think this is the best option for now, until we figure out how to (re)design LB4 to support single page applications.
@bajtos Re,
I can understand you don't have time with the GA coming this month. Anyway I managed to make it work, so I think I'll try today to make a lb4-ng command or something in the sandbox on a fork.
this is actually working well :
// MY SEQUENCE : server/src/sequence.ts
async handle(context: RequestContext) {
try {
//first we try to find a matching route in the api
const {request, response} = context;
const route = this.findRoute(request);
const args = await this.parseParams(request, route);
const result = await this.invoke(route, args);
this.send(response, result);
} catch (err) {
//this.reject(context, err); <== change this line
context.response.sendFile('public/index.html', {root: './'})
}
}
I will dig into your solution, to see if there are better performances using it.
@mightytyphoon sounds good. I think your sequence is not going to correctly report non-404 errors e.g. validation errors returned when the request body is not a valid model instance.
To fix that, you should check if err.statusCode is 404.
if (err.statusCode === 404) {
context.response.sendFile('public/index.html', {root: './'})
} else {
this.reject(context, err);
}
In #1848, we reworked the way how static files are served from LB4 apps. We have a "catch-all" route that's invoked when no API endpoints matched the incoming request, and which invokes serve-static to try to handle the request as a static asset.
I can image we could build on top of this design and allow LB4 app developers to specify their own final route handler to invoke when the request did not match any known endpoint or static asset.
A mock-up to illustrate my idea:
app.finalRoute(context: RequestContext => {
context.response.sendFile('public/index.html', {root: './'});
});
Thoughts?
A good idea for sure, at a lower priority, IMO.
In #1848, we reworked the way how static files are served from LB4 apps. We have a "catch-all" route that's invoked when no API endpoints matched the incoming request, and which invokes
serve-staticto try to handle the request as a static asset.I can image we could build on top of this design and allow LB4 app developers to specify their own final route handler to invoke when the request did not match any known endpoint or static asset.
A mock-up to illustrate my idea:
app.finalRoute(context: RequestContext => { context.response.sendFile('public/index.html', {root: './'}); });Thoughts?
Yes this is perfect for a simple app serving.
Now for CDN, it's just about putting the angular/react/vue app on a CDN and make api calls from the app to the loopback server. It does not rely on Loopback.
And to finish the very interesting part is the server side rendering.
The project I'm on should be finished middle of february 2019 and it will need a server side rendering of pages also with a CDN for better performances, so I will give a try to make this package for loopback 4 if it's not done when the app I'm working on comes to its performance optimizations.
For now using sendFile does the trick and could even be enough in production for a webapp serving customers in a same region.
It's not the same if you want your page loaded in less than 1s worldwide. Then you'll need a server side rendering by loopback, different entities running (docker + kubernetes) and a CDN. But as @hacksparrow said it's a lower priority for the project which is in GA for a little more than a month now.
Also I think it does not totally rely on loopback dev team, it should be made by community because the team has already a lot to do with authentication, relations, db etc... and optimizations. That's why I propose to work on it when it comes to optimizations in February 2019. But I will be glad to help in any way between now and then.
Regards.
@mightytyphoon We are seeking examples that expand LB functionality from the community. We want to publish them in our docs as Community Contributions. Is it ok with you if we publish a link to: https://github.com/mightytyphoon/lb4-ng-quickstart as an example in loopback.io/docs?
Hi @bschrammIBM
yes of course, if all is working well, it was a really simple quickstart and I didn't do any test to check if it is still working but if it is still working with last loopback 4 version, you have my authorization.
I will check that later, if I can make a better quickstart repo.
I've had a tinker using express to host the loopback app: https://github.com/dougal83/lb4-ng-example (EDIT: link)
@mightytyphoon If you have time to review that would be cool. Your repo was a great help. Feel free to provide feedback or reuse.
@dougal83 nice, to be honest I prefer your solution. My next step is to use Angular Universal with loopback for server side rendering.
Regards.
This issue has been marked stale because it has not seen activity within six months. If you believe this to be in error, please contact one of the code owners, listed in the CODEOWNERS file at the top-level of this repository. This issue will be closed within 30 days of being stale.
This issue has been closed due to continued inactivity. Thank you for your understanding. If you believe this to be in error, please contact one of the code owners, listed in the CODEOWNERS file at the top-level of this repository.
Most helpful comment
In #1848, we reworked the way how static files are served from LB4 apps. We have a "catch-all" route that's invoked when no API endpoints matched the incoming request, and which invokes
serve-staticto try to handle the request as a static asset.I can image we could build on top of this design and allow LB4 app developers to specify their own final route handler to invoke when the request did not match any known endpoint or static asset.
A mock-up to illustrate my idea:
Thoughts?