React server rendering, third party package window is not defined.

recently encountered a demand, the react project needs to add a rich text editor, there are many online, to find a braft-editor, their own new project to use no problem. However, when adding to the project, because the project uses server rendering (just took over the project, a novice, do not understand this concept), the server node environment cannot use environment variables such as window, so it will report an error when running the project.
P.S.: almost all rich text editor plug-ins have this problem, using window or document for implementation.
online I think there are some people who also have this problem, but they do not have a clear solution, that is, they know that there is no window variable on the node. how to solve the problem that no one has provided it? ask Daniel for help.

< hr >

add: I know that this kind of file does not need to be rendered on the server side, but now all the third-party packages used in the project should be rendered on the server side, so the solution I want to ask is how to render and block this kind of file on the server side.

< hr >


webpack

the figure above is the run error diagram. Here are a few js file codes.

1.server.js file

import path from "path";
import express from "express";
import session from "express-session";
import bodyParser from "body-parser";
// import compression from "compression";
import auth from "./middlewares/auth";
import accessId from "./middlewares/accessId";
import { access, response } from "./middlewares/log";
import errorHandler from "./middlewares/errorHandler";
import login from "./middlewares/login";
import specialLogin from "./middlewares/specialLogin";
import logoutHandler from "./middlewares/logoutHandler";
import serverRender from "./middlewares/serverRender";
import routes from "./routes";
import apiHandler from "./api";
import { nodePort } from "./serverConfig";
import { getLocalIP } from "./core/serverUtils";

const app = express();

/**
 * heart beat api for maintainer to test node alive
 */
app.get("/heartBeat", (req, res) => {
  res.status(200).send("I am fine");
});

//
// Tell any CSS tooling (such as Material UI) to use all vendor prefixes if the
// user agent is not known.
// -----------------------------------------------------------------------------
global.navigator = global.navigator || {};
global.navigator.userAgent = global.navigator.userAgent || "all";

// one hour
const cookieAge = 1 * 60 * 60 * 1000;
// one year
const cacheAge = 1 * 365 * 24 * 60 * 60 * 1000;

const sessionStore = new session.MemoryStore();

function startCleanupSessionTask() {
  setTimeout(() => {
    // store.all invokes getSession method
    // which will delete expired session
    // ref: https://github.com/expressjs/session/blob/master/session/memory.js-sharpL58
    sessionStore.all(startCleanupSessionTask);
  }, cookieAge / 2).unref();
}

startCleanupSessionTask();

app.use(session({
  store: sessionStore,
  secret: `****`,
  name: "***",
  resave: false, //  session  session  true
  rolling: true, // cookie expires
  saveUninitialized: false,
  // default: { path: "/", httpOnly: true, secure: false, maxAge: null }
  // secure: true => cookie  HTTP  HTTPS 
  cookie: { maxAge: cookieAge }
  // genid:  session_id   uid2  npm 
  // store: session 
}));

/**
 * Register log middleware
 */
app.use(accessId);
app.use(access);
app.use(response);

/**
 * compression middleware
 * compression default gzip with html, css, js or json
 *
 * The main implementation detail is to make sure that
 * the app.use call for compress is before any other middlewares
 * (there are a few exceptions like logging).
 *
 */
// app.use(compression({
//   filter: req => (req.originalUrl || req.url).indexOf("api") === -1
// }));

/**
 * Register static middleware
 */
app.use(express.static(path.join(__dirname, "public"), { maxAge: cacheAge }));
// handle request entity too large
app.use(bodyParser.urlencoded({ limit: "10mb", extended: true }));
app.use(bodyParser.json({ limit: "10mb" }));
app.use(bodyParser.raw({ limit: "10mb" }));


//
// Authentication
// -----------------------------------------------------------------------------
// app.use(expressJwt({
//   secret: auth.jwt.secret,
//   credentialsRequired: false,
//   getToken: req => req.cookies.id_token,
// }));

//  nginx
// reference: http://wiki.jikexueyuan.com/project/express-guide/express-behind-proxies.html
if (process.env.NODE_ENV === "production") {
  app.enable("trust proxy");
}

/**
 * Register special login and logout
 */
app.get("/login", login); // 
app.get("/specialLogin", specialLogin); // crm
app.get("/logout", logoutHandler);

/**
 * Register API middleware
 */
