Dan Persa, Maximilian Fellner - The recipe for scalable frontends - Codemotion Milan 2017

66
THE RECIPE FOR SCALABLE FRONTENDS DAN PERSA & MAXIMILIAN FELLNER 10-11-2017 MILAN

Transcript of Dan Persa, Maximilian Fellner - The recipe for scalable frontends - Codemotion Milan 2017

THE RECIPE FOR

SCALABLE

FRONTENDS

DAN PERSA & MAXIMILIAN FELLNER

10-11-2017

MILAN

2

DAN PERSA

Engineering Lead

@danpersa

[email protected]

3

MAXIMILIAN FELLNER

Software Engineer

@mxfellner

[email protected]

EUROPE’S LEADING ONLINE FASHION PLATFORM

15 countries

21+ million active customers

~3.6 billion € revenue 2016

200+ million visits per month

13.000+ employees in Europe

1.600 tech employees

Visit us: tech.zalando.com

5

ZALANDO FASHION STORE

6

THE RECIPE

FOR

SCALABLE

FRONTENDS

SCALING

THE

TECH TEAM

SCALING

THE

ARCHITECTURE

SCALING THE TECH TEAM

ATTRACT NEW,

TALENTED PEOPLEKEEP THE TEAMS HAPPY

ENCOURAGE

INNOVATIONCREATE DIVERSITY

500+

Apps

~1600

Tech employees

2016

2016 2017

SCALING

THE

TECH TEAM

SCALING

THE

ARCHITECTURE

13

Conway’s Law

“organizations which design systems

...are constrained to produce

designs which are copies of the

communication structures of these

organizations”

MICROSERVICES

TEAM AUTONOMY

INDEPENDENT RELEASE CYCLES

MIX DIFFERENT TECH STACKS

EASY A/B TESTING

SCALING THE ARCHITECTURE

15

TEAMS OWN BACKEND APIS

MICROSERVICES

ON THE

FRONTEND?

17

APIS ARE USED BY A FRONTEND MONOLITH

18

WEBAPP GETS CONTRIBUTIONS

FROM MULTIPLE TEAMS

WORK AUTONOMOUSLY

MIX OF DIFFERENT TECH STACKS

INDEPENDENT RELEASE CYCLES

20

MOSAIC

www.mosaic9.org

21

TEAMS OWN FRAGMENTS

Translation ServiceTeam Pathfinder

IAM APITeam GreendaleFRAGMENT

Your Team API

Your Team

From Tailor

HTML Render

AJAX APIs

Internal API Client

From Skipper

Cart ServiceTeam COAST

23

FRAGMENTS USE THE BACKEND APIS

24

LAYOUT SERVICE

25

ASSEMBLED CONTENT IS STREAMED TO THE CLIENT

26

MOSAIC COMPONENTS

JIMMY

27

SKIPPER

Forwards requests to different

endpoints based on request properties:

Host, Path, Method

Cookies, etc.

Streams content from the endpoints

Runtime updates of routing table

github.com/zalando/skipper

28

Tailor is a layout service that

uses streams to compose a web

page from fragment services.

Loads content of all fragments

from the template in parallel.

Offers nice error handling and

fallback features.

github.com/zalando/tailor

29

TEMPLATE

<html><head>

<fragment src="http://assets.domain.com"></fragment></head><body>

<fragment src="http://header.domain.com"></fragment><fragment src="http://content.domain.com" primary></fragment><fragment src="http://footer.domain.com" async></fragment>

</body></html>

HEADER

CART

TAILORlayout service CART FRAGMENT

Team COAST

HEADER FRAGMENTTeam Navigation

QUILTtemplate management API

CART TEMPLATE

TRACKINGTRACKING

FRAGMENT

Team TRCKNG

https://cart.coast.zalan.do

https://eb-fragment.trckng.zalan.do

https://header-fragment-release.navigation.zalan.do

From Skipper

https://zalando.de/cart

31

FRAGMENT JAVASCRIPT

FRAGMENT SKIPPERrouter

TAILORlayout service

CLIENT

STATIC HTML

<button>click me</button>

LINK HEADERS

<script.js>; rel="fragment-script"<style.css>; rel="stylesheet"

AMD MODULE JAVASCRIPT

define([], () => element => {element.onclick = () =>

alert('Hello, World!')})

32

FRAGMENT JAVASCRIPT

FRAGMENT SKIPPERrouter

TAILORlayout service

CLIENT

STATIC HTML

<button>click me</button>

LINK HEADERS

<script.js>; rel="fragment-script"<style.css>; rel="stylesheet"

AMD MODULE JAVASCRIPT

define([], () => element => {element.onclick = () =>

alert('Hello, World!')})

33

FRAGMENT COMMUNICATION

FRAGMENT A

bus.trigger('cart:add', {sku: 'ABZ123'

});

EVENT BUSexternal library

FRAGMENT B

bus.on('cart:add', args => {const { sku } = args;

});

publish &

