Реагируйте Redux с SSR против IE11 - клиентское приложение не запускается - PullRequest
0 голосов
/ 18 октября 2019

Мы создали приложение в React-Redux с SSR. Он прекрасно работает во всех браузерах, кроме Internet Explorer 11 и ниже. Приложение отображается на стороне сервера и отображается в браузере клиента, но приложение не запускается. Страница становится статической страницей. Любая навигация перезагрузит приложение.

Мы используем Webpack и Babel для компиляции кода. Предустановка Babel ориентирована на IE11. В консоли IE11 ошибки не отображаются.

webpack.config.js

var webpack = require('webpack');
var path = require('path');
var fs = require('fs');
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
var Visualizer = require('webpack-visualizer-plugin');

const pkgPath = path.resolve(__dirname, 'package.json');
const pkg = fs.existsSync(pkgPath) ? require(pkgPath) : {};
let theme = {};

if (pkg.theme && typeof pkg.theme === 'string') {
  let cfgPath = pkg.theme;
  // relative path
  if (cfgPath.charAt(0) === '.') {
    cfgPath = path.resolve(__dirname, cfgPath);
  }
  const getThemeConfig = require(cfgPath);
  theme = getThemeConfig();
} else if (pkg.theme && typeof pkg.theme === 'object') {
  theme = pkg.theme;
}

module.exports = {
  mode: process.env.NODE_ENV,
  entry: { bundle: './src/client.js' },
  output: {
    path: path.join(__dirname, 'build'),
    filename: 'js/[name].js',
    chunkFilename: 'js/plugins/[name].js',
    publicPath: "/",
  },

  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: [/node_modules/],
        use: {
          loader: 'babel-loader',
          query: {
            plugins: [
              "@babel/plugin-proposal-class-properties",
              "@babel/plugin-syntax-dynamic-import",
              "@babel/plugin-syntax-jsx",
              "@babel/plugin-transform-runtime",
              "@babel/plugin-transform-object-assign",
              "dynamic-import-webpack",
              ["import", { "libraryName": "antd" }],
            ],
            presets: [
              ["@babel/preset-env", {
                "targets": {
                  "edge": "17",
                  "firefox": "60",
                  "chrome": "67",
                  "safari": "11.1",
                  "opera": "55",
                  "ie": "11"
                }
              }],
              "@babel/preset-react"
            ]
          }
        }
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          MiniCssExtractPlugin.loader,
          { loader: 'css-loader', options: { importLoaders: 1 } },
          {
            loader: 'postcss-loader',
            options: {
              ident: 'postcss',
              plugins: (loader) => [
                require('autoprefixer')(),
                require('cssnano')()
              ]
            }
          }
        ]
      },
      {
        test: /\.less$/,
        use: [
          'style-loader',
          MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              ident: 'postcss',
              plugins: (loader) => [
                require('cssnano')()
              ]
            }
          },
          {
            loader: 'less-loader',
            options: {
              modifyVars: theme,
              javascriptEnabled: true
            }
          }
        ]
      },
      {
        test: /\.(svg|woff|woff2)$/,
        use: {
          loader: 'url-loader',
          query: {
            name: 'build/img/[name].[ext]',
            limit: 10000
          }
        }
      },
      {
        test: /\.(eot|ttf)$/,
        use: {
          loader: 'file-loader',
          query: {
            name: 'build/img/[name].[ext]'
          }
        }
      },
      {
        test: /\.(png|jpg|jpeg|gif)$/,
        use: {
          loader: 'file-loader',
          query: {
            name: 'assets/[name].[ext]'
          }
        }
      }
    ]
  },

  optimization: {
    minimizer: [new UglifyJsPlugin({
      uglifyOptions: {
        compress: {
          collapse_vars: false
        }
      }
    })],
  },

  plugins: [
    // Transmit Docker env variables to the browser, fallback to dev if not present
    // (in development, the build is run outside Docker)
    new webpack.EnvironmentPlugin({
      NODE_ENV: 'development',
    }),
    new webpack.DefinePlugin({
      'process.env.local': JSON.stringify(true)
    }),
    new MiniCssExtractPlugin({
      filename: 'css/[name].css'
    }),
    new CompressionPlugin(),
    // load `moment/locale/en.js` and `moment/locale/fr.js`
    new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /en|fr/),
    new Visualizer()
  ],

  // Automatically transform files with these extensions
  resolve: {
    extensions: ['.js', '.jsx'],
    modules: ['node_modules']
  }
};

