├── 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 |
--------------------------------------------------------------------------------