Skip to content

Hybrid Approach

Danilo Hoffmann edited this page Mar 12, 2020 · 32 revisions

For task tracking and current status see GitHub Project.

Motivation

What is the hybrid approach?

In short it means running Angular and classic storefront in parallel and switching between both storefronts seamlessly.

For example a customer can start shopping in the new Angular PWA and when he starts checking out, the functionality of the classic storefront takes over. After completing the checkout the customer can continue shopping in the Angular storefront.

Why do we need this feature?

As the Intershop PWA is a relatively new project of Intershop, not all features are yet supported compared to the classic Responsive Starter Store. Especially advanced B2B features will require more development time as REST APIs are required and sometimes not yet available. This way projects could use the available features of the PWA and add additional functionality of the classic storefront in parallel.

Also, migration projects could consider moving available functionality to the Angular storefront and leaving highly customized features in the realm of ISML and Pipelines. Migrations to the PWA can be started as small projects and incrementally be expanded as more and more functionality is moved to Angular code.

Status Quo

Current Deployment of Angular Storefront

Our current deployment works like this:

Current Deployment

  1. The Browser requests the page by URL from the nginx. (If cached, the response is returned immediately, go to 6)

  2. If the requested page is not cached, the SSR process is triggered. The node express.js server runs Angular Universal.

  3. Angular Universal fillst the requested page with content retrieved via ICM REST API.

  4. The response is delivered to nginx, where it is also cached.

  5. The response is delivered to the browser.

  6. The Angular Client Application is started up in the browser.

  7. Once booted up, additional REST Calls are directed straight to the ICM and the PWA acts as a single-page application. No further HTML pages are requested.

Current deployment of Responsive Starter Store

The Responsive Starter Store acts like a typical JSP-Server. Complete pages are retrieved from the server almost every time. This means that the server roundtrip is executed and it can be redirected by a forward proxy quite easily.

Requirements

Functional

Given the following example: The shopping experience should be handled by the PWA and MyAccount functionality should be handled by the ICM. Sessions are synchronized and remembered between the two systems (no further login required).

  1. It should be possible to redirect from PWA to ICM.

    i.e. User is shopping and navigates into MyAccount section.

  2. It should be possible to redirect from ICM to PWA.

    i.e. User is in MyAccount section and clicks on a product link on order.

  3. Initial link to MyAccount should land at ICM.

    i.e. User bookmarked link to MyAccount and navigates directly to it.

  4. Initial link to Product should land at PWA.

    i.e. User bookmarked link to product on order in MyAccount and navigates directly to it.

Non-Functional

  • It should be easily maintainable.

  • It should not interfere as much with other features

Discussion of Possible Solutions

Prerequisites

Is it possible to break out of the PWA and redirect to a different URL?

Yes, be using href links on the HTML template. It is also possible to do it programmatically with Location.assign().

  @Effect({ dispatch: false })
  redirectToICM$ = this.router.events.pipe(
    filter(() => isPlatformBrowser(this.platformId)),
    filter(event => event instanceof NavigationStart),
    map((event: NavigationStart) => event.url),
    withLatestFrom(
      this.store$.pipe(select(getICMChannel)),
      this.store$.pipe(select(getICMApplication)),
      this.store$.pipe(select(getCurrentLocale))
    ),
    tap(([url, channel, application, locale]) => {
      const base = `/INTERSHOP/web/WFS/${channel}/${locale.lang}/${application}/${locale.currency}/`;
      if (url === '/home') {
        location.assign(base + 'Default-Start');
      }
    })
  );

How would one decide to PWA or ICM depending on the URL?

This should be doable by a switch in any reverse proxy of our current deployment. We have:

  • nginx (via static configuration)
  • express.js (via framework with nodejs)

For express.js it would look like this:

// PWA -> ICM (initial link)
app.use('*', (req, res, next) => {
  if (req.originalUrl.startsWith('/home')) {
    res.redirect('/INTERSHOP/web/WFS/inSPIRED-inTRONICS-Site/en_US/-/USD/Default-Start');
  } else {
    next();
  }
});