package.json

{
  "name": "Project",
  "version": "1.3.3",
  "description": "client with Universal JavaScript",
  "scripts": {
    "clean": "rm -rf build && mkdir build",
    "build-server": "BABEL_ENV=node babel -d ./build ./src -s",
    "build-prod": "npm run clean && npm run build && npm run build-server && npm run transfer-file",
    "transfer-file": "babel ./src --out-dir ./build --copy-files",
    "e2e": "nightwatch",
    "start:universal": "npm run start",
    "start": "npm run build && BABEL_ENV=node babel-node src/server.js",
    "start:dev:universal": "npm run start:dev",
    "start:dev:docker": "([ -f \"build/js/bundle.js\" ] || npm run build:dev) && npm run start:dev",
    "start:local:docker": "([ -f \"build/js/bundle.js\" ] || npm run build:local) && npm run start:dev",
    "build": "webpack -p --config webpack.config.preprod.js --progress",
    "build:dev": "webpack -d --config webpack.config.dev.js",
    "start:dev": "BABEL_ENV=node nodemon --exec babel-node -- src/server.js",
    "build:dev:watch": "npm run clean && webpack -d --config webpack.config.dev.js --watch --progress",
    "build:local": "webpack -d --config webpack.config.local.js",
    "build:local:watch": "npm run clean && webpack -d --config webpack.config.local.js --watch --progress",
    "start:preprod:docker": "([ -f \"build/js/bundle.js\" ] || npm run build:preprod) && npm run start:preprod",
    "build:preprod": "webpack -d --config webpack.config.preprod.js",
    "start:preprod": "BABEL_ENV=node nodemon --exec babel-node -- src/server.js",
    "webpacker": "node --max_old_space_size=4096 node_modules/.bin/webpack-cli --progress --color --config webpack.config.dev.js"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/genclik/fundky-client.git"
  },
  "author": "ODE Technologies",
  "license": "ISC",
  "dependencies": {
    "@fortawesome/fontawesome-svg-core": "^1.2.19",
    "@fortawesome/free-brands-svg-icons": "^5.9.0",
    "@fortawesome/pro-light-svg-icons": "^5.9.0",
    "@fortawesome/pro-regular-svg-icons": "^5.9.0",
    "@fortawesome/pro-solid-svg-icons": "^5.9.0",
    "@fortawesome/react-fontawesome": "^0.1.4",
    "algoliasearch": "^3.33.0",
    "antd": "^3.22.0",
    "blueimp-load-image": "^2.24.0",
    "classnames": "^2.2.5",
    "compression": "^1.7.3",
    "credit-card-type": "^7.1.0",
    "deep-diff": "^1.0.2",
    "detect-browser": "^2.1.0",
    "express": "^4.16.4",
    "i18next": "^15.0.4",
    "i18next-express-middleware": "^1.7.1",
    "i18next-node-fs-backend": "^2.1.1",
    "instantsearch.css": "^7.3.1",
    "is-in-browser": "^1.1.3",
    "isomorphic-fetch": "^2.2.1",
    "json-markup": "^1.1.3",
    "lodash": "^4.17.11",
    "log4js": "^3.0.5",
    "log4js-node-sentry": "^1.0.0",
    "moment": "^2.19.3",
    "morgan": "^1.9.1",
    "normalize.css": "^8.0.1",
    "nuka-carousel": "^4.5.11",
    "prop-types": "^15.7.2",
    "query-string": "^4.3.4",
    "react": "^16.8.2",
    "react-beautiful-dnd": "^11.0.4",
    "react-circular-progressbar": "^0.8.0",
    "react-collapse": "^4.0.3",
    "react-color": "^2.17.3",
    "react-dom": "^16.8.2",
    "react-draft-wysiwyg": "^1.13.2",
    "react-helmet": "^5.2.0",
    "react-html-parser": "^2.0.2",
    "react-i18next": "^4.1.2",
    "react-idle-timer": "^4.2.5",
    "react-image-crop": "^7.0.5",
    "react-input-mask": "^2.0.4",
    "react-instantsearch-dom": "^5.7.0",
    "react-loadable": "^5.5.0",
    "react-motion": "^0.5.2",
    "react-perfect-scrollbar": "^1.5.3",
    "react-quill": "^1.3.3",
    "react-redux": "^5.1.1",
    "react-remarkable": "^1.1.3",
    "react-router-dom": "^4.1.1",
    "react-router-last-location": "^1.1.0",
    "react-router-redux": "^4.0.8",
    "react-router-scroll-memory": "^1.0.4",
    "react-sizes": "^1.0.3",
    "redux": "^4.0.1",
    "redux-thunk": "2.2.0",
    "sanitize-html": "^1.20.1",
    "style-loader": "^0.23.1",
    "universal-cookie": "^2.1.2"
  },
  "devDependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.2.2",
    "@babel/node": "^7.2.2",
    "@babel/plugin-proposal-class-properties": "^7.4.0",
    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
    "@babel/plugin-syntax-jsx": "^7.2.0",
    "@babel/plugin-transform-object-assign": "^7.2.0",
    "@babel/plugin-transform-runtime": "^7.4.4",
    "@babel/polyfill": "^7.2.5",
    "@babel/preset-env": "^7.3.1",
    "@babel/preset-react": "^7.0.0",
    "autoprefixer": "^9.5.0",
    "babel-loader": "^8.0.5",
    "babel-plugin-dynamic-import-webpack": "^1.1.0",
    "babel-plugin-import": "^1.11.0",
    "babel-plugin-transform-imports": "^1.5.1",
    "babel-plugin-transform-require-ignore": "^0.1.1",
    "chromedriver": "^2.46.0",
    "compression-webpack-plugin": "^2.0.0",
    "css-loader": "^2.1.1",
    "cssnano": "^4.1.10",
    "file-loader": "^1.1.11",
    "less": "3.9.0",
    "less-loader": "^4.0.4",
    "mini-css-extract-plugin": "^0.4.5",
    "nightwatch": "^1.0.19",
    "nodemon": "^1.18.10",
    "postcss-loader": "^3.0.0",
    "redux-logger": "^3.0.6",
    "uglifyjs-webpack-plugin": "^2.1.2",
    "url-loader": "^1.1.2",
    "webpack": "^4.29.6",
    "webpack-cli": "^3.3.0",
    "webpack-visualizer-plugin": "^0.1.11"
  }
}

