KEMBAR78
Building a js widget | PDF
Building a JS widget
...from whiteboard to delivery
Tudor Barbu
@motanelu
Components
Backend Frontend(s)
<div id="widget"></div>
<script type="text/javascript">
new Messaging.Widget({
selector: '#widget',
userId: 1,
// other configuration
})
</script>
> 768px
< 768px
Path to the widget
Know your customer!
Who is the customer?
Users - final customers that
● live in different geographies
● use “exotic” browsers
● have different connection speeds
● different expectations (realtime, file uploads)
Marketplaces, customers that
● are built on different technologies, ranging
from React to Smarty & PHP
● want to be able to customize the widget, but
without breaking it
● have different build systems (or none)
● have dedicated frontend teams (or not)
● have sometimes hard to predict release
cycles
And they don’t always update
to the latest version!
Messaging engineer waiting for a marketplace
update
First set of requirements:
● cross-platform / responsive
● customizable look-n-feel
● support browsers you didn’t know exist
● ...without breaking customizations /
responsiveness
● support React, direct injection via script tag
and everything in between
● be able to update the widget to newer
versions without relying on input from the
marketplaces
● don’t forget about cool features like sending
images from your phone, real-time and so on
Yandex Browser
● deprecated APIs with the new ones being
developed at the moment
● server doesn’t support real-time, but we still
want it (polling)
● ...and A/B tests, we need to be able to run tests,
independent of the marketplace
“Dream” requirements
1 Abstract the API
4 Update & A/B tests
2 Develop the widget
3 Handle customizations
1 Abstract the API
Connector
External library (npm install)
Abstracts the access to the API
connector.on('counter-update', entity => {
// do something
})
// ajax - ticks handled internally
import axios from 'axios'
import EventEmitter from 'events'
const bus = new EventEmitter();
axios
.get('url')
.then(response => {
bus.emit('counter-update', {})
})
// socket
import io from 'socket.io-client'
import EventEmitter from 'events'
const bus = new EventEmitter();
socket
.on('counter-push-received', data => {
bus.emit('counter-update', {})
})
WidgetConnector
import axios from 'axios'
// inside the use case
url = `/v1/getConversation/?id=${conversationId}`
axios.get(url)
.then(response => {
return ConversationEntity.legacy(response)
})
import axios from 'axios'
// inside the use case
url = `/api/v2/conversation/${conversationId}`
axios.get(url)
.then(response => {
return ConversationEntity.create(response)
})
const promise = connector.useCase()
.getConversation.execute(conversationId)
promise
.then((conversation) => {
conversation.markAsRead()
})
.catch((error) => {
// handle
})
Test
Unit tests
Karma / chai / sinon
PhantomJS
Release
release on tag
build on Travis
npm registry / Artifactory
Vue + Vuex + VueRouter
DOM API
Widget
<template>
<ul class="msg-list__items" ref="conversationsList">
<conversations-list-item
v-for="conversation in conversations"
:conversation="conversation"
:key="conversation.conversationId"
/>
</ul>
</template>
Architecture
● <conversation-menu> and <*-item> have state injected
from the parent
● everything else gets it from Vuex
● child components are agnostic with regards to the state of
the parent
● container components handle their own events (infinite
scrolling) and load additional children as required
DOM API directly
...practice frowned upon by the Vue community!
1. The target element of the event is a link or a form field.
2. The target element, or any of its ancestors up to but not including the <body>,
has an explicit event handler set for any of the mouse events. This event
handler may be an empty function.
3. The target element, or any of its ancestors up to and including the document
has a cursor: pointer CSS declarations.
https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
Click anywhere on the page and close it!
const body = document.querySelector('body')
body.addEventListener('click', (event) => {
closeMenu()
})
Doesn’t work!!!
// ...
overlay = document.createElement('div')
overlay.id = 'messaging-widget-overlay'
overlay.style.position = 'absolute'
overlay.style.top = 0
overlay.style.right = 0
overlay.style.bottom = 0
overlay.style.left = 0
overlay.style.background = 'transparent'
overlay.style.display = 'none'
overlay.style.zIndex = 2100
overlay.addEventListener('click', () => {
events.$emit('close-menus')
})
document.querySelector('body').appendChild(overlay)
// ...
<html>
<head>
<!-- header stuff -->
</head>
<body>
<section id="header"></section>
<section id="content">
<div id="messaging">
<!-- widget goes here -->
</div>
</section>
<section id="footer"></section>
<div id="messaging-widget-overlay"
...
></div>
</body>
</html>
export default {
name: 'Widget',
created () {
// ...
this.injectMetaTags()
},
beforeDestroy () {
// ...
this.removeMetaTags()
},
// ...
}
<meta
id="messaging-widget-meta-viewport"
name="viewport"
content="width=device-width,
initial-scale=1.0, maximum-scale=1.0,
user-scalable=no"
>
<meta
id="messaging-widget-meta-format-detection"
name="format-detection" content="telephone=no"
>
<meta
id="messaging-widget-meta-http-equiv"
http-equiv="X-UA-Compatible"
content="IE=edge"
>
3 Customizations
* Block element modifier
** Not the actual logo
* **
Layout
● Handles positioning of various elements
● Ensures responsiveness
● Should not be overridden by the marketplace
Theme
● Handles colors and border radii
● Injects the icons as data-urls
● Should be overridden by the marketplace
Vertical media query
(height < 400)
FAAST
* Frontend as a Service Technology
4 Update & A/B tests
<script
type="text/javascript"
src="cdn://faast.min.js">
</script>
<script type="text/javascript">
faast(
'messaging-widget',
'<env>',
{siteName: 'test'},
additionalConfiguration
)
.then(fn => {
new Messaging.Widget({
// configuration here
})
// call fn() to remove created elements
})
.catch(error => {
// do something
})
</script>
Create
<script>
tags
Fulfill the
promise
What’s the version for site name?
<script src="..."> & <link href="...">
JSON response
All files finished loading
FAAST server
FAAST API CLI tools
FAAST client
● Vanilla js - new XmlHttpRequest(...)
● 140 lines of code (with comments :) )
● Guarantees the order of the scripts
● Waits for all scripts to load before fulfilling the
promise
● Provides a clean-up mechanism
● cssBefore <selector> and jsBefore <selector>
{
"environment": "pre",
"resourceName": "messaging-widget",
"scripts": [
"https://cdn/messaging.widget.min.js?v=0dbb87d9"
// more files here
],
"styles": [
"https://cnd/messaging.widget.min.css?v=0dbb87d9"
// more files here
],
"version": "1.7.1"
}
<link rel="stylesheet" href="messaging.min.css">
<link rel="stylesheet" href="site-theme.css">
FAAST Marketplace site
normal flow
cssBefore: <selector>
const promise = faast(
'messaging-widget',
'pre',
{siteName: 'test'},
{cssBefore: 'link[rel=stylesheet]:first-of-type'}
)
Pros and cons:
~ 15kb of duplication, as icons are included with data urls
...but the widget will work even if the site theme is broken or missing
Player 2 has entered the game...
● Native react component (3rd js repository)
● Published to Artifactory as npm package
● Wraps around FAAST which wraps around the widget
(Wrapception)
Hydra of nondeterministic bugs
Test & release
Pain points
● Entanglement - React / FAAST / Widget
● Slow booting machines in Saucelabs (45’ - 1h)
● Not really mockable
● It must be deployed to other sites automatically
*without breaking them*
● Tickets should receive business validation before
being deployed (manual step)
● 1-2 releases per week on normal flow
Minimal set of tests
● Run against a small number of browsers
● Cover the basic functionality
● Run on mocked responses to account for
common mistakes
● Designed to run fast
● Act as an early warning system
Full set of tests
● Run against all supported browsers
● Cover most functionality
● Slow, only when releasing
● Run on live APIs
Triggered releaseContinuous delivery
www.spinnaker.ioCore contributor
● Open source delivery platform
● Multi cloud provider
● Acts like a pipeline
● Jobs are handled by Travis
● Used by Google, Netflix, Schibsted :)
FAAST
Messaging
Preprod
Business
validation
All sites preAll sites pro
(waiting period)
(engineer) (minimal)
(tag)
(full with mocks)
Release procedure (wip)
Normal
flow
(full with live APIs)
Cool?
jobs.schibsted.com
@Schibsted_Eng
bytes.schibsted.com
github.com getbem.com travis-ci.org nightwatchjs.org facebook.github.io/react AWS S3
sass-lang.com spring.io karma-runner Saucelabs webpack Phantomjs
AWS EC2AWS DynamoDByarnCassandra
Tools

