A few months ago, just before UI5con I think, SAP released the official productive version of the new UI5 tools. This was of course a big topic at UI5con with several sessions about it. During the summer I found some time to check out this new toolset and give it a try in Visual Studio Code. I immediately faced one of the basic challenges, I was able to test my app but couldn’t access my OData service. This is normally taken care of the SAP Web IDE...
After some investigation, I found two possible solutions. One is better then the other but currently, you’ll probably need them both:
- Proxy server that forwards the request of your app to the UI5 server and the other to your backend server
- A UI5 CLI Server Proxy extension: The new tool allows you to extend the server middleware which made it possible intercept the requests from the client and redirect the OData requests to your backend.
In the OpenSAP course, SAP used the npm module CORS anywhere:
https://www.npmjs.com/package/cors-anywhere . They showed it in the OpenSAP Course Evolved webapps:
https://open.sap.com/courses/ui52/items/5u41osEJNuR54XkpeJHMG7 . I didn’t use this one because of the following reasons:
- It requires to change the path in the manifest.json of your app when using the proxy. This means that you always need to change it before deploying to your system or change this in your CI setup.
- I also was not able to call a service with authentication but that could just be me…
Nevertheless, I think these proxy options are way better because you don’t need to change anything to deploy the app to the system.
Option 1: Proxy server
This option will host a proxy server by using express. It requires the node modules “http-proxy” and “express”. Define these modules as devDependencies in the package.json and run “npm install” to use them”.
Next to that, add the script to start the proxy script.
Add proxy in the root folder of your project:
And add the following code:
This code will do the following:
- Load all the required npm modules
- Start a proxy
- Define route to the host were the UI5 app is running
- Load routes to backend services from another config file “odata.json”
- Intercept all the requests from the hosted proxy and forward it to the right route
- It will use the route in the config file if it matches the pattern of the request url
- Otherwise it will use the default app host
- Run an express server for the proxy
Full code:
const express = require('express'),
httpProxy = require('http-proxy'),
fs = require('fs'),
proxy = new httpProxy.createProxyServer();
const appRoute = {
target: 'http://localhost:5000'
};
const routing = JSON.parse(fs.readFileSync('./odata.json'));
var allowCrossDomain = function(req, res) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'X-Requested-With, Accept, Origin, Referer, User-Agent, Content-Type, Authorization, X-Mindflash-SessionID');
// intercept OPTIONS method
if ('OPTIONS' === req.method) {
res.header(200);
} else {
var dirname = req.url.replace(/^\/([^\/]*).*$/, '$1');
var route = routing[dirname] || appRoute;
console.log(req.method + ': ' + route.target + req.url);
proxy.web(req, res, route);
}
};
var app = express();
app.use(allowCrossDomain);
app.listen(8005);
console.log("Proxy started on http://localhost:8005");
Additional config file with the backend system properties:
Each route requires a part of the request like for example if you have a request “/opu/odata/sap/ZGW_UI5CON_SRV/”, the route should be defined with “opu”, “odata” or like I did “SAP”. The route requires at least a target but also allows you to add authentication “username:password” :
The app with the odata service can now be tested by running the UI5 app with the UI5 CLI:
UI5 serve
Next to that, it also requires to run the proxy.
Npm run proxy
Small remark: in case that UI5 serve uses a different port, you should change this port in the appRoute object of the proxy.
Open the proxy host in the browser and you’ll see the app with the data!
This setup can be improved with the npm module “npm-run-all”. This allows you to start the UI5 host and proxy host together. (don’t forget to run “npm install” after adding the devDependency)
Option 2: UI5 Server Extension
As I was digging deeper into the extensibility of the UI5 tooling I found a way nicer solution. With the middleware extenstion of the UI5 server, you can add your own logic. This means that the logic that’s been hosted by the express server can be implemented in the UI5 server. You can find more information about these extensions in the UI5 Tooling documentation:
https://sap.github.io/ui5-tooling/pages/extensibility/CustomServerMiddleware/
First step, define the extension for the server in the "ui5.yaml" file with the following settings:
- Give it a name
- Use it in the definition and in the implementation part
- Set it to run before the “serveResources”
- kind and type are always the same for a middleware extension
- Add a path to your implementation
Create the file for your implementation and add the same logic as in the other proxy. The only differences are:
- It doesn’t need a default app route
- Also, no express server is needed
- If it doesn’t uses the proxy, you need to use the run() function.
In the end, it only requires the same logic that was used in the express interceptor.
Full code:
module.exports = function ({
resources,
options
}) {
const fs = require('fs');
const httpProxy = require('http-proxy');
const proxy = new httpProxy.createProxyServer();
const odata = fs.readFileSync('./odata.json');
const routing = JSON.parse(odata);
return function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'X-Requested-With, Accept, Origin, Referer, User-Agent, Content-Type, Authorization, X-Mindflash-SessionID');
// intercept OPTIONS method
if ('OPTIONS' === req.method) {
res.header(200);
console.log(req.method + '(options): ' + req.url);
next();
return;
}
var dirname = req.url.replace(/^\/([^\/]*).*$/, '$1'); //get root directory name eg sdk, app, sap
if (!routing[dirname]) {
console.log(req.method + ': ' + req.url);
next();
return;
}
console.log(req.method + ' (redirect): ' + routing[dirname].target + req.url);
proxy.web(req, res, routing[dirname], function (err) {
if (err) {
next(err);
}
});
}
};
It uses the same config as the other proxy for the backend systems:
You can use this by just using the UI5 tooling and run “ui5 serve”
It will have the same result as the first option but only one server will be hosted:
Conclusion
I think we all agree that the second option is the best! But, we will also need the first one… Why? Just in case you want to test the result of the build and directly run the app from the “dist” folder. Putting this all together.
Testing webapp folder:
- Just use the UI5 tools
- This will run the proxy inside the UI5 server
Testing the dist folder
- Combine npm module “serve” in combination with the proxy or run it as one command
- Don’t forget to add the “serve” module to your devDependencies + npm install
- The serve module uses a different port which needs to be changed in the config of the proxy:
- You can now run the app from the dist folder with proxy with only one command:
Hopefully UI5 will also support to serve the dist folder in the future and we can use the same extension for the webapp and dist folder.
Tip
We could also add the build process to the start command:
This will run build and after that serve the app together with the proxy
Full project:
https://github.com/lemaiwo/UI5ToolsExampleApp