server.js

//----- NODE MODULE ------//
import path from 'path';
import express from 'express';
import compression from 'compression';
import i18nMiddleware from 'i18next-express-middleware';
import morgan from 'morgan';

//----- REACT MODULE ------//
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import fetchMap from './chore/fetchMap';
import { fetchData } from './parts/chore/ssrUtils';
import Helmet from 'react-helmet';

//------- PROJECT MODULE -----//
import i18n from './chore/i18n-server';
import * as cookieUtils from './parts/common/cookieUtils';
import { fetchPlatformByName, fetchPlatformContent } from './platform/platformActions';
import { fetchOrganizationById } from './organization/organizationActions';
import {
  connect,
  setSessionLanguage,
  fetchMemberRoles,
  fetchMemberPlatformRoles,
  fetchMemberPlatformPermissions,
} from './parts/session/sessionActions';
import { getLocale } from './parts/session/sessionSelectors';
import rootReducer from './chore/rootReducer';
import configureStore from './parts/chore/configureStore';
import App from './chore/App';
import { dom as faDom } from '@fortawesome/fontawesome-svg-core';
import log4js from 'log4js';
const config = require(`./config/config.${process.env.NODE_ENV}.json`);

// logs4js configuration
log4js.configure(config.logger);

// Logger
const logger = log4js.getLogger('Client');
const sentryLogger = log4js.getLogger('Sentry');

console.log = logger.debug.bind(logger);
console.info = logger.trace.bind(logger);
console.success = logger.info.bind(logger);
console.warn = logger.warn.bind(logger);
console.error = logger.error.bind(logger);
console.fatal = sentryLogger.fatal.bind(sentryLogger);

// Process - Exception
process.on('uncaughtException', function(err) {
  console.fatal(err);

  // Close the process
  process.exit(1);
});

const app = express();
app.disable('x-powered-by');

// Set secure cookies
if (!process.env.local) {
  app.set('trust proxy', 1);
}

// use ejs templates
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

// Webpack bundles & assets
app.use(compression());
app.use(express.static('build'));
app.use(i18nMiddleware.handle(i18n));