Building a js widget

  • 1.
    Building a JSwidget ...from whiteboard to delivery Tudor Barbu @motanelu
  • 3.
  • 4.
  • 5.
    <div id="widget"></div> <script type="text/javascript"> newMessaging.Widget({ selector: '#widget', userId: 1, // other configuration }) </script> > 768px < 768px
  • 6.
  • 7.
  • 8.
    Who is thecustomer?
  • 9.
    Users - finalcustomers that ● live in different geographies ● use “exotic” browsers ● have different connection speeds ● different expectations (realtime, file uploads)
  • 10.
    Marketplaces, customers that ●are built on different technologies, ranging from React to Smarty & PHP ● want to be able to customize the widget, but without breaking it ● have different build systems (or none) ● have dedicated frontend teams (or not) ● have sometimes hard to predict release cycles
  • 11.
    And they don’talways update to the latest version! Messaging engineer waiting for a marketplace update
  • 12.
    First set ofrequirements: ● cross-platform / responsive ● customizable look-n-feel ● support browsers you didn’t know exist ● ...without breaking customizations / responsiveness ● support React, direct injection via script tag and everything in between ● be able to update the widget to newer versions without relying on input from the marketplaces ● don’t forget about cool features like sending images from your phone, real-time and so on Yandex Browser
  • 13.
    ● deprecated APIswith the new ones being developed at the moment ● server doesn’t support real-time, but we still want it (polling) ● ...and A/B tests, we need to be able to run tests, independent of the marketplace
  • 14.
  • 15.
    1 Abstract theAPI 4 Update & A/B tests 2 Develop the widget 3 Handle customizations
  • 16.
    1 Abstract theAPI Connector External library (npm install) Abstracts the access to the API
  • 17.
    connector.on('counter-update', entity =>{ // do something }) // ajax - ticks handled internally import axios from 'axios' import EventEmitter from 'events' const bus = new EventEmitter(); axios .get('url') .then(response => { bus.emit('counter-update', {}) }) // socket import io from 'socket.io-client' import EventEmitter from 'events' const bus = new EventEmitter(); socket .on('counter-push-received', data => { bus.emit('counter-update', {}) }) WidgetConnector
  • 18.
    import axios from'axios' // inside the use case url = `/v1/getConversation/?id=${conversationId}` axios.get(url) .then(response => { return ConversationEntity.legacy(response) }) import axios from 'axios' // inside the use case url = `/api/v2/conversation/${conversationId}` axios.get(url) .then(response => { return ConversationEntity.create(response) }) const promise = connector.useCase() .getConversation.execute(conversationId) promise .then((conversation) => { conversation.markAsRead() }) .catch((error) => { // handle })
  • 20.
    Test Unit tests Karma /chai / sinon PhantomJS Release release on tag build on Travis npm registry / Artifactory
  • 21.
    Vue + Vuex+ VueRouter DOM API Widget
  • 22.
    <template> <ul class="msg-list__items" ref="conversationsList"> <conversations-list-item v-for="conversationin conversations" :conversation="conversation" :key="conversation.conversationId" /> </ul> </template> Architecture ● <conversation-menu> and <*-item> have state injected from the parent ● everything else gets it from Vuex ● child components are agnostic with regards to the state of the parent ● container components handle their own events (infinite scrolling) and load additional children as required
  • 24.
    DOM API directly ...practicefrowned upon by the Vue community!
  • 25.
    1. The targetelement of the event is a link or a form field. 2. The target element, or any of its ancestors up to but not including the <body>, has an explicit event handler set for any of the mouse events. This event handler may be an empty function. 3. The target element, or any of its ancestors up to and including the document has a cursor: pointer CSS declarations. https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html Click anywhere on the page and close it! const body = document.querySelector('body') body.addEventListener('click', (event) => { closeMenu() }) Doesn’t work!!!
  • 26.
    // ... overlay =document.createElement('div') overlay.id = 'messaging-widget-overlay' overlay.style.position = 'absolute' overlay.style.top = 0 overlay.style.right = 0 overlay.style.bottom = 0 overlay.style.left = 0 overlay.style.background = 'transparent' overlay.style.display = 'none' overlay.style.zIndex = 2100 overlay.addEventListener('click', () => { events.$emit('close-menus') }) document.querySelector('body').appendChild(overlay) // ... <html> <head> <!-- header stuff --> </head> <body> <section id="header"></section> <section id="content"> <div id="messaging"> <!-- widget goes here --> </div> </section> <section id="footer"></section> <div id="messaging-widget-overlay" ... ></div> </body> </html>
  • 27.
    export default { name:'Widget', created () { // ... this.injectMetaTags() }, beforeDestroy () { // ... this.removeMetaTags() }, // ... } <meta id="messaging-widget-meta-viewport" name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" > <meta id="messaging-widget-meta-format-detection" name="format-detection" content="telephone=no" > <meta id="messaging-widget-meta-http-equiv" http-equiv="X-UA-Compatible" content="IE=edge" >
  • 28.
  • 29.
    * Block elementmodifier ** Not the actual logo * ** Layout ● Handles positioning of various elements ● Ensures responsiveness ● Should not be overridden by the marketplace Theme ● Handles colors and border radii ● Injects the icons as data-urls ● Should be overridden by the marketplace
  • 30.
  • 31.
    FAAST * Frontend asa Service Technology 4 Update & A/B tests
  • 32.
    <script type="text/javascript" src="cdn://faast.min.js"> </script> <script type="text/javascript"> faast( 'messaging-widget', '<env>', {siteName: 'test'}, additionalConfiguration ) .then(fn=> { new Messaging.Widget({ // configuration here }) // call fn() to remove created elements }) .catch(error => { // do something }) </script> Create <script> tags Fulfill the promise What’s the version for site name? <script src="..."> & <link href="..."> JSON response All files finished loading FAAST server
  • 33.
    FAAST API CLItools FAAST client ● Vanilla js - new XmlHttpRequest(...) ● 140 lines of code (with comments :) ) ● Guarantees the order of the scripts ● Waits for all scripts to load before fulfilling the promise ● Provides a clean-up mechanism ● cssBefore <selector> and jsBefore <selector> { "environment": "pre", "resourceName": "messaging-widget", "scripts": [ "https://cdn/messaging.widget.min.js?v=0dbb87d9" // more files here ], "styles": [ "https://cnd/messaging.widget.min.css?v=0dbb87d9" // more files here ], "version": "1.7.1" }
  • 34.
    <link rel="stylesheet" href="messaging.min.css"> <linkrel="stylesheet" href="site-theme.css"> FAAST Marketplace site normal flow cssBefore: <selector> const promise = faast( 'messaging-widget', 'pre', {siteName: 'test'}, {cssBefore: 'link[rel=stylesheet]:first-of-type'} ) Pros and cons: ~ 15kb of duplication, as icons are included with data urls ...but the widget will work even if the site theme is broken or missing
  • 36.
    Player 2 hasentered the game... ● Native react component (3rd js repository) ● Published to Artifactory as npm package ● Wraps around FAAST which wraps around the widget (Wrapception) Hydra of nondeterministic bugs
  • 37.
  • 38.
    Pain points ● Entanglement- React / FAAST / Widget ● Slow booting machines in Saucelabs (45’ - 1h) ● Not really mockable ● It must be deployed to other sites automatically *without breaking them* ● Tickets should receive business validation before being deployed (manual step) ● 1-2 releases per week on normal flow
  • 39.
    Minimal set oftests ● Run against a small number of browsers ● Cover the basic functionality ● Run on mocked responses to account for common mistakes ● Designed to run fast ● Act as an early warning system Full set of tests ● Run against all supported browsers ● Cover most functionality ● Slow, only when releasing ● Run on live APIs
  • 40.
  • 41.
    www.spinnaker.ioCore contributor ● Opensource delivery platform ● Multi cloud provider ● Acts like a pipeline ● Jobs are handled by Travis ● Used by Google, Netflix, Schibsted :)
  • 42.
    FAAST Messaging Preprod Business validation All sites preAllsites pro (waiting period) (engineer) (minimal) (tag) (full with mocks) Release procedure (wip) Normal flow (full with live APIs)
  • 43.
  • 44.
    github.com getbem.com travis-ci.orgnightwatchjs.org facebook.github.io/react AWS S3 sass-lang.com spring.io karma-runner Saucelabs webpack Phantomjs AWS EC2AWS DynamoDByarnCassandra Tools