How to decouple your Frontend development

A journey in Frontend Developlment

https://makeameme.org/meme/disclaimer-there-will-d173dcf663

https://www.reddit.com/r/ProgrammerHumor/comments/gt0tkj/frontend_vs_backend_developer/

...while doing

web projects...

... well, that is not done yet

Backend be like...

... well, that is not merged yet

... well, that is not deployed yet

... well, the dev server does not run yet

About me

  • Frontend Engineer since 2012
  • Built Frontends for several
    CMS & Commerce Systems
  • Senior FE at Valantic since 2021

Back in 2012

  • Gulp / Grunt Build Tools
  • Less CSS
  • JQuery
  • IE 6 / 7 / 8
  • IE10 released in Fall 2012

 

  • React yet to be born (2013)
  • VueJS yet to be born (2014)
  • Webpack yet to be born (2014)

https://arstechnica.com/information-technology/2012/04/internet-explorer-market-share-surges-as-version-9-wins-hearts-and-minds/

Hybris Ecommerce

Accelerator Demo Store

Hybris Accelerator

  • Java Server Pages (JSP)
  • Jquery (Function based)
  • Copy & Own
  • No Hot Reloading
  • Complicated Build Stack
  • Bad FE Quality

Problems

  1. FE built as static Templates
  2. FE built working directly with Hybris

Two Ways of working

Problems

  1. Slow and error-prone processs
  2. FE cannot see and test
  3. Work needs to be done twice
    (Bugfixing / Adjustments)

FE built as static

templates

https://makeameme.org/meme/i-dont-always-olwyx5

BACKEND'S

Problems

  1. FE needs to write templates in JSP
  2. FE needs to install hybris on
    their machines
  3. Branch switching time intense
    and a lot of Build Problems
  4. Does only work when Component
    in BE is already done

FE built working

directly with Hybris

https://www.reddit.com/r/ProgrammerHumor/comments/tgogft/sometimes_progress_looks_like_failure/

POV: FE when just wanting to
do Frontend things..

A few (FE) Decades later

https://www.reddit.com/r/webdev/comments/c9zqir/frontend_evolution_19952019/#lightbox

A few (FE) Decades later

https://gs.statcounter.com/browser-market-share/desktop/worldwide/2019

Meanwhile at SAP..

https://www.facebook.com/whatwaitwhat/

https://www.imdb.com/title/tt1442449/

Spartacus

https://www.linkedin.com/posts/mostafil_sap-commerce-spartacus-activity-6982974989041844224-oF1X/

Spartacus 

AKA Composable Storefront

  • Angular Application
  • Headless
  • Decoupled
  • API driven
  • Layer based
    • Core Layer
    • Service Layer
    • UI Components

 no more JSP

 no more Hybris

 no more waiting

Spartacus Demo Store

https://composable-storefront-demo.eastus.cloudapp.azure.com:543/electronics-spa/en/USD/

Backend

BUT

API Endpoint can be whereever

  • local SAP Commerce
  • development / staging environment

the missing piece

"Mock Server"

Text

Text

Mock Server

Spartacus Approach

  • local http Server
  • periodically fetch data and save
  • serve json from static file 
  • dropped in 2020
  • same problems since it would use data from dev environment

Our Approach

  • local express Server
  • data copied / built from Spartacus Demo Store API Endpoint
  • enhance / dynamize data with faker
  • define node express routes for every api call
  • define localhost as Backend API Url in Spartacus
  • enhance mock data for features on the go
  • Mostly working Backend including Add to Cart & Checkout

Some Code

// languages handler
server.get('/occ/v2/my-store/cms/languages', (_req, res) => {
   res.status(200).json({
    languages: [
      createLanguage(),
      createLanguage({
        isocode: 'de',
        name: 'German',
        nativeName: 'Deutsch',
      }),
      createLanguage({
        isocode: 'it',
        name: 'Italian',
        nativeName: 'Italiano',
      })
    ],
  });
});
const app: Express = express();
const serverHttp = http.createServer(app);