app.use("/api", auth.api, apiHandler);

/**
 * Register server-side rendering middleware
 */
app.get("*", auth.server, serverRender(routes));


/**
 * Register custom error handler middleware last
 */
app.use(errorHandler);

/**
 * Launch the server
 */
app.listen(nodePort, () => {
  const ips = getLocalIP();
  const ipstr = ips.map(ip => `http://${ip}:${nodePort}/`);
  // eslint-disable-next-line no-console
  console.log(`The server is running at ${ipstr}, now is ${new Date().toLocaleString()}, pid = ${process.pid}`);
});

2.serverRender.js file

import React from "react";
import ReactDOM from "react-dom/server";
// maybe it"s eslint bug for import/extensions
import UniversalRouter from "universal-router"; // eslint-disable-line import/extensions
import configureStore from "../redux/store/configureStore";
import App from "../components/App";
import Html from "../components/Html";
import { loginSuccess } from "../actions/Auth";
import setRuntimeVariable from "../actions/Runtime";
import assets from "./assets.json"; // eslint-disable-line import/no-unresolved
import pkg from "../../package.json";

const keywords = pkg.keywords.join(", ");

export default (routes, isLogin = true) => async (req, res, next) => {
  try {
    const user = req.session.user;
    const store = configureStore();

    store.dispatch(loginSuccess({
      user: {
        sponsorId: user.sponsorId,
        email: user.email,
        isCompany: user.isCompany,
        isDirectUser: user.isDirectUser,
        // pw: new Buffer(user.pw).toString("base64"),
        token: user.token,
      },
    }));
    store.dispatch(setRuntimeVariable({
      name: "initalNow",
      value: Date.now(),
    }));

    const css = new Set();

    const context = {
      insertCss: (...styles) => {
        // eslint-disable-next-line no-underscore-dangle
        styles.forEach(style => css.add(style._getCss()));
      },
      // for material-ui
      // getUA: () => req.headers["user-agent"],
      // Initialize a new Redux store
      // http://redux.js.org/docs/basics/UsageWithReact.html
      store,
      path: req.path,
    };

    console.log("serverRender.js request", req.path);

    const route = await UniversalRouter.resolve(routes, {
      ...context,
      query: req.query,
      locationState: null,
    });

    if (route.redirect) {
      return res.redirect(route.status || 302, route.redirect);
    }

    if (route.beforeEnter) {
      route.beforeEnter.forEach(fn => fn());
    }

    const data = {
      description: "**********",
      keywords,
      ...route
    };
    data.children = ReactDOM.renderToString(<App context={context}>{route.component}</App>);
    data.state = store.getState();

    data.styles = [
      { id: "css", cssText: [...css].join("") }
    ];
    data.isLogin = isLogin;

    data.scripts = [
      assets.vendor.js,
      assets.client.js,
    ];

    if (route.chunk) {
      data.scripts.push(assets[route.chunk].js);
    }

    const html = ReactDOM.renderToStaticMarkup(<Html {...data} />);
    res.status(route.status || 200).send(`<!DOCTYPE html>\n${html}`);
  } catch (err) {
    next(err);
  }
};

3.start.js

/**
 * React Starter Kit (https://www.reactstarterkit.com/)
 *
 * Copyright  2014-present Kriasoft, LLC. All rights reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE.txt file in the root directory of this source tree.
 */

import browserSync from "browser-sync";
import webpack from "webpack";
import webpackDevMiddleware from "webpack-dev-middleware";
import webpackHotMiddleware from "webpack-hot-middleware";
import WriteFilePlugin from "write-file-webpack-plugin";
import run from "./run";
import runServer from "./runServer";
import webpackConfig from "./webpack.config";
import clean from "./clean";
import copy from "./copy";

const isDebug = !process.argv.includes("--release");
process.argv.push("--watch");


const [clientConfig, serverConfig] = webpackConfig;

/**
 * Launches a development web server with "live reload" functionality -
 * synchronizing URLs, interactions and code changes across multiple devices.
 */
