最近遇到一个需求,react工程中需要添加一个富文本编辑器,网上有很多,找了一个braft-editor的,自己新建一个工程使用都没有问题。不过添加到项目中的时候,因为项目用到了服务端渲染(刚接手项目,新手一个,对这个概念不太懂),服务端node环境不能使用window等环境变量,所以在运行项目的时候会报错。
P.S.:基本所有的富文本编辑器插件都存在这个问题,实现上都用到了window或document。
网上我看有一些人也有这个问题,但是都没有一个明白的解决办法,就是知道是node端没有window变量,如何解决没有人提供,求助一下大牛。
补充说明:我知道这种文件没必要在服务端渲染,但是现在项目里应该是所有用到的第三方包都会在服务端渲染,所以我想问的解决办法是怎么在服务端渲染屏蔽这种文件。
可以看到webpack编译是成功的。
上面图是运行报错图。下面是几个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 没有被修改,也保存 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文件
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;
- 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];
使用require.ensure或者import(),在组件内部懒加载就可以了。查了好久,非常感谢。
并且为了这个场景我单独边写了一个异步import的插件,感兴趣的可以安装使用,非常简单.
rc-async-component
有问题可以直接给我发邮件:zhoudeyou945@126.com