ff-stream-web

git clone git://archive.git.mtrnord.blog/MTRNord/ff-stream-web.git
Log | Files | Refs | README | LICENSE

commit 10e3a4ca6ff5af81f507b5f2d7534381fe87865e
parent 07eb82510d23b3a5c3229e375ee4183a58238d75
Author: Konstantin Tarkus <hello@tarkus.me>
Date:   Thu, 14 May 2015 22:32:58 +0300

Pre-render critical CSS

Diffstat:
M.eslintrc | 5++++-
Mpackage.json | 28++++++++++++++--------------
Msrc/app.js | 15+++++++++------
Msrc/components/App/App.js | 45++++++++++++++++++++-------------------------
Msrc/components/App/App.less | 3++-
Msrc/components/ContactPage/ContactPage.js | 14++++++++++----
Msrc/components/ContactPage/ContactPage.less | 2+-
Msrc/components/ContentPage/ContentPage.js | 9++++++++-
Msrc/components/ContentPage/ContentPage.less | 2+-
Msrc/components/Feedback/Feedback.js | 4+++-
Msrc/components/Feedback/Feedback.less | 2+-
Msrc/components/Footer/Footer.js | 14+++++++-------
Msrc/components/Footer/Footer.less | 2+-
Msrc/components/Header/Header.js | 4+++-
Msrc/components/Header/Header.less | 2+-
Msrc/components/LoginPage/LoginPage.js | 14++++++++++----
Msrc/components/LoginPage/LoginPage.less | 2+-
Msrc/components/Navigation/Navigation.js | 8+++++---
Msrc/components/Navigation/Navigation.less | 2+-
Msrc/components/NotFoundPage/NotFoundPage.js | 16++++++++++++----
Msrc/components/NotFoundPage/NotFoundPage.less | 2+-
Msrc/components/RegisterPage/RegisterPage.js | 14++++++++++----
Msrc/components/RegisterPage/RegisterPage.less | 2+-
Msrc/components/TextBox/TextBox.js | 4+++-
Msrc/components/TextBox/TextBox.less | 2+-
Asrc/components/decorators/index.js | 5+++++
Dsrc/components/decorators/setViewport.js | 58----------------------------------------------------------
Asrc/components/decorators/withContext.js | 43+++++++++++++++++++++++++++++++++++++++++++
Asrc/components/decorators/withStyles.js | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/decorators/withViewport.js | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/variables.less | 2+-
Msrc/server.js | 19+++++++++++--------
Msrc/templates/index.html | 3++-
Dsrc/utils/Css.js | 42------------------------------------------
Mwebpack.config.js | 22++++++++++++++--------
35 files changed, 348 insertions(+), 205 deletions(-)