async function start() {
  await run(clean);
  await run(copy);
  await new Promise((resolve) => {
    // Save the server-side bundle files to the file system after compilation
    // https://github.com/webpack/webpack-dev-server/issues/62
    serverConfig.plugins.push(new WriteFilePlugin({ log: false }));
    clientConfig.plugins.push(new WriteFilePlugin({ log: false }));

    // Hot Module Replacement (HMR) + React Hot Reload
    if (isDebug) {
      clientConfig.entry.client = [...new Set([
        "babel-polyfill",
        "react-hot-loader/patch",
        "webpack-hot-middleware/client",
      ].concat(clientConfig.entry.client))];
      clientConfig.output.filename = clientConfig.output.filename.replace("[chunkhash", "[hash");
      clientConfig.output.chunkFilename = clientConfig.output.chunkFilename.replace("[chunkhash", "[hash");
      const { query } = clientConfig.module.rules.find(x => x.loader === "babel-loader");
      query.plugins = ["react-hot-loader/babel"].concat(query.plugins || []);
      clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
      clientConfig.plugins.push(new webpack.NoEmitOnErrorsPlugin());
    }

    const bundler = webpack(webpackConfig);
    const wpMiddleware = webpackDevMiddleware(bundler, {
      // IMPORTANT: webpack middleware can"t access config,
      // so we should provide publicPath by ourselves
      publicPath: clientConfig.output.publicPath,

      // Pretty colored output
      stats: clientConfig.stats,

      // For other settings see
      // https://webpack.github.io/docs/webpack-dev-middleware
    });
    const hotMiddleware = webpackHotMiddleware(bundler.compilers[0]);

    let handleBundleComplete = async () => {
      handleBundleComplete = stats => !stats.stats[1].compilation.errors.length && runServer();

      const server = await runServer();
      const bs = browserSync.create();

      bs.init({
        ...isDebug ? {} : { notify: false, ui: false },

        proxy: {
          target: server.host,
          middleware: [wpMiddleware, hotMiddleware],
          proxyOptions: {
            xfwd: true,
          },
        },
      }, resolve);
    };

    bundler.plugin("done", stats => handleBundleComplete(stats));
  });
}

export default start;
  1. webpack.config.js file
/**
 * React Starter Kit (https://www.reactstarterkit.com/)
 *
 * Copyright  2014-present Kriasoft, LLC. All rights reserved.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE.txt file in the root directory of this source tree.
 */

import path from "path";
import webpack from "webpack";
import AssetsPlugin from "assets-webpack-plugin";
import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer";
import pkg from "../package.json";

const isDebug = !process.argv.includes("--release");
const testMode = process.argv.includes("--test");
const isVerbose = process.argv.includes("--verbose");
const isAnalyze = process.argv.includes("--analyze") || process.argv.includes("--analyse");

//
// Common configuration chunk to be used for both
// client-side (client.js) and server-side (server.js) bundles
// -----------------------------------------------------------------------------

