Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using Angular , React , VueJS #1818

Closed
6 tasks
mightytyphoon opened this issue Oct 6, 2018 · 17 comments
Closed
6 tasks

Using Angular , React , VueJS #1818

mightytyphoon opened this issue Oct 6, 2018 · 17 comments

Comments

@mightytyphoon
Copy link

mightytyphoon commented Oct 6, 2018

Description / Feature proposal

Serving a web application with built-in router in 3 ways :

  • Serving the Web app from the loopback public folder and let the web application handles routes while cohabiting with http REST requests from the web application and websockets if needed.
  • Serving the Web app from a CDN while the loopback server handles its requests (should already work I think)
  • Serving the Web app with a server side rendering such as Angular Universal

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.

Current Behavior

Does not propose a default way to use a routed App such as Angular, React, Vuejs leading to 404 errors.

Expected Behavior

  • Having some documentation on managing web apps with built in router (Angular, React, VueJS)
  • Propose a default configuration in the CLI to handle automatically the needed changes.
  • Propose to reverse the configuration in case of error (later)

Some tries of mine

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.

@bajtos bajtos added the feature label Oct 8, 2018
@bajtos
Copy link
Member

bajtos commented Oct 8, 2018

@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?

@mightytyphoon
Copy link
Author

mightytyphoon commented Oct 8, 2018

@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 :

  • creating the angular app
  • use the deploy url option of ng build, for example : 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/
  • serving the static folder where transpiled .js are
  • having a controller to serve index.html (by default with lb4 command)
  • well, just launch the app.

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.

@mightytyphoon
Copy link
Author

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)

@mightytyphoon
Copy link
Author

mightytyphoon commented Oct 8, 2018

@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.

@mightytyphoon
Copy link
Author

mightytyphoon commented Oct 8, 2018

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.

@bajtos
Copy link
Member

bajtos commented Oct 9, 2018

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 #691 (comment)

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.

@mightytyphoon
Copy link
Author

@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.

@bajtos
Copy link
Member

bajtos commented Oct 9, 2018

@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);
}

@bajtos
Copy link
Member

bajtos commented Oct 30, 2018

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?

@hacksparrow
Copy link
Contributor

A good idea for sure, at a lower priority, IMO.

@mightytyphoon
Copy link
Author

mightytyphoon commented Oct 31, 2018

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?

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.

@bschrammIBM
Copy link
Contributor

bschrammIBM commented Feb 5, 2019

@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?

@mightytyphoon
Copy link
Author

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.

@dougal83
Copy link
Contributor

dougal83 commented Mar 6, 2019

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.

@mightytyphoon
Copy link
Author

@dougal83 nice, to be honest I prefer your solution. My next step is to use Angular Universal with loopback for server side rendering.
Regards.

@stale
Copy link

stale bot commented May 31, 2020

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.

@stale stale bot added the stale label May 31, 2020
@stale
Copy link

stale bot commented Jul 3, 2020

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.

@stale stale bot closed this as completed Jul 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants