88

Max Voloshin - "Organization of frontend development for products with microservices"

Embed Size (px)

Citation preview

Max Voloshin

● Location: Dnipro, Ukraine● Position: Team Lead, OWOX● In professional software development since 2010● Fan of microservices and continuous deployment

Photographer: Avilov Alexandr

OWOX Business Intelligence

As business:

● More than 1 million transactions per week in our clients' projects● 1 000+ projects in 50 countries rely on our solutions● $ 200 000+ daily handled our clients' costs on advertising

As software:

● 10+ microservices in PHP/Java● Web Components (Polymer 1.8)● Several releases every day

Why am I here?

I want to share 9 improvements

for making frontend development easier

Who cares?

Frontend developmentmay be a constraint for your product

Let’s improve!

#1

Let frontend developerdon't deal with content management

#1

<div class="e-wrap"> <div class="e-grid-col1 e-grid-col-empty"></div> <div class="e-grid-col8" role="main"> <h2 class="e-page-title"> Page not found </h2> <p class="error-message"> Incorrectly typed address or a page on the website no longer exists </p> <p class="error-link-wrap"><a href="/" class="error-link"> Home </a></p> </div> <div class="clear"></div></div>

Do you really wantto change one selling text to another?..

#1

Use mnemonic names of textsinstead of hard-coded content

#1

Admin panelfor content

Write contentContentmanager

Frontenddeveloper Frontend

Write code

Matched by mnemonic name

#1

Polymer<div class="e-wrap"> <div class="e-grid-col1 e-grid-col-empty"></div> <div class="e-grid-col8" role="main"> <h2 class="e-page-title"> <html-text name="NotFoundPage.Title"></html-text> </h2> <p class="error-message"> <html-text name="NotFoundPage.Message"></html-text> </p> <p class="error-link-wrap"><a href="/" class="error-link"> <html-text name="HomePage.Title"></html-text> </a></p> </div></div>

Bonus: easy internalization

#1

#2

Let frontend developeruse HTML/JS templating

#2

.answers-i-link:before { CSS as Smarty (PHP) template content: ""; position: absolute; left: -30px; width: 25px; height: 25px; background: url('/*{$settings.path}*//icons.png') 0 -414px no-repeat; } .answers-i-link:hover:before { background: url('/*{$settings.path}*//icons.png') 0 -373px no-repeat; } .answers-i-link.active:before { background: url('/*{$settings.path}*//icons.png') 0 -700px no-repeat; } .answers-i-link.active:hover:before { background: url('/*{$settings.path}*//icons.png') 0 -659px no-repeat; }

{foreach $tree as $section} HTML as Smarty (PHP) template <h3 class="level-{$level}">{$section['title']}</h3> {if $section['children']} {include file='faq/subjects.tpl' tree=$section['children'] level=$level+1} {/if} {if isset($section['records'])} {foreach $section['records'] as $record} {if $is_contents} <a href="#record-{$record.id}" class="level-{$level}">{$record.title}</a><br /> {else} <a name="record-{$record.id}" class="level-{$level}">{$record.title}</a><br /> {$record['answer'] nofilter} {/if} {/foreach} {/if}{/foreach}

HTML as Smarty (PHP) template

<div class="b-expenses-head"> {foreach from=$manual_costs item="month" name="tabs"} <div id="manual-costs-tab-{$smarty.foreach.tabs.index}" class="inline b-expenses-head-month {if $month['is_active']}active{/if}"> <span class="b-expenses-month-title">{$month['month_title']}</span> </div> <script type="text/javascript"> ManualCosts.addGroup(new ManualCostsTab_class({$smarty.foreach.tabs.index}, '{$month['hash']}')); </script> {/foreach}</div>

JS as Smarty (PHP) templateinitialize: function(data, connect_service_access) { this.parent(data, connect_service_access); Object.append( this._templates, {access_create: '/*{template_script_fetch file="access_create.jst" }*/'} ); this._popup.addEvent('createAccess', this.onCreateAccess.bind(this));},onCreateAccess: function() { this.send( '/*{$menu.my.href}*/users/access/simple_services#createAccess', { service_name: this._data['service_name'], token: token, service_account: service_account } );}

Do you really want to learn and debugnon JS/HTML templating?..

#2

Use HTML/JS templating,use data as JSON