const config = {
  context: path.resolve(__dirname, ".."),

  output: {
    path: path.resolve(__dirname, "../build/public/assets"),
    publicPath: "/assets/",
    pathinfo: isVerbose,
  },

  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: "babel-loader",
        include: [
          path.resolve(__dirname, "../src"),
        ],
        query: {
          // https://github.com/babel/babel-loader-sharpoptions
          cacheDirectory: isDebug,

          // https://babeljs.io/docs/usage/options/
          babelrc: false,
          presets: [
            // A Babel preset that can automatically determine the Babel plugins and polyfills
            // https://github.com/babel/babel-preset-env
            ["env", {
              targets: {
                browsers: pkg.browserslist,
              },
              modules: false,
              useBuiltIns: false,
              debug: false,
            }],
            // Experimental ECMAScript proposals
            // https://babeljs.io/docs/plugins/-sharppresets-stage-x-experimental-presets-
            "stage-2",
            // JSX, Flow
            // https://github.com/babel/babel/tree/master/packages/babel-preset-react
            "react",
            // Optimize React code for the production build
            // https://github.com/thejameskyle/babel-react-optimize
            ...isDebug ? [] : ["react-optimize"],
          ],
          plugins: [
            // Adds component stack to warning messages
            // https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-source
            ...isDebug ? ["transform-react-jsx-source"] : [],
            // Adds __self attribute to JSX which React will use for some warnings
            // https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-react-jsx-self
            ...isDebug ? ["transform-react-jsx-self"] : [],
          ],
        },
      },
      {
        // turn off css-modules on antd css files
        test: /\.css$/,
        include: [/node_modules(\/|\\).*antd/],
        use: [
          {
            loader: "style-loader",
          },
          {
            loader: "css-loader",
          },
          {
            loader: "postcss-loader",
            options: {
              config: "./tools/postcss.config.js",
            },
          },
        ],
      },
      {
        test: /\.css/,
        exclude: [/node_modules(\/|\\).*antd/],
        use: [
          {
            loader: "isomorphic-style-loader",
          },
          {
            loader: "css-loader",
            options: {
              // CSS Loader https://github.com/webpack/css-loader
              importLoaders: 1,
              sourceMap: isDebug,
              // CSS Modules https://github.com/css-modules/css-modules
              modules: true,
              localIdentName: isDebug ? "[name]-[local]-[hash:base64:5]" : "[hash:base64:5]",
              // CSS Nano http://cssnano.co/options/
              minimize: !isDebug,
              discardComments: { removeAll: true },
            },
          },
          {
            loader: "postcss-loader",
            options: {
              config: "./tools/postcss.config.js",
            },
          },
        ],
      },
      {
        test: /\.md$/,
        loader: path.resolve(__dirname, "./lib/markdown-loader.js"),
      },
      {
        test: /\.txt$/,
        loader: "raw-loader",
      },
      {
        test: /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2)(\?.*)?$/,
        loader: "file-loader",
        query: {
          name: isDebug ? "[path][name].[ext]?[hash:8]" : "[hash:8].[ext]",
          limit: 10000,
        },
      },
      {
        test: /\.(mp4|webm|wav|mp3|m4a|aac|oga)(\?.*)?$/,
        loader: "url-loader",
        query: {
          name: isDebug ? "[path][name].[ext]?[hash:8]" : "[hash:8].[ext]",
          limit: 10000,
        },
      },
    ],
  },

  plugins: [
    // https://github.com/webpack/docs/wiki/internal-webpack-plugins-sharpprogresspluginhandler
    // https://stackoverflow.com/questions/31052991/webpack-progress-using-node-js-api
    new webpack.ProgressPlugin((percentage, msg, current, active, modulepath) => {
      if (process.stdout.isTTY && percentage < 1) {
        process.stdout.cursorTo(0);
        const progress = (percentage * 100).toFixed(0);
        const shortPath = modulepath ? `...${modulepath.substr(modulepath.length - 30)}` : "";
        const str = `${progress}% ${msg} ${current || ""} ${active || ""} ${shortPath}`;
        process.stdout.write(str);
        process.stdout.clearLine(1);
      } else if (percentage === 1) {
        process.stdout.write("\n");
        console.log("webpack: done.");
      }
    }),
  ],

  // Don"t attempt to continue if there are any errors.
  bail: !isDebug,

  cache: isDebug,

  stats: {
    colors: true,
    reasons: isDebug,
    hash: isVerbose,
    version: isVerbose,
    timings: true,
    chunks: isVerbose,
    chunkModules: isVerbose,
    cached: isVerbose,
    cachedAssets: isVerbose,
  },
};

//
// Configuration for the client-side bundle (client.js)
// -----------------------------------------------------------------------------