if (process.env.NODE_ENV === 'development') {
  app.use(morgan('dev'));
}

app.get('*', (req, res) => {
  let markup = '';
  let preloadedState = null;
  let helmet = Helmet.renderStatic();
  let status = 200;
  const context = {
    actionCreators: []
  };
  let store;
  let locale;
  let i18nServer;

  if (!process.env.UNIVERSAL) {
    // NO SSR: return the page directly
    return res.status(status).render('index', { markup, preloadedState, helmet, locale, faDom });
  }

  // Begin SSR
  cookieUtils.init(req.headers.cookie);
  store = configureStore(rootReducer);

  // Global data fetching
  Promise.all([
    store.dispatch(connect())
  ])
  .then(() => {
    const state = store.getState();
    if(state.session.userTypeId === 2) {
      store.dispatch(fetchMemberRoles());
    }
    locale = getLocale(state, req.path);
    return store.dispatch(setSessionLanguage(locale));
  })
  .then(() => {
    return store.dispatch(fetchPlatformByName(req.headers.platform))
  })
  .then(() => {
    const state = store.getState();

    store.dispatch(fetchPlatformContent(state.platform.platform.id));

    if(state.session.userTypeId === 2) {
      store.dispatch(fetchMemberPlatformPermissions(state.platform.platform.id));
      store.dispatch(fetchMemberPlatformRoles(state.platform.platform.id));
    }
    // 1.5. Get organization data from id
    return store.dispatch(fetchOrganizationById(state.platform.platform.organizationId));
  })
  .then(() => {
    // 2. Route-specific data fetching
    return fetchData(req.path, fetchMap, store, req.query);
  })
  .then(() => {
    // 4. Rendering
    i18nServer = i18n.cloneInstance();
    locale = getLocale(store.getState(), req.path);
    i18nServer.changeLanguage(locale);

    // If sitemap is requested, pass xml as content type in headers
    if ( req.path.match(/(sitemap_index.xml|page-sitemap.xml|campaign-sitemap.xml)$/) ) {
      res.header( 'Content-Type', 'application/xml' );
    }

    const rootComponent = React.createElement(StaticRouter, { location: req.url, context },
      React.createElement(App, {i18n: i18nServer, store, locale})
    );
    markup = renderToString(rootComponent);
  })
  .then(() => {
    // 5. Response
    // context.url will contain the URL to redirect to if a <Redirect> was used
    if (context.url) {
      let url = context.url;
      if (context.url === `/en/login` || context.url === `/fr/connexion`) {
        url += `?from=${req.url}`
      }

      return res.redirect(302, url);
    }

    if (context.is404) {
      status = 404;
    }

    if ( req.path.match(/(sitemap_index.xml|page-sitemap.xml|campaign-sitemap.xml)$/) ) {
      // Use sitemap view
      res.status(status).render('sitemap', { markup });
    } else {
      preloadedState = JSON.stringify(store.getState());
      helmet = Helmet.renderStatic();
      res.status(status).render('index', { markup, preloadedState, helmet, locale, faDom });
    }
  })
  .catch(function(err) {
    console.log('===== ERROR SERVER =====');
    console.error(err);
    console.log('===== ERROR SERVER =====');

    markup = err;
    res.status(500).render('index', { markup, preloadedState, helmet, locale, faDom });
  });
});

app.options('*', (req,res) => res.status(405));

app.all('*', (req,res,next)=>{
    res.header('Accept-Control-Allow-Origin', '*');
    res.header('Accept-Control-Allow-Methods', 'POST,GET,DELETE,PUT,OPTIONS');
    res.header('Accept-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With,Accept,Origin');
    if(req.method=='OPTIONS'){
        res.sendStatus(200);
    }else {
        next();
    }
});

// start the server
const port = process.env.PORT || 5557;
const env = process.env.NODE_ENV || 'development';

app.listen(port, err => {
  if (err) {
    console.error(err.stack);
  } else {
    console.info('==============================================================================');
    console.info(`Server listening on http://localhost:${port}`);
    console.info(`Universal rendering: ${process.env.UNIVERSAL ? 'enabled' : 'disabled'}`);
    console.info(`Env: ${env}`);
    console.info('==============================================================================');
  }
});

Приложение должно работать правильно наInternet Explorer 11, как и в других браузерах.

...