Мы создали приложение в 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, как и в других браузерах.