鍍金池/ 問(wèn)答/Linux  HTML/ React服務(wù)端渲染,第三方包window is not defined.

React服務(wù)端渲染,第三方包window is not defined.

最近遇到一個(gè)需求,react工程中需要添加一個(gè)富文本編輯器,網(wǎng)上有很多,找了一個(gè)braft-editor的,自己新建一個(gè)工程使用都沒(méi)有問(wèn)題。不過(guò)添加到項(xiàng)目中的時(shí)候,因?yàn)轫?xiàng)目用到了服務(wù)端渲染(剛接手項(xiàng)目,新手一個(gè),對(duì)這個(gè)概念不太懂),服務(wù)端node環(huán)境不能使用window等環(huán)境變量,所以在運(yùn)行項(xiàng)目的時(shí)候會(huì)報(bào)錯(cuò)。
P.S.:基本所有的富文本編輯器插件都存在這個(gè)問(wèn)題,實(shí)現(xiàn)上都用到了window或document。
網(wǎng)上我看有一些人也有這個(gè)問(wèn)題,但是都沒(méi)有一個(gè)明白的解決辦法,就是知道是node端沒(méi)有window變量,如何解決沒(méi)有人提供,求助一下大牛。


補(bǔ)充說(shuō)明:我知道這種文件沒(méi)必要在服務(wù)端渲染,但是現(xiàn)在項(xiàng)目里應(yīng)該是所有用到的第三方包都會(huì)在服務(wù)端渲染,所以我想問(wèn)的解決辦法是怎么在服務(wù)端渲染屏蔽這種文件。


圖片描述
可以看到webpack編譯是成功的。
圖片描述

上面圖是運(yùn)行報(bào)錯(cuò)圖。下面是幾個(gè)js文件代碼。

1.server.js文件

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#L58
    sessionStore.all(startCleanupSessionTask);
  }, cookieAge / 2).unref();
}

startCleanupSessionTask();

app.use(session({
  store: sessionStore,
  secret: `****`,
  name: '***',
  resave: false, // 即使 session 沒(méi)有被修改,也保存 session 值,默認(rèn)為 true
  rolling: true, // 每次請(qǐng)求都更新cookie expires
  saveUninitialized: false,
  // default: { path: '/', httpOnly: true, secure: false, maxAge: null }
  // secure: true => cookie 在 HTTP 中是無(wú)效,在 HTTPS 中才有效
  cookie: { maxAge: cookieAge }
  // genid: 產(chǎn)生一個(gè)新的 session_id 時(shí),所使用的函數(shù), 默認(rèn)使用 uid2 這個(gè) npm 包。
  // store: session 的存儲(chǔ)方式,默認(rèn)存放在內(nèi)存中
}));

/**
 * 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); // 首頁(yè)登錄
app.get('/specialLogin', specialLogin); // crm系統(tǒng)登錄
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文件

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文件
/**
 * 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#options
          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/#presets-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#progresspluginhandler
    // 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#defineplugin
    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#options
    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#commonschunkplugin
    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#compressor-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#devtool
  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#node
  // 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#defineplugin
    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#limitchunkcountplugin
    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#bannerplugin
    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];
回答
編輯回答
護(hù)她命

server side render #108

作者原話,所以是什么需求需要這個(gè)組件在服務(wù)端進(jìn)行渲染。

2017年2月11日 14:47
編輯回答
舊城人

使用require.ensure或者import(),在組件內(nèi)部懶加載就可以了。查了好久,非常感謝。

2017年12月23日 09:53