diff --git a/.eslintrc b/.eslintrc @@ -21,6 +21,9 @@ // Code style "indent": [2, 2], - "quotes": [2, "single"] + "quotes": [2, "single"], + + // Misc + "no-shadow": 0 } } diff --git a/package.json b/package.json @@ -10,31 +10,31 @@ "npm": ">= 2.1" }, "dependencies": { - "babel": "5.2.16", + "babel": "5.4.3", "classnames": "2.1.1", - "eventemitter3": "1.0.2", - "express": "4.12.3", + "eventemitter3": "1.1.0", + "express": "4.12.4", "fastclick": "1.0.6", "flux": "2.0.3", "front-matter": "1.0.0", "jade": "1.9.2", "lodash": "3.8.0", "normalize.css": "3.0.3", - "react": "0.13.2", - "source-map-support": "^0.2.10", + "react": "0.13.3", + "source-map-support": "0.2.10", "superagent": "1.2.0" }, "devDependencies": { "autoprefixer-loader": "^1.2.0", - "babel-core": "^5.2.16", - "babel-eslint": "^3.1.1", - "babel-loader": "^5.0.0", + "babel-core": "^5.4.3", + "babel-eslint": "^3.1.5", + "babel-loader": "^5.1.2", "browser-sync": "^2.7.1", - "css-loader": "^0.12.0", + "css-loader": "^0.12.1", "del": "^1.1.1", - "eslint": "^0.20.0", - "eslint-loader": "^0.11.1", - "eslint-plugin-react": "^2.2.0", + "eslint": "^0.21.1", + "eslint-loader": "^0.11.2", + "eslint-plugin-react": "^2.3.0", "git-push": "^0.1.1", "gulp": "^3.8.11", "gulp-changed": "^1.2.1", @@ -43,7 +43,7 @@ "gulp-rename": "^1.2.2", "gulp-size": "^1.2.1", "gulp-util": "^3.0.4", - "jest-cli": "^0.4.1", + "jest-cli": "^0.4.5", "less": "^2.5.0", "less-loader": "^2.2.0", "minimist": "^1.1.1", @@ -52,7 +52,7 @@ "run-sequence": "^1.1.0", "style-loader": "^0.12.2", "url-loader": "^0.5.5", - "webpack": "^1.8.11" + "webpack": "^1.9.7" }, "jest": { "rootDir": "./src", diff --git a/src/app.js b/src/app.js @@ -3,14 +3,13 @@ import 'babel/polyfill'; import React from 'react'; import FastClick from 'fastclick'; -import emptyFunction from 'react/lib/emptyFunction'; import App from './components/App'; import Dispatcher from './core/Dispatcher'; import AppActions from './actions/AppActions'; import { ActionTypes } from './core/Constants'; let path = decodeURI(window.location.pathname); -let setMetaTag = (name, content) => { +let onSetMeta = (name, content) => { // Remove and create a new <meta /> tag in order to make it work // with bookmarks in Safari let elements = document.getElementsByTagName('meta'); @@ -29,12 +28,16 @@ function run() { // Render the top-level React component let props = { path: path, - onSetTitle: (title) => document.title = title, - onSetMeta: setMetaTag, - onPageNotFound: emptyFunction + context: { + onSetTitle: value => document.title = value, + onSetMeta + } }; let element = React.createElement(App, props); - React.render(element, document.getElementById('app')); + React.render(element, document.getElementById('app'), () => { + let css = document.getElementById('css'); + css.parentNode.removeChild(css); + }); // Update `Application.path` prop when `window.location` is changed Dispatcher.register((action) => { diff --git a/src/components/App/App.js b/src/components/App/App.js @@ -1,7 +1,8 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import './App.less'; import React, { PropTypes } from 'react'; +import styles from './App.less'; // eslint-disable-line no-unused-vars +import { withContext, withStyles } from '../decorators'; // eslint-disable-line no-unused-vars import AppActions from '../../actions/AppActions'; import AppStore from '../../stores/AppStore'; import Header from '../Header'; @@ -15,13 +16,12 @@ import Footer from '../Footer'; const pages = { ContentPage, ContactPage, LoginPage, RegisterPage, NotFoundPage }; +@withContext +@withStyles(styles) class App { static propTypes = { - path: PropTypes.string.isRequired, - onSetTitle: PropTypes.func.isRequired, - onSetMeta: PropTypes.func.isRequired, - onPageNotFound: PropTypes.func.isRequired + path: PropTypes.string.isRequired }; componentDidMount() { @@ -37,43 +37,38 @@ class App { } render() { + let component; + switch (this.props.path) { + case '/': + case '/about': + case '/privacy': + let page = AppStore.getPage(this.props.path); + component = React.createElement(pages[page.component], page); + break; + case '/contact': - this.props.onSetTitle(ContactPage.title); - this.component = <ContactPage />; + component = <ContactPage />; break; case '/login': - this.props.onSetTitle(LoginPage.title); - this.component = <LoginPage />; + component = <LoginPage />; break; case '/register': - this.props.onSetTitle(RegisterPage.title); - this.component = <RegisterPage />; + component = <RegisterPage />; break; - - default: - let page = AppStore.getPage(this.props.path); - if (page) { - this.props.onSetTitle(page.title); - this.component = React.createElement(pages[page.component], page); - } else { - this.props.onSetTitle(NotFoundPage.title); - this.props.onPageNotFound(); - this.component = <NotFoundPage />; - } } - return ( + return component ? ( <div> <Header /> - {this.component} + {component} <Feedback /> <Footer /> </div> - ); + ) : <NotFoundPage />; } handlePopState(event) { diff --git a/src/components/App/App.less b/src/components/App/App.less @@ -1,7 +1,8 @@ +@import (less) '../../../node_modules/normalize.css/normalize.css'; + /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ @import '../variables.less'; -@import (less) '../../../node_modules/normalize.css/normalize.css'; // // Base styles diff --git a/src/components/ContactPage/ContactPage.js b/src/components/ContactPage/ContactPage.js @@ -1,17 +1,23 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React from 'react'; // eslint-disable-line no-unused-vars -import './ContactPage.less'; +import React, { PropTypes } from 'react'; // eslint-disable-line no-unused-vars +import styles from './ContactPage.less'; // eslint-disable-line no-unused-vars +import { withStyles } from '../decorators'; // eslint-disable-line no-unused-vars +@withStyles(styles) class ContactPage { - static title = 'Contact Us'; + static contextTypes = { + onSetTitle: PropTypes.func.isRequired + }; render() { + let title = 'Contact Us'; + this.context.onSetTitle(title); return ( <div className="ContactPage"> <div className="ContactPage-container"> - <h1>{ContactPage.title}</h1> + <h1>{title}</h1> <p>...</p> </div> </div> diff --git a/src/components/ContactPage/ContactPage.less b/src/components/ContactPage/ContactPage.less @@ -1,4 +1,4 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ +/* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ @import '../variables.less'; diff --git a/src/components/ContentPage/ContentPage.js b/src/components/ContentPage/ContentPage.js @@ -1,16 +1,23 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ import React, { PropTypes } from 'react'; // eslint-disable-line no-unused-vars -import './ContentPage.less'; +import styles from './ContentPage.less'; // eslint-disable-line no-unused-vars +import { withStyles } from '../decorators'; // eslint-disable-line no-unused-vars +@withStyles(styles) class ContentPage { + static contextTypes = { + onSetTitle: PropTypes.func.isRequired + }; + static propTypes = { path: PropTypes.string.isRequired, content: PropTypes.string.isRequired }; render() { + this.context.onSetTitle(this.props.title); return ( <div className="ContentPage"> <div className="ContentPage-container"> diff --git a/src/components/ContentPage/ContentPage.less b/src/components/ContentPage/ContentPage.less @@ -1,4 +1,4 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ +/* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ @import '../variables.less'; diff --git a/src/components/Feedback/Feedback.js b/src/components/Feedback/Feedback.js @@ -1,8 +1,10 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ import React from 'react'; // eslint-disable-line no-unused-vars -import './Feedback.less'; +import styles from './Feedback.less'; // eslint-disable-line no-unused-vars +import { withStyles } from '../decorators'; // eslint-disable-line no-unused-vars +@withStyles(styles) class Feedback { render() { diff --git a/src/components/Feedback/Feedback.less b/src/components/Feedback/Feedback.less @@ -1,4 +1,4 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ +/* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ @import '../variables.less'; diff --git a/src/components/Footer/Footer.js b/src/components/Footer/Footer.js @@ -1,14 +1,12 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ import React, { PropTypes } from 'react'; // eslint-disable-line no-unused-vars -import './Footer.less'; +import styles from './Footer.less'; // eslint-disable-line no-unused-vars +import { withStyles, withViewport } from '../decorators'; // eslint-disable-line no-unused-vars import Link from '../../utils/Link'; -import Css from '../../utils/Css'; -import setViewport from '../decorators/setViewport'; // eslint-disable-line no-unused-vars -const css = new Css(); - -@setViewport +@withViewport +@withStyles(styles) class Footer { static propTypes = { @@ -21,7 +19,7 @@ class Footer { render() { // This is just an example how one can render CSS let { width, height } = this.props.viewport; - css.set(`.Footer-viewport:after {content:' ${width}x${height}';}`); + this.renderCss(`.Footer-viewport:after {content:' ${width}x${height}';}`); return ( <div className="Footer"> @@ -31,6 +29,8 @@ class Footer { <a className="Footer-link" href="/" onClick={Link.handleClick}>Home</a> <span className="Footer-spacer">·</span> <a className="Footer-link" href="/privacy" onClick={Link.handleClick}>Privacy</a> + <span className="Footer-spacer">·</span> + <a className="Footer-link" href="/not-found" onClick={Link.handleClick}>Not Found</a> <span className="Footer-spacer"> | </span> <span ref="viewport" className="Footer-viewport Footer-text Footer-text--muted">Viewport:</span> </div> diff --git a/src/components/Footer/Footer.less b/src/components/Footer/Footer.less @@ -1,4 +1,4 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ +/* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ @import '../variables.less'; diff --git a/src/components/Header/Header.js b/src/components/Header/Header.js @@ -1,10 +1,12 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ import React from 'react'; // eslint-disable-line no-unused-vars -import './Header.less'; +import styles from './Header.less'; // eslint-disable-line no-unused-vars +import { withStyles } from '../decorators'; // eslint-disable-line no-unused-vars import Link from '../../utils/Link'; import Navigation from '../Navigation'; +@withStyles(styles) class Header { render() { diff --git a/src/components/Header/Header.less b/src/components/Header/Header.less @@ -1,4 +1,4 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ +/* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ @import '../variables.less'; diff --git a/src/components/LoginPage/LoginPage.js b/src/components/LoginPage/LoginPage.js @@ -1,17 +1,23 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React from 'react'; // eslint-disable-line no-unused-vars -import './LoginPage.less'; +import React, { PropTypes } from 'react'; // eslint-disable-line no-unused-vars +import styles from './LoginPage.less'; // eslint-disable-line no-unused-vars +import { withStyles } from '../decorators'; // eslint-disable-line no-unused-vars +@withStyles(styles) class LoginPage { - static title = 'Log In'; + static contextTypes = { + onSetTitle: PropTypes.func.isRequired + }; render() { + let title = 'Log In'; + this.context.onSetTitle(title); return ( <div className="LoginPage"> <div className="LoginPage-container"> - <h1>{LoginPage.title}</h1> + <h1>{title}</h1> <p>...</p> </div> </div> diff --git a/src/components/LoginPage/LoginPage.less b/src/components/LoginPage/LoginPage.less @@ -1,4 +1,4 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ +/* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ @import '../variables.less'; diff --git a/src/components/Navigation/Navigation.js b/src/components/Navigation/Navigation.js @@ -1,15 +1,17 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ import React from 'react'; // eslint-disable-line no-unused-vars -import cx from 'classnames'; -import './Navigation.less'; +import classNames from 'classnames'; +import styles from './Navigation.less'; // eslint-disable-line no-unused-vars +import { withStyles } from '../decorators'; // eslint-disable-line no-unused-vars import Link from '../../utils/Link'; +@withStyles(styles) class Navigation { render() { return ( - <div className={cx(this.props.className, 'Navigation')} role="navigation"> + <div className={classNames(this.props.className, 'Navigation')} role="navigation"> <a className="Navigation-link" href="/about" onClick={Link.handleClick}>About</a> <a className="Navigation-link" href="/contact" onClick={Link.handleClick}>Contact</a> <span className="Navigation-spacer"> | </span> diff --git a/src/components/Navigation/Navigation.less b/src/components/Navigation/Navigation.less @@ -1,4 +1,4 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ +/* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ .Navigation { diff --git a/src/components/NotFoundPage/NotFoundPage.js b/src/components/NotFoundPage/NotFoundPage.js @@ -1,16 +1,24 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -//import './NotFoundPage.less'; -import React from 'react'; // eslint-disable-line no-unused-vars +import React, { PropTypes } from 'react'; // eslint-disable-line no-unused-vars +import { withStyles } from '../decorators'; // eslint-disable-line no-unused-vars +import styles from './NotFoundPage.less'; // eslint-disable-line no-unused-vars +@withStyles(styles) class NotFoundPage { - static title = 'Page Not Found'; + static contextTypes = { + onSetTitle: PropTypes.func.isRequired, + onPageNotFound: PropTypes.func.isRequired + }; render() { + let title = 'Page Not Found'; + this.context.onSetTitle(title); + this.context.onPageNotFound(); return ( <div> - <h1>Page Not Found</h1> + <h1>{title}</h1> <p>Sorry, but the page you were trying to view does not exist.</p> </div> ); diff --git a/src/components/NotFoundPage/NotFoundPage.less b/src/components/NotFoundPage/NotFoundPage.less @@ -1,4 +1,4 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ +/* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ * { margin: 0; diff --git a/src/components/RegisterPage/RegisterPage.js b/src/components/RegisterPage/RegisterPage.js @@ -1,17 +1,23 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import React from 'react'; // eslint-disable-line no-unused-vars -import './RegisterPage.less'; +import React, { PropTypes } from 'react'; // eslint-disable-line no-unused-vars +import { withStyles } from '../decorators'; // eslint-disable-line no-unused-vars +import styles from './RegisterPage.less'; // eslint-disable-line no-unused-vars +@withStyles(styles) class RegisterPage { - static title = 'Sign Up'; + static contextTypes = { + onSetTitle: PropTypes.func.isRequired + }; render() { + let title = 'New User Registration'; + this.context.onSetTitle(title); return ( <div className="RegisterPage"> <div className="RegisterPage-container"> - <h1>{RegisterPage.title}</h1> + <h1>{title}</h1> <p>...</p> </div> </div> diff --git a/src/components/RegisterPage/RegisterPage.less b/src/components/RegisterPage/RegisterPage.less @@ -1,4 +1,4 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ +/* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ @import '../variables.less'; diff --git a/src/components/TextBox/TextBox.js b/src/components/TextBox/TextBox.js @@ -1,8 +1,10 @@ /*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ -import './TextBox.less'; import React, { PropTypes } from 'react'; // eslint-disable-line no-unused-vars +import { withStyles } from '../decorators'; // eslint-disable-line no-unused-vars +import styles from './TextBox.less'; // eslint-disable-line no-unused-vars +@withStyles(styles) class TextBox { static propTypes = { diff --git a/src/components/TextBox/TextBox.less b/src/components/TextBox/TextBox.less @@ -1,4 +1,4 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ +/* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ .TextBox { &-input {} diff --git a/src/components/decorators/index.js b/src/components/decorators/index.js @@ -0,0 +1,5 @@ +/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ + +export { withContext } from './withContext'; +export { withStyles } from './withStyles'; +export { withViewport } from './withViewport'; diff --git a/src/components/decorators/setViewport.js b/src/components/decorators/setViewport.js @@ -1,58 +0,0 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ - -import React, { Component } from 'react'; // eslint-disable-line no-unused-vars -import EventEmitter from 'eventemitter3'; -import { canUseDOM } from 'react/lib/ExecutionEnvironment'; - -let EE; -let viewport = {width: 1366, height: 768}; // Default size for server-side rendering -const RESIZE_EVENT = 'resize'; - -function handleWindowResize() { - if (viewport.width !== window.innerWidth || viewport.height !== window.innerHeight) { - viewport = {width: window.innerWidth, height: window.innerHeight}; - EE.emit(RESIZE_EVENT, viewport); - } -} - -function setViewport(ComposedComponent) { - return class Viewport extends Component { - - constructor() { - super(); - - this.state = { - viewport: canUseDOM ? {width: window.innerWidth, height: window.innerHeight} : viewport - }; - } - - componentDidMount() { - if (!EE) { - EE = new EventEmitter(); - window.addEventListener('resize', handleWindowResize); - window.addEventListener('orientationchange', handleWindowResize); - } - EE.on('resize', this.handleResize, this); - } - - componentWillUnmount() { - EE.removeListener(RESIZE_EVENT, this.handleResize, this); - if (!EE.listeners(RESIZE_EVENT, true)) { - window.removeEventListener('resize', handleWindowResize); - window.removeEventListener('orientationchange', handleWindowResize); - EE = null; - } - } - - render() { - return <ComposedComponent {...this.props} viewport={this.state.viewport}/>; - } - - handleResize(value) { - this.setState({viewport: value}); - } - - }; -} - -export default setViewport; diff --git a/src/components/decorators/withContext.js b/src/components/decorators/withContext.js @@ -0,0 +1,43 @@ +/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ + +import React, { PropTypes } from 'react'; // eslint-disable-line no-unused-vars +import emptyFunction from 'react/lib/emptyFunction'; + +function withContext(ComposedComponent) { + return class WithContext { + + static propTypes = { + context: PropTypes.shape({ + onInsertCss: PropTypes.func, + onSetTitle: PropTypes.func, + onSetMeta: PropTypes.func, + onPageNotFound: PropTypes.func + }) + }; + + static childContextTypes = { + onInsertCss: PropTypes.func.isRequired, + onSetTitle: PropTypes.func.isRequired, + onSetMeta: PropTypes.func.isRequired, + onPageNotFound: PropTypes.func.isRequired + }; + + getChildContext() { + let context = this.props.context; + return { + onInsertCss: context.onInsertCss || emptyFunction, + onSetTitle: context.onSetTitle || emptyFunction, + onSetMeta: context.onSetMeta || emptyFunction, + onPageNotFound: context.onPageNotFound || emptyFunction + }; + } + + render() { + let { context, ...other } = this.props; // eslint-disable-line no-unused-vars + return <ComposedComponent {...other} />; + } + + }; +} + +export default { withContext }; diff --git a/src/components/decorators/withStyles.js b/src/components/decorators/withStyles.js @@ -0,0 +1,80 @@ +/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ + +import React, { PropTypes } from 'react'; // eslint-disable-line no-unused-vars +import invariant from 'react/lib/invariant'; +import { canUseDOM } from 'react/lib/ExecutionEnvironment'; + +let count = 0; + +function withStyles(styles) { + return function (ComposedComponent) { + + return class WithStyles { + + static contextTypes = { + onInsertCss: PropTypes.func + }; + + constructor() { + this.refCount = 0; + ComposedComponent.prototype.renderCss = function (css) { + let style; + if (canUseDOM) { + if (this.styleId && (style = document.getElementById(this.styleId))) { + if ('textContent' in style) { + style.textContent = css; + } else { + style.styleSheet.cssText = css; + } + } else { + this.styleId = `dynamic-css-${count++}`; + style = document.createElement('style'); + style.setAttribute('id', this.styleId); + style.setAttribute('type', 'text/css'); + + if ('textContent' in style) { + style.textContent = css; + } else { + style.styleSheet.cssText = css; + } + + document.getElementsByTagName('head')[0].appendChild(style); + this.refCount++; + } + } else { + this.context.onInsertCss(css); + } + }.bind(this); + } + + componentWillMount() { + if (canUseDOM) { + invariant(styles.use, `The style-loader must be configured with reference-counted API.`); + styles.use(); + } else { + this.context.onInsertCss(styles.toString()); + } + } + + componentWillUnmount() { + styles.unuse(); + if (this.styleId) { + this.refCount--; + if (this.refCount < 1) { + let style = document.getElementById(this.styleId); + if (style) { + style.parentNode.removeChild(style); + } + } + } + } + + render() { + return <ComposedComponent {...this.props} />; + } + + }; + }; +} + +export default { withStyles }; diff --git a/src/components/decorators/withViewport.js b/src/components/decorators/withViewport.js @@ -0,0 +1,62 @@ +/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ + +import React, { Component } from 'react'; // eslint-disable-line no-unused-vars +import EventEmitter from 'eventemitter3'; +import { canUseDOM } from 'react/lib/ExecutionEnvironment'; + +let EE; +let viewport = {width: 1366, height: 768}; // Default size for server-side rendering +const RESIZE_EVENT = 'resize'; + +function handleWindowResize() { + if (viewport.width !== window.innerWidth || viewport.height !== window.innerHeight) { + viewport = {width: window.innerWidth, height: window.innerHeight}; + EE.emit(RESIZE_EVENT, viewport); + } +} + +function withViewport(ComposedComponent) { + return class WithViewport extends Component { + + static displayName = (ComposedComponent.displayName && ComposedComponent.displayName.indexOf('.') !== -1 ? + ComposedComponent.displayName.substr(0, ComposedComponent.displayName.indexOf('.')) : + ComposedComponent.name) + '.' + WithViewport.name; + + constructor() { + super(); + + this.state = { + viewport: canUseDOM ? {width: window.innerWidth, height: window.innerHeight} : viewport + }; + } + + componentDidMount() { + if (!EE) { + EE = new EventEmitter(); + window.addEventListener('resize', handleWindowResize); + window.addEventListener('orientationchange', handleWindowResize); + } + EE.on('resize', this.handleResize, this); + } + + componentWillUnmount() { + EE.removeListener(RESIZE_EVENT, this.handleResize, this); + if (!EE.listeners(RESIZE_EVENT, true)) { + window.removeEventListener('resize', handleWindowResize); + window.removeEventListener('orientationchange', handleWindowResize); + EE = null; + } + } + + render() { + return <ComposedComponent {...this.props} viewport={this.state.viewport}/>; + } + + handleResize(value) { + this.setState({viewport: value}); + } + + }; +} + +export default { withViewport }; diff --git a/src/components/variables.less b/src/components/variables.less @@ -1,4 +1,4 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ +/* React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ // // Colors diff --git a/src/server.js b/src/server.js @@ -35,22 +35,25 @@ server.get('*', async (req, res, next) => { try { let uri = req.path; let notFound = false; + let css = []; let data = {description: ''}; let app = <App path={req.path} - onSetTitle={(title) => { data.title = title; }} - onSetMeta={(name, content) => { data[name] = content; }} - onPageNotFound={() => { notFound = true; }} />; + context={{ + onInsertCss: value => css.push(value), + onSetTitle: value => data.title = value, + onSetMeta: (key, value) => data[key] = value, + onPageNotFound: () => notFound = true + }} />; await db.getPage(uri); data.body = React.renderToString(app); - + data.css = css.join(''); + let html = template(data); if (notFound) { - res.status(404).send(); - } else { - let html = template(data); - res.send(html); + res.status(404); } + res.send(html); } catch (err) { next(err); } diff --git a/src/templates/index.html b/src/templates/index.html @@ -7,13 +7,14 @@ <meta name="description" content="<%- description %>"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="apple-touch-icon" href="apple-touch-icon.png"> - <script src="/app.js"></script> + <style id="css"><%= css %></style> </head> <body> <!--[if lt IE 8]> <p class="browserupgrade">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p> <![endif]--> <div id="app"><%= body %></div> + <script src="/app.js"></script> <!-- Google Analytics: change UA-XXXXX-X to be your site's ID. --> <script> (function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]= diff --git a/src/utils/Css.js b/src/utils/Css.js @@ -1,42 +0,0 @@ -/*! React Starter Kit | MIT License | http://www.reactstarterkit.com/ */ - -import { canUseDOM } from 'react/lib/ExecutionEnvironment'; - -class Css { - - set(css) { - if (!canUseDOM) { - return; - } - - let elem; - - if (!this.styleElement) { - elem = this.styleElement = document.createElement('style'); - elem.setAttribute('type', 'text/css'); - - if ('textContent' in elem) { - elem.textContent = css; - } else { - elem.styleSheet.cssText = css; - } - - this.lastCss = css; - document.getElementsByTagName('head')[0].appendChild(elem); - - } else if (this.lastCss !== css) { - - elem = this.styleElement; - - if ('textContent' in elem) { - elem.textContent = css; - } else { - elem.styleSheet.cssText = css; - } - - } - } - -} - -export default Css; diff --git a/webpack.config.js b/webpack.config.js @@ -13,11 +13,17 @@ var webpack = require('webpack'); var argv = require('minimist')(process.argv.slice(2)); var DEBUG = !argv.release; - -var AUTOPREFIXER_LOADER = 'autoprefixer-loader?{browsers:[' + - '"Android 2.3", "Android >= 4", "Chrome >= 20", "Firefox >= 24", ' + - '"Explorer >= 8", "iOS >= 6", "Opera >= 12", "Safari >= 6"]}'; - +var STYLE_LOADER = 'style-loader/useable'; +var CSS_LOADER = DEBUG ? 'css-loader' : 'css-loader?minimize'; +var AUTOPREFIXER_LOADER = 'autoprefixer-loader?{browsers:' + JSON.stringify([ + 'Android 2.3', + 'Android >= 4', + 'Chrome >= 20', + 'Firefox >= 24', + 'Explorer >= 8', + 'iOS >= 6', + 'Opera >= 12', + 'Safari >= 6']) + '}'; var GLOBALS = { 'process.env.NODE_ENV': DEBUG ? '"development"' : '"production"', '__DEV__': DEBUG @@ -64,11 +70,11 @@ var config = { loaders: [ { test: /\.css$/, - loader: 'style-loader!css-loader!' + AUTOPREFIXER_LOADER + loader: STYLE_LOADER + '!' + CSS_LOADER + '!' + AUTOPREFIXER_LOADER }, { test: /\.less$/, - loader: 'style-loader!css-loader!' + AUTOPREFIXER_LOADER + + loader: STYLE_LOADER + '!' + CSS_LOADER + '!' + AUTOPREFIXER_LOADER + '!less-loader' }, { @@ -142,7 +148,7 @@ var serverConfig = _.merge({}, config, { loaders: config.module.loaders.map(function(loader) { // Remove style-loader return _.merge(loader, { - loader: loader.loader = loader.loader.replace('style-loader!', '') + loader: loader.loader = loader.loader.replace(STYLE_LOADER + '!', '') }); }) }