#2

Polymer

<template is="dom-if" if="[[!isEmpty(url)]]"> <page-item item-action> <a is="pushstate-anchor" href="[[url]]" class="item-action"> <text-html name="Page.Item.Actions.Edit"></text-html> </a> </page-item> </template>

Polymer <dom-module id="page-external-link-styles"> <template> <style include="shared-styles"> :host { display: inline; white-space: nowrap; }

:host(:not(.no-link-style)) a { color: var(--blue-color-main); text-decoration: none; } </style> </template> </dom-module>

Bonus: static code analysis & tests

#2

#3

Let frontend developerdon't deal with backend routing

#3

Symfony (PHP) routing

# app/config/routing.yml

blog_list:

path: /blog/{page}

defaults: { _controller: AppBundle:Blog:list, page: 1 }

requirements:

page: '\d+'

blog_show:

# ...

Do you really want to learn and debugbackend routing?..

#3

Use frontend routingvia Single Page Application

#3

Polymer <app-router id="appRouter" mode="pushstate" init="manual"> <app-route id="dataPipeline" path$="[[globals.pathPrefix]]/:context/pipeline/" element="pipeline-page" import="/components/pages/pipeline/pipeline-page.html" ></app-route> <app-route id="costDataPage" path$="[[globals.pathPrefix]]/:context/pipeline/:property/:id/" redirect="history/" ></app-route> <app-route id="costDataHistoryPage" path$="[[globals.pathPrefix]]/:context/pipeline/:property/:id/history/" element="cost-data-page" import="/components/pages/cost-data/cost-data-page.html" ></app-route></app-router>

#4

Let frontend developerdon't deal with stagnated framework

#4

Switching framework is hard

● You can’t pause product development● You can't release all changes at one time

#4

Two approachesfor framework switching

#4

1. Continuous deploythen single release

#4

UI v1 (MooTools) → UI v2 (Polymer 0.5)

t

Release v1

