Moonshot
The future of the Weedmaps Front End
3/30/18
Charlie King
Who am I?
Staff Engineer / Architect
Topics
-
Principles
-
Architecture
-
React
-
UI library
-
Testing
-
Next.js
-
Styled Components
Principles
Learn once, write everywhere (React team)
Get content to the users as fast as possible
Mobile performance first, then enhance
Architecture
Server side templates
Enhance with jQuery
Enhance with Backbone / Angular 1.x
Core
State is easy, just refresh
Client gets real HTML on render, SEO is easy. I81n, accessibility is easier
Less bandwidth, less assets delivered to client
As views become complex, structure falls apart
Harder to create interactive experiences
Increased CPU pressure on server to render complex views
Single-page App
Move all rendering/routing client side
Cache forever, serve statically
Ionic
Developer Experience is better, single paradigm, modern JS (compiled)
Route transitions are immediate, no waiting on the server
Subsequent page visits are extremely quick
Initial page load is very slow
User downloads more data than they use
Consumes a large amount of user memory
SEO? Harder, needs third party services
Create render "middle" teir
Generate HTML from
JS server side
Bootstrap HTML with client side JS (async)
Cache when possible
Moonshot
Developer Experience is awesome, same code on client and server (including routes and data fetching)
Route transitions are immediate* (when the client is downloaded)
First page visits are Fast, subsequent visits are still fast
Server CPU pressure to render templates
Longer Time to First Byte (TTFB)
Architecture
Core
WMAPI
Moonshot
Client
📄 HTML / Assets
NodeJS
Rails
React/Next.js
NGINX
ReactJs
Why?
- Fast learning curve
- Easy reusable components
- Clean abstraction
- Control over patterns/tech
- Rich DevTools
- Tests with Jest and snapshots
ReactJs
WM UI Library
What's the big idea?
What's the big idea?
Header
ActionButton
PaginationControl
Composable and shareable
Semantic Release
What does the version mean?
- Based on actual changes
- Upgrade confidently
- Fully automated releases
- NPM
What is in the Library?
ProjectX
ProjectY
Testing
import React from 'react';
import Link from '../Link.react';
import renderer from 'react-test-renderer';
it('renders correctly', () => {
const tree = renderer.create(
<Link page="http://www.facebook.com">Facebook</Link>
).toJSON();
expect(tree).toMatchSnapshot();
});
Jest + Snapshots = Awesome
exports[`renders correctly 1`] = `
<a
className="normal"
href="http://www.facebook.com"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
Facebook
</a>
`;
Next.js
Convention driven Universal React Framework
Handles all the routing, server rendering and re-conciliation on the client
Routing
Pages are routes, map to physical routes in the file system
📄 index.js
/
📄 deals.js
/deals
Pages split JS into bundles, client only loads the JS for each route
Data fetching
getInitialProps
class LocationProvider extends Component {
static async getInitialProps(props) {
const { res, req, jsonPageRes } = props;
const isServer = !!req;
const locationStore = LocationStore.initStore();
// fetch location
if (!locationStore.location) {
await locationStore.getLocation();
}
// dehydrate the state
const initialLocationState = isServer
? locationStore.dehydrate()
: null;
// return the initial state
return {
isServer,
storeState: { locationStore: initialLocationState }
};
}
Runs once on the server, and then again during client route transitions
Reconciliation
<script type="text/javascript">
window.__NEXT_DATA__ = "{\"dataFromServer\": \"someData\"}"
</script>
Reconciles server data and re-mounts the React application with the data as Props
Prefetching / Caching
<PageLayout className="page--home">
<Head>
<title>{config.social.siteTitle}</title>
</Head>
<PageHead/>
<PageContent>
<Link route='privacy' prefetch>
<a className='button'>
Privacy
</a>
</Link>
<Link route='terms' prefetch>
<a className='button'>
Terms
</a>
</Link>
</PageContent>
<Footer/>
</PageLayout>
<Link> elements with "prefetch" attribute prefetch the route JS code with service worker.
Routes and bundles hashed and cached
SASS
Ionic / Core
Bluedream Core, mostly hand-rolled artisanal CSS
Lots of un-used CSS
Scoping issues and regressions galore
Moonshot
Foundation 6
Import only the classes, mixins that we need, resulting in a smaller CSS bundle
No need to re-write basic "util" like behaviors
BEM
Addresses regressions by making it harder to clash with styles and not have aggressive specific selectors
<LegalLayout className='legal-layout--privacy'>
<Head>
<title>{title}</title>
<meta property='og:title' content={title}/>
<meta property='twitter:title' content={title}/>
<meta property='og:url' content={url}/>
</Head>
<PageHead/>
<PageContent className='page__content--legal'>
<LegalSubheader pageTitle={pageTitle} displayDate={displayDate} url={url}/>
<div className='page__content__container row'>
<LegalSidebar className='small-12 medium-4 large-3 columns' tableOfContents={tableOfContents}/>
<LegalContent className='small-12 medium-8 large-9 columns' type='privacy' sections={sections}/>
</div>
</PageContent>
<Footer/>
</LegalLayout>
Module Scoped CSS
Addresses regressions by making promoting (not enforcing) modular CSS imports
import { withStyles } from '@ghostgroup/universal-sass-loader/runtime';
// import a footer SCSS file
import FooterStyles from './footer';
// create the footer component
const Footer = () => <footer></footer>
// export a styled component
export default withStyles(FooterStyles)(Footer);
<style type="text/css"
data-universal-sass-checksum="08328bc48d07d5d937a0bc70d4c69c58"
data-universal-sass-component-name="WithStyles(Footer)"
id="190">
/* content here */
</style>
Flushed CSS
Node Server flushes all rendered stylesheets into the <head>, no unused CSS
import { withStyles } from '@ghostgroup/universal-sass-loader/runtime';
// import some SCSS files
import GlobalStyles from './styles/global';
// create our "page" with global stuff, e.g. Foundation
const PageLayout = withStyles(GlobalStyles,() => <div></div>);
// import some deals styles
import DealsStyles from './deals_style';
// create the deals document
const Deals = () =>
<PageLayout className="page--deal">
<Head>
<title>Deal</title>
</Head>
<PageHead/>
<PageContent>
<div className='row'>
Deal {id}
</div>
</PageContent>
<Footer/>
</PageLayout>
// export styled deals
export default withStyles(DealsStyles)(Footer);
No Flash of Unstyled Content! (FOUC)
Flushed CSS cont.
<head>
<style id="global-styles">
/* global content */
</style>
<style type="text/css" data-universal-sass-checksum="8148888fbb3a584fcc71b9757eccbeb9"
data-universal-sass-component-name="WithStyles(LegalLayout)" id="739">
/* content */
</style>
<style type="text/css" data-universal-sass-checksum="061ab024b1ebe55213db95b952ce1a98"
data-universal-sass-component-name="WithStyles(GlobalHeader)" id="191">
/* content */
</style>
<style type="text/css" data-universal-sass-checksum="13cd8d8fa67b07ce0934dae922ce2a37"
data-universal-sass-component-name="WithStyles(PageSection)" id="741">
/* content */
</style>
<style type="text/css" data-universal-sass-checksum="a99dbd098159453a731931354bd5d57c"
data-universal-sass-component-name="WithStyles(LegalSidebar)" id="740">
/* content */
</style>
<style type="text/css" data-universal-sass-checksum="4ace5e1d4dd314862ebcd476cb03f0ce"
data-universal-sass-component-name="WithStyles(LegalContent)" id="738">
/* content */
</style>
<style type="text/css" data-universal-sass-checksum="08328bc48d07d5d937a0bc70d4c69c58"
data-universal-sass-component-name="WithStyles(Footer)" id="712">
/* content */
</style>
</head>
Questions / Comments?
Moonshot Final
By Charles King
Moonshot Final
Introduction to the new Weedmaps Front End, Moonshot
- 883