subscribe

github.com/grassator/happened

34

HOW IT LOOKS

Header Fragment

Cart Fragment

Tracking Fragment

*Not every fragment has to be visible

SCALING

THE

TECH TEAM

SCALING

THE

ARCHITECTURE

MOSAIC

IT’S LIVE!

SCALING

THE

TECH TEAM

SCALING

THE

ARCHITECTURE

SCALING

THE

CONTENT

SCALING

THE

TECH TEAM

SCALING

THE

ARCHITECTURE

SCALING

THE

CONTENT

SKIPPERrouter

TAILORlayout service

FRAGMENTS

39

FRAGMENT ARCHITECTURE

FRAGMENT SKIPPERrouter

TAILORlayout service

CLIENT

?MODERN, INTERACTIVE USER EXPERIENCE

DYNAMIC CONTENT

CONSISTENT LOOK & FEEL EVERYWHERE

40

FRAGMENT ARCHITECTURE

FRAGMENT

<div><button id="btn">click me

</button><script>$('btn').click(() =>alert('Hello!')

)</script>

</div>

DOESN'T SCALE

Too manual

Inconsistent

Only static content

41

FRAGMENT ARCHITECTURE

FRAGMENT

let x = parseRequest(req)

let data = await fetch(x)

let html = template(data)

res.write(html)

BETTER

HTML templates

External content data

Dynamic responses

DOESN’T SCALE

Still inconsistent

JavaScript is 2nd class

42

FRAGMENT ARCHITECTURE

FRAGMENTSOLUTION

Reusable, shared components

Isomorphic/universal code

SSR: JavaScript → HTML

*

* generic universal component framework

43

CONTENT .HTML

.JS

SKIPPERrouter

TAILORlayout service

CLIENT

DESCRIBE JAVASCRIPT COMPONENTS WITH JSON

44

DESCRIBE JAVASCRIPT COMPONENTS WITH JSON

type: divprops: nullchildren:- type: h1props: nullchildren:- Hello, World!

- type: aprops:href: https://www.zalando.de

children:- Zalando Fashion

*

* rendered as YAML for readability

45

LAYOUT .HTML

.JS

SKIPPERrouter

TAILORlayout service

CLIENT

GENERATE CODE AND HTML

USE COMMON COMPONENT LIBRARIES

46

GENERATE CODE AND HTML

<div><h1>Hello, World!</h1><a href=”https://www.zalando.de”>Zalando Fashion

</a></div>

React.createElement(‘div’, null,React.createElement(‘h1’, null,‘Hello, World!’

),React.createElement(‘a’, {

href: ‘https://www.zalando.de’},‘Zalando Fashion’

));

JSON → JavaScriptgenerateCode()

JavaScript → HTMLrenderToString()

47

LAYOUT .HTML

.JS

SKIPPERrouter

TAILORlayout service

CLIENT

GENERATE CODE AND HTML

UNIVERSAL

JAVASCRIPT RUNS

ON THE CLIENT TOO!

48

tessellateverb tes·sel·late \ˈte-sə-ˌlāt\

to form into or adorn with mosaic

github.com/zalando-incubator/tessellate

49

TESSELLATE: TRANSFORM JSON INTO JAVASCRIPT

.JSON .JSTESSELLATE

bundler

CDNstatic server

1. parse JSON AST

2. generate JavaScript code

3. compile webpack bundles

4. export static files

50

TESSELLATE: TRANSFORM JSON INTO JAVASCRIPT

import React from 'react';import ReactDOM from 'react-dom';import Foo from 'foo';import { ComponentA, ComponentB } from 'bar';

export const Root = props => {return (

<div id="root">{React.createElement(

Foo, null,React.createElement(ComponentA, null, 'Hello, World!'),React.createElement(ComponentB, {

href: 'https://www.zalando.de'})

)}</div>

);};

51

import React from 'react';import ReactDOM from 'react-dom';import Foo from 'foo';import { ComponentA, ComponentB } from 'bar';

export const Root = props => {return (

<div id="root">{React.createElement(

Foo, null,React.createElement(ComponentA, null, 'Hello, World!'),React.createElement(ComponentB, {

href: 'https://www.zalando.de'})

)}</div>

);};

TESSELLATE: TRANSFORM JSON INTO JAVASCRIPT

type: foo.default

props: null

children:

- type: bar.ComponentA

props: null

- type: bar.ComponentB

props:

href: ‘https://…’

52

import React from 'react';import ReactDOM from 'react-dom';import Foo from 'foo';import { ComponentA, ComponentB } from 'bar';

export const Root = props => {return (

<div id="root">{React.createElement(

Foo, null,React.createElement(ComponentA, null, 'Hello, World!'),React.createElement(ComponentB, {

href: 'https://www.zalando.de'})

)}</div>

);};

TESSELLATE: TRANSFORM JSON INTO JAVASCRIPT

type: foo.default

props: null

children:

- type: bar.ComponentA

props: null

- type: bar.ComponentB

props:

href: ‘https://…’

53

import React from 'react';import ReactDOM from 'react-dom';import Foo from 'foo';import { ComponentA, ComponentB } from 'bar';

export const Root = props => {const bundledProps = { myValue: 'https://www.zalando.de' };const mergedProps = Object.assign({}, bundledProps, props);return (

<div id="root" data-props={JSON.stringify(props)}>{React.createElement(

Foo, null,React.createElement(ComponentA, null, 'Hello, World!'),React.createElement(ComponentB, {

href: jsonPtr.get(mergedProps, '#/myValue')})

)}</div>

);};

TESSELLATE: TRANSFORM JSON INTO JAVASCRIPT

type: foo.default

props: null

children:

- type: bar.ComponentA

props: null

- type: bar.ComponentB

props:

href:

$ref: #/myValue

54

import React from 'react';import ReactDOM from 'react-dom';import Foo from 'foo';import { ComponentA, ComponentB } from 'bar';

export const Root = props => {const bundledProps = { myValue: 'https://www.zalando.de' };const mergedProps = Object.assign({}, bundledProps, props);return (

<div id="root" data-props={JSON.stringify(props)}>{React.createElement(

Foo, null,React.createElement(ComponentA, null, 'Hello, World!'),React.createElement(ComponentB, {

href: jsonPtr.get(mergedProps, '#/myValue')})

)}</div>

);};

TESSELLATE: TRANSFORM JSON INTO JAVASCRIPT

type: foo.default

props: null

children:

- type: bar.ComponentA

props: null

- type: bar.ComponentB

props:

href:

$ref: #/myValue

55

...

<div id="root" data-props={JSON.stringify(props)}>{React.createElement(

Foo, null,React.createElement(ComponentA, null, 'Hello, World!'),React.createElement(ComponentB, {

href: jsonPtr.get(mergedProps, '#/myValue')})

)}</div>

);};