serverHttp.listen(portHttp, 
   (): void => console.log(`listening on http://localhost:${portHttp}`)
);
// pages handler
server.get('/occ/v2/my-store/cms/pages', (req, res) => {
   const url = getUrl(req);
   const pageLabelOrId = url.searchParams?.get('pageLabelOrId');
   const pageType = url.searchParams?.get('pageType');

   if (pageType === 'ContentPage' && pageLabelOrId) {
     // logic to return a content page
     res.status(200).json(contentPages()[pageLabelOrId]);
   } else if (pageType === 'ProductPage') {
     // logic to return product detail page
     const productCode = url.searchParams?.get('code');
     res.status(200).json(productDetailPage(productCode || ''));
   } else if (!pageType && !pageLabelOrId) {
     // logic to return the homepage
     res.status(200).json(homePage());
   } else {
     res.status(200).json(tempPage(pageType || 'ContentPage', pageLabelOrId || ''));
   }
 });

Works well..but..

  • separate sub project with dependencies
  • imports from Spartacus libs needed
  • imports from main Project needed
  • All or nothing Mocking approach
  • 2 separate terminals needed
  • Did not work anymore after a Spartacus Update (CommonJS - ESM related)

https://www.globalnerdy.com/2022/08/10/the-most-important-tech-skill-isnt-googling-for-answers-but-this/

Mock Service Worker

https://mswjs.io/

Mock Service Worker

  • Service Worker to return responses
  • integrated startup
  • No separate sub Project anymore
  • Easy on / off switch
  • Same Backend API URL
  • Partial Mocking
  • Passthrough URL's
  • Requests still visible
  • Hot Reload mock data
  • Easy Migration from previous solution
  • Easy Debugging of "Mock Code"

Setup

// mock.server/browser.ts
import { setupWorker } from 'msw';
import { handlers } from './handlers';

// This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers);


// main.ts
function prepare() {
  if (environment.mockServer) {
    const { worker } = require('./mock-server/browser');

    return worker.start();
  }
  return Promise.resolve();
}

// call angular bootstrap function after mock server is ready 
prepare().then(() => bootstrap());

Handlers

export const handlers = [
  rest.get('/occ/v2/my-store/cms/languages', 
           (_req: RestRequest, res: ResponseComposition, ctx: RestContext) => {
    return res(ctx.status(200), ctx.json(languages()));
  }),
  
  // cms pages call
  rest.get('/occ/v2/my-store/cms/pages', (req: RestRequest, res: ResponseComposition, ctx: RestContext) => {
    const pageLabelOrId = req.url.searchParams?.get('pageLabelOrId');
    const pageType = req.url.searchParams?.get('pageType');

    if (pageType === 'ContentPage' && pageLabelOrId) {
      return res(ctx.status(200), ctx.json(contentPages()[pageLabelOrId]));
    } else if (pageType === 'ProductPage') {
      // its a product detail page
      //const productCode = url.searchParams?.get('code');
      const productCode = '12345';

      return res(ctx.status(200), ctx.json(productDetailPage(productCode || '')));
    } else if (!pageType && !pageLabelOrId) {
      // its the homepage
      return res(ctx.status(200), ctx.json(homePage()));
    } else {
      return res(ctx.status(200), ctx.json(tempPage(pageType || 'ContentPage', pageLabelOrId || '')));
    }
  }),
]

Passthrough

// passthrough.ts
export const passThroughUrls: PassThroughUrl[] = [
  { url: '/assets/*', requestFunction: 'get' },
  { url: '*.woff2', requestFunction: 'get' },
  { url: '*.woff', requestFunction: 'get' },
  { url: '*.js', requestFunction: 'get' },
  { url: '*.css', requestFunction: 'get' },
  { url: '*.webp', requestFunction: 'get' },
  { url: '/site.webmanifest', requestFunction: 'get' },
  { url: 'https://www.googletagmanager.com/*', requestFunction: 'get' },
  { url: 'https://maps.gstatic.com/*', requestFunction: 'get' },
  { url: 'https://maps.googleapis.com/*', requestFunction: 'get' },
  { url: 'https://fonts.googleapis.com/*', requestFunction: 'get' },
];