Deploy v2 (part #1)

Change v1

prod

Deploy v2(part #2)

Release v2Deploy v2(part #N)

...

Deploy != Release

#4

UI v1: https://domain.com/UI v2: https://domain.com/v2/ ← SPA root

Continuous deploy then single release

+ Compatible with design switching

- Suitable only for small applications because of effort duplication

#4

Off topic: why Polymer?

● Material Design out of the box● Possibilities to UI reuse (not only JS)● Google promotion of Web Components● #UseThePlatform

#4

2. Page-by-page release

#4

The first law of holes

#4

Limitations:

1. Only one version of Polymer can be on page2. We have Single Page Application

Solution: “break points” in Single Page Application

UI v2 — domain.com/v2/ UI v3 — domain.com/v3/

/foo//bar//baz//qux/

/foo//bar//baz//qux/

UI v2 (Polymer 0.5) → UI v3 (Polymer 1.8)#4

UI v2<app-router id="router" mode="pushstate" init="manual" <app-route id="pipeline" path="/ui/:context/pipeline/" import="{{$.globals.values.base_url}}pipeline-page/pipeline-page.html" ></app-route> <app-route id="cost_details" path="/ui/:context/pipeline/:property/:id/" redirect="history/" ></app-route> <app-route path="/ui/:context/pipeline/:property/:id/history/" import="{{$.globals.values.base_url}}cost-page/cost-page.html" new-page ></app-route></app-router>

<app-router id="appRouter" mode="pushstate" init="manual"> UI v3 <app-route id="pipeline" path$="[[globals.pathPrefix]]/:context/pipeline/" element="pipeline-page" import="/components/pages/pipeline/pipeline-page.html" ></app-route> <app-route id="costPage" path$="[[globals.pathPrefix]]/:context/pipeline/:property/:id/" redirect="history/" ></app-route> <app-route id="costHistoryPage" path$="[[globals.pathPrefix]]/:context/pipeline/:property/:id/history/" element="cost-data-page" import="/components/pages/cost-data/cost-data-page.html" old-page ></app-route></app-router>

+ Suitable for any size applications

- Only same design

- Time delays between UI transitions

Page-by-page release#4

#5

Let frontend developerdeliver UI independently

#5

Delivery of UI with “monorepo” approach

1. Make changes2. Wait till the whole monorepo can be deployed 3. Deploy

#5

Use standalone repositoryand continuous delivery pipeline

#5

Delivery of UI with “standalone repo” approach

1. Make changes2. Wait till the whole monorepo can be deployed3. Deploy

#5

#6

Let frontend developeruse remote backend

#6

UI changes with “local backend” approach

1. Update backend source with dependencies2. Update storage data3. Update environment4. Try to figure out via guide/FAQ why backend is not working5. Call backend developer who knows how to update this 6. Spend half hour together to find stupid environment problem7. Repeat 1-6 for each microservice8. Make changes to frontend

#6

UI changes with “remote backend” approach

1. Update backend source with dependencies2. Update storage data3. Update environment4. Try to figure out via guide/FAQ why backend is not working5. Call backend developer who knows how to update this 6. Spend half hour together to find stupid environment problem7. Repeat 1-6 for each microservice8. Make changes to frontend

#6

request identity token

How to handle authorization?

identity token (JSON Web Token)

Backend

Attach header to each further requestAuthorization: Bearer {identity token}

Frontend

#6

#6

#6

#6

#6

#6

#6

jwt.ioJSON Web Tokens libraries and debugger

#6

#7

Let frontend developerdon't deal with CORS

#7

#7

UI – https://domain.com/

https://foo.appspot.com/ https://bar.heroku.com/

Cross-origin resource sharing ?

#7

CORS (Cross-origin resource sharing)

● Request headers○ Origin○ Access-Control-Request-Method○ Access-Control-Request-Headers

● Response headers○ Access-Control-Allow-Origin○ Access-Control-Allow-Credentials○ Access-Control-Expose-Headers○ Access-Control-Max-Age○ Access-Control-Allow-Methods○ Access-Control-Allow-Headers

#7

Use your web server’s abilityto serve UI and microservices

from single domain

#7

server {

server_name domain.com;

location /api/foo/ {

proxy_pass https://foo.appspot.com/;

}

location /api/bar/ {

proxy_pass https://bar.heroku.com/;

}

}

Nginx example#7

#8

Let frontend developeruse production web server

without digging into its configuration

#8

Nginx

location /download/ {

valid_referers none blocked server_names *.example.com;

if ($invalid_referer) {

#rewrite ^/ http://www.example.com/

return 403;

}

# rewrite_log on;

# rewrite /download/*/mp3/*.any_ext to /download/*/mp3/*.mp3

rewrite ^/(download/.*)/mp3/(.*)\..*$ /$1/mp3/$2.mp3 break;

root /spool/www;

#autoindex on;

access_log /var/log/nginx-download.access_log download;

}

Do you really want to learn and debugproduction web server configuration?..

#8

Why “dev web server” approach may fail?

1. HTTP caching issues2. Security issues3. Missing redirects

#8

Use production-like Docker containerswith your local configuration

#8

.env

#8

DOMAIN=domain.com

FOO_ENDPOINT_URL=/api/fooFOO_REAL_ENDPOINT_URL=https://foo.appspot.com/

BAR_ENDPOINT_URL=/api/barBAR_REAL_ENDPOINT_URL=https://bar.heroku.com/

...

.env#8

.env

docker-compose.override.yml

#8

version: '2'services: init: environment: FOO_REAL_ENDPOINT_URL: http://foo.custom serve: extra_hosts: - "foo.custom:172.16.0.95"

docker-compose.override.yml#8

.env

docker-compose.override.yml

Configuration$ docker-compose up init

Configured web server$ docker-compose up -d serve

#8

#9

Let frontend developeruse production state

of concrete user

#9

How to fix a bug?

1. Prepare state2. Reproduce3. Fix

#9

● manual actions● accesses to services● historical entities● data uniqueness

Let frontend developerreceive identity token of concrete user

but with read-only(!) permissions

#9

request identity token

“Looks like” feature

identity token of another user (read-only)

Backend

Attach header to each further requestAuthorization: Bearer {identity token}

Frontend

#9

Frontenddeveloper

configure

#9

Recap

Let frontend developer:

1. don't deal with content management2. use HTML/JS templating3. don't deal with backend routing4. don't deal with stagnated framework5. deliver UI independently6. use remote backend7. don't deal with CORS8. use production web server without digging into its configuration9. use production state of concrete user

Questions?