export default function render(element) {const props = JSON.parse(element.getAttribute('data-props'));ReactDOM.render(<Root {...props} />, element);

}

TESSELLATE: TRANSFORM JSON INTO JAVASCRIPT

56

TESSELLATE: TRANSFORM JSON INTO JAVASCRIPT

Build portable UMD bundles

Run webpack in memory github.com/mfellner/webpack-sandboxed

Export a root component, export a render function for Tailor

Interface for injected property values

Support JSON Pointers in props { “$ref”: “#/attrs/value” }

Inline props for rehydration

Include external component libraries from npm

{ type: “[node-module-name].[export-name]” }

57

TESSELLATE: RENDER JAVASCRIPT INTO HTML

.HTML.JSTESSELLATE

fragment

CDNstatic server

1. fetch webpack bundles

2. load external data

3. execute JavaScript code

4. render static HTML

renderToString()

58

TESSELLATE: RENDER JAVASCRIPT INTO HTML

Fetch the precompiled bundle …

const code = await fetchBundle()

const { Root } = vm.runInNewContext(code)

const props = await fetchContent()

const element = React.createElement(Root, props)

ReactDOMServer.renderToString(element)

59

TESSELLATE: RENDER JAVASCRIPT INTO HTML

Run the code in the Node vm …

const code = await fetchBundle()

const { Root } = vm.runInNewContext(code)

const props = await fetchContent()

const element = React.createElement(Root, props)

ReactDOMServer.renderToString(element)

60

TESSELLATE: RENDER JAVASCRIPT INTO HTML

Fetch any external content …

const code = await fetchBundle()

const { Root } = vm.runInNewContext(code)

const props = await fetchContent()

const element = React.createElement(Root, props)

ReactDOMServer.renderToString(element)

61

TESSELLATE: RENDER JAVASCRIPT INTO HTML

Render to HTML!

const code = await fetchBundle()

const { Root } = vm.runInNewContext(code)

const props = await fetchContent()

const element = React.createElement(Root, props)

ReactDOMServer.renderToString(element)

62

TESSELLATE: RENDER JAVASCRIPT INTO HTML

Download the code

Precompiled bundle and any dependencies.

Fetch external content

To be injected as properties into the root component.

Send to Tailor

Rendered HTML and links to JavaScript (according to the Fragment API).

63

TESSELLATE

CONTENT

.HTML

.JS

SKIPPERrouter

TAILORlayout service

CLIENT

TESSELLATEbundler

TESSELLATEfragment

DATA

MODERN, INTERACTIVE USER EXPERIENCE ✓

DYNAMIC CONTENT ✓

CONSISTENT LOOK & FEEL EVERYWHERE ✓

SCALING

THE

TECH TEAM

SCALING

THE

ARCHITECTURE

SCALING

THE

CONTENT

SCALING

THE

TECH TEAM

SCALING

THE

ARCHITECTURE

SCALING

THE

CONTENT

RADICAL

AGILITY

MOSAIC

TESSELLATE

xwww.mosaic9.org

@danpersa

@mxfellner