Upload
it-event
View
131
Download
0
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
<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>
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>
.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 } );}
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>
Symfony (PHP) routing
# app/config/routing.yml
blog_list:
path: /blog/{page}
defaults: { _controller: AppBundle:Blog:list, page: 1 }
requirements:
page: '\d+'
blog_show:
# ...
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>
Switching framework is hard
● You can’t pause product development● You can't release all changes at one time
#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
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
Delivery of UI with “monorepo” approach
1. Make changes2. Wait till the whole monorepo can be deployed 3. Deploy
#5
Delivery of UI with “standalone repo” approach
1. Make changes2. Wait till the whole monorepo can be deployed3. Deploy
#5
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
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
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
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;
}
Why “dev web server” approach may fail?
1. HTTP caching issues2. Security issues3. Missing redirects
#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
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
How to fix a bug?
1. Prepare state2. Reproduce3. Fix
#9
● manual actions● accesses to services● historical entities● data uniqueness
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
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