├── webpack ├── configuration │ ├── name.js │ ├── devtool.js │ ├── extensions.js │ ├── target.js │ ├── externals.js │ ├── context.js │ ├── modules.js │ ├── output.js │ ├── entry.js │ ├── index.js │ ├── rules.js │ └── plugins.js ├── webpack.config.common.js ├── webpack.config.client.js └── webpack.config.server.js ├── src ├── shared │ ├── styles │ │ ├── _vars.scss │ │ ├── _global-mixins.scss │ │ └── _global.scss │ ├── utils │ │ ├── is.js │ │ ├── data.js │ │ ├── device.js │ │ ├── environment.js │ │ └── api.js │ ├── reducers │ │ ├── deviceReducer.js │ │ └── index.js │ ├── configureStore.js │ └── routes.js ├── app │ ├── blog │ │ ├── components │ │ │ ├── Posts.scss │ │ │ └── Posts.js │ │ ├── constants.js │ │ ├── actionTypes.js │ │ ├── api.js │ │ ├── reducer.js │ │ ├── actions.js │ │ └── index.js │ ├── home │ │ └── components │ │ │ ├── Home.scss │ │ │ └── Home.js │ ├── about │ │ └── components │ │ │ └── About.js │ ├── App.js │ └── client.js └── server │ ├── api │ ├── index.js │ └── data │ │ └── posts.js │ ├── clientRender.js │ ├── html.js │ ├── serverRender.js │ └── index.js ├── webpack.config.js ├── .editorconfig ├── .babelrc ├── .gitignore ├── README.md ├── package.json ├── package.windows.json └── .eslintrc /webpack/configuration/name.js: -------------------------------------------------------------------------------- 1 | export default type => type; 2 | -------------------------------------------------------------------------------- /webpack/configuration/devtool.js: -------------------------------------------------------------------------------- 1 | export default () => 'eval'; 2 | -------------------------------------------------------------------------------- /src/shared/styles/_vars.scss: -------------------------------------------------------------------------------- 1 | $white: #fff; 2 | $black: #000; 3 | $gray: #ccc; 4 | -------------------------------------------------------------------------------- /webpack/configuration/extensions.js: -------------------------------------------------------------------------------- 1 | export default () => ['.js', '.jsx', '.json', '.css']; 2 | -------------------------------------------------------------------------------- /webpack/configuration/target.js: -------------------------------------------------------------------------------- 1 | export default type => type === 'server' ? 'node' : 'web'; 2 | -------------------------------------------------------------------------------- /src/app/blog/components/Posts.scss: -------------------------------------------------------------------------------- 1 | .posts { 2 | background-color: #ccc; 3 | color: blue; 4 | } 5 | -------------------------------------------------------------------------------- /src/shared/styles/_global-mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin border($color: red) { 2 | border: 1px solid $color; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/blog/constants.js: -------------------------------------------------------------------------------- 1 | export const API = Object.freeze({ 2 | BLOG: { 3 | POSTS: 'blog/posts' 4 | } 5 | }); 6 | -------------------------------------------------------------------------------- /src/app/home/components/Home.scss: -------------------------------------------------------------------------------- 1 | @import '../../../shared/styles/global'; 2 | 3 | .home { 4 | border: 1px solid red; 5 | } 6 | -------------------------------------------------------------------------------- /src/shared/styles/_global.scss: -------------------------------------------------------------------------------- 1 | @import './vars'; 2 | @import './global-mixins'; 3 | 4 | body { 5 | background-color: $gray; 6 | } 7 | -------------------------------------------------------------------------------- /src/shared/utils/is.js: -------------------------------------------------------------------------------- 1 | export function isDefined(variable) { 2 | return typeof variable !== 'undefined' && variable !== null; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/blog/actionTypes.js: -------------------------------------------------------------------------------- 1 | // Actions 2 | export const FETCH_POSTS = { 3 | request: () => 'FETCH_POSTS_REQUEST', 4 | success: () => 'FETCH_POSTS_SUCCESS', 5 | error: () => 'FETCH_POSTS_ERROR' 6 | }; 7 | -------------------------------------------------------------------------------- /src/shared/utils/data.js: -------------------------------------------------------------------------------- 1 | // Utils 2 | import { isDefined } from './is'; 3 | 4 | export function isFirstRender(items) { 5 | return !isDefined(items) || items.length === 0 || Object.keys(items).length === 0; 6 | } 7 | -------------------------------------------------------------------------------- /webpack/configuration/externals.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import nodeExternals from 'webpack-node-externals'; 3 | 4 | export default () => [ 5 | nodeExternals({ 6 | whitelist: [/^redux\/(store|modules)/] 7 | }) 8 | ]; 9 | -------------------------------------------------------------------------------- /webpack/configuration/context.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import path from 'path'; 3 | 4 | export default type => type === 'server' 5 | ? path.resolve(__dirname, '../../src/server') 6 | : path.resolve(__dirname, '../../src/app'); 7 | -------------------------------------------------------------------------------- /webpack/configuration/modules.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import path from 'path'; 3 | 4 | export default () => [ 5 | 'node_modules', 6 | path.resolve(__dirname, '../../src/app'), 7 | path.resolve(__dirname, '../../src/server') 8 | ]; 9 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // Webpack Configuration (Client & Server) 2 | import clientConfig from './webpack/webpack.config.client'; 3 | import serverConfig from './webpack/webpack.config.server'; 4 | 5 | export default [ 6 | clientConfig, 7 | serverConfig 8 | ]; 9 | -------------------------------------------------------------------------------- /src/server/api/index.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import express from 'express'; 3 | import posts from './data/posts'; 4 | 5 | // Express Router 6 | const router = express.Router(); 7 | 8 | router.get('/blog/posts', (req, res) => res.json(posts)); 9 | 10 | export default router; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.scss] 12 | indent_size = 4 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /src/shared/reducers/deviceReducer.js: -------------------------------------------------------------------------------- 1 | export default function deviceReducer(state = {}) { 2 | let isMobile = false; 3 | 4 | if (state.isMobile) { 5 | isMobile = state.isMobile === 'false' ? false : true; 6 | } 7 | 8 | return { 9 | ...state, 10 | isMobile 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /webpack/webpack.config.common.js: -------------------------------------------------------------------------------- 1 | // Configuration 2 | import { rules, extensions, modules } from './configuration'; 3 | 4 | export default type => ({ 5 | module: { 6 | rules: rules(type) 7 | }, 8 | resolve: { 9 | extensions: extensions(), 10 | modules: modules() 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /src/app/about/components/About.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React, { Component } from 'react'; 3 | 4 | class About extends Component { 5 | render() { 6 | return ( 7 |
8 | About 9 |
10 | ); 11 | } 12 | } 13 | 14 | export default About; 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-0", 6 | "stage-2" 7 | ], 8 | "env": { 9 | "production": { 10 | "presets": ["react-optimize"] 11 | }, 12 | "development": { 13 | "plugins": [ 14 | "react-hot-loader/babel" 15 | ] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/blog/api.js: -------------------------------------------------------------------------------- 1 | // Constants 2 | import { API } from './constants'; 3 | 4 | // Utils 5 | import { apiFetch } from '../../shared/utils/api'; 6 | 7 | class BlogApi { 8 | static getAllPosts(query = {}, fetchingFrom = 'client') { 9 | return apiFetch(API.BLOG.POSTS, { fetchingFrom }, query); 10 | } 11 | } 12 | 13 | export default BlogApi; 14 | -------------------------------------------------------------------------------- /src/shared/reducers/index.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import { combineReducers } from 'redux'; 3 | 4 | // Containers Reducers 5 | import blog from '../../app/blog/reducer'; 6 | 7 | // Shared Reducers 8 | import device from './deviceReducer'; 9 | 10 | const rootReducer = combineReducers({ 11 | blog, 12 | device 13 | }); 14 | 15 | export default rootReducer; 16 | -------------------------------------------------------------------------------- /src/server/clientRender.js: -------------------------------------------------------------------------------- 1 | // HTML 2 | import html from './html'; 3 | 4 | export default function clientRender() { 5 | return (req, res, next) => { 6 | if (req.isBot) { 7 | return next(); 8 | } 9 | 10 | const initialState = { 11 | device: { 12 | isMobile: res.locals.isMobile 13 | } 14 | }; 15 | 16 | res.send(html({ 17 | markup: '', 18 | initialState 19 | })); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/blog/reducer.js: -------------------------------------------------------------------------------- 1 | // Action Types 2 | import { FETCH_POSTS } from './actionTypes'; 3 | 4 | const initialState = { 5 | posts: [] 6 | }; 7 | 8 | export default function blogReducer(state = initialState, action) { 9 | switch (action.type) { 10 | case FETCH_POSTS.success(): { 11 | return { 12 | ...state, 13 | posts: action.payload 14 | }; 15 | } 16 | 17 | default: 18 | return state; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/shared/configureStore.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | 5 | // Root Reducer 6 | import rootReducer from './reducers'; 7 | 8 | export default function configureStore(initialState) { 9 | const middleware = [ 10 | thunk 11 | ]; 12 | 13 | return createStore( 14 | rootReducer, 15 | initialState, 16 | applyMiddleware(...middleware) 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/shared/utils/device.js: -------------------------------------------------------------------------------- 1 | export function getCurrentDevice(ua) { 2 | return /mobile/i.test(ua) ? 'mobile' : 'desktop'; 3 | } 4 | 5 | export function isBot(ua) { 6 | return /curl|bot|googlebot|google|baidu|bing|msn|duckduckgo|teoma|slurp|crawler|spider|robot|crawling/i.test(ua); 7 | } 8 | 9 | export function isDesktop(ua) { 10 | return !/mobile/i.test(ua); 11 | } 12 | 13 | export function isMobile(ua) { 14 | return /mobile/i.test(ua); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/home/components/Home.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React, { Component } from 'react'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | // Styles 6 | import styles from './Home.scss'; 7 | 8 | class Home extends Component { 9 | render() { 10 | return ( 11 |
12 | Home - About - Blog 13 |
14 | ); 15 | } 16 | } 17 | 18 | export default Home; 19 | -------------------------------------------------------------------------------- /webpack/configuration/output.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import path from 'path'; 3 | 4 | export default type => { 5 | if (type === 'server') { 6 | return { 7 | filename: 'server.js', 8 | path: path.resolve(__dirname, '../../dist'), 9 | libraryTarget: 'commonjs2' 10 | }; 11 | } 12 | 13 | return { 14 | filename: '[name].bundle.js', 15 | path: path.resolve(__dirname, '../../public/app'), 16 | publicPath: '/app/' 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /src/shared/routes.js: -------------------------------------------------------------------------------- 1 | // Components 2 | import Home from '../app/home/components/Home'; 3 | import About from '../app/about/components/About'; 4 | 5 | // Containers 6 | import Blog from '../app/blog'; 7 | 8 | const routes = [ 9 | { 10 | path: '/', 11 | component: Home, 12 | exact: true 13 | }, 14 | { 15 | path: '/about', 16 | component: About 17 | }, 18 | { 19 | path: '/blog', 20 | component: Blog 21 | } 22 | ]; 23 | 24 | export default routes; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Dependency directories 9 | node_modules/ 10 | 11 | # Optional eslint cache 12 | .eslintcache 13 | 14 | # Yarn Integrity file 15 | .yarn-integrity 16 | 17 | # Build directory 18 | dist/ 19 | 20 | # Public 21 | *.hot-update.js 22 | *.hot-update.json 23 | public/css/style.css 24 | public/**/*.bundle.js 25 | public/**/*.bundle.js.gz 26 | public/app/report.html 27 | 28 | package-lock.json 29 | -------------------------------------------------------------------------------- /webpack/configuration/entry.js: -------------------------------------------------------------------------------- 1 | // Environment 2 | const isDevelopment = process.env.NODE_ENV !== 'production'; 3 | 4 | export default type => { 5 | if (type === 'server') { 6 | return './serverRender.js'; 7 | } 8 | 9 | const entry = { 10 | main: [] 11 | }; 12 | 13 | if (isDevelopment) { 14 | entry.main.push( 15 | 'webpack-hot-middleware/client', 16 | 'react-hot-loader/patch' 17 | ); 18 | } 19 | 20 | entry.main.push('./client.js'); 21 | 22 | return entry; 23 | }; 24 | -------------------------------------------------------------------------------- /src/app/blog/actions.js: -------------------------------------------------------------------------------- 1 | // Api 2 | import blogApi from './api'; 3 | 4 | // Action Types 5 | import { FETCH_POSTS } from './actionTypes'; 6 | 7 | export const fetchPosts = (fetchingFrom, query) => dispatch => { 8 | const requestPosts = () => ({ 9 | type: FETCH_POSTS.request() 10 | }); 11 | 12 | const receivedPosts = posts => ({ 13 | type: FETCH_POSTS.success(), 14 | payload: posts 15 | }); 16 | 17 | dispatch(requestPosts()); 18 | 19 | return blogApi.getAllPosts(query, fetchingFrom) 20 | .then(posts => dispatch(receivedPosts(posts))); 21 | }; 22 | -------------------------------------------------------------------------------- /webpack/webpack.config.client.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import webpackMerge from 'webpack-merge'; 3 | 4 | // Webpack Configuration 5 | import commonConfig from './webpack.config.common'; 6 | import { context, devtool, entry, name, output, plugins, target } from './configuration'; 7 | 8 | // Type of Configuration 9 | const type = 'client'; 10 | 11 | export default webpackMerge(commonConfig(type), { 12 | context: context(type), 13 | devtool: devtool(type), 14 | entry: entry(type), 15 | name: name(type), 16 | output: output(type), 17 | plugins: plugins(type), 18 | target: target(type) 19 | }); 20 | -------------------------------------------------------------------------------- /webpack/configuration/index.js: -------------------------------------------------------------------------------- 1 | // Configuration 2 | import context from './context'; 3 | import devtool from './devtool'; 4 | import entry from './entry'; 5 | import extensions from './extensions'; 6 | import externals from './externals'; 7 | import modules from './modules'; 8 | import name from './name'; 9 | import output from './output'; 10 | import plugins from './plugins'; 11 | import rules from './rules'; 12 | import target from './target'; 13 | 14 | export { 15 | context, 16 | devtool, 17 | entry, 18 | extensions, 19 | externals, 20 | modules, 21 | name, 22 | output, 23 | plugins, 24 | rules, 25 | target 26 | }; 27 | -------------------------------------------------------------------------------- /webpack/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import webpackMerge from 'webpack-merge'; 3 | 4 | // Webpack Configuration 5 | import commonConfig from './webpack.config.common'; 6 | 7 | // Configuration 8 | import { context, entry, externals, name, output, plugins, target } from './configuration'; 9 | 10 | // Type of Configuration 11 | const type = 'server'; 12 | 13 | export default webpackMerge(commonConfig(type), { 14 | context: context(type), 15 | entry: entry(type), 16 | externals: externals(type), 17 | name: name(type), 18 | output: output(type), 19 | plugins: plugins(type), 20 | target: target(type) 21 | }); 22 | -------------------------------------------------------------------------------- /src/server/api/data/posts.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | id: 1, 4 | title: 'Test 1', 5 | author: 'Carlos', 6 | date: new Date(Date.now() - 15000000) 7 | }, 8 | { 9 | id: 2, 10 | title: 'Test 2', 11 | author: 'Cristina', 12 | date: new Date(Date.now() - 15000000) 13 | }, 14 | { 15 | id: 3, 16 | title: 'Test 3', 17 | author: 'Carlos', 18 | date: new Date(Date.now() - 15000000) 19 | }, 20 | { 21 | id: 4, 22 | title: 'Test 4', 23 | author: 'Carlos', 24 | date: new Date(Date.now() - 15000000) 25 | }, 26 | { 27 | id: 5, 28 | title: 'Test 5', 29 | author: 'Carlos', 30 | date: new Date(Date.now() - 15000000) 31 | } 32 | ]; 33 | -------------------------------------------------------------------------------- /src/shared/utils/environment.js: -------------------------------------------------------------------------------- 1 | // Available environments 2 | const environments = ['development', 'stage', 'qa', 'production']; 3 | 4 | export function getEnvironment(env = false) { 5 | const environment = env || process.env.NODE_ENV; 6 | 7 | return isEnvironment(environment) ? environment : 'production'; 8 | } 9 | 10 | export function isDevelopment() { 11 | return getEnvironment() === 'development'; 12 | } 13 | 14 | export function isEnvironment(env) { 15 | return environments.indexOf(env) !== -1; 16 | } 17 | 18 | export function isProduction() { 19 | return getEnvironment() === 'production'; 20 | } 21 | 22 | export function isQA() { 23 | return getEnvironment() === 'qa'; 24 | } 25 | 26 | export function isStage() { 27 | return getEnvironment() === 'stage'; 28 | } 29 | -------------------------------------------------------------------------------- /src/app/blog/index.js: -------------------------------------------------------------------------------- 1 | // Dependencices 2 | import React, { Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | 5 | // Components 6 | import Posts from './components/Posts'; 7 | 8 | // Action 9 | import { fetchPosts } from './actions'; 10 | 11 | // Utils 12 | import { isFirstRender } from '../../shared/utils/data'; 13 | 14 | class Blog extends Component { 15 | static initialAction(fetchFrom) { 16 | return fetchPosts(fetchFrom); 17 | } 18 | 19 | componentDidMount() { 20 | if (isFirstRender(this.props.posts)) { 21 | this.props.dispatch(Blog.initialAction('client')); 22 | } 23 | } 24 | 25 | render() { 26 | const { posts } = this.props; 27 | 28 | return ; 29 | } 30 | } 31 | 32 | export default connect(({ blog }) => ({ 33 | posts: blog.posts 34 | }), null)(Blog); 35 | -------------------------------------------------------------------------------- /src/app/App.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import { BrowserRouter, StaticRouter, Switch, Route } from 'react-router-dom'; 4 | 5 | // Routes 6 | import routes from '../shared/routes'; 7 | 8 | export default ({ server, location, context }) => { 9 | const routesMap = routes.map((route, i) => ); 10 | 11 | // Client Router 12 | let router = ( 13 | 14 | 15 | {routesMap} 16 | 17 | 18 | ); 19 | 20 | // Server Router 21 | if (server) { 22 | router = ( 23 | 24 | 25 | {routesMap} 26 | 27 | 28 | ); 29 | } 30 | 31 | return ( 32 |
33 | {router} 34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/server/html.js: -------------------------------------------------------------------------------- 1 | export default function html(options) { 2 | const { 3 | app = 'main', 4 | title = 'Codejobs', 5 | stylesheet = '/css/style.css', 6 | markup, 7 | initialState 8 | } = options; 9 | 10 | return ` 11 | 12 | 13 | 14 | 15 | ${title} 16 | 17 | 18 | 19 | 20 | 21 |
${markup}
22 | 23 | 26 | 27 | 28 | 29 | 30 | `; 31 | } 32 | -------------------------------------------------------------------------------- /src/app/client.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import 'babel-polyfill'; 3 | import React from 'react'; 4 | import { render } from 'react-dom'; 5 | import { AppContainer } from 'react-hot-loader'; 6 | import { Provider } from 'react-redux'; 7 | 8 | // Redux Store 9 | import configureStore from '../shared/configureStore'; 10 | 11 | // Containers 12 | import App from './App'; 13 | 14 | // Configuring Redux Store 15 | const store = configureStore(window.initialState); 16 | 17 | // DOM 18 | const rootElement = document.getElementById('root'); 19 | 20 | // App Wrapper 21 | const renderApp = Component => { 22 | render( 23 | 24 | 25 | 26 | 27 | , 28 | rootElement 29 | ); 30 | }; 31 | 32 | // Rendering app 33 | renderApp(App); 34 | 35 | // HMR 36 | if (module.hot) { 37 | module.hot.accept('./App', () => { 38 | renderApp(require('./App').default); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /src/app/blog/components/Posts.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React, { Component } from 'react'; 3 | import timeAgo from 'node-time-ago'; 4 | 5 | // Utils 6 | import { isFirstRender } from '../../../shared/utils/data'; 7 | 8 | // Styles 9 | import styles from './Posts.scss'; 10 | 11 | class Posts extends Component { 12 | render() { 13 | const { posts } = this.props; 14 | 15 | if (isFirstRender(posts)) { 16 | return null; 17 | } 18 | 19 | return ( 20 |
21 |
22 |

Blog

23 |
24 | 25 | {posts && posts.map(post => 26 |
27 |

28 | {post.id} - {post.title} by {posts.author} 29 |

30 | 31 |

32 | {timeAgo(post.date)} 33 |

34 |
35 | )} 36 |
37 | ); 38 | } 39 | } 40 | 41 | export default Posts; 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Pro 2 | 3 | # Temas del Curso 4 | 5 | * Instalación de Node 8 y NPM 5 6 | * Instalación de ambiente 7 | * Instalación y Configuración de Atom 8 | * EditorConfig 9 | * Eslint 10 | * .babelrc 11 | 12 | * Configuración de NPM Scripts 13 | * build 14 | * clean 15 | * lint 16 | * git-hooks (precommit, prepush, etc.) 17 | * start 18 | * start-analyzer 19 | * start-production 20 | * test 21 | * watch-client 22 | * watch-server 23 | 24 | * Configuración de Webpack 25 | * Cliente 26 | * Servidor 27 | * Optimizaciones de Bundle 28 | * División de bundles 29 | * Webpack Hot Middleware / Webpack Dev Middleware / Webpack Hot Server Middleware 30 | 31 | * Estructuración del Proyecto 32 | 33 | * Server Side Rendering 34 | * Introducción 35 | * Data con State local 36 | * Data con Redux 37 | * Utilizar SSR sólo cuando detectamos un Search Bot 38 | 39 | * Node.js 40 | * API 41 | * Configuraciones de Express 42 | * Compresión GZip 43 | 44 | # Solución de Problemas 45 | 46 | ## Windows 47 | 48 | Si utilizas Windows, debes configurar tu package.json de la siguiente manera: https://github.com/MilkZoft/react-pro/blob/master/package.windows.json 49 | -------------------------------------------------------------------------------- /src/shared/utils/api.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import 'isomorphic-fetch'; 3 | import queryString from 'query-string'; 4 | 5 | export function apiEndpoint(endpoint, qs, fetchingFrom) { 6 | let query = ''; 7 | let apiUrl = ''; 8 | 9 | if (qs) { 10 | query = `?${qs}`; 11 | } 12 | 13 | if (fetchingFrom === 'server') { 14 | apiUrl = 'http://localhost:3000'; 15 | } 16 | 17 | return `${apiUrl}/api/${endpoint}${query}`; 18 | } 19 | 20 | export function apiFetch(endpoint, options = {}, query = false) { 21 | let qs; 22 | const { fetchingFrom = 'client' } = options; 23 | 24 | delete options.fetchFrom; 25 | 26 | if (query) { 27 | qs = queryString.stringify(query); 28 | } 29 | 30 | const fetchOptions = apiOptions(options); 31 | const fetchEndpoint = apiEndpoint(endpoint, qs, fetchingFrom); 32 | 33 | return fetch(fetchEndpoint, fetchOptions).then(response => response.json()); 34 | } 35 | 36 | export function apiOptions(options = {}) { 37 | const { 38 | method = 'GET', 39 | headers = { 40 | 'Content-Type': 'application/json' 41 | }, 42 | body = false 43 | } = options; 44 | 45 | const newOptions = { 46 | method, 47 | headers, 48 | credentials: 'include' 49 | }; 50 | 51 | if (body) { 52 | newOptions.body = body; 53 | } 54 | 55 | return newOptions; 56 | } 57 | -------------------------------------------------------------------------------- /webpack/configuration/rules.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 3 | 4 | // Environment 5 | const isDevelopment = process.env.NODE_ENV !== 'production'; 6 | 7 | // Package.json 8 | import pkg from '../../package.json'; 9 | 10 | export default type => { 11 | const rules = [ 12 | { 13 | test: /\.js$/, 14 | use: { 15 | loader: 'babel-loader', 16 | query: { 17 | presets: [ 18 | [ 19 | 'env', { 20 | modules: false, 21 | node: pkg.engines.node, 22 | browsers: pkg.browserslist 23 | } 24 | ] 25 | ] 26 | } 27 | }, 28 | exclude: /node_modules/ 29 | } 30 | ]; 31 | 32 | if (!isDevelopment || type === 'server') { 33 | rules.push({ 34 | test: /\.scss$/, 35 | use: ExtractTextPlugin.extract({ 36 | fallback: 'style-loader', 37 | use: [ 38 | 'css-loader?minimize=true&modules=true&localIdentName=[name]__[local]', 39 | 'sass-loader' 40 | ] 41 | }) 42 | }); 43 | } else { 44 | rules.push({ 45 | test: /\.scss$/, 46 | use: [ 47 | 'style-loader', 48 | 'css-loader?minimize=true&modules=true&localIdentName=[name]__[local]', 49 | 'sass-loader' 50 | ] 51 | }); 52 | } 53 | 54 | return rules; 55 | }; 56 | -------------------------------------------------------------------------------- /src/server/serverRender.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import React from 'react'; 3 | import { renderToString } from 'react-dom/server'; 4 | import { matchPath } from 'react-router-dom'; 5 | import { Provider } from 'react-redux'; 6 | 7 | // Redux Store 8 | import configureStore from '../shared/configureStore'; 9 | 10 | // Containers 11 | import App from '../app/App'; 12 | 13 | // HTML 14 | import html from './html'; 15 | 16 | // Routes 17 | import routes from '../shared/routes'; 18 | 19 | export default function serverRender() { 20 | return (req, res, next) => { 21 | // Configure Redux Store 22 | const store = configureStore(); 23 | 24 | const promises = routes.reduce((acc, route) => { 25 | if (matchPath(req.url, route) && route.component && route.component.initialAction) { 26 | acc.push(Promise.resolve(store.dispatch(route.component.initialAction('server')))); 27 | } 28 | 29 | return acc; 30 | }, []); 31 | 32 | Promise.all(promises) 33 | .then(() => { 34 | const context = {}; 35 | const initialState = store.getState(); 36 | 37 | const markup = renderToString( 38 | 39 | 44 | 45 | ); 46 | 47 | if (context.url) { 48 | res.redirect(301, context.url); 49 | } else { 50 | res.send(html({ 51 | markup, 52 | initialState 53 | })); 54 | } 55 | }) 56 | .catch(e => { 57 | console.log('Promise error: ', e); // eslint-disable-line 58 | }); 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /webpack/configuration/plugins.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import CompressionPlugin from 'compression-webpack-plugin'; 3 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 4 | import webpack from 'webpack'; 5 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 6 | 7 | // Environment 8 | const isDevelopment = process.env.NODE_ENV !== 'production'; 9 | 10 | // Analyzer 11 | const isAnalyzer = process.env.ANALYZER === 'true'; 12 | 13 | export default type => { 14 | const plugins = [ 15 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 16 | new ExtractTextPlugin({ 17 | filename: '../../public/css/style.css' 18 | }) 19 | ]; 20 | 21 | if (isAnalyzer) { 22 | plugins.push( 23 | new BundleAnalyzerPlugin({ 24 | analyzerMode: 'static' 25 | }) 26 | ); 27 | } 28 | 29 | if (type === 'client') { 30 | plugins.push( 31 | new webpack.optimize.CommonsChunkPlugin({ 32 | name: 'vendor', 33 | minChunks: m => /node_modules/.test(m.context) 34 | }) 35 | ); 36 | } 37 | 38 | if (isDevelopment) { 39 | plugins.push( 40 | new webpack.HotModuleReplacementPlugin(), 41 | new webpack.NoEmitOnErrorsPlugin() 42 | ); 43 | } else { 44 | plugins.push( 45 | new webpack.DefinePlugin({ 46 | 'process.env': { 47 | 'NODE_ENV': JSON.stringify('production') 48 | } 49 | }), 50 | new webpack.optimize.AggressiveMergingPlugin(), 51 | new webpack.optimize.UglifyJsPlugin({ minimize: true }), 52 | new CompressionPlugin({ 53 | asset: '[path].gz[query]', 54 | algorithm: 'gzip', 55 | test: /\.js$|\.css$|\.html$/, 56 | threshold: 10240, 57 | minRatio: 0.8 58 | }) 59 | ); 60 | } 61 | 62 | return plugins; 63 | }; 64 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import express from 'express'; 3 | import open from 'open'; 4 | import path from 'path'; 5 | import webpack from 'webpack'; 6 | import webpackDevMiddleware from 'webpack-dev-middleware'; 7 | import webpackHotMiddleware from 'webpack-hot-middleware'; 8 | import webpackHotServerMiddleware from 'webpack-hot-server-middleware'; 9 | 10 | // Utils 11 | import { isMobile, isBot } from '../shared/utils/device'; 12 | 13 | // Webpack Configuration 14 | import webpackConfig from '../../webpack.config'; 15 | 16 | // Client Render 17 | import clientRender from './clientRender'; 18 | 19 | // API 20 | import api from './api'; 21 | 22 | // Environment 23 | const isDevelopment = process.env.NODE_ENV !== 'production'; 24 | 25 | // Analyzer 26 | const isAnalyzer = process.env.ANALYZER === 'true'; 27 | 28 | // Express app 29 | const app = express(); 30 | const compiler = webpack(webpackConfig); 31 | const port = process.env.NODE_PORT || 3000; 32 | 33 | // GZip Compression just for Production 34 | if (!isDevelopment) { 35 | app.get('*.js', (req, res, next) => { 36 | req.url = `${req.url}.gz`; 37 | res.set('Content-Encoding', 'gzip'); 38 | 39 | next(); 40 | }); 41 | } 42 | 43 | // Public static 44 | app.use(express.static(path.join(__dirname, '../../public'))); 45 | 46 | // API Middleware 47 | app.use('/api', api); 48 | 49 | // Device Detection 50 | app.use((req, res, next) => { 51 | req.isBot = isBot(req.headers['user-agent']); 52 | req.isMobile = isMobile(req.headers['user-agent']); 53 | 54 | return next(); 55 | }); 56 | 57 | if (isDevelopment) { 58 | // Hot Module Replacement 59 | app.use(webpackDevMiddleware(compiler)); 60 | app.use(webpackHotMiddleware(compiler.compilers.find(compiler => compiler.name === 'client'))); 61 | } 62 | 63 | // Client Side Rendering 64 | app.use(clientRender()); 65 | 66 | if (!isDevelopment) { 67 | try { 68 | const serverRender = require('../../dist/server.js').default; 69 | 70 | app.use(serverRender()); 71 | } catch (e) { 72 | throw e; 73 | } 74 | } 75 | 76 | // For Server Side Rendering on Development Mode 77 | app.use(webpackHotServerMiddleware(compiler)); 78 | 79 | // Listening 80 | app.listen(port, err => { 81 | if (!err && !isAnalyzer) { 82 | open(`http://localhost:${port}`); 83 | } 84 | }); 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pro", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "npm run clean && npm run build-client && npm run build-server", 6 | "build-client": "NODE_ENV=production BABEL_ENV=production node -r babel-register node_modules/.bin/webpack --progress --display-modules --config webpack/webpack.config.client.js", 7 | "build-server": "NODE_ENV=production BABEL_ENV=production node -r babel-register node_modules/.bin/webpack --progress --config webpack/webpack.config.server.js", 8 | "clean": "rimraf dist/ && rimraf public/app", 9 | "lint": "eslint src", 10 | "postmerge": "npm install", 11 | "postrewrite": "npm install", 12 | "precommit": "npm run lint", 13 | "prepush": "npm run test", 14 | "start": "npm run clean & NODE_ENV=development BABEL_ENV=development babel-node src/server", 15 | "start-analyzer": "npm run clean && NODE_ENV=development BABEL_ENV=development ANALYZER=true babel-node src/server", 16 | "start-production": "npm run build && NODE_ENV=production BABEL_ENV=production babel-node src/server", 17 | "test": "echo \"Error: no test specified\"", 18 | "watch-client": "webpack --watch --config webpack/webpack.config.client.js", 19 | "watch-server": "webpack --watch --config webpack/webpack.config.server.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/MilkZoft/react-pro.git" 24 | }, 25 | "engines": { 26 | "node": "current" 27 | }, 28 | "browserslist": [ 29 | "> 1%", 30 | "last 2 versions" 31 | ], 32 | "author": "Carlos Santana", 33 | "license": "MIT", 34 | "dependencies": { 35 | "express": "^4.15.4", 36 | "node-time-ago": "^1.0.0", 37 | "react": "^15.6.1", 38 | "react-dom": "^15.6.1", 39 | "react-redux": "^5.0.6", 40 | "react-router-dom": "^4.2.2", 41 | "redux": "^3.7.2", 42 | "redux-thunk": "^2.2.0" 43 | }, 44 | "devDependencies": { 45 | "babel-cli": "^6.26.0", 46 | "babel-core": "^6.26.0", 47 | "babel-eslint": "^7.2.3", 48 | "babel-loader": "^7.1.2", 49 | "babel-polyfill": "^6.26.0", 50 | "babel-preset-env": "^1.6.0", 51 | "babel-preset-es2015": "^6.24.1", 52 | "babel-preset-react": "^6.24.1", 53 | "babel-preset-react-optimize": "^1.0.1", 54 | "babel-preset-stage-0": "^6.24.1", 55 | "babel-preset-stage-2": "^6.24.1", 56 | "clean-webpack-plugin": "^0.1.16", 57 | "compression-webpack-plugin": "^1.0.0", 58 | "css-loader": "^0.28.5", 59 | "eslint": "^4.5.0", 60 | "eslint-plugin-babel": "^4.1.2", 61 | "eslint-plugin-import": "^2.7.0", 62 | "eslint-plugin-jsx-a11y": "^6.0.2", 63 | "eslint-plugin-react": "^7.3.0", 64 | "eslint-plugin-standard": "^3.0.1", 65 | "extract-text-webpack-plugin": "^3.0.0", 66 | "husky": "^0.14.3", 67 | "isomorphic-style-loader": "^4.0.0", 68 | "node-sass": "^4.5.3", 69 | "open": "0.0.5", 70 | "react-hot-loader": "^3.0.0-beta.7", 71 | "sass-loader": "^6.0.6", 72 | "style-loader": "^0.18.2", 73 | "webpack": "^3.5.5", 74 | "webpack-bundle-analyzer": "^2.9.0", 75 | "webpack-dev-middleware": "^1.12.0", 76 | "webpack-hot-middleware": "^2.18.2", 77 | "webpack-hot-server-middleware": "^0.1.0", 78 | "webpack-merge": "^4.1.0", 79 | "webpack-node-externals": "^1.6.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /package.windows.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pro", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "npm run clean && npm run build-client && npm run build-server", 6 | "build-client": "SET NODE_ENV=production SET BABEL_ENV=production && node -r babel-register node_modules/.bin/webpack --progress --display-modules --config webpack/webpack.config.client.js", 7 | "build-server": "SET NODE_ENV=production SET BABEL_ENV=production && node -r babel-register node_modules/.bin/webpack --progress --config webpack/webpack.config.server.js", 8 | "clean": "rimraf dist/ && rimraf public/app", 9 | "lint": "eslint src", 10 | "postmerge": "npm install", 11 | "postrewrite": "npm install", 12 | "precommit": "npm run lint", 13 | "prepush": "npm run test", 14 | "start": "npm run clean & SET NODE_ENV=development SET BABEL_ENV=development && babel-node src/server", 15 | "start-analyzer": "npm run clean && SET NODE_ENV=development SET BABEL_ENV=development SET ANALYZER=true && babel-node src/server", 16 | "start-production": "npm run build && SET NODE_ENV=production SET BABEL_ENV=production && babel-node src/server", 17 | "test": "echo \"Error: no test specified\"", 18 | "watch-client": "webpack --watch --config webpack/webpack.config.client.js", 19 | "watch-server": "webpack --watch --config webpack/webpack.config.server.js" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/MilkZoft/react-pro.git" 24 | }, 25 | "engines": { 26 | "node": "current" 27 | }, 28 | "browserslist": [ 29 | "> 1%", 30 | "last 2 versions" 31 | ], 32 | "author": "Carlos Santana", 33 | "license": "MIT", 34 | "dependencies": { 35 | "express": "^4.15.4", 36 | "node-time-ago": "^1.0.0", 37 | "react": "^15.6.1", 38 | "react-dom": "^15.6.1", 39 | "react-redux": "^5.0.6", 40 | "react-router-dom": "^4.2.2", 41 | "redux": "^3.7.2", 42 | "redux-thunk": "^2.2.0" 43 | }, 44 | "devDependencies": { 45 | "babel-cli": "^6.26.0", 46 | "babel-core": "^6.26.0", 47 | "babel-eslint": "^7.2.3", 48 | "babel-loader": "^7.1.2", 49 | "babel-polyfill": "^6.26.0", 50 | "babel-preset-env": "^1.6.0", 51 | "babel-preset-es2015": "^6.24.1", 52 | "babel-preset-react": "^6.24.1", 53 | "babel-preset-react-optimize": "^1.0.1", 54 | "babel-preset-stage-0": "^6.24.1", 55 | "babel-preset-stage-2": "^6.24.1", 56 | "clean-webpack-plugin": "^0.1.16", 57 | "compression-webpack-plugin": "^1.0.0", 58 | "css-loader": "^0.28.5", 59 | "eslint": "^4.5.0", 60 | "eslint-plugin-babel": "^4.1.2", 61 | "eslint-plugin-import": "^2.7.0", 62 | "eslint-plugin-jsx-a11y": "^6.0.2", 63 | "eslint-plugin-react": "^7.3.0", 64 | "eslint-plugin-standard": "^3.0.1", 65 | "extract-text-webpack-plugin": "^3.0.0", 66 | "husky": "^0.14.3", 67 | "isomorphic-style-loader": "^4.0.0", 68 | "node-sass": "^4.5.3", 69 | "open": "0.0.5", 70 | "react-hot-loader": "^3.0.0-beta.7", 71 | "sass-loader": "^6.0.6", 72 | "style-loader": "^0.18.2", 73 | "webpack": "^3.5.5", 74 | "webpack-bundle-analyzer": "^2.9.0", 75 | "webpack-dev-middleware": "^1.12.0", 76 | "webpack-hot-middleware": "^2.18.2", 77 | "webpack-hot-server-middleware": "^0.1.0", 78 | "webpack-merge": "^4.1.0", 79 | "webpack-node-externals": "^1.6.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "es6": true 5 | }, 6 | "parserOptions": { 7 | "sourceType": "module", 8 | "ecmaFeatures": { 9 | "jsx": true, 10 | "experimentalObjectRestSpread": true 11 | } 12 | }, 13 | "settings": { 14 | "react": { 15 | "pragma": "React", 16 | "version": "15.6.1" 17 | } 18 | }, 19 | "plugins": [ 20 | "react" 21 | ], 22 | "rules": { 23 | "accessor-pairs": 2, 24 | "array-callback-return": 2, 25 | "block-scoped-var": 2, 26 | "brace-style": [2, 1tbs], 27 | "callback-return": 2, 28 | "comma-dangle": [2, never], 29 | "complexity": [2, 8], 30 | "curly": 2, 31 | "dot-location": [2, property], 32 | "dot-notation": 2, 33 | "default-case": 2, 34 | "eol-last": 2, 35 | "eqeqeq": 2, 36 | "indent": [2, 2, {SwitchCase: 1}], 37 | "keyword-spacing": 2, 38 | "max-depth": [2, 6], 39 | "max-len": [2, 120, 4], 40 | "max-statements": ["error", 40, { "ignoreTopLevelFunctions": true }], 41 | "no-alert": 2, 42 | "no-bitwise": 0, 43 | "no-cond-assign": 2, 44 | "no-console": 2, 45 | "no-dupe-args": 2, 46 | "no-dupe-keys": 2, 47 | "no-duplicate-case": 2, 48 | "no-dupe-class-members": 2, 49 | "no-empty": 2, 50 | "no-empty-function": 2, 51 | "no-eq-null": 2, 52 | "no-eval": 2, 53 | "no-ex-assign": 2, 54 | "no-extend-native": 2, 55 | "no-extra-boolean-cast": 2, 56 | "no-extra-parens": 0, 57 | "no-extra-semi": 2, 58 | "no-fallthrough": 2, 59 | "no-floating-decimal": 2, 60 | "no-func-assign": 2, 61 | "no-implied-eval": 2, 62 | "no-implicit-globals": 2, 63 | "no-inner-declarations": [2, "both"], 64 | "no-invalid-regexp": 2, 65 | "no-irregular-whitespace": 2, 66 | "no-iterator": 2, 67 | "no-lone-blocks": 2, 68 | "no-loop-func": 2, 69 | "no-multi-spaces": 2, 70 | "no-multi-str": 2, 71 | "no-negated-in-lhs": 2, 72 | "no-new": 2, 73 | "no-new-func": 2, 74 | "no-new-wrappers": 2, 75 | "no-obj-calls": 2, 76 | "no-redeclare": 2, 77 | "no-regex-spaces": 2, 78 | "no-restricted-syntax": [2, WithStatement], 79 | "no-sparse-arrays": 2, 80 | "no-unexpected-multiline": 2, 81 | "no-unsafe-finally": 2, 82 | "no-unreachable": 2, 83 | "no-unused-expressions": 0, 84 | "no-unused-vars": [2, {args: none}], 85 | "no-void": 2, 86 | "no-undefined": 0, 87 | "prefer-const": 2, 88 | "prefer-template": 2, 89 | "quotes": [2, single, avoid-escape], 90 | "semi": [2, "always"], 91 | "space-before-blocks": 2, 92 | "strict": [2, global], 93 | "use-isnan": 2, 94 | "valid-jsdoc": 2, 95 | "valid-typeof": 2, 96 | "vars-on-top": 2, 97 | "wrap-iife": 2, 98 | "react/jsx-boolean-value": 1, 99 | "react/jsx-closing-bracket-location": 1, 100 | "react/jsx-curly-spacing": 1, 101 | "react/jsx-handler-names": 1, 102 | "react/jsx-key": 1, 103 | "react/jsx-no-duplicate-props": 1, 104 | "react/jsx-no-undef": 1, 105 | "react/jsx-pascal-case": 1, 106 | "react/jsx-sort-props": 0, 107 | "react/jsx-uses-react": 1, 108 | "react/jsx-uses-vars": 1, 109 | "react/no-deprecated": 1, 110 | "react/no-did-mount-set-state": 1, 111 | "react/no-did-update-set-state": 1, 112 | "react/no-direct-mutation-state": 1, 113 | "react/no-multi-comp": 0, 114 | "react/no-unknown-property": 1, 115 | "react/prefer-es6-class": 1, 116 | "react/react-in-jsx-scope": 1, 117 | "react/self-closing-comp": 1, 118 | "react/sort-comp": 1, 119 | "react/sort-prop-types": 0 120 | } 121 | } 122 | --------------------------------------------------------------------------------