const clientConfig = {
  ...config,

  name: "client",
  target: "web",

  entry: {
    client: ["./src/core/polyfill", "babel-polyfill", "./src/client.js"],
  },

  output: {
    ...config.output,
    filename: isDebug ? "[name].js" : "[name].[chunkhash:8].js",
    chunkFilename: isDebug ? "[name].chunk.js" : "[name].[chunkhash:8].chunk.js",
  },

  plugins: [
    ...config.plugins,

    // Define free variables
    // https://webpack.github.io/docs/list-of-plugins.html-sharpdefineplugin
    new webpack.DefinePlugin({
      "process.env.NODE_ENV": isDebug ? ""development"" : ""production"",
      "process.env.BROWSER": true,
    }),

    // Emit a file with assets paths
    // https://github.com/sporto/assets-webpack-plugin-sharpoptions
    new AssetsPlugin({
      path: path.resolve(__dirname, "../build"),
      filename: "assets.json",
      prettyPrint: true,
    }),

    // Move modules that occur in multiple entry chunks to a new entry chunk (the commons chunk).
    // http://webpack.github.io/docs/list-of-plugins.html-sharpcommonschunkplugin
    new webpack.optimize.CommonsChunkPlugin({
      name: "vendor",
      minChunks: module => /node_modules/.test(module.resource),
    }),

    ...isDebug ? [] : [
      // Minimize all JavaScript output of chunks
      // https://github.com/mishoo/UglifyJS2-sharpcompressor-options
      new webpack.optimize.UglifyJsPlugin({
        sourceMap: true,
        compress: {
          screw_ie8: true, // React doesn"t support IE8
          warnings: isVerbose,
          unused: true,
          dead_code: true,
        },
        mangle: {
          screw_ie8: true,
        },
        output: {
          comments: false,
          screw_ie8: true,
        },
      }),
    ],

    ...isAnalyze ? [
      // Webpack Bundle Analyzer
      // https://github.com/th0r/webpack-bundle-analyzer
      new BundleAnalyzerPlugin({
        analyzerMode: "static",
        reportFilename: "bundle_analyzer.html",
      }),
    ] : [],
  ],

  // Choose a developer tool to enhance debugging
  // http://webpack.github.io/docs/configuration.html-sharpdevtool
  devtool: isDebug ? "source-map" : false,

  // Some libraries import Node modules but don"t use them in the browser.
  // Tell Webpack to provide empty mocks for them so importing them works.
  // https://webpack.github.io/docs/configuration.html-sharpnode
  // https://github.com/webpack/node-libs-browser/tree/master/mock
  node: {
    fs: "empty",
    net: "empty",
    tls: "empty",
  },
};

//
// Configuration for the server-side bundle (server.js)
// -----------------------------------------------------------------------------

const serverConfig = {
  ...config,

  name: "server",
  target: "node",

  entry: {
    server: ["./src/core/polyfill", "babel-polyfill", "./src/server2.js"],
  },

  output: {
    ...config.output,
    filename: "../../server.js",
    libraryTarget: "commonjs2",
  },

  module: {
    ...config.module,

    // Override babel-preset-env configuration for Node.js
    rules: config.module.rules.map(rule => (rule.loader !== "babel-loader" ? rule : {
      ...rule,
      query: {
        ...rule.query,
        presets: rule.query.presets.map(preset => (preset[0] !== "env" ? preset : ["env", {
          targets: {
            node: parseFloat(pkg.engines.node.replace(/^\D+/g, "")),
          },
          modules: false,
          useBuiltIns: false,
          debug: false,
        }])),
      },
    })),
  },

  externals: [
    /^\.\/assets\.json$/,
    (context, request, callback) => {
      const isExternal =
        request.match(/^[@a-z][a-z/.\-0-9]*$/i) &&
        !request.match(/\.(css|less|scss|sss)$/i);
      callback(null, Boolean(isExternal));
    },
  ],

  plugins: [
    ...config.plugins,

    // Define free variables
    // https://webpack.github.io/docs/list-of-plugins.html-sharpdefineplugin
    new webpack.DefinePlugin({
      // eslint-disable-next-line no-nested-ternary
      "process.env.NODE_ENV": isDebug ? (testMode ? ""test"" : ""development"") : ""production"",
      "process.env.BROWSER": false,
    }),

    // Do not create separate chunks of the server bundle
    // https://webpack.github.io/docs/list-of-plugins.html-sharplimitchunkcountplugin
    new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }),

    // Adds a banner to the top of each generated chunk
    // https://webpack.github.io/docs/list-of-plugins.html-sharpbannerplugin
    new webpack.BannerPlugin({
      banner: "require("source-map-support").install();",
      raw: true,
      entryOnly: false,
    }),
  ],

  node: {
    console: false,
    global: false,
    process: false,
    Buffer: false,
    __filename: false,
    __dirname: false,
  },

  devtool: "source-map", //isDebug ? "cheap-module-source-map" : "source-map",
};

// only use babel-plugin-import in client side
clientConfig.module.rules[0].query.plugins = [...clientConfig.module.rules[0].query.plugins];
clientConfig.module.rules[0].query.plugins.push(["import", { libraryName: "antd", style: "css" }]);

export default [clientConfig, serverConfig];

server side render-sharp108

the original words of the author, so what is the requirement that this component needs to be rendered on the server side.


use require.ensure or import (), to load lazily inside the component. I've been looking into it for a long time. Thank you very much.

and for this scenario, I wrote an asynchronous import plug-in separately, which can be installed and used by those who are interested. It's very simple.

rc-async-component

if you have any questions, please email me directly: zhoudeyou945@126.com

Menu