...
const icmProxy = proxy(ICM_BASE_URL, {...
...

// ICM -> PWA (both initial and subsequent links)
app.use('/INTERSHOP', (req, res, next) => {
  const product = /([^\/]+)\/([^\/]+)\/[^\/]+\/[^\/]+\/ViewProduct-Start.*SKU=(.+?)(\&|$)/.exec(req.originalUrl);
  if (product) {
    const channel = product[1];
    const lang = product[2];
    const sku = product[3];

    res.redirect(`/product/${sku};channel=${channel};lang=${lang};redirect=1`);
  } else {
    icmProxy(req, res, next);
  }
});

Running ICM with HTTPS only possible?

Yes, add SecureAccessOnly=true to SERVER/share/system/config/cluster/appserver.properties.

See Secure URL-only Server Configuration.

Synchronize Basket, User, ... ?

Available with 7.10.16.6. Concept - Integration of Progressive Web App and inSPIRED Storefront

server/share/system/config/cluster/appserver.properties

intershop.apitoken.cookie.enabled=true
intershop.apitoken.cookie.sslmode=true

Solution #1 - rejected

We could use the URL Rewriting feature of the ICM to rewrite URLs, that should be handled by the PWA in the PWA format. A reverse proxy (one of the current ones) then handles the decision to either route to the PWA or to route to the ICM. This way one direction is clear.

If the current user is in the PWA realm and wants to switch to the ICM realm, an additional service could set hard references to the ICM with browser APIs and leave the PWA.

With this solution no additional architecture is needed. However, routes must always be synchronized between the two implementation points. Also, there are always two different language or configuration formats the rules have to be synchronized. This does not look like an easy maintainable solution.

Solution #2 - approved

Possible Solution

The main player in this solution is an additional mapping table that is used by both the Angular client application as well as the express.js reverse proxy. In here it is encoded how to transform URLs from PWA into ICM pattern and vice versa. This table also defines which pages are handled by which system.

As a single source of truth for this concern, it can be used in the Angular router to decide when to leave the PWA and it is called as the main switch in the express.js server to decide which service to delegate to (either via redirect or as proxy).

This way the ICM also does not need to know about its PWA counterpart.

Mapping Table Requirements

Can every URL uniquely be transformed from PWA to ICM pattern and vice versa?

ICM PWA
Home /INTERSHOP/.../Default-Start /home
PDP /INTERSHOP/.../ViewProduct-Start?SKU=7967589 /product/7967589
Category /INTERSHOP/.../ViewStandardCatalog-Browse?CatalogID=Cameras-Camcorders&CategoryName=Cameras-Camcorders /category/Cameras-Camcorders
/INTERSHOP/.../ViewStandardCatalog-Browse?CatalogID=Home-Entertainment&CategoryName=1584 /category/Home-Entertainment.220.1584
Login /INTERSHOP/.../ViewUserAccount-ShowLogin /login

converting application-specific data

/INTERSHOP/web/WFS/inSPIRED-inTRONICS-Site/en_US/-/USD/ViewProduct-Start?SKU=8182790134363

-> /product/8182790134363;channel=inSPIRED-inTRONICS-Site;lang=en_US;application=-;redirect=1

mapping table

const ICM_CONFIG_MATCH = `^/INTERSHOP/web/WFS/(?<channel>[\\w-]+)/(?<lang>[\\w-]+)/(?<application>[\\w-]+)/[\\w-]+`;
const PWA_CONFIG_BUILD =
  ";channel=$<channel>;lang=$<lang>;application=$<application>;redirect=1";

const table = [
  {
    id: "Home",
    icm: `${ICM_CONFIG_MATCH}/(Default-Start|ViewHomepage-Start).*$`,
    pwaBuild: `home${PWA_CONFIG_BUILD}`,
    pwa: `^/home.*$`,
    icmBuild: `ViewHomepage-Start`,
    handledBy: "pwa"
  },
  {
    id: "Product Detail Page",
    icm: `${ICM_CONFIG_MATCH}/ViewProduct-Start.*(\\?|&)SKU=(?<sku>[\\w-]+).*$`,
    pwaBuild: `product/$<sku>${PWA_CONFIG_BUILD}`,
    pwa: `^.*/product/([\\w-]+).*$`,
    icmBuild: `ViewProduct-Start?SKU=$1`,
    handledBy: "icm"
  }
];

Format given with regular expressions using named capture groups. So one regex for matching the URL and one string for routing to that URL.

Cases

  • incoming ICM must be routed to PWA
    • default parameters from url
    • specific parameters from url
    • redirect
  • incoming PWA must be routed to ICM
    • default parameters from environment
    • specific parameters from url
    • redirect
  • PWA must route to ICM
    • default parameters from state
    • specific parameters from url
    • Location.assign
  • incoming ICM must be routed to ICM
    • do nothing
  • incoming PWA must be routed to PWA
    • do nothing

Setup for Testing

Everything HTTPS!!

ICM

docker run -d --restart always --tty \
           --name intershop7-devenv-hybrid \
           --publish 5322:8081 \
           --publish 5323:8444 \
           --publish 5324:8025 \
           --env HOST=jxdhoffmann.dhcp.j.ad.intershop.net \
           --env HTTP_PORT=5322 \
           --env HTTPS_PORT=5300 \
           --env SECURE_ONLY=true \
           registry.intershop.de/ispwa/intershop7-devenv/master/a_responsive
  • ICM must point to nginx host, in this case https://jxdhoffmann.dhcp.j.ad.intershop.net:5300 (set intershop.WebServerSecureURL in server/share/system/config/cluster/appserver.properties). This is used for generating links, where the incoming host cannot be determined.
  • ICM must run in secure-only mode (see SecureAccessOnly)

Universal Server

ICM_BASE_URL=https://jxdhoffmann.dhcp.j.ad.intershop.net:5323 TRUST_ICM=true SSR_HYBRID=true LOGGING=true SSL=true PORT=5301 npm run start
  • ICM_BASE_URL points to ICM https port.

    • this implies (for demo scenarios with a self-signed certificate) that you have to set TRUST_ICM=true

nginx

docker build -t my_nginx nginx && \
docker run -it -p 5300:443 \
           -e UPSTREAM_PWA=https://jxdhoffmann.dhcp.j.ad.intershop.net:5301 \
           -e PWA_1_SUBDOMAIN=b2c \
           -e PWA_1_CHANNEL=inSPIRED-inTRONICS-Site \
           -e PWA_1_THEME=blue \
           -e PWA_1_LANG=de_DE \
           -v <full-path-to>/dist/server.crt:/etc/nginx/server.crt \
           -v <full-path-to>/dist/server.key:/etc/nginx/server.key \
           my_nginx:latest

Testing with ICM URL Rewriting

  • Setup URL Rewriting: see Cookbook - URL Rewriting, recipe 6 Develop and Debug URL Rewriting.

  • edit ./server/share/system/config/cluster/domainsplittings.xml to include your hostname.

Now some default rewrites are active. Interesting for Testing:

Express.js has to be set up to proxy routes to ICM:

  • example for Category/Family Pages and PDP:
diff --git a/server.ts b/server.ts
index 92c88b7..102f850 100644
--- a/server.ts
+++ b/server.ts
@@ -215,6 +215,8 @@ if (process.env.SSR_HYBRID) {
 if (process.env.PROXY_ICM || process.env.SSR_HYBRID) {
   console.log("making ICM available for all requests to '/INTERSHOP'");
   app.use('/INTERSHOP', icmProxy);
+  app.use(/^\/(Computers|Cameras-Camcorders|Home-Entertainment|Specials).*$/, icmProxy);
+  app.use(/^\/.*-zid.*/, icmProxy);
 }
  • additional routes for Content Pages (i.e. /helpdesk) would also have to be defined

nginx must be configured to pass through those routes, not adding multi channel configuration to them:

diff --git a/nginx/channel.conf.tmpl b/nginx/channel.conf.tmpl
index 478ab13..b02e517 100644
--- a/nginx/channel.conf.tmpl
+++ b/nginx/channel.conf.tmpl
@@ -2,8 +2,17 @@ server {
     server_name ~^$SUBDOMAIN\..+$;
     include /etc/nginx/conf.d/listen.conf;

-    # let ICM handle everything ICM related
-    location ~* ^/INTERSHOP.*$ {
+    location ~* ^/.*-zid.*$ {
+        proxy_set_header Host $http_host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        add_header X-Cache-Status IGNORE;
+
+        proxy_pass $UPSTREAM_PWA;
+    }
+
+    location ~* ^/(INTERSHOP|Computers|Cameras-Camcorders|Home-Entertainment|Specials).*$ {
         proxy_set_header Host $http_host;
         proxy_set_header X-Real-IP $remote_addr;
         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

Mapping table has to be adapted:

diff --git a/src/hybrid/default-url-mapping-table.ts b/src/hybrid/default-url-mapping-table.ts
index 1d81019..4272707 100644
--- a/src/hybrid/default-url-mapping-table.ts
+++ b/src/hybrid/default-url-mapping-table.ts
@@ -25,7 +25,7 @@ export interface HybridMappingEntry {
  *  - application
  *  - currency
  */
-export const ICM_WEB_URL = '/INTERSHOP/web/WFS/$<channel>/$<lang>/$<application>/$<currency>';
+export const ICM_WEB_URL = '';

 /**
  * Mapping table for running PWA and ICM in parallel
@@ -42,7 +42,8 @@ export const HYBRID_MAPPING_TABLE: HybridMappingEntry[] = [
   {
     id: 'PDP',
     icm: `${ICM_CONFIG_MATCH}/ViewProduct-Start.*(\\?|&)SKU=(?<sku>[\\w-]+).*$`,
-    icmBuild: `ViewProduct-Start?SKU=$<sku>`,
+    // icmBuild: `ViewProduct-Start?SKU=$<sku>`,
+    icmBuild: `product-zid$<sku>`,
     pwa: `^.*/product/(?<sku>[\\w-]+).*$`,
     pwaBuild: `product/$<sku>${PWA_CONFIG_BUILD}`,
     handledBy: 'icm',

Open Concerns/Questions

  • sitemap.xml handling must build on top of this (see #35)
  • E-Mail links should also be redirected correctly ✔️
  • ✔️ Should this mapping table also be used in the Angular routing?
    • routes would not have to be managed twice.
    • routing will become more difficult for ALL PWAs, even the ones not going hybrid. --> No, it will be an additional functionality
  • Configuration Nightmare?!
    • Multi-Site & Multi-Theme ✔️
    • ICM available through nginx (we'll probably remove this one) ✔️
    • nginx does caching and pagespeed optimizations -> disable for ICM! ✔️
  • URL Rewriting
    • ICM routes can be customized ✔️
    • PWA routes can be customized #11 ⚠️
    • How far are we going with this? Usually rewritten URLs are only important for SEO, so covering the Shopping Experience would be enough? -> Documentation should be enough ✔️
  • Proxy vs. Redirect
    • redirect might reduce SEO ranking of pages, but if used on non-indexed sites, effect would be irrelevant
    • proxy impl possible (i.e. showing ICM pages while URL in browser is PWA), but more complicated
    • -> we will stick with redirects ✔️
  • Named Capture Groups are not yet widely supported 😭 https://github.com/tc39/proposal-regexp-named-groups#implementations
    • -> Instructions for parsing pwa routes and building icm routes mustn't use this feature (done in PWA, won't work for Firefox, checked via test) ✔️
    • SSR rewriting can use this feature, as node.js supports it
  • redirecting search from ICM to PWA (via search field) seems not (easily) possible as this is done via POST requests and not simple URL query parameter. [INTERNAL IS-29359]
  • logging out ín ICM does not answer with a redirect to homepage, so it cannot be reacted on in SSR. -> Ticket in ICM Backlog [INTERNAL IS-29358]
  • with default ICM URLs it is not yet possible to transform URLs from Catalog@Category to CategoryPath. We need a REST API to retrieve this information. -> will come in ICM later [INTERNAL IS-27466]

Implications/Limitations of solution

  • It seems that breaking out of the PWA into the ICM does not work with an installed service worker, as it wants to handle all routing on that domain and links itself into the browser.