// handlers.ts
export const handlers = [
  ...passThroughUrls.map((passThroughUrl) => {
    return rest[passThroughUrl.requestFunction](passThroughUrl.url, (req) => {
      return req.passthrough();
    });
  }),
]
  • Console warning for Request without handler

Path / Get Params

export const readUrlParams = (params: PathParams<string>, paramName: string): string => {
  return (params[paramName] as string) || '';
};

export const readSearchParams = (request: Request, param: string): string | undefined => {
  // Construct a URL instance out of the intercepted request.
  const url = new URL(request.url);

  // Read the "param" URL query parameter using the "URLSearchParams" API.
  return url.searchParams.get(param) || undefined;
};
  • Usecase: make your mock responses more dynamic

Redirect

export function redirect(destination: string, statusCode: number) {
  return new HttpResponse(null, {
    status: statusCode,
    headers: {
      Location: destination,
    },
  });
}

// usage
http.get('/occ/v2/media/:mediaId/16x9_1680/:mediaName', ({ request }) => {
  const urlArray = request.url.split('/');
  
  // equals to 16x9_1680, rendition is contained in second last path element
  const renditionName = urlArray[urlArray.length - 2];
  const ratio = renditionName.split('_')[0]; // equals to 16x9
  const width = parseInt(renditionName.split('_')[1], 10); // equals to 1200

  const ratioArray = ratio.split('x');
  const ratioNumber = parseInt(ratioArray[1], 10) / parseInt(ratioArray[0], 10);

  // Dynamic mock-images (Picsum)
  return redirect(`https://picsum.photos/${width}/${Math.ceil(width * ratioNumber)}.webp`, 301);
}),
  • Usecase: Redirect image requests to placeholder service

All problems solved?

no downsides from previous solution

  • still Copy and Own
  • Response Logic in the handlers is mostly the same for all Spartacus projects

BUT

Angular lib

  • separate Project... Again :-D
  • open source
  • published on npm
  • extract shared code
  • additional Features

https://github.com/valantic/spartacus-mock

Features

  • Quick Setup with Angular Schematics
  • Enable / disable default Data
  • add custom header / footer
  • add custom pages
  • add custom translation
  • working Cart (local storage)
  • createXxx Methods
  • inclusionMode

Final Setup

import { MockConfig } from '@valantic/spartacus-mock';
import { handlers } from './handlers';
import { translationResources } from './mock-data/translations/translations';
import { passThroughUrls } from './passThrough';
import { environment } from '@environments/environment';

const mockConfig: MockConfig = {
  enableDefaultData: false,
  inclusionMode: false,
  enableWorker: environment.mockServer || false,
  environment,
  passThroughRequests: passThroughUrls,
  handlers: handlers,
  translations: translationResources,
};

export async function prepareMockServer(): Promise<ServiceWorkerRegistration | undefined> {
  const { prepareMock } = await import('@valantic/spartacus-mock');

  return prepareMock(mockConfig);
}
export const environment: Environment = {
  env: 'dev-fe',
  production: false,
  cookieSecret: 'some-supersecret',
  smartEdit: '*.my-store.ch',
  mockServer: true,
  backend: {
    occ: {
      // baseUrl: 'https://api.my-store.ch',
      baseUrl: 'https://api-staging.my-store.ch',
      // baseUrl: 'https://api-dev.my-store.ch',
      prefix: '/occ/v2/',
    },
  },
};

How does it look

Experiences

  • Works well
  • used a lot at the begin of a project
  • used when new feature does not exist yet
  • used for certain edge cases
  • used for error cases
  • can be used with existing projects
    (inclusionMode)

(still) need to talk with BE ;-)

Gotchas

  • prevent too many function calls



     
  • make sure, mock code does not land in production code
  • MSW works library agnostic,
    but currently does NOT work well with nextJs (Yet)
const headerSlots = getHeaderSlots();
const footerSlots = getFooterSlots();
const pages = contentPages(headerSlots, footerSlots);

Final thoughts

  • Open Source development
  • (Angular) Library development
  • Knowhow through Reverse Engineering

Questions?

Questions?

Questions?

Made with Slides.com