From a32b05fe719da2f5b6887df71666365036a85f68 Mon Sep 17 00:00:00 2001 From: Calvin Lai Date: Sat, 3 Feb 2018 15:18:25 -0500 Subject: [PATCH] =?UTF-8?q?Feature:=20SSR=20+=20Code=20Splitting=20?= =?UTF-8?q?=F0=9F=9A=80=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * multi: Prepare for code splitting chore: Add react-loadable to dependencies chore: Move babel options to package.json so they can be retrieved more easily in webpack config chore: Remove webpack patch (https://github.com/gaearon/react-hot-loader/tree/next\#no-patch-required) chore: Update dependencies, move appropriate to devDependencies * feat: split up todos into separate components * chore: move templates to server folder instead * feat: SSR + code splitting 🚀 - Add support for import() syntax in babel and use only on the server (it is already supported on the client with webpack 2) - Update webpack config for import() code splitting - Fix an issue with static fetching for server data * chore: update readme/comments --- .babelrc | 20 - .env.example | 6 +- .gitignore | 2 + README.md | 15 +- client/index.js | 24 +- client/styles.js | 5 + client/vendor.js | 2 - common/js/components/common/Loading/index.js | 35 + common/js/components/common/index.js | 1 + common/js/components/todos/TodoForm/index.js | 54 + .../js/components/todos/TodoForm/index.scss | 0 common/js/components/todos/TodoItem/index.js | 42 + .../js/components/todos/TodoItem/index.scss | 20 + common/js/components/todos/TodoList/index.js | 32 + .../js/components/todos/TodoList/index.scss | 6 + common/js/components/todos/index.js | 3 + common/js/containers/Todos/index.js | 99 +- common/js/containers/Todos/index.scss | 23 - common/js/{containers => pages}/Home/index.js | 10 +- .../js/{containers => pages}/Home/index.scss | 0 common/js/pages/Todos/index.js | 21 + common/js/routes/index.js | 4 +- config/index.js | 8 +- nodemon.json | 4 +- package-lock.json | 3058 ++++++++++++----- package.json | 127 +- server/api/todos/index.js | 17 +- server/index.js | 57 +- server/renderer/handler.js | 37 +- server/renderer/render.js | 10 +- .../templates/layouts/application.html | 9 +- webpack/base.js | 87 +- webpack/development.hot.js | 6 - webpack/production.babel.js | 3 +- webpack/production.client.babel.js | 8 +- webpack/production.server.babel.js | 25 +- 36 files changed, 2776 insertions(+), 1104 deletions(-) delete mode 100644 .babelrc create mode 100644 client/styles.js delete mode 100644 client/vendor.js create mode 100644 common/js/components/common/Loading/index.js create mode 100644 common/js/components/todos/TodoForm/index.js create mode 100644 common/js/components/todos/TodoForm/index.scss create mode 100644 common/js/components/todos/TodoItem/index.js create mode 100644 common/js/components/todos/TodoItem/index.scss create mode 100644 common/js/components/todos/TodoList/index.js create mode 100644 common/js/components/todos/TodoList/index.scss create mode 100644 common/js/components/todos/index.js rename common/js/{containers => pages}/Home/index.js (68%) rename common/js/{containers => pages}/Home/index.scss (100%) create mode 100644 common/js/pages/Todos/index.js rename {common => server}/templates/layouts/application.html (62%) diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 373c3e5..0000000 --- a/.babelrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "presets": [ - [ - "env", { - "targets": { - "node": "current", - "uglify": true - } - }], - "react" - ], - "plugins": [ - "react-hot-loader/babel", - "transform-es2015-modules-commonjs", - "transform-class-properties", - "transform-decorators", - "transform-export-extensions", - "transform-object-rest-spread" - ] -} diff --git a/.env.example b/.env.example index 15f5d49..30a6217 100644 --- a/.env.example +++ b/.env.example @@ -6,8 +6,8 @@ APPLICATION_PORT=3000 APPLICATION_BASE_URL=http://localhost:3000 # The output path of server and client files built by webpack and babel. -OUTPUT_PATH=/dist -PUBLIC_OUTPUT_PATH=/dist/public +OUTPUT_PATH=dist +PUBLIC_OUTPUT_PATH=dist/public # Settings for webpack-dev-server. DEV_SERVER_PORT=3001 @@ -15,4 +15,4 @@ DEV_SERVER_HOSTNAME=localhost DEV_SERVER_HOST_URL=http://localhost:3001 # The primary asset path. Can be changed to be a CDN URL. -PUBLIC_ASSET_PATH=/assets +PUBLIC_ASSET_PATH=http://localhost:3001/assets/ diff --git a/.gitignore b/.gitignore index 6d1c81b..c3e3b2b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ npm-debug.log* # ignore built static files /dist /webpack-assets.json +/webpack-stats.json +/react-loadable.json diff --git a/README.md b/README.md index 74a8521..30a7f54 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,12 @@ A universal React/Redux boilerplate with sensible defaults. Out of the box, this boilerplate comes with: - Server-side rendering with Express +- Code splitting with Webpack's dynamic `import()`s and [react-loadable](https://github.com/thejameskyle/react-loadable) - Sane [webpack configurations](webpack/) - JS hot reloading with `react-hot-loader` and `webpack-dev-server` - CSS, SASS and `css-modules` support with hot reloading and no [flash of unstyled content](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) (`css-hot-loader`) - Routing with `react-router-v4` -- Full production builds that do not rely on `babel`. +- Full production builds that do not rely on `babel-node`. ## Get started @@ -161,13 +162,19 @@ Check the `.eslintignore` file for directories excluded from linting. ## Changing the public asset path -By default, assets are built into `/dist/public`. This path is then served by -express under the path `/assets`. This is the public asset path. In a production +By default, assets are built into `dist/public`. This path is then served by +express under the path `assets`. This is the public asset path. In a production scenario, you may want your assets to be hosted on a CDN. To do so, just change the `PUBLIC_ASSET_PATH` environment variant. -If you're using Heroku: +Example using Heroku, if serving via CDN: ``` heroku config:set PUBLIC_ASSET_PATH=https://my.cdn.com ``` + +Example using Heroku, if serving locally: + +``` +heroku config:set PUBLIC_ASSET_PATH=/assets +``` diff --git a/client/index.js b/client/index.js index 3483fbc..85484f2 100644 --- a/client/index.js +++ b/client/index.js @@ -1,5 +1,4 @@ import 'babel-polyfill'; -import 'css/base/index.scss'; import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; @@ -7,6 +6,9 @@ import { ConnectedRouter } from 'react-router-redux'; import createHistory from 'history/createBrowserHistory'; import configureStore from 'store'; import App from 'containers/App'; +import Loadable from 'react-loadable'; + +import './styles'; // Hydrate the redux store from server state. const initialState = window.__INITIAL_STATE__; @@ -14,11 +16,15 @@ const history = createHistory(); const store = configureStore(initialState, history); // Render the application -ReactDOM.hydrate( - - - - - , - document.getElementById('app') -); +window.main = () => { + Loadable.preloadReady().then(() => { + ReactDOM.hydrate( + + + + + , + document.getElementById('app') + ); + }); +}; diff --git a/client/styles.js b/client/styles.js new file mode 100644 index 0000000..ebc7e86 --- /dev/null +++ b/client/styles.js @@ -0,0 +1,5 @@ +// Include all Vendor CSS here +import 'semantic-ui-css/semantic.min.css'; + +// Base styles +import 'css/base/index.scss'; diff --git a/client/vendor.js b/client/vendor.js deleted file mode 100644 index 9b7dfae..0000000 --- a/client/vendor.js +++ /dev/null @@ -1,2 +0,0 @@ -// Include all Vendor CSS here. It will be bundled as vendor.css. -import 'semantic-ui-css/semantic.min.css'; diff --git a/common/js/components/common/Loading/index.js b/common/js/components/common/Loading/index.js new file mode 100644 index 0000000..9221aff --- /dev/null +++ b/common/js/components/common/Loading/index.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const Loading = (props) => { + const { error, timedOut, pastDelay } = props; + + console.log(props); + + if (error) { + // When the loader has errored + return ( +
Error!
+ ); + } else if (timedOut) { + // When the loader has taken longer than the timeout + return ( +
Taking a long time...
+ ); + } else if (pastDelay) { + // When the loader has taken longer than the delay + return ( +
Loading...
+ ); + } + + return null; +}; + +Loading.propTypes = { + error: PropTypes.bool, + timedOut: PropTypes.bool, + pastDelay: PropTypes.bool +}; + +export default Loading; diff --git a/common/js/components/common/index.js b/common/js/components/common/index.js index 5b01228..59c2c6a 100644 --- a/common/js/components/common/index.js +++ b/common/js/components/common/index.js @@ -1,4 +1,5 @@ export { default as ErrorPage } from './ErrorPage'; export { default as Footer } from './Footer'; export { default as Header } from './Header'; +export { default as Loading } from './Loading'; export { default as RouteWithSubRoutes } from './RouteWithSubRoutes'; diff --git a/common/js/components/todos/TodoForm/index.js b/common/js/components/todos/TodoForm/index.js new file mode 100644 index 0000000..a49d602 --- /dev/null +++ b/common/js/components/todos/TodoForm/index.js @@ -0,0 +1,54 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Form } from 'semantic-ui-react'; +import classnames from 'classnames/bind'; +import css from './index.scss'; + +class TodoForm extends Component { + static propTypes = { + onSubmit: PropTypes.func, + className: PropTypes.string + }; + + state = { todoText: '' }; + + submitTodo = ev => { + ev.preventDefault(); + + const { onSubmit } = this.props; + const { todoText } = this.state; + + if (todoText && todoText !== '' && typeof onSubmit === 'function') { + onSubmit(todoText); + this.setState({ todoText: '' }); + } + }; + + onTodoChange = ev => { + this.setState({ todoText: ev.target.value }); + }; + + render() { + const { className } = this.props; + const { todoText } = this.state; + + return ( +
+ + + + +
+ ); + } +} + +export default TodoForm; diff --git a/common/js/components/todos/TodoForm/index.scss b/common/js/components/todos/TodoForm/index.scss new file mode 100644 index 0000000..e69de29 diff --git a/common/js/components/todos/TodoItem/index.js b/common/js/components/todos/TodoItem/index.js new file mode 100644 index 0000000..4836c95 --- /dev/null +++ b/common/js/components/todos/TodoItem/index.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { List, Checkbox, Button } from 'semantic-ui-react'; +import classnames from 'classnames/bind'; +import css from './index.scss'; + +const cx = classnames.bind(css); + +const TodoItem = props => { + const { onRemove, onChange, todo: { id, completed, text } } = props; + + return ( + + +